diff --git a/index.js b/index.js
index 79882ce..7daccb9 100644
--- a/index.js
+++ b/index.js
@@ -2,8 +2,9 @@
// (c) Steven Sanderson - http://knockoutjs.com/
// License: MIT (http://www.opensource.org/licenses/mit-license.php)
-(function(window,document,navigator,undefined){
+(function(){
var DEBUG=true;
+(function(window,document,navigator,jQuery,undefined){
!function(factory) {
// Support three module loading scenarios
if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') {
@@ -196,7 +197,7 @@ ko.utils = new (function () {
setOptionNodeSelectionState: function (optionNode, isSelected) {
// IE6 sometimes throws "unknown error" if you try to write to .selected directly, whereas Firefox struggles with setAttribute. Pick one based on browser.
- if (navigator.userAgent.indexOf("MSIE 6") >= 0)
+ if (ieVersion < 7)
optionNode.setAttribute("selected", isSelected);
else
optionNode.selected = isSelected;
@@ -320,18 +321,21 @@ ko.utils = new (function () {
return ko.isObservable(value) ? value() : value;
},
- toggleDomNodeCssClass: function (node, className, shouldHaveClass) {
- var currentClassNames = (node.className || "").split(/\s+/);
- var hasClass = ko.utils.arrayIndexOf(currentClassNames, className) >= 0;
-
- if (shouldHaveClass && !hasClass) {
- node.className += (currentClassNames[0] ? " " : "") + className;
- } else if (hasClass && !shouldHaveClass) {
- var newClassName = "";
- for (var i = 0; i < currentClassNames.length; i++)
- if (currentClassNames[i] != className)
- newClassName += currentClassNames[i] + " ";
- node.className = ko.utils.stringTrim(newClassName);
+ toggleDomNodeCssClass: function (node, classNames, shouldHaveClass) {
+ if (classNames) {
+ var cssClassNameRegex = /[\w-]+/g,
+ currentClassNames = node.className.match(cssClassNameRegex) || [];
+ ko.utils.arrayForEach(classNames.match(cssClassNameRegex), function(className) {
+ var indexOfClass = ko.utils.arrayIndexOf(currentClassNames, className);
+ if (indexOfClass >= 0) {
+ if (!shouldHaveClass)
+ currentClassNames.splice(indexOfClass, 1);
+ } else {
+ if (shouldHaveClass)
+ currentClassNames.push(className);
+ }
+ });
+ node.className = currentClassNames.join(" ");
}
},
@@ -340,13 +344,40 @@ ko.utils = new (function () {
if ((value === null) || (value === undefined))
value = "";
- 'innerText' in element ? element.innerText = value
- : element.textContent = value;
+ // We need there to be exactly one child: a text node.
+ // If there are no children, more than one, or if it's not a text node,
+ // we'll clear everything and create a single text node.
+ var innerTextNode = ko.virtualElements.firstChild(element);
+ if (!innerTextNode || innerTextNode.nodeType != 3 || ko.virtualElements.nextSibling(innerTextNode)) {
+ ko.virtualElements.setDomNodeChildren(element, [document.createTextNode(value)]);
+ } else {
+ innerTextNode.data = value;
+ }
+
+ ko.utils.forceRefresh(element);
+ },
+
+ setElementName: function(element, name) {
+ element.name = name;
+
+ // Workaround IE 6/7 issue
+ // - https://github.com/SteveSanderson/knockout/issues/197
+ // - http://www.matts411.com/post/setting_the_name_attribute_in_ie_dom/
+ if (ieVersion <= 7) {
+ try {
+ element.mergeAttributes(document.createElement(""), false);
+ }
+ catch(e) {} // For IE9 with doc mode "IE9 Standards" and browser mode "IE9 Compatibility View"
+ }
+ },
+ forceRefresh: function(node) {
+ // Workaround for an IE9 rendering bug - https://github.com/SteveSanderson/knockout/issues/209
if (ieVersion >= 9) {
- // Believe it or not, this actually fixes an IE9 rendering bug
- // (See https://github.com/SteveSanderson/knockout/issues/209)
- element.style.display = element.style.display;
+ // For text nodes and comment nodes (most likely virtual elements), we will have to refresh the container
+ var elem = node.nodeType == 1 ? node : node.parentNode;
+ if (elem.style)
+ elem.style.zoom = elem.style.zoom;
}
},
@@ -687,6 +718,9 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD
ko.utils.setHtml = function(node, html) {
ko.utils.emptyDomNode(node);
+ // There's no legitimate reason to display a stringified observable without unwrapping it, so we'll unwrap it
+ html = ko.utils.unwrapObservable(html);
+
if ((html !== null) && (html !== undefined)) {
if (typeof html != 'string')
html = html.toString();
@@ -863,12 +897,14 @@ ko.subscribable['fn'] = {
"notifySubscribers": function (valueToNotify, event) {
event = event || defaultEvent;
if (this._subscriptions[event]) {
- ko.utils.arrayForEach(this._subscriptions[event].slice(0), function (subscription) {
- // In case a subscription was disposed during the arrayForEach cycle, check
- // for isDisposed on each subscription before invoking its callback
- if (subscription && (subscription.isDisposed !== true))
- subscription.callback(valueToNotify);
- });
+ ko.dependencyDetection.ignore(function() {
+ ko.utils.arrayForEach(this._subscriptions[event].slice(0), function (subscription) {
+ // In case a subscription was disposed during the arrayForEach cycle, check
+ // for isDisposed on each subscription before invoking its callback
+ if (subscription && (subscription.isDisposed !== true))
+ subscription.callback(valueToNotify);
+ });
+ }, this);
}
},
@@ -909,11 +945,20 @@ ko.dependencyDetection = (function () {
throw new Error("Only subscribable things can act as dependencies");
if (_frames.length > 0) {
var topFrame = _frames[_frames.length - 1];
- if (ko.utils.arrayIndexOf(topFrame.distinctDependencies, subscribable) >= 0)
+ if (!topFrame || ko.utils.arrayIndexOf(topFrame.distinctDependencies, subscribable) >= 0)
return;
topFrame.distinctDependencies.push(subscribable);
topFrame.callback(subscribable);
}
+ },
+
+ ignore: function(callback, callbackTarget) {
+ try {
+ _frames.push(null);
+ callback.call(callbackTarget);
+ } finally {
+ _frames.pop();
+ }
}
};
})();
@@ -943,10 +988,12 @@ ko.observable = function (initialValue) {
}
if (DEBUG) observable._latestValue = _latestValue;
ko.subscribable.call(observable);
+ observable.peek = function() { return _latestValue };
observable.valueHasMutated = function () { observable["notifySubscribers"](_latestValue); }
observable.valueWillMutate = function () { observable["notifySubscribers"](_latestValue, "beforeChange"); }
ko.utils.extend(observable, ko.observable['fn']);
+ ko.exportProperty(observable, 'peek', observable.peek);
ko.exportProperty(observable, "valueHasMutated", observable.valueHasMutated);
ko.exportProperty(observable, "valueWillMutate", observable.valueWillMutate);
@@ -1002,7 +1049,7 @@ ko.observableArray = function (initialValues) {
ko.observableArray['fn'] = {
'remove': function (valueOrPredicate) {
- var underlyingArray = this();
+ var underlyingArray = this.peek();
var removedValues = [];
var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) { return value === valueOrPredicate; };
for (var i = 0; i < underlyingArray.length; i++) {
@@ -1025,7 +1072,7 @@ ko.observableArray['fn'] = {
'removeAll': function (arrayOfValues) {
// If you passed zero args, we remove everything
if (arrayOfValues === undefined) {
- var underlyingArray = this();
+ var underlyingArray = this.peek();
var allValues = underlyingArray.slice(0);
this.valueWillMutate();
underlyingArray.splice(0, underlyingArray.length);
@@ -1041,7 +1088,7 @@ ko.observableArray['fn'] = {
},
'destroy': function (valueOrPredicate) {
- var underlyingArray = this();
+ var underlyingArray = this.peek();
var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) { return value === valueOrPredicate; };
this.valueWillMutate();
for (var i = underlyingArray.length - 1; i >= 0; i--) {
@@ -1074,16 +1121,20 @@ ko.observableArray['fn'] = {
var index = this['indexOf'](oldItem);
if (index >= 0) {
this.valueWillMutate();
- this()[index] = newItem;
+ this.peek()[index] = newItem;
this.valueHasMutated();
}
}
}
// Populate ko.observableArray.fn with read/write functions from native arrays
+// Important: Do not add any additional functions here that may reasonably be used to *read* data from the array
+// because we'll eval them without causing subscriptions, so ko.computed output could end up getting stale
ko.utils.arrayForEach(["pop", "push", "reverse", "shift", "sort", "splice", "unshift"], function (methodName) {
ko.observableArray['fn'][methodName] = function () {
- var underlyingArray = this();
+ // Use "peek" to avoid creating a subscription in any computed that we're executing in the context of
+ // (for consistency with mutating regular observables)
+ var underlyingArray = this.peek();
this.valueWillMutate();
var methodCallResult = underlyingArray[methodName].apply(underlyingArray, arguments);
this.valueHasMutated();
@@ -1214,26 +1265,25 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
function dependentObservable() {
if (arguments.length > 0) {
- set.apply(dependentObservable, arguments);
- } else {
- return get();
- }
- }
-
- function set() {
- if (typeof writeFunction === "function") {
- // Writing a value
- writeFunction.apply(evaluatorFunctionTarget, arguments);
+ if (typeof writeFunction === "function") {
+ // Writing a value
+ writeFunction.apply(evaluatorFunctionTarget, arguments);
+ } else {
+ throw new Error("Cannot write a value to a ko.computed unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters.");
+ }
+ return this; // Permits chained assignments
} else {
- throw new Error("Cannot write a value to a ko.computed unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters.");
+ // Reading the value
+ if (!_hasBeenEvaluated)
+ evaluateImmediate();
+ ko.dependencyDetection.registerDependency(dependentObservable);
+ return _latestValue;
}
}
- function get() {
- // Reading the value
+ dependentObservable.peek = function () {
if (!_hasBeenEvaluated)
evaluateImmediate();
- ko.dependencyDetection.registerDependency(dependentObservable);
return _latestValue;
}
@@ -1247,6 +1297,7 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
if (options['deferEvaluation'] !== true)
evaluateImmediate();
+ ko.exportProperty(dependentObservable, 'peek', dependentObservable.peek);
ko.exportProperty(dependentObservable, 'dispose', dependentObservable.dispose);
ko.exportProperty(dependentObservable, 'getDependenciesCount', dependentObservable.getDependenciesCount);
@@ -1369,7 +1420,9 @@ ko.exportSymbol('toJSON', ko.toJSON);
case 'option':
if (element[hasDomDataExpandoProperty] === true)
return ko.utils.domData.get(element, ko.bindingHandlers.options.optionValueDomDataKey);
- return element.getAttribute("value");
+ return ko.utils.ieVersion <= 7
+ ? (element.getAttributeNode('value').specified ? element.value : element.text)
+ : element.value;
case 'select':
return element.selectedIndex >= 0 ? ko.selectExtensions.readValue(element.options[element.selectedIndex]) : undefined;
default:
@@ -1419,12 +1472,14 @@ ko.exportSymbol('toJSON', ko.toJSON);
ko.exportSymbol('selectExtensions', ko.selectExtensions);
ko.exportSymbol('selectExtensions.readValue', ko.selectExtensions.readValue);
ko.exportSymbol('selectExtensions.writeValue', ko.selectExtensions.writeValue);
-
-ko.jsonExpressionRewriting = (function () {
+ko.expressionRewriting = (function () {
var restoreCapturedTokensRegex = /\@ko_token_(\d+)\@/g;
- var javaScriptAssignmentTarget = /^[\_$a-z][\_$a-z0-9]*(\[.*?\])*(\.[\_$a-z][\_$a-z0-9]*(\[.*?\])*)*$/i;
var javaScriptReservedWords = ["true", "false"];
+ // Matches something that can be assigned to--either an isolated identifier or something ending with a property accessor
+ // This is designed to be simple and avoid false negatives, but could produce false positives (e.g., a+b.c).
+ var javaScriptAssignmentTarget = /^(?:[$_a-z][$\w]*|(.+)(\.\s*[$_a-z][$\w]*|\[.+\]))$/i;
+
function restoreTokens(string, tokens) {
var prevValue = null;
while (string != prevValue) { // Keep restoring tokens until it no longer makes a difference (they may be nested)
@@ -1436,10 +1491,11 @@ ko.jsonExpressionRewriting = (function () {
return string;
}
- function isWriteableValue(expression) {
+ function getWriteableValue(expression) {
if (ko.utils.arrayIndexOf(javaScriptReservedWords, ko.utils.stringTrim(expression).toLowerCase()) >= 0)
return false;
- return expression.match(javaScriptAssignmentTarget) !== null;
+ var match = expression.match(javaScriptAssignmentTarget);
+ return match === null ? false : match[1] ? ('Object(' + match[1] + ')' + match[2]) : expression;
}
function ensureQuoted(key) {
@@ -1542,9 +1598,9 @@ ko.jsonExpressionRewriting = (function () {
return result;
},
- insertPropertyAccessorsIntoJson: function (objectLiteralStringOrKeyValueArray) {
+ preProcessBindings: function (objectLiteralStringOrKeyValueArray) {
var keyValueArray = typeof objectLiteralStringOrKeyValueArray === "string"
- ? ko.jsonExpressionRewriting.parseObjectLiteral(objectLiteralStringOrKeyValueArray)
+ ? ko.expressionRewriting.parseObjectLiteral(objectLiteralStringOrKeyValueArray)
: objectLiteralStringOrKeyValueArray;
var resultStrings = [], propertyAccessorResultStrings = [];
@@ -1559,7 +1615,7 @@ ko.jsonExpressionRewriting = (function () {
resultStrings.push(":");
resultStrings.push(val);
- if (isWriteableValue(ko.utils.stringTrim(val))) {
+ if (val = getWriteableValue(ko.utils.stringTrim(val))) {
if (propertyAccessorResultStrings.length > 0)
propertyAccessorResultStrings.push(", ");
propertyAccessorResultStrings.push(quotedKey + " : function(__ko_value) { " + val + " = __ko_value; }");
@@ -1606,11 +1662,15 @@ ko.jsonExpressionRewriting = (function () {
};
})();
-ko.exportSymbol('jsonExpressionRewriting', ko.jsonExpressionRewriting);
-ko.exportSymbol('jsonExpressionRewriting.bindingRewriteValidators', ko.jsonExpressionRewriting.bindingRewriteValidators);
-ko.exportSymbol('jsonExpressionRewriting.parseObjectLiteral', ko.jsonExpressionRewriting.parseObjectLiteral);
-ko.exportSymbol('jsonExpressionRewriting.insertPropertyAccessorsIntoJson', ko.jsonExpressionRewriting.insertPropertyAccessorsIntoJson);
-(function() {
+ko.exportSymbol('expressionRewriting', ko.expressionRewriting);
+ko.exportSymbol('expressionRewriting.bindingRewriteValidators', ko.expressionRewriting.bindingRewriteValidators);
+ko.exportSymbol('expressionRewriting.parseObjectLiteral', ko.expressionRewriting.parseObjectLiteral);
+ko.exportSymbol('expressionRewriting.preProcessBindings', ko.expressionRewriting.preProcessBindings);
+
+// For backward compatibility, define the following aliases. (Previously, these function names were misleading because
+// they referred to JSON specifically, even though they actually work with arbitrary JavaScript object literal expressions.)
+ko.exportSymbol('jsonExpressionRewriting', ko.expressionRewriting);
+ko.exportSymbol('jsonExpressionRewriting.insertPropertyAccessorsIntoJson', ko.expressionRewriting.preProcessBindings);(function() {
// "Virtual elements" is an abstraction on top of the usual DOM API which understands the notion that comment nodes
// may be used to represent hierarchy (in addition to the DOM's natural hierarchy).
// If you call the DOM-manipulating functions on ko.virtualElements, you will be able to read and write the state
@@ -1624,7 +1684,7 @@ ko.exportSymbol('jsonExpressionRewriting.insertPropertyAccessorsIntoJson', ko.js
// So, use node.text where available, and node.nodeValue elsewhere
var commentNodesHaveTextProperty = document.createComment("test").text === "";
- var startCommentRegex = commentNodesHaveTextProperty ? /^$/ : /^\s*ko\s+(.*\:.*)\s*$/;
+ var startCommentRegex = commentNodesHaveTextProperty ? /^$/ : /^\s*ko(?:\s+(.+\s*\:[\s\S]*))?\s*$/;
var endCommentRegex = commentNodesHaveTextProperty ? /^$/ : /^\s*\/ko\s*$/;
var htmlTagsWithOptionallyClosingChildren = { 'ul': true, 'ol': true };
@@ -1730,7 +1790,9 @@ ko.exportSymbol('jsonExpressionRewriting.insertPropertyAccessorsIntoJson', ko.js
},
insertAfter: function(containerNode, nodeToInsert, insertAfterNode) {
- if (!isStartComment(containerNode)) {
+ if (!insertAfterNode) {
+ ko.virtualElements.prepend(containerNode, nodeToInsert);
+ } else if (!isStartComment(containerNode)) {
// Insert after insertion point
if (insertAfterNode.nextSibling)
containerNode.insertBefore(nodeToInsert, insertAfterNode.nextSibling);
@@ -1855,7 +1917,7 @@ ko.exportSymbol('virtualElements.setDomNodeChildren', ko.virtualElements.setDomN
}
function createBindingsStringEvaluator(bindingsString, scopesCount) {
- var rewrittenBindings = " { " + ko.jsonExpressionRewriting.insertPropertyAccessorsIntoJson(bindingsString) + " } ";
+ var rewrittenBindings = " { " + ko.expressionRewriting.preProcessBindings(bindingsString) + " } ";
return ko.utils.buildEvalWithinScopeFunction(rewrittenBindings, scopesCount);
}
})();
@@ -1864,7 +1926,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
(function () {
ko.bindingHandlers = {};
- ko.bindingContext = function(dataItem, parentBindingContext) {
+ ko.bindingContext = function(dataItem, parentBindingContext, dataItemAlias) {
if (parentBindingContext) {
ko.utils.extend(this, parentBindingContext); // Inherit $root and any custom properties
this['$parentContext'] = parentBindingContext;
@@ -1874,11 +1936,17 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
} else {
this['$parents'] = [];
this['$root'] = dataItem;
+ // Export 'ko' in the binding context so it will be available in bindings and templates
+ // even if 'ko' isn't exported as a global, such as when using an AMD loader.
+ // See https://github.com/SteveSanderson/knockout/issues/490
+ this['ko'] = ko;
}
this['$data'] = dataItem;
+ if (dataItemAlias)
+ this[dataItemAlias] = dataItem;
}
- ko.bindingContext.prototype['createChildContext'] = function (dataItem) {
- return new ko.bindingContext(dataItem, this);
+ ko.bindingContext.prototype['createChildContext'] = function (dataItem, dataItemAlias) {
+ return new ko.bindingContext(dataItem, this, dataItemAlias);
};
ko.bindingContext.prototype['extend'] = function(properties) {
var clone = ko.utils.extend(new ko.bindingContext(), this);
@@ -2194,7 +2262,7 @@ ko.bindingHandlers['value'] = {
var valueUpdateHandler = function() {
var modelValue = valueAccessor();
var elementValue = ko.selectExtensions.readValue(element);
- ko.jsonExpressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'value', elementValue, /* checkIfDifferent: */ true);
+ ko.expressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'value', elementValue, /* checkIfDifferent: */ true);
}
// Workaround for https://github.com/SteveSanderson/knockout/issues/122
@@ -2278,7 +2346,9 @@ ko.bindingHandlers['options'] = {
}
if (value) {
- var allBindings = allBindingsAccessor();
+ var allBindings = allBindingsAccessor(),
+ includeDestroyed = allBindings['optionsIncludeDestroyed'];
+
if (typeof value.length != "number")
value = [value];
if (allBindings['optionsCaption']) {
@@ -2287,26 +2357,31 @@ ko.bindingHandlers['options'] = {
ko.selectExtensions.writeValue(option, undefined);
element.appendChild(option);
}
+
for (var i = 0, j = value.length; i < j; i++) {
+ // Skip destroyed items
+ var arrayEntry = value[i];
+ if (arrayEntry && arrayEntry['_destroy'] && !includeDestroyed)
+ continue;
+
var option = document.createElement("option");
+ function applyToObject(object, predicate, defaultValue) {
+ var predicateType = typeof predicate;
+ if (predicateType == "function") // Given a function; run it against the data value
+ return predicate(object);
+ else if (predicateType == "string") // Given a string; treat it as a property name on the data value
+ return object[predicate];
+ else // Given no optionsText arg; use the data value itself
+ return defaultValue;
+ }
+
// Apply a value to the option element
- var optionValue = typeof allBindings['optionsValue'] == "string" ? value[i][allBindings['optionsValue']] : value[i];
- optionValue = ko.utils.unwrapObservable(optionValue);
- ko.selectExtensions.writeValue(option, optionValue);
+ var optionValue = applyToObject(arrayEntry, allBindings['optionsValue'], arrayEntry);
+ ko.selectExtensions.writeValue(option, ko.utils.unwrapObservable(optionValue));
// Apply some text to the option element
- var optionsTextValue = allBindings['optionsText'];
- var optionText;
- if (typeof optionsTextValue == "function")
- optionText = optionsTextValue(value[i]); // Given a function; run it against the data value
- else if (typeof optionsTextValue == "string")
- optionText = value[i][optionsTextValue]; // Given a string; treat it as a property name on the data value
- else
- optionText = optionValue; // Given no optionsText arg; use the data value itself
- if ((optionText === null) || (optionText === undefined))
- optionText = "";
-
+ var optionText = applyToObject(arrayEntry, allBindings['optionsText'], optionValue);
ko.utils.setTextContent(option, optionText);
element.appendChild(option);
@@ -2340,25 +2415,14 @@ ko.bindingHandlers['options'] = {
ko.bindingHandlers['options'].optionValueDomDataKey = '__ko.optionValueDomData__';
ko.bindingHandlers['selectedOptions'] = {
- getSelectedValuesFromSelectNode: function (selectNode) {
- var result = [];
- var nodes = selectNode.childNodes;
- for (var i = 0, j = nodes.length; i < j; i++) {
- var node = nodes[i], tagName = ko.utils.tagNameLower(node);
- if (tagName == "option" && node.selected)
- result.push(ko.selectExtensions.readValue(node));
- else if (tagName == "optgroup") {
- var selectedValuesFromOptGroup = ko.bindingHandlers['selectedOptions'].getSelectedValuesFromSelectNode(node);
- Array.prototype.splice.apply(result, [result.length, 0].concat(selectedValuesFromOptGroup)); // Add new entries to existing 'result' instance
- }
- }
- return result;
- },
'init': function (element, valueAccessor, allBindingsAccessor) {
ko.utils.registerEventHandler(element, "change", function () {
- var value = valueAccessor();
- var valueToWrite = ko.bindingHandlers['selectedOptions'].getSelectedValuesFromSelectNode(this);
- ko.jsonExpressionRewriting.writeValueToProperty(value, allBindingsAccessor, 'value', valueToWrite);
+ 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, allBindingsAccessor, 'value', valueToWrite);
});
},
'update': function (element, valueAccessor) {
@@ -2367,12 +2431,10 @@ ko.bindingHandlers['selectedOptions'] = {
var newValue = ko.utils.unwrapObservable(valueAccessor());
if (newValue && typeof newValue.length == "number") {
- var nodes = element.childNodes;
- for (var i = 0, j = nodes.length; i < j; i++) {
- var node = nodes[i];
- if (ko.utils.tagNameLower(node) === "option")
- ko.utils.setOptionNodeSelectionState(node, ko.utils.arrayIndexOf(newValue, ko.selectExtensions.readValue(node)) >= 0);
- }
+ ko.utils.arrayForEach(element.getElementsByTagName("option"), function(node) {
+ var isSelected = ko.utils.arrayIndexOf(newValue, ko.selectExtensions.readValue(node)) >= 0;
+ ko.utils.setOptionNodeSelectionState(node, isSelected);
+ });
}
}
};
@@ -2382,6 +2444,7 @@ ko.bindingHandlers['text'] = {
ko.utils.setTextContent(element, valueAccessor());
}
};
+ko.virtualElements.allowedBindings['text'] = true;
ko.bindingHandlers['html'] = {
'init': function() {
@@ -2389,19 +2452,25 @@ ko.bindingHandlers['html'] = {
return { 'controlsDescendantBindings': true };
},
'update': function (element, valueAccessor) {
- var value = ko.utils.unwrapObservable(valueAccessor());
- ko.utils.setHtml(element, value);
+ // setHtml will unwrap the value if needed
+ ko.utils.setHtml(element, valueAccessor());
}
};
+var classesWrittenByBindingKey = '__ko__cssValue';
ko.bindingHandlers['css'] = {
'update': function (element, valueAccessor) {
- var value = ko.utils.unwrapObservable(valueAccessor() || {});
- for (var className in value) {
- if (typeof className == "string") {
+ var value = ko.utils.unwrapObservable(valueAccessor());
+ if (typeof value == "object") {
+ for (var className in value) {
var shouldHaveClass = ko.utils.unwrapObservable(value[className]);
ko.utils.toggleDomNodeCssClass(element, className, shouldHaveClass);
}
+ } else {
+ value = 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);
}
}
};
@@ -2421,13 +2490,8 @@ ko.bindingHandlers['style'] = {
ko.bindingHandlers['uniqueName'] = {
'init': function (element, valueAccessor) {
if (valueAccessor()) {
- element.name = "ko_unique_" + (++ko.bindingHandlers['uniqueName'].currentIndex);
-
- // Workaround IE 6/7 issue
- // - https://github.com/SteveSanderson/knockout/issues/197
- // - http://www.matts411.com/post/setting_the_name_attribute_in_ie_dom/
- if (ko.utils.isIe6 || ko.utils.isIe7)
- element.mergeAttributes(document.createElement(""), false);
+ var name = "ko_unique_" + (++ko.bindingHandlers['uniqueName'].currentIndex);
+ ko.utils.setElementName(element, name);
}
}
};
@@ -2455,7 +2519,7 @@ ko.bindingHandlers['checked'] = {
else if ((!element.checked) && (existingEntryIndex >= 0))
modelValue.splice(existingEntryIndex, 1);
} else {
- ko.jsonExpressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'checked', valueToWrite, true);
+ ko.expressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'checked', valueToWrite, true);
}
};
ko.utils.registerEventHandler(element, "click", updateHandler);
@@ -2509,26 +2573,52 @@ ko.bindingHandlers['attr'] = {
} else if (!toRemove) {
element.setAttribute(attrName, attrValue.toString());
}
+
+ // Treat "name" specially - although you can think of it as an attribute, it also needs
+ // special handling on older versions of IE (https://github.com/SteveSanderson/knockout/pull/333)
+ // 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());
+ }
}
}
}
};
+var hasfocusUpdatingProperty = '__ko_hasfocusUpdating';
ko.bindingHandlers['hasfocus'] = {
'init': function(element, valueAccessor, allBindingsAccessor) {
- var writeValue = function(valueToWrite) {
+ var handleElementFocusChange = function(isFocused) {
+ // Where possible, ignore which event was raised and determine focus state using activeElement,
+ // as this avoids phantom focus/blur events raised when changing tabs in modern browsers.
+ // However, not all KO-targeted browsers (Firefox 2) support activeElement. For those browsers,
+ // prevent a loss of focus when changing tabs/windows by setting a flag that prevents hasfocus
+ // from calling 'blur()' on the element when it loses focus.
+ // Discussion at https://github.com/SteveSanderson/knockout/pull/352
+ element[hasfocusUpdatingProperty] = true;
+ var ownerDoc = element.ownerDocument;
+ if ("activeElement" in ownerDoc) {
+ isFocused = (ownerDoc.activeElement === element);
+ }
var modelValue = valueAccessor();
- ko.jsonExpressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'hasfocus', valueToWrite, true);
+ ko.expressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'hasfocus', isFocused, true);
+ element[hasfocusUpdatingProperty] = false;
};
- ko.utils.registerEventHandler(element, "focus", function() { writeValue(true) });
- ko.utils.registerEventHandler(element, "focusin", function() { writeValue(true) }); // For IE
- ko.utils.registerEventHandler(element, "blur", function() { writeValue(false) });
- ko.utils.registerEventHandler(element, "focusout", function() { writeValue(false) }); // For IE
+ var handleElementFocusIn = handleElementFocusChange.bind(null, true);
+ var handleElementFocusOut = handleElementFocusChange.bind(null, false);
+
+ ko.utils.registerEventHandler(element, "focus", handleElementFocusIn);
+ ko.utils.registerEventHandler(element, "focusin", handleElementFocusIn); // For IE
+ ko.utils.registerEventHandler(element, "blur", handleElementFocusOut);
+ ko.utils.registerEventHandler(element, "focusout", handleElementFocusOut); // For IE
},
'update': function(element, valueAccessor) {
- var value = ko.utils.unwrapObservable(valueAccessor());
- value ? element.focus() : element.blur();
- ko.utils.triggerEvent(element, value ? "focusin" : "focusout"); // For IE, which doesn't reliably fire "focus" or "blur" events synchronously
+ if (!element[hasfocusUpdatingProperty]) {
+ var value = ko.utils.unwrapObservable(valueAccessor());
+ value ? element.focus() : element.blur();
+ ko.utils.triggerEvent(element, value ? "focusin" : "focusout"); // For IE, which doesn't reliably fire "focus" or "blur" events synchronously
+ }
}
};
@@ -2544,7 +2634,7 @@ ko.bindingHandlers['with'] = {
return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['with'].makeTemplateValueAccessor(valueAccessor), allBindingsAccessor, viewModel, bindingContext);
}
};
-ko.jsonExpressionRewriting.bindingRewriteValidators['with'] = false; // Can't rewrite control flow bindings
+ko.expressionRewriting.bindingRewriteValidators['with'] = false; // Can't rewrite control flow bindings
ko.virtualElements.allowedBindings['with'] = true;
// "if: someExpression" is equivalent to "template: { if: someExpression }"
@@ -2559,7 +2649,7 @@ ko.bindingHandlers['if'] = {
return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['if'].makeTemplateValueAccessor(valueAccessor), allBindingsAccessor, viewModel, bindingContext);
}
};
-ko.jsonExpressionRewriting.bindingRewriteValidators['if'] = false; // Can't rewrite control flow bindings
+ko.expressionRewriting.bindingRewriteValidators['if'] = false; // Can't rewrite control flow bindings
ko.virtualElements.allowedBindings['if'] = true;
// "ifnot: someExpression" is equivalent to "template: { ifnot: someExpression }"
@@ -2574,7 +2664,7 @@ ko.bindingHandlers['ifnot'] = {
return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['ifnot'].makeTemplateValueAccessor(valueAccessor), allBindingsAccessor, viewModel, bindingContext);
}
};
-ko.jsonExpressionRewriting.bindingRewriteValidators['ifnot'] = false; // Can't rewrite control flow bindings
+ko.expressionRewriting.bindingRewriteValidators['ifnot'] = false; // Can't rewrite control flow bindings
ko.virtualElements.allowedBindings['ifnot'] = true;
// "foreach: someExpression" is equivalent to "template: { foreach: someExpression }"
@@ -2591,6 +2681,7 @@ ko.bindingHandlers['foreach'] = {
// If bindingValue.data is the array, preserve all relevant options
return {
'foreach': bindingValue['data'],
+ 'as': bindingValue['as'],
'includeDestroyed': bindingValue['includeDestroyed'],
'afterAdd': bindingValue['afterAdd'],
'beforeRemove': bindingValue['beforeRemove'],
@@ -2606,9 +2697,8 @@ ko.bindingHandlers['foreach'] = {
return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['foreach'].makeTemplateValueAccessor(valueAccessor), allBindingsAccessor, viewModel, bindingContext);
}
};
-ko.jsonExpressionRewriting.bindingRewriteValidators['foreach'] = false; // Can't rewrite control flow bindings
-ko.virtualElements.allowedBindings['foreach'] = true;
-// If you want to make a custom template engine,
+ko.expressionRewriting.bindingRewriteValidators['foreach'] = false; // Can't rewrite control flow bindings
+ko.virtualElements.allowedBindings['foreach'] = true;// If you want to make a custom template engine,
//
// [1] Inherit from this class (like ko.nativeTemplateEngine does)
// [2] Override 'renderTemplateSource', supplying a function with this signature:
@@ -2668,12 +2758,6 @@ ko.templateEngine.prototype['isTemplateRewritten'] = function (template, templat
// Skip rewriting if requested
if (this['allowTemplateRewriting'] === false)
return true;
-
- // Perf optimisation - see below
- var templateIsInExternalDocument = templateDocument && templateDocument != document;
- if (!templateIsInExternalDocument && this.knownRewrittenTemplates && this.knownRewrittenTemplates[template])
- return true;
-
return this['makeTemplateSource'](template, templateDocument)['data']("isRewritten");
};
@@ -2682,19 +2766,6 @@ ko.templateEngine.prototype['rewriteTemplate'] = function (template, rewriterCal
var rewritten = rewriterCallback(templateSource['text']());
templateSource['text'](rewritten);
templateSource['data']("isRewritten", true);
-
- // Perf optimisation - for named templates, track which ones have been rewritten so we can
- // answer 'isTemplateRewritten' *without* having to use getElementById (which is slow on IE < 8)
- //
- // Note that we only cache the status for templates in the main document, because caching on a per-doc
- // basis complicates the implementation excessively. In a future version of KO, we will likely remove
- // this 'isRewritten' cache entirely anyway, because the benefit is extremely minor and only applies
- // to rewritable templates, which are pretty much deprecated since KO 2.0.
- var templateIsInExternalDocument = templateDocument && templateDocument != document;
- if (!templateIsInExternalDocument && typeof template == "string") {
- this.knownRewrittenTemplates = this.knownRewrittenTemplates || {};
- this.knownRewrittenTemplates[template] = true;
- }
};
ko.exportSymbol('templateEngine', ko.templateEngine);
@@ -2704,7 +2775,7 @@ ko.templateRewriting = (function () {
var memoizeVirtualContainerBindingSyntaxRegex = //g;
function validateDataBindValuesForRewriting(keyValueArray) {
- var allValidators = ko.jsonExpressionRewriting.bindingRewriteValidators;
+ var allValidators = ko.expressionRewriting.bindingRewriteValidators;
for (var i = 0; i < keyValueArray.length; i++) {
var key = keyValueArray[i]['key'];
if (allValidators.hasOwnProperty(key)) {
@@ -2722,16 +2793,15 @@ ko.templateRewriting = (function () {
}
function constructMemoizedTagReplacement(dataBindAttributeValue, tagToRetain, templateEngine) {
- var dataBindKeyValueArray = ko.jsonExpressionRewriting.parseObjectLiteral(dataBindAttributeValue);
+ var dataBindKeyValueArray = ko.expressionRewriting.parseObjectLiteral(dataBindAttributeValue);
validateDataBindValuesForRewriting(dataBindKeyValueArray);
- var rewrittenDataBindAttributeValue = ko.jsonExpressionRewriting.insertPropertyAccessorsIntoJson(dataBindKeyValueArray);
+ var rewrittenDataBindAttributeValue = ko.expressionRewriting.preProcessBindings(dataBindKeyValueArray);
// For no obvious reason, Opera fails to evaluate rewrittenDataBindAttributeValue unless it's wrapped in an additional
// anonymous function, even though Opera's built-in debugger can evaluate it anyway. No other browser requires this
// extra indirection.
- var applyBindingsToNextSiblingScript = "ko.templateRewriting.applyMemoizedBindingsToNextSibling(function() { \
- return (function() { return { " + rewrittenDataBindAttributeValue + " } })() \
- })";
+ var applyBindingsToNextSiblingScript =
+ "ko.__tr_ambtns(function(){return(function(){return{" + rewrittenDataBindAttributeValue + "} })()})";
return templateEngine['createJavaScriptEvaluatorBlock'](applyBindingsToNextSiblingScript) + tagToRetain;
}
@@ -2760,8 +2830,9 @@ ko.templateRewriting = (function () {
}
})();
-ko.exportSymbol('templateRewriting', ko.templateRewriting);
-ko.exportSymbol('templateRewriting.applyMemoizedBindingsToNextSibling', ko.templateRewriting.applyMemoizedBindingsToNextSibling); // Exported only because it has to be referenced by string lookup from within rewritten template
+
+// Exported only because it has to be referenced by string lookup from within rewritten template
+ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextSibling);
(function() {
// A template source represents a read/write way of accessing a template. This is to eliminate the need for template loading/saving
// logic to be duplicated in every template engine (and means they can all work with anonymous templates, etc.)
@@ -2955,7 +3026,7 @@ ko.exportSymbol('templateRewriting.applyMemoizedBindingsToNextSibling', ko.templ
: new ko.bindingContext(ko.utils.unwrapObservable(dataOrBindingContext));
// Support selecting template as a function of the data being rendered
- var templateName = typeof(template) == 'function' ? template(bindingContext['$data']) : template;
+ var templateName = typeof(template) == 'function' ? template(bindingContext['$data'], bindingContext) : template;
var renderedNodesArray = executeTemplate(targetNodeOrNodeArray, renderMode, templateName, bindingContext, options);
if (renderMode == "replaceNode") {
@@ -2982,9 +3053,9 @@ ko.exportSymbol('templateRewriting.applyMemoizedBindingsToNextSibling', ko.templ
// This will be called by setDomNodeChildrenFromArrayMapping to get the nodes to add to targetNode
var executeTemplateForArrayItem = function (arrayValue, index) {
// Support selecting template as a function of the data being rendered
- var templateName = typeof(template) == 'function' ? template(arrayValue) : template;
- arrayItemContext = parentBindingContext['createChildContext'](ko.utils.unwrapObservable(arrayValue));
+ arrayItemContext = parentBindingContext['createChildContext'](ko.utils.unwrapObservable(arrayValue), options['as']);
arrayItemContext['$index'] = index;
+ var templateName = typeof(template) == 'function' ? template(arrayValue, arrayItemContext) : template;
return executeTemplate(null, "ignoreTargetNode", templateName, arrayItemContext, options);
}
@@ -3057,7 +3128,7 @@ ko.exportSymbol('templateRewriting.applyMemoizedBindingsToNextSibling', ko.templ
if (shouldDisplay) {
// Render once for this single data point (or use the viewModel if no data was provided)
var innerBindingContext = (typeof bindingValue == 'object') && ('data' in bindingValue)
- ? bindingContext['createChildContext'](ko.utils.unwrapObservable(bindingValue['data'])) // Given an explitit 'data' value, we create a child binding context for it
+ ? bindingContext['createChildContext'](ko.utils.unwrapObservable(bindingValue['data']), bindingValue['as']) // Given an explitit 'data' value, we create a child binding context for it
: bindingContext; // Given no explicit 'data' value, we retain the same binding context
templateSubscription = ko.renderTemplate(templateName || element, innerBindingContext, /* options: */ bindingValue, element);
} else
@@ -3070,13 +3141,13 @@ ko.exportSymbol('templateRewriting.applyMemoizedBindingsToNextSibling', ko.templ
};
// Anonymous templates can't be rewritten. Give a nice error message if you try to do it.
- ko.jsonExpressionRewriting.bindingRewriteValidators['template'] = function(bindingValue) {
- var parsedBindingValue = ko.jsonExpressionRewriting.parseObjectLiteral(bindingValue);
+ ko.expressionRewriting.bindingRewriteValidators['template'] = function(bindingValue) {
+ var parsedBindingValue = ko.expressionRewriting.parseObjectLiteral(bindingValue);
if ((parsedBindingValue.length == 1) && parsedBindingValue[0]['unknown'])
return null; // It looks like a string literal, not an object literal, so treat it as a named template (which is allowed for rewriting)
- if (ko.jsonExpressionRewriting.keyValueArrayContainsKey(parsedBindingValue, "name"))
+ if (ko.expressionRewriting.keyValueArrayContainsKey(parsedBindingValue, "name"))
return null; // Named templates can be rewritten, so return "no error"
return "This template engine does not support anonymous templates nested within its templates";
};
@@ -3087,85 +3158,95 @@ ko.exportSymbol('templateRewriting.applyMemoizedBindingsToNextSibling', ko.templ
ko.exportSymbol('setTemplateEngine', ko.setTemplateEngine);
ko.exportSymbol('renderTemplate', ko.renderTemplate);
-(function () {
+ko.utils.compareArrays = (function () {
+ var statusNotInOld = 'added', statusNotInNew = 'deleted';
+
// Simple calculation based on Levenshtein distance.
- function calculateEditDistanceMatrix(oldArray, newArray, maxAllowedDistance) {
- var distances = [];
- for (var i = 0; i <= newArray.length; i++)
- distances[i] = [];
-
- // Top row - transform old array into empty array via deletions
- for (var i = 0, j = Math.min(oldArray.length, maxAllowedDistance); i <= j; i++)
- distances[0][i] = i;
-
- // Left row - transform empty array into new array via additions
- for (var i = 1, j = Math.min(newArray.length, maxAllowedDistance); i <= j; i++) {
- distances[i][0] = i;
- }
-
- // Fill out the body of the array
- var oldIndex, oldIndexMax = oldArray.length, newIndex, newIndexMax = newArray.length;
- var distanceViaAddition, distanceViaDeletion;
- for (oldIndex = 1; oldIndex <= oldIndexMax; oldIndex++) {
- var newIndexMinForRow = Math.max(1, oldIndex - maxAllowedDistance);
- var newIndexMaxForRow = Math.min(newIndexMax, oldIndex + maxAllowedDistance);
- for (newIndex = newIndexMinForRow; newIndex <= newIndexMaxForRow; newIndex++) {
- if (oldArray[oldIndex - 1] === newArray[newIndex - 1])
- distances[newIndex][oldIndex] = distances[newIndex - 1][oldIndex - 1];
+ function compareArrays(oldArray, newArray, dontLimitMoves) {
+ oldArray = oldArray || [];
+ newArray = newArray || [];
+
+ if (oldArray.length <= newArray.length)
+ return compareSmallArrayToBigArray(oldArray, newArray, statusNotInOld, statusNotInNew, dontLimitMoves);
+ else
+ return compareSmallArrayToBigArray(newArray, oldArray, statusNotInNew, statusNotInOld, dontLimitMoves);
+ }
+
+ function compareSmallArrayToBigArray(smlArray, bigArray, statusNotInSml, statusNotInBig, dontLimitMoves) {
+ var myMin = Math.min,
+ myMax = Math.max,
+ editDistanceMatrix = [],
+ smlIndex, smlIndexMax = smlArray.length,
+ bigIndex, bigIndexMax = bigArray.length,
+ compareRange = (bigIndexMax - smlIndexMax) || 1,
+ maxDistance = smlIndexMax + bigIndexMax + 1,
+ thisRow, lastRow,
+ bigIndexMaxForRow, bigIndexMinForRow;
+
+ for (smlIndex = 0; smlIndex <= smlIndexMax; smlIndex++) {
+ lastRow = thisRow;
+ editDistanceMatrix.push(thisRow = []);
+ bigIndexMaxForRow = myMin(bigIndexMax, smlIndex + compareRange);
+ bigIndexMinForRow = myMax(0, smlIndex - 1);
+ for (bigIndex = bigIndexMinForRow; bigIndex <= bigIndexMaxForRow; bigIndex++) {
+ if (!bigIndex)
+ thisRow[bigIndex] = smlIndex + 1;
+ else if (!smlIndex) // Top row - transform empty array into new array via additions
+ thisRow[bigIndex] = bigIndex + 1;
+ else if (smlArray[smlIndex - 1] === bigArray[bigIndex - 1])
+ thisRow[bigIndex] = lastRow[bigIndex - 1]; // copy value (no edit)
else {
- var northDistance = distances[newIndex - 1][oldIndex] === undefined ? Number.MAX_VALUE : distances[newIndex - 1][oldIndex] + 1;
- var westDistance = distances[newIndex][oldIndex - 1] === undefined ? Number.MAX_VALUE : distances[newIndex][oldIndex - 1] + 1;
- distances[newIndex][oldIndex] = Math.min(northDistance, westDistance);
+ var northDistance = lastRow[bigIndex] || maxDistance; // not in big (deletion)
+ var westDistance = thisRow[bigIndex - 1] || maxDistance; // not in small (addition)
+ thisRow[bigIndex] = myMin(northDistance, westDistance) + 1;
}
}
}
- return distances;
- }
-
- function findEditScriptFromEditDistanceMatrix(editDistanceMatrix, oldArray, newArray) {
- var oldIndex = oldArray.length;
- var newIndex = newArray.length;
- var editScript = [];
- var maxDistance = editDistanceMatrix[newIndex][oldIndex];
- if (maxDistance === undefined)
- return null; // maxAllowedDistance must be too small
- while ((oldIndex > 0) || (newIndex > 0)) {
- var me = editDistanceMatrix[newIndex][oldIndex];
- var distanceViaAdd = (newIndex > 0) ? editDistanceMatrix[newIndex - 1][oldIndex] : maxDistance + 1;
- var distanceViaDelete = (oldIndex > 0) ? editDistanceMatrix[newIndex][oldIndex - 1] : maxDistance + 1;
- var distanceViaRetain = (newIndex > 0) && (oldIndex > 0) ? editDistanceMatrix[newIndex - 1][oldIndex - 1] : maxDistance + 1;
- if ((distanceViaAdd === undefined) || (distanceViaAdd < me - 1)) distanceViaAdd = maxDistance + 1;
- if ((distanceViaDelete === undefined) || (distanceViaDelete < me - 1)) distanceViaDelete = maxDistance + 1;
- if (distanceViaRetain < me - 1) distanceViaRetain = maxDistance + 1;
-
- if ((distanceViaAdd <= distanceViaDelete) && (distanceViaAdd < distanceViaRetain)) {
- editScript.push({ status: "added", value: newArray[newIndex - 1] });
- newIndex--;
- } else if ((distanceViaDelete < distanceViaAdd) && (distanceViaDelete < distanceViaRetain)) {
- editScript.push({ status: "deleted", value: oldArray[oldIndex - 1] });
- oldIndex--;
+ var editScript = [], meMinusOne, notInSml = [], notInBig = [];
+ for (smlIndex = smlIndexMax, bigIndex = bigIndexMax; smlIndex || bigIndex;) {
+ meMinusOne = editDistanceMatrix[smlIndex][bigIndex] - 1;
+ if (bigIndex && meMinusOne === editDistanceMatrix[smlIndex][bigIndex-1]) {
+ notInSml.push(editScript[editScript.length] = { // added
+ 'status': statusNotInSml,
+ 'value': bigArray[--bigIndex],
+ 'index': bigIndex });
+ } else if (smlIndex && meMinusOne === editDistanceMatrix[smlIndex - 1][bigIndex]) {
+ notInBig.push(editScript[editScript.length] = { // deleted
+ 'status': statusNotInBig,
+ 'value': smlArray[--smlIndex],
+ 'index': smlIndex });
} else {
- editScript.push({ status: "retained", value: oldArray[oldIndex - 1] });
- newIndex--;
- oldIndex--;
+ editScript.push({
+ 'status': "retained",
+ 'value': bigArray[--bigIndex] });
+ --smlIndex;
+ }
+ }
+
+ if (notInSml.length && notInBig.length) {
+ // Set a limit on the number of consecutive non-matching comparisons; having it a multiple of
+ // smlIndexMax keeps the time complexity of this algorithm linear.
+ var limitFailedCompares = smlIndexMax * 10, failedCompares,
+ a, d, notInSmlItem, notInBigItem;
+ // Go through the items that have been added and deleted and try to find matches between them.
+ for (failedCompares = a = 0; (dontLimitMoves || failedCompares < limitFailedCompares) && (notInSmlItem = notInSml[a]); a++) {
+ for (d = 0; notInBigItem = notInBig[d]; d++) {
+ if (notInSmlItem['value'] === notInBigItem['value']) {
+ notInSmlItem['moved'] = notInBigItem['index'];
+ notInBigItem['moved'] = notInSmlItem['index'];
+ notInBig.splice(d,1); // This item is marked as moved; so remove it from notInBig list
+ failedCompares = d = 0; // Reset failed compares count because we're checking for consecutive failures
+ break;
+ }
+ }
+ failedCompares += d;
}
}
return editScript.reverse();
}
- ko.utils.compareArrays = function (oldArray, newArray, maxEditsToConsider) {
- if (maxEditsToConsider === undefined) {
- return ko.utils.compareArrays(oldArray, newArray, 1) // First consider likely case where there is at most one edit (very fast)
- || ko.utils.compareArrays(oldArray, newArray, 10) // If that fails, account for a fair number of changes while still being fast
- || ko.utils.compareArrays(oldArray, newArray, Number.MAX_VALUE); // Ultimately give the right answer, even though it may take a long time
- } else {
- oldArray = oldArray || [];
- newArray = newArray || [];
- var editDistanceMatrix = calculateEditDistanceMatrix(oldArray, newArray, maxEditsToConsider);
- return findEditScriptFromEditDistanceMatrix(editDistanceMatrix, oldArray, newArray);
- }
- };
+ return compareArrays;
})();
ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);
@@ -3181,13 +3262,26 @@ ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);
// "callbackAfterAddingNodes" will be invoked after any "mapping"-generated nodes are inserted into the container node
// You can use this, for example, to activate bindings on those nodes.
- function fixUpVirtualElements(contiguousNodeArray) {
- // Ensures that contiguousNodeArray really *is* an array of contiguous siblings, even if some of the interior
- // ones have changed since your array was first built (e.g., because your array contains virtual elements, and
- // their virtual children changed when binding was applied to them).
- // This is needed so that we can reliably remove or update the nodes corresponding to a given array item
-
- if (contiguousNodeArray.length > 2) {
+ function fixUpNodesToBeMovedOrRemoved(contiguousNodeArray) {
+ // Before moving, deleting, or replacing a set of nodes that were previously outputted by the "map" function, we have to reconcile
+ // them against what is in the DOM right now. It may be that some of the nodes have already been removed from the document,
+ // or that new nodes might have been inserted in the middle, for example by a binding. Also, there may previously have been
+ // leading comment nodes (created by rewritten string-based templates) that have since been removed during binding.
+ // So, this function translates the old "map" output array into its best guess of what set of current DOM nodes should be removed.
+ //
+ // Rules:
+ // [A] Any leading nodes that aren't in the document any more should be ignored
+ // These most likely correspond to memoization nodes that were already removed during binding
+ // See https://github.com/SteveSanderson/knockout/pull/440
+ // [B] We want to output a contiguous series of nodes that are still in the document. So, ignore any nodes that
+ // have already been removed, and include any nodes that have been inserted among the previous collection
+
+ // Rule [A]
+ while (contiguousNodeArray.length && !ko.utils.domNodeIsAttachedToDocument(contiguousNodeArray[0]))
+ contiguousNodeArray.splice(0, 1);
+
+ // Rule [B]
+ if (contiguousNodeArray.length > 1) {
// Build up the actual new contiguous node set
var current = contiguousNodeArray[0], last = contiguousNodeArray[contiguousNodeArray.length - 1], newContiguousSet = [current];
while (current !== last) {
@@ -3201,6 +3295,7 @@ ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);
// (The following line replaces the contents of contiguousNodeArray with newContiguousSet)
Array.prototype.splice.apply(contiguousNodeArray, [0, contiguousNodeArray.length].concat(newContiguousSet));
}
+ return contiguousNodeArray;
}
function mapNodeAndRefreshWhenChanged(containerNode, mapping, valueToMap, callbackAfterAddingNodes, index) {
@@ -3211,10 +3306,9 @@ ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);
// On subsequent evaluations, just replace the previously-inserted DOM nodes
if (mappedNodes.length > 0) {
- fixUpVirtualElements(mappedNodes);
- ko.utils.replaceDomNodes(mappedNodes, newMappedNodes);
+ ko.utils.replaceDomNodes(fixUpNodesToBeMovedOrRemoved(mappedNodes), newMappedNodes);
if (callbackAfterAddingNodes)
- callbackAfterAddingNodes(valueToMap, newMappedNodes);
+ callbackAfterAddingNodes(valueToMap, newMappedNodes, index);
}
// Replace the contents of the mappedNodes array, thereby updating the record
@@ -3239,96 +3333,106 @@ ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);
// Build the new mapping result
var newMappingResult = [];
var lastMappingResultIndex = 0;
- var nodesToDelete = [];
var newMappingResultIndex = 0;
- var nodesAdded = [];
- var insertAfterNode = null;
- for (var i = 0, j = editScript.length; i < j; i++) {
- switch (editScript[i].status) {
- case "retained":
- // Just keep the information - don't touch the nodes
- var dataToRetain = lastMappingResult[lastMappingResultIndex];
- dataToRetain.indexObservable(newMappingResultIndex);
- newMappingResultIndex = newMappingResult.push(dataToRetain);
- if (dataToRetain.domNodes.length > 0)
- insertAfterNode = dataToRetain.domNodes[dataToRetain.domNodes.length - 1];
- lastMappingResultIndex++;
- break;
- case "deleted":
- // Stop tracking changes to the mapping for these nodes
- lastMappingResult[lastMappingResultIndex].dependentObservable.dispose();
-
- // Queue these nodes for later removal
- fixUpVirtualElements(lastMappingResult[lastMappingResultIndex].domNodes);
- ko.utils.arrayForEach(lastMappingResult[lastMappingResultIndex].domNodes, function (node) {
- nodesToDelete.push({
- element: node,
- index: i,
- value: editScript[i].value
+ var nodesToDelete = [];
+ var itemsToProcess = [];
+ var itemsForBeforeRemoveCallbacks = [];
+ var itemsForMoveCallbacks = [];
+ var itemsForAfterAddCallbacks = [];
+ var mapData;
+
+ function itemMovedOrRetained(editScriptIndex, oldPosition) {
+ mapData = lastMappingResult[oldPosition];
+ if (newMappingResultIndex !== oldPosition)
+ itemsForMoveCallbacks[editScriptIndex] = mapData;
+ // Since updating the index might change the nodes, do so before calling fixUpNodesToBeMovedOrRemoved
+ mapData.indexObservable(newMappingResultIndex++);
+ fixUpNodesToBeMovedOrRemoved(mapData.mappedNodes);
+ newMappingResult.push(mapData);
+ itemsToProcess.push(mapData);
+ }
+
+ function callCallback(callback, items) {
+ if (callback) {
+ for (var i = 0, n = items.length; i < n; i++) {
+ if (items[i]) {
+ ko.utils.arrayForEach(items[i].mappedNodes, function(node) {
+ callback(node, i, items[i].arrayEntry);
});
- insertAfterNode = node;
- });
+ }
+ }
+ }
+ }
+
+ for (var i = 0, editScriptItem, movedIndex; editScriptItem = editScript[i]; i++) {
+ movedIndex = editScriptItem['moved'];
+ switch (editScriptItem['status']) {
+ case "deleted":
+ if (movedIndex === undefined) {
+ mapData = lastMappingResult[lastMappingResultIndex];
+
+ // Stop tracking changes to the mapping for these nodes
+ mapData.dependentObservable.dispose();
+
+ // Queue these nodes for later removal
+ nodesToDelete.push.apply(nodesToDelete, fixUpNodesToBeMovedOrRemoved(mapData.mappedNodes));
+ if (options['beforeRemove']) {
+ itemsForBeforeRemoveCallbacks[i] = mapData;
+ itemsToProcess.push(mapData);
+ }
+ }
lastMappingResultIndex++;
break;
+ case "retained":
+ itemMovedOrRetained(i, lastMappingResultIndex++);
+ break;
+
case "added":
- var valueToMap = editScript[i].value;
- var indexObservable = ko.observable(newMappingResultIndex);
- var mapData = mapNodeAndRefreshWhenChanged(domNode, mapping, valueToMap, callbackAfterAddingNodes, indexObservable);
- var mappedNodes = mapData.mappedNodes;
-
- // On the first evaluation, insert the nodes at the current insertion point
- newMappingResultIndex = newMappingResult.push({
- arrayEntry: editScript[i].value,
- domNodes: mappedNodes,
- dependentObservable: mapData.dependentObservable,
- indexObservable: indexObservable
- });
- for (var nodeIndex = 0, nodeIndexMax = mappedNodes.length; nodeIndex < nodeIndexMax; nodeIndex++) {
- var node = mappedNodes[nodeIndex];
- nodesAdded.push({
- element: node,
- index: i,
- value: editScript[i].value
- });
- if (insertAfterNode == null) {
- // Insert "node" (the newly-created node) as domNode's first child
- ko.virtualElements.prepend(domNode, node);
- } else {
- // Insert "node" into "domNode" immediately after "insertAfterNode"
- ko.virtualElements.insertAfter(domNode, node, insertAfterNode);
- }
- insertAfterNode = node;
+ if (movedIndex !== undefined) {
+ itemMovedOrRetained(i, movedIndex);
+ } else {
+ mapData = { arrayEntry: editScriptItem['value'], indexObservable: ko.observable(newMappingResultIndex++) };
+ newMappingResult.push(mapData);
+ itemsToProcess.push(mapData);
+ if (!isFirstExecution)
+ itemsForAfterAddCallbacks[i] = mapData;
}
- if (callbackAfterAddingNodes)
- callbackAfterAddingNodes(valueToMap, mappedNodes, indexObservable);
break;
}
}
- ko.utils.arrayForEach(nodesToDelete, function (node) { ko.cleanNode(node.element) });
+ // Call beforeMove first before any changes have been made to the DOM
+ callCallback(options['beforeMove'], itemsForMoveCallbacks);
- var invokedBeforeRemoveCallback = false;
- if (!isFirstExecution) {
- if (options['afterAdd']) {
- for (var i = 0; i < nodesAdded.length; i++)
- options['afterAdd'](nodesAdded[i].element, nodesAdded[i].index, nodesAdded[i].value);
- }
- if (options['beforeRemove']) {
- for (var i = 0; i < nodesToDelete.length; i++)
- options['beforeRemove'](nodesToDelete[i].element, nodesToDelete[i].index, nodesToDelete[i].value);
- invokedBeforeRemoveCallback = true;
+ // Next remove nodes for deleted items; or call beforeRemove, which will remove them
+ ko.utils.arrayForEach(nodesToDelete, options['beforeRemove'] ? ko.cleanNode : ko.removeNode);
+ callCallback(options['beforeRemove'], itemsForBeforeRemoveCallbacks);
+
+ // Next add/reorder the remaining items (will include deleted items if there's a beforeRemove callback)
+ for (var i = 0, nextNode = ko.virtualElements.firstChild(domNode), lastNode, node; mapData = itemsToProcess[i]; i++) {
+ // Get nodes for newly added items
+ if (!mapData.mappedNodes)
+ ko.utils.extend(mapData, mapNodeAndRefreshWhenChanged(domNode, mapping, mapData.arrayEntry, callbackAfterAddingNodes, mapData.indexObservable));
+
+ // Put nodes in the right place if they aren't there already
+ for (var j = 0; node = mapData.mappedNodes[j]; nextNode = node.nextSibling, lastNode = node, j++) {
+ if (node !== nextNode)
+ ko.virtualElements.insertAfter(domNode, node, lastNode);
}
- }
- if (!invokedBeforeRemoveCallback && nodesToDelete.length) {
- for (var i = 0; i < nodesToDelete.length; i++) {
- var element = nodesToDelete[i].element;
- if (element.parentNode)
- element.parentNode.removeChild(element);
+
+ // Run the callbacks for newly added nodes (for example, to apply bindings, etc.)
+ if (!mapData.initialized && callbackAfterAddingNodes) {
+ callbackAfterAddingNodes(mapData.arrayEntry, mapData.mappedNodes, mapData.indexObservable);
+ mapData.initialized = true;
}
}
+ // Finally call afterMove and afterAdd callbacks
+ callCallback(options['afterMove'], itemsForMoveCallbacks);
+ callCallback(options['afterAdd'], itemsForAfterAddCallbacks);
+
// Store a copy of the array items we just considered so we can difference it next time
ko.utils.domData.set(domNode, lastMappingResultDomDataKey, newMappingResult);
}
@@ -3440,4 +3544,5 @@ ko.exportSymbol('nativeTemplateEngine', ko.nativeTemplateEngine);
ko.exportSymbol('jqueryTmplTemplateEngine', ko.jqueryTmplTemplateEngine);
})();
});
-})(window,document,navigator);
+})(window,document,navigator,window["jQuery"]);
+})();
diff --git a/package.json b/package.json
index 5843e52..cf11b58 100644
--- a/package.json
+++ b/package.json
@@ -1,14 +1,11 @@
{
"name": "knockoutify",
- "description": "Knockout.js lib for browsers",
- "version": "2.1.0",
+ "description": "Knockout makes it easier to create rich, responsive UIs with JavaScript",
+ "version": "2.2.0-alpha.1",
"repository": {
"type": "git",
- "url": "git://github.com/guillaume86/knockoutify.git"
+ "url": "git://github.com/domenic/knockoutify.git"
},
"author": "Steven Sanderson",
- "engines": {
- "node": "*"
- },
"main": "./index.js"
-}
\ No newline at end of file
+}