-
Notifications
You must be signed in to change notification settings - Fork 375
/
Copy pathapi.js
511 lines (440 loc) · 15.5 KB
/
api.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
var api = (function () {
var isInitialized = 0,
configuration = ko.validation.configuration,
utils = ko.validation.utils;
function cleanUpSubscriptions(context) {
ko.utils.arrayForEach(context.subscriptions, function (subscription) {
subscription.dispose();
});
context.subscriptions = [];
}
function dispose(context) {
if (context.options.deep) {
ko.utils.arrayForEach(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 = [];
ko.utils.arrayForEach(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;
ko.utils.extend(configuration, options);
if (configuration.registerExtenders) {
ko.validation.registerExtenders();
}
isInitialized = 1;
},
// resets the config back to its original state
reset: ko.validation.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 = ko.utils.extend(ko.utils.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 ko.utils.arrayFilter(context.validatables, predicate);
};
result.find = function(predicate) {
predicate = predicate || function () { return true; };
// ensure we have latest changes
result();
return ko.utils.arrayFirst(context.validatables, predicate);
};
result.forEach = function(callback) {
callback = callback || function () { };
// ensure we have latest changes
result();
ko.utils.arrayForEach(context.validatables, callback);
};
result.map = function(mapping) {
mapping = mapping || function (item) { return item; };
// ensure we have latest changes
result();
return ko.utils.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 = ko.utils.unwrapObservable(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: ko.validation.addRule(myObservable, {
// rule: 'required',
// params: true
// });
//
addRule: function (observable, rule) {
observable.extend({ validatable: true });
//calculate if the observable already has this rule
//peek the set of rules so this function does not cause any encapsulating subsciptions to fire if the rules change
var hasRule = !!ko.utils.arrayFirst(observable.rules.peek(), function(item) {
return item.rule && item.rule === rule.rule;
});
//do not add the rule if it already exists on the observable
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 'ko.validation.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
ko.validation.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 ko.validation.addRule(observable, {
rule: ruleName,
message: params.message,
params: utils.isEmptyVal(params.params) ? true : params.params,
condition: params.onlyIf
});
} else {
return ko.validation.addRule(observable, {
rule: ruleName,
params: params
});
}
};
},
// loops through all ko.validation.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 ko.validation.rules) {
if (ko.validation.rules.hasOwnProperty(ruleName)) {
if (!ko.extenders[ruleName]) {
ko.validation.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) {
ko.utils.arrayForEach(ko.validation.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};
}
ko.validation.addRule(valueAccessor(), {
rule: attr,
params: params
});
}
});
var currentType = element.getAttribute('type');
ko.utils.arrayForEach(ko.validation.configuration.html5InputTypes, function (type) {
if (type === currentType) {
ko.validation.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
ko.utils.arrayForEach(ko.validation.configuration.html5Attributes, function (attr) {
var ctx = ko.utils.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 = ko.utils.unwrapObservable(targetValue),
rules = {},
nonRules = {};
for (var rule in ruleDefinitions) {
if (!ruleDefinitions.hasOwnProperty(rule)) { continue; }
if (ko.validation.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
ko.utils.extend(ko.validation, api);