knockoutjs-rails 2.2.1 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -25,7 +25,7 @@ application.js):
25
25
 
26
26
  ## Versioning
27
27
 
28
- knockoutjs-rails 2.2.1 == Knockout.js 2.2.1
28
+ knockoutjs-rails 2.3.0 == Knockout.js 2.3.0
29
29
 
30
30
  Every attempt is made to mirror the currently shipping Knockout.js version number wherever possible.
31
31
  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.2.1"
3
+ VERSION = "2.3.0"
4
4
  end
5
5
  end
@@ -1,11 +1,18 @@
1
- // Knockout JavaScript library v2.2.1
1
+ // Knockout JavaScript library v2.3.0
2
2
  // (c) Steven Sanderson - http://knockoutjs.com/
3
3
  // License: MIT (http://www.opensource.org/licenses/mit-license.php)
4
4
 
5
5
  (function(){
6
6
  var DEBUG=true;
7
- (function(window,document,navigator,jQuery,undefined){
8
- !function(factory) {
7
+ (function(undefined){
8
+ // (0, eval)('this') is a robust way of getting a reference to the global object
9
+ // For details, see http://stackoverflow.com/questions/14119988/return-this-0-evalthis/14120023#14120023
10
+ var window = this || (0, eval)('this'),
11
+ document = window['document'],
12
+ navigator = window['navigator'],
13
+ jQuery = window["jQuery"],
14
+ JSON = window["JSON"];
15
+ (function(factory) {
9
16
  // Support three module loading scenarios
10
17
  if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') {
11
18
  // [1] CommonJS/Node.js
@@ -37,38 +44,43 @@ ko.exportSymbol = function(koPath, object) {
37
44
  ko.exportProperty = function(owner, publicName, object) {
38
45
  owner[publicName] = object;
39
46
  };
40
- ko.version = "2.2.1";
47
+ ko.version = "2.3.0";
41
48
 
42
49
  ko.exportSymbol('version', ko.version);
43
- ko.utils = new (function () {
44
- var stringTrimRegex = /^(\s|\u00A0)+|(\s|\u00A0)+$/g;
50
+ ko.utils = (function () {
51
+ var objectForEach = function(obj, action) {
52
+ for (var prop in obj) {
53
+ if (obj.hasOwnProperty(prop)) {
54
+ action(prop, obj[prop]);
55
+ }
56
+ }
57
+ };
45
58
 
46
59
  // Represent the known event types in a compact way, then at runtime transform it into a hash with event name as key (for fast lookup)
47
60
  var knownEvents = {}, knownEventTypesByEventName = {};
48
- var keyEventTypeName = /Firefox\/2/i.test(navigator.userAgent) ? 'KeyboardEvent' : 'UIEvents';
61
+ var keyEventTypeName = (navigator && /Firefox\/2/i.test(navigator.userAgent)) ? 'KeyboardEvent' : 'UIEvents';
49
62
  knownEvents[keyEventTypeName] = ['keyup', 'keydown', 'keypress'];
50
63
  knownEvents['MouseEvents'] = ['click', 'dblclick', 'mousedown', 'mouseup', 'mousemove', 'mouseover', 'mouseout', 'mouseenter', 'mouseleave'];
51
- for (var eventType in knownEvents) {
52
- var knownEventsForType = knownEvents[eventType];
64
+ objectForEach(knownEvents, function(eventType, knownEventsForType) {
53
65
  if (knownEventsForType.length) {
54
66
  for (var i = 0, j = knownEventsForType.length; i < j; i++)
55
67
  knownEventTypesByEventName[knownEventsForType[i]] = eventType;
56
68
  }
57
- }
69
+ });
58
70
  var eventsThatMustBeRegisteredUsingAttachEvent = { 'propertychange': true }; // Workaround for an IE9 issue - https://github.com/SteveSanderson/knockout/issues/406
59
71
 
60
72
  // Detect IE versions for bug workarounds (uses IE conditionals, not UA string, for robustness)
61
73
  // Note that, since IE 10 does not support conditional comments, the following logic only detects IE < 10.
62
74
  // Currently this is by design, since IE 10+ behaves correctly when treated as a standard browser.
63
75
  // If there is a future need to detect specific versions of IE10+, we will amend this.
64
- var ieVersion = (function() {
76
+ var ieVersion = document && (function() {
65
77
  var version = 3, div = document.createElement('div'), iElems = div.getElementsByTagName('i');
66
78
 
67
79
  // Keep constructing conditional HTML blocks until we hit one that resolves to an empty fragment
68
80
  while (
69
81
  div.innerHTML = '<!--[if gt IE ' + (++version) + ']><i></i><![endif]-->',
70
82
  iElems[0]
71
- );
83
+ ) {}
72
84
  return version > 4 ? version : undefined;
73
85
  }());
74
86
  var isIe6 = ieVersion === 6,
@@ -147,6 +159,17 @@ ko.utils = new (function () {
147
159
  return array;
148
160
  },
149
161
 
162
+ addOrRemoveItem: function(array, value, included) {
163
+ var existingEntryIndex = array.indexOf ? array.indexOf(value) : ko.utils.arrayIndexOf(array, value);
164
+ if (existingEntryIndex < 0) {
165
+ if (included)
166
+ array.push(value);
167
+ } else {
168
+ if (!included)
169
+ array.splice(existingEntryIndex, 1);
170
+ }
171
+ },
172
+
150
173
  extend: function (target, source) {
151
174
  if (source) {
152
175
  for(var prop in source) {
@@ -158,6 +181,8 @@ ko.utils = new (function () {
158
181
  return target;
159
182
  },
160
183
 
184
+ objectForEach: objectForEach,
185
+
161
186
  emptyDomNode: function (domNode) {
162
187
  while (domNode.firstChild) {
163
188
  ko.removeNode(domNode.firstChild);
@@ -214,7 +239,10 @@ ko.utils = new (function () {
214
239
  },
215
240
 
216
241
  stringTrim: function (string) {
217
- return (string || "").replace(stringTrimRegex, "");
242
+ return string === null || string === undefined ? '' :
243
+ string.trim ?
244
+ string.trim() :
245
+ string.toString().replace(/^[\s\xa0]+|[\s\xa0]+$/g, '');
218
246
  },
219
247
 
220
248
  stringTokenize: function (string, delimiter) {
@@ -250,6 +278,10 @@ ko.utils = new (function () {
250
278
  return ko.utils.domNodeIsContainedBy(node, node.ownerDocument);
251
279
  },
252
280
 
281
+ anyDomNodeIsAttachedToDocument: function(nodes) {
282
+ return !!ko.utils.arrayFirst(nodes, ko.utils.domNodeIsAttachedToDocument);
283
+ },
284
+
253
285
  tagNameLower: function(element) {
254
286
  // For HTML elements, tagName will always be upper case; for XHTML elements, it'll be lower case.
255
287
  // Possible future optimization: If we know it's an element from an XHTML document (not HTML),
@@ -277,11 +309,17 @@ ko.utils = new (function () {
277
309
  jQuery(element)['bind'](eventType, handler);
278
310
  } else if (!mustUseAttachEvent && typeof element.addEventListener == "function")
279
311
  element.addEventListener(eventType, handler, false);
280
- else if (typeof element.attachEvent != "undefined")
281
- element.attachEvent("on" + eventType, function (event) {
282
- handler.call(element, event);
312
+ else if (typeof element.attachEvent != "undefined") {
313
+ var attachEventHandler = function (event) { handler.call(element, event); },
314
+ attachEventName = "on" + eventType;
315
+ element.attachEvent(attachEventName, attachEventHandler);
316
+
317
+ // IE does not dispose attachEvent handlers automatically (unlike with addEventListener)
318
+ // so to avoid leaks, we have to remove them manually. See bug #856
319
+ ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
320
+ element.detachEvent(attachEventName, attachEventHandler);
283
321
  });
284
- else
322
+ } else
285
323
  throw new Error("Browser doesn't support addEventListener or attachEvent");
286
324
  },
287
325
 
@@ -326,17 +364,10 @@ ko.utils = new (function () {
326
364
 
327
365
  toggleDomNodeCssClass: function (node, classNames, shouldHaveClass) {
328
366
  if (classNames) {
329
- var cssClassNameRegex = /[\w-]+/g,
367
+ var cssClassNameRegex = /\S+/g,
330
368
  currentClassNames = node.className.match(cssClassNameRegex) || [];
331
369
  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
- }
370
+ ko.utils.addOrRemoveItem(currentClassNames, className, shouldHaveClass);
340
371
  });
341
372
  node.className = currentClassNames.join(" ");
342
373
  }
@@ -347,21 +378,17 @@ ko.utils = new (function () {
347
378
  if ((value === null) || (value === undefined))
348
379
  value = "";
349
380
 
350
- if (element.nodeType === 3) {
351
- element.data = value;
381
+ // We need there to be exactly one child: a text node.
382
+ // If there are no children, more than one, or if it's not a text node,
383
+ // we'll clear everything and create a single text node.
384
+ var innerTextNode = ko.virtualElements.firstChild(element);
385
+ if (!innerTextNode || innerTextNode.nodeType != 3 || ko.virtualElements.nextSibling(innerTextNode)) {
386
+ ko.virtualElements.setDomNodeChildren(element, [document.createTextNode(value)]);
352
387
  } 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);
388
+ innerTextNode.data = value;
364
389
  }
390
+
391
+ ko.utils.forceRefresh(element);
365
392
  },
366
393
 
367
394
  setElementName: function(element, name) {
@@ -391,7 +418,8 @@ ko.utils = new (function () {
391
418
  ensureSelectElementIsRenderedCorrectly: function(selectElement) {
392
419
  // Workaround for IE9 rendering bug - it doesn't reliably display all the text in dynamically-added select boxes unless you force it to re-render by updating the width.
393
420
  // (See https://github.com/SteveSanderson/knockout/issues/312, http://stackoverflow.com/questions/5908494/select-only-shows-first-char-of-selected-option)
394
- if (ieVersion >= 9) {
421
+ // Also fixes IE7 and IE8 bug that causes selects to be zero width if enclosed by 'if' or 'with'. (See issue #839)
422
+ if (ieVersion) {
395
423
  var originalWidth = selectElement.style.width;
396
424
  selectElement.style.width = 0;
397
425
  selectElement.style.width = originalWidth;
@@ -436,8 +464,8 @@ ko.utils = new (function () {
436
464
  if (typeof jsonString == "string") {
437
465
  jsonString = ko.utils.stringTrim(jsonString);
438
466
  if (jsonString) {
439
- if (window.JSON && window.JSON.parse) // Use native parsing where available
440
- return window.JSON.parse(jsonString);
467
+ if (JSON && JSON.parse) // Use native parsing where available
468
+ return JSON.parse(jsonString);
441
469
  return (new Function("return " + jsonString))(); // Fallback on less safe parsing for older browsers
442
470
  }
443
471
  }
@@ -445,7 +473,7 @@ ko.utils = new (function () {
445
473
  },
446
474
 
447
475
  stringifyJson: function (data, replacer, space) { // replacer and space are optional
448
- if ((typeof JSON == "undefined") || (typeof JSON.stringify == "undefined"))
476
+ if (!JSON || !JSON.stringify)
449
477
  throw new Error("Cannot find JSON.stringify(). Some browsers (e.g., IE < 8) don't support it natively, but you can overcome this by adding a script reference to json2.js, downloadable from http://www.json.org/json2.js");
450
478
  return JSON.stringify(ko.utils.unwrapObservable(data), replacer, space);
451
479
  },
@@ -473,23 +501,24 @@ ko.utils = new (function () {
473
501
  form.action = url;
474
502
  form.method = "post";
475
503
  for (var key in data) {
504
+ // Since 'data' this is a model object, we include all properties including those inherited from its prototype
476
505
  var input = document.createElement("input");
477
506
  input.name = key;
478
507
  input.value = ko.utils.stringifyJson(ko.utils.unwrapObservable(data[key]));
479
508
  form.appendChild(input);
480
509
  }
481
- for (var key in params) {
510
+ objectForEach(params, function(key, value) {
482
511
  var input = document.createElement("input");
483
512
  input.name = key;
484
- input.value = params[key];
513
+ input.value = value;
485
514
  form.appendChild(input);
486
- }
515
+ });
487
516
  document.body.appendChild(form);
488
517
  options['submitter'] ? options['submitter'](form) : form.submit();
489
518
  setTimeout(function () { form.parentNode.removeChild(form); }, 0);
490
519
  }
491
520
  }
492
- })();
521
+ }());
493
522
 
494
523
  ko.exportSymbol('utils', ko.utils);
495
524
  ko.exportSymbol('utils.arrayForEach', ko.utils.arrayForEach);
@@ -512,6 +541,9 @@ ko.exportSymbol('utils.range', ko.utils.range);
512
541
  ko.exportSymbol('utils.toggleDomNodeCssClass', ko.utils.toggleDomNodeCssClass);
513
542
  ko.exportSymbol('utils.triggerEvent', ko.utils.triggerEvent);
514
543
  ko.exportSymbol('utils.unwrapObservable', ko.utils.unwrapObservable);
544
+ ko.exportSymbol('utils.objectForEach', ko.utils.objectForEach);
545
+ ko.exportSymbol('utils.addOrRemoveItem', ko.utils.addOrRemoveItem);
546
+ ko.exportSymbol('unwrap', ko.utils.unwrapObservable); // Convenient shorthand, because this is used so commonly
515
547
 
516
548
  if (!Function.prototype['bind']) {
517
549
  // Function.prototype.bind is a standard part of ECMAScript 5th Edition (December 2009, http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-262.pdf)
@@ -705,7 +737,7 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD
705
737
  function jQueryHtmlParse(html) {
706
738
  // jQuery's "parseHTML" function was introduced in jQuery 1.8.0 and is a documented public API.
707
739
  if (jQuery['parseHTML']) {
708
- return jQuery['parseHTML'](html);
740
+ return jQuery['parseHTML'](html) || []; // Ensure we always return an array and never null
709
741
  } else {
710
742
  // For jQuery < 1.8.0, we fall back on the undocumented internal "clean" function.
711
743
  var elems = jQuery['clean']([html]);
@@ -862,12 +894,12 @@ ko.extenders = {
862
894
  function applyExtenders(requestedExtenders) {
863
895
  var target = this;
864
896
  if (requestedExtenders) {
865
- for (var key in requestedExtenders) {
897
+ ko.utils.objectForEach(requestedExtenders, function(key, value) {
866
898
  var extenderHandler = ko.extenders[key];
867
899
  if (typeof extenderHandler == 'function') {
868
- target = extenderHandler(target, requestedExtenders[key]);
900
+ target = extenderHandler(target, value);
869
901
  }
870
- }
902
+ });
871
903
  }
872
904
  return target;
873
905
  }
@@ -927,10 +959,9 @@ ko.subscribable['fn'] = {
927
959
 
928
960
  getSubscriptionsCount: function () {
929
961
  var total = 0;
930
- for (var eventName in this._subscriptions) {
931
- if (this._subscriptions.hasOwnProperty(eventName))
932
- total += this._subscriptions[eventName].length;
933
- }
962
+ ko.utils.objectForEach(this._subscriptions, function(eventName, subscriptions) {
963
+ total += subscriptions.length;
964
+ });
934
965
  return total;
935
966
  },
936
967
 
@@ -939,7 +970,7 @@ ko.subscribable['fn'] = {
939
970
 
940
971
 
941
972
  ko.isSubscribable = function (instance) {
942
- return typeof instance.subscribe == "function" && typeof instance["notifySubscribers"] == "function";
973
+ return instance != null && typeof instance.subscribe == "function" && typeof instance["notifySubscribers"] == "function";
943
974
  };
944
975
 
945
976
  ko.exportSymbol('subscribable', ko.subscribable);
@@ -1052,17 +1083,15 @@ ko.exportSymbol('observable', ko.observable);
1052
1083
  ko.exportSymbol('isObservable', ko.isObservable);
1053
1084
  ko.exportSymbol('isWriteableObservable', ko.isWriteableObservable);
1054
1085
  ko.observableArray = function (initialValues) {
1055
- if (arguments.length == 0) {
1056
- // Zero-parameter constructor initializes to empty array
1057
- initialValues = [];
1058
- }
1059
- if ((initialValues !== null) && (initialValues !== undefined) && !('length' in initialValues))
1086
+ initialValues = initialValues || [];
1087
+
1088
+ if (typeof initialValues != 'object' || !('length' in initialValues))
1060
1089
  throw new Error("The argument passed when initializing an observable array must be an array, or null, or undefined.");
1061
1090
 
1062
1091
  var result = ko.observable(initialValues);
1063
1092
  ko.utils.extend(result, ko.observableArray['fn']);
1064
1093
  return result;
1065
- }
1094
+ };
1066
1095
 
1067
1096
  ko.observableArray['fn'] = {
1068
1097
  'remove': function (valueOrPredicate) {
@@ -1142,7 +1171,7 @@ ko.observableArray['fn'] = {
1142
1171
  this.valueHasMutated();
1143
1172
  }
1144
1173
  }
1145
- }
1174
+ };
1146
1175
 
1147
1176
  // Populate ko.observableArray.fn with read/write functions from native arrays
1148
1177
  // Important: Do not add any additional functions here that may reasonably be used to *read* data from the array
@@ -1248,14 +1277,16 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
1248
1277
  _hasBeenEvaluated = true;
1249
1278
 
1250
1279
  dependentObservable["notifySubscribers"](_latestValue, "beforeChange");
1280
+
1251
1281
  _latestValue = newValue;
1252
1282
  if (DEBUG) dependentObservable._latestValue = _latestValue;
1283
+ dependentObservable["notifySubscribers"](_latestValue);
1284
+
1253
1285
  } finally {
1254
1286
  ko.dependencyDetection.end();
1287
+ _isBeingEvaluated = false;
1255
1288
  }
1256
1289
 
1257
- dependentObservable["notifySubscribers"](_latestValue);
1258
- _isBeingEvaluated = false;
1259
1290
  if (!_subscriptionsToDependencies.length)
1260
1291
  dispose();
1261
1292
  }
@@ -1323,7 +1354,7 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
1323
1354
  // plus adds a "disposeWhen" callback that, on each evaluation, disposes if the node was removed by some other means.)
1324
1355
  if (disposeWhenNodeIsRemoved && isActive()) {
1325
1356
  dispose = function() {
1326
- ko.utils.domNodeDisposal.removeDisposeCallback(disposeWhenNodeIsRemoved, arguments.callee);
1357
+ ko.utils.domNodeDisposal.removeDisposeCallback(disposeWhenNodeIsRemoved, dispose);
1327
1358
  disposeAllSubscriptionsToDependencies();
1328
1359
  };
1329
1360
  ko.utils.domNodeDisposal.addDisposeCallback(disposeWhenNodeIsRemoved, dispose);
@@ -1375,7 +1406,7 @@ ko.exportSymbol('isComputed', ko.isComputed);
1375
1406
  visitedObjects = visitedObjects || new objectLookup();
1376
1407
 
1377
1408
  rootObject = mapInputCallback(rootObject);
1378
- var canHaveProperties = (typeof rootObject == "object") && (rootObject !== null) && (rootObject !== undefined) && (!(rootObject instanceof Date));
1409
+ var canHaveProperties = (typeof rootObject == "object") && (rootObject !== null) && (rootObject !== undefined) && (!(rootObject instanceof Date)) && (!(rootObject instanceof String)) && (!(rootObject instanceof Number)) && (!(rootObject instanceof Boolean));
1379
1410
  if (!canHaveProperties)
1380
1411
  return rootObject;
1381
1412
 
@@ -1414,27 +1445,32 @@ ko.exportSymbol('isComputed', ko.isComputed);
1414
1445
  if (typeof rootObject['toJSON'] == 'function')
1415
1446
  visitorCallback('toJSON');
1416
1447
  } else {
1417
- for (var propertyName in rootObject)
1448
+ for (var propertyName in rootObject) {
1418
1449
  visitorCallback(propertyName);
1450
+ }
1419
1451
  }
1420
1452
  };
1421
1453
 
1422
1454
  function objectLookup() {
1423
- var keys = [];
1424
- var values = [];
1425
- this.save = function(key, value) {
1426
- var existingIndex = ko.utils.arrayIndexOf(keys, key);
1455
+ this.keys = [];
1456
+ this.values = [];
1457
+ };
1458
+
1459
+ objectLookup.prototype = {
1460
+ constructor: objectLookup,
1461
+ save: function(key, value) {
1462
+ var existingIndex = ko.utils.arrayIndexOf(this.keys, key);
1427
1463
  if (existingIndex >= 0)
1428
- values[existingIndex] = value;
1464
+ this.values[existingIndex] = value;
1429
1465
  else {
1430
- keys.push(key);
1431
- values.push(value);
1466
+ this.keys.push(key);
1467
+ this.values.push(value);
1432
1468
  }
1433
- };
1434
- this.get = function(key) {
1435
- var existingIndex = ko.utils.arrayIndexOf(keys, key);
1436
- return (existingIndex >= 0) ? values[existingIndex] : undefined;
1437
- };
1469
+ },
1470
+ get: function(key) {
1471
+ var existingIndex = ko.utils.arrayIndexOf(this.keys, key);
1472
+ return (existingIndex >= 0) ? this.values[existingIndex] : undefined;
1473
+ }
1438
1474
  };
1439
1475
  })();
1440
1476
 
@@ -1453,7 +1489,7 @@ ko.exportSymbol('toJSON', ko.toJSON);
1453
1489
  if (element[hasDomDataExpandoProperty] === true)
1454
1490
  return ko.utils.domData.get(element, ko.bindingHandlers.options.optionValueDomDataKey);
1455
1491
  return ko.utils.ieVersion <= 7
1456
- ? (element.getAttributeNode('value').specified ? element.value : element.text)
1492
+ ? (element.getAttributeNode('value') && element.getAttributeNode('value').specified ? element.value : element.text)
1457
1493
  : element.value;
1458
1494
  case 'select':
1459
1495
  return element.selectedIndex >= 0 ? ko.selectExtensions.readValue(element.options[element.selectedIndex]) : undefined;
@@ -1484,12 +1520,20 @@ ko.exportSymbol('toJSON', ko.toJSON);
1484
1520
  }
1485
1521
  break;
1486
1522
  case 'select':
1523
+ if (value === "")
1524
+ value = undefined;
1525
+ if (value === null || value === undefined)
1526
+ element.selectedIndex = -1;
1487
1527
  for (var i = element.options.length - 1; i >= 0; i--) {
1488
1528
  if (ko.selectExtensions.readValue(element.options[i]) == value) {
1489
1529
  element.selectedIndex = i;
1490
1530
  break;
1491
1531
  }
1492
1532
  }
1533
+ // for drop-down select, ensure first is selected
1534
+ if (!(element.size > 1) && element.selectedIndex === -1) {
1535
+ element.selectedIndex = 0;
1536
+ }
1493
1537
  break;
1494
1538
  default:
1495
1539
  if ((value === null) || (value === undefined))
@@ -1506,7 +1550,7 @@ ko.exportSymbol('selectExtensions.readValue', ko.selectExtensions.readValue);
1506
1550
  ko.exportSymbol('selectExtensions.writeValue', ko.selectExtensions.writeValue);
1507
1551
  ko.expressionRewriting = (function () {
1508
1552
  var restoreCapturedTokensRegex = /\@ko_token_(\d+)\@/g;
1509
- var javaScriptReservedWords = ["true", "false"];
1553
+ var javaScriptReservedWords = ["true", "false", "null", "undefined"];
1510
1554
 
1511
1555
  // Matches something that can be assigned to--either an isolated identifier or something ending with a property accessor
1512
1556
  // This is designed to be simple and avoid false negatives, but could produce false positives (e.g., a+b.c).
@@ -1683,11 +1727,11 @@ ko.expressionRewriting = (function () {
1683
1727
  // checkIfDifferent: If true, and if the property being written is a writable observable, the value will only be written if
1684
1728
  // it is !== existing value on that writable observable
1685
1729
  writeValueToProperty: function(property, allBindingsAccessor, key, value, checkIfDifferent) {
1686
- if (!property || !ko.isWriteableObservable(property)) {
1730
+ if (!property || !ko.isObservable(property)) {
1687
1731
  var propWriters = allBindingsAccessor()['_ko_property_writers'];
1688
1732
  if (propWriters && propWriters[key])
1689
1733
  propWriters[key](value);
1690
- } else if (!checkIfDifferent || property.peek() !== value) {
1734
+ } else if (ko.isWriteableObservable(property) && (!checkIfDifferent || property.peek() !== value)) {
1691
1735
  property(value);
1692
1736
  }
1693
1737
  }
@@ -1714,7 +1758,7 @@ ko.exportSymbol('jsonExpressionRewriting.insertPropertyAccessorsIntoJson', ko.ex
1714
1758
  // IE 9 cannot reliably read the "nodeValue" property of a comment node (see https://github.com/SteveSanderson/knockout/issues/186)
1715
1759
  // but it does give them a nonstandard alternative property called "text" that it can read reliably. Other browsers don't have that property.
1716
1760
  // So, use node.text where available, and node.nodeValue elsewhere
1717
- var commentNodesHaveTextProperty = document.createComment("test").text === "<!--test-->";
1761
+ var commentNodesHaveTextProperty = document && document.createComment("test").text === "<!--test-->";
1718
1762
 
1719
1763
  var startCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*ko(?:\s+(.+\s*\:[\s\S]*))?\s*-->$/ : /^\s*ko(?:\s+(.+\s*\:[\s\S]*))?\s*$/;
1720
1764
  var endCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*\/ko\s*-->$/ : /^\s*\/ko\s*$/;
@@ -1933,7 +1977,8 @@ ko.exportSymbol('virtualElements.setDomNodeChildren', ko.virtualElements.setDomN
1933
1977
  var bindingFunction = createBindingsStringEvaluatorViaCache(bindingsString, this.bindingCache);
1934
1978
  return bindingFunction(bindingContext, node);
1935
1979
  } catch (ex) {
1936
- throw new Error("Unable to parse bindings.\nMessage: " + ex + ";\nBindings value: " + bindingsString);
1980
+ ex.message = "Unable to parse bindings.\nBindings value: " + bindingsString + "\nMessage: " + ex.message;
1981
+ throw ex;
1937
1982
  }
1938
1983
  }
1939
1984
  });
@@ -2029,6 +2074,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
2029
2074
  }
2030
2075
  }
2031
2076
 
2077
+ var boundElementDomDataKey = '__ko_boundElement';
2032
2078
  function applyBindingsToNodeInternal (node, bindings, viewModelOrBindingContext, bindingContextMayDifferFromDomParentElement) {
2033
2079
  // Need to be sure that inits are only run once, and updates never run until all the inits have been run
2034
2080
  var initPhase = 0; // 0 = before all inits, 1 = during inits, 2 = after all inits
@@ -2048,6 +2094,16 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
2048
2094
  }
2049
2095
 
2050
2096
  var bindingHandlerThatControlsDescendantBindings;
2097
+
2098
+ // Prevent multiple applyBindings calls for the same node, except when a binding value is specified
2099
+ var alreadyBound = ko.utils.domData.get(node, boundElementDomDataKey);
2100
+ if (!bindings) {
2101
+ if (alreadyBound) {
2102
+ throw Error("You cannot apply bindings multiple times to the same element.");
2103
+ }
2104
+ ko.utils.domData.set(node, boundElementDomDataKey, true);
2105
+ }
2106
+
2051
2107
  ko.dependentObservable(
2052
2108
  function () {
2053
2109
  // Ensure we have a nonnull binding context to work with
@@ -2059,7 +2115,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
2059
2115
  // Optimization: Don't store the binding context on this node if it's definitely the same as on node.parentNode, because
2060
2116
  // we can easily recover it just by scanning up the node's ancestors in the DOM
2061
2117
  // (note: here, parent node means "real DOM parent" not "virtual parent", as there's no O(1) way to find the virtual parent)
2062
- if (bindingContextMayDifferFromDomParentElement)
2118
+ if (!alreadyBound && bindingContextMayDifferFromDomParentElement)
2063
2119
  ko.storedBindingContextForNode(node, bindingContextInstance);
2064
2120
 
2065
2121
  // Use evaluatedBindings if given, otherwise fall back on asking the bindings provider to give us some bindings
@@ -2070,7 +2126,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
2070
2126
  // First run all the inits, so bindings can register for notification on changes
2071
2127
  if (initPhase === 0) {
2072
2128
  initPhase = 1;
2073
- for (var bindingKey in parsedBindings) {
2129
+ ko.utils.objectForEach(parsedBindings, function(bindingKey) {
2074
2130
  var binding = ko.bindingHandlers[bindingKey];
2075
2131
  if (binding && node.nodeType === 8)
2076
2132
  validateThatBindingIsAllowedForVirtualElements(bindingKey);
@@ -2086,19 +2142,19 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
2086
2142
  bindingHandlerThatControlsDescendantBindings = bindingKey;
2087
2143
  }
2088
2144
  }
2089
- }
2145
+ });
2090
2146
  initPhase = 2;
2091
2147
  }
2092
2148
 
2093
2149
  // ... then run all the updates, which might trigger changes even on the first evaluation
2094
2150
  if (initPhase === 2) {
2095
- for (var bindingKey in parsedBindings) {
2151
+ ko.utils.objectForEach(parsedBindings, function(bindingKey) {
2096
2152
  var binding = ko.bindingHandlers[bindingKey];
2097
2153
  if (binding && typeof binding["update"] == "function") {
2098
2154
  var handlerUpdateFn = binding["update"];
2099
2155
  handlerUpdateFn(node, makeValueAccessor(bindingKey), parsedBindingsAccessor, viewModel, bindingContextInstance);
2100
2156
  }
2101
- }
2157
+ });
2102
2158
  }
2103
2159
  }
2104
2160
  },
@@ -2167,40 +2223,38 @@ var attrHtmlToJavascriptMap = { 'class': 'className', 'for': 'htmlFor' };
2167
2223
  ko.bindingHandlers['attr'] = {
2168
2224
  'update': function(element, valueAccessor, allBindingsAccessor) {
2169
2225
  var value = ko.utils.unwrapObservable(valueAccessor()) || {};
2170
- for (var attrName in value) {
2171
- if (typeof attrName == "string") {
2172
- var attrValue = ko.utils.unwrapObservable(value[attrName]);
2173
-
2174
- // To cover cases like "attr: { checked:someProp }", we want to remove the attribute entirely
2175
- // when someProp is a "no value"-like value (strictly null, false, or undefined)
2176
- // (because the absence of the "checked" attr is how to mark an element as not checked, etc.)
2177
- var toRemove = (attrValue === false) || (attrValue === null) || (attrValue === undefined);
2226
+ ko.utils.objectForEach(value, function(attrName, attrValue) {
2227
+ attrValue = ko.utils.unwrapObservable(attrValue);
2228
+
2229
+ // To cover cases like "attr: { checked:someProp }", we want to remove the attribute entirely
2230
+ // when someProp is a "no value"-like value (strictly null, false, or undefined)
2231
+ // (because the absence of the "checked" attr is how to mark an element as not checked, etc.)
2232
+ var toRemove = (attrValue === false) || (attrValue === null) || (attrValue === undefined);
2233
+ if (toRemove)
2234
+ element.removeAttribute(attrName);
2235
+
2236
+ // In IE <= 7 and IE8 Quirks Mode, you have to use the Javascript property name instead of the
2237
+ // HTML attribute name for certain attributes. IE8 Standards Mode supports the correct behavior,
2238
+ // but instead of figuring out the mode, we'll just set the attribute through the Javascript
2239
+ // property for IE <= 8.
2240
+ if (ko.utils.ieVersion <= 8 && attrName in attrHtmlToJavascriptMap) {
2241
+ attrName = attrHtmlToJavascriptMap[attrName];
2178
2242
  if (toRemove)
2179
2243
  element.removeAttribute(attrName);
2244
+ else
2245
+ element[attrName] = attrValue;
2246
+ } else if (!toRemove) {
2247
+ element.setAttribute(attrName, attrValue.toString());
2248
+ }
2180
2249
 
2181
- // In IE <= 7 and IE8 Quirks Mode, you have to use the Javascript property name instead of the
2182
- // HTML attribute name for certain attributes. IE8 Standards Mode supports the correct behavior,
2183
- // but instead of figuring out the mode, we'll just set the attribute through the Javascript
2184
- // property for IE <= 8.
2185
- if (ko.utils.ieVersion <= 8 && attrName in attrHtmlToJavascriptMap) {
2186
- attrName = attrHtmlToJavascriptMap[attrName];
2187
- if (toRemove)
2188
- element.removeAttribute(attrName);
2189
- else
2190
- element[attrName] = attrValue;
2191
- } else if (!toRemove) {
2192
- element.setAttribute(attrName, attrValue.toString());
2193
- }
2194
-
2195
- // Treat "name" specially - although you can think of it as an attribute, it also needs
2196
- // special handling on older versions of IE (https://github.com/SteveSanderson/knockout/pull/333)
2197
- // Deliberately being case-sensitive here because XHTML would regard "Name" as a different thing
2198
- // entirely, and there's no strong reason to allow for such casing in HTML.
2199
- if (attrName === "name") {
2200
- ko.utils.setElementName(element, toRemove ? "" : attrValue.toString());
2201
- }
2250
+ // Treat "name" specially - although you can think of it as an attribute, it also needs
2251
+ // special handling on older versions of IE (https://github.com/SteveSanderson/knockout/pull/333)
2252
+ // Deliberately being case-sensitive here because XHTML would regard "Name" as a different thing
2253
+ // entirely, and there's no strong reason to allow for such casing in HTML.
2254
+ if (attrName === "name") {
2255
+ ko.utils.setElementName(element, toRemove ? "" : attrValue.toString());
2202
2256
  }
2203
- }
2257
+ });
2204
2258
  }
2205
2259
  };
2206
2260
  ko.bindingHandlers['checked'] = {
@@ -2219,11 +2273,7 @@ ko.bindingHandlers['checked'] = {
2219
2273
  if ((element.type == "checkbox") && (unwrappedValue instanceof Array)) {
2220
2274
  // For checkboxes bound to an array, we add/remove the checkbox value to that array
2221
2275
  // This works for both observable and non-observable arrays
2222
- var existingEntryIndex = ko.utils.arrayIndexOf(unwrappedValue, element.value);
2223
- if (element.checked && (existingEntryIndex < 0))
2224
- modelValue.push(element.value);
2225
- else if ((!element.checked) && (existingEntryIndex >= 0))
2226
- modelValue.splice(existingEntryIndex, 1);
2276
+ ko.utils.addOrRemoveItem(modelValue, element.value, element.checked);
2227
2277
  } else {
2228
2278
  ko.expressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'checked', valueToWrite, true);
2229
2279
  }
@@ -2242,7 +2292,7 @@ ko.bindingHandlers['checked'] = {
2242
2292
  // When bound to an array, the checkbox being checked represents its value being present in that array
2243
2293
  element.checked = ko.utils.arrayIndexOf(value, element.value) >= 0;
2244
2294
  } else {
2245
- // When bound to anything other value (not an array), the checkbox being checked represents the value being trueish
2295
+ // When bound to any other value (not an array), the checkbox being checked represents the value being trueish
2246
2296
  element.checked = value;
2247
2297
  }
2248
2298
  } else if (element.type == "radio") {
@@ -2255,10 +2305,10 @@ ko.bindingHandlers['css'] = {
2255
2305
  'update': function (element, valueAccessor) {
2256
2306
  var value = ko.utils.unwrapObservable(valueAccessor());
2257
2307
  if (typeof value == "object") {
2258
- for (var className in value) {
2259
- var shouldHaveClass = ko.utils.unwrapObservable(value[className]);
2308
+ ko.utils.objectForEach(value, function(className, shouldHaveClass) {
2309
+ shouldHaveClass = ko.utils.unwrapObservable(shouldHaveClass);
2260
2310
  ko.utils.toggleDomNodeCssClass(element, className, shouldHaveClass);
2261
- }
2311
+ });
2262
2312
  } else {
2263
2313
  value = String(value || ''); // Make sure we don't try to store or set a non-string value
2264
2314
  ko.utils.toggleDomNodeCssClass(element, element[classesWrittenByBindingKey], false);
@@ -2300,41 +2350,38 @@ function makeEventHandlerShortcut(eventName) {
2300
2350
  ko.bindingHandlers['event'] = {
2301
2351
  'init' : function (element, valueAccessor, allBindingsAccessor, viewModel) {
2302
2352
  var eventsToHandle = valueAccessor() || {};
2303
- for(var eventNameOutsideClosure in eventsToHandle) {
2304
- (function() {
2305
- var eventName = eventNameOutsideClosure; // Separate variable to be captured by event handler closure
2306
- if (typeof eventName == "string") {
2307
- ko.utils.registerEventHandler(element, eventName, function (event) {
2308
- var handlerReturnValue;
2309
- var handlerFunction = valueAccessor()[eventName];
2310
- if (!handlerFunction)
2311
- return;
2312
- var allBindings = allBindingsAccessor();
2313
-
2314
- try {
2315
- // Take all the event args, and prefix with the viewmodel
2316
- var argsForHandler = ko.utils.makeArray(arguments);
2317
- argsForHandler.unshift(viewModel);
2318
- handlerReturnValue = handlerFunction.apply(viewModel, argsForHandler);
2319
- } finally {
2320
- if (handlerReturnValue !== true) { // Normally we want to prevent default action. Developer can override this be explicitly returning true.
2321
- if (event.preventDefault)
2322
- event.preventDefault();
2323
- else
2324
- event.returnValue = false;
2325
- }
2353
+ ko.utils.objectForEach(eventsToHandle, function(eventName) {
2354
+ if (typeof eventName == "string") {
2355
+ ko.utils.registerEventHandler(element, eventName, function (event) {
2356
+ var handlerReturnValue;
2357
+ var handlerFunction = valueAccessor()[eventName];
2358
+ if (!handlerFunction)
2359
+ return;
2360
+ var allBindings = allBindingsAccessor();
2361
+
2362
+ try {
2363
+ // Take all the event args, and prefix with the viewmodel
2364
+ var argsForHandler = ko.utils.makeArray(arguments);
2365
+ argsForHandler.unshift(viewModel);
2366
+ handlerReturnValue = handlerFunction.apply(viewModel, argsForHandler);
2367
+ } finally {
2368
+ if (handlerReturnValue !== true) { // Normally we want to prevent default action. Developer can override this be explicitly returning true.
2369
+ if (event.preventDefault)
2370
+ event.preventDefault();
2371
+ else
2372
+ event.returnValue = false;
2326
2373
  }
2374
+ }
2327
2375
 
2328
- var bubble = allBindings[eventName + 'Bubble'] !== false;
2329
- if (!bubble) {
2330
- event.cancelBubble = true;
2331
- if (event.stopPropagation)
2332
- event.stopPropagation();
2333
- }
2334
- });
2335
- }
2336
- })();
2337
- }
2376
+ var bubble = allBindings[eventName + 'Bubble'] !== false;
2377
+ if (!bubble) {
2378
+ event.cancelBubble = true;
2379
+ if (event.stopPropagation)
2380
+ event.stopPropagation();
2381
+ }
2382
+ });
2383
+ }
2384
+ });
2338
2385
  }
2339
2386
  };
2340
2387
  // "foreach: someExpression" is equivalent to "template: { foreach: someExpression }"
@@ -2376,6 +2423,7 @@ ko.bindingHandlers['foreach'] = {
2376
2423
  ko.expressionRewriting.bindingRewriteValidators['foreach'] = false; // Can't rewrite control flow bindings
2377
2424
  ko.virtualElements.allowedBindings['foreach'] = true;
2378
2425
  var hasfocusUpdatingProperty = '__ko_hasfocusUpdating';
2426
+ var hasfocusLastValue = '__ko_hasfocusLastValue';
2379
2427
  ko.bindingHandlers['hasfocus'] = {
2380
2428
  'init': function(element, valueAccessor, allBindingsAccessor) {
2381
2429
  var handleElementFocusChange = function(isFocused) {
@@ -2388,10 +2436,20 @@ ko.bindingHandlers['hasfocus'] = {
2388
2436
  element[hasfocusUpdatingProperty] = true;
2389
2437
  var ownerDoc = element.ownerDocument;
2390
2438
  if ("activeElement" in ownerDoc) {
2391
- isFocused = (ownerDoc.activeElement === element);
2439
+ var active;
2440
+ try {
2441
+ active = ownerDoc.activeElement;
2442
+ } catch(e) {
2443
+ // IE9 throws if you access activeElement during page load (see issue #703)
2444
+ active = ownerDoc.body;
2445
+ }
2446
+ isFocused = (active === element);
2392
2447
  }
2393
2448
  var modelValue = valueAccessor();
2394
2449
  ko.expressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'hasfocus', isFocused, true);
2450
+
2451
+ //cache the latest value, so we can avoid unnecessarily calling focus/blur in the update function
2452
+ element[hasfocusLastValue] = isFocused;
2395
2453
  element[hasfocusUpdatingProperty] = false;
2396
2454
  };
2397
2455
  var handleElementFocusIn = handleElementFocusChange.bind(null, true);
@@ -2403,13 +2461,15 @@ ko.bindingHandlers['hasfocus'] = {
2403
2461
  ko.utils.registerEventHandler(element, "focusout", handleElementFocusOut); // For IE
2404
2462
  },
2405
2463
  'update': function(element, valueAccessor) {
2406
- var value = ko.utils.unwrapObservable(valueAccessor());
2407
- if (!element[hasfocusUpdatingProperty]) {
2464
+ var value = !!ko.utils.unwrapObservable(valueAccessor()); //force boolean to compare with last value
2465
+ if (!element[hasfocusUpdatingProperty] && element[hasfocusLastValue] !== value) {
2408
2466
  value ? element.focus() : element.blur();
2409
2467
  ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, value ? "focusin" : "focusout"]); // For IE, which doesn't reliably fire "focus" or "blur" events synchronously
2410
2468
  }
2411
2469
  }
2412
2470
  };
2471
+
2472
+ ko.bindingHandlers['hasFocus'] = ko.bindingHandlers['hasfocus']; // Make "hasFocus" an alias
2413
2473
  ko.bindingHandlers['html'] = {
2414
2474
  'init': function() {
2415
2475
  // Prevent binding on the dynamically-injected HTML (as developers are unlikely to expect that, and it has security implications)
@@ -2479,59 +2539,83 @@ function ensureDropdownSelectionIsConsistentWithModelValue(element, modelValue,
2479
2539
  };
2480
2540
 
2481
2541
  ko.bindingHandlers['options'] = {
2482
- 'update': function (element, valueAccessor, allBindingsAccessor) {
2542
+ 'init': function(element) {
2483
2543
  if (ko.utils.tagNameLower(element) !== "select")
2484
2544
  throw new Error("options binding applies only to SELECT elements");
2485
2545
 
2486
- var selectWasPreviouslyEmpty = element.length == 0;
2487
- var previousSelectedValues = ko.utils.arrayMap(ko.utils.arrayFilter(element.childNodes, function (node) {
2488
- return node.tagName && (ko.utils.tagNameLower(node) === "option") && node.selected;
2489
- }), function (node) {
2490
- return ko.selectExtensions.readValue(node) || node.innerText || node.textContent;
2491
- });
2492
- var previousScrollTop = element.scrollTop;
2493
-
2494
- var value = ko.utils.unwrapObservable(valueAccessor());
2495
- var selectedValue = element.value;
2496
-
2497
2546
  // Remove all existing <option>s.
2498
- // Need to use .remove() rather than .removeChild() for <option>s otherwise IE behaves oddly (https://github.com/SteveSanderson/knockout/issues/134)
2499
2547
  while (element.length > 0) {
2500
- ko.cleanNode(element.options[0]);
2501
2548
  element.remove(0);
2502
2549
  }
2503
2550
 
2504
- if (value) {
2505
- var allBindings = allBindingsAccessor(),
2506
- includeDestroyed = allBindings['optionsIncludeDestroyed'];
2507
-
2508
- if (typeof value.length != "number")
2509
- value = [value];
2510
- if (allBindings['optionsCaption']) {
2511
- var option = document.createElement("option");
2512
- ko.utils.setHtml(option, allBindings['optionsCaption']);
2513
- ko.selectExtensions.writeValue(option, undefined);
2514
- element.appendChild(option);
2515
- }
2551
+ // Ensures that the binding processor doesn't try to bind the options
2552
+ return { 'controlsDescendantBindings': true };
2553
+ },
2554
+ 'update': function (element, valueAccessor, allBindingsAccessor) {
2555
+ var selectWasPreviouslyEmpty = element.length == 0;
2556
+ var previousScrollTop = (!selectWasPreviouslyEmpty && element.multiple) ? element.scrollTop : null;
2557
+
2558
+ var unwrappedArray = ko.utils.unwrapObservable(valueAccessor());
2559
+ var allBindings = allBindingsAccessor();
2560
+ var includeDestroyed = allBindings['optionsIncludeDestroyed'];
2561
+ var captionPlaceholder = {};
2562
+ var captionValue;
2563
+ var previousSelectedValues;
2564
+ if (element.multiple) {
2565
+ previousSelectedValues = ko.utils.arrayMap(element.selectedOptions || ko.utils.arrayFilter(element.childNodes, function (node) {
2566
+ return node.tagName && (ko.utils.tagNameLower(node) === "option") && node.selected;
2567
+ }), function (node) {
2568
+ return ko.selectExtensions.readValue(node);
2569
+ });
2570
+ } else if (element.selectedIndex >= 0) {
2571
+ previousSelectedValues = [ ko.selectExtensions.readValue(element.options[element.selectedIndex]) ];
2572
+ }
2516
2573
 
2517
- for (var i = 0, j = value.length; i < j; i++) {
2518
- // Skip destroyed items
2519
- var arrayEntry = value[i];
2520
- if (arrayEntry && arrayEntry['_destroy'] && !includeDestroyed)
2521
- continue;
2574
+ if (unwrappedArray) {
2575
+ if (typeof unwrappedArray.length == "undefined") // Coerce single value into array
2576
+ unwrappedArray = [unwrappedArray];
2522
2577
 
2523
- var option = document.createElement("option");
2578
+ // Filter out any entries marked as destroyed
2579
+ var filteredArray = ko.utils.arrayFilter(unwrappedArray, function(item) {
2580
+ return includeDestroyed || item === undefined || item === null || !ko.utils.unwrapObservable(item['_destroy']);
2581
+ });
2524
2582
 
2525
- function applyToObject(object, predicate, defaultValue) {
2526
- var predicateType = typeof predicate;
2527
- if (predicateType == "function") // Given a function; run it against the data value
2528
- return predicate(object);
2529
- else if (predicateType == "string") // Given a string; treat it as a property name on the data value
2530
- return object[predicate];
2531
- else // Given no optionsText arg; use the data value itself
2532
- return defaultValue;
2583
+ // If caption is included, add it to the array
2584
+ if ('optionsCaption' in allBindings) {
2585
+ captionValue = ko.utils.unwrapObservable(allBindings['optionsCaption']);
2586
+ // If caption value is null or undefined, don't show a caption
2587
+ if (captionValue !== null && captionValue !== undefined) {
2588
+ filteredArray.unshift(captionPlaceholder);
2533
2589
  }
2534
-
2590
+ }
2591
+ } else {
2592
+ // If a falsy value is provided (e.g. null), we'll simply empty the select element
2593
+ unwrappedArray = [];
2594
+ }
2595
+
2596
+ function applyToObject(object, predicate, defaultValue) {
2597
+ var predicateType = typeof predicate;
2598
+ if (predicateType == "function") // Given a function; run it against the data value
2599
+ return predicate(object);
2600
+ else if (predicateType == "string") // Given a string; treat it as a property name on the data value
2601
+ return object[predicate];
2602
+ else // Given no optionsText arg; use the data value itself
2603
+ return defaultValue;
2604
+ }
2605
+
2606
+ // The following functions can run at two different times:
2607
+ // The first is when the whole array is being updated directly from this binding handler.
2608
+ // The second is when an observable value for a specific array entry is updated.
2609
+ // oldOptions will be empty in the first case, but will be filled with the previously generated option in the second.
2610
+ function optionForArrayItem(arrayEntry, index, oldOptions) {
2611
+ if (oldOptions.length) {
2612
+ previousSelectedValues = oldOptions[0].selected && [ ko.selectExtensions.readValue(oldOptions[0]) ];
2613
+ }
2614
+ var option = document.createElement("option");
2615
+ if (arrayEntry === captionPlaceholder) {
2616
+ ko.utils.setHtml(option, captionValue);
2617
+ ko.selectExtensions.writeValue(option, undefined);
2618
+ } else {
2535
2619
  // Apply a value to the option element
2536
2620
  var optionValue = applyToObject(arrayEntry, allBindings['optionsValue'], arrayEntry);
2537
2621
  ko.selectExtensions.writeValue(option, ko.utils.unwrapObservable(optionValue));
@@ -2539,33 +2623,44 @@ ko.bindingHandlers['options'] = {
2539
2623
  // Apply some text to the option element
2540
2624
  var optionText = applyToObject(arrayEntry, allBindings['optionsText'], optionValue);
2541
2625
  ko.utils.setTextContent(option, optionText);
2542
-
2543
- element.appendChild(option);
2544
2626
  }
2627
+ return [option];
2628
+ }
2545
2629
 
2630
+ function setSelectionCallback(arrayEntry, newOptions) {
2546
2631
  // IE6 doesn't like us to assign selection to OPTION nodes before they're added to the document.
2547
2632
  // That's why we first added them without selection. Now it's time to set the selection.
2548
- var newOptions = element.getElementsByTagName("option");
2549
- var countSelectionsRetained = 0;
2550
- for (var i = 0, j = newOptions.length; i < j; i++) {
2551
- if (ko.utils.arrayIndexOf(previousSelectedValues, ko.selectExtensions.readValue(newOptions[i])) >= 0) {
2552
- ko.utils.setOptionNodeSelectionState(newOptions[i], true);
2553
- countSelectionsRetained++;
2554
- }
2633
+ if (previousSelectedValues) {
2634
+ var isSelected = ko.utils.arrayIndexOf(previousSelectedValues, ko.selectExtensions.readValue(newOptions[0])) >= 0;
2635
+ ko.utils.setOptionNodeSelectionState(newOptions[0], isSelected);
2555
2636
  }
2637
+ }
2556
2638
 
2557
- element.scrollTop = previousScrollTop;
2558
-
2559
- if (selectWasPreviouslyEmpty && ('value' in allBindings)) {
2560
- // Ensure consistency between model value and selected option.
2561
- // If the dropdown is being populated for the first time here (or was otherwise previously empty),
2562
- // the dropdown selection state is meaningless, so we preserve the model value.
2563
- ensureDropdownSelectionIsConsistentWithModelValue(element, ko.utils.peekObservable(allBindings['value']), /* preferModelValue */ true);
2639
+ var callback = setSelectionCallback;
2640
+ if (allBindings['optionsAfterRender']) {
2641
+ callback = function(arrayEntry, newOptions) {
2642
+ setSelectionCallback(arrayEntry, newOptions);
2643
+ ko.dependencyDetection.ignore(allBindings['optionsAfterRender'], null, [newOptions[0], arrayEntry !== captionPlaceholder ? arrayEntry : undefined]);
2564
2644
  }
2645
+ }
2565
2646
 
2566
- // Workaround for IE9 bug
2567
- ko.utils.ensureSelectElementIsRenderedCorrectly(element);
2647
+ ko.utils.setDomNodeChildrenFromArrayMapping(element, filteredArray, optionForArrayItem, null, callback);
2648
+
2649
+ // Clear previousSelectedValues so that future updates to individual objects don't get stale data
2650
+ previousSelectedValues = null;
2651
+
2652
+ if (selectWasPreviouslyEmpty && ('value' in allBindings)) {
2653
+ // Ensure consistency between model value and selected option.
2654
+ // If the dropdown is being populated for the first time here (or was otherwise previously empty),
2655
+ // the dropdown selection state is meaningless, so we preserve the model value.
2656
+ ensureDropdownSelectionIsConsistentWithModelValue(element, ko.utils.peekObservable(allBindings['value']), /* preferModelValue */ true);
2568
2657
  }
2658
+
2659
+ // Workaround for IE bug
2660
+ ko.utils.ensureSelectElementIsRenderedCorrectly(element);
2661
+
2662
+ if (previousScrollTop && Math.abs(previousScrollTop - element.scrollTop) > 20)
2663
+ element.scrollTop = previousScrollTop;
2569
2664
  }
2570
2665
  };
2571
2666
  ko.bindingHandlers['options'].optionValueDomDataKey = '__ko.optionValueDomData__';
@@ -2577,7 +2672,7 @@ ko.bindingHandlers['selectedOptions'] = {
2577
2672
  if (node.selected)
2578
2673
  valueToWrite.push(ko.selectExtensions.readValue(node));
2579
2674
  });
2580
- ko.expressionRewriting.writeValueToProperty(value, allBindingsAccessor, 'value', valueToWrite);
2675
+ ko.expressionRewriting.writeValueToProperty(value, allBindingsAccessor, 'selectedOptions', valueToWrite);
2581
2676
  });
2582
2677
  },
2583
2678
  'update': function (element, valueAccessor) {
@@ -2596,12 +2691,10 @@ ko.bindingHandlers['selectedOptions'] = {
2596
2691
  ko.bindingHandlers['style'] = {
2597
2692
  'update': function (element, valueAccessor) {
2598
2693
  var value = ko.utils.unwrapObservable(valueAccessor() || {});
2599
- for (var styleName in value) {
2600
- if (typeof styleName == "string") {
2601
- var styleValue = ko.utils.unwrapObservable(value[styleName]);
2602
- element.style[styleName] = styleValue || ""; // Empty string removes the value, whereas null/undefined have no effect
2603
- }
2604
- }
2694
+ ko.utils.objectForEach(value, function(styleName, styleValue) {
2695
+ styleValue = ko.utils.unwrapObservable(styleValue);
2696
+ element.style[styleName] = styleValue || ""; // Empty string removes the value, whereas null/undefined have no effect
2697
+ });
2605
2698
  }
2606
2699
  };
2607
2700
  ko.bindingHandlers['submit'] = {
@@ -2687,12 +2780,7 @@ ko.bindingHandlers['value'] = {
2687
2780
  var valueIsSelectOption = ko.utils.tagNameLower(element) === "select";
2688
2781
  var newValue = ko.utils.unwrapObservable(valueAccessor());
2689
2782
  var elementValue = ko.selectExtensions.readValue(element);
2690
- var valueHasChanged = (newValue != elementValue);
2691
-
2692
- // 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).
2693
- // 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.
2694
- if ((newValue === 0) && (elementValue !== 0) && (elementValue !== "0"))
2695
- valueHasChanged = true;
2783
+ var valueHasChanged = (newValue !== elementValue);
2696
2784
 
2697
2785
  if (valueHasChanged) {
2698
2786
  var applyValueAction = function () { ko.selectExtensions.writeValue(element, newValue); };
@@ -2797,7 +2885,7 @@ ko.templateEngine.prototype['rewriteTemplate'] = function (template, rewriterCal
2797
2885
  ko.exportSymbol('templateEngine', ko.templateEngine);
2798
2886
 
2799
2887
  ko.templateRewriting = (function () {
2800
- var memoizeDataBindingAttributeSyntaxRegex = /(<[a-z]+\d*(\s+(?!data-bind=)[a-z0-9\-]+(=(\"[^\"]*\"|\'[^\']*\'))?)*\s+)data-bind=(["'])([\s\S]*?)\5/gi;
2888
+ var memoizeDataBindingAttributeSyntaxRegex = /(<([a-z]+\d*)(?:\s+(?!data-bind\s*=\s*)[a-z0-9\-]+(?:=(?:\"[^\"]*\"|\'[^\']*\'))?)*\s+)data-bind\s*=\s*(["'])([\s\S]*?)\3/gi;
2801
2889
  var memoizeVirtualContainerBindingSyntaxRegex = /<!--\s*ko\b\s*([\s\S]*?)\s*-->/g;
2802
2890
 
2803
2891
  function validateDataBindValuesForRewriting(keyValueArray) {
@@ -2818,7 +2906,7 @@ ko.templateRewriting = (function () {
2818
2906
  }
2819
2907
  }
2820
2908
 
2821
- function constructMemoizedTagReplacement(dataBindAttributeValue, tagToRetain, templateEngine) {
2909
+ function constructMemoizedTagReplacement(dataBindAttributeValue, tagToRetain, nodeName, templateEngine) {
2822
2910
  var dataBindKeyValueArray = ko.expressionRewriting.parseObjectLiteral(dataBindAttributeValue);
2823
2911
  validateDataBindValuesForRewriting(dataBindKeyValueArray);
2824
2912
  var rewrittenDataBindAttributeValue = ko.expressionRewriting.preProcessBindings(dataBindKeyValueArray);
@@ -2827,7 +2915,7 @@ ko.templateRewriting = (function () {
2827
2915
  // anonymous function, even though Opera's built-in debugger can evaluate it anyway. No other browser requires this
2828
2916
  // extra indirection.
2829
2917
  var applyBindingsToNextSiblingScript =
2830
- "ko.__tr_ambtns(function($context,$element){return(function(){return{ " + rewrittenDataBindAttributeValue + " } })()})";
2918
+ "ko.__tr_ambtns(function($context,$element){return(function(){return{ " + rewrittenDataBindAttributeValue + " } })()},'" + nodeName.toLowerCase() + "')";
2831
2919
  return templateEngine['createJavaScriptEvaluatorBlock'](applyBindingsToNextSiblingScript) + tagToRetain;
2832
2920
  }
2833
2921
 
@@ -2841,16 +2929,18 @@ ko.templateRewriting = (function () {
2841
2929
 
2842
2930
  memoizeBindingAttributeSyntax: function (htmlString, templateEngine) {
2843
2931
  return htmlString.replace(memoizeDataBindingAttributeSyntaxRegex, function () {
2844
- return constructMemoizedTagReplacement(/* dataBindAttributeValue: */ arguments[6], /* tagToRetain: */ arguments[1], templateEngine);
2932
+ return constructMemoizedTagReplacement(/* dataBindAttributeValue: */ arguments[4], /* tagToRetain: */ arguments[1], /* nodeName: */ arguments[2], templateEngine);
2845
2933
  }).replace(memoizeVirtualContainerBindingSyntaxRegex, function() {
2846
- return constructMemoizedTagReplacement(/* dataBindAttributeValue: */ arguments[1], /* tagToRetain: */ "<!-- ko -->", templateEngine);
2934
+ return constructMemoizedTagReplacement(/* dataBindAttributeValue: */ arguments[1], /* tagToRetain: */ "<!-- ko -->", /* nodeName: */ "#comment", templateEngine);
2847
2935
  });
2848
2936
  },
2849
2937
 
2850
- applyMemoizedBindingsToNextSibling: function (bindings) {
2938
+ applyMemoizedBindingsToNextSibling: function (bindings, nodeName) {
2851
2939
  return ko.memoization.memoize(function (domNode, bindingContext) {
2852
- if (domNode.nextSibling)
2853
- ko.applyBindingsToNode(domNode.nextSibling, bindings, bindingContext);
2940
+ var nodeToBind = domNode.nextSibling;
2941
+ if (nodeToBind && nodeToBind.nodeName.toLowerCase() === nodeName) {
2942
+ ko.applyBindingsToNode(nodeToBind, bindings, bindingContext);
2943
+ }
2854
2944
  });
2855
2945
  }
2856
2946
  }
@@ -2927,6 +3017,7 @@ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextS
2927
3017
  this.domElement = element;
2928
3018
  }
2929
3019
  ko.templateSources.anonymousTemplate.prototype = new ko.templateSources.domElement();
3020
+ ko.templateSources.anonymousTemplate.prototype.constructor = ko.templateSources.anonymousTemplate;
2930
3021
  ko.templateSources.anonymousTemplate.prototype['text'] = function(/* valueToWrite */) {
2931
3022
  if (arguments.length == 0) {
2932
3023
  var templateData = ko.utils.domData.get(this.domElement, anonymousTemplatesDomDataKey) || {};
@@ -3138,7 +3229,7 @@ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextS
3138
3229
 
3139
3230
  if (typeof templateName != "string") {
3140
3231
  options = templateName;
3141
- templateName = options['name'];
3232
+ templateName = ko.utils.unwrapObservable(options['name']);
3142
3233
 
3143
3234
  // Support "if"/"ifnot" conditions
3144
3235
  if ('if' in options)
@@ -3330,11 +3421,11 @@ ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);
3330
3421
  // Map this array value inside a dependentObservable so we re-map when any dependency changes
3331
3422
  var mappedNodes = [];
3332
3423
  var dependentObservable = ko.dependentObservable(function() {
3333
- var newMappedNodes = mapping(valueToMap, index) || [];
3424
+ var newMappedNodes = mapping(valueToMap, index, fixUpNodesToBeMovedOrRemoved(mappedNodes)) || [];
3334
3425
 
3335
3426
  // On subsequent evaluations, just replace the previously-inserted DOM nodes
3336
3427
  if (mappedNodes.length > 0) {
3337
- ko.utils.replaceDomNodes(fixUpNodesToBeMovedOrRemoved(mappedNodes), newMappedNodes);
3428
+ ko.utils.replaceDomNodes(mappedNodes, newMappedNodes);
3338
3429
  if (callbackAfterAddingNodes)
3339
3430
  ko.dependencyDetection.ignore(callbackAfterAddingNodes, null, [valueToMap, newMappedNodes, index]);
3340
3431
  }
@@ -3343,7 +3434,7 @@ ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);
3343
3434
  // of which nodes would be deleted if valueToMap was itself later removed
3344
3435
  mappedNodes.splice(0, mappedNodes.length);
3345
3436
  ko.utils.arrayPushAll(mappedNodes, newMappedNodes);
3346
- }, null, { disposeWhenNodeIsRemoved: containerNode, disposeWhen: function() { return (mappedNodes.length == 0) || !ko.utils.domNodeIsAttachedToDocument(mappedNodes[0]) } });
3437
+ }, null, { disposeWhenNodeIsRemoved: containerNode, disposeWhen: function() { return !ko.utils.anyDomNodeIsAttachedToDocument(mappedNodes); } });
3347
3438
  return { mappedNodes : mappedNodes, dependentObservable : (dependentObservable.isActive() ? dependentObservable : undefined) };
3348
3439
  }
3349
3440
 
@@ -3356,7 +3447,7 @@ ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);
3356
3447
  var isFirstExecution = ko.utils.domData.get(domNode, lastMappingResultDomDataKey) === undefined;
3357
3448
  var lastMappingResult = ko.utils.domData.get(domNode, lastMappingResultDomDataKey) || [];
3358
3449
  var lastArray = ko.utils.arrayMap(lastMappingResult, function (x) { return x.arrayEntry; });
3359
- var editScript = ko.utils.compareArrays(lastArray, array);
3450
+ var editScript = ko.utils.compareArrays(lastArray, array, options['dontLimitMoves']);
3360
3451
 
3361
3452
  // Build the new mapping result
3362
3453
  var newMappingResult = [];
@@ -3479,6 +3570,7 @@ ko.nativeTemplateEngine = function () {
3479
3570
  }
3480
3571
 
3481
3572
  ko.nativeTemplateEngine.prototype = new ko.templateEngine();
3573
+ ko.nativeTemplateEngine.prototype.constructor = ko.nativeTemplateEngine;
3482
3574
  ko.nativeTemplateEngine.prototype['renderTemplateSource'] = function (templateSource, bindingContext, options) {
3483
3575
  var useNodesIfAvailable = !(ko.utils.ieVersion < 9), // IE<9 cloneNode doesn't work properly
3484
3576
  templateNodesFunc = useNodesIfAvailable ? templateSource['nodes'] : null,
@@ -3555,7 +3647,7 @@ ko.exportSymbol('nativeTemplateEngine', ko.nativeTemplateEngine);
3555
3647
  };
3556
3648
 
3557
3649
  this['addTemplate'] = function(templateName, templateMarkup) {
3558
- document.write("<script type='text/html' id='" + templateName + "'>" + templateMarkup + "</script>");
3650
+ document.write("<script type='text/html' id='" + templateName + "'>" + templateMarkup + "<" + "/script>");
3559
3651
  };
3560
3652
 
3561
3653
  if (jQueryTmplVersion > 0) {
@@ -3570,6 +3662,7 @@ ko.exportSymbol('nativeTemplateEngine', ko.nativeTemplateEngine);
3570
3662
  };
3571
3663
 
3572
3664
  ko.jqueryTmplTemplateEngine.prototype = new ko.templateEngine();
3665
+ ko.jqueryTmplTemplateEngine.prototype.constructor = ko.jqueryTmplTemplateEngine;
3573
3666
 
3574
3667
  // Use this one by default *only if jquery.tmpl is referenced*
3575
3668
  var jqueryTmplTemplateEngineInstance = new ko.jqueryTmplTemplateEngine();
@@ -3578,6 +3671,6 @@ ko.exportSymbol('nativeTemplateEngine', ko.nativeTemplateEngine);
3578
3671
 
3579
3672
  ko.exportSymbol('jqueryTmplTemplateEngine', ko.jqueryTmplTemplateEngine);
3580
3673
  })();
3581
- });
3582
- })(window,document,navigator,window["jQuery"]);
3674
+ }));
3675
+ }());
3583
3676
  })();
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: knockoutjs-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.1
4
+ version: 2.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-02-26 00:00:00.000000000 Z
12
+ date: 2013-07-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: railties
@@ -67,7 +67,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
67
67
  version: '0'
68
68
  requirements: []
69
69
  rubyforge_project:
70
- rubygems_version: 1.8.23
70
+ rubygems_version: 1.8.24
71
71
  signing_key:
72
72
  specification_version: 3
73
73
  summary: The Knockout.js JavaScript library ready for the Rails asset pipeline.