forked from knockout/knockout
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbindingAttributeSyntax.js
executable file
·444 lines (390 loc) · 23.8 KB
/
bindingAttributeSyntax.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
(function () {
ko.bindingHandlers = {};
// The following element types will not be recursed into during binding. In the future, we
// may consider adding <template> to this list, because such elements' contents are always
// intended to be bound in a different context from where they appear in the document.
var bindingDoesNotRecurseIntoElementTypes = {
// Don't want bindings that operate on text nodes to mutate <script> contents,
// because it's unexpected and a potential XSS issue
'script': true
};
// Use an overridable method for retrieving binding handlers so that a plugins may support dynamically created handlers
ko['getBindingHandler'] = function(bindingKey) {
return ko.bindingHandlers[bindingKey];
};
// The ko.bindingContext constructor is only called directly to create the root context. For child
// contexts, use bindingContext.createChildContext or bindingContext.extend.
ko.bindingContext = function(dataItemOrAccessor, parentContext, dataItemAlias, extendCallback) {
// The binding context object includes static properties for the current, parent, and root view models.
// If a view model is actually stored in an observable, the corresponding binding context object, and
// any child contexts, must be updated when the view model is changed.
function updateContext() {
// Most of the time, the context will directly get a view model object, but if a function is given,
// we call the function to retrieve the view model. If the function accesses any obsevables (or is
// itself an observable), the dependency is tracked, and those observables can later cause the binding
// context to be updated.
var dataItem = isFunc ? dataItemOrAccessor() : dataItemOrAccessor;
if (parentContext) {
// When a "parent" context is given, register a dependency on the parent context. Thus whenever the
// parent context is updated, this context will also be updated.
if (parentContext._subscribable)
parentContext._subscribable();
// Copy $root and any custom properties from the parent context
ko.utils.extend(self, parentContext);
// Because the above copy overwrites our own properties, we need to reset them.
// During the first execution, "subscribable" isn't set, so don't bother doing the update then.
if (subscribable) {
self['$dataFn'] = self._subscribable = subscribable;
}
} else {
self['$parents'] = [];
self['$root'] = dataItem;
// Export 'ko' in the binding context so it will be available in bindings and templates
// even if 'ko' isn't exported as a global, such as when using an AMD loader.
// See https://github.com/SteveSanderson/knockout/issues/490
self['ko'] = ko;
}
self['$data'] = dataItem;
if (dataItemAlias)
self[dataItemAlias] = dataItem;
// The extendCallback function is provided when creating a child context or extending a context.
// It handles the specific actions needed to finish setting up the binding context. Actions in this
// function could also add dependencies to this binding context.
if (extendCallback)
extendCallback(self, parentContext, dataItem);
return self['$data'];
}
function disposeWhen() {
return !ko.utils.anyDomNodeIsAttachedToDocument(nodes);
}
var self = this,
isFunc = typeof(dataItemOrAccessor) == "function",
nodes = [],
subscribable = ko.dependentObservable(updateContext, null, { disposeWhen: disposeWhen });
// At this point, the binding context has been initialized, and the "subscribable" computed observable is
// subscribed to any observables that were accessed in the process. If there is nothing to track, the
// computed will be inactive, and we can safely throw it away. If it's active, the computed is stored in
// the context object.
if (subscribable.isActive()) {
self['$dataFn'] = self._subscribable = subscribable;
// We need to be able to dispose of this computed observable when it's no longer needed. This would be
// easy if we had a single node to watch, but binding contexts can be used by many different nodes, and
// we cannot assume that those nodes have any relation to each other. So instead we track any node that
// the context is attached to, and dispose the computed when all of those nodes have been cleaned.
// Add properties to *subscribable* instead of *self* because any properties added to *self* may be overwritten on updates
subscribable._nodes = nodes;
subscribable._addNode = function(node) {
nodes.push(node);
ko.utils.domNodeDisposal.addDisposeCallback(node, function(node) {
ko.utils.arrayRemoveItem(nodes, node);
if (!nodes.length) {
subscribable.dispose();
self._subscribable = subscribable = undefined;
}
});
};
} else {
self['$dataFn'] = function() { return self['$data']; }
}
}
// Extend the binding context hierarchy with a new view model object. If the parent context is watching
// any obsevables, the new child context will automatically get a dependency on the parent context.
// But this does not mean that the $data value of the child context will also get updated. If the child
// view model also depends on the parent view model, you must provide a function that returns the correct
// view model on each update.
ko.bindingContext.prototype['createChildContext'] = function (dataItemOrAccessor, dataItemAlias, extendCallback) {
return new ko.bindingContext(dataItemOrAccessor, this, dataItemAlias, function(self, parentContext) {
// Extend the context hierarchy by setting the appropriate pointers
self['$parentContext'] = parentContext;
self['$parent'] = parentContext['$data'];
self['$parents'] = (parentContext['$parents'] || []).slice(0);
self['$parents'].unshift(self['$parent']);
if (extendCallback)
extendCallback(self);
});
};
// Extend the binding context with new custom properties. This doesn't change the context hierarchy.
// Similarly to "child" contexts, provide a function here to make sure that the correct values are set
// when an observable view model is updated.
ko.bindingContext.prototype['extend'] = function(properties) {
return new ko.bindingContext(this['$dataFn'], this, null, function(self) {
ko.utils.extend(self, typeof(properties) == "function" ? properties() : properties);
});
};
// Returns the valueAccesor function for a binding value
function makeValueAccessor(value) {
return function() {
return value;
};
}
// Returns the value of a valueAccessor function
function evaluateValueAccessor(valueAccessor) {
return valueAccessor();
}
// Given a function that returns bindings, create and return a new object that contains
// binding value-accessors functions. Each accessor function calls the original function
// so that it always gets the latest value and all dependencies are captured. This is used
// by ko.applyBindingsToNode and getBindingsAndMakeAccessors.
function makeAccessorsFromFunction(callback) {
return ko.utils.objectMap(ko.dependencyDetection.ignore(callback), function(value, key) {
return function() {
return callback()[key];
};
});
}
// Given a bindings function or object, create and return a new object that contains
// binding value-accessors functions. This is used by ko.applyBindingsToNode.
function makeBindingAccessors(bindings, context, node) {
if (typeof bindings === 'function') {
return makeAccessorsFromFunction(bindings.bind(null, context, node));
} else {
return ko.utils.objectMap(bindings, makeValueAccessor);
}
}
// This function is used if the binding provider doesn't include a getBindingAccessors function.
// It must be called with 'this' set to the provider instance.
function getBindingsAndMakeAccessors(node, context) {
return makeAccessorsFromFunction(this['getBindings'].bind(this, node, context));
}
function validateThatBindingIsAllowedForVirtualElements(bindingName) {
var validator = ko.virtualElements.allowedBindings[bindingName];
if (!validator)
throw new Error("The binding '" + bindingName + "' cannot be used with virtual elements")
}
function applyBindingsToDescendantsInternal (bindingContext, elementOrVirtualElement, bindingContextsMayDifferFromDomParentElement) {
var currentChild,
nextInQueue = ko.virtualElements.firstChild(elementOrVirtualElement),
provider = ko.bindingProvider['instance'],
preprocessNode = provider['preprocessNode'];
// Preprocessing allows a binding provider to mutate a node before bindings are applied to it. For example it's
// possible to insert new siblings after it, and/or replace the node with a different one. This can be used to
// implement custom binding syntaxes, such as {{ value }} for string interpolation, or custom element types that
// trigger insertion of <template> contents at that point in the document.
if (preprocessNode) {
while (currentChild = nextInQueue) {
nextInQueue = ko.virtualElements.nextSibling(currentChild);
preprocessNode.call(provider, currentChild);
}
// Reset nextInQueue for the next loop
nextInQueue = ko.virtualElements.firstChild(elementOrVirtualElement);
}
while (currentChild = nextInQueue) {
// Keep a record of the next child *before* applying bindings, in case the binding removes the current child from its position
nextInQueue = ko.virtualElements.nextSibling(currentChild);
applyBindingsToNodeAndDescendantsInternal(bindingContext, currentChild, bindingContextsMayDifferFromDomParentElement);
}
}
function applyBindingsToNodeAndDescendantsInternal (bindingContext, nodeVerified, bindingContextMayDifferFromDomParentElement) {
var shouldBindDescendants = true;
// Perf optimisation: Apply bindings only if...
// (1) We need to store the binding context on this node (because it may differ from the DOM parent node's binding context)
// Note that we can't store binding contexts on non-elements (e.g., text nodes), as IE doesn't allow expando properties for those
// (2) It might have bindings (e.g., it has a data-bind attribute, or it's a marker for a containerless template)
var isElement = (nodeVerified.nodeType === 1);
if (isElement) // Workaround IE <= 8 HTML parsing weirdness
ko.virtualElements.normaliseVirtualElementDomStructure(nodeVerified);
var shouldApplyBindings = (isElement && bindingContextMayDifferFromDomParentElement) // Case (1)
|| ko.bindingProvider['instance']['nodeHasBindings'](nodeVerified); // Case (2)
if (shouldApplyBindings)
shouldBindDescendants = applyBindingsToNodeInternal(nodeVerified, null, bindingContext, bindingContextMayDifferFromDomParentElement)['shouldBindDescendants'];
if (shouldBindDescendants && !bindingDoesNotRecurseIntoElementTypes[ko.utils.tagNameLower(nodeVerified)]) {
// We're recursing automatically into (real or virtual) child nodes without changing binding contexts. So,
// * For children of a *real* element, the binding context is certainly the same as on their DOM .parentNode,
// hence bindingContextsMayDifferFromDomParentElement is false
// * For children of a *virtual* element, we can't be sure. Evaluating .parentNode on those children may
// skip over any number of intermediate virtual elements, any of which might define a custom binding context,
// hence bindingContextsMayDifferFromDomParentElement is true
applyBindingsToDescendantsInternal(bindingContext, nodeVerified, /* bindingContextsMayDifferFromDomParentElement: */ !isElement);
}
}
var boundElementDomDataKey = '__ko_boundElement';
function topologicalSortBindings(bindings) {
// Depth-first sort
var result = [], // The list of key/handler pairs that we will return
bindingsConsidered = {}, // A temporary record of which bindings are already in 'result'
cyclicDependencyStack = []; // Keeps track of a depth-search so that, if there's a cycle, we know which bindings caused it
ko.utils.objectForEach(bindings, function pushBinding(bindingKey) {
if (!bindingsConsidered[bindingKey]) {
var binding = ko['getBindingHandler'](bindingKey);
if (binding) {
// First add dependencies (if any) of the current binding
if (binding['after']) {
cyclicDependencyStack.push(bindingKey);
ko.utils.arrayForEach(binding['after'], function(bindingDependencyKey) {
if (bindings[bindingDependencyKey]) {
if (ko.utils.arrayIndexOf(cyclicDependencyStack, bindingDependencyKey) !== -1) {
throw Error("Cannot combine the following bindings, because they have a cyclic dependency: " + cyclicDependencyStack.join(", "));
} else {
pushBinding(bindingDependencyKey);
}
}
});
cyclicDependencyStack.pop();
}
// Next add the current binding
result.push({ key: bindingKey, handler: binding });
}
bindingsConsidered[bindingKey] = true;
}
});
return result;
}
function applyBindingsToNodeInternal(node, sourceBindings, bindingContext, bindingContextMayDifferFromDomParentElement) {
// Prevent multiple applyBindings calls for the same node, except when a binding value is specified
var alreadyBound = ko.utils.domData.get(node, boundElementDomDataKey);
if (!sourceBindings) {
if (alreadyBound) {
throw Error("You cannot apply bindings multiple times to the same element.");
}
ko.utils.domData.set(node, boundElementDomDataKey, true);
}
// Optimization: Don't store the binding context on this node if it's definitely the same as on node.parentNode, because
// we can easily recover it just by scanning up the node's ancestors in the DOM
// (note: here, parent node means "real DOM parent" not "virtual parent", as there's no O(1) way to find the virtual parent)
if (!alreadyBound && bindingContextMayDifferFromDomParentElement)
ko.storedBindingContextForNode(node, bindingContext);
// Use bindings if given, otherwise fall back on asking the bindings provider to give us some bindings
var bindings;
if (sourceBindings && typeof sourceBindings !== 'function') {
bindings = sourceBindings;
} else {
var provider = ko.bindingProvider['instance'],
getBindings = provider['getBindingAccessors'] || getBindingsAndMakeAccessors;
// When an obsevable view model is used, the binding context will expose an observable _subscribable value.
// Get the binding from the provider within a computed observable so that we can update the bindings whenever
// the binding context is updated.
var bindingsUpdater = ko.dependentObservable(
function() {
bindings = sourceBindings ? sourceBindings(bindingContext, node) : getBindings.call(provider, node, bindingContext);
// Register a dependency on the binding context
if (bindings && bindingContext._subscribable)
bindingContext._subscribable();
return bindings;
},
null, { disposeWhenNodeIsRemoved: node }
);
if (!bindings || !bindingsUpdater.isActive())
bindingsUpdater = null;
}
var bindingHandlerThatControlsDescendantBindings;
if (bindings) {
// Return the value accessor for a given binding. When bindings are static (won't be updated because of a binding
// context update), just return the value accessor from the binding. Otherwise, return a function that always gets
// the latest binding value and registers a dependency on the binding updater.
var getValueAccessor = bindingsUpdater
? function(bindingKey) {
return function() {
return evaluateValueAccessor(bindingsUpdater()[bindingKey]);
};
} : function(bindingKey) {
return bindings[bindingKey];
};
// Use of allBindings as a function is maintained for backwards compatibility, but its use is deprecated
function allBindings() {
return ko.utils.objectMap(bindingsUpdater ? bindingsUpdater() : bindings, evaluateValueAccessor);
}
// The following is the 3.x allBindings API
allBindings['get'] = function(key) {
return bindings[key] && evaluateValueAccessor(getValueAccessor(key));
};
allBindings['has'] = function(key) {
return key in bindings;
};
// First put the bindings into the right order
var orderedBindings = topologicalSortBindings(bindings);
// Go through the sorted bindings, calling init and update for each
ko.utils.arrayForEach(orderedBindings, function(bindingKeyAndHandler) {
var bindingKey = bindingKeyAndHandler.key,
bindingHandler = bindingKeyAndHandler.handler;
if (node.nodeType === 8) {
validateThatBindingIsAllowedForVirtualElements(bindingKey);
}
// Run init, ignoring any dependencies
ko.dependencyDetection.ignore(function() {
var handlerInitFn = bindingHandler["init"];
if (typeof handlerInitFn == "function") {
var initResult = handlerInitFn(node, getValueAccessor(bindingKey), allBindings, bindingContext['$data'], bindingContext);
// If this binding handler claims to control descendant bindings, make a note of this
if (initResult && initResult['controlsDescendantBindings']) {
if (bindingHandlerThatControlsDescendantBindings !== undefined)
throw new Error("Multiple bindings (" + bindingHandlerThatControlsDescendantBindings + " and " + bindingKey + ") are trying to control descendant bindings of the same element. You cannot use these bindings together on the same element.");
bindingHandlerThatControlsDescendantBindings = bindingKey;
}
}
});
// Run update in its own computed wrapper
ko.dependentObservable(
function() {
var handlerUpdateFn = bindingHandler["update"];
if (typeof handlerUpdateFn == "function") {
handlerUpdateFn(node, getValueAccessor(bindingKey), allBindings, bindingContext['$data'], bindingContext);
}
},
null,
{ disposeWhenNodeIsRemoved: node }
);
});
}
return {
'shouldBindDescendants': bindingHandlerThatControlsDescendantBindings === undefined
};
};
var storedBindingContextDomDataKey = "__ko_bindingContext__";
ko.storedBindingContextForNode = function (node, bindingContext) {
if (arguments.length == 2) {
ko.utils.domData.set(node, storedBindingContextDomDataKey, bindingContext);
if (bindingContext._subscribable)
bindingContext._subscribable._addNode(node);
} else {
return ko.utils.domData.get(node, storedBindingContextDomDataKey);
}
}
function getBindingContext(viewModelOrBindingContext) {
return viewModelOrBindingContext && (viewModelOrBindingContext instanceof ko.bindingContext)
? viewModelOrBindingContext
: new ko.bindingContext(viewModelOrBindingContext);
}
ko.applyBindingAccessorsToNode = function (node, bindings, viewModelOrBindingContext) {
if (node.nodeType === 1) // If it's an element, workaround IE <= 8 HTML parsing weirdness
ko.virtualElements.normaliseVirtualElementDomStructure(node);
return applyBindingsToNodeInternal(node, bindings, getBindingContext(viewModelOrBindingContext), true);
};
ko.applyBindingsToNode = function (node, bindings, viewModelOrBindingContext) {
var context = getBindingContext(viewModelOrBindingContext);
return ko.applyBindingAccessorsToNode(node, makeBindingAccessors(bindings, context, node), context);
};
ko.applyBindingsToDescendants = function(viewModelOrBindingContext, rootNode) {
if (rootNode.nodeType === 1 || rootNode.nodeType === 8)
applyBindingsToDescendantsInternal(getBindingContext(viewModelOrBindingContext), rootNode, true);
};
ko.applyBindings = function (viewModelOrBindingContext, rootNode) {
if (rootNode && (rootNode.nodeType !== 1) && (rootNode.nodeType !== 8))
throw new Error("ko.applyBindings: first parameter should be your view model; second parameter should be a DOM node");
rootNode = rootNode || window.document.body; // Make "rootNode" parameter optional
applyBindingsToNodeAndDescendantsInternal(getBindingContext(viewModelOrBindingContext), rootNode, true);
};
// Retrieving binding context from arbitrary nodes
ko.contextFor = function(node) {
// We can only do something meaningful for elements and comment nodes (in particular, not text nodes, as IE can't store domdata for them)
switch (node.nodeType) {
case 1:
case 8:
var context = ko.storedBindingContextForNode(node);
if (context) return context;
if (node.parentNode) return ko.contextFor(node.parentNode);
break;
}
return undefined;
};
ko.dataFor = function(node) {
var context = ko.contextFor(node);
return context ? context['$data'] : undefined;
};
ko.exportSymbol('bindingHandlers', ko.bindingHandlers);
ko.exportSymbol('applyBindings', ko.applyBindings);
ko.exportSymbol('applyBindingsToDescendants', ko.applyBindingsToDescendants);
ko.exportSymbol('applyBindingAccessorsToNode', ko.applyBindingAccessorsToNode);
ko.exportSymbol('applyBindingsToNode', ko.applyBindingsToNode);
ko.exportSymbol('contextFor', ko.contextFor);
ko.exportSymbol('dataFor', ko.dataFor);
})();