From 2fb7fadfc22b58186b6660958364fd7bd76f2cb8 Mon Sep 17 00:00:00 2001 From: Benedek Farkas Date: Thu, 2 Oct 2025 20:17:00 +0200 Subject: [PATCH] Upgrading Knockout from 3.4.0 to 3.5.1 --- .../Assets/Js/Knockout/knockout.js | 2168 +++++++++++------ .../ResourceManifests/Knockout.cs | 3 +- .../Scripts/Knockout/knockout.js | 2036 ++++++++++------ .../Scripts/Knockout/knockout.min.js | 6 +- 4 files changed, 2810 insertions(+), 1403 deletions(-) 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