Commit c5e53bc8 authored by Pascal Hartig's avatar Pascal Hartig

Upgrade Knockout to v3.1.0

parent 0a50f32a
......@@ -3,7 +3,7 @@
"version": "0.0.0",
"dependencies": {
"todomvc-common": "~0.1.4",
"knockout.js": "~3.0.0",
"component-knockout-passy": "~3.1.0",
"director": "~1.2.0"
}
}
// Knockout JavaScript library v3.0.0
// (c) Steven Sanderson - http://knockoutjs.com/
// License: MIT (http://www.opensource.org/licenses/mit-license.php)
/*!
* Knockout JavaScript library v3.1.0
* (c) Steven Sanderson - http://knockoutjs.com/
* License: MIT (http://www.opensource.org/licenses/mit-license.php)
*/
(function(){
var DEBUG=true;
......@@ -17,15 +19,15 @@ var DEBUG=true;
if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') {
// [1] CommonJS/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']) {
// [2] AMD anonymous module
define(['exports'], factory);
define(['exports', 'require'], factory);
} else {
// [3] No module loader (plain <script> tag) - put directly in global namespace
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).
// 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 : {};
......@@ -44,17 +46,35 @@ ko.exportSymbol = function(koPath, object) {
ko.exportProperty = function(owner, publicName, object) {
owner[publicName] = object;
};
ko.version = "3.0.0";
ko.version = "3.1.0";
ko.exportSymbol('version', ko.version);
ko.utils = (function () {
var objectForEach = function(obj, action) {
function objectForEach(obj, action) {
for (var prop in obj) {
if (obj.hasOwnProperty(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)
var knownEvents = {}, knownEventTypesByEventName = {};
......@@ -98,7 +118,7 @@ ko.utils = (function () {
arrayForEach: function (array, action) {
for (var i = 0, j = array.length; i < j; i++)
action(array[i]);
action(array[i], i);
},
arrayIndexOf: function (array, item) {
......@@ -112,15 +132,19 @@ ko.utils = (function () {
arrayFirst: function (array, predicate, predicateOwner) {
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 null;
},
arrayRemoveItem: function (array, itemToRemove) {
var index = ko.utils.arrayIndexOf(array, itemToRemove);
if (index >= 0)
if (index > 0) {
array.splice(index, 1);
}
else if (index === 0) {
array.shift();
}
},
arrayGetDistinctValues: function (array) {
......@@ -137,7 +161,7 @@ ko.utils = (function () {
array = array || [];
var result = [];
for (var i = 0, j = array.length; i < j; i++)
result.push(mapping(array[i]));
result.push(mapping(array[i], i));
return result;
},
......@@ -145,7 +169,7 @@ ko.utils = (function () {
array = array || [];
var result = [];
for (var i = 0, j = array.length; i < j; i++)
if (predicate(array[i]))
if (predicate(array[i], i))
result.push(array[i]);
return result;
},
......@@ -170,16 +194,13 @@ ko.utils = (function () {
}
},
extend: function (target, source) {
if (source) {
for(var prop in source) {
if(source.hasOwnProperty(prop)) {
target[prop] = source[prop];
}
}
}
return target;
},
canSetPrototype: canSetPrototype,
extend: extend,
setPrototypeOf: setPrototypeOf,
setPrototypeOfOrExtend: canSetPrototype ? setPrototypeOf : extend,
objectForEach: objectForEach,
......@@ -262,7 +283,7 @@ ko.utils = (function () {
// Rule [A]
while (continuousNodeArray.length && continuousNodeArray[0].parentNode !== parentNode)
continuousNodeArray.splice(0, 1);
continuousNodeArray.shift();
// Rule [B]
if (continuousNodeArray.length > 1) {
......@@ -346,21 +367,7 @@ ko.utils = (function () {
registerEventHandler: function (element, eventType, handler) {
var mustUseAttachEvent = ieVersion && eventsThatMustBeRegisteredUsingAttachEvent[eventType];
if (!mustUseAttachEvent && typeof jQuery != "undefined") {
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
};
}
if (!mustUseAttachEvent && jQuery) {
jQuery(element)['bind'](eventType, handler);
} else if (!mustUseAttachEvent && typeof element.addEventListener == "function")
element.addEventListener(eventType, handler, false);
......@@ -382,13 +389,14 @@ ko.utils = (function () {
if (!(element && element.nodeType))
throw new Error("element must be a DOM node when calling triggerEvent");
if (typeof jQuery != "undefined") {
var eventData = [];
if (isClickOnCheckableElement(element, eventType)) {
// Work around the jQuery "click events on checkboxes" issue described above by storing the original checked state before triggering the handler
eventData.push({ checkedStateBeforeEvent: element.checked });
}
jQuery(element)['trigger'](eventType, eventData);
// For click events on checkboxes and radio buttons, jQuery toggles the element checked state *after* the
// event handler runs instead of *before*. (This was fixed in 1.9 for checkboxes but not for radio buttons.)
// IE doesn't change the checked state when you trigger the click event using "fireEvent".
// In both cases, we'll use the click method instead.
var useClickWorkaround = isClickOnCheckableElement(element, eventType);
if (jQuery && !useClickWorkaround) {
jQuery(element)['trigger'](eventType);
} else if (typeof document.createEvent == "function") {
if (typeof element.dispatchEvent == "function") {
var eventCategory = knownEventTypesByEventName[eventType] || "HTMLEvents";
......@@ -398,15 +406,13 @@ ko.utils = (function () {
}
else
throw new Error("The supplied element doesn't support dispatchEvent");
} else if (useClickWorkaround && element.click) {
element.click();
} 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);
}
else
} else {
throw new Error("Browser doesn't support triggering events");
}
},
unwrapObservable: function (value) {
......@@ -438,7 +444,7 @@ ko.utils = (function () {
// we'll clear everything and create a single text node.
var innerTextNode = ko.virtualElements.firstChild(element);
if (!innerTextNode || innerTextNode.nodeType != 3 || ko.virtualElements.nextSibling(innerTextNode)) {
ko.virtualElements.setDomNodeChildren(element, [document.createTextNode(value)]);
ko.virtualElements.setDomNodeChildren(element, [element.ownerDocument.createTextNode(value)]);
} else {
innerTextNode.data = value;
}
......@@ -687,16 +693,13 @@ ko.utils.domNodeDisposal = new (function () {
callbacks[i](node);
}
// Also erase the DOM data
// Erase the DOM data
ko.utils.domData.clear(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 ((typeof jQuery == "function") && (typeof jQuery['cleanData'] == "function"))
jQuery['cleanData']([node]);
// Perform cleanup needed by external libraries (currently only jQuery, but can be extended)
ko.utils.domNodeDisposal["cleanExternalData"](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)
if (cleanableNodeTypesWithDescendants[node.nodeType])
cleanImmediateCommentTypeChildren(node);
......@@ -748,6 +751,14 @@ ko.utils.domNodeDisposal = new (function () {
ko.cleanNode(node);
if (node.parentNode)
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,7 +832,7 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD
}
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.
};
......@@ -838,7 +849,7 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD
// 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.
// If you've referenced jQuery we'll use that rather than duplicating its code.
if (typeof jQuery != 'undefined') {
if (jQuery) {
jQuery(node)['html'](html);
} else {
// ... otherwise, use KO's own parsing logic.
......@@ -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) {
target["equalityComparer"] = notifyWhen == "always" ?
null : // null equalityComparer means to always notify
......@@ -957,6 +984,26 @@ function valuesArePrimitiveAndEqual(a, b) {
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) {
var target = this;
if (requestedExtenders) {
......@@ -976,6 +1023,7 @@ ko.subscription = function (target, callback, disposeCallback) {
this.target = target;
this.callback = callback;
this.disposeCallback = disposeCallback;
this.isDisposed = false;
ko.exportProperty(this, 'dispose', this.dispose);
};
ko.subscription.prototype.dispose = function () {
......@@ -984,28 +1032,32 @@ ko.subscription.prototype.dispose = function () {
};
ko.subscribable = function () {
ko.utils.setPrototypeOfOrExtend(this, ko.subscribable['fn']);
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";
ko.subscribable['fn'] = {
var ko_subscribable_fn = {
subscribe: function (callback, callbackTarget, event) {
var self = this;
event = event || defaultEvent;
var boundCallback = callbackTarget ? callback.bind(callbackTarget) : callback;
var subscription = new ko.subscription(this, boundCallback, function () {
ko.utils.arrayRemoveItem(this._subscriptions[event], subscription);
}.bind(this));
var subscription = new ko.subscription(self, boundCallback, function () {
ko.utils.arrayRemoveItem(self._subscriptions[event], subscription);
});
// This will force a computed with deferEvaluation to evaluate before any subscriptions
// are registered.
if (self.peek) {
self.peek();
}
if (!this._subscriptions[event])
this._subscriptions[event] = [];
this._subscriptions[event].push(subscription);
if (!self._subscriptions[event])
self._subscriptions[event] = [];
self._subscriptions[event].push(subscription);
return subscription;
},
......@@ -1013,19 +1065,61 @@ ko.subscribable['fn'] = {
event = event || defaultEvent;
if (this.hasSubscriptionsForEvent(event)) {
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) {
// In case a subscription was disposed during the arrayForEach cycle, check
// for isDisposed on each subscription before invoking its callback
if (subscription && (subscription.isDisposed !== true))
if (!subscription.isDisposed)
subscription.callback(valueToNotify);
}
} 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) {
return this._subscriptions[event] && this._subscriptions[event].length;
},
......@@ -1038,9 +1132,26 @@ ko.subscribable['fn'] = {
return total;
},
isDifferent: function(oldValue, newValue) {
return !this['equalityComparer'] || !this['equalityComparer'](oldValue, newValue);
},
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) {
return instance != null && typeof instance.subscribe == "function" && typeof instance["notifySubscribers"] == "function";
......@@ -1049,40 +1160,67 @@ ko.isSubscribable = function (instance) {
ko.exportSymbol('subscribable', ko.subscribable);
ko.exportSymbol('isSubscribable', ko.isSubscribable);
ko.dependencyDetection = (function () {
var _frames = [];
ko.computedContext = ko.dependencyDetection = (function () {
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 {
begin: function (callback) {
_frames.push(callback && { callback: callback, distinctDependencies:[] });
},
begin: begin,
end: function () {
_frames.pop();
},
end: end,
registerDependency: function (subscribable) {
if (currentFrame) {
if (!ko.isSubscribable(subscribable))
throw new Error("Only subscribable things can act as dependencies");
if (_frames.length > 0) {
var topFrame = _frames[_frames.length - 1];
if (!topFrame || ko.utils.arrayIndexOf(topFrame.distinctDependencies, subscribable) >= 0)
return;
topFrame.distinctDependencies.push(subscribable);
topFrame.callback(subscribable);
currentFrame.callback(subscribable, subscribable._id || (subscribable._id = getId()));
}
},
ignore: function(callback, callbackTarget, callbackArgs) {
ignore: function (callback, callbackTarget, callbackArgs) {
try {
_frames.push(null);
begin();
return callback.apply(callbackTarget, callbackArgs || []);
} 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) {
var _latestValue = initialValue;
......@@ -1091,7 +1229,7 @@ ko.observable = function (initialValue) {
// Write
// Ignore writes if the value hasn't changed
if (!observable['equalityComparer'] || !observable['equalityComparer'](_latestValue, arguments[0])) {
if (observable.isDifferent(_latestValue, arguments[0])) {
observable.valueWillMutate();
_latestValue = arguments[0];
if (DEBUG) observable._latestValue = _latestValue;
......@@ -1105,12 +1243,13 @@ ko.observable = function (initialValue) {
return _latestValue;
}
}
if (DEBUG) observable._latestValue = _latestValue;
ko.subscribable.call(observable);
ko.utils.setPrototypeOfOrExtend(observable, ko.observable['fn']);
if (DEBUG) observable._latestValue = _latestValue;
observable.peek = function() { return _latestValue };
observable.valueHasMutated = function () { observable["notifySubscribers"](_latestValue); }
observable.valueWillMutate = function () { observable["notifySubscribers"](_latestValue, "beforeChange"); }
ko.utils.extend(observable, ko.observable['fn']);
ko.exportProperty(observable, 'peek', observable.peek);
ko.exportProperty(observable, "valueHasMutated", observable.valueHasMutated);
......@@ -1126,6 +1265,12 @@ ko.observable['fn'] = {
var protoProperty = ko.observable.protoProperty = "__ko_proto__";
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) {
if ((instance === null) || (instance === undefined) || (instance[protoProperty] === undefined)) return false;
if (instance[protoProperty] === prototype) return true;
......@@ -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.");
var result = ko.observable(initialValues);
ko.utils.extend(result, ko.observableArray['fn']);
ko.utils.setPrototypeOfOrExtend(result, ko.observableArray['fn']);
return result.extend({'trackArrayChanges':true});
};
......@@ -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);
var arrayChangeEventName = 'arrayChange';
ko.extenders['trackArrayChanges'] = function(target) {
......@@ -1327,9 +1478,9 @@ ko.extenders['trackArrayChanges'] = function(target) {
function getChanges(previousContents, currentContents) {
// We try to re-use cached diffs.
// The only scenario where pendingNotifications > 1 is when using the KO 'deferred updates' plugin,
// which without this check would not be compatible with arrayChange notifications. Without that
// plugin, notifications are always issued immediately so we wouldn't be queueing up more than one.
// The scenarios where pendingNotifications > 1 are when using rate-limiting or the Deferred Updates
// plugin, which without this check would not be compatible with arrayChange notifications. Normally,
// notifications are issued immediately so we wouldn't be queueing up more than one.
if (!cachedDiff || pendingNotifications > 1) {
cachedDiff = ko.utils.compareArrays(previousContents, currentContents, { 'sparse': true });
}
......@@ -1349,7 +1500,7 @@ ko.extenders['trackArrayChanges'] = function(target) {
offset = 0;
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) {
case 'push':
......@@ -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),
endDeleteIndex = argsLength === 1 ? arrayLength : Math.min(startIndex + (args[1] || 0), arrayLength),
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) {
if (index < endDeleteIndex)
pushDiff('deleted', rawArray[index], index);
deletions.push(pushDiff('deleted', rawArray[index], index));
if (index < endAddIndex)
pushDiff('added', args[argsIndex], index);
additions.push(pushDiff('added', args[argsIndex], index));
}
ko.utils.findMovesInArrayComparison(deletions, additions);
break;
default:
......@@ -1389,11 +1542,12 @@ ko.extenders['trackArrayChanges'] = function(target) {
cachedDiff = diff;
};
};
ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget, options) {
ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget, options) {
var _latestValue,
_hasBeenEvaluated = false,
_needsEvaluation = true,
_isBeingEvaluated = false,
_suppressDisposalUntilDisposeWhenReturnsFalse = false,
_isDisposed = false,
readFunction = evaluatorFunctionOrOptions;
if (readFunction && typeof readFunction == "object") {
......@@ -1409,15 +1563,21 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
if (typeof readFunction != "function")
throw new Error("Pass a function that returns the value of the ko.computed");
function addSubscriptionToDependency(subscribable) {
_subscriptionsToDependencies.push(subscribable.subscribe(evaluatePossiblyAsync));
function addSubscriptionToDependency(subscribable, id) {
if (!_subscriptionsToDependencies[id]) {
_subscriptionsToDependencies[id] = subscribable.subscribe(evaluatePossiblyAsync);
++_dependenciesCount;
}
}
function disposeAllSubscriptionsToDependencies() {
ko.utils.arrayForEach(_subscriptionsToDependencies, function (subscription) {
_isDisposed = true;
ko.utils.objectForEach(_subscriptionsToDependencies, function (id, subscription) {
subscription.dispose();
});
_subscriptionsToDependencies = [];
_subscriptionsToDependencies = {};
_dependenciesCount = 0;
_needsEvaluation = false;
}
function evaluatePossiblyAsync() {
......@@ -1425,9 +1585,12 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
if (throttleEvaluationTimeout && throttleEvaluationTimeout >= 0) {
clearTimeout(evaluationTimeoutInstance);
evaluationTimeoutInstance = setTimeout(evaluateImmediate, throttleEvaluationTimeout);
} else
} else if (dependentObservable._evalRateLimited) {
dependentObservable._evalRateLimited();
} else {
evaluateImmediate();
}
}
function evaluateImmediate() {
if (_isBeingEvaluated) {
......@@ -1438,11 +1601,15 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
return;
}
// Do not evaluate (and possibly capture new dependencies) if disposed
if (_isDisposed) {
return;
}
if (disposeWhen && disposeWhen()) {
// See comment below about _suppressDisposalUntilDisposeWhenReturnsFalse
if (!_suppressDisposalUntilDisposeWhenReturnsFalse) {
dispose();
_hasBeenEvaluated = true;
return;
}
} else {
......@@ -1454,38 +1621,63 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
try {
// 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.
var disposalCandidates = ko.utils.arrayMap(_subscriptionsToDependencies, function(item) {return item.target;});
ko.dependencyDetection.begin(function(subscribable) {
var inOld;
if ((inOld = ko.utils.arrayIndexOf(disposalCandidates, subscribable)) >= 0)
disposalCandidates[inOld] = undefined; // Don't want to dispose this subscription, as it's still being used
else
addSubscriptionToDependency(subscribable); // Brand new subscription - add it
var disposalCandidates = _subscriptionsToDependencies, disposalCount = _dependenciesCount;
ko.dependencyDetection.begin({
callback: function(subscribable, id) {
if (!_isDisposed) {
if (disposalCount && disposalCandidates[id]) {
// Don't want to dispose this subscription, as it's still being used
_subscriptionsToDependencies[id] = disposalCandidates[id];
++_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
});
_subscriptionsToDependencies = {};
_dependenciesCount = 0;
try {
var newValue = evaluatorFunctionTarget ? readFunction.call(evaluatorFunctionTarget) : readFunction();
} finally {
ko.dependencyDetection.end();
// For each subscription no longer being used, remove it from the active subscriptions list and dispose it
for (var i = disposalCandidates.length - 1; i >= 0; i--) {
if (disposalCandidates[i])
_subscriptionsToDependencies.splice(i, 1)[0].dispose();
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");
_latestValue = newValue;
if (DEBUG) dependentObservable._latestValue = _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 {
ko.dependencyDetection.end();
_isBeingEvaluated = false;
}
if (!_subscriptionsToDependencies.length)
if (!_dependenciesCount)
dispose();
}
......@@ -1500,7 +1692,7 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
return this; // Permits chained assignments
} else {
// Reading the value
if (!_hasBeenEvaluated)
if (_needsEvaluation)
evaluateImmediate();
ko.dependencyDetection.registerDependency(dependentObservable);
return _latestValue;
......@@ -1508,13 +1700,15 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
}
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();
return _latestValue;
}
function isActive() {
return !_hasBeenEvaluated || _subscriptionsToDependencies.length > 0;
return _needsEvaluation || _dependenciesCount > 0;
}
// By here, "options" is always non-null
......@@ -1523,20 +1717,36 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
disposeWhenOption = options["disposeWhen"] || options.disposeWhen,
disposeWhen = disposeWhenOption,
dispose = disposeAllSubscriptionsToDependencies,
_subscriptionsToDependencies = [],
_subscriptionsToDependencies = {},
_dependenciesCount = 0,
evaluationTimeoutInstance = null;
if (!evaluatorFunctionTarget)
evaluatorFunctionTarget = options["owner"];
ko.subscribable.call(dependentObservable);
ko.utils.setPrototypeOfOrExtend(dependentObservable, ko.dependentObservable['fn']);
dependentObservable.peek = peek;
dependentObservable.getDependenciesCount = function () { return _subscriptionsToDependencies.length; };
dependentObservable.getDependenciesCount = function () { return _dependenciesCount; };
dependentObservable.hasWriteFunction = typeof options["write"] === "function";
dependentObservable.dispose = function () { dispose(); };
dependentObservable.isActive = isActive;
ko.subscribable.call(dependentObservable);
ko.utils.extend(dependentObservable, ko.dependentObservable['fn']);
// Replace the limit function with one that delays evaluation as well.
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, 'dispose', dependentObservable.dispose);
......@@ -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
// 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() {
ko.utils.domNodeDisposal.removeDisposeCallback(disposeWhenNodeIsRemoved, dispose);
disposeAllSubscriptionsToDependencies();
......@@ -1591,6 +1801,12 @@ ko.dependentObservable['fn'] = {
};
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('computed', ko.dependentObservable); // Make "ko.computed" an alias for "ko.dependentObservable"
ko.exportSymbol('isComputed', ko.isComputed);
......@@ -1712,7 +1928,7 @@ ko.exportSymbol('toJSON', ko.toJSON);
}
},
writeValue: function(element, value) {
writeValue: function(element, value, allowUnset) {
switch (ko.utils.tagNameLower(element)) {
case 'option':
switch(typeof value) {
......@@ -1734,19 +1950,19 @@ ko.exportSymbol('toJSON', ko.toJSON);
}
break;
case 'select':
if (value === "")
if (value === "" || value === null) // A blank string or null value will select the caption
value = undefined;
if (value === null || value === undefined)
element.selectedIndex = -1;
for (var i = element.options.length - 1; i >= 0; i--) {
if (ko.selectExtensions.readValue(element.options[i]) == value) {
element.selectedIndex = i;
var selection = -1;
for (var i = 0, n = element.options.length, optionValue; i < n; ++i) {
optionValue = ko.selectExtensions.readValue(element.options[i]);
// Include special check to handle selecting a caption with a blank string value
if (optionValue == value || (optionValue == "" && value === undefined)) {
selection = i;
break;
}
}
// for drop-down select, ensure first is selected
if (!(element.size > 1) && element.selectedIndex === -1) {
element.selectedIndex = 0;
if (allowUnset || selection >= 0 || (value === undefined && element.size > 1)) {
element.selectedIndex = selection;
}
break;
default:
......@@ -1902,7 +2118,7 @@ ko.expressionRewriting = (function () {
});
if (propertyAccessorResultStrings.length)
processKeyValue('_ko_property_writers', "{" + propertyAccessorResultStrings.join(",") + "}");
processKeyValue('_ko_property_writers', "{" + propertyAccessorResultStrings.join(",") + " }");
return resultStrings.join(",");
}
......@@ -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.prepend', ko.virtualElements.prepend);
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() {
var defaultBindingAttributeName = "data-bind";
......@@ -2251,10 +2797,11 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
// any child contexts, must be updated when the view model is changed.
function updateContext() {
// 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
// itself an observable), the dependency is tracked, and those observables can later cause the binding
// we call the function to retrieve the view model. If the function accesses any obsevables or returns
// an observable, the dependency is tracked, and those observables can later cause the binding
// context to be updated.
var dataItem = isFunc ? dataItemOrAccessor() : dataItemOrAccessor;
var dataItemOrObservable = isFunc ? dataItemOrAccessor() : dataItemOrAccessor,
dataItem = ko.utils.unwrapObservable(dataItemOrObservable);
if (parentContext) {
// 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);
// See https://github.com/SteveSanderson/knockout/issues/490
self['ko'] = ko;
}
self['$rawData'] = dataItemOrAccessor;
self['$rawData'] = dataItemOrObservable;
self['$data'] = dataItem;
if (dataItemAlias)
self[dataItemAlias] = dataItem;
......@@ -2297,7 +2844,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
}
var self = this,
isFunc = typeof(dataItemOrAccessor) == "function",
isFunc = typeof(dataItemOrAccessor) == "function" && !ko.isObservable(dataItemOrAccessor),
nodes,
subscribable = ko.dependentObservable(updateContext, null, { disposeWhen: disposeWhen, disposeWhenNodeIsRemoved: true });
......@@ -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
// when an observable view model is updated.
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);
});
};
......@@ -2480,7 +3032,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
}
}
});
cyclicDependencyStack.pop();
cyclicDependencyStack.length--;
}
// Next add the current binding
result.push({ key: bindingKey, handler: binding });
......@@ -2516,14 +3068,12 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
var provider = ko.bindingProvider['instance'],
getBindings = provider['getBindingAccessors'] || getBindingsAndMakeAccessors;
if (sourceBindings || bindingContext._subscribable) {
// When an obsevable view model is used, the binding context will expose an observable _subscribable value.
// Get the binding from the provider within a computed observable so that we can update the bindings whenever
// the binding context is updated.
// the binding context is updated or if the binding provider accesses observables.
var bindingsUpdater = ko.dependentObservable(
function() {
bindings = sourceBindings ? sourceBindings(bindingContext, node) : getBindings.call(provider, node, bindingContext);
// Register a dependency on the binding context
// Register a dependency on the binding context to support obsevable view models.
if (bindings && bindingContext._subscribable)
bindingContext._subscribable();
return bindings;
......@@ -2533,9 +3083,6 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
if (!bindings || !bindingsUpdater.isActive())
bindingsUpdater = null;
} else {
bindings = ko.dependencyDetection.ignore(getBindings, provider, [node, bindingContext]);
}
}
var bindingHandlerThatControlsDescendantBindings;
......@@ -2650,6 +3197,11 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
};
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))
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
......@@ -2683,6 +3235,87 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
ko.exportSymbol('contextFor', ko.contextFor);
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' };
ko.bindingHandlers['attr'] = {
'update': function(element, valueAccessor, allBindings) {
......@@ -2739,7 +3372,7 @@ ko.bindingHandlers['checked'] = {
elemValue = useCheckedValue ? checkedValue() : isChecked;
// When we're first setting up this computed, don't change any model state.
if (!shouldSet) {
if (ko.computedContext.isInitial()) {
return;
}
......@@ -2798,8 +3431,7 @@ ko.bindingHandlers['checked'] = {
var isValueArray = isCheckbox && (ko.utils.unwrapObservable(valueAccessor()) instanceof Array),
oldElemValue = isValueArray ? checkedValue() : undefined,
useCheckedValue = isRadio || isValueArray,
shouldSet = false;
useCheckedValue = isRadio || isValueArray;
// IE 6 won't allow radio buttons to be selected unless they have a name
if (isRadio && !element.name)
......@@ -2808,13 +3440,11 @@ ko.bindingHandlers['checked'] = {
// Set up two computeds to update the binding:
// 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);
// The second responds to changes in the model value (the one associated with the checked binding)
ko.dependentObservable(updateView, null, { disposeWhenNodeIsRemoved: element });
shouldSet = true;
ko.computed(updateView, null, { disposeWhenNodeIsRemoved: element });
}
};
ko.expressionRewriting.twoWayBindings['checked'] = true;
......@@ -3007,37 +3637,37 @@ ko.bindingHandlers['html'] = {
ko.utils.setHtml(element, valueAccessor());
}
};
var withIfDomDataKey = ko.utils.domData.nextKey();
// Makes a binding like with or if
function makeWithIfBinding(bindingKey, isWith, isNot, makeContextCallback) {
ko.bindingHandlers[bindingKey] = {
'init': function(element) {
ko.utils.domData.set(element, withIfDomDataKey, {});
return { 'controlsDescendantBindings': true };
},
'update': function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var withIfData = ko.utils.domData.get(element, withIfDomDataKey),
dataValue = ko.utils.unwrapObservable(valueAccessor()),
'init': function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var didDisplayOnLastUpdate,
savedNodes;
ko.computed(function() {
var dataValue = ko.utils.unwrapObservable(valueAccessor()),
shouldDisplay = !isNot !== !dataValue, // equivalent to isNot ? !dataValue : !!dataValue
isFirstRender = !withIfData.savedNodes,
needsRefresh = isFirstRender || isWith || (shouldDisplay !== withIfData.didDisplayOnLastUpdate);
isFirstRender = !savedNodes,
needsRefresh = isFirstRender || isWith || (shouldDisplay !== didDisplayOnLastUpdate);
if (needsRefresh) {
if (isFirstRender) {
withIfData.savedNodes = ko.utils.cloneNodes(ko.virtualElements.childNodes(element), true /* shouldCleanNodes */);
// Save a copy of the inner nodes on the initial update, but only if we have dependencies.
if (isFirstRender && ko.computedContext.getDependenciesCount()) {
savedNodes = ko.utils.cloneNodes(ko.virtualElements.childNodes(element), true /* shouldCleanNodes */);
}
if (shouldDisplay) {
if (!isFirstRender) {
ko.virtualElements.setDomNodeChildren(element, ko.utils.cloneNodes(withIfData.savedNodes));
ko.virtualElements.setDomNodeChildren(element, ko.utils.cloneNodes(savedNodes));
}
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
......@@ -3052,6 +3682,7 @@ makeWithIfBinding('with', true /* isWith */, false /* isNot */,
return bindingContext['createChildContext'](dataValue);
}
);
var captionPlaceholder = {};
ko.bindingHandlers['options'] = {
'init': function(element) {
if (ko.utils.tagNameLower(element) !== "select")
......@@ -3072,12 +3703,13 @@ ko.bindingHandlers['options'] = {
var selectWasPreviouslyEmpty = element.length == 0;
var previousScrollTop = (!selectWasPreviouslyEmpty && element.multiple) ? element.scrollTop : null;
var unwrappedArray = ko.utils.unwrapObservable(valueAccessor());
var includeDestroyed = allBindings.get('optionsIncludeDestroyed');
var captionPlaceholder = {};
var arrayToDomNodeChildrenOptions = {};
var captionValue;
var filteredArray;
var previousSelectedValues;
if (element.multiple) {
previousSelectedValues = ko.utils.arrayMap(selectedOptions(), ko.selectExtensions.readValue);
} else {
......@@ -3089,7 +3721,7 @@ ko.bindingHandlers['options'] = {
unwrappedArray = [unwrappedArray];
// 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']);
});
......@@ -3103,7 +3735,6 @@ ko.bindingHandlers['options'] = {
}
} else {
// If a falsy value is provided (e.g. null), we'll simply empty the select element
unwrappedArray = [];
}
function applyToObject(object, predicate, defaultValue) {
......@@ -3126,7 +3757,7 @@ ko.bindingHandlers['options'] = {
previousSelectedValues = oldOptions[0].selected ? [ ko.selectExtensions.readValue(oldOptions[0]) ] : [];
itemUpdate = true;
}
var option = document.createElement("option");
var option = element.ownerDocument.createElement("option");
if (arrayEntry === captionPlaceholder) {
ko.utils.setTextContent(option, allBindings.get('optionsCaption'));
ko.selectExtensions.writeValue(option, undefined);
......@@ -3142,6 +3773,13 @@ ko.bindingHandlers['options'] = {
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) {
// 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.
......@@ -3163,8 +3801,13 @@ ko.bindingHandlers['options'] = {
}
}
ko.utils.setDomNodeChildrenFromArrayMapping(element, filteredArray, optionForArrayItem, null, callback);
ko.utils.setDomNodeChildrenFromArrayMapping(element, filteredArray, optionForArrayItem, arrayToDomNodeChildrenOptions, callback);
ko.dependencyDetection.ignore(function () {
if (allBindings.get('valueAllowUnset') && allBindings['has']('value')) {
// The model value is authoritative, so make sure its value is the one selected
ko.selectExtensions.writeValue(element, ko.utils.unwrapObservable(allBindings.get('value')), true /* allowUnset */);
} else {
// Determine if the selection has changed as a result of updating the options list
var selectionChanged;
if (element.multiple) {
......@@ -3182,8 +3825,11 @@ ko.bindingHandlers['options'] = {
// Ensure consistency between model value and selected option.
// If the dropdown was changed so that selection is no longer the same,
// notify the value or selectedOptions binding.
if (selectionChanged)
ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, "change"]);
if (selectionChanged) {
ko.utils.triggerEvent(element, "change");
}
}
});
// Workaround for IE bug
ko.utils.ensureSelectElementIsRenderedCorrectly(element);
......@@ -3294,6 +3940,7 @@ ko.bindingHandlers['value'] = {
&& element.autocomplete != "off" && (!element.form || element.form.autocomplete != "off");
if (ieAutoCompleteHackNeeded && ko.utils.arrayIndexOf(eventsToCatch, "propertychange") == -1) {
ko.utils.registerEventHandler(element, "propertychange", function () { propertyChangedFired = true });
ko.utils.registerEventHandler(element, "focus", function () { propertyChangedFired = false });
ko.utils.registerEventHandler(element, "blur", function() {
if (propertyChangedFired) {
valueUpdateHandler();
......@@ -3313,18 +3960,20 @@ ko.bindingHandlers['value'] = {
ko.utils.registerEventHandler(element, eventName, handler);
});
},
'update': function (element, valueAccessor) {
var valueIsSelectOption = ko.utils.tagNameLower(element) === "select";
'update': function (element, valueAccessor, allBindings) {
var newValue = ko.utils.unwrapObservable(valueAccessor());
var elementValue = ko.selectExtensions.readValue(element);
var valueHasChanged = (newValue !== elementValue);
if (valueHasChanged) {
var applyValueAction = function () { ko.selectExtensions.writeValue(element, newValue); };
if (ko.utils.tagNameLower(element) === "select") {
var allowUnset = allBindings.get('valueAllowUnset');
var applyValueAction = function () {
ko.selectExtensions.writeValue(element, newValue, allowUnset);
};
applyValueAction();
if (valueIsSelectOption) {
if (newValue !== ko.selectExtensions.readValue(element)) {
if (!allowUnset && 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,
// 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"]);
......@@ -3334,6 +3983,8 @@ ko.bindingHandlers['value'] = {
// to apply the value as well.
setTimeout(applyValueAction, 0);
}
} else {
ko.selectExtensions.writeValue(element, newValue);
}
}
}
......@@ -3718,7 +4369,8 @@ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextS
: new ko.bindingContext(ko.utils.unwrapObservable(dataOrBindingContext));
// Support selecting template as a function of the data being rendered
var templateName = typeof(template) == 'function' ? template(bindingContext['$data'], bindingContext) : template;
var templateName = ko.isObservable(template) ? template()
: typeof(template) == 'function' ? template(bindingContext['$data'], bindingContext) : template;
var renderedNodesArray = executeTemplate(targetNodeOrNodeArray, renderMode, templateName, bindingContext, options);
if (renderMode == "replaceNode") {
......@@ -3800,15 +4452,18 @@ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextS
return { 'controlsDescendantBindings': true };
},
'update': function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var templateName = ko.utils.unwrapObservable(valueAccessor()),
options = {},
shouldDisplay = true,
var value = valueAccessor(),
dataValue,
templateComputed = null;
options = ko.utils.unwrapObservable(value),
shouldDisplay = true,
templateComputed = null,
templateName;
if (typeof templateName != "string") {
options = templateName;
templateName = ko.utils.unwrapObservable(options['name']);
if (typeof options == "string") {
templateName = value;
options = {};
} else {
templateName = options['name'];
// Support "if"/"ifnot" conditions
if ('if' in options)
......@@ -3855,6 +4510,24 @@ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextS
ko.exportSymbol('setTemplateEngine', ko.setTemplateEngine);
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 () {
var statusNotInOld = 'added', statusNotInNew = 'deleted';
......@@ -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
// smlIndexMax keeps the time complexity of this algorithm linear.
var limitFailedCompares = smlIndexMax * 10, failedCompares,
a, d, notInSmlItem, notInBigItem;
// Go through the items that have been added and deleted and try to find matches between them.
for (failedCompares = a = 0; (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;
}
}
ko.utils.findMovesInArrayComparison(notInSml, notInBig, smlIndexMax * 10);
return editScript.reverse();
}
......@@ -3954,7 +4612,6 @@ ko.utils.compareArrays = (function () {
})();
ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);
(function () {
// Objective:
// * 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);
// Replace the contents of the mappedNodes array, thereby updating the record
// 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);
}, null, { disposeWhenNodeIsRemoved: containerNode, disposeWhen: function() { return !ko.utils.anyDomNodeIsAttachedToDocument(mappedNodes); } });
return { mappedNodes : mappedNodes, dependentObservable : (dependentObservable.isActive() ? dependentObservable : undefined) };
......@@ -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,
// which KO internally refers to as version "2", so older versions are no longer detected.
var jQueryTmplVersion = this.jQueryTmplVersion = (function() {
if ((typeof(jQuery) == "undefined") || !(jQuery['tmpl']))
if (!jQuery || !(jQuery['tmpl']))
return 0;
// Since it exposes no official version number, we use our own numbering system. To be updated as jquery-tmpl evolves.
try {
......
//
// Generated on Sun Dec 16 2012 22:47:05 GMT-0500 (EST) by Nodejitsu, Inc (Using Codesurgeon).
// Version 1.1.9
// Generated on Fri Dec 27 2013 12:02:11 GMT-0500 (EST) by Nodejitsu, Inc (Using Codesurgeon).
// Version 1.2.2
//
(function (exports) {
/*
* browser.js: Browser specific functionality for director.
*
......@@ -201,7 +200,7 @@ Router.prototype.init = function (r) {
this.handler = function(onChangeEvent) {
var newURL = onChangeEvent && onChangeEvent.newURL || window.location.hash;
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);
......@@ -210,7 +209,7 @@ Router.prototype.init = function (r) {
if (dlocHashEmpty() && r) {
dloc.hash = r;
} else if (!dlocHashEmpty()) {
self.dispatch('on', dloc.hash.replace(/^#/, ''));
self.dispatch('on', '/' + dloc.hash.replace(/^(#\/|#|\/)/, ''));
}
}
else {
......@@ -363,11 +362,16 @@ function regifyString(str, params) {
out += str.substr(0, matches.index) + matches[0];
}
str = out += str.substr(last);
var captures = str.match(/:([^\/]+)/ig), length;
var captures = str.match(/:([^\/]+)/ig), capture, length;
if (captures) {
length = captures.length;
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;
......@@ -485,20 +489,22 @@ Router.prototype.dispatch = function(method, path, callback) {
Router.prototype.invoke = function(fns, thisArg, callback) {
var self = this;
var apply;
if (this.async) {
_asyncEverySeries(fns, function apply(fn, next) {
apply = function(fn, next) {
if (Array.isArray(fn)) {
return _asyncEverySeries(fn, apply, next);
} else if (typeof fn == "function") {
fn.apply(thisArg, fns.captures.concat(next));
}
}, function() {
};
_asyncEverySeries(fns, apply, function() {
if (callback) {
callback.apply(thisArg, arguments);
}
});
} else {
_every(fns, function apply(fn) {
apply = function(fn) {
if (Array.isArray(fn)) {
return _every(fn, apply);
} else if (typeof fn === "function") {
......@@ -506,7 +512,8 @@ Router.prototype.invoke = function(fns, thisArg, callback) {
} else if (typeof fn === "string" && self.resource) {
self.resource[fn].apply(thisArg, fns.captures || []);
}
});
};
_every(fns, apply);
}
};
......@@ -686,7 +693,7 @@ Router.prototype.mount = function(routes, path) {
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;
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();
}
if (isRoute && routeType === "object" && !Array.isArray(routes[route])) {
......
......@@ -52,7 +52,7 @@
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<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="js/app.js"></script>
</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