- 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]) : "";
- 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.mail_to;
- 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 ? errors : {html: errors._errors.map(function(error) { return error.html(); }).join("<br />")};
- }
- }
- }) +
- "</form>";
- };