Commit c5e53bc8 authored by Pascal Hartig's avatar Pascal Hartig

Upgrade Knockout to v3.1.0

parent 0a50f32a
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"todomvc-common": "~0.1.4", "todomvc-common": "~0.1.4",
"knockout.js": "~3.0.0", "component-knockout-passy": "~3.1.0",
"director": "~1.2.0" "director": "~1.2.0"
} }
} }
// Knockout JavaScript library v3.0.0 /*!
// (c) Steven Sanderson - http://knockoutjs.com/ * Knockout JavaScript library v3.1.0
// License: MIT (http://www.opensource.org/licenses/mit-license.php) * (c) Steven Sanderson - http://knockoutjs.com/
* License: MIT (http://www.opensource.org/licenses/mit-license.php)
*/
(function(){ (function(){
var DEBUG=true; var DEBUG=true;
...@@ -17,15 +19,15 @@ var DEBUG=true; ...@@ -17,15 +19,15 @@ var DEBUG=true;
if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') { if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') {
// [1] CommonJS/Node.js // [1] CommonJS/Node.js
var target = module['exports'] || exports; // module.exports is for Node.js var target = module['exports'] || exports; // module.exports is for Node.js
factory(target); factory(target, require);
} else if (typeof define === 'function' && define['amd']) { } else if (typeof define === 'function' && define['amd']) {
// [2] AMD anonymous module // [2] AMD anonymous module
define(['exports'], factory); define(['exports', 'require'], factory);
} else { } else {
// [3] No module loader (plain <script> tag) - put directly in global namespace // [3] No module loader (plain <script> tag) - put directly in global namespace
factory(window['ko'] = {}); factory(window['ko'] = {});
} }
}(function(koExports){ }(function(koExports, require){
// Internally, all KO objects are attached to koExports (even the non-exported ones whose names will be minified by the closure compiler). // Internally, all KO objects are attached to koExports (even the non-exported ones whose names will be minified by the closure compiler).
// In the future, the following "ko" variable may be made distinct from "koExports" so that private objects are not externally reachable. // In the future, the following "ko" variable may be made distinct from "koExports" so that private objects are not externally reachable.
var ko = typeof koExports !== 'undefined' ? koExports : {}; var ko = typeof koExports !== 'undefined' ? koExports : {};
...@@ -44,17 +46,35 @@ ko.exportSymbol = function(koPath, object) { ...@@ -44,17 +46,35 @@ ko.exportSymbol = function(koPath, object) {
ko.exportProperty = function(owner, publicName, object) { ko.exportProperty = function(owner, publicName, object) {
owner[publicName] = object; owner[publicName] = object;
}; };
ko.version = "3.0.0"; ko.version = "3.1.0";
ko.exportSymbol('version', ko.version); ko.exportSymbol('version', ko.version);
ko.utils = (function () { ko.utils = (function () {
var objectForEach = function(obj, action) { function objectForEach(obj, action) {
for (var prop in obj) { for (var prop in obj) {
if (obj.hasOwnProperty(prop)) { if (obj.hasOwnProperty(prop)) {
action(prop, obj[prop]); action(prop, obj[prop]);
} }
} }
}; }
function extend(target, source) {
if (source) {
for(var prop in source) {
if(source.hasOwnProperty(prop)) {
target[prop] = source[prop];
}
}
}
return target;
}
function setPrototypeOf(obj, proto) {
obj.__proto__ = proto;
return obj;
}
var canSetPrototype = ({ __proto__: [] } instanceof Array);
// Represent the known event types in a compact way, then at runtime transform it into a hash with event name as key (for fast lookup) // Represent the known event types in a compact way, then at runtime transform it into a hash with event name as key (for fast lookup)
var knownEvents = {}, knownEventTypesByEventName = {}; var knownEvents = {}, knownEventTypesByEventName = {};
...@@ -98,7 +118,7 @@ ko.utils = (function () { ...@@ -98,7 +118,7 @@ ko.utils = (function () {
arrayForEach: function (array, action) { arrayForEach: function (array, action) {
for (var i = 0, j = array.length; i < j; i++) for (var i = 0, j = array.length; i < j; i++)
action(array[i]); action(array[i], i);
}, },
arrayIndexOf: function (array, item) { arrayIndexOf: function (array, item) {
...@@ -112,15 +132,19 @@ ko.utils = (function () { ...@@ -112,15 +132,19 @@ ko.utils = (function () {
arrayFirst: function (array, predicate, predicateOwner) { arrayFirst: function (array, predicate, predicateOwner) {
for (var i = 0, j = array.length; i < j; i++) for (var i = 0, j = array.length; i < j; i++)
if (predicate.call(predicateOwner, array[i])) if (predicate.call(predicateOwner, array[i], i))
return array[i]; return array[i];
return null; return null;
}, },
arrayRemoveItem: function (array, itemToRemove) { arrayRemoveItem: function (array, itemToRemove) {
var index = ko.utils.arrayIndexOf(array, itemToRemove); var index = ko.utils.arrayIndexOf(array, itemToRemove);
if (index >= 0) if (index > 0) {
array.splice(index, 1); array.splice(index, 1);
}
else if (index === 0) {
array.shift();
}
}, },
arrayGetDistinctValues: function (array) { arrayGetDistinctValues: function (array) {
...@@ -137,7 +161,7 @@ ko.utils = (function () { ...@@ -137,7 +161,7 @@ ko.utils = (function () {
array = array || []; array = array || [];
var result = []; var result = [];
for (var i = 0, j = array.length; i < j; i++) for (var i = 0, j = array.length; i < j; i++)
result.push(mapping(array[i])); result.push(mapping(array[i], i));
return result; return result;
}, },
...@@ -145,7 +169,7 @@ ko.utils = (function () { ...@@ -145,7 +169,7 @@ ko.utils = (function () {
array = array || []; array = array || [];
var result = []; var result = [];
for (var i = 0, j = array.length; i < j; i++) for (var i = 0, j = array.length; i < j; i++)
if (predicate(array[i])) if (predicate(array[i], i))
result.push(array[i]); result.push(array[i]);
return result; return result;
}, },
...@@ -170,16 +194,13 @@ ko.utils = (function () { ...@@ -170,16 +194,13 @@ ko.utils = (function () {
} }
}, },
extend: function (target, source) { canSetPrototype: canSetPrototype,
if (source) {
for(var prop in source) { extend: extend,
if(source.hasOwnProperty(prop)) {
target[prop] = source[prop]; setPrototypeOf: setPrototypeOf,
}
} setPrototypeOfOrExtend: canSetPrototype ? setPrototypeOf : extend,
}
return target;
},
objectForEach: objectForEach, objectForEach: objectForEach,
...@@ -262,7 +283,7 @@ ko.utils = (function () { ...@@ -262,7 +283,7 @@ ko.utils = (function () {
// Rule [A] // Rule [A]
while (continuousNodeArray.length && continuousNodeArray[0].parentNode !== parentNode) while (continuousNodeArray.length && continuousNodeArray[0].parentNode !== parentNode)
continuousNodeArray.splice(0, 1); continuousNodeArray.shift();
// Rule [B] // Rule [B]
if (continuousNodeArray.length > 1) { if (continuousNodeArray.length > 1) {
...@@ -346,21 +367,7 @@ ko.utils = (function () { ...@@ -346,21 +367,7 @@ ko.utils = (function () {
registerEventHandler: function (element, eventType, handler) { registerEventHandler: function (element, eventType, handler) {
var mustUseAttachEvent = ieVersion && eventsThatMustBeRegisteredUsingAttachEvent[eventType]; var mustUseAttachEvent = ieVersion && eventsThatMustBeRegisteredUsingAttachEvent[eventType];
if (!mustUseAttachEvent && typeof jQuery != "undefined") { if (!mustUseAttachEvent && jQuery) {
if (isClickOnCheckableElement(element, eventType)) {
// For click events on checkboxes, jQuery interferes with the event handling in an awkward way:
// it toggles the element checked state *after* the click event handlers run, whereas native
// click events toggle the checked state *before* the event handler.
// Fix this by intecepting the handler and applying the correct checkedness before it runs.
var originalHandler = handler;
handler = function(event, eventData) {
var jQuerySuppliedCheckedState = this.checked;
if (eventData)
this.checked = eventData.checkedStateBeforeEvent !== true;
originalHandler.call(this, event);
this.checked = jQuerySuppliedCheckedState; // Restore the state jQuery applied
};
}
jQuery(element)['bind'](eventType, handler); jQuery(element)['bind'](eventType, handler);
} else if (!mustUseAttachEvent && typeof element.addEventListener == "function") } else if (!mustUseAttachEvent && typeof element.addEventListener == "function")
element.addEventListener(eventType, handler, false); element.addEventListener(eventType, handler, false);
...@@ -382,13 +389,14 @@ ko.utils = (function () { ...@@ -382,13 +389,14 @@ ko.utils = (function () {
if (!(element && element.nodeType)) if (!(element && element.nodeType))
throw new Error("element must be a DOM node when calling triggerEvent"); throw new Error("element must be a DOM node when calling triggerEvent");
if (typeof jQuery != "undefined") { // For click events on checkboxes and radio buttons, jQuery toggles the element checked state *after* the
var eventData = []; // event handler runs instead of *before*. (This was fixed in 1.9 for checkboxes but not for radio buttons.)
if (isClickOnCheckableElement(element, eventType)) { // IE doesn't change the checked state when you trigger the click event using "fireEvent".
// Work around the jQuery "click events on checkboxes" issue described above by storing the original checked state before triggering the handler // In both cases, we'll use the click method instead.
eventData.push({ checkedStateBeforeEvent: element.checked }); var useClickWorkaround = isClickOnCheckableElement(element, eventType);
}
jQuery(element)['trigger'](eventType, eventData); if (jQuery && !useClickWorkaround) {
jQuery(element)['trigger'](eventType);
} else if (typeof document.createEvent == "function") { } else if (typeof document.createEvent == "function") {
if (typeof element.dispatchEvent == "function") { if (typeof element.dispatchEvent == "function") {
var eventCategory = knownEventTypesByEventName[eventType] || "HTMLEvents"; var eventCategory = knownEventTypesByEventName[eventType] || "HTMLEvents";
...@@ -398,15 +406,13 @@ ko.utils = (function () { ...@@ -398,15 +406,13 @@ ko.utils = (function () {
} }
else else
throw new Error("The supplied element doesn't support dispatchEvent"); throw new Error("The supplied element doesn't support dispatchEvent");
} else if (useClickWorkaround && element.click) {
element.click();
} else if (typeof element.fireEvent != "undefined") { } else if (typeof element.fireEvent != "undefined") {
// Unlike other browsers, IE doesn't change the checked state of checkboxes/radiobuttons when you trigger their "click" event
// so to make it consistent, we'll do it manually here
if (isClickOnCheckableElement(element, eventType))
element.checked = element.checked !== true;
element.fireEvent("on" + eventType); element.fireEvent("on" + eventType);
} } else {
else
throw new Error("Browser doesn't support triggering events"); throw new Error("Browser doesn't support triggering events");
}
}, },
unwrapObservable: function (value) { unwrapObservable: function (value) {
...@@ -438,7 +444,7 @@ ko.utils = (function () { ...@@ -438,7 +444,7 @@ ko.utils = (function () {
// we'll clear everything and create a single text node. // we'll clear everything and create a single text node.
var innerTextNode = ko.virtualElements.firstChild(element); var innerTextNode = ko.virtualElements.firstChild(element);
if (!innerTextNode || innerTextNode.nodeType != 3 || ko.virtualElements.nextSibling(innerTextNode)) { if (!innerTextNode || innerTextNode.nodeType != 3 || ko.virtualElements.nextSibling(innerTextNode)) {
ko.virtualElements.setDomNodeChildren(element, [document.createTextNode(value)]); ko.virtualElements.setDomNodeChildren(element, [element.ownerDocument.createTextNode(value)]);
} else { } else {
innerTextNode.data = value; innerTextNode.data = value;
} }
...@@ -687,16 +693,13 @@ ko.utils.domNodeDisposal = new (function () { ...@@ -687,16 +693,13 @@ ko.utils.domNodeDisposal = new (function () {
callbacks[i](node); callbacks[i](node);
} }
// Also erase the DOM data // Erase the DOM data
ko.utils.domData.clear(node); ko.utils.domData.clear(node);
// Special support for jQuery here because it's so commonly used. // Perform cleanup needed by external libraries (currently only jQuery, but can be extended)
// Many jQuery plugins (including jquery.tmpl) store data using jQuery's equivalent of domData ko.utils.domNodeDisposal["cleanExternalData"](node);
// so notify it to tear down any resources associated with the node & descendants here.
if ((typeof jQuery == "function") && (typeof jQuery['cleanData'] == "function"))
jQuery['cleanData']([node]);
// Also clear any immediate-child comment nodes, as these wouldn't have been found by // Clear any immediate-child comment nodes, as these wouldn't have been found by
// node.getElementsByTagName("*") in cleanNode() (comment nodes aren't elements) // node.getElementsByTagName("*") in cleanNode() (comment nodes aren't elements)
if (cleanableNodeTypesWithDescendants[node.nodeType]) if (cleanableNodeTypesWithDescendants[node.nodeType])
cleanImmediateCommentTypeChildren(node); cleanImmediateCommentTypeChildren(node);
...@@ -748,6 +751,14 @@ ko.utils.domNodeDisposal = new (function () { ...@@ -748,6 +751,14 @@ ko.utils.domNodeDisposal = new (function () {
ko.cleanNode(node); ko.cleanNode(node);
if (node.parentNode) if (node.parentNode)
node.parentNode.removeChild(node); node.parentNode.removeChild(node);
},
"cleanExternalData" : function (node) {
// Special support for jQuery here because it's so commonly used.
// Many jQuery plugins (including jquery.tmpl) store data using jQuery's equivalent of domData
// so notify it to tear down any resources associated with the node & descendants here.
if (jQuery && (typeof jQuery['cleanData'] == "function"))
jQuery['cleanData']([node]);
} }
} }
})(); })();
...@@ -821,8 +832,8 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD ...@@ -821,8 +832,8 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD
} }
ko.utils.parseHtmlFragment = function(html) { ko.utils.parseHtmlFragment = function(html) {
return typeof jQuery != 'undefined' ? jQueryHtmlParse(html) // As below, benefit from jQuery's optimisations where possible return jQuery ? jQueryHtmlParse(html) // As below, benefit from jQuery's optimisations where possible
: simpleHtmlParse(html); // ... otherwise, this simple logic will do in most common cases. : simpleHtmlParse(html); // ... otherwise, this simple logic will do in most common cases.
}; };
ko.utils.setHtml = function(node, html) { ko.utils.setHtml = function(node, html) {
...@@ -838,7 +849,7 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD ...@@ -838,7 +849,7 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD
// jQuery contains a lot of sophisticated code to parse arbitrary HTML fragments, // jQuery contains a lot of sophisticated code to parse arbitrary HTML fragments,
// for example <tr> elements which are not normally allowed to exist on their own. // for example <tr> elements which are not normally allowed to exist on their own.
// If you've referenced jQuery we'll use that rather than duplicating its code. // If you've referenced jQuery we'll use that rather than duplicating its code.
if (typeof jQuery != 'undefined') { if (jQuery) {
jQuery(node)['html'](html); jQuery(node)['html'](html);
} else { } else {
// ... otherwise, use KO's own parsing logic. // ... otherwise, use KO's own parsing logic.
...@@ -944,6 +955,22 @@ ko.extenders = { ...@@ -944,6 +955,22 @@ ko.extenders = {
}); });
}, },
'rateLimit': function(target, options) {
var timeout, method, limitFunction;
if (typeof options == 'number') {
timeout = options;
} else {
timeout = options['timeout'];
method = options['method'];
}
limitFunction = method == 'notifyWhenChangesStop' ? debounce : throttle;
target.limit(function(callback) {
return limitFunction(callback, timeout);
});
},
'notify': function(target, notifyWhen) { 'notify': function(target, notifyWhen) {
target["equalityComparer"] = notifyWhen == "always" ? target["equalityComparer"] = notifyWhen == "always" ?
null : // null equalityComparer means to always notify null : // null equalityComparer means to always notify
...@@ -957,6 +984,26 @@ function valuesArePrimitiveAndEqual(a, b) { ...@@ -957,6 +984,26 @@ function valuesArePrimitiveAndEqual(a, b) {
return oldValueIsPrimitive ? (a === b) : false; return oldValueIsPrimitive ? (a === b) : false;
} }
function throttle(callback, timeout) {
var timeoutInstance;
return function () {
if (!timeoutInstance) {
timeoutInstance = setTimeout(function() {
timeoutInstance = undefined;
callback();
}, timeout);
}
};
}
function debounce(callback, timeout) {
var timeoutInstance;
return function () {
clearTimeout(timeoutInstance);
timeoutInstance = setTimeout(callback, timeout);
};
}
function applyExtenders(requestedExtenders) { function applyExtenders(requestedExtenders) {
var target = this; var target = this;
if (requestedExtenders) { if (requestedExtenders) {
...@@ -976,6 +1023,7 @@ ko.subscription = function (target, callback, disposeCallback) { ...@@ -976,6 +1023,7 @@ ko.subscription = function (target, callback, disposeCallback) {
this.target = target; this.target = target;
this.callback = callback; this.callback = callback;
this.disposeCallback = disposeCallback; this.disposeCallback = disposeCallback;
this.isDisposed = false;
ko.exportProperty(this, 'dispose', this.dispose); ko.exportProperty(this, 'dispose', this.dispose);
}; };
ko.subscription.prototype.dispose = function () { ko.subscription.prototype.dispose = function () {
...@@ -984,28 +1032,32 @@ ko.subscription.prototype.dispose = function () { ...@@ -984,28 +1032,32 @@ ko.subscription.prototype.dispose = function () {
}; };
ko.subscribable = function () { ko.subscribable = function () {
ko.utils.setPrototypeOfOrExtend(this, ko.subscribable['fn']);
this._subscriptions = {}; this._subscriptions = {};
ko.utils.extend(this, ko.subscribable['fn']);
ko.exportProperty(this, 'subscribe', this.subscribe);
ko.exportProperty(this, 'extend', this.extend);
ko.exportProperty(this, 'getSubscriptionsCount', this.getSubscriptionsCount);
} }
var defaultEvent = "change"; var defaultEvent = "change";
ko.subscribable['fn'] = { var ko_subscribable_fn = {
subscribe: function (callback, callbackTarget, event) { subscribe: function (callback, callbackTarget, event) {
var self = this;
event = event || defaultEvent; event = event || defaultEvent;
var boundCallback = callbackTarget ? callback.bind(callbackTarget) : callback; var boundCallback = callbackTarget ? callback.bind(callbackTarget) : callback;
var subscription = new ko.subscription(this, boundCallback, function () { var subscription = new ko.subscription(self, boundCallback, function () {
ko.utils.arrayRemoveItem(this._subscriptions[event], subscription); ko.utils.arrayRemoveItem(self._subscriptions[event], subscription);
}.bind(this)); });
if (!this._subscriptions[event]) // This will force a computed with deferEvaluation to evaluate before any subscriptions
this._subscriptions[event] = []; // are registered.
this._subscriptions[event].push(subscription); if (self.peek) {
self.peek();
}
if (!self._subscriptions[event])
self._subscriptions[event] = [];
self._subscriptions[event].push(subscription);
return subscription; return subscription;
}, },
...@@ -1013,19 +1065,61 @@ ko.subscribable['fn'] = { ...@@ -1013,19 +1065,61 @@ ko.subscribable['fn'] = {
event = event || defaultEvent; event = event || defaultEvent;
if (this.hasSubscriptionsForEvent(event)) { if (this.hasSubscriptionsForEvent(event)) {
try { try {
ko.dependencyDetection.begin(); 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 a = this._subscriptions[event].slice(0), i = 0, subscription; subscription = a[i]; ++i) {
// In case a subscription was disposed during the arrayForEach cycle, check // In case a subscription was disposed during the arrayForEach cycle, check
// for isDisposed on each subscription before invoking its callback // for isDisposed on each subscription before invoking its callback
if (subscription && (subscription.isDisposed !== true)) if (!subscription.isDisposed)
subscription.callback(valueToNotify); subscription.callback(valueToNotify);
} }
} finally { } finally {
ko.dependencyDetection.end(); ko.dependencyDetection.end(); // End suppressing dependency detection
} }
} }
}, },
limit: function(limitFunction) {
var self = this, selfIsObservable = ko.isObservable(self),
isPending, previousValue, pendingValue, beforeChange = 'beforeChange';
if (!self._origNotifySubscribers) {
self._origNotifySubscribers = self["notifySubscribers"];
self["notifySubscribers"] = function(value, event) {
if (!event || event === defaultEvent) {
self._rateLimitedChange(value);
} else if (event === beforeChange) {
self._rateLimitedBeforeChange(value);
} else {
self._origNotifySubscribers(value, event);
}
};
}
var finish = limitFunction(function() {
// 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();
}
isPending = false;
if (self.isDifferent(previousValue, pendingValue)) {
self._origNotifySubscribers(previousValue = pendingValue);
}
});
self._rateLimitedChange = function(value) {
isPending = true;
pendingValue = value;
finish();
};
self._rateLimitedBeforeChange = function(value) {
if (!isPending) {
previousValue = value;
self._origNotifySubscribers(value, beforeChange);
}
};
},
hasSubscriptionsForEvent: function(event) { hasSubscriptionsForEvent: function(event) {
return this._subscriptions[event] && this._subscriptions[event].length; return this._subscriptions[event] && this._subscriptions[event].length;
}, },
...@@ -1038,9 +1132,26 @@ ko.subscribable['fn'] = { ...@@ -1038,9 +1132,26 @@ ko.subscribable['fn'] = {
return total; return total;
}, },
isDifferent: function(oldValue, newValue) {
return !this['equalityComparer'] || !this['equalityComparer'](oldValue, newValue);
},
extend: applyExtenders extend: applyExtenders
}; };
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);
// For browsers that support proto assignment, we overwrite the prototype of each
// observable instance. Since observables are functions, we need Function.prototype
// to still be in the prototype chain.
if (ko.utils.canSetPrototype) {
ko.utils.setPrototypeOf(ko_subscribable_fn, Function.prototype);
}
ko.subscribable['fn'] = ko_subscribable_fn;
ko.isSubscribable = function (instance) { ko.isSubscribable = function (instance) {
return instance != null && typeof instance.subscribe == "function" && typeof instance["notifySubscribers"] == "function"; return instance != null && typeof instance.subscribe == "function" && typeof instance["notifySubscribers"] == "function";
...@@ -1049,40 +1160,67 @@ ko.isSubscribable = function (instance) { ...@@ -1049,40 +1160,67 @@ ko.isSubscribable = function (instance) {
ko.exportSymbol('subscribable', ko.subscribable); ko.exportSymbol('subscribable', ko.subscribable);
ko.exportSymbol('isSubscribable', ko.isSubscribable); ko.exportSymbol('isSubscribable', ko.isSubscribable);
ko.dependencyDetection = (function () { ko.computedContext = ko.dependencyDetection = (function () {
var _frames = []; var outerFrames = [],
currentFrame,
lastId = 0;
// Return a unique ID that can be assigned to an observable for dependency tracking.
// Theoretically, you could eventually overflow the number storage size, resulting
// in duplicate IDs. But in JavaScript, the largest exact integral value is 2^53
// or 9,007,199,254,740,992. If you created 1,000,000 IDs per second, it would
// take over 285 years to reach that number.
// Reference http://blog.vjeux.com/2010/javascript/javascript-max_int-number-limits.html
function getId() {
return ++lastId;
}
function begin(options) {
outerFrames.push(currentFrame);
currentFrame = options;
}
function end() {
currentFrame = outerFrames.pop();
}
return { return {
begin: function (callback) { begin: begin,
_frames.push(callback && { callback: callback, distinctDependencies:[] });
},
end: function () { end: end,
_frames.pop();
},
registerDependency: function (subscribable) { registerDependency: function (subscribable) {
if (!ko.isSubscribable(subscribable)) if (currentFrame) {
throw new Error("Only subscribable things can act as dependencies"); if (!ko.isSubscribable(subscribable))
if (_frames.length > 0) { throw new Error("Only subscribable things can act as dependencies");
var topFrame = _frames[_frames.length - 1]; currentFrame.callback(subscribable, subscribable._id || (subscribable._id = getId()));
if (!topFrame || ko.utils.arrayIndexOf(topFrame.distinctDependencies, subscribable) >= 0)
return;
topFrame.distinctDependencies.push(subscribable);
topFrame.callback(subscribable);
} }
}, },
ignore: function(callback, callbackTarget, callbackArgs) { ignore: function (callback, callbackTarget, callbackArgs) {
try { try {
_frames.push(null); begin();
return callback.apply(callbackTarget, callbackArgs || []); return callback.apply(callbackTarget, callbackArgs || []);
} finally { } finally {
_frames.pop(); end();
} }
},
getDependenciesCount: function () {
if (currentFrame)
return currentFrame.computed.getDependenciesCount();
},
isInitial: function() {
if (currentFrame)
return currentFrame.isInitial;
} }
}; };
})(); })();
ko.exportSymbol('computedContext', ko.computedContext);
ko.exportSymbol('computedContext.getDependenciesCount', ko.computedContext.getDependenciesCount);
ko.exportSymbol('computedContext.isInitial', ko.computedContext.isInitial);
ko.observable = function (initialValue) { ko.observable = function (initialValue) {
var _latestValue = initialValue; var _latestValue = initialValue;
...@@ -1091,7 +1229,7 @@ ko.observable = function (initialValue) { ...@@ -1091,7 +1229,7 @@ ko.observable = function (initialValue) {
// Write // Write
// Ignore writes if the value hasn't changed // Ignore writes if the value hasn't changed
if (!observable['equalityComparer'] || !observable['equalityComparer'](_latestValue, arguments[0])) { if (observable.isDifferent(_latestValue, arguments[0])) {
observable.valueWillMutate(); observable.valueWillMutate();
_latestValue = arguments[0]; _latestValue = arguments[0];
if (DEBUG) observable._latestValue = _latestValue; if (DEBUG) observable._latestValue = _latestValue;
...@@ -1105,12 +1243,13 @@ ko.observable = function (initialValue) { ...@@ -1105,12 +1243,13 @@ ko.observable = function (initialValue) {
return _latestValue; return _latestValue;
} }
} }
if (DEBUG) observable._latestValue = _latestValue;
ko.subscribable.call(observable); ko.subscribable.call(observable);
ko.utils.setPrototypeOfOrExtend(observable, ko.observable['fn']);
if (DEBUG) observable._latestValue = _latestValue;
observable.peek = function() { return _latestValue }; observable.peek = function() { return _latestValue };
observable.valueHasMutated = function () { observable["notifySubscribers"](_latestValue); } observable.valueHasMutated = function () { observable["notifySubscribers"](_latestValue); }
observable.valueWillMutate = function () { observable["notifySubscribers"](_latestValue, "beforeChange"); } observable.valueWillMutate = function () { observable["notifySubscribers"](_latestValue, "beforeChange"); }
ko.utils.extend(observable, ko.observable['fn']);
ko.exportProperty(observable, 'peek', observable.peek); ko.exportProperty(observable, 'peek', observable.peek);
ko.exportProperty(observable, "valueHasMutated", observable.valueHasMutated); ko.exportProperty(observable, "valueHasMutated", observable.valueHasMutated);
...@@ -1126,6 +1265,12 @@ ko.observable['fn'] = { ...@@ -1126,6 +1265,12 @@ ko.observable['fn'] = {
var protoProperty = ko.observable.protoProperty = "__ko_proto__"; var protoProperty = ko.observable.protoProperty = "__ko_proto__";
ko.observable['fn'][protoProperty] = ko.observable; ko.observable['fn'][protoProperty] = ko.observable;
// Note that for browsers that don't support proto assignment, the
// inheritance chain is created manually in the ko.observable constructor
if (ko.utils.canSetPrototype) {
ko.utils.setPrototypeOf(ko.observable['fn'], ko.subscribable['fn']);
}
ko.hasPrototype = function(instance, prototype) { ko.hasPrototype = function(instance, prototype) {
if ((instance === null) || (instance === undefined) || (instance[protoProperty] === undefined)) return false; if ((instance === null) || (instance === undefined) || (instance[protoProperty] === undefined)) return false;
if (instance[protoProperty] === prototype) return true; if (instance[protoProperty] === prototype) return true;
...@@ -1157,7 +1302,7 @@ ko.observableArray = function (initialValues) { ...@@ -1157,7 +1302,7 @@ ko.observableArray = function (initialValues) {
throw new Error("The argument passed when initializing an observable array must be an array, or null, or undefined."); throw new Error("The argument passed when initializing an observable array must be an array, or null, or undefined.");
var result = ko.observable(initialValues); var result = ko.observable(initialValues);
ko.utils.extend(result, ko.observableArray['fn']); ko.utils.setPrototypeOfOrExtend(result, ko.observableArray['fn']);
return result.extend({'trackArrayChanges':true}); return result.extend({'trackArrayChanges':true});
}; };
...@@ -1265,6 +1410,12 @@ ko.utils.arrayForEach(["slice"], function (methodName) { ...@@ -1265,6 +1410,12 @@ ko.utils.arrayForEach(["slice"], function (methodName) {
}; };
}); });
// Note that for browsers that don't support proto assignment, the
// inheritance chain is created manually in the ko.observableArray constructor
if (ko.utils.canSetPrototype) {
ko.utils.setPrototypeOf(ko.observableArray['fn'], ko.observable['fn']);
}
ko.exportSymbol('observableArray', ko.observableArray); ko.exportSymbol('observableArray', ko.observableArray);
var arrayChangeEventName = 'arrayChange'; var arrayChangeEventName = 'arrayChange';
ko.extenders['trackArrayChanges'] = function(target) { ko.extenders['trackArrayChanges'] = function(target) {
...@@ -1327,9 +1478,9 @@ ko.extenders['trackArrayChanges'] = function(target) { ...@@ -1327,9 +1478,9 @@ ko.extenders['trackArrayChanges'] = function(target) {
function getChanges(previousContents, currentContents) { function getChanges(previousContents, currentContents) {
// We try to re-use cached diffs. // We try to re-use cached diffs.
// The only scenario where pendingNotifications > 1 is when using the KO 'deferred updates' plugin, // The scenarios where pendingNotifications > 1 are when using rate-limiting or the Deferred Updates
// which without this check would not be compatible with arrayChange notifications. Without that // plugin, which without this check would not be compatible with arrayChange notifications. Normally,
// plugin, notifications are always issued immediately so we wouldn't be queueing up more than one. // notifications are issued immediately so we wouldn't be queueing up more than one.
if (!cachedDiff || pendingNotifications > 1) { if (!cachedDiff || pendingNotifications > 1) {
cachedDiff = ko.utils.compareArrays(previousContents, currentContents, { 'sparse': true }); cachedDiff = ko.utils.compareArrays(previousContents, currentContents, { 'sparse': true });
} }
...@@ -1349,7 +1500,7 @@ ko.extenders['trackArrayChanges'] = function(target) { ...@@ -1349,7 +1500,7 @@ ko.extenders['trackArrayChanges'] = function(target) {
offset = 0; offset = 0;
function pushDiff(status, value, index) { function pushDiff(status, value, index) {
diff.push({ 'status': status, 'value': value, 'index': index }); return diff[diff.length] = { 'status': status, 'value': value, 'index': index };
} }
switch (operationName) { switch (operationName) {
case 'push': case 'push':
...@@ -1374,13 +1525,15 @@ ko.extenders['trackArrayChanges'] = function(target) { ...@@ -1374,13 +1525,15 @@ ko.extenders['trackArrayChanges'] = function(target) {
var startIndex = Math.min(Math.max(0, args[0] < 0 ? arrayLength + args[0] : args[0]), arrayLength), var startIndex = Math.min(Math.max(0, args[0] < 0 ? arrayLength + args[0] : args[0]), arrayLength),
endDeleteIndex = argsLength === 1 ? arrayLength : Math.min(startIndex + (args[1] || 0), arrayLength), endDeleteIndex = argsLength === 1 ? arrayLength : Math.min(startIndex + (args[1] || 0), arrayLength),
endAddIndex = startIndex + argsLength - 2, endAddIndex = startIndex + argsLength - 2,
endIndex = Math.max(endDeleteIndex, endAddIndex); endIndex = Math.max(endDeleteIndex, endAddIndex),
additions = [], deletions = [];
for (var index = startIndex, argsIndex = 2; index < endIndex; ++index, ++argsIndex) { for (var index = startIndex, argsIndex = 2; index < endIndex; ++index, ++argsIndex) {
if (index < endDeleteIndex) if (index < endDeleteIndex)
pushDiff('deleted', rawArray[index], index); deletions.push(pushDiff('deleted', rawArray[index], index));
if (index < endAddIndex) if (index < endAddIndex)
pushDiff('added', args[argsIndex], index); additions.push(pushDiff('added', args[argsIndex], index));
} }
ko.utils.findMovesInArrayComparison(deletions, additions);
break; break;
default: default:
...@@ -1389,11 +1542,12 @@ ko.extenders['trackArrayChanges'] = function(target) { ...@@ -1389,11 +1542,12 @@ ko.extenders['trackArrayChanges'] = function(target) {
cachedDiff = diff; cachedDiff = diff;
}; };
}; };
ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget, options) { ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget, options) {
var _latestValue, var _latestValue,
_hasBeenEvaluated = false, _needsEvaluation = true,
_isBeingEvaluated = false, _isBeingEvaluated = false,
_suppressDisposalUntilDisposeWhenReturnsFalse = false, _suppressDisposalUntilDisposeWhenReturnsFalse = false,
_isDisposed = false,
readFunction = evaluatorFunctionOrOptions; readFunction = evaluatorFunctionOrOptions;
if (readFunction && typeof readFunction == "object") { if (readFunction && typeof readFunction == "object") {
...@@ -1409,15 +1563,21 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction ...@@ -1409,15 +1563,21 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
if (typeof readFunction != "function") if (typeof readFunction != "function")
throw new Error("Pass a function that returns the value of the ko.computed"); throw new Error("Pass a function that returns the value of the ko.computed");
function addSubscriptionToDependency(subscribable) { function addSubscriptionToDependency(subscribable, id) {
_subscriptionsToDependencies.push(subscribable.subscribe(evaluatePossiblyAsync)); if (!_subscriptionsToDependencies[id]) {
_subscriptionsToDependencies[id] = subscribable.subscribe(evaluatePossiblyAsync);
++_dependenciesCount;
}
} }
function disposeAllSubscriptionsToDependencies() { function disposeAllSubscriptionsToDependencies() {
ko.utils.arrayForEach(_subscriptionsToDependencies, function (subscription) { _isDisposed = true;
ko.utils.objectForEach(_subscriptionsToDependencies, function (id, subscription) {
subscription.dispose(); subscription.dispose();
}); });
_subscriptionsToDependencies = []; _subscriptionsToDependencies = {};
_dependenciesCount = 0;
_needsEvaluation = false;
} }
function evaluatePossiblyAsync() { function evaluatePossiblyAsync() {
...@@ -1425,8 +1585,11 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction ...@@ -1425,8 +1585,11 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
if (throttleEvaluationTimeout && throttleEvaluationTimeout >= 0) { if (throttleEvaluationTimeout && throttleEvaluationTimeout >= 0) {
clearTimeout(evaluationTimeoutInstance); clearTimeout(evaluationTimeoutInstance);
evaluationTimeoutInstance = setTimeout(evaluateImmediate, throttleEvaluationTimeout); evaluationTimeoutInstance = setTimeout(evaluateImmediate, throttleEvaluationTimeout);
} else } else if (dependentObservable._evalRateLimited) {
dependentObservable._evalRateLimited();
} else {
evaluateImmediate(); evaluateImmediate();
}
} }
function evaluateImmediate() { function evaluateImmediate() {
...@@ -1438,11 +1601,15 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction ...@@ -1438,11 +1601,15 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
return; return;
} }
// Do not evaluate (and possibly capture new dependencies) if disposed
if (_isDisposed) {
return;
}
if (disposeWhen && disposeWhen()) { if (disposeWhen && disposeWhen()) {
// See comment below about _suppressDisposalUntilDisposeWhenReturnsFalse // See comment below about _suppressDisposalUntilDisposeWhenReturnsFalse
if (!_suppressDisposalUntilDisposeWhenReturnsFalse) { if (!_suppressDisposalUntilDisposeWhenReturnsFalse) {
dispose(); dispose();
_hasBeenEvaluated = true;
return; return;
} }
} else { } else {
...@@ -1454,38 +1621,63 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction ...@@ -1454,38 +1621,63 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
try { try {
// Initially, we assume that none of the subscriptions are still being used (i.e., all are candidates for disposal). // 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. // Then, during evaluation, we cross off any that are in fact still being used.
var disposalCandidates = ko.utils.arrayMap(_subscriptionsToDependencies, function(item) {return item.target;}); var disposalCandidates = _subscriptionsToDependencies, disposalCount = _dependenciesCount;
ko.dependencyDetection.begin({
ko.dependencyDetection.begin(function(subscribable) { callback: function(subscribable, id) {
var inOld; if (!_isDisposed) {
if ((inOld = ko.utils.arrayIndexOf(disposalCandidates, subscribable)) >= 0) if (disposalCount && disposalCandidates[id]) {
disposalCandidates[inOld] = undefined; // Don't want to dispose this subscription, as it's still being used // Don't want to dispose this subscription, as it's still being used
else _subscriptionsToDependencies[id] = disposalCandidates[id];
addSubscriptionToDependency(subscribable); // Brand new subscription - add it ++_dependenciesCount;
delete disposalCandidates[id];
--disposalCount;
} else {
// Brand new subscription - add it
addSubscriptionToDependency(subscribable, id);
}
}
},
computed: dependentObservable,
isInitial: !_dependenciesCount // If we're evaluating when there are no previous dependencies, it must be the first time
}); });
var newValue = evaluatorFunctionTarget ? readFunction.call(evaluatorFunctionTarget) : readFunction(); _subscriptionsToDependencies = {};
_dependenciesCount = 0;
// For each subscription no longer being used, remove it from the active subscriptions list and dispose it try {
for (var i = disposalCandidates.length - 1; i >= 0; i--) { var newValue = evaluatorFunctionTarget ? readFunction.call(evaluatorFunctionTarget) : readFunction();
if (disposalCandidates[i])
_subscriptionsToDependencies.splice(i, 1)[0].dispose(); } finally {
ko.dependencyDetection.end();
// For each subscription no longer being used, remove it from the active subscriptions list and dispose it
if (disposalCount) {
ko.utils.objectForEach(disposalCandidates, function(id, toDispose) {
toDispose.dispose();
});
}
_needsEvaluation = false;
} }
_hasBeenEvaluated = true;
if (!dependentObservable['equalityComparer'] || !dependentObservable['equalityComparer'](_latestValue, newValue)) { if (dependentObservable.isDifferent(_latestValue, newValue)) {
dependentObservable["notifySubscribers"](_latestValue, "beforeChange"); dependentObservable["notifySubscribers"](_latestValue, "beforeChange");
_latestValue = newValue; _latestValue = newValue;
if (DEBUG) dependentObservable._latestValue = _latestValue; if (DEBUG) dependentObservable._latestValue = _latestValue;
dependentObservable["notifySubscribers"](_latestValue);
// If rate-limited, the notification will happen within the limit function. Otherwise,
// notify as soon as the value changes. Check specifically for the throttle setting since
// it overrides rateLimit.
if (!dependentObservable._evalRateLimited || dependentObservable['throttleEvaluation']) {
dependentObservable["notifySubscribers"](_latestValue);
}
} }
} finally { } finally {
ko.dependencyDetection.end();
_isBeingEvaluated = false; _isBeingEvaluated = false;
} }
if (!_subscriptionsToDependencies.length) if (!_dependenciesCount)
dispose(); dispose();
} }
...@@ -1500,7 +1692,7 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction ...@@ -1500,7 +1692,7 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
return this; // Permits chained assignments return this; // Permits chained assignments
} else { } else {
// Reading the value // Reading the value
if (!_hasBeenEvaluated) if (_needsEvaluation)
evaluateImmediate(); evaluateImmediate();
ko.dependencyDetection.registerDependency(dependentObservable); ko.dependencyDetection.registerDependency(dependentObservable);
return _latestValue; return _latestValue;
...@@ -1508,13 +1700,15 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction ...@@ -1508,13 +1700,15 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
} }
function peek() { function peek() {
if (!_hasBeenEvaluated) // Peek won't re-evaluate, except to get the initial value when "deferEvaluation" is set.
// That's the only time that both of these conditions will be satisfied.
if (_needsEvaluation && !_dependenciesCount)
evaluateImmediate(); evaluateImmediate();
return _latestValue; return _latestValue;
} }
function isActive() { function isActive() {
return !_hasBeenEvaluated || _subscriptionsToDependencies.length > 0; return _needsEvaluation || _dependenciesCount > 0;
} }
// By here, "options" is always non-null // By here, "options" is always non-null
...@@ -1523,20 +1717,36 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction ...@@ -1523,20 +1717,36 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
disposeWhenOption = options["disposeWhen"] || options.disposeWhen, disposeWhenOption = options["disposeWhen"] || options.disposeWhen,
disposeWhen = disposeWhenOption, disposeWhen = disposeWhenOption,
dispose = disposeAllSubscriptionsToDependencies, dispose = disposeAllSubscriptionsToDependencies,
_subscriptionsToDependencies = [], _subscriptionsToDependencies = {},
_dependenciesCount = 0,
evaluationTimeoutInstance = null; evaluationTimeoutInstance = null;
if (!evaluatorFunctionTarget) if (!evaluatorFunctionTarget)
evaluatorFunctionTarget = options["owner"]; evaluatorFunctionTarget = options["owner"];
ko.subscribable.call(dependentObservable);
ko.utils.setPrototypeOfOrExtend(dependentObservable, ko.dependentObservable['fn']);
dependentObservable.peek = peek; dependentObservable.peek = peek;
dependentObservable.getDependenciesCount = function () { return _subscriptionsToDependencies.length; }; dependentObservable.getDependenciesCount = function () { return _dependenciesCount; };
dependentObservable.hasWriteFunction = typeof options["write"] === "function"; dependentObservable.hasWriteFunction = typeof options["write"] === "function";
dependentObservable.dispose = function () { dispose(); }; dependentObservable.dispose = function () { dispose(); };
dependentObservable.isActive = isActive; dependentObservable.isActive = isActive;
ko.subscribable.call(dependentObservable); // Replace the limit function with one that delays evaluation as well.
ko.utils.extend(dependentObservable, ko.dependentObservable['fn']); var originalLimit = dependentObservable.limit;
dependentObservable.limit = function(limitFunction) {
originalLimit.call(dependentObservable, limitFunction);
dependentObservable._evalRateLimited = function() {
dependentObservable._rateLimitedBeforeChange(_latestValue);
_needsEvaluation = true; // Mark as dirty
// Pass the observable to the rate-limit code, which will access it when
// it's time to do the notification.
dependentObservable._rateLimitedChange(dependentObservable);
}
};
ko.exportProperty(dependentObservable, 'peek', dependentObservable.peek); ko.exportProperty(dependentObservable, 'peek', dependentObservable.peek);
ko.exportProperty(dependentObservable, 'dispose', dependentObservable.dispose); ko.exportProperty(dependentObservable, 'dispose', dependentObservable.dispose);
...@@ -1568,7 +1778,7 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction ...@@ -1568,7 +1778,7 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
// Attach a DOM node disposal callback so that the computed will be proactively disposed as soon as the node is // Attach a DOM node disposal callback so that the computed will be proactively disposed as soon as the node is
// removed using ko.removeNode. But skip if isActive is false (there will never be any dependencies to dispose). // removed using ko.removeNode. But skip if isActive is false (there will never be any dependencies to dispose).
if (disposeWhenNodeIsRemoved && isActive()) { if (disposeWhenNodeIsRemoved && isActive() && disposeWhenNodeIsRemoved.nodeType) {
dispose = function() { dispose = function() {
ko.utils.domNodeDisposal.removeDisposeCallback(disposeWhenNodeIsRemoved, dispose); ko.utils.domNodeDisposal.removeDisposeCallback(disposeWhenNodeIsRemoved, dispose);
disposeAllSubscriptionsToDependencies(); disposeAllSubscriptionsToDependencies();
...@@ -1591,6 +1801,12 @@ ko.dependentObservable['fn'] = { ...@@ -1591,6 +1801,12 @@ ko.dependentObservable['fn'] = {
}; };
ko.dependentObservable['fn'][protoProp] = ko.dependentObservable; ko.dependentObservable['fn'][protoProp] = ko.dependentObservable;
// Note that for browsers that don't support proto assignment, the
// inheritance chain is created manually in the ko.dependentObservable constructor
if (ko.utils.canSetPrototype) {
ko.utils.setPrototypeOf(ko.dependentObservable['fn'], ko.subscribable['fn']);
}
ko.exportSymbol('dependentObservable', ko.dependentObservable); ko.exportSymbol('dependentObservable', ko.dependentObservable);
ko.exportSymbol('computed', ko.dependentObservable); // Make "ko.computed" an alias for "ko.dependentObservable" ko.exportSymbol('computed', ko.dependentObservable); // Make "ko.computed" an alias for "ko.dependentObservable"
ko.exportSymbol('isComputed', ko.isComputed); ko.exportSymbol('isComputed', ko.isComputed);
...@@ -1712,7 +1928,7 @@ ko.exportSymbol('toJSON', ko.toJSON); ...@@ -1712,7 +1928,7 @@ ko.exportSymbol('toJSON', ko.toJSON);
} }
}, },
writeValue: function(element, value) { writeValue: function(element, value, allowUnset) {
switch (ko.utils.tagNameLower(element)) { switch (ko.utils.tagNameLower(element)) {
case 'option': case 'option':
switch(typeof value) { switch(typeof value) {
...@@ -1734,19 +1950,19 @@ ko.exportSymbol('toJSON', ko.toJSON); ...@@ -1734,19 +1950,19 @@ ko.exportSymbol('toJSON', ko.toJSON);
} }
break; break;
case 'select': case 'select':
if (value === "") if (value === "" || value === null) // A blank string or null value will select the caption
value = undefined; value = undefined;
if (value === null || value === undefined) var selection = -1;
element.selectedIndex = -1; for (var i = 0, n = element.options.length, optionValue; i < n; ++i) {
for (var i = element.options.length - 1; i >= 0; i--) { optionValue = ko.selectExtensions.readValue(element.options[i]);
if (ko.selectExtensions.readValue(element.options[i]) == value) { // Include special check to handle selecting a caption with a blank string value
element.selectedIndex = i; if (optionValue == value || (optionValue == "" && value === undefined)) {
selection = i;
break; break;
} }
} }
// for drop-down select, ensure first is selected if (allowUnset || selection >= 0 || (value === undefined && element.size > 1)) {
if (!(element.size > 1) && element.selectedIndex === -1) { element.selectedIndex = selection;
element.selectedIndex = 0;
} }
break; break;
default: default:
...@@ -1902,7 +2118,7 @@ ko.expressionRewriting = (function () { ...@@ -1902,7 +2118,7 @@ ko.expressionRewriting = (function () {
}); });
if (propertyAccessorResultStrings.length) if (propertyAccessorResultStrings.length)
processKeyValue('_ko_property_writers', "{" + propertyAccessorResultStrings.join(",") + "}"); processKeyValue('_ko_property_writers', "{" + propertyAccessorResultStrings.join(",") + " }");
return resultStrings.join(","); return resultStrings.join(",");
} }
...@@ -2157,6 +2373,336 @@ ko.exportSymbol('virtualElements.insertAfter', ko.virtualElements.insertAfter); ...@@ -2157,6 +2373,336 @@ ko.exportSymbol('virtualElements.insertAfter', ko.virtualElements.insertAfter);
//ko.exportSymbol('virtualElements.nextSibling', ko.virtualElements.nextSibling); // nextSibling is not minified //ko.exportSymbol('virtualElements.nextSibling', ko.virtualElements.nextSibling); // nextSibling is not minified
ko.exportSymbol('virtualElements.prepend', ko.virtualElements.prepend); ko.exportSymbol('virtualElements.prepend', ko.virtualElements.prepend);
ko.exportSymbol('virtualElements.setDomNodeChildren', ko.virtualElements.setDomNodeChildren); ko.exportSymbol('virtualElements.setDomNodeChildren', ko.virtualElements.setDomNodeChildren);
(function(undefined) {
var loadingSubscribablesCache = {}, // Tracks component loads that are currently in flight
loadedDefinitionsCache = {}; // Tracks component loads that have already completed
ko.components = {
get: function(componentName, callback) {
var cachedDefinition = getObjectOwnProperty(loadedDefinitionsCache, componentName);
if (cachedDefinition) {
// It's already loaded and cached. Reuse the same definition object.
// Note that for API consistency, even cache hits complete asynchronously.
setTimeout(function() { callback(cachedDefinition) }, 0);
} else {
// Join the loading process that is already underway, or start a new one.
loadComponentAndNotify(componentName, callback);
}
},
clearCachedDefinition: function(componentName) {
delete loadedDefinitionsCache[componentName];
},
_getFirstResultFromLoaders: getFirstResultFromLoaders
};
function getObjectOwnProperty(obj, propName) {
return obj.hasOwnProperty(propName) ? obj[propName] : undefined;
}
function loadComponentAndNotify(componentName, callback) {
var subscribable = getObjectOwnProperty(loadingSubscribablesCache, componentName),
completedAsync;
if (!subscribable) {
// It's not started loading yet. Start loading, and when it's done, move it to loadedDefinitionsCache.
subscribable = loadingSubscribablesCache[componentName] = new ko.subscribable();
beginLoadingComponent(componentName, function(definition) {
loadedDefinitionsCache[componentName] = definition;
delete loadingSubscribablesCache[componentName];
// For API consistency, all loads complete asynchronously. However we want to avoid
// adding an extra setTimeout if it's unnecessary (i.e., the completion is already
// async) since setTimeout(..., 0) still takes about 16ms or more on most browsers.
if (completedAsync) {
subscribable['notifySubscribers'](definition);
} else {
setTimeout(function() {
subscribable['notifySubscribers'](definition);
}, 0);
}
});
completedAsync = true;
}
subscribable.subscribe(callback);
}
function beginLoadingComponent(componentName, callback) {
getFirstResultFromLoaders('getConfig', [componentName], function(config) {
if (config) {
// We have a config, so now load its definition
getFirstResultFromLoaders('loadComponent', [componentName, config], function(definition) {
callback(definition);
});
} else {
// The component has no config - it's unknown to all the loaders.
// Note that this is not an error (e.g., a module loading error) - that would abort the
// process and this callback would not run. For this callback to run, all loaders must
// have confirmed they don't know about this component.
callback(null);
}
});
}
function getFirstResultFromLoaders(methodName, argsExceptCallback, callback, candidateLoaders) {
// On the first call in the stack, start with the full set of loaders
if (!candidateLoaders) {
candidateLoaders = ko.components['loaders'].slice(0); // Use a copy, because we'll be mutating this array
}
// Try the next candidate
var currentCandidateLoader = candidateLoaders.shift();
if (currentCandidateLoader) {
var methodInstance = currentCandidateLoader[methodName];
if (methodInstance) {
var wasAborted = false,
synchronousReturnValue = methodInstance.apply(currentCandidateLoader, argsExceptCallback.concat(function(result) {
if (wasAborted) {
callback(null);
} else if (result !== null) {
// This candidate returned a value. Use it.
callback(result);
} else {
// Try the next candidate
getFirstResultFromLoaders(methodName, argsExceptCallback, callback, candidateLoaders);
}
}));
// Currently, loaders may not return anything synchronously. This leaves open the possibility
// that we'll extend the API to support synchronous return values in the future. It won't be
// a breaking change, because currently no loader is allowed to return anything except undefined.
if (synchronousReturnValue !== undefined) {
wasAborted = true;
// Method to suppress exceptions will remain undocumented. This is only to keep
// KO's specs running tidily, since we can observe the loading got aborted without
// having exceptions cluttering up the console too.
if (!currentCandidateLoader['suppressLoaderExceptions']) {
throw new Error('Component loaders must supply values by invoking the callback, not by returning values synchronously.');
}
}
} else {
// This candidate doesn't have the relevant handler. Synchronously move on to the next one.
getFirstResultFromLoaders(methodName, argsExceptCallback, callback, candidateLoaders);
}
} else {
// No candidates returned a value
callback(null);
}
}
// Reference the loaders via string name so it's possible for developers
// to replace the whole array by assigning to ko.components.loaders
ko.components['loaders'] = [];
ko.exportSymbol('components', ko.components);
ko.exportSymbol('components.get', ko.components.get);
ko.exportSymbol('components.clearCachedDefinition', ko.components.clearCachedDefinition);
})();
(function(undefined) {
// The default loader is responsible for two things:
// 1. Maintaining the default in-memory registry of component configuration objects
// (i.e., the thing you're writing to when you call ko.components.register(someName, ...))
// 2. Answering requests for components by fetching configuration objects
// from that default in-memory registry and resolving them into standard
// component definition objects (of the form { createViewModel: ..., template: ... })
// Custom loaders may override either of these facilities, i.e.,
// 1. To supply configuration objects from some other source (e.g., conventions)
// 2. Or, to resolve configuration objects by loading viewmodels/templates via arbitrary logic.
var defaultConfigRegistry = {};
ko.components.register = function(componentName, config) {
if (!config) {
throw new Error('Invalid configuration for ' + componentName);
}
if (ko.components.isRegistered(componentName)) {
throw new Error('Component ' + componentName + ' is already registered');
}
defaultConfigRegistry[componentName] = config;
}
ko.components.isRegistered = function(componentName) {
return componentName in defaultConfigRegistry;
}
ko.components.unregister = function(componentName) {
delete defaultConfigRegistry[componentName];
ko.components.clearCachedDefinition(componentName);
}
ko.components.defaultLoader = {
'getConfig': function(componentName, callback) {
var result = defaultConfigRegistry.hasOwnProperty(componentName)
? defaultConfigRegistry[componentName]
: null;
callback(result);
},
'loadComponent': function(componentName, config, callback) {
var errorCallback = makeErrorCallback(componentName);
possiblyGetConfigFromAmd(errorCallback, config, function(loadedConfig) {
resolveConfig(componentName, errorCallback, loadedConfig, callback);
});
},
'loadTemplate': function(componentName, templateConfig, callback) {
resolveTemplate(makeErrorCallback(componentName), templateConfig, callback);
},
'loadViewModel': function(componentName, viewModelConfig, callback) {
resolveViewModel(makeErrorCallback(componentName), viewModelConfig, callback);
}
};
var createViewModelKey = 'createViewModel';
// Takes a config object of the form { template: ..., viewModel: ... }, and asynchronously convert it
// into the standard component definition format:
// { template: <ArrayOfDomNodes>, createViewModel: function(params, componentInfo) { ... } }.
// Since both template and viewModel may need to be resolved asynchronously, both tasks are performed
// in parallel, and the results joined when both are ready. We don't depend on any promises infrastructure,
// so this is implemented manually below.
function resolveConfig(componentName, errorCallback, config, callback) {
var result = {},
makeCallBackWhenZero = 2,
tryIssueCallback = function() {
if (--makeCallBackWhenZero === 0) {
callback(result);
}
},
templateConfig = config['template'],
viewModelConfig = config['viewModel'];
if (templateConfig) {
possiblyGetConfigFromAmd(errorCallback, templateConfig, function(loadedConfig) {
ko.components._getFirstResultFromLoaders('loadTemplate', [componentName, loadedConfig], function(resolvedTemplate) {
result['template'] = resolvedTemplate;
tryIssueCallback();
});
});
} else {
tryIssueCallback();
}
if (viewModelConfig) {
possiblyGetConfigFromAmd(errorCallback, viewModelConfig, function(loadedConfig) {
ko.components._getFirstResultFromLoaders('loadViewModel', [componentName, loadedConfig], function(resolvedViewModel) {
result[createViewModelKey] = resolvedViewModel;
tryIssueCallback();
});
});
} else {
tryIssueCallback();
}
}
function resolveTemplate(errorCallback, templateConfig, callback) {
if (typeof templateConfig === 'string') {
// Markup - parse it
callback(ko.utils.parseHtmlFragment(templateConfig));
} else if (templateConfig instanceof Array) {
// Assume already an array of DOM nodes - pass through unchanged
callback(templateConfig);
} else if (isDocumentFragment(templateConfig)) {
// Document fragment - use its child nodes
callback(ko.utils.makeArray(templateConfig.childNodes));
} else if (templateConfig['element']) {
var element = templateConfig['element'];
if (isDomElement(element)) {
// Element instance - copy its child nodes
callback(ko.utils.cloneNodes(element.childNodes));
} else if (typeof element === 'string') {
// Element ID - find it, then copy its child nodes
var elemInstance = document.getElementById(element);
if (elemInstance) {
callback(ko.utils.cloneNodes(elemInstance.childNodes));
} else {
errorCallback('Cannot find element with ID ' + element);
}
} else {
errorCallback('Unknown element type: ' + element);
}
} else {
errorCallback('Unknown template value: ' + templateConfig);
}
}
function resolveViewModel(errorCallback, viewModelConfig, callback) {
if (typeof viewModelConfig === 'function') {
// Constructor - convert to standard factory function format
// By design, this does *not* supply componentInfo to the constructor, as the intent is that
// componentInfo contains non-viewmodel data (e.g., the component's element) that should only
// be used in factory functions, not viewmodel constructors.
callback(function (params /*, componentInfo */) {
return new viewModelConfig(params);
});
} else if (typeof viewModelConfig[createViewModelKey] === 'function') {
// Already a factory function - use it as-is
callback(viewModelConfig[createViewModelKey]);
} else if ('instance' in viewModelConfig) {
// Fixed object instance - promote to createViewModel format for API consistency
var fixedInstance = viewModelConfig['instance'];
callback(function (params, componentInfo) {
return fixedInstance;
});
} else if ('viewModel' in viewModelConfig) {
// Resolved AMD module whose value is of the form { viewModel: ... }
resolveViewModel(errorCallback, viewModelConfig['viewModel'], callback);
} else {
errorCallback('Unknown viewModel value: ' + viewModelConfig);
}
}
function isDomElement(obj) {
if (window['HTMLElement']) {
return obj instanceof HTMLElement;
} else {
return obj && obj.tagName && obj.nodeType === 1;
}
}
function isDocumentFragment(obj) {
if (window['DocumentFragment']) {
return obj instanceof DocumentFragment;
} else {
return obj && obj.nodeType === 11;
}
}
function possiblyGetConfigFromAmd(errorCallback, config, callback) {
if (typeof config['require'] === 'string') {
// The config is the value of an AMD module
if (require || window['require']) {
(require || window['require'])([config['require']], callback);
} else {
errorCallback('Uses require, but no AMD loader is present');
}
} else {
callback(config);
}
}
function makeErrorCallback(componentName) {
return function (message) {
throw new Error('Component \'' + componentName + '\': ' + message);
};
}
ko.exportSymbol('components.register', ko.components.register);
ko.exportSymbol('components.isRegistered', ko.components.isRegistered);
ko.exportSymbol('components.unregister', ko.components.unregister);
// Expose the default loader so that developers can directly ask it for configuration
// or to resolve configuration
ko.exportSymbol('components.defaultLoader', ko.components.defaultLoader);
// By default, the default loader is the only registered component loader
ko.components['loaders'].push(ko.components.defaultLoader);
})();
(function() { (function() {
var defaultBindingAttributeName = "data-bind"; var defaultBindingAttributeName = "data-bind";
...@@ -2251,10 +2797,11 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider); ...@@ -2251,10 +2797,11 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
// any child contexts, must be updated when the view model is changed. // any child contexts, must be updated when the view model is changed.
function updateContext() { function updateContext() {
// Most of the time, the context will directly get a view model object, but if a function is given, // Most of the time, the context will directly get a view model object, but if a function is given,
// we call the function to retrieve the view model. If the function accesses any obsevables (or is // we call the function to retrieve the view model. If the function accesses any obsevables or returns
// itself an observable), the dependency is tracked, and those observables can later cause the binding // an observable, the dependency is tracked, and those observables can later cause the binding
// context to be updated. // context to be updated.
var dataItem = isFunc ? dataItemOrAccessor() : dataItemOrAccessor; var dataItemOrObservable = isFunc ? dataItemOrAccessor() : dataItemOrAccessor,
dataItem = ko.utils.unwrapObservable(dataItemOrObservable);
if (parentContext) { if (parentContext) {
// When a "parent" context is given, register a dependency on the parent context. Thus whenever the // When a "parent" context is given, register a dependency on the parent context. Thus whenever the
...@@ -2279,7 +2826,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider); ...@@ -2279,7 +2826,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
// See https://github.com/SteveSanderson/knockout/issues/490 // See https://github.com/SteveSanderson/knockout/issues/490
self['ko'] = ko; self['ko'] = ko;
} }
self['$rawData'] = dataItemOrAccessor; self['$rawData'] = dataItemOrObservable;
self['$data'] = dataItem; self['$data'] = dataItem;
if (dataItemAlias) if (dataItemAlias)
self[dataItemAlias] = dataItem; self[dataItemAlias] = dataItem;
...@@ -2297,7 +2844,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider); ...@@ -2297,7 +2844,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
} }
var self = this, var self = this,
isFunc = typeof(dataItemOrAccessor) == "function", isFunc = typeof(dataItemOrAccessor) == "function" && !ko.isObservable(dataItemOrAccessor),
nodes, nodes,
subscribable = ko.dependentObservable(updateContext, null, { disposeWhen: disposeWhen, disposeWhenNodeIsRemoved: true }); subscribable = ko.dependentObservable(updateContext, null, { disposeWhen: disposeWhen, disposeWhenNodeIsRemoved: true });
...@@ -2352,7 +2899,12 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider); ...@@ -2352,7 +2899,12 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
// Similarly to "child" contexts, provide a function here to make sure that the correct values are set // Similarly to "child" contexts, provide a function here to make sure that the correct values are set
// when an observable view model is updated. // when an observable view model is updated.
ko.bindingContext.prototype['extend'] = function(properties) { ko.bindingContext.prototype['extend'] = function(properties) {
return new ko.bindingContext(this['$rawData'], this, null, function(self) { // 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.utils.extend(self, typeof(properties) == "function" ? properties() : properties);
}); });
}; };
...@@ -2480,7 +3032,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider); ...@@ -2480,7 +3032,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
} }
} }
}); });
cyclicDependencyStack.pop(); cyclicDependencyStack.length--;
} }
// Next add the current binding // Next add the current binding
result.push({ key: bindingKey, handler: binding }); result.push({ key: bindingKey, handler: binding });
...@@ -2516,26 +3068,21 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider); ...@@ -2516,26 +3068,21 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
var provider = ko.bindingProvider['instance'], var provider = ko.bindingProvider['instance'],
getBindings = provider['getBindingAccessors'] || getBindingsAndMakeAccessors; getBindings = provider['getBindingAccessors'] || getBindingsAndMakeAccessors;
if (sourceBindings || bindingContext._subscribable) { // Get the binding from the provider within a computed observable so that we can update the bindings whenever
// When an obsevable view model is used, the binding context will expose an observable _subscribable value. // the binding context is updated or if the binding provider accesses observables.
// Get the binding from the provider within a computed observable so that we can update the bindings whenever var bindingsUpdater = ko.dependentObservable(
// the binding context is updated. function() {
var bindingsUpdater = ko.dependentObservable( bindings = sourceBindings ? sourceBindings(bindingContext, node) : getBindings.call(provider, node, bindingContext);
function() { // Register a dependency on the binding context to support obsevable view models.
bindings = sourceBindings ? sourceBindings(bindingContext, node) : getBindings.call(provider, node, bindingContext); if (bindings && bindingContext._subscribable)
// Register a dependency on the binding context bindingContext._subscribable();
if (bindings && bindingContext._subscribable) return bindings;
bindingContext._subscribable(); },
return bindings; null, { disposeWhenNodeIsRemoved: node }
}, );
null, { disposeWhenNodeIsRemoved: node }
); if (!bindings || !bindingsUpdater.isActive())
bindingsUpdater = null;
if (!bindings || !bindingsUpdater.isActive())
bindingsUpdater = null;
} else {
bindings = ko.dependencyDetection.ignore(getBindings, provider, [node, bindingContext]);
}
} }
var bindingHandlerThatControlsDescendantBindings; var bindingHandlerThatControlsDescendantBindings;
...@@ -2650,6 +3197,11 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider); ...@@ -2650,6 +3197,11 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
}; };
ko.applyBindings = function (viewModelOrBindingContext, rootNode) { ko.applyBindings = function (viewModelOrBindingContext, rootNode) {
// If jQuery is loaded after Knockout, we won't initially have access to it. So save it here.
if (!jQuery && window['jQuery']) {
jQuery = window['jQuery'];
}
if (rootNode && (rootNode.nodeType !== 1) && (rootNode.nodeType !== 8)) if (rootNode && (rootNode.nodeType !== 1) && (rootNode.nodeType !== 8))
throw new Error("ko.applyBindings: first parameter should be your view model; second parameter should be a DOM node"); throw new Error("ko.applyBindings: first parameter should be your view model; second parameter should be a DOM node");
rootNode = rootNode || window.document.body; // Make "rootNode" parameter optional rootNode = rootNode || window.document.body; // Make "rootNode" parameter optional
...@@ -2683,6 +3235,87 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider); ...@@ -2683,6 +3235,87 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
ko.exportSymbol('contextFor', ko.contextFor); ko.exportSymbol('contextFor', ko.contextFor);
ko.exportSymbol('dataFor', ko.dataFor); ko.exportSymbol('dataFor', ko.dataFor);
})(); })();
(function(undefined) {
var componentLoadingOperationUniqueId = 0;
ko.bindingHandlers['component'] = {
'init': function(element, valueAccessor, ignored1, ignored2, bindingContext) {
var currentViewModel,
currentLoadingOperationId,
disposeAssociatedComponentViewModel = function () {
var currentViewModelDispose = currentViewModel && currentViewModel['dispose'];
if (typeof currentViewModelDispose === 'function') {
currentViewModelDispose.call(currentViewModel);
}
// Any in-flight loading operation is no longer relevant, so make sure we ignore its completion
currentLoadingOperationId = null;
};
ko.utils.domNodeDisposal.addDisposeCallback(element, disposeAssociatedComponentViewModel);
ko.computed(function () {
var value = ko.utils.unwrapObservable(valueAccessor()),
componentName, componentParams;
if (typeof value === 'string') {
componentName = value;
} else {
componentName = ko.utils.unwrapObservable(value['name']);
componentParams = ko.utils.unwrapObservable(value['params']);
}
if (!componentName) {
throw new Error('No component name specified');
}
var loadingOperationId = currentLoadingOperationId = ++componentLoadingOperationUniqueId;
ko.components.get(componentName, function(componentDefinition) {
// If this is not the current load operation for this element, ignore it.
if (currentLoadingOperationId !== loadingOperationId) {
return;
}
// Clean up previous state
disposeAssociatedComponentViewModel();
// Instantiate and bind new component. Implicitly this cleans any old DOM nodes.
if (!componentDefinition) {
throw new Error('Unknown component \'' + componentName + '\'');
}
cloneTemplateIntoElement(componentName, componentDefinition, element);
var componentViewModel = createViewModel(componentDefinition, element, componentParams),
childBindingContext = bindingContext['createChildContext'](componentViewModel);
currentViewModel = componentViewModel;
ko.applyBindingsToDescendants(childBindingContext, element);
});
}, null, { disposeWhenNodeIsRemoved: element });
return { 'controlsDescendantBindings': true };
}
};
ko.virtualElements.allowedBindings['component'] = true;
function cloneTemplateIntoElement(componentName, componentDefinition, element) {
var template = componentDefinition['template'];
if (!template) {
throw new Error('Component \'' + componentName + '\' has no template');
}
var clonedNodesArray = ko.utils.cloneNodes(template);
ko.virtualElements.setDomNodeChildren(element, clonedNodesArray);
}
function createViewModel(componentDefinition, element, componentParams) {
var componentViewModelFactory = componentDefinition['createViewModel'];
return componentViewModelFactory
? componentViewModelFactory.call(componentDefinition, componentParams, { element: element })
: componentParams; // Template-only component
}
})();
var attrHtmlToJavascriptMap = { 'class': 'className', 'for': 'htmlFor' }; var attrHtmlToJavascriptMap = { 'class': 'className', 'for': 'htmlFor' };
ko.bindingHandlers['attr'] = { ko.bindingHandlers['attr'] = {
'update': function(element, valueAccessor, allBindings) { 'update': function(element, valueAccessor, allBindings) {
...@@ -2739,7 +3372,7 @@ ko.bindingHandlers['checked'] = { ...@@ -2739,7 +3372,7 @@ ko.bindingHandlers['checked'] = {
elemValue = useCheckedValue ? checkedValue() : isChecked; elemValue = useCheckedValue ? checkedValue() : isChecked;
// When we're first setting up this computed, don't change any model state. // When we're first setting up this computed, don't change any model state.
if (!shouldSet) { if (ko.computedContext.isInitial()) {
return; return;
} }
...@@ -2798,8 +3431,7 @@ ko.bindingHandlers['checked'] = { ...@@ -2798,8 +3431,7 @@ ko.bindingHandlers['checked'] = {
var isValueArray = isCheckbox && (ko.utils.unwrapObservable(valueAccessor()) instanceof Array), var isValueArray = isCheckbox && (ko.utils.unwrapObservable(valueAccessor()) instanceof Array),
oldElemValue = isValueArray ? checkedValue() : undefined, oldElemValue = isValueArray ? checkedValue() : undefined,
useCheckedValue = isRadio || isValueArray, useCheckedValue = isRadio || isValueArray;
shouldSet = false;
// IE 6 won't allow radio buttons to be selected unless they have a name // IE 6 won't allow radio buttons to be selected unless they have a name
if (isRadio && !element.name) if (isRadio && !element.name)
...@@ -2808,13 +3440,11 @@ ko.bindingHandlers['checked'] = { ...@@ -2808,13 +3440,11 @@ ko.bindingHandlers['checked'] = {
// Set up two computeds to update the binding: // Set up two computeds to update the binding:
// The first responds to changes in the checkedValue value and to element clicks // The first responds to changes in the checkedValue value and to element clicks
ko.dependentObservable(updateModel, null, { disposeWhenNodeIsRemoved: element }); ko.computed(updateModel, null, { disposeWhenNodeIsRemoved: element });
ko.utils.registerEventHandler(element, "click", updateModel); ko.utils.registerEventHandler(element, "click", updateModel);
// The second responds to changes in the model value (the one associated with the checked binding) // The second responds to changes in the model value (the one associated with the checked binding)
ko.dependentObservable(updateView, null, { disposeWhenNodeIsRemoved: element }); ko.computed(updateView, null, { disposeWhenNodeIsRemoved: element });
shouldSet = true;
} }
}; };
ko.expressionRewriting.twoWayBindings['checked'] = true; ko.expressionRewriting.twoWayBindings['checked'] = true;
...@@ -3007,37 +3637,37 @@ ko.bindingHandlers['html'] = { ...@@ -3007,37 +3637,37 @@ ko.bindingHandlers['html'] = {
ko.utils.setHtml(element, valueAccessor()); ko.utils.setHtml(element, valueAccessor());
} }
}; };
var withIfDomDataKey = ko.utils.domData.nextKey();
// Makes a binding like with or if // Makes a binding like with or if
function makeWithIfBinding(bindingKey, isWith, isNot, makeContextCallback) { function makeWithIfBinding(bindingKey, isWith, isNot, makeContextCallback) {
ko.bindingHandlers[bindingKey] = { ko.bindingHandlers[bindingKey] = {
'init': function(element) { 'init': function(element, valueAccessor, allBindings, viewModel, bindingContext) {
ko.utils.domData.set(element, withIfDomDataKey, {}); var didDisplayOnLastUpdate,
return { 'controlsDescendantBindings': true }; savedNodes;
}, ko.computed(function() {
'update': function(element, valueAccessor, allBindings, viewModel, bindingContext) { var dataValue = ko.utils.unwrapObservable(valueAccessor()),
var withIfData = ko.utils.domData.get(element, withIfDomDataKey), shouldDisplay = !isNot !== !dataValue, // equivalent to isNot ? !dataValue : !!dataValue
dataValue = ko.utils.unwrapObservable(valueAccessor()), isFirstRender = !savedNodes,
shouldDisplay = !isNot !== !dataValue, // equivalent to isNot ? !dataValue : !!dataValue needsRefresh = isFirstRender || isWith || (shouldDisplay !== didDisplayOnLastUpdate);
isFirstRender = !withIfData.savedNodes,
needsRefresh = isFirstRender || isWith || (shouldDisplay !== withIfData.didDisplayOnLastUpdate); if (needsRefresh) {
// Save a copy of the inner nodes on the initial update, but only if we have dependencies.
if (needsRefresh) { if (isFirstRender && ko.computedContext.getDependenciesCount()) {
if (isFirstRender) { savedNodes = ko.utils.cloneNodes(ko.virtualElements.childNodes(element), true /* shouldCleanNodes */);
withIfData.savedNodes = ko.utils.cloneNodes(ko.virtualElements.childNodes(element), true /* shouldCleanNodes */); }
}
if (shouldDisplay) { if (shouldDisplay) {
if (!isFirstRender) { if (!isFirstRender) {
ko.virtualElements.setDomNodeChildren(element, ko.utils.cloneNodes(withIfData.savedNodes)); ko.virtualElements.setDomNodeChildren(element, ko.utils.cloneNodes(savedNodes));
}
ko.applyBindingsToDescendants(makeContextCallback ? makeContextCallback(bindingContext, dataValue) : bindingContext, element);
} else {
ko.virtualElements.emptyNode(element);
} }
ko.applyBindingsToDescendants(makeContextCallback ? makeContextCallback(bindingContext, dataValue) : bindingContext, element);
} else {
ko.virtualElements.emptyNode(element);
}
withIfData.didDisplayOnLastUpdate = shouldDisplay; didDisplayOnLastUpdate = shouldDisplay;
} }
}, null, { disposeWhenNodeIsRemoved: element });
return { 'controlsDescendantBindings': true };
} }
}; };
ko.expressionRewriting.bindingRewriteValidators[bindingKey] = false; // Can't rewrite control flow bindings ko.expressionRewriting.bindingRewriteValidators[bindingKey] = false; // Can't rewrite control flow bindings
...@@ -3052,6 +3682,7 @@ makeWithIfBinding('with', true /* isWith */, false /* isNot */, ...@@ -3052,6 +3682,7 @@ makeWithIfBinding('with', true /* isWith */, false /* isNot */,
return bindingContext['createChildContext'](dataValue); return bindingContext['createChildContext'](dataValue);
} }
); );
var captionPlaceholder = {};
ko.bindingHandlers['options'] = { ko.bindingHandlers['options'] = {
'init': function(element) { 'init': function(element) {
if (ko.utils.tagNameLower(element) !== "select") if (ko.utils.tagNameLower(element) !== "select")
...@@ -3072,12 +3703,13 @@ ko.bindingHandlers['options'] = { ...@@ -3072,12 +3703,13 @@ ko.bindingHandlers['options'] = {
var selectWasPreviouslyEmpty = element.length == 0; var selectWasPreviouslyEmpty = element.length == 0;
var previousScrollTop = (!selectWasPreviouslyEmpty && element.multiple) ? element.scrollTop : null; var previousScrollTop = (!selectWasPreviouslyEmpty && element.multiple) ? element.scrollTop : null;
var unwrappedArray = ko.utils.unwrapObservable(valueAccessor()); var unwrappedArray = ko.utils.unwrapObservable(valueAccessor());
var includeDestroyed = allBindings.get('optionsIncludeDestroyed'); var includeDestroyed = allBindings.get('optionsIncludeDestroyed');
var captionPlaceholder = {}; var arrayToDomNodeChildrenOptions = {};
var captionValue; var captionValue;
var filteredArray;
var previousSelectedValues; var previousSelectedValues;
if (element.multiple) { if (element.multiple) {
previousSelectedValues = ko.utils.arrayMap(selectedOptions(), ko.selectExtensions.readValue); previousSelectedValues = ko.utils.arrayMap(selectedOptions(), ko.selectExtensions.readValue);
} else { } else {
...@@ -3089,7 +3721,7 @@ ko.bindingHandlers['options'] = { ...@@ -3089,7 +3721,7 @@ ko.bindingHandlers['options'] = {
unwrappedArray = [unwrappedArray]; unwrappedArray = [unwrappedArray];
// Filter out any entries marked as destroyed // Filter out any entries marked as destroyed
var filteredArray = ko.utils.arrayFilter(unwrappedArray, function(item) { filteredArray = ko.utils.arrayFilter(unwrappedArray, function(item) {
return includeDestroyed || item === undefined || item === null || !ko.utils.unwrapObservable(item['_destroy']); return includeDestroyed || item === undefined || item === null || !ko.utils.unwrapObservable(item['_destroy']);
}); });
...@@ -3103,7 +3735,6 @@ ko.bindingHandlers['options'] = { ...@@ -3103,7 +3735,6 @@ ko.bindingHandlers['options'] = {
} }
} else { } else {
// If a falsy value is provided (e.g. null), we'll simply empty the select element // If a falsy value is provided (e.g. null), we'll simply empty the select element
unwrappedArray = [];
} }
function applyToObject(object, predicate, defaultValue) { function applyToObject(object, predicate, defaultValue) {
...@@ -3126,7 +3757,7 @@ ko.bindingHandlers['options'] = { ...@@ -3126,7 +3757,7 @@ ko.bindingHandlers['options'] = {
previousSelectedValues = oldOptions[0].selected ? [ ko.selectExtensions.readValue(oldOptions[0]) ] : []; previousSelectedValues = oldOptions[0].selected ? [ ko.selectExtensions.readValue(oldOptions[0]) ] : [];
itemUpdate = true; itemUpdate = true;
} }
var option = document.createElement("option"); var option = element.ownerDocument.createElement("option");
if (arrayEntry === captionPlaceholder) { if (arrayEntry === captionPlaceholder) {
ko.utils.setTextContent(option, allBindings.get('optionsCaption')); ko.utils.setTextContent(option, allBindings.get('optionsCaption'));
ko.selectExtensions.writeValue(option, undefined); ko.selectExtensions.writeValue(option, undefined);
...@@ -3142,6 +3773,13 @@ ko.bindingHandlers['options'] = { ...@@ -3142,6 +3773,13 @@ ko.bindingHandlers['options'] = {
return [option]; return [option];
} }
// By using a beforeRemove callback, we delay the removal until after new items are added. This fixes a selection
// problem in IE<=8 and Firefox. See https://github.com/knockout/knockout/issues/1208
arrayToDomNodeChildrenOptions['beforeRemove'] =
function (option) {
element.removeChild(option);
};
function setSelectionCallback(arrayEntry, newOptions) { function setSelectionCallback(arrayEntry, newOptions) {
// IE6 doesn't like us to assign selection to OPTION nodes before they're added to the document. // IE6 doesn't like us to assign selection to OPTION nodes before they're added to the document.
// That's why we first added them without selection. Now it's time to set the selection. // That's why we first added them without selection. Now it's time to set the selection.
...@@ -3163,27 +3801,35 @@ ko.bindingHandlers['options'] = { ...@@ -3163,27 +3801,35 @@ ko.bindingHandlers['options'] = {
} }
} }
ko.utils.setDomNodeChildrenFromArrayMapping(element, filteredArray, optionForArrayItem, null, callback); ko.utils.setDomNodeChildrenFromArrayMapping(element, filteredArray, optionForArrayItem, arrayToDomNodeChildrenOptions, callback);
// Determine if the selection has changed as a result of updating the options list ko.dependencyDetection.ignore(function () {
var selectionChanged; if (allBindings.get('valueAllowUnset') && allBindings['has']('value')) {
if (element.multiple) { // The model value is authoritative, so make sure its value is the one selected
// For a multiple-select box, compare the new selection count to the previous one ko.selectExtensions.writeValue(element, ko.utils.unwrapObservable(allBindings.get('value')), true /* allowUnset */);
// But if nothing was selected before, the selection can't have changed } else {
selectionChanged = previousSelectedValues.length && selectedOptions().length < previousSelectedValues.length; // Determine if the selection has changed as a result of updating the options list
} else { var selectionChanged;
// For a single-select box, compare the current value to the previous value if (element.multiple) {
// But if nothing was selected before or nothing is selected now, just look for a change in selection // For a multiple-select box, compare the new selection count to the previous one
selectionChanged = (previousSelectedValues.length && element.selectedIndex >= 0) // But if nothing was selected before, the selection can't have changed
? (ko.selectExtensions.readValue(element.options[element.selectedIndex]) !== previousSelectedValues[0]) selectionChanged = previousSelectedValues.length && selectedOptions().length < previousSelectedValues.length;
: (previousSelectedValues.length || element.selectedIndex >= 0); } else {
} // For a single-select box, compare the current value to the previous value
// But if nothing was selected before or nothing is selected now, just look for a change in selection
selectionChanged = (previousSelectedValues.length && element.selectedIndex >= 0)
? (ko.selectExtensions.readValue(element.options[element.selectedIndex]) !== previousSelectedValues[0])
: (previousSelectedValues.length || element.selectedIndex >= 0);
}
// Ensure consistency between model value and selected option. // Ensure consistency between model value and selected option.
// If the dropdown was changed so that selection is no longer the same, // If the dropdown was changed so that selection is no longer the same,
// notify the value or selectedOptions binding. // notify the value or selectedOptions binding.
if (selectionChanged) if (selectionChanged) {
ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, "change"]); ko.utils.triggerEvent(element, "change");
}
}
});
// Workaround for IE bug // Workaround for IE bug
ko.utils.ensureSelectElementIsRenderedCorrectly(element); ko.utils.ensureSelectElementIsRenderedCorrectly(element);
...@@ -3294,6 +3940,7 @@ ko.bindingHandlers['value'] = { ...@@ -3294,6 +3940,7 @@ ko.bindingHandlers['value'] = {
&& element.autocomplete != "off" && (!element.form || element.form.autocomplete != "off"); && element.autocomplete != "off" && (!element.form || element.form.autocomplete != "off");
if (ieAutoCompleteHackNeeded && ko.utils.arrayIndexOf(eventsToCatch, "propertychange") == -1) { if (ieAutoCompleteHackNeeded && ko.utils.arrayIndexOf(eventsToCatch, "propertychange") == -1) {
ko.utils.registerEventHandler(element, "propertychange", function () { propertyChangedFired = true }); ko.utils.registerEventHandler(element, "propertychange", function () { propertyChangedFired = true });
ko.utils.registerEventHandler(element, "focus", function () { propertyChangedFired = false });
ko.utils.registerEventHandler(element, "blur", function() { ko.utils.registerEventHandler(element, "blur", function() {
if (propertyChangedFired) { if (propertyChangedFired) {
valueUpdateHandler(); valueUpdateHandler();
...@@ -3313,18 +3960,20 @@ ko.bindingHandlers['value'] = { ...@@ -3313,18 +3960,20 @@ ko.bindingHandlers['value'] = {
ko.utils.registerEventHandler(element, eventName, handler); ko.utils.registerEventHandler(element, eventName, handler);
}); });
}, },
'update': function (element, valueAccessor) { 'update': function (element, valueAccessor, allBindings) {
var valueIsSelectOption = ko.utils.tagNameLower(element) === "select";
var newValue = ko.utils.unwrapObservable(valueAccessor()); var newValue = ko.utils.unwrapObservable(valueAccessor());
var elementValue = ko.selectExtensions.readValue(element); var elementValue = ko.selectExtensions.readValue(element);
var valueHasChanged = (newValue !== elementValue); var valueHasChanged = (newValue !== elementValue);
if (valueHasChanged) { if (valueHasChanged) {
var applyValueAction = function () { ko.selectExtensions.writeValue(element, newValue); }; if (ko.utils.tagNameLower(element) === "select") {
applyValueAction(); var allowUnset = allBindings.get('valueAllowUnset');
var applyValueAction = function () {
ko.selectExtensions.writeValue(element, newValue, allowUnset);
};
applyValueAction();
if (valueIsSelectOption) { if (!allowUnset && newValue !== ko.selectExtensions.readValue(element)) {
if (newValue !== ko.selectExtensions.readValue(element)) {
// If you try to set a model value that can't be represented in an already-populated dropdown, reject that change, // If you try to set a model value that can't be represented in an already-populated dropdown, reject that change,
// because you're not allowed to have a model value that disagrees with a visible UI selection. // because you're not allowed to have a model value that disagrees with a visible UI selection.
ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, "change"]); ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, "change"]);
...@@ -3334,6 +3983,8 @@ ko.bindingHandlers['value'] = { ...@@ -3334,6 +3983,8 @@ ko.bindingHandlers['value'] = {
// to apply the value as well. // to apply the value as well.
setTimeout(applyValueAction, 0); setTimeout(applyValueAction, 0);
} }
} else {
ko.selectExtensions.writeValue(element, newValue);
} }
} }
} }
...@@ -3718,7 +4369,8 @@ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextS ...@@ -3718,7 +4369,8 @@ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextS
: new ko.bindingContext(ko.utils.unwrapObservable(dataOrBindingContext)); : new ko.bindingContext(ko.utils.unwrapObservable(dataOrBindingContext));
// Support selecting template as a function of the data being rendered // Support selecting template as a function of the data being rendered
var templateName = typeof(template) == 'function' ? template(bindingContext['$data'], bindingContext) : template; var templateName = ko.isObservable(template) ? template()
: typeof(template) == 'function' ? template(bindingContext['$data'], bindingContext) : template;
var renderedNodesArray = executeTemplate(targetNodeOrNodeArray, renderMode, templateName, bindingContext, options); var renderedNodesArray = executeTemplate(targetNodeOrNodeArray, renderMode, templateName, bindingContext, options);
if (renderMode == "replaceNode") { if (renderMode == "replaceNode") {
...@@ -3800,15 +4452,18 @@ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextS ...@@ -3800,15 +4452,18 @@ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextS
return { 'controlsDescendantBindings': true }; return { 'controlsDescendantBindings': true };
}, },
'update': function (element, valueAccessor, allBindings, viewModel, bindingContext) { 'update': function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var templateName = ko.utils.unwrapObservable(valueAccessor()), var value = valueAccessor(),
options = {},
shouldDisplay = true,
dataValue, dataValue,
templateComputed = null; options = ko.utils.unwrapObservable(value),
shouldDisplay = true,
templateComputed = null,
templateName;
if (typeof templateName != "string") { if (typeof options == "string") {
options = templateName; templateName = value;
templateName = ko.utils.unwrapObservable(options['name']); options = {};
} else {
templateName = options['name'];
// Support "if"/"ifnot" conditions // Support "if"/"ifnot" conditions
if ('if' in options) if ('if' in options)
...@@ -3855,6 +4510,24 @@ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextS ...@@ -3855,6 +4510,24 @@ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextS
ko.exportSymbol('setTemplateEngine', ko.setTemplateEngine); ko.exportSymbol('setTemplateEngine', ko.setTemplateEngine);
ko.exportSymbol('renderTemplate', ko.renderTemplate); ko.exportSymbol('renderTemplate', ko.renderTemplate);
// Go through the items that have been added and deleted and try to find matches between them.
ko.utils.findMovesInArrayComparison = function (left, right, limitFailedCompares) {
if (left.length && right.length) {
var failedCompares, l, r, leftItem, rightItem;
for (failedCompares = l = 0; (!limitFailedCompares || failedCompares < limitFailedCompares) && (leftItem = left[l]); ++l) {
for (r = 0; rightItem = right[r]; ++r) {
if (leftItem['value'] === rightItem['value']) {
leftItem['moved'] = rightItem['index'];
rightItem['moved'] = leftItem['index'];
right.splice(r, 1); // This item is marked as moved; so remove it from right list
failedCompares = r = 0; // Reset failed compares count because we're checking for consecutive failures
break;
}
}
failedCompares += r;
}
}
};
ko.utils.compareArrays = (function () { ko.utils.compareArrays = (function () {
var statusNotInOld = 'added', statusNotInNew = 'deleted'; var statusNotInOld = 'added', statusNotInNew = 'deleted';
...@@ -3928,25 +4601,10 @@ ko.utils.compareArrays = (function () { ...@@ -3928,25 +4601,10 @@ ko.utils.compareArrays = (function () {
} }
} }
if (notInSml.length && notInBig.length) { // Set a limit on the number of consecutive non-matching comparisons; having it a multiple of
// 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.
// smlIndexMax keeps the time complexity of this algorithm linear. ko.utils.findMovesInArrayComparison(notInSml, notInBig, smlIndexMax * 10);
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; (options['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(); return editScript.reverse();
} }
...@@ -3954,7 +4612,6 @@ ko.utils.compareArrays = (function () { ...@@ -3954,7 +4612,6 @@ ko.utils.compareArrays = (function () {
})(); })();
ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays); ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);
(function () { (function () {
// Objective: // Objective:
// * Given an input array, a container DOM node, and a function from array elements to arrays of DOM nodes, // * Given an input array, a container DOM node, and a function from array elements to arrays of DOM nodes,
...@@ -3981,7 +4638,7 @@ ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays); ...@@ -3981,7 +4638,7 @@ ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);
// Replace the contents of the mappedNodes array, thereby updating the record // Replace the contents of the mappedNodes array, thereby updating the record
// of which nodes would be deleted if valueToMap was itself later removed // of which nodes would be deleted if valueToMap was itself later removed
mappedNodes.splice(0, mappedNodes.length); mappedNodes.length = 0;
ko.utils.arrayPushAll(mappedNodes, newMappedNodes); ko.utils.arrayPushAll(mappedNodes, newMappedNodes);
}, null, { disposeWhenNodeIsRemoved: containerNode, disposeWhen: function() { return !ko.utils.anyDomNodeIsAttachedToDocument(mappedNodes); } }); }, null, { disposeWhenNodeIsRemoved: containerNode, disposeWhen: function() { return !ko.utils.anyDomNodeIsAttachedToDocument(mappedNodes); } });
return { mappedNodes : mappedNodes, dependentObservable : (dependentObservable.isActive() ? dependentObservable : undefined) }; return { mappedNodes : mappedNodes, dependentObservable : (dependentObservable.isActive() ? dependentObservable : undefined) };
...@@ -4144,7 +4801,7 @@ ko.exportSymbol('nativeTemplateEngine', ko.nativeTemplateEngine); ...@@ -4144,7 +4801,7 @@ ko.exportSymbol('nativeTemplateEngine', ko.nativeTemplateEngine);
// Note that as of Knockout 1.3, we only support jQuery.tmpl 1.0.0pre and later, // Note that as of Knockout 1.3, we only support jQuery.tmpl 1.0.0pre and later,
// which KO internally refers to as version "2", so older versions are no longer detected. // which KO internally refers to as version "2", so older versions are no longer detected.
var jQueryTmplVersion = this.jQueryTmplVersion = (function() { var jQueryTmplVersion = this.jQueryTmplVersion = (function() {
if ((typeof(jQuery) == "undefined") || !(jQuery['tmpl'])) if (!jQuery || !(jQuery['tmpl']))
return 0; return 0;
// Since it exposes no official version number, we use our own numbering system. To be updated as jquery-tmpl evolves. // Since it exposes no official version number, we use our own numbering system. To be updated as jquery-tmpl evolves.
try { try {
......
// //
// Generated on Sun Dec 16 2012 22:47:05 GMT-0500 (EST) by Nodejitsu, Inc (Using Codesurgeon). // Generated on Fri Dec 27 2013 12:02:11 GMT-0500 (EST) by Nodejitsu, Inc (Using Codesurgeon).
// Version 1.1.9 // Version 1.2.2
// //
(function (exports) { (function (exports) {
/* /*
* browser.js: Browser specific functionality for director. * browser.js: Browser specific functionality for director.
* *
...@@ -201,7 +200,7 @@ Router.prototype.init = function (r) { ...@@ -201,7 +200,7 @@ Router.prototype.init = function (r) {
this.handler = function(onChangeEvent) { this.handler = function(onChangeEvent) {
var newURL = onChangeEvent && onChangeEvent.newURL || window.location.hash; var newURL = onChangeEvent && onChangeEvent.newURL || window.location.hash;
var url = self.history === true ? self.getPath() : newURL.replace(/.*#/, ''); var url = self.history === true ? self.getPath() : newURL.replace(/.*#/, '');
self.dispatch('on', url); self.dispatch('on', url.charAt(0) === '/' ? url : '/' + url);
}; };
listener.init(this.handler, this.history); listener.init(this.handler, this.history);
...@@ -210,7 +209,7 @@ Router.prototype.init = function (r) { ...@@ -210,7 +209,7 @@ Router.prototype.init = function (r) {
if (dlocHashEmpty() && r) { if (dlocHashEmpty() && r) {
dloc.hash = r; dloc.hash = r;
} else if (!dlocHashEmpty()) { } else if (!dlocHashEmpty()) {
self.dispatch('on', dloc.hash.replace(/^#/, '')); self.dispatch('on', '/' + dloc.hash.replace(/^(#\/|#|\/)/, ''));
} }
} }
else { else {
...@@ -363,11 +362,16 @@ function regifyString(str, params) { ...@@ -363,11 +362,16 @@ function regifyString(str, params) {
out += str.substr(0, matches.index) + matches[0]; out += str.substr(0, matches.index) + matches[0];
} }
str = out += str.substr(last); str = out += str.substr(last);
var captures = str.match(/:([^\/]+)/ig), length; var captures = str.match(/:([^\/]+)/ig), capture, length;
if (captures) { if (captures) {
length = captures.length; length = captures.length;
for (var i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
str = str.replace(captures[i], paramifyString(captures[i], params)); capture = captures[i];
if (capture.slice(0, 2) === "::") {
str = capture.slice(1);
} else {
str = str.replace(capture, paramifyString(capture, params));
}
} }
} }
return str; return str;
...@@ -485,20 +489,22 @@ Router.prototype.dispatch = function(method, path, callback) { ...@@ -485,20 +489,22 @@ Router.prototype.dispatch = function(method, path, callback) {
Router.prototype.invoke = function(fns, thisArg, callback) { Router.prototype.invoke = function(fns, thisArg, callback) {
var self = this; var self = this;
var apply;
if (this.async) { if (this.async) {
_asyncEverySeries(fns, function apply(fn, next) { apply = function(fn, next) {
if (Array.isArray(fn)) { if (Array.isArray(fn)) {
return _asyncEverySeries(fn, apply, next); return _asyncEverySeries(fn, apply, next);
} else if (typeof fn == "function") { } else if (typeof fn == "function") {
fn.apply(thisArg, fns.captures.concat(next)); fn.apply(thisArg, fns.captures.concat(next));
} }
}, function() { };
_asyncEverySeries(fns, apply, function() {
if (callback) { if (callback) {
callback.apply(thisArg, arguments); callback.apply(thisArg, arguments);
} }
}); });
} else { } else {
_every(fns, function apply(fn) { apply = function(fn) {
if (Array.isArray(fn)) { if (Array.isArray(fn)) {
return _every(fn, apply); return _every(fn, apply);
} else if (typeof fn === "function") { } else if (typeof fn === "function") {
...@@ -506,7 +512,8 @@ Router.prototype.invoke = function(fns, thisArg, callback) { ...@@ -506,7 +512,8 @@ Router.prototype.invoke = function(fns, thisArg, callback) {
} else if (typeof fn === "string" && self.resource) { } else if (typeof fn === "string" && self.resource) {
self.resource[fn].apply(thisArg, fns.captures || []); self.resource[fn].apply(thisArg, fns.captures || []);
} }
}); };
_every(fns, apply);
} }
}; };
...@@ -686,7 +693,7 @@ Router.prototype.mount = function(routes, path) { ...@@ -686,7 +693,7 @@ Router.prototype.mount = function(routes, path) {
function insertOrMount(route, local) { function insertOrMount(route, local) {
var rename = route, parts = route.split(self.delimiter), routeType = typeof routes[route], isRoute = parts[0] === "" || !self._methods[parts[0]], event = isRoute ? "on" : rename; var rename = route, parts = route.split(self.delimiter), routeType = typeof routes[route], isRoute = parts[0] === "" || !self._methods[parts[0]], event = isRoute ? "on" : rename;
if (isRoute) { if (isRoute) {
rename = rename.slice((rename.match(new RegExp(self.delimiter)) || [ "" ])[0].length); rename = rename.slice((rename.match(new RegExp("^" + self.delimiter)) || [ "" ])[0].length);
parts.shift(); parts.shift();
} }
if (isRoute && routeType === "object" && !Array.isArray(routes[route])) { if (isRoute && routeType === "object" && !Array.isArray(routes[route])) {
......
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p> <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer> </footer>
<script src="bower_components/todomvc-common/base.js"></script> <script src="bower_components/todomvc-common/base.js"></script>
<script src="bower_components/knockout.js/knockout.debug.js"></script> <script src="bower_components/component-knockout-passy/knockout.js"></script>
<script src="bower_components/director/build/director.js"></script> <script src="bower_components/director/build/director.js"></script>
<script src="js/app.js"></script> <script src="js/app.js"></script>
</body> </body>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment