knockout
Version:
NodeJs Testable version of client script which Simplifies Dynamic JavaScript UIs by applying the Model-View-ViewModel (MVVM) Pattern.
1,139 lines (1,004 loc) • 171 kB
JavaScript
var jsdom = require('jsdom').jsdom,
document = jsdom('<html><head></head><body></body></html>'),
window = document.createWindow(),
navigator = window.navigator = {},
DEBUG = false;
navigator.userAgent = 'NodeJs JsDom';
(function(window,document,navigator,undefined){
!function(factory) {
// Support three module loading scenarios
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);
} else if (typeof define === 'function' && define['amd']) {
// [2] AMD anonymous module
define(['exports'], factory);
} else {
// [3] No module loader (plain <script> tag) - put directly in global namespace
factory(window['ko'] = {});
}
}(function(koExports){
// 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 : {};
// Google Closure Compiler helpers (used only to make the minified file smaller)
ko.exportSymbol = function(koPath, object) {
var tokens = koPath.split(".");
// In the future, "ko" may become distinct from "koExports" (so that non-exported objects are not reachable)
// At that point, "target" would be set to: (typeof koExports !== "undefined" ? koExports : ko)
var target = ko;
for (var i = 0; i < tokens.length - 1; i++)
target = target[tokens[i]];
target[tokens[tokens.length - 1]] = object;
};
ko.exportProperty = function(owner, publicName, object) {
owner[publicName] = object;
};
ko.version = "2.1.0";
ko.exportSymbol('version', ko.version);
ko.utils = new (function () {
var stringTrimRegex = /^(\s|\u00A0)+|(\s|\u00A0)+$/g;
// 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 keyEventTypeName = /Firefox\/2/i.test(navigator.userAgent) ? 'KeyboardEvent' : 'UIEvents';
knownEvents[keyEventTypeName] = ['keyup', 'keydown', 'keypress'];
knownEvents['MouseEvents'] = ['click', 'dblclick', 'mousedown', 'mouseup', 'mousemove', 'mouseover', 'mouseout', 'mouseenter', 'mouseleave'];
for (var eventType in knownEvents) {
var knownEventsForType = knownEvents[eventType];
if (knownEventsForType.length) {
for (var i = 0, j = knownEventsForType.length; i < j; i++)
knownEventTypesByEventName[knownEventsForType[i]] = eventType;
}
}
var eventsThatMustBeRegisteredUsingAttachEvent = { 'propertychange': true }; // Workaround for an IE9 issue - https://github.com/SteveSanderson/knockout/issues/406
// Detect IE versions for bug workarounds (uses IE conditionals, not UA string, for robustness)
var ieVersion = (function() {
var version = 3, div = document.createElement('div'), iElems = div.getElementsByTagName('i');
// Keep constructing conditional HTML blocks until we hit one that resolves to an empty fragment
while (
div.innerHTML = '<!--[if gt IE ' + (++version) + ']><i></i><![endif]-->',
iElems[0]
);
return version > 4 ? version : undefined;
}());
var isIe6 = ieVersion === 6,
isIe7 = ieVersion === 7;
function isClickOnCheckableElement(element, eventType) {
if ((ko.utils.tagNameLower(element) !== "input") || !element.type) return false;
if (eventType.toLowerCase() != "click") return false;
var inputType = element.type;
return (inputType == "checkbox") || (inputType == "radio");
}
return {
fieldsIncludedWithJsonPost: ['authenticity_token', /^__RequestVerificationToken(_.*)?$/],
arrayForEach: function (array, action) {
for (var i = 0, j = array.length; i < j; i++)
action(array[i]);
},
arrayIndexOf: function (array, item) {
if (typeof Array.prototype.indexOf == "function")
return Array.prototype.indexOf.call(array, item);
for (var i = 0, j = array.length; i < j; i++)
if (array[i] === item)
return i;
return -1;
},
arrayFirst: function (array, predicate, predicateOwner) {
for (var i = 0, j = array.length; i < j; i++)
if (predicate.call(predicateOwner, array[i]))
return array[i];
return null;
},
arrayRemoveItem: function (array, itemToRemove) {
var index = ko.utils.arrayIndexOf(array, itemToRemove);
if (index >= 0)
array.splice(index, 1);
},
arrayGetDistinctValues: function (array) {
array = array || [];
var result = [];
for (var i = 0, j = array.length; i < j; i++) {
if (ko.utils.arrayIndexOf(result, array[i]) < 0)
result.push(array[i]);
}
return result;
},
arrayMap: function (array, mapping) {
array = array || [];
var result = [];
for (var i = 0, j = array.length; i < j; i++)
result.push(mapping(array[i]));
return result;
},
arrayFilter: function (array, predicate) {
array = array || [];
var result = [];
for (var i = 0, j = array.length; i < j; i++)
if (predicate(array[i]))
result.push(array[i]);
return result;
},
arrayPushAll: function (array, valuesToPush) {
if (valuesToPush instanceof Array)
array.push.apply(array, valuesToPush);
else
for (var i = 0, j = valuesToPush.length; i < j; i++)
array.push(valuesToPush[i]);
return array;
},
extend: function (target, source) {
if (source) {
for(var prop in source) {
if(source.hasOwnProperty(prop)) {
target[prop] = source[prop];
}
}
}
return target;
},
emptyDomNode: function (domNode) {
while (domNode.firstChild) {
ko.removeNode(domNode.firstChild);
}
},
moveCleanedNodesToContainerElement: function(nodes) {
// Ensure it's a real array, as we're about to reparent the nodes and
// we don't want the underlying collection to change while we're doing that.
var nodesArray = ko.utils.makeArray(nodes);
var container = document.createElement('div');
for (var i = 0, j = nodesArray.length; i < j; i++) {
ko.cleanNode(nodesArray[i]);
container.appendChild(nodesArray[i]);
}
return container;
},
setDomNodeChildren: function (domNode, childNodes) {
ko.utils.emptyDomNode(domNode);
if (childNodes) {
for (var i = 0, j = childNodes.length; i < j; i++)
domNode.appendChild(childNodes[i]);
}
},
replaceDomNodes: function (nodeToReplaceOrNodeArray, newNodesArray) {
var nodesToReplaceArray = nodeToReplaceOrNodeArray.nodeType ? [nodeToReplaceOrNodeArray] : nodeToReplaceOrNodeArray;
if (nodesToReplaceArray.length > 0) {
var insertionPoint = nodesToReplaceArray[0];
var parent = insertionPoint.parentNode;
for (var i = 0, j = newNodesArray.length; i < j; i++)
parent.insertBefore(newNodesArray[i], insertionPoint);
for (var i = 0, j = nodesToReplaceArray.length; i < j; i++) {
ko.removeNode(nodesToReplaceArray[i]);
}
}
},
setOptionNodeSelectionState: function (optionNode, isSelected) {
// IE6 sometimes throws "unknown error" if you try to write to .selected directly, whereas Firefox struggles with setAttribute. Pick one based on browser.
if (navigator.userAgent.indexOf("MSIE 6") >= 0)
optionNode.setAttribute("selected", isSelected);
else
optionNode.selected = isSelected;
},
stringTrim: function (string) {
return (string || "").replace(stringTrimRegex, "");
},
stringTokenize: function (string, delimiter) {
var result = [];
var tokens = (string || "").split(delimiter);
for (var i = 0, j = tokens.length; i < j; i++) {
var trimmed = ko.utils.stringTrim(tokens[i]);
if (trimmed !== "")
result.push(trimmed);
}
return result;
},
stringStartsWith: function (string, startsWith) {
string = string || "";
if (startsWith.length > string.length)
return false;
return string.substring(0, startsWith.length) === startsWith;
},
buildEvalWithinScopeFunction: function (expression, scopeLevels) {
// Build the source for a function that evaluates "expression"
// For each scope variable, add an extra level of "with" nesting
// Example result: with(sc[1]) { with(sc[0]) { return (expression) } }
var functionBody = "return (" + expression + ")";
for (var i = 0; i < scopeLevels; i++) {
functionBody = "with(sc[" + i + "]) { " + functionBody + " } ";
}
return new Function("sc", functionBody);
},
domNodeIsContainedBy: function (node, containedByNode) {
if (containedByNode.compareDocumentPosition)
return (containedByNode.compareDocumentPosition(node) & 16) == 16;
while (node != null) {
if (node == containedByNode)
return true;
node = node.parentNode;
}
return false;
},
domNodeIsAttachedToDocument: function (node) {
return ko.utils.domNodeIsContainedBy(node, node.ownerDocument);
},
tagNameLower: function(element) {
// For HTML elements, tagName will always be upper case; for XHTML elements, it'll be lower case.
// Possible future optimization: If we know it's an element from an XHTML document (not HTML),
// we don't need to do the .toLowerCase() as it will always be lower case anyway.
return element && element.tagName && element.tagName.toLowerCase();
},
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
};
}
jQuery(element)['bind'](eventType, handler);
} else if (!mustUseAttachEvent && typeof element.addEventListener == "function")
element.addEventListener(eventType, handler, false);
else if (typeof element.attachEvent != "undefined")
element.attachEvent("on" + eventType, function (event) {
handler.call(element, event);
});
else
throw new Error("Browser doesn't support addEventListener or attachEvent");
},
triggerEvent: function (element, eventType) {
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);
} else if (typeof document.createEvent == "function") {
if (typeof element.dispatchEvent == "function") {
var eventCategory = knownEventTypesByEventName[eventType] || "HTMLEvents";
var event = document.createEvent(eventCategory);
event.initEvent(eventType, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, element);
element.dispatchEvent(event);
}
else
throw new Error("The supplied element doesn't support dispatchEvent");
} 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
throw new Error("Browser doesn't support triggering events");
},
unwrapObservable: function (value) {
return ko.isObservable(value) ? value() : value;
},
toggleDomNodeCssClass: function (node, className, shouldHaveClass) {
var currentClassNames = (node.className || "").split(/\s+/);
var hasClass = ko.utils.arrayIndexOf(currentClassNames, className) >= 0;
if (shouldHaveClass && !hasClass) {
node.className += (currentClassNames[0] ? " " : "") + className;
} else if (hasClass && !shouldHaveClass) {
var newClassName = "";
for (var i = 0; i < currentClassNames.length; i++)
if (currentClassNames[i] != className)
newClassName += currentClassNames[i] + " ";
node.className = ko.utils.stringTrim(newClassName);
}
},
setTextContent: function(element, textContent) {
var value = ko.utils.unwrapObservable(textContent);
if ((value === null) || (value === undefined))
value = "";
'innerText' in element ? element.innerText = value
: element.textContent = value;
if (ieVersion >= 9) {
// Believe it or not, this actually fixes an IE9 rendering bug
// (See https://github.com/SteveSanderson/knockout/issues/209)
element.style.display = element.style.display;
}
},
ensureSelectElementIsRenderedCorrectly: function(selectElement) {
// Workaround for IE9 rendering bug - it doesn't reliably display all the text in dynamically-added select boxes unless you force it to re-render by updating the width.
// (See https://github.com/SteveSanderson/knockout/issues/312, http://stackoverflow.com/questions/5908494/select-only-shows-first-char-of-selected-option)
if (ieVersion >= 9) {
var originalWidth = selectElement.style.width;
selectElement.style.width = 0;
selectElement.style.width = originalWidth;
}
},
range: function (min, max) {
min = ko.utils.unwrapObservable(min);
max = ko.utils.unwrapObservable(max);
var result = [];
for (var i = min; i <= max; i++)
result.push(i);
return result;
},
makeArray: function(arrayLikeObject) {
var result = [];
for (var i = 0, j = arrayLikeObject.length; i < j; i++) {
result.push(arrayLikeObject[i]);
};
return result;
},
isIe6 : isIe6,
isIe7 : isIe7,
ieVersion : ieVersion,
getFormFields: function(form, fieldName) {
var fields = ko.utils.makeArray(form.getElementsByTagName("input")).concat(ko.utils.makeArray(form.getElementsByTagName("textarea")));
var isMatchingField = (typeof fieldName == 'string')
? function(field) { return field.name === fieldName }
: function(field) { return fieldName.test(field.name) }; // Treat fieldName as regex or object containing predicate
var matches = [];
for (var i = fields.length - 1; i >= 0; i--) {
if (isMatchingField(fields[i]))
matches.push(fields[i]);
};
return matches;
},
parseJson: function (jsonString) {
if (typeof jsonString == "string") {
jsonString = ko.utils.stringTrim(jsonString);
if (jsonString) {
if (window.JSON && window.JSON.parse) // Use native parsing where available
return window.JSON.parse(jsonString);
return (new Function("return " + jsonString))(); // Fallback on less safe parsing for older browsers
}
}
return null;
},
stringifyJson: function (data, replacer, space) { // replacer and space are optional
if ((typeof JSON == "undefined") || (typeof JSON.stringify == "undefined"))
throw new Error("Cannot find JSON.stringify(). Some browsers (e.g., IE < 8) don't support it natively, but you can overcome this by adding a script reference to json2.js, downloadable from http://www.json.org/json2.js");
return JSON.stringify(ko.utils.unwrapObservable(data), replacer, space);
},
postJson: function (urlOrForm, data, options) {
options = options || {};
var params = options['params'] || {};
var includeFields = options['includeFields'] || this.fieldsIncludedWithJsonPost;
var url = urlOrForm;
// If we were given a form, use its 'action' URL and pick out any requested field values
if((typeof urlOrForm == 'object') && (ko.utils.tagNameLower(urlOrForm) === "form")) {
var originalForm = urlOrForm;
url = originalForm.action;
for (var i = includeFields.length - 1; i >= 0; i--) {
var fields = ko.utils.getFormFields(originalForm, includeFields[i]);
for (var j = fields.length - 1; j >= 0; j--)
params[fields[j].name] = fields[j].value;
}
}
data = ko.utils.unwrapObservable(data);
var form = document.createElement("form");
form.style.display = "none";
form.action = url;
form.method = "post";
for (var key in data) {
var input = document.createElement("input");
input.name = key;
input.value = ko.utils.stringifyJson(ko.utils.unwrapObservable(data[key]));
form.appendChild(input);
}
for (var key in params) {
var input = document.createElement("input");
input.name = key;
input.value = params[key];
form.appendChild(input);
}
document.body.appendChild(form);
options['submitter'] ? options['submitter'](form) : form.submit();
setTimeout(function () { form.parentNode.removeChild(form); }, 0);
}
}
})();
ko.exportSymbol('utils', ko.utils);
ko.exportSymbol('utils.arrayForEach', ko.utils.arrayForEach);
ko.exportSymbol('utils.arrayFirst', ko.utils.arrayFirst);
ko.exportSymbol('utils.arrayFilter', ko.utils.arrayFilter);
ko.exportSymbol('utils.arrayGetDistinctValues', ko.utils.arrayGetDistinctValues);
ko.exportSymbol('utils.arrayIndexOf', ko.utils.arrayIndexOf);
ko.exportSymbol('utils.arrayMap', ko.utils.arrayMap);
ko.exportSymbol('utils.arrayPushAll', ko.utils.arrayPushAll);
ko.exportSymbol('utils.arrayRemoveItem', ko.utils.arrayRemoveItem);
ko.exportSymbol('utils.extend', ko.utils.extend);
ko.exportSymbol('utils.fieldsIncludedWithJsonPost', ko.utils.fieldsIncludedWithJsonPost);
ko.exportSymbol('utils.getFormFields', ko.utils.getFormFields);
ko.exportSymbol('utils.postJson', ko.utils.postJson);
ko.exportSymbol('utils.parseJson', ko.utils.parseJson);
ko.exportSymbol('utils.registerEventHandler', ko.utils.registerEventHandler);
ko.exportSymbol('utils.stringifyJson', ko.utils.stringifyJson);
ko.exportSymbol('utils.range', ko.utils.range);
ko.exportSymbol('utils.toggleDomNodeCssClass', ko.utils.toggleDomNodeCssClass);
ko.exportSymbol('utils.triggerEvent', ko.utils.triggerEvent);
ko.exportSymbol('utils.unwrapObservable', ko.utils.unwrapObservable);
if (!Function.prototype['bind']) {
// Function.prototype.bind is a standard part of ECMAScript 5th Edition (December 2009, http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-262.pdf)
// In case the browser doesn't implement it natively, provide a JavaScript implementation. This implementation is based on the one in prototype.js
Function.prototype['bind'] = function (object) {
var originalFunction = this, args = Array.prototype.slice.call(arguments), object = args.shift();
return function () {
return originalFunction.apply(object, args.concat(Array.prototype.slice.call(arguments)));
};
};
}
ko.utils.domData = new (function () {
var uniqueId = 0;
var dataStoreKeyExpandoPropertyName = "__ko__" + (new Date).getTime();
var dataStore = {};
return {
get: function (node, key) {
var allDataForNode = ko.utils.domData.getAll(node, false);
return allDataForNode === undefined ? undefined : allDataForNode[key];
},
set: function (node, key, value) {
if (value === undefined) {
// Make sure we don't actually create a new domData key if we are actually deleting a value
if (ko.utils.domData.getAll(node, false) === undefined)
return;
}
var allDataForNode = ko.utils.domData.getAll(node, true);
allDataForNode[key] = value;
},
getAll: function (node, createIfNotFound) {
var dataStoreKey = node[dataStoreKeyExpandoPropertyName];
var hasExistingDataStore = dataStoreKey && (dataStoreKey !== "null");
if (!hasExistingDataStore) {
if (!createIfNotFound)
return undefined;
dataStoreKey = node[dataStoreKeyExpandoPropertyName] = "ko" + uniqueId++;
dataStore[dataStoreKey] = {};
}
return dataStore[dataStoreKey];
},
clear: function (node) {
var dataStoreKey = node[dataStoreKeyExpandoPropertyName];
if (dataStoreKey) {
delete dataStore[dataStoreKey];
node[dataStoreKeyExpandoPropertyName] = null;
}
}
}
})();
ko.exportSymbol('utils.domData', ko.utils.domData);
ko.exportSymbol('utils.domData.clear', ko.utils.domData.clear); // Exporting only so specs can clear up after themselves fully
ko.utils.domNodeDisposal = new (function () {
var domDataKey = "__ko_domNodeDisposal__" + (new Date).getTime();
var cleanableNodeTypes = { 1: true, 8: true, 9: true }; // Element, Comment, Document
var cleanableNodeTypesWithDescendants = { 1: true, 9: true }; // Element, Document
function getDisposeCallbacksCollection(node, createIfNotFound) {
var allDisposeCallbacks = ko.utils.domData.get(node, domDataKey);
if ((allDisposeCallbacks === undefined) && createIfNotFound) {
allDisposeCallbacks = [];
ko.utils.domData.set(node, domDataKey, allDisposeCallbacks);
}
return allDisposeCallbacks;
}
function destroyCallbacksCollection(node) {
ko.utils.domData.set(node, domDataKey, undefined);
}
function cleanSingleNode(node) {
// Run all the dispose callbacks
var callbacks = getDisposeCallbacksCollection(node, false);
if (callbacks) {
callbacks = callbacks.slice(0); // Clone, as the array may be modified during iteration (typically, callbacks will remove themselves)
for (var i = 0; i < callbacks.length; i++)
callbacks[i](node);
}
// Also 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]);
// Also 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);
}
function cleanImmediateCommentTypeChildren(nodeWithChildren) {
var child, nextChild = nodeWithChildren.firstChild;
while (child = nextChild) {
nextChild = child.nextSibling;
if (child.nodeType === 8)
cleanSingleNode(child);
}
}
return {
addDisposeCallback : function(node, callback) {
if (typeof callback != "function")
throw new Error("Callback must be a function");
getDisposeCallbacksCollection(node, true).push(callback);
},
removeDisposeCallback : function(node, callback) {
var callbacksCollection = getDisposeCallbacksCollection(node, false);
if (callbacksCollection) {
ko.utils.arrayRemoveItem(callbacksCollection, callback);
if (callbacksCollection.length == 0)
destroyCallbacksCollection(node);
}
},
cleanNode : function(node) {
// First clean this node, where applicable
if (cleanableNodeTypes[node.nodeType]) {
cleanSingleNode(node);
// ... then its descendants, where applicable
if (cleanableNodeTypesWithDescendants[node.nodeType]) {
// Clone the descendants list in case it changes during iteration
var descendants = [];
ko.utils.arrayPushAll(descendants, node.getElementsByTagName("*"));
for (var i = 0, j = descendants.length; i < j; i++)
cleanSingleNode(descendants[i]);
}
}
},
removeNode : function(node) {
ko.cleanNode(node);
if (node.parentNode)
node.parentNode.removeChild(node);
}
}
})();
ko.cleanNode = ko.utils.domNodeDisposal.cleanNode; // Shorthand name for convenience
ko.removeNode = ko.utils.domNodeDisposal.removeNode; // Shorthand name for convenience
ko.exportSymbol('cleanNode', ko.cleanNode);
ko.exportSymbol('removeNode', ko.removeNode);
ko.exportSymbol('utils.domNodeDisposal', ko.utils.domNodeDisposal);
ko.exportSymbol('utils.domNodeDisposal.addDisposeCallback', ko.utils.domNodeDisposal.addDisposeCallback);
ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeDisposal.removeDisposeCallback);
(function () {
var leadingCommentRegex = /^(\s*)<!--(.*?)-->/;
function simpleHtmlParse(html) {
// Based on jQuery's "clean" function, but only accounting for table-related elements.
// If you have referenced jQuery, this won't be used anyway - KO will use jQuery's "clean" function directly
// Note that there's still an issue in IE < 9 whereby it will discard comment nodes that are the first child of
// a descendant node. For example: "<div><!-- mycomment -->abc</div>" will get parsed as "<div>abc</div>"
// This won't affect anyone who has referenced jQuery, and there's always the workaround of inserting a dummy node
// (possibly a text node) in front of the comment. So, KO does not attempt to workaround this IE issue automatically at present.
// Trim whitespace, otherwise indexOf won't work as expected
var tags = ko.utils.stringTrim(html).toLowerCase(), div = document.createElement("div");
// Finds the first match from the left column, and returns the corresponding "wrap" data from the right column
var wrap = tags.match(/^<(thead|tbody|tfoot)/) && [1, "<table>", "</table>"] ||
!tags.indexOf("<tr") && [2, "<table><tbody>", "</tbody></table>"] ||
(!tags.indexOf("<td") || !tags.indexOf("<th")) && [3, "<table><tbody><tr>", "</tr></tbody></table>"] ||
/* anything else */ [0, "", ""];
// Go to html and back, then peel off extra wrappers
// Note that we always prefix with some dummy text, because otherwise, IE<9 will strip out leading comment nodes in descendants. Total madness.
var markup = "ignored<div>" + wrap[1] + html + wrap[2] + "</div>";
if (typeof window['innerShiv'] == "function") {
div.appendChild(window['innerShiv'](markup));
} else {
div.innerHTML = markup;
}
// Move to the right depth
while (wrap[0]--)
div = div.lastChild;
return ko.utils.makeArray(div.lastChild.childNodes);
}
function jQueryHtmlParse(html) {
var elems = jQuery['clean']([html]);
// As of jQuery 1.7.1, jQuery parses the HTML by appending it to some dummy parent nodes held in an in-memory document fragment.
// Unfortunately, it never clears the dummy parent nodes from the document fragment, so it leaks memory over time.
// Fix this by finding the top-most dummy parent element, and detaching it from its owner fragment.
if (elems && elems[0]) {
// Find the top-most parent element that's a direct child of a document fragment
var elem = elems[0];
while (elem.parentNode && elem.parentNode.nodeType !== 11 /* i.e., DocumentFragment */)
elem = elem.parentNode;
// ... then detach it
if (elem.parentNode)
elem.parentNode.removeChild(elem);
}
return elems;
}
ko.utils.parseHtmlFragment = function(html) {
return typeof jQuery != 'undefined' ? jQueryHtmlParse(html) // As below, benefit from jQuery's optimisations where possible
: simpleHtmlParse(html); // ... otherwise, this simple logic will do in most common cases.
};
ko.utils.setHtml = function(node, html) {
ko.utils.emptyDomNode(node);
if ((html !== null) && (html !== undefined)) {
if (typeof html != 'string')
html = html.toString();
// 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') {
jQuery(node)['html'](html);
} else {
// ... otherwise, use KO's own parsing logic.
var parsedNodes = ko.utils.parseHtmlFragment(html);
for (var i = 0; i < parsedNodes.length; i++)
node.appendChild(parsedNodes[i]);
}
}
};
})();
ko.exportSymbol('utils.parseHtmlFragment', ko.utils.parseHtmlFragment);
ko.exportSymbol('utils.setHtml', ko.utils.setHtml);
ko.memoization = (function () {
var memos = {};
function randomMax8HexChars() {
return (((1 + Math.random()) * 0x100000000) | 0).toString(16).substring(1);
}
function generateRandomId() {
return randomMax8HexChars() + randomMax8HexChars();
}
function findMemoNodes(rootNode, appendToArray) {
if (!rootNode)
return;
if (rootNode.nodeType == 8) {
var memoId = ko.memoization.parseMemoText(rootNode.nodeValue);
if (memoId != null)
appendToArray.push({ domNode: rootNode, memoId: memoId });
} else if (rootNode.nodeType == 1) {
for (var i = 0, childNodes = rootNode.childNodes, j = childNodes.length; i < j; i++)
findMemoNodes(childNodes[i], appendToArray);
}
}
return {
memoize: function (callback) {
if (typeof callback != "function")
throw new Error("You can only pass a function to ko.memoization.memoize()");
var memoId = generateRandomId();
memos[memoId] = callback;
return "<!--[ko_memo:" + memoId + "]-->";
},
unmemoize: function (memoId, callbackParams) {
var callback = memos[memoId];
if (callback === undefined)
throw new Error("Couldn't find any memo with ID " + memoId + ". Perhaps it's already been unmemoized.");
try {
callback.apply(null, callbackParams || []);
return true;
}
finally { delete memos[memoId]; }
},
unmemoizeDomNodeAndDescendants: function (domNode, extraCallbackParamsArray) {
var memos = [];
findMemoNodes(domNode, memos);
for (var i = 0, j = memos.length; i < j; i++) {
var node = memos[i].domNode;
var combinedParams = [node];
if (extraCallbackParamsArray)
ko.utils.arrayPushAll(combinedParams, extraCallbackParamsArray);
ko.memoization.unmemoize(memos[i].memoId, combinedParams);
node.nodeValue = ""; // Neuter this node so we don't try to unmemoize it again
if (node.parentNode)
node.parentNode.removeChild(node); // If possible, erase it totally (not always possible - someone else might just hold a reference to it then call unmemoizeDomNodeAndDescendants again)
}
},
parseMemoText: function (memoText) {
var match = memoText.match(/^\[ko_memo\:(.*?)\]$/);
return match ? match[1] : null;
}
};
})();
ko.exportSymbol('memoization', ko.memoization);
ko.exportSymbol('memoization.memoize', ko.memoization.memoize);
ko.exportSymbol('memoization.unmemoize', ko.memoization.unmemoize);
ko.exportSymbol('memoization.parseMemoText', ko.memoization.parseMemoText);
ko.exportSymbol('memoization.unmemoizeDomNodeAndDescendants', ko.memoization.unmemoizeDomNodeAndDescendants);
ko.extenders = {
'throttle': function(target, timeout) {
// Throttling means two things:
// (1) For dependent observables, we throttle *evaluations* so that, no matter how fast its dependencies
// notify updates, the target doesn't re-evaluate (and hence doesn't notify) faster than a certain rate
target['throttleEvaluation'] = timeout;
// (2) For writable targets (observables, or writable dependent observables), we throttle *writes*
// so the target cannot change value synchronously or faster than a certain rate
var writeTimeoutInstance = null;
return ko.dependentObservable({
'read': target,
'write': function(value) {
clearTimeout(writeTimeoutInstance);
writeTimeoutInstance = setTimeout(function() {
target(value);
}, timeout);
}
});
},
'notify': function(target, notifyWhen) {
target["equalityComparer"] = notifyWhen == "always"
? function() { return false } // Treat all values as not equal
: ko.observable["fn"]["equalityComparer"];
return target;
}
};
function applyExtenders(requestedExtenders) {
var target = this;
if (requestedExtenders) {
for (var key in requestedExtenders) {
var extenderHandler = ko.extenders[key];
if (typeof extenderHandler == 'function') {
target = extenderHandler(target, requestedExtenders[key]);
}
}
}
return target;
}
ko.exportSymbol('extenders', ko.extenders);
ko.subscription = function (target, callback, disposeCallback) {
this.target = target;
this.callback = callback;
this.disposeCallback = disposeCallback;
ko.exportProperty(this, 'dispose', this.dispose);
};
ko.subscription.prototype.dispose = function () {
this.isDisposed = true;
this.disposeCallback();
};
ko.subscribable = function () {
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'] = {
subscribe: function (callback, callbackTarget, event) {
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));
if (!this._subscriptions[event])
this._subscriptions[event] = [];
this._subscriptions[event].push(subscription);
return subscription;
},
"notifySubscribers": function (valueToNotify, event) {
event = event || defaultEvent;
if (this._subscriptions[event]) {
ko.utils.arrayForEach(this._subscriptions[event].slice(0), function (subscription) {
// In case a subscription was disposed during the arrayForEach cycle, check
// for isDisposed on each subscription before invoking its callback
if (subscription && (subscription.isDisposed !== true))
subscription.callback(valueToNotify);
});
}
},
getSubscriptionsCount: function () {
var total = 0;
for (var eventName in this._subscriptions) {
if (this._subscriptions.hasOwnProperty(eventName))
total += this._subscriptions[eventName].length;
}
return total;
},
extend: applyExtenders
};
ko.isSubscribable = function (instance) {
return typeof instance.subscribe == "function" && typeof instance["notifySubscribers"] == "function";
};
ko.exportSymbol('subscribable', ko.subscribable);
ko.exportSymbol('isSubscribable', ko.isSubscribable);
ko.dependencyDetection = (function () {
var _frames = [];
return {
begin: function (callback) {
_frames.push({ callback: callback, distinctDependencies:[] });
},
end: function () {
_frames.pop();
},
registerDependency: function (subscribable) {
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 (ko.utils.arrayIndexOf(topFrame.distinctDependencies, subscribable) >= 0)
return;
topFrame.distinctDependencies.push(subscribable);
topFrame.callback(subscribable);
}
}
};
})();
var primitiveTypes = { 'undefined':true, 'boolean':true, 'number':true, 'string':true };
ko.observable = function (initialValue) {
var _latestValue = initialValue;
function observable() {
if (arguments.length > 0) {
// Write
// Ignore writes if the value hasn't changed
if ((!observable['equalityComparer']) || !observable['equalityComparer'](_latestValue, arguments[0])) {
observable.valueWillMutate();
_latestValue = arguments[0];
if (DEBUG) observable._latestValue = _latestValue;
observable.valueHasMutated();
}
return this; // Permits chained assignments
}
else {
// Read
ko.dependencyDetection.registerDependency(observable); // The caller only needs to be notified of changes if they did a "read" operation
return _latestValue;
}
}
if (DEBUG) observable._latestValue = _latestValue;
ko.subscribable.call(observable);
observable.valueHasMutated = function () { observable["notifySubscribers"](_latestValue); }
observable.valueWillMutate = function () { observable["notifySubscribers"](_latestValue, "beforeChange"); }
ko.utils.extend(observable, ko.observable['fn']);
ko.exportProperty(observable, "valueHasMutated", observable.valueHasMutated);
ko.exportProperty(observable, "valueWillMutate", observable.valueWillMutate);
return observable;
}
ko.observable['fn'] = {
"equalityComparer": function valuesArePrimitiveAndEqual(a, b) {
var oldValueIsPrimitive = (a === null) || (typeof(a) in primitiveTypes);
return oldValueIsPrimitive ? (a === b) : false;
}
};
var protoProperty = ko.observable.protoProperty = "__ko_proto__";
ko.observable['fn'][protoProperty] = ko.observable;
ko.hasPrototype = function(instance, prototype) {
if ((instance === null) || (instance === undefined) || (instance[protoProperty] === undefined)) return false;
if (instance[protoProperty] === prototype) return true;
return ko.hasPrototype(instance[protoProperty], prototype); // Walk the prototype chain
};
ko.isObservable = function (instance) {
return ko.hasPrototype(instance, ko.observable);
}
ko.isWriteableObservable = function (instance) {
// Observable
if ((typeof instance == "function") && instance[protoProperty] === ko.observable)
return true;
// Writeable dependent observable
if ((typeof instance == "function") && (instance[protoProperty] === ko.dependentObservable) && (instance.hasWriteFunction))
return true;
// Anything else
return false;
}
ko.exportSymbol('observable', ko.observable);
ko.exportSymbol('isObservable', ko.isObservable);
ko.exportSymbol('isWriteableObservable', ko.isWriteableObservable);
ko.observableArray = function (initialValues) {
if (arguments.length == 0) {
// Zero-parameter constructor initializes to empty array
initialValues = [];
}
if ((initialValues !== null) && (initialValues !== undefined) && !('length' in 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']);
return result;
}
ko.observableArray['fn'] = {
'remove': function (valueOrPredicate) {
var underlyingArray = this();
var removedValues = [];
var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) { return value === valueOrPredicate; };
for (var i = 0; i < underlyingArray.length; i++) {
var value = underlyingArray[i];
if (predicate(value)) {
if (removedValues.length === 0) {
this.valueWillMutate();
}
removedValues.push(value);
underlyingArray.splice(i, 1);
i--;
}
}
if (removedValues.length) {
this.valueHasMutated();
}
return removedValues;
},
'removeAll': function (arrayOfValues) {
// If you passed zero args, we remove everything
if (arrayOfValues === undefined) {
var underlyingArray = this();
var allValues = underlyingArray.slice(0);
this.valueWillMutate();
underlyingArray.splice(0, underlyingArray.length);
this.valueHasMutated();
return allValues;
}
// If you passed an arg, we interpret it as an array of entries to remove
if (!arrayOfValues)
return [];
return this['remove'](function (value) {
return ko.utils.arrayIndexOf(arrayOfValues, value) >= 0;
});
},
'destroy': function (valueOrPredicate) {
var underlyingArray = this();
var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) { return value === valueOrPredicate; };
this.valueWillMutate();
for (var i = underlyingArray.length - 1; i >= 0; i--) {
var value = underlyingArray[i];
if (predicate(value))
underlyingArray[i]["_destroy"] = true;
}
this.valueHasMutated();
},
'destroyAll': function (arrayOfValues) {
// If you passed zero args, we destroy everything
if (arrayOfValues === undefined)
return this['destroy'](function() { return true });
// If you passed an arg, we interpret it as an array of entries to destroy
if (!arrayOfValues)
return [];
return this['destroy'](function (value) {
return ko.utils.arrayIndexOf(arrayOfValues, value) >= 0;
});
},
'indexOf': function (item) {
var underlyingArray = this();
return ko.utils.arrayIndexOf(underlyingArray, item);
},
'replace': function(oldItem, newItem) {
var index = this['indexOf'](oldItem);
if (index >= 0) {
this.valueWillMutate();
this()[index] = newItem;
this.valueHasMutated();
}
}
}
// Populate ko.observableArray.fn with read/write functions from native arrays
ko.utils.arrayForEach(["pop", "push", "reverse", "shift", "sort", "splice", "unshift"], function (methodName) {
ko.observableArray['fn'][methodName] = function () {
var underlyingArray = this();
this.valueWillMutate();
var methodCallResult = underlyingArray[methodName].apply(underlyingArray, arguments);
this.valueHasMutated();
return methodCallResult;
};
});
// Populate ko.observableArray.fn with read-only functions from native arrays
ko.utils.arrayForEach(["slice"], function (methodName) {
ko.observableArray['fn'][methodName] = function () {
var underlyingArray = this();
return underlyingArray[methodName].apply(underlyingArray, arguments);
};
});
ko.exportSymbol('observableArray', ko.observableArray);
ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget, options) {
var _latestValue,
_hasBeenEvaluated = false,
_isBeingEvaluated = false,
readFunction = evaluatorFunctionOrOptions;
if (readFunction && typeof readFunction == "object") {
// Single-parameter syntax - everything is on this "options" param
options = readFunction;
readFunction = options["read"];
} else {
// Multi-parameter syntax - construct the options according to the params passed
options = options || {};
if (!readFunction)
readFunction = options["read"];
}
// By here, "options" is always non-null
if (typeof readFunction != "function")
throw new Error("Pass a function that returns the value of the ko.computed");
var writeFunction = options["write"];
if (!evaluatorFunctionTarget)
evaluatorFunctionTarget = options["owner"];
var _subscriptionsToDependencies = [];
function disposeAllSubscriptionsToDependencies() {
ko.utils.arrayForEach(_subscriptionsToDependencies, function (subscription) {
subscription.dispose();
});
_subscriptionsToDependencies = [];
}
var dispose = disposeAllSubscriptionsToDependencies;
// Build "disposeWhenNodeIsRemoved" and "disposeWhenNodeIsRemovedCallback" option values
// (Note: "disposeWhenNodeIsRemoved" option both proactively disposes as soon as the node is removed using ko.removeNode(),
// plus adds a "dispose