nodegame-widgets
Version:
Collections of useful and reusable javascript / HTML snippets for nodeGame
1,359 lines (1,235 loc) • 59.6 kB
JavaScript
/**
* # CustomInput
* Copyright(c) 2021 Stefano Balietti
* MIT Licensed
*
* Creates a configurable input form with validation
*
* www.nodegame.org
*/
(function(node) {
"use strict";
node.widgets.register('CustomInput', CustomInput);
// ## Meta-data
CustomInput.version = '0.12.0';
CustomInput.description = 'Creates a configurable input form';
CustomInput.title = false;
CustomInput.panel = false;
CustomInput.className = 'custominput';
CustomInput.types = {
text: true,
number: true,
'float': true,
'int': true,
date: true,
list: true,
us_city_state_zip: true,
us_state: true,
us_zip: true
};
var sepNames = {
',': 'comma',
' ': 'space',
'.': 'dot'
};
var usStates = {
Alabama: 'AL',
Alaska: 'AK',
Arizona: 'AZ',
Arkansas: 'AR',
California: 'CA',
Colorado: 'CO',
Connecticut: 'CT',
Delaware: 'DE',
Florida: 'FL',
Georgia: 'GA',
Hawaii: 'HI',
Idaho: 'ID',
Illinois: 'IL',
Indiana: 'IN',
Iowa: 'IA',
Kansas: 'KS',
Kentucky: 'KY',
Louisiana: 'LA',
Maine: 'ME',
Maryland: 'MD',
Massachusetts: 'MA',
Michigan: 'MI',
Minnesota: 'MN',
Mississippi: 'MS',
Missouri: 'MO',
Montana: 'MT',
Nebraska: 'NE',
Nevada: 'NV',
'New Hampshire': 'NH',
'New Jersey': 'NJ',
'New Mexico': 'NM',
'New York': 'NY',
'North Carolina': 'NC',
'North Dakota': 'ND',
Ohio: 'OH',
Oklahoma: 'OK',
Oregon: 'OR',
Pennsylvania: 'PA',
'Rhode Island': 'RI',
'South Carolina': 'SC',
'South Dakota': 'SD',
Tennessee: 'TN',
Texas: 'TX',
Utah: 'UT',
Vermont: 'VT',
Virginia: 'VA',
Washington: 'WA',
'West Virginia': 'WV',
Wisconsin: 'WI',
Wyoming: 'WY',
};
var usTerr = {
'American Samoa': 'AS',
'District of Columbia': 'DC',
'Federated States of Micronesia': 'FM',
Guam: 'GU',
'Marshall Islands': 'MH',
'Northern Mariana Islands': 'MP',
Palau: 'PW',
'Puerto Rico': 'PR',
'Virgin Islands': 'VI'
};
// To be filled if requested.
var usTerrByAbbr;
var usStatesByAbbr;
var usStatesTerr;
var usStatesTerrByAbbr;
// Lower case keys.
var usStatesLow;
var usTerrLow;
var usTerrByAbbrLow;
var usStatesByAbbrLow;
var usStatesTerrLow;
var usStatesTerrByAbbrLow;
CustomInput.texts = {
listErr: 'Check that there are no empty items; do not end with ' +
'the separator',
listSizeErr: function(w, param) {
if (w.params.fixedSize) {
return w.params.minItems + ' items required';
}
if (param === 'min') {
return 'Too few items. Min: ' + w.params.minItems;
}
return 'Too many items. Max: ' + w.params.maxItems;
},
usStateAbbrErr: 'Not a valid state abbreviation (must be 2 characters)',
usStateErr: 'Not a valid state (full name required)',
usZipErr: 'Not a valid ZIP code (must be 5 digits)',
autoHint: function(w) {
var res, sep;
if (w.type === 'list') {
sep = sepNames[w.params.listSep] || w.params.listSep;
res = '(if more than one, separate with ' + sep + ')';
}
else if (w.type === 'us_state') {
res = w.params.abbr ? '(Use 2-letter abbreviation)' :
'(Type the full name of the state)';
}
else if (w.type === 'us_zip') {
res = '(Use 5-digit ZIP code)';
}
else if (w.type === 'us_city_state_zip') {
sep = w.params.listSep;
res = '(Format: Town' + sep + ' State' + sep + ' ZIP code)';
}
else if (w.type === 'date') {
if (w.params.minDate && w.params.maxDate) {
res = '(Must be between ' + w.params.minDate.str + ' and ' +
w.params.maxDate.str + ')';
}
else if (w.params.minDate) {
res = '(Must be after ' + w.params.minDate.str + ')';
}
else if (w.params.maxDate) {
res = '(Must be before ' + w.params.maxDate.str + ')';
}
else {
res = '(Format: ' + w.params.format + ')';
}
}
else if (w.type === 'number' || w.type === 'int' ||
w.type === 'float') {
if (w.params.min && w.params.max) {
res = '(Must be between ' + w.params.min + ' and ' +
w.params.max + ')';
}
else if (w.params.min) {
res = '(Must be after ' + w.params.min + ')';
}
else if (w.params.max) {
res = '(Must be before ' + w.params.max + ')';
}
}
return w.required ? ((res || '') + ' *') : (res || false);
},
numericErr: function(w) {
var str, p;
p = w.params;
// Weird, but valid, case.
if (p.exactly) return 'Must enter ' + p.lower;
// Others.
str = 'Must be ';
if (w.type === 'float') str += 'a floating point number';
else if (w.type === 'int') str += 'an integer';
if (p.between) {
str += ' ' + (p.leq ? '≥ ' : '<' ) + p.lower;
str += ' and ';
str += (p.ueq ? '≤ ' : '> ') + p.upper;
}
else if ('undefined' !== typeof p.lower) {
str += ' ' + (p.leq ? '≥ ' : '< ') + p.lower;
}
// It can be also a non-numeric error, e.g. a string here.
else if ('undefined' !== typeof p.upper) {
str += ' ' + (p.ueq ? '≤ ' : '> ') + p.upper;
}
return str;
},
textErr: function(w, param) {
var str, p;
if (param === 'num') return 'Cannot contain numbers';
p = w.params;
str = 'Must be ';
if (p.exactly) {
str += 'exactly ' + (p.lower + 1);
}
else if (p.between) {
str += 'between ' + p.lower + ' and ' + p.upper;
}
else if ('undefined' !== typeof p.lower) {
str += ' more than ' + (p.lower -1);
}
else if ('undefined' !== typeof p.upper) {
str += ' less than ' + (p.upper + 1);
}
str += ' characters long';
if (p.between) str += ' (extremes included)';
str += '. Current length: ' + param;
return str;
},
dateErr: function(w, param) {
if (param === 'invalid') return 'Date is invalid';
if (param === 'min') {
return 'Date must be after ' + w.params.minDate.str;
}
if (param === 'max') {
return 'Date must be before ' + w.params.maxDate.str;
}
return 'Must follow format ' + w.params.format;
},
emptyErr: 'Cannot be empty'
};
// ## Dependencies
CustomInput.dependencies = {
JSUS: {}
};
/**
* ## CustomInput constructor
*
* Creates a new instance of CustomInput
*/
function CustomInput() {
/**
* ### CustomInput.input
*
* The HTML input element
*/
this.input = null;
/**
* ### CustomInput.placeholder
*
* The placeholder text for the input form
*
* Some types preset it automatically
*/
this.placeholder = null;
/**
* ### CustomInput.inputWidth
*
* The width of the input form as string (css attribute)
*
* Some types preset it automatically
*/
this.inputWidth = null;
/**
* ### CustomInput.type
*
* The type of input
*/
this.type = null;
/**
* ### CustomInput.preprocess
*
* The function that preprocess the input before validation
*
* The function receives the input form and must modify it directly
*/
this.preprocess = null;
/**
* ### CustomInput.validation
*
* The validation function for the input
*
* The function returns an object like:
*
* ```javascript
* {
* value: 'validvalue',
* err: 'This error occurred' // If invalid.
* }
* ```
*/
this.validation = null;
/**
* ### CustomInput.userValidation
*
* An additional validation executed after the main validation function
*
* Executes only if validation does not fail, takes as input the return
* value from the validation function which can be modified accordingly.
*
* ```javascript
* {
* value: 'validvalue',
* err: 'This error occurred' // If invalid.
* }
* ```
*/
this.userValidation = null;
/**
* ### CustomInput.validationSpeed
*
* How often (in milliseconds) the validation function is called
*
* Default: 500
*/
this.validationSpeed = 500;
/**
* ### CustomInput.postprocess
*
* The function that postprocess the input after validation
*
* The function returns the postprocessed value
*/
this.postprocess = null;
/**
* ### CustomInput.oninput
*
* A function that is executed after any input
*
* It is executed after validation and receives a result object
* and a reference to this widget.
*/
this.oninput = null;
/**
* ### CustomInput.params
*
* Object containing extra validation params
*
* This object is populated by the init function
*/
this.params = {};
/**
* ### CustomInput.errorBox
*
* An HTML element displayed when a validation error occurs
*/
this.errorBox = null;
/**
* ### CustomInput.mainText
*
* A text preceeding the custom input
*/
this.mainText = null;
/**
* ### CustomInput.hint
*
* An additional text with information about the input
*
* If not specified, it may be auto-filled, e.g. '*'.
*
* @see CustomInput.texts.autoHint
*/
this.hint = null;
/**
* ### CustomInput.requiredChoice
*
* If TRUE, the input form cannot be left empty
*
* Default: TRUE
*
* @deprecated Use CustomInput.required
*/
this.requiredChoice = null;
/**
* ### CustomInput.required
*
* If TRUE, the input form cannot be left empty
*
* Default: TRUE
*/
this.required = null;
/**
* ### CustomInput.timeBegin
*
* When the first character was inserted
*/
this.timeBegin = null;
/**
* ### CustomInput.timeEnd
*
* When the last character was inserted
*/
this.timeEnd = null;
/**
* ### CustomInput.checkbox
*
* A checkbox element for an additional action
*/
this.checkbox = null;
/**
* ### CustomInput.checkboxText
*
* The text next to the checkbox
*/
this.checkboxText = null;
/**
* ### CustomInput.checkboxCb
*
* The callback executed when the checkbox is clicked
*/
this.checkboxCb = null;
/**
* ### CustomInput.orientation
*
* The orientation of main text relative to the input box
*
* Options:
* - 'V': main text above input box
* - 'H': main text next to input box
*
* Default: 'V'
*/
this.orientation = null;
}
// ## CustomInput methods
/**
* ### CustomInput.init
*
* Initializes the instance
*
* @param {object} opts Configuration options
*/
CustomInput.prototype.init = function(opts) {
var tmp, that, e, isText, setValues;
that = this;
e = 'CustomInput.init: ';
// Option orientation, default 'H'.
if ('undefined' === typeof opts.orientation) {
tmp = 'V';
}
else if ('string' !== typeof opts.orientation) {
throw new TypeError('CustomInput.init: orientation must ' +
'be string, or undefined. Found: ' +
opts.orientation);
}
else {
tmp = opts.orientation.toLowerCase().trim();
if (tmp === 'h') tmp = 'H';
else if (tmp === 'v') tmp = 'V';
else {
throw new Error('CustomInput.init: unknown orientation: ' +
tmp);
}
}
this.orientation = tmp;
// Backward compatible checks.
// Option required will be used in the future.
if ('undefined' !== typeof opts.required) {
this.required = this.requiredChoice = !!opts.required;
}
if ('undefined' !== typeof opts.requiredChoice) {
if (!!this.required !== !!opts.requiredChoice) {
throw new TypeError('CustomInput.init: required and ' +
'requiredChoice are incompatible. Option ' +
'requiredChoice will be deprecated.');
}
this.required = this.requiredChoice = !!opts.requiredChoice;
}
if ('undefined' === typeof this.required) {
this.required = this.requiredChoice = false;
}
if (opts.userValidation) {
if ('function' !== typeof opts.userValidation) {
throw new TypeError('CustomInput.init: userValidation must ' +
'be function or undefined. Found: ' +
opts.userValidation);
}
this.userValidation = opts.userValidation;
}
if (opts.type) {
if (!CustomInput.types[opts.type]) {
throw new Error(e + 'type not supported: ' + opts.type);
}
this.type = opts.type;
}
else {
this.type = 'text';
}
if (opts.validation) {
if ('function' !== typeof opts.validation) {
throw new TypeError(e + 'validation must be function ' +
'or undefined. Found: ' +
opts.validation);
}
tmp = opts.validation;
}
else {
// Add default validations based on type.
if (this.type === 'number' || this.type === 'float' ||
this.type === 'int' || this.type === 'text') {
isText = this.type === 'text';
// Greater than.
if ('undefined' !== typeof opts.min) {
tmp = J.isNumber(opts.min);
if (false === tmp) {
throw new TypeError(e + 'min must be number or ' +
'undefined. Found: ' + opts.min);
}
this.params.lower = opts.min;
this.params.leq = true;
}
// Less than.
if ('undefined' !== typeof opts.max) {
tmp = J.isNumber(opts.max);
if (false === tmp) {
throw new TypeError(e + 'max must be number or ' +
'undefined. Found: ' + opts.max);
}
this.params.upper = opts.max;
this.params.ueq = true;
}
if (opts.strictlyGreater) this.params.leq = false;
if (opts.strictlyLess) this.params.ueq = false;
// Checks on both min and max.
if ('undefined' !== typeof this.params.lower &&
'undefined' !== typeof this.params.upper) {
if (this.params.lower > this.params.upper) {
throw new TypeError(e + 'min cannot be greater ' +
'than max. Found: ' +
opts.min + '> ' + opts.max);
}
// Exact length.
if (this.params.lower === this.params.upper) {
if (!this.params.leq || !this.params.ueq) {
throw new TypeError(e + 'min cannot be equal to ' +
'max when strictlyGreater or ' +
'strictlyLess are set. ' +
'Found: ' + opts.min);
}
if (this.type === 'int' || this.type === 'text') {
if (J.isFloat(this.params.lower)) {
throw new TypeError(e + 'min cannot be a ' +
'floating point number ' +
'and equal to ' +
'max, when type ' +
'is not "float". Found: ' +
opts.min);
}
}
// Store this to create better error strings.
this.params.exactly = true;
}
else {
// Store this to create better error strings.
this.params.between = true;
}
}
// Checks for text only.
if (isText) {
this.params.noNumbers = opts.noNumbers;
if ('undefined' !== typeof this.params.lower) {
if (this.params.lower < 0) {
throw new TypeError(e + 'min cannot be negative ' +
'when type is "text". Found: ' +
this.params.lower);
}
if (!this.params.leq) this.params.lower++;
}
if ('undefined' !== typeof this.params.upper) {
if (this.params.upper < 0) {
throw new TypeError(e + 'max cannot be negative ' +
'when type is "text". Found: ' +
this.params.upper);
}
if (!this.params.ueq) this.params.upper--;
}
tmp = function(value) {
var len, p, out, err;
p = that.params;
len = value.length;
out = { value: value };
if (p.noNumbers && /\d/.test(value)) {
err = that.getText('textErr', 'num');
}
else {
if (p.exactly) {
err = len !== p.lower;
}
else {
if (('undefined' !== typeof p.lower &&
len < p.lower) ||
('undefined' !== typeof p.upper &&
len > p.upper)) {
err = true;
}
}
if (err) err = that.getText('textErr', len);
}
if (err) out.err = err;
return out;
};
setValues = function() {
var a, b;
a = 'undefined' !== typeof that.params.lower ?
(that.params.lower + 1) : 5;
b = 'undefined' !== typeof that.params.upper ?
that.params.upper : (a + 5);
return J.randomString(J.randomInt(a, b));
};
}
else {
tmp = (function() {
var cb;
if (that.type === 'float') cb = J.isFloat;
else if (that.type === 'int') cb = J.isInt;
else cb = J.isNumber;
return function(value) {
var res, p;
p = that.params;
res = cb(value, p.lower, p.upper, p.leq, p.ueq);
if (res !== false) return { value: res };
return {
value: value,
err: that.getText('numericErr')
};
};
})();
setValues = function() {
var p, a, b;
p = that.params;
if (that.type === 'float') return J.random();
a = 0;
if ('undefined' !== typeof p.lower) {
a = p.leq ? (p.lower - 1) : p.lower;
}
if ('undefined' !== typeof p.upper) {
b = p.ueq ? p.upper : (p.upper - 1);
}
else {
b = 100 + a;
}
return J.randomInt(a, b);
};
}
// Preset inputWidth.
if (this.params.upper) {
if (this.params.upper < 10) this.inputWidth = '100px';
else if (this.params.upper < 20) this.inputWidth = '200px';
}
}
else if (this.type === 'date') {
if ('undefined' !== typeof opts.format) {
// TODO: use regex.
if (opts.format !== 'mm-dd-yy' &&
opts.format !== 'dd-mm-yy' &&
opts.format !== 'mm-dd-yyyy' &&
opts.format !== 'dd-mm-yyyy' &&
opts.format !== 'mm.dd.yy' &&
opts.format !== 'dd.mm.yy' &&
opts.format !== 'mm.dd.yyyy' &&
opts.format !== 'dd.mm.yyyy' &&
opts.format !== 'mm/dd/yy' &&
opts.format !== 'dd/mm/yy' &&
opts.format !== 'mm/dd/yyyy' &&
opts.format !== 'dd/mm/yyyy') {
throw new Error(e + 'date format is invalid. Found: ' +
opts.format);
}
this.params.format = opts.format;
}
else {
this.params.format = 'mm/dd/yyyy';
}
this.params.sep = this.params.format.charAt(2);
tmp = this.params.format.split(this.params.sep);
this.params.yearDigits = tmp[2].length;
this.params.dayPos = tmp[0].charAt(0) === 'd' ? 0 : 1;
this.params.monthPos = this.params.dayPos ? 0 : 1;
this.params.dateLen = tmp[2].length + 6;
if (opts.minDate) {
tmp = getParsedDate(opts.minDate, this.params);
if (!tmp) {
throw new Error(e + 'minDate must be a Date object. ' +
'Found: ' + opts.minDate);
}
this.params.minDate = tmp;
}
if (opts.maxDate) {
tmp = getParsedDate(opts.maxDate, this.params);
if (!tmp) {
throw new Error(e + 'maxDate must be a Date object. ' +
'Found: ' + opts.maxDate);
}
if (this.params.minDate &&
this.params.minDate.obj > tmp.obj) {
throw new Error(e + 'maxDate cannot be prior to ' +
'minDate. Found: ' + tmp.str +
' < ' + this.params.minDate.str);
}
this.params.maxDate = tmp;
}
// Preset inputWidth.
if (this.params.yearDigits === 2) this.inputWidth = '100px';
else this.inputWidth = '150px';
// Preset placeholder.
this.placeholder = this.params.format;
tmp = function(value) {
var p, tokens, tmp, res, dayNum, l1, l2;
p = that.params;
// Is the format valid.
tokens = value.split(p.sep);
if (tokens.length !== 3) {
return { err: that.getText('dateErr') };
}
// Year.
if (tokens[2].length !== p.yearDigits) {
return { err: that.getText('dateErr') };
}
// Now we check if the date is valid.
res = {};
if (p.yearDigits === 2) {
l1 = -1;
l2 = 100;
}
else {
l1 = -1;
l2 = 10000;
}
tmp = J.isInt(tokens[2], l1, l2);
if (tmp !== false) res.year = tmp;
else res.err = true;
// Month.
tmp = J.isInt(tokens[p.monthPos], 1, 12, 1, 1);
if (!tmp) res.err = true;
else res.month = tmp;
// 31 or 30 days?
if (tmp === 1 || tmp === 3 || tmp === 5 || tmp === 7 ||
tmp === 8 || tmp === 10 || tmp === 12) {
dayNum = 31;
}
else if (tmp !== 2) {
dayNum = 30;
}
else {
// Is it leap year?
dayNum = (res.year % 4 === 0 && res.year % 100 !== 0) ||
res.year % 400 === 0 ? 29 : 28;
}
res.month = tmp;
// Day.
tmp = J.isInt(tokens[p.dayPos], 1, dayNum, 1, 1);
if (!tmp) res.err = true;
else res.day = tmp;
if (res.err) {
res.err = that.getText('dateErr', 'invalid');
}
else if (p.minDate || p.maxDate) {
tmp = new Date(value);
if (p.minDate.obj && p.minDate.obj > tmp) {
res.err = that.getText('dateErr', 'min');
}
else if (p.maxDate.obj && p.maxDate.obj < tmp) {
res.err = that.getText('dateErr', 'max');
}
}
if (!res.err) {
res.value = value;
res = { value: res };
}
return res;
};
setValues = function() {
var p, minD, maxD, d, day, month, year;
p = that.params;
minD = p.minDate ? p.minDate.obj : new Date('01/01/1900');
maxD = p.maxDate ? p.maxDate.obj : undefined;
d = J.randomDate(minD, maxD);
day = d.getDate();
month = (d.getMonth() + 1);
year = d.getFullYear();
if (p.yearDigits === 2) year = ('' + year).substr(2);
if (p.monthPos === 0) d = month + p.sep + day;
else d = day + p.sep + month;
d += p.sep + year;
return d;
};
}
else if (this.type === 'us_state') {
if (opts.abbreviation) {
this.params.abbr = true;
this.inputWidth = '100px';
}
else {
this.inputWidth = '200px';
}
if (opts.territories !== false) {
this.terr = true;
if (this.params.abbr) {
tmp = getUsStatesList('usStatesTerrByAbbrLow');
}
else {
tmp = getUsStatesList('usStatesTerrLow');
}
}
else {
if (this.params.abbr) {
tmp = getUsStatesList('usStatesByAbbrLow');
}
else {
tmp = getUsStatesList('usStatesLow');
}
}
this.params.usStateVal = tmp;
tmp = function(value) {
var res;
res = { value: value };
if (!that.params.usStateVal[value.toLowerCase()]) {
res.err = that.getText('usStateErr');
}
return res;
};
setValues = function() {
return J.randomKey(that.params.usStateVal);
};
}
else if (this.type === 'us_zip') {
tmp = function(value) {
var res;
res = { value: value };
if (!isValidUSZip(value)) {
res.err = that.getText('usZipErr');
}
return res;
};
setValues = function() {
return Math.floor(Math.random()*90000) + 10000;
};
}
// Lists.
else if (this.type === 'list' ||
this.type === 'us_city_state_zip') {
if (opts.listSeparator) {
if ('string' !== typeof opts.listSeparator) {
throw new TypeError(e + 'listSeparator must be ' +
'string or undefined. Found: ' +
opts.listSeperator);
}
this.params.listSep = opts.listSeparator;
}
else {
this.params.listSep = ',';
}
if (this.type === 'us_city_state_zip') {
getUsStatesList('usStatesTerrByAbbr');
this.params.minItems = this.params.maxItems = 3;
this.params.fixedSize = true;
this.params.itemValidation = function(item, idx) {
if (idx === 2) {
if (!usStatesTerrByAbbr[item.toUpperCase()]) {
return { err: that.getText('usStateAbbrErr') };
}
}
else if (idx === 3) {
if (!isValidUSZip(item)) {
return { err: that.getText('usZipErr') };
}
}
};
this.placeholder = 'Town' + this.params.listSep +
' State' + this.params.listSep + ' ZIP';
}
else {
if ('undefined' !== typeof opts.minItems) {
tmp = J.isInt(opts.minItems, 0);
if (tmp === false) {
throw new TypeError(e + 'minItems must be ' +
'a positive integer. Found: ' +
opts.minItems);
}
this.params.minItems = tmp;
}
else if (this.required) {
this.params.minItems = 1;
}
if ('undefined' !== typeof opts.maxItems) {
tmp = J.isInt(opts.maxItems, 0);
if (tmp === false) {
throw new TypeError(e + 'maxItems must be ' +
'a positive integer. Found: ' +
opts.maxItems);
}
if (this.params.minItems &&
this.params.minItems > tmp) {
throw new TypeError(e + 'maxItems must be larger ' +
'than minItems. Found: ' +
tmp + ' < ' +
this.params.minItems);
}
this.params.maxItems = tmp;
}
}
tmp = function(value) {
var i, len, v, iVal, err;
value = value.split(that.params.listSep);
len = value.length;
if (!len) return value;
iVal = that.params.itemValidation;
i = 0;
v = value[0].trim();
if (!v) return { err: that.getText('listErr') };
if (iVal) {
err = iVal(v, 1);
if (err) return err;
}
value[i++] = v;
if (len > 1) {
v = value[1].trim();
if (!v) return { err: that.getText('listErr') };
if (iVal) {
err = iVal(v, (i+1));
if (err) return err;
}
value[i++] = v;
}
if (len > 2) {
v = value[2].trim();
if (!v) return { err: that.getText('listErr') };
if (iVal) {
err = iVal(v, (i+1));
if (err) return err;
}
value[i++] = v;
}
if (len > 3) {
for ( ; i < len ; ) {
v = value[i].trim();
if (!v) return { err: that.getText('listErr') };
if (iVal) {
err = iVal(v, (i+1));
if (err) return err;
}
value[i++] = v;
}
}
// Need to do it here, because some elements might be empty.
if (that.params.minItems && i < that.params.minItems) {
return { err: that.getText('listSizeErr', 'min') };
}
if (that.params.maxItems && i > that.params.maxItems) {
return { err: that.getText('listSizeErr', 'max') };
}
return { value: value };
};
if (this.type === 'us_city_state_zip') {
setValues = function() {
var sep;
sep = that.params.listSep + ' ';
return J.randomString(8) + sep +
J.randomKey(usStatesTerrByAbbr) + sep +
(Math.floor(Math.random()*90000) + 10000);
};
}
else {
setValues = function(opts) {
var p, minItems, nItems, i, str, sample;
p = that.params;
minItems = p.minItems || 0;
if (opts.availableValues) {
nItems = J.randomInt(minItems,
opts.availableValues.length);
nItems--;
sample = J.sample(0, (nItems-1));
}
else {
nItems = J.randomInt(minItems,
p.maxItems || (minItems + 5));
nItems--;
}
str = '';
for (i = 0; i < nItems; i++) {
if (i !== 0) str += p.listSep + ' ';
if (sample) str += opts.availableValues[sample[i]];
else str += J.randomString(J.randomInt(3,10));
}
return str;
};
}
}
// US_Town,State, Zip Code
// TODO: add other types, e.g., email.
}
// Variable tmp contains a validation function, either from
// defaults, or from user option.
this.validation = function(value) {
var res;
res = { value: value };
if (value.trim() === '') {
if (that.required) res.err = that.getText('emptyErr');
}
else if (tmp) {
res = tmp(value);
}
if (that.userValidation) that.userValidation(res);
return res;
};
this._setValues = setValues;
// Preprocess
if (opts.preprocess) {
if ('function' !== typeof opts.preprocess) {
throw new TypeError(e + 'preprocess must be function or ' +
'undefined. Found: ' + opts.preprocess);
}
this.preprocess = opts.preprocess;
}
else if (opts.preprocess !== false) {
if (this.type === 'date') {
this.preprocess = function(input) {
var sep, len;
len = input.value.length;
sep = that.params.sep;
if (len === 2) {
if (input.selectionStart === 2) {
if (input.value.charAt(1) !== sep) {
input.value += sep;
}
}
}
else if (len === 5) {
if (input.selectionStart === 5) {
if (input.value.charAt(4) !== sep &&
(input.value.split(sep).length - 1) === 1) {
input.value += sep;
}
}
}
else if (len > this.params.dateLen) {
input.value =
input.value.substring(0, this.params.dateLen);
}
};
}
else if (this.type === 'list' ||
this.type === 'us_city_state_zip') {
// Add a space after separator, if separator is not space.
if (this.params.listSep.trim() !== '') {
this.preprocess = function(input) {
var sep, len;
len = input.value.length;
sep = that.params.listSep;
if (len > 1 &&
len === input.selectionStart &&
input.value.charAt(len-1) === sep &&
input.value.charAt(len-2) !== sep) {
input.value += ' ';
}
};
}
}
}
// Postprocess.
if (opts.postprocess) {
if ('function' !== typeof opts.postprocess) {
throw new TypeError(e + 'postprocess must be function or ' +
'undefined. Found: ' + opts.postprocess);
}
this.postprocess = opts.postprocess;
}
else {
// Add postprocess as needed.
}
// Oninput.
if (opts.oninput) {
if ('function' !== typeof opts.oninput) {
throw new TypeError(e + 'oninput must be function or ' +
'undefined. Found: ' + opts.oninput);
}
this.oninput = opts.oninput;
}
// Validation Speed
if ('undefined' !== typeof opts.validationSpeed) {
tmp = J.isInt(opts.valiadtionSpeed, 0, undefined, true);
if (tmp === false) {
throw new TypeError(e + 'validationSpeed must a non-negative ' +
'number or undefined. Found: ' +
opts.validationSpeed);
}
this.validationSpeed = tmp;
}
// MainText, Hint, and other visuals.
if (opts.mainText) {
if ('string' !== typeof opts.mainText) {
throw new TypeError(e + 'mainText must be string or ' +
'undefined. Found: ' + opts.mainText);
}
this.mainText = opts.mainText;
}
if ('undefined' !== typeof opts.hint) {
if (false !== opts.hint && 'string' !== typeof opts.hint) {
throw new TypeError(e + 'hint must be a string, false, or ' +
'undefined. Found: ' + opts.hint);
}
this.hint = opts.hint;
if (this.required) this.hint += ' *';
}
else {
this.hint = this.getText('autoHint');
}
if (opts.placeholder) {
if ('string' !== typeof opts.placeholder) {
throw new TypeError(e + 'placeholder must be string or ' +
'undefined. Found: ' + opts.placeholder);
}
this.placeholder = opts.placeholder;
}
if (opts.width) {
if ('string' !== typeof opts.width) {
throw new TypeError(e + 'width must be string or ' +
'undefined. Found: ' + opts.width);
}
this.inputWidth = opts.width;
}
if (opts.checkboxText) {
if ('string' !== typeof opts.checkboxText) {
throw new TypeError(e + 'checkboxText must be string or ' +
'undefined. Found: ' + opts.checkboxText);
}
this.checkboxText = opts.checkboxText;
}
if (opts.checkboxCb) {
if (!this.checkboxText) {
throw new TypeError(e + 'checkboxCb cannot be defined ' +
'if checkboxText is not defined');
}
if ('function' !== typeof opts.checkboxCb) {
throw new TypeError(e + 'checkboxCb must be function or ' +
'undefined. Found: ' + opts.checkboxCb);
}
this.checkboxCb = opts.checkboxCb;
}
};
/**
* ### CustomInput.append
*
* Implements Widget.append
*
* @see Widget.append
*/
CustomInput.prototype.append = function() {
var that, timeout;
that = this;
// MainText.
if (this.mainText) {
this.spanMainText = W.append('span', this.bodyDiv, {
className: 'custominput-maintext',
innerHTML: this.mainText
});
}
// Hint.
if (this.hint) {
W.append('span', this.spanMainText || this.bodyDiv, {
className: 'custominput-hint',
innerHTML: this.hint
});
}
this.input = W.append('input', this.bodyDiv);
if (this.placeholder) this.input.placeholder = this.placeholder;
if (this.inputWidth) this.input.style.width = this.inputWidth;
this.errorBox = W.append('div', this.bodyDiv, { className: 'errbox' });
this.input.oninput = function() {
if (!that.timeBegin) {
that.timeEnd = that.timeBegin = node.timer.getTimeSince('step');
}
else {
that.timeEnd = node.timer.getTimeSince('step');
}
if (timeout) clearTimeout(timeout);
if (that.isHighlighted()) that.unhighlight();
if (that.preprocess) that.preprocess(that.input);
timeout = setTimeout(function() {
var res;
if (that.validation) {
res = that.validation(that.input.value);
if (res.err) that.setError(res.err);
}
// In case something else needs to be updated.
if (that.oninput) that.oninput(res, that);
}, that.validationSpeed);
};
this.input.onclick = function() {
if (that.isHighlighted()) that.unhighlight();
};
// Checkbox.
if (this.checkboxText) {
this.checkbox = W.append('input', this.bodyDiv, {
type: 'checkbox',
className: 'custominput-checkbox'
});
W.append('span', this.bodyDiv, {
className: 'custominput-checkbox-text',
innerHTML: this.checkboxText
});
if (this.checkboxCb) {
J.addEvent(this.checkbox, 'change', function() {
that.checkboxCb(that.checkbox.checked, that);
});
}
}
};
/**
* ### CustomInput.setError
*
* Set the error msg inside the errorBox and call highlight
*
* @param {string} The error msg (can contain HTML)
*
* @see CustomInput.highlight
* @see CustomInput.errorBox
*/
CustomInput.prototype.setError = function(err) {
this.errorBox.innerHTML = err;
this.highlight();
};
/**
* ### CustomInput.highlight
*
* Highlights the input
*
* @param {string} The style for the table's border.
* Default '3px solid red'
*
* @see CustomInput.highlighted
*/
CustomInput.prototype.highlight = function(border) {
if (border && 'string' !== typeof border) {
throw new TypeError('CustomInput.highlight: border must be ' +
'string or undefined. Found: ' + border);
}
if (!this.input || this.highlighted)