Comparing version 1.2.0 to 1.3.0
@@ -0,1 +1,12 @@ | ||
1.3.0 / 2016-11-15 | ||
=================== | ||
* [New] implement nested and ordered choices (#191) | ||
* [New] [Fix] ensure that all content in `tag` is properly escaped | ||
* [Fix] use `is.array` instead of `Array.isArray`, to continue supporting ES3 | ||
* [Fix] ES3: use `object-keys` instead of `Object.keys` | ||
* [Deps] update `is`, `async`, `qs` | ||
* [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `nsp`, `tape` | ||
* [Tests] up to `node` `v7.0`, `v6.9`, `v4.6`; improve test matrix | ||
* [Tests] execute all tests in test directory (#190) | ||
1.2.0 / 2016-08-25 | ||
@@ -2,0 +13,0 @@ =================== |
@@ -9,3 +9,3 @@ 'use strict'; | ||
var coerceArray = function (arr) { | ||
return Array.isArray(arr) && arr.length > 0 ? arr : []; | ||
return is.array(arr) && arr.length > 0 ? arr : []; | ||
}; | ||
@@ -165,3 +165,3 @@ var assign = require('object.assign'); | ||
if (typeof raw_data === 'undefined') { return []; } | ||
return Array.isArray(raw_data) ? raw_data : [raw_data]; | ||
return is.array(raw_data) ? raw_data : [raw_data]; | ||
}; | ||
@@ -168,0 +168,0 @@ return f; |
@@ -10,2 +10,3 @@ 'use strict'; | ||
var assign = require('object.assign'); | ||
var keys = require('object-keys'); | ||
@@ -22,3 +23,3 @@ exports.widgets = require('./widgets'); | ||
Object.keys(fields).forEach(function (k) { | ||
keys(fields).forEach(function (k) { | ||
// if it's not a field object, create an object field. | ||
@@ -37,3 +38,3 @@ if (!is.fn(fields[k].toHTML) && is.object(fields[k])) { | ||
}; | ||
Object.keys(f.fields).forEach(function (k) { | ||
keys(f.fields).forEach(function (k) { | ||
if (data != null || f.fields[k].required) { | ||
@@ -43,3 +44,3 @@ b.fields[k] = f.fields[k].bind((data || {})[k]); | ||
}); | ||
b.data = Object.keys(b.fields).reduce(function (a, k) { | ||
b.data = keys(b.fields).reduce(function (a, k) { | ||
a[k] = b.fields[k].data; | ||
@@ -51,3 +52,3 @@ return a; | ||
async.forEach(Object.keys(b.fields), function (k, asyncCallback) { | ||
async.forEach(keys(b.fields), function (k, asyncCallback) { | ||
b.fields[k].validate(b, function (err, bound_field) { | ||
@@ -63,3 +64,3 @@ b.fields[k] = bound_field; | ||
var form = this; | ||
return Object.keys(form.fields).every(function (k) { | ||
return keys(form.fields).every(function (k) { | ||
var field = form.fields[k]; | ||
@@ -113,3 +114,3 @@ if (is.fn(field.isValid)) { return field.isValid(); } | ||
return Object.keys(form.fields).reduce(function (html, k) { | ||
return keys(form.fields).reduce(function (html, k) { | ||
var kname = is.string(name) ? name + '[' + k + ']' : k; | ||
@@ -116,0 +117,0 @@ return html + form.fields[k].toHTML(kname, iterator); |
@@ -24,3 +24,3 @@ 'use strict'; | ||
opt.errorAfterField ? errorHTML : '' | ||
].join('')); | ||
].join(''), true); | ||
wrappedContent.push(fieldset); | ||
@@ -39,3 +39,3 @@ } else { | ||
} | ||
return tag(tagName, { classes: field.classes() }, wrappedContent.join('')); | ||
return tag(tagName, { classes: field.classes() }, wrappedContent.join(''), true); | ||
}; | ||
@@ -50,3 +50,3 @@ }; | ||
var th = tag('th', {}, field.labelHTML(name, field.id)); | ||
var th = tag('th', {}, field.labelHTML(name, field.id), true); | ||
@@ -64,5 +64,5 @@ var tdContent = field.widget.toHTML(name, field); | ||
var td = tag('td', {}, tdContent); | ||
var td = tag('td', {}, tdContent, true); | ||
return tag('tr', { classes: field.classes() }, th + td); | ||
return tag('tr', { classes: field.classes() }, th + td, true); | ||
}; |
'use strict'; | ||
var htmlEscape = require('./htmlEscape'); | ||
var is = require('is'); | ||
var keys = require('object-keys'); | ||
@@ -10,3 +12,3 @@ // generates a string for common HTML tag attributes | ||
} | ||
if (Array.isArray(a.classes) && a.classes.length > 0) { | ||
if (is.array(a.classes) && a.classes.length > 0) { | ||
a['class'] = htmlEscape(a.classes.join(' ')); | ||
@@ -16,3 +18,3 @@ } | ||
var pairs = []; | ||
Object.keys(a).forEach(function (field) { | ||
keys(a).forEach(function (field) { | ||
var value = a[field]; | ||
@@ -53,8 +55,9 @@ if (typeof value === 'boolean') { | ||
var tag = function tag(tagName, attrsMap, content) { | ||
var tag = function tag(tagName, attrsMap, content, contentIsEscaped) { | ||
var safeTagName = htmlEscape(tagName); | ||
var attrsHTML = !Array.isArray(attrsMap) ? attrs(attrsMap) : attrsMap.reduce(function (html, map) { | ||
var attrsHTML = !is.array(attrsMap) ? attrs(attrsMap) : attrsMap.reduce(function (html, map) { | ||
return html + attrs(map); | ||
}, ''); | ||
return '<' + safeTagName + attrsHTML + (isSelfClosing(safeTagName) ? ' />' : '>' + content + '</' + safeTagName + '>'); | ||
var safeContent = contentIsEscaped ? content : htmlEscape(content); | ||
return '<' + safeTagName + attrsHTML + (isSelfClosing(safeTagName) ? ' />' : '>' + safeContent + '</' + safeTagName + '>'); | ||
}; | ||
@@ -61,0 +64,0 @@ |
@@ -172,3 +172,3 @@ 'use strict'; | ||
// http://projects.scottsplayground.com/email_address_validation/ | ||
// eslint-disable-next-line no-control-regex | ||
// eslint-disable-next-line no-control-regex, no-useless-escape | ||
return exports.regexp(/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i, msg); | ||
@@ -181,5 +181,6 @@ }; | ||
// http://projects.scottsplayground.com/iri/ | ||
// eslint-disable-next-line no-control-regex | ||
var external_regex = /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i, | ||
with_localhost_regex = /^(https?|ftp):\/\/(localhost|((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i; | ||
// eslint-disable-next-line no-control-regex, no-useless-escape | ||
var external_regex = /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i; | ||
// eslint-disable-next-line no-useless-escape | ||
var with_localhost_regex = /^(https?|ftp):\/\/(localhost|((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i; | ||
return exports.regexp(include_localhost ? with_localhost_regex : external_regex, msg); | ||
@@ -186,0 +187,0 @@ }; |
'use strict'; | ||
var is = require('is'); | ||
var keys = require('object-keys'); | ||
var tag = require('./tag'); | ||
@@ -8,6 +9,6 @@ | ||
var ariaRegExp = /^aria-[a-z]+$/; | ||
var legalAttrs = ['autocomplete', 'autocorrect', 'autofocus', 'autosuggest', 'checked', 'dirname', 'disabled', 'tabindex', 'list', 'max', 'maxlength', 'min', 'multiple', 'novalidate', 'pattern', 'placeholder', 'readonly', 'required', 'size', 'step']; | ||
var ignoreAttrs = ['id', 'name', 'class', 'classes', 'type', 'value']; | ||
var legalAttrs = ['autocomplete', 'autocorrect', 'autofocus', 'autosuggest', 'checked', 'dirname', 'disabled', 'tabindex', 'list', 'max', 'maxlength', 'min', 'novalidate', 'pattern', 'placeholder', 'readonly', 'required', 'size', 'step']; | ||
var ignoreAttrs = ['id', 'name', 'class', 'classes', 'type', 'value', 'multiple']; | ||
var getUserAttrs = function (opt) { | ||
return Object.keys(opt).reduce(function (attrs, k) { | ||
return keys(opt).reduce(function (attrs, k) { | ||
if ((ignoreAttrs.indexOf(k) === -1 && legalAttrs.indexOf(k) > -1) || dataRegExp.test(k) || ariaRegExp.test(k)) { | ||
@@ -47,2 +48,94 @@ attrs[k] = opt[k]; | ||
var choiceValueEquals = function (value1, value2) { | ||
return !is.array(value1) && !is.array(value2) && String(value1) === String(value2); | ||
}; | ||
var isSelected = function (value, choice) { | ||
return value && (is.array(value) ? value.some(choiceValueEquals.bind(null, choice)) : choiceValueEquals(value, choice)); | ||
}; | ||
var renderChoices = function (choices, renderer) { | ||
return choices.reduce(function (partialRendered, choice) { | ||
var isNested = is.array(choice[1]); | ||
var renderData = isNested ? | ||
{ isNested: true, label: choice[0], choices: choice[1] } : | ||
{ isNested: false, value: choice[0], label: choice[1] }; | ||
return partialRendered + renderer(renderData); | ||
}, ''); | ||
}; | ||
var isScalar = function (value) { | ||
return !value || is.string(value) || is.number(value) || is.bool(value); | ||
}; | ||
var unifyChoices = function (choices, nestingLevel) { | ||
if (nestingLevel < 0) { | ||
throw new RangeError('choices nested too deep'); | ||
} | ||
var unifyChoiceArray = function (arrayChoices, currentLevel) { | ||
return arrayChoices.reduce(function (result, choice) { | ||
if (!is.array(choice) || choice.length !== 2) { | ||
throw new TypeError('choice must be array with two elements'); | ||
} | ||
if (isScalar(choice[0]) && isScalar(choice[1])) { | ||
result.push(choice); | ||
} else if (isScalar(choice[0]) && (is.array(choice[1]) || is.object(choice[1]))) { | ||
result.push([choice[0], unifyChoices(choice[1], currentLevel - 1)]); | ||
} else { | ||
throw new TypeError('expected primitive value as first and primitive value, object, or array as second element'); | ||
} | ||
return result; | ||
}, []); | ||
}; | ||
var unifyChoiceObject = function (objectChoices, currentLevel) { | ||
return keys(objectChoices).reduce(function (result, key) { | ||
var label = objectChoices[key]; | ||
if (isScalar(label)) { | ||
result.push([key, label]); | ||
} else if (is.array(label) || is.object(label)) { | ||
result.push([key, unifyChoices(label, currentLevel - 1)]); | ||
} else { | ||
throw new TypeError('expected primitive value, object, or array as object value'); | ||
} | ||
return result; | ||
}, []); | ||
}; | ||
return is.array(choices) ? unifyChoiceArray(choices, nestingLevel) : unifyChoiceObject(choices, nestingLevel); | ||
}; | ||
var select = function (isMultiple) { | ||
return function (options) { | ||
var opt = options || {}; | ||
var w = { | ||
classes: opt.classes, | ||
type: isMultiple ? 'multipleSelect' : 'select' | ||
}; | ||
var userAttrs = getUserAttrs(opt); | ||
w.toHTML = function (name, field) { | ||
var f = field || {}; | ||
var choices = unifyChoices(f.choices, 1); | ||
var optionsHTML = renderChoices(choices, function render(choice) { | ||
if (choice.isNested) { | ||
return tag('optgroup', { label: choice.label }, renderChoices(choice.choices, render), true); | ||
} else { | ||
return tag('option', { value: choice.value, selected: !!isSelected(f.value, choice.value) }, choice.label); | ||
} | ||
}); | ||
var attrs = { | ||
name: name, | ||
id: f.id === false ? false : (f.id || true), | ||
classes: w.classes | ||
}; | ||
if (isMultiple) { | ||
attrs.multiple = true; | ||
} | ||
return tag('select', [attrs, userAttrs, w.attrs || {}], optionsHTML, true); | ||
}; | ||
return w; | ||
}; | ||
}; | ||
exports.text = input('text'); | ||
@@ -82,2 +175,5 @@ exports.email = input('email'); | ||
exports.select = select(false); | ||
exports.multipleSelect = select(true); | ||
exports.checkbox = function (options) { | ||
@@ -105,27 +201,2 @@ var opt = options || {}; | ||
exports.select = function (options) { | ||
var opt = options || {}; | ||
var w = { | ||
classes: opt.classes, | ||
type: 'select' | ||
}; | ||
var userAttrs = getUserAttrs(opt); | ||
w.toHTML = function (name, field) { | ||
var f = field || {}; | ||
var optionsHTML = Object.keys(f.choices).reduce(function (html, k) { | ||
return html + tag('option', { | ||
value: k, | ||
selected: !!(f.value && String(f.value) === String(k)) | ||
}, f.choices[k]); | ||
}, ''); | ||
var attrs = { | ||
name: name, | ||
id: f.id === false ? false : (f.id || true), | ||
classes: w.classes | ||
}; | ||
return tag('select', [attrs, userAttrs, w.attrs || {}], optionsHTML); | ||
}; | ||
return w; | ||
}; | ||
exports.textarea = function (options) { | ||
@@ -162,6 +233,7 @@ var opt = options || {}; | ||
var f = field || {}; | ||
return Object.keys(f.choices).reduce(function (html, k) { | ||
var choices = unifyChoices(f.choices, 0); | ||
return renderChoices(choices, function (choice) { | ||
// input element | ||
var id = f.id === false ? false : (f.id ? f.id + '_' + k : 'id_' + name + '_' + k); | ||
var checked = f.value && (Array.isArray(f.value) ? f.value.some(function (v) { return String(v) === String(k); }) : String(f.value) === String(k)); | ||
var id = f.id === false ? false : (f.id ? f.id + '_' + choice.value : 'id_' + name + '_' + choice.value); | ||
var checked = isSelected(f.value, choice.value); | ||
@@ -173,3 +245,3 @@ var attrs = { | ||
classes: w.classes, | ||
value: k, | ||
value: choice.value, | ||
checked: !!checked | ||
@@ -180,6 +252,6 @@ }; | ||
// label element | ||
var labelHTML = tag('label', { 'for': id, classes: w.labelClasses }, f.choices[k]); | ||
var labelHTML = tag('label', { 'for': id, classes: w.labelClasses }, choice.label); | ||
return html + inputHTML + labelHTML; | ||
}, ''); | ||
return inputHTML + labelHTML; | ||
}); | ||
}; | ||
@@ -213,6 +285,7 @@ return w; | ||
var f = field || {}; | ||
return Object.keys(f.choices).reduce(function (html, k) { | ||
var choices = unifyChoices(f.choices, 0); | ||
return renderChoices(choices, function (choice) { | ||
// input element | ||
var id = f.id === false ? false : (f.id ? f.id + '_' + k : 'id_' + name + '_' + k); | ||
var checked = f.value && (Array.isArray(f.value) ? f.value.some(function (v) { return String(v) === String(k); }) : String(f.value) === String(k)); | ||
var id = f.id === false ? false : (f.id ? f.id + '_' + choice.value : 'id_' + name + '_' + choice.value); | ||
var checked = isSelected(f.value, choice.value); | ||
var attrs = { | ||
@@ -223,3 +296,3 @@ type: 'radio', | ||
classes: w.classes, | ||
value: k, | ||
value: choice.value, | ||
checked: !!checked | ||
@@ -229,35 +302,8 @@ }; | ||
// label element | ||
var labelHTML = tag('label', { 'for': id, classes: w.labelClasses }, f.choices[k]); | ||
var labelHTML = tag('label', { 'for': id, classes: w.labelClasses }, choice.label); | ||
return html + inputHTML + labelHTML; | ||
}, ''); | ||
return inputHTML + labelHTML; | ||
}); | ||
}; | ||
return w; | ||
}; | ||
exports.multipleSelect = function (options) { | ||
var opt = options || {}; | ||
var w = { | ||
classes: opt.classes, | ||
type: 'multipleSelect' | ||
}; | ||
var userAttrs = getUserAttrs(opt); | ||
w.toHTML = function (name, field) { | ||
var f = field || {}; | ||
var optionsHTML = Object.keys(f.choices).reduce(function (html, k) { | ||
var selected = f.value && (Array.isArray(f.value) ? f.value.some(function (v) { return String(v) === String(k); }) : String(f.value) === String(k)); | ||
return html + tag('option', { | ||
value: k, | ||
selected: !!selected | ||
}, f.choices[k]); | ||
}, ''); | ||
var attrs = { | ||
multiple: true, | ||
name: name, | ||
id: f.id === false ? false : (f.id || true), | ||
classes: w.classes | ||
}; | ||
return tag('select', [attrs, userAttrs, w.attrs || {}], optionsHTML); | ||
}; | ||
return w; | ||
}; |
{ | ||
"name": "forms", | ||
"version": "1.3.0", | ||
"description": "An easy way to create, parse, and validate forms", | ||
@@ -17,3 +18,2 @@ "main": "./index", | ||
], | ||
"version": "1.2.0", | ||
"repository": { | ||
@@ -27,4 +27,6 @@ "type": "git", | ||
"scripts": { | ||
"tests-only": "node test/*.js", | ||
"test": "npm run lint && npm run tests-only && npm run security", | ||
"pretest": "npm run lint", | ||
"test": "npm run tests-only", | ||
"tests-only": "tape test/*.js", | ||
"posttest": "npm run security", | ||
"test-browser": "testling", | ||
@@ -39,11 +41,12 @@ "coverage": "covert test/*.js", | ||
"dependencies": { | ||
"async": "^2.0.1", | ||
"qs": "^6.2.1", | ||
"async": "^2.1.2", | ||
"qs": "^6.3.0", | ||
"formidable": "^1.0.17", | ||
"is": "^3.1.0", | ||
"is": "^3.2.0", | ||
"object.assign": "^4.0.4", | ||
"object-keys": "^1.0.11", | ||
"string.prototype.trim": "^1.1.2" | ||
}, | ||
"devDependencies": { | ||
"tape": "^4.6.0", | ||
"tape": "^4.6.2", | ||
"tape-dom": "^0.0.12", | ||
@@ -53,5 +56,5 @@ "testling": "^1.7.1", | ||
"jscs": "^3.0.7", | ||
"eslint": "^3.3.1", | ||
"@ljharb/eslint-config": "^7.0.0", | ||
"nsp": "^2.6.1", | ||
"eslint": "^3.10.2", | ||
"@ljharb/eslint-config": "^8.0.0", | ||
"nsp": "^2.6.2", | ||
"evalmd": "^0.0.17" | ||
@@ -58,0 +61,0 @@ }, |
@@ -311,3 +311,3 @@ # Forms <sup>[![Version Badge][npm-version-svg]][npm-url]</sup> | ||
* ``id`` - An optional id to override the default | ||
* ``choices`` - A list of options, used for multiple choice fields | ||
* ``choices`` - A list of options, used for multiple choice fields (see the field.choices section below) | ||
* ``cssClasses`` - A list of CSS classes for label and field wrapper | ||
@@ -320,2 +320,37 @@ * ``hideError`` - if true, errors won't be rendered automatically | ||
#### field.choices | ||
The choices property is used for radio, checkbox, and select fields. Two | ||
formats are supported and in case of select fields the format can be nested once to support option groups. | ||
The first format is based on objects and is easy to write. Object keys are treated as values and object values are treated as labels. If the value is another object and nesting is supported by the widget the key will be used as label and the value as nested list. | ||
The second format is array-based and therefore ordered (object keys are unordered by definition). The array should contain arrays with two values the first being the value and the second being the label. If the label is an array and nesting is supported by the widget the value will be used as label and the label as nested list. | ||
Both formats are demonstrated below: | ||
``` | ||
// objects | ||
{ | ||
'val-1': 'text-1', | ||
'val-2': 'text-2', | ||
'text-3': { | ||
'nested-val-1': 'nested-text-1', | ||
'nested-val-2': 'nested-text-2', | ||
'nested-val-3': 'nested-text-3' | ||
} | ||
} | ||
// arrays | ||
[ | ||
['val-1', 'text-1'], | ||
['val-2', 'text-2'], | ||
['text-3', [ | ||
['nested-val-1', 'nested-text-1'], | ||
['nested-val-2', 'nested-text-2'], | ||
['nested-val-3', 'nested-text-3'], | ||
]] | ||
] | ||
``` | ||
#### field.parse(rawdata) | ||
@@ -322,0 +357,0 @@ |
@@ -7,2 +7,3 @@ 'use strict'; | ||
var test = require('tape'); | ||
var keys = require('object-keys'); | ||
@@ -276,3 +277,3 @@ test('bind', function (t) { | ||
empty: function testing(form, callbacks) { | ||
t.equal(Object.keys(callbacks).length, 1); | ||
t.equal(keys(callbacks).length, 1); | ||
t.equal(typeof callbacks.empty, 'function'); | ||
@@ -283,3 +284,3 @@ } | ||
success: function testing(form, callbacks) { | ||
t.equal(Object.keys(callbacks).length, 1); | ||
t.equal(keys(callbacks).length, 1); | ||
t.equal(typeof callbacks.success, 'function'); | ||
@@ -296,3 +297,3 @@ } | ||
error: function nay(form, callbacks) { | ||
t.equal(Object.keys(callbacks).length, 2); | ||
t.equal(keys(callbacks).length, 2); | ||
t.equal(typeof callbacks.success, 'function'); | ||
@@ -305,3 +306,3 @@ t.equal(typeof callbacks.error, 'function'); | ||
other: function testing(form, callbacks) { | ||
t.equal(Object.keys(callbacks).length, 1); | ||
t.equal(keys(callbacks).length, 1); | ||
t.equal(typeof callbacks.other, 'function'); | ||
@@ -308,0 +309,0 @@ } |
@@ -5,2 +5,3 @@ 'use strict'; | ||
var test = require('tape'); | ||
var keys = require('object-keys'); | ||
@@ -116,2 +117,111 @@ var test_input = function (type) { | ||
t.test('throws on invalid choices', function (st) { | ||
st['throws'](function () { | ||
forms.widgets.select().toHTML('name', { | ||
choices: [ | ||
[['invalid'], 'text1'] | ||
] | ||
}); | ||
}); | ||
st['throws'](function () { | ||
forms.widgets.select().toHTML('name', { choices: ['invalid'] }); | ||
}); | ||
st['throws'](function () { | ||
forms.widgets.select().toHTML('name', { | ||
choices: [ | ||
[{ invalid: 'invalid' }, 'text1'] | ||
] | ||
}); | ||
}); | ||
st['throws'](function () { | ||
forms.widgets.select().toHTML('name', { | ||
choices: [ | ||
['val1', function () { return 'invalid'; }] | ||
] | ||
}); | ||
}); | ||
st.end(); | ||
}); | ||
t.test('supports array choices', function (st) { | ||
st.equal( | ||
forms.widgets.select().toHTML('name', { | ||
choices: [ | ||
['val1', 'text1'], | ||
['val2', 'text2'], | ||
['text3', | ||
[ | ||
['val3', 'text4'], | ||
['val4', 'text5'], | ||
['val5', 'text6'] | ||
] | ||
], | ||
['val6', 'text7'] | ||
] | ||
}), | ||
'<select name="name" id="id_name">' + | ||
'<option value="val1">text1</option>' + | ||
'<option value="val2">text2</option>' + | ||
'<optgroup label="text3">' + | ||
'<option value="val3">text4</option>' + | ||
'<option value="val4">text5</option>' + | ||
'<option value="val5">text6</option>' + | ||
'</optgroup>' + | ||
'<option value="val6">text7</option>' + | ||
'</select>' | ||
); | ||
st.end(); | ||
}); | ||
t.test('throws on deeply nested choices', function (st) { | ||
st['throws'](function () { | ||
forms.widgets.select().toHTML('name', { | ||
choices: [ | ||
['text1', | ||
[ | ||
['text2', | ||
[ | ||
['val1', 'text3'] | ||
] | ||
] | ||
] | ||
] | ||
] | ||
}); | ||
}); | ||
st.end(); | ||
}); | ||
t.test('throws on invalid array format', function (st) { | ||
st['throws'](function () { | ||
forms.widgets.select().toHTML('name', { | ||
choices: [ | ||
[] | ||
] | ||
}); | ||
}); | ||
st['throws'](function () { | ||
forms.widgets.select().toHTML('name', { | ||
choices: [ | ||
['val1'] | ||
] | ||
}); | ||
}); | ||
st['throws'](function () { | ||
forms.widgets.select().toHTML('name', { | ||
choices: [ | ||
['val1', 'text1', 'invalid'] | ||
] | ||
}); | ||
}); | ||
st.end(); | ||
}); | ||
t.test('stringifies values', function (st) { | ||
@@ -151,2 +261,12 @@ var html = widget.toHTML('name', { | ||
t.equal(forms.widgets.textarea().type, 'textarea'); | ||
t.test('properly escapes contents', function (st) { | ||
st.equal( | ||
forms.widgets.textarea().toHTML('name', { value: 'Inside</textarea>Escaped the textarea!' }), | ||
'<textarea name="name" id="id_name">Inside</textarea>Escaped the textarea!</textarea>' | ||
); | ||
st.end(); | ||
}); | ||
t.end(); | ||
@@ -161,4 +281,4 @@ }); | ||
one: 'Item one', | ||
three: 'Item three', | ||
two: 'Item two' | ||
two: 'Item two', | ||
three: 'Item three' | ||
}, | ||
@@ -195,2 +315,21 @@ value: 'two' | ||
t.test('throws on nested choices', function (st) { | ||
st['throws']( | ||
function () { | ||
w.toHTML('name', { | ||
choices: [ | ||
['val1', 'text1'], | ||
['val2', 'text2'], | ||
['text3', | ||
[ | ||
['val3', 'text4'] | ||
] | ||
] | ||
] | ||
}); | ||
} | ||
); | ||
st.end(); | ||
}); | ||
t.test('stringifies values', function (st) { | ||
@@ -238,3 +377,7 @@ st.test('single bound value', function (t2) { | ||
field = { | ||
choices: { one: 'Item one', three: 'Item three', two: 'Item two' }, | ||
choices: { | ||
one: 'Item one', | ||
two: 'Item two', | ||
three: 'Item three' | ||
}, | ||
value: ['two', 'three'] | ||
@@ -258,3 +401,7 @@ }; | ||
var field = { | ||
choices: { one: 'Item one', three: 'Item three', two: 'Item two' }, | ||
choices: { | ||
one: 'Item one', | ||
two: 'Item two', | ||
three: 'Item three' | ||
}, | ||
value: 'two' | ||
@@ -273,2 +420,21 @@ }; | ||
t.test('throw on nested choices', function (st) { | ||
st['throws']( | ||
function () { | ||
w.toHTML('name', { | ||
choices: [ | ||
['val1', 'text1'], | ||
['val2', 'text2'], | ||
['text3', | ||
[ | ||
['val3', 'text4'] | ||
] | ||
] | ||
] | ||
}); | ||
} | ||
); | ||
st.end(); | ||
}); | ||
t.test('stringifies values', function (st) { | ||
@@ -330,3 +496,7 @@ st.test('single bound value', function (t2) { | ||
var field = { | ||
choices: { one: 'Item one', three: 'Item three', two: 'Item two' }, | ||
choices: { | ||
one: 'Item one', | ||
two: 'Item two', | ||
three: 'Item three' | ||
}, | ||
value: ['two', 'three'] | ||
@@ -355,3 +525,3 @@ }; | ||
}), | ||
'<select multiple="multiple" name="name" id="id_name">' + | ||
'<select name="name" id="id_name" multiple="multiple">' + | ||
'<option value="val1">text1</option>' + | ||
@@ -371,3 +541,3 @@ '<option value="val2">text2</option>' + | ||
}), | ||
'<select multiple="multiple" name="name" id="someid" class="one two">' + | ||
'<select name="name" id="someid" multiple="multiple" class="one two">' + | ||
'<option value="val1">text1</option>' + | ||
@@ -393,3 +563,3 @@ '<option value="val2" selected="selected">text2</option>' + | ||
}); | ||
var expectedHTML = '<select multiple="multiple" name="name" id="someid" class="one two">' + | ||
var expectedHTML = '<select name="name" id="someid" multiple="multiple" class="one two">' + | ||
'<option value="1">text1</option>' + | ||
@@ -413,3 +583,3 @@ '<option value="2" selected="selected">text2</option>' + | ||
}); | ||
var expectedHTML = '<select multiple="multiple" name="name" id="someid" class="one two">' + | ||
var expectedHTML = '<select name="name" id="someid" multiple="multiple" class="one two">' + | ||
'<option value="1">text1</option>' + | ||
@@ -435,3 +605,3 @@ '<option value="2" selected="selected">text2</option>' + | ||
}).toHTML('field1'), | ||
'<input type="text" name="field1" id="id_field1" placeholder="Enter some comment" data-trigger="focus" />' | ||
'<input type="text" name="field1" id="id_field1" data-trigger="focus" placeholder="Enter some comment" />' | ||
); | ||
@@ -445,3 +615,3 @@ t.equal( | ||
}).toHTML('field1'), | ||
'<input type="text" name="field1" id="id_field1" class="one two" placeholder="Enter some comment" data-trigger="focus" aria-required="false" />' | ||
'<input type="text" name="field1" id="id_field1" class="one two" aria-required="false" data-trigger="focus" placeholder="Enter some comment" />' | ||
); | ||
@@ -462,3 +632,3 @@ t.equal( | ||
}).toHTML('field1'), | ||
'<input type="text" name="field1" id="id_field1" min="5" max="10" autocomplete="on" />' | ||
'<input type="text" name="field1" id="id_field1" autocomplete="on" max="10" min="5" />' | ||
); | ||
@@ -542,3 +712,3 @@ t.equal( | ||
}), | ||
'<select multiple="multiple" name="name" id="id_name" data-test="foo">' + | ||
'<select name="name" id="id_name" multiple="multiple" data-test="foo">' + | ||
'<option value="val1">text1</option>' + | ||
@@ -551,3 +721,7 @@ '<option value="val2">text2</option>' + | ||
var field = { | ||
choices: { one: 'Item one', three: 'Item three', two: 'Item two' }, | ||
choices: { | ||
one: 'Item one', | ||
two: 'Item two', | ||
three: 'Item three' | ||
}, | ||
value: 'two' | ||
@@ -567,3 +741,7 @@ }; | ||
var field2 = { | ||
choices: { one: 'Item one', three: 'Item three', two: 'Item two' }, | ||
choices: { | ||
one: 'Item one', | ||
two: 'Item two', | ||
three: 'Item three' | ||
}, | ||
value: 'two' | ||
@@ -603,6 +781,6 @@ }; | ||
test('dynamic widget attributes', function (t) { | ||
var keys = Object.keys(forms.widgets); | ||
t.plan(keys.length); | ||
var theKeys = keys(forms.widgets); | ||
t.plan(theKeys.length); | ||
var re = /autocomplete="no"/; | ||
keys.forEach(function (name) { | ||
theKeys.forEach(function (name) { | ||
var w = forms.widgets[name](); | ||
@@ -609,0 +787,0 @@ w.attrs = { autocomplete: 'no' }; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
Found 1 instance in 1 package
185686
4264
0
449
7
+ Addedobject-keys@^1.0.11
Updatedasync@^2.1.2
Updatedis@^3.2.0
Updatedqs@^6.3.0