| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511 |
- /*=============================================================================
- Author: Eric M. Barnard - @ericmbarnard
- License: MIT (http://opensource.org/licenses/mit-license.php)
-
- Description: Validation Library for KnockoutJS
- Version: 2.0.3
- ===============================================================================
- */
- /*globals require: false, exports: false, define: false, ko: false */
- (function (factory) {
- // Module systems magic dance.
- if (typeof require === "function" && typeof exports === "object" && typeof module === "object") {
- // CommonJS or Node: hard-coded dependency on "knockout"
- factory(require("knockout"), exports);
- } else if (typeof define === "function" && define["amd"]) {
- // AMD anonymous module with hard-coded dependency on "knockout"
- define(["knockout", "exports"], factory);
- } else {
- // <script> tag: use the global `ko` object, attaching a `mapping` property
- factory(ko, ko.validation = {});
- }
- }(function ( ko, exports ) {
- if (typeof (ko) === 'undefined') {
- throw new Error('Knockout is required, please ensure it is loaded before loading this validation plug-in');
- }
- // create our namespace object
- ko.validation = exports;
- var kv = ko.validation,
- koUtils = ko.utils,
- unwrap = koUtils.unwrapObservable,
- forEach = koUtils.arrayForEach,
- extend = koUtils.extend;
- ;/*global ko: false*/
- var defaults = {
- registerExtenders: true,
- messagesOnModified: true,
- errorsAsTitle: true, // enables/disables showing of errors as title attribute of the target element.
- errorsAsTitleOnModified: false, // shows the error when hovering the input field (decorateElement must be true)
- messageTemplate: null,
- insertMessages: true, // automatically inserts validation messages as <span></span>
- parseInputAttributes: false, // parses the HTML5 validation attribute from a form element and adds that to the object
- writeInputAttributes: false, // adds HTML5 input validation attributes to form elements that ko observable's are bound to
- decorateInputElement: false, // false to keep backward compatibility
- decorateElementOnModified: true,// true to keep backward compatibility
- errorClass: null, // single class for error message and element
- errorElementClass: 'validationElement', // class to decorate error element
- errorMessageClass: 'validationMessage', // class to decorate error message
- allowHtmlMessages: false, // allows HTML in validation messages
- grouping: {
- deep: false, //by default grouping is shallow
- observable: true, //and using observables
- live: false //react to changes to observableArrays if observable === true
- },
- validate: {
- // throttle: 10
- }
- };
- // make a copy so we can use 'reset' later
- var configuration = extend({}, defaults);
- configuration.html5Attributes = ['required', 'pattern', 'min', 'max', 'step'];
- configuration.html5InputTypes = ['email', 'number', 'date'];
- configuration.reset = function () {
- extend(configuration, defaults);
- };
- kv.configuration = configuration;
- ;kv.utils = (function () {
- var seedId = new Date().getTime();
- var domData = {}; //hash of data objects that we reference from dom elements
- var domDataKey = '__ko_validation__';
- return {
- isArray: function (o) {
- return o.isArray || Object.prototype.toString.call(o) === '[object Array]';
- },
- isObject: function (o) {
- return o !== null && typeof o === 'object';
- },
- isNumber: function(o) {
- return !isNaN(o);
- },
- isObservableArray: function(instance) {
- return !!instance &&
- typeof instance["remove"] === "function" &&
- typeof instance["removeAll"] === "function" &&
- typeof instance["destroy"] === "function" &&
- typeof instance["destroyAll"] === "function" &&
- typeof instance["indexOf"] === "function" &&
- typeof instance["replace"] === "function";
- },
- values: function (o) {
- var r = [];
- for (var i in o) {
- if (o.hasOwnProperty(i)) {
- r.push(o[i]);
- }
- }
- return r;
- },
- getValue: function (o) {
- return (typeof o === 'function' ? o() : o);
- },
- hasAttribute: function (node, attr) {
- return node.getAttribute(attr) !== null;
- },
- getAttribute: function (element, attr) {
- return element.getAttribute(attr);
- },
- setAttribute: function (element, attr, value) {
- return element.setAttribute(attr, value);
- },
- isValidatable: function (o) {
- return !!(o && o.rules && o.isValid && o.isModified);
- },
- insertAfter: function (node, newNode) {
- node.parentNode.insertBefore(newNode, node.nextSibling);
- },
- newId: function () {
- return seedId += 1;
- },
- getConfigOptions: function (element) {
- var options = kv.utils.contextFor(element);
- return options || kv.configuration;
- },
- setDomData: function (node, data) {
- var key = node[domDataKey];
- if (!key) {
- node[domDataKey] = key = kv.utils.newId();
- }
- domData[key] = data;
- },
- getDomData: function (node) {
- var key = node[domDataKey];
- if (!key) {
- return undefined;
- }
- return domData[key];
- },
- contextFor: function (node) {
- switch (node.nodeType) {
- case 1:
- case 8:
- var context = kv.utils.getDomData(node);
- if (context) { return context; }
- if (node.parentNode) { return kv.utils.contextFor(node.parentNode); }
- break;
- }
- return undefined;
- },
- isEmptyVal: function (val) {
- if (val === undefined) {
- return true;
- }
- if (val === null) {
- return true;
- }
- if (val === "") {
- return true;
- }
- },
- getOriginalElementTitle: function (element) {
- var savedOriginalTitle = kv.utils.getAttribute(element, 'data-orig-title'),
- currentTitle = element.title,
- hasSavedOriginalTitle = kv.utils.hasAttribute(element, 'data-orig-title');
- return hasSavedOriginalTitle ?
- savedOriginalTitle : currentTitle;
- },
- async: function (expr) {
- if (window.setImmediate) { window.setImmediate(expr); }
- else { window.setTimeout(expr, 0); }
- },
- forEach: function (object, callback) {
- if (kv.utils.isArray(object)) {
- return forEach(object, callback);
- }
- for (var prop in object) {
- if (object.hasOwnProperty(prop)) {
- callback(object[prop], prop);
- }
- }
- }
- };
- }());;var api = (function () {
- var isInitialized = 0,
- configuration = kv.configuration,
- utils = kv.utils;
- function cleanUpSubscriptions(context) {
- forEach(context.subscriptions, function (subscription) {
- subscription.dispose();
- });
- context.subscriptions = [];
- }
- function dispose(context) {
- if (context.options.deep) {
- forEach(context.flagged, function (obj) {
- delete obj.__kv_traversed;
- });
- context.flagged.length = 0;
- }
- if (!context.options.live) {
- cleanUpSubscriptions(context);
- }
- }
- function runTraversal(obj, context) {
- context.validatables = [];
- cleanUpSubscriptions(context);
- traverseGraph(obj, context);
- dispose(context);
- }
- function traverseGraph(obj, context, level) {
- var objValues = [],
- val = obj.peek ? obj.peek() : obj;
- if (obj.__kv_traversed === true) {
- return;
- }
- if (context.options.deep) {
- obj.__kv_traversed = true;
- context.flagged.push(obj);
- }
- //default level value depends on deep option.
- level = (level !== undefined ? level : context.options.deep ? 1 : -1);
- // if object is observable then add it to the list
- if (ko.isObservable(obj)) {
- // ensure it's validatable but don't extend validatedObservable because it
- // would overwrite isValid property.
- if (!obj.errors && !utils.isValidatable(obj)) {
- obj.extend({ validatable: true });
- }
- context.validatables.push(obj);
- if (context.options.live && utils.isObservableArray(obj)) {
- context.subscriptions.push(obj.subscribe(function () {
- context.graphMonitor.valueHasMutated();
- }));
- }
- }
- //get list of values either from array or object but ignore non-objects
- // and destroyed objects
- if (val && !val._destroy) {
- if (utils.isArray(val)) {
- objValues = val;
- }
- else if (utils.isObject(val)) {
- objValues = utils.values(val);
- }
- }
- //process recursively if it is deep grouping
- if (level !== 0) {
- utils.forEach(objValues, function (observable) {
- //but not falsy things and not HTML Elements
- if (observable && !observable.nodeType && (!ko.isComputed(observable) || observable.rules)) {
- traverseGraph(observable, context, level + 1);
- }
- });
- }
- }
- function collectErrors(array) {
- var errors = [];
- forEach(array, function (observable) {
- // Do not collect validatedObservable errors
- if (utils.isValidatable(observable) && !observable.isValid()) {
- // Use peek because we don't want a dependency for 'error' property because it
- // changes before 'isValid' does. (Issue #99)
- errors.push(observable.error.peek());
- }
- });
- return errors;
- }
- return {
- //Call this on startup
- //any config can be overridden with the passed in options
- init: function (options, force) {
- //done run this multiple times if we don't really want to
- if (isInitialized > 0 && !force) {
- return;
- }
- //because we will be accessing options properties it has to be an object at least
- options = options || {};
- //if specific error classes are not provided then apply generic errorClass
- //it has to be done on option so that options.errorClass can override default
- //errorElementClass and errorMessage class but not those provided in options
- options.errorElementClass = options.errorElementClass || options.errorClass || configuration.errorElementClass;
- options.errorMessageClass = options.errorMessageClass || options.errorClass || configuration.errorMessageClass;
- extend(configuration, options);
- if (configuration.registerExtenders) {
- kv.registerExtenders();
- }
- isInitialized = 1;
- },
- // resets the config back to its original state
- reset: kv.configuration.reset,
- // recursively walks a viewModel and creates an object that
- // provides validation information for the entire viewModel
- // obj -> the viewModel to walk
- // options -> {
- // deep: false, // if true, will walk past the first level of viewModel properties
- // observable: false // if true, returns a computed observable indicating if the viewModel is valid
- // }
- group: function group(obj, options) { // array of observables or viewModel
- options = extend(extend({}, configuration.grouping), options);
- var context = {
- options: options,
- graphMonitor: ko.observable(),
- flagged: [],
- subscriptions: [],
- validatables: []
- };
- var result = null;
- //if using observables then traverse structure once and add observables
- if (options.observable) {
- result = ko.computed(function () {
- context.graphMonitor(); //register dependency
- runTraversal(obj, context);
- return collectErrors(context.validatables);
- });
- }
- else { //if not using observables then every call to error() should traverse the structure
- result = function () {
- runTraversal(obj, context);
- return collectErrors(context.validatables);
- };
- }
- result.showAllMessages = function (show) { // thanks @heliosPortal
- if (show === undefined) {//default to true
- show = true;
- }
- result.forEach(function (observable) {
- if (utils.isValidatable(observable)) {
- observable.isModified(show);
- }
- });
- };
- result.isAnyMessageShown = function () {
- var invalidAndModifiedPresent;
- invalidAndModifiedPresent = !!result.find(function (observable) {
- return utils.isValidatable(observable) && !observable.isValid() && observable.isModified();
- });
- return invalidAndModifiedPresent;
- };
- result.filter = function(predicate) {
- predicate = predicate || function () { return true; };
- // ensure we have latest changes
- result();
- return koUtils.arrayFilter(context.validatables, predicate);
- };
- result.find = function(predicate) {
- predicate = predicate || function () { return true; };
- // ensure we have latest changes
- result();
- return koUtils.arrayFirst(context.validatables, predicate);
- };
- result.forEach = function(callback) {
- callback = callback || function () { };
- // ensure we have latest changes
- result();
- forEach(context.validatables, callback);
- };
- result.map = function(mapping) {
- mapping = mapping || function (item) { return item; };
- // ensure we have latest changes
- result();
- return koUtils.arrayMap(context.validatables, mapping);
- };
- /**
- * @private You should not rely on this method being here.
- * It's a private method and it may change in the future.
- *
- * @description Updates the validated object and collects errors from it.
- */
- result._updateState = function(newValue) {
- if (!utils.isObject(newValue)) {
- throw new Error('An object is required.');
- }
- obj = newValue;
- if (options.observable) {
- context.graphMonitor.valueHasMutated();
- }
- else {
- runTraversal(newValue, context);
- return collectErrors(context.validatables);
- }
- };
- return result;
- },
- formatMessage: function (message, params, observable) {
- if (utils.isObject(params) && params.typeAttr) {
- params = params.value;
- }
- if (typeof message === 'function') {
- return message(params, observable);
- }
- var replacements = unwrap(params);
- if (replacements == null) {
- replacements = [];
- }
- if (!utils.isArray(replacements)) {
- replacements = [replacements];
- }
- return message.replace(/{(\d+)}/gi, function(match, index) {
- if (typeof replacements[index] !== 'undefined') {
- return replacements[index];
- }
- return match;
- });
- },
- // addRule:
- // This takes in a ko.observable and a Rule Context - which is just a rule name and params to supply to the validator
- // ie: kv.addRule(myObservable, {
- // rule: 'required',
- // params: true
- // });
- //
- addRule: function (observable, rule) {
- observable.extend({ validatable: true });
- var hasRule = !!koUtils.arrayFirst(observable.rules(), function(item) {
- return item.rule && item.rule === rule.rule;
- });
- if (!hasRule) {
- //push a Rule Context to the observables local array of Rule Contexts
- observable.rules.push(rule);
- }
- return observable;
- },
- // addAnonymousRule:
- // Anonymous Rules essentially have all the properties of a Rule, but are only specific for a certain property
- // and developers typically are wanting to add them on the fly or not register a rule with the 'kv.rules' object
- //
- // Example:
- // var test = ko.observable('something').extend{(
- // validation: {
- // validator: function(val, someOtherVal){
- // return true;
- // },
- // message: "Something must be really wrong!',
- // params: true
- // }
- // )};
- addAnonymousRule: function (observable, ruleObj) {
- if (ruleObj['message'] === undefined) {
- ruleObj['message'] = 'Error';
- }
- //make sure onlyIf is honoured
- if (ruleObj.onlyIf) {
- ruleObj.condition = ruleObj.onlyIf;
- }
- //add the anonymous rule to the observable
- kv.addRule(observable, ruleObj);
- },
- addExtender: function (ruleName) {
- ko.extenders[ruleName] = function (observable, params) {
- //params can come in a few flavors
- // 1. Just the params to be passed to the validator
- // 2. An object containing the Message to be used and the Params to pass to the validator
- // 3. A condition when the validation rule to be applied
- //
- // Example:
- // var test = ko.observable(3).extend({
- // max: {
- // message: 'This special field has a Max of {0}',
- // params: 2,
- // onlyIf: function() {
- // return specialField.IsVisible();
- // }
- // }
- // )};
- //
- if (params && (params.message || params.onlyIf)) { //if it has a message or condition object, then its an object literal to use
- return kv.addRule(observable, {
- rule: ruleName,
- message: params.message,
- params: utils.isEmptyVal(params.params) ? true : params.params,
- condition: params.onlyIf
- });
- } else {
- return kv.addRule(observable, {
- rule: ruleName,
- params: params
- });
- }
- };
- },
- // loops through all kv.rules and adds them as extenders to
- // ko.extenders
- registerExtenders: function () { // root extenders optional, use 'validation' extender if would cause conflicts
- if (configuration.registerExtenders) {
- for (var ruleName in kv.rules) {
- if (kv.rules.hasOwnProperty(ruleName)) {
- if (!ko.extenders[ruleName]) {
- kv.addExtender(ruleName);
- }
- }
- }
- }
- },
- //creates a span next to the @element with the specified error class
- insertValidationMessage: function (element) {
- var span = document.createElement('SPAN');
- span.className = utils.getConfigOptions(element).errorMessageClass;
- utils.insertAfter(element, span);
- return span;
- },
- // if html-5 validation attributes have been specified, this parses
- // the attributes on @element
- parseInputValidationAttributes: function (element, valueAccessor) {
- forEach(kv.configuration.html5Attributes, function (attr) {
- if (utils.hasAttribute(element, attr)) {
- var params = element.getAttribute(attr) || true;
- if (attr === 'min' || attr === 'max')
- {
- // If we're validating based on the min and max attributes, we'll
- // need to know what the 'type' attribute is set to
- var typeAttr = element.getAttribute('type');
- if (typeof typeAttr === "undefined" || !typeAttr)
- {
- // From http://www.w3.org/TR/html-markup/input:
- // An input element with no type attribute specified represents the
- // same thing as an input element with its type attribute set to "text".
- typeAttr = "text";
- }
- params = {typeAttr: typeAttr, value: params};
- }
- kv.addRule(valueAccessor(), {
- rule: attr,
- params: params
- });
- }
- });
- var currentType = element.getAttribute('type');
- forEach(kv.configuration.html5InputTypes, function (type) {
- if (type === currentType) {
- kv.addRule(valueAccessor(), {
- rule: (type === 'date') ? 'dateISO' : type,
- params: true
- });
- }
- });
- },
- // writes html5 validation attributes on the element passed in
- writeInputValidationAttributes: function (element, valueAccessor) {
- var observable = valueAccessor();
- if (!observable || !observable.rules) {
- return;
- }
- var contexts = observable.rules(); // observable array
- // loop through the attributes and add the information needed
- forEach(kv.configuration.html5Attributes, function (attr) {
- var ctx = koUtils.arrayFirst(contexts, function (ctx) {
- return ctx.rule && ctx.rule.toLowerCase() === attr.toLowerCase();
- });
- if (!ctx) {
- return;
- }
- // we have a rule matching a validation attribute at this point
- // so lets add it to the element along with the params
- ko.computed({
- read: function() {
- var params = ko.unwrap(ctx.params);
- // we have to do some special things for the pattern validation
- if (ctx.rule === "pattern" && params instanceof RegExp) {
- // we need the pure string representation of the RegExpr without the //gi stuff
- params = params.source;
- }
- element.setAttribute(attr, params);
- },
- disposeWhenNodeIsRemoved: element
- });
- });
- contexts = null;
- },
- //take an existing binding handler and make it cause automatic validations
- makeBindingHandlerValidatable: function (handlerName) {
- var init = ko.bindingHandlers[handlerName].init;
- ko.bindingHandlers[handlerName].init = function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
- init(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
- return ko.bindingHandlers['validationCore'].init(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
- };
- },
- // visit an objects properties and apply validation rules from a definition
- setRules: function (target, definition) {
- var setRules = function (target, definition) {
- if (!target || !definition) { return; }
- for (var prop in definition) {
- if (!definition.hasOwnProperty(prop)) { continue; }
- var ruleDefinitions = definition[prop];
- //check the target property exists and has a value
- if (!target[prop]) { continue; }
- var targetValue = target[prop],
- unwrappedTargetValue = unwrap(targetValue),
- rules = {},
- nonRules = {};
- for (var rule in ruleDefinitions) {
- if (!ruleDefinitions.hasOwnProperty(rule)) { continue; }
- if (kv.rules[rule]) {
- rules[rule] = ruleDefinitions[rule];
- } else {
- nonRules[rule] = ruleDefinitions[rule];
- }
- }
- //apply rules
- if (ko.isObservable(targetValue)) {
- targetValue.extend(rules);
- }
- //then apply child rules
- //if it's an array, apply rules to all children
- if (unwrappedTargetValue && utils.isArray(unwrappedTargetValue)) {
- for (var i = 0; i < unwrappedTargetValue.length; i++) {
- setRules(unwrappedTargetValue[i], nonRules);
- }
- //otherwise, just apply to this property
- } else {
- setRules(unwrappedTargetValue, nonRules);
- }
- }
- };
- setRules(target, definition);
- }
- };
- }());
- // expose api publicly
- extend(ko.validation, api);
- ;//Validation Rules:
- // You can view and override messages or rules via:
- // kv.rules[ruleName]
- //
- // To implement a custom Rule, simply use this template:
- // kv.rules['<custom rule name>'] = {
- // validator: function (val, param) {
- // <custom logic>
- // return <true or false>;
- // },
- // message: '<custom validation message>' //optionally you can also use a '{0}' to denote a placeholder that will be replaced with your 'param'
- // };
- //
- // Example:
- // kv.rules['mustEqual'] = {
- // validator: function( val, mustEqualVal ){
- // return val === mustEqualVal;
- // },
- // message: 'This field must equal {0}'
- // };
- //
- kv.rules = {};
- kv.rules['required'] = {
- validator: function (val, required) {
- var testVal;
- if (val === undefined || val === null) {
- return !required;
- }
- testVal = val;
- if (typeof (val) === 'string') {
- if (String.prototype.trim) {
- testVal = val.trim();
- }
- else {
- testVal = val.replace(/^\s+|\s+$/g, '');
- }
- }
- if (!required) {// if they passed: { required: false }, then don't require this
- return true;
- }
- return ((testVal + '').length > 0);
- },
- message: 'This field is required.'
- };
- function minMaxValidatorFactory(validatorName) {
- var isMaxValidation = validatorName === "max";
- return function (val, options) {
- if (kv.utils.isEmptyVal(val)) {
- return true;
- }
- var comparisonValue, type;
- if (options.typeAttr === undefined) {
- // This validator is being called from javascript rather than
- // being bound from markup
- type = "text";
- comparisonValue = options;
- } else {
- type = options.typeAttr;
- comparisonValue = options.value;
- }
- // From http://www.w3.org/TR/2012/WD-html5-20121025/common-input-element-attributes.html#attr-input-min,
- // if the value is parseable to a number, then the minimum should be numeric
- if (!isNaN(comparisonValue) && !(comparisonValue instanceof Date)) {
- type = "number";
- }
- var regex, valMatches, comparisonValueMatches;
- switch (type.toLowerCase()) {
- case "week":
- regex = /^(\d{4})-W(\d{2})$/;
- valMatches = val.match(regex);
- if (valMatches === null) {
- throw new Error("Invalid value for " + validatorName + " attribute for week input. Should look like " +
- "'2000-W33' http://www.w3.org/TR/html-markup/input.week.html#input.week.attrs.min");
- }
- comparisonValueMatches = comparisonValue.match(regex);
- // If no regex matches were found, validation fails
- if (!comparisonValueMatches) {
- return false;
- }
- if (isMaxValidation) {
- return (valMatches[1] < comparisonValueMatches[1]) || // older year
- // same year, older week
- ((valMatches[1] === comparisonValueMatches[1]) && (valMatches[2] <= comparisonValueMatches[2]));
- } else {
- return (valMatches[1] > comparisonValueMatches[1]) || // newer year
- // same year, newer week
- ((valMatches[1] === comparisonValueMatches[1]) && (valMatches[2] >= comparisonValueMatches[2]));
- }
- break;
- case "month":
- regex = /^(\d{4})-(\d{2})$/;
- valMatches = val.match(regex);
- if (valMatches === null) {
- throw new Error("Invalid value for " + validatorName + " attribute for month input. Should look like " +
- "'2000-03' http://www.w3.org/TR/html-markup/input.month.html#input.month.attrs.min");
- }
- comparisonValueMatches = comparisonValue.match(regex);
- // If no regex matches were found, validation fails
- if (!comparisonValueMatches) {
- return false;
- }
- if (isMaxValidation) {
- return ((valMatches[1] < comparisonValueMatches[1]) || // older year
- // same year, older month
- ((valMatches[1] === comparisonValueMatches[1]) && (valMatches[2] <= comparisonValueMatches[2])));
- } else {
- return (valMatches[1] > comparisonValueMatches[1]) || // newer year
- // same year, newer month
- ((valMatches[1] === comparisonValueMatches[1]) && (valMatches[2] >= comparisonValueMatches[2]));
- }
- break;
- case "number":
- case "range":
- if (isMaxValidation) {
- return (!isNaN(val) && parseFloat(val) <= parseFloat(comparisonValue));
- } else {
- return (!isNaN(val) && parseFloat(val) >= parseFloat(comparisonValue));
- }
- break;
- default:
- if (isMaxValidation) {
- return val <= comparisonValue;
- } else {
- return val >= comparisonValue;
- }
- }
- };
- }
- kv.rules['min'] = {
- validator: minMaxValidatorFactory("min"),
- message: 'Please enter a value greater than or equal to {0}.'
- };
- kv.rules['max'] = {
- validator: minMaxValidatorFactory("max"),
- message: 'Please enter a value less than or equal to {0}.'
- };
- kv.rules['minLength'] = {
- validator: function (val, minLength) {
- if(kv.utils.isEmptyVal(val)) { return true; }
- var normalizedVal = kv.utils.isNumber(val) ? ('' + val) : val;
- return normalizedVal.length >= minLength;
- },
- message: 'Please enter at least {0} characters.'
- };
- kv.rules['maxLength'] = {
- validator: function (val, maxLength) {
- if(kv.utils.isEmptyVal(val)) { return true; }
- var normalizedVal = kv.utils.isNumber(val) ? ('' + val) : val;
- return normalizedVal.length <= maxLength;
- },
- message: 'Please enter no more than {0} characters.'
- };
- kv.rules['pattern'] = {
- validator: function (val, regex) {
- return kv.utils.isEmptyVal(val) || val.toString().match(regex) !== null;
- },
- message: 'Please check this value.'
- };
- kv.rules['step'] = {
- validator: function (val, step) {
- // in order to handle steps of .1 & .01 etc.. Modulus won't work
- // if the value is a decimal, so we have to correct for that
- if (kv.utils.isEmptyVal(val) || step === 'any') { return true; }
- var dif = (val * 100) % (step * 100);
- return Math.abs(dif) < 0.00001 || Math.abs(1 - dif) < 0.00001;
- },
- message: 'The value must increment by {0}.'
- };
- kv.rules['email'] = {
- validator: function (val, validate) {
- if (!validate) { return true; }
- //I think an empty email address is also a valid entry
- //if one want's to enforce entry it should be done with 'required: true'
- return kv.utils.isEmptyVal(val) || (
- // jquery validate regex - thanks Scott Gonzalez
- validate && /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i.test(val)
- );
- },
- message: 'Please enter a proper email address.'
- };
- kv.rules['date'] = {
- validator: function (value, validate) {
- if (!validate) { return true; }
- return kv.utils.isEmptyVal(value) || (validate && !/Invalid|NaN/.test(new Date(value)));
- },
- message: 'Please enter a proper date.'
- };
- kv.rules['dateISO'] = {
- validator: function (value, validate) {
- if (!validate) { return true; }
- return kv.utils.isEmptyVal(value) || (validate && /^\d{4}[-/](?:0?[1-9]|1[012])[-/](?:0?[1-9]|[12][0-9]|3[01])$/.test(value));
- },
- message: 'Please enter a proper date.'
- };
- kv.rules['number'] = {
- validator: function (value, validate) {
- if (!validate) { return true; }
- return kv.utils.isEmptyVal(value) || (validate && /^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value));
- },
- message: 'Please enter a number.'
- };
- kv.rules['digit'] = {
- validator: function (value, validate) {
- if (!validate) { return true; }
- return kv.utils.isEmptyVal(value) || (validate && /^\d+$/.test(value));
- },
- message: 'Please enter a digit.'
- };
- kv.rules['phoneUS'] = {
- validator: function (phoneNumber, validate) {
- if (!validate) { return true; }
- if (kv.utils.isEmptyVal(phoneNumber)) { return true; } // makes it optional, use 'required' rule if it should be required
- if (typeof (phoneNumber) !== 'string') { return false; }
- phoneNumber = phoneNumber.replace(/\s+/g, "");
- return validate && phoneNumber.length > 9 && phoneNumber.match(/^(1-?)?(\([2-9]\d{2}\)|[2-9]\d{2})-?[2-9]\d{2}-?\d{4}$/);
- },
- message: 'Please specify a valid phone number.'
- };
- kv.rules['equal'] = {
- validator: function (val, params) {
- var otherValue = params;
- return val === kv.utils.getValue(otherValue);
- },
- message: 'Values must equal.'
- };
- kv.rules['notEqual'] = {
- validator: function (val, params) {
- var otherValue = params;
- return val !== kv.utils.getValue(otherValue);
- },
- message: 'Please choose another value.'
- };
- //unique in collection
- // options are:
- // collection: array or function returning (observable) array
- // in which the value has to be unique
- // valueAccessor: function that returns value from an object stored in collection
- // if it is null the value is compared directly
- // external: set to true when object you are validating is automatically updating collection
- kv.rules['unique'] = {
- validator: function (val, options) {
- var c = kv.utils.getValue(options.collection),
- external = kv.utils.getValue(options.externalValue),
- counter = 0;
- if (!val || !c) { return true; }
- koUtils.arrayFilter(c, function (item) {
- if (val === (options.valueAccessor ? options.valueAccessor(item) : item)) { counter++; }
- });
- // if value is external even 1 same value in collection means the value is not unique
- return counter < (!!external ? 1 : 2);
- },
- message: 'Please make sure the value is unique.'
- };
- //now register all of these!
- (function () {
- kv.registerExtenders();
- }());
- ;// The core binding handler
- // this allows us to setup any value binding that internally always
- // performs the same functionality
- ko.bindingHandlers['validationCore'] = (function () {
- return {
- init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
- var config = kv.utils.getConfigOptions(element);
- var observable = valueAccessor();
- // parse html5 input validation attributes, optional feature
- if (config.parseInputAttributes) {
- kv.utils.async(function () { kv.parseInputValidationAttributes(element, valueAccessor); });
- }
- // if requested insert message element and apply bindings
- if (config.insertMessages && kv.utils.isValidatable(observable)) {
- // insert the <span></span>
- var validationMessageElement = kv.insertValidationMessage(element);
- // if we're told to use a template, make sure that gets rendered
- if (config.messageTemplate) {
- ko.renderTemplate(config.messageTemplate, { field: observable }, null, validationMessageElement, 'replaceNode');
- } else {
- ko.applyBindingsToNode(validationMessageElement, { validationMessage: observable });
- }
- }
- // write the html5 attributes if indicated by the config
- if (config.writeInputAttributes && kv.utils.isValidatable(observable)) {
- kv.writeInputValidationAttributes(element, valueAccessor);
- }
- // if requested, add binding to decorate element
- if (config.decorateInputElement && kv.utils.isValidatable(observable)) {
- ko.applyBindingsToNode(element, { validationElement: observable });
- }
- }
- };
- }());
- // override for KO's default 'value', 'checked', 'textInput' and selectedOptions bindings
- kv.makeBindingHandlerValidatable("value");
- kv.makeBindingHandlerValidatable("checked");
- if (ko.bindingHandlers.textInput) {
- kv.makeBindingHandlerValidatable("textInput");
- }
- kv.makeBindingHandlerValidatable("selectedOptions");
- ko.bindingHandlers['validationMessage'] = { // individual error message, if modified or post binding
- update: function (element, valueAccessor) {
- var obsv = valueAccessor(),
- config = kv.utils.getConfigOptions(element),
- val = unwrap(obsv),
- msg = null,
- isModified = false,
- isValid = false;
- if (obsv === null || typeof obsv === 'undefined') {
- throw new Error('Cannot bind validationMessage to undefined value. data-bind expression: ' +
- element.getAttribute('data-bind'));
- }
- isModified = obsv.isModified && obsv.isModified();
- isValid = obsv.isValid && obsv.isValid();
- var error = null;
- if (!config.messagesOnModified || isModified) {
- error = isValid ? null : obsv.error;
- }
- var isVisible = !config.messagesOnModified || isModified ? !isValid : false;
- var isCurrentlyVisible = element.style.display !== "none";
- if (config.allowHtmlMessages) {
- koUtils.setHtml(element, error);
- } else {
- ko.bindingHandlers.text.update(element, function () { return error; });
- }
- if (isCurrentlyVisible && !isVisible) {
- element.style.display = 'none';
- } else if (!isCurrentlyVisible && isVisible) {
- element.style.display = '';
- }
- }
- };
- ko.bindingHandlers['validationElement'] = {
- update: function (element, valueAccessor, allBindingsAccessor) {
- var obsv = valueAccessor(),
- config = kv.utils.getConfigOptions(element),
- val = unwrap(obsv),
- msg = null,
- isModified = false,
- isValid = false;
- if (obsv === null || typeof obsv === 'undefined') {
- throw new Error('Cannot bind validationElement to undefined value. data-bind expression: ' +
- element.getAttribute('data-bind'));
- }
- isModified = obsv.isModified && obsv.isModified();
- isValid = obsv.isValid && obsv.isValid();
- // create an evaluator function that will return something like:
- // css: { validationElement: true }
- var cssSettingsAccessor = function () {
- var css = {};
- var shouldShow = ((!config.decorateElementOnModified || isModified) ? !isValid : false);
- // css: { validationElement: false }
- css[config.errorElementClass] = shouldShow;
- return css;
- };
- //add or remove class on the element;
- ko.bindingHandlers.css.update(element, cssSettingsAccessor, allBindingsAccessor);
- if (!config.errorsAsTitle) { return; }
- ko.bindingHandlers.attr.update(element, function () {
- var
- hasModification = !config.errorsAsTitleOnModified || isModified,
- title = kv.utils.getOriginalElementTitle(element);
- if (hasModification && !isValid) {
- return { title: obsv.error, 'data-orig-title': title };
- } else if (!hasModification || isValid) {
- return { title: title, 'data-orig-title': null };
- }
- });
- }
- };
- // ValidationOptions:
- // This binding handler allows you to override the initial config by setting any of the options for a specific element or context of elements
- //
- // Example:
- // <div data-bind="validationOptions: { insertMessages: true, messageTemplate: 'customTemplate', errorMessageClass: 'mySpecialClass'}">
- // <input type="text" data-bind="value: someValue"/>
- // <input type="text" data-bind="value: someValue2"/>
- // </div>
- ko.bindingHandlers['validationOptions'] = (function () {
- return {
- init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
- var options = unwrap(valueAccessor());
- if (options) {
- var newConfig = extend({}, kv.configuration);
- extend(newConfig, options);
- //store the validation options on the node so we can retrieve it later
- kv.utils.setDomData(element, newConfig);
- }
- }
- };
- }());
- ;// Validation Extender:
- // This is for creating custom validation logic on the fly
- // Example:
- // var test = ko.observable('something').extend{(
- // validation: {
- // validator: function(val, someOtherVal){
- // return true;
- // },
- // message: "Something must be really wrong!',
- // params: true
- // }
- // )};
- ko.extenders['validation'] = function (observable, rules) { // allow single rule or array
- forEach(kv.utils.isArray(rules) ? rules : [rules], function (rule) {
- // the 'rule' being passed in here has no name to identify a core Rule,
- // so we add it as an anonymous rule
- // If the developer is wanting to use a core Rule, but use a different message see the 'addExtender' logic for examples
- kv.addAnonymousRule(observable, rule);
- });
- return observable;
- };
- //This is the extender that makes a Knockout Observable also 'Validatable'
- //examples include:
- // 1. var test = ko.observable('something').extend({validatable: true});
- // this will ensure that the Observable object is setup properly to respond to rules
- //
- // 2. test.extend({validatable: false});
- // this will remove the validation properties from the Observable object should you need to do that.
- ko.extenders['validatable'] = function (observable, options) {
- if (!kv.utils.isObject(options)) {
- options = { enable: options };
- }
- if (!('enable' in options)) {
- options.enable = true;
- }
- if (options.enable && !kv.utils.isValidatable(observable)) {
- var config = kv.configuration.validate || {};
- var validationOptions = {
- throttleEvaluation : options.throttle || config.throttle
- };
- observable.error = ko.observable(null); // holds the error message, we only need one since we stop processing validators when one is invalid
- // observable.rules:
- // ObservableArray of Rule Contexts, where a Rule Context is simply the name of a rule and the params to supply to it
- //
- // Rule Context = { rule: '<rule name>', params: '<passed in params>', message: '<Override of default Message>' }
- observable.rules = ko.observableArray(); //holds the rule Contexts to use as part of validation
- //in case async validation is occurring
- observable.isValidating = ko.observable(false);
- //the true holder of whether the observable is valid or not
- observable.__valid__ = ko.observable(true);
- observable.isModified = ko.observable(false);
- // a semi-protected observable
- observable.isValid = ko.computed(observable.__valid__);
- //manually set error state
- observable.setError = function (error) {
- var previousError = observable.error.peek();
- var previousIsValid = observable.__valid__.peek();
- observable.error(error);
- observable.__valid__(false);
- if (previousError !== error && !previousIsValid) {
- // if the observable was not valid before then isValid will not mutate,
- // hence causing any grouping to not display the latest error.
- observable.isValid.notifySubscribers();
- }
- };
- //manually clear error state
- observable.clearError = function () {
- observable.error(null);
- observable.__valid__(true);
- return observable;
- };
- //subscribe to changes in the observable
- var h_change = observable.subscribe(function () {
- observable.isModified(true);
- });
- // we use a computed here to ensure that anytime a dependency changes, the
- // validation logic evaluates
- var h_obsValidationTrigger = ko.computed(extend({
- read: function () {
- var obs = observable(),
- ruleContexts = observable.rules();
- kv.validateObservable(observable);
- return true;
- }
- }, validationOptions));
- extend(h_obsValidationTrigger, validationOptions);
- observable._disposeValidation = function () {
- //first dispose of the subscriptions
- observable.isValid.dispose();
- observable.rules.removeAll();
- h_change.dispose();
- h_obsValidationTrigger.dispose();
- delete observable['rules'];
- delete observable['error'];
- delete observable['isValid'];
- delete observable['isValidating'];
- delete observable['__valid__'];
- delete observable['isModified'];
- delete observable['setError'];
- delete observable['clearError'];
- delete observable['_disposeValidation'];
- };
- } else if (options.enable === false && observable._disposeValidation) {
- observable._disposeValidation();
- }
- return observable;
- };
- function validateSync(observable, rule, ctx) {
- //Execute the validator and see if its valid
- if (!rule.validator(observable(), (ctx.params === undefined ? true : unwrap(ctx.params)))) { // default param is true, eg. required = true
- //not valid, so format the error message and stick it in the 'error' variable
- observable.setError(kv.formatMessage(
- ctx.message || rule.message,
- unwrap(ctx.params),
- observable));
- return false;
- } else {
- return true;
- }
- }
- function validateAsync(observable, rule, ctx) {
- observable.isValidating(true);
- var callBack = function (valObj) {
- var isValid = false,
- msg = '';
- if (!observable.__valid__()) {
- // since we're returning early, make sure we turn this off
- observable.isValidating(false);
- return; //if its already NOT valid, don't add to that
- }
- //we were handed back a complex object
- if (valObj['message']) {
- isValid = valObj.isValid;
- msg = valObj.message;
- } else {
- isValid = valObj;
- }
- if (!isValid) {
- //not valid, so format the error message and stick it in the 'error' variable
- observable.error(kv.formatMessage(
- msg || ctx.message || rule.message,
- unwrap(ctx.params),
- observable));
- observable.__valid__(isValid);
- }
- // tell it that we're done
- observable.isValidating(false);
- };
- kv.utils.async(function() {
- //fire the validator and hand it the callback
- rule.validator(observable(), ctx.params === undefined ? true : unwrap(ctx.params), callBack);
- });
- }
- kv.validateObservable = function (observable) {
- var i = 0,
- rule, // the rule validator to execute
- ctx, // the current Rule Context for the loop
- ruleContexts = observable.rules(), //cache for iterator
- len = ruleContexts.length; //cache for iterator
- for (; i < len; i++) {
- //get the Rule Context info to give to the core Rule
- ctx = ruleContexts[i];
- // checks an 'onlyIf' condition
- if (ctx.condition && !ctx.condition()) {
- continue;
- }
- //get the core Rule to use for validation
- rule = ctx.rule ? kv.rules[ctx.rule] : ctx;
- if (rule['async'] || ctx['async']) {
- //run async validation
- validateAsync(observable, rule, ctx);
- } else {
- //run normal sync validation
- if (!validateSync(observable, rule, ctx)) {
- return false; //break out of the loop
- }
- }
- }
- //finally if we got this far, make the observable valid again!
- observable.clearError();
- return true;
- };
- ;
- var _locales = {};
- var _currentLocale;
- kv.defineLocale = function(name, values) {
- if (name && values) {
- _locales[name.toLowerCase()] = values;
- return values;
- }
- return null;
- };
- kv.locale = function(name) {
- if (name) {
- name = name.toLowerCase();
- if (_locales.hasOwnProperty(name)) {
- kv.localize(_locales[name]);
- _currentLocale = name;
- }
- else {
- throw new Error('Localization ' + name + ' has not been loaded.');
- }
- }
- return _currentLocale;
- };
- //quick function to override rule messages
- kv.localize = function (msgTranslations) {
- var rules = kv.rules;
- //loop the properties in the object and assign the msg to the rule
- for (var ruleName in msgTranslations) {
- if (rules.hasOwnProperty(ruleName)) {
- rules[ruleName].message = msgTranslations[ruleName];
- }
- }
- };
- // Populate default locale (this will make en-US.js somewhat redundant)
- (function() {
- var localeData = {};
- var rules = kv.rules;
- for (var ruleName in rules) {
- if (rules.hasOwnProperty(ruleName)) {
- localeData[ruleName] = rules[ruleName].message;
- }
- }
- kv.defineLocale('en-us', localeData);
- })();
- // No need to invoke locale because the messages are already defined along with the rules for en-US
- _currentLocale = 'en-us';
- ;/**
- * Possible invocations:
- * applyBindingsWithValidation(viewModel)
- * applyBindingsWithValidation(viewModel, options)
- * applyBindingsWithValidation(viewModel, rootNode)
- * applyBindingsWithValidation(viewModel, rootNode, options)
- */
- ko.applyBindingsWithValidation = function (viewModel, rootNode, options) {
- var node = document.body,
- config;
- if (rootNode && rootNode.nodeType) {
- node = rootNode;
- config = options;
- }
- else {
- config = rootNode;
- }
- kv.init();
- if (config) {
- config = extend(extend({}, kv.configuration), config);
- kv.utils.setDomData(node, config);
- }
- ko.applyBindings(viewModel, node);
- };
- //override the original applyBindings so that we can ensure all new rules and what not are correctly registered
- var origApplyBindings = ko.applyBindings;
- ko.applyBindings = function (viewModel, rootNode) {
- kv.init();
- origApplyBindings(viewModel, rootNode);
- };
- ko.validatedObservable = function (initialValue, options) {
- if (!options && !kv.utils.isObject(initialValue)) {
- return ko.observable(initialValue).extend({ validatable: true });
- }
- var obsv = ko.observable(initialValue);
- obsv.errors = kv.group(kv.utils.isObject(initialValue) ? initialValue : {}, options);
- obsv.isValid = ko.observable(obsv.errors().length === 0);
- if (ko.isObservable(obsv.errors)) {
- obsv.errors.subscribe(function(errors) {
- obsv.isValid(errors.length === 0);
- });
- }
- else {
- ko.computed(obsv.errors).subscribe(function (errors) {
- obsv.isValid(errors.length === 0);
- });
- }
- obsv.subscribe(function(newValue) {
- if (!kv.utils.isObject(newValue)) {
- /*
- * The validation group works on objects.
- * Since the new value is a primitive (scalar, null or undefined) we need
- * to create an empty object to pass along.
- */
- newValue = {};
- }
- // Force the group to refresh
- obsv.errors._updateState(newValue);
- obsv.isValid(obsv.errors().length === 0);
- });
- return obsv;
- };
- ;}));
|