|
var mailer = require('mailer'); |
|
|
|
var humanize = function(string) { |
|
var str=string.toLowerCase(); |
|
str=str.replace(/_/g,' '); |
|
str=str.substring(0,1).toUpperCase()+str.substring(1); |
|
return str; |
|
}; |
|
|
|
var escapeHTML = function(html) { |
|
return new String(html) |
|
.replace(/&/gmi, '&') |
|
.replace(/'/gmi, ''') |
|
.replace(/"/gmi, '"') |
|
.replace(/>/gmi, '>') |
|
.replace(/</gmi, '<'); |
|
}; |
|
|
|
var Errors = function() { |
|
this._errors = []; |
|
}; |
|
|
|
Errors.prototype.isEmpty = function() { |
|
return this._errors.length === 0; |
|
}; |
|
|
|
Errors.prototype.add = function(error) { |
|
this._errors.push(error); |
|
}; |
|
|
|
Errors.prototype.__defineGetter__('length', function() { return this._errors.length; }); |
|
|
|
Errors.prototype.forEach = function(fn) { |
|
return this._errors.forEach(function(error) { |
|
fn(error); |
|
}); |
|
}; |
|
|
|
var Error = function(field, name, message) { |
|
this.field = field; |
|
this.name = name; |
|
this.message = message; |
|
}; |
|
|
|
Error.prototype.html = function() { |
|
return this.field.title + ": " + this.message; |
|
}; |
|
|
|
var Field = function(formId, options, data) { |
|
this.type = options.type || "text"; |
|
this.title = options.title || options.label || humanize(options.name); |
|
this._label = options.label || this.title; |
|
this.name = formId + "[" + options.name + "]"; |
|
this.id = formId + "_" + options.name; |
|
this['class'] = options['class']; |
|
this.value_options = options.options && options.options.split(',').map(function(v) { return v.trim(); }); |
|
this.labels = options.labels && options.labels.split(',').map(function(v) { return v.trim(); }); |
|
this.value = data ? this._cast(data[options.name]) : (options['default'] || ""); |
|
this.placeholder = options.placeholder || ""; |
|
this.required = {"true": true, "yes": true}[options.required] || false; |
|
this.include_blank = options.include_blank ? true : false; |
|
this.pattern = options.pattern || ""; |
|
this.on = options.on || "Checked"; |
|
this.off = options.off || ""; |
|
this.min = this._cast(options.min); |
|
this.max = this._cast(options.max); |
|
this.multiple = options.multiple || ""; |
|
this._options = options; |
|
if (this.type == "range") { |
|
this.min = this.min || 0; |
|
this.max = this.max || 100; |
|
} |
|
this.validate = data ? true : false; |
|
this._errors = []; |
|
}; |
|
Field.prototype._cast = function(value) { |
|
var self = this; |
|
if (value === "" || typeof value === "undefined") return ""; |
|
try { |
|
switch(this.type) { |
|
case "number": |
|
case "range": |
|
return parseInt(value, 10); |
|
case "select": |
|
case "radio": |
|
var val = value.map ? value.map(function(val) { return self._castOption.call(self, val); }) : this._castOption(value); |
|
return value.map ? Array.prototype.slice.call(val) : val; |
|
default: |
|
return (value && value.trim()) || ""; |
|
} |
|
} catch(e) { |
|
this._typeMismatch = true; |
|
return value; |
|
} |
|
}; |
|
Field.prototype._castOption = function(value) { |
|
value = value.trim(); |
|
if (this.value_options) { |
|
for (var i=0; i<this.value_options.length; i++) { |
|
if (value === this.value_options[i]) return value; |
|
} |
|
} else { |
|
return ""; |
|
} |
|
return ""; |
|
}; |
|
Field.prototype.isRange = function() { |
|
return this.type == "range" || (this.type == "number" && this.max); |
|
}; |
|
|
|
Field.prototype.label = function(options) { |
|
return { |
|
html: "<label for='" + this.id + "'>" + this._label + "</label>" |
|
}; |
|
}; |
|
Field.prototype.classes = function() { |
|
return [ |
|
this.type, |
|
this.validate && this.valid() ? "valid" : "", |
|
this.validate && this.invalid() ? "invalid" : "", |
|
this.required ? "required" : "optional", |
|
this.validate && this.isRange() ? (this.rangeOverflow() || this.rangeUnderflow() ? "out-of-range" : "in-range") : "" |
|
].filter(function(klass) { return klass; }).join(" "); |
|
}; |
|
|
|
Field.prototype._attrFor = function(attr, enable, value) { |
|
return enable ? " " + attr + "='" + (value || attr) + "'" : ""; |
|
}; |
|
|
|
Field.prototype._htmlForOption = function(option, label) { |
|
if (this.type == "radio") { |
|
var id = this.id + "_" + option; |
|
return "<label for='" + id + "'>" + label + "</label>" + |
|
"<input type='radio' name='" + this.name + "' " + this._attrFor("required", this.required) + |
|
" value='" + option + "' id='" + id + "' " + this._attrFor("checked", this.value === option) + "/>"; |
|
} else { |
|
var selected = this.value.indexOf ? this.value.indexOf(option) >= 0 : this.value === option; |
|
return "<option value='" + option + "' " + this._attrFor("selected", selected) + ">" + label + "</option>"; |
|
} |
|
}; |
|
|
|
Field.prototype.options = function() { |
|
var self = this; |
|
return (this.value_options || []).map(function(option, i) { |
|
var label = self.labels && self.labels[i] || option; |
|
return { |
|
label: label, |
|
option: option, |
|
value: option, |
|
html: function() { return self._htmlForOption(option, label); } |
|
}; |
|
}); |
|
}; |
|
|
|
Field.prototype.input = function(options) { |
|
var field = this; |
|
var html; |
|
switch(this.type) { |
|
case "textarea": |
|
html = "<textarea id='" + this.id + "' name='" + this.name + "'" + this._attrFor("required", this.required) + |
|
this._attrFor("placeholder", this.placeholder, this.placeholder) + ">" + escapeHTML(this.value) + "</textarea>"; |
|
break; |
|
case "checkbox": |
|
html = (this.off ? "<input type='hidden' name='" + this.name +"' value='" + this.off + "' />" : "") + |
|
"<input id='" + this.id + "' name='" + this.name + "' type='" + this.type + |
|
"' value='" + this.on + "'" + (this.value === this.on && " checked='checked'") + |
|
(this._attrFor("required", this.required)) + " />"; |
|
break; |
|
case "radio": |
|
html = this.options().map(function(option) { return option.html(); }).join("\n"); |
|
break; |
|
case "select": |
|
html = "<select id='" + this.id + "' name='" + this.name + (this.multiple && "[]") + "' " + |
|
this._attrFor("required", this.required) + this._attrFor("multiple", this.multiple) + ">" + |
|
(this.required && !this.include_blank ? "" : "<option value='' " + this._attrFor("selected", this.value === "") + "></option>") + |
|
this.options().map(function(option) { return option.html(); }).join("\n") + |
|
"</select>"; |
|
break; |
|
default: |
|
html = "<input id='" + this.id + "' name='" + this.name + "' type='" + this.type + |
|
"' value='" + escapeHTML(this.value) + "'" + (this.placeholder ? " placeholder='" + this.placeholder + "'" : "") + |
|
this._attrFor("multiple", this.multiple) + this._attrFor("required", this.required) + |
|
this._attrFor("pattern", this.pattern, this.pattern) + "/>"; |
|
} |
|
return { |
|
name: this.name, |
|
type: this.type, |
|
value: this.value, |
|
html: html |
|
}; |
|
}; |
|
Field.prototype.html = function() { |
|
if (this.type == "hidden") return this.input().html; |
|
|
|
return "<p class='" + (this['class'] || "field " + this.classes()) + "'>" + |
|
this.label().html + this.input().html + |
|
"</p>"; |
|
}; |
|
Field.prototype.errors = function() { |
|
if (this._errors.length) return this._errors; |
|
var errors = { |
|
valueMissing: "Please fill out this field", |
|
tooLong: "Please enter a shorter value", |
|
patternMismatch: "Please match the requested format", |
|
typeMismatch: "Please match the requested format", |
|
rangeUnderflow: "Please enter a higher value", |
|
rangeOverflow: "Please enter a lower value", |
|
stepMismatch: "Please choose an allowed value" |
|
}; |
|
|
|
for (var error in errors) { |
|
if (this[error]()) { |
|
this["_" + error] = true; |
|
this._errors.push(new Error(this, error, this._options[error] || errors[error])); |
|
} |
|
} |
|
|
|
return this._errors; |
|
}; |
|
Field.prototype.valid = function() { return this.validate ? this.errors().length == 0 : true; }; |
|
Field.prototype.invalid = function() { return !this.valid(); }; |
|
Field.prototype.valueMissing = function() { |
|
return this._valueMissing || (this.required ? this.off === this.value : false); |
|
}; |
|
Field.prototype.typeMismatch = function() { |
|
if (this._typeMismatch) return this._typeMismatch; |
|
if (this.pattern) return false; |
|
if (this.type == "email") { |
|
var regExp = /^[a-z0-9_.%+\-]+@[0-9a-z.\-]+\.[a-z.]{2,6}$/i; |
|
if (this.multiple) { |
|
return this.value && this.value.split(",").reduce(function(invalid, email) { |
|
return invalid || !regExp.test(email.trim()); |
|
}, false); |
|
} else { |
|
return this.value && !regExp.test(this.value); |
|
} |
|
} |
|
if (this.type == "url") return this.value && !(/[a-z][\-\.+a-z]*:\/\//i).test(this.value); |
|
return false; |
|
}; |
|
Field.prototype.patternMismatch = function() { |
|
if (this._patternMismatch) return this._patternMismatch; |
|
return this.pattern && this.value && !(new RegExp(this.pattern).test(this.value)); |
|
}; |
|
Field.prototype.tooLong = function() { |
|
return this._tooLong || (this.maxlength ? (this.value && this.value.length > this.maxlength ? true : false) : false); |
|
}; |
|
Field.prototype.rangeUnderflow = function() { |
|
return this._rangeUnderflow || (this.min === "" ? false : this.value < this.min); |
|
}; |
|
Field.prototype.rangeOverflow = function() { |
|
return this._rangeOverflow || (this.max === "" ? false : this.value > this.max); |
|
}; |
|
Field.prototype.stepMismatch = function() { |
|
if (this._stepMismatch) return this._stepMismatch; |
|
if (this.step) { |
|
return ((this.value - this.min) % this.step) === 0; |
|
} |
|
return false; |
|
}; |
|
|
|
exports.form = function(options, enclosed) { |
|
var name = options.name || "form"; |
|
|
|
var success = false; |
|
var errors = new Errors(); |
|
var title = options.title || humanize(name); |
|
var data = request.params[name]; |
|
var submitted = request.request_method == "POST" && data; |
|
var to = options.mailto; |
|
var fields = []; |
|
var curField = 0; |
|
|
|
enclosed.tags(function(tag) { return tag.name == "fields";}).forEach(function(fieldsTag) { |
|
fieldsTag.tags(function(tag) { return tag.name == "field";}).forEach(function(tag) { |
|
var field = new Field(name, tag.options, data); |
|
fields.push(field); |
|
if (submitted) { |
|
field.errors().forEach(function(error) { |
|
errors.add(error); |
|
}); |
|
} |
|
}); |
|
}); |
|
|
|
if (request.request_method == "POST" && data) { |
|
if (errors.isEmpty()) { |
|
var body = []; |
|
|
|
fields.forEach(function(field) { |
|
body.push(field.title + ": " + (field.value.join ? field.value.join(", ") : field.value)); |
|
}); |
|
|
|
if (to) { |
|
mailer.send( |
|
to, |
|
options.mail_subject || data.subject || "Form submission", |
|
body.join("\n\n"), |
|
{reply_to: data.email} |
|
); |
|
} |
|
success = true; |
|
} |
|
} |
|
|
|
return success ? {success: success, fields: null} : |
|
"<form" + (options.name ? " name='" + options.name + "'" : "") + (options.id ? " id='" + options.id + "'" : "") + |
|
" action='" + (options.action || "") + "' method='post' " + (options.novalidate ? "novalidate='novalidate'" : "") + ">" + |
|
enclosed.render({ |
|
success: false, |
|
fields: { |
|
field: function(fieldOptions) { |
|
var field = fields[curField]; |
|
curField++; |
|
return field; |
|
} |
|
}, |
|
errors: errors.isEmpty() ? null : function(options, enclosing) { |
|
return enclosing.tags().length ? errors : {html: errors._errors.map(function(error) { return error.html(); }).join("<br />")}; |
|
} |
|
}) + |
|
"</form>"; |
|
}; |