knockoutjs-rails 1.05 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -20,7 +20,7 @@ Add the following directive to your Javascript manifest file (application.js):
20
20
 
21
21
  ## Versioning
22
22
 
23
- knockoutjs-rails 1.05 == Knockout.js 1.05
23
+ knockoutjs-rails 2.0.0 == Knockout.js 2.0.0
24
24
 
25
25
  Every attempt is made to mirror the currently shipping Knockout.js version number wherever possible.
26
26
  The major and minor version numbers will always represent the Knockout.js version, but the patch level
@@ -1,5 +1,5 @@
1
1
  module Knockoutjs
2
2
  module Rails
3
- VERSION = "1.05"
3
+ VERSION = "2.0.0"
4
4
  end
5
5
  end
@@ -1,13 +1,57 @@
1
- // Knockout JavaScript library v1.05
2
- // (c) 2010 Steven Sanderson - http://knockoutjs.com/
3
- // License: Ms-Pl (http://www.opensource.org/licenses/ms-pl.html)
4
-
5
- var ko = window.ko = {};
6
- /// <reference path="namespace.js" />
7
-
1
+ // Knockout JavaScript library v2.0.0
2
+ // (c) Steven Sanderson - http://knockoutjs.com/
3
+ // License: MIT (http://www.opensource.org/licenses/mit-license.php)
4
+
5
+ (function(window,undefined){
6
+ var ko = window["ko"] = {};
7
+ // Google Closure Compiler helpers (used only to make the minified file smaller)
8
+ ko.exportSymbol = function(publicPath, object) {
9
+ var tokens = publicPath.split(".");
10
+ var target = window;
11
+ for (var i = 0; i < tokens.length - 1; i++)
12
+ target = target[tokens[i]];
13
+ target[tokens[tokens.length - 1]] = object;
14
+ };
15
+ ko.exportProperty = function(owner, publicName, object) {
16
+ owner[publicName] = object;
17
+ };
8
18
  ko.utils = new (function () {
9
19
  var stringTrimRegex = /^(\s|\u00A0)+|(\s|\u00A0)+$/g;
10
20
 
21
+ // Represent the known event types in a compact way, then at runtime transform it into a hash with event name as key (for fast lookup)
22
+ var knownEvents = {}, knownEventTypesByEventName = {};
23
+ var keyEventTypeName = /Firefox\/2/i.test(navigator.userAgent) ? 'KeyboardEvent' : 'UIEvents';
24
+ knownEvents[keyEventTypeName] = ['keyup', 'keydown', 'keypress'];
25
+ knownEvents['MouseEvents'] = ['click', 'dblclick', 'mousedown', 'mouseup', 'mousemove', 'mouseover', 'mouseout', 'mouseenter', 'mouseleave'];
26
+ for (var eventType in knownEvents) {
27
+ var knownEventsForType = knownEvents[eventType];
28
+ if (knownEventsForType.length) {
29
+ for (var i = 0, j = knownEventsForType.length; i < j; i++)
30
+ knownEventTypesByEventName[knownEventsForType[i]] = eventType;
31
+ }
32
+ }
33
+
34
+ // Detect IE versions for bug workarounds (uses IE conditionals, not UA string, for robustness)
35
+ var ieVersion = (function() {
36
+ var version = 3, div = document.createElement('div'), iElems = div.getElementsByTagName('i');
37
+
38
+ // Keep constructing conditional HTML blocks until we hit one that resolves to an empty fragment
39
+ while (
40
+ div.innerHTML = '<!--[if gt IE ' + (++version) + ']><i></i><![endif]-->',
41
+ iElems[0]
42
+ );
43
+ return version > 4 ? version : undefined;
44
+ }());
45
+ var isIe6 = ieVersion === 6,
46
+ isIe7 = ieVersion === 7;
47
+
48
+ function isClickOnCheckableElement(element, eventType) {
49
+ if ((element.tagName != "INPUT") || !element.type) return false;
50
+ if (eventType.toLowerCase() != "click") return false;
51
+ var inputType = element.type.toLowerCase();
52
+ return (inputType == "checkbox") || (inputType == "radio");
53
+ }
54
+
11
55
  return {
12
56
  fieldsIncludedWithJsonPost: ['authenticity_token', /^__RequestVerificationToken(_.*)?$/],
13
57
 
@@ -17,10 +61,10 @@ ko.utils = new (function () {
17
61
  },
18
62
 
19
63
  arrayIndexOf: function (array, item) {
20
- if (typeof array.indexOf == "function")
21
- return array.indexOf(item);
64
+ if (typeof Array.prototype.indexOf == "function")
65
+ return Array.prototype.indexOf.call(array, item);
22
66
  for (var i = 0, j = array.length; i < j; i++)
23
- if (array[i] == item)
67
+ if (array[i] === item)
24
68
  return i;
25
69
  return -1;
26
70
  },
@@ -46,7 +90,7 @@ ko.utils = new (function () {
46
90
  result.push(array[i]);
47
91
  }
48
92
  return result;
49
- },
93
+ },
50
94
 
51
95
  arrayMap: function (array, mapping) {
52
96
  array = array || [];
@@ -68,12 +112,21 @@ ko.utils = new (function () {
68
112
  arrayPushAll: function (array, valuesToPush) {
69
113
  for (var i = 0, j = valuesToPush.length; i < j; i++)
70
114
  array.push(valuesToPush[i]);
115
+ return array;
116
+ },
117
+
118
+ extend: function (target, source) {
119
+ for(var prop in source) {
120
+ if(source.hasOwnProperty(prop)) {
121
+ target[prop] = source[prop];
122
+ }
123
+ }
124
+ return target;
71
125
  },
72
126
 
73
127
  emptyDomNode: function (domNode) {
74
128
  while (domNode.firstChild) {
75
- ko.utils.domData.cleanNodeAndDescendants(domNode.firstChild);
76
- domNode.removeChild(domNode.firstChild);
129
+ ko.removeNode(domNode.firstChild);
77
130
  }
78
131
  },
79
132
 
@@ -94,8 +147,7 @@ ko.utils = new (function () {
94
147
  for (var i = 0, j = newNodesArray.length; i < j; i++)
95
148
  parent.insertBefore(newNodesArray[i], insertionPoint);
96
149
  for (var i = 0, j = nodesToReplaceArray.length; i < j; i++) {
97
- ko.utils.domData.cleanNodeAndDescendants(nodesToReplaceArray[i]);
98
- parent.removeChild(nodesToReplaceArray[i]);
150
+ ko.removeNode(nodesToReplaceArray[i]);
99
151
  }
100
152
  }
101
153
  },
@@ -107,19 +159,7 @@ ko.utils = new (function () {
107
159
  else
108
160
  optionNode.selected = isSelected;
109
161
  },
110
-
111
- getElementsHavingAttribute: function (rootNode, attributeName) {
112
- if ((!rootNode) || (rootNode.nodeType != 1)) return [];
113
- var results = [];
114
- if (rootNode.getAttribute(attributeName) !== null)
115
- results.push(rootNode);
116
- var descendants = rootNode.getElementsByTagName("*");
117
- for (var i = 0, j = descendants.length; i < j; i++)
118
- if (descendants[i].getAttribute(attributeName) !== null)
119
- results.push(descendants[i]);
120
- return results;
121
- },
122
-
162
+
123
163
  stringTrim: function (string) {
124
164
  return (string || "").replace(stringTrimRegex, "");
125
165
  },
@@ -134,11 +174,25 @@ ko.utils = new (function () {
134
174
  }
135
175
  return result;
136
176
  },
177
+
178
+ stringStartsWith: function (string, startsWith) {
179
+ string = string || "";
180
+ if (startsWith.length > string.length)
181
+ return false;
182
+ return string.substring(0, startsWith.length) === startsWith;
183
+ },
137
184
 
138
- evalWithinScope: function (expression, scope) {
139
- if (scope === undefined)
140
- return (new Function("return " + expression))();
141
- with (scope) { return eval("(" + expression + ")"); }
185
+ evalWithinScope: function (expression /*, scope1, scope2, scope3... */) {
186
+ // Build the source for a function that evaluates "expression"
187
+ // For each scope variable, add an extra level of "with" nesting
188
+ // Example result: with(sc[1]) { with(sc[0]) { return (expression) } }
189
+ var scopes = Array.prototype.slice.call(arguments, 1);
190
+ var functionBody = "return (" + expression + ")";
191
+ for (var i = 0; i < scopes.length; i++) {
192
+ if (scopes[i] && typeof scopes[i] == "object")
193
+ functionBody = "with(sc[" + i + "]) { " + functionBody + " } ";
194
+ }
195
+ return (new Function("sc", functionBody))(scopes);
142
196
  },
143
197
 
144
198
  domNodeIsContainedBy: function (node, containedByNode) {
@@ -157,9 +211,23 @@ ko.utils = new (function () {
157
211
  },
158
212
 
159
213
  registerEventHandler: function (element, eventType, handler) {
160
- if (typeof jQuery != "undefined")
161
- jQuery(element).bind(eventType, handler);
162
- else if (typeof element.addEventListener == "function")
214
+ if (typeof jQuery != "undefined") {
215
+ if (isClickOnCheckableElement(element, eventType)) {
216
+ // For click events on checkboxes, jQuery interferes with the event handling in an awkward way:
217
+ // it toggles the element checked state *after* the click event handlers run, whereas native
218
+ // click events toggle the checked state *before* the event handler.
219
+ // Fix this by intecepting the handler and applying the correct checkedness before it runs.
220
+ var originalHandler = handler;
221
+ handler = function(event, eventData) {
222
+ var jQuerySuppliedCheckedState = this.checked;
223
+ if (eventData)
224
+ this.checked = eventData.checkedStateBeforeEvent !== true;
225
+ originalHandler.call(this, event);
226
+ this.checked = jQuerySuppliedCheckedState; // Restore the state jQuery applied
227
+ };
228
+ }
229
+ jQuery(element)['bind'](eventType, handler);
230
+ } else if (typeof element.addEventListener == "function")
163
231
  element.addEventListener(eventType, handler, false);
164
232
  else if (typeof element.attachEvent != "undefined")
165
233
  element.attachEvent("on" + eventType, function (event) {
@@ -173,23 +241,30 @@ ko.utils = new (function () {
173
241
  if (!(element && element.nodeType))
174
242
  throw new Error("element must be a DOM node when calling triggerEvent");
175
243
 
176
- if (typeof element.fireEvent != "undefined") {
177
- // Unlike other browsers, IE doesn't change the checked state of checkboxes/radiobuttons when you trigger their "click" event
178
- // so to make it consistent, we'll do it manually here
179
- if (eventType == "click") {
180
- if ((element.tagName == "INPUT") && ((element.type.toLowerCase() == "checkbox") || (element.type.toLowerCase() == "radio")))
181
- element.checked = element.checked !== true;
244
+ if (typeof jQuery != "undefined") {
245
+ var eventData = [];
246
+ if (isClickOnCheckableElement(element, eventType)) {
247
+ // Work around the jQuery "click events on checkboxes" issue described above by storing the original checked state before triggering the handler
248
+ eventData.push({ checkedStateBeforeEvent: element.checked });
182
249
  }
183
- element.fireEvent("on" + eventType);
250
+ jQuery(element)['trigger'](eventType, eventData);
184
251
  } else if (typeof document.createEvent == "function") {
185
252
  if (typeof element.dispatchEvent == "function") {
186
- var eventCategory = (eventType == "click" ? "MouseEvents" : "HTMLEvents"); // Might need to account for other event names at some point
253
+ var eventCategory = knownEventTypesByEventName[eventType] || "HTMLEvents";
187
254
  var event = document.createEvent(eventCategory);
188
255
  event.initEvent(eventType, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, element);
189
256
  element.dispatchEvent(event);
190
257
  }
191
258
  else
192
259
  throw new Error("The supplied element doesn't support dispatchEvent");
260
+ } else if (typeof element.fireEvent != "undefined") {
261
+ // Unlike other browsers, IE doesn't change the checked state of checkboxes/radiobuttons when you trigger their "click" event
262
+ // so to make it consistent, we'll do it manually here
263
+ if (eventType == "click") {
264
+ if ((element.tagName == "INPUT") && ((element.type.toLowerCase() == "checkbox") || (element.type.toLowerCase() == "radio")))
265
+ element.checked = element.checked !== true;
266
+ }
267
+ element.fireEvent("on" + eventType);
193
268
  }
194
269
  else
195
270
  throw new Error("Browser doesn't support triggering events");
@@ -218,6 +293,35 @@ ko.utils = new (function () {
218
293
  }
219
294
  },
220
295
 
296
+ outerHTML: function(node) {
297
+ // For Chrome on non-text nodes
298
+ // (Although IE supports outerHTML, the way it formats HTML is inconsistent - sometimes closing </li> tags are omitted, sometimes not. That caused https://github.com/SteveSanderson/knockout/issues/212.)
299
+ if (ieVersion === undefined) {
300
+ var nativeOuterHtml = node.outerHTML;
301
+ if (typeof nativeOuterHtml == "string")
302
+ return nativeOuterHtml;
303
+ }
304
+
305
+ // Other browsers
306
+ var dummyContainer = window.document.createElement("div");
307
+ dummyContainer.appendChild(node.cloneNode(true));
308
+ return dummyContainer.innerHTML;
309
+ },
310
+
311
+ setTextContent: function(element, textContent) {
312
+ var value = ko.utils.unwrapObservable(textContent);
313
+ if ((value === null) || (value === undefined))
314
+ value = "";
315
+
316
+ 'innerText' in element ? element.innerText = value
317
+ : element.textContent = value;
318
+
319
+ if (ieVersion >= 9) {
320
+ // Believe it or not, this actually fixes an IE9 rendering bug. Insane. https://github.com/SteveSanderson/knockout/issues/209
321
+ element.innerHTML = element.innerHTML;
322
+ }
323
+ },
324
+
221
325
  range: function (min, max) {
222
326
  min = ko.utils.unwrapObservable(min);
223
327
  max = ko.utils.unwrapObservable(max);
@@ -229,12 +333,15 @@ ko.utils = new (function () {
229
333
 
230
334
  makeArray: function(arrayLikeObject) {
231
335
  var result = [];
232
- for (var i = arrayLikeObject.length - 1; i >= 0; i--){
336
+ for (var i = 0, j = arrayLikeObject.length; i < j; i++) {
233
337
  result.push(arrayLikeObject[i]);
234
338
  };
235
339
  return result;
236
340
  },
237
341
 
342
+ isIe6 : isIe6,
343
+ isIe7 : isIe7,
344
+
238
345
  getFormFields: function(form, fieldName) {
239
346
  var fields = ko.utils.makeArray(form.getElementsByTagName("INPUT")).concat(ko.utils.makeArray(form.getElementsByTagName("TEXTAREA")));
240
347
  var isMatchingField = (typeof fieldName == 'string')
@@ -247,6 +354,18 @@ ko.utils = new (function () {
247
354
  };
248
355
  return matches;
249
356
  },
357
+
358
+ parseJson: function (jsonString) {
359
+ if (typeof jsonString == "string") {
360
+ jsonString = ko.utils.stringTrim(jsonString);
361
+ if (jsonString) {
362
+ if (window.JSON && window.JSON.parse) // Use native parsing where available
363
+ return window.JSON.parse(jsonString);
364
+ return (new Function("return " + jsonString))(); // Fallback on less safe parsing for older browsers
365
+ }
366
+ }
367
+ return null;
368
+ },
250
369
 
251
370
  stringifyJson: function (data) {
252
371
  if ((typeof JSON == "undefined") || (typeof JSON.stringify == "undefined"))
@@ -256,8 +375,8 @@ ko.utils = new (function () {
256
375
 
257
376
  postJson: function (urlOrForm, data, options) {
258
377
  options = options || {};
259
- var params = options.params || {};
260
- var includeFields = options.includeFields || this.fieldsIncludedWithJsonPost;
378
+ var params = options['params'] || {};
379
+ var includeFields = options['includeFields'] || this.fieldsIncludedWithJsonPost;
261
380
  var url = urlOrForm;
262
381
 
263
382
  // If we were given a form, use its 'action' URL and pick out any requested field values
@@ -289,62 +408,248 @@ ko.utils = new (function () {
289
408
  form.appendChild(input);
290
409
  }
291
410
  document.body.appendChild(form);
292
- options.submitter ? options.submitter(form) : form.submit();
411
+ options['submitter'] ? options['submitter'](form) : form.submit();
293
412
  setTimeout(function () { form.parentNode.removeChild(form); }, 0);
294
- },
295
-
296
- domData: {
297
- uniqueId: 0,
298
- dataStoreKeyExpandoPropertyName: "__ko__" + (new Date).getTime(),
299
- dataStore: {},
300
- get: function (node, key) {
301
- var allDataForNode = ko.utils.domData.getAll(node, false);
302
- return allDataForNode === undefined ? undefined : allDataForNode[key];
303
- },
304
- set: function (node, key, value) {
305
- var allDataForNode = ko.utils.domData.getAll(node, true);
306
- allDataForNode[key] = value;
307
- },
308
- getAll: function (node, createIfNotFound) {
309
- var dataStoreKey = node[ko.utils.domData.dataStoreKeyExpandoPropertyName];
310
- if (!dataStoreKey) {
311
- if (!createIfNotFound)
312
- return undefined;
313
- dataStoreKey = node[ko.utils.domData.dataStoreKeyExpandoPropertyName] = "ko" + ko.utils.domData.uniqueId++;
314
- ko.utils.domData[dataStoreKey] = {};
315
- }
316
- return ko.utils.domData[dataStoreKey];
317
- },
318
- cleanNode: function (node) {
319
- var dataStoreKey = node[ko.utils.domData.dataStoreKeyExpandoPropertyName];
320
- if (dataStoreKey) {
321
- delete ko.utils.domData[dataStoreKey];
322
- node[ko.utils.domData.dataStoreKeyExpandoPropertyName] = null;
323
- }
324
- },
325
- cleanNodeAndDescendants: function (node) {
326
- if ((node.nodeType != 1) && (node.nodeType != 9))
327
- return;
328
- ko.utils.domData.cleanNode(node);
329
- var descendants = node.getElementsByTagName("*");
330
- for (var i = 0, j = descendants.length; i < j; i++)
331
- ko.utils.domData.cleanNode(descendants[i]);
332
- }
333
413
  }
334
414
  }
335
415
  })();
336
416
 
337
- if (!Function.prototype.bind) {
417
+ ko.exportSymbol('ko.utils', ko.utils);
418
+ ko.utils.arrayForEach([
419
+ ['arrayForEach', ko.utils.arrayForEach],
420
+ ['arrayFirst', ko.utils.arrayFirst],
421
+ ['arrayFilter', ko.utils.arrayFilter],
422
+ ['arrayGetDistinctValues', ko.utils.arrayGetDistinctValues],
423
+ ['arrayIndexOf', ko.utils.arrayIndexOf],
424
+ ['arrayMap', ko.utils.arrayMap],
425
+ ['arrayPushAll', ko.utils.arrayPushAll],
426
+ ['arrayRemoveItem', ko.utils.arrayRemoveItem],
427
+ ['extend', ko.utils.extend],
428
+ ['fieldsIncludedWithJsonPost', ko.utils.fieldsIncludedWithJsonPost],
429
+ ['getFormFields', ko.utils.getFormFields],
430
+ ['postJson', ko.utils.postJson],
431
+ ['parseJson', ko.utils.parseJson],
432
+ ['registerEventHandler', ko.utils.registerEventHandler],
433
+ ['stringifyJson', ko.utils.stringifyJson],
434
+ ['range', ko.utils.range],
435
+ ['toggleDomNodeCssClass', ko.utils.toggleDomNodeCssClass],
436
+ ['triggerEvent', ko.utils.triggerEvent],
437
+ ['unwrapObservable', ko.utils.unwrapObservable]
438
+ ], function(item) {
439
+ ko.exportSymbol('ko.utils.' + item[0], item[1]);
440
+ });
441
+
442
+ if (!Function.prototype['bind']) {
338
443
  // Function.prototype.bind is a standard part of ECMAScript 5th Edition (December 2009, http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-262.pdf)
339
444
  // In case the browser doesn't implement it natively, provide a JavaScript implementation. This implementation is based on the one in prototype.js
340
- Function.prototype.bind = function (object) {
445
+ Function.prototype['bind'] = function (object) {
341
446
  var originalFunction = this, args = Array.prototype.slice.call(arguments), object = args.shift();
342
447
  return function () {
343
448
  return originalFunction.apply(object, args.concat(Array.prototype.slice.call(arguments)));
344
449
  };
345
450
  };
346
- }/// <reference path="utils.js" />
451
+ }
452
+ ko.utils.domData = new (function () {
453
+ var uniqueId = 0;
454
+ var dataStoreKeyExpandoPropertyName = "__ko__" + (new Date).getTime();
455
+ var dataStore = {};
456
+ return {
457
+ get: function (node, key) {
458
+ var allDataForNode = ko.utils.domData.getAll(node, false);
459
+ return allDataForNode === undefined ? undefined : allDataForNode[key];
460
+ },
461
+ set: function (node, key, value) {
462
+ if (value === undefined) {
463
+ // Make sure we don't actually create a new domData key if we are actually deleting a value
464
+ if (ko.utils.domData.getAll(node, false) === undefined)
465
+ return;
466
+ }
467
+ var allDataForNode = ko.utils.domData.getAll(node, true);
468
+ allDataForNode[key] = value;
469
+ },
470
+ getAll: function (node, createIfNotFound) {
471
+ var dataStoreKey = node[dataStoreKeyExpandoPropertyName];
472
+ var hasExistingDataStore = dataStoreKey && (dataStoreKey !== "null");
473
+ if (!hasExistingDataStore) {
474
+ if (!createIfNotFound)
475
+ return undefined;
476
+ dataStoreKey = node[dataStoreKeyExpandoPropertyName] = "ko" + uniqueId++;
477
+ dataStore[dataStoreKey] = {};
478
+ }
479
+ return dataStore[dataStoreKey];
480
+ },
481
+ clear: function (node) {
482
+ var dataStoreKey = node[dataStoreKeyExpandoPropertyName];
483
+ if (dataStoreKey) {
484
+ delete dataStore[dataStoreKey];
485
+ node[dataStoreKeyExpandoPropertyName] = null;
486
+ }
487
+ }
488
+ }
489
+ })();
490
+
491
+ ko.exportSymbol('ko.utils.domData', ko.utils.domData);
492
+ ko.exportSymbol('ko.utils.domData.clear', ko.utils.domData.clear); // Exporting only so specs can clear up after themselves fully
493
+ ko.utils.domNodeDisposal = new (function () {
494
+ var domDataKey = "__ko_domNodeDisposal__" + (new Date).getTime();
495
+
496
+ function getDisposeCallbacksCollection(node, createIfNotFound) {
497
+ var allDisposeCallbacks = ko.utils.domData.get(node, domDataKey);
498
+ if ((allDisposeCallbacks === undefined) && createIfNotFound) {
499
+ allDisposeCallbacks = [];
500
+ ko.utils.domData.set(node, domDataKey, allDisposeCallbacks);
501
+ }
502
+ return allDisposeCallbacks;
503
+ }
504
+ function destroyCallbacksCollection(node) {
505
+ ko.utils.domData.set(node, domDataKey, undefined);
506
+ }
507
+
508
+ function cleanSingleNode(node) {
509
+ // Run all the dispose callbacks
510
+ var callbacks = getDisposeCallbacksCollection(node, false);
511
+ if (callbacks) {
512
+ callbacks = callbacks.slice(0); // Clone, as the array may be modified during iteration (typically, callbacks will remove themselves)
513
+ for (var i = 0; i < callbacks.length; i++)
514
+ callbacks[i](node);
515
+ }
516
+
517
+ // Also erase the DOM data
518
+ ko.utils.domData.clear(node);
519
+
520
+ // Special support for jQuery here because it's so commonly used.
521
+ // Many jQuery plugins (including jquery.tmpl) store data using jQuery's equivalent of domData
522
+ // so notify it to tear down any resources associated with the node & descendants here.
523
+ if ((typeof jQuery == "function") && (typeof jQuery['cleanData'] == "function"))
524
+ jQuery['cleanData']([node]);
525
+ }
526
+
527
+ return {
528
+ addDisposeCallback : function(node, callback) {
529
+ if (typeof callback != "function")
530
+ throw new Error("Callback must be a function");
531
+ getDisposeCallbacksCollection(node, true).push(callback);
532
+ },
533
+
534
+ removeDisposeCallback : function(node, callback) {
535
+ var callbacksCollection = getDisposeCallbacksCollection(node, false);
536
+ if (callbacksCollection) {
537
+ ko.utils.arrayRemoveItem(callbacksCollection, callback);
538
+ if (callbacksCollection.length == 0)
539
+ destroyCallbacksCollection(node);
540
+ }
541
+ },
542
+
543
+ cleanNode : function(node) {
544
+ if ((node.nodeType != 1) && (node.nodeType != 9))
545
+ return;
546
+ cleanSingleNode(node);
547
+
548
+ // Clone the descendants list in case it changes during iteration
549
+ var descendants = [];
550
+ ko.utils.arrayPushAll(descendants, node.getElementsByTagName("*"));
551
+ for (var i = 0, j = descendants.length; i < j; i++)
552
+ cleanSingleNode(descendants[i]);
553
+ },
554
+
555
+ removeNode : function(node) {
556
+ ko.cleanNode(node);
557
+ if (node.parentNode)
558
+ node.parentNode.removeChild(node);
559
+ }
560
+ }
561
+ })();
562
+ ko.cleanNode = ko.utils.domNodeDisposal.cleanNode; // Shorthand name for convenience
563
+ ko.removeNode = ko.utils.domNodeDisposal.removeNode; // Shorthand name for convenience
564
+ ko.exportSymbol('ko.cleanNode', ko.cleanNode);
565
+ ko.exportSymbol('ko.removeNode', ko.removeNode);
566
+ ko.exportSymbol('ko.utils.domNodeDisposal', ko.utils.domNodeDisposal);
567
+ ko.exportSymbol('ko.utils.domNodeDisposal.addDisposeCallback', ko.utils.domNodeDisposal.addDisposeCallback);
568
+ ko.exportSymbol('ko.utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeDisposal.removeDisposeCallback);(function () {
569
+ var leadingCommentRegex = /^(\s*)<!--(.*?)-->/;
570
+
571
+ function simpleHtmlParse(html) {
572
+ // Based on jQuery's "clean" function, but only accounting for table-related elements.
573
+ // If you have referenced jQuery, this won't be used anyway - KO will use jQuery's "clean" function directly
574
+
575
+ // Note that there's still an issue in IE < 9 whereby it will discard comment nodes that are the first child of
576
+ // a descendant node. For example: "<div><!-- mycomment -->abc</div>" will get parsed as "<div>abc</div>"
577
+ // This won't affect anyone who has referenced jQuery, and there's always the workaround of inserting a dummy node
578
+ // (possibly a text node) in front of the comment. So, KO does not attempt to workaround this IE issue automatically at present.
579
+
580
+ // Trim whitespace, otherwise indexOf won't work as expected
581
+ var tags = ko.utils.stringTrim(html).toLowerCase(), div = document.createElement("div");
582
+
583
+ // Finds the first match from the left column, and returns the corresponding "wrap" data from the right column
584
+ var wrap = tags.match(/^<(thead|tbody|tfoot)/) && [1, "<table>", "</table>"] ||
585
+ !tags.indexOf("<tr") && [2, "<table><tbody>", "</tbody></table>"] ||
586
+ (!tags.indexOf("<td") || !tags.indexOf("<th")) && [3, "<table><tbody><tr>", "</tr></tbody></table>"] ||
587
+ /* anything else */ [0, "", ""];
588
+
589
+ // Go to html and back, then peel off extra wrappers
590
+ // Note that we always prefix with some dummy text, because otherwise, IE<9 will strip out leading comment nodes in descendants. Total madness.
591
+ var markup = "ignored<div>" + wrap[1] + html + wrap[2] + "</div>";
592
+ if (typeof window['innerShiv'] == "function") {
593
+ div.appendChild(window['innerShiv'](markup));
594
+ } else {
595
+ div.innerHTML = markup;
596
+ }
597
+
598
+ // Move to the right depth
599
+ while (wrap[0]--)
600
+ div = div.lastChild;
601
+
602
+ return ko.utils.makeArray(div.lastChild.childNodes);
603
+ }
604
+
605
+ function jQueryHtmlParse(html) {
606
+ var elems = jQuery['clean']([html]);
607
+
608
+ // As of jQuery 1.7.1, jQuery parses the HTML by appending it to some dummy parent nodes held in an in-memory document fragment.
609
+ // Unfortunately, it never clears the dummy parent nodes from the document fragment, so it leaks memory over time.
610
+ // Fix this by finding the top-most dummy parent element, and detaching it from its owner fragment.
611
+ if (elems && elems[0]) {
612
+ // Find the top-most parent element that's a direct child of a document fragment
613
+ var elem = elems[0];
614
+ while (elem.parentNode && elem.parentNode.nodeType !== 11 /* i.e., DocumentFragment */)
615
+ elem = elem.parentNode;
616
+ // ... then detach it
617
+ if (elem.parentNode)
618
+ elem.parentNode.removeChild(elem);
619
+ }
620
+
621
+ return elems;
622
+ }
623
+
624
+ ko.utils.parseHtmlFragment = function(html) {
625
+ return typeof jQuery != 'undefined' ? jQueryHtmlParse(html) // As below, benefit from jQuery's optimisations where possible
626
+ : simpleHtmlParse(html); // ... otherwise, this simple logic will do in most common cases.
627
+ };
628
+
629
+ ko.utils.setHtml = function(node, html) {
630
+ ko.utils.emptyDomNode(node);
631
+
632
+ if ((html !== null) && (html !== undefined)) {
633
+ if (typeof html != 'string')
634
+ html = html.toString();
635
+
636
+ // jQuery contains a lot of sophisticated code to parse arbitrary HTML fragments,
637
+ // for example <tr> elements which are not normally allowed to exist on their own.
638
+ // If you've referenced jQuery we'll use that rather than duplicating its code.
639
+ if (typeof jQuery != 'undefined') {
640
+ jQuery(node)['html'](html);
641
+ } else {
642
+ // ... otherwise, use KO's own parsing logic.
643
+ var parsedNodes = ko.utils.parseHtmlFragment(html);
644
+ for (var i = 0; i < parsedNodes.length; i++)
645
+ node.appendChild(parsedNodes[i]);
646
+ }
647
+ }
648
+ };
649
+ })();
347
650
 
651
+ ko.exportSymbol('ko.utils.parseHtmlFragment', ko.utils.parseHtmlFragment);
652
+ ko.exportSymbol('ko.utils.setHtml', ko.utils.setHtml);
348
653
  ko.memoization = (function () {
349
654
  var memos = {};
350
655
 
@@ -387,12 +692,15 @@ ko.memoization = (function () {
387
692
  finally { delete memos[memoId]; }
388
693
  },
389
694
 
390
- unmemoizeDomNodeAndDescendants: function (domNode) {
695
+ unmemoizeDomNodeAndDescendants: function (domNode, extraCallbackParamsArray) {
391
696
  var memos = [];
392
697
  findMemoNodes(domNode, memos);
393
698
  for (var i = 0, j = memos.length; i < j; i++) {
394
699
  var node = memos[i].domNode;
395
- ko.memoization.unmemoize(memos[i].memoId, [node]);
700
+ var combinedParams = [node];
701
+ if (extraCallbackParamsArray)
702
+ ko.utils.arrayPushAll(combinedParams, extraCallbackParamsArray);
703
+ ko.memoization.unmemoize(memos[i].memoId, combinedParams);
396
704
  node.nodeValue = ""; // Neuter this node so we don't try to unmemoize it again
397
705
  if (node.parentNode)
398
706
  node.parentNode.removeChild(node); // If possible, erase it totally (not always possible - someone else might just hold a reference to it then call unmemoizeDomNodeAndDescendants again)
@@ -404,166 +712,370 @@ ko.memoization = (function () {
404
712
  return match ? match[1] : null;
405
713
  }
406
714
  };
407
- })();/// <reference path="../utils.js" />
715
+ })();
716
+
717
+ ko.exportSymbol('ko.memoization', ko.memoization);
718
+ ko.exportSymbol('ko.memoization.memoize', ko.memoization.memoize);
719
+ ko.exportSymbol('ko.memoization.unmemoize', ko.memoization.unmemoize);
720
+ ko.exportSymbol('ko.memoization.parseMemoText', ko.memoization.parseMemoText);
721
+ ko.exportSymbol('ko.memoization.unmemoizeDomNodeAndDescendants', ko.memoization.unmemoizeDomNodeAndDescendants);
722
+ ko.extenders = {
723
+ 'throttle': function(target, timeout) {
724
+ // Throttling means two things:
725
+
726
+ // (1) For dependent observables, we throttle *evaluations* so that, no matter how fast its dependencies
727
+ // notify updates, the target doesn't re-evaluate (and hence doesn't notify) faster than a certain rate
728
+ target['throttleEvaluation'] = timeout;
729
+
730
+ // (2) For writable targets (observables, or writable dependent observables), we throttle *writes*
731
+ // so the target cannot change value synchronously or faster than a certain rate
732
+ var writeTimeoutInstance = null;
733
+ return ko.dependentObservable({
734
+ 'read': target,
735
+ 'write': function(value) {
736
+ clearTimeout(writeTimeoutInstance);
737
+ writeTimeoutInstance = setTimeout(function() {
738
+ target(value);
739
+ }, timeout);
740
+ }
741
+ });
742
+ },
743
+
744
+ 'notify': function(target, notifyWhen) {
745
+ target["equalityComparer"] = notifyWhen == "always"
746
+ ? function() { return false } // Treat all values as not equal
747
+ : ko.observable["fn"]["equalityComparer"];
748
+ return target;
749
+ }
750
+ };
751
+
752
+ function applyExtenders(requestedExtenders) {
753
+ var target = this;
754
+ if (requestedExtenders) {
755
+ for (var key in requestedExtenders) {
756
+ var extenderHandler = ko.extenders[key];
757
+ if (typeof extenderHandler == 'function') {
758
+ target = extenderHandler(target, requestedExtenders[key]);
759
+ }
760
+ }
761
+ }
762
+ return target;
763
+ }
408
764
 
765
+ ko.exportSymbol('ko.extenders', ko.extenders);
409
766
  ko.subscription = function (callback, disposeCallback) {
410
767
  this.callback = callback;
411
- this.dispose = disposeCallback;
768
+ this.disposeCallback = disposeCallback;
769
+ ko.exportProperty(this, 'dispose', this.dispose);
770
+ };
771
+ ko.subscription.prototype.dispose = function () {
772
+ this.isDisposed = true;
773
+ this.disposeCallback();
412
774
  };
413
775
 
414
776
  ko.subscribable = function () {
415
- var _subscriptions = [];
777
+ this._subscriptions = {};
778
+
779
+ ko.utils.extend(this, ko.subscribable['fn']);
780
+ ko.exportProperty(this, 'subscribe', this.subscribe);
781
+ ko.exportProperty(this, 'extend', this.extend);
782
+ ko.exportProperty(this, 'getSubscriptionsCount', this.getSubscriptionsCount);
783
+ }
416
784
 
417
- this.subscribe = function (callback, callbackTarget) {
418
- var boundCallback = callbackTarget ? function () { callback.call(callbackTarget) } : callback;
785
+ var defaultEvent = "change";
786
+
787
+ ko.subscribable['fn'] = {
788
+ subscribe: function (callback, callbackTarget, event) {
789
+ event = event || defaultEvent;
790
+ var boundCallback = callbackTarget ? callback.bind(callbackTarget) : callback;
419
791
 
420
792
  var subscription = new ko.subscription(boundCallback, function () {
421
- ko.utils.arrayRemoveItem(_subscriptions, subscription);
422
- });
423
- _subscriptions.push(subscription);
793
+ ko.utils.arrayRemoveItem(this._subscriptions[event], subscription);
794
+ }.bind(this));
795
+
796
+ if (!this._subscriptions[event])
797
+ this._subscriptions[event] = [];
798
+ this._subscriptions[event].push(subscription);
424
799
  return subscription;
425
- };
800
+ },
426
801
 
427
- this.notifySubscribers = function (valueToNotify) {
428
- ko.utils.arrayForEach(_subscriptions.slice(0), function (subscription) {
429
- if (subscription)
430
- subscription.callback(valueToNotify);
431
- });
432
- };
802
+ "notifySubscribers": function (valueToNotify, event) {
803
+ event = event || defaultEvent;
804
+ if (this._subscriptions[event]) {
805
+ ko.utils.arrayForEach(this._subscriptions[event].slice(0), function (subscription) {
806
+ // In case a subscription was disposed during the arrayForEach cycle, check
807
+ // for isDisposed on each subscription before invoking its callback
808
+ if (subscription && (subscription.isDisposed !== true))
809
+ subscription.callback(valueToNotify);
810
+ });
811
+ }
812
+ },
813
+
814
+ getSubscriptionsCount: function () {
815
+ var total = 0;
816
+ for (var eventName in this._subscriptions) {
817
+ if (this._subscriptions.hasOwnProperty(eventName))
818
+ total += this._subscriptions[eventName].length;
819
+ }
820
+ return total;
821
+ },
822
+
823
+ extend: applyExtenders
824
+ };
433
825
 
434
- this.getSubscriptionsCount = function () {
435
- return _subscriptions.length;
436
- };
437
- }
438
826
 
439
827
  ko.isSubscribable = function (instance) {
440
- return typeof instance.subscribe == "function" && typeof instance.notifySubscribers == "function";
441
- };/// <reference path="subscribable.js" />
828
+ return typeof instance.subscribe == "function" && typeof instance["notifySubscribers"] == "function";
829
+ };
442
830
 
443
- ko.dependencyDetection = (function () {
444
- var _detectedDependencies = [];
831
+ ko.exportSymbol('ko.subscribable', ko.subscribable);
832
+ ko.exportSymbol('ko.isSubscribable', ko.isSubscribable);
445
833
 
834
+ ko.dependencyDetection = (function () {
835
+ var _frames = [];
836
+
446
837
  return {
447
- begin: function () {
448
- _detectedDependencies.push([]);
838
+ begin: function (callback) {
839
+ _frames.push({ callback: callback, distinctDependencies:[] });
449
840
  },
450
841
 
451
842
  end: function () {
452
- return _detectedDependencies.pop();
843
+ _frames.pop();
453
844
  },
454
845
 
455
846
  registerDependency: function (subscribable) {
456
847
  if (!ko.isSubscribable(subscribable))
457
848
  throw "Only subscribable things can act as dependencies";
458
- if (_detectedDependencies.length > 0) {
459
- _detectedDependencies[_detectedDependencies.length - 1].push(subscribable);
849
+ if (_frames.length > 0) {
850
+ var topFrame = _frames[_frames.length - 1];
851
+ if (ko.utils.arrayIndexOf(topFrame.distinctDependencies, subscribable) >= 0)
852
+ return;
853
+ topFrame.distinctDependencies.push(subscribable);
854
+ topFrame.callback(subscribable);
460
855
  }
461
856
  }
462
857
  };
463
- })();/// <reference path="dependencyDetection.js" />
858
+ })();var primitiveTypes = { 'undefined':true, 'boolean':true, 'number':true, 'string':true };
464
859
 
465
860
  ko.observable = function (initialValue) {
466
861
  var _latestValue = initialValue;
467
862
 
468
- function observable(newValue) {
863
+ function observable() {
469
864
  if (arguments.length > 0) {
470
- _latestValue = newValue;
471
- observable.notifySubscribers(_latestValue);
865
+ // Write
866
+
867
+ // Ignore writes if the value hasn't changed
868
+ if ((!observable['equalityComparer']) || !observable['equalityComparer'](_latestValue, arguments[0])) {
869
+ observable.valueWillMutate();
870
+ _latestValue = arguments[0];
871
+ observable.valueHasMutated();
872
+ }
873
+ return this; // Permits chained assignments
874
+ }
875
+ else {
876
+ // Read
877
+ ko.dependencyDetection.registerDependency(observable); // The caller only needs to be notified of changes if they did a "read" operation
878
+ return _latestValue;
472
879
  }
473
- else // The caller only needs to be notified of changes if they did a "read" operation
474
- ko.dependencyDetection.registerDependency(observable);
475
-
476
- return _latestValue;
477
880
  }
478
- observable.__ko_proto__ = ko.observable;
479
- observable.valueHasMutated = function () { observable.notifySubscribers(_latestValue); }
480
-
481
881
  ko.subscribable.call(observable);
882
+ observable.valueHasMutated = function () { observable["notifySubscribers"](_latestValue); }
883
+ observable.valueWillMutate = function () { observable["notifySubscribers"](_latestValue, "beforeChange"); }
884
+ ko.utils.extend(observable, ko.observable['fn']);
885
+
886
+ ko.exportProperty(observable, "valueHasMutated", observable.valueHasMutated);
887
+ ko.exportProperty(observable, "valueWillMutate", observable.valueWillMutate);
888
+
482
889
  return observable;
483
890
  }
891
+
892
+ ko.observable['fn'] = {
893
+ __ko_proto__: ko.observable,
894
+
895
+ "equalityComparer": function valuesArePrimitiveAndEqual(a, b) {
896
+ var oldValueIsPrimitive = (a === null) || (typeof(a) in primitiveTypes);
897
+ return oldValueIsPrimitive ? (a === b) : false;
898
+ }
899
+ };
900
+
484
901
  ko.isObservable = function (instance) {
485
902
  if ((instance === null) || (instance === undefined) || (instance.__ko_proto__ === undefined)) return false;
486
903
  if (instance.__ko_proto__ === ko.observable) return true;
487
904
  return ko.isObservable(instance.__ko_proto__); // Walk the prototype chain
488
905
  }
489
906
  ko.isWriteableObservable = function (instance) {
490
- return (typeof instance == "function") && instance.__ko_proto__ === ko.observable;
491
- }/// <reference path="observable.js" />
907
+ // Observable
908
+ if ((typeof instance == "function") && instance.__ko_proto__ === ko.observable)
909
+ return true;
910
+ // Writeable dependent observable
911
+ if ((typeof instance == "function") && (instance.__ko_proto__ === ko.dependentObservable) && (instance.hasWriteFunction))
912
+ return true;
913
+ // Anything else
914
+ return false;
915
+ }
492
916
 
917
+
918
+ ko.exportSymbol('ko.observable', ko.observable);
919
+ ko.exportSymbol('ko.isObservable', ko.isObservable);
920
+ ko.exportSymbol('ko.isWriteableObservable', ko.isWriteableObservable);
493
921
  ko.observableArray = function (initialValues) {
922
+ if (arguments.length == 0) {
923
+ // Zero-parameter constructor initializes to empty array
924
+ initialValues = [];
925
+ }
926
+ if ((initialValues !== null) && (initialValues !== undefined) && !('length' in initialValues))
927
+ throw new Error("The argument passed when initializing an observable array must be an array, or null, or undefined.");
928
+
494
929
  var result = new ko.observable(initialValues);
930
+ ko.utils.extend(result, ko.observableArray['fn']);
931
+
932
+ ko.exportProperty(result, "remove", result.remove);
933
+ ko.exportProperty(result, "removeAll", result.removeAll);
934
+ ko.exportProperty(result, "destroy", result.destroy);
935
+ ko.exportProperty(result, "destroyAll", result.destroyAll);
936
+ ko.exportProperty(result, "indexOf", result.indexOf);
937
+ ko.exportProperty(result, "replace", result.replace);
938
+
939
+ return result;
940
+ }
495
941
 
496
- ko.utils.arrayForEach(["pop", "push", "reverse", "shift", "sort", "splice", "unshift"], function (methodName) {
497
- result[methodName] = function () {
498
- var underlyingArray = result();
499
- var methodCallResult = underlyingArray[methodName].apply(underlyingArray, arguments);
500
- result.valueHasMutated();
501
- return methodCallResult;
502
- };
503
- });
504
-
505
- ko.utils.arrayForEach(["slice"], function (methodName) {
506
- result[methodName] = function () {
507
- var underlyingArray = result();
508
- return underlyingArray[methodName].apply(underlyingArray, arguments);
509
- };
510
- });
511
-
512
- result.remove = function (valueOrPredicate) {
513
- var underlyingArray = result();
514
- var remainingValues = [];
942
+ ko.observableArray['fn'] = {
943
+ remove: function (valueOrPredicate) {
944
+ var underlyingArray = this();
515
945
  var removedValues = [];
516
946
  var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) { return value === valueOrPredicate; };
517
- for (var i = 0, j = underlyingArray.length; i < j; i++) {
947
+ for (var i = 0; i < underlyingArray.length; i++) {
518
948
  var value = underlyingArray[i];
519
- if (!predicate(value))
520
- remainingValues.push(value);
521
- else
949
+ if (predicate(value)) {
950
+ if (removedValues.length === 0) {
951
+ this.valueWillMutate();
952
+ }
522
953
  removedValues.push(value);
954
+ underlyingArray.splice(i, 1);
955
+ i--;
956
+ }
957
+ }
958
+ if (removedValues.length) {
959
+ this.valueHasMutated();
523
960
  }
524
- result(remainingValues);
525
961
  return removedValues;
526
- };
962
+ },
527
963
 
528
- result.removeAll = function (arrayOfValues) {
964
+ removeAll: function (arrayOfValues) {
965
+ // If you passed zero args, we remove everything
966
+ if (arrayOfValues === undefined) {
967
+ var underlyingArray = this();
968
+ var allValues = underlyingArray.slice(0);
969
+ this.valueWillMutate();
970
+ underlyingArray.splice(0, underlyingArray.length);
971
+ this.valueHasMutated();
972
+ return allValues;
973
+ }
974
+ // If you passed an arg, we interpret it as an array of entries to remove
529
975
  if (!arrayOfValues)
530
976
  return [];
531
- return result.remove(function (value) {
977
+ return this.remove(function (value) {
532
978
  return ko.utils.arrayIndexOf(arrayOfValues, value) >= 0;
533
979
  });
534
- };
535
-
536
- result.destroy = function (valueOrPredicate) {
537
- var underlyingArray = result();
538
- var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) { return value === valueOrPredicate; };
539
- for (var i = underlyingArray.length - 1; i >= 0; i--) {
540
- var value = underlyingArray[i];
541
- if (predicate(value))
542
- underlyingArray[i]._destroy = true;
543
- }
544
- result.valueHasMutated();
545
- };
980
+ },
546
981
 
547
- result.destroyAll = function (arrayOfValues) {
982
+ destroy: function (valueOrPredicate) {
983
+ var underlyingArray = this();
984
+ var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) { return value === valueOrPredicate; };
985
+ this.valueWillMutate();
986
+ for (var i = underlyingArray.length - 1; i >= 0; i--) {
987
+ var value = underlyingArray[i];
988
+ if (predicate(value))
989
+ underlyingArray[i]["_destroy"] = true;
990
+ }
991
+ this.valueHasMutated();
992
+ },
993
+
994
+ destroyAll: function (arrayOfValues) {
995
+ // If you passed zero args, we destroy everything
996
+ if (arrayOfValues === undefined)
997
+ return this.destroy(function() { return true });
998
+
999
+ // If you passed an arg, we interpret it as an array of entries to destroy
548
1000
  if (!arrayOfValues)
549
1001
  return [];
550
- return result.destroy(function (value) {
1002
+ return this.destroy(function (value) {
551
1003
  return ko.utils.arrayIndexOf(arrayOfValues, value) >= 0;
552
- });
553
- };
1004
+ });
1005
+ },
554
1006
 
555
- result.indexOf = function (item) {
556
- var underlyingArray = result();
1007
+ indexOf: function (item) {
1008
+ var underlyingArray = this();
557
1009
  return ko.utils.arrayIndexOf(underlyingArray, item);
558
- };
1010
+ },
1011
+
1012
+ replace: function(oldItem, newItem) {
1013
+ var index = this.indexOf(oldItem);
1014
+ if (index >= 0) {
1015
+ this.valueWillMutate();
1016
+ this()[index] = newItem;
1017
+ this.valueHasMutated();
1018
+ }
1019
+ }
1020
+ }
559
1021
 
560
- return result;
561
- }/// <reference path="observable.js" />
1022
+ // Populate ko.observableArray.fn with read/write functions from native arrays
1023
+ ko.utils.arrayForEach(["pop", "push", "reverse", "shift", "sort", "splice", "unshift"], function (methodName) {
1024
+ ko.observableArray['fn'][methodName] = function () {
1025
+ var underlyingArray = this();
1026
+ this.valueWillMutate();
1027
+ var methodCallResult = underlyingArray[methodName].apply(underlyingArray, arguments);
1028
+ this.valueHasMutated();
1029
+ return methodCallResult;
1030
+ };
1031
+ });
562
1032
 
563
- ko.dependentObservable = function (evaluatorFunction, evaluatorFunctionTarget, options) {
564
- if (typeof evaluatorFunction != "function")
1033
+ // Populate ko.observableArray.fn with read-only functions from native arrays
1034
+ ko.utils.arrayForEach(["slice"], function (methodName) {
1035
+ ko.observableArray['fn'][methodName] = function () {
1036
+ var underlyingArray = this();
1037
+ return underlyingArray[methodName].apply(underlyingArray, arguments);
1038
+ };
1039
+ });
1040
+
1041
+ ko.exportSymbol('ko.observableArray', ko.observableArray);
1042
+ function prepareOptions(evaluatorFunctionOrOptions, evaluatorFunctionTarget, options) {
1043
+ if (evaluatorFunctionOrOptions && typeof evaluatorFunctionOrOptions == "object") {
1044
+ // Single-parameter syntax - everything is on this "options" param
1045
+ options = evaluatorFunctionOrOptions;
1046
+ } else {
1047
+ // Multi-parameter syntax - construct the options according to the params passed
1048
+ options = options || {};
1049
+ options["read"] = evaluatorFunctionOrOptions || options["read"];
1050
+ }
1051
+ // By here, "options" is always non-null
1052
+
1053
+ if (typeof options["read"] != "function")
565
1054
  throw "Pass a function that returns the value of the dependentObservable";
1055
+
1056
+ return options;
1057
+ }
566
1058
 
1059
+ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget, options) {
1060
+ var _latestValue,
1061
+ _hasBeenEvaluated = false,
1062
+ options = prepareOptions(evaluatorFunctionOrOptions, evaluatorFunctionTarget, options);
1063
+
1064
+ // Build "disposeWhenNodeIsRemoved" and "disposeWhenNodeIsRemovedCallback" option values
1065
+ // (Note: "disposeWhenNodeIsRemoved" option both proactively disposes as soon as the node is removed using ko.removeNode(),
1066
+ // plus adds a "disposeWhen" callback that, on each evaluation, disposes if the node was removed by some other means.)
1067
+ var disposeWhenNodeIsRemoved = (typeof options["disposeWhenNodeIsRemoved"] == "object") ? options["disposeWhenNodeIsRemoved"] : null;
1068
+ var disposeWhenNodeIsRemovedCallback = null;
1069
+ if (disposeWhenNodeIsRemoved) {
1070
+ disposeWhenNodeIsRemovedCallback = function() { dependentObservable.dispose() };
1071
+ ko.utils.domNodeDisposal.addDisposeCallback(disposeWhenNodeIsRemoved, disposeWhenNodeIsRemovedCallback);
1072
+ var existingDisposeWhenFunction = options["disposeWhen"];
1073
+ options["disposeWhen"] = function () {
1074
+ return (!ko.utils.domNodeIsAttachedToDocument(disposeWhenNodeIsRemoved))
1075
+ || ((typeof existingDisposeWhenFunction == "function") && existingDisposeWhenFunction());
1076
+ }
1077
+ }
1078
+
567
1079
  var _subscriptionsToDependencies = [];
568
1080
  function disposeAllSubscriptionsToDependencies() {
569
1081
  ko.utils.arrayForEach(_subscriptionsToDependencies, function (subscription) {
@@ -571,97 +1083,246 @@ ko.dependentObservable = function (evaluatorFunction, evaluatorFunctionTarget, o
571
1083
  });
572
1084
  _subscriptionsToDependencies = [];
573
1085
  }
1086
+
1087
+ var evaluationTimeoutInstance = null;
1088
+ function evaluatePossiblyAsync() {
1089
+ var throttleEvaluationTimeout = dependentObservable['throttleEvaluation'];
1090
+ if (throttleEvaluationTimeout && throttleEvaluationTimeout >= 0) {
1091
+ clearTimeout(evaluationTimeoutInstance);
1092
+ evaluationTimeoutInstance = setTimeout(evaluateImmediate, throttleEvaluationTimeout);
1093
+ } else
1094
+ evaluateImmediate();
1095
+ }
574
1096
 
575
- function replaceSubscriptionsToDependencies(newDependencies) {
576
- disposeAllSubscriptionsToDependencies();
577
- ko.utils.arrayForEach(newDependencies, function (dependency) {
578
- _subscriptionsToDependencies.push(dependency.subscribe(evaluate));
579
- });
580
- };
581
-
582
- var _latestValue, _isFirstEvaluation = true;
583
- function evaluate() {
584
- if ((!_isFirstEvaluation) && options && typeof options.disposeWhen == "function") {
585
- if (options.disposeWhen()) {
1097
+ function evaluateImmediate() {
1098
+ // Don't dispose on first evaluation, because the "disposeWhen" callback might
1099
+ // e.g., dispose when the associated DOM element isn't in the doc, and it's not
1100
+ // going to be in the doc until *after* the first evaluation
1101
+ if ((_hasBeenEvaluated) && typeof options["disposeWhen"] == "function") {
1102
+ if (options["disposeWhen"]()) {
586
1103
  dependentObservable.dispose();
587
1104
  return;
588
1105
  }
589
1106
  }
590
1107
 
591
1108
  try {
592
- ko.dependencyDetection.begin();
593
- _latestValue = evaluatorFunctionTarget ? evaluatorFunction.call(evaluatorFunctionTarget) : evaluatorFunction();
594
- } catch (ex) {
595
- throw ex;
1109
+ disposeAllSubscriptionsToDependencies();
1110
+ ko.dependencyDetection.begin(function(subscribable) {
1111
+ _subscriptionsToDependencies.push(subscribable.subscribe(evaluatePossiblyAsync));
1112
+ });
1113
+ var valueForThis = options["owner"] || evaluatorFunctionTarget; // If undefined, it will default to "window" by convention. This might change in the future.
1114
+ var newValue = options["read"].call(valueForThis);
1115
+ dependentObservable["notifySubscribers"](_latestValue, "beforeChange");
1116
+ _latestValue = newValue;
596
1117
  } finally {
597
- var distinctDependencies = ko.utils.arrayGetDistinctValues(ko.dependencyDetection.end());
598
- replaceSubscriptionsToDependencies(distinctDependencies);
1118
+ ko.dependencyDetection.end();
599
1119
  }
600
1120
 
601
- dependentObservable.notifySubscribers(_latestValue);
602
- _isFirstEvaluation = false;
1121
+ dependentObservable["notifySubscribers"](_latestValue);
1122
+ _hasBeenEvaluated = true;
603
1123
  }
604
1124
 
605
1125
  function dependentObservable() {
606
- if (arguments.length > 0)
607
- throw "Cannot write a value to a dependentObservable. Do not pass any parameters to it";
608
-
609
- ko.dependencyDetection.registerDependency(dependentObservable);
610
- return _latestValue;
1126
+ if (arguments.length > 0) {
1127
+ if (typeof options["write"] === "function") {
1128
+ // Writing a value
1129
+ var valueForThis = options["owner"] || evaluatorFunctionTarget; // If undefined, it will default to "window" by convention. This might change in the future.
1130
+ options["write"].apply(valueForThis, arguments);
1131
+ } else {
1132
+ throw "Cannot write a value to a dependentObservable unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters.";
1133
+ }
1134
+ } else {
1135
+ // Reading the value
1136
+ if (!_hasBeenEvaluated)
1137
+ evaluateImmediate();
1138
+ ko.dependencyDetection.registerDependency(dependentObservable);
1139
+ return _latestValue;
1140
+ }
611
1141
  }
612
- dependentObservable.__ko_proto__ = ko.dependentObservable;
613
1142
  dependentObservable.getDependenciesCount = function () { return _subscriptionsToDependencies.length; }
1143
+ dependentObservable.hasWriteFunction = typeof options["write"] === "function";
614
1144
  dependentObservable.dispose = function () {
1145
+ if (disposeWhenNodeIsRemoved)
1146
+ ko.utils.domNodeDisposal.removeDisposeCallback(disposeWhenNodeIsRemoved, disposeWhenNodeIsRemovedCallback);
615
1147
  disposeAllSubscriptionsToDependencies();
616
1148
  };
617
-
1149
+
618
1150
  ko.subscribable.call(dependentObservable);
619
- evaluate();
1151
+ ko.utils.extend(dependentObservable, ko.dependentObservable['fn']);
1152
+
1153
+ if (options['deferEvaluation'] !== true)
1154
+ evaluateImmediate();
1155
+
1156
+ ko.exportProperty(dependentObservable, 'dispose', dependentObservable.dispose);
1157
+ ko.exportProperty(dependentObservable, 'getDependenciesCount', dependentObservable.getDependenciesCount);
1158
+
620
1159
  return dependentObservable;
621
1160
  };
622
- ko.dependentObservable.__ko_proto__ = ko.observable;(function () {
623
- // Normally, SELECT elements and their OPTIONs can only take value of type 'string' (because the values
624
- // are stored on DOM attributes). ko.selectExtensions provides a way for SELECTs/OPTIONs to have values
625
- // that are arbitrary objects. This is very convenient when implementing things like cascading dropdowns.
626
- ko.selectExtensions = {
627
- readValue : function(element) {
628
- if (element.tagName == 'OPTION') {
629
- var valueAttributeValue = element.getAttribute("value");
630
- if (valueAttributeValue !== ko.bindingHandlers.options.optionValueDomDataKey)
631
- return valueAttributeValue;
632
- return ko.utils.domData.get(element, ko.bindingHandlers.options.optionValueDomDataKey);
633
- } else if (element.tagName == 'SELECT')
634
- return element.selectedIndex >= 0 ? ko.selectExtensions.readValue(element.options[element.selectedIndex]) : undefined;
635
- else
636
- return element.value;
637
- },
1161
+
1162
+ ko.dependentObservable['fn'] = {
1163
+ __ko_proto__: ko.dependentObservable
1164
+ };
1165
+
1166
+ ko.dependentObservable.__ko_proto__ = ko.observable;
1167
+
1168
+ ko.exportSymbol('ko.dependentObservable', ko.dependentObservable);
1169
+ ko.exportSymbol('ko.computed', ko.dependentObservable); // Make "ko.computed" an alias for "ko.dependentObservable"
1170
+ (function() {
1171
+ var maxNestedObservableDepth = 10; // Escape the (unlikely) pathalogical case where an observable's current value is itself (or similar reference cycle)
1172
+
1173
+ ko.toJS = function(rootObject) {
1174
+ if (arguments.length == 0)
1175
+ throw new Error("When calling ko.toJS, pass the object you want to convert.");
638
1176
 
639
- writeValue: function(element, value) {
640
- if (element.tagName == 'OPTION') {
641
- ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, value);
642
- element.value = ko.bindingHandlers.options.optionValueDomDataKey;
643
- } else if (element.tagName == 'SELECT') {
644
- for (var i = element.options.length - 1; i >= 0; i--) {
645
- if (ko.selectExtensions.readValue(element.options[i]) == value) {
646
- element.selectedIndex = i;
647
- break;
1177
+ // We just unwrap everything at every level in the object graph
1178
+ return mapJsObjectGraph(rootObject, function(valueToMap) {
1179
+ // Loop because an observable's value might in turn be another observable wrapper
1180
+ for (var i = 0; ko.isObservable(valueToMap) && (i < maxNestedObservableDepth); i++)
1181
+ valueToMap = valueToMap();
1182
+ return valueToMap;
1183
+ });
1184
+ };
1185
+
1186
+ ko.toJSON = function(rootObject) {
1187
+ var plainJavaScriptObject = ko.toJS(rootObject);
1188
+ return ko.utils.stringifyJson(plainJavaScriptObject);
1189
+ };
1190
+
1191
+ function mapJsObjectGraph(rootObject, mapInputCallback, visitedObjects) {
1192
+ visitedObjects = visitedObjects || new objectLookup();
1193
+
1194
+ rootObject = mapInputCallback(rootObject);
1195
+ var canHaveProperties = (typeof rootObject == "object") && (rootObject !== null) && (rootObject !== undefined) && (!(rootObject instanceof Date));
1196
+ if (!canHaveProperties)
1197
+ return rootObject;
1198
+
1199
+ var outputProperties = rootObject instanceof Array ? [] : {};
1200
+ visitedObjects.save(rootObject, outputProperties);
1201
+
1202
+ visitPropertiesOrArrayEntries(rootObject, function(indexer) {
1203
+ var propertyValue = mapInputCallback(rootObject[indexer]);
1204
+
1205
+ switch (typeof propertyValue) {
1206
+ case "boolean":
1207
+ case "number":
1208
+ case "string":
1209
+ case "function":
1210
+ outputProperties[indexer] = propertyValue;
1211
+ break;
1212
+ case "object":
1213
+ case "undefined":
1214
+ var previouslyMappedValue = visitedObjects.get(propertyValue);
1215
+ outputProperties[indexer] = (previouslyMappedValue !== undefined)
1216
+ ? previouslyMappedValue
1217
+ : mapJsObjectGraph(propertyValue, mapInputCallback, visitedObjects);
1218
+ break;
1219
+ }
1220
+ });
1221
+
1222
+ return outputProperties;
1223
+ }
1224
+
1225
+ function visitPropertiesOrArrayEntries(rootObject, visitorCallback) {
1226
+ if (rootObject instanceof Array) {
1227
+ for (var i = 0; i < rootObject.length; i++)
1228
+ visitorCallback(i);
1229
+ } else {
1230
+ for (var propertyName in rootObject)
1231
+ visitorCallback(propertyName);
1232
+ }
1233
+ };
1234
+
1235
+ function objectLookup() {
1236
+ var keys = [];
1237
+ var values = [];
1238
+ this.save = function(key, value) {
1239
+ var existingIndex = ko.utils.arrayIndexOf(keys, key);
1240
+ if (existingIndex >= 0)
1241
+ values[existingIndex] = value;
1242
+ else {
1243
+ keys.push(key);
1244
+ values.push(value);
1245
+ }
1246
+ };
1247
+ this.get = function(key) {
1248
+ var existingIndex = ko.utils.arrayIndexOf(keys, key);
1249
+ return (existingIndex >= 0) ? values[existingIndex] : undefined;
1250
+ };
1251
+ };
1252
+ })();
1253
+
1254
+ ko.exportSymbol('ko.toJS', ko.toJS);
1255
+ ko.exportSymbol('ko.toJSON', ko.toJSON);(function () {
1256
+ var hasDomDataExpandoProperty = '__ko__hasDomDataOptionValue__';
1257
+
1258
+ // Normally, SELECT elements and their OPTIONs can only take value of type 'string' (because the values
1259
+ // are stored on DOM attributes). ko.selectExtensions provides a way for SELECTs/OPTIONs to have values
1260
+ // that are arbitrary objects. This is very convenient when implementing things like cascading dropdowns.
1261
+ ko.selectExtensions = {
1262
+ readValue : function(element) {
1263
+ if (element.tagName == 'OPTION') {
1264
+ if (element[hasDomDataExpandoProperty] === true)
1265
+ return ko.utils.domData.get(element, ko.bindingHandlers.options.optionValueDomDataKey);
1266
+ return element.getAttribute("value");
1267
+ } else if (element.tagName == 'SELECT')
1268
+ return element.selectedIndex >= 0 ? ko.selectExtensions.readValue(element.options[element.selectedIndex]) : undefined;
1269
+ else
1270
+ return element.value;
1271
+ },
1272
+
1273
+ writeValue: function(element, value) {
1274
+ if (element.tagName == 'OPTION') {
1275
+ switch(typeof value) {
1276
+ case "string":
1277
+ ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, undefined);
1278
+ if (hasDomDataExpandoProperty in element) { // IE <= 8 throws errors if you delete non-existent properties from a DOM node
1279
+ delete element[hasDomDataExpandoProperty];
1280
+ }
1281
+ element.value = value;
1282
+ break;
1283
+ default:
1284
+ // Store arbitrary object using DomData
1285
+ ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, value);
1286
+ element[hasDomDataExpandoProperty] = true;
1287
+
1288
+ // Special treatment of numbers is just for backward compatibility. KO 1.2.1 wrote numerical values to element.value.
1289
+ element.value = typeof value === "number" ? value : "";
1290
+ break;
1291
+ }
1292
+ } else if (element.tagName == 'SELECT') {
1293
+ for (var i = element.options.length - 1; i >= 0; i--) {
1294
+ if (ko.selectExtensions.readValue(element.options[i]) == value) {
1295
+ element.selectedIndex = i;
1296
+ break;
648
1297
  }
649
1298
  }
650
- } else
1299
+ } else {
1300
+ if ((value === null) || (value === undefined))
1301
+ value = "";
651
1302
  element.value = value;
1303
+ }
652
1304
  }
653
- };
654
- })();/// <reference path="../utils.js" />
1305
+ };
1306
+ })();
1307
+
1308
+ ko.exportSymbol('ko.selectExtensions', ko.selectExtensions);
1309
+ ko.exportSymbol('ko.selectExtensions.readValue', ko.selectExtensions.readValue);
1310
+ ko.exportSymbol('ko.selectExtensions.writeValue', ko.selectExtensions.writeValue);
655
1311
 
656
1312
  ko.jsonExpressionRewriting = (function () {
657
- var restoreCapturedTokensRegex = /\[ko_token_(\d+)\]/g;
658
- var javaScriptAssignmentTarget = /^[\_$a-z][\_$a-z]*(\[.*?\])*(\.[\_$a-z][\_$a-z]*(\[.*?\])*)*$/i;
1313
+ var restoreCapturedTokensRegex = /\@ko_token_(\d+)\@/g;
1314
+ var javaScriptAssignmentTarget = /^[\_$a-z][\_$a-z0-9]*(\[.*?\])*(\.[\_$a-z][\_$a-z0-9]*(\[.*?\])*)*$/i;
659
1315
  var javaScriptReservedWords = ["true", "false"];
660
1316
 
661
1317
  function restoreTokens(string, tokens) {
662
- return string.replace(restoreCapturedTokensRegex, function (match, tokenIndex) {
663
- return tokens[tokenIndex];
664
- });
1318
+ var prevValue = null;
1319
+ while (string != prevValue) { // Keep restoring tokens until it no longer makes a difference (they may be nested)
1320
+ prevValue = string;
1321
+ string = string.replace(restoreCapturedTokensRegex, function (match, tokenIndex) {
1322
+ return tokens[tokenIndex];
1323
+ });
1324
+ }
1325
+ return string;
665
1326
  }
666
1327
 
667
1328
  function isWriteableValue(expression) {
@@ -670,17 +1331,35 @@ ko.jsonExpressionRewriting = (function () {
670
1331
  return expression.match(javaScriptAssignmentTarget) !== null;
671
1332
  }
672
1333
 
1334
+ function ensureQuoted(key) {
1335
+ var trimmedKey = ko.utils.stringTrim(key);
1336
+ switch (trimmedKey.length && trimmedKey.charAt(0)) {
1337
+ case "'":
1338
+ case '"':
1339
+ return key;
1340
+ default:
1341
+ return "'" + trimmedKey + "'";
1342
+ }
1343
+ }
1344
+
673
1345
  return {
674
- parseJson: function (jsonString) {
675
- jsonString = ko.utils.stringTrim(jsonString);
676
- if (jsonString.length < 3)
677
- return {};
1346
+ bindingRewriteValidators: [],
1347
+
1348
+ parseObjectLiteral: function(objectLiteralString) {
1349
+ // A full tokeniser+lexer would add too much weight to this library, so here's a simple parser
1350
+ // that is sufficient just to split an object literal string into a set of top-level key-value pairs
678
1351
 
679
- // We're going to split on commas, so first extract any blocks that may contain commas other than those at the top level
1352
+ var str = ko.utils.stringTrim(objectLiteralString);
1353
+ if (str.length < 3)
1354
+ return [];
1355
+ if (str.charAt(0) === "{")// Ignore any braces surrounding the whole object literal
1356
+ str = str.substring(1, str.length - 1);
1357
+
1358
+ // Pull out any string literals and regex literals
680
1359
  var tokens = [];
681
1360
  var tokenStart = null, tokenEndChar;
682
- for (var position = jsonString.charAt(0) == "{" ? 1 : 0; position < jsonString.length; position++) {
683
- var c = jsonString.charAt(position);
1361
+ for (var position = 0; position < str.length; position++) {
1362
+ var c = str.charAt(position);
684
1363
  if (tokenStart === null) {
685
1364
  switch (c) {
686
1365
  case '"':
@@ -689,153 +1368,630 @@ ko.jsonExpressionRewriting = (function () {
689
1368
  tokenStart = position;
690
1369
  tokenEndChar = c;
691
1370
  break;
692
- case "{":
693
- tokenStart = position;
694
- tokenEndChar = "}";
695
- break;
696
- case "[":
697
- tokenStart = position;
698
- tokenEndChar = "]";
699
- break;
700
1371
  }
701
- } else if (c == tokenEndChar) {
702
- var token = jsonString.substring(tokenStart, position + 1);
1372
+ } else if ((c == tokenEndChar) && (str.charAt(position - 1) !== "\\")) {
1373
+ var token = str.substring(tokenStart, position + 1);
703
1374
  tokens.push(token);
704
- var replacement = "[ko_token_" + (tokens.length - 1) + "]";
705
- jsonString = jsonString.substring(0, tokenStart) + replacement + jsonString.substring(position + 1);
1375
+ var replacement = "@ko_token_" + (tokens.length - 1) + "@";
1376
+ str = str.substring(0, tokenStart) + replacement + str.substring(position + 1);
706
1377
  position -= (token.length - replacement.length);
707
1378
  tokenStart = null;
708
1379
  }
709
1380
  }
710
1381
 
1382
+ // Next pull out balanced paren, brace, and bracket blocks
1383
+ tokenStart = null;
1384
+ tokenEndChar = null;
1385
+ var tokenDepth = 0, tokenStartChar = null;
1386
+ for (var position = 0; position < str.length; position++) {
1387
+ var c = str.charAt(position);
1388
+ if (tokenStart === null) {
1389
+ switch (c) {
1390
+ case "{": tokenStart = position; tokenStartChar = c;
1391
+ tokenEndChar = "}";
1392
+ break;
1393
+ case "(": tokenStart = position; tokenStartChar = c;
1394
+ tokenEndChar = ")";
1395
+ break;
1396
+ case "[": tokenStart = position; tokenStartChar = c;
1397
+ tokenEndChar = "]";
1398
+ break;
1399
+ }
1400
+ }
1401
+
1402
+ if (c === tokenStartChar)
1403
+ tokenDepth++;
1404
+ else if (c === tokenEndChar) {
1405
+ tokenDepth--;
1406
+ if (tokenDepth === 0) {
1407
+ var token = str.substring(tokenStart, position + 1);
1408
+ tokens.push(token);
1409
+ var replacement = "@ko_token_" + (tokens.length - 1) + "@";
1410
+ str = str.substring(0, tokenStart) + replacement + str.substring(position + 1);
1411
+ position -= (token.length - replacement.length);
1412
+ tokenStart = null;
1413
+ }
1414
+ }
1415
+ }
1416
+
711
1417
  // Now we can safely split on commas to get the key/value pairs
712
- var result = {};
713
- var keyValuePairs = jsonString.split(",");
1418
+ var result = [];
1419
+ var keyValuePairs = str.split(",");
714
1420
  for (var i = 0, j = keyValuePairs.length; i < j; i++) {
715
1421
  var pair = keyValuePairs[i];
716
1422
  var colonPos = pair.indexOf(":");
717
1423
  if ((colonPos > 0) && (colonPos < pair.length - 1)) {
718
- var key = ko.utils.stringTrim(pair.substring(0, colonPos));
719
- var value = ko.utils.stringTrim(pair.substring(colonPos + 1));
720
- if (key.charAt(0) == "{")
721
- key = key.substring(1);
722
- if (value.charAt(value.length - 1) == "}")
723
- value = value.substring(0, value.length - 1);
724
- key = ko.utils.stringTrim(restoreTokens(key, tokens));
725
- value = ko.utils.stringTrim(restoreTokens(value, tokens));
726
- result[key] = value;
1424
+ var key = pair.substring(0, colonPos);
1425
+ var value = pair.substring(colonPos + 1);
1426
+ result.push({ 'key': restoreTokens(key, tokens), 'value': restoreTokens(value, tokens) });
1427
+ } else {
1428
+ result.push({ 'unknown': restoreTokens(pair, tokens) });
727
1429
  }
728
1430
  }
729
- return result;
1431
+ return result;
730
1432
  },
731
1433
 
732
- insertPropertyAccessorsIntoJson: function (jsonString) {
733
- var parsed = ko.jsonExpressionRewriting.parseJson(jsonString);
734
- var propertyAccessorTokens = [];
735
- for (var key in parsed) {
736
- var value = parsed[key];
737
- if (isWriteableValue(value)) {
738
- if (propertyAccessorTokens.length > 0)
739
- propertyAccessorTokens.push(", ");
740
- propertyAccessorTokens.push(key + " : function(__ko_value) { " + value + " = __ko_value; }");
1434
+ insertPropertyAccessorsIntoJson: function (objectLiteralStringOrKeyValueArray) {
1435
+ var keyValueArray = typeof objectLiteralStringOrKeyValueArray === "string"
1436
+ ? ko.jsonExpressionRewriting.parseObjectLiteral(objectLiteralStringOrKeyValueArray)
1437
+ : objectLiteralStringOrKeyValueArray;
1438
+ var resultStrings = [], propertyAccessorResultStrings = [];
1439
+
1440
+ var keyValueEntry;
1441
+ for (var i = 0; keyValueEntry = keyValueArray[i]; i++) {
1442
+ if (resultStrings.length > 0)
1443
+ resultStrings.push(",");
1444
+
1445
+ if (keyValueEntry['key']) {
1446
+ var quotedKey = ensureQuoted(keyValueEntry['key']), val = keyValueEntry['value'];
1447
+ resultStrings.push(quotedKey);
1448
+ resultStrings.push(":");
1449
+ resultStrings.push(val);
1450
+
1451
+ if (isWriteableValue(ko.utils.stringTrim(val))) {
1452
+ if (propertyAccessorResultStrings.length > 0)
1453
+ propertyAccessorResultStrings.push(", ");
1454
+ propertyAccessorResultStrings.push(quotedKey + " : function(__ko_value) { " + val + " = __ko_value; }");
1455
+ }
1456
+ } else if (keyValueEntry['unknown']) {
1457
+ resultStrings.push(keyValueEntry['unknown']);
741
1458
  }
742
1459
  }
743
1460
 
744
- if (propertyAccessorTokens.length > 0) {
745
- var allPropertyAccessors = propertyAccessorTokens.join("");
746
- jsonString = jsonString + ", '_ko_property_writers' : { " + allPropertyAccessors + " } ";
1461
+ var combinedResult = resultStrings.join("");
1462
+ if (propertyAccessorResultStrings.length > 0) {
1463
+ var allPropertyAccessors = propertyAccessorResultStrings.join("");
1464
+ combinedResult = combinedResult + ", '_ko_property_writers' : { " + allPropertyAccessors + " } ";
747
1465
  }
748
1466
 
749
- return jsonString;
1467
+ return combinedResult;
1468
+ },
1469
+
1470
+ keyValueArrayContainsKey: function(keyValueArray, key) {
1471
+ for (var i = 0; i < keyValueArray.length; i++)
1472
+ if (ko.utils.stringTrim(keyValueArray[i]['key']) == key)
1473
+ return true;
1474
+ return false;
750
1475
  }
751
1476
  };
752
- })();/// <reference path="../subscribables/dependentObservable.js" />
1477
+ })();
753
1478
 
754
- (function () {
755
- var bindingAttributeName = "data-bind";
1479
+ ko.exportSymbol('ko.jsonExpressionRewriting', ko.jsonExpressionRewriting);
1480
+ ko.exportSymbol('ko.jsonExpressionRewriting.bindingRewriteValidators', ko.jsonExpressionRewriting.bindingRewriteValidators);
1481
+ ko.exportSymbol('ko.jsonExpressionRewriting.parseObjectLiteral', ko.jsonExpressionRewriting.parseObjectLiteral);
1482
+ ko.exportSymbol('ko.jsonExpressionRewriting.insertPropertyAccessorsIntoJson', ko.jsonExpressionRewriting.insertPropertyAccessorsIntoJson);
1483
+ (function() {
1484
+ // "Virtual elements" is an abstraction on top of the usual DOM API which understands the notion that comment nodes
1485
+ // may be used to represent hierarchy (in addition to the DOM's natural hierarchy).
1486
+ // If you call the DOM-manipulating functions on ko.virtualElements, you will be able to read and write the state
1487
+ // of that virtual hierarchy
1488
+ //
1489
+ // The point of all this is to support containerless templates (e.g., <!-- ko foreach:someCollection -->blah<!-- /ko -->)
1490
+ // without having to scatter special cases all over the binding and templating code.
1491
+
1492
+ // IE 9 cannot reliably read the "nodeValue" property of a comment node (see https://github.com/SteveSanderson/knockout/issues/186)
1493
+ // but it does give them a nonstandard alternative property called "text" that it can read reliably. Other browsers don't have that property.
1494
+ // So, use node.text where available, and node.nodeValue elsewhere
1495
+ var commentNodesHaveTextProperty = document.createComment("test").text === "<!--test-->";
1496
+
1497
+ var startCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*ko\s+(.*\:.*)\s*-->$/ : /^\s*ko\s+(.*\:.*)\s*$/;
1498
+ var endCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*\/ko\s*-->$/ : /^\s*\/ko\s*$/;
1499
+ var htmlTagsWithOptionallyClosingChildren = { 'ul': true, 'ol': true };
1500
+
1501
+ function isStartComment(node) {
1502
+ return (node.nodeType == 8) && (commentNodesHaveTextProperty ? node.text : node.nodeValue).match(startCommentRegex);
1503
+ }
1504
+
1505
+ function isEndComment(node) {
1506
+ return (node.nodeType == 8) && (commentNodesHaveTextProperty ? node.text : node.nodeValue).match(endCommentRegex);
1507
+ }
1508
+
1509
+ function getVirtualChildren(startComment, allowUnbalanced) {
1510
+ var currentNode = startComment;
1511
+ var depth = 1;
1512
+ var children = [];
1513
+ while (currentNode = currentNode.nextSibling) {
1514
+ if (isEndComment(currentNode)) {
1515
+ depth--;
1516
+ if (depth === 0)
1517
+ return children;
1518
+ }
1519
+
1520
+ children.push(currentNode);
1521
+
1522
+ if (isStartComment(currentNode))
1523
+ depth++;
1524
+ }
1525
+ if (!allowUnbalanced)
1526
+ throw new Error("Cannot find closing comment tag to match: " + startComment.nodeValue);
1527
+ return null;
1528
+ }
1529
+
1530
+ function getMatchingEndComment(startComment, allowUnbalanced) {
1531
+ var allVirtualChildren = getVirtualChildren(startComment, allowUnbalanced);
1532
+ if (allVirtualChildren) {
1533
+ if (allVirtualChildren.length > 0)
1534
+ return allVirtualChildren[allVirtualChildren.length - 1].nextSibling;
1535
+ return startComment.nextSibling;
1536
+ } else
1537
+ return null; // Must have no matching end comment, and allowUnbalanced is true
1538
+ }
1539
+
1540
+ function nodeArrayToText(nodeArray, cleanNodes) {
1541
+ var texts = [];
1542
+ for (var i = 0, j = nodeArray.length; i < j; i++) {
1543
+ if (cleanNodes)
1544
+ ko.utils.domNodeDisposal.cleanNode(nodeArray[i]);
1545
+ texts.push(ko.utils.outerHTML(nodeArray[i]));
1546
+ }
1547
+ return String.prototype.concat.apply("", texts);
1548
+ }
1549
+
1550
+ function getUnbalancedChildTags(node) {
1551
+ // e.g., from <div>OK</div><!-- ko blah --><span>Another</span>, returns: <!-- ko blah --><span>Another</span>
1552
+ // from <div>OK</div><!-- /ko --><!-- /ko -->, returns: <!-- /ko --><!-- /ko -->
1553
+ var childNode = node.firstChild, captureRemaining = null;
1554
+ if (childNode) {
1555
+ do {
1556
+ if (captureRemaining) // We already hit an unbalanced node and are now just scooping up all subsequent nodes
1557
+ captureRemaining.push(childNode);
1558
+ else if (isStartComment(childNode)) {
1559
+ var matchingEndComment = getMatchingEndComment(childNode, /* allowUnbalanced: */ true);
1560
+ if (matchingEndComment) // It's a balanced tag, so skip immediately to the end of this virtual set
1561
+ childNode = matchingEndComment;
1562
+ else
1563
+ captureRemaining = [childNode]; // It's unbalanced, so start capturing from this point
1564
+ } else if (isEndComment(childNode)) {
1565
+ captureRemaining = [childNode]; // It's unbalanced (if it wasn't, we'd have skipped over it already), so start capturing
1566
+ }
1567
+ } while (childNode = childNode.nextSibling);
1568
+ }
1569
+ return captureRemaining;
1570
+ }
1571
+
1572
+ ko.virtualElements = {
1573
+ allowedBindings: {},
1574
+
1575
+ childNodes: function(node) {
1576
+ return isStartComment(node) ? getVirtualChildren(node) : node.childNodes;
1577
+ },
1578
+
1579
+ emptyNode: function(node) {
1580
+ if (!isStartComment(node))
1581
+ ko.utils.emptyDomNode(node);
1582
+ else {
1583
+ var virtualChildren = ko.virtualElements.childNodes(node);
1584
+ for (var i = 0, j = virtualChildren.length; i < j; i++)
1585
+ ko.removeNode(virtualChildren[i]);
1586
+ }
1587
+ },
1588
+
1589
+ setDomNodeChildren: function(node, childNodes) {
1590
+ if (!isStartComment(node))
1591
+ ko.utils.setDomNodeChildren(node, childNodes);
1592
+ else {
1593
+ ko.virtualElements.emptyNode(node);
1594
+ var endCommentNode = node.nextSibling; // Must be the next sibling, as we just emptied the children
1595
+ for (var i = 0, j = childNodes.length; i < j; i++)
1596
+ endCommentNode.parentNode.insertBefore(childNodes[i], endCommentNode);
1597
+ }
1598
+ },
1599
+
1600
+ prepend: function(containerNode, nodeToPrepend) {
1601
+ if (!isStartComment(containerNode)) {
1602
+ if (containerNode.firstChild)
1603
+ containerNode.insertBefore(nodeToPrepend, containerNode.firstChild);
1604
+ else
1605
+ containerNode.appendChild(nodeToPrepend);
1606
+ } else {
1607
+ // Start comments must always have a parent and at least one following sibling (the end comment)
1608
+ containerNode.parentNode.insertBefore(nodeToPrepend, containerNode.nextSibling);
1609
+ }
1610
+ },
1611
+
1612
+ insertAfter: function(containerNode, nodeToInsert, insertAfterNode) {
1613
+ if (!isStartComment(containerNode)) {
1614
+ // Insert after insertion point
1615
+ if (insertAfterNode.nextSibling)
1616
+ containerNode.insertBefore(nodeToInsert, insertAfterNode.nextSibling);
1617
+ else
1618
+ containerNode.appendChild(nodeToInsert);
1619
+ } else {
1620
+ // Children of start comments must always have a parent and at least one following sibling (the end comment)
1621
+ containerNode.parentNode.insertBefore(nodeToInsert, insertAfterNode.nextSibling);
1622
+ }
1623
+ },
1624
+
1625
+ nextSibling: function(node) {
1626
+ if (!isStartComment(node)) {
1627
+ if (node.nextSibling && isEndComment(node.nextSibling))
1628
+ return undefined;
1629
+ return node.nextSibling;
1630
+ } else {
1631
+ return getMatchingEndComment(node).nextSibling;
1632
+ }
1633
+ },
1634
+
1635
+ virtualNodeBindingValue: function(node) {
1636
+ var regexMatch = isStartComment(node);
1637
+ return regexMatch ? regexMatch[1] : null;
1638
+ },
1639
+
1640
+ extractAnonymousTemplateIfVirtualElement: function(node) {
1641
+ if (ko.virtualElements.virtualNodeBindingValue(node)) {
1642
+ // Empty out the virtual children, and associate "node" with an anonymous template matching its previous virtual children
1643
+ var virtualChildren = ko.virtualElements.childNodes(node);
1644
+ var anonymousTemplateText = nodeArrayToText(virtualChildren, true);
1645
+ ko.virtualElements.emptyNode(node);
1646
+ new ko.templateSources.anonymousTemplate(node).text(anonymousTemplateText);
1647
+ }
1648
+ },
1649
+
1650
+ normaliseVirtualElementDomStructure: function(elementVerified) {
1651
+ // Workaround for https://github.com/SteveSanderson/knockout/issues/155
1652
+ // (IE <= 8 or IE 9 quirks mode parses your HTML weirdly, treating closing </li> tags as if they don't exist, thereby moving comment nodes
1653
+ // that are direct descendants of <ul> into the preceding <li>)
1654
+ if (!htmlTagsWithOptionallyClosingChildren[elementVerified.tagName.toLowerCase()])
1655
+ return;
1656
+
1657
+ // Scan immediate children to see if they contain unbalanced comment tags. If they do, those comment tags
1658
+ // must be intended to appear *after* that child, so move them there.
1659
+ var childNode = elementVerified.firstChild;
1660
+ if (childNode) {
1661
+ do {
1662
+ if (childNode.nodeType === 1) {
1663
+ var unbalancedTags = getUnbalancedChildTags(childNode);
1664
+ if (unbalancedTags) {
1665
+ // Fix up the DOM by moving the unbalanced tags to where they most likely were intended to be placed - *after* the child
1666
+ var nodeToInsertBefore = childNode.nextSibling;
1667
+ for (var i = 0; i < unbalancedTags.length; i++) {
1668
+ if (nodeToInsertBefore)
1669
+ elementVerified.insertBefore(unbalancedTags[i], nodeToInsertBefore);
1670
+ else
1671
+ elementVerified.appendChild(unbalancedTags[i]);
1672
+ }
1673
+ }
1674
+ }
1675
+ } while (childNode = childNode.nextSibling);
1676
+ }
1677
+ }
1678
+ };
1679
+ })();
1680
+ (function() {
1681
+ var defaultBindingAttributeName = "data-bind";
1682
+
1683
+ ko.bindingProvider = function() { };
1684
+
1685
+ ko.utils.extend(ko.bindingProvider.prototype, {
1686
+ 'nodeHasBindings': function(node) {
1687
+ switch (node.nodeType) {
1688
+ case 1: return node.getAttribute(defaultBindingAttributeName) != null; // Element
1689
+ case 8: return ko.virtualElements.virtualNodeBindingValue(node) != null; // Comment node
1690
+ default: return false;
1691
+ }
1692
+ },
1693
+
1694
+ 'getBindings': function(node, bindingContext) {
1695
+ var bindingsString = this['getBindingsString'](node, bindingContext);
1696
+ return bindingsString ? this['parseBindingsString'](bindingsString, bindingContext) : null;
1697
+ },
1698
+
1699
+ // The following function is only used internally by this default provider.
1700
+ // It's not part of the interface definition for a general binding provider.
1701
+ 'getBindingsString': function(node, bindingContext) {
1702
+ switch (node.nodeType) {
1703
+ case 1: return node.getAttribute(defaultBindingAttributeName); // Element
1704
+ case 8: return ko.virtualElements.virtualNodeBindingValue(node); // Comment node
1705
+ default: return null;
1706
+ }
1707
+ },
1708
+
1709
+ // The following function is only used internally by this default provider.
1710
+ // It's not part of the interface definition for a general binding provider.
1711
+ 'parseBindingsString': function(bindingsString, bindingContext) {
1712
+ try {
1713
+ var viewModel = bindingContext['$data'];
1714
+ var rewrittenBindings = " { " + ko.jsonExpressionRewriting.insertPropertyAccessorsIntoJson(bindingsString) + " } ";
1715
+ return ko.utils.evalWithinScope(rewrittenBindings, viewModel === null ? window : viewModel, bindingContext);
1716
+ } catch (ex) {
1717
+ throw new Error("Unable to parse bindings.\nMessage: " + ex + ";\nBindings value: " + bindingsString);
1718
+ }
1719
+ }
1720
+ });
1721
+
1722
+ ko.bindingProvider['instance'] = new ko.bindingProvider();
1723
+ })();
1724
+
1725
+ ko.exportSymbol('ko.bindingProvider', ko.bindingProvider);(function () {
756
1726
  ko.bindingHandlers = {};
757
1727
 
758
- function parseBindingAttribute(attributeText, viewModel) {
759
- try {
760
- var json = " { " + ko.jsonExpressionRewriting.insertPropertyAccessorsIntoJson(attributeText) + " } ";
761
- return ko.utils.evalWithinScope(json, viewModel === null ? window : viewModel);
762
- } catch (ex) {
763
- throw new Error("Unable to parse binding attribute.\nMessage: " + ex + ";\nAttribute value: " + attributeText);
1728
+ ko.bindingContext = function(dataItem, parentBindingContext) {
1729
+ this['$data'] = dataItem;
1730
+ if (parentBindingContext) {
1731
+ this['$parent'] = parentBindingContext['$data'];
1732
+ this['$parents'] = (parentBindingContext['$parents'] || []).slice(0);
1733
+ this['$parents'].unshift(this['$parent']);
1734
+ this['$root'] = parentBindingContext['$root'];
1735
+ } else {
1736
+ this['$parents'] = [];
1737
+ this['$root'] = dataItem;
764
1738
  }
765
1739
  }
1740
+ ko.bindingContext.prototype['createChildContext'] = function (dataItem) {
1741
+ return new ko.bindingContext(dataItem, this);
1742
+ };
766
1743
 
767
- function invokeBindingHandler(handler, element, dataValue, allBindings, viewModel) {
768
- handler(element, dataValue, allBindings, viewModel);
1744
+ function validateThatBindingIsAllowedForVirtualElements(bindingName) {
1745
+ var validator = ko.virtualElements.allowedBindings[bindingName];
1746
+ if (!validator)
1747
+ throw new Error("The binding '" + bindingName + "' cannot be used with virtual elements")
769
1748
  }
770
1749
 
771
- ko.applyBindingsToNode = function (node, bindings, viewModel) {
772
- var isFirstEvaluation = true;
1750
+ function applyBindingsToDescendantsInternal (viewModel, elementVerified) {
1751
+ var currentChild, nextInQueue = elementVerified.childNodes[0];
1752
+ while (currentChild = nextInQueue) {
1753
+ // Keep a record of the next child *before* applying bindings, in case the binding removes the current child from its position
1754
+ nextInQueue = ko.virtualElements.nextSibling(currentChild);
1755
+ applyBindingsToNodeAndDescendantsInternal(viewModel, currentChild, false);
1756
+ }
1757
+ }
1758
+
1759
+ function applyBindingsToNodeAndDescendantsInternal (viewModel, nodeVerified, isRootNodeForBindingContext) {
1760
+ var shouldBindDescendants = true;
1761
+
1762
+ // Perf optimisation: Apply bindings only if...
1763
+ // (1) It's a root element for this binding context, as we will need to store the binding context on this node
1764
+ // Note that we can't store binding contexts on non-elements (e.g., text nodes), as IE doesn't allow expando properties for those
1765
+ // (2) It might have bindings (e.g., it has a data-bind attribute, or it's a marker for a containerless template)
1766
+ var isElement = (nodeVerified.nodeType == 1);
1767
+ if (isElement) // Workaround IE <= 8 HTML parsing weirdness
1768
+ ko.virtualElements.normaliseVirtualElementDomStructure(nodeVerified);
1769
+
1770
+ var shouldApplyBindings = (isElement && isRootNodeForBindingContext) // Case (1)
1771
+ || ko.bindingProvider['instance']['nodeHasBindings'](nodeVerified); // Case (2)
1772
+ if (shouldApplyBindings)
1773
+ shouldBindDescendants = applyBindingsToNodeInternal(nodeVerified, null, viewModel, isRootNodeForBindingContext).shouldBindDescendants;
1774
+
1775
+ if (isElement && shouldBindDescendants)
1776
+ applyBindingsToDescendantsInternal(viewModel, nodeVerified);
1777
+ }
1778
+
1779
+ function applyBindingsToNodeInternal (node, bindings, viewModelOrBindingContext, isRootNodeForBindingContext) {
1780
+ // Need to be sure that inits are only run once, and updates never run until all the inits have been run
1781
+ var initPhase = 0; // 0 = before all inits, 1 = during inits, 2 = after all inits
1782
+
1783
+ // Pre-process any anonymous template bounded by comment nodes
1784
+ ko.virtualElements.extractAnonymousTemplateIfVirtualElement(node);
1785
+
1786
+ // Each time the dependentObservable is evaluated (after data changes),
1787
+ // the binding attribute is reparsed so that it can pick out the correct
1788
+ // model properties in the context of the changed data.
1789
+ // DOM event callbacks need to be able to access this changed data,
1790
+ // so we need a single parsedBindings variable (shared by all callbacks
1791
+ // associated with this node's bindings) that all the closures can access.
1792
+ var parsedBindings;
1793
+ function makeValueAccessor(bindingKey) {
1794
+ return function () { return parsedBindings[bindingKey] }
1795
+ }
1796
+ function parsedBindingsAccessor() {
1797
+ return parsedBindings;
1798
+ }
1799
+
1800
+ var bindingHandlerThatControlsDescendantBindings;
773
1801
  new ko.dependentObservable(
774
1802
  function () {
1803
+ // Ensure we have a nonnull binding context to work with
1804
+ var bindingContextInstance = viewModelOrBindingContext && (viewModelOrBindingContext instanceof ko.bindingContext)
1805
+ ? viewModelOrBindingContext
1806
+ : new ko.bindingContext(ko.utils.unwrapObservable(viewModelOrBindingContext));
1807
+ var viewModel = bindingContextInstance['$data'];
1808
+
1809
+ // We only need to store the bindingContext at the root of the subtree where it applies
1810
+ // as all descendants will be able to find it by scanning up their ancestry
1811
+ if (isRootNodeForBindingContext)
1812
+ ko.storedBindingContextForNode(node, bindingContextInstance);
1813
+
1814
+ // Use evaluatedBindings if given, otherwise fall back on asking the bindings provider to give us some bindings
775
1815
  var evaluatedBindings = (typeof bindings == "function") ? bindings() : bindings;
776
- var parsedBindings = evaluatedBindings || parseBindingAttribute(node.getAttribute(bindingAttributeName), viewModel);
777
-
778
- // First run all the inits, so bindings can register for notification on changes
779
- if (isFirstEvaluation) {
780
- for (var bindingKey in parsedBindings) {
781
- if (ko.bindingHandlers[bindingKey] && typeof ko.bindingHandlers[bindingKey].init == "function")
782
- invokeBindingHandler(ko.bindingHandlers[bindingKey].init, node, parsedBindings[bindingKey], parsedBindings, viewModel);
783
- }
784
- }
785
-
786
- // ... then run all the updates, which might trigger changes even on the first evaluation
787
- for (var bindingKey in parsedBindings) {
788
- if (ko.bindingHandlers[bindingKey] && typeof ko.bindingHandlers[bindingKey].update == "function")
789
- invokeBindingHandler(ko.bindingHandlers[bindingKey].update, node, parsedBindings[bindingKey], parsedBindings, viewModel);
1816
+ parsedBindings = evaluatedBindings || ko.bindingProvider['instance']['getBindings'](node, bindingContextInstance);
1817
+
1818
+ if (parsedBindings) {
1819
+ // First run all the inits, so bindings can register for notification on changes
1820
+ if (initPhase === 0) {
1821
+ initPhase = 1;
1822
+ for (var bindingKey in parsedBindings) {
1823
+ var binding = ko.bindingHandlers[bindingKey];
1824
+ if (binding && node.nodeType === 8)
1825
+ validateThatBindingIsAllowedForVirtualElements(bindingKey);
1826
+
1827
+ if (binding && typeof binding["init"] == "function") {
1828
+ var handlerInitFn = binding["init"];
1829
+ var initResult = handlerInitFn(node, makeValueAccessor(bindingKey), parsedBindingsAccessor, viewModel, bindingContextInstance);
1830
+
1831
+ // If this binding handler claims to control descendant bindings, make a note of this
1832
+ if (initResult && initResult['controlsDescendantBindings']) {
1833
+ if (bindingHandlerThatControlsDescendantBindings !== undefined)
1834
+ 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.");
1835
+ bindingHandlerThatControlsDescendantBindings = bindingKey;
1836
+ }
1837
+ }
1838
+ }
1839
+ initPhase = 2;
1840
+ }
1841
+
1842
+ // ... then run all the updates, which might trigger changes even on the first evaluation
1843
+ if (initPhase === 2) {
1844
+ for (var bindingKey in parsedBindings) {
1845
+ var binding = ko.bindingHandlers[bindingKey];
1846
+ if (binding && typeof binding["update"] == "function") {
1847
+ var handlerUpdateFn = binding["update"];
1848
+ handlerUpdateFn(node, makeValueAccessor(bindingKey), parsedBindingsAccessor, viewModel, bindingContextInstance);
1849
+ }
1850
+ }
1851
+ }
790
1852
  }
791
1853
  },
792
1854
  null,
793
- { disposeWhen: function () { return !ko.utils.domNodeIsAttachedToDocument(node); } }
1855
+ { 'disposeWhenNodeIsRemoved' : node }
794
1856
  );
795
- isFirstEvaluation = false;
1857
+
1858
+ return {
1859
+ shouldBindDescendants: bindingHandlerThatControlsDescendantBindings === undefined
1860
+ };
796
1861
  };
797
1862
 
798
- ko.applyBindings = function (rootNode, viewModel) {
799
- var elemsWithBindingAttribute = ko.utils.getElementsHavingAttribute(rootNode, bindingAttributeName);
800
- ko.utils.arrayForEach(elemsWithBindingAttribute, function (element) {
801
- ko.applyBindingsToNode(element, null, viewModel);
802
- });
1863
+ var storedBindingContextDomDataKey = "__ko_bindingContext__";
1864
+ ko.storedBindingContextForNode = function (node, bindingContext) {
1865
+ if (arguments.length == 2)
1866
+ ko.utils.domData.set(node, storedBindingContextDomDataKey, bindingContext);
1867
+ else
1868
+ return ko.utils.domData.get(node, storedBindingContextDomDataKey);
1869
+ }
1870
+
1871
+ ko.applyBindingsToNode = function (node, bindings, viewModel) {
1872
+ if (node.nodeType === 1) // If it's an element, workaround IE <= 8 HTML parsing weirdness
1873
+ ko.virtualElements.normaliseVirtualElementDomStructure(node);
1874
+ return applyBindingsToNodeInternal(node, bindings, viewModel, true);
803
1875
  };
804
- })();/// <reference path="bindingAttributeSyntax.js" />
805
1876
 
806
- ko.bindingHandlers.click = {
807
- init: function (element, value, allBindings, viewModel) {
808
- ko.utils.registerEventHandler(element, "click", function (event) {
809
- try { value.call(viewModel); }
810
- finally {
811
- if (event.preventDefault)
812
- event.preventDefault();
813
- else
814
- event.returnValue = false;
815
- }
816
- });
1877
+ ko.applyBindingsToDescendants = function(viewModel, rootNode) {
1878
+ if (rootNode.nodeType === 1)
1879
+ applyBindingsToDescendantsInternal(viewModel, rootNode);
1880
+ };
1881
+
1882
+ ko.applyBindings = function (viewModel, rootNode) {
1883
+ if (rootNode && (rootNode.nodeType !== 1) && (rootNode.nodeType !== 8))
1884
+ throw new Error("ko.applyBindings: first parameter should be your view model; second parameter should be a DOM node");
1885
+ rootNode = rootNode || window.document.body; // Make "rootNode" parameter optional
1886
+
1887
+ applyBindingsToNodeAndDescendantsInternal(viewModel, rootNode, true);
1888
+ };
1889
+
1890
+ // Retrieving binding context from arbitrary nodes
1891
+ ko.contextFor = function(node) {
1892
+ // We can only do something meaningful for elements and comment nodes (in particular, not text nodes, as IE can't store domdata for them)
1893
+ switch (node.nodeType) {
1894
+ case 1:
1895
+ case 8:
1896
+ var context = ko.storedBindingContextForNode(node);
1897
+ if (context) return context;
1898
+ if (node.parentNode) return ko.contextFor(node.parentNode);
1899
+ break;
1900
+ }
1901
+ return undefined;
1902
+ };
1903
+ ko.dataFor = function(node) {
1904
+ var context = ko.contextFor(node);
1905
+ return context ? context['$data'] : undefined;
1906
+ };
1907
+
1908
+ ko.exportSymbol('ko.bindingHandlers', ko.bindingHandlers);
1909
+ ko.exportSymbol('ko.applyBindings', ko.applyBindings);
1910
+ ko.exportSymbol('ko.applyBindingsToDescendants', ko.applyBindingsToDescendants);
1911
+ ko.exportSymbol('ko.applyBindingsToNode', ko.applyBindingsToNode);
1912
+ ko.exportSymbol('ko.contextFor', ko.contextFor);
1913
+ ko.exportSymbol('ko.dataFor', ko.dataFor);
1914
+ })();// For certain common events (currently just 'click'), allow a simplified data-binding syntax
1915
+ // e.g. click:handler instead of the usual full-length event:{click:handler}
1916
+ var eventHandlersWithShortcuts = ['click'];
1917
+ ko.utils.arrayForEach(eventHandlersWithShortcuts, function(eventName) {
1918
+ ko.bindingHandlers[eventName] = {
1919
+ 'init': function(element, valueAccessor, allBindingsAccessor, viewModel) {
1920
+ var newValueAccessor = function () {
1921
+ var result = {};
1922
+ result[eventName] = valueAccessor();
1923
+ return result;
1924
+ };
1925
+ return ko.bindingHandlers['event']['init'].call(this, element, newValueAccessor, allBindingsAccessor, viewModel);
1926
+ }
1927
+ }
1928
+ });
1929
+
1930
+
1931
+ ko.bindingHandlers['event'] = {
1932
+ 'init' : function (element, valueAccessor, allBindingsAccessor, viewModel) {
1933
+ var eventsToHandle = valueAccessor() || {};
1934
+ for(var eventNameOutsideClosure in eventsToHandle) {
1935
+ (function() {
1936
+ var eventName = eventNameOutsideClosure; // Separate variable to be captured by event handler closure
1937
+ if (typeof eventName == "string") {
1938
+ ko.utils.registerEventHandler(element, eventName, function (event) {
1939
+ var handlerReturnValue;
1940
+ var handlerFunction = valueAccessor()[eventName];
1941
+ if (!handlerFunction)
1942
+ return;
1943
+ var allBindings = allBindingsAccessor();
1944
+
1945
+ try {
1946
+ // Take all the event args, and prefix with the viewmodel
1947
+ var argsForHandler = ko.utils.makeArray(arguments);
1948
+ argsForHandler.unshift(viewModel);
1949
+ handlerReturnValue = handlerFunction.apply(viewModel, argsForHandler);
1950
+ } finally {
1951
+ if (handlerReturnValue !== true) { // Normally we want to prevent default action. Developer can override this be explicitly returning true.
1952
+ if (event.preventDefault)
1953
+ event.preventDefault();
1954
+ else
1955
+ event.returnValue = false;
1956
+ }
1957
+ }
1958
+
1959
+ var bubble = allBindings[eventName + 'Bubble'] !== false;
1960
+ if (!bubble) {
1961
+ event.cancelBubble = true;
1962
+ if (event.stopPropagation)
1963
+ event.stopPropagation();
1964
+ }
1965
+ });
1966
+ }
1967
+ })();
1968
+ }
817
1969
  }
818
1970
  };
819
1971
 
820
- ko.bindingHandlers.submit = {
821
- init: function (element, value, allBindings, viewModel) {
822
- if (typeof value != "function")
823
- throw new Error("The value for a submit binding must be a function to invoke on submit");
1972
+ ko.bindingHandlers['submit'] = {
1973
+ 'init': function (element, valueAccessor, allBindingsAccessor, viewModel) {
1974
+ if (typeof valueAccessor() != "function")
1975
+ throw new Error("The value for a submit binding must be a function");
824
1976
  ko.utils.registerEventHandler(element, "submit", function (event) {
825
- try { value.call(viewModel, element); }
1977
+ var handlerReturnValue;
1978
+ var value = valueAccessor();
1979
+ try { handlerReturnValue = value.call(viewModel, element); }
826
1980
  finally {
827
- if (event.preventDefault)
828
- event.preventDefault();
829
- else
830
- event.returnValue = false;
1981
+ if (handlerReturnValue !== true) { // Normally we want to prevent default action. Developer can override this be explicitly returning true.
1982
+ if (event.preventDefault)
1983
+ event.preventDefault();
1984
+ else
1985
+ event.returnValue = false;
1986
+ }
831
1987
  }
832
1988
  });
833
1989
  }
834
1990
  };
835
1991
 
836
- ko.bindingHandlers.visible = {
837
- update: function (element, value) {
838
- value = ko.utils.unwrapObservable(value);
1992
+ ko.bindingHandlers['visible'] = {
1993
+ 'update': function (element, valueAccessor) {
1994
+ var value = ko.utils.unwrapObservable(valueAccessor());
839
1995
  var isCurrentlyVisible = !(element.style.display == "none");
840
1996
  if (value && !isCurrentlyVisible)
841
1997
  element.style.display = "";
@@ -844,9 +2000,9 @@ ko.bindingHandlers.visible = {
844
2000
  }
845
2001
  }
846
2002
 
847
- ko.bindingHandlers.enable = {
848
- update: function (element, value) {
849
- value = ko.utils.unwrapObservable(value);
2003
+ ko.bindingHandlers['enable'] = {
2004
+ 'update': function (element, valueAccessor) {
2005
+ var value = ko.utils.unwrapObservable(valueAccessor());
850
2006
  if (value && element.disabled)
851
2007
  element.removeAttribute("disabled");
852
2008
  else if ((!value) && (!element.disabled))
@@ -854,23 +2010,75 @@ ko.bindingHandlers.enable = {
854
2010
  }
855
2011
  };
856
2012
 
857
- ko.bindingHandlers.disable = { update: function (element, value) { ko.bindingHandlers.enable.update(element, !ko.utils.unwrapObservable(value)); } };
2013
+ ko.bindingHandlers['disable'] = {
2014
+ 'update': function (element, valueAccessor) {
2015
+ ko.bindingHandlers['enable']['update'](element, function() { return !ko.utils.unwrapObservable(valueAccessor()) });
2016
+ }
2017
+ };
858
2018
 
859
- ko.bindingHandlers.value = {
860
- init: function (element, value, allBindings) {
861
- var eventName = allBindings.valueUpdate || "change";
862
- if (ko.isWriteableObservable(value))
863
- ko.utils.registerEventHandler(element, eventName, function () {
864
- value(ko.selectExtensions.readValue(this));
865
- });
866
- else if (allBindings._ko_property_writers && allBindings._ko_property_writers.value)
867
- ko.utils.registerEventHandler(element, eventName, function () {
868
- allBindings._ko_property_writers.value(ko.selectExtensions.readValue(this));
869
- });
2019
+ function ensureDropdownSelectionIsConsistentWithModelValue(element, modelValue, preferModelValue) {
2020
+ if (preferModelValue) {
2021
+ if (modelValue !== ko.selectExtensions.readValue(element))
2022
+ ko.selectExtensions.writeValue(element, modelValue);
2023
+ }
2024
+
2025
+ // No matter which direction we're syncing in, we want the end result to be equality between dropdown value and model value.
2026
+ // If they aren't equal, either we prefer the dropdown value, or the model value couldn't be represented, so either way,
2027
+ // change the model value to match the dropdown.
2028
+ if (modelValue !== ko.selectExtensions.readValue(element))
2029
+ ko.utils.triggerEvent(element, "change");
2030
+ };
2031
+
2032
+ ko.bindingHandlers['value'] = {
2033
+ 'init': function (element, valueAccessor, allBindingsAccessor) {
2034
+ // Always catch "change" event; possibly other events too if asked
2035
+ var eventsToCatch = ["change"];
2036
+ var requestedEventsToCatch = allBindingsAccessor()["valueUpdate"];
2037
+ if (requestedEventsToCatch) {
2038
+ if (typeof requestedEventsToCatch == "string") // Allow both individual event names, and arrays of event names
2039
+ requestedEventsToCatch = [requestedEventsToCatch];
2040
+ ko.utils.arrayPushAll(eventsToCatch, requestedEventsToCatch);
2041
+ eventsToCatch = ko.utils.arrayGetDistinctValues(eventsToCatch);
2042
+ }
2043
+
2044
+ ko.utils.arrayForEach(eventsToCatch, function(eventName) {
2045
+ // The syntax "after<eventname>" means "run the handler asynchronously after the event"
2046
+ // This is useful, for example, to catch "keydown" events after the browser has updated the control
2047
+ // (otherwise, ko.selectExtensions.readValue(this) will receive the control's value *before* the key event)
2048
+ var handleEventAsynchronously = false;
2049
+ if (ko.utils.stringStartsWith(eventName, "after")) {
2050
+ handleEventAsynchronously = true;
2051
+ eventName = eventName.substring("after".length);
2052
+ }
2053
+ var runEventHandler = handleEventAsynchronously ? function(handler) { setTimeout(handler, 0) }
2054
+ : function(handler) { handler() };
2055
+
2056
+ ko.utils.registerEventHandler(element, eventName, function () {
2057
+ runEventHandler(function() {
2058
+ var modelValue = valueAccessor();
2059
+ var elementValue = ko.selectExtensions.readValue(element);
2060
+ if (ko.isWriteableObservable(modelValue))
2061
+ modelValue(elementValue);
2062
+ else {
2063
+ var allBindings = allBindingsAccessor();
2064
+ if (allBindings['_ko_property_writers'] && allBindings['_ko_property_writers']['value'])
2065
+ allBindings['_ko_property_writers']['value'](elementValue);
2066
+ }
2067
+ });
2068
+ });
2069
+ });
870
2070
  },
871
- update: function (element, value) {
872
- var newValue = ko.utils.unwrapObservable(value);
873
- if (newValue != ko.selectExtensions.readValue(element)) {
2071
+ 'update': function (element, valueAccessor) {
2072
+ var newValue = ko.utils.unwrapObservable(valueAccessor());
2073
+ var elementValue = ko.selectExtensions.readValue(element);
2074
+ var valueHasChanged = (newValue != elementValue);
2075
+
2076
+ // JavaScript's 0 == "" behavious is unfortunate here as it prevents writing 0 to an empty text box (loose equality suggests the values are the same).
2077
+ // We don't want to do a strict equality comparison as that is more confusing for developers in certain cases, so we specifically special case 0 != "" here.
2078
+ if ((newValue === 0) && (elementValue !== 0) && (elementValue !== "0"))
2079
+ valueHasChanged = true;
2080
+
2081
+ if (valueHasChanged) {
874
2082
  var applyValueAction = function () { ko.selectExtensions.writeValue(element, newValue); };
875
2083
  applyValueAction();
876
2084
 
@@ -881,41 +2089,70 @@ ko.bindingHandlers.value = {
881
2089
  if (alsoApplyAsynchronously)
882
2090
  setTimeout(applyValueAction, 0);
883
2091
  }
2092
+
2093
+ // If you try to set a model value that can't be represented in an already-populated dropdown, reject that change,
2094
+ // because you're not allowed to have a model value that disagrees with a visible UI selection.
2095
+ if ((element.tagName == "SELECT") && (element.length > 0))
2096
+ ensureDropdownSelectionIsConsistentWithModelValue(element, newValue, /* preferModelValue */ false);
884
2097
  }
885
2098
  };
886
2099
 
887
- ko.bindingHandlers.options = {
888
- update: function (element, value, allBindings) {
2100
+ ko.bindingHandlers['options'] = {
2101
+ 'update': function (element, valueAccessor, allBindingsAccessor) {
889
2102
  if (element.tagName != "SELECT")
890
2103
  throw new Error("options binding applies only to SELECT elements");
891
2104
 
2105
+ var selectWasPreviouslyEmpty = element.length == 0;
892
2106
  var previousSelectedValues = ko.utils.arrayMap(ko.utils.arrayFilter(element.childNodes, function (node) {
893
2107
  return node.tagName && node.tagName == "OPTION" && node.selected;
894
2108
  }), function (node) {
895
2109
  return ko.selectExtensions.readValue(node) || node.innerText || node.textContent;
896
2110
  });
2111
+ var previousScrollTop = element.scrollTop;
2112
+ element.scrollTop = 0; // Workaround for a Chrome rendering bug. Note that we restore the scroll position later. (https://github.com/SteveSanderson/knockout/issues/215)
897
2113
 
898
- value = ko.utils.unwrapObservable(value);
2114
+ var value = ko.utils.unwrapObservable(valueAccessor());
899
2115
  var selectedValue = element.value;
900
- ko.utils.emptyDomNode(element);
2116
+
2117
+ // Remove all existing <option>s.
2118
+ // Need to use .remove() rather than .removeChild() for <option>s otherwise IE behaves oddly (https://github.com/SteveSanderson/knockout/issues/134)
2119
+ while (element.length > 0) {
2120
+ ko.cleanNode(element.options[0]);
2121
+ element.remove(0);
2122
+ }
901
2123
 
902
2124
  if (value) {
2125
+ var allBindings = allBindingsAccessor();
903
2126
  if (typeof value.length != "number")
904
2127
  value = [value];
905
- if (allBindings.optionsCaption) {
2128
+ if (allBindings['optionsCaption']) {
906
2129
  var option = document.createElement("OPTION");
907
- option.innerHTML = allBindings.optionsCaption;
2130
+ ko.utils.setHtml(option, allBindings['optionsCaption']);
908
2131
  ko.selectExtensions.writeValue(option, undefined);
909
2132
  element.appendChild(option);
910
2133
  }
911
2134
  for (var i = 0, j = value.length; i < j; i++) {
912
2135
  var option = document.createElement("OPTION");
913
- var optionValue = typeof allBindings.optionsValue == "string" ? value[i][allBindings.optionsValue] : value[i];
914
- if (typeof optionValue == 'object')
915
- ko.selectExtensions.writeValue(option, optionValue);
2136
+
2137
+ // Apply a value to the option element
2138
+ var optionValue = typeof allBindings['optionsValue'] == "string" ? value[i][allBindings['optionsValue']] : value[i];
2139
+ optionValue = ko.utils.unwrapObservable(optionValue);
2140
+ ko.selectExtensions.writeValue(option, optionValue);
2141
+
2142
+ // Apply some text to the option element
2143
+ var optionsTextValue = allBindings['optionsText'];
2144
+ var optionText;
2145
+ if (typeof optionsTextValue == "function")
2146
+ optionText = optionsTextValue(value[i]); // Given a function; run it against the data value
2147
+ else if (typeof optionsTextValue == "string")
2148
+ optionText = value[i][optionsTextValue]; // Given a string; treat it as a property name on the data value
916
2149
  else
917
- option.value = optionValue.toString();
918
- option.innerHTML = (typeof allBindings.optionsText == "string" ? value[i][allBindings.optionsText] : optionValue).toString();
2150
+ optionText = optionValue; // Given no optionsText arg; use the data value itself
2151
+ if ((optionText === null) || (optionText === undefined))
2152
+ optionText = "";
2153
+
2154
+ ko.utils.setTextContent(option, optionText);
2155
+
919
2156
  element.appendChild(option);
920
2157
  }
921
2158
 
@@ -929,12 +2166,22 @@ ko.bindingHandlers.options = {
929
2166
  countSelectionsRetained++;
930
2167
  }
931
2168
  }
2169
+
2170
+ if (previousScrollTop)
2171
+ element.scrollTop = previousScrollTop;
2172
+
2173
+ if (selectWasPreviouslyEmpty && ('value' in allBindings)) {
2174
+ // Ensure consistency between model value and selected option.
2175
+ // If the dropdown is being populated for the first time here (or was otherwise previously empty),
2176
+ // the dropdown selection state is meaningless, so we preserve the model value.
2177
+ ensureDropdownSelectionIsConsistentWithModelValue(element, ko.utils.unwrapObservable(allBindings['value']), /* preferModelValue */ true);
2178
+ }
932
2179
  }
933
2180
  }
934
2181
  };
935
- ko.bindingHandlers.options.optionValueDomDataKey = '__ko.bindingHandlers.options.optionValueDomData__';
2182
+ ko.bindingHandlers['options'].optionValueDomDataKey = '__ko.optionValueDomData__';
936
2183
 
937
- ko.bindingHandlers.selectedOptions = {
2184
+ ko.bindingHandlers['selectedOptions'] = {
938
2185
  getSelectedValuesFromSelectNode: function (selectNode) {
939
2186
  var result = [];
940
2187
  var nodes = selectNode.childNodes;
@@ -945,17 +2192,23 @@ ko.bindingHandlers.selectedOptions = {
945
2192
  }
946
2193
  return result;
947
2194
  },
948
- init: function (element, value, allBindings) {
949
- if (ko.isWriteableObservable(value))
950
- ko.utils.registerEventHandler(element, "change", function () { value(ko.bindingHandlers.selectedOptions.getSelectedValuesFromSelectNode(this)); });
951
- else if (allBindings._ko_property_writers && allBindings._ko_property_writers.value)
952
- ko.utils.registerEventHandler(element, "change", function () { allBindings._ko_property_writers.value(ko.bindingHandlers.selectedOptions.getSelectedValuesFromSelectNode(this)); });
2195
+ 'init': function (element, valueAccessor, allBindingsAccessor) {
2196
+ ko.utils.registerEventHandler(element, "change", function () {
2197
+ var value = valueAccessor();
2198
+ if (ko.isWriteableObservable(value))
2199
+ value(ko.bindingHandlers['selectedOptions'].getSelectedValuesFromSelectNode(this));
2200
+ else {
2201
+ var allBindings = allBindingsAccessor();
2202
+ if (allBindings['_ko_property_writers'] && allBindings['_ko_property_writers']['value'])
2203
+ allBindings['_ko_property_writers']['value'](ko.bindingHandlers['selectedOptions'].getSelectedValuesFromSelectNode(this));
2204
+ }
2205
+ });
953
2206
  },
954
- update: function (element, value) {
2207
+ 'update': function (element, valueAccessor) {
955
2208
  if (element.tagName != "SELECT")
956
2209
  throw new Error("values binding applies only to SELECT elements");
957
2210
 
958
- var newValue = ko.utils.unwrapObservable(value);
2211
+ var newValue = ko.utils.unwrapObservable(valueAccessor());
959
2212
  if (newValue && typeof newValue.length == "number") {
960
2213
  var nodes = element.childNodes;
961
2214
  for (var i = 0, j = nodes.length; i < j; i++) {
@@ -967,17 +2220,26 @@ ko.bindingHandlers.selectedOptions = {
967
2220
  }
968
2221
  };
969
2222
 
970
- ko.bindingHandlers.text = {
971
- update: function (element, value) {
972
- value = ko.utils.unwrapObservable(value);
973
- typeof element.innerText == "string" ? element.innerText = value
974
- : element.textContent = value;
2223
+ ko.bindingHandlers['text'] = {
2224
+ 'update': function (element, valueAccessor) {
2225
+ ko.utils.setTextContent(element, valueAccessor());
2226
+ }
2227
+ };
2228
+
2229
+ ko.bindingHandlers['html'] = {
2230
+ 'init': function() {
2231
+ // Prevent binding on the dynamically-injected HTML (as developers are unlikely to expect that, and it has security implications)
2232
+ return { 'controlsDescendantBindings': true };
2233
+ },
2234
+ 'update': function (element, valueAccessor) {
2235
+ var value = ko.utils.unwrapObservable(valueAccessor());
2236
+ ko.utils.setHtml(element, value);
975
2237
  }
976
2238
  };
977
2239
 
978
- ko.bindingHandlers.css = {
979
- update: function (element, value) {
980
- value = value || {};
2240
+ ko.bindingHandlers['css'] = {
2241
+ 'update': function (element, valueAccessor) {
2242
+ var value = ko.utils.unwrapObservable(valueAccessor() || {});
981
2243
  for (var className in value) {
982
2244
  if (typeof className == "string") {
983
2245
  var shouldHaveClass = ko.utils.unwrapObservable(value[className]);
@@ -987,9 +2249,9 @@ ko.bindingHandlers.css = {
987
2249
  }
988
2250
  };
989
2251
 
990
- ko.bindingHandlers.style = {
991
- update: function (element, value) {
992
- value = ko.utils.unwrapObservable(value || {});
2252
+ ko.bindingHandlers['style'] = {
2253
+ 'update': function (element, valueAccessor) {
2254
+ var value = ko.utils.unwrapObservable(valueAccessor() || {});
993
2255
  for (var styleName in value) {
994
2256
  if (typeof styleName == "string") {
995
2257
  var styleValue = ko.utils.unwrapObservable(value[styleName]);
@@ -999,108 +2261,411 @@ ko.bindingHandlers.style = {
999
2261
  }
1000
2262
  };
1001
2263
 
1002
- ko.bindingHandlers.uniqueName = {
1003
- init: function (element, value) {
1004
- if (value) {
1005
- element.name = "ko_unique_" + (++ko.bindingHandlers.uniqueName.currentIndex);
2264
+ ko.bindingHandlers['uniqueName'] = {
2265
+ 'init': function (element, valueAccessor) {
2266
+ if (valueAccessor()) {
2267
+ element.name = "ko_unique_" + (++ko.bindingHandlers['uniqueName'].currentIndex);
1006
2268
 
1007
- // Workaround IE 6 issue - http://www.matts411.com/post/setting_the_name_attribute_in_ie_dom/
1008
- if (/MSIE 6/i.test(navigator.userAgent))
1009
- element.mergeAttributes(document.createElement("<INPUT name='" + element.name + "'/>"), false);
2269
+ // Workaround IE 6/7 issue
2270
+ // - https://github.com/SteveSanderson/knockout/issues/197
2271
+ // - http://www.matts411.com/post/setting_the_name_attribute_in_ie_dom/
2272
+ if (ko.utils.isIe6 || ko.utils.isIe7)
2273
+ element.mergeAttributes(document.createElement("<input name='" + element.name + "'/>"), false);
1010
2274
  }
1011
2275
  }
1012
2276
  };
1013
- ko.bindingHandlers.uniqueName.currentIndex = 0;
1014
-
1015
- ko.bindingHandlers.checked = {
1016
- init: function (element, value, allBindings) {
1017
- if (ko.isWriteableObservable(value)) {
1018
- var updateHandler;
1019
- if (element.type == "checkbox")
1020
- updateHandler = function () { value(this.checked) };
1021
- else if (element.type == "radio")
1022
- updateHandler = function () { if (this.checked) value(this.value) };
1023
- if (updateHandler) {
1024
- ko.utils.registerEventHandler(element, "change", updateHandler);
1025
- ko.utils.registerEventHandler(element, "click", updateHandler);
1026
- }
1027
- } else if (allBindings._ko_property_writers && allBindings._ko_property_writers.checked) {
1028
- var updateHandler;
1029
- if (element.type == "checkbox")
1030
- updateHandler = function () { allBindings._ko_property_writers.checked(this.checked) };
1031
- else if (element.type == "radio")
1032
- updateHandler = function () { if (this.checked) allBindings._ko_property_writers.checked(this.value) };
1033
- if (updateHandler) {
1034
- ko.utils.registerEventHandler(element, "change", updateHandler);
1035
- ko.utils.registerEventHandler(element, "click", updateHandler);
2277
+ ko.bindingHandlers['uniqueName'].currentIndex = 0;
2278
+
2279
+ ko.bindingHandlers['checked'] = {
2280
+ 'init': function (element, valueAccessor, allBindingsAccessor) {
2281
+ var updateHandler = function() {
2282
+ var valueToWrite;
2283
+ if (element.type == "checkbox") {
2284
+ valueToWrite = element.checked;
2285
+ } else if ((element.type == "radio") && (element.checked)) {
2286
+ valueToWrite = element.value;
2287
+ } else {
2288
+ return; // "checked" binding only responds to checkboxes and selected radio buttons
1036
2289
  }
1037
- }
2290
+
2291
+ var modelValue = valueAccessor();
2292
+ if ((element.type == "checkbox") && (ko.utils.unwrapObservable(modelValue) instanceof Array)) {
2293
+ // For checkboxes bound to an array, we add/remove the checkbox value to that array
2294
+ // This works for both observable and non-observable arrays
2295
+ var existingEntryIndex = ko.utils.arrayIndexOf(ko.utils.unwrapObservable(modelValue), element.value);
2296
+ if (element.checked && (existingEntryIndex < 0))
2297
+ modelValue.push(element.value);
2298
+ else if ((!element.checked) && (existingEntryIndex >= 0))
2299
+ modelValue.splice(existingEntryIndex, 1);
2300
+ } else if (ko.isWriteableObservable(modelValue)) {
2301
+ if (modelValue() !== valueToWrite) { // Suppress repeated events when there's nothing new to notify (some browsers raise them)
2302
+ modelValue(valueToWrite);
2303
+ }
2304
+ } else {
2305
+ var allBindings = allBindingsAccessor();
2306
+ if (allBindings['_ko_property_writers'] && allBindings['_ko_property_writers']['checked']) {
2307
+ allBindings['_ko_property_writers']['checked'](valueToWrite);
2308
+ }
2309
+ }
2310
+ };
2311
+ ko.utils.registerEventHandler(element, "click", updateHandler);
1038
2312
 
1039
2313
  // IE 6 won't allow radio buttons to be selected unless they have a name
1040
2314
  if ((element.type == "radio") && !element.name)
1041
- ko.bindingHandlers.uniqueName.init(element, true);
2315
+ ko.bindingHandlers['uniqueName']['init'](element, function() { return true });
1042
2316
  },
1043
- update: function (element, value) {
1044
- value = ko.utils.unwrapObservable(value);
1045
- if (element.type == "checkbox")
1046
- element.checked = value;
1047
- else if (element.type == "radio")
2317
+ 'update': function (element, valueAccessor) {
2318
+ var value = ko.utils.unwrapObservable(valueAccessor());
2319
+
2320
+ if (element.type == "checkbox") {
2321
+ if (value instanceof Array) {
2322
+ // When bound to an array, the checkbox being checked represents its value being present in that array
2323
+ element.checked = ko.utils.arrayIndexOf(value, element.value) >= 0;
2324
+ } else {
2325
+ // When bound to anything other value (not an array), the checkbox being checked represents the value being trueish
2326
+ element.checked = value;
2327
+ }
2328
+ } else if (element.type == "radio") {
1048
2329
  element.checked = (element.value == value);
2330
+ }
1049
2331
  }
1050
- };/// <reference path="../utils.js" />
2332
+ };
1051
2333
 
1052
- ko.templateEngine = function () {
1053
- this.renderTemplate = function (templateName, data, options) {
1054
- throw "Override renderTemplate in your ko.templateEngine subclass";
2334
+ ko.bindingHandlers['attr'] = {
2335
+ 'update': function(element, valueAccessor, allBindingsAccessor) {
2336
+ var value = ko.utils.unwrapObservable(valueAccessor()) || {};
2337
+ for (var attrName in value) {
2338
+ if (typeof attrName == "string") {
2339
+ var attrValue = ko.utils.unwrapObservable(value[attrName]);
2340
+
2341
+ // To cover cases like "attr: { checked:someProp }", we want to remove the attribute entirely
2342
+ // when someProp is a "no value"-like value (strictly null, false, or undefined)
2343
+ // (because the absence of the "checked" attr is how to mark an element as not checked, etc.)
2344
+ if ((attrValue === false) || (attrValue === null) || (attrValue === undefined))
2345
+ element.removeAttribute(attrName);
2346
+ else
2347
+ element.setAttribute(attrName, attrValue.toString());
2348
+ }
2349
+ }
2350
+ }
2351
+ };
2352
+
2353
+ ko.bindingHandlers['hasfocus'] = {
2354
+ 'init': function(element, valueAccessor, allBindingsAccessor) {
2355
+ var writeValue = function(valueToWrite) {
2356
+ var modelValue = valueAccessor();
2357
+ if (valueToWrite == ko.utils.unwrapObservable(modelValue))
2358
+ return;
2359
+
2360
+ if (ko.isWriteableObservable(modelValue))
2361
+ modelValue(valueToWrite);
2362
+ else {
2363
+ var allBindings = allBindingsAccessor();
2364
+ if (allBindings['_ko_property_writers'] && allBindings['_ko_property_writers']['hasfocus']) {
2365
+ allBindings['_ko_property_writers']['hasfocus'](valueToWrite);
2366
+ }
2367
+ }
2368
+ };
2369
+ ko.utils.registerEventHandler(element, "focus", function() { writeValue(true) });
2370
+ ko.utils.registerEventHandler(element, "focusin", function() { writeValue(true) }); // For IE
2371
+ ko.utils.registerEventHandler(element, "blur", function() { writeValue(false) });
2372
+ ko.utils.registerEventHandler(element, "focusout", function() { writeValue(false) }); // For IE
2373
+ },
2374
+ 'update': function(element, valueAccessor) {
2375
+ var value = ko.utils.unwrapObservable(valueAccessor());
2376
+ value ? element.focus() : element.blur();
2377
+ ko.utils.triggerEvent(element, value ? "focusin" : "focusout"); // For IE, which doesn't reliably fire "focus" or "blur" events synchronously
2378
+ }
2379
+ };
2380
+
2381
+ // "with: someExpression" is equivalent to "template: { if: someExpression, data: someExpression }"
2382
+ ko.bindingHandlers['with'] = {
2383
+ makeTemplateValueAccessor: function(valueAccessor) {
2384
+ return function() { var value = valueAccessor(); return { 'if': value, 'data': value, 'templateEngine': ko.nativeTemplateEngine.instance } };
2385
+ },
2386
+ 'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2387
+ return ko.bindingHandlers['template']['init'](element, ko.bindingHandlers['with'].makeTemplateValueAccessor(valueAccessor));
1055
2388
  },
1056
- this.isTemplateRewritten = function (templateName) {
1057
- throw "Override isTemplateRewritten in your ko.templateEngine subclass";
2389
+ 'update': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2390
+ return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['with'].makeTemplateValueAccessor(valueAccessor), allBindingsAccessor, viewModel, bindingContext);
2391
+ }
2392
+ };
2393
+ ko.jsonExpressionRewriting.bindingRewriteValidators['with'] = false; // Can't rewrite control flow bindings
2394
+ ko.virtualElements.allowedBindings['with'] = true;
2395
+
2396
+ // "if: someExpression" is equivalent to "template: { if: someExpression }"
2397
+ ko.bindingHandlers['if'] = {
2398
+ makeTemplateValueAccessor: function(valueAccessor) {
2399
+ return function() { return { 'if': valueAccessor(), 'templateEngine': ko.nativeTemplateEngine.instance } };
2400
+ },
2401
+ 'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2402
+ return ko.bindingHandlers['template']['init'](element, ko.bindingHandlers['if'].makeTemplateValueAccessor(valueAccessor));
1058
2403
  },
1059
- this.rewriteTemplate = function (templateName, rewriterCallback) {
1060
- throw "Override rewriteTemplate in your ko.templateEngine subclass";
2404
+ 'update': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2405
+ return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['if'].makeTemplateValueAccessor(valueAccessor), allBindingsAccessor, viewModel, bindingContext);
2406
+ }
2407
+ };
2408
+ ko.jsonExpressionRewriting.bindingRewriteValidators['if'] = false; // Can't rewrite control flow bindings
2409
+ ko.virtualElements.allowedBindings['if'] = true;
2410
+
2411
+ // "ifnot: someExpression" is equivalent to "template: { ifnot: someExpression }"
2412
+ ko.bindingHandlers['ifnot'] = {
2413
+ makeTemplateValueAccessor: function(valueAccessor) {
2414
+ return function() { return { 'ifnot': valueAccessor(), 'templateEngine': ko.nativeTemplateEngine.instance } };
2415
+ },
2416
+ 'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2417
+ return ko.bindingHandlers['template']['init'](element, ko.bindingHandlers['ifnot'].makeTemplateValueAccessor(valueAccessor));
1061
2418
  },
1062
- this.createJavaScriptEvaluatorBlock = function (script) {
1063
- throw "Override createJavaScriptEvaluatorBlock in your ko.templateEngine subclass";
2419
+ 'update': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2420
+ return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['ifnot'].makeTemplateValueAccessor(valueAccessor), allBindingsAccessor, viewModel, bindingContext);
2421
+ }
2422
+ };
2423
+ ko.jsonExpressionRewriting.bindingRewriteValidators['ifnot'] = false; // Can't rewrite control flow bindings
2424
+ ko.virtualElements.allowedBindings['ifnot'] = true;
2425
+
2426
+ // "foreach: someExpression" is equivalent to "template: { foreach: someExpression }"
2427
+ // "foreach: { data: someExpression, afterAdd: myfn }" is equivalent to "template: { foreach: someExpression, afterAdd: myfn }"
2428
+ ko.bindingHandlers['foreach'] = {
2429
+ makeTemplateValueAccessor: function(valueAccessor) {
2430
+ return function() {
2431
+ var bindingValue = ko.utils.unwrapObservable(valueAccessor());
2432
+
2433
+ // If bindingValue is the array, just pass it on its own
2434
+ if ((!bindingValue) || typeof bindingValue.length == "number")
2435
+ return { 'foreach': bindingValue, 'templateEngine': ko.nativeTemplateEngine.instance };
2436
+
2437
+ // If bindingValue.data is the array, preserve all relevant options
2438
+ return {
2439
+ 'foreach': bindingValue['data'],
2440
+ 'includeDestroyed': bindingValue['includeDestroyed'],
2441
+ 'afterAdd': bindingValue['afterAdd'],
2442
+ 'beforeRemove': bindingValue['beforeRemove'],
2443
+ 'afterRender': bindingValue['afterRender'],
2444
+ 'templateEngine': ko.nativeTemplateEngine.instance
2445
+ };
2446
+ };
2447
+ },
2448
+ 'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2449
+ return ko.bindingHandlers['template']['init'](element, ko.bindingHandlers['foreach'].makeTemplateValueAccessor(valueAccessor));
2450
+ },
2451
+ 'update': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2452
+ return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['foreach'].makeTemplateValueAccessor(valueAccessor), allBindingsAccessor, viewModel, bindingContext);
2453
+ }
2454
+ };
2455
+ ko.jsonExpressionRewriting.bindingRewriteValidators['foreach'] = false; // Can't rewrite control flow bindings
2456
+ ko.virtualElements.allowedBindings['foreach'] = true;
2457
+ ko.exportSymbol('ko.allowedVirtualElementBindings', ko.virtualElements.allowedBindings);// If you want to make a custom template engine,
2458
+ //
2459
+ // [1] Inherit from this class (like ko.nativeTemplateEngine does)
2460
+ // [2] Override 'renderTemplateSource', supplying a function with this signature:
2461
+ //
2462
+ // function (templateSource, bindingContext, options) {
2463
+ // // - templateSource.text() is the text of the template you should render
2464
+ // // - bindingContext.$data is the data you should pass into the template
2465
+ // // - you might also want to make bindingContext.$parent, bindingContext.$parents,
2466
+ // // and bindingContext.$root available in the template too
2467
+ // // - options gives you access to any other properties set on "data-bind: { template: options }"
2468
+ // //
2469
+ // // Return value: an array of DOM nodes
2470
+ // }
2471
+ //
2472
+ // [3] Override 'createJavaScriptEvaluatorBlock', supplying a function with this signature:
2473
+ //
2474
+ // function (script) {
2475
+ // // Return value: Whatever syntax means "Evaluate the JavaScript statement 'script' and output the result"
2476
+ // // For example, the jquery.tmpl template engine converts 'someScript' to '${ someScript }'
2477
+ // }
2478
+ //
2479
+ // This is only necessary if you want to allow data-bind attributes to reference arbitrary template variables.
2480
+ // If you don't want to allow that, you can set the property 'allowTemplateRewriting' to false (like ko.nativeTemplateEngine does)
2481
+ // and then you don't need to override 'createJavaScriptEvaluatorBlock'.
2482
+
2483
+ ko.templateEngine = function () { };
2484
+
2485
+ ko.templateEngine.prototype['renderTemplateSource'] = function (templateSource, bindingContext, options) {
2486
+ throw "Override renderTemplateSource";
2487
+ };
2488
+
2489
+ ko.templateEngine.prototype['createJavaScriptEvaluatorBlock'] = function (script) {
2490
+ throw "Override createJavaScriptEvaluatorBlock";
2491
+ };
2492
+
2493
+ ko.templateEngine.prototype['makeTemplateSource'] = function(template) {
2494
+ // Named template
2495
+ if (typeof template == "string") {
2496
+ var elem = document.getElementById(template);
2497
+ if (!elem)
2498
+ throw new Error("Cannot find template with ID " + template);
2499
+ return new ko.templateSources.domElement(elem);
2500
+ } else if ((template.nodeType == 1) || (template.nodeType == 8)) {
2501
+ // Anonymous template
2502
+ return new ko.templateSources.anonymousTemplate(template);
2503
+ } else
2504
+ throw new Error("Unknown template type: " + template);
2505
+ };
2506
+
2507
+ ko.templateEngine.prototype['renderTemplate'] = function (template, bindingContext, options) {
2508
+ var templateSource = this['makeTemplateSource'](template);
2509
+ return this['renderTemplateSource'](templateSource, bindingContext, options);
2510
+ };
2511
+
2512
+ ko.templateEngine.prototype['isTemplateRewritten'] = function (template) {
2513
+ // Skip rewriting if requested
2514
+ if (this['allowTemplateRewriting'] === false)
2515
+ return true;
2516
+
2517
+ // Perf optimisation - see below
2518
+ if (this.knownRewrittenTemplates && this.knownRewrittenTemplates[template])
2519
+ return true;
2520
+
2521
+ return this['makeTemplateSource'](template)['data']("isRewritten");
2522
+ };
2523
+
2524
+ ko.templateEngine.prototype['rewriteTemplate'] = function (template, rewriterCallback) {
2525
+ var templateSource = this['makeTemplateSource'](template);
2526
+ var rewritten = rewriterCallback(templateSource['text']());
2527
+ templateSource['text'](rewritten);
2528
+ templateSource['data']("isRewritten", true);
2529
+
2530
+ // Perf optimisation - for named templates, track which ones have been rewritten so we can
2531
+ // answer 'isTemplateRewritten' *without* having to use getElementById (which is slow on IE < 8)
2532
+ if (typeof template == "string") {
2533
+ this.knownRewrittenTemplates = this.knownRewrittenTemplates || {};
2534
+ this.knownRewrittenTemplates[template] = true;
1064
2535
  }
1065
- };/// <reference path="templateEngine.js" />
2536
+ };
1066
2537
 
2538
+ ko.exportSymbol('ko.templateEngine', ko.templateEngine);
1067
2539
  ko.templateRewriting = (function () {
1068
- var memoizeBindingAttributeSyntaxRegex = /(<[a-z]+(\s+(?!data-bind=)[a-z0-9]+(=(\"[^\"]*\"|\'[^\']*\'))?)*\s+)data-bind=(["'])(.*?)\5/g;
2540
+ var memoizeDataBindingAttributeSyntaxRegex = /(<[a-z]+\d*(\s+(?!data-bind=)[a-z0-9\-]+(=(\"[^\"]*\"|\'[^\']*\'))?)*\s+)data-bind=(["'])([\s\S]*?)\5/gi;
2541
+ var memoizeVirtualContainerBindingSyntaxRegex = /<!--\s*ko\b\s*([\s\S]*?)\s*-->/g;
2542
+
2543
+ function validateDataBindValuesForRewriting(keyValueArray) {
2544
+ var allValidators = ko.jsonExpressionRewriting.bindingRewriteValidators;
2545
+ for (var i = 0; i < keyValueArray.length; i++) {
2546
+ var key = keyValueArray[i]['key'];
2547
+ if (allValidators.hasOwnProperty(key)) {
2548
+ var validator = allValidators[key];
2549
+
2550
+ if (typeof validator === "function") {
2551
+ var possibleErrorMessage = validator(keyValueArray[i]['value']);
2552
+ if (possibleErrorMessage)
2553
+ throw new Error(possibleErrorMessage);
2554
+ } else if (!validator) {
2555
+ throw new Error("This template engine does not support the '" + key + "' binding within its templates");
2556
+ }
2557
+ }
2558
+ }
2559
+ }
2560
+
2561
+ function constructMemoizedTagReplacement(dataBindAttributeValue, tagToRetain, templateEngine) {
2562
+ var dataBindKeyValueArray = ko.jsonExpressionRewriting.parseObjectLiteral(dataBindAttributeValue);
2563
+ validateDataBindValuesForRewriting(dataBindKeyValueArray);
2564
+ var rewrittenDataBindAttributeValue = ko.jsonExpressionRewriting.insertPropertyAccessorsIntoJson(dataBindKeyValueArray);
2565
+
2566
+ // For no obvious reason, Opera fails to evaluate rewrittenDataBindAttributeValue unless it's wrapped in an additional
2567
+ // anonymous function, even though Opera's built-in debugger can evaluate it anyway. No other browser requires this
2568
+ // extra indirection.
2569
+ var applyBindingsToNextSiblingScript = "ko.templateRewriting.applyMemoizedBindingsToNextSibling(function() { \
2570
+ return (function() { return { " + rewrittenDataBindAttributeValue + " } })() \
2571
+ })";
2572
+ return templateEngine['createJavaScriptEvaluatorBlock'](applyBindingsToNextSiblingScript) + tagToRetain;
2573
+ }
1069
2574
 
1070
2575
  return {
1071
2576
  ensureTemplateIsRewritten: function (template, templateEngine) {
1072
- if (!templateEngine.isTemplateRewritten(template))
1073
- templateEngine.rewriteTemplate(template, function (htmlString) {
2577
+ if (!templateEngine['isTemplateRewritten'](template))
2578
+ templateEngine['rewriteTemplate'](template, function (htmlString) {
1074
2579
  return ko.templateRewriting.memoizeBindingAttributeSyntax(htmlString, templateEngine);
1075
2580
  });
1076
2581
  },
1077
2582
 
1078
2583
  memoizeBindingAttributeSyntax: function (htmlString, templateEngine) {
1079
- return htmlString.replace(memoizeBindingAttributeSyntaxRegex, function () {
1080
- var tagToRetain = arguments[1];
1081
- var dataBindAttributeValue = arguments[6];
1082
-
1083
- dataBindAttributeValue = ko.jsonExpressionRewriting.insertPropertyAccessorsIntoJson(dataBindAttributeValue);
1084
-
1085
- // For no obvious reason, Opera fails to evaluate dataBindAttributeValue unless it's wrapped in an additional anonymous function,
1086
- // even though Opera's built-in debugger can evaluate it anyway. No other browser requires this extra indirection.
1087
- var applyBindingsToNextSiblingScript = "ko.templateRewriting.applyMemoizedBindingsToNextSibling(function() { \
1088
- return (function() { return { " + dataBindAttributeValue + " } })() \
1089
- })";
1090
- return templateEngine.createJavaScriptEvaluatorBlock(applyBindingsToNextSiblingScript) + tagToRetain;
2584
+ return htmlString.replace(memoizeDataBindingAttributeSyntaxRegex, function () {
2585
+ return constructMemoizedTagReplacement(/* dataBindAttributeValue: */ arguments[6], /* tagToRetain: */ arguments[1], templateEngine);
2586
+ }).replace(memoizeVirtualContainerBindingSyntaxRegex, function() {
2587
+ return constructMemoizedTagReplacement(/* dataBindAttributeValue: */ arguments[1], /* tagToRetain: */ "<!-- ko -->", templateEngine);
1091
2588
  });
1092
2589
  },
1093
2590
 
1094
2591
  applyMemoizedBindingsToNextSibling: function (bindings) {
1095
- return ko.memoization.memoize(function (domNode) {
2592
+ return ko.memoization.memoize(function (domNode, bindingContext) {
1096
2593
  if (domNode.nextSibling)
1097
- ko.applyBindingsToNode(domNode.nextSibling, bindings, null);
2594
+ ko.applyBindingsToNode(domNode.nextSibling, bindings, bindingContext);
1098
2595
  });
1099
2596
  }
1100
2597
  }
1101
- })();/// <reference path="templating.js" />
1102
- /// <reference path="../subscribables/dependentObservable.js" />
2598
+ })();
1103
2599
 
2600
+ ko.exportSymbol('ko.templateRewriting', ko.templateRewriting);
2601
+ ko.exportSymbol('ko.templateRewriting.applyMemoizedBindingsToNextSibling', ko.templateRewriting.applyMemoizedBindingsToNextSibling); // Exported only because it has to be referenced by string lookup from within rewritten template
2602
+ (function() {
2603
+ // A template source represents a read/write way of accessing a template. This is to eliminate the need for template loading/saving
2604
+ // logic to be duplicated in every template engine (and means they can all work with anonymous templates, etc.)
2605
+ //
2606
+ // Two are provided by default:
2607
+ // 1. ko.templateSources.domElement - reads/writes the text content of an arbitrary DOM element
2608
+ // 2. ko.templateSources.anonymousElement - uses ko.utils.domData to read/write text *associated* with the DOM element, but
2609
+ // without reading/writing the actual element text content, since it will be overwritten
2610
+ // with the rendered template output.
2611
+ // You can implement your own template source if you want to fetch/store templates somewhere other than in DOM elements.
2612
+ // Template sources need to have the following functions:
2613
+ // text() - returns the template text from your storage location
2614
+ // text(value) - writes the supplied template text to your storage location
2615
+ // data(key) - reads values stored using data(key, value) - see below
2616
+ // data(key, value) - associates "value" with this template and the key "key". Is used to store information like "isRewritten".
2617
+ //
2618
+ // Once you've implemented a templateSource, make your template engine use it by subclassing whatever template engine you were
2619
+ // using and overriding "makeTemplateSource" to return an instance of your custom template source.
2620
+
2621
+ ko.templateSources = {};
2622
+
2623
+ // ---- ko.templateSources.domElement -----
2624
+
2625
+ ko.templateSources.domElement = function(element) {
2626
+ this.domElement = element;
2627
+ }
2628
+
2629
+ ko.templateSources.domElement.prototype['text'] = function(/* valueToWrite */) {
2630
+ if (arguments.length == 0) {
2631
+ return this.domElement.tagName.toLowerCase() == "script" ? this.domElement.text : this.domElement.innerHTML;
2632
+ } else {
2633
+ var valueToWrite = arguments[0];
2634
+ if (this.domElement.tagName.toLowerCase() == "script")
2635
+ this.domElement.text = valueToWrite;
2636
+ else
2637
+ ko.utils.setHtml(this.domElement, valueToWrite);
2638
+ }
2639
+ };
2640
+
2641
+ ko.templateSources.domElement.prototype['data'] = function(key /*, valueToWrite */) {
2642
+ if (arguments.length === 1) {
2643
+ return ko.utils.domData.get(this.domElement, "templateSourceData_" + key);
2644
+ } else {
2645
+ ko.utils.domData.set(this.domElement, "templateSourceData_" + key, arguments[1]);
2646
+ }
2647
+ };
2648
+
2649
+ // ---- ko.templateSources.anonymousTemplate -----
2650
+
2651
+ var anonymousTemplatesDomDataKey = "__ko_anon_template__";
2652
+ ko.templateSources.anonymousTemplate = function(element) {
2653
+ this.domElement = element;
2654
+ }
2655
+ ko.templateSources.anonymousTemplate.prototype = new ko.templateSources.domElement();
2656
+ ko.templateSources.anonymousTemplate.prototype['text'] = function(/* valueToWrite */) {
2657
+ if (arguments.length == 0) {
2658
+ return ko.utils.domData.get(this.domElement, anonymousTemplatesDomDataKey);
2659
+ } else {
2660
+ var valueToWrite = arguments[0];
2661
+ ko.utils.domData.set(this.domElement, anonymousTemplatesDomDataKey, valueToWrite);
2662
+ }
2663
+ };
2664
+
2665
+ ko.exportSymbol('ko.templateSources', ko.templateSources);
2666
+ ko.exportSymbol('ko.templateSources.domElement', ko.templateSources.domElement);
2667
+ ko.exportSymbol('ko.templateSources.anonymousTemplate', ko.templateSources.anonymousTemplate);
2668
+ })();
1104
2669
  (function () {
1105
2670
  var _templateEngine;
1106
2671
  ko.setTemplateEngine = function (templateEngine) {
@@ -1109,107 +2674,224 @@ ko.templateRewriting = (function () {
1109
2674
  _templateEngine = templateEngine;
1110
2675
  }
1111
2676
 
2677
+ function invokeForEachNodeOrCommentInParent(nodeArray, parent, action) {
2678
+ for (var i = 0; node = nodeArray[i]; i++) {
2679
+ if (node.parentNode !== parent) // Skip anything that has been removed during binding
2680
+ continue;
2681
+ if ((node.nodeType === 1) || (node.nodeType === 8))
2682
+ action(node);
2683
+ }
2684
+ }
2685
+
2686
+ ko.activateBindingsOnTemplateRenderedNodes = function(nodeArray, bindingContext) {
2687
+ // To be used on any nodes that have been rendered by a template and have been inserted into some parent element.
2688
+ // Safely iterates through nodeArray (being tolerant of any changes made to it during binding, e.g.,
2689
+ // if a binding inserts siblings), and for each:
2690
+ // (1) Does a regular "applyBindings" to associate bindingContext with this node and to activate any non-memoized bindings
2691
+ // (2) Unmemoizes any memos in the DOM subtree (e.g., to activate bindings that had been memoized during template rewriting)
2692
+
2693
+ var nodeArrayClone = ko.utils.arrayPushAll([], nodeArray); // So we can tolerate insertions/deletions during binding
2694
+ var commonParentElement = (nodeArray.length > 0) ? nodeArray[0].parentNode : null; // All items must be in the same parent, so this is OK
2695
+
2696
+ // Need to applyBindings *before* unmemoziation, because unmemoization might introduce extra nodes (that we don't want to re-bind)
2697
+ // whereas a regular applyBindings won't introduce new memoized nodes
2698
+
2699
+ invokeForEachNodeOrCommentInParent(nodeArrayClone, commonParentElement, function(node) {
2700
+ ko.applyBindings(bindingContext, node);
2701
+ });
2702
+ invokeForEachNodeOrCommentInParent(nodeArrayClone, commonParentElement, function(node) {
2703
+ ko.memoization.unmemoizeDomNodeAndDescendants(node, [bindingContext]);
2704
+ });
2705
+ }
2706
+
1112
2707
  function getFirstNodeFromPossibleArray(nodeOrNodeArray) {
1113
2708
  return nodeOrNodeArray.nodeType ? nodeOrNodeArray
1114
2709
  : nodeOrNodeArray.length > 0 ? nodeOrNodeArray[0]
1115
2710
  : null;
1116
2711
  }
1117
2712
 
1118
- function executeTemplate(targetNodeOrNodeArray, renderMode, template, data, options) {
1119
- var dataForTemplate = ko.utils.unwrapObservable(data);
1120
-
2713
+ function executeTemplate(targetNodeOrNodeArray, renderMode, template, bindingContext, options) {
1121
2714
  options = options || {};
1122
- var templateEngineToUse = (options.templateEngine || _templateEngine);
2715
+ var templateEngineToUse = (options['templateEngine'] || _templateEngine);
1123
2716
  ko.templateRewriting.ensureTemplateIsRewritten(template, templateEngineToUse);
1124
- var renderedNodesArray = templateEngineToUse.renderTemplate(template, dataForTemplate, options);
2717
+ var renderedNodesArray = templateEngineToUse['renderTemplate'](template, bindingContext, options);
1125
2718
 
1126
2719
  // Loosely check result is an array of DOM nodes
1127
2720
  if ((typeof renderedNodesArray.length != "number") || (renderedNodesArray.length > 0 && typeof renderedNodesArray[0].nodeType != "number"))
1128
2721
  throw "Template engine must return an array of DOM nodes";
1129
2722
 
1130
- if (renderedNodesArray)
1131
- ko.utils.arrayForEach(renderedNodesArray, function (renderedNode) {
1132
- ko.memoization.unmemoizeDomNodeAndDescendants(renderedNode);
1133
- });
1134
-
2723
+ var haveAddedNodesToParent = false;
1135
2724
  switch (renderMode) {
1136
- case "replaceChildren": ko.utils.setDomNodeChildren(targetNodeOrNodeArray, renderedNodesArray); break;
1137
- case "replaceNode": ko.utils.replaceDomNodes(targetNodeOrNodeArray, renderedNodesArray); break;
2725
+ case "replaceChildren":
2726
+ ko.virtualElements.setDomNodeChildren(targetNodeOrNodeArray, renderedNodesArray);
2727
+ haveAddedNodesToParent = true;
2728
+ break;
2729
+ case "replaceNode":
2730
+ ko.utils.replaceDomNodes(targetNodeOrNodeArray, renderedNodesArray);
2731
+ haveAddedNodesToParent = true;
2732
+ break;
1138
2733
  case "ignoreTargetNode": break;
1139
- default: throw new Error("Unknown renderMode: " + renderMode);
2734
+ default:
2735
+ throw new Error("Unknown renderMode: " + renderMode);
2736
+ }
2737
+
2738
+ if (haveAddedNodesToParent) {
2739
+ ko.activateBindingsOnTemplateRenderedNodes(renderedNodesArray, bindingContext);
2740
+ if (options['afterRender'])
2741
+ options['afterRender'](renderedNodesArray, bindingContext['$data']);
1140
2742
  }
1141
2743
 
1142
2744
  return renderedNodesArray;
1143
2745
  }
1144
2746
 
1145
- ko.renderTemplate = function (template, data, options, targetNodeOrNodeArray, renderMode) {
2747
+ ko.renderTemplate = function (template, dataOrBindingContext, options, targetNodeOrNodeArray, renderMode) {
1146
2748
  options = options || {};
1147
- if ((options.templateEngine || _templateEngine) == undefined)
2749
+ if ((options['templateEngine'] || _templateEngine) == undefined)
1148
2750
  throw "Set a template engine before calling renderTemplate";
1149
2751
  renderMode = renderMode || "replaceChildren";
1150
2752
 
1151
2753
  if (targetNodeOrNodeArray) {
1152
2754
  var firstTargetNode = getFirstNodeFromPossibleArray(targetNodeOrNodeArray);
1153
- var whenToDispose = function () { return (!firstTargetNode) || !ko.utils.domNodeIsAttachedToDocument(firstTargetNode); };
1154
-
2755
+
2756
+ var whenToDispose = function () { return (!firstTargetNode) || !ko.utils.domNodeIsAttachedToDocument(firstTargetNode); }; // Passive disposal (on next evaluation)
2757
+ var activelyDisposeWhenNodeIsRemoved = (firstTargetNode && renderMode == "replaceNode") ? firstTargetNode.parentNode : firstTargetNode;
2758
+
1155
2759
  return new ko.dependentObservable( // So the DOM is automatically updated when any dependency changes
1156
2760
  function () {
1157
- var renderedNodesArray = executeTemplate(targetNodeOrNodeArray, renderMode, template, data, options);
2761
+ // Ensure we've got a proper binding context to work with
2762
+ var bindingContext = (dataOrBindingContext && (dataOrBindingContext instanceof ko.bindingContext))
2763
+ ? dataOrBindingContext
2764
+ : new ko.bindingContext(ko.utils.unwrapObservable(dataOrBindingContext));
2765
+
2766
+ // Support selecting template as a function of the data being rendered
2767
+ var templateName = typeof(template) == 'function' ? template(bindingContext['$data']) : template;
2768
+
2769
+ var renderedNodesArray = executeTemplate(targetNodeOrNodeArray, renderMode, templateName, bindingContext, options);
1158
2770
  if (renderMode == "replaceNode") {
1159
2771
  targetNodeOrNodeArray = renderedNodesArray;
1160
2772
  firstTargetNode = getFirstNodeFromPossibleArray(targetNodeOrNodeArray);
1161
2773
  }
1162
2774
  },
1163
2775
  null,
1164
- { disposeWhen: whenToDispose }
2776
+ { 'disposeWhen': whenToDispose, 'disposeWhenNodeIsRemoved': activelyDisposeWhenNodeIsRemoved }
1165
2777
  );
1166
2778
  } else {
1167
2779
  // We don't yet have a DOM node to evaluate, so use a memo and render the template later when there is a DOM node
1168
2780
  return ko.memoization.memoize(function (domNode) {
1169
- ko.renderTemplate(template, data, options, domNode, "replaceNode");
2781
+ ko.renderTemplate(template, dataOrBindingContext, options, domNode, "replaceNode");
1170
2782
  });
1171
2783
  }
1172
2784
  };
1173
2785
 
1174
- ko.renderTemplateForEach = function (template, arrayOrObservableArray, options, targetNode) {
1175
- var whenToDispose = function () { return !ko.utils.domNodeIsAttachedToDocument(targetNode); };
2786
+ ko.renderTemplateForEach = function (template, arrayOrObservableArray, options, targetNode, parentBindingContext) {
2787
+ var createInnerBindingContext = function(arrayValue) {
2788
+ return parentBindingContext['createChildContext'](ko.utils.unwrapObservable(arrayValue));
2789
+ };
1176
2790
 
1177
- new ko.dependentObservable(function () {
1178
- var unwrappedArray = ko.utils.unwrapObservable(arrayOrObservableArray);
2791
+ // This will be called whenever setDomNodeChildrenFromArrayMapping has added nodes to targetNode
2792
+ var activateBindingsCallback = function(arrayValue, addedNodesArray) {
2793
+ var bindingContext = createInnerBindingContext(arrayValue);
2794
+ ko.activateBindingsOnTemplateRenderedNodes(addedNodesArray, bindingContext);
2795
+ if (options['afterRender'])
2796
+ options['afterRender'](addedNodesArray, bindingContext['$data']);
2797
+ };
2798
+
2799
+ return new ko.dependentObservable(function () {
2800
+ var unwrappedArray = ko.utils.unwrapObservable(arrayOrObservableArray) || [];
1179
2801
  if (typeof unwrappedArray.length == "undefined") // Coerce single value into array
1180
2802
  unwrappedArray = [unwrappedArray];
1181
2803
 
1182
2804
  // Filter out any entries marked as destroyed
1183
- var filteredArray = ko.utils.arrayFilter(unwrappedArray, function(item) {
1184
- return options.includeDestroyed || !item._destroy;
2805
+ var filteredArray = ko.utils.arrayFilter(unwrappedArray, function(item) {
2806
+ return options['includeDestroyed'] || item === undefined || item === null || !ko.utils.unwrapObservable(item['_destroy']);
1185
2807
  });
1186
2808
 
1187
2809
  ko.utils.setDomNodeChildrenFromArrayMapping(targetNode, filteredArray, function (arrayValue) {
1188
- return executeTemplate(null, "ignoreTargetNode", template, arrayValue, options);
1189
- }, options);
1190
- }, null, { disposeWhen: whenToDispose });
2810
+ // Support selecting template as a function of the data being rendered
2811
+ var templateName = typeof(template) == 'function' ? template(arrayValue) : template;
2812
+ return executeTemplate(null, "ignoreTargetNode", templateName, createInnerBindingContext(arrayValue), options);
2813
+ }, options, activateBindingsCallback);
2814
+
2815
+ }, null, { 'disposeWhenNodeIsRemoved': targetNode });
1191
2816
  };
1192
2817
 
1193
- ko.bindingHandlers.template = {
1194
- update: function (element, bindingValue, allBindings, viewModel) {
1195
- var templateName = typeof bindingValue == "string" ? bindingValue : bindingValue.name;
1196
-
1197
- if (typeof bindingValue.foreach != "undefined") {
1198
- // Render once for each data point
1199
- ko.renderTemplateForEach(templateName, bindingValue.foreach || [], { afterAdd: bindingValue.afterAdd, beforeRemove: bindingValue.beforeRemove, includeDestroyed: bindingValue.includeDestroyed }, element);
2818
+ var templateSubscriptionDomDataKey = '__ko__templateSubscriptionDomDataKey__';
2819
+ function disposeOldSubscriptionAndStoreNewOne(element, newSubscription) {
2820
+ var oldSubscription = ko.utils.domData.get(element, templateSubscriptionDomDataKey);
2821
+ if (oldSubscription && (typeof(oldSubscription.dispose) == 'function'))
2822
+ oldSubscription.dispose();
2823
+ ko.utils.domData.set(element, templateSubscriptionDomDataKey, newSubscription);
2824
+ }
2825
+
2826
+ ko.bindingHandlers['template'] = {
2827
+ 'init': function(element, valueAccessor) {
2828
+ // Support anonymous templates
2829
+ var bindingValue = ko.utils.unwrapObservable(valueAccessor());
2830
+ if ((typeof bindingValue != "string") && (!bindingValue.name) && (element.nodeType == 1)) {
2831
+ // It's an anonymous template - store the element contents, then clear the element
2832
+ new ko.templateSources.anonymousTemplate(element).text(element.innerHTML);
2833
+ ko.utils.emptyDomNode(element);
1200
2834
  }
1201
- else {
1202
- // Render once for this single data point (or use the viewModel if no data was provided)
1203
- var templateData = bindingValue.data;
1204
- ko.renderTemplate(templateName, typeof templateData == "undefined" ? viewModel : templateData, null, element);
2835
+ return { 'controlsDescendantBindings': true };
2836
+ },
2837
+ 'update': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2838
+ var bindingValue = ko.utils.unwrapObservable(valueAccessor());
2839
+ var templateName;
2840
+ var shouldDisplay = true;
2841
+
2842
+ if (typeof bindingValue == "string") {
2843
+ templateName = bindingValue;
2844
+ } else {
2845
+ templateName = bindingValue.name;
2846
+
2847
+ // Support "if"/"ifnot" conditions
2848
+ if ('if' in bindingValue)
2849
+ shouldDisplay = shouldDisplay && ko.utils.unwrapObservable(bindingValue['if']);
2850
+ if ('ifnot' in bindingValue)
2851
+ shouldDisplay = shouldDisplay && !ko.utils.unwrapObservable(bindingValue['ifnot']);
2852
+ }
2853
+
2854
+ var templateSubscription = null;
2855
+
2856
+ if ((typeof bindingValue === 'object') && ('foreach' in bindingValue)) { // Note: can't use 'in' operator on strings
2857
+ // Render once for each data point (treating data set as empty if shouldDisplay==false)
2858
+ var dataArray = (shouldDisplay && bindingValue['foreach']) || [];
2859
+ templateSubscription = ko.renderTemplateForEach(templateName || element, dataArray, /* options: */ bindingValue, element, bindingContext);
2860
+ } else {
2861
+ if (shouldDisplay) {
2862
+ // Render once for this single data point (or use the viewModel if no data was provided)
2863
+ var innerBindingContext = (typeof bindingValue == 'object') && ('data' in bindingValue)
2864
+ ? bindingContext['createChildContext'](ko.utils.unwrapObservable(bindingValue['data'])) // Given an explitit 'data' value, we create a child binding context for it
2865
+ : bindingContext; // Given no explicit 'data' value, we retain the same binding context
2866
+ templateSubscription = ko.renderTemplate(templateName || element, innerBindingContext, /* options: */ bindingValue, element);
2867
+ } else
2868
+ ko.virtualElements.emptyNode(element);
1205
2869
  }
2870
+
2871
+ // It only makes sense to have a single template subscription per element (otherwise which one should have its output displayed?)
2872
+ disposeOldSubscriptionAndStoreNewOne(element, templateSubscription);
1206
2873
  }
1207
2874
  };
1208
- })();/// <reference path="../../utils.js" />
1209
2875
 
1210
- // Simple calculation based on Levenshtein distance.
1211
- (function () {
2876
+ // Anonymous templates can't be rewritten. Give a nice error message if you try to do it.
2877
+ ko.jsonExpressionRewriting.bindingRewriteValidators['template'] = function(bindingValue) {
2878
+ var parsedBindingValue = ko.jsonExpressionRewriting.parseObjectLiteral(bindingValue);
2879
+
2880
+ if ((parsedBindingValue.length == 1) && parsedBindingValue[0]['unknown'])
2881
+ return null; // It looks like a string literal, not an object literal, so treat it as a named template (which is allowed for rewriting)
2882
+
2883
+ if (ko.jsonExpressionRewriting.keyValueArrayContainsKey(parsedBindingValue, "name"))
2884
+ return null; // Named templates can be rewritten, so return "no error"
2885
+ return "This template engine does not support anonymous templates nested within its templates";
2886
+ };
1212
2887
 
2888
+ ko.virtualElements.allowedBindings['template'] = true;
2889
+ })();
2890
+
2891
+ ko.exportSymbol('ko.setTemplateEngine', ko.setTemplateEngine);
2892
+ ko.exportSymbol('ko.renderTemplate', ko.renderTemplate);
2893
+ (function () {
2894
+ // Simple calculation based on Levenshtein distance.
1213
2895
  function calculateEditDistanceMatrix(oldArray, newArray, maxAllowedDistance) {
1214
2896
  var distances = [];
1215
2897
  for (var i = 0; i <= newArray.length; i++)
@@ -1286,8 +2968,10 @@ ko.templateRewriting = (function () {
1286
2968
  var editDistanceMatrix = calculateEditDistanceMatrix(oldArray, newArray, maxEditsToConsider);
1287
2969
  return findEditScriptFromEditDistanceMatrix(editDistanceMatrix, oldArray, newArray);
1288
2970
  }
1289
- };
1290
- })();/// <reference path="compareArrays.js" />
2971
+ };
2972
+ })();
2973
+
2974
+ ko.exportSymbol('ko.utils.compareArrays', ko.utils.compareArrays);
1291
2975
 
1292
2976
  (function () {
1293
2977
  // Objective:
@@ -1297,30 +2981,61 @@ ko.templateRewriting = (function () {
1297
2981
  // so that its children is again the concatenation of the mappings of the array elements, but don't re-map any array elements that we
1298
2982
  // previously mapped - retain those nodes, and just insert/delete other ones
1299
2983
 
1300
- function mapNodeAndRefreshWhenChanged(mapping, valueToMap) {
2984
+ // "callbackAfterAddingNodes" will be invoked after any "mapping"-generated nodes are inserted into the container node
2985
+ // You can use this, for example, to activate bindings on those nodes.
2986
+
2987
+ function fixUpVirtualElements(contiguousNodeArray) {
2988
+ // Ensures that contiguousNodeArray really *is* an array of contiguous siblings, even if some of the interior
2989
+ // ones have changed since your array was first built (e.g., because your array contains virtual elements, and
2990
+ // their virtual children changed when binding was applied to them).
2991
+ // This is needed so that we can reliably remove or update the nodes corresponding to a given array item
2992
+
2993
+ if (contiguousNodeArray.length > 2) {
2994
+ // Build up the actual new contiguous node set
2995
+ var current = contiguousNodeArray[0], last = contiguousNodeArray[contiguousNodeArray.length - 1], newContiguousSet = [current];
2996
+ while (current !== last) {
2997
+ current = current.nextSibling;
2998
+ if (!current) // Won't happen, except if the developer has manually removed some DOM elements (then we're in an undefined scenario)
2999
+ return;
3000
+ newContiguousSet.push(current);
3001
+ }
3002
+
3003
+ // ... then mutate the input array to match this.
3004
+ // (The following line replaces the contents of contiguousNodeArray with newContiguousSet)
3005
+ Array.prototype.splice.apply(contiguousNodeArray, [0, contiguousNodeArray.length].concat(newContiguousSet));
3006
+ }
3007
+ }
3008
+
3009
+ function mapNodeAndRefreshWhenChanged(containerNode, mapping, valueToMap, callbackAfterAddingNodes) {
1301
3010
  // Map this array value inside a dependentObservable so we re-map when any dependency changes
1302
3011
  var mappedNodes = [];
1303
- ko.dependentObservable(function() {
3012
+ var dependentObservable = ko.dependentObservable(function() {
1304
3013
  var newMappedNodes = mapping(valueToMap) || [];
1305
3014
 
1306
3015
  // On subsequent evaluations, just replace the previously-inserted DOM nodes
1307
- if (mappedNodes.length > 0)
3016
+ if (mappedNodes.length > 0) {
3017
+ fixUpVirtualElements(mappedNodes);
1308
3018
  ko.utils.replaceDomNodes(mappedNodes, newMappedNodes);
3019
+ if (callbackAfterAddingNodes)
3020
+ callbackAfterAddingNodes(valueToMap, newMappedNodes);
3021
+ }
1309
3022
 
1310
3023
  // Replace the contents of the mappedNodes array, thereby updating the record
1311
3024
  // of which nodes would be deleted if valueToMap was itself later removed
1312
3025
  mappedNodes.splice(0, mappedNodes.length);
1313
3026
  ko.utils.arrayPushAll(mappedNodes, newMappedNodes);
1314
- }, null, { disposeWhen: function() { return (mappedNodes.length == 0) || !ko.utils.domNodeIsAttachedToDocument(mappedNodes[0]) } });
1315
- return mappedNodes;
1316
- }
3027
+ }, null, { 'disposeWhenNodeIsRemoved': containerNode, 'disposeWhen': function() { return (mappedNodes.length == 0) || !ko.utils.domNodeIsAttachedToDocument(mappedNodes[0]) } });
3028
+ return { mappedNodes : mappedNodes, dependentObservable : dependentObservable };
3029
+ }
3030
+
3031
+ var lastMappingResultDomDataKey = "setDomNodeChildrenFromArrayMapping_lastMappingResult";
1317
3032
 
1318
- ko.utils.setDomNodeChildrenFromArrayMapping = function (domNode, array, mapping, options) {
3033
+ ko.utils.setDomNodeChildrenFromArrayMapping = function (domNode, array, mapping, options, callbackAfterAddingNodes) {
1319
3034
  // Compare the provided array against the previous one
1320
3035
  array = array || [];
1321
3036
  options = options || {};
1322
- var isFirstExecution = ko.utils.domData.get(domNode, "setDomNodeChildrenFromArrayMapping_lastMappingResult") === undefined;
1323
- var lastMappingResult = ko.utils.domData.get(domNode, "setDomNodeChildrenFromArrayMapping_lastMappingResult") || [];
3037
+ var isFirstExecution = ko.utils.domData.get(domNode, lastMappingResultDomDataKey) === undefined;
3038
+ var lastMappingResult = ko.utils.domData.get(domNode, lastMappingResultDomDataKey) || [];
1324
3039
  var lastArray = ko.utils.arrayMap(lastMappingResult, function (x) { return x.arrayEntry; });
1325
3040
  var editScript = ko.utils.compareArrays(lastArray, array);
1326
3041
 
@@ -1342,116 +3057,167 @@ ko.templateRewriting = (function () {
1342
3057
  break;
1343
3058
 
1344
3059
  case "deleted":
3060
+ // Stop tracking changes to the mapping for these nodes
3061
+ lastMappingResult[lastMappingResultIndex].dependentObservable.dispose();
3062
+
1345
3063
  // Queue these nodes for later removal
3064
+ fixUpVirtualElements(lastMappingResult[lastMappingResultIndex].domNodes);
1346
3065
  ko.utils.arrayForEach(lastMappingResult[lastMappingResultIndex].domNodes, function (node) {
1347
- nodesToDelete.push(node);
3066
+ nodesToDelete.push({
3067
+ element: node,
3068
+ index: i,
3069
+ value: editScript[i].value
3070
+ });
1348
3071
  insertAfterNode = node;
1349
3072
  });
1350
3073
  lastMappingResultIndex++;
1351
3074
  break;
1352
3075
 
1353
3076
  case "added":
1354
- var mappedNodes = mapNodeAndRefreshWhenChanged(mapping, editScript[i].value);
1355
- // On the first evaluation, insert the nodes at the current insertion point
1356
- newMappingResult.push({ arrayEntry: editScript[i].value, domNodes: mappedNodes });
1357
- for (var nodeIndex = 0, nodeIndexMax = mappedNodes.length; nodeIndex < nodeIndexMax; nodeIndex++) {
1358
- var node = mappedNodes[nodeIndex];
1359
- nodesAdded.push(node);
1360
- if (insertAfterNode == null) {
1361
- // Insert at beginning
1362
- if (domNode.firstChild)
1363
- domNode.insertBefore(node, domNode.firstChild);
1364
- else
1365
- domNode.appendChild(node);
1366
- } else {
1367
- // Insert after insertion point
1368
- if (insertAfterNode.nextSibling)
1369
- domNode.insertBefore(node, insertAfterNode.nextSibling);
1370
- else
1371
- domNode.appendChild(node);
1372
- }
1373
- insertAfterNode = node;
1374
- }
3077
+ var valueToMap = editScript[i].value;
3078
+ var mapData = mapNodeAndRefreshWhenChanged(domNode, mapping, valueToMap, callbackAfterAddingNodes);
3079
+ var mappedNodes = mapData.mappedNodes;
3080
+
3081
+ // On the first evaluation, insert the nodes at the current insertion point
3082
+ newMappingResult.push({ arrayEntry: editScript[i].value, domNodes: mappedNodes, dependentObservable: mapData.dependentObservable });
3083
+ for (var nodeIndex = 0, nodeIndexMax = mappedNodes.length; nodeIndex < nodeIndexMax; nodeIndex++) {
3084
+ var node = mappedNodes[nodeIndex];
3085
+ nodesAdded.push({
3086
+ element: node,
3087
+ index: i,
3088
+ value: editScript[i].value
3089
+ });
3090
+ if (insertAfterNode == null) {
3091
+ // Insert "node" (the newly-created node) as domNode's first child
3092
+ ko.virtualElements.prepend(domNode, node);
3093
+ } else {
3094
+ // Insert "node" into "domNode" immediately after "insertAfterNode"
3095
+ ko.virtualElements.insertAfter(domNode, node, insertAfterNode);
3096
+ }
3097
+ insertAfterNode = node;
3098
+ }
3099
+ if (callbackAfterAddingNodes)
3100
+ callbackAfterAddingNodes(valueToMap, mappedNodes);
1375
3101
  break;
1376
3102
  }
1377
3103
  }
1378
3104
 
1379
- ko.utils.arrayForEach(nodesToDelete, function (node) { ko.utils.domData.cleanNodeAndDescendants(node); });
3105
+ ko.utils.arrayForEach(nodesToDelete, function (node) { ko.cleanNode(node.element) });
1380
3106
 
1381
3107
  var invokedBeforeRemoveCallback = false;
1382
3108
  if (!isFirstExecution) {
1383
- if (options.afterAdd)
1384
- options.afterAdd(nodesAdded);
1385
- if (options.beforeRemove) {
1386
- options.beforeRemove(nodesToDelete);
3109
+ if (options['afterAdd']) {
3110
+ for (var i = 0; i < nodesAdded.length; i++)
3111
+ options['afterAdd'](nodesAdded[i].element, nodesAdded[i].index, nodesAdded[i].value);
3112
+ }
3113
+ if (options['beforeRemove']) {
3114
+ for (var i = 0; i < nodesToDelete.length; i++)
3115
+ options['beforeRemove'](nodesToDelete[i].element, nodesToDelete[i].index, nodesToDelete[i].value);
1387
3116
  invokedBeforeRemoveCallback = true;
1388
3117
  }
1389
3118
  }
1390
3119
  if (!invokedBeforeRemoveCallback)
1391
3120
  ko.utils.arrayForEach(nodesToDelete, function (node) {
1392
- if (node.parentNode)
1393
- node.parentNode.removeChild(node);
3121
+ ko.removeNode(node.element);
1394
3122
  });
1395
3123
 
1396
3124
  // Store a copy of the array items we just considered so we can difference it next time
1397
- ko.utils.domData.set(domNode, "setDomNodeChildrenFromArrayMapping_lastMappingResult", newMappingResult);
1398
- }
1399
- })();/// <reference path="../templating.js" />
1400
-
1401
- ko.jqueryTmplTemplateEngine = function () {
1402
- function getTemplateNode(template) {
1403
- var templateNode = document.getElementById(template);
1404
- if (templateNode == null)
1405
- throw new Error("Cannot find template with ID=" + template);
1406
- return templateNode;
1407
- }
1408
-
1409
- var aposMarker = "__ko_apos__";
1410
- var aposRegex = new RegExp(aposMarker, "g");
1411
- this.renderTemplate = function (template, data, options) {
1412
- // jquery.tmpl doesn't like it if the template returns just text content or nothing - it only likes you to return DOM nodes.
1413
- // To make things more flexible, we can wrap the whole template in a <script> node so that jquery.tmpl just processes it as
1414
- // text and doesn't try to parse the output. Then, since jquery.tmpl has jQuery as a dependency anyway, we can use jQuery to
1415
- // parse that text into a document fragment using jQuery.clean().
1416
- var templateTextInWrapper = "<script type=\"text/html\">" + getTemplateNode(template).text + "</script>";
1417
- var renderedMarkupInWrapper = $.tmpl(templateTextInWrapper, data);
1418
- var renderedMarkup = renderedMarkupInWrapper[0].text.replace(aposRegex, "'");
1419
- return jQuery.clean([renderedMarkup], document);
1420
- },
3125
+ ko.utils.domData.set(domNode, lastMappingResultDomDataKey, newMappingResult);
3126
+ }
3127
+ })();
1421
3128
 
1422
- this.isTemplateRewritten = function (template) {
1423
- return getTemplateNode(template).isRewritten === true;
1424
- },
3129
+ ko.exportSymbol('ko.utils.setDomNodeChildrenFromArrayMapping', ko.utils.setDomNodeChildrenFromArrayMapping);
3130
+ ko.nativeTemplateEngine = function () {
3131
+ this['allowTemplateRewriting'] = false;
3132
+ }
1425
3133
 
1426
- this.rewriteTemplate = function (template, rewriterCallback) {
1427
- var templateNode = getTemplateNode(template);
1428
- var rewritten = rewriterCallback(templateNode.text);
1429
-
1430
- // jquery.tmpl falls over if you use single-quotes, so replace these with a temporary marker for template rendering,
1431
- // and then replace back after the template was rendered. This is slightly complicated by the fact that we must not interfere
1432
- // with any code blocks - only replace apos characters outside code blocks.
1433
- rewritten = ko.utils.stringTrim(rewritten);
1434
- rewritten = rewritten.replace(/([\s\S]*?)(\${[\s\S]*?}|{{[\=a-z][\s\S]*?}}|$)/g, function(match) {
1435
- // Called for each non-code-block followed by a code block (or end of template)
1436
- var nonCodeSnippet = arguments[1];
1437
- var codeSnippet = arguments[2];
1438
- return nonCodeSnippet.replace(/\'/g, aposMarker) + codeSnippet;
1439
- });
1440
-
1441
- templateNode.text = rewritten;
1442
- templateNode.isRewritten = true;
1443
- },
3134
+ ko.nativeTemplateEngine.prototype = new ko.templateEngine();
3135
+ ko.nativeTemplateEngine.prototype['renderTemplateSource'] = function (templateSource, bindingContext, options) {
3136
+ var templateText = templateSource.text();
3137
+ return ko.utils.parseHtmlFragment(templateText);
3138
+ };
1444
3139
 
1445
- this.createJavaScriptEvaluatorBlock = function (script) {
1446
- return "{{= " + script + "}}";
1447
- },
3140
+ ko.nativeTemplateEngine.instance = new ko.nativeTemplateEngine();
3141
+ ko.setTemplateEngine(ko.nativeTemplateEngine.instance);
3142
+
3143
+ ko.exportSymbol('ko.nativeTemplateEngine', ko.nativeTemplateEngine);(function() {
3144
+ ko.jqueryTmplTemplateEngine = function () {
3145
+ // Detect which version of jquery-tmpl you're using. Unfortunately jquery-tmpl
3146
+ // doesn't expose a version number, so we have to infer it.
3147
+ // Note that as of Knockout 1.3, we only support jQuery.tmpl 1.0.0pre and later,
3148
+ // which KO internally refers to as version "2", so older versions are no longer detected.
3149
+ var jQueryTmplVersion = this.jQueryTmplVersion = (function() {
3150
+ if ((typeof(jQuery) == "undefined") || !(jQuery['tmpl']))
3151
+ return 0;
3152
+ // Since it exposes no official version number, we use our own numbering system. To be updated as jquery-tmpl evolves.
3153
+ try {
3154
+ if (jQuery['tmpl']['tag']['tmpl']['open'].toString().indexOf('__') >= 0) {
3155
+ // Since 1.0.0pre, custom tags should append markup to an array called "__"
3156
+ return 2; // Final version of jquery.tmpl
3157
+ }
3158
+ } catch(ex) { /* Apparently not the version we were looking for */ }
3159
+
3160
+ return 1; // Any older version that we don't support
3161
+ })();
3162
+
3163
+ function ensureHasReferencedJQueryTemplates() {
3164
+ if (jQueryTmplVersion < 2)
3165
+ throw new Error("Your version of jQuery.tmpl is too old. Please upgrade to jQuery.tmpl 1.0.0pre or later.");
3166
+ }
1448
3167
 
1449
- // Am considering making template registration a native part of the API (and storing added templates centrally), but for now it's specific to this template engine
1450
- this.addTemplate = function (templateName, templateMarkup) {
1451
- document.write("<script type='text/html' id='" + templateName + "'>" + templateMarkup + "</script>");
1452
- }
1453
- };
1454
- ko.jqueryTmplTemplateEngine.prototype = new ko.templateEngine();
3168
+ function executeTemplate(compiledTemplate, data, jQueryTemplateOptions) {
3169
+ return jQuery['tmpl'](compiledTemplate, data, jQueryTemplateOptions);
3170
+ }
3171
+
3172
+ this['renderTemplateSource'] = function(templateSource, bindingContext, options) {
3173
+ options = options || {};
3174
+ ensureHasReferencedJQueryTemplates();
3175
+
3176
+ // Ensure we have stored a precompiled version of this template (don't want to reparse on every render)
3177
+ var precompiled = templateSource['data']('precompiled');
3178
+ if (!precompiled) {
3179
+ var templateText = templateSource.text() || "";
3180
+ // Wrap in "with($whatever.koBindingContext) { ... }"
3181
+ templateText = "{{ko_with $item.koBindingContext}}" + templateText + "{{/ko_with}}";
3182
+
3183
+ precompiled = jQuery['template'](null, templateText);
3184
+ templateSource['data']('precompiled', precompiled);
3185
+ }
3186
+
3187
+ var data = [bindingContext['$data']]; // Prewrap the data in an array to stop jquery.tmpl from trying to unwrap any arrays
3188
+ var jQueryTemplateOptions = jQuery['extend']({ 'koBindingContext': bindingContext }, options['templateOptions']);
1455
3189
 
1456
- // Use this one by default
1457
- ko.setTemplateEngine(new ko.jqueryTmplTemplateEngine());
3190
+ var resultNodes = executeTemplate(precompiled, data, jQueryTemplateOptions);
3191
+ resultNodes['appendTo'](document.createElement("div")); // Using "appendTo" forces jQuery/jQuery.tmpl to perform necessary cleanup work
3192
+ jQuery['fragments'] = {}; // Clear jQuery's fragment cache to avoid a memory leak after a large number of template renders
3193
+ return resultNodes;
3194
+ };
3195
+
3196
+ this['createJavaScriptEvaluatorBlock'] = function(script) {
3197
+ return "{{ko_code ((function() { return " + script + " })()) }}";
3198
+ };
3199
+
3200
+ this['addTemplate'] = function(templateName, templateMarkup) {
3201
+ document.write("<script type='text/html' id='" + templateName + "'>" + templateMarkup + "</script>");
3202
+ };
3203
+
3204
+ if (jQueryTmplVersion > 0) {
3205
+ jQuery['tmpl']['tag']['ko_code'] = {
3206
+ open: "__.push($1 || '');"
3207
+ };
3208
+ jQuery['tmpl']['tag']['ko_with'] = {
3209
+ open: "with($1) {",
3210
+ close: "} "
3211
+ };
3212
+ }
3213
+ };
3214
+
3215
+ ko.jqueryTmplTemplateEngine.prototype = new ko.templateEngine();
3216
+
3217
+ // Use this one by default *only if jquery.tmpl is referenced*
3218
+ var jqueryTmplTemplateEngineInstance = new ko.jqueryTmplTemplateEngine();
3219
+ if (jqueryTmplTemplateEngineInstance.jQueryTmplVersion > 0)
3220
+ ko.setTemplateEngine(jqueryTmplTemplateEngineInstance);
3221
+
3222
+ ko.exportSymbol('ko.jqueryTmplTemplateEngine', ko.jqueryTmplTemplateEngine);
3223
+ })();})(window);