Last active
August 29, 2015 13:58
-
-
Save insin/9952028 to your computer and use it in GitHub Desktop.
Newforms hacked for Mithril output - http://bl.ocks.org/insin/raw/9952028/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** @jsx m */ | |
'use strict'; | |
var choices = [ | |
[1, 'foo'] | |
, [2, 'bar'] | |
, [3, 'baz'] | |
, [4, 'ter'] | |
] | |
var choicesWithCategories = [ | |
['B Choices', [[2, 'bar'], [3, 'baz']]] | |
, ['F Choices', [[1, 'foo']]] | |
, ['T Choices', [[4, 'ter']]] | |
] | |
var choicesWithEmpty = [['', '----']].concat(choices) | |
var dateFormats = [ | |
'%Y-%m-%d' // '2006-10-25' | |
, '%d/%m/%Y' // '25/10/2006' | |
, '%d/%m/%y' // '25/10/06' | |
] | |
var timeFormat = '%H:%M' // '14:30' | |
var dateTimeFormats = dateFormats.map(function(df) { return df + ' ' + timeFormat}) | |
function FakeFile(name, url) { | |
this.name = name | |
this.url = url | |
} | |
FakeFile.prototype.toString = function() { return this.name } | |
var AllFieldsForm = forms.Form.extend({ | |
CharField: forms.CharField({minLength: 5, maxLength: 10, helpText: ('Any text between 5 and 10 characters long.<br>(Try "Answer" then the Integer field below)')}) | |
, CharFieldWithTextareaWidget: forms.CharField({label: 'Char field (textarea)', widget: forms.Textarea}) | |
, CharFieldWithPasswordWidget: forms.CharField({widget: forms.PasswordInput}) | |
, IntegerField: forms.IntegerField({minValue: 42, maxValue: 420, helpText: 'Any whole number between 42 and 420'}) | |
, FloatField: forms.FloatField({minValue: 4.2, maxValue: 42, helpText: 'Any number between 4.2 and 42'}) | |
, DecimalField: forms.DecimalField({maxDigits: 5, decimalPlaces: 2, helpText: '3 digits allowed before the decimal point, 2 after it'}) | |
, DateField: forms.DateField({inputFormats: dateFormats, helpText: ('<em>yyyy-mm-dd</em> or <em>dd/mm/yyyy</em>')}) | |
, TimeField: forms.TimeField({inputFormats: [timeFormat], helpText: 'hh:mm, 24 hour'}) | |
, DateTimeField: forms.DateTimeField({inputFormats: dateTimeFormats, helpText: 'e.g. 2014-03-01 20:08'}) | |
, RegexField: forms.RegexField(/^I am Jack's /, {initial: "I am Jack's ", minLength: 20, helpText: 'Must begin with "I am Jack\'s " and be at least 20 characters long'}) | |
, EmailField: forms.EmailField() | |
, FileField: forms.FileField({helpText: 'Required'}) | |
, FileFieldWithInitial: forms.FileField({initial: new FakeFile('Fake File', 'fake.file')}) | |
, ImageField: forms.ImageField({required: false, helpText: 'Optional'}) | |
, ImageFieldWithIniitial: forms.ImageField({required: false, initial: new FakeFile('Fake File', 'fake.file') }) | |
, URLField: forms.URLField({label: 'URL field'}) | |
, BooleanField: forms.BooleanField() | |
, NullBooleanField: forms.NullBooleanField() | |
, ChoiceField: forms.ChoiceField({choices: choicesWithEmpty}) | |
, ChoiceFieldWithCategories: forms.ChoiceField({choices: choicesWithCategories}) | |
, ChoiceFieldWithRadioWidget: forms.ChoiceField({label: 'Choice field (radios)', choices: choices, initial: 4, widget: forms.RadioSelect}) | |
, ChoiceFieldWithRadioWidgetCategories: forms.ChoiceField({label: 'Choice field (radios + categories)', choices: choicesWithCategories, initial: 4, widget: forms.RadioSelect}) | |
, TypedChoiceField: forms.TypedChoiceField({choices: choicesWithEmpty, coerce: Number}) | |
, MultipleChoiceField: forms.MultipleChoiceField({choices: choices}) | |
, MultipleChoiceFieldWithCategories: forms.MultipleChoiceField({choices: choicesWithCategories}) | |
, MultipleChoiceFieldWithCheckboxWidget: forms.MultipleChoiceField({label: 'Multiple choice field (checkboxes)', choices: choices, initial: [1, 3], widget: forms.CheckboxSelectMultiple}) | |
, MultipleChoiceFieldWithCheckboxWidgetCategories: forms.MultipleChoiceField({label: 'Multiple choice field (checkboxes + categories)', choices: choicesWithCategories, initial: [1, 3], widget: forms.CheckboxSelectMultiple}) | |
, TypedMultipleChoiceField: forms.TypedMultipleChoiceField({choices: choices, coerce: Number}) | |
, ComboField: forms.ComboField({fields: [ | |
forms.EmailField() | |
, forms.RegexField(/ferret/i, {errorMessages: {invalid: 'Where is ferret? ಠ_ಠ'}}) | |
], helpText: 'An email address which contains the word "ferret"'}) | |
, SplitDateTimeField: forms.SplitDateTimeField({label: 'Split date/time field (a MultiValueField)', inputDateFormats: dateFormats, inputTimeFormats: [timeFormat]}) | |
, IPAddressField: forms.IPAddressField({label: 'IP address field', helpText: '(Deprecated)'}) | |
, GenericIPAddressField: forms.GenericIPAddressField({label: 'Generic IP address field', helpText: 'An IPv4 or IPv6 address'}) | |
, SlugField: forms.SlugField({helpText: 'Letters, numbers, underscores, and hyphens only'}) | |
, clean: function() { | |
if (this.cleanedData.CharField == 'Answer' && | |
this.cleanedData.IntegerField && | |
this.cleanedData.IntegerField != 42) { | |
this.addError('IntegerField', "That's not The Answer!") | |
throw forms.ValidationError('Please enter The Answer to the Ultimate Question of Life, the Universe, and Everything') | |
} | |
} | |
, render: function() { | |
return this.boundFields().map(function(bf) { | |
// Display cleaneddata, indicating its type | |
var cleanedData | |
if (this.cleanedData && bf.name in this.cleanedData) { | |
cleanedData = this.cleanedData[bf.name] | |
if (Array.isArray(cleanedData)) { | |
cleanedData = JSON.stringify(cleanedData) | |
} | |
else { | |
var isString = (Object.prototype.toString.call(cleanedData) == '[object String]') | |
cleanedData = ''+cleanedData | |
if (isString) { | |
cleanedData = '"' + cleanedData + '"' | |
} | |
} | |
} | |
var help | |
if (bf.helpText) { | |
help = m("p", [bf.helpText]) | |
} | |
var errors = bf.errors().messages().map(function(message) { | |
return m("div", [message]) | |
}) | |
return m("tr", [ | |
m("th", [bf.labelTag()]), | |
m("td", [bf.render(),help]), | |
m("td", [JSON.stringify(bf.validation(), null, ' ').replace(/"/g, '')]), | |
m("td", [errors]), | |
m("td", {className:"cleaned-data"}, [cleanedData]) | |
]) | |
}.bind(this)) | |
} | |
}) | |
var CartItemForm = forms.Form.extend({ | |
price: forms.DecimalField({minValue: 0, decimalPlaces: 2}) | |
}) | |
var CartItemFormSet = forms.formsetFactory(CartItemForm) | |
m.module(document.getElementById('app'), { | |
controller: function() { | |
this.form = new AllFieldsForm() | |
this.formset = new CartItemFormSet({initial: [{price: '44.99'}]}) | |
this.addAnother = function() { | |
this.formset.extra++ | |
} | |
this.onsubmit = function(e) { | |
e.preventDefault() | |
var data = forms.formData(document.forms[0]) | |
this.form.setData(data) | |
this.formset.setData(data) | |
} | |
} | |
, view: function(ctrl) { | |
var nonFieldErrors = ctrl.form.nonFieldErrors() | |
var topErrors = nonFieldErrors.isPopulated() | |
? m("div", [m("strong", ["Non field errors:"]),nonFieldErrors.render()]) | |
: null | |
return m("form", {id:"edit_cart", onsubmit:ctrl.onsubmit.bind(ctrl)}, [ | |
m("h2", ["All Fields"]), | |
topErrors, | |
m("table", [ | |
m("thead", [ | |
m("tr", [ | |
m("th", ["Label"]), | |
m("th", ["Input"]), | |
m("th", ["Validation"]), | |
m("th", ["Errors"]), | |
m("th", ["Cleaned Data"]) | |
]) | |
]), | |
m("tbody", [ | |
ctrl.form.render(), | |
m("tr", [ | |
m("td"), | |
m("td", {colSpan:"3"}, [ | |
m("input", {type:"submit", value:"Submit"}) | |
]) | |
]) | |
]) | |
]), | |
m("div", [ | |
m("h2", ["Cart Items"]), | |
ctrl.formset.asDiv(), | |
m("p", [ | |
m("button", {type:"button", onclick:m.withAttr('target', ctrl.addAnother.bind(ctrl))}, ["Add Another"]), | |
m("button", {type:"submit"}, ["Submit"]) | |
]) | |
]) | |
]) | |
} | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** @jsx m */ | |
'use strict'; | |
var choices = [ | |
[1, 'foo'] | |
, [2, 'bar'] | |
, [3, 'baz'] | |
, [4, 'ter'] | |
] | |
var choicesWithCategories = [ | |
['B Choices', [[2, 'bar'], [3, 'baz']]] | |
, ['F Choices', [[1, 'foo']]] | |
, ['T Choices', [[4, 'ter']]] | |
] | |
var choicesWithEmpty = [['', '----']].concat(choices) | |
var dateFormats = [ | |
'%Y-%m-%d' // '2006-10-25' | |
, '%d/%m/%Y' // '25/10/2006' | |
, '%d/%m/%y' // '25/10/06' | |
] | |
var timeFormat = '%H:%M' // '14:30' | |
var dateTimeFormats = dateFormats.map(function(df) { return df + ' ' + timeFormat}) | |
function FakeFile(name, url) { | |
this.name = name | |
this.url = url | |
} | |
FakeFile.prototype.toString = function() { return this.name } | |
var AllFieldsForm = forms.Form.extend({ | |
CharField: forms.CharField({minLength: 5, maxLength: 10, helpText: ('Any text between 5 and 10 characters long.<br>(Try "Answer" then the Integer field below)')}) | |
, CharFieldWithTextareaWidget: forms.CharField({label: 'Char field (textarea)', widget: forms.Textarea}) | |
, CharFieldWithPasswordWidget: forms.CharField({widget: forms.PasswordInput}) | |
, IntegerField: forms.IntegerField({minValue: 42, maxValue: 420, helpText: 'Any whole number between 42 and 420'}) | |
, FloatField: forms.FloatField({minValue: 4.2, maxValue: 42, helpText: 'Any number between 4.2 and 42'}) | |
, DecimalField: forms.DecimalField({maxDigits: 5, decimalPlaces: 2, helpText: '3 digits allowed before the decimal point, 2 after it'}) | |
, DateField: forms.DateField({inputFormats: dateFormats, helpText: ('<em>yyyy-mm-dd</em> or <em>dd/mm/yyyy</em>')}) | |
, TimeField: forms.TimeField({inputFormats: [timeFormat], helpText: 'hh:mm, 24 hour'}) | |
, DateTimeField: forms.DateTimeField({inputFormats: dateTimeFormats, helpText: 'e.g. 2014-03-01 20:08'}) | |
, RegexField: forms.RegexField(/^I am Jack's /, {initial: "I am Jack's ", minLength: 20, helpText: 'Must begin with "I am Jack\'s " and be at least 20 characters long'}) | |
, EmailField: forms.EmailField() | |
, FileField: forms.FileField({helpText: 'Required'}) | |
, FileFieldWithInitial: forms.FileField({initial: new FakeFile('Fake File', 'fake.file')}) | |
, ImageField: forms.ImageField({required: false, helpText: 'Optional'}) | |
, ImageFieldWithIniitial: forms.ImageField({required: false, initial: new FakeFile('Fake File', 'fake.file') }) | |
, URLField: forms.URLField({label: 'URL field'}) | |
, BooleanField: forms.BooleanField() | |
, NullBooleanField: forms.NullBooleanField() | |
, ChoiceField: forms.ChoiceField({choices: choicesWithEmpty}) | |
, ChoiceFieldWithCategories: forms.ChoiceField({choices: choicesWithCategories}) | |
, ChoiceFieldWithRadioWidget: forms.ChoiceField({label: 'Choice field (radios)', choices: choices, initial: 4, widget: forms.RadioSelect}) | |
, ChoiceFieldWithRadioWidgetCategories: forms.ChoiceField({label: 'Choice field (radios + categories)', choices: choicesWithCategories, initial: 4, widget: forms.RadioSelect}) | |
, TypedChoiceField: forms.TypedChoiceField({choices: choicesWithEmpty, coerce: Number}) | |
, MultipleChoiceField: forms.MultipleChoiceField({choices: choices}) | |
, MultipleChoiceFieldWithCategories: forms.MultipleChoiceField({choices: choicesWithCategories}) | |
, MultipleChoiceFieldWithCheckboxWidget: forms.MultipleChoiceField({label: 'Multiple choice field (checkboxes)', choices: choices, initial: [1, 3], widget: forms.CheckboxSelectMultiple}) | |
, MultipleChoiceFieldWithCheckboxWidgetCategories: forms.MultipleChoiceField({label: 'Multiple choice field (checkboxes + categories)', choices: choicesWithCategories, initial: [1, 3], widget: forms.CheckboxSelectMultiple}) | |
, TypedMultipleChoiceField: forms.TypedMultipleChoiceField({choices: choices, coerce: Number}) | |
, ComboField: forms.ComboField({fields: [ | |
forms.EmailField() | |
, forms.RegexField(/ferret/i, {errorMessages: {invalid: 'Where is ferret? ಠ_ಠ'}}) | |
], helpText: 'An email address which contains the word "ferret"'}) | |
, SplitDateTimeField: forms.SplitDateTimeField({label: 'Split date/time field (a MultiValueField)', inputDateFormats: dateFormats, inputTimeFormats: [timeFormat]}) | |
, IPAddressField: forms.IPAddressField({label: 'IP address field', helpText: '(Deprecated)'}) | |
, GenericIPAddressField: forms.GenericIPAddressField({label: 'Generic IP address field', helpText: 'An IPv4 or IPv6 address'}) | |
, SlugField: forms.SlugField({helpText: 'Letters, numbers, underscores, and hyphens only'}) | |
, clean: function() { | |
if (this.cleanedData.CharField == 'Answer' && | |
this.cleanedData.IntegerField && | |
this.cleanedData.IntegerField != 42) { | |
this.addError('IntegerField', "That's not The Answer!") | |
throw forms.ValidationError('Please enter The Answer to the Ultimate Question of Life, the Universe, and Everything') | |
} | |
} | |
, render: function() { | |
return this.boundFields().map(function(bf) { | |
// Display cleaneddata, indicating its type | |
var cleanedData | |
if (this.cleanedData && bf.name in this.cleanedData) { | |
cleanedData = this.cleanedData[bf.name] | |
if (Array.isArray(cleanedData)) { | |
cleanedData = JSON.stringify(cleanedData) | |
} | |
else { | |
var isString = (Object.prototype.toString.call(cleanedData) == '[object String]') | |
cleanedData = ''+cleanedData | |
if (isString) { | |
cleanedData = '"' + cleanedData + '"' | |
} | |
} | |
} | |
var help | |
if (bf.helpText) { | |
help = <p>{bf.helpText}</p> | |
} | |
var errors = bf.errors().messages().map(function(message) { | |
return <div>{message}</div> | |
}) | |
return <tr> | |
<th>{bf.labelTag()}</th> | |
<td>{bf.render()}{help}</td> | |
<td>{JSON.stringify(bf.validation(), null, ' ').replace(/"/g, '')}</td> | |
<td>{errors}</td> | |
<td className="cleaned-data">{cleanedData}</td> | |
</tr> | |
}.bind(this)) | |
} | |
}) | |
var CartItemForm = forms.Form.extend({ | |
price: forms.DecimalField({minValue: 0, decimalPlaces: 2}) | |
}) | |
var CartItemFormSet = forms.formsetFactory(CartItemForm) | |
m.module(document.getElementById('app'), { | |
controller: function() { | |
this.form = new AllFieldsForm() | |
this.formset = new CartItemFormSet({initial: [{price: '44.99'}]}) | |
this.addAnother = function() { | |
this.formset.extra++ | |
} | |
this.onsubmit = function(e) { | |
e.preventDefault() | |
var data = forms.formData(document.forms[0]) | |
this.form.setData(data) | |
this.formset.setData(data) | |
} | |
} | |
, view: function(ctrl) { | |
var nonFieldErrors = ctrl.form.nonFieldErrors() | |
var topErrors = nonFieldErrors.isPopulated() | |
? <div><strong>Non field errors:</strong>{nonFieldErrors.render()}</div> | |
: null | |
return <form id="edit_cart" onsubmit={ctrl.onsubmit.bind(ctrl)}> | |
<h2>All Fields</h2> | |
{topErrors} | |
<table> | |
<thead> | |
<tr> | |
<th>Label</th> | |
<th>Input</th> | |
<th>Validation</th> | |
<th>Errors</th> | |
<th>Cleaned Data</th> | |
</tr> | |
</thead> | |
<tbody> | |
{ctrl.form.render()} | |
<tr> | |
<td></td> | |
<td colSpan="3"> | |
<input type="submit" value="Submit"/> | |
</td> | |
</tr> | |
</tbody> | |
</table> | |
<div> | |
<h2>Cart Items</h2> | |
{ctrl.formset.asDiv()} | |
<p> | |
<button type="button" onclick={m.withAttr('target', ctrl.addAnother.bind(ctrl))}>Add Another</button> | |
<button type="submit">Submit</button> | |
</p> | |
</div> | |
</form> | |
} | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Newforms Mithril Test</title> | |
<script src="vendor-mithril.min.js"></script> | |
<script src="vendor-newforms-0.6-alpha-hacked.js"></script> | |
</head> | |
<body> | |
<div id="app"></div> | |
<script src="app.js"></script> | |
<a href="https://gist.github.com/insin/9952028"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub"></a> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Mithril v0.1.6 | |
http://github.com/lhorie/mithril.js | |
(c) Leo Horie | |
License: MIT | |
*/ | |
!new function(a){function b(e,f,g,h,i,j){if(null===f||void 0===f)return void(g&&d(g.nodes));if("retain"!==f.subtree){var k=s.call(g),l=s.call(f);if(k!=l&&(null!==g&&void 0!==g&&d(g.nodes),g=new f.constructor,g.nodes=[]),"[object Array]"==l){for(var m=[],n=g.length===f.length,o=0;o<f.length;o++){var p=b(e,f[o],g[o],h,i+o||o,j);void 0!==p&&(p.nodes.intact||(n=!1),g[o]=p)}if(!n){for(var o=0;o<f.length;o++)void 0!==g[o]&&(m=m.concat(g[o].nodes));for(var q,o=m.length;q=g.nodes[o];o++)null!==q.parentNode&&q.parentNode.removeChild(q);for(var q,o=g.nodes.length;q=m[o];o++)null===q.parentNode&&e.appendChild(q);g.length=f.length,g.nodes=m}}else if("[object Object]"==l){if((f.tag!=g.tag||Object.keys(f.attrs).join()!=Object.keys(g.attrs).join())&&d(g.nodes),"string"!=typeof f.tag)return;var q,r=0===g.nodes.length;"svg"===f.tag&&(j="http://www.w3.org/2000/svg"),r?(q=void 0===j?a.document.createElement(f.tag):a.document.createElementNS(j,f.tag),g={tag:f.tag,attrs:c(q,f.attrs,{},j),children:b(q,f.children,g.children,!0,i,j),nodes:[q]},e.insertBefore(q,void 0!==i?e.childNodes[i]:null)):(q=g.nodes[0],c(q,f.attrs,g.attrs,j),g.children=b(q,f.children,g.children,!1,i,j),g.nodes.intact=!0,h===!0&&e.insertBefore(q,void 0!==i?e.childNodes[i]:null)),"[object Function]"==s.call(f.attrs.config)&&f.attrs.config(q,!r)}else{var q;if(0===g.nodes.length){if(f.$trusted){var t=e.lastChild;e.insertAdjacentHTML("beforeend",f),q=t?t.nextSibling:e.firstChild}else q=a.document.createTextNode(f),e.insertBefore(q,void 0!==i?e.childNodes[i]:null);g="string number boolean".indexOf(typeof f)>-1?new f.constructor(f):f,g.nodes=[q]}else if(g.valueOf()!==f.valueOf()||h===!0){if(f.$trusted){var u=g.nodes[0],m=[u];if(u){for(;u=u.nextSibling;)m.push(u);d(m);var t=e.lastChild;e.insertAdjacentHTML("beforeend",f),q=t?t.nextSibling:e.firstChild}else e.innerHTML=f}else q=g.nodes[0],e.insertBefore(q,void 0!==i?e.childNodes[i]:null),q.nodeValue=f;g=new f.constructor(f),g.nodes=[q]}else g.nodes.intact=!0}return g}}function c(b,c,d,e){for(var g in c){var h=c[g],i=d[g];if(!(g in d)||i!==h||b===a.document.activeElement){if(d[g]=h,"config"===g)continue;if("function"==typeof h&&0==g.indexOf("on"))b[g]=f(h,b);else if("style"===g)for(var j in h)(void 0===i||i[j]!==h[j])&&(b.style[j]=h[j]);else void 0!==e?"href"===g?b.setAttributeNS("http://www.w3.org/1999/xlink","href",h):"className"===g?b.setAttribute("class",h):b.setAttribute(g,h):g in b?b[g]=h:b.setAttribute(g,h)}}return d}function d(a){for(var b=0;b<a.length;b++)a[b].parentNode.removeChild(a[b]);a.length=0}function e(a){var b={};for(var c in a)b[c]=a[c];return b}function f(a,b){return function(c){m.startComputation();var d=a.call(b,c);return m.endComputation(),d}}function g(){if(C=a.performance&&a.performance.now?a.performance.now():(new a.Date).getTime(),C-D>16)m.redraw();else{var b=a.cancelAnimationFrame||a.clearTimeout,c=a.requestAnimationFrame||a.setTimeout;b(E),E=c(m.redraw,0)}}function h(a,b,c){J={};for(var d in b){if(d==c)return!void m.module(a,b[d]);var e=new RegExp("^"+d.replace(/:[^\/]+/g,"([^\\/]+)")+"$");if(e.test(c))return!void c.replace(e,function(){for(var c=d.match(/:[^\/]+/g),e=[].slice.call(arguments,1,-2),f=0;f<c.length;f++)J[c[f].slice(1)]=e[f];m.module(a,b[d])})}}function i(a){a.preventDefault(),m.route(a.currentTarget.getAttribute("href"))}function j(){"hash"!=m.route.mode&&a.location.hash&&(a.location.hash=a.location.hash)}function k(a){return a}function l(b){var c=a.XDomainRequest?new a.XDomainRequest:new a.XMLHttpRequest;return c.open(b.method,b.url,!0,b.user,b.password),c.onload="function"==typeof b.onload?b.onload:function(){},c.onerror="function"==typeof b.onerror?b.onerror:function(){},"function"==typeof b.config&&b.config(c,b),c.send(b.data),c}function n(a,b){var c=[];for(var d in a){var e=b?b+"["+d+"]":d,f=a[d];c.push("object"==typeof f?n(f,e):encodeURIComponent(e)+"="+encodeURIComponent(f))}return c.join("&")}function o(a,b,c){return b&&Object.keys(b).length>0&&("GET"==a.method?a.url=a.url+(a.url.indexOf("?")<0?"?":"&")+n(b):a.data=c(b)),a}function p(a,b){var c=a.match(/:[a-z]\w+/gi);if(c&&b)for(var d=0;d<c.length;d++){var e=c[d].slice(1);a=a.replace(c[d],b[e]),delete b[e]}return a}function q(a){var b=a.then;return function(a,c){var d=b(function(b){return d(a(b))},function(a){return d(c(a))});return d.then=q(d),d}}var r={},s={}.toString,t=/(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g,u=/\[(.+?)(?:=("|'|)(.+?)\2)?\]/;Mithril=m=function(){var a=arguments,b="[object Object]"==s.call(a[1]),c=b?a[1]:{},d="class"in c?"class":"className",f=r[a[0]];if(void 0===f){r[a[0]]=f={tag:"div",attrs:{}};for(var g,h=[];g=t.exec(a[0]);)if(""==g[1])f.tag=g[2];else if("#"==g[1])f.attrs.id=g[2];else if("."==g[1])h.push(g[2]);else if("["==g[3][0]){var i=u.exec(g[3]);f.attrs[i[1]]=i[3]||!0}h.length>0&&(f.attrs[d]=h.join(" "))}f=e(f),f.attrs=e(f.attrs),f.children=b?a[2]:a[1];for(var j in c)f.attrs[j]=j==d?(f.attrs[j]||"")+" "+c[j]:c[j];return f};var v,w={insertAdjacentHTML:function(b,c){a.document.write(c),a.document.close()},appendChild:function(b){void 0===v&&(v=a.document.createElement("html")),"HTML"==b.nodeName?v=b:v.appendChild(b),a.document.documentElement!==v&&a.document.replaceChild(v,a.document.documentElement)},insertBefore:function(a){this.appendChild(a)}},x=[],y={};m.render=function(c,d){var e=x.indexOf(c),f=0>e?x.push(c)-1:e,g=c==a.document||c==a.document.documentElement?w:c;y[f]=b(g,d,y[f],!1)},m.trust=function(a){return a=new String(a),a.$trusted=!0,a};var z,A={view:function(){}},B={},C=0,D=0,E=0;m.module=function(a,b){m.startComputation(),z=a,A=b,B=new b.controller,m.endComputation()},m.redraw=function(){m.render(z,A.view(B)),D=C};var F=0,G=null;m.startComputation=function(){F++},m.endComputation=function(){F=Math.max(F-1,0),0==F&&(g(),G&&(G(),G=null))},m.withAttr=function(a,b){return function(c){b(a in c.currentTarget?c.currentTarget[a]:c.currentTarget.getAttribute(a))}};var H={pathname:"",hash:"#",search:"?"},I=function(){},J={};m.route=function(){if(3==arguments.length){var b=arguments[0],c=arguments[1],d=arguments[2];I=function(a){var e=a.slice(H[m.route.mode].length);h(b,d,e)||m.route(c,!0)};var e="hash"==m.route.mode?"onhashchange":"onpopstate";a[e]=function(){I(a.location[m.route.mode])},G=j,a[e]()}else if(arguments[0].addEventListener){var f=arguments[0],g=arguments[1];g||(f.removeEventListener("click",i),f.addEventListener("click",i))}else if("string"==typeof arguments[0]){var k=arguments[0],l=arguments[1]===!0;a.history.pushState?(G=function(){a.history[l?"replaceState":"pushState"](null,a.document.title,H[m.route.mode]+k),j()},I(H[m.route.mode]+k)):a.location[m.route.mode]=k}},m.route.param=function(a){return J[a]},m.route.mode="search",m.prop=function(a){return function(){return arguments.length&&(a=arguments[0]),a}},m.deferred=function(){var a=[],b=[],c={resolve:function(b){for(var c=0;c<a.length;c++)a[c](b)},reject:function(a){for(var c=0;c<b.length;c++)b[c](a)},promise:m.prop()};return c.promise.resolvers=a,c.promise.then=function(c,d){function e(a,b,c){a.push(function(a){try{var e=c(a);e&&"function"==typeof e.then?e.then(f[b],d):f[b](void 0!==e?e:a)}catch(g){if(g instanceof Error&&g.constructor!==Error)throw g;f.reject(g)}})}var f=m.deferred();return c||(c=k),d||(d=k),e(a,"resolve",c),e(b,"reject",d),f.promise},c},m.sync=function(a){function b(b){return function(f){return e.push(f),b||(c="reject"),e.length==a.length&&(d.promise(e),d[c](e)),f}}for(var c="resolve",d=m.deferred(),e=[],f=0;f<a.length;f++)a[f].then(b(!0),b(!1));return d.promise},m.request=function(a){m.startComputation();var b=m.deferred(),c=a.serialize||JSON.stringify,d=a.deserialize||JSON.parse;return a.url=p(a.url,a.data),a=o(a,a.data,c),a.onload=a.onerror=function(c){var e=("load"==c.type?a.unwrapSuccess:a.unwrapError)||k,f=e(d(c.target.responseText));if(f instanceof Array&&a.type)for(var g=0;g<f.length;g++)f[g]=new a.type(f[g]);else a.type&&(f=new a.type(f));b.promise(f),b["load"==c.type?"resolve":"reject"](f),m.endComputation()},l(a),b.promise.then=q(b.promise),b.promise},"undefined"!=typeof module&&null!==module&&(module.exports=m),"function"==typeof define&&define.amd&&define(function(){return m}),m.deps=function(b){return a=b}}(this); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* newforms 0.6.0-alpha (manually hacked for mithril output based on a build made from the React branch on Tue, 1 Apr 2014 12:44 GMT) - https://github.com/insin/newforms | |
* MIT Licensed | |
*/ | |
!function(e){if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.forms=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){ | |
'use strict'; | |
module.exports = { | |
browser: typeof process == 'undefined' | |
} | |
},{}],2:[function(_dereq_,module,exports){ | |
'use strict'; | |
var Concur = _dereq_('Concur') | |
var is = _dereq_('isomorph/is') | |
var object = _dereq_('isomorph/object') | |
var time = _dereq_('isomorph/time') | |
var url = _dereq_('isomorph/url') | |
var validators = _dereq_('validators') | |
var env = _dereq_('./env') | |
var formats = _dereq_('./formats') | |
var util = _dereq_('./util') | |
var widgets = _dereq_('./widgets') | |
var ValidationError = validators.ValidationError | |
var Widget = widgets.Widget | |
var cleanIPv6Address = validators.ipv6.cleanIPv6Address | |
/** | |
* An object that is responsible for doing validation and normalisation, or | |
* "cleaning", for example: an EmailField makes sure its data is a valid | |
* e-mail address and makes sure that acceptable "blank" values all have the | |
* same representation. | |
* @constructor | |
* @param {Object=} kwargs | |
*/ | |
var Field = Concur.extend({ | |
widget: widgets.TextInput // Default widget to use when rendering this type of Field | |
, hiddenWidget: widgets.HiddenInput // Default widget to use when rendering this as "hidden" | |
, defaultValidators: [] // Default list of validators | |
// Add an 'invalid' entry to defaultErrorMessages if you want a specific | |
// field error message not raised by the field validators. | |
, defaultErrorMessages: { | |
required: 'This field is required.' | |
} | |
, emptyValues: validators.EMPTY_VALUES.slice() | |
, emptyValueArray: true // Should isEmptyValue check for empty Arrays? | |
, constructor: function Field(kwargs) { | |
kwargs = object.extend({ | |
required: true, widget: null, label: null, initial: null, | |
helpText: null, errorMessages: null, showHiddenInitial: false, | |
validators: [], cssClass: null, validation: null, custom: null | |
}, kwargs) | |
this.required = kwargs.required | |
this.label = kwargs.label | |
this.initial = kwargs.initial | |
this.showHiddenInitial = kwargs.showHiddenInitial | |
this.helpText = kwargs.helpText || '' | |
this.cssClass = kwargs.cssClass | |
this.validation = kwargs.validation | |
// Normalise validation config to an object if it's not set to manual | |
if (is.String(this.validation) && this.validation != 'manual') { | |
this.validation = (this.validation == 'auto' | |
? {event: 'onChange', delay: 250} | |
: {event: this.validation}) | |
} | |
this.custom = kwargs.custom | |
var widget = kwargs.widget || this.widget | |
if (!(widget instanceof Widget)) { | |
// We must have a Widget constructor, so construct with it | |
widget = new widget() | |
} | |
// Let the widget know whether it should display as required | |
widget.isRequired = this.required | |
// Hook into this.widgetAttrs() for any Field-specific HTML attributes | |
object.extend(widget.attrs, this.widgetAttrs(widget)) | |
this.widget = widget | |
// Increment the creation counter and save our local copy | |
this.creationCounter = Field.creationCounter++ | |
// Copy error messages for this instance into a new object and override | |
// with any provided error messages. | |
var messages = [{}] | |
for (var i = this.constructor.__mro__.length - 1; i >=0; i--) { | |
messages.push(object.get(this.constructor.__mro__[i].prototype, | |
'defaultErrorMessages', null)) | |
} | |
messages.push(kwargs.errorMessages) | |
this.errorMessages = object.extend.apply(object, messages) | |
this.validators = this.defaultValidators.concat(kwargs.validators) | |
} | |
}) | |
/** | |
* Tracks each time a Field instance is created; used to retain order. | |
*/ | |
Field.creationCounter = 0 | |
Field.prototype.prepareValue = function(value) { | |
return value | |
} | |
Field.prototype.toJavaScript = function(value) { | |
return value | |
} | |
/** | |
* Checks for the given value being === one of the configured empty values, plus | |
* any additional checks required due to JavaScript's lack of a generic object | |
* equality checking mechanism. | |
*/ | |
Field.prototype.isEmptyValue = function(value) { | |
if (this.emptyValues.indexOf(value) != -1) { | |
return true | |
} | |
if (this.emptyValueArray === true && is.Array(value) && value.length === 0) { | |
return true | |
} | |
return false | |
} | |
Field.prototype.validate = function(value) { | |
if (this.required && this.isEmptyValue(value)) { | |
throw ValidationError(this.errorMessages.required, {code: 'required'}) | |
} | |
} | |
Field.prototype.runValidators = function(value) { | |
if (this.isEmptyValue(value)) { | |
return | |
} | |
var errors = [] | |
for (var i = 0, l = this.validators.length; i < l; i++) { | |
var validator = this.validators[i] | |
try { | |
validator(value) | |
} | |
catch (e) { | |
if (!(e instanceof ValidationError)) { throw e } | |
if (object.hasOwn(e, 'code') && | |
object.hasOwn(this.errorMessages, e.code)) { | |
e.message = this.errorMessages[e.code] | |
} | |
errors.push.apply(errors, e.errorList) | |
} | |
} | |
if (errors.length > 0) { | |
throw ValidationError(errors) | |
} | |
} | |
/** | |
* Validates the given value and returns its "cleaned" value as an appropriate | |
* JavaScript object. | |
* Throws a ValidationError for any errors. | |
* @param {String} value the value to be validated. | |
*/ | |
Field.prototype.clean = function(value) { | |
value = this.toJavaScript(value) | |
this.validate(value) | |
this.runValidators(value) | |
return value | |
} | |
/** | |
* Return the value that should be shown for this field on render of a bound | |
* form, given the submitted POST data for the field and the initial data, if | |
* any. | |
* | |
* For most fields, this will simply be data; FileFields need to handle it a bit | |
* differently. | |
*/ | |
Field.prototype.boundData = function(data, initial) { | |
return data | |
} | |
/** | |
* Specifies HTML attributes which should be added to a given widget for this | |
* field. | |
* | |
* @param {Widget} widget a widget. | |
* @return an object specifying HTML attributes that should be added to the | |
* given widget, based on this field. | |
*/ | |
Field.prototype.widgetAttrs = function(widget) { | |
return {} | |
} | |
/** | |
* Returns true if data differs from initial. | |
*/ | |
Field.prototype._hasChanged = function(initial, data) { | |
// For purposes of seeing whether something has changed, null is the same | |
// as an empty string, if the data or inital value we get is null, replace | |
// it with ''. | |
var initialValue = (initial === null ? '' : initial) | |
try { | |
data = this.toJavaScript(data) | |
if (typeof this._coerce == 'function') { | |
data = this._coerce(data) | |
} | |
} | |
catch (e) { | |
if (!(e instanceof ValidationError)) { throw e } | |
return true | |
} | |
var dataValue = (data === null ? '' : data) | |
return (''+initialValue != ''+dataValue) // TODO is forcing to string necessary? | |
} | |
/** | |
* Validates that its input is a valid String. | |
* @constructor | |
* @extends {Field} | |
* @param {Object=} kwargs | |
*/ | |
var CharField = Field.extend({ | |
constructor: function CharField(kwargs) { | |
if (!(this instanceof Field)) { return new CharField(kwargs) } | |
kwargs = object.extend({maxLength: null, minLength: null}, kwargs) | |
this.maxLength = kwargs.maxLength | |
this.minLength = kwargs.minLength | |
Field.call(this, kwargs) | |
if (this.minLength !== null) { | |
this.validators.push(validators.MinLengthValidator(this.minLength)) | |
} | |
if (this.maxLength !== null) { | |
this.validators.push(validators.MaxLengthValidator(this.maxLength)) | |
} | |
} | |
}) | |
CharField.prototype.toJavaScript = function(value) { | |
if (this.isEmptyValue(value)) { | |
return '' | |
} | |
return ''+value | |
} | |
/** | |
* If this field is configured to enforce a maximum length, adds a suitable | |
* maxLength attribute to text input fields. | |
* | |
* @param {Widget} widget the widget being used to render this field's value. | |
* | |
* @return additional attributes which should be added to the given widget. | |
*/ | |
CharField.prototype.widgetAttrs = function(widget) { | |
var attrs = {} | |
if (this.maxLength !== null && (widget instanceof widgets.TextInput || | |
widget instanceof widgets.PasswordInput)) { | |
attrs.maxLength = ''+this.maxLength | |
} | |
return attrs | |
} | |
/** | |
* Validates that its input is a valid integer. | |
* @constructor | |
* @extends {Field} | |
* @param {Object=} kwargs | |
*/ | |
var IntegerField = Field.extend({ | |
widget: widgets.NumberInput | |
, defaultErrorMessages: { | |
invalid: 'Enter a whole number.' | |
} | |
, constructor: function IntegerField(kwargs) { | |
if (!(this instanceof Field)) { return new IntegerField(kwargs) } | |
kwargs = object.extend({maxValue: null, minValue: null}, kwargs) | |
this.maxValue = kwargs.maxValue | |
this.minValue = kwargs.minValue | |
Field.call(this, kwargs) | |
if (this.minValue !== null) { | |
this.validators.push(validators.MinValueValidator(this.minValue)) | |
} | |
if (this.maxValue !== null) { | |
this.validators.push(validators.MaxValueValidator(this.maxValue)) | |
} | |
} | |
}) | |
/** | |
* Validates that Number() can be called on the input with a result that isn't | |
* NaN and doesn't contain any decimal points. | |
* | |
* @param value the value to be val idated. | |
* @return the result of Number(), or null for empty values. | |
*/ | |
IntegerField.prototype.toJavaScript = function(value) { | |
value = Field.prototype.toJavaScript.call(this, value) | |
if (this.isEmptyValue(value)) { | |
return null | |
} | |
value = Number(value) | |
if (isNaN(value) || value.toString().indexOf('.') != -1) { | |
throw ValidationError(this.errorMessages.invalid, {code: 'invalid'}) | |
} | |
return value | |
} | |
IntegerField.prototype.widgetAttrs = function(widget) { | |
var attrs = Field.prototype.widgetAttrs.call(this, widget) | |
if (widget instanceof widgets.NumberInput) { | |
if (this.minValue !== null) { | |
attrs.min = this.minValue | |
} | |
if (this.maxValue !== null) { | |
attrs.max = this.maxValue | |
} | |
} | |
return attrs | |
} | |
/** | |
* Validates that its input is a valid float. | |
* @constructor | |
* @extends {IntegerField} | |
* @param {Object=} kwargs | |
*/ | |
var FloatField = IntegerField.extend({ | |
defaultErrorMessages: { | |
invalid: 'Enter a number.' | |
} | |
, constructor: function FloatField(kwargs) { | |
if (!(this instanceof Field)) { return new FloatField(kwargs) } | |
IntegerField.call(this, kwargs) | |
} | |
}) | |
/** Float validation regular expression, as parseFloat() is too forgiving. */ | |
FloatField.FLOAT_REGEXP = /^[-+]?(?:\d+(?:\.\d*)?|(?:\d+)?\.\d+)$/ | |
/** | |
* Validates that the input looks like valid input for parseFloat() and the | |
* result of calling it isn't NaN. | |
* @param value the value to be validated. | |
* @return a Number obtained from parseFloat(), or null for empty values. | |
*/ | |
FloatField.prototype.toJavaScript = function(value) { | |
value = Field.prototype.toJavaScript.call(this, value) | |
if (this.isEmptyValue(value)) { | |
return null | |
} | |
value = util.strip(value) | |
if (!FloatField.FLOAT_REGEXP.test(value)) { | |
throw ValidationError(this.errorMessages.invalid, {code: 'invalid'}) | |
} | |
value = parseFloat(value) | |
if (isNaN(value)) { | |
throw ValidationError(this.errorMessages.invalid, {code: 'invalid'}) | |
} | |
return value | |
} | |
/** | |
* Determines if data has changed from initial. In JavaScript, trailing zeroes | |
* in floats are dropped when a float is coerced to a String, so e.g., an | |
* initial value of 1.0 would not match a data value of '1.0' if we were to use | |
* the Widget object's _hasChanged, which checks coerced String values. | |
* @type Boolean | |
*/ | |
FloatField.prototype._hasChanged = function(initial, data) { | |
// For purposes of seeing whether something has changed, null is the same | |
// as an empty string, if the data or inital value we get is null, replace | |
// it with ''. | |
var dataValue = (data === null ? '' : data) | |
var initialValue = (initial === null ? '' : initial) | |
if (initialValue === dataValue) { | |
return false | |
} | |
else if (initialValue === '' || dataValue === '') { | |
return true | |
} | |
return (parseFloat(''+initialValue) != parseFloat(''+dataValue)) | |
} | |
FloatField.prototype.widgetAttrs = function(widget) { | |
var attrs = IntegerField.prototype.widgetAttrs.call(this, widget) | |
if (widget instanceof widgets.NumberInput && | |
!object.hasOwn(widget.attrs, 'step')) { | |
object.setDefault(attrs, 'step', 'any') | |
} | |
return attrs | |
} | |
/** | |
* Validates that its input is a decimal number. | |
* @constructor | |
* @extends {Field} | |
* @param {Object=} kwargs | |
*/ | |
var DecimalField = IntegerField.extend({ | |
defaultErrorMessages: { | |
invalid: 'Enter a number.' | |
, maxDigits: 'Ensure that there are no more than {max} digits in total.' | |
, maxDecimalPlaces: 'Ensure that there are no more than {max} decimal places.' | |
, maxWholeDigits: 'Ensure that there are no more than {max} digits before the decimal point.' | |
} | |
, constructor: function DecimalField(kwargs) { | |
if (!(this instanceof Field)) { return new DecimalField(kwargs) } | |
kwargs = object.extend({maxDigits: null, decimalPlaces: null}, kwargs) | |
this.maxDigits = kwargs.maxDigits | |
this.decimalPlaces = kwargs.decimalPlaces | |
IntegerField.call(this, kwargs) | |
} | |
}) | |
/** Decimal validation regular expression, in lieu of a Decimal type. */ | |
DecimalField.DECIMAL_REGEXP = /^[-+]?(?:\d+(?:\.\d*)?|(?:\d+)?\.\d+)$/ | |
/** | |
* DecimalField overrides the clean() method as it performs its own validation | |
* against a different value than that given to any defined validators, due to | |
* JavaScript lacking a built-in Decimal type. Decimal format and component size | |
* checks will be performed against a normalised string representation of the | |
* input, whereas Validators will be passed a float version of the value for | |
* min/max checking. | |
* @param {string|Number} value | |
* @return {string} a normalised version of the input. | |
*/ | |
DecimalField.prototype.clean = function(value) { | |
// Take care of empty, required validation | |
Field.prototype.validate.call(this, value) | |
if (this.isEmptyValue(value)) { | |
return null | |
} | |
// Coerce to string and validate that it looks Decimal-like | |
value = util.strip(''+value) | |
if (!DecimalField.DECIMAL_REGEXP.test(value)) { | |
throw ValidationError(this.errorMessages.invalid, {code: 'invalid'}) | |
} | |
// In lieu of a Decimal type, DecimalField validates against a string | |
// representation of a Decimal, in which: | |
// * Any leading sign has been stripped | |
var negative = false | |
if (value.charAt(0) == '+' || value.charAt(0) == '-') { | |
negative = (value.charAt(0) == '-') | |
value = value.substr(1) | |
} | |
// * Leading zeros have been stripped from digits before the decimal point, | |
// but trailing digits are retained after the decimal point. | |
value = value.replace(/^0+/, '') | |
// * If the input ended with a '.', it is stripped | |
if (value.indexOf('.') == value.length - 1) { | |
value = value.substring(0, value.length - 1) | |
} | |
// Perform own validation | |
var pieces = value.split('.') | |
var wholeDigits = pieces[0].length | |
var decimals = (pieces.length == 2 ? pieces[1].length : 0) | |
var digits = wholeDigits + decimals | |
if (this.maxDigits !== null && digits > this.maxDigits) { | |
throw ValidationError(this.errorMessages.maxDigits, { | |
code: 'maxDigits' | |
, params: {max: this.maxDigits} | |
}) | |
} | |
if (this.decimalPlaces !== null && decimals > this.decimalPlaces) { | |
throw ValidationError(this.errorMessages.maxDecimalPlaces, { | |
code: 'maxDecimalPlaces' | |
, params: {max: this.decimalPlaces} | |
}) | |
} | |
if (this.maxDigits !== null && | |
this.decimalPlaces !== null && | |
wholeDigits > (this.maxDigits - this.decimalPlaces)) { | |
throw ValidationError(this.errorMessages.maxWholeDigits, { | |
code: 'maxWholeDigits' | |
, params: {max: (this.maxDigits - this.decimalPlaces)} | |
}) | |
} | |
// * Values which did not have a leading zero gain a single leading zero | |
if (value.charAt(0) == '.') { | |
value = '0' + value | |
} | |
// Restore sign if necessary | |
if (negative) { | |
value = '-' + value | |
} | |
// Validate against a float value - best we can do in the meantime | |
this.runValidators(parseFloat(value)) | |
// Return the normalited String representation | |
return value | |
} | |
DecimalField.prototype.widgetAttrs = function(widget) { | |
var attrs = IntegerField.prototype.widgetAttrs.call(this, widget) | |
if (widget instanceof widgets.NumberInput && | |
!object.hasOwn(widget.attrs, 'step')) { | |
var step = 'any' | |
if (this.decimalPlaces !== null) { | |
// Use exponential notation for small values since they might | |
// be parsed as 0 otherwise. | |
if (this.decimalPlaces === 0) { | |
step = '1' | |
} | |
else if (this.decimalPlaces < 7) { | |
step = '0.' + '000001'.slice(-this.decimalPlaces) | |
} | |
else { | |
step = '1e-' + this.decimalPlaces | |
} | |
} | |
object.setDefault(attrs, 'step', step) | |
} | |
return attrs | |
} | |
/** | |
* Base field for fields which validate that their input is a date or time. | |
* @constructor | |
* @extends {Field} | |
* @param {Object=} kwargs | |
*/ | |
var BaseTemporalField = Field.extend({ | |
constructor: function BaseTemporalField(kwargs) { | |
kwargs = object.extend({inputFormats: null}, kwargs) | |
Field.call(this, kwargs) | |
if (kwargs.inputFormats !== null) { | |
this.inputFormats = kwargs.inputFormats | |
} | |
} | |
}) | |
/** | |
* Validates that its input is a valid date or time. | |
* @param {String|Date} | |
* @return {Date} | |
*/ | |
BaseTemporalField.prototype.toJavaScript = function(value) { | |
if (!is.Date(value)) { | |
value = util.strip(value) | |
} | |
if (is.String(value)) { | |
for (var i = 0, l = this.inputFormats.length; i < l; i++) { | |
try { | |
return this.strpdate(value, this.inputFormats[i]) | |
} | |
catch (e) { | |
continue | |
} | |
} | |
} | |
throw ValidationError(this.errorMessages.invalid, {code: 'invalid'}) | |
} | |
/** | |
* Creates a Date from the given input if it's valid based on a format. | |
* @param {String} value | |
* @param {String} format | |
* @return {Date} | |
*/ | |
BaseTemporalField.prototype.strpdate = function(value, format) { | |
return time.strpdate(value, format) | |
} | |
BaseTemporalField.prototype._hasChanged = function(initial, data) { | |
try { | |
data = this.toJavaScript(data) | |
} | |
catch (e) { | |
if (!(e instanceof ValidationError)) { throw e } | |
return true | |
} | |
initial = this.toJavaScript(initial) | |
if (!!initial && !!data) { | |
return initial.getTime() !== data.getTime() | |
} | |
else { | |
return initial !== data | |
} | |
} | |
/** | |
* Validates that its input is a date. | |
* @constructor | |
* @extends {BaseTemporalField} | |
* @param {Object=} kwargs | |
*/ | |
var DateField = BaseTemporalField.extend({ | |
widget: widgets.DateInput | |
, inputFormats: formats.DEFAULT_DATE_INPUT_FORMATS | |
, defaultErrorMessages: { | |
invalid: 'Enter a valid date.' | |
} | |
, constructor: function DateField(kwargs) { | |
if (!(this instanceof Field)) { return new DateField(kwargs) } | |
BaseTemporalField.call(this, kwargs) | |
} | |
}) | |
/** | |
* Validates that the input can be converted to a date. | |
* @param {String|Date} value the value to be validated. | |
* @return {?Date} a with its year, month and day attributes set, or null for | |
* empty values when they are allowed. | |
*/ | |
DateField.prototype.toJavaScript = function(value) { | |
if (this.isEmptyValue(value)) { | |
return null | |
} | |
if (value instanceof Date) { | |
return new Date(value.getFullYear(), value.getMonth(), value.getDate()) | |
} | |
return BaseTemporalField.prototype.toJavaScript.call(this, value) | |
} | |
/** | |
* Validates that its input is a time. | |
* @constructor | |
* @extends {BaseTemporalField} | |
* @param {Object=} kwargs | |
*/ | |
var TimeField = BaseTemporalField.extend({ | |
widget: widgets.TimeInput | |
, inputFormats: formats.DEFAULT_TIME_INPUT_FORMATS | |
, defaultErrorMessages: { | |
invalid: 'Enter a valid time.' | |
} | |
, constructor: function TimeField(kwargs) { | |
if (!(this instanceof Field)) { return new TimeField(kwargs) } | |
BaseTemporalField.call(this, kwargs) | |
} | |
}) | |
/** | |
* Validates that the input can be converted to a time. | |
* @param {String|Date} value the value to be validated. | |
* @return {?Date} a Date with its hour, minute and second attributes set, or | |
* null for empty values when they are allowed. | |
*/ | |
TimeField.prototype.toJavaScript = function(value) { | |
if (this.isEmptyValue(value)) { | |
return null | |
} | |
if (value instanceof Date) { | |
return new Date(1900, 0, 1, value.getHours(), value.getMinutes(), value.getSeconds()) | |
} | |
return BaseTemporalField.prototype.toJavaScript.call(this, value) | |
} | |
/** | |
* Creates a Date representing a time from the given input if it's valid based | |
* on the format. | |
* @param {String} value | |
* @param {String} format | |
* @return {Date} | |
*/ | |
TimeField.prototype.strpdate = function(value, format) { | |
var t = time.strptime(value, format) | |
return new Date(1900, 0, 1, t[3], t[4], t[5]) | |
} | |
/** | |
* Validates that its input is a date/time. | |
* @constructor | |
* @extends {BaseTemporalField} | |
* @param {Object=} kwargs | |
*/ | |
var DateTimeField = BaseTemporalField.extend({ | |
widget: widgets.DateTimeInput | |
, inputFormats: formats.DEFAULT_DATETIME_INPUT_FORMATS | |
, defaultErrorMessages: { | |
invalid: 'Enter a valid date/time.' | |
} | |
, constructor: function DateTimeField(kwargs) { | |
if (!(this instanceof Field)) { return new DateTimeField(kwargs) } | |
BaseTemporalField.call(this, kwargs) | |
} | |
}) | |
/** | |
* @param {String|Date|Array.<Date>} | |
* @return {?Date} | |
*/ | |
DateTimeField.prototype.toJavaScript = function(value) { | |
if (this.isEmptyValue(value)) { | |
return null | |
} | |
if (value instanceof Date) { | |
return value | |
} | |
if (is.Array(value)) { | |
// Input comes from a SplitDateTimeWidget, for example, so it's two | |
// components: date and time. | |
if (value.length != 2) { | |
throw ValidationError(this.errorMessages.invalid, {code: 'invalid'}) | |
} | |
if (this.isEmptyValue(value[0]) && this.isEmptyValue(value[1])) { | |
return null | |
} | |
value = value.join(' ') | |
} | |
return BaseTemporalField.prototype.toJavaScript.call(this, value) | |
} | |
/** | |
* Validates that its input matches a given regular expression. | |
* @constructor | |
* @extends {CharField} | |
* @param {{regexp|string}} regex | |
* @param {Object=} kwargs | |
*/ | |
var RegexField = CharField.extend({ | |
constructor: function RegexField(regex, kwargs) { | |
if (!(this instanceof Field)) { return new RegexField(regex, kwargs) } | |
CharField.call(this, kwargs) | |
if (is.String(regex)) { | |
regex = new RegExp(regex) | |
} | |
this.regex = regex | |
this.validators.push(validators.RegexValidator({regex: this.regex})) | |
} | |
}) | |
/** | |
* Validates that its input appears to be a valid e-mail address. | |
* @constructor | |
* @extends {CharField} | |
* @param {Object=} kwargs | |
*/ | |
var EmailField = CharField.extend({ | |
widget: widgets.EmailInput | |
, defaultValidators: [validators.validateEmail] | |
, constructor: function EmailField(kwargs) { | |
if (!(this instanceof Field)) { return new EmailField(kwargs) } | |
CharField.call(this, kwargs) | |
} | |
}) | |
EmailField.prototype.clean = function(value) { | |
value = util.strip(this.toJavaScript(value)) | |
return CharField.prototype.clean.call(this, value) | |
} | |
/** | |
* Validates that its input is a valid uploaded file. | |
* @constructor | |
* @extends {Field} | |
* @param {Object=} kwargs | |
*/ | |
var FileField = Field.extend({ | |
widget: widgets.ClearableFileInput | |
, defaultErrorMessages: { | |
invalid: 'No file was submitted. Check the encoding type on the form.' | |
, missing: 'No file was submitted.' | |
, empty: 'The submitted file is empty.' | |
, maxLength: 'Ensure this filename has at most {max} characters (it has {length}).' | |
, contradicton: 'Please either submit a file or check the clear checkbox, not both.' | |
} | |
, constructor: function FileField(kwargs) { | |
if (!(this instanceof Field)) { return new FileField(kwargs) } | |
kwargs = object.extend({maxLength: null, allowEmptyFile: false}, kwargs) | |
this.maxLength = kwargs.maxLength | |
this.allowEmptyFile = kwargs.allowEmptyFile | |
delete kwargs.maxLength | |
Field.call(this, kwargs) | |
} | |
}) | |
FileField.prototype.toJavaScript = function(data, initial) { | |
if (this.isEmptyValue(data)) { | |
return null | |
} | |
if (env.browser) { | |
return data | |
} | |
// UploadedFile objects should have name and size attributes | |
if (typeof data.name == 'undefined' || typeof data.size == 'undefined') { | |
throw ValidationError(this.errorMessages.invalid, {code: 'invalid'}) | |
} | |
var fileName = data.name | |
var fileSize = data.size | |
if (this.maxLength !== null && fileName.length > this.maxLength) { | |
throw ValidationError(this.errorMessages.maxLength, { | |
code: 'maxLength' | |
, params: {max: this.maxLength, length: fileName.length} | |
}) | |
} | |
if (!fileName) { | |
throw ValidationError(this.errorMessages.invalid, {code: 'invalid'}) | |
} | |
if (!this.allowEmptyFile && !fileSize) { | |
throw ValidationError(this.errorMessages.empty, {code: 'empty'}) | |
} | |
return data | |
} | |
FileField.prototype.clean = function(data, initial) { | |
// If the widget got contradictory inputs, we raise a validation error | |
if (data === widgets.FILE_INPUT_CONTRADICTION) { | |
throw ValidationError(this.errorMessages.contradiction, | |
{code: 'contradiction'}) | |
} | |
// false means the field value should be cleared; further validation is | |
// not needed. | |
if (data === false) { | |
if (!this.required) { | |
return false | |
} | |
// If the field is required, clearing is not possible (the widget | |
// shouldn't return false data in that case anyway). false is not | |
// in EMPTY_VALUES; if a false value makes it this far it should be | |
// validated from here on out as null (so it will be caught by the | |
// required check). | |
data = null | |
} | |
if (!data && initial) { | |
return initial | |
} | |
return Field.prototype.clean.call(this, data) | |
} | |
FileField.prototype.boundData = function(data, initial) { | |
if (data === null || data === widgets.FILE_INPUT_CONTRADICTION) { | |
return initial | |
} | |
return data | |
} | |
FileField.prototype._hasChanged = function(initial, data) { | |
if (data === null) { | |
return false | |
} | |
return true | |
} | |
/** | |
* Validates that its input is a valid uploaded image. | |
* @constructor | |
* @extends {Field} | |
* @param {Object=} kwargs | |
*/ | |
var ImageField = FileField.extend({ | |
defaultErrorMessages: { | |
invalidImage: 'Upload a valid image. The file you uploaded was either not an image or a corrupted image.' | |
} | |
, constructor: function ImageField(kwargs) { | |
if (!(this instanceof Field)) { return new ImageField(kwargs) } | |
FileField.call(this, kwargs) | |
} | |
}) | |
/** | |
* Checks that the file-upload field data contains a valid image. | |
*/ | |
ImageField.prototype.toJavaScript = function(data, initial) { | |
var f = FileField.prototype.toJavaScript.call(this, data, initial) | |
if (f === null) { | |
return null | |
} | |
// TODO Plug in image processing code when running on the server | |
return f | |
} | |
ImageField.prototype.widgetAttrs = function(widget) { | |
var attrs = FileField.prototype.widgetAttrs.call(this, widget) | |
attrs.accept = 'image/*' | |
return attrs | |
} | |
/** | |
* Validates that its input appears to be a valid URL. | |
* @constructor | |
* @extends {CharField} | |
* @param {Object=} kwargs | |
*/ | |
var URLField = CharField.extend({ | |
widget: widgets.URLInput | |
, defaultErrorMessages: { | |
invalid: 'Enter a valid URL.' | |
} | |
, defaultValidators: [validators.URLValidator()] | |
, constructor: function URLField(kwargs) { | |
if (!(this instanceof Field)) { return new URLField(kwargs) } | |
CharField.call(this, kwargs) | |
} | |
}) | |
URLField.prototype.toJavaScript = function(value) { | |
if (value) { | |
var urlFields = url.parseUri(value) | |
if (!urlFields.protocol) { | |
// If no URL protocol given, assume http:// | |
urlFields.protocol = 'http' | |
} | |
if (!urlFields.path) { | |
// The path portion may need to be added before query params | |
urlFields.path = '/' | |
} | |
value = url.makeUri(urlFields) | |
} | |
return CharField.prototype.toJavaScript.call(this, value) | |
} | |
URLField.prototype.clean = function(value) { | |
value = util.strip(this.toJavaScript(value)) | |
return CharField.prototype.clean.call(this, value) | |
} | |
/** | |
* Normalises its input to a Booleanprimitive. | |
* @constructor | |
* @extends {Field} | |
* @param {Object=} kwargs | |
*/ | |
var BooleanField = Field.extend({ | |
widget: widgets.CheckboxInput | |
, constructor: function BooleanField(kwargs) { | |
if (!(this instanceof Field)) { return new BooleanField(kwargs) } | |
Field.call(this, kwargs) | |
} | |
}) | |
BooleanField.prototype.toJavaScript = function(value) { | |
// Explicitly check for a 'false' string, which is what a hidden field will | |
// submit for false. Also check for '0', since this is what RadioSelect will | |
// provide. Because Boolean('anything') == true, we don't need to handle that | |
// explicitly. | |
if (is.String(value) && (value.toLowerCase() == 'false' || value == '0')) { | |
value = false | |
} | |
else { | |
value = Boolean(value) | |
} | |
value = Field.prototype.toJavaScript.call(this, value) | |
if (!value && this.required) { | |
throw ValidationError(this.errorMessages.required, {code: 'required'}) | |
} | |
return value | |
} | |
BooleanField.prototype._hasChanged = function(initial, data) { | |
// Sometimes data or initial could be null or '' which should be the same | |
// thing as false. | |
if (initial === 'false') { | |
// showHiddenInitial may have transformed false to 'false' | |
initial = false | |
} | |
return (Boolean(initial) != Boolean(data)) | |
} | |
/** | |
* A field whose valid values are null, true and false. | |
* Invalid values are cleaned to null. | |
* @constructor | |
* @extends {BooleanField} | |
* @param {Object=} kwargs | |
*/ | |
var NullBooleanField = BooleanField.extend({ | |
widget: widgets.NullBooleanSelect | |
, constructor: function NullBooleanField(kwargs) { | |
if (!(this instanceof Field)) { return new NullBooleanField(kwargs) } | |
BooleanField.call(this, kwargs) | |
} | |
}) | |
NullBooleanField.prototype.toJavaScript = function(value) { | |
// Explicitly checks for the string 'True' and 'False', which is what a | |
// hidden field will submit for true and false, and for '1' and '0', which | |
// is what a RadioField will submit. Unlike the Booleanfield we also need | |
// to check for true, because we are not using Boolean() function. | |
if (value === true || value == 'True' || value == 'true' || value == '1') { | |
return true | |
} | |
else if (value === false || value == 'False' || value == 'false' || value == '0') { | |
return false | |
} | |
return null | |
} | |
NullBooleanField.prototype.validate = function(value) {} | |
NullBooleanField.prototype._hasChanged = function(initial, data) { | |
// null (unknown) and false (No) are not the same | |
if (initial !== null) { | |
initial = Boolean(initial) | |
} | |
if (data !== null) { | |
data = Boolean(data) | |
} | |
return initial != data | |
} | |
/** | |
* Validates that its input is one of a valid list of choices. | |
* @constructor | |
* @extends {Field} | |
* @param {Object=} kwargs | |
*/ | |
var ChoiceField = Field.extend({ | |
widget: widgets.Select | |
, defaultErrorMessages: { | |
invalidChoice: 'Select a valid choice. {value} is not one of the available choices.' | |
} | |
, constructor: function ChoiceField(kwargs) { | |
if (!(this instanceof Field)) { return new ChoiceField(kwargs) } | |
kwargs = object.extend({choices: []}, kwargs) | |
Field.call(this, kwargs) | |
this.setChoices(kwargs.choices) | |
} | |
}) | |
ChoiceField.prototype.choices = function() { return this._choices } | |
ChoiceField.prototype.setChoices = function(choices) { | |
// Setting choices also sets the choices on the widget | |
this._choices = this.widget.choices = util.normaliseChoices(choices) | |
} | |
ChoiceField.prototype.toJavaScript = function(value) { | |
if (this.isEmptyValue(value)) { | |
return '' | |
} | |
return ''+value | |
} | |
/** | |
* Validates that the given value is in this field's choices. | |
*/ | |
ChoiceField.prototype.validate = function(value) { | |
Field.prototype.validate.call(this, value) | |
if (value && !this.validValue(value)) { | |
throw ValidationError(this.errorMessages.invalidChoice, { | |
code: 'invalidChoice' | |
, params: {value: value} | |
}) | |
} | |
} | |
/** | |
* Checks to see if the provided value is a valid choice. | |
* | |
* @param {String} value the value to be validated. | |
*/ | |
ChoiceField.prototype.validValue = function(value) { | |
var choices = this.choices() | |
for (var i = 0, l = choices.length; i < l; i++) { | |
if (is.Array(choices[i][1])) { | |
// This is an optgroup, so look inside the group for options | |
var optgroupChoices = choices[i][1] | |
for (var j = 0, m = optgroupChoices.length; j < m; j++) { | |
if (value === ''+optgroupChoices[j][0]) { | |
return true | |
} | |
} | |
} | |
else if (value === ''+choices[i][0]) { | |
return true | |
} | |
} | |
return false | |
} | |
/** | |
* A ChoiceField which returns a value coerced by some provided function. | |
* @constructor | |
* @extends {ChoiceField} | |
* @param {Object=} kwargs | |
*/ | |
var TypedChoiceField = ChoiceField.extend({ | |
constructor: function TypedChoiceField(kwargs) { | |
if (!(this instanceof Field)) { return new TypedChoiceField(kwargs) } | |
kwargs = object.extend({ | |
coerce: function(val) { return val }, emptyValue: '' | |
}, kwargs) | |
this.coerce = object.pop(kwargs, 'coerce') | |
this.emptyValue = object.pop(kwargs, 'emptyValue') | |
ChoiceField.call(this, kwargs) | |
} | |
}) | |
/** | |
* Validate that the value can be coerced to the right type (if not empty). | |
*/ | |
TypedChoiceField.prototype._coerce = function(value) { | |
if (value === this.emptyValue || this.isEmptyValue(value)) { | |
return this.emptyValue | |
} | |
try { | |
value = this.coerce(value) | |
} | |
catch (e) { | |
throw ValidationError(this.errorMessages.invalidChoice, { | |
code: 'invalidChoice' | |
, params: {value: value} | |
}) | |
} | |
return value | |
} | |
TypedChoiceField.prototype.clean = function(value) { | |
value = ChoiceField.prototype.clean.call(this, value) | |
return this._coerce(value) | |
} | |
/** | |
* Validates that its input is one or more of a valid list of choices. | |
* @constructor | |
* @extends {ChoiceField} | |
* @param {Object=} kwargs | |
*/ | |
var MultipleChoiceField = ChoiceField.extend({ | |
hiddenWidget: widgets.MultipleHiddenInput | |
, widget: widgets.SelectMultiple | |
, defaultErrorMessages: { | |
invalidChoice: 'Select a valid choice. {value} is not one of the available choices.' | |
, invalidList: 'Enter a list of values.' | |
} | |
, constructor: function MultipleChoiceField(kwargs) { | |
if (!(this instanceof Field)) { return new MultipleChoiceField(kwargs) } | |
ChoiceField.call(this, kwargs) | |
} | |
}) | |
MultipleChoiceField.prototype.toJavaScript = function(value) { | |
if (this.isEmptyValue(value)) { | |
return [] | |
} | |
else if (!is.Array(value)) { | |
throw ValidationError(this.errorMessages.invalidList, {code: 'invalidList'}) | |
} | |
var stringValues = [] | |
for (var i = 0, l = value.length; i < l; i++) { | |
stringValues.push(''+value[i]) | |
} | |
return stringValues | |
} | |
/** | |
* Validates that the input is a list and that each item is in this field's | |
* choices. | |
*/ | |
MultipleChoiceField.prototype.validate = function(value) { | |
if (this.required && !value.length) { | |
throw ValidationError(this.errorMessages.required, {code: 'required'}) | |
} | |
for (var i = 0, l = value.length; i < l; i++) { | |
if (!this.validValue(value[i])) { | |
throw ValidationError(this.errorMessages.invalidChoice, { | |
code: 'invalidChoice' | |
, params: {value: value[i]} | |
}) | |
} | |
} | |
} | |
MultipleChoiceField.prototype._hasChanged = function(initial, data) { | |
if (initial === null) { | |
initial = [] | |
} | |
if (data === null) { | |
data = [] | |
} | |
if (initial.length != data.length) { | |
return true | |
} | |
var dataLookup = object.lookup(data) | |
for (var i = 0, l = initial.length; i < l; i++) { | |
if (typeof dataLookup[''+initial[i]] == 'undefined') { | |
return true | |
} | |
} | |
return false | |
} | |
/** | |
* AMultipleChoiceField which returns values coerced by some provided function. | |
* @constructor | |
* @extends {MultipleChoiceField} | |
* @param {Object=} kwargs | |
*/ | |
var TypedMultipleChoiceField = MultipleChoiceField.extend({ | |
constructor: function TypedMultipleChoiceField(kwargs) { | |
if (!(this instanceof Field)) { return new TypedMultipleChoiceField(kwargs) } | |
kwargs = object.extend({ | |
coerce: function(val) { return val }, emptyValue: [] | |
}, kwargs) | |
this.coerce = object.pop(kwargs, 'coerce') | |
this.emptyValue = object.pop(kwargs, 'emptyValue') | |
MultipleChoiceField.call(this, kwargs) | |
} | |
}) | |
TypedMultipleChoiceField.prototype._coerce = function(value) { | |
if (value === this.emptyValue || this.isEmptyValue(value) || | |
(is.Array(value) && !value.length)) { | |
return this.emptyValue | |
} | |
var newValue = [] | |
for (var i = 0, l = value.length; i < l; i++) { | |
try { | |
newValue.push(this.coerce(value[i])) | |
} | |
catch (e) { | |
throw ValidationError(this.errorMessages.invalidChoice, { | |
code: 'invalidChoice' | |
, params: {value: value[i]} | |
}) | |
} | |
} | |
return newValue | |
} | |
TypedMultipleChoiceField.prototype.clean = function(value) { | |
value = MultipleChoiceField.prototype.clean.call(this, value) | |
return this._coerce(value) | |
} | |
TypedMultipleChoiceField.prototype.validate = function(value) { | |
if (value !== this.emptyValue || (is.Array(value) && value.length)) { | |
MultipleChoiceField.prototype.validate.call(this, value) | |
} | |
else if (this.required) { | |
throw ValidationError(this.errorMessages.required, {code: 'required'}) | |
} | |
} | |
/** | |
* Allows choosing from files inside a certain directory. | |
* @constructor | |
* @extends {ChoiceField} | |
* @param {string} path | |
* @param {Object=} kwargs | |
*/ | |
var FilePathField = ChoiceField.extend({ | |
constructor: function FilePathField(path, kwargs) { | |
if (!(this instanceof Field)) { return new FilePathField(path, kwargs) } | |
kwargs = object.extend({ | |
match: null, recursive: false, required: true, widget: null, | |
label: null, initial: null, helpText: null, | |
allowFiles: true, allowFolders: false | |
}, kwargs) | |
this.path = path | |
this.match = object.pop(kwargs, 'match') | |
this.recursive = object.pop(kwargs, 'recursive') | |
this.allowFiles = object.pop(kwargs, 'allowFiles') | |
this.allowFolders = object.pop(kwargs, 'allowFolders') | |
delete kwargs.match | |
delete kwargs.recursive | |
kwargs.choices = [] | |
ChoiceField.call(this, kwargs) | |
if (this.required) { | |
this.setChoices([]) | |
} | |
else { | |
this.setChoices([['', '---------']]) | |
} | |
if (this.match !== null) { | |
this.matchRE = new RegExp(this.match) | |
} | |
// TODO Plug in file paths when running on the server | |
this.widget.choices = this.choices() | |
} | |
}) | |
/** | |
* A Field whose clean() method calls multiple Field clean() methods. | |
* @constructor | |
* @extends {Field} | |
* @param {Object=} kwargs | |
*/ | |
var ComboField = Field.extend({ | |
constructor: function ComboField(kwargs) { | |
if (!(this instanceof Field)) { return new ComboField(kwargs) } | |
kwargs = object.extend({fields: []}, kwargs) | |
Field.call(this, kwargs) | |
// Set required to False on the individual fields, because the required | |
// validation will be handled by ComboField, not by those individual fields. | |
for (var i = 0, l = kwargs.fields.length; i < l; i++) { | |
kwargs.fields[i].required = false | |
} | |
this.fields = kwargs.fields | |
} | |
}) | |
ComboField.prototype.clean = function(value) { | |
Field.prototype.clean.call(this, value) | |
for (var i = 0, l = this.fields.length; i < l; i++) { | |
value = this.fields[i].clean(value) | |
} | |
return value | |
} | |
/** | |
* A Field that aggregates the logic of multiple Fields. | |
* | |
* Its clean() method takes a "decompressed" list of values, which are then | |
* cleaned into a single value according to this.fields. Each value in this | |
* list is cleaned by the corresponding field -- the first value is cleaned by | |
* the first field, the second value is cleaned by the second field, etc. Once | |
* all fields are cleaned, the list of clean values is "compressed" into a | |
* single value. | |
* | |
* Subclasses should not have to implement clean(). Instead, they must | |
* implement compress(), which takes a list of valid values and returns a | |
* "compressed" version of those values -- a single value. | |
* | |
* You'll probably want to use this with MultiWidget. | |
* | |
* @constructor | |
* @extends {Field} | |
* @param {Object=} kwargs | |
*/ | |
var MultiValueField = Field.extend({ | |
defaultErrorMessages: { | |
invalid: 'Enter a list of values.' | |
, incomplete: 'Enter a complete value.' | |
} | |
, constructor: function MultiValueField(kwargs) { | |
if (!(this instanceof Field)) { return new MultiValueField(kwargs) } | |
kwargs = object.extend({fields: []}, kwargs) | |
this.requireAllFields = object.pop(kwargs, 'requireAllFields', true) | |
Field.call(this, kwargs) | |
for (var i = 0, l = kwargs.fields.length; i < l; i++) { | |
var f = kwargs.fields[i] | |
object.setDefault(f.errorMessages, 'incomplete', | |
this.errorMessages.incomplete) | |
if (this.requireAllFields) { | |
// Set required to false on the individual fields, because the required | |
// validation will be handled by MultiValueField, not by those | |
// individual fields. | |
f.required = false | |
} | |
} | |
this.fields = kwargs.fields | |
} | |
}) | |
MultiValueField.prototype.validate = function() {} | |
/** | |
* Validates every value in the given list. A value is validated against the | |
* corresponding Field in this.fields. | |
* | |
* For example, if this MultiValueField was instantiated with | |
* {fields: [forms.DateField(), forms.TimeField()]}, clean() would call | |
* DateField.clean(value[0]) and TimeField.clean(value[1]). | |
* | |
* @param {Array} value the input to be validated. | |
* | |
* @return the result of calling compress() on the cleaned input. | |
*/ | |
MultiValueField.prototype.clean = function(value) { | |
var cleanData = [] | |
var errors = [] | |
if (!value || is.Array(value)) { | |
var allValuesEmpty = true | |
if (is.Array(value)) { | |
for (var i = 0, l = value.length; i < l; i++) { | |
if (value[i]) { | |
allValuesEmpty = false | |
break | |
} | |
} | |
} | |
if (!value || allValuesEmpty) { | |
if (this.required) { | |
throw ValidationError(this.errorMessages.required, {code: 'required'}) | |
} | |
else { | |
return this.compress([]) | |
} | |
} | |
} | |
else { | |
throw ValidationError(this.errorMessages.invalid, {code: 'invalid'}) | |
} | |
for (i = 0, l = this.fields.length; i < l; i++) { | |
var field = this.fields[i] | |
var fieldValue = value[i] | |
if (fieldValue === undefined) { | |
fieldValue = null | |
} | |
if (this.isEmptyValue(fieldValue)) { | |
if (this.requireAllFields) { | |
// Throw a 'required' error if the MultiValueField is required and any | |
// field is empty. | |
if (this.required) { | |
throw ValidationError(this.errorMessages.required, {code: 'required'}) | |
} | |
} | |
else if (field.required) { | |
// Otherwise, add an 'incomplete' error to the list of collected errors | |
// and skip field cleaning, if a required field is empty. | |
if (errors.indexOf(field.errorMessages.incomplete) == -1) { | |
errors.push(field.errorMessages.incomplete) | |
} | |
continue | |
} | |
} | |
try { | |
cleanData.push(field.clean(fieldValue)) | |
} | |
catch (e) { | |
if (!(e instanceof ValidationError)) { throw e } | |
// Collect all validation errors in a single list, which we'll throw at | |
// the end of clean(), rather than throwing a single exception for the | |
// first error we encounter. Skip duplicates. | |
errors = errors.concat(e.messages().filter(function(m) { | |
return errors.indexOf(m) == -1 | |
})) | |
} | |
} | |
if (errors.length !== 0) { | |
throw ValidationError(errors) | |
} | |
var out = this.compress(cleanData) | |
this.validate(out) | |
this.runValidators(out) | |
return out | |
} | |
/** | |
* Returns a single value for the given list of values. The values can be | |
* assumed to be valid. | |
* | |
* For example, if this MultiValueField was instantiated with | |
* {fields: [forms.DateField(), forms.TimeField()]}, this might return a Date | |
* object created by combining the date and time in dataList. | |
* | |
* @param {Array} dataList | |
*/ | |
MultiValueField.prototype.compress = function(dataList) { | |
throw new Error('Subclasses must implement this method.') | |
} | |
MultiValueField.prototype._hasChanged = function(initial, data) { | |
if (initial === null) { | |
initial = [] | |
for (var i = 0, l = data.length; i < l; i++) { | |
initial.push('') | |
} | |
} | |
else if (!(is.Array(initial))) { | |
initial = this.widget.decompress(initial) | |
} | |
for (i = 0, l = this.fields.length; i < l; i++) { | |
if (this.fields[i]._hasChanged(initial[i], data[i])) { | |
return true | |
} | |
} | |
return false | |
} | |
/** | |
* A MultiValueField consisting of a DateField and a TimeField. | |
* @constructor | |
* @extends {MultiValueField} | |
* @param {Object=} kwargs | |
*/ | |
var SplitDateTimeField = MultiValueField.extend({ | |
hiddenWidget: widgets.SplitHiddenDateTimeWidget | |
, widget: widgets.SplitDateTimeWidget | |
, defaultErrorMessages: { | |
invalidDate: 'Enter a valid date.' | |
, invalidTime: 'Enter a valid time.' | |
} | |
, constructor: function SplitDateTimeField(kwargs) { | |
if (!(this instanceof Field)) { return new SplitDateTimeField(kwargs) } | |
kwargs = object.extend({ | |
inputDateFormats: null, inputTimeFormats: null | |
}, kwargs) | |
var errors = object.extend({}, this.defaultErrorMessages) | |
if (typeof kwargs.errorMessages != 'undefined') { | |
object.extend(errors, kwargs.errorMessages) | |
} | |
kwargs.fields = [ | |
DateField({inputFormats: kwargs.inputDateFormats, | |
errorMessages: {invalid: errors.invalidDate}}) | |
, TimeField({inputFormats: kwargs.inputTimeFormats, | |
errorMessages: {invalid: errors.invalidTime}}) | |
] | |
MultiValueField.call(this, kwargs) | |
} | |
}) | |
/** | |
* Validates that, if given, its input does not contain empty values. | |
* | |
* @param {Array} [dataList] a two-item list consisting of two Date | |
* objects, the first of which represents a date, the | |
* second a time. | |
* | |
* @return a Date object representing the given date and time, or | |
* null for empty values. | |
*/ | |
SplitDateTimeField.prototype.compress = function(dataList) { | |
if (is.Array(dataList) && dataList.length > 0) { | |
var d = dataList[0] | |
var t = dataList[1] | |
// Raise a validation error if date or time is empty (possible if | |
// SplitDateTimeField has required == false). | |
if (this.isEmptyValue(d)) { | |
throw ValidationError(this.errorMessages.invalidDate, {code: 'invalidDate'}) | |
} | |
if (this.isEmptyValue(t)) { | |
throw ValidationError(this.errorMessages.invalidTime, {code: 'invalidTime'}) | |
} | |
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), | |
t.getHours(), t.getMinutes(), t.getSeconds()) | |
} | |
return null | |
} | |
/** | |
* Validates that its input is a valid IPv4 address. | |
* @constructor | |
* @extends {CharField} | |
* @param {Object=} kwargs | |
* @deprecated | |
*/ | |
var IPAddressField = CharField.extend({ | |
defaultValidators: [validators.validateIPv4Address] | |
, constructor: function IPAddressField(kwargs) { | |
if (!(this instanceof Field)) { return new IPAddressField(kwargs) } | |
CharField.call(this, kwargs) | |
} | |
}) | |
var GenericIPAddressField = CharField.extend({ | |
constructor: function GenericIPAddressField(kwargs) { | |
if (!(this instanceof Field)) { return new GenericIPAddressField(kwargs) } | |
kwargs = object.extend({protocol: 'both', unpackIPv4: false}, kwargs) | |
this.unpackIPv4 = kwargs.unpackIPv4 | |
this.defaultValidators = | |
validators.ipAddressValidators(kwargs.protocol, kwargs.unpackIPv4).validators | |
CharField.call(this, kwargs) | |
} | |
}) | |
GenericIPAddressField.prototype.toJavaScript = function(value) { | |
if (!value) { | |
return '' | |
} | |
if (value && value.indexOf(':') != -1) { | |
return cleanIPv6Address(value, {unpackIPv4: this.unpackIPv4}) | |
} | |
return value | |
} | |
/** | |
* Validates that its input is a valid slug. | |
* @constructor | |
* @extends {CharField} | |
* @param {Object=} kwargs | |
*/ | |
var SlugField = CharField.extend({ | |
defaultValidators: [validators.validateSlug] | |
, constructor: function SlugField(kwargs) { | |
if (!(this instanceof Field)) { return new SlugField(kwargs) } | |
CharField.call(this, kwargs) | |
} | |
}) | |
SlugField.prototype.clean = function(value) { | |
value = util.strip(this.toJavaScript(value)) | |
return CharField.prototype.clean.call(this, value) | |
} | |
module.exports = { | |
Field: Field | |
, CharField: CharField | |
, IntegerField: IntegerField | |
, FloatField: FloatField | |
, DecimalField: DecimalField | |
, BaseTemporalField: BaseTemporalField | |
, DateField: DateField | |
, TimeField: TimeField | |
, DateTimeField: DateTimeField | |
, RegexField: RegexField | |
, EmailField: EmailField | |
, FileField: FileField | |
, ImageField: ImageField | |
, URLField: URLField | |
, BooleanField: BooleanField | |
, NullBooleanField: NullBooleanField | |
, ChoiceField: ChoiceField | |
, TypedChoiceField: TypedChoiceField | |
, MultipleChoiceField: MultipleChoiceField | |
, TypedMultipleChoiceField: TypedMultipleChoiceField | |
, FilePathField: FilePathField | |
, ComboField: ComboField | |
, MultiValueField: MultiValueField | |
, SplitDateTimeField: SplitDateTimeField | |
, IPAddressField: IPAddressField | |
, GenericIPAddressField: GenericIPAddressField | |
, SlugField: SlugField | |
} | |
},{"./env":1,"./formats":3,"./util":7,"./widgets":8,"Concur":9,"isomorph/is":13,"isomorph/object":14,"isomorph/time":15,"isomorph/url":16,"validators":19}],3:[function(_dereq_,module,exports){ | |
'use strict'; | |
var DEFAULT_DATE_INPUT_FORMATS = [ | |
'%Y-%m-%d' // '2006-10-25' | |
, '%m/%d/%Y', '%m/%d/%y' // '10/25/2006', '10/25/06' | |
, '%b %d %Y', '%b %d, %Y' // 'Oct 25 2006', 'Oct 25, 2006' | |
, '%d %b %Y', '%d %b, %Y' // '25 Oct 2006', '25 Oct, 2006' | |
, '%B %d %Y', '%B %d, %Y' // 'October 25 2006', 'October 25, 2006' | |
, '%d %B %Y', '%d %B, %Y' // '25 October 2006', '25 October, 2006' | |
] | |
var DEFAULT_TIME_INPUT_FORMATS = [ | |
'%H:%M:%S' // '14:30:59' | |
, '%H:%M' // '14:30' | |
] | |
var DEFAULT_DATETIME_INPUT_FORMATS = [ | |
'%Y-%m-%d %H:%M:%S' // '2006-10-25 14:30:59' | |
, '%Y-%m-%d %H:%M' // '2006-10-25 14:30' | |
, '%Y-%m-%d' // '2006-10-25' | |
, '%m/%d/%Y %H:%M:%S' // '10/25/2006 14:30:59' | |
, '%m/%d/%Y %H:%M' // '10/25/2006 14:30' | |
, '%m/%d/%Y' // '10/25/2006' | |
, '%m/%d/%y %H:%M:%S' // '10/25/06 14:30:59' | |
, '%m/%d/%y %H:%M' // '10/25/06 14:30' | |
, '%m/%d/%y' // '10/25/06' | |
] | |
module.exports = { | |
DEFAULT_DATE_INPUT_FORMATS: DEFAULT_DATE_INPUT_FORMATS | |
, DEFAULT_TIME_INPUT_FORMATS: DEFAULT_TIME_INPUT_FORMATS | |
, DEFAULT_DATETIME_INPUT_FORMATS: DEFAULT_DATETIME_INPUT_FORMATS | |
} | |
},{}],4:[function(_dereq_,module,exports){ | |
'use strict'; | |
var Concur = _dereq_('Concur') | |
var is = _dereq_('isomorph/is') | |
var format = _dereq_('isomorph/format').formatObj | |
var object = _dereq_('isomorph/object') | |
var copy = _dereq_('isomorph/copy') | |
var validators = _dereq_('validators') | |
var env = _dereq_('./env') | |
var util = _dereq_('./util') | |
var fields = _dereq_('./fields') | |
var widgets = _dereq_('./widgets') | |
var ErrorList = util.ErrorList | |
var ErrorObject = util.ErrorObject | |
var ValidationError = validators.ValidationError | |
var Field = fields.Field | |
var FileField = fields.FileField | |
var Textarea = widgets.Textarea | |
var TextInput = widgets.TextInput | |
/** Property under which non-field-specific errors are stored. */ | |
var NON_FIELD_ERRORS = '__all__' | |
/** | |
* A field and its associated data. | |
* @param {Form} form a form. | |
* @param {Field} field one of the form's fields. | |
* @param {String} name the name under which the field is held in the form. | |
* @constructor | |
*/ | |
var BoundField = Concur.extend({ | |
constructor: function BoundField(form, field, name) { | |
if (!(this instanceof BoundField)) { return new BoundField(form, field, name) } | |
this.form = form | |
this.field = field | |
this.name = name | |
this.htmlName = form.addPrefix(name) | |
this.htmlInitialName = form.addInitialPrefix(name) | |
this.htmlInitialId = form.addInitialPrefix(this.autoId()) | |
this.label = this.field.label !== null ? this.field.label : util.prettyName(name) | |
this.helpText = field.helpText || '' | |
} | |
}) | |
BoundField.prototype.errors = function() { | |
return this.form.errors(this.name) || new this.form.errorConstructor() | |
} | |
BoundField.prototype.isHidden = function() { | |
return this.field.widget.isHidden | |
} | |
/** | |
* Calculates and returns the id attribute for this BoundField if the associated | |
* form has an autoId. Returns an empty string otherwise. | |
*/ | |
BoundField.prototype.autoId = function() { | |
var autoId = this.form.autoId | |
if (autoId) { | |
autoId = ''+autoId | |
if (autoId.indexOf('{name}') != -1) { | |
return format(autoId, {name: this.htmlName}) | |
} | |
return this.htmlName | |
} | |
return '' | |
} | |
/** | |
* Returns the data for this BoundFIeld, or null if it wasn't given. | |
*/ | |
BoundField.prototype.data = function() { | |
return this.field.widget.valueFromData(this.form.data, | |
this.form.files, | |
this.htmlName) | |
} | |
/** | |
* Wrapper around the field widget's idForLabel method. Useful, for example, for | |
* focusing on this field regardless of whether it has a single widget or a | |
* MutiWidget. | |
*/ | |
BoundField.prototype.idForLabel = function() { | |
var widget = this.field.widget | |
var id = object.get(widget.attrs, 'id', this.autoId()) | |
return widget.idForLabel(id) | |
} | |
BoundField.prototype.render = function(kwargs) { | |
if (this.field.showHiddenInitial) { | |
return m('div', [this.asWidget(kwargs), | |
this.asHidden({onlyInitial: true})]) | |
} | |
return this.asWidget(kwargs) | |
} | |
/** | |
* Returns a list of SubWidgets that comprise all widgets in this BoundField. | |
* This really is only useful for RadioSelect and CheckboxSelectMultiple | |
* widgets, so that you can iterate over individual inputs when rendering. | |
*/ | |
BoundField.prototype.subWidgets = function() { | |
var id = this.field.widget.attrs.id || this.autoId() | |
var kwargs = {attrs: {}} | |
if (id) { | |
kwargs.attrs.id = id | |
} | |
return this.field.widget.subWidgets(this.htmlName, this.value(), kwargs) | |
} | |
/** | |
* Renders a widget for the field. | |
* @param {Object} [kwargs] configuration options | |
* @config {Widget} [widget] an override for the widget used to render the field | |
* - if not provided, the field's configured widget will be used | |
* @config {Object} [attrs] additional attributes to be added to the field's widget. | |
*/ | |
BoundField.prototype.asWidget = function(kwargs) { | |
kwargs = object.extend({ | |
widget: null, attrs: null, onlyInitial: false | |
}, kwargs) | |
var widget = (kwargs.widget !== null ? kwargs.widget : this.field.widget) | |
var attrs = (kwargs.attrs !== null ? kwargs.attrs : {}) | |
var validation = this.validation(widget) | |
var autoId = this.autoId() | |
var name = !kwargs.onlyInitial ? this.htmlName : this.htmlInitialName | |
if (autoId && | |
typeof attrs.id == 'undefined' && | |
typeof widget.attrs.id == 'undefined') { | |
attrs.id = (!kwargs.onlyInitial ? autoId : this.htmlInitialId) | |
} | |
return widget.render(name, this.value(), {attrs: attrs, validation: validation}) | |
} | |
BoundField.prototype.validation = function(widget) { | |
if (arguments.length === 0) { | |
widget = this.field.widget | |
} | |
// If the field has any validation config set, it should take precedence, | |
// otherwise use the form's, as it has a default. | |
var validation = this.field.validation || this.form.validation | |
if (validation != 'manual') { | |
// Allow widgets to override the type of validation that's used for them - | |
// primarily for inputs which can only be changed by click or focus. | |
if (widget.validation !== null) { | |
validation = widget.validation | |
} | |
// We're going to add stuff to validation now, so make a shallow copy | |
validation = object.extend({}, validation) | |
// Validation is driven off form.data, so we always need to keep it up to | |
// date on change, regardless of whether or not validation will happen then. | |
validation.onChange = | |
util.bindRight(this.form._handleFieldChange, this.form, validation) | |
if (validation.event != 'onChange') { | |
validation.eventHandler = | |
util.bindRight(this.form._handleFieldValidation, this.form, validation) | |
} | |
} | |
return validation | |
} | |
/** | |
* Renders the field as a text input. | |
* @param {Object} [kwargs] widget options. | |
*/ | |
BoundField.prototype.asText = function(kwargs) { | |
kwargs = object.extend({}, kwargs, {widget: TextInput()}) | |
return this.asWidget(kwargs) | |
} | |
/** | |
* Renders the field as a textarea. | |
* @param {Object} [kwargs] widget options. | |
*/ | |
BoundField.prototype.asTextarea = function(kwargs) { | |
kwargs = object.extend({}, kwargs, {widget: Textarea()}) | |
return this.asWidget(kwargs) | |
} | |
/** | |
* Renders the field as a hidden field. | |
* @param {Object} [kwargs] widget options. | |
*/ | |
BoundField.prototype.asHidden = function(kwargs) { | |
kwargs = object.extend({}, kwargs, {widget: new this.field.hiddenWidget()}) | |
return this.asWidget(kwargs) | |
} | |
/** | |
* Returns the value for this BoundField, using the initial value if the form | |
* is not bound or the data otherwise. | |
*/ | |
BoundField.prototype.value = function() { | |
var data | |
if (!this.form.isBound) { | |
data = object.get(this.form.initial, this.name, this.field.initial) | |
if (is.Function(data)) { | |
data = data() | |
} | |
} | |
else { | |
data = this.field.boundData(this.data(), | |
object.get(this.form.initial, | |
this.name, | |
this.field.initial)) | |
} | |
return this.field.prepareValue(data) | |
} | |
BoundField.prototype._addLabelSuffix = function(label, labelSuffix) { | |
// Only add the suffix if the label does not end in punctuation | |
if (labelSuffix && ':?.!'.indexOf(label.charAt(label.length - 1)) == -1) { | |
return label + labelSuffix | |
} | |
return label | |
} | |
/** | |
* Wraps the given contents in a <label> if the field has an id attribute. If | |
* contents aren't given, uses the field's label. | |
* | |
* If attrs are given, they're used as HTML attributes on the <label> tag. | |
* | |
* @param {Object} [kwargs] configuration options. | |
* @config {String} [contents] contents for the label - if not provided, label | |
* contents will be generated from the field itself. | |
* @config {Object} [attrs] additional attributes to be added to the label. | |
* @config {String} [labelSuffix] allows overriding the form's labelSuffix. | |
*/ | |
BoundField.prototype.labelTag = function(kwargs) { | |
kwargs = object.extend({ | |
contents: this.label, attrs: null, labelSuffix: this.form.labelSuffix | |
}, kwargs) | |
var contents = this._addLabelSuffix(kwargs.contents, kwargs.labelSuffix) | |
var widget = this.field.widget | |
var id = object.get(widget.attrs, 'id', this.autoId()) | |
if (id) { | |
var attrs = object.extend(kwargs.attrs || {}, {htmlFor: widget.idForLabel(id)}) | |
contents = m('label', attrs, contents) | |
} | |
return contents | |
} | |
/** | |
* Puts together additional CSS classes for this field based on the field, the | |
* form and whether or not the field has errors. | |
* @param {string=} extra CSS classes for the field. | |
* @return {string} space-separated CSS classes for this field. | |
*/ | |
BoundField.prototype.cssClasses = function(extraCssClasses) { | |
var cssClasses = extraCssClasses ? [extraCssClasses] : [] | |
if (this.field.cssClass !== null) { | |
cssClasses.push(this.field.cssClass) | |
} | |
if (typeof this.form.rowCssClass != 'undefined') { | |
cssClasses.push(this.form.rowCssClass) | |
} | |
if (this.errors().isPopulated() && | |
typeof this.form.errorCssClass != 'undefined') { | |
cssClasses.push(this.form.errorCssClass) | |
} | |
if (this.field.required && | |
typeof this.form.requiredCssClass != 'undefined') { | |
cssClasses.push(this.form.requiredCssClass) | |
} | |
return cssClasses.join(' ') | |
} | |
/** | |
* A collection of Fields that knows how to validate and display itself. | |
* @constructor | |
* @param {Object} | |
*/ | |
var BaseForm = Concur.extend({ | |
constructor: function BaseForm(kwargs) { | |
kwargs = object.extend({ | |
data: null, files: null, autoId: 'id_{name}', prefix: null, | |
initial: null, errorConstructor: ErrorList, labelSuffix: ':', | |
emptyPermitted: false, validation: 'manual', onStateChange: null | |
}, kwargs) | |
this.isBound = kwargs.data !== null || kwargs.files !== null | |
this.data = kwargs.data || {} | |
this.files = kwargs.files || {} | |
this.autoId = kwargs.autoId | |
this.prefix = kwargs.prefix | |
this.initial = kwargs.initial || {} | |
this.errorConstructor = kwargs.errorConstructor | |
this.labelSuffix = kwargs.labelSuffix | |
this.emptyPermitted = kwargs.emptyPermitted | |
this.validation = kwargs.validation | |
// Normalise validation config to an object if it's not set to manual | |
if (is.String(this.validation) && this.validation != 'manual') { | |
this.validation = (this.validation == 'auto' | |
? {event: 'onChange', delay: 250} | |
: {event: this.validation}) | |
} | |
this.onStateChange = kwargs.onStateChange | |
this._errors = null // Stores errors after clean() has been called | |
this._changedData = null | |
this._pendingFieldValidation = {} // Pending field validation functions | |
// The baseFields attribute is the *prototype-wide* definition of fields. | |
// Because a particular *instance* might want to alter this.fields, we | |
// create this.fields here by deep copying baseFields. Instances should | |
// always modify this.fields; they should not modify baseFields. | |
this.fields = copy.deepCopy(this.baseFields) | |
// Now that form.fields exists, we can check if there's any active | |
// validation configured on the form or any of its fields. | |
if (this.hasActiveValidation()) { | |
// For active validation, we *must* have an onStateChange function to call | |
if (!is.Function(kwargs.onStateChange)) { | |
throw new Error( | |
'Forms must be given an onStateChange callback when their validation ' + | |
"- or any of their fields' validation - is not manual") | |
} | |
// isBound will flip to true as soon as the first field is validated. At | |
// that point, rendering will flip to using form.data as its source, so | |
// ensure data has a copy of any initial data that's been configured. | |
if (!this.isBound) { | |
object.extend(this.data, | |
this._prefixedFieldInitialData(), | |
this._prefixData(this.initial)) | |
} | |
} | |
} | |
}) | |
/** | |
* This will always be hooked up to a wiget's onChange to ensure form.data is | |
* kept up-to-date. Since we're here anyway, we can deal with onChange | |
* validation too. | |
*/ | |
BaseForm.prototype._handleFieldChange = function(e, validation) { | |
// Get the data from the form element(s) in the DOM | |
var htmlName = e.target.name | |
var data = util.fieldData(e.target.form, htmlName) | |
// Keep data up-to-date | |
if (!this.isBound) { | |
this.isBound = true | |
} | |
this.data[htmlName] = data | |
this.onStateChange() | |
// If we should be validating now, do so | |
if (validation.event == 'onChange') { | |
this._handleFieldValidation(e, validation) | |
} | |
} | |
/** | |
* Handles validating the field which is the target of the given event based | |
* on its validation config. This will be hooked up to the appropriate event | |
* as per the field's validation config. React special cases onChange to ensure | |
* the controlled value is kept up to date, so we should be sure that the date | |
* we'll be validating against is current. | |
*/ | |
BaseForm.prototype._handleFieldValidation = function(e, validation) { | |
// Special case for fields whose widget names aren't the same as their form | |
// field name. | |
var field = this.removePrefix(e.target.getAttribute('data-newforms-field') || | |
e.target.name) | |
if (validation.delay) { | |
this._delayedFieldValidation(field, validation.delay) | |
} | |
else { | |
this._immediateFieldValidation(field) | |
} | |
} | |
/** | |
* Validates a single field and notifies the React component that state has | |
* changed. | |
*/ | |
BaseForm.prototype._immediateFieldValidation = function(field) { | |
this.partialClean([field]) | |
this.onStateChange() | |
} | |
/** | |
* Sets up delayed validation of a single field with a debounced function and | |
* calls it, or just calls the function again if it already exists to reset the | |
* delay. | |
*/ | |
BaseForm.prototype._delayedFieldValidation = function(field, delay) { | |
if (!is.Function(this._pendingFieldValidation[field])) { | |
this._pendingFieldValidation[field] = util.debounce(function() { | |
delete this._pendingFieldValidation[field] | |
this._immediateFieldValidation(field) | |
}.bind(this), delay) | |
} | |
this._pendingFieldValidation[field]() | |
} | |
/** | |
* Resets validation state, replaces the form's input data (and flips its bound | |
* flag if necessary) and revalidates, returning the result of isValid(). | |
* @param {Object.<string,*>} data new input data for the form. | |
* @retun {boolean} true if the new data is valid. | |
*/ | |
BaseForm.prototype.setData = function(data) { | |
this._errors = null | |
this._changedData = null | |
this.data = data | |
if (!this.isBound) { | |
this.isBound = true | |
} | |
// This call ultimately triggers a fullClean() because _errors isn't set | |
return this.isValid() | |
} | |
/** | |
* Updates some of the form's input data, then triggers validation of fields | |
* which had their input data updated as well as form-wide cleaning. | |
* @param {Object.<string,*>} data updated input data for the form. | |
*/ | |
BaseForm.prototype.updateData = function(data) { | |
this._changedData = null | |
object.extend(this.data, data) | |
if (!this.isBound) { | |
this.isBound = true | |
} | |
var fields = Object.keys(data) | |
if (this.prefix !== null) { | |
for (var i = 0, l = fields.length; i < l; i++) { | |
fields[i] = this.removePrefix(fields[i]) | |
} | |
} | |
this.partialClean(fields) | |
} | |
/** | |
* Getter for errors, which first cleans the form if there are no errors | |
* defined yet. | |
* @param {string=} name if given, errors for this field name will be returned | |
* instead of the full error object. | |
* @return {(ErrorObject|ErrorList)} form or field errors | |
*/ | |
BaseForm.prototype.errors = function(name) { | |
if (this._errors === null) { | |
this.fullClean() | |
} | |
if (name) { | |
return this._errors.get(name) | |
} | |
return this._errors | |
} | |
BaseForm.prototype.changedData = function() { | |
if (!env.browser && this._changedData != null) { return this._changedData } | |
var changedData = [] | |
var initialValue | |
// XXX: For now we're asking the individual fields whether or not | |
// the data has changed. It would probably be more efficient to hash | |
// the initial data, store it in a hidden field, and compare a hash | |
// of the submitted data, but we'd need a way to easily get the | |
// string value for a given field. Right now, that logic is embedded | |
// in the render method of each field's widget. | |
for (var name in this.fields) { | |
if (!object.hasOwn(this.fields, name)) { continue } | |
var field = this.fields[name] | |
var prefixedName = this.addPrefix(name) | |
var dataValue = field.widget.valueFromData(this.data, this.files, prefixedName) | |
if (!field.showHiddenInitial) { | |
initialValue = object.get(this.initial, name, field.initial) | |
if (is.Function(initialValue)) { | |
initialValue = initialValue() | |
} | |
} | |
else { | |
var initialPrefixedName = this.addInitialPrefix(name) | |
var hiddenWidget = new field.hiddenWidget() | |
try { | |
initialValue = hiddenWidget.valueFromData( | |
this.data, this.files, initialPrefixedName) | |
} | |
catch (e) { | |
if (!(e instanceof ValidationError)) { throw e } | |
// Always assume data has changed if validation fails | |
changedData.push(name) | |
continue | |
} | |
} | |
if (field._hasChanged(initialValue, dataValue)) { | |
changedData.push(name) | |
} | |
} | |
if (!env.browser) { this._changedData = changedData } | |
return changedData | |
} | |
BaseForm.prototype.render = function() { | |
return this.asTable() | |
} | |
/** | |
* Creates a BoundField for each field in the form, in the order in which the | |
* fields were created. | |
* @param {Function} [test] if provided, this function will be called with | |
* field and name arguments - BoundFields will only be generated for fields | |
* for which true is returned. | |
* @return a list of BoundField objects - one for each field in the form, in the | |
* order in which the fields were created. | |
*/ | |
BaseForm.prototype.boundFields = function(test) { | |
test = test || function() { return true } | |
var fields = [] | |
for (var name in this.fields) { | |
if (object.hasOwn(this.fields, name) && | |
test(this.fields[name], name) === true) { | |
fields.push(BoundField(this, this.fields[name], name)) | |
} | |
} | |
return fields | |
} | |
/** | |
* {name -> BoundField} version of boundFields | |
*/ | |
BaseForm.prototype.boundFieldsObj = function(test) { | |
test = test || function() { return true } | |
var fields = {} | |
for (var name in this.fields) { | |
if (object.hasOwn(this.fields, name) && | |
test(this.fields[name], name) === true) { | |
fields[name] = BoundField(this, this.fields[name], name) | |
} | |
} | |
return fields | |
} | |
/** | |
* Creates a BoundField for the field with the given name. | |
* | |
* @param {String} name a field name. | |
* @return a BoundField for the field with the given name, if one exists. | |
*/ | |
BaseForm.prototype.boundField = function(name) { | |
if (!object.hasOwn(this.fields, name)) { | |
throw new Error("Form does not have a '" + name + "' field.") | |
} | |
return BoundField(this, this.fields[name], name) | |
} | |
/** | |
* Determines whether or not the form has errors. | |
* @return {Boolean} | |
*/ | |
BaseForm.prototype.isValid = function() { | |
if (!this.isBound) { | |
return false | |
} | |
return !this.errors().isPopulated() | |
} | |
/** | |
* Returns the field name with a prefix appended, if this Form has a prefix set. | |
* @param {String} fieldName a field name. | |
* @return a field name with a prefix appended, if this Form has a prefix set, | |
* otherwise <code>fieldName</code> is returned as-is. | |
*/ | |
BaseForm.prototype.addPrefix = function(fieldName) { | |
if (this.prefix !== null) { | |
return this.prefix + '-' + fieldName | |
} | |
return fieldName | |
} | |
/** | |
* Adds an initial prefix for checking dynamic initial values. | |
*/ | |
BaseForm.prototype.addInitialPrefix = function(fieldName) { | |
return 'initial-' + this.addPrefix(fieldName) | |
} | |
/** | |
* Returns the field with a prefix-size chunk chopped off the start if this | |
* Form has a prefix set. | |
*/ | |
BaseForm.prototype.removePrefix = function(fieldName) { | |
if (this.prefix !== null) { | |
return fieldName.substring(this.prefix.length + 1) | |
} | |
return fieldName | |
} | |
/** | |
* Helper function for outputting HTML. | |
* @param {Function} normalRow a function which produces a normal row. | |
* @param {Function} errorRow a function which produces an error row. | |
* @return a list of React.DOM components representing rows. | |
*/ | |
BaseForm.prototype._htmlOutput = function(normalRow, errorRow) { | |
var bf | |
var bfErrors | |
var topErrors = this.nonFieldErrors() // Errors that should be displayed above all fields | |
var hiddenFields = [] | |
var hiddenBoundFields = this.hiddenFields() | |
for (var i = 0, l = hiddenBoundFields.length; i < l; i++) { | |
bf = hiddenBoundFields[i] | |
bfErrors = bf.errors() | |
if (bfErrors.isPopulated) { | |
topErrors.extend(bfErrors.messages().map(function(error) { | |
return '(Hidden field ' + bf.name + ') ' + error | |
})) | |
} | |
hiddenFields.push(bf.render()) | |
} | |
var rows = [] | |
var errors | |
var label | |
var helpText | |
var extraContent | |
var visibleBoundFields = this.visibleFields() | |
for (i = 0, l = visibleBoundFields.length; i < l; i++) { | |
bf = visibleBoundFields[i] | |
bfErrors = bf.errors() | |
// Variables which can be optional in each row | |
errors = (bfErrors.isPopulated() ? bfErrors.render() : null) | |
label = (bf.label ? bf.labelTag() : null) | |
helpText = bf.helpText | |
if (helpText) { | |
helpText = m('span', {className: 'helpText'}, [helpText]) | |
} | |
// If this is the last row, it should include any hidden fields | |
extraContent = (i == l - 1 && hiddenFields.length > 0 ? hiddenFields : null) | |
rows.push(normalRow(bf.htmlName, | |
bf.cssClasses(), | |
label, | |
bf.render(), | |
helpText, | |
errors, | |
extraContent)) | |
} | |
if (topErrors.isPopulated()) { | |
// Add hidden fields to the top error row if it's being displayed and | |
// there are no other rows. | |
extraContent = (hiddenFields.length > 0 && rows.length === 0 ? hiddenFields : null) | |
rows.unshift(errorRow(this.addPrefix(NON_FIELD_ERRORS), | |
topErrors.render(), | |
extraContent)) | |
} | |
// Put hidden fields in their own error row if there were no rows to | |
// display. | |
if (hiddenFields.length > 0 && rows.length === 0) { | |
rows.push(errorRow(this.addPrefix('__hiddenFields__'), | |
null, | |
hiddenFields, | |
this.hiddenFieldRowCssClass)) | |
} | |
return rows | |
} | |
/** | |
* Returns this form rendered as HTML <tr>s - excluding the <table>. | |
*/ | |
BaseForm.prototype.asTable = (function() { | |
function normalRow(key, cssClasses, label, field, helpText, errors, extraContent) { | |
var contents = [] | |
if (errors) { contents.push(errors) } | |
contents.push(field) | |
if (helpText) { | |
contents.push(m('br')) | |
contents.push(helpText) | |
} | |
if (extraContent) { contents.push.apply(contents, extraContent) } | |
if (cssClasses) { rowAttrs.className = cssClasses } | |
return m('tr', rowAttrs, [ | |
m('th', label) | |
, m('td', contents) | |
]) | |
} | |
function errorRow(key, errors, extraContent, cssClasses) { | |
var contents = [] | |
if (errors) { contents.push(errors) } | |
if (extraContent) { contents.push.apply(contents, extraContent) } | |
if (cssClasses) { rowAttrs.className = cssClasses } | |
return m('tr', rowAttrs, [m('td', {colSpan: 2}, contents)]) | |
} | |
return function() { return this._htmlOutput(normalRow, errorRow) } | |
})() | |
function _normalRow(tagName, key, cssClasses, label, field, helpText, errors, extraContent) { | |
var rowAttrs = {} | |
if (cssClasses) { rowAttrs.className = cssClasses } | |
var contents = [] | |
if (errors) { contents.push(errors) } | |
if (label) { contents.push(label) } | |
contents.push(' ') | |
contents.push(field) | |
if (helpText) { | |
contents.push(' ') | |
contents.push(helpText) | |
} | |
if (extraContent) { contents.push.apply(contents, extraContent) } | |
return m(tagName, rowAttrs, contents) | |
} | |
function _errorRow(tagName, key, errors, extraContent, cssClasses) { | |
var rowAttrs = {} | |
if (cssClasses) { rowAttrs.className = cssClasses } | |
var contents = [] | |
if (errors) { contents.push(errors) } | |
if (extraContent) { contents.push.apply(contents, extraContent) } | |
return m(tagName, rowAttrs, contents) | |
} | |
function _singleElementRow(tagName) { | |
var normalRow = _normalRow.bind(null, tagName) | |
var errorRow = _errorRow.bind(null, tagName) | |
return function() { | |
return this._htmlOutput(normalRow, errorRow) | |
} | |
} | |
/** | |
* Returns this form rendered as HTML <li>s - excluding the <ul>. | |
*/ | |
BaseForm.prototype.asUl = _singleElementRow('li') | |
/** | |
* Returns this form rendered as HTML <div>s. | |
*/ | |
BaseForm.prototype.asDiv = _singleElementRow('div') | |
/** | |
* Returns errors that aren't associated with a particular field. | |
* @return errors that aren't associated with a particular field - i.e., errors | |
* generated by clean(). Will be empty if there are none. | |
*/ | |
BaseForm.prototype.nonFieldErrors = function() { | |
return (this.errors(NON_FIELD_ERRORS) || new this.errorConstructor()) | |
} | |
/** | |
* Returns the raw value for a particular field name. This is just a convenient | |
* wrapper around widget.valueFromData. | |
*/ | |
BaseForm.prototype._rawValue = function(fieldname) { | |
var field = this.fields[fieldname] | |
var prefix = this.addPrefix(fieldname) | |
return field.widget.valueFromData(this.data, this.files, prefix) | |
} | |
/** | |
* Updates the content of this._errors. | |
* | |
* The field argument is the name of the field to which the errors should be | |
* added. If its value is null the errors will be treated as NON_FIELD_ERRORS. | |
* | |
* The error argument can be a single error, a list of errors, or an object that | |
* maps field names to lists of errors. What we define as an "error" can be | |
* either a simple string or an instance of ValidationError with its message | |
* attribute set and what we define as list or object can be an actual list or | |
* object or an instance of ValidationError with its errorList or errorObj | |
* property set. | |
* | |
* If error is an object, the field argument *must* be null and errors will be | |
* added to the fields that correspond to the properties of the object. | |
*/ | |
BaseForm.prototype.addError = function(field, error) { | |
if (!(error instanceof ValidationError)) { | |
// Normalise to ValidationError and let its constructor do the hard work of | |
// making sense of the input. | |
error = ValidationError(error) | |
} | |
if (object.hasOwn(error, 'errorObj')) { | |
if (field !== null) { | |
throw new Error("The argument 'field' must be null when the 'error' " + | |
'argument contains errors for multiple fields.') | |
} | |
error = error.errorObj | |
} | |
else { | |
var errorList = error.errorList | |
error = {} | |
error[field || NON_FIELD_ERRORS] = errorList | |
} | |
var fields = Object.keys(error) | |
for (var i = 0, l = fields.length; i < l; i++) { | |
field = fields[i] | |
errorList = error[field] | |
if (!this._errors.hasField(field)) { | |
if (field !== NON_FIELD_ERRORS && !object.hasOwn(this.fields, field)) { | |
throw new Error(this._formName() + " has no field named '" + field + "'") | |
} | |
this._errors.set(field, new this.errorConstructor()) | |
} | |
this._errors.get(field).extend(errorList) | |
if (object.hasOwn(this.cleanedData, field)) { | |
delete this.cleanedData[field] | |
} | |
} | |
} | |
BaseForm.prototype._formName = function() { | |
return (this.constructor.name ? "'" + this.constructor.name + "'" : 'Form') | |
} | |
/** | |
* Cleans data for all fields and triggers cross-form cleaning. | |
*/ | |
BaseForm.prototype.fullClean = function() { | |
this._errors = ErrorObject() | |
if (!this.isBound) { | |
return // Stop further processing | |
} | |
this.cleanedData = {} | |
// If the form is permitted to be empty, and none of the form data has | |
// changed from the initial data, short circuit any validation. | |
if (this.emptyPermitted && !this.hasChanged()) { | |
return | |
} | |
this._cleanFields() | |
this._cleanForm() | |
this._postClean() | |
} | |
/** | |
* Cleans data for the given field names and triggers cross-form cleaning in | |
* case any cleanedData it uses has changed. | |
* @param {Array.<string>} fields field names. | |
*/ | |
BaseForm.prototype.partialClean = function(fields) { | |
if (this._errors === null) { | |
this._errors = ErrorObject() | |
} | |
else { | |
this._errors.remove(NON_FIELD_ERRORS) | |
this._errors.removeAll(fields) | |
} | |
if (typeof this.cleanedData == 'undefined') { | |
this.cleanedData = {} | |
} | |
for (var i = 0, l = fields.length; i < l; i++) { | |
this._cleanField(fields[i]) | |
} | |
this._cleanForm() | |
} | |
BaseForm.prototype._cleanFields = function() { | |
for (var name in this.fields) { | |
if (object.hasOwn(this.fields, name)) { | |
this._cleanField(name) | |
} | |
} | |
} | |
BaseForm.prototype._cleanField = function(name) { | |
if (!object.hasOwn(this.fields, name)) { | |
throw new Error(this._formName() + " has no field named '" + name + "'") | |
} | |
var field = this.fields[name] | |
// valueFromData() gets the data from the data objects. | |
// Each widget type knows how to retrieve its own data, because some widgets | |
// split data over several HTML fields. | |
var value = field.widget.valueFromData(this.data, this.files, | |
this.addPrefix(name)) | |
try { | |
if (field instanceof FileField) { | |
var initial = object.get(this.initial, name, field.initial) | |
value = field.clean(value, initial) | |
} | |
else { | |
value = field.clean(value) | |
} | |
this.cleanedData[name] = value | |
// Try cleanName | |
var customClean = 'clean' + name.charAt(0).toUpperCase() + name.substr(1) | |
if (typeof this[customClean] != 'undefined' && | |
is.Function(this[customClean])) { | |
value = this[customClean]() | |
if (typeof value != 'undefined') { | |
this.cleanedData[name] = value | |
} | |
} | |
else { | |
// Otherwise, try clean_name | |
customClean = 'clean_' + name | |
if (typeof this[customClean] != 'undefined' && | |
is.Function(this[customClean])) { | |
value = this[customClean]() | |
if (typeof value != 'undefined') { | |
this.cleanedData[name] = value | |
} | |
} | |
} | |
} | |
catch (e) { | |
if (!(e instanceof ValidationError)) { | |
throw e | |
} | |
this.addError(name, e) | |
} | |
} | |
BaseForm.prototype._cleanForm = function() { | |
var cleanedData | |
try { | |
cleanedData = this.clean() | |
} | |
catch (e) { | |
if (!(e instanceof ValidationError)) { | |
throw e | |
} | |
this.addError(null, e) | |
} | |
if (cleanedData) { | |
this.cleanedData = cleanedData | |
} | |
} | |
/** | |
* An internal hook for performing additional cleaning after form cleaning is | |
* complete. | |
*/ | |
BaseForm.prototype._postClean = function() {} | |
/** | |
* Hook for doing any extra form-wide cleaning after each Field's clean() has | |
* been called. Any ValidationError raised by this method will not be associated | |
* with a particular field; it will have a special-case association with the | |
* field named '__all__'. | |
* @return validated, cleaned data (optionally) | |
*/ | |
BaseForm.prototype.clean = function() { | |
return this.cleanedData | |
} | |
/** | |
* Determines if data differs from initial. | |
*/ | |
BaseForm.prototype.hasChanged = function() { | |
return (this.changedData().length > 0) | |
} | |
/** | |
* @return {boolean} true if the form needs to be multipart-encoded, in other | |
* words, if it has a FileInput. | |
*/ | |
BaseForm.prototype.isMultipart = function() { | |
for (var name in this.fields) { | |
if (object.hasOwn(this.fields, name) && | |
this.fields[name].widget.needsMultipartForm) { | |
return true | |
} | |
} | |
return false | |
} | |
/** | |
* @return {boolean} true if the form or any of its fields have active | |
* validation configured. | |
*/ | |
BaseForm.prototype.hasActiveValidation = function() { | |
if (this.validation !== 'manual') { return true} | |
for (var name in this.fields) { | |
if (!object.hasOwn(this.fields, name)) { continue } | |
var fieldvalidation = this.fields[name].validation | |
if (fieldvalidation !== null && fieldvalidation !== 'manual') { | |
return true | |
} | |
} | |
return false | |
} | |
BaseForm.prototype._prefixedFieldInitialData = function() { | |
var fieldInitial = {} | |
var fieldNames = Object.keys(this.fields) | |
for (var i = 0, l = fieldNames.length, fieldName, initial; i < l; i++) { | |
fieldName = fieldNames[i] | |
initial = this.fields[fieldName].initial | |
if (initial !== null) { | |
fieldInitial[this.addPrefix(fieldName)] = initial | |
} | |
} | |
return fieldInitial | |
} | |
BaseForm.prototype._prefixData = function(data) { | |
var prefixedData = {} | |
var fieldNames = Object.keys(data) | |
for (var i = 0, l = fieldNames.length, fieldName; i < l; i++) { | |
fieldName = fieldNames[i] | |
prefixedData[fieldName] = this.addPrefix(data[fieldName]) | |
} | |
return prefixedData | |
} | |
/** | |
* Returns a list of all the BoundField objects that correspond to hidden | |
* fields. Useful for manual form layout. | |
*/ | |
BaseForm.prototype.hiddenFields = function() { | |
return this.boundFields(function(field) { | |
return field.widget.isHidden | |
}) | |
} | |
/** | |
* Returns a list of BoundField objects that do not correspond to hidden fields. | |
* The opposite of the hiddenFields() method. | |
*/ | |
BaseForm.prototype.visibleFields = function() { | |
return this.boundFields(function(field) { | |
return !field.widget.isHidden | |
}) | |
} | |
function DeclarativeFieldsMeta(prototypeProps, constructorProps) { | |
// Pop Fields instances from prototypeProps to build up the new form's own | |
// declaredFields. | |
var fields = [] | |
Object.keys(prototypeProps).forEach(function(name) { | |
if (prototypeProps[name] instanceof Field) { | |
fields.push([name, prototypeProps[name]]) | |
delete prototypeProps[name] | |
} | |
}) | |
fields.sort(function(a, b) { | |
return a[1].creationCounter - b[1].creationCounter | |
}) | |
prototypeProps.declaredFields = object.fromItems(fields) | |
// Build up final declaredFields from the form being extended, forms being | |
// mixed in and the new form's own declaredFields, in that order of | |
// precedence. | |
var declaredFields = {} | |
// If we're extending another form, we don't need to check for shadowed | |
// fields, as it's at the bottom of the pile for inheriting declaredFields. | |
if (object.hasOwn(this, 'declaredFields')) { | |
object.extend(declaredFields, this.declaredFields) | |
} | |
// If any mixins which look like Form constructors were given, inherit their | |
// declaredFields and check for shadowed fields. | |
if (object.hasOwn(prototypeProps, '__mixin__')) { | |
var mixins = prototypeProps.__mixin__ | |
if (!is.Array(mixins)) { mixins = [mixins] } | |
// Process mixins from left-to-right, the same precedence they'll get for | |
// having their prototype properties mixed in. | |
for (var i = 0, l = mixins.length; i < l; i++) { | |
var mixin = mixins[i] | |
if (is.Function(mixin) && object.hasOwn(mixin.prototype, 'declaredFields')) { | |
// Extend mixed-in declaredFields over the top of what's already there, | |
// then delete any fields which have been shadowed by a non-Field | |
// property in its prototype. | |
object.extend(declaredFields, mixin.prototype.declaredFields) | |
Object.keys(mixin.prototype).forEach(function(name) { | |
if (object.hasOwn(declaredFields, name)) { | |
delete declaredFields[name] | |
} | |
}) | |
// To avoid overwriting the new form's baseFields, declaredFields or | |
// constructor when the rest of the mixin's prototype is mixed-in by | |
// Concur, replace the mixin with an object containing only its other | |
// prototype properties. | |
var mixinPrototype = object.extend({}, mixin.prototype) | |
delete mixinPrototype.baseFields | |
delete mixinPrototype.declaredFields | |
delete mixinPrototype.constructor | |
mixins[i] = mixinPrototype | |
} | |
} | |
// We may have wrapped a single mixin in an Array - assign it back to the | |
// new form's prototype for processing by Concur. | |
prototypeProps.__mixin__ = mixins | |
} | |
// Finally - extend the new form's own declaredFields over the top of | |
// decalredFields being inherited, then delete any fields which have been | |
// shadowed by a non-Field property in its prototype. | |
object.extend(declaredFields, prototypeProps.declaredFields) | |
Object.keys(prototypeProps).forEach(function(name) { | |
if (object.hasOwn(declaredFields, name)) { | |
delete declaredFields[name] | |
} | |
}) | |
prototypeProps.baseFields = declaredFields | |
prototypeProps.declaredFields = declaredFields | |
} | |
var Form = BaseForm.extend({ | |
__meta__: DeclarativeFieldsMeta | |
, constructor: function Form() { | |
BaseForm.apply(this, arguments) | |
} | |
}) | |
module.exports = { | |
NON_FIELD_ERRORS: NON_FIELD_ERRORS | |
, BoundField: BoundField | |
, BaseForm: BaseForm | |
, DeclarativeFieldsMeta: DeclarativeFieldsMeta | |
, Form: Form | |
} | |
},{"./env":1,"./fields":2,"./util":7,"./widgets":8,"Concur":9,"isomorph/copy":11,"isomorph/format":12,"isomorph/is":13,"isomorph/object":14,"validators":19}],5:[function(_dereq_,module,exports){ | |
'use strict'; | |
var Concur = _dereq_('Concur') | |
var object = _dereq_('isomorph/object') | |
var validators = _dereq_('validators') | |
var env = _dereq_('./env') | |
var util = _dereq_('./util') | |
var widgets = _dereq_('./widgets') | |
var fields = _dereq_('./fields') | |
var forms = _dereq_('./forms') | |
var ErrorList = util.ErrorList | |
var ValidationError = validators.ValidationError | |
var IntegerField = fields.IntegerField | |
var BooleanField = fields.BooleanField | |
var HiddenInput = widgets.HiddenInput | |
// Special field names | |
var TOTAL_FORM_COUNT = 'TOTAL_FORMS' | |
var INITIAL_FORM_COUNT = 'INITIAL_FORMS' | |
var MIN_NUM_FORM_COUNT = 'MIN_NUM_FORMS' | |
var MAX_NUM_FORM_COUNT = 'MAX_NUM_FORMS' | |
var ORDERING_FIELD_NAME = 'ORDER' | |
var DELETION_FIELD_NAME = 'DELETE' | |
// Default minimum number of forms in a formset | |
var DEFAULT_MIN_NUM = 0 | |
// Default maximum number of forms in a formset, to prevent memory exhaustion | |
var DEFAULT_MAX_NUM = 1000 | |
/** | |
* ManagementForm is used to keep track of how many form instances are displayed | |
* on the page. If adding new forms via JavaScript, you should increment the | |
* count field of this form as well. | |
* @constructor | |
*/ | |
var ManagementForm = (function() { | |
var fields = {} | |
fields[TOTAL_FORM_COUNT] = IntegerField({widget: HiddenInput}) | |
fields[INITIAL_FORM_COUNT] = IntegerField({widget: HiddenInput}) | |
// MIN_NUM_FORM_COUNT and MAX_NUM_FORM_COUNT are output with the rest of | |
// the management form, but only for the convenience of client-side | |
// code. The POST value of them returned from the client is not checked. | |
fields[MIN_NUM_FORM_COUNT] = IntegerField({required: false, widget: HiddenInput}) | |
fields[MAX_NUM_FORM_COUNT] = IntegerField({required: false, widget: HiddenInput}) | |
return forms.Form.extend(fields) | |
})() | |
/** | |
* A collection of instances of the same Form. | |
* @constructor | |
* @param {Object=} kwargs | |
*/ | |
var BaseFormSet = Concur.extend({ | |
constructor: function BaseFormSet(kwargs) { | |
kwargs = object.extend({ | |
data: null, files: null, autoId: 'id_{name}', prefix: null, | |
initial: null, errorConstructor: ErrorList, managementFormCssClass: null | |
}, kwargs) | |
this.isBound = kwargs.data !== null || kwargs.files !== null | |
this.prefix = kwargs.prefix || this.getDefaultPrefix() | |
this.autoId = kwargs.autoId | |
this.data = kwargs.data || {} | |
this.files = kwargs.files || {} | |
this.initial = kwargs.initial | |
this.errorConstructor = kwargs.errorConstructor | |
this.managementFormCssClass = kwargs.managementFormCssClass | |
this._forms = null | |
this._errors = null | |
this._nonFormErrors = null | |
} | |
}) | |
/** | |
* Resets validation state, updates the formset's input data (and bound status | |
* if necessary) and revalidates, returning the result of isValid(). | |
* @param {Object.<string, *} data new input data for the formset. | |
* @retun {boolean} true if the new data is valid. | |
*/ | |
BaseFormSet.prototype.setData = function(data) { | |
this._errors = null | |
this.data = data | |
if (!this.isBound) { | |
this.isBound = true | |
} | |
return this.isValid() | |
} | |
/** | |
* Returns the ManagementForm instance for this FormSet. | |
* @browser the form is unbound and uses initial data from this FormSet. | |
* @server the form is bound to submitted data. | |
*/ | |
BaseFormSet.prototype.managementForm = function() { | |
var form | |
if (!env.browser && this.isBound) { | |
form = new ManagementForm({data: this.data, autoId: this.autoId, | |
prefix: this.prefix}) | |
if (!form.isValid()) { | |
throw ValidationError('ManagementForm data is missing or has been tampered with', | |
{code: 'missing_management_form'}) | |
} | |
} | |
else { | |
var initial = {} | |
initial[TOTAL_FORM_COUNT] = this.totalFormCount() | |
initial[INITIAL_FORM_COUNT] = this.initialFormCount() | |
initial[MIN_NUM_FORM_COUNT] = this.minNum | |
initial[MAX_NUM_FORM_COUNT] = this.maxNum | |
form = new ManagementForm({autoId: this.autoId, | |
prefix: this.prefix, | |
initial: initial}) | |
} | |
if (this.managementFormCssClass !== null) { | |
form.hiddenFieldRowCssClass = this.managementFormCssClass | |
} | |
return form | |
} | |
/** | |
* Determines the number of form instances this formset contains, based on | |
* either submitted management data or initial configuration, as appropriate. | |
*/ | |
BaseFormSet.prototype.totalFormCount = function() { | |
if (!env.browser && this.isBound) { | |
// Return absoluteMax if it is lower than the actual total form count in | |
// the data; this is DoS protection to prevent clients from forcing the | |
// server to instantiate arbitrary numbers of forms. | |
return Math.min(this.managementForm().cleanedData[TOTAL_FORM_COUNT], this.absoluteMax) | |
} | |
else { | |
var initialForms = this.initialFormCount() | |
var totalForms = this.initialFormCount() + this.extra | |
// Allow all existing related objects/inlines to be displayed, but don't | |
// allow extra beyond max_num. | |
if (this.maxNum !== null && | |
initialForms > this.maxNum && | |
this.maxNum >= 0) { | |
totalForms = initialForms | |
} | |
if (this.maxNum !== null && | |
totalForms > this.maxNum && | |
this.maxNum >= 0) { | |
totalForms = this.maxNum | |
} | |
return totalForms | |
} | |
} | |
/** | |
* Determines the number of initial form instances this formset contains, based | |
* on either submitted management data or initial configuration, as appropriate. | |
*/ | |
BaseFormSet.prototype.initialFormCount = function() { | |
if (!env.browser && this.isBound) { | |
return this.managementForm().cleanedData[INITIAL_FORM_COUNT] | |
} | |
else { | |
// Use the length of the inital data if it's there, 0 otherwise. | |
var initialForms = (this.initial !== null && this.initial.length > 0 | |
? this.initial.length | |
: 0) | |
return initialForms | |
} | |
} | |
/** | |
* @browser Instantiates forms. | |
* @server Instantiates forms only when first accessed. | |
*/ | |
BaseFormSet.prototype.forms = function() { | |
if (!env.browser && this._forms !== null) { return this._forms } | |
var forms = [] | |
var totalFormCount = this.totalFormCount() | |
for (var i = 0; i < totalFormCount; i++) { | |
forms.push(this._constructForm(i)) | |
} | |
if (!env.browser) { this._forms = forms } | |
return forms | |
} | |
/** | |
* Instantiates and returns the ith form instance in the formset. | |
*/ | |
BaseFormSet.prototype._constructForm = function(i) { | |
var defaults = { | |
autoId: this.autoId | |
, prefix: this.addPrefix(i) | |
, errorConstructor: this.errorConstructor | |
} | |
if (this.isBound) { | |
defaults.data = this.data | |
defaults.files = this.files | |
} | |
if (this.initial !== null && this.initial.length > 0) { | |
if (typeof this.initial[i] != 'undefined') { | |
defaults.initial = this.initial[i] | |
} | |
} | |
// Allow extra forms to be empty | |
if (i >= this.initialFormCount()) { | |
defaults.emptyPermitted = true | |
} | |
var form = new this.form(defaults) | |
this.addFields(form, i) | |
return form | |
} | |
/** | |
* Returns a list of all the initial forms in this formset. | |
*/ | |
BaseFormSet.prototype.initialForms = function() { | |
return this.forms().slice(0, this.initialFormCount()) | |
} | |
/** | |
* Returns a list of all the extra forms in this formset. | |
*/ | |
BaseFormSet.prototype.extraForms = function() { | |
return this.forms().slice(this.initialFormCount()) | |
} | |
BaseFormSet.prototype.emptyForm = function() { | |
var kwargs = { | |
autoId: this.autoId, | |
prefix: this.addPrefix('__prefix__'), | |
emptyPermitted: true | |
} | |
var form = new this.form(kwargs) | |
this.addFields(form, null) | |
return form | |
} | |
/** | |
* Returns a list of form.cleanedData objects for every form in this.forms(). | |
*/ | |
BaseFormSet.prototype.cleanedData = function() { | |
if (!this.isValid()) { | |
throw new Error(this.constructor.name + | |
" object has no attribute 'cleanedData'") | |
} | |
return this.forms().map(function(form) { return form.cleanedData }) | |
} | |
/** | |
* Returns a list of forms that have been marked for deletion. | |
*/ | |
BaseFormSet.prototype.deletedForms = function() { | |
if (!this.isValid() || !this.canDelete) { return [] } | |
var forms = this.forms() | |
// Construct _deletedFormIndexes, which is just a list of form indexes | |
// that have had their deletion widget set to true. | |
if (typeof this._deletedFormIndexes == 'undefined') { | |
this._deletedFormIndexes = [] | |
for (var i = 0, l = forms.length; i < l; i++) { | |
var form = forms[i] | |
// If this is an extra form and hasn't changed, ignore it | |
if (i >= this.initialFormCount() && !form.hasChanged()) { | |
continue | |
} | |
if (this._shouldDeleteForm(form)) { | |
this._deletedFormIndexes.push(i) | |
} | |
} | |
} | |
return this._deletedFormIndexes.map(function(i) { return forms[i] }) | |
} | |
/** | |
* Returns a list of forms in the order specified by the incoming data. | |
* Throws an Error if ordering is not allowed. | |
*/ | |
BaseFormSet.prototype.orderedForms = function() { | |
if (!this.isValid() || !this.canOrder) { | |
throw new Error(this.constructor.name + | |
" object has no attribute 'orderedForms'") | |
} | |
var forms = this.forms() | |
// Construct _ordering, which is a list of [form index, orderFieldValue] | |
// pairs. After constructing this list, we'll sort it by orderFieldValue | |
// so we have a way to get to the form indexes in the order specified by | |
// the form data. | |
if (typeof this._ordering == 'undefined') { | |
this._ordering = [] | |
for (var i = 0, l = forms.length; i < l; i++) { | |
var form = forms[i] | |
// If this is an extra form and hasn't changed, ignore it | |
if (i >= this.initialFormCount() && !form.hasChanged()) { | |
continue | |
} | |
// Don't add data marked for deletion | |
if (this.canDelete && this._shouldDeleteForm(form)) { | |
continue | |
} | |
this._ordering.push([i, form.cleanedData[ORDERING_FIELD_NAME]]) | |
} | |
// Null should be sorted below anything else. Allowing null as a | |
// comparison value makes it so we can leave ordering fields blank. | |
this._ordering.sort(function(x, y) { | |
if (x[1] === null && y[1] === null) { | |
// Sort by form index if both order field values are null | |
return x[0] - y[0] | |
} | |
if (x[1] === null) { | |
return 1 | |
} | |
if (y[1] === null) { | |
return -1 | |
} | |
return x[1] - y[1] | |
}) | |
} | |
return this._ordering.map(function(ordering) { return forms[ordering[0]]}) | |
} | |
BaseFormSet.prototype.getDefaultPrefix = function() { | |
return 'form' | |
} | |
/** | |
* Returns an ErrorList of errors that aren't associated with a particular | |
* form -- i.e., from formset.clean(). Returns an empty ErrorList if there are | |
* none. | |
*/ | |
BaseFormSet.prototype.nonFormErrors = function() { | |
if (this._nonFormErrors === null) { | |
this.fullClean() | |
} | |
return this._nonFormErrors | |
} | |
/** | |
* Returns a list of form.errors for every form in this.forms. | |
*/ | |
BaseFormSet.prototype.errors = function() { | |
if (this._errors === null) { | |
this.fullClean() | |
} | |
return this._errors | |
} | |
/** | |
* Returns the number of errors across all forms in the formset. | |
*/ | |
BaseFormSet.prototype.totalErrorCount = function() { | |
return (this.nonFormErrors().length() + | |
this.errors().reduce(function(sum, formErrors) { | |
return sum + formErrors.length() | |
}, 0)) | |
} | |
/** | |
* Returns whether or not the form was marked for deletion. | |
*/ | |
BaseFormSet.prototype._shouldDeleteForm = function(form) { | |
return object.get(form.cleanedData, DELETION_FIELD_NAME, false) | |
} | |
/** | |
* Returns true if every form in this.forms() is valid. | |
*/ | |
BaseFormSet.prototype.isValid = function() { | |
if (!this.isBound) { return false } | |
// We loop over every form.errors here rather than short circuiting on the | |
// first failure to make sure validation gets triggered for every form. | |
var formsValid = true | |
// Triggers a full clean | |
this.errors() | |
var forms = this.forms() | |
for (var i = 0, l = forms.length; i < l; i++) { | |
var form = forms[i] | |
if (this.canDelete && this._shouldDeleteForm(form)) { | |
// This form is going to be deleted so any of its errors should | |
// not cause the entire formset to be invalid. | |
continue | |
} | |
if (!form.isValid()) { | |
formsValid = false | |
} | |
} | |
return (formsValid && !this.nonFormErrors().isPopulated()) | |
} | |
/** | |
* Cleans all of this.data and populates this._errors and this._nonFormErrors. | |
*/ | |
BaseFormSet.prototype.fullClean = function() { | |
this._errors = [] | |
this._nonFormErrors = new this.errorConstructor() | |
if (!this.isBound) { | |
return // Stop further processing | |
} | |
var forms = this.forms() | |
for (var i = 0, l = forms.length; i < l; i++) { | |
var form = forms[i] | |
this._errors.push(form.errors()) | |
} | |
try { | |
var totalFormCount = this.totalFormCount() | |
var deletedFormCount = this.deletedForms().length | |
if ((this.validateMax && totalFormCount - deletedFormCount > this.maxNum) || | |
(!env.browser && this.managementForm().cleanedData[TOTAL_FORM_COUNT] > this.absoluteMax)) { | |
throw ValidationError('Please submit ' + this.maxNum + ' or fewer forms.', | |
{code: 'tooManyForms'}) | |
} | |
if (this.validateMin && totalFormCount - deletedFormCount < this.minNum) { | |
throw ValidationError('Please submit ' + this.minNum + ' or more forms.', | |
{code: 'tooFewForms'}) | |
} | |
// Give this.clean() a chance to do cross-form validation. | |
this.clean() | |
} | |
catch (e) { | |
if (!(e instanceof ValidationError)) { throw e } | |
this._nonFormErrors = new this.errorConstructor(e.messages()) | |
} | |
} | |
/** | |
* Hook for doing any extra formset-wide cleaning after Form.clean() has been | |
* called on every form. Any ValidationError raised by this method will not be | |
* associated with a particular form; it will be accesible via | |
* formset.nonFormErrors() | |
*/ | |
BaseFormSet.prototype.clean = function() {} | |
/** | |
* Returns true if any form differs from initial. | |
*/ | |
BaseFormSet.prototype.hasChanged = function() { | |
var forms = this.forms() | |
for (var i = 0, l = forms.length; i < l; i++) { | |
if (forms[i].hasChanged()) { | |
return true | |
} | |
} | |
return false | |
} | |
/** | |
* A hook for adding extra fields on to each form instance. | |
* @param {Form} form the form fields are to be added to. | |
* @param {Number} index the index of the given form in the formset. | |
*/ | |
BaseFormSet.prototype.addFields = function(form, index) { | |
if (this.canOrder) { | |
// Only pre-fill the ordering field for initial forms | |
if (index != null && index < this.initialFormCount()) { | |
form.fields[ORDERING_FIELD_NAME] = | |
IntegerField({label: 'Order', initial: index + 1, | |
required: false}) | |
} | |
else { | |
form.fields[ORDERING_FIELD_NAME] = | |
IntegerField({label: 'Order', required: false}) | |
} | |
} | |
if (this.canDelete) { | |
form.fields[DELETION_FIELD_NAME] = | |
BooleanField({label: 'Delete', required: false}) | |
} | |
} | |
/** | |
* Returns the formset prefix with the form index appended. | |
* @param {Number} index the index of a form in the formset. | |
*/ | |
BaseFormSet.prototype.addPrefix = function(index) { | |
return this.prefix + '-' + index | |
} | |
/** | |
* Returns true if the formset needs to be multipart-encoded, i.e. it has a | |
* FileInput. Otherwise, false. | |
*/ | |
BaseFormSet.prototype.isMultipart = function() { | |
return (this.forms().length > 0 && this.forms()[0].isMultipart()) | |
} | |
BaseFormSet.prototype.render = function() { | |
return this.asTable() | |
} | |
/** | |
* Returns this formset rendered as HTML <tr>s - excluding the <table>. | |
*/ | |
BaseFormSet.prototype.asTable = function() { | |
// XXX: there is no semantic division between forms here, there probably | |
// should be. It might make sense to render each form as a table row with | |
// each field as a td. | |
var rows = this.managementForm().asTable() | |
this.forms().forEach(function(form) { rows = rows.concat(form.asTable()) }) | |
return rows | |
} | |
BaseFormSet.prototype.asDiv = function() { | |
var rows = this.managementForm().asDiv() | |
this.forms().forEach(function(form) { rows = rows.concat(form.asDiv()) }) | |
return rows | |
} | |
BaseFormSet.prototype.asUl = function() { | |
var rows = this.managementForm().asUl() | |
this.forms().forEach(function(form) { rows = rows.concat(form.asUl()) }) | |
return rows | |
} | |
/** | |
* Creates a FormSet constructor for the given Form constructor. | |
* @param {Form} form | |
* @param {Object=} kwargs | |
*/ | |
function formsetFactory(form, kwargs) { | |
kwargs = object.extend({ | |
formset: BaseFormSet, extra: 1, canOrder: false, canDelete: false, | |
maxNum: DEFAULT_MAX_NUM, validateMax: false, | |
minNum: DEFAULT_MIN_NUM, validateMin: false | |
}, kwargs) | |
// Remove special properties from kwargs, as it will subsequently be used to | |
// add properties to the new formset's prototype. | |
var formset = object.pop(kwargs, 'formset') | |
var extra = object.pop(kwargs, 'extra') | |
var canOrder = object.pop(kwargs, 'canOrder') | |
var canDelete = object.pop(kwargs, 'canDelete') | |
var maxNum = object.pop(kwargs, 'maxNum') | |
var validateMax = object.pop(kwargs, 'validateMax') | |
var minNum = object.pop(kwargs, 'minNum') | |
var validateMin = object.pop(kwargs, 'validateMin') | |
// Hard limit on forms instantiated, to prevent memory-exhaustion attacks | |
// limit is simply maxNum + DEFAULT_MAX_NUM (which is 2 * DEFAULT_MAX_NUM | |
// if maxNum is not provided in the first place) | |
var absoluteMax = maxNum + DEFAULT_MAX_NUM | |
extra += minNum | |
kwargs.constructor = function(kwargs) { | |
this.form = form | |
this.extra = extra | |
this.canOrder = canOrder | |
this.canDelete = canDelete | |
this.maxNum = maxNum | |
this.validateMax = validateMax | |
this.minNum = minNum | |
this.validateMin = validateMin | |
this.absoluteMax = absoluteMax | |
formset.call(this, kwargs) | |
} | |
var formsetConstructor = formset.extend(kwargs) | |
return formsetConstructor | |
} | |
/** | |
* Returns true if every formset in formsets is valid. | |
*/ | |
function allValid(formsets) { | |
var valid = true | |
for (var i = 0, l = formsets.length; i < l; i++) { | |
if (!formsets[i].isValid()) { | |
valid = false | |
} | |
} | |
return valid | |
} | |
module.exports = { | |
DEFAULT_MAX_NUM: DEFAULT_MAX_NUM | |
, BaseFormSet: BaseFormSet | |
, formsetFactory: formsetFactory | |
, allValid: allValid | |
} | |
},{"./env":1,"./fields":2,"./forms":4,"./util":7,"./widgets":8,"Concur":9,"isomorph/object":14,"validators":19}],6:[function(_dereq_,module,exports){ | |
'use strict'; | |
var object = _dereq_('isomorph/object') | |
var validators = _dereq_('validators') | |
var env = _dereq_('./env') | |
var util = _dereq_('./util') | |
var formats = _dereq_('./formats') | |
var widgets = _dereq_('./widgets') | |
var fields = _dereq_('./fields') | |
var forms = _dereq_('./forms') | |
var formsets = _dereq_('./formsets') | |
object.extend( | |
module.exports | |
, { env: env | |
, ValidationError: validators.ValidationError | |
, ErrorObject: util.ErrorObject | |
, ErrorList: util.ErrorList | |
, formData: util.formData | |
, util: { | |
formatToArray: util.formatToArray | |
, makeChoices: util.makeChoices | |
, prettyName: util.prettyName | |
} | |
, formats: formats | |
, validators: validators | |
} | |
, widgets | |
, fields | |
, forms | |
, formsets | |
) | |
},{"./env":1,"./fields":2,"./formats":3,"./forms":4,"./formsets":5,"./util":7,"./widgets":8,"isomorph/object":14,"validators":19}],7:[function(_dereq_,module,exports){ | |
'use strict'; | |
var Concur = _dereq_('Concur') | |
var is = _dereq_('isomorph/is') | |
var object = _dereq_('isomorph/object') | |
var validators = _dereq_('validators') | |
var ValidationError = validators.ValidationError | |
var slice = Array.prototype.slice | |
/** | |
* Replaces String {placeholders} with properties of a given object, but | |
* interpolates into and returns an Array instead of a String. | |
* By default, any resulting empty strings are stripped out of the Array. To | |
* disable this, pass an options object with a 'strip' property which is false. | |
*/ | |
function formatToArray(str, obj, options) { | |
var parts = str.split(/\{(\w+)\}/g) | |
for (var i = 1, l = parts.length; i < l; i += 2) { | |
parts[i] = (object.hasOwn(obj, parts[i]) | |
? obj[parts[i]] | |
: '{' + parts[i] + '}') | |
} | |
if (!options || (options && options.strip !== false)) { | |
parts = parts.filter(function(p) { return p !== ''}) | |
} | |
return parts | |
} | |
/** | |
* Get a named property from an object, calling it and returning its result if | |
* it's a function. | |
*/ | |
function maybeCall(obj, prop) { | |
var value = obj[prop] | |
if (is.Function(value)) { | |
value = value.call(obj) | |
} | |
return value | |
} | |
/** | |
* Creates a list of choice pairs from a list of objects using the given named | |
* properties for the value and label. | |
*/ | |
function makeChoices(list, valueProp, labelProp) { | |
return list.map(function(item) { | |
return [maybeCall(item, valueProp), maybeCall(item, labelProp)] | |
}) | |
} | |
/** | |
* A version of bind which passes any extra arguments *after* the eventual | |
* function call's own arguments. | |
*/ | |
function bindRight(func, context) { | |
var partials = slice.call(arguments, 2) | |
return function() { | |
return func.apply(context, slice.call(arguments).concat(partials)) | |
} | |
} | |
/** | |
* Validates choice input and normalises lazy, non-Array choices to be | |
* [value, label] pairs | |
* @returning {Array} a normalised version of the given choices. | |
* @throws if an Array with length != 2 was found where a choice pair was expected. | |
*/ | |
function normaliseChoices(choices) { | |
if (!choices.length) { return choices } | |
var normalisedChoices = [] | |
for (var i = 0, l = choices.length, choice; i < l; i++) { | |
choice = choices[i] | |
if (!is.Array(choice)) { | |
// TODO In the development build, emit a warning about a choice being | |
// autmatically converted from 'blah' to ['blah', 'blah'] in case it | |
// wasn't intentional | |
choice = [choice, choice] | |
} | |
if (choice.length != 2) { | |
throw new Error('Choices in a choice list must contain exactly 2 values, ' + | |
'but got ' + JSON.stringify(choice)) | |
} | |
if (is.Array(choice[1])) { | |
var normalisedOptgroupChoices = [] | |
// This is an optgroup, so look inside the group for options | |
var optgroupChoices = choice[1] | |
for (var j = 0, m = optgroupChoices.length, optgroupChoice; j < m; j++) { | |
optgroupChoice = optgroupChoices[j] | |
if (!is.Array(optgroupChoice)) { | |
// TODO In the development build, emit a warning about an optgroup | |
// choice being autmatically converted from 'blah' to | |
// ['blah', 'blah'] in case it wasn't intentional. | |
optgroupChoice = [optgroupChoice, optgroupChoice] | |
} | |
if (optgroupChoice.length != 2) { | |
throw new Error('Choices in an optgroup choice list must contain ' + | |
'exactly 2 values, but got ' + | |
JSON.stringify(optgroupChoice)) | |
} | |
normalisedOptgroupChoices.push(optgroupChoice) | |
} | |
normalisedChoices.push([choice[0], normalisedOptgroupChoices]) | |
} | |
else { | |
normalisedChoices.push(choice) | |
} | |
} | |
return normalisedChoices | |
} | |
/** | |
* Converts 'firstName' and 'first_name' to 'First name', and | |
* 'SHOUTING_LIKE_THIS' to 'SHOUTING LIKE THIS'. | |
*/ | |
var prettyName = (function() { | |
var capsRE = /([A-Z]+)/g | |
var splitRE = /[ _]+/ | |
var allCapsRE = /^[A-Z][A-Z0-9]+$/ | |
return function(name) { | |
// Prefix sequences of caps with spaces and split on all space | |
// characters. | |
var parts = name.replace(capsRE, ' $1').split(splitRE) | |
// If we had an initial cap... | |
if (parts[0] === '') { | |
parts.splice(0, 1) | |
} | |
// Give the first word an initial cap and all subsequent words an | |
// initial lowercase if not all caps. | |
for (var i = 0, l = parts.length; i < l; i++) { | |
if (i === 0) { | |
parts[0] = parts[0].charAt(0).toUpperCase() + | |
parts[0].substr(1) | |
} | |
else if (!allCapsRE.test(parts[i])) { | |
parts[i] = parts[i].charAt(0).toLowerCase() + | |
parts[i].substr(1) | |
} | |
} | |
return parts.join(' ') | |
} | |
})() | |
/** | |
* @param {HTMLFormElement} form a form element. | |
* @return {Object.<string,(string|Array.<string>)>} an object containing the | |
* submittable value(s) held in each of the form's elements. | |
*/ | |
function formData(form) { | |
if (!form) { | |
throw new Error('formData was given form=' + form) | |
} | |
var data = {} | |
for (var i = 0, l = form.elements.length; i < l; i++) { | |
var element = form.elements[i] | |
var value = getFormElementValue(element) | |
// Add any value obtained to the data object | |
if (value !== null) { | |
if (object.hasOwn(data, element.name)) { | |
if (is.Array(data[element.name])) { | |
data[element.name] = data[element.name].concat(value) | |
} | |
else { | |
data[element.name] = [data[element.name], value] | |
} | |
} | |
else { | |
data[element.name] = value | |
} | |
} | |
} | |
return data | |
} | |
/** | |
* @param {HTMLFormElement} form a form element. | |
* @field {string} a field name. | |
* @return {(string|Array.<string>)} the named field's submittable value(s), | |
*/ | |
function fieldData(form, field) { | |
/* global NodeList */ | |
if (!form) { | |
throw new Error('fieldData was given form=' + form) | |
} | |
var data = null | |
var element = form.elements[field] | |
// Check if we've got a NodeList | |
if (element instanceof NodeList) { | |
for (var i = 0, l = element.length; i < l; i++) { | |
var value = getFormElementValue(element[i]) | |
if (value !== null) { | |
if (data !== null) { | |
if (is.Array(data)) { | |
data= data.concat(value) | |
} | |
else { | |
data = [data, value] | |
} | |
} | |
else { | |
data = value | |
} | |
} | |
} | |
} | |
else { | |
data = getFormElementValue(element) | |
} | |
return data | |
} | |
/** | |
* Lookup for <input>s whose value can be accessed with .value. | |
*/ | |
var textInputTypes = object.lookup([ | |
'hidden', 'password', 'text', 'email', 'url', 'number', 'file', 'textarea' | |
]) | |
/** | |
* Lookup for <inputs> which have a .checked property. | |
*/ | |
var checkedInputTypes = object.lookup(['checkbox', 'radio']) | |
/** | |
* @param {HTMLElement} element a form element. | |
* @return {(string|Array.<string>)} the element's submittable value(s), | |
*/ | |
function getFormElementValue(element) { | |
var value = null | |
var type = element.type | |
if (textInputTypes[type] || checkedInputTypes[type] && element.checked) { | |
value = element.value | |
} | |
else if (type == 'select-one') { | |
if (element.options.length) { | |
value = element.options[element.selectedIndex].value | |
} | |
} | |
else if (type == 'select-multiple') { | |
value = [] | |
for (var i = 0, l = element.options.length; i < l; i++) { | |
if (element.options[i].selected) { | |
value.push(element.options[i].value) | |
} | |
} | |
} | |
return value | |
} | |
/** | |
* Coerces to string and strips leading and trailing spaces. | |
*/ | |
var strip = function() { | |
var stripRE =/(^\s+|\s+$)/g | |
return function strip(s) { | |
return (''+s).replace(stripRE, '') | |
} | |
}() | |
/** | |
* From Underscore.js 1.5.2 | |
* http://underscorejs.org | |
* (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors | |
* Returns a function, that, as long as it continues to be invoked, will not | |
* be triggered. The function will be called after it stops being called for | |
* N milliseconds. If `immediate` is passed, trigger the function on the | |
* leading edge, instead of the trailing. | |
*/ | |
function debounce(func, wait, immediate) { | |
var timeout, args, context, timestamp, result | |
return function() { | |
context = this | |
args = arguments | |
timestamp = new Date() | |
var later = function() { | |
var last = (new Date()) - timestamp | |
if (last < wait) { | |
timeout = setTimeout(later, wait - last) | |
} else { | |
timeout = null | |
if (!immediate) { result = func.apply(context, args) } | |
} | |
}; | |
var callNow = immediate && !timeout | |
if (!timeout) { | |
timeout = setTimeout(later, wait) | |
} | |
if (callNow) { result = func.apply(context, args) } | |
return result | |
} | |
} | |
/** | |
* A collection of field errors that knows how to display itself in various | |
* formats. This object's .error properties are the field names and | |
* corresponding values are the errors. | |
* @constructor | |
*/ | |
var ErrorObject = Concur.extend({ | |
constructor: function ErrorObject(errors) { | |
if (!(this instanceof ErrorObject)) { return new ErrorObject(errors) } | |
this.errors = errors || {} | |
} | |
}) | |
ErrorObject.prototype.set = function(field, error) { | |
this.errors[field] = error | |
} | |
ErrorObject.prototype.get = function(field) { | |
return this.errors[field] | |
} | |
ErrorObject.prototype.remove = function(fields) { | |
return delete this.errors[fields] | |
} | |
ErrorObject.prototype.removeAll = function(fields) { | |
for (var i = 0, l = fields.length; i < l; i++) { | |
delete this.errors[fields[i]] | |
} | |
} | |
ErrorObject.prototype.hasField = function(field) { | |
return object.hasOwn(this.errors, field) | |
} | |
ErrorObject.prototype.length = function() { | |
return Object.keys(this.errors).length | |
} | |
/** | |
* Determines if any errors are present. | |
*/ | |
ErrorObject.prototype.isPopulated = function() { | |
return (this.length() > 0) | |
} | |
/** | |
* Default display is as a list. | |
*/ | |
ErrorObject.prototype.render = function() { | |
return this.asUl() | |
} | |
/** | |
* Displays error details as a list. | |
*/ | |
ErrorObject.prototype.asUl = function() { | |
var items = Object.keys(this.errors).map(function(field) { | |
return m('li', [field, this.errors[field].asUl()]) | |
}.bind(this)) | |
if (items.length === 0) { return } | |
return m('ul', {className: 'errorlist'}, items) | |
} | |
/** | |
* Displays error details as text. | |
*/ | |
ErrorObject.prototype.asText = ErrorObject.prototype.toString = function() { | |
return Object.keys(this.errors).map(function(field) { | |
var mesages = this.errors[field].messages() | |
return ['* ' + field].concat(mesages.map(function(message) { | |
return (' * ' + message) | |
})).join('\n') | |
}.bind(this)).join('\n') | |
} | |
ErrorObject.prototype.asData = function() { | |
var data = {} | |
Object.keys(this.errors).map(function(field) { | |
data[field] = this.errors[field].asData() | |
}.bind(this)) | |
return data | |
} | |
ErrorObject.prototype.toJSON = function() { | |
var jsonObj = {} | |
Object.keys(this.errors).map(function(field) { | |
jsonObj[field] = this.errors[field].toJSON() | |
}.bind(this)) | |
return jsonObj | |
} | |
/** | |
* A list of errors which knows how to display itself in various formats. | |
* @param {Array=} list a list of errors. | |
* @constructor | |
*/ | |
var ErrorList = Concur.extend({ | |
constructor: function ErrorList(list) { | |
if (!(this instanceof ErrorList)) { return new ErrorList(list) } | |
this.data = list || [] | |
} | |
}) | |
/** | |
* Adds more errors. | |
* @param {Array} errorList a list of errors | |
*/ | |
ErrorList.prototype.extend = function(errorList) { | |
this.data.push.apply(this.data, errorList) | |
} | |
ErrorList.prototype.length = function() { | |
return this.data.length | |
} | |
/** | |
* Determines if any errors are present. | |
*/ | |
ErrorList.prototype.isPopulated = function() { | |
return (this.length() > 0) | |
} | |
/** | |
* Returns the list of messages held in this ErrorList. | |
*/ | |
ErrorList.prototype.messages = function() { | |
var messages = [] | |
for (var i = 0, l = this.data.length; i < l; i++) { | |
var error = this.data[i] | |
if (error instanceof ValidationError) { | |
error = error.messages()[0] | |
} | |
messages.push(error) | |
} | |
return messages | |
} | |
/** | |
* Default display is as a list. | |
*/ | |
ErrorList.prototype.render = function() { | |
return this.asUl() | |
} | |
/** | |
* Displays errors as a list. | |
*/ | |
ErrorList.prototype.asUl = function() { | |
if (!this.isPopulated()) { | |
return | |
} | |
return m('ul', {className: 'errorlist'} | |
, this.messages().map(function(error) { | |
return m('li', [error]) | |
}) | |
) | |
} | |
/** | |
* Displays errors as text. | |
*/ | |
ErrorList.prototype.asText = ErrorList.prototype.toString =function() { | |
return this.messages().map(function(error) { | |
return '* ' + error | |
}).join('\n') | |
} | |
ErrorList.prototype.asData = function() { | |
return this.data | |
} | |
ErrorList.prototype.toJSON = function() { | |
return ValidationError(this.data).errorList.map(function(error) { | |
return { | |
message: error.messages()[0] | |
, code: error.code || '' | |
} | |
}) | |
} | |
module.exports = { | |
ErrorObject: ErrorObject | |
, ErrorList: ErrorList | |
, formData: formData | |
, fieldData: fieldData | |
, formatToArray: formatToArray | |
, makeChoices: makeChoices | |
, normaliseChoices: normaliseChoices | |
, prettyName: prettyName | |
, strip: strip | |
, debounce: debounce | |
, bindRight: bindRight | |
} | |
},{"Concur":9,"isomorph/is":13,"isomorph/object":14,"validators":19}],8:[function(_dereq_,module,exports){ | |
'use strict'; | |
var Concur = _dereq_('Concur') | |
var is = _dereq_('isomorph/is') | |
var format = _dereq_('isomorph/format').formatObj | |
var object = _dereq_('isomorph/object') | |
var time = _dereq_('isomorph/time') | |
var env = _dereq_('./env') | |
var formats = _dereq_('./formats') | |
var util = _dereq_('./util') | |
/** | |
* Some widgets are made of multiple HTML elements -- namely, RadioSelect. | |
* This represents the "inner" HTML element of a widget. | |
*/ | |
var SubWidget = Concur.extend({ | |
constructor: function SubWidget(parentWidget, name, value, kwargs) { | |
if (!(this instanceof SubWidget)) { | |
return new SubWidget(parentWidget, name, value, kwargs) | |
} | |
this.parentWidget = parentWidget | |
this.name = name | |
this.value = value | |
kwargs = object.extend({attrs: null, choices: []}, kwargs) | |
this.attrs = kwargs.attrs | |
this.choices = kwargs.choices | |
} | |
}) | |
SubWidget.prototype.render = function() { | |
var kwargs = {attrs: this.attrs} | |
if (this.choices.length) { | |
kwargs.choices = this.choices | |
} | |
return this.parentWidget.render(this.name, this.value, kwargs) | |
} | |
/** | |
* An HTML form widget. | |
* @constructor | |
* @param {Object=} kwargs | |
*/ | |
var Widget = Concur.extend({ | |
constructor: function Widget(kwargs) { | |
kwargs = object.extend({attrs: null}, kwargs) | |
this.attrs = object.extend({}, kwargs.attrs) | |
} | |
/** Determines whether this corresponds to an <input type="hidden">. */ | |
, isHidden: false | |
/** Determines whether this widget needs a multipart-encoded form. */ | |
, needsMultipartForm: false | |
/** Determines whether this widget is for a required field.. */ | |
, isRequired: false | |
/** Override for active validation config a partcular widget needs to use. */ | |
, validation: null | |
}) | |
/** | |
* Yields all "subwidgets" of this widget. Used only by RadioSelect to | |
* allow access to individual <input type="radio"> buttons. | |
* | |
* Arguments are the same as for render(). | |
*/ | |
Widget.prototype.subWidgets = function(name, value, kwargs) { | |
return [SubWidget(this, name, value, kwargs)] | |
} | |
/** | |
* Returns this Widget rendered as HTML. | |
* | |
* The value given is not guaranteed to be valid input, so subclass | |
* implementations should program defensively. | |
* | |
* @abstract | |
*/ | |
Widget.prototype.render = function(name, value, kwargs) { | |
throw new Error('Constructors extending must implement a render() method.') | |
} | |
/** | |
* Helper function for building an HTML attributes object. | |
*/ | |
Widget.prototype.buildAttrs = function(kwargAttrs, renderAttrs, validation) { | |
var attrs = object.extend({}, this.attrs, renderAttrs, kwargAttrs) | |
if (validation && validation !== 'manual') { | |
// Add an onChange handler to let the form know when the field changes | |
attrs.onChange = validation.onChange | |
// If validation should be performed when a different event fires, hook up | |
// the supplied handler for it. | |
if (validation.event != 'onChange') { | |
attrs[validation.event] = validation.eventHandler | |
} | |
} | |
return attrs | |
} | |
/** | |
* Retrieves a value for this widget from the given data. | |
* @param {Object} data form data. | |
* @param {Object} files file data. | |
* @param {String} name the field name to be used to retrieve data. | |
* @return a value for this widget, or null if no value was provided. | |
*/ | |
Widget.prototype.valueFromData = function(data, files, name) { | |
return object.get(data, name, null) | |
} | |
/** | |
* Determines the HTML id attribute of this Widget for use by a | |
* <label>, given the id of the field. | |
* | |
* This hook is necessary because some widgets have multiple HTML elements and, | |
* thus, multiple ids. In that case, this method should return an ID value that | |
* corresponds to the first id in the widget's tags. | |
* | |
* @param {String} id a field id. | |
* @return the id which should be used by a <label>> for this Widget. | |
*/ | |
Widget.prototype.idForLabel = function(id) { | |
return id | |
} | |
/** | |
* An HTML <input> widget. | |
* @constructor | |
* @extends {Widget} | |
* @param {Object=} kwargs | |
*/ | |
var Input = Widget.extend({ | |
constructor: function Input(kwargs) { | |
if (!(this instanceof Widget)) { return new Input(kwargs) } | |
Widget.call(this, kwargs) | |
} | |
/** The type attribute of this input - subclasses must define it. */ | |
, inputType: null | |
}) | |
Input.prototype._formatValue = function(value) { | |
return value | |
} | |
Input.prototype.render = function(name, value, kwargs) { | |
kwargs = object.extend({attrs: null}, kwargs) | |
if (value === null) { | |
value = '' | |
} | |
var finalAttrs = this.buildAttrs(kwargs.attrs, {type: this.inputType, | |
name: name}, kwargs.validation) | |
// Hidden inputs can be made controlled inputs by default, as the user | |
// can't directly interact with them. | |
finalAttrs.value = (value !== '' ? ''+this._formatValue(value) : value) | |
return m('input', finalAttrs) | |
} | |
/** | |
* An HTML <input type="text"> widget. | |
* @constructor | |
* @extends {Input} | |
* @param {Object=} kwargs | |
*/ | |
var TextInput = Input.extend({ | |
constructor: function TextInput(kwargs) { | |
if (!(this instanceof Widget)) { return new TextInput(kwargs) } | |
kwargs = object.extend({attrs: null}, kwargs) | |
if (kwargs.attrs != null) { | |
this.inputType = object.pop(kwargs.attrs, 'type', this.inputType) | |
} | |
Input.call(this, kwargs) | |
} | |
, inputType: 'text' | |
}) | |
/** | |
* An HTML <input type="number"> widget. | |
* @constructor | |
* @extends {TextInput} | |
* @param {Object=} kwargs | |
*/ | |
var NumberInput = TextInput.extend({ | |
constructor: function NumberInput(kwargs) { | |
if (!(this instanceof Widget)) { return new NumberInput(kwargs) } | |
TextInput.call(this, kwargs) | |
} | |
, inputType: 'number' | |
}) | |
/** | |
* An HTML <input type="email"> widget. | |
* @constructor | |
* @extends {TextInput} | |
* @param {Object=} kwargs | |
*/ | |
var EmailInput = TextInput.extend({ | |
constructor: function EmailInput(kwargs) { | |
if (!(this instanceof Widget)) { return new EmailInput(kwargs) } | |
TextInput.call(this, kwargs) | |
} | |
, inputType: 'email' | |
}) | |
/** | |
* An HTML <input type="url"> widget. | |
* @constructor | |
* @extends {TextInput} | |
* @param {Object=} kwargs | |
*/ | |
var URLInput = TextInput.extend({ | |
constructor: function URLInput(kwargs) { | |
if (!(this instanceof Widget)) { return new URLInput(kwargs) } | |
TextInput.call(this, kwargs) | |
} | |
, inputType: 'url' | |
}) | |
/** | |
* An HTML <input type="password"> widget. | |
* @constructor | |
* @extends {TextInput} | |
* @param {Object=} kwargs | |
*/ | |
var PasswordInput = TextInput.extend({ | |
constructor: function PasswordInput(kwargs) { | |
if (!(this instanceof Widget)) { return new PasswordInput(kwargs) } | |
kwargs = object.extend({renderValue: false}, kwargs) | |
TextInput.call(this, kwargs) | |
this.renderValue = kwargs.renderValue | |
} | |
, inputType: 'password' | |
}) | |
PasswordInput.prototype.render = function(name, value, kwargs) { | |
kwargs = object.extend({validation: 'manual'}, kwargs) | |
var validation = kwargs.validation | |
if (((!validation || validation == 'manual') || !env.browser) && !this.renderValue) { | |
value = '' | |
} | |
return TextInput.prototype.render.call(this, name, value, kwargs) | |
} | |
/** | |
* An HTML <input type="hidden"> widget. | |
* @constructor | |
* @extends {Input} | |
* @param {Object=} kwargs | |
*/ | |
var HiddenInput = Input.extend({ | |
constructor: function HiddenInput(kwargs) { | |
if (!(this instanceof Widget)) { return new HiddenInput(kwargs) } | |
Input.call(this, kwargs) | |
} | |
, inputType: 'hidden' | |
, isHidden: true | |
}) | |
/** | |
* A widget that handles <input type="hidden"> for fields that have a list of | |
* values. | |
* @constructor | |
* @extends {HiddenInput} | |
* @param {Object=} kwargs | |
*/ | |
var MultipleHiddenInput = HiddenInput.extend({ | |
constructor: function MultipleHiddenInput(kwargs) { | |
if (!(this instanceof Widget)) { return new MultipleHiddenInput(kwargs) } | |
HiddenInput.call(this, kwargs) | |
} | |
}) | |
MultipleHiddenInput.prototype.render = function(name, value, kwargs) { | |
kwargs = object.extend({attrs: null}, kwargs) | |
if (value === null) { | |
value = [] | |
} | |
var finalAttrs = this.buildAttrs(kwargs.attrs, {type: this.inputType, | |
name: name}) | |
var id = object.get(finalAttrs, 'id', null) | |
var inputs = [] | |
for (var i = 0, l = value.length; i < l; i++) { | |
var inputAttrs = object.extend({}, finalAttrs, {value: value[i]}) | |
if (id) { | |
// An ID attribute was given. Add a numeric index as a suffix | |
// so that the inputs don't all have the same ID attribute. | |
inputAttrs.id = format('{id}_{i}', {id: id, i: i}) | |
} | |
inputs.push(m('input', inputAttrs)) | |
} | |
return m('div', inputs) | |
} | |
MultipleHiddenInput.prototype.valueFromData = function(data, files, name) { | |
if (typeof data[name] != 'undefined') { | |
return [].concat(data[name]) | |
} | |
return null | |
} | |
/** | |
* An HTML <input type="file"> widget. | |
* @constructor | |
* @extends {Input} | |
* @param {Object=} kwargs | |
*/ | |
var FileInput = Input.extend({ | |
constructor: function FileInput(kwargs) { | |
if (!(this instanceof Widget)) { return new FileInput(kwargs) } | |
Input.call(this, kwargs) | |
} | |
, inputType: 'file' | |
, needsMultipartForm: true | |
, validation: {event: 'onChange'} | |
}) | |
FileInput.prototype.render = function(name, value, kwargs) { | |
return Input.prototype.render.call(this, name, null, kwargs) | |
} | |
/** | |
* File widgets take data from file wrappers on the server. On the client, they | |
* take it from data so the presence of a .value can be validated when required. | |
*/ | |
FileInput.prototype.valueFromData = function(data, files, name) { | |
return object.get(env.browser ? data : files, name, null) | |
} | |
var FILE_INPUT_CONTRADICTION = {} | |
/** | |
* @constructor | |
* @extends {FileInput} | |
* @param {Object=} kwargs | |
*/ | |
var ClearableFileInput = FileInput.extend({ | |
constructor: function ClearableFileInput(kwargs) { | |
if (!(this instanceof Widget)) { return new ClearableFileInput(kwargs) } | |
FileInput.call(this, kwargs) | |
} | |
, initialText: 'Currently' | |
, inputText: 'Change' | |
, clearCheckboxLabel: 'Clear' | |
, templateWithInitial: function(params) { | |
return util.formatToArray( | |
'{initialText}: {initial} {clearTemplate}{br}{inputText}: {input}' | |
, object.extend(params, {br: m('br')}) | |
) | |
} | |
, templateWithClear: function(params) { | |
return util.formatToArray( | |
'{checkbox} {label}' | |
, object.extend(params, { | |
label: m('label', {htmlFor: params.checkboxId}, [params.label]) | |
}) | |
) | |
} | |
, urlMarkupTemplate: function(href, name) { | |
return m('a', {href: href}, [name]) | |
} | |
}) | |
/** | |
* Given the name of the file input, return the name of the clear checkbox | |
* input. | |
*/ | |
ClearableFileInput.prototype.clearCheckboxName = function(name) { | |
return name + '-clear' | |
} | |
/** | |
* Given the name of the clear checkbox input, return the HTML id for it. | |
*/ | |
ClearableFileInput.prototype.clearCheckboxId = function(name) { | |
return name + '_id' | |
} | |
ClearableFileInput.prototype.render = function(name, value, kwargs) { | |
kwargs = object.extend({attrs: {}}, kwargs) | |
var input = FileInput.prototype.render.call(this, name, value, kwargs) | |
if (value && typeof value.url != 'undefined') { | |
var clearTemplate | |
if (!this.isRequired) { | |
var clearCheckboxName = this.clearCheckboxName(name) | |
var clearCheckboxId = this.clearCheckboxId(clearCheckboxName) | |
clearTemplate = this.templateWithClear({ | |
checkbox: CheckboxInput().render(clearCheckboxName, false, {attrs: {'id': clearCheckboxId}}) | |
, checkboxId: clearCheckboxId | |
, label: this.clearCheckboxLabel | |
}) | |
} | |
var contents = this.templateWithInitial({ | |
initialText: this.initialText | |
, initial: this.urlMarkupTemplate(value.url, ''+value) | |
, clearTemplate: clearTemplate | |
, inputText: this.inputText | |
, input: input | |
}) | |
return m('span', contents) | |
} | |
else { | |
return m('span', [input]) | |
} | |
} | |
ClearableFileInput.prototype.valueFromData = function(data, files, name) { | |
var upload = FileInput.prototype.valueFromData(data, files, name) | |
if (!this.isRequired && | |
CheckboxInput.prototype.valueFromData.call(this, data, files, | |
this.clearCheckboxName(name))) { | |
if (upload) { | |
// If the user contradicts themselves (uploads a new file AND | |
// checks the "clear" checkbox), we return a unique marker | |
// object that FileField will turn into a ValidationError. | |
return FILE_INPUT_CONTRADICTION | |
} | |
// false signals to clear any existing value, as opposed to just null | |
return false | |
} | |
return upload | |
} | |
/** | |
* An HTML <textarea> widget. | |
* @param {Object} [kwargs] configuration options | |
* @config {Object} [attrs] HTML attributes for the rendered widget. Default | |
* rows and cols attributes will be used if not provided. | |
* @constructor | |
* @extends {Widget} | |
* @param {Object=} kwargs | |
*/ | |
var Textarea = Widget.extend({ | |
constructor: function Textarea(kwargs) { | |
if (!(this instanceof Widget)) { return new Textarea(kwargs) } | |
// Ensure we have something in attrs | |
kwargs = object.extend({attrs: null}, kwargs) | |
// Provide default 'cols' and 'rows' attributes | |
kwargs.attrs = object.extend({rows: '10', cols: '40'}, kwargs.attrs) | |
Widget.call(this, kwargs) | |
} | |
}) | |
Textarea.prototype.render = function(name, value, kwargs) { | |
kwargs = object.extend({validation: 'manual'}, kwargs) | |
if (value === null) { | |
value = '' | |
} | |
var finalAttrs = this.buildAttrs(kwargs.attrs, {name: name}, kwargs.validation) | |
finalAttrs.value = value | |
return m('textarea', finalAttrs) | |
} | |
/** | |
* A <input type="text"> which, if given a Date object to display, formats it as | |
* an appropriate date/time String. | |
* @constructor | |
* @extends {TextInput} | |
* @param {Object=} kwargs | |
*/ | |
var DateTimeBaseInput = TextInput.extend({ | |
constructor: function DateTimeBaseInput(kwargs) { | |
kwargs = object.extend({format: null}, kwargs) | |
TextInput.call(this, kwargs) | |
this.format = (kwargs.format !== null ? kwargs.format : this.defaultFormat) | |
} | |
}) | |
DateTimeBaseInput.prototype._formatValue = function(value) { | |
if (is.Date(value)) { | |
return time.strftime(value, this.format) | |
} | |
return value | |
} | |
/** | |
* @constructor | |
* @extends {DateTimeBaseInput} | |
* @param {Object=} kwargs | |
*/ | |
var DateInput = DateTimeBaseInput.extend({ | |
constructor: function DateInput(kwargs) { | |
if (!(this instanceof DateInput)) { return new DateInput(kwargs) } | |
DateTimeBaseInput.call(this, kwargs) | |
} | |
, defaultFormat: formats.DEFAULT_DATE_INPUT_FORMATS[0] | |
}) | |
/** | |
* @constructor | |
* @extends {DateTimeBaseInput} | |
* @param {Object=} kwargs | |
*/ | |
var DateTimeInput = DateTimeBaseInput.extend({ | |
constructor: function DateTimeInput(kwargs) { | |
if (!(this instanceof DateTimeInput)) { return new DateTimeInput(kwargs) } | |
DateTimeBaseInput.call(this, kwargs) | |
} | |
, defaultFormat: formats.DEFAULT_DATETIME_INPUT_FORMATS[0] | |
}) | |
/** | |
* @constructor | |
* @extends {DateTimeBaseInput} | |
* @param {Object=} kwargs | |
*/ | |
var TimeInput = DateTimeBaseInput.extend({ | |
constructor: function TimeInput(kwargs) { | |
if (!(this instanceof TimeInput)) { return new TimeInput(kwargs) } | |
DateTimeBaseInput.call(this, kwargs) | |
} | |
, defaultFormat: formats.DEFAULT_TIME_INPUT_FORMATS[0] | |
}) | |
var defaultCheckTest = function(value) { | |
return (value !== false && | |
value !== null && | |
value !== '') | |
} | |
/** | |
* An HTML <input type="checkbox"> widget. | |
* @constructor | |
* @extends {Widget} | |
* @param {Object=} kwargs | |
*/ | |
var CheckboxInput = Widget.extend({ | |
constructor: function CheckboxInput(kwargs) { | |
if (!(this instanceof Widget)) { return new CheckboxInput(kwargs) } | |
kwargs = object.extend({checkTest: defaultCheckTest}, kwargs) | |
Widget.call(this, kwargs) | |
this.checkTest = kwargs.checkTest | |
} | |
, validation: {event: 'onChange'} | |
}) | |
CheckboxInput.prototype.render = function(name, value, kwargs) { | |
kwargs = object.extend({validation: 'manual'}, kwargs) | |
var finalAttrs = this.buildAttrs(kwargs.attrs, {type: 'checkbox', | |
name: name}, kwargs.validation) | |
if (value !== '' && value !== true && value !== false && value !== null && | |
value !== undefined) { | |
// Only add the value attribute if value is non-empty | |
finalAttrs.value = value | |
} | |
finalAttrs.checked = this.checkTest(value) | |
return m('input', finalAttrs) | |
} | |
CheckboxInput.prototype.valueFromData = function(data, files, name) { | |
if (typeof data[name] == 'undefined') { | |
// A missing value means False because HTML form submission does not | |
// send results for unselected checkboxes. | |
return false | |
} | |
var value = data[name] | |
var values = {'true': true, 'false': false} | |
// Translate true and false strings to boolean values | |
if (is.String(value)) { | |
value = object.get(values, value.toLowerCase(), value) | |
} | |
return !!value | |
} | |
/** | |
* An HTML <select> widget. | |
* @constructor | |
* @extends {Widget} | |
* @param {Object=} kwargs | |
*/ | |
var Select = Widget.extend({ | |
constructor: function Select(kwargs) { | |
if (!(this instanceof Widget)) { return new Select(kwargs) } | |
kwargs = object.extend({choices: []}, kwargs) | |
Widget.call(this, kwargs) | |
this.choices = util.normaliseChoices(kwargs.choices) | |
} | |
, allowMultipleSelected: false | |
, validation: {event: 'onChange'} | |
}) | |
/** | |
* Renders the widget. | |
* @param {String} name the field name. | |
* @param selectedValue the value of an option which should be marked as | |
* selected, or null if no value is selected -- will be normalised to a String | |
* for comparison with choice values. | |
* @param {Object} [attrs] additional HTML attributes for the rendered widget. | |
* @param {Array} [choices] choices to be used when rendering the widget, in | |
* addition to those already held by the widget itself. | |
* @return a <select> element. | |
*/ | |
Select.prototype.render = function(name, selectedValue, kwargs) { | |
kwargs = object.extend({choices: []}, kwargs) | |
if (selectedValue === null) { | |
selectedValue = '' | |
} | |
var finalAttrs = this.buildAttrs(kwargs.attrs, {name: name}, kwargs.validation) | |
var options = this.renderOptions(kwargs.choices, [selectedValue]) | |
return m('select', finalAttrs, options) | |
} | |
Select.prototype.renderOptions = function(additionalChoices, selectedValues) { | |
var selectedValuesLookup = object.lookup(selectedValues) | |
var options = [] | |
var choices = this.choices.concat(util.normaliseChoices(additionalChoices)) | |
for (var i = 0, l = choices.length, choice; i < l; i++) { | |
choice = choices[i] | |
if (is.Array(choice[1])) { | |
var optgroupOptions = [] | |
var optgroupChoices = choice[1] | |
for (var j = 0, ll = optgroupChoices.length; j < ll; j++) { | |
optgroupOptions.push(this.renderOption(selectedValuesLookup, | |
optgroupChoices[j][0], | |
optgroupChoices[j][1])) | |
} | |
options.push(m('optgroup', {label: choice[0]}, optgroupOptions)) | |
} | |
else { | |
options.push(this.renderOption(selectedValuesLookup, | |
choice[0], | |
choice[1])) | |
} | |
} | |
return options | |
} | |
Select.prototype.renderOption = function(selectedValuesLookup, optValue, | |
optLabel) { | |
optValue = ''+optValue | |
var attrs = {value: optValue} | |
if (typeof selectedValuesLookup[optValue] != 'undefined') { | |
attrs['selected'] = 'selected' | |
if (!this.allowMultipleSelected) { | |
// Only allow for a single selection with this value | |
delete selectedValuesLookup[optValue] | |
} | |
} | |
return m('option', attrs, [optLabel]) | |
} | |
/** | |
* A <select> widget intended to be used with NullBooleanField. | |
* @constructor | |
* @extends {Select} | |
* @param {Object=} kwargs | |
*/ | |
var NullBooleanSelect = Select.extend({ | |
constructor: function NullBooleanSelect(kwargs) { | |
if (!(this instanceof Widget)) { return new NullBooleanSelect(kwargs) } | |
kwargs = kwargs || {} | |
// Set or overrride choices | |
kwargs.choices = [['1', 'Unknown'], ['2', 'Yes'], ['3', 'No']] | |
Select.call(this, kwargs) | |
} | |
}) | |
NullBooleanSelect.prototype.render = function(name, value, kwargs) { | |
if (value === true || value == '2') { | |
value = '2' | |
} | |
else if (value === false || value == '3') { | |
value = '3' | |
} | |
else { | |
value = '1' | |
} | |
return Select.prototype.render.call(this, name, value, kwargs) | |
} | |
NullBooleanSelect.prototype.valueFromData = function(data, files, name) { | |
var value = null | |
if (typeof data[name] != 'undefined') { | |
var dataValue = data[name] | |
if (dataValue === true || dataValue == 'True' || dataValue == 'true' || | |
dataValue == '2') { | |
value = true | |
} | |
else if (dataValue === false || dataValue == 'False' || | |
dataValue == 'false' || dataValue == '3') { | |
value = false | |
} | |
} | |
return value | |
} | |
/** | |
* An HTML <select> widget which allows multiple selections. | |
* @constructor | |
* @extends {Select} | |
* @param {Object=} kwargs | |
*/ | |
var SelectMultiple = Select.extend({ | |
constructor: function SelectMultiple(kwargs) { | |
if (!(this instanceof Widget)) { return new SelectMultiple(kwargs) } | |
Select.call(this, kwargs) | |
} | |
, allowMultipleSelected: true | |
, validation: {event: 'onChange'} | |
}) | |
/** | |
* Renders the widget. | |
* @param {String} name the field name. | |
* @param {Array} selectedValues the values of options which should be marked as | |
* selected, or null if no values are selected - these will be normalised to | |
* Strings for comparison with choice values. | |
* @param {Object} [kwargs] additional rendering options. | |
* @config {Object} [attrs] additional HTML attributes for the rendered widget. | |
* @config {Array} [choices] choices to be used when rendering the widget, in | |
* addition to those already held by the widget itself. | |
* @return a <select> element which allows multiple selections. | |
*/ | |
SelectMultiple.prototype.render = function(name, selectedValues, kwargs) { | |
kwargs = object.extend({choices: []}, kwargs) | |
if (selectedValues === null) { | |
selectedValues = [] | |
} | |
if (!is.Array(selectedValues)) { | |
selectedValues = [selectedValues] | |
} | |
var finalAttrs = this.buildAttrs(kwargs.attrs, {name: name, | |
multiple: 'multiple'}, kwargs.validation) | |
var options = this.renderOptions(kwargs.choices, selectedValues) | |
return m('select', finalAttrs, options) | |
} | |
/** | |
* Retrieves values for this widget from the given data. | |
* @param {Object} data form data. | |
* @param {Object} files file data. | |
* @param {String} name the field name to be used to retrieve data. | |
* @return {Array} values for this widget, or null if no values were provided. | |
*/ | |
SelectMultiple.prototype.valueFromData = function(data, files, name) { | |
if (object.hasOwn(data, name) && data[name] != null) { | |
return [].concat(data[name]) | |
} | |
return null | |
} | |
/** | |
* An object used by ChoiceFieldRenderer that represents a single | |
* <input>. | |
*/ | |
var ChoiceInput = SubWidget.extend({ | |
constructor: function ChoiceInput(name, value, attrs, validation, choice, index) { | |
this.name = name | |
this.value = value | |
this.attrs = attrs | |
this.validation = validation | |
this.choiceValue = ''+choice[0] | |
this.choiceLabel = ''+choice[1] | |
this.index = index | |
if (typeof this.attrs.id != 'undefined') { | |
this.attrs.id += '_' + this.index | |
} | |
} | |
, inputType: null // Subclasses must define this | |
}) | |
/** | |
* Renders a <label> enclosing the widget and its label text. | |
*/ | |
ChoiceInput.prototype.render = function() { | |
var labelAttrs = {} | |
if (this.idForLabel()) { | |
labelAttrs.htmlFor = this.idForLabel() | |
} | |
return m('label', labelAttrs, [this.tag(), ' ', this.choiceLabel]) | |
} | |
ChoiceInput.prototype.isChecked = function() { | |
return this.value === this.choiceValue | |
} | |
/** | |
* Renders the <input> portion of the widget. | |
*/ | |
ChoiceInput.prototype.tag = function() { | |
var finalAttrs = Widget.prototype.buildAttrs.call(this, {}, { | |
type: this.inputType, name: this.name, value: this.choiceValue | |
}, this.validation) | |
finalAttrs.checked = this.isChecked() | |
return m('input', finalAttrs) | |
} | |
ChoiceInput.prototype.idForLabel = function() { | |
return object.get(this.attrs, 'id', '') | |
} | |
var RadioChoiceInput = ChoiceInput.extend({ | |
constructor: function RadioChoiceInput(name, value, attrs, validation, choice, index) { | |
if (!(this instanceof RadioChoiceInput)) { | |
return new RadioChoiceInput(name, value, attrs, validation, choice, index) | |
} | |
ChoiceInput.call(this, name, value, attrs, validation, choice, index) | |
this.value = ''+this.value | |
} | |
, inputType: 'radio' | |
}) | |
var CheckboxChoiceInput = ChoiceInput.extend({ | |
constructor: function CheckboxChoiceInput(name, value, attrs, validation, choice, index) { | |
if (!(this instanceof CheckboxChoiceInput)) { | |
return new CheckboxChoiceInput(name, value, attrs, validation, choice, index) | |
} | |
if (!is.Array(value)) { | |
value = [value] | |
} | |
ChoiceInput.call(this, name, value, attrs, validation, choice, index) | |
for (var i = 0, l = this.value.length; i < l; i++) { | |
this.value[i] = ''+this.value[i] | |
} | |
} | |
, inputType: 'checkbox' | |
}) | |
CheckboxChoiceInput.prototype.isChecked = function() { | |
return this.value.indexOf(this.choiceValue) !== -1 | |
} | |
/** | |
* An object used by choice Selects to enable customisation of choice widgets. | |
* @constructor | |
* @param {string} name | |
* @param {string} value | |
* @param {Object} attrs | |
* @param {Object} validation | |
* @param {Array} choices | |
*/ | |
var ChoiceFieldRenderer = Concur.extend({ | |
constructor: function ChoiceFieldRenderer(name, value, attrs, validation, choices) { | |
if (!(this instanceof ChoiceFieldRenderer)) { | |
return new ChoiceFieldRenderer(name, value, attrs, validation, choices) | |
} | |
this.name = name | |
this.value = value | |
this.attrs = attrs | |
this.validation = validation | |
this.choices = choices | |
} | |
, choiceInputConstructor: null | |
}) | |
ChoiceFieldRenderer.prototype.choiceInputs = function() { | |
var inputs = [] | |
for (var i = 0, l = this.choices.length; i < l; i++) { | |
inputs.push(this.choiceInputConstructor(this.name, this.value, | |
object.extend({}, this.attrs), | |
this.validation, | |
this.choices[i], i)) | |
} | |
return inputs | |
} | |
ChoiceFieldRenderer.prototype.choiceInput = function(i) { | |
if (i >= this.choices.length) { | |
throw new Error('Index out of bounds: ' + i) | |
} | |
return this.choiceInputConstructor(this.name, this.value, | |
object.extend({}, this.attrs), | |
this.validation, | |
this.choices[i], i) | |
} | |
/** | |
* Outputs a <ul> for this set of choice fields. | |
* If an id was given to the field, it is applied to the <ul> (each item in the | |
* list will get an id of `$id_$i`). | |
*/ | |
ChoiceFieldRenderer.prototype.render = function() { | |
var id = object.get(this.attrs, 'id', null) | |
var items = [] | |
for (var i = 0, l = this.choices.length; i < l; i++) { | |
var choice = this.choices[i] | |
var choiceValue = choice[0] | |
var choiceLabel = choice[1] | |
if (is.Array(choiceLabel)) { | |
var attrsPlus = object.extend({}, this.attrs) | |
if (id) { | |
attrsPlus.id +='_' + i | |
} | |
var subRenderer = ChoiceFieldRenderer(this.name, this.value, | |
attrsPlus, this.validation, | |
choiceLabel) | |
subRenderer.choiceInputConstructor = this.choiceInputConstructor | |
items.push(m('li', [choiceValue, subRenderer.render()])) | |
} | |
else { | |
var w = this.choiceInputConstructor(this.name, this.value, | |
object.extend({}, this.attrs), | |
this.validation, | |
choice, i) | |
items.push(m('li', [w.render()])) | |
} | |
} | |
var listAttrs = {} | |
if (id) { | |
listAttrs.id = id | |
} | |
return m('ul', listAttrs, items) | |
} | |
var RadioFieldRenderer = ChoiceFieldRenderer.extend({ | |
constructor: function RadioFieldRenderer(name, value, attrs, validation, choices) { | |
if (!(this instanceof RadioFieldRenderer)) { | |
return new RadioFieldRenderer(name, value, attrs, validation, choices) | |
} | |
ChoiceFieldRenderer.apply(this, arguments) | |
} | |
, choiceInputConstructor: RadioChoiceInput | |
}) | |
var CheckboxFieldRenderer = ChoiceFieldRenderer.extend({ | |
constructor: function CheckboxFieldRenderer(name, value, attrs, validation, choices) { | |
if (!(this instanceof CheckboxFieldRenderer)) { | |
return new CheckboxFieldRenderer(name, value, attrs, validation, choices) | |
} | |
ChoiceFieldRenderer.apply(this, arguments) | |
} | |
, choiceInputConstructor: CheckboxChoiceInput | |
}) | |
var RendererMixin = Concur.extend({ | |
constructor: function RendererMixin(kwargs) { | |
kwargs = object.extend({renderer: null}, kwargs) | |
// Override the default renderer if we were passed one | |
if (kwargs.renderer !== null) { | |
this.renderer = kwargs.renderer | |
} | |
} | |
, _emptyValue: null | |
, validation: {event: 'onChange'} | |
}) | |
RendererMixin.prototype.subWidgets = function(name, value, kwargs, validation) { | |
return this.getRenderer(name, value, kwargs, validation).choiceInputs() | |
} | |
/** | |
* @return an instance of the renderer to be used to render this widget. | |
*/ | |
RendererMixin.prototype.getRenderer = function(name, value, kwargs) { | |
kwargs = object.extend({choices: [], validation: 'manual'}, kwargs) | |
if (value === null) { | |
value = this._emptyValue | |
} | |
var finalAttrs = this.buildAttrs(kwargs.attrs) | |
var choices = this.choices.concat(kwargs.choices) | |
return new this.renderer(name, value, finalAttrs, kwargs.validation, choices) | |
} | |
RendererMixin.prototype.render = function(name, value, kwargs) { | |
return this.getRenderer(name, value, kwargs).render() | |
} | |
/** | |
* Widgets using this RendererMixin are made of a collection of subwidgets, each | |
* with their own <label>, and distinct ID. | |
* The IDs are made distinct by y "_X" suffix, where X is the zero-based index | |
* of the choice field. Thus, the label for the main widget should reference the | |
* first subwidget, hence the "_0" suffix. | |
*/ | |
RendererMixin.prototype.idForLabel = function(id) { | |
if (id) { | |
id += '_0' | |
} | |
return id | |
} | |
/** | |
* Renders a single select as a list of <input type="radio"> elements. | |
* @constructor | |
* @extends {Select} | |
* @param {Object=} kwargs | |
*/ | |
var RadioSelect = Select.extend({ | |
__mixin__: RendererMixin | |
, constructor: function(kwargs) { | |
if (!(this instanceof RadioSelect)) { return new RadioSelect(kwargs) } | |
RendererMixin.call(this, kwargs) | |
Select.call(this, kwargs) | |
} | |
, renderer: RadioFieldRenderer | |
, _emptyValue: '' | |
}) | |
/** | |
* Multiple selections represented as a list of <input type="checkbox"> widgets. | |
* @constructor | |
* @extends {SelectMultiple} | |
* @param {Object=} kwargs | |
*/ | |
var CheckboxSelectMultiple = SelectMultiple.extend({ | |
__mixin__: RendererMixin | |
, constructor: function(kwargs) { | |
if (!(this instanceof CheckboxSelectMultiple)) { return new CheckboxSelectMultiple(kwargs) } | |
RendererMixin.call(this, kwargs) | |
SelectMultiple.call(this, kwargs) | |
} | |
, renderer: CheckboxFieldRenderer | |
, _emptyValue: [] | |
}) | |
/** | |
* A widget that is composed of multiple widgets. | |
* @constructor | |
* @extends {Widget} | |
* @param {Object=} kwargs | |
*/ | |
var MultiWidget = Widget.extend({ | |
constructor: function MultiWidget(widgets, kwargs) { | |
if (!(this instanceof Widget)) { return new MultiWidget(widgets, kwargs) } | |
this.widgets = [] | |
var needsMultipartForm = false | |
for (var i = 0, l = widgets.length; i < l; i++) { | |
var widget = widgets[i] instanceof Widget ? widgets[i] : new widgets[i]() | |
if (widget.needsMultipartForm) { | |
needsMultipartForm = true | |
} | |
this.widgets.push(widget) | |
} | |
this.needsMultipartForm = needsMultipartForm | |
Widget.call(this, kwargs) | |
} | |
}) | |
/** | |
* This method is different than other widgets', because it has to figure out | |
* how to split a single value for display in multiple widgets. | |
* | |
* If the given value is NOT a list, it will first be "decompressed" into a list | |
* before it is rendered by calling the MultiWidget#decompress function. | |
* | |
* Each value in the list is rendered with the corresponding widget -- the | |
* first value is rendered in the first widget, the second value is rendered in | |
* the second widget, and so on. | |
* | |
* @param {String} name the field name. | |
* @param {(Array.<*>|*)} value a list of values, or a normal value (e.g., a String that has | |
* been "compressed" from a list of values). | |
* @param {Object=} kwargs]additional rendering options | |
* @config {Object=} validation | |
* @return a rendered collection of widgets. | |
*/ | |
MultiWidget.prototype.render = function(name, value, kwargs) { | |
kwargs = object.extend({validation: 'manual'}, kwargs) | |
var validation = kwargs.validation | |
if (!(is.Array(value))) { | |
value = this.decompress(value) | |
} | |
var finalAttrs = this.buildAttrs(kwargs.attrs, {'data-newforms-field': name}) | |
var id = (typeof finalAttrs.id != 'undefined' ? finalAttrs.id : null) | |
var renderedWidgets = [] | |
for (var i = 0, l = this.widgets.length; i < l; i++) { | |
var widget = this.widgets[i] | |
var widgetValue = null | |
if (typeof value[i] != 'undefined') { | |
widgetValue = value[i] | |
} | |
if (id) { | |
finalAttrs.id = id + '_' + i | |
} | |
if (validation && validation !== 'manual') { | |
finalAttrs.onChange = validation.onChange | |
if (validation.event != 'onChange') { | |
finalAttrs[validation.event] = validation.eventHandler | |
} | |
} | |
renderedWidgets.push( | |
widget.render(name + '_' + i, widgetValue, {attrs: finalAttrs})) | |
} | |
return this.formatOutput(renderedWidgets) | |
} | |
MultiWidget.prototype.idForLabel = function(id) { | |
if (id) { | |
id += '_0' | |
} | |
return id | |
} | |
MultiWidget.prototype.valueFromData = function(data, files, name) { | |
var values = [] | |
for (var i = 0, l = this.widgets.length; i < l; i++) { | |
values[i] = this.widgets[i].valueFromData(data, files, name + '_' + i) | |
} | |
return values | |
} | |
/** | |
* Creates an element containing a given list of rendered widgets. | |
* | |
* This hook allows you to format the HTML design of the widgets, if needed. | |
* | |
* @param {Array} renderedWidgets a list of rendered widgets. | |
* @return a <div> containing the rendered widgets. | |
*/ | |
MultiWidget.prototype.formatOutput = function(renderedWidgets) { | |
return m('div', renderedWidgets) | |
} | |
/** | |
* Creates a list of decompressed values for the given compressed value. | |
* @abstract | |
* @param value a compressed value, which can be assumed to be valid, but not | |
* necessarily non-empty. | |
* @return a list of decompressed values for the given compressed value. | |
*/ | |
MultiWidget.prototype.decompress = function(value) { | |
throw new Error('MultiWidget subclasses must implement a decompress() method.') | |
} | |
/** | |
* Splits Date input into two <input type="text"> elements. | |
* @constructor | |
* @extends {MultiWidget} | |
* @param {Object=} kwargs | |
*/ | |
var SplitDateTimeWidget = MultiWidget.extend({ | |
constructor: function SplitDateTimeWidget(kwargs) { | |
if (!(this instanceof Widget)) { return new SplitDateTimeWidget(kwargs) } | |
kwargs = object.extend({dateFormat: null, timeFormat: null}, kwargs) | |
var widgets = [ | |
DateInput({attrs: kwargs.attrs, format: kwargs.dateFormat}) | |
, TimeInput({attrs: kwargs.attrs, format: kwargs.timeFormat}) | |
] | |
MultiWidget.call(this, widgets, kwargs.attrs) | |
} | |
}) | |
SplitDateTimeWidget.prototype.decompress = function(value) { | |
if (value) { | |
return [ | |
new Date(value.getFullYear(), value.getMonth(), value.getDate()) | |
, new Date(1900, 0, 1, value.getHours(), value.getMinutes(), value.getSeconds()) | |
] | |
} | |
return [null, null] | |
} | |
/** | |
* Splits Date input into two <input type="hidden"> elements. | |
* @constructor | |
* @extends {SplitDateTimeWidget} | |
* @param {Object=} kwargs | |
*/ | |
var SplitHiddenDateTimeWidget = SplitDateTimeWidget.extend({ | |
constructor: function SplitHiddenDateTimeWidget(kwargs) { | |
if (!(this instanceof Widget)) { return new SplitHiddenDateTimeWidget(kwargs) } | |
SplitDateTimeWidget.call(this, kwargs) | |
for (var i = 0, l = this.widgets.length; i < l; i++) { | |
this.widgets[i].inputType = 'hidden' | |
this.widgets[i].isHidden = true | |
} | |
} | |
, isHidden: true | |
}) | |
module.exports = { | |
SubWidget: SubWidget | |
, Widget: Widget | |
, Input: Input | |
, TextInput: TextInput | |
, NumberInput: NumberInput | |
, EmailInput: EmailInput | |
, URLInput: URLInput | |
, PasswordInput: PasswordInput | |
, HiddenInput: HiddenInput | |
, MultipleHiddenInput: MultipleHiddenInput | |
, FileInput: FileInput | |
, FILE_INPUT_CONTRADICTION: FILE_INPUT_CONTRADICTION | |
, ClearableFileInput: ClearableFileInput | |
, Textarea: Textarea | |
, DateInput: DateInput | |
, DateTimeInput: DateTimeInput | |
, TimeInput: TimeInput | |
, CheckboxInput: CheckboxInput | |
, Select: Select | |
, NullBooleanSelect: NullBooleanSelect | |
, SelectMultiple: SelectMultiple | |
, ChoiceInput: ChoiceInput | |
, RadioChoiceInput: RadioChoiceInput | |
, CheckboxChoiceInput: CheckboxChoiceInput | |
, ChoiceFieldRenderer: ChoiceFieldRenderer | |
, RendererMixin: RendererMixin | |
, RadioFieldRenderer: RadioFieldRenderer | |
, CheckboxFieldRenderer: CheckboxFieldRenderer | |
, RadioSelect: RadioSelect | |
, CheckboxSelectMultiple: CheckboxSelectMultiple | |
, MultiWidget: MultiWidget | |
, SplitDateTimeWidget: SplitDateTimeWidget | |
, SplitHiddenDateTimeWidget: SplitHiddenDateTimeWidget | |
} | |
},{"./env":1,"./formats":3,"./util":7,"Concur":9,"isomorph/format":12,"isomorph/is":13,"isomorph/object":14,"isomorph/time":15}],9:[function(_dereq_,module,exports){ | |
'use strict'; | |
var is = _dereq_('isomorph/is') | |
var object = _dereq_('isomorph/object') | |
/** | |
* Mixes in properties from one object to another. If the source object is a | |
* Function, its prototype is mixed in instead. | |
*/ | |
function mixin(dest, src) { | |
if (is.Function(src)) { | |
object.extend(dest, src.prototype) | |
} | |
else { | |
object.extend(dest, src) | |
} | |
} | |
/** | |
* Applies mixins specified as a __mixin__ property on the given properties | |
* object, returning an object containing the mixed in properties. | |
*/ | |
function applyMixins(properties) { | |
var mixins = properties.__mixin__ | |
if (!is.Array(mixins)) { | |
mixins = [mixins] | |
} | |
var mixedProperties = {} | |
for (var i = 0, l = mixins.length; i < l; i++) { | |
mixin(mixedProperties, mixins[i]) | |
} | |
delete properties.__mixin__ | |
return object.extend(mixedProperties, properties) | |
} | |
/** | |
* Inherits another constructor's prototype and sets its prototype and | |
* constructor properties in one fell swoop. | |
* | |
* If a child constructor is not provided via prototypeProps.constructor, | |
* a new constructor will be created. | |
*/ | |
function inheritFrom(parentConstructor, childConstructor, prototypeProps, constructorProps) { | |
// Create a child constructor if one wasn't given | |
if (childConstructor == null) { | |
childConstructor = function() { | |
parentConstructor.apply(this, arguments) | |
} | |
} | |
// Make sure the new prototype has the correct constructor set up | |
prototypeProps.constructor = childConstructor | |
// Base constructors should only have the properties they're defined with | |
if (parentConstructor !== Concur) { | |
// Inherit the parent's prototype | |
object.inherits(childConstructor, parentConstructor) | |
childConstructor.__super__ = parentConstructor.prototype | |
} | |
// Add prototype properties - this is why we took a copy of the child | |
// constructor reference in extend() - if a .constructor had been passed as a | |
// __mixin__ and overitten prototypeProps.constructor, these properties would | |
// be getting set on the mixed-in constructor's prototype. | |
object.extend(childConstructor.prototype, prototypeProps) | |
// Add constructor properties | |
object.extend(childConstructor, constructorProps) | |
return childConstructor | |
} | |
/** | |
* Namespace and dummy constructor for initial extension. | |
*/ | |
var Concur = module.exports = function() {} | |
/** | |
* Details of a coonstructor's inheritance chain - Concur just facilitates sugar | |
* so we don't include it in the initial chain. Arguably, Object.prototype could | |
* go here, but it's just not that interesting. | |
*/ | |
Concur.__mro__ = [] | |
/** | |
* Creates or uses a child constructor to inherit from the the call | |
* context, which is expected to be a constructor. | |
*/ | |
Concur.extend = function(prototypeProps, constructorProps) { | |
// Ensure we have prop objects to work with | |
prototypeProps = prototypeProps || {} | |
constructorProps = constructorProps || {} | |
// If the constructor being inherited from has a __meta__ function somewhere | |
// in its prototype chain, call it to customise prototype and constructor | |
// properties before they're used to set up the new constructor's prototype. | |
if (typeof this.prototype.__meta__ != 'undefined') { | |
this.prototype.__meta__(prototypeProps, constructorProps) | |
} | |
// Any child constructor passed in should take precedence - grab a reference | |
// to it befoer we apply any mixins. | |
var childConstructor = object.get(prototypeProps, 'constructor', null) | |
// If any mixins are specified, mix them into the property objects | |
if (object.hasOwn(prototypeProps, '__mixin__')) { | |
prototypeProps = applyMixins(prototypeProps) | |
} | |
if (object.hasOwn(constructorProps, '__mixin__')) { | |
constructorProps = applyMixins(constructorProps) | |
} | |
// Set up the new child constructor and its prototype | |
childConstructor = inheritFrom(this, | |
childConstructor, | |
prototypeProps, | |
constructorProps) | |
// Pass on the extend function for extension in turn | |
childConstructor.extend = this.extend | |
// Expose the inheritance chain for programmatic access | |
childConstructor.__mro__ = [childConstructor].concat(this.__mro__) | |
return childConstructor | |
} | |
},{"isomorph/is":13,"isomorph/object":14}],10:[function(_dereq_,module,exports){ | |
/*! http://mths.be/punycode v1.2.4 by @mathias */ | |
;(function(root) { | |
/** Detect free variables */ | |
var freeExports = typeof exports == 'object' && exports; | |
var freeModule = typeof module == 'object' && module && | |
module.exports == freeExports && module; | |
var freeGlobal = typeof global == 'object' && global; | |
if (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal) { | |
root = freeGlobal; | |
} | |
/** | |
* The `punycode` object. | |
* @name punycode | |
* @type Object | |
*/ | |
var punycode, | |
/** Highest positive signed 32-bit float value */ | |
maxInt = 2147483647, // aka. 0x7FFFFFFF or 2^31-1 | |
/** Bootstring parameters */ | |
base = 36, | |
tMin = 1, | |
tMax = 26, | |
skew = 38, | |
damp = 700, | |
initialBias = 72, | |
initialN = 128, // 0x80 | |
delimiter = '-', // '\x2D' | |
/** Regular expressions */ | |
regexPunycode = /^xn--/, | |
regexNonASCII = /[^ -~]/, // unprintable ASCII chars + non-ASCII chars | |
regexSeparators = /\x2E|\u3002|\uFF0E|\uFF61/g, // RFC 3490 separators | |
/** Error messages */ | |
errors = { | |
'overflow': 'Overflow: input needs wider integers to process', | |
'not-basic': 'Illegal input >= 0x80 (not a basic code point)', | |
'invalid-input': 'Invalid input' | |
}, | |
/** Convenience shortcuts */ | |
baseMinusTMin = base - tMin, | |
floor = Math.floor, | |
stringFromCharCode = String.fromCharCode, | |
/** Temporary variable */ | |
key; | |
/*--------------------------------------------------------------------------*/ | |
/** | |
* A generic error utility function. | |
* @private | |
* @param {String} type The error type. | |
* @returns {Error} Throws a `RangeError` with the applicable error message. | |
*/ | |
function error(type) { | |
throw RangeError(errors[type]); | |
} | |
/** | |
* A generic `Array#map` utility function. | |
* @private | |
* @param {Array} array The array to iterate over. | |
* @param {Function} callback The function that gets called for every array | |
* item. | |
* @returns {Array} A new array of values returned by the callback function. | |
*/ | |
function map(array, fn) { | |
var length = array.length; | |
while (length--) { | |
array[length] = fn(array[length]); | |
} | |
return array; | |
} | |
/** | |
* A simple `Array#map`-like wrapper to work with domain name strings. | |
* @private | |
* @param {String} domain The domain name. | |
* @param {Function} callback The function that gets called for every | |
* character. | |
* @returns {Array} A new string of characters returned by the callback | |
* function. | |
*/ | |
function mapDomain(string, fn) { | |
return map(string.split(regexSeparators), fn).join('.'); | |
} | |
/** | |
* Creates an array containing the numeric code points of each Unicode | |
* character in the string. While JavaScript uses UCS-2 internally, | |
* this function will convert a pair of surrogate halves (each of which | |
* UCS-2 exposes as separate characters) into a single code point, | |
* matching UTF-16. | |
* @see `punycode.ucs2.encode` | |
* @see <http://mathiasbynens.be/notes/javascript-encoding> | |
* @memberOf punycode.ucs2 | |
* @name decode | |
* @param {String} string The Unicode input string (UCS-2). | |
* @returns {Array} The new array of code points. | |
*/ | |
function ucs2decode(string) { | |
var output = [], | |
counter = 0, | |
length = string.length, | |
value, | |
extra; | |
while (counter < length) { | |
value = string.charCodeAt(counter++); | |
if (value >= 0xD800 && value <= 0xDBFF && counter < length) { | |
// high surrogate, and there is a next character | |
extra = string.charCodeAt(counter++); | |
if ((extra & 0xFC00) == 0xDC00) { // low surrogate | |
output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000); | |
} else { | |
// unmatched surrogate; only append this code unit, in case the next | |
// code unit is the high surrogate of a surrogate pair | |
output.push(value); | |
counter--; | |
} | |
} else { | |
output.push(value); | |
} | |
} | |
return output; | |
} | |
/** | |
* Creates a string based on an array of numeric code points. | |
* @see `punycode.ucs2.decode` | |
* @memberOf punycode.ucs2 | |
* @name encode | |
* @param {Array} codePoints The array of numeric code points. | |
* @returns {String} The new Unicode string (UCS-2). | |
*/ | |
function ucs2encode(array) { | |
return map(array, function(value) { | |
var output = ''; | |
if (value > 0xFFFF) { | |
value -= 0x10000; | |
output += stringFromCharCode(value >>> 10 & 0x3FF | 0xD800); | |
value = 0xDC00 | value & 0x3FF; | |
} | |
output += stringFromCharCode(value); | |
return output; | |
}).join(''); | |
} | |
/** | |
* Converts a basic code point into a digit/integer. | |
* @see `digitToBasic()` | |
* @private | |
* @param {Number} codePoint The basic numeric code point value. | |
* @returns {Number} The numeric value of a basic code point (for use in | |
* representing integers) in the range `0` to `base - 1`, or `base` if | |
* the code point does not represent a value. | |
*/ | |
function basicToDigit(codePoint) { | |
if (codePoint - 48 < 10) { | |
return codePoint - 22; | |
} | |
if (codePoint - 65 < 26) { | |
return codePoint - 65; | |
} | |
if (codePoint - 97 < 26) { | |
return codePoint - 97; | |
} | |
return base; | |
} | |
/** | |
* Converts a digit/integer into a basic code point. | |
* @see `basicToDigit()` | |
* @private | |
* @param {Number} digit The numeric value of a basic code point. | |
* @returns {Number} The basic code point whose value (when used for | |
* representing integers) is `digit`, which needs to be in the range | |
* `0` to `base - 1`. If `flag` is non-zero, the uppercase form is | |
* used; else, the lowercase form is used. The behavior is undefined | |
* if `flag` is non-zero and `digit` has no uppercase form. | |
*/ | |
function digitToBasic(digit, flag) { | |
// 0..25 map to ASCII a..z or A..Z | |
// 26..35 map to ASCII 0..9 | |
return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5); | |
} | |
/** | |
* Bias adaptation function as per section 3.4 of RFC 3492. | |
* http://tools.ietf.org/html/rfc3492#section-3.4 | |
* @private | |
*/ | |
function adapt(delta, numPoints, firstTime) { | |
var k = 0; | |
delta = firstTime ? floor(delta / damp) : delta >> 1; | |
delta += floor(delta / numPoints); | |
for (/* no initialization */; delta > baseMinusTMin * tMax >> 1; k += base) { | |
delta = floor(delta / baseMinusTMin); | |
} | |
return floor(k + (baseMinusTMin + 1) * delta / (delta + skew)); | |
} | |
/** | |
* Converts a Punycode string of ASCII-only symbols to a string of Unicode | |
* symbols. | |
* @memberOf punycode | |
* @param {String} input The Punycode string of ASCII-only symbols. | |
* @returns {String} The resulting string of Unicode symbols. | |
*/ | |
function decode(input) { | |
// Don't use UCS-2 | |
var output = [], | |
inputLength = input.length, | |
out, | |
i = 0, | |
n = initialN, | |
bias = initialBias, | |
basic, | |
j, | |
index, | |
oldi, | |
w, | |
k, | |
digit, | |
t, | |
/** Cached calculation results */ | |
baseMinusT; | |
// Handle the basic code points: let `basic` be the number of input code | |
// points before the last delimiter, or `0` if there is none, then copy | |
// the first basic code points to the output. | |
basic = input.lastIndexOf(delimiter); | |
if (basic < 0) { | |
basic = 0; | |
} | |
for (j = 0; j < basic; ++j) { | |
// if it's not a basic code point | |
if (input.charCodeAt(j) >= 0x80) { | |
error('not-basic'); | |
} | |
output.push(input.charCodeAt(j)); | |
} | |
// Main decoding loop: start just after the last delimiter if any basic code | |
// points were copied; start at the beginning otherwise. | |
for (index = basic > 0 ? basic + 1 : 0; index < inputLength; /* no final expression */) { | |
// `index` is the index of the next character to be consumed. | |
// Decode a generalized variable-length integer into `delta`, | |
// which gets added to `i`. The overflow checking is easier | |
// if we increase `i` as we go, then subtract off its starting | |
// value at the end to obtain `delta`. | |
for (oldi = i, w = 1, k = base; /* no condition */; k += base) { | |
if (index >= inputLength) { | |
error('invalid-input'); | |
} | |
digit = basicToDigit(input.charCodeAt(index++)); | |
if (digit >= base || digit > floor((maxInt - i) / w)) { | |
error('overflow'); | |
} | |
i += digit * w; | |
t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias); | |
if (digit < t) { | |
break; | |
} | |
baseMinusT = base - t; | |
if (w > floor(maxInt / baseMinusT)) { | |
error('overflow'); | |
} | |
w *= baseMinusT; | |
} | |
out = output.length + 1; | |
bias = adapt(i - oldi, out, oldi == 0); | |
// `i` was supposed to wrap around from `out` to `0`, | |
// incrementing `n` each time, so we'll fix that now: | |
if (floor(i / out) > maxInt - n) { | |
error('overflow'); | |
} | |
n += floor(i / out); | |
i %= out; | |
// Insert `n` at position `i` of the output | |
output.splice(i++, 0, n); | |
} | |
return ucs2encode(output); | |
} | |
/** | |
* Converts a string of Unicode symbols to a Punycode string of ASCII-only | |
* symbols. | |
* @memberOf punycode | |
* @param {String} input The string of Unicode symbols. | |
* @returns {String} The resulting Punycode string of ASCII-only symbols. | |
*/ | |
function encode(input) { | |
var n, | |
delta, | |
handledCPCount, | |
basicLength, | |
bias, | |
j, | |
m, | |
q, | |
k, | |
t, | |
currentValue, | |
output = [], | |
/** `inputLength` will hold the number of code points in `input`. */ | |
inputLength, | |
/** Cached calculation results */ | |
handledCPCountPlusOne, | |
baseMinusT, | |
qMinusT; | |
// Convert the input in UCS-2 to Unicode | |
input = ucs2decode(input); | |
// Cache the length | |
inputLength = input.length; | |
// Initialize the state | |
n = initialN; | |
delta = 0; | |
bias = initialBias; | |
// Handle the basic code points | |
for (j = 0; j < inputLength; ++j) { | |
currentValue = input[j]; | |
if (currentValue < 0x80) { | |
output.push(stringFromCharCode(currentValue)); | |
} | |
} | |
handledCPCount = basicLength = output.length; | |
// `handledCPCount` is the number of code points that have been handled; | |
// `basicLength` is the number of basic code points. | |
// Finish the basic string - if it is not empty - with a delimiter | |
if (basicLength) { | |
output.push(delimiter); | |
} | |
// Main encoding loop: | |
while (handledCPCount < inputLength) { | |
// All non-basic code points < n have been handled already. Find the next | |
// larger one: | |
for (m = maxInt, j = 0; j < inputLength; ++j) { | |
currentValue = input[j]; | |
if (currentValue >= n && currentValue < m) { | |
m = currentValue; | |
} | |
} | |
// Increase `delta` enough to advance the decoder's <n,i> state to <m,0>, | |
// but guard against overflow | |
handledCPCountPlusOne = handledCPCount + 1; | |
if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) { | |
error('overflow'); | |
} | |
delta += (m - n) * handledCPCountPlusOne; | |
n = m; | |
for (j = 0; j < inputLength; ++j) { | |
currentValue = input[j]; | |
if (currentValue < n && ++delta > maxInt) { | |
error('overflow'); | |
} | |
if (currentValue == n) { | |
// Represent delta as a generalized variable-length integer | |
for (q = delta, k = base; /* no condition */; k += base) { | |
t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias); | |
if (q < t) { | |
break; | |
} | |
qMinusT = q - t; | |
baseMinusT = base - t; | |
output.push( | |
stringFromCharCode(digitToBasic(t + qMinusT % baseMinusT, 0)) | |
); | |
q = floor(qMinusT / baseMinusT); | |
} | |
output.push(stringFromCharCode(digitToBasic(q, 0))); | |
bias = adapt(delta, handledCPCountPlusOne, handledCPCount == basicLength); | |
delta = 0; | |
++handledCPCount; | |
} | |
} | |
++delta; | |
++n; | |
} | |
return output.join(''); | |
} | |
/** | |
* Converts a Punycode string representing a domain name to Unicode. Only the | |
* Punycoded parts of the domain name will be converted, i.e. it doesn't | |
* matter if you call it on a string that has already been converted to | |
* Unicode. | |
* @memberOf punycode | |
* @param {String} domain The Punycode domain name to convert to Unicode. | |
* @returns {String} The Unicode representation of the given Punycode | |
* string. | |
*/ | |
function toUnicode(domain) { | |
return mapDomain(domain, function(string) { | |
return regexPunycode.test(string) | |
? decode(string.slice(4).toLowerCase()) | |
: string; | |
}); | |
} | |
/** | |
* Converts a Unicode string representing a domain name to Punycode. Only the | |
* non-ASCII parts of the domain name will be converted, i.e. it doesn't | |
* matter if you call it with a domain that's already in ASCII. | |
* @memberOf punycode | |
* @param {String} domain The domain name to convert, as a Unicode string. | |
* @returns {String} The Punycode representation of the given domain name. | |
*/ | |
function toASCII(domain) { | |
return mapDomain(domain, function(string) { | |
return regexNonASCII.test(string) | |
? 'xn--' + encode(string) | |
: string; | |
}); | |
} | |
/*--------------------------------------------------------------------------*/ | |
/** Define the public API */ | |
punycode = { | |
/** | |
* A string representing the current Punycode.js version number. | |
* @memberOf punycode | |
* @type String | |
*/ | |
'version': '1.2.4', | |
/** | |
* An object of methods to convert from JavaScript's internal character | |
* representation (UCS-2) to Unicode code points, and back. | |
* @see <http://mathiasbynens.be/notes/javascript-encoding> | |
* @memberOf punycode | |
* @type Object | |
*/ | |
'ucs2': { | |
'decode': ucs2decode, | |
'encode': ucs2encode | |
}, | |
'decode': decode, | |
'encode': encode, | |
'toASCII': toASCII, | |
'toUnicode': toUnicode | |
}; | |
/** Expose `punycode` */ | |
// Some AMD build optimizers, like r.js, check for specific condition patterns | |
// like the following: | |
if ( | |
typeof define == 'function' && | |
typeof define.amd == 'object' && | |
define.amd | |
) { | |
define('punycode', function() { | |
return punycode; | |
}); | |
} else if (freeExports && !freeExports.nodeType) { | |
if (freeModule) { // in Node.js or RingoJS v0.8.0+ | |
freeModule.exports = punycode; | |
} else { // in Narwhal or RingoJS v0.7.0- | |
for (key in punycode) { | |
punycode.hasOwnProperty(key) && (freeExports[key] = punycode[key]); | |
} | |
} | |
} else { // in Rhino or a web browser | |
root.punycode = punycode; | |
} | |
}(this)); | |
},{}],11:[function(_dereq_,module,exports){ | |
'use strict'; | |
var is = _dereq_('./is') | |
/* This file is part of OWL JavaScript Utilities. | |
OWL JavaScript Utilities is free software: you can redistribute it and/or | |
modify it under the terms of the GNU Lesser General Public License | |
as published by the Free Software Foundation, either version 3 of | |
the License, or (at your option) any later version. | |
OWL JavaScript Utilities is distributed in the hope that it will be useful, | |
but WITHOUT ANY WARRANTY; without even the implied warranty of | |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
GNU Lesser General Public License for more details. | |
You should have received a copy of the GNU Lesser General Public | |
License along with OWL JavaScript Utilities. If not, see | |
<http://www.gnu.org/licenses/>. | |
*/ | |
// Re-usable constructor function used by clone() | |
function Clone() {} | |
// Clone objects, skip other types | |
function clone(target) { | |
if (typeof target == 'object') { | |
Clone.prototype = target | |
return new Clone() | |
} | |
else { | |
return target | |
} | |
} | |
// Shallow Copy | |
function copy(target) { | |
if (typeof target != 'object') { | |
// Non-objects have value semantics, so target is already a copy | |
return target | |
} | |
else { | |
var value = target.valueOf() | |
if (target != value) { | |
// the object is a standard object wrapper for a native type, say String. | |
// we can make a copy by instantiating a new object around the value. | |
return new target.constructor(value) | |
} | |
else { | |
var c, property | |
// We have a normal object. If possible, we'll clone the original's | |
// prototype (not the original) to get an empty object with the same | |
// prototype chain as the original. If just copy the instance properties. | |
// Otherwise, we have to copy the whole thing, property-by-property. | |
if (target instanceof target.constructor && target.constructor !== Object) { | |
c = clone(target.constructor.prototype) | |
// Give the copy all the instance properties of target. It has the same | |
// prototype as target, so inherited properties are already there. | |
for (property in target) { | |
if (target.hasOwnProperty(property)) { | |
c[property] = target[property] | |
} | |
} | |
} | |
else { | |
c = {} | |
for (property in target) { | |
c[property] = target[property] | |
} | |
} | |
return c | |
} | |
} | |
} | |
// Deep Copy | |
var deepCopiers = [] | |
function DeepCopier(config) { | |
for (var key in config) { | |
this[key] = config[key] | |
} | |
} | |
DeepCopier.prototype = { | |
constructor: DeepCopier | |
// Determines if this DeepCopier can handle the given object. | |
, canCopy: function(source) { return false } | |
// Starts the deep copying process by creating the copy object. You can | |
// initialize any properties you want, but you can't call recursively into the | |
// DeepCopyAlgorithm. | |
, create: function(source) {} | |
// Completes the deep copy of the source object by populating any properties | |
// that need to be recursively deep copied. You can do this by using the | |
// provided deepCopyAlgorithm instance's deepCopy() method. This will handle | |
// cyclic references for objects already deepCopied, including the source | |
// object itself. The "result" passed in is the object returned from create(). | |
, populate: function(deepCopyAlgorithm, source, result) {} | |
} | |
function DeepCopyAlgorithm() { | |
// copiedObjects keeps track of objects already copied by this deepCopy | |
// operation, so we can correctly handle cyclic references. | |
this.copiedObjects = [] | |
var thisPass = this | |
this.recursiveDeepCopy = function(source) { | |
return thisPass.deepCopy(source) | |
} | |
this.depth = 0 | |
} | |
DeepCopyAlgorithm.prototype = { | |
constructor: DeepCopyAlgorithm | |
, maxDepth: 256 | |
// Add an object to the cache. No attempt is made to filter duplicates; we | |
// always check getCachedResult() before calling it. | |
, cacheResult: function(source, result) { | |
this.copiedObjects.push([source, result]) | |
} | |
// Returns the cached copy of a given object, or undefined if it's an object | |
// we haven't seen before. | |
, getCachedResult: function(source) { | |
var copiedObjects = this.copiedObjects | |
var length = copiedObjects.length | |
for ( var i=0; i<length; i++ ) { | |
if ( copiedObjects[i][0] === source ) { | |
return copiedObjects[i][1] | |
} | |
} | |
return undefined | |
} | |
// deepCopy handles the simple cases itself: non-objects and object's we've | |
// seen before. For complex cases, it first identifies an appropriate | |
// DeepCopier, then calls applyDeepCopier() to delegate the details of copying | |
// the object to that DeepCopier. | |
, deepCopy: function(source) { | |
// null is a special case: it's the only value of type 'object' without | |
// properties. | |
if (source === null) { return null } | |
// All non-objects use value semantics and don't need explict copying | |
if (typeof source != 'object') { return source } | |
var cachedResult = this.getCachedResult(source) | |
// We've already seen this object during this deep copy operation so can | |
// immediately return the result. This preserves the cyclic reference | |
// structure and protects us from infinite recursion. | |
if (cachedResult) { return cachedResult } | |
// Objects may need special handling depending on their class. There is a | |
// class of handlers call "DeepCopiers" that know how to copy certain | |
// objects. There is also a final, generic deep copier that can handle any | |
// object. | |
for (var i=0; i<deepCopiers.length; i++) { | |
var deepCopier = deepCopiers[i] | |
if (deepCopier.canCopy(source)) { | |
return this.applyDeepCopier(deepCopier, source) | |
} | |
} | |
// The generic copier can handle anything, so we should never reach this | |
// line. | |
throw new Error('no DeepCopier is able to copy ' + source) | |
} | |
// Once we've identified which DeepCopier to use, we need to call it in a | |
// very particular order: create, cache, populate.This is the key to detecting | |
// cycles. We also keep track of recursion depth when calling the potentially | |
// recursive populate(): this is a fail-fast to prevent an infinite loop from | |
// consuming all available memory and crashing or slowing down the browser. | |
, applyDeepCopier: function(deepCopier, source) { | |
// Start by creating a stub object that represents the copy. | |
var result = deepCopier.create(source) | |
// We now know the deep copy of source should always be result, so if we | |
// encounter source again during this deep copy we can immediately use | |
// result instead of descending into it recursively. | |
this.cacheResult(source, result) | |
// Only DeepCopier.populate() can recursively deep copy. o, to keep track | |
// of recursion depth, we increment this shared counter before calling it, | |
// and decrement it afterwards. | |
this.depth++ | |
if (this.depth > this.maxDepth) { | |
throw new Error("Exceeded max recursion depth in deep copy.") | |
} | |
// It's now safe to let the deepCopier recursively deep copy its properties | |
deepCopier.populate(this.recursiveDeepCopy, source, result) | |
this.depth-- | |
return result | |
} | |
} | |
// Entry point for deep copy. | |
// source is the object to be deep copied. | |
// maxDepth is an optional recursion limit. Defaults to 256. | |
function deepCopy(source, maxDepth) { | |
var deepCopyAlgorithm = new DeepCopyAlgorithm() | |
if (maxDepth) { | |
deepCopyAlgorithm.maxDepth = maxDepth | |
} | |
return deepCopyAlgorithm.deepCopy(source) | |
} | |
// Publicly expose the DeepCopier class | |
deepCopy.DeepCopier = DeepCopier | |
// Publicly expose the list of deepCopiers | |
deepCopy.deepCopiers = deepCopiers | |
// Make deepCopy() extensible by allowing others to register their own custom | |
// DeepCopiers. | |
deepCopy.register = function(deepCopier) { | |
if (!(deepCopier instanceof DeepCopier)) { | |
deepCopier = new DeepCopier(deepCopier) | |
} | |
deepCopiers.unshift(deepCopier) | |
} | |
// Generic Object copier | |
// The ultimate fallback DeepCopier, which tries to handle the generic case. | |
// This should work for base Objects and many user-defined classes. | |
deepCopy.register({ | |
canCopy: function(source) { return true } | |
, create: function(source) { | |
if (source instanceof source.constructor) { | |
return clone(source.constructor.prototype) | |
} | |
else { | |
return {} | |
} | |
} | |
, populate: function(deepCopy, source, result) { | |
for (var key in source) { | |
if (source.hasOwnProperty(key)) { | |
result[key] = deepCopy(source[key]) | |
} | |
} | |
return result | |
} | |
}) | |
// Array copier | |
deepCopy.register({ | |
canCopy: function(source) { | |
return is.Array(source) | |
} | |
, create: function(source) { | |
return new source.constructor() | |
} | |
, populate: function(deepCopy, source, result) { | |
for (var i = 0; i < source.length; i++) { | |
result.push(deepCopy(source[i])) | |
} | |
return result | |
} | |
}) | |
// Date copier | |
deepCopy.register({ | |
canCopy: function(source) { | |
return is.Date(source) | |
} | |
, create: function(source) { | |
return new Date(source) | |
} | |
}) | |
// RegExp copier | |
deepCopy.register({ | |
canCopy: function(source) { | |
return is.RegExp(source) | |
} | |
, create: function(source) { | |
return source | |
} | |
}) | |
module.exports = { | |
DeepCopyAlgorithm: DeepCopyAlgorithm | |
, copy: copy | |
, clone: clone | |
, deepCopy: deepCopy | |
} | |
},{"./is":13}],12:[function(_dereq_,module,exports){ | |
'use strict'; | |
var slice = Array.prototype.slice | |
, formatRegExp = /%[%s]/g | |
, formatObjRegExp = /({{?)(\w+)}/g | |
/** | |
* Replaces %s placeholders in a string with positional arguments. | |
*/ | |
function format(s) { | |
return formatArr(s, slice.call(arguments, 1)) | |
} | |
/** | |
* Replaces %s placeholders in a string with array contents. | |
*/ | |
function formatArr(s, a) { | |
var i = 0 | |
return s.replace(formatRegExp, function(m) { return m == '%%' ? '%' : a[i++] }) | |
} | |
/** | |
* Replaces {propertyName} placeholders in a string with object properties. | |
*/ | |
function formatObj(s, o) { | |
return s.replace(formatObjRegExp, function(m, b, p) { return b.length == 2 ? m.slice(1) : o[p] }) | |
} | |
var units = 'kMGTPEZY' | |
, stripDecimals = /\.00$|0$/ | |
/** | |
* Formats bytes as a file size with the appropriately scaled units. | |
*/ | |
function fileSize(bytes, threshold) { | |
threshold = Math.min(threshold || 768, 1024) | |
var i = -1 | |
, unit = 'bytes' | |
, size = bytes | |
while (size > threshold && i < units.length) { | |
size = size / 1024 | |
i++ | |
} | |
if (i > -1) { | |
unit = units.charAt(i) + 'B' | |
} | |
return size.toFixed(2).replace(stripDecimals, '') + ' ' + unit | |
} | |
module.exports = { | |
format: format | |
, formatArr: formatArr | |
, formatObj: formatObj | |
, fileSize: fileSize | |
} | |
},{}],13:[function(_dereq_,module,exports){ | |
'use strict'; | |
var toString = Object.prototype.toString | |
// Type checks | |
function isArray(o) { | |
return toString.call(o) == '[object Array]' | |
} | |
function isBoolean(o) { | |
return toString.call(o) == '[object Boolean]' | |
} | |
function isDate(o) { | |
return toString.call(o) == '[object Date]' | |
} | |
function isError(o) { | |
return toString.call(o) == '[object Error]' | |
} | |
function isFunction(o) { | |
return toString.call(o) == '[object Function]' | |
} | |
function isNumber(o) { | |
return toString.call(o) == '[object Number]' | |
} | |
function isObject(o) { | |
return toString.call(o) == '[object Object]' | |
} | |
function isRegExp(o) { | |
return toString.call(o) == '[object RegExp]' | |
} | |
function isString(o) { | |
return toString.call(o) == '[object String]' | |
} | |
// Content checks | |
function isEmpty(o) { | |
/* jshint ignore:start */ | |
for (var prop in o) { | |
return false | |
} | |
/* jshint ignore:end */ | |
return true | |
} | |
module.exports = { | |
Array: isArray | |
, Boolean: isBoolean | |
, Date: isDate | |
, Empty: isEmpty | |
, Error: isError | |
, Function: isFunction | |
, NaN: isNaN | |
, Number: isNumber | |
, Object: isObject | |
, RegExp: isRegExp | |
, String: isString | |
} | |
},{}],14:[function(_dereq_,module,exports){ | |
'use strict'; | |
/** | |
* Wraps Object.prototype.hasOwnProperty() so it can be called with an object | |
* and property name. | |
*/ | |
var hasOwn = (function() { | |
var hasOwnProperty = Object.prototype.hasOwnProperty | |
return function(obj, prop) { return hasOwnProperty.call(obj, prop) } | |
})() | |
/** | |
* Copies own properties from any given objects to a destination object. | |
*/ | |
function extend(dest) { | |
for (var i = 1, l = arguments.length, src; i < l; i++) { | |
src = arguments[i] | |
if (src) { | |
for (var prop in src) { | |
if (hasOwn(src, prop)) { | |
dest[prop] = src[prop] | |
} | |
} | |
} | |
} | |
return dest | |
} | |
/** | |
* Makes a constructor inherit another constructor's prototype without | |
* having to actually use the constructor. | |
*/ | |
function inherits(childConstructor, parentConstructor) { | |
var F = function() {} | |
F.prototype = parentConstructor.prototype | |
childConstructor.prototype = new F() | |
childConstructor.prototype.constructor = childConstructor | |
return childConstructor | |
} | |
/** | |
* Creates an Array of [property, value] pairs from an Object. | |
*/ | |
function items(obj) { | |
var items_ = [] | |
for (var prop in obj) { | |
if (hasOwn(obj, prop)) { | |
items_.push([prop, obj[prop]]) | |
} | |
} | |
return items_ | |
} | |
/** | |
* Creates an Object from an Array of [property, value] pairs. | |
*/ | |
function fromItems(items) { | |
var obj = {} | |
for (var i = 0, l = items.length, item; i < l; i++) { | |
item = items[i] | |
obj[item[0]] = item[1] | |
} | |
return obj | |
} | |
/** | |
* Creates a lookup Object from an Array, coercing each item to a String. | |
*/ | |
function lookup(arr) { | |
var obj = {} | |
for (var i = 0, l = arr.length; i < l; i++) { | |
obj[''+arr[i]] = true | |
} | |
return obj | |
} | |
/** | |
* If the given object has the given property, returns its value, otherwise | |
* returns the given default value. | |
*/ | |
function get(obj, prop, defaultValue) { | |
return (hasOwn(obj, prop) ? obj[prop] : defaultValue) | |
} | |
/** | |
* Deletes and returns an own property from an object, optionally returning a | |
* default value if the object didn't have theproperty. | |
* @throws if given an object which is null (or undefined), or if the property | |
* doesn't exist and there was no defaultValue given. | |
*/ | |
function pop(obj, prop, defaultValue) { | |
if (obj == null) { | |
throw new Error('popProp was given ' + obj) | |
} | |
if (hasOwn(obj, prop)) { | |
var value = obj[prop] | |
delete obj[prop] | |
return value | |
} | |
else if (arguments.length == 2) { | |
throw new Error("popProp was given an object which didn't have an own '" + | |
prop + "' property, without a default value to return") | |
} | |
return defaultValue | |
} | |
/** | |
* If the prop is in the object, return its value. If not, set the prop to | |
* defaultValue and return defaultValue. | |
*/ | |
function setDefault(obj, prop, defaultValue) { | |
if (obj == null) { | |
throw new Error('setDefault was given ' + obj) | |
} | |
defaultValue = defaultValue || null | |
if (hasOwn(obj, prop)) { | |
return obj[prop] | |
} | |
else { | |
obj[prop] = defaultValue | |
return defaultValue | |
} | |
} | |
module.exports = { | |
hasOwn: hasOwn | |
, extend: extend | |
, inherits: inherits | |
, items: items | |
, fromItems: fromItems | |
, lookup: lookup | |
, get: get | |
, pop: pop | |
, setDefault: setDefault | |
} | |
},{}],15:[function(_dereq_,module,exports){ | |
'use strict'; | |
var is = _dereq_('./is') | |
/** | |
* Pads a number with a leading zero if necessary. | |
*/ | |
function pad(number) { | |
return (number < 10 ? '0' + number : number) | |
} | |
/** | |
* Returns the index of item in list, or -1 if it's not in list. | |
*/ | |
function indexOf(item, list) { | |
for (var i = 0, l = list.length; i < l; i++) { | |
if (item === list[i]) { | |
return i | |
} | |
} | |
return -1 | |
} | |
/** | |
* Maps directive codes to regular expression patterns which will capture the | |
* data the directive corresponds to, or in the case of locale-dependent | |
* directives, a function which takes a locale and generates a regular | |
* expression pattern. | |
*/ | |
var parserDirectives = { | |
// Locale's abbreviated month name | |
'b': function(l) { return '(' + l.b.join('|') + ')' } | |
// Locale's full month name | |
, 'B': function(l) { return '(' + l.B.join('|') + ')' } | |
// Locale's equivalent of either AM or PM. | |
, 'p': function(l) { return '(' + l.AM + '|' + l.PM + ')' } | |
, 'd': '(\\d\\d?)' // Day of the month as a decimal number [01,31] | |
, 'H': '(\\d\\d?)' // Hour (24-hour clock) as a decimal number [00,23] | |
, 'I': '(\\d\\d?)' // Hour (12-hour clock) as a decimal number [01,12] | |
, 'm': '(\\d\\d?)' // Month as a decimal number [01,12] | |
, 'M': '(\\d\\d?)' // Minute as a decimal number [00,59] | |
, 'S': '(\\d\\d?)' // Second as a decimal number [00,59] | |
, 'y': '(\\d\\d?)' // Year without century as a decimal number [00,99] | |
, 'Y': '(\\d{4})' // Year with century as a decimal number | |
, '%': '%' // A literal '%' character | |
} | |
/** | |
* Maps directive codes to functions which take the date to be formatted and | |
* locale details (if required), returning an appropriate formatted value. | |
*/ | |
var formatterDirectives = { | |
'a': function(d, l) { return l.a[d.getDay()] } | |
, 'A': function(d, l) { return l.A[d.getDay()] } | |
, 'b': function(d, l) { return l.b[d.getMonth()] } | |
, 'B': function(d, l) { return l.B[d.getMonth()] } | |
, 'd': function(d) { return pad(d.getDate(), 2) } | |
, 'H': function(d) { return pad(d.getHours(), 2) } | |
, 'M': function(d) { return pad(d.getMinutes(), 2) } | |
, 'm': function(d) { return pad(d.getMonth() + 1, 2) } | |
, 'S': function(d) { return pad(d.getSeconds(), 2) } | |
, 'w': function(d) { return d.getDay() } | |
, 'Y': function(d) { return d.getFullYear() } | |
, '%': function(d) { return '%' } | |
} | |
/** Test for hanging percentage symbols. */ | |
var strftimeFormatCheck = /[^%]%$/ | |
/** | |
* A partial implementation of strptime which parses time details from a string, | |
* based on a format string. | |
* @param {String} format | |
* @param {Object} locale | |
*/ | |
function TimeParser(format, locale) { | |
this.format = format | |
this.locale = locale | |
var cachedPattern = TimeParser._cache[locale.name + '|' + format] | |
if (cachedPattern !== undefined) { | |
this.re = cachedPattern[0] | |
this.matchOrder = cachedPattern[1] | |
} | |
else { | |
this.compilePattern() | |
} | |
} | |
/** | |
* Caches RegExps and match orders generated per locale/format string combo. | |
*/ | |
TimeParser._cache = {} | |
TimeParser.prototype.compilePattern = function() { | |
// Normalise whitespace before further processing | |
var format = this.format.split(/(?:\s|\t|\n)+/).join(' ') | |
, pattern = [] | |
, matchOrder = [] | |
, c | |
, directive | |
for (var i = 0, l = format.length; i < l; i++) { | |
c = format.charAt(i) | |
if (c != '%') { | |
if (c === ' ') { | |
pattern.push(' +') | |
} | |
else { | |
pattern.push(c) | |
} | |
continue | |
} | |
if (i == l - 1) { | |
throw new Error('strptime format ends with raw %') | |
} | |
c = format.charAt(++i) | |
directive = parserDirectives[c] | |
if (directive === undefined) { | |
throw new Error('strptime format contains an unknown directive: %' + c) | |
} | |
else if (is.Function(directive)) { | |
pattern.push(directive(this.locale)) | |
} | |
else { | |
pattern.push(directive) | |
} | |
if (c != '%') { | |
matchOrder.push(c) | |
} | |
} | |
this.re = new RegExp('^' + pattern.join('') + '$') | |
this.matchOrder = matchOrder | |
TimeParser._cache[this.locale.name + '|' + this.format] = [this.re, matchOrder] | |
} | |
/** | |
* Attempts to extract date and time details from the given input. | |
* @param {string} input | |
* @return {Array.<number>} | |
*/ | |
TimeParser.prototype.parse = function(input) { | |
var matches = this.re.exec(input) | |
if (matches === null) { | |
throw new Error('Time data did not match format: data=' + input + | |
', format=' + this.format) | |
} | |
// Default values for when more accurate values cannot be inferred | |
var time = [1900, 1, 1, 0, 0, 0] | |
// Matched time data, keyed by directive code | |
, data = {} | |
for (var i = 1, l = matches.length; i < l; i++) { | |
data[this.matchOrder[i - 1]] = matches[i] | |
} | |
// Extract year | |
if (data.hasOwnProperty('Y')) { | |
time[0] = parseInt(data.Y, 10) | |
} | |
else if (data.hasOwnProperty('y')) { | |
var year = parseInt(data.y, 10) | |
if (year < 68) { | |
year = 2000 + year | |
} | |
else if (year < 100) { | |
year = 1900 + year | |
} | |
time[0] = year | |
} | |
// Extract month | |
if (data.hasOwnProperty('m')) { | |
var month = parseInt(data.m, 10) | |
if (month < 1 || month > 12) { | |
throw new Error('Month is out of range: ' + month) | |
} | |
time[1] = month | |
} | |
else if (data.hasOwnProperty('B')) { | |
time[1] = indexOf(data.B, this.locale.B) + 1 | |
} | |
else if (data.hasOwnProperty('b')) { | |
time[1] = indexOf(data.b, this.locale.b) + 1 | |
} | |
// Extract day of month | |
if (data.hasOwnProperty('d')) { | |
var day = parseInt(data.d, 10) | |
if (day < 1 || day > 31) { | |
throw new Error('Day is out of range: ' + day) | |
} | |
time[2] = day | |
} | |
// Extract hour | |
var hour | |
if (data.hasOwnProperty('H')) { | |
hour = parseInt(data.H, 10) | |
if (hour > 23) { | |
throw new Error('Hour is out of range: ' + hour) | |
} | |
time[3] = hour | |
} | |
else if (data.hasOwnProperty('I')) { | |
hour = parseInt(data.I, 10) | |
if (hour < 1 || hour > 12) { | |
throw new Error('Hour is out of range: ' + hour) | |
} | |
// If we don't get any more information, we'll assume this time is | |
// a.m. - 12 a.m. is midnight. | |
if (hour == 12) { | |
hour = 0 | |
} | |
time[3] = hour | |
if (data.hasOwnProperty('p')) { | |
if (data.p == this.locale.PM) { | |
// We've already handled the midnight special case, so it's | |
// safe to bump the time by 12 hours without further checks. | |
time[3] = time[3] + 12 | |
} | |
} | |
} | |
// Extract minute | |
if (data.hasOwnProperty('M')) { | |
var minute = parseInt(data.M, 10) | |
if (minute > 59) { | |
throw new Error('Minute is out of range: ' + minute) | |
} | |
time[4] = minute | |
} | |
// Extract seconds | |
if (data.hasOwnProperty('S')) { | |
var second = parseInt(data.S, 10) | |
if (second > 59) { | |
throw new Error('Second is out of range: ' + second) | |
} | |
time[5] = second | |
} | |
// Validate day of month | |
day = time[2], month = time[1], year = time[0] | |
if (((month == 4 || month == 6 || month == 9 || month == 11) && | |
day > 30) || | |
(month == 2 && day > ((year % 4 === 0 && year % 100 !== 0 || | |
year % 400 === 0) ? 29 : 28))) { | |
throw new Error('Day is out of range: ' + day) | |
} | |
return time | |
} | |
var time = { | |
/** Default locale name. */ | |
defaultLocale: 'en' | |
/** Locale details. */ | |
, locales: { | |
en: { | |
name: 'en' | |
, a: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] | |
, A: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', | |
'Friday', 'Saturday'] | |
, AM: 'AM' | |
, b: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', | |
'Oct', 'Nov', 'Dec'] | |
, B: ['January', 'February', 'March', 'April', 'May', 'June', 'July', | |
'August', 'September', 'October', 'November', 'December'] | |
, PM: 'PM' | |
} | |
} | |
} | |
/** | |
* Retrieves the locale with the given code. | |
* @param {string} code | |
* @return {Object} | |
*/ | |
var getLocale = time.getLocale = function(code) { | |
if (code) { | |
if (time.locales.hasOwnProperty(code)) { | |
return time.locales[code] | |
} | |
else if (code.length > 2) { | |
// If we appear to have more than a language code, try the | |
// language code on its own. | |
var languageCode = code.substring(0, 2) | |
if (time.locales.hasOwnProperty(languageCode)) { | |
return time.locales[languageCode] | |
} | |
} | |
} | |
return time.locales[time.defaultLocale] | |
} | |
/** | |
* Parses time details from a string, based on a format string. | |
* @param {string} input | |
* @param {string} format | |
* @param {string=} locale | |
* @return {Array.<number>} | |
*/ | |
var strptime = time.strptime = function(input, format, locale) { | |
return new TimeParser(format, getLocale(locale)).parse(input) | |
} | |
/** | |
* Convenience wrapper around time.strptime which returns a JavaScript Date. | |
* @param {string} input | |
* @param {string} format | |
* @param {string=} locale | |
* @return {date} | |
*/ | |
time.strpdate = function(input, format, locale) { | |
var t = strptime(input, format, locale) | |
return new Date(t[0], t[1] - 1, t[2], t[3], t[4], t[5]) | |
} | |
/** | |
* A partial implementation of <code>strftime</code>, which formats a date | |
* according to a format string. An Error will be thrown if an invalid | |
* format string is given. | |
* @param {date} date | |
* @param {string} format | |
* @param {string=} locale | |
* @return {string} | |
*/ | |
time.strftime = function(date, format, locale) { | |
if (strftimeFormatCheck.test(format)) { | |
throw new Error('strftime format ends with raw %') | |
} | |
locale = getLocale(locale) | |
return format.replace(/(%.)/g, function(s, f) { | |
var code = f.charAt(1) | |
if (typeof formatterDirectives[code] == 'undefined') { | |
throw new Error('strftime format contains an unknown directive: ' + f) | |
} | |
return formatterDirectives[code](date, locale) | |
}) | |
} | |
module.exports = time | |
},{"./is":13}],16:[function(_dereq_,module,exports){ | |
'use strict'; | |
// parseUri 1.2.2 | |
// (c) Steven Levithan <stevenlevithan.com> | |
// MIT License | |
function parseUri (str) { | |
var o = parseUri.options | |
, m = o.parser[o.strictMode ? "strict" : "loose"].exec(str) | |
, uri = {} | |
, i = 14 | |
while (i--) { uri[o.key[i]] = m[i] || "" } | |
uri[o.q.name] = {}; | |
uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) { | |
if ($1) { uri[o.q.name][$1] = $2 } | |
}) | |
return uri | |
} | |
parseUri.options = { | |
strictMode: false | |
, key: ['source','protocol','authority','userInfo','user','password','host','port','relative','path','directory','file','query','anchor'] | |
, q: { | |
name: 'queryKey' | |
, parser: /(?:^|&)([^&=]*)=?([^&]*)/g | |
} | |
, parser: { | |
strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/ | |
, loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ | |
} | |
} | |
// makeURI 1.2.2 - create a URI from an object specification; compatible with | |
// parseURI (http://blog.stevenlevithan.com/archives/parseuri) | |
// (c) Niall Smart <niallsmart.com> | |
// MIT License | |
function makeUri(u) { | |
var uri = '' | |
if (u.protocol) { | |
uri += u.protocol + '://' | |
} | |
if (u.user) { | |
uri += u.user | |
} | |
if (u.password) { | |
uri += ':' + u.password | |
} | |
if (u.user || u.password) { | |
uri += '@' | |
} | |
if (u.host) { | |
uri += u.host | |
} | |
if (u.port) { | |
uri += ':' + u.port | |
} | |
if (u.path) { | |
uri += u.path | |
} | |
var qk = u.queryKey | |
var qs = [] | |
for (var k in qk) { | |
if (!qk.hasOwnProperty(k)) { | |
continue | |
} | |
var v = encodeURIComponent(qk[k]) | |
k = encodeURIComponent(k) | |
if (v) { | |
qs.push(k + '=' + v) | |
} | |
else { | |
qs.push(k) | |
} | |
} | |
if (qs.length > 0) { | |
uri += '?' + qs.join('&') | |
} | |
if (u.anchor) { | |
uri += '#' + u.anchor | |
} | |
return uri | |
} | |
module.exports = { | |
parseUri: parseUri | |
, makeUri: makeUri | |
} | |
},{}],17:[function(_dereq_,module,exports){ | |
'use strict'; | |
var Concur = _dereq_('Concur') | |
var format = _dereq_('isomorph/format').formatObj | |
var is = _dereq_('isomorph/is') | |
var object = _dereq_('isomorph/object') | |
var NON_FIELD_ERRORS = '__all__' | |
/** | |
* A validation error, containing a list of messages. Single messages (e.g. | |
* those produced by validators) may have an associated error code and | |
* parameters to allow customisation by fields. | |
* | |
* The message argument can be a single error, a list of errors, or an object | |
* that maps field names to lists of errors. What we define as an "error" can | |
* be either a simple string or an instance of ValidationError with its message | |
* attribute set, and what we define as list or object can be an actual list or | |
* object or an instance of ValidationError with its errorList or errorObj | |
* property set. | |
*/ | |
var ValidationError = Concur.extend({ | |
constructor: function ValidationError(message, kwargs) { | |
if (!(this instanceof ValidationError)) { return new ValidationError(message, kwargs) } | |
kwargs = object.extend({code: null, params: null}, kwargs) | |
var code = kwargs.code | |
var params = kwargs.params | |
if (message instanceof ValidationError) { | |
if (object.hasOwn(message, 'errorObj')) { | |
message = message.errorObj | |
} | |
else if (object.hasOwn(message, 'message')) { | |
message = message.errorList | |
} | |
else { | |
code = message.code | |
params = message.params | |
message = message.message | |
} | |
} | |
if (is.Object(message)) { | |
this.errorObj = {} | |
Object.keys(message).forEach(function(field) { | |
var messages = message[field] | |
if (!(messages instanceof ValidationError)) { | |
messages = ValidationError(messages) | |
} | |
this.errorObj[field] = messages.errorList | |
}.bind(this)) | |
} | |
else if (is.Array(message)) { | |
this.errorList = [] | |
message.forEach(function(message) { | |
// Normalize strings to instances of ValidationError | |
if (!(message instanceof ValidationError)) { | |
message = ValidationError(message) | |
} | |
this.errorList.push.apply(this.errorList, message.errorList) | |
}.bind(this)) | |
} | |
else { | |
this.message = message | |
this.code = code | |
this.params = params | |
this.errorList = [this] | |
} | |
} | |
}) | |
/** | |
* Returns validation messages as an object with field names as properties. | |
* Throws an error if this validation error was not created with a field error | |
* object. | |
*/ | |
ValidationError.prototype.messageObj = function() { | |
if (!object.hasOwn(this, 'errorObj')) { | |
throw new Error('ValidationError has no errorObj') | |
} | |
return this.__iter__() | |
} | |
/** | |
* Returns validation messages as a list. | |
*/ | |
ValidationError.prototype.messages = function() { | |
if (object.hasOwn(this, 'errorObj')) { | |
var messages = [] | |
Object.keys(this.errorObj).forEach(function(field) { | |
var errors = this.errorObj[field] | |
messages.push.apply(messages, ValidationError(errors).__iter__()) | |
}.bind(this)) | |
return messages | |
} | |
else { | |
return this.__iter__() | |
} | |
} | |
/** | |
* Generates an object of field error messags or a list of error messages | |
* depending on how this ValidationError has been constructed. | |
*/ | |
ValidationError.prototype.__iter__ = function() { | |
if (object.hasOwn(this, 'errorObj')) { | |
var messageObj = {} | |
Object.keys(this.errorObj).forEach(function(field) { | |
var errors = this.errorObj[field] | |
messageObj[field] = ValidationError(errors).__iter__() | |
}.bind(this)) | |
return messageObj | |
} | |
else { | |
return this.errorList.map(function(error) { | |
var message = error.message | |
if (error.params) { | |
message = format(message, error.params) | |
} | |
return message | |
}) | |
} | |
} | |
/** | |
* Passes this error's messages on to the given error object, adding to a | |
* particular field's error messages if already present. | |
*/ | |
ValidationError.prototype.updateErrorObj = function(errorObj) { | |
if (object.hasOwn(this, 'errorObj')) { | |
if (errorObj) { | |
Object.keys(this.errorObj).forEach(function(field) { | |
if (!object.hasOwn(errorObj, field)) { | |
errorObj[field] = [] | |
} | |
var errors = errorObj[field] | |
errors.push.apply(errors, this.errorObj[field]) | |
}.bind(this)) | |
} | |
else { | |
errorObj = this.errorObj | |
} | |
} | |
else { | |
if (!object.hasOwn(errorObj, NON_FIELD_ERRORS)) { | |
errorObj[NON_FIELD_ERRORS] = [] | |
} | |
var nonFieldErrors = errorObj[NON_FIELD_ERRORS] | |
nonFieldErrors.push.apply(nonFieldErrors, this.errorList) | |
} | |
return errorObj | |
} | |
ValidationError.prototype.toString = function() { | |
return ('ValidationError(' + JSON.stringify(this.__iter__()) + ')') | |
} | |
module.exports = { | |
ValidationError: ValidationError | |
} | |
},{"Concur":9,"isomorph/format":12,"isomorph/is":13,"isomorph/object":14}],18:[function(_dereq_,module,exports){ | |
'use strict'; | |
var object = _dereq_('isomorph/object') | |
var errors = _dereq_('./errors') | |
var ValidationError = errors.ValidationError | |
var hexRE = /^[0-9a-f]+$/ | |
/** | |
* Cleans a IPv6 address string. | |
* | |
* Validity is checked by calling isValidIPv6Address() - if an invalid address | |
* is passed, a ValidationError is thrown. | |
* | |
* Replaces the longest continious zero-sequence with '::' and removes leading | |
* zeroes and makes sure all hextets are lowercase. | |
*/ | |
function cleanIPv6Address(ipStr, kwargs) { | |
kwargs = object.extend({ | |
unpackIPv4: false, errorMessage: 'This is not a valid IPv6 address.' | |
}, kwargs) | |
var bestDoublecolonStart = -1 | |
var bestDoublecolonLen = 0 | |
var doublecolonStart = -1 | |
var doublecolonLen = 0 | |
if (!isValidIPv6Address(ipStr)) { | |
throw ValidationError(kwargs.errorMessage, {code: 'invalid'}) | |
} | |
// This algorithm can only handle fully exploded IP strings | |
ipStr = _explodeShorthandIPstring(ipStr) | |
ipStr = _sanitiseIPv4Mapping(ipStr) | |
// If needed, unpack the IPv4 and return straight away | |
if (kwargs.unpackIPv4) { | |
var ipv4Unpacked = _unpackIPv4(ipStr) | |
if (ipv4Unpacked) { | |
return ipv4Unpacked | |
} | |
} | |
var hextets = ipStr.split(':') | |
for (var i = 0, l = hextets.length; i < l; i++) { | |
// Remove leading zeroes | |
hextets[i] = hextets[i].replace(/^0+/, '') | |
if (hextets[i] === '') { | |
hextets[i] = '0' | |
} | |
// Determine best hextet to compress | |
if (hextets[i] == '0') { | |
doublecolonLen += 1 | |
if (doublecolonStart == -1) { | |
// Start a sequence of zeros | |
doublecolonStart = i | |
} | |
if (doublecolonLen > bestDoublecolonLen) { | |
// This is the longest sequence so far | |
bestDoublecolonLen = doublecolonLen | |
bestDoublecolonStart = doublecolonStart | |
} | |
} | |
else { | |
doublecolonLen = 0 | |
doublecolonStart = -1 | |
} | |
} | |
// Compress the most suitable hextet | |
if (bestDoublecolonLen > 1) { | |
var bestDoublecolonEnd = bestDoublecolonStart + bestDoublecolonLen | |
// For zeros at the end of the address | |
if (bestDoublecolonEnd == hextets.length) { | |
hextets.push('') | |
} | |
hextets.splice(bestDoublecolonStart, bestDoublecolonLen, '') | |
// For zeros at the beginning of the address | |
if (bestDoublecolonStart === 0) { | |
hextets.unshift('') | |
} | |
} | |
return hextets.join(':').toLowerCase() | |
} | |
/** | |
* Sanitises IPv4 mapping in a expanded IPv6 address. | |
* | |
* This converts ::ffff:0a0a:0a0a to ::ffff:10.10.10.10. | |
* If there is nothing to sanitise, returns an unchanged string. | |
*/ | |
function _sanitiseIPv4Mapping(ipStr) { | |
if (ipStr.toLowerCase().indexOf('0000:0000:0000:0000:0000:ffff:') !== 0) { | |
// Not an ipv4 mapping | |
return ipStr | |
} | |
var hextets = ipStr.split(':') | |
if (hextets[hextets.length - 1].indexOf('.') != -1) { | |
// Already sanitized | |
return ipStr | |
} | |
var ipv4Address = [ | |
parseInt(hextets[6].substring(0, 2), 16) | |
, parseInt(hextets[6].substring(2, 4), 16) | |
, parseInt(hextets[7].substring(0, 2), 16) | |
, parseInt(hextets[7].substring(2, 4), 16) | |
].join('.') | |
return hextets.slice(0, 6).join(':') + ':' + ipv4Address | |
} | |
/** | |
* Unpacks an IPv4 address that was mapped in a compressed IPv6 address. | |
* | |
* This converts 0000:0000:0000:0000:0000:ffff:10.10.10.10 to 10.10.10.10. | |
* If there is nothing to sanitize, returns null. | |
*/ | |
function _unpackIPv4(ipStr) { | |
if (ipStr.toLowerCase().indexOf('0000:0000:0000:0000:0000:ffff:') !== 0) { | |
return null | |
} | |
var hextets = ipStr.split(':') | |
return hextets.pop() | |
} | |
/** | |
* Determines if we have a valid IPv6 address. | |
*/ | |
function isValidIPv6Address(ipStr) { | |
var validateIPv4Address = _dereq_('./validators').validateIPv4Address | |
// We need to have at least one ':' | |
if (ipStr.indexOf(':') == -1) { | |
return false | |
} | |
// We can only have one '::' shortener | |
if (String_count(ipStr, '::') > 1) { | |
return false | |
} | |
// '::' should be encompassed by start, digits or end | |
if (ipStr.indexOf(':::') != -1) { | |
return false | |
} | |
// A single colon can neither start nor end an address | |
if ((ipStr.charAt(0) == ':' && ipStr.charAt(1) != ':') || | |
(ipStr.charAt(ipStr.length - 1) == ':' && | |
ipStr.charAt(ipStr.length - 2) != ':')) { | |
return false | |
} | |
// We can never have more than 7 ':' (1::2:3:4:5:6:7:8 is invalid) | |
if (String_count(ipStr, ':') > 7) { | |
return false | |
} | |
// If we have no concatenation, we need to have 8 fields with 7 ':' | |
if (ipStr.indexOf('::') == -1 && String_count(ipStr, ':') != 7) { | |
// We might have an IPv4 mapped address | |
if (String_count(ipStr, '.') != 3) { | |
return false | |
} | |
} | |
ipStr = _explodeShorthandIPstring(ipStr) | |
// Now that we have that all squared away, let's check that each of the | |
// hextets are between 0x0 and 0xFFFF. | |
var hextets = ipStr.split(':') | |
for (var i = 0, l = hextets.length, hextet; i < l; i++) { | |
hextet = hextets[i] | |
if (String_count(hextet, '.') == 3) { | |
// If we have an IPv4 mapped address, the IPv4 portion has to | |
// be at the end of the IPv6 portion. | |
if (ipStr.split(':').pop() != hextet) { | |
return false | |
} | |
try { | |
validateIPv4Address(hextet) | |
} | |
catch (e) { | |
if (!(e instanceof ValidationError)) { | |
throw e | |
} | |
return false | |
} | |
} | |
else { | |
if (!hexRE.test(hextet)) { | |
return false | |
} | |
var intValue = parseInt(hextet, 16) | |
if (isNaN(intValue) || intValue < 0x0 || intValue > 0xFFFF) { | |
return false | |
} | |
} | |
} | |
return true | |
} | |
/** | |
* Expands a shortened IPv6 address. | |
*/ | |
function _explodeShorthandIPstring(ipStr) { | |
if (!_isShortHand(ipStr)) { | |
// We've already got a longhand ipStr | |
return ipStr | |
} | |
var newIp = [] | |
var hextets = ipStr.split('::') | |
// If there is a ::, we need to expand it with zeroes to get to 8 hextets - | |
// unless there is a dot in the last hextet, meaning we're doing v4-mapping | |
var fillTo = (ipStr.split(':').pop().indexOf('.') != -1) ? 7 : 8 | |
if (hextets.length > 1) { | |
var sep = hextets[0].split(':').length + hextets[1].split(':').length | |
newIp = hextets[0].split(':') | |
for (var i = 0, l = fillTo - sep; i < l; i++) { | |
newIp.push('0000') | |
} | |
newIp = newIp.concat(hextets[1].split(':')) | |
} | |
else { | |
newIp = ipStr.split(':') | |
} | |
// Now need to make sure every hextet is 4 lower case characters. | |
// If a hextet is < 4 characters, we've got missing leading 0's. | |
var retIp = [] | |
for (i = 0, l = newIp.length; i < l; i++) { | |
retIp.push(zeroPadding(newIp[i], 4) + newIp[i].toLowerCase()) | |
} | |
return retIp.join(':') | |
} | |
/** | |
* Determines if the address is shortened. | |
*/ | |
function _isShortHand(ipStr) { | |
if (String_count(ipStr, '::') == 1) { | |
return true | |
} | |
var parts = ipStr.split(':') | |
for (var i = 0, l = parts.length; i < l; i++) { | |
if (parts[i].length < 4) { | |
return true | |
} | |
} | |
return false | |
} | |
// Utilities | |
function zeroPadding(str, length) { | |
if (str.length >= length) { | |
return '' | |
} | |
return new Array(length - str.length + 1).join('0') | |
} | |
function String_count(str, subStr) { | |
return str.split(subStr).length - 1 | |
} | |
module.exports = { | |
cleanIPv6Address: cleanIPv6Address | |
, isValidIPv6Address: isValidIPv6Address | |
} | |
},{"./errors":17,"./validators":19,"isomorph/object":14}],19:[function(_dereq_,module,exports){ | |
'use strict'; | |
var Concur = _dereq_('Concur') | |
var is = _dereq_('isomorph/is') | |
var object = _dereq_('isomorph/object') | |
var punycode = _dereq_('punycode') | |
var url = _dereq_('isomorph/url') | |
var errors = _dereq_('./errors') | |
var ipv6 = _dereq_('./ipv6') | |
var ValidationError = errors.ValidationError | |
var isValidIPv6Address = ipv6.isValidIPv6Address | |
var EMPTY_VALUES = [null, undefined, ''] | |
function String_rsplit(str, sep, maxsplit) { | |
var split = str.split(sep) | |
return maxsplit ? [split.slice(0, -maxsplit).join(sep)].concat(split.slice(-maxsplit)) : split | |
} | |
/** | |
* Validates that input matches a regular expression. | |
*/ | |
var RegexValidator = Concur.extend({ | |
constructor: function(kwargs) { | |
if (!(this instanceof RegexValidator)) { return new RegexValidator(kwargs) } | |
kwargs = object.extend({ | |
regex: null, message: null, code: null, inverseMatch: null | |
}, kwargs) | |
if (kwargs.regex) { | |
this.regex = kwargs.regex | |
} | |
if (kwargs.message) { | |
this.message = kwargs.message | |
} | |
if (kwargs.code) { | |
this.code = kwargs.code | |
} | |
if (kwargs.inverseMatch) { | |
this.inverseMatch = kwargs.inverseMatch | |
} | |
// Compile the regex if it was not passed pre-compiled | |
if (is.String(this.regex)) { | |
this.regex = new RegExp(this.regex) | |
} | |
return this.__call__.bind(this) | |
} | |
, regex: '' | |
, message: 'Enter a valid value.' | |
, code: 'invalid' | |
, inverseMatch: false | |
, __call__: function(value) { | |
if (this.inverseMatch === this.regex.test(''+value)) { | |
throw ValidationError(this.message, {code: this.code}) | |
} | |
} | |
}) | |
/** | |
* Validates that input looks like a valid URL. | |
*/ | |
var URLValidator = RegexValidator.extend({ | |
regex: new RegExp( | |
'^(?:[a-z0-9\\.\\-]*)://' // schema is validated separately | |
+ '(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+(?:[A-Z]{2,6}\\.?|[A-Z0-9-]{2,}\\.?)|' // Domain... | |
+ 'localhost|' // localhost... | |
+ '\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|' // ...or IPv4 | |
+ '\\[?[A-F0-9]*:[A-F0-9:]+\\]?)' // ...or IPv6 | |
+ '(?::\\d+)?' // Optional port | |
+ '(?:/?|[/?]\\S+)$' | |
, 'i' | |
) | |
, message: 'Enter a valid URL.' | |
, schemes: ['http', 'https', 'ftp', 'ftps'] | |
, constructor:function(kwargs) { | |
if (!(this instanceof URLValidator)) { return new URLValidator(kwargs) } | |
kwargs = object.extend({schemes: null}, kwargs) | |
RegexValidator.call(this, kwargs) | |
if (kwargs.schemes !== null) { | |
this.schemes = kwargs.schemes | |
} | |
return this.__call__.bind(this) | |
} | |
, __call__: function(value) { | |
value = ''+value | |
// Check if the scheme is valid first | |
var scheme = value.split('://')[0].toLowerCase() | |
if (this.schemes.indexOf(scheme) === -1) { | |
throw ValidationError(this.message, {code: this.code}) | |
} | |
// Check the full URL | |
try { | |
RegexValidator.prototype.__call__.call(this, value) | |
} | |
catch (e) { | |
if (!(e instanceof ValidationError)) { throw e } | |
// Trivial case failed - try for possible IDN domain | |
var urlFields = url.parseUri(value) | |
try { | |
urlFields.host = punycode.toASCII(urlFields.host) | |
} | |
catch (unicodeError) { | |
throw e | |
} | |
value = url.makeUri(urlFields) | |
RegexValidator.prototype.__call__.call(this, value) | |
} | |
} | |
}) | |
/** Validates that input looks like a valid e-mail address. */ | |
var EmailValidator = Concur.extend({ | |
message: 'Enter a valid email address.' | |
, code: 'invalid' | |
, userRegex: new RegExp( | |
"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*$" // Dot-atom | |
+ '|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"$)' // Quoted-string | |
, 'i') | |
, domainRegex: new RegExp( | |
'^(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+(?:[A-Z]{2,6}|[A-Z0-9-]{2,})$' // Domain | |
+ '|^\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$' // Literal form, ipv4 address (SMTP 4.1.3) | |
, 'i') | |
, domainWhitelist: ['localhost'] | |
, constructor: function(kwargs) { | |
if (!(this instanceof EmailValidator)) { return new EmailValidator(kwargs) } | |
kwargs = object.extend({message: null, code: null, whitelist: null}, kwargs) | |
if (kwargs.message !== null) { | |
this.message = kwargs.message | |
} | |
if (kwargs.code !== null) { | |
this.code = kwargs.code | |
} | |
if (kwargs.whitelist !== null) { | |
this.domainWhitelist = kwargs.whitelist | |
} | |
return this.__call__.bind(this) | |
} | |
, __call__ : function(value) { | |
value = ''+value | |
if (!value || value.indexOf('@') == -1) { | |
throw ValidationError(this.message, {code: this.code}) | |
} | |
var parts = String_rsplit(value, '@', 1) | |
var userPart = parts[0] | |
var domainPart = parts[1] | |
if (!this.userRegex.test(userPart)) { | |
throw ValidationError(this.message, {code: this.code}) | |
} | |
if (this.domainWhitelist.indexOf(domainPart) == -1 && | |
!this.domainRegex.test(domainPart)) { | |
// Try for possible IDN domain-part | |
try { | |
domainPart = punycode.toASCII(domainPart) | |
if (this.domainRegex.test(domainPart)) { | |
return | |
} | |
} | |
catch (unicodeError) { | |
// Pass through to throw the ValidationError | |
} | |
throw ValidationError(this.message, {code: this.code}) | |
} | |
} | |
}) | |
var validateEmail = EmailValidator() | |
var SLUG_RE = /^[-a-zA-Z0-9_]+$/ | |
/** Validates that input is a valid slug. */ | |
var validateSlug = RegexValidator({ | |
regex: SLUG_RE | |
, message: 'Enter a valid "slug" consisting of letters, numbers, underscores or hyphens.' | |
, code: 'invalid' | |
}) | |
var IPV4_RE = /^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$/ | |
/** Validates that input is a valid IPv4 address. */ | |
var validateIPv4Address = RegexValidator({ | |
regex: IPV4_RE | |
, message: 'Enter a valid IPv4 address.' | |
, code: 'invalid' | |
}) | |
/** Validates that input is a valid IPv6 address. */ | |
function validateIPv6Address(value) { | |
if (!isValidIPv6Address(value)) { | |
throw ValidationError('Enter a valid IPv6 address.', {code: 'invalid'}) | |
} | |
} | |
/** Validates that input is a valid IPv4 or IPv6 address. */ | |
function validateIPv46Address(value) { | |
try { | |
validateIPv4Address(value) | |
} | |
catch (e) { | |
if (!(e instanceof ValidationError)) { throw e } | |
try { | |
validateIPv6Address(value) | |
} | |
catch (e) { | |
if (!(e instanceof ValidationError)) { throw e } | |
throw ValidationError('Enter a valid IPv4 or IPv6 address.', | |
{code: 'invalid'}) | |
} | |
} | |
} | |
var ipAddressValidatorLookup = { | |
both: {validators: [validateIPv46Address], message: 'Enter a valid IPv4 or IPv6 address.'} | |
, ipv4: {validators: [validateIPv4Address], message: 'Enter a valid IPv4 address.'} | |
, ipv6: {validators: [validateIPv6Address], message: 'Enter a valid IPv6 address.'} | |
} | |
/** | |
* Depending on the given parameters returns the appropriate validators for | |
* a GenericIPAddressField. | |
*/ | |
function ipAddressValidators(protocol, unpackIPv4) { | |
if (protocol != 'both' && unpackIPv4) { | |
throw new Error('You can only use unpackIPv4 if protocol is set to "both"') | |
} | |
protocol = protocol.toLowerCase() | |
if (typeof ipAddressValidatorLookup[protocol] == 'undefined') { | |
throw new Error('The protocol "' + protocol +'" is unknown') | |
} | |
return ipAddressValidatorLookup[protocol] | |
} | |
var COMMA_SEPARATED_INT_LIST_RE = /^[\d,]+$/ | |
/** Validates that input is a comma-separated list of integers. */ | |
var validateCommaSeparatedIntegerList = RegexValidator({ | |
regex: COMMA_SEPARATED_INT_LIST_RE | |
, message: 'Enter only digits separated by commas.' | |
, code: 'invalid' | |
}) | |
/** | |
* Base for validators which compare input against a given value. | |
*/ | |
var BaseValidator = Concur.extend({ | |
constructor: function(limitValue) { | |
if (!(this instanceof BaseValidator)) { return new BaseValidator(limitValue) } | |
this.limitValue = limitValue | |
return this.__call__.bind(this) | |
} | |
, compare: function(a, b) { return a !== b } | |
, clean: function(x) { return x } | |
, message: 'Ensure this value is {limitValue} (it is {showValue}).' | |
, code: 'limitValue' | |
, __call__: function(value) { | |
var cleaned = this.clean(value) | |
var params = {limitValue: this.limitValue, showValue: cleaned} | |
if (this.compare(cleaned, this.limitValue)) { | |
throw ValidationError(this.message, {code: this.code, params: params}) | |
} | |
} | |
}) | |
/** | |
* Validates that input is less than or equal to a given value. | |
*/ | |
var MaxValueValidator = BaseValidator.extend({ | |
constructor: function(limitValue) { | |
if (!(this instanceof MaxValueValidator)) { return new MaxValueValidator(limitValue) } | |
return BaseValidator.call(this, limitValue) | |
} | |
, compare: function(a, b) { return a > b } | |
, message: 'Ensure this value is less than or equal to {limitValue}.' | |
, code: 'maxValue' | |
}) | |
/** | |
* Validates that input is greater than or equal to a given value. | |
*/ | |
var MinValueValidator = BaseValidator.extend({ | |
constructor: function(limitValue) { | |
if (!(this instanceof MinValueValidator)) { return new MinValueValidator(limitValue) } | |
return BaseValidator.call(this, limitValue) | |
} | |
, compare: function(a, b) { return a < b } | |
, message: 'Ensure this value is greater than or equal to {limitValue}.' | |
, code: 'minValue' | |
}) | |
/** | |
* Validates that input is at least a given length. | |
*/ | |
var MinLengthValidator = BaseValidator.extend({ | |
constructor: function(limitValue) { | |
if (!(this instanceof MinLengthValidator)) { return new MinLengthValidator(limitValue) } | |
return BaseValidator.call(this, limitValue) | |
} | |
, compare: function(a, b) { return a < b } | |
, clean: function(x) { return x.length } | |
, message: 'Ensure this value has at least {limitValue} characters (it has {showValue}).' | |
, code: 'minLength' | |
}) | |
/** | |
* Validates that input is at most a given length. | |
*/ | |
var MaxLengthValidator = BaseValidator.extend({ | |
constructor: function(limitValue) { | |
if (!(this instanceof MaxLengthValidator)) { return new MaxLengthValidator(limitValue) } | |
return BaseValidator.call(this, limitValue) | |
} | |
, compare: function(a, b) { return a > b } | |
, clean: function(x) { return x.length } | |
, message: 'Ensure this value has at most {limitValue} characters (it has {showValue}).' | |
, code: 'maxLength' | |
}) | |
module.exports = { | |
EMPTY_VALUES: EMPTY_VALUES | |
, RegexValidator: RegexValidator | |
, URLValidator: URLValidator | |
, EmailValidator: EmailValidator | |
, validateEmail: validateEmail | |
, validateSlug: validateSlug | |
, validateIPv4Address: validateIPv4Address | |
, validateIPv6Address: validateIPv6Address | |
, validateIPv46Address: validateIPv46Address | |
, ipAddressValidators: ipAddressValidators | |
, validateCommaSeparatedIntegerList: validateCommaSeparatedIntegerList | |
, BaseValidator: BaseValidator | |
, MaxValueValidator: MaxValueValidator | |
, MinValueValidator: MinValueValidator | |
, MaxLengthValidator: MaxLengthValidator | |
, MinLengthValidator: MinLengthValidator | |
, ValidationError: ValidationError | |
, ipv6: ipv6 | |
} | |
},{"./errors":17,"./ipv6":18,"Concur":9,"isomorph/is":13,"isomorph/object":14,"isomorph/url":16,"punycode":10}]},{},[6]) | |
(6) | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment