knockout.validation.js 47 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511
  1. /*=============================================================================
  2. Author: Eric M. Barnard - @ericmbarnard
  3. License: MIT (http://opensource.org/licenses/mit-license.php)
  4. Description: Validation Library for KnockoutJS
  5. Version: 2.0.3
  6. ===============================================================================
  7. */
  8. /*globals require: false, exports: false, define: false, ko: false */
  9. (function (factory) {
  10. // Module systems magic dance.
  11. if (typeof require === "function" && typeof exports === "object" && typeof module === "object") {
  12. // CommonJS or Node: hard-coded dependency on "knockout"
  13. factory(require("knockout"), exports);
  14. } else if (typeof define === "function" && define["amd"]) {
  15. // AMD anonymous module with hard-coded dependency on "knockout"
  16. define(["knockout", "exports"], factory);
  17. } else {
  18. // <script> tag: use the global `ko` object, attaching a `mapping` property
  19. factory(ko, ko.validation = {});
  20. }
  21. }(function ( ko, exports ) {
  22. if (typeof (ko) === 'undefined') {
  23. throw new Error('Knockout is required, please ensure it is loaded before loading this validation plug-in');
  24. }
  25. // create our namespace object
  26. ko.validation = exports;
  27. var kv = ko.validation,
  28. koUtils = ko.utils,
  29. unwrap = koUtils.unwrapObservable,
  30. forEach = koUtils.arrayForEach,
  31. extend = koUtils.extend;
  32. ;/*global ko: false*/
  33. var defaults = {
  34. registerExtenders: true,
  35. messagesOnModified: true,
  36. errorsAsTitle: true, // enables/disables showing of errors as title attribute of the target element.
  37. errorsAsTitleOnModified: false, // shows the error when hovering the input field (decorateElement must be true)
  38. messageTemplate: null,
  39. insertMessages: true, // automatically inserts validation messages as <span></span>
  40. parseInputAttributes: false, // parses the HTML5 validation attribute from a form element and adds that to the object
  41. writeInputAttributes: false, // adds HTML5 input validation attributes to form elements that ko observable's are bound to
  42. decorateInputElement: false, // false to keep backward compatibility
  43. decorateElementOnModified: true,// true to keep backward compatibility
  44. errorClass: null, // single class for error message and element
  45. errorElementClass: 'validationElement', // class to decorate error element
  46. errorMessageClass: 'validationMessage', // class to decorate error message
  47. allowHtmlMessages: false, // allows HTML in validation messages
  48. grouping: {
  49. deep: false, //by default grouping is shallow
  50. observable: true, //and using observables
  51. live: false //react to changes to observableArrays if observable === true
  52. },
  53. validate: {
  54. // throttle: 10
  55. }
  56. };
  57. // make a copy so we can use 'reset' later
  58. var configuration = extend({}, defaults);
  59. configuration.html5Attributes = ['required', 'pattern', 'min', 'max', 'step'];
  60. configuration.html5InputTypes = ['email', 'number', 'date'];
  61. configuration.reset = function () {
  62. extend(configuration, defaults);
  63. };
  64. kv.configuration = configuration;
  65. ;kv.utils = (function () {
  66. var seedId = new Date().getTime();
  67. var domData = {}; //hash of data objects that we reference from dom elements
  68. var domDataKey = '__ko_validation__';
  69. return {
  70. isArray: function (o) {
  71. return o.isArray || Object.prototype.toString.call(o) === '[object Array]';
  72. },
  73. isObject: function (o) {
  74. return o !== null && typeof o === 'object';
  75. },
  76. isNumber: function(o) {
  77. return !isNaN(o);
  78. },
  79. isObservableArray: function(instance) {
  80. return !!instance &&
  81. typeof instance["remove"] === "function" &&
  82. typeof instance["removeAll"] === "function" &&
  83. typeof instance["destroy"] === "function" &&
  84. typeof instance["destroyAll"] === "function" &&
  85. typeof instance["indexOf"] === "function" &&
  86. typeof instance["replace"] === "function";
  87. },
  88. values: function (o) {
  89. var r = [];
  90. for (var i in o) {
  91. if (o.hasOwnProperty(i)) {
  92. r.push(o[i]);
  93. }
  94. }
  95. return r;
  96. },
  97. getValue: function (o) {
  98. return (typeof o === 'function' ? o() : o);
  99. },
  100. hasAttribute: function (node, attr) {
  101. return node.getAttribute(attr) !== null;
  102. },
  103. getAttribute: function (element, attr) {
  104. return element.getAttribute(attr);
  105. },
  106. setAttribute: function (element, attr, value) {
  107. return element.setAttribute(attr, value);
  108. },
  109. isValidatable: function (o) {
  110. return !!(o && o.rules && o.isValid && o.isModified);
  111. },
  112. insertAfter: function (node, newNode) {
  113. node.parentNode.insertBefore(newNode, node.nextSibling);
  114. },
  115. newId: function () {
  116. return seedId += 1;
  117. },
  118. getConfigOptions: function (element) {
  119. var options = kv.utils.contextFor(element);
  120. return options || kv.configuration;
  121. },
  122. setDomData: function (node, data) {
  123. var key = node[domDataKey];
  124. if (!key) {
  125. node[domDataKey] = key = kv.utils.newId();
  126. }
  127. domData[key] = data;
  128. },
  129. getDomData: function (node) {
  130. var key = node[domDataKey];
  131. if (!key) {
  132. return undefined;
  133. }
  134. return domData[key];
  135. },
  136. contextFor: function (node) {
  137. switch (node.nodeType) {
  138. case 1:
  139. case 8:
  140. var context = kv.utils.getDomData(node);
  141. if (context) { return context; }
  142. if (node.parentNode) { return kv.utils.contextFor(node.parentNode); }
  143. break;
  144. }
  145. return undefined;
  146. },
  147. isEmptyVal: function (val) {
  148. if (val === undefined) {
  149. return true;
  150. }
  151. if (val === null) {
  152. return true;
  153. }
  154. if (val === "") {
  155. return true;
  156. }
  157. },
  158. getOriginalElementTitle: function (element) {
  159. var savedOriginalTitle = kv.utils.getAttribute(element, 'data-orig-title'),
  160. currentTitle = element.title,
  161. hasSavedOriginalTitle = kv.utils.hasAttribute(element, 'data-orig-title');
  162. return hasSavedOriginalTitle ?
  163. savedOriginalTitle : currentTitle;
  164. },
  165. async: function (expr) {
  166. if (window.setImmediate) { window.setImmediate(expr); }
  167. else { window.setTimeout(expr, 0); }
  168. },
  169. forEach: function (object, callback) {
  170. if (kv.utils.isArray(object)) {
  171. return forEach(object, callback);
  172. }
  173. for (var prop in object) {
  174. if (object.hasOwnProperty(prop)) {
  175. callback(object[prop], prop);
  176. }
  177. }
  178. }
  179. };
  180. }());;var api = (function () {
  181. var isInitialized = 0,
  182. configuration = kv.configuration,
  183. utils = kv.utils;
  184. function cleanUpSubscriptions(context) {
  185. forEach(context.subscriptions, function (subscription) {
  186. subscription.dispose();
  187. });
  188. context.subscriptions = [];
  189. }
  190. function dispose(context) {
  191. if (context.options.deep) {
  192. forEach(context.flagged, function (obj) {
  193. delete obj.__kv_traversed;
  194. });
  195. context.flagged.length = 0;
  196. }
  197. if (!context.options.live) {
  198. cleanUpSubscriptions(context);
  199. }
  200. }
  201. function runTraversal(obj, context) {
  202. context.validatables = [];
  203. cleanUpSubscriptions(context);
  204. traverseGraph(obj, context);
  205. dispose(context);
  206. }
  207. function traverseGraph(obj, context, level) {
  208. var objValues = [],
  209. val = obj.peek ? obj.peek() : obj;
  210. if (obj.__kv_traversed === true) {
  211. return;
  212. }
  213. if (context.options.deep) {
  214. obj.__kv_traversed = true;
  215. context.flagged.push(obj);
  216. }
  217. //default level value depends on deep option.
  218. level = (level !== undefined ? level : context.options.deep ? 1 : -1);
  219. // if object is observable then add it to the list
  220. if (ko.isObservable(obj)) {
  221. // ensure it's validatable but don't extend validatedObservable because it
  222. // would overwrite isValid property.
  223. if (!obj.errors && !utils.isValidatable(obj)) {
  224. obj.extend({ validatable: true });
  225. }
  226. context.validatables.push(obj);
  227. if (context.options.live && utils.isObservableArray(obj)) {
  228. context.subscriptions.push(obj.subscribe(function () {
  229. context.graphMonitor.valueHasMutated();
  230. }));
  231. }
  232. }
  233. //get list of values either from array or object but ignore non-objects
  234. // and destroyed objects
  235. if (val && !val._destroy) {
  236. if (utils.isArray(val)) {
  237. objValues = val;
  238. }
  239. else if (utils.isObject(val)) {
  240. objValues = utils.values(val);
  241. }
  242. }
  243. //process recursively if it is deep grouping
  244. if (level !== 0) {
  245. utils.forEach(objValues, function (observable) {
  246. //but not falsy things and not HTML Elements
  247. if (observable && !observable.nodeType && (!ko.isComputed(observable) || observable.rules)) {
  248. traverseGraph(observable, context, level + 1);
  249. }
  250. });
  251. }
  252. }
  253. function collectErrors(array) {
  254. var errors = [];
  255. forEach(array, function (observable) {
  256. // Do not collect validatedObservable errors
  257. if (utils.isValidatable(observable) && !observable.isValid()) {
  258. // Use peek because we don't want a dependency for 'error' property because it
  259. // changes before 'isValid' does. (Issue #99)
  260. errors.push(observable.error.peek());
  261. }
  262. });
  263. return errors;
  264. }
  265. return {
  266. //Call this on startup
  267. //any config can be overridden with the passed in options
  268. init: function (options, force) {
  269. //done run this multiple times if we don't really want to
  270. if (isInitialized > 0 && !force) {
  271. return;
  272. }
  273. //because we will be accessing options properties it has to be an object at least
  274. options = options || {};
  275. //if specific error classes are not provided then apply generic errorClass
  276. //it has to be done on option so that options.errorClass can override default
  277. //errorElementClass and errorMessage class but not those provided in options
  278. options.errorElementClass = options.errorElementClass || options.errorClass || configuration.errorElementClass;
  279. options.errorMessageClass = options.errorMessageClass || options.errorClass || configuration.errorMessageClass;
  280. extend(configuration, options);
  281. if (configuration.registerExtenders) {
  282. kv.registerExtenders();
  283. }
  284. isInitialized = 1;
  285. },
  286. // resets the config back to its original state
  287. reset: kv.configuration.reset,
  288. // recursively walks a viewModel and creates an object that
  289. // provides validation information for the entire viewModel
  290. // obj -> the viewModel to walk
  291. // options -> {
  292. // deep: false, // if true, will walk past the first level of viewModel properties
  293. // observable: false // if true, returns a computed observable indicating if the viewModel is valid
  294. // }
  295. group: function group(obj, options) { // array of observables or viewModel
  296. options = extend(extend({}, configuration.grouping), options);
  297. var context = {
  298. options: options,
  299. graphMonitor: ko.observable(),
  300. flagged: [],
  301. subscriptions: [],
  302. validatables: []
  303. };
  304. var result = null;
  305. //if using observables then traverse structure once and add observables
  306. if (options.observable) {
  307. result = ko.computed(function () {
  308. context.graphMonitor(); //register dependency
  309. runTraversal(obj, context);
  310. return collectErrors(context.validatables);
  311. });
  312. }
  313. else { //if not using observables then every call to error() should traverse the structure
  314. result = function () {
  315. runTraversal(obj, context);
  316. return collectErrors(context.validatables);
  317. };
  318. }
  319. result.showAllMessages = function (show) { // thanks @heliosPortal
  320. if (show === undefined) {//default to true
  321. show = true;
  322. }
  323. result.forEach(function (observable) {
  324. if (utils.isValidatable(observable)) {
  325. observable.isModified(show);
  326. }
  327. });
  328. };
  329. result.isAnyMessageShown = function () {
  330. var invalidAndModifiedPresent;
  331. invalidAndModifiedPresent = !!result.find(function (observable) {
  332. return utils.isValidatable(observable) && !observable.isValid() && observable.isModified();
  333. });
  334. return invalidAndModifiedPresent;
  335. };
  336. result.filter = function(predicate) {
  337. predicate = predicate || function () { return true; };
  338. // ensure we have latest changes
  339. result();
  340. return koUtils.arrayFilter(context.validatables, predicate);
  341. };
  342. result.find = function(predicate) {
  343. predicate = predicate || function () { return true; };
  344. // ensure we have latest changes
  345. result();
  346. return koUtils.arrayFirst(context.validatables, predicate);
  347. };
  348. result.forEach = function(callback) {
  349. callback = callback || function () { };
  350. // ensure we have latest changes
  351. result();
  352. forEach(context.validatables, callback);
  353. };
  354. result.map = function(mapping) {
  355. mapping = mapping || function (item) { return item; };
  356. // ensure we have latest changes
  357. result();
  358. return koUtils.arrayMap(context.validatables, mapping);
  359. };
  360. /**
  361. * @private You should not rely on this method being here.
  362. * It's a private method and it may change in the future.
  363. *
  364. * @description Updates the validated object and collects errors from it.
  365. */
  366. result._updateState = function(newValue) {
  367. if (!utils.isObject(newValue)) {
  368. throw new Error('An object is required.');
  369. }
  370. obj = newValue;
  371. if (options.observable) {
  372. context.graphMonitor.valueHasMutated();
  373. }
  374. else {
  375. runTraversal(newValue, context);
  376. return collectErrors(context.validatables);
  377. }
  378. };
  379. return result;
  380. },
  381. formatMessage: function (message, params, observable) {
  382. if (utils.isObject(params) && params.typeAttr) {
  383. params = params.value;
  384. }
  385. if (typeof message === 'function') {
  386. return message(params, observable);
  387. }
  388. var replacements = unwrap(params);
  389. if (replacements == null) {
  390. replacements = [];
  391. }
  392. if (!utils.isArray(replacements)) {
  393. replacements = [replacements];
  394. }
  395. return message.replace(/{(\d+)}/gi, function(match, index) {
  396. if (typeof replacements[index] !== 'undefined') {
  397. return replacements[index];
  398. }
  399. return match;
  400. });
  401. },
  402. // addRule:
  403. // This takes in a ko.observable and a Rule Context - which is just a rule name and params to supply to the validator
  404. // ie: kv.addRule(myObservable, {
  405. // rule: 'required',
  406. // params: true
  407. // });
  408. //
  409. addRule: function (observable, rule) {
  410. observable.extend({ validatable: true });
  411. var hasRule = !!koUtils.arrayFirst(observable.rules(), function(item) {
  412. return item.rule && item.rule === rule.rule;
  413. });
  414. if (!hasRule) {
  415. //push a Rule Context to the observables local array of Rule Contexts
  416. observable.rules.push(rule);
  417. }
  418. return observable;
  419. },
  420. // addAnonymousRule:
  421. // Anonymous Rules essentially have all the properties of a Rule, but are only specific for a certain property
  422. // and developers typically are wanting to add them on the fly or not register a rule with the 'kv.rules' object
  423. //
  424. // Example:
  425. // var test = ko.observable('something').extend{(
  426. // validation: {
  427. // validator: function(val, someOtherVal){
  428. // return true;
  429. // },
  430. // message: "Something must be really wrong!',
  431. // params: true
  432. // }
  433. // )};
  434. addAnonymousRule: function (observable, ruleObj) {
  435. if (ruleObj['message'] === undefined) {
  436. ruleObj['message'] = 'Error';
  437. }
  438. //make sure onlyIf is honoured
  439. if (ruleObj.onlyIf) {
  440. ruleObj.condition = ruleObj.onlyIf;
  441. }
  442. //add the anonymous rule to the observable
  443. kv.addRule(observable, ruleObj);
  444. },
  445. addExtender: function (ruleName) {
  446. ko.extenders[ruleName] = function (observable, params) {
  447. //params can come in a few flavors
  448. // 1. Just the params to be passed to the validator
  449. // 2. An object containing the Message to be used and the Params to pass to the validator
  450. // 3. A condition when the validation rule to be applied
  451. //
  452. // Example:
  453. // var test = ko.observable(3).extend({
  454. // max: {
  455. // message: 'This special field has a Max of {0}',
  456. // params: 2,
  457. // onlyIf: function() {
  458. // return specialField.IsVisible();
  459. // }
  460. // }
  461. // )};
  462. //
  463. if (params && (params.message || params.onlyIf)) { //if it has a message or condition object, then its an object literal to use
  464. return kv.addRule(observable, {
  465. rule: ruleName,
  466. message: params.message,
  467. params: utils.isEmptyVal(params.params) ? true : params.params,
  468. condition: params.onlyIf
  469. });
  470. } else {
  471. return kv.addRule(observable, {
  472. rule: ruleName,
  473. params: params
  474. });
  475. }
  476. };
  477. },
  478. // loops through all kv.rules and adds them as extenders to
  479. // ko.extenders
  480. registerExtenders: function () { // root extenders optional, use 'validation' extender if would cause conflicts
  481. if (configuration.registerExtenders) {
  482. for (var ruleName in kv.rules) {
  483. if (kv.rules.hasOwnProperty(ruleName)) {
  484. if (!ko.extenders[ruleName]) {
  485. kv.addExtender(ruleName);
  486. }
  487. }
  488. }
  489. }
  490. },
  491. //creates a span next to the @element with the specified error class
  492. insertValidationMessage: function (element) {
  493. var span = document.createElement('SPAN');
  494. span.className = utils.getConfigOptions(element).errorMessageClass;
  495. utils.insertAfter(element, span);
  496. return span;
  497. },
  498. // if html-5 validation attributes have been specified, this parses
  499. // the attributes on @element
  500. parseInputValidationAttributes: function (element, valueAccessor) {
  501. forEach(kv.configuration.html5Attributes, function (attr) {
  502. if (utils.hasAttribute(element, attr)) {
  503. var params = element.getAttribute(attr) || true;
  504. if (attr === 'min' || attr === 'max')
  505. {
  506. // If we're validating based on the min and max attributes, we'll
  507. // need to know what the 'type' attribute is set to
  508. var typeAttr = element.getAttribute('type');
  509. if (typeof typeAttr === "undefined" || !typeAttr)
  510. {
  511. // From http://www.w3.org/TR/html-markup/input:
  512. // An input element with no type attribute specified represents the
  513. // same thing as an input element with its type attribute set to "text".
  514. typeAttr = "text";
  515. }
  516. params = {typeAttr: typeAttr, value: params};
  517. }
  518. kv.addRule(valueAccessor(), {
  519. rule: attr,
  520. params: params
  521. });
  522. }
  523. });
  524. var currentType = element.getAttribute('type');
  525. forEach(kv.configuration.html5InputTypes, function (type) {
  526. if (type === currentType) {
  527. kv.addRule(valueAccessor(), {
  528. rule: (type === 'date') ? 'dateISO' : type,
  529. params: true
  530. });
  531. }
  532. });
  533. },
  534. // writes html5 validation attributes on the element passed in
  535. writeInputValidationAttributes: function (element, valueAccessor) {
  536. var observable = valueAccessor();
  537. if (!observable || !observable.rules) {
  538. return;
  539. }
  540. var contexts = observable.rules(); // observable array
  541. // loop through the attributes and add the information needed
  542. forEach(kv.configuration.html5Attributes, function (attr) {
  543. var ctx = koUtils.arrayFirst(contexts, function (ctx) {
  544. return ctx.rule && ctx.rule.toLowerCase() === attr.toLowerCase();
  545. });
  546. if (!ctx) {
  547. return;
  548. }
  549. // we have a rule matching a validation attribute at this point
  550. // so lets add it to the element along with the params
  551. ko.computed({
  552. read: function() {
  553. var params = ko.unwrap(ctx.params);
  554. // we have to do some special things for the pattern validation
  555. if (ctx.rule === "pattern" && params instanceof RegExp) {
  556. // we need the pure string representation of the RegExpr without the //gi stuff
  557. params = params.source;
  558. }
  559. element.setAttribute(attr, params);
  560. },
  561. disposeWhenNodeIsRemoved: element
  562. });
  563. });
  564. contexts = null;
  565. },
  566. //take an existing binding handler and make it cause automatic validations
  567. makeBindingHandlerValidatable: function (handlerName) {
  568. var init = ko.bindingHandlers[handlerName].init;
  569. ko.bindingHandlers[handlerName].init = function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
  570. init(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
  571. return ko.bindingHandlers['validationCore'].init(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
  572. };
  573. },
  574. // visit an objects properties and apply validation rules from a definition
  575. setRules: function (target, definition) {
  576. var setRules = function (target, definition) {
  577. if (!target || !definition) { return; }
  578. for (var prop in definition) {
  579. if (!definition.hasOwnProperty(prop)) { continue; }
  580. var ruleDefinitions = definition[prop];
  581. //check the target property exists and has a value
  582. if (!target[prop]) { continue; }
  583. var targetValue = target[prop],
  584. unwrappedTargetValue = unwrap(targetValue),
  585. rules = {},
  586. nonRules = {};
  587. for (var rule in ruleDefinitions) {
  588. if (!ruleDefinitions.hasOwnProperty(rule)) { continue; }
  589. if (kv.rules[rule]) {
  590. rules[rule] = ruleDefinitions[rule];
  591. } else {
  592. nonRules[rule] = ruleDefinitions[rule];
  593. }
  594. }
  595. //apply rules
  596. if (ko.isObservable(targetValue)) {
  597. targetValue.extend(rules);
  598. }
  599. //then apply child rules
  600. //if it's an array, apply rules to all children
  601. if (unwrappedTargetValue && utils.isArray(unwrappedTargetValue)) {
  602. for (var i = 0; i < unwrappedTargetValue.length; i++) {
  603. setRules(unwrappedTargetValue[i], nonRules);
  604. }
  605. //otherwise, just apply to this property
  606. } else {
  607. setRules(unwrappedTargetValue, nonRules);
  608. }
  609. }
  610. };
  611. setRules(target, definition);
  612. }
  613. };
  614. }());
  615. // expose api publicly
  616. extend(ko.validation, api);
  617. ;//Validation Rules:
  618. // You can view and override messages or rules via:
  619. // kv.rules[ruleName]
  620. //
  621. // To implement a custom Rule, simply use this template:
  622. // kv.rules['<custom rule name>'] = {
  623. // validator: function (val, param) {
  624. // <custom logic>
  625. // return <true or false>;
  626. // },
  627. // message: '<custom validation message>' //optionally you can also use a '{0}' to denote a placeholder that will be replaced with your 'param'
  628. // };
  629. //
  630. // Example:
  631. // kv.rules['mustEqual'] = {
  632. // validator: function( val, mustEqualVal ){
  633. // return val === mustEqualVal;
  634. // },
  635. // message: 'This field must equal {0}'
  636. // };
  637. //
  638. kv.rules = {};
  639. kv.rules['required'] = {
  640. validator: function (val, required) {
  641. var testVal;
  642. if (val === undefined || val === null) {
  643. return !required;
  644. }
  645. testVal = val;
  646. if (typeof (val) === 'string') {
  647. if (String.prototype.trim) {
  648. testVal = val.trim();
  649. }
  650. else {
  651. testVal = val.replace(/^\s+|\s+$/g, '');
  652. }
  653. }
  654. if (!required) {// if they passed: { required: false }, then don't require this
  655. return true;
  656. }
  657. return ((testVal + '').length > 0);
  658. },
  659. message: 'This field is required.'
  660. };
  661. function minMaxValidatorFactory(validatorName) {
  662. var isMaxValidation = validatorName === "max";
  663. return function (val, options) {
  664. if (kv.utils.isEmptyVal(val)) {
  665. return true;
  666. }
  667. var comparisonValue, type;
  668. if (options.typeAttr === undefined) {
  669. // This validator is being called from javascript rather than
  670. // being bound from markup
  671. type = "text";
  672. comparisonValue = options;
  673. } else {
  674. type = options.typeAttr;
  675. comparisonValue = options.value;
  676. }
  677. // From http://www.w3.org/TR/2012/WD-html5-20121025/common-input-element-attributes.html#attr-input-min,
  678. // if the value is parseable to a number, then the minimum should be numeric
  679. if (!isNaN(comparisonValue) && !(comparisonValue instanceof Date)) {
  680. type = "number";
  681. }
  682. var regex, valMatches, comparisonValueMatches;
  683. switch (type.toLowerCase()) {
  684. case "week":
  685. regex = /^(\d{4})-W(\d{2})$/;
  686. valMatches = val.match(regex);
  687. if (valMatches === null) {
  688. throw new Error("Invalid value for " + validatorName + " attribute for week input. Should look like " +
  689. "'2000-W33' http://www.w3.org/TR/html-markup/input.week.html#input.week.attrs.min");
  690. }
  691. comparisonValueMatches = comparisonValue.match(regex);
  692. // If no regex matches were found, validation fails
  693. if (!comparisonValueMatches) {
  694. return false;
  695. }
  696. if (isMaxValidation) {
  697. return (valMatches[1] < comparisonValueMatches[1]) || // older year
  698. // same year, older week
  699. ((valMatches[1] === comparisonValueMatches[1]) && (valMatches[2] <= comparisonValueMatches[2]));
  700. } else {
  701. return (valMatches[1] > comparisonValueMatches[1]) || // newer year
  702. // same year, newer week
  703. ((valMatches[1] === comparisonValueMatches[1]) && (valMatches[2] >= comparisonValueMatches[2]));
  704. }
  705. break;
  706. case "month":
  707. regex = /^(\d{4})-(\d{2})$/;
  708. valMatches = val.match(regex);
  709. if (valMatches === null) {
  710. throw new Error("Invalid value for " + validatorName + " attribute for month input. Should look like " +
  711. "'2000-03' http://www.w3.org/TR/html-markup/input.month.html#input.month.attrs.min");
  712. }
  713. comparisonValueMatches = comparisonValue.match(regex);
  714. // If no regex matches were found, validation fails
  715. if (!comparisonValueMatches) {
  716. return false;
  717. }
  718. if (isMaxValidation) {
  719. return ((valMatches[1] < comparisonValueMatches[1]) || // older year
  720. // same year, older month
  721. ((valMatches[1] === comparisonValueMatches[1]) && (valMatches[2] <= comparisonValueMatches[2])));
  722. } else {
  723. return (valMatches[1] > comparisonValueMatches[1]) || // newer year
  724. // same year, newer month
  725. ((valMatches[1] === comparisonValueMatches[1]) && (valMatches[2] >= comparisonValueMatches[2]));
  726. }
  727. break;
  728. case "number":
  729. case "range":
  730. if (isMaxValidation) {
  731. return (!isNaN(val) && parseFloat(val) <= parseFloat(comparisonValue));
  732. } else {
  733. return (!isNaN(val) && parseFloat(val) >= parseFloat(comparisonValue));
  734. }
  735. break;
  736. default:
  737. if (isMaxValidation) {
  738. return val <= comparisonValue;
  739. } else {
  740. return val >= comparisonValue;
  741. }
  742. }
  743. };
  744. }
  745. kv.rules['min'] = {
  746. validator: minMaxValidatorFactory("min"),
  747. message: 'Please enter a value greater than or equal to {0}.'
  748. };
  749. kv.rules['max'] = {
  750. validator: minMaxValidatorFactory("max"),
  751. message: 'Please enter a value less than or equal to {0}.'
  752. };
  753. kv.rules['minLength'] = {
  754. validator: function (val, minLength) {
  755. if(kv.utils.isEmptyVal(val)) { return true; }
  756. var normalizedVal = kv.utils.isNumber(val) ? ('' + val) : val;
  757. return normalizedVal.length >= minLength;
  758. },
  759. message: 'Please enter at least {0} characters.'
  760. };
  761. kv.rules['maxLength'] = {
  762. validator: function (val, maxLength) {
  763. if(kv.utils.isEmptyVal(val)) { return true; }
  764. var normalizedVal = kv.utils.isNumber(val) ? ('' + val) : val;
  765. return normalizedVal.length <= maxLength;
  766. },
  767. message: 'Please enter no more than {0} characters.'
  768. };
  769. kv.rules['pattern'] = {
  770. validator: function (val, regex) {
  771. return kv.utils.isEmptyVal(val) || val.toString().match(regex) !== null;
  772. },
  773. message: 'Please check this value.'
  774. };
  775. kv.rules['step'] = {
  776. validator: function (val, step) {
  777. // in order to handle steps of .1 & .01 etc.. Modulus won't work
  778. // if the value is a decimal, so we have to correct for that
  779. if (kv.utils.isEmptyVal(val) || step === 'any') { return true; }
  780. var dif = (val * 100) % (step * 100);
  781. return Math.abs(dif) < 0.00001 || Math.abs(1 - dif) < 0.00001;
  782. },
  783. message: 'The value must increment by {0}.'
  784. };
  785. kv.rules['email'] = {
  786. validator: function (val, validate) {
  787. if (!validate) { return true; }
  788. //I think an empty email address is also a valid entry
  789. //if one want's to enforce entry it should be done with 'required: true'
  790. return kv.utils.isEmptyVal(val) || (
  791. // jquery validate regex - thanks Scott Gonzalez
  792. 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)
  793. );
  794. },
  795. message: 'Please enter a proper email address.'
  796. };
  797. kv.rules['date'] = {
  798. validator: function (value, validate) {
  799. if (!validate) { return true; }
  800. return kv.utils.isEmptyVal(value) || (validate && !/Invalid|NaN/.test(new Date(value)));
  801. },
  802. message: 'Please enter a proper date.'
  803. };
  804. kv.rules['dateISO'] = {
  805. validator: function (value, validate) {
  806. if (!validate) { return true; }
  807. return kv.utils.isEmptyVal(value) || (validate && /^\d{4}[-/](?:0?[1-9]|1[012])[-/](?:0?[1-9]|[12][0-9]|3[01])$/.test(value));
  808. },
  809. message: 'Please enter a proper date.'
  810. };
  811. kv.rules['number'] = {
  812. validator: function (value, validate) {
  813. if (!validate) { return true; }
  814. return kv.utils.isEmptyVal(value) || (validate && /^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value));
  815. },
  816. message: 'Please enter a number.'
  817. };
  818. kv.rules['digit'] = {
  819. validator: function (value, validate) {
  820. if (!validate) { return true; }
  821. return kv.utils.isEmptyVal(value) || (validate && /^\d+$/.test(value));
  822. },
  823. message: 'Please enter a digit.'
  824. };
  825. kv.rules['phoneUS'] = {
  826. validator: function (phoneNumber, validate) {
  827. if (!validate) { return true; }
  828. if (kv.utils.isEmptyVal(phoneNumber)) { return true; } // makes it optional, use 'required' rule if it should be required
  829. if (typeof (phoneNumber) !== 'string') { return false; }
  830. phoneNumber = phoneNumber.replace(/\s+/g, "");
  831. return validate && phoneNumber.length > 9 && phoneNumber.match(/^(1-?)?(\([2-9]\d{2}\)|[2-9]\d{2})-?[2-9]\d{2}-?\d{4}$/);
  832. },
  833. message: 'Please specify a valid phone number.'
  834. };
  835. kv.rules['equal'] = {
  836. validator: function (val, params) {
  837. var otherValue = params;
  838. return val === kv.utils.getValue(otherValue);
  839. },
  840. message: 'Values must equal.'
  841. };
  842. kv.rules['notEqual'] = {
  843. validator: function (val, params) {
  844. var otherValue = params;
  845. return val !== kv.utils.getValue(otherValue);
  846. },
  847. message: 'Please choose another value.'
  848. };
  849. //unique in collection
  850. // options are:
  851. // collection: array or function returning (observable) array
  852. // in which the value has to be unique
  853. // valueAccessor: function that returns value from an object stored in collection
  854. // if it is null the value is compared directly
  855. // external: set to true when object you are validating is automatically updating collection
  856. kv.rules['unique'] = {
  857. validator: function (val, options) {
  858. var c = kv.utils.getValue(options.collection),
  859. external = kv.utils.getValue(options.externalValue),
  860. counter = 0;
  861. if (!val || !c) { return true; }
  862. koUtils.arrayFilter(c, function (item) {
  863. if (val === (options.valueAccessor ? options.valueAccessor(item) : item)) { counter++; }
  864. });
  865. // if value is external even 1 same value in collection means the value is not unique
  866. return counter < (!!external ? 1 : 2);
  867. },
  868. message: 'Please make sure the value is unique.'
  869. };
  870. //now register all of these!
  871. (function () {
  872. kv.registerExtenders();
  873. }());
  874. ;// The core binding handler
  875. // this allows us to setup any value binding that internally always
  876. // performs the same functionality
  877. ko.bindingHandlers['validationCore'] = (function () {
  878. return {
  879. init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
  880. var config = kv.utils.getConfigOptions(element);
  881. var observable = valueAccessor();
  882. // parse html5 input validation attributes, optional feature
  883. if (config.parseInputAttributes) {
  884. kv.utils.async(function () { kv.parseInputValidationAttributes(element, valueAccessor); });
  885. }
  886. // if requested insert message element and apply bindings
  887. if (config.insertMessages && kv.utils.isValidatable(observable)) {
  888. // insert the <span></span>
  889. var validationMessageElement = kv.insertValidationMessage(element);
  890. // if we're told to use a template, make sure that gets rendered
  891. if (config.messageTemplate) {
  892. ko.renderTemplate(config.messageTemplate, { field: observable }, null, validationMessageElement, 'replaceNode');
  893. } else {
  894. ko.applyBindingsToNode(validationMessageElement, { validationMessage: observable });
  895. }
  896. }
  897. // write the html5 attributes if indicated by the config
  898. if (config.writeInputAttributes && kv.utils.isValidatable(observable)) {
  899. kv.writeInputValidationAttributes(element, valueAccessor);
  900. }
  901. // if requested, add binding to decorate element
  902. if (config.decorateInputElement && kv.utils.isValidatable(observable)) {
  903. ko.applyBindingsToNode(element, { validationElement: observable });
  904. }
  905. }
  906. };
  907. }());
  908. // override for KO's default 'value', 'checked', 'textInput' and selectedOptions bindings
  909. kv.makeBindingHandlerValidatable("value");
  910. kv.makeBindingHandlerValidatable("checked");
  911. if (ko.bindingHandlers.textInput) {
  912. kv.makeBindingHandlerValidatable("textInput");
  913. }
  914. kv.makeBindingHandlerValidatable("selectedOptions");
  915. ko.bindingHandlers['validationMessage'] = { // individual error message, if modified or post binding
  916. update: function (element, valueAccessor) {
  917. var obsv = valueAccessor(),
  918. config = kv.utils.getConfigOptions(element),
  919. val = unwrap(obsv),
  920. msg = null,
  921. isModified = false,
  922. isValid = false;
  923. if (obsv === null || typeof obsv === 'undefined') {
  924. throw new Error('Cannot bind validationMessage to undefined value. data-bind expression: ' +
  925. element.getAttribute('data-bind'));
  926. }
  927. isModified = obsv.isModified && obsv.isModified();
  928. isValid = obsv.isValid && obsv.isValid();
  929. var error = null;
  930. if (!config.messagesOnModified || isModified) {
  931. error = isValid ? null : obsv.error;
  932. }
  933. var isVisible = !config.messagesOnModified || isModified ? !isValid : false;
  934. var isCurrentlyVisible = element.style.display !== "none";
  935. if (config.allowHtmlMessages) {
  936. koUtils.setHtml(element, error);
  937. } else {
  938. ko.bindingHandlers.text.update(element, function () { return error; });
  939. }
  940. if (isCurrentlyVisible && !isVisible) {
  941. element.style.display = 'none';
  942. } else if (!isCurrentlyVisible && isVisible) {
  943. element.style.display = '';
  944. }
  945. }
  946. };
  947. ko.bindingHandlers['validationElement'] = {
  948. update: function (element, valueAccessor, allBindingsAccessor) {
  949. var obsv = valueAccessor(),
  950. config = kv.utils.getConfigOptions(element),
  951. val = unwrap(obsv),
  952. msg = null,
  953. isModified = false,
  954. isValid = false;
  955. if (obsv === null || typeof obsv === 'undefined') {
  956. throw new Error('Cannot bind validationElement to undefined value. data-bind expression: ' +
  957. element.getAttribute('data-bind'));
  958. }
  959. isModified = obsv.isModified && obsv.isModified();
  960. isValid = obsv.isValid && obsv.isValid();
  961. // create an evaluator function that will return something like:
  962. // css: { validationElement: true }
  963. var cssSettingsAccessor = function () {
  964. var css = {};
  965. var shouldShow = ((!config.decorateElementOnModified || isModified) ? !isValid : false);
  966. // css: { validationElement: false }
  967. css[config.errorElementClass] = shouldShow;
  968. return css;
  969. };
  970. //add or remove class on the element;
  971. ko.bindingHandlers.css.update(element, cssSettingsAccessor, allBindingsAccessor);
  972. if (!config.errorsAsTitle) { return; }
  973. ko.bindingHandlers.attr.update(element, function () {
  974. var
  975. hasModification = !config.errorsAsTitleOnModified || isModified,
  976. title = kv.utils.getOriginalElementTitle(element);
  977. if (hasModification && !isValid) {
  978. return { title: obsv.error, 'data-orig-title': title };
  979. } else if (!hasModification || isValid) {
  980. return { title: title, 'data-orig-title': null };
  981. }
  982. });
  983. }
  984. };
  985. // ValidationOptions:
  986. // This binding handler allows you to override the initial config by setting any of the options for a specific element or context of elements
  987. //
  988. // Example:
  989. // <div data-bind="validationOptions: { insertMessages: true, messageTemplate: 'customTemplate', errorMessageClass: 'mySpecialClass'}">
  990. // <input type="text" data-bind="value: someValue"/>
  991. // <input type="text" data-bind="value: someValue2"/>
  992. // </div>
  993. ko.bindingHandlers['validationOptions'] = (function () {
  994. return {
  995. init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
  996. var options = unwrap(valueAccessor());
  997. if (options) {
  998. var newConfig = extend({}, kv.configuration);
  999. extend(newConfig, options);
  1000. //store the validation options on the node so we can retrieve it later
  1001. kv.utils.setDomData(element, newConfig);
  1002. }
  1003. }
  1004. };
  1005. }());
  1006. ;// Validation Extender:
  1007. // This is for creating custom validation logic on the fly
  1008. // Example:
  1009. // var test = ko.observable('something').extend{(
  1010. // validation: {
  1011. // validator: function(val, someOtherVal){
  1012. // return true;
  1013. // },
  1014. // message: "Something must be really wrong!',
  1015. // params: true
  1016. // }
  1017. // )};
  1018. ko.extenders['validation'] = function (observable, rules) { // allow single rule or array
  1019. forEach(kv.utils.isArray(rules) ? rules : [rules], function (rule) {
  1020. // the 'rule' being passed in here has no name to identify a core Rule,
  1021. // so we add it as an anonymous rule
  1022. // If the developer is wanting to use a core Rule, but use a different message see the 'addExtender' logic for examples
  1023. kv.addAnonymousRule(observable, rule);
  1024. });
  1025. return observable;
  1026. };
  1027. //This is the extender that makes a Knockout Observable also 'Validatable'
  1028. //examples include:
  1029. // 1. var test = ko.observable('something').extend({validatable: true});
  1030. // this will ensure that the Observable object is setup properly to respond to rules
  1031. //
  1032. // 2. test.extend({validatable: false});
  1033. // this will remove the validation properties from the Observable object should you need to do that.
  1034. ko.extenders['validatable'] = function (observable, options) {
  1035. if (!kv.utils.isObject(options)) {
  1036. options = { enable: options };
  1037. }
  1038. if (!('enable' in options)) {
  1039. options.enable = true;
  1040. }
  1041. if (options.enable && !kv.utils.isValidatable(observable)) {
  1042. var config = kv.configuration.validate || {};
  1043. var validationOptions = {
  1044. throttleEvaluation : options.throttle || config.throttle
  1045. };
  1046. observable.error = ko.observable(null); // holds the error message, we only need one since we stop processing validators when one is invalid
  1047. // observable.rules:
  1048. // ObservableArray of Rule Contexts, where a Rule Context is simply the name of a rule and the params to supply to it
  1049. //
  1050. // Rule Context = { rule: '<rule name>', params: '<passed in params>', message: '<Override of default Message>' }
  1051. observable.rules = ko.observableArray(); //holds the rule Contexts to use as part of validation
  1052. //in case async validation is occurring
  1053. observable.isValidating = ko.observable(false);
  1054. //the true holder of whether the observable is valid or not
  1055. observable.__valid__ = ko.observable(true);
  1056. observable.isModified = ko.observable(false);
  1057. // a semi-protected observable
  1058. observable.isValid = ko.computed(observable.__valid__);
  1059. //manually set error state
  1060. observable.setError = function (error) {
  1061. var previousError = observable.error.peek();
  1062. var previousIsValid = observable.__valid__.peek();
  1063. observable.error(error);
  1064. observable.__valid__(false);
  1065. if (previousError !== error && !previousIsValid) {
  1066. // if the observable was not valid before then isValid will not mutate,
  1067. // hence causing any grouping to not display the latest error.
  1068. observable.isValid.notifySubscribers();
  1069. }
  1070. };
  1071. //manually clear error state
  1072. observable.clearError = function () {
  1073. observable.error(null);
  1074. observable.__valid__(true);
  1075. return observable;
  1076. };
  1077. //subscribe to changes in the observable
  1078. var h_change = observable.subscribe(function () {
  1079. observable.isModified(true);
  1080. });
  1081. // we use a computed here to ensure that anytime a dependency changes, the
  1082. // validation logic evaluates
  1083. var h_obsValidationTrigger = ko.computed(extend({
  1084. read: function () {
  1085. var obs = observable(),
  1086. ruleContexts = observable.rules();
  1087. kv.validateObservable(observable);
  1088. return true;
  1089. }
  1090. }, validationOptions));
  1091. extend(h_obsValidationTrigger, validationOptions);
  1092. observable._disposeValidation = function () {
  1093. //first dispose of the subscriptions
  1094. observable.isValid.dispose();
  1095. observable.rules.removeAll();
  1096. h_change.dispose();
  1097. h_obsValidationTrigger.dispose();
  1098. delete observable['rules'];
  1099. delete observable['error'];
  1100. delete observable['isValid'];
  1101. delete observable['isValidating'];
  1102. delete observable['__valid__'];
  1103. delete observable['isModified'];
  1104. delete observable['setError'];
  1105. delete observable['clearError'];
  1106. delete observable['_disposeValidation'];
  1107. };
  1108. } else if (options.enable === false && observable._disposeValidation) {
  1109. observable._disposeValidation();
  1110. }
  1111. return observable;
  1112. };
  1113. function validateSync(observable, rule, ctx) {
  1114. //Execute the validator and see if its valid
  1115. if (!rule.validator(observable(), (ctx.params === undefined ? true : unwrap(ctx.params)))) { // default param is true, eg. required = true
  1116. //not valid, so format the error message and stick it in the 'error' variable
  1117. observable.setError(kv.formatMessage(
  1118. ctx.message || rule.message,
  1119. unwrap(ctx.params),
  1120. observable));
  1121. return false;
  1122. } else {
  1123. return true;
  1124. }
  1125. }
  1126. function validateAsync(observable, rule, ctx) {
  1127. observable.isValidating(true);
  1128. var callBack = function (valObj) {
  1129. var isValid = false,
  1130. msg = '';
  1131. if (!observable.__valid__()) {
  1132. // since we're returning early, make sure we turn this off
  1133. observable.isValidating(false);
  1134. return; //if its already NOT valid, don't add to that
  1135. }
  1136. //we were handed back a complex object
  1137. if (valObj['message']) {
  1138. isValid = valObj.isValid;
  1139. msg = valObj.message;
  1140. } else {
  1141. isValid = valObj;
  1142. }
  1143. if (!isValid) {
  1144. //not valid, so format the error message and stick it in the 'error' variable
  1145. observable.error(kv.formatMessage(
  1146. msg || ctx.message || rule.message,
  1147. unwrap(ctx.params),
  1148. observable));
  1149. observable.__valid__(isValid);
  1150. }
  1151. // tell it that we're done
  1152. observable.isValidating(false);
  1153. };
  1154. kv.utils.async(function() {
  1155. //fire the validator and hand it the callback
  1156. rule.validator(observable(), ctx.params === undefined ? true : unwrap(ctx.params), callBack);
  1157. });
  1158. }
  1159. kv.validateObservable = function (observable) {
  1160. var i = 0,
  1161. rule, // the rule validator to execute
  1162. ctx, // the current Rule Context for the loop
  1163. ruleContexts = observable.rules(), //cache for iterator
  1164. len = ruleContexts.length; //cache for iterator
  1165. for (; i < len; i++) {
  1166. //get the Rule Context info to give to the core Rule
  1167. ctx = ruleContexts[i];
  1168. // checks an 'onlyIf' condition
  1169. if (ctx.condition && !ctx.condition()) {
  1170. continue;
  1171. }
  1172. //get the core Rule to use for validation
  1173. rule = ctx.rule ? kv.rules[ctx.rule] : ctx;
  1174. if (rule['async'] || ctx['async']) {
  1175. //run async validation
  1176. validateAsync(observable, rule, ctx);
  1177. } else {
  1178. //run normal sync validation
  1179. if (!validateSync(observable, rule, ctx)) {
  1180. return false; //break out of the loop
  1181. }
  1182. }
  1183. }
  1184. //finally if we got this far, make the observable valid again!
  1185. observable.clearError();
  1186. return true;
  1187. };
  1188. ;
  1189. var _locales = {};
  1190. var _currentLocale;
  1191. kv.defineLocale = function(name, values) {
  1192. if (name && values) {
  1193. _locales[name.toLowerCase()] = values;
  1194. return values;
  1195. }
  1196. return null;
  1197. };
  1198. kv.locale = function(name) {
  1199. if (name) {
  1200. name = name.toLowerCase();
  1201. if (_locales.hasOwnProperty(name)) {
  1202. kv.localize(_locales[name]);
  1203. _currentLocale = name;
  1204. }
  1205. else {
  1206. throw new Error('Localization ' + name + ' has not been loaded.');
  1207. }
  1208. }
  1209. return _currentLocale;
  1210. };
  1211. //quick function to override rule messages
  1212. kv.localize = function (msgTranslations) {
  1213. var rules = kv.rules;
  1214. //loop the properties in the object and assign the msg to the rule
  1215. for (var ruleName in msgTranslations) {
  1216. if (rules.hasOwnProperty(ruleName)) {
  1217. rules[ruleName].message = msgTranslations[ruleName];
  1218. }
  1219. }
  1220. };
  1221. // Populate default locale (this will make en-US.js somewhat redundant)
  1222. (function() {
  1223. var localeData = {};
  1224. var rules = kv.rules;
  1225. for (var ruleName in rules) {
  1226. if (rules.hasOwnProperty(ruleName)) {
  1227. localeData[ruleName] = rules[ruleName].message;
  1228. }
  1229. }
  1230. kv.defineLocale('en-us', localeData);
  1231. })();
  1232. // No need to invoke locale because the messages are already defined along with the rules for en-US
  1233. _currentLocale = 'en-us';
  1234. ;/**
  1235. * Possible invocations:
  1236. * applyBindingsWithValidation(viewModel)
  1237. * applyBindingsWithValidation(viewModel, options)
  1238. * applyBindingsWithValidation(viewModel, rootNode)
  1239. * applyBindingsWithValidation(viewModel, rootNode, options)
  1240. */
  1241. ko.applyBindingsWithValidation = function (viewModel, rootNode, options) {
  1242. var node = document.body,
  1243. config;
  1244. if (rootNode && rootNode.nodeType) {
  1245. node = rootNode;
  1246. config = options;
  1247. }
  1248. else {
  1249. config = rootNode;
  1250. }
  1251. kv.init();
  1252. if (config) {
  1253. config = extend(extend({}, kv.configuration), config);
  1254. kv.utils.setDomData(node, config);
  1255. }
  1256. ko.applyBindings(viewModel, node);
  1257. };
  1258. //override the original applyBindings so that we can ensure all new rules and what not are correctly registered
  1259. var origApplyBindings = ko.applyBindings;
  1260. ko.applyBindings = function (viewModel, rootNode) {
  1261. kv.init();
  1262. origApplyBindings(viewModel, rootNode);
  1263. };
  1264. ko.validatedObservable = function (initialValue, options) {
  1265. if (!options && !kv.utils.isObject(initialValue)) {
  1266. return ko.observable(initialValue).extend({ validatable: true });
  1267. }
  1268. var obsv = ko.observable(initialValue);
  1269. obsv.errors = kv.group(kv.utils.isObject(initialValue) ? initialValue : {}, options);
  1270. obsv.isValid = ko.observable(obsv.errors().length === 0);
  1271. if (ko.isObservable(obsv.errors)) {
  1272. obsv.errors.subscribe(function(errors) {
  1273. obsv.isValid(errors.length === 0);
  1274. });
  1275. }
  1276. else {
  1277. ko.computed(obsv.errors).subscribe(function (errors) {
  1278. obsv.isValid(errors.length === 0);
  1279. });
  1280. }
  1281. obsv.subscribe(function(newValue) {
  1282. if (!kv.utils.isObject(newValue)) {
  1283. /*
  1284. * The validation group works on objects.
  1285. * Since the new value is a primitive (scalar, null or undefined) we need
  1286. * to create an empty object to pass along.
  1287. */
  1288. newValue = {};
  1289. }
  1290. // Force the group to refresh
  1291. obsv.errors._updateState(newValue);
  1292. obsv.isValid(obsv.errors().length === 0);
  1293. });
  1294. return obsv;
  1295. };
  1296. ;}));