diff --git a/src/Orchard.Web/Modules/Orchard.Resources/Assets/Js/Knockout/knockout.js b/src/Orchard.Web/Modules/Orchard.Resources/Assets/Js/Knockout/knockout.js
index 3bbeb22af..74b89916e 100644
--- a/src/Orchard.Web/Modules/Orchard.Resources/Assets/Js/Knockout/knockout.js
+++ b/src/Orchard.Web/Modules/Orchard.Resources/Assets/Js/Knockout/knockout.js
@@ -1,6 +1,6 @@
/*!
- * Knockout JavaScript library v3.4.0
- * (c) Steven Sanderson - http://knockoutjs.com/
+ * Knockout JavaScript library v3.5.1
+ * (c) The Knockout.js team - http://knockoutjs.com/
* License: MIT (http://www.opensource.org/licenses/mit-license.php)
*/
@@ -14,6 +14,10 @@ var DEBUG=true;
navigator = window['navigator'],
jQueryInstance = window["jQuery"],
JSON = window["JSON"];
+
+ if (!jQueryInstance && typeof jQuery !== "undefined") {
+ jQueryInstance = jQuery;
+ }
(function(factory) {
// Support three module loading scenarios
if (typeof define === 'function' && define['amd']) {
@@ -45,20 +49,23 @@ ko.exportSymbol = function(koPath, object) {
ko.exportProperty = function(owner, publicName, object) {
owner[publicName] = object;
};
-ko.version = "3.4.0";
+ko.version = "3.5.1";
ko.exportSymbol('version', ko.version);
// For any options that may affect various areas of Knockout and aren't directly associated with data binding.
ko.options = {
'deferUpdates': false,
- 'useOnlyNativeEvents': false
+ 'useOnlyNativeEvents': false,
+ 'foreachHidesDestroyed': false
};
//ko.exportSymbol('options', ko.options); // 'options' isn't minified
ko.utils = (function () {
+ var hasOwnProperty = Object.prototype.hasOwnProperty;
+
function objectForEach(obj, action) {
for (var prop in obj) {
- if (obj.hasOwnProperty(prop)) {
+ if (hasOwnProperty.call(obj, prop)) {
action(prop, obj[prop]);
}
}
@@ -67,7 +74,7 @@ ko.utils = (function () {
function extend(target, source) {
if (source) {
for(var prop in source) {
- if(source.hasOwnProperty(prop)) {
+ if(hasOwnProperty.call(source, prop)) {
target[prop] = source[prop];
}
}
@@ -124,6 +131,8 @@ ko.utils = (function () {
// see: https://github.com/knockout/knockout/issues/1597
var cssClassNameRegex = /\S+/g;
+ var jQueryEventAttachName;
+
function toggleDomNodeCssClass(node, classNames, shouldHaveClass) {
var addOrRemoveFn;
if (classNames) {
@@ -154,25 +163,30 @@ ko.utils = (function () {
return {
fieldsIncludedWithJsonPost: ['authenticity_token', /^__RequestVerificationToken(_.*)?$/],
- arrayForEach: function (array, action) {
- for (var i = 0, j = array.length; i < j; i++)
- action(array[i], i);
+ arrayForEach: function (array, action, actionOwner) {
+ for (var i = 0, j = array.length; i < j; i++) {
+ action.call(actionOwner, array[i], i, array);
+ }
},
- arrayIndexOf: function (array, item) {
- if (typeof Array.prototype.indexOf == "function")
+ arrayIndexOf: typeof Array.prototype.indexOf == "function"
+ ? function (array, item) {
return Array.prototype.indexOf.call(array, item);
- for (var i = 0, j = array.length; i < j; i++)
- if (array[i] === item)
- return i;
- return -1;
- },
+ }
+ : function (array, item) {
+ for (var i = 0, j = array.length; i < j; i++) {
+ if (array[i] === item)
+ return i;
+ }
+ return -1;
+ },
arrayFirst: function (array, predicate, predicateOwner) {
- for (var i = 0, j = array.length; i < j; i++)
- if (predicate.call(predicateOwner, array[i], i))
+ for (var i = 0, j = array.length; i < j; i++) {
+ if (predicate.call(predicateOwner, array[i], i, array))
return array[i];
- return null;
+ }
+ return undefined;
},
arrayRemoveItem: function (array, itemToRemove) {
@@ -186,29 +200,32 @@ ko.utils = (function () {
},
arrayGetDistinctValues: function (array) {
- array = array || [];
var result = [];
- for (var i = 0, j = array.length; i < j; i++) {
- if (ko.utils.arrayIndexOf(result, array[i]) < 0)
- result.push(array[i]);
+ if (array) {
+ ko.utils.arrayForEach(array, function(item) {
+ if (ko.utils.arrayIndexOf(result, item) < 0)
+ result.push(item);
+ });
}
return result;
},
- arrayMap: function (array, mapping) {
- array = array || [];
+ arrayMap: function (array, mapping, mappingOwner) {
var result = [];
- for (var i = 0, j = array.length; i < j; i++)
- result.push(mapping(array[i], i));
+ if (array) {
+ for (var i = 0, j = array.length; i < j; i++)
+ result.push(mapping.call(mappingOwner, array[i], i));
+ }
return result;
},
- arrayFilter: function (array, predicate) {
- array = array || [];
+ arrayFilter: function (array, predicate, predicateOwner) {
var result = [];
- for (var i = 0, j = array.length; i < j; i++)
- if (predicate(array[i], i))
- result.push(array[i]);
+ if (array) {
+ for (var i = 0, j = array.length; i < j; i++)
+ if (predicate.call(predicateOwner, array[i], i))
+ result.push(array[i]);
+ }
return result;
},
@@ -242,13 +259,13 @@ ko.utils = (function () {
objectForEach: objectForEach,
- objectMap: function(source, mapping) {
+ objectMap: function(source, mapping, mappingOwner) {
if (!source)
return source;
var target = {};
for (var prop in source) {
- if (source.hasOwnProperty(prop)) {
- target[prop] = mapping(source[prop], prop, source);
+ if (hasOwnProperty.call(source, prop)) {
+ target[prop] = mapping.call(mappingOwner, source[prop], prop, source);
}
}
return target;
@@ -374,7 +391,7 @@ ko.utils = (function () {
if (node.nodeType === 11)
return false; // Fixes issue #1162 - can't use node.contains for document fragments on IE8
if (containedByNode.contains)
- return containedByNode.contains(node.nodeType === 3 ? node.parentNode : node);
+ return containedByNode.contains(node.nodeType !== 1 ? node.parentNode : node);
if (containedByNode.compareDocumentPosition)
return (containedByNode.compareDocumentPosition(node) & 16) == 16;
while (node && node != containedByNode) {
@@ -423,9 +440,12 @@ ko.utils = (function () {
registerEventHandler: function (element, eventType, handler) {
var wrappedHandler = ko.utils.catchFunctionErrors(handler);
- var mustUseAttachEvent = ieVersion && eventsThatMustBeRegisteredUsingAttachEvent[eventType];
+ var mustUseAttachEvent = eventsThatMustBeRegisteredUsingAttachEvent[eventType];
if (!ko.options['useOnlyNativeEvents'] && !mustUseAttachEvent && jQueryInstance) {
- jQueryInstance(element)['bind'](eventType, wrappedHandler);
+ if (!jQueryEventAttachName) {
+ jQueryEventAttachName = (typeof jQueryInstance(element)['on'] == 'function') ? 'on' : 'bind';
+ }
+ jQueryInstance(element)[jQueryEventAttachName](eventType, wrappedHandler);
} else if (!mustUseAttachEvent && typeof element.addEventListener == "function")
element.addEventListener(eventType, wrappedHandler, false);
else if (typeof element.attachEvent != "undefined") {
@@ -508,7 +528,8 @@ ko.utils = (function () {
// - http://www.matts411.com/post/setting_the_name_attribute_in_ie_dom/
if (ieVersion <= 7) {
try {
- element.mergeAttributes(document.createElement(""), false);
+ var escapedName = element.name.replace(/[&<>'"]/g, function(r){ return "" + r.charCodeAt(0) + ";"; });
+ element.mergeAttributes(document.createElement(""), false);
}
catch(e) {} // For IE9 with doc mode "IE9 Standards" and browser mode "IE9 Compatibility View"
}
@@ -644,9 +665,12 @@ ko.exportSymbol('utils.arrayIndexOf', ko.utils.arrayIndexOf);
ko.exportSymbol('utils.arrayMap', ko.utils.arrayMap);
ko.exportSymbol('utils.arrayPushAll', ko.utils.arrayPushAll);
ko.exportSymbol('utils.arrayRemoveItem', ko.utils.arrayRemoveItem);
+ko.exportSymbol('utils.cloneNodes', ko.utils.cloneNodes);
+ko.exportSymbol('utils.createSymbolOrString', ko.utils.createSymbolOrString);
ko.exportSymbol('utils.extend', ko.utils.extend);
ko.exportSymbol('utils.fieldsIncludedWithJsonPost', ko.utils.fieldsIncludedWithJsonPost);
ko.exportSymbol('utils.getFormFields', ko.utils.getFormFields);
+ko.exportSymbol('utils.objectMap', ko.utils.objectMap);
ko.exportSymbol('utils.peekObservable', ko.utils.peekObservable);
ko.exportSymbol('utils.postJson', ko.utils.postJson);
ko.exportSymbol('utils.parseJson', ko.utils.parseJson);
@@ -686,33 +710,40 @@ ko.utils.domData = new (function () {
var dataStoreKeyExpandoPropertyName = "__ko__" + (new Date).getTime();
var dataStore = {};
- function getAll(node, createIfNotFound) {
- var dataStoreKey = node[dataStoreKeyExpandoPropertyName];
- var hasExistingDataStore = dataStoreKey && (dataStoreKey !== "null") && dataStore[dataStoreKey];
- if (!hasExistingDataStore) {
- if (!createIfNotFound)
- return undefined;
- dataStoreKey = node[dataStoreKeyExpandoPropertyName] = "ko" + uniqueId++;
- dataStore[dataStoreKey] = {};
- }
- return dataStore[dataStoreKey];
- }
-
- return {
- get: function (node, key) {
- var allDataForNode = getAll(node, false);
- return allDataForNode === undefined ? undefined : allDataForNode[key];
- },
- set: function (node, key, value) {
- if (value === undefined) {
- // Make sure we don't actually create a new domData key if we are actually deleting a value
- if (getAll(node, false) === undefined)
- return;
+ var getDataForNode, clear;
+ if (!ko.utils.ieVersion) {
+ // We considered using WeakMap, but it has a problem in IE 11 and Edge that prevents using
+ // it cross-window, so instead we just store the data directly on the node.
+ // See https://github.com/knockout/knockout/issues/2141
+ getDataForNode = function (node, createIfNotFound) {
+ var dataForNode = node[dataStoreKeyExpandoPropertyName];
+ if (!dataForNode && createIfNotFound) {
+ dataForNode = node[dataStoreKeyExpandoPropertyName] = {};
}
- var allDataForNode = getAll(node, true);
- allDataForNode[key] = value;
- },
- clear: function (node) {
+ return dataForNode;
+ };
+ clear = function (node) {
+ if (node[dataStoreKeyExpandoPropertyName]) {
+ delete node[dataStoreKeyExpandoPropertyName];
+ return true; // Exposing "did clean" flag purely so specs can infer whether things have been cleaned up as intended
+ }
+ return false;
+ };
+ } else {
+ // Old IE versions have memory issues if you store objects on the node, so we use a
+ // separate data storage and link to it from the node using a string key.
+ getDataForNode = function (node, createIfNotFound) {
+ var dataStoreKey = node[dataStoreKeyExpandoPropertyName];
+ var hasExistingDataStore = dataStoreKey && (dataStoreKey !== "null") && dataStore[dataStoreKey];
+ if (!hasExistingDataStore) {
+ if (!createIfNotFound)
+ return undefined;
+ dataStoreKey = node[dataStoreKeyExpandoPropertyName] = "ko" + uniqueId++;
+ dataStore[dataStoreKey] = {};
+ }
+ return dataStore[dataStoreKey];
+ };
+ clear = function (node) {
var dataStoreKey = node[dataStoreKeyExpandoPropertyName];
if (dataStoreKey) {
delete dataStore[dataStoreKey];
@@ -720,7 +751,24 @@ ko.utils.domData = new (function () {
return true; // Exposing "did clean" flag purely so specs can infer whether things have been cleaned up as intended
}
return false;
+ };
+ }
+
+ return {
+ get: function (node, key) {
+ var dataForNode = getDataForNode(node, false);
+ return dataForNode && dataForNode[key];
},
+ set: function (node, key, value) {
+ // Make sure we don't actually create a new domData key if we are actually deleting a value
+ var dataForNode = getDataForNode(node, value !== undefined /* createIfNotFound */);
+ dataForNode && (dataForNode[key] = value);
+ },
+ getOrSet: function (node, key, value) {
+ var dataForNode = getDataForNode(node, true /* createIfNotFound */);
+ return dataForNode[key] || (dataForNode[key] = value);
+ },
+ clear: clear,
nextKey: function () {
return (uniqueId++) + dataStoreKeyExpandoPropertyName;
@@ -765,16 +813,20 @@ ko.utils.domNodeDisposal = new (function () {
// Clear any immediate-child comment nodes, as these wouldn't have been found by
// node.getElementsByTagName("*") in cleanNode() (comment nodes aren't elements)
- if (cleanableNodeTypesWithDescendants[node.nodeType])
- cleanImmediateCommentTypeChildren(node);
+ if (cleanableNodeTypesWithDescendants[node.nodeType]) {
+ cleanNodesInList(node.childNodes, true/*onlyComments*/);
+ }
}
- function cleanImmediateCommentTypeChildren(nodeWithChildren) {
- var child, nextChild = nodeWithChildren.firstChild;
- while (child = nextChild) {
- nextChild = child.nextSibling;
- if (child.nodeType === 8)
- cleanSingleNode(child);
+ function cleanNodesInList(nodeList, onlyComments) {
+ var cleanedNodes = [], lastCleanedNode;
+ for (var i = 0; i < nodeList.length; i++) {
+ if (!onlyComments || nodeList[i].nodeType === 8) {
+ cleanSingleNode(cleanedNodes[cleanedNodes.length] = lastCleanedNode = nodeList[i]);
+ if (nodeList[i] !== lastCleanedNode) {
+ while (i-- && ko.utils.arrayIndexOf(cleanedNodes, nodeList[i]) == -1) {}
+ }
+ }
}
}
@@ -795,19 +847,18 @@ ko.utils.domNodeDisposal = new (function () {
},
cleanNode : function(node) {
- // First clean this node, where applicable
- if (cleanableNodeTypes[node.nodeType]) {
- cleanSingleNode(node);
+ ko.dependencyDetection.ignore(function () {
+ // First clean this node, where applicable
+ if (cleanableNodeTypes[node.nodeType]) {
+ cleanSingleNode(node);
- // ... then its descendants, where applicable
- if (cleanableNodeTypesWithDescendants[node.nodeType]) {
- // Clone the descendants list in case it changes during iteration
- var descendants = [];
- ko.utils.arrayPushAll(descendants, node.getElementsByTagName("*"));
- for (var i = 0, j = descendants.length; i < j; i++)
- cleanSingleNode(descendants[i]);
+ // ... then its descendants, where applicable
+ if (cleanableNodeTypesWithDescendants[node.nodeType]) {
+ cleanNodesInList(node.getElementsByTagName("*"));
+ }
}
- }
+ });
+
return node;
},
@@ -854,7 +905,7 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD
mayRequireCreateElementHack = ko.utils.ieVersion <= 8;
function getWrap(tags) {
- var m = tags.match(/^<([a-z]+)[ >]/);
+ var m = tags.match(/^(?:\s*?)*?<([a-z]+)[\s>]/);
return (m && lookup[m[1]]) || none;
}
@@ -887,7 +938,7 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD
if (mayRequireCreateElementHack) {
// The document.createElement('my-element') trick to enable custom elements in IE6-8
// only works if we assign innerHTML on an element associated with that document.
- documentContext.appendChild(div);
+ documentContext.body.appendChild(div);
}
div.innerHTML = markup;
@@ -935,6 +986,11 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD
simpleHtmlParse(html, documentContext); // ... otherwise, this simple logic will do in most common cases.
};
+ ko.utils.parseHtmlForTemplateNodes = function(html, documentContext) {
+ var nodes = ko.utils.parseHtmlFragment(html, documentContext);
+ return (nodes.length && nodes[0].parentElement) || ko.utils.moveCleanedNodesToContainerElement(nodes);
+ };
+
ko.utils.setHtml = function(node, html) {
ko.utils.emptyDomNode(node);
@@ -1175,9 +1231,9 @@ ko.extenders = {
// rateLimit supersedes deferred updates
target._deferUpdates = false;
- limitFunction = method == 'notifyWhenChangesStop' ? debounce : throttle;
+ limitFunction = typeof method == 'function' ? method : method == 'notifyWhenChangesStop' ? debounce : throttle;
target.limit(function(callback) {
- return limitFunction(callback, timeout);
+ return limitFunction(callback, timeout, options);
});
},
@@ -1189,11 +1245,20 @@ ko.extenders = {
if (!target._deferUpdates) {
target._deferUpdates = true;
target.limit(function (callback) {
- var handle;
+ var handle,
+ ignoreUpdates = false;
return function () {
- ko.tasks.cancel(handle);
- handle = ko.tasks.schedule(callback);
- target['notifySubscribers'](undefined, 'dirty');
+ if (!ignoreUpdates) {
+ ko.tasks.cancel(handle);
+ handle = ko.tasks.schedule(callback);
+
+ try {
+ ignoreUpdates = true;
+ target['notifySubscribers'](undefined, 'dirty');
+ } finally {
+ ignoreUpdates = false;
+ }
+ }
};
});
}
@@ -1249,14 +1314,29 @@ ko.exportSymbol('extenders', ko.extenders);
ko.subscription = function (target, callback, disposeCallback) {
this._target = target;
- this.callback = callback;
- this.disposeCallback = disposeCallback;
- this.isDisposed = false;
+ this._callback = callback;
+ this._disposeCallback = disposeCallback;
+ this._isDisposed = false;
+ this._node = null;
+ this._domNodeDisposalCallback = null;
ko.exportProperty(this, 'dispose', this.dispose);
+ ko.exportProperty(this, 'disposeWhenNodeIsRemoved', this.disposeWhenNodeIsRemoved);
};
ko.subscription.prototype.dispose = function () {
- this.isDisposed = true;
- this.disposeCallback();
+ var self = this;
+ if (!self._isDisposed) {
+ if (self._domNodeDisposalCallback) {
+ ko.utils.domNodeDisposal.removeDisposeCallback(self._node, self._domNodeDisposalCallback);
+ }
+ self._isDisposed = true;
+ self._disposeCallback();
+
+ self._target = self._callback = self._disposeCallback = self._node = self._domNodeDisposalCallback = null;
+ }
+};
+ko.subscription.prototype.disposeWhenNodeIsRemoved = function (node) {
+ this._node = node;
+ ko.utils.domNodeDisposal.addDisposeCallback(node, this._domNodeDisposalCallback = this.dispose.bind(this));
};
ko.subscribable = function () {
@@ -1279,7 +1359,7 @@ function limitNotifySubscribers(value, event) {
var ko_subscribable_fn = {
init: function(instance) {
- instance._subscriptions = {};
+ instance._subscriptions = { "change": [] };
instance._versionNumber = 1;
},
@@ -1311,13 +1391,14 @@ var ko_subscribable_fn = {
this.updateVersion();
}
if (this.hasSubscriptionsForEvent(event)) {
+ var subs = event === defaultEvent && this._changeSubscriptions || this._subscriptions[event].slice(0);
try {
ko.dependencyDetection.begin(); // Begin suppressing dependency detection (by setting the top frame to undefined)
- for (var a = this._subscriptions[event].slice(0), i = 0, subscription; subscription = a[i]; ++i) {
+ for (var i = 0, subscription; subscription = subs[i]; ++i) {
// In case a subscription was disposed during the arrayForEach cycle, check
// for isDisposed on each subscription before invoking its callback
- if (!subscription.isDisposed)
- subscription.callback(valueToNotify);
+ if (!subscription._isDisposed)
+ subscription._callback(valueToNotify);
}
} finally {
ko.dependencyDetection.end(); // End suppressing dependency detection
@@ -1339,7 +1420,8 @@ var ko_subscribable_fn = {
limit: function(limitFunction) {
var self = this, selfIsObservable = ko.isObservable(self),
- ignoreBeforeChange, previousValue, pendingValue, beforeChange = 'beforeChange';
+ ignoreBeforeChange, notifyNextChange, previousValue, pendingValue, didUpdate,
+ beforeChange = 'beforeChange';
if (!self._origNotifySubscribers) {
self._origNotifySubscribers = self["notifySubscribers"];
@@ -1352,15 +1434,22 @@ var ko_subscribable_fn = {
// If an observable provided a reference to itself, access it to get the latest value.
// This allows computed observables to delay calculating their value until needed.
if (selfIsObservable && pendingValue === self) {
- pendingValue = self();
+ pendingValue = self._evalIfChanged ? self._evalIfChanged() : self();
}
- ignoreBeforeChange = false;
- if (self.isDifferent(previousValue, pendingValue)) {
+ var shouldNotify = notifyNextChange || (didUpdate && self.isDifferent(previousValue, pendingValue));
+
+ didUpdate = notifyNextChange = ignoreBeforeChange = false;
+
+ if (shouldNotify) {
self._origNotifySubscribers(previousValue = pendingValue);
}
});
- self._limitChange = function(value) {
+ self._limitChange = function(value, isDirty) {
+ if (!isDirty || !self._notificationIsPending) {
+ didUpdate = !isDirty;
+ }
+ self._changeSubscriptions = self._subscriptions[defaultEvent].slice(0);
self._notificationIsPending = ignoreBeforeChange = true;
pendingValue = value;
finish();
@@ -1371,6 +1460,14 @@ var ko_subscribable_fn = {
self._origNotifySubscribers(value, beforeChange);
}
};
+ self._recordUpdate = function() {
+ didUpdate = true;
+ };
+ self._notifyNextChangeIfValueIsDifferent = function() {
+ if (self.isDifferent(previousValue, self.peek(true /*evaluate*/))) {
+ notifyNextChange = true;
+ }
+ };
},
hasSubscriptionsForEvent: function(event) {
@@ -1394,9 +1491,14 @@ var ko_subscribable_fn = {
return !this['equalityComparer'] || !this['equalityComparer'](oldValue, newValue);
},
+ toString: function() {
+ return '[object Object]'
+ },
+
extend: applyExtenders
};
+ko.exportProperty(ko_subscribable_fn, 'init', ko_subscribable_fn.init);
ko.exportProperty(ko_subscribable_fn, 'subscribe', ko_subscribable_fn.subscribe);
ko.exportProperty(ko_subscribable_fn, 'extend', ko_subscribable_fn.extend);
ko.exportProperty(ko_subscribable_fn, 'getSubscriptionsCount', ko_subscribable_fn.getSubscriptionsCount);
@@ -1469,16 +1571,28 @@ ko.computedContext = ko.dependencyDetection = (function () {
return currentFrame.computed.getDependenciesCount();
},
+ getDependencies: function () {
+ if (currentFrame)
+ return currentFrame.computed.getDependencies();
+ },
+
isInitial: function() {
if (currentFrame)
return currentFrame.isInitial;
+ },
+
+ computed: function() {
+ if (currentFrame)
+ return currentFrame.computed;
}
};
})();
ko.exportSymbol('computedContext', ko.computedContext);
ko.exportSymbol('computedContext.getDependenciesCount', ko.computedContext.getDependenciesCount);
+ko.exportSymbol('computedContext.getDependencies', ko.computedContext.getDependencies);
ko.exportSymbol('computedContext.isInitial', ko.computedContext.isInitial);
+ko.exportSymbol('computedContext.registerDependency', ko.computedContext.registerDependency);
ko.exportSymbol('ignoreDependencies', ko.ignoreDependencies = ko.dependencyDetection.ignore);
var observableLatestValue = ko.utils.createSymbolOrString('_latestValue');
@@ -1526,7 +1640,10 @@ ko.observable = function (initialValue) {
var observableFn = {
'equalityComparer': valuesArePrimitiveAndEqual,
peek: function() { return this[observableLatestValue]; },
- valueHasMutated: function () { this['notifySubscribers'](this[observableLatestValue]); },
+ valueHasMutated: function () {
+ this['notifySubscribers'](this[observableLatestValue], 'spectate');
+ this['notifySubscribers'](this[observableLatestValue]);
+ },
valueWillMutate: function () { this['notifySubscribers'](this[observableLatestValue], 'beforeChange'); }
};
@@ -1539,25 +1656,19 @@ if (ko.utils.canSetPrototype) {
var protoProperty = ko.observable.protoProperty = '__ko_proto__';
observableFn[protoProperty] = ko.observable;
-ko.hasPrototype = function(instance, prototype) {
- if ((instance === null) || (instance === undefined) || (instance[protoProperty] === undefined)) return false;
- if (instance[protoProperty] === prototype) return true;
- return ko.hasPrototype(instance[protoProperty], prototype); // Walk the prototype chain
+ko.isObservable = function (instance) {
+ var proto = typeof instance == 'function' && instance[protoProperty];
+ if (proto && proto !== observableFn[protoProperty] && proto !== ko.computed['fn'][protoProperty]) {
+ throw Error("Invalid object that looks like an observable; possibly from another Knockout instance");
+ }
+ return !!proto;
};
-ko.isObservable = function (instance) {
- return ko.hasPrototype(instance, ko.observable);
-}
ko.isWriteableObservable = function (instance) {
- // Observable
- if ((typeof instance == 'function') && instance[protoProperty] === ko.observable)
- return true;
- // Writeable dependent observable
- if ((typeof instance == 'function') && (instance[protoProperty] === ko.dependentObservable) && (instance.hasWriteFunction))
- return true;
- // Anything else
- return false;
-}
+ return (typeof instance == 'function' && (
+ (instance[protoProperty] === observableFn[protoProperty]) || // Observable
+ (instance[protoProperty] === ko.computed['fn'][protoProperty] && instance.hasWriteFunction))); // Writable computed observable
+};
ko.exportSymbol('observable', ko.observable);
ko.exportSymbol('isObservable', ko.isObservable);
@@ -1589,6 +1700,9 @@ ko.observableArray['fn'] = {
if (removedValues.length === 0) {
this.valueWillMutate();
}
+ if (underlyingArray[i] !== value) {
+ throw Error("Array modified during remove; cannot remove item");
+ }
removedValues.push(value);
underlyingArray.splice(i, 1);
i--;
@@ -1625,7 +1739,7 @@ ko.observableArray['fn'] = {
for (var i = underlyingArray.length - 1; i >= 0; i--) {
var value = underlyingArray[i];
if (predicate(value))
- underlyingArray[i]["_destroy"] = true;
+ value["_destroy"] = true;
}
this.valueHasMutated();
},
@@ -1655,6 +1769,15 @@ ko.observableArray['fn'] = {
this.peek()[index] = newItem;
this.valueHasMutated();
}
+ },
+
+ 'sorted': function (compareFunction) {
+ var arrayCopy = this().slice(0);
+ return compareFunction ? arrayCopy.sort(compareFunction) : arrayCopy.sort();
+ },
+
+ 'reversed': function () {
+ return this().slice(0).reverse();
}
};
@@ -1689,7 +1812,14 @@ ko.utils.arrayForEach(["slice"], function (methodName) {
};
});
+ko.isObservableArray = function (instance) {
+ return ko.isObservable(instance)
+ && typeof instance["remove"] == "function"
+ && typeof instance["push"] == "function";
+};
+
ko.exportSymbol('observableArray', ko.observableArray);
+ko.exportSymbol('isObservableArray', ko.isObservableArray);
var arrayChangeEventName = 'arrayChange';
ko.extenders['trackArrayChanges'] = function(target, options) {
// Use the provided options--each call to trackArrayChanges overwrites the previously set options
@@ -1705,76 +1835,89 @@ ko.extenders['trackArrayChanges'] = function(target, options) {
}
var trackingChanges = false,
cachedDiff = null,
- arrayChangeSubscription,
- pendingNotifications = 0,
+ changeSubscription,
+ spectateSubscription,
+ pendingChanges = 0,
+ previousContents,
underlyingBeforeSubscriptionAddFunction = target.beforeSubscriptionAdd,
underlyingAfterSubscriptionRemoveFunction = target.afterSubscriptionRemove;
// Watch "subscribe" calls, and for array change events, ensure change tracking is enabled
target.beforeSubscriptionAdd = function (event) {
- if (underlyingBeforeSubscriptionAddFunction)
+ if (underlyingBeforeSubscriptionAddFunction) {
underlyingBeforeSubscriptionAddFunction.call(target, event);
+ }
if (event === arrayChangeEventName) {
trackChanges();
}
};
// Watch "dispose" calls, and for array change events, ensure change tracking is disabled when all are disposed
target.afterSubscriptionRemove = function (event) {
- if (underlyingAfterSubscriptionRemoveFunction)
+ if (underlyingAfterSubscriptionRemoveFunction) {
underlyingAfterSubscriptionRemoveFunction.call(target, event);
+ }
if (event === arrayChangeEventName && !target.hasSubscriptionsForEvent(arrayChangeEventName)) {
- arrayChangeSubscription.dispose();
+ if (changeSubscription) {
+ changeSubscription.dispose();
+ }
+ if (spectateSubscription) {
+ spectateSubscription.dispose();
+ }
+ spectateSubscription = changeSubscription = null;
trackingChanges = false;
+ previousContents = undefined;
}
};
function trackChanges() {
- // Calling 'trackChanges' multiple times is the same as calling it once
if (trackingChanges) {
+ // Whenever there's a new subscription and there are pending notifications, make sure all previous
+ // subscriptions are notified of the change so that all subscriptions are in sync.
+ notifyChanges();
return;
}
trackingChanges = true;
- // Intercept "notifySubscribers" to track how many times it was called.
- var underlyingNotifySubscribersFunction = target['notifySubscribers'];
- target['notifySubscribers'] = function(valueToNotify, event) {
- if (!event || event === defaultEvent) {
- ++pendingNotifications;
- }
- return underlyingNotifySubscribersFunction.apply(this, arguments);
- };
+ // Track how many times the array actually changed value
+ spectateSubscription = target.subscribe(function () {
+ ++pendingChanges;
+ }, null, "spectate");
// Each time the array changes value, capture a clone so that on the next
// change it's possible to produce a diff
- var previousContents = [].concat(target.peek() || []);
+ previousContents = [].concat(target.peek() || []);
cachedDiff = null;
- arrayChangeSubscription = target.subscribe(function(currentContents) {
- // Make a copy of the current contents and ensure it's an array
- currentContents = [].concat(currentContents || []);
+ changeSubscription = target.subscribe(notifyChanges);
- // Compute the diff and issue notifications, but only if someone is listening
- if (target.hasSubscriptionsForEvent(arrayChangeEventName)) {
- var changes = getChanges(previousContents, currentContents);
+ function notifyChanges() {
+ if (pendingChanges) {
+ // Make a copy of the current contents and ensure it's an array
+ var currentContents = [].concat(target.peek() || []), changes;
+
+ // Compute the diff and issue notifications, but only if someone is listening
+ if (target.hasSubscriptionsForEvent(arrayChangeEventName)) {
+ changes = getChanges(previousContents, currentContents);
+ }
+
+ // Eliminate references to the old, removed items, so they can be GCed
+ previousContents = currentContents;
+ cachedDiff = null;
+ pendingChanges = 0;
+
+ if (changes && changes.length) {
+ target['notifySubscribers'](changes, arrayChangeEventName);
+ }
}
-
- // Eliminate references to the old, removed items, so they can be GCed
- previousContents = currentContents;
- cachedDiff = null;
- pendingNotifications = 0;
-
- if (changes && changes.length) {
- target['notifySubscribers'](changes, arrayChangeEventName);
- }
- });
+ }
}
function getChanges(previousContents, currentContents) {
// We try to re-use cached diffs.
- // The scenarios where pendingNotifications > 1 are when using rate-limiting or the Deferred Updates
- // plugin, which without this check would not be compatible with arrayChange notifications. Normally,
+ // The scenarios where pendingChanges > 1 are when using rate limiting or deferred updates,
+ // which without this check would not be compatible with arrayChange notifications. Normally,
// notifications are issued immediately so we wouldn't be queueing up more than one.
- if (!cachedDiff || pendingNotifications > 1) {
+ if (!cachedDiff || pendingChanges > 1) {
cachedDiff = ko.utils.compareArrays(previousContents, currentContents, target.compareArrayOptions);
}
@@ -1784,7 +1927,7 @@ ko.extenders['trackArrayChanges'] = function(target, options) {
target.cacheDiffForKnownOperation = function(rawArray, operationName, args) {
// Only run if we're currently tracking changes for this observable array
// and there aren't any pending deferred notifications.
- if (!trackingChanges || pendingNotifications) {
+ if (!trackingChanges || pendingChanges) {
return;
}
var diff = [],
@@ -1855,6 +1998,7 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva
var state = {
latestValue: undefined,
isStale: true,
+ isDirty: true,
isBeingEvaluated: false,
suppressDisposalUntilDisposeWhenReturnsFalse: false,
isDisposed: false,
@@ -1881,8 +2025,10 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva
return this; // Permits chained assignments
} else {
// Reading the value
- ko.dependencyDetection.registerDependency(computedObservable);
- if (state.isStale || (state.isSleeping && computedObservable.haveDependenciesChanged())) {
+ if (!state.isDisposed) {
+ ko.dependencyDetection.registerDependency(computedObservable);
+ }
+ if (state.isDirty || (state.isSleeping && computedObservable.haveDependenciesChanged())) {
computedObservable.evaluateImmediate();
}
return state.latestValue;
@@ -1972,6 +2118,10 @@ function computedBeginDependencyDetectionCallback(subscribable, id) {
// Brand new subscription - add it
computedObservable.addDependencyTracking(id, subscribable, state.isSleeping ? { _target: subscribable } : computedObservable.subscribeToDependency(subscribable));
}
+ // If the observable we've accessed has a pending notification, ensure we get notified of the actual final value (bypass equality checks)
+ if (subscribable._notificationIsPending) {
+ subscribable._notifyNextChangeIfValueIsDifferent();
+ }
}
}
@@ -1980,6 +2130,27 @@ var computedFn = {
getDependenciesCount: function () {
return this[computedState].dependenciesCount;
},
+ getDependencies: function () {
+ var dependencyTracking = this[computedState].dependencyTracking, dependentObservables = [];
+
+ ko.utils.objectForEach(dependencyTracking, function (id, dependency) {
+ dependentObservables[dependency._order] = dependency._target;
+ });
+
+ return dependentObservables;
+ },
+ hasAncestorDependency: function (obs) {
+ if (!this[computedState].dependenciesCount) {
+ return false;
+ }
+ var dependencies = this.getDependencies();
+ if (ko.utils.arrayIndexOf(dependencies, obs) !== -1) {
+ return true;
+ }
+ return !!ko.utils.arrayFirst(dependencies, function (dep) {
+ return dep.hasAncestorDependency && dep.hasAncestorDependency(obs);
+ });
+ },
addDependencyTracking: function (id, target, trackingObj) {
if (this[computedState].pure && target === this) {
throw Error("A 'pure' computed must not be called recursively");
@@ -1992,9 +2163,9 @@ var computedFn = {
haveDependenciesChanged: function () {
var id, dependency, dependencyTracking = this[computedState].dependencyTracking;
for (id in dependencyTracking) {
- if (dependencyTracking.hasOwnProperty(id)) {
+ if (Object.prototype.hasOwnProperty.call(dependencyTracking, id)) {
dependency = dependencyTracking[id];
- if (dependency._target.hasChanged(dependency._version)) {
+ if ((this._evalDelayed && dependency._target._notificationIsPending) || dependency._target.hasChanged(dependency._version)) {
return true;
}
}
@@ -2003,20 +2174,23 @@ var computedFn = {
markDirty: function () {
// Process "dirty" events if we can handle delayed notifications
if (this._evalDelayed && !this[computedState].isBeingEvaluated) {
- this._evalDelayed();
+ this._evalDelayed(false /*isChange*/);
}
},
isActive: function () {
- return this[computedState].isStale || this[computedState].dependenciesCount > 0;
+ var state = this[computedState];
+ return state.isDirty || state.dependenciesCount > 0;
},
respondToChange: function () {
// Ignore "change" events if we've already scheduled a delayed notification
if (!this._notificationIsPending) {
this.evaluatePossiblyAsync();
+ } else if (this[computedState].isDirty) {
+ this[computedState].isStale = true;
}
},
subscribeToDependency: function (target) {
- if (target._deferUpdates && !this[computedState].disposeWhenNodeIsRemoved) {
+ if (target._deferUpdates) {
var dirtySub = target.subscribe(this.markDirty, this, 'dirty'),
changeSub = target.subscribe(this.respondToChange, this);
return {
@@ -2039,7 +2213,7 @@ var computedFn = {
computedObservable.evaluateImmediate(true /*notifyChange*/);
}, throttleEvaluationTimeout);
} else if (computedObservable._evalDelayed) {
- computedObservable._evalDelayed();
+ computedObservable._evalDelayed(true /*isChange*/);
} else {
computedObservable.evaluateImmediate(true /*notifyChange*/);
}
@@ -2047,7 +2221,8 @@ var computedFn = {
evaluateImmediate: function (notifyChange) {
var computedObservable = this,
state = computedObservable[computedState],
- disposeWhen = state.disposeWhen;
+ disposeWhen = state.disposeWhen,
+ changed = false;
if (state.isBeingEvaluated) {
// If the evaluation of a ko.computed causes side effects, it's possible that it will trigger its own re-evaluation.
@@ -2075,14 +2250,12 @@ var computedFn = {
state.isBeingEvaluated = true;
try {
- this.evaluateImmediate_CallReadWithDependencyDetection(notifyChange);
+ changed = this.evaluateImmediate_CallReadWithDependencyDetection(notifyChange);
} finally {
state.isBeingEvaluated = false;
}
- if (!state.dependenciesCount) {
- computedObservable.dispose();
- }
+ return changed;
},
evaluateImmediate_CallReadWithDependencyDetection: function (notifyChange) {
// This function is really just part of the evaluateImmediate logic. You would never call it from anywhere else.
@@ -2090,7 +2263,8 @@ var computedFn = {
// which contributes to saving about 40% off the CPU overhead of computed evaluation (on V8 at least).
var computedObservable = this,
- state = computedObservable[computedState];
+ state = computedObservable[computedState],
+ changed = false;
// Initially, we assume that none of the subscriptions are still being used (i.e., all are candidates for disposal).
// Then, during evaluation, we cross off any that are in fact still being used.
@@ -2113,23 +2287,38 @@ var computedFn = {
var newValue = this.evaluateImmediate_CallReadThenEndDependencyDetection(state, dependencyDetectionContext);
- if (computedObservable.isDifferent(state.latestValue, newValue)) {
+ if (!state.dependenciesCount) {
+ computedObservable.dispose();
+ changed = true; // When evaluation causes a disposal, make sure all dependent computeds get notified so they'll see the new state
+ } else {
+ changed = computedObservable.isDifferent(state.latestValue, newValue);
+ }
+
+ if (changed) {
if (!state.isSleeping) {
computedObservable["notifySubscribers"](state.latestValue, "beforeChange");
+ } else {
+ computedObservable.updateVersion();
}
state.latestValue = newValue;
+ if (DEBUG) computedObservable._latestValue = newValue;
- if (state.isSleeping) {
- computedObservable.updateVersion();
- } else if (notifyChange) {
+ computedObservable["notifySubscribers"](state.latestValue, "spectate");
+
+ if (!state.isSleeping && notifyChange) {
computedObservable["notifySubscribers"](state.latestValue);
}
+ if (computedObservable._recordUpdate) {
+ computedObservable._recordUpdate();
+ }
}
if (isInitial) {
computedObservable["notifySubscribers"](state.latestValue, "awake");
}
+
+ return changed;
},
evaluateImmediate_CallReadThenEndDependencyDetection: function (state, dependencyDetectionContext) {
// This function is really part of the evaluateImmediate_CallReadWithDependencyDetection logic.
@@ -2148,13 +2337,14 @@ var computedFn = {
ko.utils.objectForEach(dependencyDetectionContext.disposalCandidates, computedDisposeDependencyCallback);
}
- state.isStale = false;
+ state.isStale = state.isDirty = false;
}
},
- peek: function () {
- // Peek won't re-evaluate, except while the computed is sleeping or to get the initial value when "deferEvaluation" is set.
+ peek: function (evaluate) {
+ // By default, peek won't re-evaluate, except while the computed is sleeping or to get the initial value when "deferEvaluation" is set.
+ // Pass in true to evaluate if needed.
var state = this[computedState];
- if ((state.isStale && !state.dependenciesCount) || (state.isSleeping && this.haveDependenciesChanged())) {
+ if ((state.isDirty && (evaluate || !state.dependenciesCount)) || (state.isSleeping && this.haveDependenciesChanged())) {
this.evaluateImmediate();
}
return state.latestValue;
@@ -2162,15 +2352,29 @@ var computedFn = {
limit: function (limitFunction) {
// Override the limit function with one that delays evaluation as well
ko.subscribable['fn'].limit.call(this, limitFunction);
- this._evalDelayed = function () {
+ this._evalIfChanged = function () {
+ if (!this[computedState].isSleeping) {
+ if (this[computedState].isStale) {
+ this.evaluateImmediate();
+ } else {
+ this[computedState].isDirty = false;
+ }
+ }
+ return this[computedState].latestValue;
+ };
+ this._evalDelayed = function (isChange) {
this._limitBeforeChange(this[computedState].latestValue);
- this[computedState].isStale = true; // Mark as dirty
+ // Mark as dirty
+ this[computedState].isDirty = true;
+ if (isChange) {
+ this[computedState].isStale = true;
+ }
- // Pass the observable to the "limit" code, which will access it when
+ // Pass the observable to the "limit" code, which will evaluate it when
// it's time to do the notification.
- this._limitChange(this);
- }
+ this._limitChange(this, !isChange /* isDirty */);
+ };
},
dispose: function () {
var state = this[computedState];
@@ -2183,12 +2387,18 @@ var computedFn = {
if (state.disposeWhenNodeIsRemoved && state.domNodeDisposalCallback) {
ko.utils.domNodeDisposal.removeDisposeCallback(state.disposeWhenNodeIsRemoved, state.domNodeDisposalCallback);
}
- state.dependencyTracking = null;
+ state.dependencyTracking = undefined;
state.dependenciesCount = 0;
state.isDisposed = true;
state.isStale = false;
+ state.isDirty = false;
state.isSleeping = false;
- state.disposeWhenNodeIsRemoved = null;
+ state.disposeWhenNodeIsRemoved = undefined;
+ state.disposeWhen = undefined;
+ state.readFunction = undefined;
+ if (!this.hasWriteFunction) {
+ state.evaluatorFunctionTarget = undefined;
+ }
}
};
@@ -2202,23 +2412,31 @@ var pureComputedOverrides = {
if (state.isStale || computedObservable.haveDependenciesChanged()) {
state.dependencyTracking = null;
state.dependenciesCount = 0;
- state.isStale = true;
- computedObservable.evaluateImmediate();
+ if (computedObservable.evaluateImmediate()) {
+ computedObservable.updateVersion();
+ }
} else {
// First put the dependencies in order
- var dependeciesOrder = [];
+ var dependenciesOrder = [];
ko.utils.objectForEach(state.dependencyTracking, function (id, dependency) {
- dependeciesOrder[dependency._order] = id;
+ dependenciesOrder[dependency._order] = id;
});
// Next, subscribe to each one
- ko.utils.arrayForEach(dependeciesOrder, function (id, order) {
+ ko.utils.arrayForEach(dependenciesOrder, function (id, order) {
var dependency = state.dependencyTracking[id],
subscription = computedObservable.subscribeToDependency(dependency._target);
subscription._order = order;
subscription._version = dependency._version;
state.dependencyTracking[id] = subscription;
});
+ // Waking dependencies may have triggered effects
+ if (computedObservable.haveDependenciesChanged()) {
+ if (computedObservable.evaluateImmediate()) {
+ computedObservable.updateVersion();
+ }
+ }
}
+
if (!state.isDisposed) { // test since evaluating could trigger disposal
computedObservable["notifySubscribers"](state.latestValue, "awake");
}
@@ -2268,18 +2486,16 @@ if (ko.utils.canSetPrototype) {
ko.utils.setPrototypeOf(computedFn, ko.subscribable['fn']);
}
-// Set the proto chain values for ko.hasPrototype
+// Set the proto values for ko.computed
var protoProp = ko.observable.protoProperty; // == "__ko_proto__"
-ko.computed[protoProp] = ko.observable;
computedFn[protoProp] = ko.computed;
ko.isComputed = function (instance) {
- return ko.hasPrototype(instance, ko.computed);
+ return (typeof instance == 'function' && instance[protoProp] === computedFn[protoProp]);
};
ko.isPureComputed = function (instance) {
- return ko.hasPrototype(instance, ko.computed)
- && instance[computedState] && instance[computedState].pure;
+ return ko.isComputed(instance) && instance[computedState] && instance[computedState].pure;
};
ko.exportSymbol('computed', ko.computed);
@@ -2291,6 +2507,7 @@ ko.exportProperty(computedFn, 'peek', computedFn.peek);
ko.exportProperty(computedFn, 'dispose', computedFn.dispose);
ko.exportProperty(computedFn, 'isActive', computedFn.isActive);
ko.exportProperty(computedFn, 'getDependenciesCount', computedFn.getDependenciesCount);
+ko.exportProperty(computedFn, 'getDependencies', computedFn.getDependencies);
ko.pureComputed = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget) {
if (typeof evaluatorFunctionOrOptions === 'function') {
@@ -2304,7 +2521,7 @@ ko.pureComputed = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget)
ko.exportSymbol('pureComputed', ko.pureComputed);
(function() {
- var maxNestedObservableDepth = 10; // Escape the (unlikely) pathalogical case where an observable's current value is itself (or similar reference cycle)
+ var maxNestedObservableDepth = 10; // Escape the (unlikely) pathological case where an observable's current value is itself (or similar reference cycle)
ko.toJS = function(rootObject) {
if (arguments.length == 0)
@@ -2398,6 +2615,28 @@ ko.exportSymbol('pureComputed', ko.pureComputed);
ko.exportSymbol('toJS', ko.toJS);
ko.exportSymbol('toJSON', ko.toJSON);
+ko.when = function(predicate, callback, context) {
+ function kowhen (resolve) {
+ var observable = ko.pureComputed(predicate, context).extend({notify:'always'});
+ var subscription = observable.subscribe(function(value) {
+ if (value) {
+ subscription.dispose();
+ resolve(value);
+ }
+ });
+ // In case the initial value is true, process it right away
+ observable['notifySubscribers'](observable.peek());
+
+ return subscription;
+ }
+ if (typeof Promise === "function" && !callback) {
+ return new Promise(kowhen);
+ } else {
+ return kowhen(callback.bind(context));
+ }
+};
+
+ko.exportSymbol('when', ko.when);
(function () {
var hasDomDataExpandoProperty = '__ko__hasDomDataOptionValue__';
@@ -2423,22 +2662,20 @@ ko.exportSymbol('toJSON', ko.toJSON);
writeValue: function(element, value, allowUnset) {
switch (ko.utils.tagNameLower(element)) {
case 'option':
- switch(typeof value) {
- case "string":
- ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, undefined);
- if (hasDomDataExpandoProperty in element) { // IE <= 8 throws errors if you delete non-existent properties from a DOM node
- delete element[hasDomDataExpandoProperty];
- }
- element.value = value;
- break;
- default:
- // Store arbitrary object using DomData
- ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, value);
- element[hasDomDataExpandoProperty] = true;
+ if (typeof value === "string") {
+ ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, undefined);
+ if (hasDomDataExpandoProperty in element) { // IE <= 8 throws errors if you delete non-existent properties from a DOM node
+ delete element[hasDomDataExpandoProperty];
+ }
+ element.value = value;
+ }
+ else {
+ // Store arbitrary object using DomData
+ ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, value);
+ element[hasDomDataExpandoProperty] = true;
- // Special treatment of numbers is just for backward compatibility. KO 1.2.1 wrote numerical values to element.value.
- element.value = typeof value === "number" ? value : "";
- break;
+ // Special treatment of numbers is just for backward compatibility. KO 1.2.1 wrote numerical values to element.value.
+ element.value = typeof value === "number" ? value : "";
}
break;
case 'select':
@@ -2448,13 +2685,21 @@ ko.exportSymbol('toJSON', ko.toJSON);
for (var i = 0, n = element.options.length, optionValue; i < n; ++i) {
optionValue = ko.selectExtensions.readValue(element.options[i]);
// Include special check to handle selecting a caption with a blank string value
- if (optionValue == value || (optionValue == "" && value === undefined)) {
+ if (optionValue == value || (optionValue === "" && value === undefined)) {
selection = i;
break;
}
}
if (allowUnset || selection >= 0 || (value === undefined && element.size > 1)) {
element.selectedIndex = selection;
+ if (ko.utils.ieVersion === 6) {
+ // Workaround for IE6 bug: It won't reliably apply values to SELECT nodes during the same execution thread
+ // right after you've changed the set of OPTION nodes on it. So for that node type, we'll schedule a second thread
+ // to apply the value as well.
+ ko.utils.setTimeout(function () {
+ element.selectedIndex = selection;
+ }, 0);
+ }
}
break;
default:
@@ -2487,26 +2732,29 @@ ko.expressionRewriting = (function () {
// The following regular expressions will be used to split an object-literal string into tokens
- // These two match strings, either with double quotes or single quotes
- var stringDouble = '"(?:[^"\\\\]|\\\\.)*"',
- stringSingle = "'(?:[^'\\\\]|\\\\.)*'",
- // Matches a regular expression (text enclosed by slashes), but will also match sets of divisions
- // as a regular expression (this is handled by the parsing loop below).
- stringRegexp = '/(?:[^/\\\\]|\\\\.)*/\w*',
- // These characters have special meaning to the parser and must not appear in the middle of a
- // token, except as part of a string.
- specials = ',"\'{}()/:[\\]',
- // Match text (at least two characters) that does not contain any of the above special characters,
- // although some of the special characters are allowed to start it (all but the colon and comma).
- // The text can contain spaces, but leading or trailing spaces are skipped.
- everyThingElse = '[^\\s:,/][^' + specials + ']*[^\\s' + specials + ']',
- // Match any non-space character not matched already. This will match colons and commas, since they're
- // not matched by "everyThingElse", but will also match any other single character that wasn't already
- // matched (for example: in "a: 1, b: 2", each of the non-space characters will be matched by oneNotSpace).
- oneNotSpace = '[^\\s]',
-
- // Create the actual regular expression by or-ing the above strings. The order is important.
- bindingToken = RegExp(stringDouble + '|' + stringSingle + '|' + stringRegexp + '|' + everyThingElse + '|' + oneNotSpace, 'g'),
+ var specials = ',"\'`{}()/:[\\]', // These characters have special meaning to the parser and must not appear in the middle of a token, except as part of a string.
+ // Create the actual regular expression by or-ing the following regex strings. The order is important.
+ bindingToken = RegExp([
+ // These match strings, either with double quotes, single quotes, or backticks
+ '"(?:\\\\.|[^"])*"',
+ "'(?:\\\\.|[^'])*'",
+ "`(?:\\\\.|[^`])*`",
+ // Match C style comments
+ "/\\*(?:[^*]|\\*+[^*/])*\\*+/",
+ // Match C++ style comments
+ "//.*\n",
+ // Match a regular expression (text enclosed by slashes), but will also match sets of divisions
+ // as a regular expression (this is handled by the parsing loop below).
+ '/(?:\\\\.|[^/])+/\w*',
+ // Match text (at least two characters) that does not contain any of the above special characters,
+ // although some of the special characters are allowed to start it (all but the colon and comma).
+ // The text can contain spaces, but leading or trailing spaces are skipped.
+ '[^\\s:,/][^' + specials + ']*[^\\s' + specials + ']',
+ // Match any non-space character not matched already. This will match colons and commas, since they're
+ // not matched by "everyThingElse", but will also match any other single character that wasn't already
+ // matched (for example: in "a: 1, b: 2", each of the non-space characters will be matched by oneNotSpace).
+ '[^\\s]'
+ ].join('|'), 'g'),
// Match end of previous token to determine whether a slash is a division or regex.
divisionLookBehind = /[\])"'A-Za-z0-9_$]+$/,
@@ -2519,13 +2767,14 @@ ko.expressionRewriting = (function () {
// Trim braces '{' surrounding the whole object literal
if (str.charCodeAt(0) === 123) str = str.slice(1, -1);
+ // Add a newline to correctly match a C++ style comment at the end of the string and
+ // add a comma so that we don't need a separate code block to deal with the last item
+ str += "\n,";
+
// Split into tokens
var result = [], toks = str.match(bindingToken), key, values = [], depth = 0;
- if (toks) {
- // Append a comma so that we don't need a separate code block to deal with the last item
- toks.push(',');
-
+ if (toks.length > 1) {
for (var i = 0, tok; tok = toks[i]; ++i) {
var c = tok.charCodeAt(0);
// A comma signals the end of a key/value pair if depth is zero
@@ -2542,6 +2791,9 @@ ko.expressionRewriting = (function () {
key = values.pop();
continue;
}
+ // Comments: skip them
+ } else if (c === 47 && tok.length > 1 && (tok.charCodeAt(1) === 47 || tok.charCodeAt(1) === 42)) { // "//" or "/*"
+ continue;
// A set of slashes is initially matched as a regular expression, but could be division
} else if (c === 47 && i && tok.length > 1) { // "/"
// Look at the end of the previous token to determine if the slash is actually division
@@ -2550,7 +2802,6 @@ ko.expressionRewriting = (function () {
// The slash is actually a division punctuator; re-parse the remainder of the string (not including the slash)
str = str.substr(str.indexOf(tok) + 1);
toks = str.match(bindingToken);
- toks.push(',');
i = -1;
// Continue with just the slash
tok = '/';
@@ -2566,6 +2817,9 @@ ko.expressionRewriting = (function () {
}
values.push(tok);
}
+ if (depth > 0) {
+ throw Error("Unbalanced parentheses, braces, or brackets");
+ }
}
return result;
}
@@ -2588,7 +2842,8 @@ ko.expressionRewriting = (function () {
if (twoWayBindings[key] && (writableVal = getWriteableValue(val))) {
// For two-way bindings, provide a write method in case the value
// isn't a writable observable.
- propertyAccessorResultStrings.push("'" + key + "':function(_z){" + writableVal + "=_z}");
+ var writeKey = typeof twoWayBindings[key] == 'string' ? twoWayBindings[key] : key;
+ propertyAccessorResultStrings.push("'" + writeKey + "':function(_z){" + writableVal + "=_z}");
}
}
// Values are wrapped in a function so that each value can be accessed independently
@@ -2696,12 +2951,19 @@ ko.exportSymbol('jsonExpressionRewriting.insertPropertyAccessorsIntoJson', ko.ex
return (node.nodeType == 8) && endCommentRegex.test(commentNodesHaveTextProperty ? node.text : node.nodeValue);
}
+ function isUnmatchedEndComment(node) {
+ return isEndComment(node) && !(ko.utils.domData.get(node, matchedEndCommentDataKey));
+ }
+
+ var matchedEndCommentDataKey = "__ko_matchedEndComment__"
+
function getVirtualChildren(startComment, allowUnbalanced) {
var currentNode = startComment;
var depth = 1;
var children = [];
while (currentNode = currentNode.nextSibling) {
if (isEndComment(currentNode)) {
+ ko.utils.domData.set(currentNode, matchedEndCommentDataKey, true);
depth--;
if (depth === 0)
return children;
@@ -2778,46 +3040,69 @@ ko.exportSymbol('jsonExpressionRewriting.insertPropertyAccessorsIntoJson', ko.ex
},
prepend: function(containerNode, nodeToPrepend) {
- if (!isStartComment(containerNode)) {
- if (containerNode.firstChild)
- containerNode.insertBefore(nodeToPrepend, containerNode.firstChild);
- else
- containerNode.appendChild(nodeToPrepend);
- } else {
+ var insertBeforeNode;
+
+ if (isStartComment(containerNode)) {
// Start comments must always have a parent and at least one following sibling (the end comment)
- containerNode.parentNode.insertBefore(nodeToPrepend, containerNode.nextSibling);
+ insertBeforeNode = containerNode.nextSibling;
+ containerNode = containerNode.parentNode;
+ } else {
+ insertBeforeNode = containerNode.firstChild;
+ }
+
+ if (!insertBeforeNode) {
+ containerNode.appendChild(nodeToPrepend);
+ } else if (nodeToPrepend !== insertBeforeNode) { // IE will sometimes crash if you try to insert a node before itself
+ containerNode.insertBefore(nodeToPrepend, insertBeforeNode);
}
},
insertAfter: function(containerNode, nodeToInsert, insertAfterNode) {
if (!insertAfterNode) {
ko.virtualElements.prepend(containerNode, nodeToInsert);
- } else if (!isStartComment(containerNode)) {
- // Insert after insertion point
- if (insertAfterNode.nextSibling)
- containerNode.insertBefore(nodeToInsert, insertAfterNode.nextSibling);
- else
- containerNode.appendChild(nodeToInsert);
} else {
// Children of start comments must always have a parent and at least one following sibling (the end comment)
- containerNode.parentNode.insertBefore(nodeToInsert, insertAfterNode.nextSibling);
+ var insertBeforeNode = insertAfterNode.nextSibling;
+
+ if (isStartComment(containerNode)) {
+ containerNode = containerNode.parentNode;
+ }
+
+ if (!insertBeforeNode) {
+ containerNode.appendChild(nodeToInsert);
+ } else if (nodeToInsert !== insertBeforeNode) { // IE will sometimes crash if you try to insert a node before itself
+ containerNode.insertBefore(nodeToInsert, insertBeforeNode);
+ }
}
},
firstChild: function(node) {
- if (!isStartComment(node))
+ if (!isStartComment(node)) {
+ if (node.firstChild && isEndComment(node.firstChild)) {
+ throw new Error("Found invalid end comment, as the first child of " + node);
+ }
return node.firstChild;
- if (!node.nextSibling || isEndComment(node.nextSibling))
+ } else if (!node.nextSibling || isEndComment(node.nextSibling)) {
return null;
- return node.nextSibling;
+ } else {
+ return node.nextSibling;
+ }
},
nextSibling: function(node) {
- if (isStartComment(node))
+ if (isStartComment(node)) {
node = getMatchingEndComment(node);
- if (node.nextSibling && isEndComment(node.nextSibling))
- return null;
- return node.nextSibling;
+ }
+
+ if (node.nextSibling && isEndComment(node.nextSibling)) {
+ if (isUnmatchedEndComment(node.nextSibling)) {
+ throw Error("Found end comment without a matching opening comment, as child of " + node);
+ } else {
+ return null;
+ }
+ } else {
+ return node.nextSibling;
+ }
},
hasBindingValue: isStartComment,
@@ -2939,6 +3224,11 @@ ko.exportSymbol('virtualElements.setDomNodeChildren', ko.virtualElements.setDomN
ko.exportSymbol('bindingProvider', ko.bindingProvider);
(function () {
+ // Hide or don't minify context properties, see https://github.com/knockout/knockout/issues/2294
+ var contextSubscribable = ko.utils.createSymbolOrString('_subscribable');
+ var contextAncestorBindingInfo = ko.utils.createSymbolOrString('_ancestorBindingInfo');
+ var contextDataDependency = ko.utils.createSymbolOrString('_dataDependency');
+
ko.bindingHandlers = {};
// The following element types will not be recursed into during binding.
@@ -2953,14 +3243,16 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
'template': true
};
- // Use an overridable method for retrieving binding handlers so that a plugins may support dynamically created handlers
+ // Use an overridable method for retrieving binding handlers so that plugins may support dynamically created handlers
ko['getBindingHandler'] = function(bindingKey) {
return ko.bindingHandlers[bindingKey];
};
+ var inheritParentVm = {};
+
// The ko.bindingContext constructor is only called directly to create the root context. For child
// contexts, use bindingContext.createChildContext or bindingContext.extend.
- ko.bindingContext = function(dataItemOrAccessor, parentContext, dataItemAlias, extendCallback) {
+ ko.bindingContext = function(dataItemOrAccessor, parentContext, dataItemAlias, extendCallback, options) {
// The binding context object includes static properties for the current, parent, and root view models.
// If a view model is actually stored in an observable, the corresponding binding context object, and
@@ -2970,22 +3262,16 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
// we call the function to retrieve the view model. If the function accesses any observables or returns
// an observable, the dependency is tracked, and those observables can later cause the binding
// context to be updated.
- var dataItemOrObservable = isFunc ? dataItemOrAccessor() : dataItemOrAccessor,
+ var dataItemOrObservable = isFunc ? realDataItemOrAccessor() : realDataItemOrAccessor,
dataItem = ko.utils.unwrapObservable(dataItemOrObservable);
if (parentContext) {
- // When a "parent" context is given, register a dependency on the parent context. Thus whenever the
- // parent context is updated, this context will also be updated.
- if (parentContext._subscribable)
- parentContext._subscribable();
-
// Copy $root and any custom properties from the parent context
ko.utils.extend(self, parentContext);
- // Because the above copy overwrites our own properties, we need to reset them.
- // During the first execution, "subscribable" isn't set, so don't bother doing the update then.
- if (subscribable) {
- self._subscribable = subscribable;
+ // Copy Symbol properties
+ if (contextAncestorBindingInfo in parentContext) {
+ self[contextAncestorBindingInfo] = parentContext[contextAncestorBindingInfo];
}
} else {
self['$parents'] = [];
@@ -2996,8 +3282,16 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
// See https://github.com/SteveSanderson/knockout/issues/490
self['ko'] = ko;
}
- self['$rawData'] = dataItemOrObservable;
- self['$data'] = dataItem;
+
+ self[contextSubscribable] = subscribable;
+
+ if (shouldInheritData) {
+ dataItem = self['$data'];
+ } else {
+ self['$rawData'] = dataItemOrObservable;
+ self['$data'] = dataItem;
+ }
+
if (dataItemAlias)
self[dataItemAlias] = dataItem;
@@ -3007,44 +3301,45 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
if (extendCallback)
extendCallback(self, parentContext, dataItem);
+ // When a "parent" context is given and we don't already have a dependency on its context, register a dependency on it.
+ // Thus whenever the parent context is updated, this context will also be updated.
+ if (parentContext && parentContext[contextSubscribable] && !ko.computedContext.computed().hasAncestorDependency(parentContext[contextSubscribable])) {
+ parentContext[contextSubscribable]();
+ }
+
+ if (dataDependency) {
+ self[contextDataDependency] = dataDependency;
+ }
+
return self['$data'];
}
- function disposeWhen() {
- return nodes && !ko.utils.anyDomNodeIsAttachedToDocument(nodes);
- }
var self = this,
- isFunc = typeof(dataItemOrAccessor) == "function" && !ko.isObservable(dataItemOrAccessor),
+ shouldInheritData = dataItemOrAccessor === inheritParentVm,
+ realDataItemOrAccessor = shouldInheritData ? undefined : dataItemOrAccessor,
+ isFunc = typeof(realDataItemOrAccessor) == "function" && !ko.isObservable(realDataItemOrAccessor),
nodes,
- subscribable = ko.dependentObservable(updateContext, null, { disposeWhen: disposeWhen, disposeWhenNodeIsRemoved: true });
+ subscribable,
+ dataDependency = options && options['dataDependency'];
- // At this point, the binding context has been initialized, and the "subscribable" computed observable is
- // subscribed to any observables that were accessed in the process. If there is nothing to track, the
- // computed will be inactive, and we can safely throw it away. If it's active, the computed is stored in
- // the context object.
- if (subscribable.isActive()) {
- self._subscribable = subscribable;
+ if (options && options['exportDependencies']) {
+ // The "exportDependencies" option means that the calling code will track any dependencies and re-create
+ // the binding context when they change.
+ updateContext();
+ } else {
+ subscribable = ko.pureComputed(updateContext);
+ subscribable.peek();
- // Always notify because even if the model ($data) hasn't changed, other context properties might have changed
- subscribable['equalityComparer'] = null;
-
- // We need to be able to dispose of this computed observable when it's no longer needed. This would be
- // easy if we had a single node to watch, but binding contexts can be used by many different nodes, and
- // we cannot assume that those nodes have any relation to each other. So instead we track any node that
- // the context is attached to, and dispose the computed when all of those nodes have been cleaned.
-
- // Add properties to *subscribable* instead of *self* because any properties added to *self* may be overwritten on updates
- nodes = [];
- subscribable._addNode = function(node) {
- nodes.push(node);
- ko.utils.domNodeDisposal.addDisposeCallback(node, function(node) {
- ko.utils.arrayRemoveItem(nodes, node);
- if (!nodes.length) {
- subscribable.dispose();
- self._subscribable = subscribable = undefined;
- }
- });
- };
+ // At this point, the binding context has been initialized, and the "subscribable" computed observable is
+ // subscribed to any observables that were accessed in the process. If there is nothing to track, the
+ // computed will be inactive, and we can safely throw it away. If it's active, the computed is stored in
+ // the context object.
+ if (subscribable.isActive()) {
+ // Always notify because even if the model ($data) hasn't changed, other context properties might have changed
+ subscribable['equalityComparer'] = null;
+ } else {
+ self[contextSubscribable] = undefined;
+ }
}
}
@@ -3053,8 +3348,23 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
// But this does not mean that the $data value of the child context will also get updated. If the child
// view model also depends on the parent view model, you must provide a function that returns the correct
// view model on each update.
- ko.bindingContext.prototype['createChildContext'] = function (dataItemOrAccessor, dataItemAlias, extendCallback) {
- return new ko.bindingContext(dataItemOrAccessor, this, dataItemAlias, function(self, parentContext) {
+ ko.bindingContext.prototype['createChildContext'] = function (dataItemOrAccessor, dataItemAlias, extendCallback, options) {
+ if (!options && dataItemAlias && typeof dataItemAlias == "object") {
+ options = dataItemAlias;
+ dataItemAlias = options['as'];
+ extendCallback = options['extend'];
+ }
+
+ if (dataItemAlias && options && options['noChildContext']) {
+ var isFunc = typeof(dataItemOrAccessor) == "function" && !ko.isObservable(dataItemOrAccessor);
+ return new ko.bindingContext(inheritParentVm, this, null, function (self) {
+ if (extendCallback)
+ extendCallback(self);
+ self[dataItemAlias] = isFunc ? dataItemOrAccessor() : dataItemOrAccessor;
+ }, options);
+ }
+
+ return new ko.bindingContext(dataItemOrAccessor, this, dataItemAlias, function (self, parentContext) {
// Extend the context hierarchy by setting the appropriate pointers
self['$parentContext'] = parentContext;
self['$parent'] = parentContext['$data'];
@@ -3062,24 +3372,117 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
self['$parents'].unshift(self['$parent']);
if (extendCallback)
extendCallback(self);
- });
+ }, options);
};
// Extend the binding context with new custom properties. This doesn't change the context hierarchy.
// Similarly to "child" contexts, provide a function here to make sure that the correct values are set
// when an observable view model is updated.
- ko.bindingContext.prototype['extend'] = function(properties) {
- // If the parent context references an observable view model, "_subscribable" will always be the
- // latest view model object. If not, "_subscribable" isn't set, and we can use the static "$data" value.
- return new ko.bindingContext(this._subscribable || this['$data'], this, null, function(self, parentContext) {
- // This "child" context doesn't directly track a parent observable view model,
- // so we need to manually set the $rawData value to match the parent.
- self['$rawData'] = parentContext['$rawData'];
- ko.utils.extend(self, typeof(properties) == "function" ? properties() : properties);
- });
+ ko.bindingContext.prototype['extend'] = function(properties, options) {
+ return new ko.bindingContext(inheritParentVm, this, null, function(self, parentContext) {
+ ko.utils.extend(self, typeof(properties) == "function" ? properties(self) : properties);
+ }, options);
};
- // Returns the valueAccesor function for a binding value
+ var boundElementDomDataKey = ko.utils.domData.nextKey();
+
+ function asyncContextDispose(node) {
+ var bindingInfo = ko.utils.domData.get(node, boundElementDomDataKey),
+ asyncContext = bindingInfo && bindingInfo.asyncContext;
+ if (asyncContext) {
+ bindingInfo.asyncContext = null;
+ asyncContext.notifyAncestor();
+ }
+ }
+ function AsyncCompleteContext(node, bindingInfo, ancestorBindingInfo) {
+ this.node = node;
+ this.bindingInfo = bindingInfo;
+ this.asyncDescendants = [];
+ this.childrenComplete = false;
+
+ if (!bindingInfo.asyncContext) {
+ ko.utils.domNodeDisposal.addDisposeCallback(node, asyncContextDispose);
+ }
+
+ if (ancestorBindingInfo && ancestorBindingInfo.asyncContext) {
+ ancestorBindingInfo.asyncContext.asyncDescendants.push(node);
+ this.ancestorBindingInfo = ancestorBindingInfo;
+ }
+ }
+ AsyncCompleteContext.prototype.notifyAncestor = function () {
+ if (this.ancestorBindingInfo && this.ancestorBindingInfo.asyncContext) {
+ this.ancestorBindingInfo.asyncContext.descendantComplete(this.node);
+ }
+ };
+ AsyncCompleteContext.prototype.descendantComplete = function (node) {
+ ko.utils.arrayRemoveItem(this.asyncDescendants, node);
+ if (!this.asyncDescendants.length && this.childrenComplete) {
+ this.completeChildren();
+ }
+ };
+ AsyncCompleteContext.prototype.completeChildren = function () {
+ this.childrenComplete = true;
+ if (this.bindingInfo.asyncContext && !this.asyncDescendants.length) {
+ this.bindingInfo.asyncContext = null;
+ ko.utils.domNodeDisposal.removeDisposeCallback(this.node, asyncContextDispose);
+ ko.bindingEvent.notify(this.node, ko.bindingEvent.descendantsComplete);
+ this.notifyAncestor();
+ }
+ };
+
+ ko.bindingEvent = {
+ childrenComplete: "childrenComplete",
+ descendantsComplete : "descendantsComplete",
+
+ subscribe: function (node, event, callback, context, options) {
+ var bindingInfo = ko.utils.domData.getOrSet(node, boundElementDomDataKey, {});
+ if (!bindingInfo.eventSubscribable) {
+ bindingInfo.eventSubscribable = new ko.subscribable;
+ }
+ if (options && options['notifyImmediately'] && bindingInfo.notifiedEvents[event]) {
+ ko.dependencyDetection.ignore(callback, context, [node]);
+ }
+ return bindingInfo.eventSubscribable.subscribe(callback, context, event);
+ },
+
+ notify: function (node, event) {
+ var bindingInfo = ko.utils.domData.get(node, boundElementDomDataKey);
+ if (bindingInfo) {
+ bindingInfo.notifiedEvents[event] = true;
+ if (bindingInfo.eventSubscribable) {
+ bindingInfo.eventSubscribable['notifySubscribers'](node, event);
+ }
+ if (event == ko.bindingEvent.childrenComplete) {
+ if (bindingInfo.asyncContext) {
+ bindingInfo.asyncContext.completeChildren();
+ } else if (bindingInfo.asyncContext === undefined && bindingInfo.eventSubscribable && bindingInfo.eventSubscribable.hasSubscriptionsForEvent(ko.bindingEvent.descendantsComplete)) {
+ // It's currently an error to register a descendantsComplete handler for a node that was never registered as completing asynchronously.
+ // That's because without the asyncContext, we don't have a way to know that all descendants have completed.
+ throw new Error("descendantsComplete event not supported for bindings on this node");
+ }
+ }
+ }
+ },
+
+ startPossiblyAsyncContentBinding: function (node, bindingContext) {
+ var bindingInfo = ko.utils.domData.getOrSet(node, boundElementDomDataKey, {});
+
+ if (!bindingInfo.asyncContext) {
+ bindingInfo.asyncContext = new AsyncCompleteContext(node, bindingInfo, bindingContext[contextAncestorBindingInfo]);
+ }
+
+ // If the provided context was already extended with this node's binding info, just return the extended context
+ if (bindingContext[contextAncestorBindingInfo] == bindingInfo) {
+ return bindingContext;
+ }
+
+ return bindingContext['extend'](function (ctx) {
+ ctx[contextAncestorBindingInfo] = bindingInfo;
+ });
+ }
+ };
+
+ // Returns the valueAccessor function for a binding value
function makeValueAccessor(value) {
return function() {
return value;
@@ -3125,62 +3528,55 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
throw new Error("The binding '" + bindingName + "' cannot be used with virtual elements")
}
- function applyBindingsToDescendantsInternal (bindingContext, elementOrVirtualElement, bindingContextsMayDifferFromDomParentElement) {
- var currentChild,
- nextInQueue = ko.virtualElements.firstChild(elementOrVirtualElement),
- provider = ko.bindingProvider['instance'],
- preprocessNode = provider['preprocessNode'];
+ function applyBindingsToDescendantsInternal(bindingContext, elementOrVirtualElement) {
+ var nextInQueue = ko.virtualElements.firstChild(elementOrVirtualElement);
- // Preprocessing allows a binding provider to mutate a node before bindings are applied to it. For example it's
- // possible to insert new siblings after it, and/or replace the node with a different one. This can be used to
- // implement custom binding syntaxes, such as {{ value }} for string interpolation, or custom element types that
- // trigger insertion of contents at that point in the document.
- if (preprocessNode) {
- while (currentChild = nextInQueue) {
- nextInQueue = ko.virtualElements.nextSibling(currentChild);
- preprocessNode.call(provider, currentChild);
+ if (nextInQueue) {
+ var currentChild,
+ provider = ko.bindingProvider['instance'],
+ preprocessNode = provider['preprocessNode'];
+
+ // Preprocessing allows a binding provider to mutate a node before bindings are applied to it. For example it's
+ // possible to insert new siblings after it, and/or replace the node with a different one. This can be used to
+ // implement custom binding syntaxes, such as {{ value }} for string interpolation, or custom element types that
+ // trigger insertion of contents at that point in the document.
+ if (preprocessNode) {
+ while (currentChild = nextInQueue) {
+ nextInQueue = ko.virtualElements.nextSibling(currentChild);
+ preprocessNode.call(provider, currentChild);
+ }
+ // Reset nextInQueue for the next loop
+ nextInQueue = ko.virtualElements.firstChild(elementOrVirtualElement);
}
- // Reset nextInQueue for the next loop
- nextInQueue = ko.virtualElements.firstChild(elementOrVirtualElement);
- }
- while (currentChild = nextInQueue) {
- // Keep a record of the next child *before* applying bindings, in case the binding removes the current child from its position
- nextInQueue = ko.virtualElements.nextSibling(currentChild);
- applyBindingsToNodeAndDescendantsInternal(bindingContext, currentChild, bindingContextsMayDifferFromDomParentElement);
+ while (currentChild = nextInQueue) {
+ // Keep a record of the next child *before* applying bindings, in case the binding removes the current child from its position
+ nextInQueue = ko.virtualElements.nextSibling(currentChild);
+ applyBindingsToNodeAndDescendantsInternal(bindingContext, currentChild);
+ }
}
+ ko.bindingEvent.notify(elementOrVirtualElement, ko.bindingEvent.childrenComplete);
}
- function applyBindingsToNodeAndDescendantsInternal (bindingContext, nodeVerified, bindingContextMayDifferFromDomParentElement) {
- var shouldBindDescendants = true;
+ function applyBindingsToNodeAndDescendantsInternal(bindingContext, nodeVerified) {
+ var bindingContextForDescendants = bindingContext;
- // Perf optimisation: Apply bindings only if...
- // (1) We need to store the binding context on this node (because it may differ from the DOM parent node's binding context)
- // Note that we can't store binding contexts on non-elements (e.g., text nodes), as IE doesn't allow expando properties for those
- // (2) It might have bindings (e.g., it has a data-bind attribute, or it's a marker for a containerless template)
var isElement = (nodeVerified.nodeType === 1);
if (isElement) // Workaround IE <= 8 HTML parsing weirdness
ko.virtualElements.normaliseVirtualElementDomStructure(nodeVerified);
- var shouldApplyBindings = (isElement && bindingContextMayDifferFromDomParentElement) // Case (1)
- || ko.bindingProvider['instance']['nodeHasBindings'](nodeVerified); // Case (2)
+ // Perf optimisation: Apply bindings only if...
+ // (1) We need to store the binding info for the node (all element nodes)
+ // (2) It might have bindings (e.g., it has a data-bind attribute, or it's a marker for a containerless template)
+ var shouldApplyBindings = isElement || ko.bindingProvider['instance']['nodeHasBindings'](nodeVerified);
if (shouldApplyBindings)
- shouldBindDescendants = applyBindingsToNodeInternal(nodeVerified, null, bindingContext, bindingContextMayDifferFromDomParentElement)['shouldBindDescendants'];
+ bindingContextForDescendants = applyBindingsToNodeInternal(nodeVerified, null, bindingContext)['bindingContextForDescendants'];
- if (shouldBindDescendants && !bindingDoesNotRecurseIntoElementTypes[ko.utils.tagNameLower(nodeVerified)]) {
- // We're recursing automatically into (real or virtual) child nodes without changing binding contexts. So,
- // * For children of a *real* element, the binding context is certainly the same as on their DOM .parentNode,
- // hence bindingContextsMayDifferFromDomParentElement is false
- // * For children of a *virtual* element, we can't be sure. Evaluating .parentNode on those children may
- // skip over any number of intermediate virtual elements, any of which might define a custom binding context,
- // hence bindingContextsMayDifferFromDomParentElement is true
- applyBindingsToDescendantsInternal(bindingContext, nodeVerified, /* bindingContextsMayDifferFromDomParentElement: */ !isElement);
+ if (bindingContextForDescendants && !bindingDoesNotRecurseIntoElementTypes[ko.utils.tagNameLower(nodeVerified)]) {
+ applyBindingsToDescendantsInternal(bindingContextForDescendants, nodeVerified);
}
}
- var boundElementDomDataKey = ko.utils.domData.nextKey();
-
-
function topologicalSortBindings(bindings) {
// Depth-first sort
var result = [], // The list of key/handler pairs that we will return
@@ -3214,21 +3610,23 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
return result;
}
- function applyBindingsToNodeInternal(node, sourceBindings, bindingContext, bindingContextMayDifferFromDomParentElement) {
+ function applyBindingsToNodeInternal(node, sourceBindings, bindingContext) {
+ var bindingInfo = ko.utils.domData.getOrSet(node, boundElementDomDataKey, {});
+
// Prevent multiple applyBindings calls for the same node, except when a binding value is specified
- var alreadyBound = ko.utils.domData.get(node, boundElementDomDataKey);
+ var alreadyBound = bindingInfo.alreadyBound;
if (!sourceBindings) {
if (alreadyBound) {
throw Error("You cannot apply bindings multiple times to the same element.");
}
- ko.utils.domData.set(node, boundElementDomDataKey, true);
+ bindingInfo.alreadyBound = true;
+ }
+ if (!alreadyBound) {
+ bindingInfo.context = bindingContext;
+ }
+ if (!bindingInfo.notifiedEvents) {
+ bindingInfo.notifiedEvents = {};
}
-
- // Optimization: Don't store the binding context on this node if it's definitely the same as on node.parentNode, because
- // we can easily recover it just by scanning up the node's ancestors in the DOM
- // (note: here, parent node means "real DOM parent" not "virtual parent", as there's no O(1) way to find the virtual parent)
- if (!alreadyBound && bindingContextMayDifferFromDomParentElement)
- ko.storedBindingContextForNode(node, bindingContext);
// Use bindings if given, otherwise fall back on asking the bindings provider to give us some bindings
var bindings;
@@ -3244,8 +3642,14 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
function() {
bindings = sourceBindings ? sourceBindings(bindingContext, node) : getBindings.call(provider, node, bindingContext);
// Register a dependency on the binding context to support observable view models.
- if (bindings && bindingContext._subscribable)
- bindingContext._subscribable();
+ if (bindings) {
+ if (bindingContext[contextSubscribable]) {
+ bindingContext[contextSubscribable]();
+ }
+ if (bindingContext[contextDataDependency]) {
+ bindingContext[contextDataDependency]();
+ }
+ }
return bindings;
},
null, { disposeWhenNodeIsRemoved: node }
@@ -3255,6 +3659,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
bindingsUpdater = null;
}
+ var contextToExtend = bindingContext;
var bindingHandlerThatControlsDescendantBindings;
if (bindings) {
// Return the value accessor for a given binding. When bindings are static (won't be updated because of a binding
@@ -3281,6 +3686,28 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
return key in bindings;
};
+ if (ko.bindingEvent.childrenComplete in bindings) {
+ ko.bindingEvent.subscribe(node, ko.bindingEvent.childrenComplete, function () {
+ var callback = evaluateValueAccessor(bindings[ko.bindingEvent.childrenComplete]);
+ if (callback) {
+ var nodes = ko.virtualElements.childNodes(node);
+ if (nodes.length) {
+ callback(nodes, ko.dataFor(nodes[0]));
+ }
+ }
+ });
+ }
+
+ if (ko.bindingEvent.descendantsComplete in bindings) {
+ contextToExtend = ko.bindingEvent.startPossiblyAsyncContentBinding(node, bindingContext);
+ ko.bindingEvent.subscribe(node, ko.bindingEvent.descendantsComplete, function () {
+ var callback = evaluateValueAccessor(bindings[ko.bindingEvent.descendantsComplete]);
+ if (callback && ko.virtualElements.firstChild(node)) {
+ callback(node);
+ }
+ });
+ }
+
// First put the bindings into the right order
var orderedBindings = topologicalSortBindings(bindings);
@@ -3300,7 +3727,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
// Run init, ignoring any dependencies
if (typeof handlerInitFn == "function") {
ko.dependencyDetection.ignore(function() {
- var initResult = handlerInitFn(node, getValueAccessor(bindingKey), allBindings, bindingContext['$data'], bindingContext);
+ var initResult = handlerInitFn(node, getValueAccessor(bindingKey), allBindings, contextToExtend['$data'], contextToExtend);
// If this binding handler claims to control descendant bindings, make a note of this
if (initResult && initResult['controlsDescendantBindings']) {
@@ -3315,7 +3742,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
if (typeof handlerUpdateFn == "function") {
ko.dependentObservable(
function() {
- handlerUpdateFn(node, getValueAccessor(bindingKey), allBindings, bindingContext['$data'], bindingContext);
+ handlerUpdateFn(node, getValueAccessor(bindingKey), allBindings, contextToExtend['$data'], contextToExtend);
},
null,
{ disposeWhenNodeIsRemoved: node }
@@ -3328,32 +3755,28 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
});
}
+ var shouldBindDescendants = bindingHandlerThatControlsDescendantBindings === undefined;
return {
- 'shouldBindDescendants': bindingHandlerThatControlsDescendantBindings === undefined
+ 'shouldBindDescendants': shouldBindDescendants,
+ 'bindingContextForDescendants': shouldBindDescendants && contextToExtend
};
};
- var storedBindingContextDomDataKey = ko.utils.domData.nextKey();
- ko.storedBindingContextForNode = function (node, bindingContext) {
- if (arguments.length == 2) {
- ko.utils.domData.set(node, storedBindingContextDomDataKey, bindingContext);
- if (bindingContext._subscribable)
- bindingContext._subscribable._addNode(node);
- } else {
- return ko.utils.domData.get(node, storedBindingContextDomDataKey);
- }
+ ko.storedBindingContextForNode = function (node) {
+ var bindingInfo = ko.utils.domData.get(node, boundElementDomDataKey);
+ return bindingInfo && bindingInfo.context;
}
- function getBindingContext(viewModelOrBindingContext) {
+ function getBindingContext(viewModelOrBindingContext, extendContextCallback) {
return viewModelOrBindingContext && (viewModelOrBindingContext instanceof ko.bindingContext)
? viewModelOrBindingContext
- : new ko.bindingContext(viewModelOrBindingContext);
+ : new ko.bindingContext(viewModelOrBindingContext, undefined, undefined, extendContextCallback);
}
ko.applyBindingAccessorsToNode = function (node, bindings, viewModelOrBindingContext) {
if (node.nodeType === 1) // If it's an element, workaround IE <= 8 HTML parsing weirdness
ko.virtualElements.normaliseVirtualElementDomStructure(node);
- return applyBindingsToNodeInternal(node, bindings, getBindingContext(viewModelOrBindingContext), true);
+ return applyBindingsToNodeInternal(node, bindings, getBindingContext(viewModelOrBindingContext));
};
ko.applyBindingsToNode = function (node, bindings, viewModelOrBindingContext) {
@@ -3363,32 +3786,32 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
ko.applyBindingsToDescendants = function(viewModelOrBindingContext, rootNode) {
if (rootNode.nodeType === 1 || rootNode.nodeType === 8)
- applyBindingsToDescendantsInternal(getBindingContext(viewModelOrBindingContext), rootNode, true);
+ applyBindingsToDescendantsInternal(getBindingContext(viewModelOrBindingContext), rootNode);
};
- ko.applyBindings = function (viewModelOrBindingContext, rootNode) {
+ ko.applyBindings = function (viewModelOrBindingContext, rootNode, extendContextCallback) {
// If jQuery is loaded after Knockout, we won't initially have access to it. So save it here.
if (!jQueryInstance && window['jQuery']) {
jQueryInstance = window['jQuery'];
}
- if (rootNode && (rootNode.nodeType !== 1) && (rootNode.nodeType !== 8))
- throw new Error("ko.applyBindings: first parameter should be your view model; second parameter should be a DOM node");
- rootNode = rootNode || window.document.body; // Make "rootNode" parameter optional
+ if (arguments.length < 2) {
+ rootNode = document.body;
+ if (!rootNode) {
+ throw Error("ko.applyBindings: could not find document.body; has the document been loaded?");
+ }
+ } else if (!rootNode || (rootNode.nodeType !== 1 && rootNode.nodeType !== 8)) {
+ throw Error("ko.applyBindings: first parameter should be your view model; second parameter should be a DOM node");
+ }
- applyBindingsToNodeAndDescendantsInternal(getBindingContext(viewModelOrBindingContext), rootNode, true);
+ applyBindingsToNodeAndDescendantsInternal(getBindingContext(viewModelOrBindingContext, extendContextCallback), rootNode);
};
// Retrieving binding context from arbitrary nodes
ko.contextFor = function(node) {
// We can only do something meaningful for elements and comment nodes (in particular, not text nodes, as IE can't store domdata for them)
- switch (node.nodeType) {
- case 1:
- case 8:
- var context = ko.storedBindingContextForNode(node);
- if (context) return context;
- if (node.parentNode) return ko.contextFor(node.parentNode);
- break;
+ if (node && (node.nodeType === 1 || node.nodeType === 8)) {
+ return ko.storedBindingContextForNode(node);
}
return undefined;
};
@@ -3398,6 +3821,9 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
};
ko.exportSymbol('bindingHandlers', ko.bindingHandlers);
+ ko.exportSymbol('bindingEvent', ko.bindingEvent);
+ ko.exportSymbol('bindingEvent.subscribe', ko.bindingEvent.subscribe);
+ ko.exportSymbol('bindingEvent.startPossiblyAsyncContentBinding', ko.bindingEvent.startPossiblyAsyncContentBinding);
ko.exportSymbol('applyBindings', ko.applyBindings);
ko.exportSymbol('applyBindingsToDescendants', ko.applyBindingsToDescendants);
ko.exportSymbol('applyBindingAccessorsToNode', ko.applyBindingAccessorsToNode);
@@ -3437,7 +3863,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
};
function getObjectOwnProperty(obj, propName) {
- return obj.hasOwnProperty(propName) ? obj[propName] : undefined;
+ return Object.prototype.hasOwnProperty.call(obj, propName) ? obj[propName] : undefined;
}
function loadComponentAndNotify(componentName, callback) {
@@ -3574,7 +4000,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
};
ko.components.isRegistered = function(componentName) {
- return defaultConfigRegistry.hasOwnProperty(componentName);
+ return Object.prototype.hasOwnProperty.call(defaultConfigRegistry, componentName);
};
ko.components.unregister = function(componentName) {
@@ -3584,7 +4010,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
ko.components.defaultLoader = {
'getConfig': function(componentName, callback) {
- var result = defaultConfigRegistry.hasOwnProperty(componentName)
+ var result = ko.components.isRegistered(componentName)
? defaultConfigRegistry[componentName]
: null;
callback(result);
@@ -3744,7 +4170,12 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
if (typeof config['require'] === 'string') {
// The config is the value of an AMD module
if (amdRequire || window['require']) {
- (amdRequire || window['require'])([config['require']], callback);
+ (amdRequire || window['require'])([config['require']], function (module) {
+ if (module && typeof module === 'object' && module.__esModule && module.default) {
+ module = module.default;
+ }
+ callback(module);
+ });
} else {
errorCallback('Uses require, but no AMD loader is present');
}
@@ -3847,7 +4278,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
// Give access to the raw computeds, as long as that wouldn't overwrite any custom param also called '$raw'
// This is in case the developer wants to react to outer (binding) observability separately from inner
// (model value) observability, or in case the model value observable has subobservables.
- if (!result.hasOwnProperty('$raw')) {
+ if (!Object.prototype.hasOwnProperty.call(result, '$raw')) {
result['$raw'] = rawParamComputedValues;
}
@@ -3879,7 +4310,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
var newDocFrag = originalFunction(),
allComponents = ko.components._allRegisteredComponents;
for (var componentName in allComponents) {
- if (allComponents.hasOwnProperty(componentName)) {
+ if (Object.prototype.hasOwnProperty.call(allComponents, componentName)) {
newDocFrag.createElement(componentName);
}
}
@@ -3888,24 +4319,29 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
})(document.createDocumentFragment);
}
})();(function(undefined) {
-
var componentLoadingOperationUniqueId = 0;
ko.bindingHandlers['component'] = {
'init': function(element, valueAccessor, ignored1, ignored2, bindingContext) {
var currentViewModel,
currentLoadingOperationId,
+ afterRenderSub,
disposeAssociatedComponentViewModel = function () {
var currentViewModelDispose = currentViewModel && currentViewModel['dispose'];
if (typeof currentViewModelDispose === 'function') {
currentViewModelDispose.call(currentViewModel);
}
+ if (afterRenderSub) {
+ afterRenderSub.dispose();
+ }
+ afterRenderSub = null;
currentViewModel = null;
// Any in-flight loading operation is no longer relevant, so make sure we ignore its completion
currentLoadingOperationId = null;
},
originalChildNodes = ko.utils.makeArray(ko.virtualElements.childNodes(element));
+ ko.virtualElements.emptyNode(element);
ko.utils.domNodeDisposal.addDisposeCallback(element, disposeAssociatedComponentViewModel);
ko.computed(function () {
@@ -3923,6 +4359,8 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
throw new Error('No component name specified');
}
+ var asyncContext = ko.bindingEvent.startPossiblyAsyncContentBinding(element, bindingContext);
+
var loadingOperationId = currentLoadingOperationId = ++componentLoadingOperationUniqueId;
ko.components.get(componentName, function(componentDefinition) {
// If this is not the current load operation for this element, ignore it.
@@ -3938,11 +4376,24 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
throw new Error('Unknown component \'' + componentName + '\'');
}
cloneTemplateIntoElement(componentName, componentDefinition, element);
- var componentViewModel = createViewModel(componentDefinition, element, originalChildNodes, componentParams),
- childBindingContext = bindingContext['createChildContext'](componentViewModel, /* dataItemAlias */ undefined, function(ctx) {
- ctx['$component'] = componentViewModel;
- ctx['$componentTemplateNodes'] = originalChildNodes;
+
+ var componentInfo = {
+ 'element': element,
+ 'templateNodes': originalChildNodes
+ };
+
+ var componentViewModel = createViewModel(componentDefinition, componentParams, componentInfo),
+ childBindingContext = asyncContext['createChildContext'](componentViewModel, {
+ 'extend': function(ctx) {
+ ctx['$component'] = componentViewModel;
+ ctx['$componentTemplateNodes'] = originalChildNodes;
+ }
});
+
+ if (componentViewModel && componentViewModel['koDescendantsComplete']) {
+ afterRenderSub = ko.bindingEvent.subscribe(element, ko.bindingEvent.descendantsComplete, componentViewModel['koDescendantsComplete'], componentViewModel);
+ }
+
currentViewModel = componentViewModel;
ko.applyBindingsToDescendants(childBindingContext, element);
});
@@ -3964,40 +4415,47 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
ko.virtualElements.setDomNodeChildren(element, clonedNodesArray);
}
- function createViewModel(componentDefinition, element, originalChildNodes, componentParams) {
+ function createViewModel(componentDefinition, componentParams, componentInfo) {
var componentViewModelFactory = componentDefinition['createViewModel'];
return componentViewModelFactory
- ? componentViewModelFactory.call(componentDefinition, componentParams, { 'element': element, 'templateNodes': originalChildNodes })
+ ? componentViewModelFactory.call(componentDefinition, componentParams, componentInfo)
: componentParams; // Template-only component
}
})();
-var attrHtmlToJavascriptMap = { 'class': 'className', 'for': 'htmlFor' };
+var attrHtmlToJavaScriptMap = { 'class': 'className', 'for': 'htmlFor' };
ko.bindingHandlers['attr'] = {
'update': function(element, valueAccessor, allBindings) {
var value = ko.utils.unwrapObservable(valueAccessor()) || {};
ko.utils.objectForEach(value, function(attrName, attrValue) {
attrValue = ko.utils.unwrapObservable(attrValue);
+ // Find the namespace of this attribute, if any.
+ var prefixLen = attrName.indexOf(':');
+ var namespace = "lookupNamespaceURI" in element && prefixLen > 0 && element.lookupNamespaceURI(attrName.substr(0, prefixLen));
+
// To cover cases like "attr: { checked:someProp }", we want to remove the attribute entirely
// when someProp is a "no value"-like value (strictly null, false, or undefined)
// (because the absence of the "checked" attr is how to mark an element as not checked, etc.)
var toRemove = (attrValue === false) || (attrValue === null) || (attrValue === undefined);
- if (toRemove)
- element.removeAttribute(attrName);
+ if (toRemove) {
+ namespace ? element.removeAttributeNS(namespace, attrName) : element.removeAttribute(attrName);
+ } else {
+ attrValue = attrValue.toString();
+ }
- // In IE <= 7 and IE8 Quirks Mode, you have to use the Javascript property name instead of the
+ // In IE <= 7 and IE8 Quirks Mode, you have to use the JavaScript property name instead of the
// HTML attribute name for certain attributes. IE8 Standards Mode supports the correct behavior,
- // but instead of figuring out the mode, we'll just set the attribute through the Javascript
+ // but instead of figuring out the mode, we'll just set the attribute through the JavaScript
// property for IE <= 8.
- if (ko.utils.ieVersion <= 8 && attrName in attrHtmlToJavascriptMap) {
- attrName = attrHtmlToJavascriptMap[attrName];
+ if (ko.utils.ieVersion <= 8 && attrName in attrHtmlToJavaScriptMap) {
+ attrName = attrHtmlToJavaScriptMap[attrName];
if (toRemove)
element.removeAttribute(attrName);
else
element[attrName] = attrValue;
} else if (!toRemove) {
- element.setAttribute(attrName, attrValue.toString());
+ namespace ? element.setAttributeNS(namespace, attrName, attrValue) : element.setAttribute(attrName, attrValue);
}
// Treat "name" specially - although you can think of it as an attribute, it also needs
@@ -4005,7 +4463,7 @@ ko.bindingHandlers['attr'] = {
// Deliberately being case-sensitive here because XHTML would regard "Name" as a different thing
// entirely, and there's no strong reason to allow for such casing in HTML.
if (attrName === "name") {
- ko.utils.setElementName(element, toRemove ? "" : attrValue.toString());
+ ko.utils.setElementName(element, toRemove ? "" : attrValue);
}
});
}
@@ -4019,18 +4477,20 @@ ko.bindingHandlers['checked'] = {
// Treat "value" like "checkedValue" when it is included with "checked" binding
if (allBindings['has']('checkedValue')) {
return ko.utils.unwrapObservable(allBindings.get('checkedValue'));
- } else if (allBindings['has']('value')) {
- return ko.utils.unwrapObservable(allBindings.get('value'));
+ } else if (useElementValue) {
+ if (allBindings['has']('value')) {
+ return ko.utils.unwrapObservable(allBindings.get('value'));
+ } else {
+ return element.value;
+ }
}
-
- return element.value;
});
function updateModel() {
// This updates the model value from the view value.
// It runs in response to DOM events (click) and changes in checkedValue.
var isChecked = element.checked,
- elemValue = useCheckedValue ? checkedValue() : isChecked;
+ elemValue = checkedValue();
// When we're first setting up this computed, don't change any model state.
if (ko.computedContext.isInitial()) {
@@ -4038,33 +4498,43 @@ ko.bindingHandlers['checked'] = {
}
// We can ignore unchecked radio buttons, because some other radio
- // button will be getting checked, and that one can take care of updating state.
- if (isRadio && !isChecked) {
+ // button will be checked, and that one can take care of updating state.
+ // Also ignore value changes to an already unchecked checkbox.
+ if (!isChecked && (isRadio || ko.computedContext.getDependenciesCount())) {
return;
}
var modelValue = ko.dependencyDetection.ignore(valueAccessor);
if (valueIsArray) {
- var writableValue = rawValueIsNonArrayObservable ? modelValue.peek() : modelValue;
- if (oldElemValue !== elemValue) {
+ var writableValue = rawValueIsNonArrayObservable ? modelValue.peek() : modelValue,
+ saveOldValue = oldElemValue;
+ oldElemValue = elemValue;
+
+ if (saveOldValue !== elemValue) {
// When we're responding to the checkedValue changing, and the element is
// currently checked, replace the old elem value with the new elem value
// in the model array.
if (isChecked) {
ko.utils.addOrRemoveItem(writableValue, elemValue, true);
- ko.utils.addOrRemoveItem(writableValue, oldElemValue, false);
+ ko.utils.addOrRemoveItem(writableValue, saveOldValue, false);
}
-
- oldElemValue = elemValue;
} else {
// When we're responding to the user having checked/unchecked a checkbox,
// add/remove the element value to the model array.
ko.utils.addOrRemoveItem(writableValue, elemValue, isChecked);
}
+
if (rawValueIsNonArrayObservable && ko.isWriteableObservable(modelValue)) {
modelValue(writableValue);
}
} else {
+ if (isCheckbox) {
+ if (elemValue === undefined) {
+ elemValue = isChecked;
+ } else if (!isChecked) {
+ elemValue = undefined;
+ }
+ }
ko.expressionRewriting.writeValueToProperty(modelValue, allBindings, 'checked', elemValue, true);
}
};
@@ -4072,16 +4542,19 @@ ko.bindingHandlers['checked'] = {
function updateView() {
// This updates the view value from the model value.
// It runs in response to changes in the bound (checked) value.
- var modelValue = ko.utils.unwrapObservable(valueAccessor());
+ var modelValue = ko.utils.unwrapObservable(valueAccessor()),
+ elemValue = checkedValue();
if (valueIsArray) {
// When a checkbox is bound to an array, being checked represents its value being present in that array
- element.checked = ko.utils.arrayIndexOf(modelValue, checkedValue()) >= 0;
- } else if (isCheckbox) {
- // When a checkbox is bound to any other value (not an array), being checked represents the value being trueish
- element.checked = modelValue;
+ element.checked = ko.utils.arrayIndexOf(modelValue, elemValue) >= 0;
+ oldElemValue = elemValue;
+ } else if (isCheckbox && elemValue === undefined) {
+ // When a checkbox is bound to any other value (not an array) and "checkedValue" is not defined,
+ // being checked represents the value being trueish
+ element.checked = !!modelValue;
} else {
- // For radio buttons, being checked means that the radio button's value corresponds to the model value
+ // Otherwise, being checked means that the checkbox or radio button's value corresponds to the model value
element.checked = (checkedValue() === modelValue);
}
};
@@ -4097,8 +4570,8 @@ ko.bindingHandlers['checked'] = {
var rawValue = valueAccessor(),
valueIsArray = isCheckbox && (ko.utils.unwrapObservable(rawValue) instanceof Array),
rawValueIsNonArrayObservable = !(valueIsArray && rawValue.push && rawValue.splice),
- oldElemValue = valueIsArray ? checkedValue() : undefined,
- useCheckedValue = isRadio || valueIsArray;
+ useElementValue = isRadio || valueIsArray,
+ oldElemValue = valueIsArray ? checkedValue() : undefined;
// IE 6 won't allow radio buttons to be selected unless they have a name
if (isRadio && !element.name)
@@ -4125,6 +4598,15 @@ ko.bindingHandlers['checkedValue'] = {
};
})();var classesWrittenByBindingKey = '__ko__cssValue';
+ko.bindingHandlers['class'] = {
+ 'update': function (element, valueAccessor) {
+ var value = ko.utils.stringTrim(ko.utils.unwrapObservable(valueAccessor()));
+ ko.utils.toggleDomNodeCssClass(element, element[classesWrittenByBindingKey], false);
+ element[classesWrittenByBindingKey] = value;
+ ko.utils.toggleDomNodeCssClass(element, value, true);
+ }
+};
+
ko.bindingHandlers['css'] = {
'update': function (element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor());
@@ -4134,10 +4616,7 @@ ko.bindingHandlers['css'] = {
ko.utils.toggleDomNodeCssClass(element, className, shouldHaveClass);
});
} else {
- value = ko.utils.stringTrim(String(value || '')); // Make sure we don't try to store or set a non-string value
- ko.utils.toggleDomNodeCssClass(element, element[classesWrittenByBindingKey], false);
- element[classesWrittenByBindingKey] = value;
- ko.utils.toggleDomNodeCssClass(element, value, true);
+ ko.bindingHandlers['class']['update'](element, valueAccessor);
}
}
};
@@ -4227,6 +4706,7 @@ ko.bindingHandlers['foreach'] = {
return {
'foreach': unwrappedValue['data'],
'as': unwrappedValue['as'],
+ 'noChildContext': unwrappedValue['noChildContext'],
'includeDestroyed': unwrappedValue['includeDestroyed'],
'afterAdd': unwrappedValue['afterAdd'],
'beforeRemove': unwrappedValue['beforeRemove'],
@@ -4283,6 +4763,9 @@ ko.bindingHandlers['hasfocus'] = {
ko.utils.registerEventHandler(element, "focusin", handleElementFocusIn); // For IE
ko.utils.registerEventHandler(element, "blur", handleElementFocusOut);
ko.utils.registerEventHandler(element, "focusout", handleElementFocusOut); // For IE
+
+ // Assume element is not focused (prevents "blur" being called initially)
+ element[hasfocusLastValue] = false;
},
'update': function(element, valueAccessor) {
var value = !!ko.utils.unwrapObservable(valueAccessor());
@@ -4305,7 +4788,7 @@ ko.bindingHandlers['hasfocus'] = {
ko.expressionRewriting.twoWayBindings['hasfocus'] = true;
ko.bindingHandlers['hasFocus'] = ko.bindingHandlers['hasfocus']; // Make "hasFocus" an alias
-ko.expressionRewriting.twoWayBindings['hasFocus'] = true;
+ko.expressionRewriting.twoWayBindings['hasFocus'] = 'hasfocus';
ko.bindingHandlers['html'] = {
'init': function() {
// Prevent binding on the dynamically-injected HTML (as developers are unlikely to expect that, and it has security implications)
@@ -4316,36 +4799,74 @@ ko.bindingHandlers['html'] = {
ko.utils.setHtml(element, valueAccessor());
}
};
+(function () {
+
// Makes a binding like with or if
-function makeWithIfBinding(bindingKey, isWith, isNot, makeContextCallback) {
+function makeWithIfBinding(bindingKey, isWith, isNot) {
ko.bindingHandlers[bindingKey] = {
'init': function(element, valueAccessor, allBindings, viewModel, bindingContext) {
- var didDisplayOnLastUpdate,
- savedNodes;
+ var didDisplayOnLastUpdate, savedNodes, contextOptions = {}, completeOnRender, needAsyncContext, renderOnEveryChange;
+
+ if (isWith) {
+ var as = allBindings.get('as'), noChildContext = allBindings.get('noChildContext');
+ renderOnEveryChange = !(as && noChildContext);
+ contextOptions = { 'as': as, 'noChildContext': noChildContext, 'exportDependencies': renderOnEveryChange };
+ }
+
+ completeOnRender = allBindings.get("completeOn") == "render";
+ needAsyncContext = completeOnRender || allBindings['has'](ko.bindingEvent.descendantsComplete);
+
ko.computed(function() {
- var dataValue = ko.utils.unwrapObservable(valueAccessor()),
- shouldDisplay = !isNot !== !dataValue, // equivalent to isNot ? !dataValue : !!dataValue
- isFirstRender = !savedNodes,
- needsRefresh = isFirstRender || isWith || (shouldDisplay !== didDisplayOnLastUpdate);
+ var value = ko.utils.unwrapObservable(valueAccessor()),
+ shouldDisplay = !isNot !== !value, // equivalent to isNot ? !value : !!value,
+ isInitial = !savedNodes,
+ childContext;
- if (needsRefresh) {
- // Save a copy of the inner nodes on the initial update, but only if we have dependencies.
- if (isFirstRender && ko.computedContext.getDependenciesCount()) {
- savedNodes = ko.utils.cloneNodes(ko.virtualElements.childNodes(element), true /* shouldCleanNodes */);
- }
-
- if (shouldDisplay) {
- if (!isFirstRender) {
- ko.virtualElements.setDomNodeChildren(element, ko.utils.cloneNodes(savedNodes));
- }
- ko.applyBindingsToDescendants(makeContextCallback ? makeContextCallback(bindingContext, dataValue) : bindingContext, element);
- } else {
- ko.virtualElements.emptyNode(element);
- }
-
- didDisplayOnLastUpdate = shouldDisplay;
+ if (!renderOnEveryChange && shouldDisplay === didDisplayOnLastUpdate) {
+ return;
}
+
+ if (needAsyncContext) {
+ bindingContext = ko.bindingEvent.startPossiblyAsyncContentBinding(element, bindingContext);
+ }
+
+ if (shouldDisplay) {
+ if (!isWith || renderOnEveryChange) {
+ contextOptions['dataDependency'] = ko.computedContext.computed();
+ }
+
+ if (isWith) {
+ childContext = bindingContext['createChildContext'](typeof value == "function" ? value : valueAccessor, contextOptions);
+ } else if (ko.computedContext.getDependenciesCount()) {
+ childContext = bindingContext['extend'](null, contextOptions);
+ } else {
+ childContext = bindingContext;
+ }
+ }
+
+ // Save a copy of the inner nodes on the initial update, but only if we have dependencies.
+ if (isInitial && ko.computedContext.getDependenciesCount()) {
+ savedNodes = ko.utils.cloneNodes(ko.virtualElements.childNodes(element), true /* shouldCleanNodes */);
+ }
+
+ if (shouldDisplay) {
+ if (!isInitial) {
+ ko.virtualElements.setDomNodeChildren(element, ko.utils.cloneNodes(savedNodes));
+ }
+
+ ko.applyBindingsToDescendants(childContext, element);
+ } else {
+ ko.virtualElements.emptyNode(element);
+
+ if (!completeOnRender) {
+ ko.bindingEvent.notify(element, ko.bindingEvent.childrenComplete);
+ }
+ }
+
+ didDisplayOnLastUpdate = shouldDisplay;
+
}, null, { disposeWhenNodeIsRemoved: element });
+
return { 'controlsDescendantBindings': true };
}
};
@@ -4356,11 +4877,18 @@ function makeWithIfBinding(bindingKey, isWith, isNot, makeContextCallback) {
// Construct the actual binding handlers
makeWithIfBinding('if');
makeWithIfBinding('ifnot', false /* isWith */, true /* isNot */);
-makeWithIfBinding('with', true /* isWith */, false /* isNot */,
- function(bindingContext, dataValue) {
- return bindingContext['createChildContext'](dataValue);
+makeWithIfBinding('with', true /* isWith */);
+
+})();ko.bindingHandlers['let'] = {
+ 'init': function(element, valueAccessor, allBindings, viewModel, bindingContext) {
+ // Make a modified binding context, with extra properties, and apply it to descendant elements
+ var innerContext = bindingContext['extend'](valueAccessor);
+ ko.applyBindingsToDescendants(innerContext, element);
+
+ return { 'controlsDescendantBindings': true };
}
-);
+};
+ko.virtualElements.allowedBindings['let'] = true;
var captionPlaceholder = {};
ko.bindingHandlers['options'] = {
'init': function(element) {
@@ -4466,8 +4994,7 @@ ko.bindingHandlers['options'] = {
function setSelectionCallback(arrayEntry, newOptions) {
if (itemUpdate && valueAllowUnset) {
// The model value is authoritative, so make sure its value is the one selected
- // There is no need to use dependencyDetection.ignore since setDomNodeChildrenFromArrayMapping does so already.
- ko.selectExtensions.writeValue(element, ko.utils.unwrapObservable(allBindings.get('value')), true /* allowUnset */);
+ ko.bindingEvent.notify(element, ko.bindingEvent.childrenComplete);
} else if (previousSelectedValues.length) {
// IE6 doesn't like us to assign selection to OPTION nodes before they're added to the document.
// That's why we first added them without selection. Now it's time to set the selection.
@@ -4491,33 +5018,32 @@ ko.bindingHandlers['options'] = {
ko.utils.setDomNodeChildrenFromArrayMapping(element, filteredArray, optionForArrayItem, arrayToDomNodeChildrenOptions, callback);
- ko.dependencyDetection.ignore(function () {
- if (valueAllowUnset) {
- // The model value is authoritative, so make sure its value is the one selected
- ko.selectExtensions.writeValue(element, ko.utils.unwrapObservable(allBindings.get('value')), true /* allowUnset */);
+ if (!valueAllowUnset) {
+ // Determine if the selection has changed as a result of updating the options list
+ var selectionChanged;
+ if (multiple) {
+ // For a multiple-select box, compare the new selection count to the previous one
+ // But if nothing was selected before, the selection can't have changed
+ selectionChanged = previousSelectedValues.length && selectedOptions().length < previousSelectedValues.length;
} else {
- // Determine if the selection has changed as a result of updating the options list
- var selectionChanged;
- if (multiple) {
- // For a multiple-select box, compare the new selection count to the previous one
- // But if nothing was selected before, the selection can't have changed
- selectionChanged = previousSelectedValues.length && selectedOptions().length < previousSelectedValues.length;
- } else {
- // For a single-select box, compare the current value to the previous value
- // But if nothing was selected before or nothing is selected now, just look for a change in selection
- selectionChanged = (previousSelectedValues.length && element.selectedIndex >= 0)
- ? (ko.selectExtensions.readValue(element.options[element.selectedIndex]) !== previousSelectedValues[0])
- : (previousSelectedValues.length || element.selectedIndex >= 0);
- }
-
- // Ensure consistency between model value and selected option.
- // If the dropdown was changed so that selection is no longer the same,
- // notify the value or selectedOptions binding.
- if (selectionChanged) {
- ko.utils.triggerEvent(element, "change");
- }
+ // For a single-select box, compare the current value to the previous value
+ // But if nothing was selected before or nothing is selected now, just look for a change in selection
+ selectionChanged = (previousSelectedValues.length && element.selectedIndex >= 0)
+ ? (ko.selectExtensions.readValue(element.options[element.selectedIndex]) !== previousSelectedValues[0])
+ : (previousSelectedValues.length || element.selectedIndex >= 0);
}
- });
+
+ // Ensure consistency between model value and selected option.
+ // If the dropdown was changed so that selection is no longer the same,
+ // notify the value or selectedOptions binding.
+ if (selectionChanged) {
+ ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, "change"]);
+ }
+ }
+
+ if (valueAllowUnset || ko.computedContext.isInitial()) {
+ ko.bindingEvent.notify(element, ko.bindingEvent.childrenComplete);
+ }
// Workaround for IE bug
ko.utils.ensureSelectElementIsRenderedCorrectly(element);
@@ -4528,35 +5054,47 @@ ko.bindingHandlers['options'] = {
};
ko.bindingHandlers['options'].optionValueDomDataKey = ko.utils.domData.nextKey();
ko.bindingHandlers['selectedOptions'] = {
- 'after': ['options', 'foreach'],
'init': function (element, valueAccessor, allBindings) {
- ko.utils.registerEventHandler(element, "change", function () {
+ function updateFromView() {
var value = valueAccessor(), valueToWrite = [];
ko.utils.arrayForEach(element.getElementsByTagName("option"), function(node) {
if (node.selected)
valueToWrite.push(ko.selectExtensions.readValue(node));
});
ko.expressionRewriting.writeValueToProperty(value, allBindings, 'selectedOptions', valueToWrite);
- });
- },
- 'update': function (element, valueAccessor) {
- if (ko.utils.tagNameLower(element) != "select")
- throw new Error("values binding applies only to SELECT elements");
-
- var newValue = ko.utils.unwrapObservable(valueAccessor()),
- previousScrollTop = element.scrollTop;
-
- if (newValue && typeof newValue.length == "number") {
- ko.utils.arrayForEach(element.getElementsByTagName("option"), function(node) {
- var isSelected = ko.utils.arrayIndexOf(newValue, ko.selectExtensions.readValue(node)) >= 0;
- if (node.selected != isSelected) { // This check prevents flashing of the select element in IE
- ko.utils.setOptionNodeSelectionState(node, isSelected);
- }
- });
}
- element.scrollTop = previousScrollTop;
- }
+ function updateFromModel() {
+ var newValue = ko.utils.unwrapObservable(valueAccessor()),
+ previousScrollTop = element.scrollTop;
+
+ if (newValue && typeof newValue.length == "number") {
+ ko.utils.arrayForEach(element.getElementsByTagName("option"), function(node) {
+ var isSelected = ko.utils.arrayIndexOf(newValue, ko.selectExtensions.readValue(node)) >= 0;
+ if (node.selected != isSelected) { // This check prevents flashing of the select element in IE
+ ko.utils.setOptionNodeSelectionState(node, isSelected);
+ }
+ });
+ }
+
+ element.scrollTop = previousScrollTop;
+ }
+
+ if (ko.utils.tagNameLower(element) != "select") {
+ throw new Error("selectedOptions binding applies only to SELECT elements");
+ }
+
+ var updateFromModelComputed;
+ ko.bindingEvent.subscribe(element, ko.bindingEvent.childrenComplete, function () {
+ if (!updateFromModelComputed) {
+ ko.utils.registerEventHandler(element, "change", updateFromView);
+ updateFromModelComputed = ko.computed(updateFromModel, null, { disposeWhenNodeIsRemoved: element });
+ } else {
+ updateFromView();
+ }
+ }, null, { 'notifyImmediately': true });
+ },
+ 'update': function() {} // Keep for backwards compatibility with code that may have wrapped binding
};
ko.expressionRewriting.twoWayBindings['selectedOptions'] = true;
ko.bindingHandlers['style'] = {
@@ -4570,7 +5108,23 @@ ko.bindingHandlers['style'] = {
styleValue = "";
}
- element.style[styleName] = styleValue;
+ if (jQueryInstance) {
+ jQueryInstance(element)['css'](styleName, styleValue);
+ } else if (/^--/.test(styleName)) {
+ // Is styleName a custom CSS property?
+ element.style.setProperty(styleName, styleValue);
+ } else {
+ styleName = styleName.replace(/-(\w)/g, function (all, letter) {
+ return letter.toUpperCase();
+ });
+
+ var previousStyle = element.style[styleName];
+ element.style[styleName] = styleValue;
+
+ if (styleValue !== previousStyle && element.style[styleName] == previousStyle && !isNaN(styleValue)) {
+ element.style[styleName] = styleValue + "px";
+ }
+ }
});
}
};
@@ -4614,10 +5168,16 @@ if (window && window.navigator) {
};
// Detect various browser versions because some old versions don't fully support the 'input' event
- var operaVersion = window.opera && window.opera.version && parseInt(window.opera.version()),
- userAgent = window.navigator.userAgent,
- safariVersion = parseVersion(userAgent.match(/^(?:(?!chrome).)*version\/([^ ]*) safari/i)),
- firefoxVersion = parseVersion(userAgent.match(/Firefox\/([^ ]*)/));
+ var userAgent = window.navigator.userAgent,
+ operaVersion, chromeVersion, safariVersion, firefoxVersion, ieVersion, edgeVersion;
+
+ (operaVersion = window.opera && window.opera.version && parseInt(window.opera.version()))
+ || (edgeVersion = parseVersion(userAgent.match(/Edge\/([^ ]+)$/)))
+ || (chromeVersion = parseVersion(userAgent.match(/Chrome\/([^ ]+)/)))
+ || (safariVersion = parseVersion(userAgent.match(/Version\/([^ ]+) Safari/)))
+ || (firefoxVersion = parseVersion(userAgent.match(/Firefox\/([^ ]+)/)))
+ || (ieVersion = ko.utils.ieVersion || parseVersion(userAgent.match(/MSIE ([^ ]+)/))) // Detects up to IE 10
+ || (ieVersion = parseVersion(userAgent.match(/rv:([^ )]+)/))); // Detects IE 11
}
// IE 8 and 9 have bugs that prevent the normal events from firing when the value changes.
@@ -4626,7 +5186,7 @@ if (window && window.navigator) {
// fired at the document level only and doesn't directly indicate which element changed. We
// set up just one event handler for the document and use 'activeElement' to determine which
// element was changed.
-if (ko.utils.ieVersion < 10) {
+if (ieVersion >= 8 && ieVersion < 10) {
var selectionChangeRegisteredName = ko.utils.domData.nextKey(),
selectionChangeHandlerName = ko.utils.domData.nextKey();
var selectionChangeHandler = function(event) {
@@ -4680,7 +5240,8 @@ ko.bindingHandlers['textInput'] = {
// IE9 will mess up the DOM if you handle events synchronously which results in DOM changes (such as other bindings);
// so we'll make sure all updates are asynchronous
- var ieUpdateModel = ko.utils.ieVersion == 9 ? deferUpdateModel : updateModel;
+ var ieUpdateModel = ko.utils.ieVersion == 9 ? deferUpdateModel : updateModel,
+ ourUpdate = false;
var updateView = function () {
var modelValue = ko.utils.unwrapObservable(valueAccessor());
@@ -4697,8 +5258,10 @@ ko.bindingHandlers['textInput'] = {
// Update the element only if the element and model are different. On some browsers, updating the value
// will move the cursor to the end of the input, which would be bad while the user is typing.
if (element.value !== modelValue) {
- previousElementValue = modelValue; // Make sure we ignore events (propertychange) that result from updating the value
+ ourUpdate = true; // Make sure we ignore events (propertychange) that result from updating the value
element.value = modelValue;
+ ourUpdate = false;
+ previousElementValue = element.value; // In case the browser changes the value (see #2281)
}
};
@@ -4716,62 +5279,74 @@ ko.bindingHandlers['textInput'] = {
}
});
} else {
- if (ko.utils.ieVersion < 10) {
+ if (ieVersion) {
+ // All versions (including 11) of Internet Explorer have a bug that they don't generate an input or propertychange event when ESC is pressed
+ onEvent('keypress', updateModel);
+ }
+ if (ieVersion < 11) {
// Internet Explorer <= 8 doesn't support the 'input' event, but does include 'propertychange' that fires whenever
// any property of an element changes. Unlike 'input', it also fires if a property is changed from JavaScript code,
- // but that's an acceptable compromise for this binding. IE 9 does support 'input', but since it doesn't fire it
- // when using autocomplete, we'll use 'propertychange' for it also.
+ // but that's an acceptable compromise for this binding. IE 9 and 10 support 'input', but since they don't always
+ // fire it when using autocomplete, we'll use 'propertychange' for them also.
onEvent('propertychange', function(event) {
- if (event.propertyName === 'value') {
+ if (!ourUpdate && event.propertyName === 'value') {
ieUpdateModel(event);
}
});
+ }
+ if (ieVersion == 8) {
+ // IE 8 has a bug where it fails to fire 'propertychange' on the first update following a value change from
+ // JavaScript code. It also doesn't fire if you clear the entire value. To fix this, we bind to the following
+ // events too.
+ onEvent('keyup', updateModel); // A single keystoke
+ onEvent('keydown', updateModel); // The first character when a key is held down
+ }
+ if (registerForSelectionChangeEvent) {
+ // Internet Explorer 9 doesn't fire the 'input' event when deleting text, including using
+ // the backspace, delete, or ctrl-x keys, clicking the 'x' to clear the input, dragging text
+ // out of the field, and cutting or deleting text using the context menu. 'selectionchange'
+ // can detect all of those except dragging text out of the field, for which we use 'dragend'.
+ // These are also needed in IE8 because of the bug described above.
+ registerForSelectionChangeEvent(element, ieUpdateModel); // 'selectionchange' covers cut, paste, drop, delete, etc.
+ onEvent('dragend', deferUpdateModel);
+ }
- if (ko.utils.ieVersion == 8) {
- // IE 8 has a bug where it fails to fire 'propertychange' on the first update following a value change from
- // JavaScript code. It also doesn't fire if you clear the entire value. To fix this, we bind to the following
- // events too.
- onEvent('keyup', updateModel); // A single keystoke
- onEvent('keydown', updateModel); // The first character when a key is held down
- }
- if (ko.utils.ieVersion >= 8) {
- // Internet Explorer 9 doesn't fire the 'input' event when deleting text, including using
- // the backspace, delete, or ctrl-x keys, clicking the 'x' to clear the input, dragging text
- // out of the field, and cutting or deleting text using the context menu. 'selectionchange'
- // can detect all of those except dragging text out of the field, for which we use 'dragend'.
- // These are also needed in IE8 because of the bug described above.
- registerForSelectionChangeEvent(element, ieUpdateModel); // 'selectionchange' covers cut, paste, drop, delete, etc.
- onEvent('dragend', deferUpdateModel);
- }
- } else {
+ if (!ieVersion || ieVersion >= 9) {
// All other supported browsers support the 'input' event, which fires whenever the content of the element is changed
// through the user interface.
- onEvent('input', updateModel);
+ onEvent('input', ieUpdateModel);
+ }
- if (safariVersion < 5 && ko.utils.tagNameLower(element) === "textarea") {
- // Safari <5 doesn't fire the 'input' event for