knockoutjs-rails 2.1.0 → 2.2.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 2.1.0 == Knockout.js 2.1.0
23
+ knockoutjs-rails 2.2.0 == Knockout.js 2.2.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 = "2.1.0"
3
+ VERSION = "2.2.0"
4
4
  end
5
5
  end
@@ -1,9 +1,10 @@
1
- // Knockout JavaScript library v2.1.0
1
+ // Knockout JavaScript library v2.2.0
2
2
  // (c) Steven Sanderson - http://knockoutjs.com/
3
3
  // License: MIT (http://www.opensource.org/licenses/mit-license.php)
4
4
 
5
- (function(window,document,navigator,undefined){
5
+ (function(){
6
6
  var DEBUG=true;
7
+ (function(window,document,navigator,jQuery,undefined){
7
8
  !function(factory) {
8
9
  // Support three module loading scenarios
9
10
  if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') {
@@ -36,7 +37,7 @@ ko.exportSymbol = function(koPath, object) {
36
37
  ko.exportProperty = function(owner, publicName, object) {
37
38
  owner[publicName] = object;
38
39
  };
39
- ko.version = "2.1.0";
40
+ ko.version = "2.2.0";
40
41
 
41
42
  ko.exportSymbol('version', ko.version);
42
43
  ko.utils = new (function () {
@@ -57,6 +58,9 @@ ko.utils = new (function () {
57
58
  var eventsThatMustBeRegisteredUsingAttachEvent = { 'propertychange': true }; // Workaround for an IE9 issue - https://github.com/SteveSanderson/knockout/issues/406
58
59
 
59
60
  // Detect IE versions for bug workarounds (uses IE conditionals, not UA string, for robustness)
61
+ // Note that, since IE 10 does not support conditional comments, the following logic only detects IE < 10.
62
+ // Currently this is by design, since IE 10+ behaves correctly when treated as a standard browser.
63
+ // If there is a future need to detect specific versions of IE10+, we will amend this.
60
64
  var ieVersion = (function() {
61
65
  var version = 3, div = document.createElement('div'), iElems = div.getElementsByTagName('i');
62
66
 
@@ -167,12 +171,19 @@ ko.utils = new (function () {
167
171
 
168
172
  var container = document.createElement('div');
169
173
  for (var i = 0, j = nodesArray.length; i < j; i++) {
170
- ko.cleanNode(nodesArray[i]);
171
- container.appendChild(nodesArray[i]);
174
+ container.appendChild(ko.cleanNode(nodesArray[i]));
172
175
  }
173
176
  return container;
174
177
  },
175
178
 
179
+ cloneNodes: function (nodesArray, shouldCleanNodes) {
180
+ for (var i = 0, j = nodesArray.length, newNodesArray = []; i < j; i++) {
181
+ var clonedNode = nodesArray[i].cloneNode(true);
182
+ newNodesArray.push(shouldCleanNodes ? ko.cleanNode(clonedNode) : clonedNode);
183
+ }
184
+ return newNodesArray;
185
+ },
186
+
176
187
  setDomNodeChildren: function (domNode, childNodes) {
177
188
  ko.utils.emptyDomNode(domNode);
178
189
  if (childNodes) {
@@ -196,7 +207,7 @@ ko.utils = new (function () {
196
207
 
197
208
  setOptionNodeSelectionState: function (optionNode, isSelected) {
198
209
  // IE6 sometimes throws "unknown error" if you try to write to .selected directly, whereas Firefox struggles with setAttribute. Pick one based on browser.
199
- if (navigator.userAgent.indexOf("MSIE 6") >= 0)
210
+ if (ieVersion < 7)
200
211
  optionNode.setAttribute("selected", isSelected);
201
212
  else
202
213
  optionNode.selected = isSelected;
@@ -224,17 +235,6 @@ ko.utils = new (function () {
224
235
  return string.substring(0, startsWith.length) === startsWith;
225
236
  },
226
237
 
227
- buildEvalWithinScopeFunction: function (expression, scopeLevels) {
228
- // Build the source for a function that evaluates "expression"
229
- // For each scope variable, add an extra level of "with" nesting
230
- // Example result: with(sc[1]) { with(sc[0]) { return (expression) } }
231
- var functionBody = "return (" + expression + ")";
232
- for (var i = 0; i < scopeLevels; i++) {
233
- functionBody = "with(sc[" + i + "]) { " + functionBody + " } ";
234
- }
235
- return new Function("sc", functionBody);
236
- },
237
-
238
238
  domNodeIsContainedBy: function (node, containedByNode) {
239
239
  if (containedByNode.compareDocumentPosition)
240
240
  return (containedByNode.compareDocumentPosition(node) & 16) == 16;
@@ -320,18 +320,25 @@ ko.utils = new (function () {
320
320
  return ko.isObservable(value) ? value() : value;
321
321
  },
322
322
 
323
- toggleDomNodeCssClass: function (node, className, shouldHaveClass) {
324
- var currentClassNames = (node.className || "").split(/\s+/);
325
- var hasClass = ko.utils.arrayIndexOf(currentClassNames, className) >= 0;
323
+ peekObservable: function (value) {
324
+ return ko.isObservable(value) ? value.peek() : value;
325
+ },
326
326
 
327
- if (shouldHaveClass && !hasClass) {
328
- node.className += (currentClassNames[0] ? " " : "") + className;
329
- } else if (hasClass && !shouldHaveClass) {
330
- var newClassName = "";
331
- for (var i = 0; i < currentClassNames.length; i++)
332
- if (currentClassNames[i] != className)
333
- newClassName += currentClassNames[i] + " ";
334
- node.className = ko.utils.stringTrim(newClassName);
327
+ toggleDomNodeCssClass: function (node, classNames, shouldHaveClass) {
328
+ if (classNames) {
329
+ var cssClassNameRegex = /[\w-]+/g,
330
+ currentClassNames = node.className.match(cssClassNameRegex) || [];
331
+ ko.utils.arrayForEach(classNames.match(cssClassNameRegex), function(className) {
332
+ var indexOfClass = ko.utils.arrayIndexOf(currentClassNames, className);
333
+ if (indexOfClass >= 0) {
334
+ if (!shouldHaveClass)
335
+ currentClassNames.splice(indexOfClass, 1);
336
+ } else {
337
+ if (shouldHaveClass)
338
+ currentClassNames.push(className);
339
+ }
340
+ });
341
+ node.className = currentClassNames.join(" ");
335
342
  }
336
343
  },
337
344
 
@@ -340,13 +347,44 @@ ko.utils = new (function () {
340
347
  if ((value === null) || (value === undefined))
341
348
  value = "";
342
349
 
343
- 'innerText' in element ? element.innerText = value
344
- : element.textContent = value;
350
+ if (element.nodeType === 3) {
351
+ element.data = value;
352
+ } else {
353
+ // We need there to be exactly one child: a text node.
354
+ // If there are no children, more than one, or if it's not a text node,
355
+ // we'll clear everything and create a single text node.
356
+ var innerTextNode = ko.virtualElements.firstChild(element);
357
+ if (!innerTextNode || innerTextNode.nodeType != 3 || ko.virtualElements.nextSibling(innerTextNode)) {
358
+ ko.virtualElements.setDomNodeChildren(element, [document.createTextNode(value)]);
359
+ } else {
360
+ innerTextNode.data = value;
361
+ }
362
+
363
+ ko.utils.forceRefresh(element);
364
+ }
365
+ },
366
+
367
+ setElementName: function(element, name) {
368
+ element.name = name;
345
369
 
370
+ // Workaround IE 6/7 issue
371
+ // - https://github.com/SteveSanderson/knockout/issues/197
372
+ // - http://www.matts411.com/post/setting_the_name_attribute_in_ie_dom/
373
+ if (ieVersion <= 7) {
374
+ try {
375
+ element.mergeAttributes(document.createElement("<input name='" + element.name + "'/>"), false);
376
+ }
377
+ catch(e) {} // For IE9 with doc mode "IE9 Standards" and browser mode "IE9 Compatibility View"
378
+ }
379
+ },
380
+
381
+ forceRefresh: function(node) {
382
+ // Workaround for an IE9 rendering bug - https://github.com/SteveSanderson/knockout/issues/209
346
383
  if (ieVersion >= 9) {
347
- // Believe it or not, this actually fixes an IE9 rendering bug
348
- // (See https://github.com/SteveSanderson/knockout/issues/209)
349
- element.style.display = element.style.display;
384
+ // For text nodes and comment nodes (most likely virtual elements), we will have to refresh the container
385
+ var elem = node.nodeType == 1 ? node : node.parentNode;
386
+ if (elem.style)
387
+ elem.style.zoom = elem.style.zoom;
350
388
  }
351
389
  },
352
390
 
@@ -465,6 +503,7 @@ ko.exportSymbol('utils.arrayRemoveItem', ko.utils.arrayRemoveItem);
465
503
  ko.exportSymbol('utils.extend', ko.utils.extend);
466
504
  ko.exportSymbol('utils.fieldsIncludedWithJsonPost', ko.utils.fieldsIncludedWithJsonPost);
467
505
  ko.exportSymbol('utils.getFormFields', ko.utils.getFormFields);
506
+ ko.exportSymbol('utils.peekObservable', ko.utils.peekObservable);
468
507
  ko.exportSymbol('utils.postJson', ko.utils.postJson);
469
508
  ko.exportSymbol('utils.parseJson', ko.utils.parseJson);
470
509
  ko.exportSymbol('utils.registerEventHandler', ko.utils.registerEventHandler);
@@ -505,7 +544,7 @@ ko.utils.domData = new (function () {
505
544
  },
506
545
  getAll: function (node, createIfNotFound) {
507
546
  var dataStoreKey = node[dataStoreKeyExpandoPropertyName];
508
- var hasExistingDataStore = dataStoreKey && (dataStoreKey !== "null");
547
+ var hasExistingDataStore = dataStoreKey && (dataStoreKey !== "null") && dataStore[dataStoreKey];
509
548
  if (!hasExistingDataStore) {
510
549
  if (!createIfNotFound)
511
550
  return undefined;
@@ -519,7 +558,9 @@ ko.utils.domData = new (function () {
519
558
  if (dataStoreKey) {
520
559
  delete dataStore[dataStoreKey];
521
560
  node[dataStoreKeyExpandoPropertyName] = null;
561
+ return true; // Exposing "did clean" flag purely so specs can infer whether things have been cleaned up as intended
522
562
  }
563
+ return false;
523
564
  }
524
565
  }
525
566
  })();
@@ -607,6 +648,7 @@ ko.utils.domNodeDisposal = new (function () {
607
648
  cleanSingleNode(descendants[i]);
608
649
  }
609
650
  }
651
+ return node;
610
652
  },
611
653
 
612
654
  removeNode : function(node) {
@@ -687,6 +729,9 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD
687
729
  ko.utils.setHtml = function(node, html) {
688
730
  ko.utils.emptyDomNode(node);
689
731
 
732
+ // There's no legitimate reason to display a stringified observable without unwrapping it, so we'll unwrap it
733
+ html = ko.utils.unwrapObservable(html);
734
+
690
735
  if ((html !== null) && (html !== undefined)) {
691
736
  if (typeof html != 'string')
692
737
  html = html.toString();
@@ -863,12 +908,14 @@ ko.subscribable['fn'] = {
863
908
  "notifySubscribers": function (valueToNotify, event) {
864
909
  event = event || defaultEvent;
865
910
  if (this._subscriptions[event]) {
866
- ko.utils.arrayForEach(this._subscriptions[event].slice(0), function (subscription) {
867
- // In case a subscription was disposed during the arrayForEach cycle, check
868
- // for isDisposed on each subscription before invoking its callback
869
- if (subscription && (subscription.isDisposed !== true))
870
- subscription.callback(valueToNotify);
871
- });
911
+ ko.dependencyDetection.ignore(function() {
912
+ ko.utils.arrayForEach(this._subscriptions[event].slice(0), function (subscription) {
913
+ // In case a subscription was disposed during the arrayForEach cycle, check
914
+ // for isDisposed on each subscription before invoking its callback
915
+ if (subscription && (subscription.isDisposed !== true))
916
+ subscription.callback(valueToNotify);
917
+ });
918
+ }, this);
872
919
  }
873
920
  },
874
921
 
@@ -909,11 +956,20 @@ ko.dependencyDetection = (function () {
909
956
  throw new Error("Only subscribable things can act as dependencies");
910
957
  if (_frames.length > 0) {
911
958
  var topFrame = _frames[_frames.length - 1];
912
- if (ko.utils.arrayIndexOf(topFrame.distinctDependencies, subscribable) >= 0)
959
+ if (!topFrame || ko.utils.arrayIndexOf(topFrame.distinctDependencies, subscribable) >= 0)
913
960
  return;
914
961
  topFrame.distinctDependencies.push(subscribable);
915
962
  topFrame.callback(subscribable);
916
963
  }
964
+ },
965
+
966
+ ignore: function(callback, callbackTarget, callbackArgs) {
967
+ try {
968
+ _frames.push(null);
969
+ return callback.apply(callbackTarget, callbackArgs || []);
970
+ } finally {
971
+ _frames.pop();
972
+ }
917
973
  }
918
974
  };
919
975
  })();
@@ -943,10 +999,12 @@ ko.observable = function (initialValue) {
943
999
  }
944
1000
  if (DEBUG) observable._latestValue = _latestValue;
945
1001
  ko.subscribable.call(observable);
1002
+ observable.peek = function() { return _latestValue };
946
1003
  observable.valueHasMutated = function () { observable["notifySubscribers"](_latestValue); }
947
1004
  observable.valueWillMutate = function () { observable["notifySubscribers"](_latestValue, "beforeChange"); }
948
1005
  ko.utils.extend(observable, ko.observable['fn']);
949
1006
 
1007
+ ko.exportProperty(observable, 'peek', observable.peek);
950
1008
  ko.exportProperty(observable, "valueHasMutated", observable.valueHasMutated);
951
1009
  ko.exportProperty(observable, "valueWillMutate", observable.valueWillMutate);
952
1010
 
@@ -1002,7 +1060,7 @@ ko.observableArray = function (initialValues) {
1002
1060
 
1003
1061
  ko.observableArray['fn'] = {
1004
1062
  'remove': function (valueOrPredicate) {
1005
- var underlyingArray = this();
1063
+ var underlyingArray = this.peek();
1006
1064
  var removedValues = [];
1007
1065
  var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) { return value === valueOrPredicate; };
1008
1066
  for (var i = 0; i < underlyingArray.length; i++) {
@@ -1025,7 +1083,7 @@ ko.observableArray['fn'] = {
1025
1083
  'removeAll': function (arrayOfValues) {
1026
1084
  // If you passed zero args, we remove everything
1027
1085
  if (arrayOfValues === undefined) {
1028
- var underlyingArray = this();
1086
+ var underlyingArray = this.peek();
1029
1087
  var allValues = underlyingArray.slice(0);
1030
1088
  this.valueWillMutate();
1031
1089
  underlyingArray.splice(0, underlyingArray.length);
@@ -1041,7 +1099,7 @@ ko.observableArray['fn'] = {
1041
1099
  },
1042
1100
 
1043
1101
  'destroy': function (valueOrPredicate) {
1044
- var underlyingArray = this();
1102
+ var underlyingArray = this.peek();
1045
1103
  var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) { return value === valueOrPredicate; };
1046
1104
  this.valueWillMutate();
1047
1105
  for (var i = underlyingArray.length - 1; i >= 0; i--) {
@@ -1074,16 +1132,20 @@ ko.observableArray['fn'] = {
1074
1132
  var index = this['indexOf'](oldItem);
1075
1133
  if (index >= 0) {
1076
1134
  this.valueWillMutate();
1077
- this()[index] = newItem;
1135
+ this.peek()[index] = newItem;
1078
1136
  this.valueHasMutated();
1079
1137
  }
1080
1138
  }
1081
1139
  }
1082
1140
 
1083
1141
  // Populate ko.observableArray.fn with read/write functions from native arrays
1142
+ // Important: Do not add any additional functions here that may reasonably be used to *read* data from the array
1143
+ // because we'll eval them without causing subscriptions, so ko.computed output could end up getting stale
1084
1144
  ko.utils.arrayForEach(["pop", "push", "reverse", "shift", "sort", "splice", "unshift"], function (methodName) {
1085
1145
  ko.observableArray['fn'][methodName] = function () {
1086
- var underlyingArray = this();
1146
+ // Use "peek" to avoid creating a subscription in any computed that we're executing in the context of
1147
+ // (for consistency with mutating regular observables)
1148
+ var underlyingArray = this.peek();
1087
1149
  this.valueWillMutate();
1088
1150
  var methodCallResult = underlyingArray[methodName].apply(underlyingArray, arguments);
1089
1151
  this.valueHasMutated();
@@ -1116,41 +1178,20 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
1116
1178
  if (!readFunction)
1117
1179
  readFunction = options["read"];
1118
1180
  }
1119
- // By here, "options" is always non-null
1120
1181
  if (typeof readFunction != "function")
1121
1182
  throw new Error("Pass a function that returns the value of the ko.computed");
1122
1183
 
1123
- var writeFunction = options["write"];
1124
- if (!evaluatorFunctionTarget)
1125
- evaluatorFunctionTarget = options["owner"];
1184
+ function addSubscriptionToDependency(subscribable) {
1185
+ _subscriptionsToDependencies.push(subscribable.subscribe(evaluatePossiblyAsync));
1186
+ }
1126
1187
 
1127
- var _subscriptionsToDependencies = [];
1128
1188
  function disposeAllSubscriptionsToDependencies() {
1129
1189
  ko.utils.arrayForEach(_subscriptionsToDependencies, function (subscription) {
1130
1190
  subscription.dispose();
1131
1191
  });
1132
1192
  _subscriptionsToDependencies = [];
1133
1193
  }
1134
- var dispose = disposeAllSubscriptionsToDependencies;
1135
-
1136
- // Build "disposeWhenNodeIsRemoved" and "disposeWhenNodeIsRemovedCallback" option values
1137
- // (Note: "disposeWhenNodeIsRemoved" option both proactively disposes as soon as the node is removed using ko.removeNode(),
1138
- // plus adds a "disposeWhen" callback that, on each evaluation, disposes if the node was removed by some other means.)
1139
- var disposeWhenNodeIsRemoved = (typeof options["disposeWhenNodeIsRemoved"] == "object") ? options["disposeWhenNodeIsRemoved"] : null;
1140
- var disposeWhen = options["disposeWhen"] || function() { return false; };
1141
- if (disposeWhenNodeIsRemoved) {
1142
- dispose = function() {
1143
- ko.utils.domNodeDisposal.removeDisposeCallback(disposeWhenNodeIsRemoved, arguments.callee);
1144
- disposeAllSubscriptionsToDependencies();
1145
- };
1146
- ko.utils.domNodeDisposal.addDisposeCallback(disposeWhenNodeIsRemoved, dispose);
1147
- var existingDisposeWhenFunction = disposeWhen;
1148
- disposeWhen = function () {
1149
- return !ko.utils.domNodeIsAttachedToDocument(disposeWhenNodeIsRemoved) || existingDisposeWhenFunction();
1150
- }
1151
- }
1152
1194
 
1153
- var evaluationTimeoutInstance = null;
1154
1195
  function evaluatePossiblyAsync() {
1155
1196
  var throttleEvaluationTimeout = dependentObservable['throttleEvaluation'];
1156
1197
  if (throttleEvaluationTimeout && throttleEvaluationTimeout >= 0) {
@@ -1188,7 +1229,7 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
1188
1229
  if ((inOld = ko.utils.arrayIndexOf(disposalCandidates, subscribable)) >= 0)
1189
1230
  disposalCandidates[inOld] = undefined; // Don't want to dispose this subscription, as it's still being used
1190
1231
  else
1191
- _subscriptionsToDependencies.push(subscribable.subscribe(evaluatePossiblyAsync)); // Brand new subscription - add it
1232
+ addSubscriptionToDependency(subscribable); // Brand new subscription - add it
1192
1233
  });
1193
1234
 
1194
1235
  var newValue = readFunction.call(evaluatorFunctionTarget);
@@ -1209,46 +1250,82 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
1209
1250
 
1210
1251
  dependentObservable["notifySubscribers"](_latestValue);
1211
1252
  _isBeingEvaluated = false;
1212
-
1253
+ if (!_subscriptionsToDependencies.length)
1254
+ dispose();
1213
1255
  }
1214
1256
 
1215
1257
  function dependentObservable() {
1216
1258
  if (arguments.length > 0) {
1217
- set.apply(dependentObservable, arguments);
1218
- } else {
1219
- return get();
1220
- }
1221
- }
1222
-
1223
- function set() {
1224
- if (typeof writeFunction === "function") {
1225
- // Writing a value
1226
- writeFunction.apply(evaluatorFunctionTarget, arguments);
1259
+ if (typeof writeFunction === "function") {
1260
+ // Writing a value
1261
+ writeFunction.apply(evaluatorFunctionTarget, arguments);
1262
+ } else {
1263
+ throw new Error("Cannot write a value to a ko.computed unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters.");
1264
+ }
1265
+ return this; // Permits chained assignments
1227
1266
  } else {
1228
- throw new Error("Cannot write a value to a ko.computed unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters.");
1267
+ // Reading the value
1268
+ if (!_hasBeenEvaluated)
1269
+ evaluateImmediate();
1270
+ ko.dependencyDetection.registerDependency(dependentObservable);
1271
+ return _latestValue;
1229
1272
  }
1230
1273
  }
1231
1274
 
1232
- function get() {
1233
- // Reading the value
1275
+ function peek() {
1234
1276
  if (!_hasBeenEvaluated)
1235
1277
  evaluateImmediate();
1236
- ko.dependencyDetection.registerDependency(dependentObservable);
1237
1278
  return _latestValue;
1238
1279
  }
1239
1280
 
1281
+ function isActive() {
1282
+ return !_hasBeenEvaluated || _subscriptionsToDependencies.length > 0;
1283
+ }
1284
+
1285
+ // By here, "options" is always non-null
1286
+ var writeFunction = options["write"],
1287
+ disposeWhenNodeIsRemoved = options["disposeWhenNodeIsRemoved"] || options.disposeWhenNodeIsRemoved || null,
1288
+ disposeWhen = options["disposeWhen"] || options.disposeWhen || function() { return false; },
1289
+ dispose = disposeAllSubscriptionsToDependencies,
1290
+ _subscriptionsToDependencies = [],
1291
+ evaluationTimeoutInstance = null;
1292
+
1293
+ if (!evaluatorFunctionTarget)
1294
+ evaluatorFunctionTarget = options["owner"];
1295
+
1296
+ dependentObservable.peek = peek;
1240
1297
  dependentObservable.getDependenciesCount = function () { return _subscriptionsToDependencies.length; };
1241
1298
  dependentObservable.hasWriteFunction = typeof options["write"] === "function";
1242
1299
  dependentObservable.dispose = function () { dispose(); };
1300
+ dependentObservable.isActive = isActive;
1243
1301
 
1244
1302
  ko.subscribable.call(dependentObservable);
1245
1303
  ko.utils.extend(dependentObservable, ko.dependentObservable['fn']);
1246
1304
 
1305
+ ko.exportProperty(dependentObservable, 'peek', dependentObservable.peek);
1306
+ ko.exportProperty(dependentObservable, 'dispose', dependentObservable.dispose);
1307
+ ko.exportProperty(dependentObservable, 'isActive', dependentObservable.isActive);
1308
+ ko.exportProperty(dependentObservable, 'getDependenciesCount', dependentObservable.getDependenciesCount);
1309
+
1310
+ // Evaluate, unless deferEvaluation is true
1247
1311
  if (options['deferEvaluation'] !== true)
1248
1312
  evaluateImmediate();
1249
1313
 
1250
- ko.exportProperty(dependentObservable, 'dispose', dependentObservable.dispose);
1251
- ko.exportProperty(dependentObservable, 'getDependenciesCount', dependentObservable.getDependenciesCount);
1314
+ // Build "disposeWhenNodeIsRemoved" and "disposeWhenNodeIsRemovedCallback" option values.
1315
+ // But skip if isActive is false (there will never be any dependencies to dispose).
1316
+ // (Note: "disposeWhenNodeIsRemoved" option both proactively disposes as soon as the node is removed using ko.removeNode(),
1317
+ // plus adds a "disposeWhen" callback that, on each evaluation, disposes if the node was removed by some other means.)
1318
+ if (disposeWhenNodeIsRemoved && isActive()) {
1319
+ dispose = function() {
1320
+ ko.utils.domNodeDisposal.removeDisposeCallback(disposeWhenNodeIsRemoved, arguments.callee);
1321
+ disposeAllSubscriptionsToDependencies();
1322
+ };
1323
+ ko.utils.domNodeDisposal.addDisposeCallback(disposeWhenNodeIsRemoved, dispose);
1324
+ var existingDisposeWhenFunction = disposeWhen;
1325
+ disposeWhen = function () {
1326
+ return !ko.utils.domNodeIsAttachedToDocument(disposeWhenNodeIsRemoved) || existingDisposeWhenFunction();
1327
+ }
1328
+ }
1252
1329
 
1253
1330
  return dependentObservable;
1254
1331
  };
@@ -1369,7 +1446,9 @@ ko.exportSymbol('toJSON', ko.toJSON);
1369
1446
  case 'option':
1370
1447
  if (element[hasDomDataExpandoProperty] === true)
1371
1448
  return ko.utils.domData.get(element, ko.bindingHandlers.options.optionValueDomDataKey);
1372
- return element.getAttribute("value");
1449
+ return ko.utils.ieVersion <= 7
1450
+ ? (element.getAttributeNode('value').specified ? element.value : element.text)
1451
+ : element.value;
1373
1452
  case 'select':
1374
1453
  return element.selectedIndex >= 0 ? ko.selectExtensions.readValue(element.options[element.selectedIndex]) : undefined;
1375
1454
  default:
@@ -1419,12 +1498,14 @@ ko.exportSymbol('toJSON', ko.toJSON);
1419
1498
  ko.exportSymbol('selectExtensions', ko.selectExtensions);
1420
1499
  ko.exportSymbol('selectExtensions.readValue', ko.selectExtensions.readValue);
1421
1500
  ko.exportSymbol('selectExtensions.writeValue', ko.selectExtensions.writeValue);
1422
-
1423
- ko.jsonExpressionRewriting = (function () {
1501
+ ko.expressionRewriting = (function () {
1424
1502
  var restoreCapturedTokensRegex = /\@ko_token_(\d+)\@/g;
1425
- var javaScriptAssignmentTarget = /^[\_$a-z][\_$a-z0-9]*(\[.*?\])*(\.[\_$a-z][\_$a-z0-9]*(\[.*?\])*)*$/i;
1426
1503
  var javaScriptReservedWords = ["true", "false"];
1427
1504
 
1505
+ // Matches something that can be assigned to--either an isolated identifier or something ending with a property accessor
1506
+ // This is designed to be simple and avoid false negatives, but could produce false positives (e.g., a+b.c).
1507
+ var javaScriptAssignmentTarget = /^(?:[$_a-z][$\w]*|(.+)(\.\s*[$_a-z][$\w]*|\[.+\]))$/i;
1508
+
1428
1509
  function restoreTokens(string, tokens) {
1429
1510
  var prevValue = null;
1430
1511
  while (string != prevValue) { // Keep restoring tokens until it no longer makes a difference (they may be nested)
@@ -1436,10 +1517,11 @@ ko.jsonExpressionRewriting = (function () {
1436
1517
  return string;
1437
1518
  }
1438
1519
 
1439
- function isWriteableValue(expression) {
1520
+ function getWriteableValue(expression) {
1440
1521
  if (ko.utils.arrayIndexOf(javaScriptReservedWords, ko.utils.stringTrim(expression).toLowerCase()) >= 0)
1441
1522
  return false;
1442
- return expression.match(javaScriptAssignmentTarget) !== null;
1523
+ var match = expression.match(javaScriptAssignmentTarget);
1524
+ return match === null ? false : match[1] ? ('Object(' + match[1] + ')' + match[2]) : expression;
1443
1525
  }
1444
1526
 
1445
1527
  function ensureQuoted(key) {
@@ -1542,9 +1624,9 @@ ko.jsonExpressionRewriting = (function () {
1542
1624
  return result;
1543
1625
  },
1544
1626
 
1545
- insertPropertyAccessorsIntoJson: function (objectLiteralStringOrKeyValueArray) {
1627
+ preProcessBindings: function (objectLiteralStringOrKeyValueArray) {
1546
1628
  var keyValueArray = typeof objectLiteralStringOrKeyValueArray === "string"
1547
- ? ko.jsonExpressionRewriting.parseObjectLiteral(objectLiteralStringOrKeyValueArray)
1629
+ ? ko.expressionRewriting.parseObjectLiteral(objectLiteralStringOrKeyValueArray)
1548
1630
  : objectLiteralStringOrKeyValueArray;
1549
1631
  var resultStrings = [], propertyAccessorResultStrings = [];
1550
1632
 
@@ -1559,7 +1641,7 @@ ko.jsonExpressionRewriting = (function () {
1559
1641
  resultStrings.push(":");
1560
1642
  resultStrings.push(val);
1561
1643
 
1562
- if (isWriteableValue(ko.utils.stringTrim(val))) {
1644
+ if (val = getWriteableValue(ko.utils.stringTrim(val))) {
1563
1645
  if (propertyAccessorResultStrings.length > 0)
1564
1646
  propertyAccessorResultStrings.push(", ");
1565
1647
  propertyAccessorResultStrings.push(quotedKey + " : function(__ko_value) { " + val + " = __ko_value; }");
@@ -1599,18 +1681,22 @@ ko.jsonExpressionRewriting = (function () {
1599
1681
  var propWriters = allBindingsAccessor()['_ko_property_writers'];
1600
1682
  if (propWriters && propWriters[key])
1601
1683
  propWriters[key](value);
1602
- } else if (!checkIfDifferent || property() !== value) {
1684
+ } else if (!checkIfDifferent || property.peek() !== value) {
1603
1685
  property(value);
1604
1686
  }
1605
1687
  }
1606
1688
  };
1607
1689
  })();
1608
1690
 
1609
- ko.exportSymbol('jsonExpressionRewriting', ko.jsonExpressionRewriting);
1610
- ko.exportSymbol('jsonExpressionRewriting.bindingRewriteValidators', ko.jsonExpressionRewriting.bindingRewriteValidators);
1611
- ko.exportSymbol('jsonExpressionRewriting.parseObjectLiteral', ko.jsonExpressionRewriting.parseObjectLiteral);
1612
- ko.exportSymbol('jsonExpressionRewriting.insertPropertyAccessorsIntoJson', ko.jsonExpressionRewriting.insertPropertyAccessorsIntoJson);
1613
- (function() {
1691
+ ko.exportSymbol('expressionRewriting', ko.expressionRewriting);
1692
+ ko.exportSymbol('expressionRewriting.bindingRewriteValidators', ko.expressionRewriting.bindingRewriteValidators);
1693
+ ko.exportSymbol('expressionRewriting.parseObjectLiteral', ko.expressionRewriting.parseObjectLiteral);
1694
+ ko.exportSymbol('expressionRewriting.preProcessBindings', ko.expressionRewriting.preProcessBindings);
1695
+
1696
+ // For backward compatibility, define the following aliases. (Previously, these function names were misleading because
1697
+ // they referred to JSON specifically, even though they actually work with arbitrary JavaScript object literal expressions.)
1698
+ ko.exportSymbol('jsonExpressionRewriting', ko.expressionRewriting);
1699
+ ko.exportSymbol('jsonExpressionRewriting.insertPropertyAccessorsIntoJson', ko.expressionRewriting.preProcessBindings);(function() {
1614
1700
  // "Virtual elements" is an abstraction on top of the usual DOM API which understands the notion that comment nodes
1615
1701
  // may be used to represent hierarchy (in addition to the DOM's natural hierarchy).
1616
1702
  // If you call the DOM-manipulating functions on ko.virtualElements, you will be able to read and write the state
@@ -1624,7 +1710,7 @@ ko.exportSymbol('jsonExpressionRewriting.insertPropertyAccessorsIntoJson', ko.js
1624
1710
  // So, use node.text where available, and node.nodeValue elsewhere
1625
1711
  var commentNodesHaveTextProperty = document.createComment("test").text === "<!--test-->";
1626
1712
 
1627
- var startCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*ko\s+(.*\:.*)\s*-->$/ : /^\s*ko\s+(.*\:.*)\s*$/;
1713
+ var startCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*ko(?:\s+(.+\s*\:[\s\S]*))?\s*-->$/ : /^\s*ko(?:\s+(.+\s*\:[\s\S]*))?\s*$/;
1628
1714
  var endCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*\/ko\s*-->$/ : /^\s*\/ko\s*$/;
1629
1715
  var htmlTagsWithOptionallyClosingChildren = { 'ul': true, 'ol': true };
1630
1716
 
@@ -1730,7 +1816,9 @@ ko.exportSymbol('jsonExpressionRewriting.insertPropertyAccessorsIntoJson', ko.js
1730
1816
  },
1731
1817
 
1732
1818
  insertAfter: function(containerNode, nodeToInsert, insertAfterNode) {
1733
- if (!isStartComment(containerNode)) {
1819
+ if (!insertAfterNode) {
1820
+ ko.virtualElements.prepend(containerNode, nodeToInsert);
1821
+ } else if (!isStartComment(containerNode)) {
1734
1822
  // Insert after insertion point
1735
1823
  if (insertAfterNode.nextSibling)
1736
1824
  containerNode.insertBefore(nodeToInsert, insertAfterNode.nextSibling);
@@ -1819,7 +1907,7 @@ ko.exportSymbol('virtualElements.setDomNodeChildren', ko.virtualElements.setDomN
1819
1907
 
1820
1908
  'getBindings': function(node, bindingContext) {
1821
1909
  var bindingsString = this['getBindingsString'](node, bindingContext);
1822
- return bindingsString ? this['parseBindingsString'](bindingsString, bindingContext) : null;
1910
+ return bindingsString ? this['parseBindingsString'](bindingsString, bindingContext, node) : null;
1823
1911
  },
1824
1912
 
1825
1913
  // The following function is only used internally by this default provider.
@@ -1834,12 +1922,10 @@ ko.exportSymbol('virtualElements.setDomNodeChildren', ko.virtualElements.setDomN
1834
1922
 
1835
1923
  // The following function is only used internally by this default provider.
1836
1924
  // It's not part of the interface definition for a general binding provider.
1837
- 'parseBindingsString': function(bindingsString, bindingContext) {
1925
+ 'parseBindingsString': function(bindingsString, bindingContext, node) {
1838
1926
  try {
1839
- var viewModel = bindingContext['$data'],
1840
- scopes = (typeof viewModel == 'object' && viewModel != null) ? [viewModel, bindingContext] : [bindingContext],
1841
- bindingFunction = createBindingsStringEvaluatorViaCache(bindingsString, scopes.length, this.bindingCache);
1842
- return bindingFunction(scopes);
1927
+ var bindingFunction = createBindingsStringEvaluatorViaCache(bindingsString, this.bindingCache);
1928
+ return bindingFunction(bindingContext, node);
1843
1929
  } catch (ex) {
1844
1930
  throw new Error("Unable to parse bindings.\nMessage: " + ex + ";\nBindings value: " + bindingsString);
1845
1931
  }
@@ -1848,15 +1934,19 @@ ko.exportSymbol('virtualElements.setDomNodeChildren', ko.virtualElements.setDomN
1848
1934
 
1849
1935
  ko.bindingProvider['instance'] = new ko.bindingProvider();
1850
1936
 
1851
- function createBindingsStringEvaluatorViaCache(bindingsString, scopesCount, cache) {
1852
- var cacheKey = scopesCount + '_' + bindingsString;
1937
+ function createBindingsStringEvaluatorViaCache(bindingsString, cache) {
1938
+ var cacheKey = bindingsString;
1853
1939
  return cache[cacheKey]
1854
- || (cache[cacheKey] = createBindingsStringEvaluator(bindingsString, scopesCount));
1940
+ || (cache[cacheKey] = createBindingsStringEvaluator(bindingsString));
1855
1941
  }
1856
1942
 
1857
- function createBindingsStringEvaluator(bindingsString, scopesCount) {
1858
- var rewrittenBindings = " { " + ko.jsonExpressionRewriting.insertPropertyAccessorsIntoJson(bindingsString) + " } ";
1859
- return ko.utils.buildEvalWithinScopeFunction(rewrittenBindings, scopesCount);
1943
+ function createBindingsStringEvaluator(bindingsString) {
1944
+ // Build the source for a function that evaluates "expression"
1945
+ // For each scope variable, add an extra level of "with" nesting
1946
+ // Example result: with(sc1) { with(sc0) { return (expression) } }
1947
+ var rewrittenBindings = ko.expressionRewriting.preProcessBindings(bindingsString),
1948
+ functionBody = "with($context){with($data||{}){return{" + rewrittenBindings + "}}}";
1949
+ return new Function("$context", "$element", functionBody);
1860
1950
  }
1861
1951
  })();
1862
1952
 
@@ -1864,7 +1954,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
1864
1954
  (function () {
1865
1955
  ko.bindingHandlers = {};
1866
1956
 
1867
- ko.bindingContext = function(dataItem, parentBindingContext) {
1957
+ ko.bindingContext = function(dataItem, parentBindingContext, dataItemAlias) {
1868
1958
  if (parentBindingContext) {
1869
1959
  ko.utils.extend(this, parentBindingContext); // Inherit $root and any custom properties
1870
1960
  this['$parentContext'] = parentBindingContext;
@@ -1874,11 +1964,17 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
1874
1964
  } else {
1875
1965
  this['$parents'] = [];
1876
1966
  this['$root'] = dataItem;
1967
+ // Export 'ko' in the binding context so it will be available in bindings and templates
1968
+ // even if 'ko' isn't exported as a global, such as when using an AMD loader.
1969
+ // See https://github.com/SteveSanderson/knockout/issues/490
1970
+ this['ko'] = ko;
1877
1971
  }
1878
1972
  this['$data'] = dataItem;
1973
+ if (dataItemAlias)
1974
+ this[dataItemAlias] = dataItem;
1879
1975
  }
1880
- ko.bindingContext.prototype['createChildContext'] = function (dataItem) {
1881
- return new ko.bindingContext(dataItem, this);
1976
+ ko.bindingContext.prototype['createChildContext'] = function (dataItem, dataItemAlias) {
1977
+ return new ko.bindingContext(dataItem, this, dataItemAlias);
1882
1978
  };
1883
1979
  ko.bindingContext.prototype['extend'] = function(properties) {
1884
1980
  var clone = ko.utils.extend(new ko.bindingContext(), this);
@@ -1961,7 +2057,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
1961
2057
  ko.storedBindingContextForNode(node, bindingContextInstance);
1962
2058
 
1963
2059
  // Use evaluatedBindings if given, otherwise fall back on asking the bindings provider to give us some bindings
1964
- var evaluatedBindings = (typeof bindings == "function") ? bindings() : bindings;
2060
+ var evaluatedBindings = (typeof bindings == "function") ? bindings(bindingContextInstance, node) : bindings;
1965
2061
  parsedBindings = evaluatedBindings || ko.bindingProvider['instance']['getBindings'](node, bindingContextInstance);
1966
2062
 
1967
2063
  if (parsedBindings) {
@@ -2001,7 +2097,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
2001
2097
  }
2002
2098
  },
2003
2099
  null,
2004
- { 'disposeWhenNodeIsRemoved' : node }
2100
+ { disposeWhenNodeIsRemoved : node }
2005
2101
  );
2006
2102
 
2007
2103
  return {
@@ -2061,10 +2157,128 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
2061
2157
  ko.exportSymbol('contextFor', ko.contextFor);
2062
2158
  ko.exportSymbol('dataFor', ko.dataFor);
2063
2159
  })();
2160
+ var attrHtmlToJavascriptMap = { 'class': 'className', 'for': 'htmlFor' };
2161
+ ko.bindingHandlers['attr'] = {
2162
+ 'update': function(element, valueAccessor, allBindingsAccessor) {
2163
+ var value = ko.utils.unwrapObservable(valueAccessor()) || {};
2164
+ for (var attrName in value) {
2165
+ if (typeof attrName == "string") {
2166
+ var attrValue = ko.utils.unwrapObservable(value[attrName]);
2167
+
2168
+ // To cover cases like "attr: { checked:someProp }", we want to remove the attribute entirely
2169
+ // when someProp is a "no value"-like value (strictly null, false, or undefined)
2170
+ // (because the absence of the "checked" attr is how to mark an element as not checked, etc.)
2171
+ var toRemove = (attrValue === false) || (attrValue === null) || (attrValue === undefined);
2172
+ if (toRemove)
2173
+ element.removeAttribute(attrName);
2174
+
2175
+ // In IE <= 7 and IE8 Quirks Mode, you have to use the Javascript property name instead of the
2176
+ // HTML attribute name for certain attributes. IE8 Standards Mode supports the correct behavior,
2177
+ // but instead of figuring out the mode, we'll just set the attribute through the Javascript
2178
+ // property for IE <= 8.
2179
+ if (ko.utils.ieVersion <= 8 && attrName in attrHtmlToJavascriptMap) {
2180
+ attrName = attrHtmlToJavascriptMap[attrName];
2181
+ if (toRemove)
2182
+ element.removeAttribute(attrName);
2183
+ else
2184
+ element[attrName] = attrValue;
2185
+ } else if (!toRemove) {
2186
+ element.setAttribute(attrName, attrValue.toString());
2187
+ }
2188
+
2189
+ // Treat "name" specially - although you can think of it as an attribute, it also needs
2190
+ // special handling on older versions of IE (https://github.com/SteveSanderson/knockout/pull/333)
2191
+ // Deliberately being case-sensitive here because XHTML would regard "Name" as a different thing
2192
+ // entirely, and there's no strong reason to allow for such casing in HTML.
2193
+ if (attrName === "name") {
2194
+ ko.utils.setElementName(element, toRemove ? "" : attrValue.toString());
2195
+ }
2196
+ }
2197
+ }
2198
+ }
2199
+ };
2200
+ ko.bindingHandlers['checked'] = {
2201
+ 'init': function (element, valueAccessor, allBindingsAccessor) {
2202
+ var updateHandler = function() {
2203
+ var valueToWrite;
2204
+ if (element.type == "checkbox") {
2205
+ valueToWrite = element.checked;
2206
+ } else if ((element.type == "radio") && (element.checked)) {
2207
+ valueToWrite = element.value;
2208
+ } else {
2209
+ return; // "checked" binding only responds to checkboxes and selected radio buttons
2210
+ }
2211
+
2212
+ var modelValue = valueAccessor(), unwrappedValue = ko.utils.unwrapObservable(modelValue);
2213
+ if ((element.type == "checkbox") && (unwrappedValue instanceof Array)) {
2214
+ // For checkboxes bound to an array, we add/remove the checkbox value to that array
2215
+ // This works for both observable and non-observable arrays
2216
+ var existingEntryIndex = ko.utils.arrayIndexOf(unwrappedValue, element.value);
2217
+ if (element.checked && (existingEntryIndex < 0))
2218
+ modelValue.push(element.value);
2219
+ else if ((!element.checked) && (existingEntryIndex >= 0))
2220
+ modelValue.splice(existingEntryIndex, 1);
2221
+ } else {
2222
+ ko.expressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'checked', valueToWrite, true);
2223
+ }
2224
+ };
2225
+ ko.utils.registerEventHandler(element, "click", updateHandler);
2226
+
2227
+ // IE 6 won't allow radio buttons to be selected unless they have a name
2228
+ if ((element.type == "radio") && !element.name)
2229
+ ko.bindingHandlers['uniqueName']['init'](element, function() { return true });
2230
+ },
2231
+ 'update': function (element, valueAccessor) {
2232
+ var value = ko.utils.unwrapObservable(valueAccessor());
2233
+
2234
+ if (element.type == "checkbox") {
2235
+ if (value instanceof Array) {
2236
+ // When bound to an array, the checkbox being checked represents its value being present in that array
2237
+ element.checked = ko.utils.arrayIndexOf(value, element.value) >= 0;
2238
+ } else {
2239
+ // When bound to anything other value (not an array), the checkbox being checked represents the value being trueish
2240
+ element.checked = value;
2241
+ }
2242
+ } else if (element.type == "radio") {
2243
+ element.checked = (element.value == value);
2244
+ }
2245
+ }
2246
+ };
2247
+ var classesWrittenByBindingKey = '__ko__cssValue';
2248
+ ko.bindingHandlers['css'] = {
2249
+ 'update': function (element, valueAccessor) {
2250
+ var value = ko.utils.unwrapObservable(valueAccessor());
2251
+ if (typeof value == "object") {
2252
+ for (var className in value) {
2253
+ var shouldHaveClass = ko.utils.unwrapObservable(value[className]);
2254
+ ko.utils.toggleDomNodeCssClass(element, className, shouldHaveClass);
2255
+ }
2256
+ } else {
2257
+ value = String(value || ''); // Make sure we don't try to store or set a non-string value
2258
+ ko.utils.toggleDomNodeCssClass(element, element[classesWrittenByBindingKey], false);
2259
+ element[classesWrittenByBindingKey] = value;
2260
+ ko.utils.toggleDomNodeCssClass(element, value, true);
2261
+ }
2262
+ }
2263
+ };
2264
+ ko.bindingHandlers['enable'] = {
2265
+ 'update': function (element, valueAccessor) {
2266
+ var value = ko.utils.unwrapObservable(valueAccessor());
2267
+ if (value && element.disabled)
2268
+ element.removeAttribute("disabled");
2269
+ else if ((!value) && (!element.disabled))
2270
+ element.disabled = true;
2271
+ }
2272
+ };
2273
+
2274
+ ko.bindingHandlers['disable'] = {
2275
+ 'update': function (element, valueAccessor) {
2276
+ ko.bindingHandlers['enable']['update'](element, function() { return !ko.utils.unwrapObservable(valueAccessor()) });
2277
+ }
2278
+ };
2064
2279
  // For certain common events (currently just 'click'), allow a simplified data-binding syntax
2065
2280
  // e.g. click:handler instead of the usual full-length event:{click:handler}
2066
- var eventHandlersWithShortcuts = ['click'];
2067
- ko.utils.arrayForEach(eventHandlersWithShortcuts, function(eventName) {
2281
+ function makeEventHandlerShortcut(eventName) {
2068
2282
  ko.bindingHandlers[eventName] = {
2069
2283
  'init': function(element, valueAccessor, allBindingsAccessor, viewModel) {
2070
2284
  var newValueAccessor = function () {
@@ -2075,8 +2289,7 @@ ko.utils.arrayForEach(eventHandlersWithShortcuts, function(eventName) {
2075
2289
  return ko.bindingHandlers['event']['init'].call(this, element, newValueAccessor, allBindingsAccessor, viewModel);
2076
2290
  }
2077
2291
  }
2078
- });
2079
-
2292
+ }
2080
2293
 
2081
2294
  ko.bindingHandlers['event'] = {
2082
2295
  'init' : function (element, valueAccessor, allBindingsAccessor, viewModel) {
@@ -2118,54 +2331,134 @@ ko.bindingHandlers['event'] = {
2118
2331
  }
2119
2332
  }
2120
2333
  };
2334
+ // "foreach: someExpression" is equivalent to "template: { foreach: someExpression }"
2335
+ // "foreach: { data: someExpression, afterAdd: myfn }" is equivalent to "template: { foreach: someExpression, afterAdd: myfn }"
2336
+ ko.bindingHandlers['foreach'] = {
2337
+ makeTemplateValueAccessor: function(valueAccessor) {
2338
+ return function() {
2339
+ var modelValue = valueAccessor(),
2340
+ unwrappedValue = ko.utils.peekObservable(modelValue); // Unwrap without setting a dependency here
2121
2341
 
2122
- ko.bindingHandlers['submit'] = {
2123
- 'init': function (element, valueAccessor, allBindingsAccessor, viewModel) {
2124
- if (typeof valueAccessor() != "function")
2125
- throw new Error("The value for a submit binding must be a function");
2126
- ko.utils.registerEventHandler(element, "submit", function (event) {
2127
- var handlerReturnValue;
2128
- var value = valueAccessor();
2129
- try { handlerReturnValue = value.call(viewModel, element); }
2130
- finally {
2131
- if (handlerReturnValue !== true) { // Normally we want to prevent default action. Developer can override this be explicitly returning true.
2132
- if (event.preventDefault)
2133
- event.preventDefault();
2134
- else
2135
- event.returnValue = false;
2136
- }
2137
- }
2138
- });
2139
- }
2140
- };
2342
+ // If unwrappedValue is the array, pass in the wrapped value on its own
2343
+ // The value will be unwrapped and tracked within the template binding
2344
+ // (See https://github.com/SteveSanderson/knockout/issues/523)
2345
+ if ((!unwrappedValue) || typeof unwrappedValue.length == "number")
2346
+ return { 'foreach': modelValue, 'templateEngine': ko.nativeTemplateEngine.instance };
2141
2347
 
2142
- ko.bindingHandlers['visible'] = {
2143
- 'update': function (element, valueAccessor) {
2144
- var value = ko.utils.unwrapObservable(valueAccessor());
2145
- var isCurrentlyVisible = !(element.style.display == "none");
2146
- if (value && !isCurrentlyVisible)
2147
- element.style.display = "";
2148
- else if ((!value) && isCurrentlyVisible)
2149
- element.style.display = "none";
2348
+ // If unwrappedValue.data is the array, preserve all relevant options and unwrap again value so we get updates
2349
+ ko.utils.unwrapObservable(modelValue);
2350
+ return {
2351
+ 'foreach': unwrappedValue['data'],
2352
+ 'as': unwrappedValue['as'],
2353
+ 'includeDestroyed': unwrappedValue['includeDestroyed'],
2354
+ 'afterAdd': unwrappedValue['afterAdd'],
2355
+ 'beforeRemove': unwrappedValue['beforeRemove'],
2356
+ 'afterRender': unwrappedValue['afterRender'],
2357
+ 'beforeMove': unwrappedValue['beforeMove'],
2358
+ 'afterMove': unwrappedValue['afterMove'],
2359
+ 'templateEngine': ko.nativeTemplateEngine.instance
2360
+ };
2361
+ };
2362
+ },
2363
+ 'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2364
+ return ko.bindingHandlers['template']['init'](element, ko.bindingHandlers['foreach'].makeTemplateValueAccessor(valueAccessor));
2365
+ },
2366
+ 'update': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2367
+ return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['foreach'].makeTemplateValueAccessor(valueAccessor), allBindingsAccessor, viewModel, bindingContext);
2150
2368
  }
2151
- }
2369
+ };
2370
+ ko.expressionRewriting.bindingRewriteValidators['foreach'] = false; // Can't rewrite control flow bindings
2371
+ ko.virtualElements.allowedBindings['foreach'] = true;
2372
+ var hasfocusUpdatingProperty = '__ko_hasfocusUpdating';
2373
+ ko.bindingHandlers['hasfocus'] = {
2374
+ 'init': function(element, valueAccessor, allBindingsAccessor) {
2375
+ var handleElementFocusChange = function(isFocused) {
2376
+ // Where possible, ignore which event was raised and determine focus state using activeElement,
2377
+ // as this avoids phantom focus/blur events raised when changing tabs in modern browsers.
2378
+ // However, not all KO-targeted browsers (Firefox 2) support activeElement. For those browsers,
2379
+ // prevent a loss of focus when changing tabs/windows by setting a flag that prevents hasfocus
2380
+ // from calling 'blur()' on the element when it loses focus.
2381
+ // Discussion at https://github.com/SteveSanderson/knockout/pull/352
2382
+ element[hasfocusUpdatingProperty] = true;
2383
+ var ownerDoc = element.ownerDocument;
2384
+ if ("activeElement" in ownerDoc) {
2385
+ isFocused = (ownerDoc.activeElement === element);
2386
+ }
2387
+ var modelValue = valueAccessor();
2388
+ ko.expressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'hasfocus', isFocused, true);
2389
+ element[hasfocusUpdatingProperty] = false;
2390
+ };
2391
+ var handleElementFocusIn = handleElementFocusChange.bind(null, true);
2392
+ var handleElementFocusOut = handleElementFocusChange.bind(null, false);
2152
2393
 
2153
- ko.bindingHandlers['enable'] = {
2154
- 'update': function (element, valueAccessor) {
2394
+ ko.utils.registerEventHandler(element, "focus", handleElementFocusIn);
2395
+ ko.utils.registerEventHandler(element, "focusin", handleElementFocusIn); // For IE
2396
+ ko.utils.registerEventHandler(element, "blur", handleElementFocusOut);
2397
+ ko.utils.registerEventHandler(element, "focusout", handleElementFocusOut); // For IE
2398
+ },
2399
+ 'update': function(element, valueAccessor) {
2155
2400
  var value = ko.utils.unwrapObservable(valueAccessor());
2156
- if (value && element.disabled)
2157
- element.removeAttribute("disabled");
2158
- else if ((!value) && (!element.disabled))
2159
- element.disabled = true;
2401
+ if (!element[hasfocusUpdatingProperty]) {
2402
+ value ? element.focus() : element.blur();
2403
+ ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, value ? "focusin" : "focusout"]); // For IE, which doesn't reliably fire "focus" or "blur" events synchronously
2404
+ }
2160
2405
  }
2161
2406
  };
2162
-
2163
- ko.bindingHandlers['disable'] = {
2407
+ ko.bindingHandlers['html'] = {
2408
+ 'init': function() {
2409
+ // Prevent binding on the dynamically-injected HTML (as developers are unlikely to expect that, and it has security implications)
2410
+ return { 'controlsDescendantBindings': true };
2411
+ },
2164
2412
  'update': function (element, valueAccessor) {
2165
- ko.bindingHandlers['enable']['update'](element, function() { return !ko.utils.unwrapObservable(valueAccessor()) });
2413
+ // setHtml will unwrap the value if needed
2414
+ ko.utils.setHtml(element, valueAccessor());
2166
2415
  }
2167
2416
  };
2417
+ var withIfDomDataKey = '__ko_withIfBindingData';
2418
+ // Makes a binding like with or if
2419
+ function makeWithIfBinding(bindingKey, isWith, isNot, makeContextCallback) {
2420
+ ko.bindingHandlers[bindingKey] = {
2421
+ 'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2422
+ ko.utils.domData.set(element, withIfDomDataKey, {});
2423
+ return { 'controlsDescendantBindings': true };
2424
+ },
2425
+ 'update': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2426
+ var withIfData = ko.utils.domData.get(element, withIfDomDataKey),
2427
+ dataValue = ko.utils.unwrapObservable(valueAccessor()),
2428
+ shouldDisplay = !isNot !== !dataValue, // equivalent to isNot ? !dataValue : !!dataValue
2429
+ isFirstRender = !withIfData.savedNodes,
2430
+ needsRefresh = isFirstRender || isWith || (shouldDisplay !== withIfData.didDisplayOnLastUpdate);
2431
+
2432
+ if (needsRefresh) {
2433
+ if (isFirstRender) {
2434
+ withIfData.savedNodes = ko.utils.cloneNodes(ko.virtualElements.childNodes(element), true /* shouldCleanNodes */);
2435
+ }
2168
2436
 
2437
+ if (shouldDisplay) {
2438
+ if (!isFirstRender) {
2439
+ ko.virtualElements.setDomNodeChildren(element, ko.utils.cloneNodes(withIfData.savedNodes));
2440
+ }
2441
+ ko.applyBindingsToDescendants(makeContextCallback ? makeContextCallback(bindingContext, dataValue) : bindingContext, element);
2442
+ } else {
2443
+ ko.virtualElements.emptyNode(element);
2444
+ }
2445
+
2446
+ withIfData.didDisplayOnLastUpdate = shouldDisplay;
2447
+ }
2448
+ }
2449
+ };
2450
+ ko.expressionRewriting.bindingRewriteValidators[bindingKey] = false; // Can't rewrite control flow bindings
2451
+ ko.virtualElements.allowedBindings[bindingKey] = true;
2452
+ }
2453
+
2454
+ // Construct the actual binding handlers
2455
+ makeWithIfBinding('if');
2456
+ makeWithIfBinding('ifnot', false /* isWith */, true /* isNot */);
2457
+ makeWithIfBinding('with', true /* isWith */, false /* isNot */,
2458
+ function(bindingContext, dataValue) {
2459
+ return bindingContext['createChildContext'](dataValue);
2460
+ }
2461
+ );
2169
2462
  function ensureDropdownSelectionIsConsistentWithModelValue(element, modelValue, preferModelValue) {
2170
2463
  if (preferModelValue) {
2171
2464
  if (modelValue !== ko.selectExtensions.readValue(element))
@@ -2176,82 +2469,7 @@ function ensureDropdownSelectionIsConsistentWithModelValue(element, modelValue,
2176
2469
  // If they aren't equal, either we prefer the dropdown value, or the model value couldn't be represented, so either way,
2177
2470
  // change the model value to match the dropdown.
2178
2471
  if (modelValue !== ko.selectExtensions.readValue(element))
2179
- ko.utils.triggerEvent(element, "change");
2180
- };
2181
-
2182
- ko.bindingHandlers['value'] = {
2183
- 'init': function (element, valueAccessor, allBindingsAccessor) {
2184
- // Always catch "change" event; possibly other events too if asked
2185
- var eventsToCatch = ["change"];
2186
- var requestedEventsToCatch = allBindingsAccessor()["valueUpdate"];
2187
- if (requestedEventsToCatch) {
2188
- if (typeof requestedEventsToCatch == "string") // Allow both individual event names, and arrays of event names
2189
- requestedEventsToCatch = [requestedEventsToCatch];
2190
- ko.utils.arrayPushAll(eventsToCatch, requestedEventsToCatch);
2191
- eventsToCatch = ko.utils.arrayGetDistinctValues(eventsToCatch);
2192
- }
2193
-
2194
- var valueUpdateHandler = function() {
2195
- var modelValue = valueAccessor();
2196
- var elementValue = ko.selectExtensions.readValue(element);
2197
- ko.jsonExpressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'value', elementValue, /* checkIfDifferent: */ true);
2198
- }
2199
-
2200
- // Workaround for https://github.com/SteveSanderson/knockout/issues/122
2201
- // IE doesn't fire "change" events on textboxes if the user selects a value from its autocomplete list
2202
- var ieAutoCompleteHackNeeded = ko.utils.ieVersion && element.tagName.toLowerCase() == "input" && element.type == "text"
2203
- && element.autocomplete != "off" && (!element.form || element.form.autocomplete != "off");
2204
- if (ieAutoCompleteHackNeeded && ko.utils.arrayIndexOf(eventsToCatch, "propertychange") == -1) {
2205
- var propertyChangedFired = false;
2206
- ko.utils.registerEventHandler(element, "propertychange", function () { propertyChangedFired = true });
2207
- ko.utils.registerEventHandler(element, "blur", function() {
2208
- if (propertyChangedFired) {
2209
- propertyChangedFired = false;
2210
- valueUpdateHandler();
2211
- }
2212
- });
2213
- }
2214
-
2215
- ko.utils.arrayForEach(eventsToCatch, function(eventName) {
2216
- // The syntax "after<eventname>" means "run the handler asynchronously after the event"
2217
- // This is useful, for example, to catch "keydown" events after the browser has updated the control
2218
- // (otherwise, ko.selectExtensions.readValue(this) will receive the control's value *before* the key event)
2219
- var handler = valueUpdateHandler;
2220
- if (ko.utils.stringStartsWith(eventName, "after")) {
2221
- handler = function() { setTimeout(valueUpdateHandler, 0) };
2222
- eventName = eventName.substring("after".length);
2223
- }
2224
- ko.utils.registerEventHandler(element, eventName, handler);
2225
- });
2226
- },
2227
- 'update': function (element, valueAccessor) {
2228
- var valueIsSelectOption = ko.utils.tagNameLower(element) === "select";
2229
- var newValue = ko.utils.unwrapObservable(valueAccessor());
2230
- var elementValue = ko.selectExtensions.readValue(element);
2231
- var valueHasChanged = (newValue != elementValue);
2232
-
2233
- // 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).
2234
- // 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.
2235
- if ((newValue === 0) && (elementValue !== 0) && (elementValue !== "0"))
2236
- valueHasChanged = true;
2237
-
2238
- if (valueHasChanged) {
2239
- var applyValueAction = function () { ko.selectExtensions.writeValue(element, newValue); };
2240
- applyValueAction();
2241
-
2242
- // Workaround for IE6 bug: It won't reliably apply values to SELECT nodes during the same execution thread
2243
- // right after you've changed the set of OPTION nodes on it. So for that node type, we'll schedule a second thread
2244
- // to apply the value as well.
2245
- var alsoApplyAsynchronously = valueIsSelectOption;
2246
- if (alsoApplyAsynchronously)
2247
- setTimeout(applyValueAction, 0);
2248
- }
2249
-
2250
- // If you try to set a model value that can't be represented in an already-populated dropdown, reject that change,
2251
- // because you're not allowed to have a model value that disagrees with a visible UI selection.
2252
- if (valueIsSelectOption && (element.length > 0))
2253
- ensureDropdownSelectionIsConsistentWithModelValue(element, newValue, /* preferModelValue */ false);
2254
- }
2472
+ ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, "change"]);
2255
2473
  };
2256
2474
 
2257
2475
  ko.bindingHandlers['options'] = {
@@ -2278,7 +2496,9 @@ ko.bindingHandlers['options'] = {
2278
2496
  }
2279
2497
 
2280
2498
  if (value) {
2281
- var allBindings = allBindingsAccessor();
2499
+ var allBindings = allBindingsAccessor(),
2500
+ includeDestroyed = allBindings['optionsIncludeDestroyed'];
2501
+
2282
2502
  if (typeof value.length != "number")
2283
2503
  value = [value];
2284
2504
  if (allBindings['optionsCaption']) {
@@ -2287,26 +2507,31 @@ ko.bindingHandlers['options'] = {
2287
2507
  ko.selectExtensions.writeValue(option, undefined);
2288
2508
  element.appendChild(option);
2289
2509
  }
2510
+
2290
2511
  for (var i = 0, j = value.length; i < j; i++) {
2512
+ // Skip destroyed items
2513
+ var arrayEntry = value[i];
2514
+ if (arrayEntry && arrayEntry['_destroy'] && !includeDestroyed)
2515
+ continue;
2516
+
2291
2517
  var option = document.createElement("option");
2292
2518
 
2519
+ function applyToObject(object, predicate, defaultValue) {
2520
+ var predicateType = typeof predicate;
2521
+ if (predicateType == "function") // Given a function; run it against the data value
2522
+ return predicate(object);
2523
+ else if (predicateType == "string") // Given a string; treat it as a property name on the data value
2524
+ return object[predicate];
2525
+ else // Given no optionsText arg; use the data value itself
2526
+ return defaultValue;
2527
+ }
2528
+
2293
2529
  // Apply a value to the option element
2294
- var optionValue = typeof allBindings['optionsValue'] == "string" ? value[i][allBindings['optionsValue']] : value[i];
2295
- optionValue = ko.utils.unwrapObservable(optionValue);
2296
- ko.selectExtensions.writeValue(option, optionValue);
2530
+ var optionValue = applyToObject(arrayEntry, allBindings['optionsValue'], arrayEntry);
2531
+ ko.selectExtensions.writeValue(option, ko.utils.unwrapObservable(optionValue));
2297
2532
 
2298
2533
  // Apply some text to the option element
2299
- var optionsTextValue = allBindings['optionsText'];
2300
- var optionText;
2301
- if (typeof optionsTextValue == "function")
2302
- optionText = optionsTextValue(value[i]); // Given a function; run it against the data value
2303
- else if (typeof optionsTextValue == "string")
2304
- optionText = value[i][optionsTextValue]; // Given a string; treat it as a property name on the data value
2305
- else
2306
- optionText = optionValue; // Given no optionsText arg; use the data value itself
2307
- if ((optionText === null) || (optionText === undefined))
2308
- optionText = "";
2309
-
2534
+ var optionText = applyToObject(arrayEntry, allBindings['optionsText'], optionValue);
2310
2535
  ko.utils.setTextContent(option, optionText);
2311
2536
 
2312
2537
  element.appendChild(option);
@@ -2329,7 +2554,7 @@ ko.bindingHandlers['options'] = {
2329
2554
  // Ensure consistency between model value and selected option.
2330
2555
  // If the dropdown is being populated for the first time here (or was otherwise previously empty),
2331
2556
  // the dropdown selection state is meaningless, so we preserve the model value.
2332
- ensureDropdownSelectionIsConsistentWithModelValue(element, ko.utils.unwrapObservable(allBindings['value']), /* preferModelValue */ true);
2557
+ ensureDropdownSelectionIsConsistentWithModelValue(element, ko.utils.peekObservable(allBindings['value']), /* preferModelValue */ true);
2333
2558
  }
2334
2559
 
2335
2560
  // Workaround for IE9 bug
@@ -2338,27 +2563,15 @@ ko.bindingHandlers['options'] = {
2338
2563
  }
2339
2564
  };
2340
2565
  ko.bindingHandlers['options'].optionValueDomDataKey = '__ko.optionValueDomData__';
2341
-
2342
2566
  ko.bindingHandlers['selectedOptions'] = {
2343
- getSelectedValuesFromSelectNode: function (selectNode) {
2344
- var result = [];
2345
- var nodes = selectNode.childNodes;
2346
- for (var i = 0, j = nodes.length; i < j; i++) {
2347
- var node = nodes[i], tagName = ko.utils.tagNameLower(node);
2348
- if (tagName == "option" && node.selected)
2349
- result.push(ko.selectExtensions.readValue(node));
2350
- else if (tagName == "optgroup") {
2351
- var selectedValuesFromOptGroup = ko.bindingHandlers['selectedOptions'].getSelectedValuesFromSelectNode(node);
2352
- Array.prototype.splice.apply(result, [result.length, 0].concat(selectedValuesFromOptGroup)); // Add new entries to existing 'result' instance
2353
- }
2354
- }
2355
- return result;
2356
- },
2357
2567
  'init': function (element, valueAccessor, allBindingsAccessor) {
2358
2568
  ko.utils.registerEventHandler(element, "change", function () {
2359
- var value = valueAccessor();
2360
- var valueToWrite = ko.bindingHandlers['selectedOptions'].getSelectedValuesFromSelectNode(this);
2361
- ko.jsonExpressionRewriting.writeValueToProperty(value, allBindingsAccessor, 'value', valueToWrite);
2569
+ var value = valueAccessor(), valueToWrite = [];
2570
+ ko.utils.arrayForEach(element.getElementsByTagName("option"), function(node) {
2571
+ if (node.selected)
2572
+ valueToWrite.push(ko.selectExtensions.readValue(node));
2573
+ });
2574
+ ko.expressionRewriting.writeValueToProperty(value, allBindingsAccessor, 'value', valueToWrite);
2362
2575
  });
2363
2576
  },
2364
2577
  'update': function (element, valueAccessor) {
@@ -2367,45 +2580,13 @@ ko.bindingHandlers['selectedOptions'] = {
2367
2580
 
2368
2581
  var newValue = ko.utils.unwrapObservable(valueAccessor());
2369
2582
  if (newValue && typeof newValue.length == "number") {
2370
- var nodes = element.childNodes;
2371
- for (var i = 0, j = nodes.length; i < j; i++) {
2372
- var node = nodes[i];
2373
- if (ko.utils.tagNameLower(node) === "option")
2374
- ko.utils.setOptionNodeSelectionState(node, ko.utils.arrayIndexOf(newValue, ko.selectExtensions.readValue(node)) >= 0);
2375
- }
2376
- }
2377
- }
2378
- };
2379
-
2380
- ko.bindingHandlers['text'] = {
2381
- 'update': function (element, valueAccessor) {
2382
- ko.utils.setTextContent(element, valueAccessor());
2383
- }
2384
- };
2385
-
2386
- ko.bindingHandlers['html'] = {
2387
- 'init': function() {
2388
- // Prevent binding on the dynamically-injected HTML (as developers are unlikely to expect that, and it has security implications)
2389
- return { 'controlsDescendantBindings': true };
2390
- },
2391
- 'update': function (element, valueAccessor) {
2392
- var value = ko.utils.unwrapObservable(valueAccessor());
2393
- ko.utils.setHtml(element, value);
2394
- }
2395
- };
2396
-
2397
- ko.bindingHandlers['css'] = {
2398
- 'update': function (element, valueAccessor) {
2399
- var value = ko.utils.unwrapObservable(valueAccessor() || {});
2400
- for (var className in value) {
2401
- if (typeof className == "string") {
2402
- var shouldHaveClass = ko.utils.unwrapObservable(value[className]);
2403
- ko.utils.toggleDomNodeCssClass(element, className, shouldHaveClass);
2404
- }
2583
+ ko.utils.arrayForEach(element.getElementsByTagName("option"), function(node) {
2584
+ var isSelected = ko.utils.arrayIndexOf(newValue, ko.selectExtensions.readValue(node)) >= 0;
2585
+ ko.utils.setOptionNodeSelectionState(node, isSelected);
2586
+ });
2405
2587
  }
2406
2588
  }
2407
2589
  };
2408
-
2409
2590
  ko.bindingHandlers['style'] = {
2410
2591
  'update': function (element, valueAccessor) {
2411
2592
  var value = ko.utils.unwrapObservable(valueAccessor() || {});
@@ -2417,197 +2598,126 @@ ko.bindingHandlers['style'] = {
2417
2598
  }
2418
2599
  }
2419
2600
  };
2420
-
2601
+ ko.bindingHandlers['submit'] = {
2602
+ 'init': function (element, valueAccessor, allBindingsAccessor, viewModel) {
2603
+ if (typeof valueAccessor() != "function")
2604
+ throw new Error("The value for a submit binding must be a function");
2605
+ ko.utils.registerEventHandler(element, "submit", function (event) {
2606
+ var handlerReturnValue;
2607
+ var value = valueAccessor();
2608
+ try { handlerReturnValue = value.call(viewModel, element); }
2609
+ finally {
2610
+ if (handlerReturnValue !== true) { // Normally we want to prevent default action. Developer can override this be explicitly returning true.
2611
+ if (event.preventDefault)
2612
+ event.preventDefault();
2613
+ else
2614
+ event.returnValue = false;
2615
+ }
2616
+ }
2617
+ });
2618
+ }
2619
+ };
2620
+ ko.bindingHandlers['text'] = {
2621
+ 'update': function (element, valueAccessor) {
2622
+ ko.utils.setTextContent(element, valueAccessor());
2623
+ }
2624
+ };
2625
+ ko.virtualElements.allowedBindings['text'] = true;
2421
2626
  ko.bindingHandlers['uniqueName'] = {
2422
2627
  'init': function (element, valueAccessor) {
2423
2628
  if (valueAccessor()) {
2424
- element.name = "ko_unique_" + (++ko.bindingHandlers['uniqueName'].currentIndex);
2425
-
2426
- // Workaround IE 6/7 issue
2427
- // - https://github.com/SteveSanderson/knockout/issues/197
2428
- // - http://www.matts411.com/post/setting_the_name_attribute_in_ie_dom/
2429
- if (ko.utils.isIe6 || ko.utils.isIe7)
2430
- element.mergeAttributes(document.createElement("<input name='" + element.name + "'/>"), false);
2629
+ var name = "ko_unique_" + (++ko.bindingHandlers['uniqueName'].currentIndex);
2630
+ ko.utils.setElementName(element, name);
2431
2631
  }
2432
2632
  }
2433
2633
  };
2434
2634
  ko.bindingHandlers['uniqueName'].currentIndex = 0;
2435
-
2436
- ko.bindingHandlers['checked'] = {
2635
+ ko.bindingHandlers['value'] = {
2437
2636
  'init': function (element, valueAccessor, allBindingsAccessor) {
2438
- var updateHandler = function() {
2439
- var valueToWrite;
2440
- if (element.type == "checkbox") {
2441
- valueToWrite = element.checked;
2442
- } else if ((element.type == "radio") && (element.checked)) {
2443
- valueToWrite = element.value;
2444
- } else {
2445
- return; // "checked" binding only responds to checkboxes and selected radio buttons
2446
- }
2637
+ // Always catch "change" event; possibly other events too if asked
2638
+ var eventsToCatch = ["change"];
2639
+ var requestedEventsToCatch = allBindingsAccessor()["valueUpdate"];
2640
+ var propertyChangedFired = false;
2641
+ if (requestedEventsToCatch) {
2642
+ if (typeof requestedEventsToCatch == "string") // Allow both individual event names, and arrays of event names
2643
+ requestedEventsToCatch = [requestedEventsToCatch];
2644
+ ko.utils.arrayPushAll(eventsToCatch, requestedEventsToCatch);
2645
+ eventsToCatch = ko.utils.arrayGetDistinctValues(eventsToCatch);
2646
+ }
2447
2647
 
2648
+ var valueUpdateHandler = function() {
2649
+ propertyChangedFired = false;
2448
2650
  var modelValue = valueAccessor();
2449
- if ((element.type == "checkbox") && (ko.utils.unwrapObservable(modelValue) instanceof Array)) {
2450
- // For checkboxes bound to an array, we add/remove the checkbox value to that array
2451
- // This works for both observable and non-observable arrays
2452
- var existingEntryIndex = ko.utils.arrayIndexOf(ko.utils.unwrapObservable(modelValue), element.value);
2453
- if (element.checked && (existingEntryIndex < 0))
2454
- modelValue.push(element.value);
2455
- else if ((!element.checked) && (existingEntryIndex >= 0))
2456
- modelValue.splice(existingEntryIndex, 1);
2457
- } else {
2458
- ko.jsonExpressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'checked', valueToWrite, true);
2459
- }
2460
- };
2461
- ko.utils.registerEventHandler(element, "click", updateHandler);
2462
-
2463
- // IE 6 won't allow radio buttons to be selected unless they have a name
2464
- if ((element.type == "radio") && !element.name)
2465
- ko.bindingHandlers['uniqueName']['init'](element, function() { return true });
2466
- },
2467
- 'update': function (element, valueAccessor) {
2468
- var value = ko.utils.unwrapObservable(valueAccessor());
2469
-
2470
- if (element.type == "checkbox") {
2471
- if (value instanceof Array) {
2472
- // When bound to an array, the checkbox being checked represents its value being present in that array
2473
- element.checked = ko.utils.arrayIndexOf(value, element.value) >= 0;
2474
- } else {
2475
- // When bound to anything other value (not an array), the checkbox being checked represents the value being trueish
2476
- element.checked = value;
2477
- }
2478
- } else if (element.type == "radio") {
2479
- element.checked = (element.value == value);
2651
+ var elementValue = ko.selectExtensions.readValue(element);
2652
+ ko.expressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'value', elementValue);
2480
2653
  }
2481
- }
2482
- };
2483
-
2484
- var attrHtmlToJavascriptMap = { 'class': 'className', 'for': 'htmlFor' };
2485
- ko.bindingHandlers['attr'] = {
2486
- 'update': function(element, valueAccessor, allBindingsAccessor) {
2487
- var value = ko.utils.unwrapObservable(valueAccessor()) || {};
2488
- for (var attrName in value) {
2489
- if (typeof attrName == "string") {
2490
- var attrValue = ko.utils.unwrapObservable(value[attrName]);
2491
-
2492
- // To cover cases like "attr: { checked:someProp }", we want to remove the attribute entirely
2493
- // when someProp is a "no value"-like value (strictly null, false, or undefined)
2494
- // (because the absence of the "checked" attr is how to mark an element as not checked, etc.)
2495
- var toRemove = (attrValue === false) || (attrValue === null) || (attrValue === undefined);
2496
- if (toRemove)
2497
- element.removeAttribute(attrName);
2498
2654
 
2499
- // In IE <= 7 and IE8 Quirks Mode, you have to use the Javascript property name instead of the
2500
- // HTML attribute name for certain attributes. IE8 Standards Mode supports the correct behavior,
2501
- // but instead of figuring out the mode, we'll just set the attribute through the Javascript
2502
- // property for IE <= 8.
2503
- if (ko.utils.ieVersion <= 8 && attrName in attrHtmlToJavascriptMap) {
2504
- attrName = attrHtmlToJavascriptMap[attrName];
2505
- if (toRemove)
2506
- element.removeAttribute(attrName);
2507
- else
2508
- element[attrName] = attrValue;
2509
- } else if (!toRemove) {
2510
- element.setAttribute(attrName, attrValue.toString());
2655
+ // Workaround for https://github.com/SteveSanderson/knockout/issues/122
2656
+ // IE doesn't fire "change" events on textboxes if the user selects a value from its autocomplete list
2657
+ var ieAutoCompleteHackNeeded = ko.utils.ieVersion && element.tagName.toLowerCase() == "input" && element.type == "text"
2658
+ && element.autocomplete != "off" && (!element.form || element.form.autocomplete != "off");
2659
+ if (ieAutoCompleteHackNeeded && ko.utils.arrayIndexOf(eventsToCatch, "propertychange") == -1) {
2660
+ ko.utils.registerEventHandler(element, "propertychange", function () { propertyChangedFired = true });
2661
+ ko.utils.registerEventHandler(element, "blur", function() {
2662
+ if (propertyChangedFired) {
2663
+ valueUpdateHandler();
2511
2664
  }
2512
- }
2665
+ });
2513
2666
  }
2514
- }
2515
- };
2516
2667
 
2517
- ko.bindingHandlers['hasfocus'] = {
2518
- 'init': function(element, valueAccessor, allBindingsAccessor) {
2519
- var writeValue = function(valueToWrite) {
2520
- var modelValue = valueAccessor();
2521
- ko.jsonExpressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'hasfocus', valueToWrite, true);
2522
- };
2523
- ko.utils.registerEventHandler(element, "focus", function() { writeValue(true) });
2524
- ko.utils.registerEventHandler(element, "focusin", function() { writeValue(true) }); // For IE
2525
- ko.utils.registerEventHandler(element, "blur", function() { writeValue(false) });
2526
- ko.utils.registerEventHandler(element, "focusout", function() { writeValue(false) }); // For IE
2668
+ ko.utils.arrayForEach(eventsToCatch, function(eventName) {
2669
+ // The syntax "after<eventname>" means "run the handler asynchronously after the event"
2670
+ // This is useful, for example, to catch "keydown" events after the browser has updated the control
2671
+ // (otherwise, ko.selectExtensions.readValue(this) will receive the control's value *before* the key event)
2672
+ var handler = valueUpdateHandler;
2673
+ if (ko.utils.stringStartsWith(eventName, "after")) {
2674
+ handler = function() { setTimeout(valueUpdateHandler, 0) };
2675
+ eventName = eventName.substring("after".length);
2676
+ }
2677
+ ko.utils.registerEventHandler(element, eventName, handler);
2678
+ });
2527
2679
  },
2528
- 'update': function(element, valueAccessor) {
2529
- var value = ko.utils.unwrapObservable(valueAccessor());
2530
- value ? element.focus() : element.blur();
2531
- ko.utils.triggerEvent(element, value ? "focusin" : "focusout"); // For IE, which doesn't reliably fire "focus" or "blur" events synchronously
2532
- }
2533
- };
2680
+ 'update': function (element, valueAccessor) {
2681
+ var valueIsSelectOption = ko.utils.tagNameLower(element) === "select";
2682
+ var newValue = ko.utils.unwrapObservable(valueAccessor());
2683
+ var elementValue = ko.selectExtensions.readValue(element);
2684
+ var valueHasChanged = (newValue != elementValue);
2534
2685
 
2535
- // "with: someExpression" is equivalent to "template: { if: someExpression, data: someExpression }"
2536
- ko.bindingHandlers['with'] = {
2537
- makeTemplateValueAccessor: function(valueAccessor) {
2538
- return function() { var value = valueAccessor(); return { 'if': value, 'data': value, 'templateEngine': ko.nativeTemplateEngine.instance } };
2539
- },
2540
- 'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2541
- return ko.bindingHandlers['template']['init'](element, ko.bindingHandlers['with'].makeTemplateValueAccessor(valueAccessor));
2542
- },
2543
- 'update': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2544
- return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['with'].makeTemplateValueAccessor(valueAccessor), allBindingsAccessor, viewModel, bindingContext);
2545
- }
2546
- };
2547
- ko.jsonExpressionRewriting.bindingRewriteValidators['with'] = false; // Can't rewrite control flow bindings
2548
- ko.virtualElements.allowedBindings['with'] = true;
2686
+ // 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).
2687
+ // 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.
2688
+ if ((newValue === 0) && (elementValue !== 0) && (elementValue !== "0"))
2689
+ valueHasChanged = true;
2549
2690
 
2550
- // "if: someExpression" is equivalent to "template: { if: someExpression }"
2551
- ko.bindingHandlers['if'] = {
2552
- makeTemplateValueAccessor: function(valueAccessor) {
2553
- return function() { return { 'if': valueAccessor(), 'templateEngine': ko.nativeTemplateEngine.instance } };
2554
- },
2555
- 'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2556
- return ko.bindingHandlers['template']['init'](element, ko.bindingHandlers['if'].makeTemplateValueAccessor(valueAccessor));
2557
- },
2558
- 'update': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2559
- return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['if'].makeTemplateValueAccessor(valueAccessor), allBindingsAccessor, viewModel, bindingContext);
2560
- }
2561
- };
2562
- ko.jsonExpressionRewriting.bindingRewriteValidators['if'] = false; // Can't rewrite control flow bindings
2563
- ko.virtualElements.allowedBindings['if'] = true;
2691
+ if (valueHasChanged) {
2692
+ var applyValueAction = function () { ko.selectExtensions.writeValue(element, newValue); };
2693
+ applyValueAction();
2564
2694
 
2565
- // "ifnot: someExpression" is equivalent to "template: { ifnot: someExpression }"
2566
- ko.bindingHandlers['ifnot'] = {
2567
- makeTemplateValueAccessor: function(valueAccessor) {
2568
- return function() { return { 'ifnot': valueAccessor(), 'templateEngine': ko.nativeTemplateEngine.instance } };
2569
- },
2570
- 'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2571
- return ko.bindingHandlers['template']['init'](element, ko.bindingHandlers['ifnot'].makeTemplateValueAccessor(valueAccessor));
2572
- },
2573
- 'update': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2574
- return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['ifnot'].makeTemplateValueAccessor(valueAccessor), allBindingsAccessor, viewModel, bindingContext);
2695
+ // Workaround for IE6 bug: It won't reliably apply values to SELECT nodes during the same execution thread
2696
+ // right after you've changed the set of OPTION nodes on it. So for that node type, we'll schedule a second thread
2697
+ // to apply the value as well.
2698
+ var alsoApplyAsynchronously = valueIsSelectOption;
2699
+ if (alsoApplyAsynchronously)
2700
+ setTimeout(applyValueAction, 0);
2701
+ }
2702
+
2703
+ // If you try to set a model value that can't be represented in an already-populated dropdown, reject that change,
2704
+ // because you're not allowed to have a model value that disagrees with a visible UI selection.
2705
+ if (valueIsSelectOption && (element.length > 0))
2706
+ ensureDropdownSelectionIsConsistentWithModelValue(element, newValue, /* preferModelValue */ false);
2575
2707
  }
2576
2708
  };
2577
- ko.jsonExpressionRewriting.bindingRewriteValidators['ifnot'] = false; // Can't rewrite control flow bindings
2578
- ko.virtualElements.allowedBindings['ifnot'] = true;
2579
-
2580
- // "foreach: someExpression" is equivalent to "template: { foreach: someExpression }"
2581
- // "foreach: { data: someExpression, afterAdd: myfn }" is equivalent to "template: { foreach: someExpression, afterAdd: myfn }"
2582
- ko.bindingHandlers['foreach'] = {
2583
- makeTemplateValueAccessor: function(valueAccessor) {
2584
- return function() {
2585
- var bindingValue = ko.utils.unwrapObservable(valueAccessor());
2586
-
2587
- // If bindingValue is the array, just pass it on its own
2588
- if ((!bindingValue) || typeof bindingValue.length == "number")
2589
- return { 'foreach': bindingValue, 'templateEngine': ko.nativeTemplateEngine.instance };
2590
-
2591
- // If bindingValue.data is the array, preserve all relevant options
2592
- return {
2593
- 'foreach': bindingValue['data'],
2594
- 'includeDestroyed': bindingValue['includeDestroyed'],
2595
- 'afterAdd': bindingValue['afterAdd'],
2596
- 'beforeRemove': bindingValue['beforeRemove'],
2597
- 'afterRender': bindingValue['afterRender'],
2598
- 'templateEngine': ko.nativeTemplateEngine.instance
2599
- };
2600
- };
2601
- },
2602
- 'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2603
- return ko.bindingHandlers['template']['init'](element, ko.bindingHandlers['foreach'].makeTemplateValueAccessor(valueAccessor));
2604
- },
2605
- 'update': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
2606
- return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['foreach'].makeTemplateValueAccessor(valueAccessor), allBindingsAccessor, viewModel, bindingContext);
2709
+ ko.bindingHandlers['visible'] = {
2710
+ 'update': function (element, valueAccessor) {
2711
+ var value = ko.utils.unwrapObservable(valueAccessor());
2712
+ var isCurrentlyVisible = !(element.style.display == "none");
2713
+ if (value && !isCurrentlyVisible)
2714
+ element.style.display = "";
2715
+ else if ((!value) && isCurrentlyVisible)
2716
+ element.style.display = "none";
2607
2717
  }
2608
2718
  };
2609
- ko.jsonExpressionRewriting.bindingRewriteValidators['foreach'] = false; // Can't rewrite control flow bindings
2610
- ko.virtualElements.allowedBindings['foreach'] = true;
2719
+ // 'click' is just a shorthand for the usual full-length event:{click:handler}
2720
+ makeEventHandlerShortcut('click');
2611
2721
  // If you want to make a custom template engine,
2612
2722
  //
2613
2723
  // [1] Inherit from this class (like ko.nativeTemplateEngine does)
@@ -2668,12 +2778,6 @@ ko.templateEngine.prototype['isTemplateRewritten'] = function (template, templat
2668
2778
  // Skip rewriting if requested
2669
2779
  if (this['allowTemplateRewriting'] === false)
2670
2780
  return true;
2671
-
2672
- // Perf optimisation - see below
2673
- var templateIsInExternalDocument = templateDocument && templateDocument != document;
2674
- if (!templateIsInExternalDocument && this.knownRewrittenTemplates && this.knownRewrittenTemplates[template])
2675
- return true;
2676
-
2677
2781
  return this['makeTemplateSource'](template, templateDocument)['data']("isRewritten");
2678
2782
  };
2679
2783
 
@@ -2682,19 +2786,6 @@ ko.templateEngine.prototype['rewriteTemplate'] = function (template, rewriterCal
2682
2786
  var rewritten = rewriterCallback(templateSource['text']());
2683
2787
  templateSource['text'](rewritten);
2684
2788
  templateSource['data']("isRewritten", true);
2685
-
2686
- // Perf optimisation - for named templates, track which ones have been rewritten so we can
2687
- // answer 'isTemplateRewritten' *without* having to use getElementById (which is slow on IE < 8)
2688
- //
2689
- // Note that we only cache the status for templates in the main document, because caching on a per-doc
2690
- // basis complicates the implementation excessively. In a future version of KO, we will likely remove
2691
- // this 'isRewritten' cache entirely anyway, because the benefit is extremely minor and only applies
2692
- // to rewritable templates, which are pretty much deprecated since KO 2.0.
2693
- var templateIsInExternalDocument = templateDocument && templateDocument != document;
2694
- if (!templateIsInExternalDocument && typeof template == "string") {
2695
- this.knownRewrittenTemplates = this.knownRewrittenTemplates || {};
2696
- this.knownRewrittenTemplates[template] = true;
2697
- }
2698
2789
  };
2699
2790
 
2700
2791
  ko.exportSymbol('templateEngine', ko.templateEngine);
@@ -2704,7 +2795,7 @@ ko.templateRewriting = (function () {
2704
2795
  var memoizeVirtualContainerBindingSyntaxRegex = /<!--\s*ko\b\s*([\s\S]*?)\s*-->/g;
2705
2796
 
2706
2797
  function validateDataBindValuesForRewriting(keyValueArray) {
2707
- var allValidators = ko.jsonExpressionRewriting.bindingRewriteValidators;
2798
+ var allValidators = ko.expressionRewriting.bindingRewriteValidators;
2708
2799
  for (var i = 0; i < keyValueArray.length; i++) {
2709
2800
  var key = keyValueArray[i]['key'];
2710
2801
  if (allValidators.hasOwnProperty(key)) {
@@ -2722,16 +2813,15 @@ ko.templateRewriting = (function () {
2722
2813
  }
2723
2814
 
2724
2815
  function constructMemoizedTagReplacement(dataBindAttributeValue, tagToRetain, templateEngine) {
2725
- var dataBindKeyValueArray = ko.jsonExpressionRewriting.parseObjectLiteral(dataBindAttributeValue);
2816
+ var dataBindKeyValueArray = ko.expressionRewriting.parseObjectLiteral(dataBindAttributeValue);
2726
2817
  validateDataBindValuesForRewriting(dataBindKeyValueArray);
2727
- var rewrittenDataBindAttributeValue = ko.jsonExpressionRewriting.insertPropertyAccessorsIntoJson(dataBindKeyValueArray);
2818
+ var rewrittenDataBindAttributeValue = ko.expressionRewriting.preProcessBindings(dataBindKeyValueArray);
2728
2819
 
2729
2820
  // For no obvious reason, Opera fails to evaluate rewrittenDataBindAttributeValue unless it's wrapped in an additional
2730
2821
  // anonymous function, even though Opera's built-in debugger can evaluate it anyway. No other browser requires this
2731
2822
  // extra indirection.
2732
- var applyBindingsToNextSiblingScript = "ko.templateRewriting.applyMemoizedBindingsToNextSibling(function() { \
2733
- return (function() { return { " + rewrittenDataBindAttributeValue + " } })() \
2734
- })";
2823
+ var applyBindingsToNextSiblingScript =
2824
+ "ko.__tr_ambtns(function($context,$element){return(function(){return{ " + rewrittenDataBindAttributeValue + " } })()})";
2735
2825
  return templateEngine['createJavaScriptEvaluatorBlock'](applyBindingsToNextSiblingScript) + tagToRetain;
2736
2826
  }
2737
2827
 
@@ -2760,8 +2850,9 @@ ko.templateRewriting = (function () {
2760
2850
  }
2761
2851
  })();
2762
2852
 
2763
- ko.exportSymbol('templateRewriting', ko.templateRewriting);
2764
- ko.exportSymbol('templateRewriting.applyMemoizedBindingsToNextSibling', ko.templateRewriting.applyMemoizedBindingsToNextSibling); // Exported only because it has to be referenced by string lookup from within rewritten template
2853
+
2854
+ // Exported only because it has to be referenced by string lookup from within rewritten template
2855
+ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextSibling);
2765
2856
  (function() {
2766
2857
  // A template source represents a read/write way of accessing a template. This is to eliminate the need for template loading/saving
2767
2858
  // logic to be duplicated in every template engine (and means they can all work with anonymous templates, etc.)
@@ -2929,7 +3020,7 @@ ko.exportSymbol('templateRewriting.applyMemoizedBindingsToNextSibling', ko.templ
2929
3020
  if (haveAddedNodesToParent) {
2930
3021
  activateBindingsOnContinuousNodeArray(renderedNodesArray, bindingContext);
2931
3022
  if (options['afterRender'])
2932
- options['afterRender'](renderedNodesArray, bindingContext['$data']);
3023
+ ko.dependencyDetection.ignore(options['afterRender'], null, [renderedNodesArray, bindingContext['$data']]);
2933
3024
  }
2934
3025
 
2935
3026
  return renderedNodesArray;
@@ -2955,7 +3046,7 @@ ko.exportSymbol('templateRewriting.applyMemoizedBindingsToNextSibling', ko.templ
2955
3046
  : new ko.bindingContext(ko.utils.unwrapObservable(dataOrBindingContext));
2956
3047
 
2957
3048
  // Support selecting template as a function of the data being rendered
2958
- var templateName = typeof(template) == 'function' ? template(bindingContext['$data']) : template;
3049
+ var templateName = typeof(template) == 'function' ? template(bindingContext['$data'], bindingContext) : template;
2959
3050
 
2960
3051
  var renderedNodesArray = executeTemplate(targetNodeOrNodeArray, renderMode, templateName, bindingContext, options);
2961
3052
  if (renderMode == "replaceNode") {
@@ -2964,7 +3055,7 @@ ko.exportSymbol('templateRewriting.applyMemoizedBindingsToNextSibling', ko.templ
2964
3055
  }
2965
3056
  },
2966
3057
  null,
2967
- { 'disposeWhen': whenToDispose, 'disposeWhenNodeIsRemoved': activelyDisposeWhenNodeIsRemoved }
3058
+ { disposeWhen: whenToDispose, disposeWhenNodeIsRemoved: activelyDisposeWhenNodeIsRemoved }
2968
3059
  );
2969
3060
  } else {
2970
3061
  // 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
@@ -2982,9 +3073,9 @@ ko.exportSymbol('templateRewriting.applyMemoizedBindingsToNextSibling', ko.templ
2982
3073
  // This will be called by setDomNodeChildrenFromArrayMapping to get the nodes to add to targetNode
2983
3074
  var executeTemplateForArrayItem = function (arrayValue, index) {
2984
3075
  // Support selecting template as a function of the data being rendered
2985
- var templateName = typeof(template) == 'function' ? template(arrayValue) : template;
2986
- arrayItemContext = parentBindingContext['createChildContext'](ko.utils.unwrapObservable(arrayValue));
3076
+ arrayItemContext = parentBindingContext['createChildContext'](ko.utils.unwrapObservable(arrayValue), options['as']);
2987
3077
  arrayItemContext['$index'] = index;
3078
+ var templateName = typeof(template) == 'function' ? template(arrayValue, arrayItemContext) : template;
2988
3079
  return executeTemplate(null, "ignoreTargetNode", templateName, arrayItemContext, options);
2989
3080
  }
2990
3081
 
@@ -3005,17 +3096,19 @@ ko.exportSymbol('templateRewriting.applyMemoizedBindingsToNextSibling', ko.templ
3005
3096
  return options['includeDestroyed'] || item === undefined || item === null || !ko.utils.unwrapObservable(item['_destroy']);
3006
3097
  });
3007
3098
 
3008
- ko.utils.setDomNodeChildrenFromArrayMapping(targetNode, filteredArray, executeTemplateForArrayItem, options, activateBindingsCallback);
3099
+ // Call setDomNodeChildrenFromArrayMapping, ignoring any observables unwrapped within (most likely from a callback function).
3100
+ // If the array items are observables, though, they will be unwrapped in executeTemplateForArrayItem and managed within setDomNodeChildrenFromArrayMapping.
3101
+ ko.dependencyDetection.ignore(ko.utils.setDomNodeChildrenFromArrayMapping, null, [targetNode, filteredArray, executeTemplateForArrayItem, options, activateBindingsCallback]);
3009
3102
 
3010
- }, null, { 'disposeWhenNodeIsRemoved': targetNode });
3103
+ }, null, { disposeWhenNodeIsRemoved: targetNode });
3011
3104
  };
3012
3105
 
3013
- var templateSubscriptionDomDataKey = '__ko__templateSubscriptionDomDataKey__';
3014
- function disposeOldSubscriptionAndStoreNewOne(element, newSubscription) {
3015
- var oldSubscription = ko.utils.domData.get(element, templateSubscriptionDomDataKey);
3016
- if (oldSubscription && (typeof(oldSubscription.dispose) == 'function'))
3017
- oldSubscription.dispose();
3018
- ko.utils.domData.set(element, templateSubscriptionDomDataKey, newSubscription);
3106
+ var templateComputedDomDataKey = '__ko__templateComputedDomDataKey__';
3107
+ function disposeOldComputedAndStoreNewOne(element, newComputed) {
3108
+ var oldComputed = ko.utils.domData.get(element, templateComputedDomDataKey);
3109
+ if (oldComputed && (typeof(oldComputed.dispose) == 'function'))
3110
+ oldComputed.dispose();
3111
+ ko.utils.domData.set(element, templateComputedDomDataKey, (newComputed && newComputed.isActive()) ? newComputed : undefined);
3019
3112
  }
3020
3113
 
3021
3114
  ko.bindingHandlers['template'] = {
@@ -3031,52 +3124,52 @@ ko.exportSymbol('templateRewriting.applyMemoizedBindingsToNextSibling', ko.templ
3031
3124
  return { 'controlsDescendantBindings': true };
3032
3125
  },
3033
3126
  'update': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
3034
- var bindingValue = ko.utils.unwrapObservable(valueAccessor());
3035
- var templateName;
3036
- var shouldDisplay = true;
3127
+ var templateName = ko.utils.unwrapObservable(valueAccessor()),
3128
+ options = {},
3129
+ shouldDisplay = true,
3130
+ dataValue,
3131
+ templateComputed = null;
3037
3132
 
3038
- if (typeof bindingValue == "string") {
3039
- templateName = bindingValue;
3040
- } else {
3041
- templateName = bindingValue['name'];
3133
+ if (typeof templateName != "string") {
3134
+ options = templateName;
3135
+ templateName = options['name'];
3042
3136
 
3043
3137
  // Support "if"/"ifnot" conditions
3044
- if ('if' in bindingValue)
3045
- shouldDisplay = shouldDisplay && ko.utils.unwrapObservable(bindingValue['if']);
3046
- if ('ifnot' in bindingValue)
3047
- shouldDisplay = shouldDisplay && !ko.utils.unwrapObservable(bindingValue['ifnot']);
3048
- }
3138
+ if ('if' in options)
3139
+ shouldDisplay = ko.utils.unwrapObservable(options['if']);
3140
+ if (shouldDisplay && 'ifnot' in options)
3141
+ shouldDisplay = !ko.utils.unwrapObservable(options['ifnot']);
3049
3142
 
3050
- var templateSubscription = null;
3143
+ dataValue = ko.utils.unwrapObservable(options['data']);
3144
+ }
3051
3145
 
3052
- if ((typeof bindingValue === 'object') && ('foreach' in bindingValue)) { // Note: can't use 'in' operator on strings
3146
+ if ('foreach' in options) {
3053
3147
  // Render once for each data point (treating data set as empty if shouldDisplay==false)
3054
- var dataArray = (shouldDisplay && bindingValue['foreach']) || [];
3055
- templateSubscription = ko.renderTemplateForEach(templateName || element, dataArray, /* options: */ bindingValue, element, bindingContext);
3148
+ var dataArray = (shouldDisplay && options['foreach']) || [];
3149
+ templateComputed = ko.renderTemplateForEach(templateName || element, dataArray, options, element, bindingContext);
3150
+ } else if (!shouldDisplay) {
3151
+ ko.virtualElements.emptyNode(element);
3056
3152
  } else {
3057
- if (shouldDisplay) {
3058
- // Render once for this single data point (or use the viewModel if no data was provided)
3059
- var innerBindingContext = (typeof bindingValue == 'object') && ('data' in bindingValue)
3060
- ? bindingContext['createChildContext'](ko.utils.unwrapObservable(bindingValue['data'])) // Given an explitit 'data' value, we create a child binding context for it
3061
- : bindingContext; // Given no explicit 'data' value, we retain the same binding context
3062
- templateSubscription = ko.renderTemplate(templateName || element, innerBindingContext, /* options: */ bindingValue, element);
3063
- } else
3064
- ko.virtualElements.emptyNode(element);
3153
+ // Render once for this single data point (or use the viewModel if no data was provided)
3154
+ var innerBindingContext = ('data' in options) ?
3155
+ bindingContext['createChildContext'](dataValue, options['as']) : // Given an explitit 'data' value, we create a child binding context for it
3156
+ bindingContext; // Given no explicit 'data' value, we retain the same binding context
3157
+ templateComputed = ko.renderTemplate(templateName || element, innerBindingContext, options, element);
3065
3158
  }
3066
3159
 
3067
- // It only makes sense to have a single template subscription per element (otherwise which one should have its output displayed?)
3068
- disposeOldSubscriptionAndStoreNewOne(element, templateSubscription);
3160
+ // It only makes sense to have a single template computed per element (otherwise which one should have its output displayed?)
3161
+ disposeOldComputedAndStoreNewOne(element, templateComputed);
3069
3162
  }
3070
3163
  };
3071
3164
 
3072
3165
  // Anonymous templates can't be rewritten. Give a nice error message if you try to do it.
3073
- ko.jsonExpressionRewriting.bindingRewriteValidators['template'] = function(bindingValue) {
3074
- var parsedBindingValue = ko.jsonExpressionRewriting.parseObjectLiteral(bindingValue);
3166
+ ko.expressionRewriting.bindingRewriteValidators['template'] = function(bindingValue) {
3167
+ var parsedBindingValue = ko.expressionRewriting.parseObjectLiteral(bindingValue);
3075
3168
 
3076
3169
  if ((parsedBindingValue.length == 1) && parsedBindingValue[0]['unknown'])
3077
3170
  return null; // It looks like a string literal, not an object literal, so treat it as a named template (which is allowed for rewriting)
3078
3171
 
3079
- if (ko.jsonExpressionRewriting.keyValueArrayContainsKey(parsedBindingValue, "name"))
3172
+ if (ko.expressionRewriting.keyValueArrayContainsKey(parsedBindingValue, "name"))
3080
3173
  return null; // Named templates can be rewritten, so return "no error"
3081
3174
  return "This template engine does not support anonymous templates nested within its templates";
3082
3175
  };
@@ -3087,85 +3180,95 @@ ko.exportSymbol('templateRewriting.applyMemoizedBindingsToNextSibling', ko.templ
3087
3180
  ko.exportSymbol('setTemplateEngine', ko.setTemplateEngine);
3088
3181
  ko.exportSymbol('renderTemplate', ko.renderTemplate);
3089
3182
 
3090
- (function () {
3183
+ ko.utils.compareArrays = (function () {
3184
+ var statusNotInOld = 'added', statusNotInNew = 'deleted';
3185
+
3091
3186
  // Simple calculation based on Levenshtein distance.
3092
- function calculateEditDistanceMatrix(oldArray, newArray, maxAllowedDistance) {
3093
- var distances = [];
3094
- for (var i = 0; i <= newArray.length; i++)
3095
- distances[i] = [];
3096
-
3097
- // Top row - transform old array into empty array via deletions
3098
- for (var i = 0, j = Math.min(oldArray.length, maxAllowedDistance); i <= j; i++)
3099
- distances[0][i] = i;
3100
-
3101
- // Left row - transform empty array into new array via additions
3102
- for (var i = 1, j = Math.min(newArray.length, maxAllowedDistance); i <= j; i++) {
3103
- distances[i][0] = i;
3104
- }
3105
-
3106
- // Fill out the body of the array
3107
- var oldIndex, oldIndexMax = oldArray.length, newIndex, newIndexMax = newArray.length;
3108
- var distanceViaAddition, distanceViaDeletion;
3109
- for (oldIndex = 1; oldIndex <= oldIndexMax; oldIndex++) {
3110
- var newIndexMinForRow = Math.max(1, oldIndex - maxAllowedDistance);
3111
- var newIndexMaxForRow = Math.min(newIndexMax, oldIndex + maxAllowedDistance);
3112
- for (newIndex = newIndexMinForRow; newIndex <= newIndexMaxForRow; newIndex++) {
3113
- if (oldArray[oldIndex - 1] === newArray[newIndex - 1])
3114
- distances[newIndex][oldIndex] = distances[newIndex - 1][oldIndex - 1];
3187
+ function compareArrays(oldArray, newArray, dontLimitMoves) {
3188
+ oldArray = oldArray || [];
3189
+ newArray = newArray || [];
3190
+
3191
+ if (oldArray.length <= newArray.length)
3192
+ return compareSmallArrayToBigArray(oldArray, newArray, statusNotInOld, statusNotInNew, dontLimitMoves);
3193
+ else
3194
+ return compareSmallArrayToBigArray(newArray, oldArray, statusNotInNew, statusNotInOld, dontLimitMoves);
3195
+ }
3196
+
3197
+ function compareSmallArrayToBigArray(smlArray, bigArray, statusNotInSml, statusNotInBig, dontLimitMoves) {
3198
+ var myMin = Math.min,
3199
+ myMax = Math.max,
3200
+ editDistanceMatrix = [],
3201
+ smlIndex, smlIndexMax = smlArray.length,
3202
+ bigIndex, bigIndexMax = bigArray.length,
3203
+ compareRange = (bigIndexMax - smlIndexMax) || 1,
3204
+ maxDistance = smlIndexMax + bigIndexMax + 1,
3205
+ thisRow, lastRow,
3206
+ bigIndexMaxForRow, bigIndexMinForRow;
3207
+
3208
+ for (smlIndex = 0; smlIndex <= smlIndexMax; smlIndex++) {
3209
+ lastRow = thisRow;
3210
+ editDistanceMatrix.push(thisRow = []);
3211
+ bigIndexMaxForRow = myMin(bigIndexMax, smlIndex + compareRange);
3212
+ bigIndexMinForRow = myMax(0, smlIndex - 1);
3213
+ for (bigIndex = bigIndexMinForRow; bigIndex <= bigIndexMaxForRow; bigIndex++) {
3214
+ if (!bigIndex)
3215
+ thisRow[bigIndex] = smlIndex + 1;
3216
+ else if (!smlIndex) // Top row - transform empty array into new array via additions
3217
+ thisRow[bigIndex] = bigIndex + 1;
3218
+ else if (smlArray[smlIndex - 1] === bigArray[bigIndex - 1])
3219
+ thisRow[bigIndex] = lastRow[bigIndex - 1]; // copy value (no edit)
3115
3220
  else {
3116
- var northDistance = distances[newIndex - 1][oldIndex] === undefined ? Number.MAX_VALUE : distances[newIndex - 1][oldIndex] + 1;
3117
- var westDistance = distances[newIndex][oldIndex - 1] === undefined ? Number.MAX_VALUE : distances[newIndex][oldIndex - 1] + 1;
3118
- distances[newIndex][oldIndex] = Math.min(northDistance, westDistance);
3221
+ var northDistance = lastRow[bigIndex] || maxDistance; // not in big (deletion)
3222
+ var westDistance = thisRow[bigIndex - 1] || maxDistance; // not in small (addition)
3223
+ thisRow[bigIndex] = myMin(northDistance, westDistance) + 1;
3119
3224
  }
3120
3225
  }
3121
3226
  }
3122
3227
 
3123
- return distances;
3124
- }
3125
-
3126
- function findEditScriptFromEditDistanceMatrix(editDistanceMatrix, oldArray, newArray) {
3127
- var oldIndex = oldArray.length;
3128
- var newIndex = newArray.length;
3129
- var editScript = [];
3130
- var maxDistance = editDistanceMatrix[newIndex][oldIndex];
3131
- if (maxDistance === undefined)
3132
- return null; // maxAllowedDistance must be too small
3133
- while ((oldIndex > 0) || (newIndex > 0)) {
3134
- var me = editDistanceMatrix[newIndex][oldIndex];
3135
- var distanceViaAdd = (newIndex > 0) ? editDistanceMatrix[newIndex - 1][oldIndex] : maxDistance + 1;
3136
- var distanceViaDelete = (oldIndex > 0) ? editDistanceMatrix[newIndex][oldIndex - 1] : maxDistance + 1;
3137
- var distanceViaRetain = (newIndex > 0) && (oldIndex > 0) ? editDistanceMatrix[newIndex - 1][oldIndex - 1] : maxDistance + 1;
3138
- if ((distanceViaAdd === undefined) || (distanceViaAdd < me - 1)) distanceViaAdd = maxDistance + 1;
3139
- if ((distanceViaDelete === undefined) || (distanceViaDelete < me - 1)) distanceViaDelete = maxDistance + 1;
3140
- if (distanceViaRetain < me - 1) distanceViaRetain = maxDistance + 1;
3141
-
3142
- if ((distanceViaAdd <= distanceViaDelete) && (distanceViaAdd < distanceViaRetain)) {
3143
- editScript.push({ status: "added", value: newArray[newIndex - 1] });
3144
- newIndex--;
3145
- } else if ((distanceViaDelete < distanceViaAdd) && (distanceViaDelete < distanceViaRetain)) {
3146
- editScript.push({ status: "deleted", value: oldArray[oldIndex - 1] });
3147
- oldIndex--;
3228
+ var editScript = [], meMinusOne, notInSml = [], notInBig = [];
3229
+ for (smlIndex = smlIndexMax, bigIndex = bigIndexMax; smlIndex || bigIndex;) {
3230
+ meMinusOne = editDistanceMatrix[smlIndex][bigIndex] - 1;
3231
+ if (bigIndex && meMinusOne === editDistanceMatrix[smlIndex][bigIndex-1]) {
3232
+ notInSml.push(editScript[editScript.length] = { // added
3233
+ 'status': statusNotInSml,
3234
+ 'value': bigArray[--bigIndex],
3235
+ 'index': bigIndex });
3236
+ } else if (smlIndex && meMinusOne === editDistanceMatrix[smlIndex - 1][bigIndex]) {
3237
+ notInBig.push(editScript[editScript.length] = { // deleted
3238
+ 'status': statusNotInBig,
3239
+ 'value': smlArray[--smlIndex],
3240
+ 'index': smlIndex });
3148
3241
  } else {
3149
- editScript.push({ status: "retained", value: oldArray[oldIndex - 1] });
3150
- newIndex--;
3151
- oldIndex--;
3242
+ editScript.push({
3243
+ 'status': "retained",
3244
+ 'value': bigArray[--bigIndex] });
3245
+ --smlIndex;
3246
+ }
3247
+ }
3248
+
3249
+ if (notInSml.length && notInBig.length) {
3250
+ // Set a limit on the number of consecutive non-matching comparisons; having it a multiple of
3251
+ // smlIndexMax keeps the time complexity of this algorithm linear.
3252
+ var limitFailedCompares = smlIndexMax * 10, failedCompares,
3253
+ a, d, notInSmlItem, notInBigItem;
3254
+ // Go through the items that have been added and deleted and try to find matches between them.
3255
+ for (failedCompares = a = 0; (dontLimitMoves || failedCompares < limitFailedCompares) && (notInSmlItem = notInSml[a]); a++) {
3256
+ for (d = 0; notInBigItem = notInBig[d]; d++) {
3257
+ if (notInSmlItem['value'] === notInBigItem['value']) {
3258
+ notInSmlItem['moved'] = notInBigItem['index'];
3259
+ notInBigItem['moved'] = notInSmlItem['index'];
3260
+ notInBig.splice(d,1); // This item is marked as moved; so remove it from notInBig list
3261
+ failedCompares = d = 0; // Reset failed compares count because we're checking for consecutive failures
3262
+ break;
3263
+ }
3264
+ }
3265
+ failedCompares += d;
3152
3266
  }
3153
3267
  }
3154
3268
  return editScript.reverse();
3155
3269
  }
3156
3270
 
3157
- ko.utils.compareArrays = function (oldArray, newArray, maxEditsToConsider) {
3158
- if (maxEditsToConsider === undefined) {
3159
- return ko.utils.compareArrays(oldArray, newArray, 1) // First consider likely case where there is at most one edit (very fast)
3160
- || ko.utils.compareArrays(oldArray, newArray, 10) // If that fails, account for a fair number of changes while still being fast
3161
- || ko.utils.compareArrays(oldArray, newArray, Number.MAX_VALUE); // Ultimately give the right answer, even though it may take a long time
3162
- } else {
3163
- oldArray = oldArray || [];
3164
- newArray = newArray || [];
3165
- var editDistanceMatrix = calculateEditDistanceMatrix(oldArray, newArray, maxEditsToConsider);
3166
- return findEditScriptFromEditDistanceMatrix(editDistanceMatrix, oldArray, newArray);
3167
- }
3168
- };
3271
+ return compareArrays;
3169
3272
  })();
3170
3273
 
3171
3274
  ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);
@@ -3181,13 +3284,26 @@ ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);
3181
3284
  // "callbackAfterAddingNodes" will be invoked after any "mapping"-generated nodes are inserted into the container node
3182
3285
  // You can use this, for example, to activate bindings on those nodes.
3183
3286
 
3184
- function fixUpVirtualElements(contiguousNodeArray) {
3185
- // Ensures that contiguousNodeArray really *is* an array of contiguous siblings, even if some of the interior
3186
- // ones have changed since your array was first built (e.g., because your array contains virtual elements, and
3187
- // their virtual children changed when binding was applied to them).
3188
- // This is needed so that we can reliably remove or update the nodes corresponding to a given array item
3189
-
3190
- if (contiguousNodeArray.length > 2) {
3287
+ function fixUpNodesToBeMovedOrRemoved(contiguousNodeArray) {
3288
+ // Before moving, deleting, or replacing a set of nodes that were previously outputted by the "map" function, we have to reconcile
3289
+ // them against what is in the DOM right now. It may be that some of the nodes have already been removed from the document,
3290
+ // or that new nodes might have been inserted in the middle, for example by a binding. Also, there may previously have been
3291
+ // leading comment nodes (created by rewritten string-based templates) that have since been removed during binding.
3292
+ // So, this function translates the old "map" output array into its best guess of what set of current DOM nodes should be removed.
3293
+ //
3294
+ // Rules:
3295
+ // [A] Any leading nodes that aren't in the document any more should be ignored
3296
+ // These most likely correspond to memoization nodes that were already removed during binding
3297
+ // See https://github.com/SteveSanderson/knockout/pull/440
3298
+ // [B] We want to output a contiguous series of nodes that are still in the document. So, ignore any nodes that
3299
+ // have already been removed, and include any nodes that have been inserted among the previous collection
3300
+
3301
+ // Rule [A]
3302
+ while (contiguousNodeArray.length && !ko.utils.domNodeIsAttachedToDocument(contiguousNodeArray[0]))
3303
+ contiguousNodeArray.splice(0, 1);
3304
+
3305
+ // Rule [B]
3306
+ if (contiguousNodeArray.length > 1) {
3191
3307
  // Build up the actual new contiguous node set
3192
3308
  var current = contiguousNodeArray[0], last = contiguousNodeArray[contiguousNodeArray.length - 1], newContiguousSet = [current];
3193
3309
  while (current !== last) {
@@ -3201,6 +3317,7 @@ ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);
3201
3317
  // (The following line replaces the contents of contiguousNodeArray with newContiguousSet)
3202
3318
  Array.prototype.splice.apply(contiguousNodeArray, [0, contiguousNodeArray.length].concat(newContiguousSet));
3203
3319
  }
3320
+ return contiguousNodeArray;
3204
3321
  }
3205
3322
 
3206
3323
  function mapNodeAndRefreshWhenChanged(containerNode, mapping, valueToMap, callbackAfterAddingNodes, index) {
@@ -3211,18 +3328,17 @@ ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);
3211
3328
 
3212
3329
  // On subsequent evaluations, just replace the previously-inserted DOM nodes
3213
3330
  if (mappedNodes.length > 0) {
3214
- fixUpVirtualElements(mappedNodes);
3215
- ko.utils.replaceDomNodes(mappedNodes, newMappedNodes);
3331
+ ko.utils.replaceDomNodes(fixUpNodesToBeMovedOrRemoved(mappedNodes), newMappedNodes);
3216
3332
  if (callbackAfterAddingNodes)
3217
- callbackAfterAddingNodes(valueToMap, newMappedNodes);
3333
+ ko.dependencyDetection.ignore(callbackAfterAddingNodes, null, [valueToMap, newMappedNodes, index]);
3218
3334
  }
3219
3335
 
3220
3336
  // Replace the contents of the mappedNodes array, thereby updating the record
3221
3337
  // of which nodes would be deleted if valueToMap was itself later removed
3222
3338
  mappedNodes.splice(0, mappedNodes.length);
3223
3339
  ko.utils.arrayPushAll(mappedNodes, newMappedNodes);
3224
- }, null, { 'disposeWhenNodeIsRemoved': containerNode, 'disposeWhen': function() { return (mappedNodes.length == 0) || !ko.utils.domNodeIsAttachedToDocument(mappedNodes[0]) } });
3225
- return { mappedNodes : mappedNodes, dependentObservable : dependentObservable };
3340
+ }, null, { disposeWhenNodeIsRemoved: containerNode, disposeWhen: function() { return (mappedNodes.length == 0) || !ko.utils.domNodeIsAttachedToDocument(mappedNodes[0]) } });
3341
+ return { mappedNodes : mappedNodes, dependentObservable : (dependentObservable.isActive() ? dependentObservable : undefined) };
3226
3342
  }
3227
3343
 
3228
3344
  var lastMappingResultDomDataKey = "setDomNodeChildrenFromArrayMapping_lastMappingResult";
@@ -3239,96 +3355,113 @@ ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);
3239
3355
  // Build the new mapping result
3240
3356
  var newMappingResult = [];
3241
3357
  var lastMappingResultIndex = 0;
3242
- var nodesToDelete = [];
3243
3358
  var newMappingResultIndex = 0;
3244
- var nodesAdded = [];
3245
- var insertAfterNode = null;
3246
- for (var i = 0, j = editScript.length; i < j; i++) {
3247
- switch (editScript[i].status) {
3248
- case "retained":
3249
- // Just keep the information - don't touch the nodes
3250
- var dataToRetain = lastMappingResult[lastMappingResultIndex];
3251
- dataToRetain.indexObservable(newMappingResultIndex);
3252
- newMappingResultIndex = newMappingResult.push(dataToRetain);
3253
- if (dataToRetain.domNodes.length > 0)
3254
- insertAfterNode = dataToRetain.domNodes[dataToRetain.domNodes.length - 1];
3255
- lastMappingResultIndex++;
3256
- break;
3257
3359
 
3258
- case "deleted":
3259
- // Stop tracking changes to the mapping for these nodes
3260
- lastMappingResult[lastMappingResultIndex].dependentObservable.dispose();
3261
-
3262
- // Queue these nodes for later removal
3263
- fixUpVirtualElements(lastMappingResult[lastMappingResultIndex].domNodes);
3264
- ko.utils.arrayForEach(lastMappingResult[lastMappingResultIndex].domNodes, function (node) {
3265
- nodesToDelete.push({
3266
- element: node,
3267
- index: i,
3268
- value: editScript[i].value
3360
+ var nodesToDelete = [];
3361
+ var itemsToProcess = [];
3362
+ var itemsForBeforeRemoveCallbacks = [];
3363
+ var itemsForMoveCallbacks = [];
3364
+ var itemsForAfterAddCallbacks = [];
3365
+ var mapData;
3366
+
3367
+ function itemMovedOrRetained(editScriptIndex, oldPosition) {
3368
+ mapData = lastMappingResult[oldPosition];
3369
+ if (newMappingResultIndex !== oldPosition)
3370
+ itemsForMoveCallbacks[editScriptIndex] = mapData;
3371
+ // Since updating the index might change the nodes, do so before calling fixUpNodesToBeMovedOrRemoved
3372
+ mapData.indexObservable(newMappingResultIndex++);
3373
+ fixUpNodesToBeMovedOrRemoved(mapData.mappedNodes);
3374
+ newMappingResult.push(mapData);
3375
+ itemsToProcess.push(mapData);
3376
+ }
3377
+
3378
+ function callCallback(callback, items) {
3379
+ if (callback) {
3380
+ for (var i = 0, n = items.length; i < n; i++) {
3381
+ if (items[i]) {
3382
+ ko.utils.arrayForEach(items[i].mappedNodes, function(node) {
3383
+ callback(node, i, items[i].arrayEntry);
3269
3384
  });
3270
- insertAfterNode = node;
3271
- });
3385
+ }
3386
+ }
3387
+ }
3388
+ }
3389
+
3390
+ for (var i = 0, editScriptItem, movedIndex; editScriptItem = editScript[i]; i++) {
3391
+ movedIndex = editScriptItem['moved'];
3392
+ switch (editScriptItem['status']) {
3393
+ case "deleted":
3394
+ if (movedIndex === undefined) {
3395
+ mapData = lastMappingResult[lastMappingResultIndex];
3396
+
3397
+ // Stop tracking changes to the mapping for these nodes
3398
+ if (mapData.dependentObservable)
3399
+ mapData.dependentObservable.dispose();
3400
+
3401
+ // Queue these nodes for later removal
3402
+ nodesToDelete.push.apply(nodesToDelete, fixUpNodesToBeMovedOrRemoved(mapData.mappedNodes));
3403
+ if (options['beforeRemove']) {
3404
+ itemsForBeforeRemoveCallbacks[i] = mapData;
3405
+ itemsToProcess.push(mapData);
3406
+ }
3407
+ }
3272
3408
  lastMappingResultIndex++;
3273
3409
  break;
3274
3410
 
3411
+ case "retained":
3412
+ itemMovedOrRetained(i, lastMappingResultIndex++);
3413
+ break;
3414
+
3275
3415
  case "added":
3276
- var valueToMap = editScript[i].value;
3277
- var indexObservable = ko.observable(newMappingResultIndex);
3278
- var mapData = mapNodeAndRefreshWhenChanged(domNode, mapping, valueToMap, callbackAfterAddingNodes, indexObservable);
3279
- var mappedNodes = mapData.mappedNodes;
3280
-
3281
- // On the first evaluation, insert the nodes at the current insertion point
3282
- newMappingResultIndex = newMappingResult.push({
3283
- arrayEntry: editScript[i].value,
3284
- domNodes: mappedNodes,
3285
- dependentObservable: mapData.dependentObservable,
3286
- indexObservable: indexObservable
3287
- });
3288
- for (var nodeIndex = 0, nodeIndexMax = mappedNodes.length; nodeIndex < nodeIndexMax; nodeIndex++) {
3289
- var node = mappedNodes[nodeIndex];
3290
- nodesAdded.push({
3291
- element: node,
3292
- index: i,
3293
- value: editScript[i].value
3294
- });
3295
- if (insertAfterNode == null) {
3296
- // Insert "node" (the newly-created node) as domNode's first child
3297
- ko.virtualElements.prepend(domNode, node);
3298
- } else {
3299
- // Insert "node" into "domNode" immediately after "insertAfterNode"
3300
- ko.virtualElements.insertAfter(domNode, node, insertAfterNode);
3301
- }
3302
- insertAfterNode = node;
3416
+ if (movedIndex !== undefined) {
3417
+ itemMovedOrRetained(i, movedIndex);
3418
+ } else {
3419
+ mapData = { arrayEntry: editScriptItem['value'], indexObservable: ko.observable(newMappingResultIndex++) };
3420
+ newMappingResult.push(mapData);
3421
+ itemsToProcess.push(mapData);
3422
+ if (!isFirstExecution)
3423
+ itemsForAfterAddCallbacks[i] = mapData;
3303
3424
  }
3304
- if (callbackAfterAddingNodes)
3305
- callbackAfterAddingNodes(valueToMap, mappedNodes, indexObservable);
3306
3425
  break;
3307
3426
  }
3308
3427
  }
3309
3428
 
3310
- ko.utils.arrayForEach(nodesToDelete, function (node) { ko.cleanNode(node.element) });
3429
+ // Call beforeMove first before any changes have been made to the DOM
3430
+ callCallback(options['beforeMove'], itemsForMoveCallbacks);
3311
3431
 
3312
- var invokedBeforeRemoveCallback = false;
3313
- if (!isFirstExecution) {
3314
- if (options['afterAdd']) {
3315
- for (var i = 0; i < nodesAdded.length; i++)
3316
- options['afterAdd'](nodesAdded[i].element, nodesAdded[i].index, nodesAdded[i].value);
3317
- }
3318
- if (options['beforeRemove']) {
3319
- for (var i = 0; i < nodesToDelete.length; i++)
3320
- options['beforeRemove'](nodesToDelete[i].element, nodesToDelete[i].index, nodesToDelete[i].value);
3321
- invokedBeforeRemoveCallback = true;
3432
+ // Next remove nodes for deleted items (or just clean if there's a beforeRemove callback)
3433
+ ko.utils.arrayForEach(nodesToDelete, options['beforeRemove'] ? ko.cleanNode : ko.removeNode);
3434
+
3435
+ // Next add/reorder the remaining items (will include deleted items if there's a beforeRemove callback)
3436
+ for (var i = 0, nextNode = ko.virtualElements.firstChild(domNode), lastNode, node; mapData = itemsToProcess[i]; i++) {
3437
+ // Get nodes for newly added items
3438
+ if (!mapData.mappedNodes)
3439
+ ko.utils.extend(mapData, mapNodeAndRefreshWhenChanged(domNode, mapping, mapData.arrayEntry, callbackAfterAddingNodes, mapData.indexObservable));
3440
+
3441
+ // Put nodes in the right place if they aren't there already
3442
+ for (var j = 0; node = mapData.mappedNodes[j]; nextNode = node.nextSibling, lastNode = node, j++) {
3443
+ if (node !== nextNode)
3444
+ ko.virtualElements.insertAfter(domNode, node, lastNode);
3322
3445
  }
3323
- }
3324
- if (!invokedBeforeRemoveCallback && nodesToDelete.length) {
3325
- for (var i = 0; i < nodesToDelete.length; i++) {
3326
- var element = nodesToDelete[i].element;
3327
- if (element.parentNode)
3328
- element.parentNode.removeChild(element);
3446
+
3447
+ // Run the callbacks for newly added nodes (for example, to apply bindings, etc.)
3448
+ if (!mapData.initialized && callbackAfterAddingNodes) {
3449
+ callbackAfterAddingNodes(mapData.arrayEntry, mapData.mappedNodes, mapData.indexObservable);
3450
+ mapData.initialized = true;
3329
3451
  }
3330
3452
  }
3331
3453
 
3454
+ // If there's a beforeRemove callback, call it after reordering.
3455
+ // Note that we assume that the beforeRemove callback will usually be used to remove the nodes using
3456
+ // some sort of animation, which is why we first reorder the nodes that will be removed. If the
3457
+ // callback instead removes the nodes right away, it would be more efficient to skip reordering them.
3458
+ // Perhaps we'll make that change in the future if this scenario becomes more common.
3459
+ callCallback(options['beforeRemove'], itemsForBeforeRemoveCallbacks);
3460
+
3461
+ // Finally call afterMove and afterAdd callbacks
3462
+ callCallback(options['afterMove'], itemsForMoveCallbacks);
3463
+ callCallback(options['afterAdd'], itemsForAfterAddCallbacks);
3464
+
3332
3465
  // Store a copy of the array items we just considered so we can difference it next time
3333
3466
  ko.utils.domData.set(domNode, lastMappingResultDomDataKey, newMappingResult);
3334
3467
  }
@@ -3440,4 +3573,5 @@ ko.exportSymbol('nativeTemplateEngine', ko.nativeTemplateEngine);
3440
3573
  ko.exportSymbol('jqueryTmplTemplateEngine', ko.jqueryTmplTemplateEngine);
3441
3574
  })();
3442
3575
  });
3443
- })(window,document,navigator);
3576
+ })(window,document,navigator,window["jQuery"]);
3577
+ })();