knockout.js
6558 lines
| 1 | /*! |
| 2 | * Knockout JavaScript library v3.5.0 |
| 3 | * (c) The Knockout.js team - http://knockoutjs.com/ |
| 4 | * License: MIT (http://www.opensource.org/licenses/mit-license.php) |
| 5 | */ |
| 6 | |
| 7 | (function(){ |
| 8 | var DEBUG=true; |
| 9 | (function(undefined){ |
| 10 | // (0, eval)('this') is a robust way of getting a reference to the global object |
| 11 | // For details, see http://stackoverflow.com/questions/14119988/return-this-0-evalthis/14120023#14120023 |
| 12 | var window = this || (0, eval)('this'), |
| 13 | document = window['document'], |
| 14 | navigator = window['navigator'], |
| 15 | jQueryInstance = window["jQuery"], |
| 16 | JSON = window["JSON"]; |
| 17 | |
| 18 | if (!jQueryInstance && typeof jQuery !== "undefined") { |
| 19 | jQueryInstance = jQuery; |
| 20 | } |
| 21 | (function(factory) { |
| 22 | // Support three module loading scenarios |
| 23 | if (typeof define === 'function' && define['amd']) { |
| 24 | // [1] AMD anonymous module |
| 25 | define(['exports', 'require'], factory); |
| 26 | } else if (typeof exports === 'object' && typeof module === 'object') { |
| 27 | // [2] CommonJS/Node.js |
| 28 | factory(module['exports'] || exports); // module.exports is for Node.js |
| 29 | } else { |
| 30 | // [3] No module loader (plain <script> tag) - put directly in global namespace |
| 31 | factory(window['ko'] = {}); |
| 32 | } |
| 33 | }(function(koExports, amdRequire){ |
| 34 | // Internally, all KO objects are attached to koExports (even the non-exported ones whose names will be minified by the closure compiler). |
| 35 | // In the future, the following "ko" variable may be made distinct from "koExports" so that private objects are not externally reachable. |
| 36 | var ko = typeof koExports !== 'undefined' ? koExports : {}; |
| 37 | // Google Closure Compiler helpers (used only to make the minified file smaller) |
| 38 | ko.exportSymbol = function(koPath, object) { |
| 39 | var tokens = koPath.split("."); |
| 40 | |
| 41 | // In the future, "ko" may become distinct from "koExports" (so that non-exported objects are not reachable) |
| 42 | // At that point, "target" would be set to: (typeof koExports !== "undefined" ? koExports : ko) |
| 43 | var target = ko; |
| 44 | |
| 45 | for (var i = 0; i < tokens.length - 1; i++) |
| 46 | target = target[tokens[i]]; |
| 47 | target[tokens[tokens.length - 1]] = object; |
| 48 | }; |
| 49 | ko.exportProperty = function(owner, publicName, object) { |
| 50 | owner[publicName] = object; |
| 51 | }; |
| 52 | ko.version = "3.5.0"; |
| 53 | |
| 54 | ko.exportSymbol('version', ko.version); |
| 55 | // For any options that may affect various areas of Knockout and aren't directly associated with data binding. |
| 56 | ko.options = { |
| 57 | 'deferUpdates': false, |
| 58 | 'useOnlyNativeEvents': false, |
| 59 | 'foreachHidesDestroyed': false |
| 60 | }; |
| 61 | |
| 62 | //ko.exportSymbol('options', ko.options); // 'options' isn't minified |
| 63 | ko.utils = (function () { |
| 64 | var hasOwnProperty = Object.prototype.hasOwnProperty; |
| 65 | |
| 66 | function objectForEach(obj, action) { |
| 67 | for (var prop in obj) { |
| 68 | if (hasOwnProperty.call(obj, prop)) { |
| 69 | action(prop, obj[prop]); |
| 70 | } |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | function extend(target, source) { |
| 75 | if (source) { |
| 76 | for(var prop in source) { |
| 77 | if(hasOwnProperty.call(source, prop)) { |
| 78 | target[prop] = source[prop]; |
| 79 | } |
| 80 | } |
| 81 | } |
| 82 | return target; |
| 83 | } |
| 84 | |
| 85 | function setPrototypeOf(obj, proto) { |
| 86 | obj.__proto__ = proto; |
| 87 | return obj; |
| 88 | } |
| 89 | |
| 90 | var canSetPrototype = ({ __proto__: [] } instanceof Array); |
| 91 | var canUseSymbols = !DEBUG && typeof Symbol === 'function'; |
| 92 | |
| 93 | // 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) |
| 94 | var knownEvents = {}, knownEventTypesByEventName = {}; |
| 95 | var keyEventTypeName = (navigator && /Firefox\/2/i.test(navigator.userAgent)) ? 'KeyboardEvent' : 'UIEvents'; |
| 96 | knownEvents[keyEventTypeName] = ['keyup', 'keydown', 'keypress']; |
| 97 | knownEvents['MouseEvents'] = ['click', 'dblclick', 'mousedown', 'mouseup', 'mousemove', 'mouseover', 'mouseout', 'mouseenter', 'mouseleave']; |
| 98 | objectForEach(knownEvents, function(eventType, knownEventsForType) { |
| 99 | if (knownEventsForType.length) { |
| 100 | for (var i = 0, j = knownEventsForType.length; i < j; i++) |
| 101 | knownEventTypesByEventName[knownEventsForType[i]] = eventType; |
| 102 | } |
| 103 | }); |
| 104 | var eventsThatMustBeRegisteredUsingAttachEvent = { 'propertychange': true }; // Workaround for an IE9 issue - https://github.com/SteveSanderson/knockout/issues/406 |
| 105 | |
| 106 | // Detect IE versions for bug workarounds (uses IE conditionals, not UA string, for robustness) |
| 107 | // Note that, since IE 10 does not support conditional comments, the following logic only detects IE < 10. |
| 108 | // Currently this is by design, since IE 10+ behaves correctly when treated as a standard browser. |
| 109 | // If there is a future need to detect specific versions of IE10+, we will amend this. |
| 110 | var ieVersion = document && (function() { |
| 111 | var version = 3, div = document.createElement('div'), iElems = div.getElementsByTagName('i'); |
| 112 | |
| 113 | // Keep constructing conditional HTML blocks until we hit one that resolves to an empty fragment |
| 114 | while ( |
| 115 | div.innerHTML = '<!--[if gt IE ' + (++version) + ']><i></i><![endif]-->', |
| 116 | iElems[0] |
| 117 | ) {} |
| 118 | return version > 4 ? version : undefined; |
| 119 | }()); |
| 120 | var isIe6 = ieVersion === 6, |
| 121 | isIe7 = ieVersion === 7; |
| 122 | |
| 123 | function isClickOnCheckableElement(element, eventType) { |
| 124 | if ((ko.utils.tagNameLower(element) !== "input") || !element.type) return false; |
| 125 | if (eventType.toLowerCase() != "click") return false; |
| 126 | var inputType = element.type; |
| 127 | return (inputType == "checkbox") || (inputType == "radio"); |
| 128 | } |
| 129 | |
| 130 | // For details on the pattern for changing node classes |
| 131 | // see: https://github.com/knockout/knockout/issues/1597 |
| 132 | var cssClassNameRegex = /\S+/g; |
| 133 | |
| 134 | var jQueryEventAttachName; |
| 135 | |
| 136 | function toggleDomNodeCssClass(node, classNames, shouldHaveClass) { |
| 137 | var addOrRemoveFn; |
| 138 | if (classNames) { |
| 139 | if (typeof node.classList === 'object') { |
| 140 | addOrRemoveFn = node.classList[shouldHaveClass ? 'add' : 'remove']; |
| 141 | ko.utils.arrayForEach(classNames.match(cssClassNameRegex), function(className) { |
| 142 | addOrRemoveFn.call(node.classList, className); |
| 143 | }); |
| 144 | } else if (typeof node.className['baseVal'] === 'string') { |
| 145 | // SVG tag .classNames is an SVGAnimatedString instance |
| 146 | toggleObjectClassPropertyString(node.className, 'baseVal', classNames, shouldHaveClass); |
| 147 | } else { |
| 148 | // node.className ought to be a string. |
| 149 | toggleObjectClassPropertyString(node, 'className', classNames, shouldHaveClass); |
| 150 | } |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | function toggleObjectClassPropertyString(obj, prop, classNames, shouldHaveClass) { |
| 155 | // obj/prop is either a node/'className' or a SVGAnimatedString/'baseVal'. |
| 156 | var currentClassNames = obj[prop].match(cssClassNameRegex) || []; |
| 157 | ko.utils.arrayForEach(classNames.match(cssClassNameRegex), function(className) { |
| 158 | ko.utils.addOrRemoveItem(currentClassNames, className, shouldHaveClass); |
| 159 | }); |
| 160 | obj[prop] = currentClassNames.join(" "); |
| 161 | } |
| 162 | |
| 163 | return { |
| 164 | fieldsIncludedWithJsonPost: ['authenticity_token', /^__RequestVerificationToken(_.*)?$/], |
| 165 | |
| 166 | arrayForEach: function (array, action, actionOwner) { |
| 167 | for (var i = 0, j = array.length; i < j; i++) { |
| 168 | action.call(actionOwner, array[i], i, array); |
| 169 | } |
| 170 | }, |
| 171 | |
| 172 | arrayIndexOf: typeof Array.prototype.indexOf == "function" |
| 173 | ? function (array, item) { |
| 174 | return Array.prototype.indexOf.call(array, item); |
| 175 | } |
| 176 | : function (array, item) { |
| 177 | for (var i = 0, j = array.length; i < j; i++) { |
| 178 | if (array[i] === item) |
| 179 | return i; |
| 180 | } |
| 181 | return -1; |
| 182 | }, |
| 183 | |
| 184 | arrayFirst: function (array, predicate, predicateOwner) { |
| 185 | for (var i = 0, j = array.length; i < j; i++) { |
| 186 | if (predicate.call(predicateOwner, array[i], i, array)) |
| 187 | return array[i]; |
| 188 | } |
| 189 | return undefined; |
| 190 | }, |
| 191 | |
| 192 | arrayRemoveItem: function (array, itemToRemove) { |
| 193 | var index = ko.utils.arrayIndexOf(array, itemToRemove); |
| 194 | if (index > 0) { |
| 195 | array.splice(index, 1); |
| 196 | } |
| 197 | else if (index === 0) { |
| 198 | array.shift(); |
| 199 | } |
| 200 | }, |
| 201 | |
| 202 | arrayGetDistinctValues: function (array) { |
| 203 | var result = []; |
| 204 | if (array) { |
| 205 | ko.utils.arrayForEach(array, function(item) { |
| 206 | if (ko.utils.arrayIndexOf(result, item) < 0) |
| 207 | result.push(item); |
| 208 | }); |
| 209 | } |
| 210 | return result; |
| 211 | }, |
| 212 | |
| 213 | arrayMap: function (array, mapping, mappingOwner) { |
| 214 | var result = []; |
| 215 | if (array) { |
| 216 | for (var i = 0, j = array.length; i < j; i++) |
| 217 | result.push(mapping.call(mappingOwner, array[i], i)); |
| 218 | } |
| 219 | return result; |
| 220 | }, |
| 221 | |
| 222 | arrayFilter: function (array, predicate, predicateOwner) { |
| 223 | var result = []; |
| 224 | if (array) { |
| 225 | for (var i = 0, j = array.length; i < j; i++) |
| 226 | if (predicate.call(predicateOwner, array[i], i)) |
| 227 | result.push(array[i]); |
| 228 | } |
| 229 | return result; |
| 230 | }, |
| 231 | |
| 232 | arrayPushAll: function (array, valuesToPush) { |
| 233 | if (valuesToPush instanceof Array) |
| 234 | array.push.apply(array, valuesToPush); |
| 235 | else |
| 236 | for (var i = 0, j = valuesToPush.length; i < j; i++) |
| 237 | array.push(valuesToPush[i]); |
| 238 | return array; |
| 239 | }, |
| 240 | |
| 241 | addOrRemoveItem: function(array, value, included) { |
| 242 | var existingEntryIndex = ko.utils.arrayIndexOf(ko.utils.peekObservable(array), value); |
| 243 | if (existingEntryIndex < 0) { |
| 244 | if (included) |
| 245 | array.push(value); |
| 246 | } else { |
| 247 | if (!included) |
| 248 | array.splice(existingEntryIndex, 1); |
| 249 | } |
| 250 | }, |
| 251 | |
| 252 | canSetPrototype: canSetPrototype, |
| 253 | |
| 254 | extend: extend, |
| 255 | |
| 256 | setPrototypeOf: setPrototypeOf, |
| 257 | |
| 258 | setPrototypeOfOrExtend: canSetPrototype ? setPrototypeOf : extend, |
| 259 | |
| 260 | objectForEach: objectForEach, |
| 261 | |
| 262 | objectMap: function(source, mapping, mappingOwner) { |
| 263 | if (!source) |
| 264 | return source; |
| 265 | var target = {}; |
| 266 | for (var prop in source) { |
| 267 | if (hasOwnProperty.call(source, prop)) { |
| 268 | target[prop] = mapping.call(mappingOwner, source[prop], prop, source); |
| 269 | } |
| 270 | } |
| 271 | return target; |
| 272 | }, |
| 273 | |
| 274 | emptyDomNode: function (domNode) { |
| 275 | while (domNode.firstChild) { |
| 276 | ko.removeNode(domNode.firstChild); |
| 277 | } |
| 278 | }, |
| 279 | |
| 280 | moveCleanedNodesToContainerElement: function(nodes) { |
| 281 | // Ensure it's a real array, as we're about to reparent the nodes and |
| 282 | // we don't want the underlying collection to change while we're doing that. |
| 283 | var nodesArray = ko.utils.makeArray(nodes); |
| 284 | var templateDocument = (nodesArray[0] && nodesArray[0].ownerDocument) || document; |
| 285 | |
| 286 | var container = templateDocument.createElement('div'); |
| 287 | for (var i = 0, j = nodesArray.length; i < j; i++) { |
| 288 | container.appendChild(ko.cleanNode(nodesArray[i])); |
| 289 | } |
| 290 | return container; |
| 291 | }, |
| 292 | |
| 293 | cloneNodes: function (nodesArray, shouldCleanNodes) { |
| 294 | for (var i = 0, j = nodesArray.length, newNodesArray = []; i < j; i++) { |
| 295 | var clonedNode = nodesArray[i].cloneNode(true); |
| 296 | newNodesArray.push(shouldCleanNodes ? ko.cleanNode(clonedNode) : clonedNode); |
| 297 | } |
| 298 | return newNodesArray; |
| 299 | }, |
| 300 | |
| 301 | setDomNodeChildren: function (domNode, childNodes) { |
| 302 | ko.utils.emptyDomNode(domNode); |
| 303 | if (childNodes) { |
| 304 | for (var i = 0, j = childNodes.length; i < j; i++) |
| 305 | domNode.appendChild(childNodes[i]); |
| 306 | } |
| 307 | }, |
| 308 | |
| 309 | replaceDomNodes: function (nodeToReplaceOrNodeArray, newNodesArray) { |
| 310 | var nodesToReplaceArray = nodeToReplaceOrNodeArray.nodeType ? [nodeToReplaceOrNodeArray] : nodeToReplaceOrNodeArray; |
| 311 | if (nodesToReplaceArray.length > 0) { |
| 312 | var insertionPoint = nodesToReplaceArray[0]; |
| 313 | var parent = insertionPoint.parentNode; |
| 314 | for (var i = 0, j = newNodesArray.length; i < j; i++) |
| 315 | parent.insertBefore(newNodesArray[i], insertionPoint); |
| 316 | for (var i = 0, j = nodesToReplaceArray.length; i < j; i++) { |
| 317 | ko.removeNode(nodesToReplaceArray[i]); |
| 318 | } |
| 319 | } |
| 320 | }, |
| 321 | |
| 322 | fixUpContinuousNodeArray: function(continuousNodeArray, parentNode) { |
| 323 | // Before acting on a set of nodes that were previously outputted by a template function, we have to reconcile |
| 324 | // them against what is in the DOM right now. It may be that some of the nodes have already been removed, or that |
| 325 | // new nodes might have been inserted in the middle, for example by a binding. Also, there may previously have been |
| 326 | // leading comment nodes (created by rewritten string-based templates) that have since been removed during binding. |
| 327 | // So, this function translates the old "map" output array into its best guess of the set of current DOM nodes. |
| 328 | // |
| 329 | // Rules: |
| 330 | // [A] Any leading nodes that have been removed should be ignored |
| 331 | // These most likely correspond to memoization nodes that were already removed during binding |
| 332 | // See https://github.com/knockout/knockout/pull/440 |
| 333 | // [B] Any trailing nodes that have been remove should be ignored |
| 334 | // This prevents the code here from adding unrelated nodes to the array while processing rule [C] |
| 335 | // See https://github.com/knockout/knockout/pull/1903 |
| 336 | // [C] We want to output a continuous series of nodes. So, ignore any nodes that have already been removed, |
| 337 | // and include any nodes that have been inserted among the previous collection |
| 338 | |
| 339 | if (continuousNodeArray.length) { |
| 340 | // The parent node can be a virtual element; so get the real parent node |
| 341 | parentNode = (parentNode.nodeType === 8 && parentNode.parentNode) || parentNode; |
| 342 | |
| 343 | // Rule [A] |
| 344 | while (continuousNodeArray.length && continuousNodeArray[0].parentNode !== parentNode) |
| 345 | continuousNodeArray.splice(0, 1); |
| 346 | |
| 347 | // Rule [B] |
| 348 | while (continuousNodeArray.length > 1 && continuousNodeArray[continuousNodeArray.length - 1].parentNode !== parentNode) |
| 349 | continuousNodeArray.length--; |
| 350 | |
| 351 | // Rule [C] |
| 352 | if (continuousNodeArray.length > 1) { |
| 353 | var current = continuousNodeArray[0], last = continuousNodeArray[continuousNodeArray.length - 1]; |
| 354 | // Replace with the actual new continuous node set |
| 355 | continuousNodeArray.length = 0; |
| 356 | while (current !== last) { |
| 357 | continuousNodeArray.push(current); |
| 358 | current = current.nextSibling; |
| 359 | } |
| 360 | continuousNodeArray.push(last); |
| 361 | } |
| 362 | } |
| 363 | return continuousNodeArray; |
| 364 | }, |
| 365 | |
| 366 | setOptionNodeSelectionState: function (optionNode, isSelected) { |
| 367 | // IE6 sometimes throws "unknown error" if you try to write to .selected directly, whereas Firefox struggles with setAttribute. Pick one based on browser. |
| 368 | if (ieVersion < 7) |
| 369 | optionNode.setAttribute("selected", isSelected); |
| 370 | else |
| 371 | optionNode.selected = isSelected; |
| 372 | }, |
| 373 | |
| 374 | stringTrim: function (string) { |
| 375 | return string === null || string === undefined ? '' : |
| 376 | string.trim ? |
| 377 | string.trim() : |
| 378 | string.toString().replace(/^[\s\xa0]+|[\s\xa0]+$/g, ''); |
| 379 | }, |
| 380 | |
| 381 | stringStartsWith: function (string, startsWith) { |
| 382 | string = string || ""; |
| 383 | if (startsWith.length > string.length) |
| 384 | return false; |
| 385 | return string.substring(0, startsWith.length) === startsWith; |
| 386 | }, |
| 387 | |
| 388 | domNodeIsContainedBy: function (node, containedByNode) { |
| 389 | if (node === containedByNode) |
| 390 | return true; |
| 391 | if (node.nodeType === 11) |
| 392 | return false; // Fixes issue #1162 - can't use node.contains for document fragments on IE8 |
| 393 | if (containedByNode.contains) |
| 394 | return containedByNode.contains(node.nodeType !== 1 ? node.parentNode : node); |
| 395 | if (containedByNode.compareDocumentPosition) |
| 396 | return (containedByNode.compareDocumentPosition(node) & 16) == 16; |
| 397 | while (node && node != containedByNode) { |
| 398 | node = node.parentNode; |
| 399 | } |
| 400 | return !!node; |
| 401 | }, |
| 402 | |
| 403 | domNodeIsAttachedToDocument: function (node) { |
| 404 | return ko.utils.domNodeIsContainedBy(node, node.ownerDocument.documentElement); |
| 405 | }, |
| 406 | |
| 407 | anyDomNodeIsAttachedToDocument: function(nodes) { |
| 408 | return !!ko.utils.arrayFirst(nodes, ko.utils.domNodeIsAttachedToDocument); |
| 409 | }, |
| 410 | |
| 411 | tagNameLower: function(element) { |
| 412 | // For HTML elements, tagName will always be upper case; for XHTML elements, it'll be lower case. |
| 413 | // Possible future optimization: If we know it's an element from an XHTML document (not HTML), |
| 414 | // we don't need to do the .toLowerCase() as it will always be lower case anyway. |
| 415 | return element && element.tagName && element.tagName.toLowerCase(); |
| 416 | }, |
| 417 | |
| 418 | catchFunctionErrors: function (delegate) { |
| 419 | return ko['onError'] ? function () { |
| 420 | try { |
| 421 | return delegate.apply(this, arguments); |
| 422 | } catch (e) { |
| 423 | ko['onError'] && ko['onError'](e); |
| 424 | throw e; |
| 425 | } |
| 426 | } : delegate; |
| 427 | }, |
| 428 | |
| 429 | setTimeout: function (handler, timeout) { |
| 430 | return setTimeout(ko.utils.catchFunctionErrors(handler), timeout); |
| 431 | }, |
| 432 | |
| 433 | deferError: function (error) { |
| 434 | setTimeout(function () { |
| 435 | ko['onError'] && ko['onError'](error); |
| 436 | throw error; |
| 437 | }, 0); |
| 438 | }, |
| 439 | |
| 440 | registerEventHandler: function (element, eventType, handler) { |
| 441 | var wrappedHandler = ko.utils.catchFunctionErrors(handler); |
| 442 | |
| 443 | var mustUseAttachEvent = eventsThatMustBeRegisteredUsingAttachEvent[eventType]; |
| 444 | if (!ko.options['useOnlyNativeEvents'] && !mustUseAttachEvent && jQueryInstance) { |
| 445 | if (!jQueryEventAttachName) { |
| 446 | jQueryEventAttachName = (typeof jQueryInstance(element)['on'] == 'function') ? 'on' : 'bind'; |
| 447 | } |
| 448 | jQueryInstance(element)[jQueryEventAttachName](eventType, wrappedHandler); |
| 449 | } else if (!mustUseAttachEvent && typeof element.addEventListener == "function") |
| 450 | element.addEventListener(eventType, wrappedHandler, false); |
| 451 | else if (typeof element.attachEvent != "undefined") { |
| 452 | var attachEventHandler = function (event) { wrappedHandler.call(element, event); }, |
| 453 | attachEventName = "on" + eventType; |
| 454 | element.attachEvent(attachEventName, attachEventHandler); |
| 455 | |
| 456 | // IE does not dispose attachEvent handlers automatically (unlike with addEventListener) |
| 457 | // so to avoid leaks, we have to remove them manually. See bug #856 |
| 458 | ko.utils.domNodeDisposal.addDisposeCallback(element, function() { |
| 459 | element.detachEvent(attachEventName, attachEventHandler); |
| 460 | }); |
| 461 | } else |
| 462 | throw new Error("Browser doesn't support addEventListener or attachEvent"); |
| 463 | }, |
| 464 | |
| 465 | triggerEvent: function (element, eventType) { |
| 466 | if (!(element && element.nodeType)) |
| 467 | throw new Error("element must be a DOM node when calling triggerEvent"); |
| 468 | |
| 469 | // For click events on checkboxes and radio buttons, jQuery toggles the element checked state *after* the |
| 470 | // event handler runs instead of *before*. (This was fixed in 1.9 for checkboxes but not for radio buttons.) |
| 471 | // IE doesn't change the checked state when you trigger the click event using "fireEvent". |
| 472 | // In both cases, we'll use the click method instead. |
| 473 | var useClickWorkaround = isClickOnCheckableElement(element, eventType); |
| 474 | |
| 475 | if (!ko.options['useOnlyNativeEvents'] && jQueryInstance && !useClickWorkaround) { |
| 476 | jQueryInstance(element)['trigger'](eventType); |
| 477 | } else if (typeof document.createEvent == "function") { |
| 478 | if (typeof element.dispatchEvent == "function") { |
| 479 | var eventCategory = knownEventTypesByEventName[eventType] || "HTMLEvents"; |
| 480 | var event = document.createEvent(eventCategory); |
| 481 | event.initEvent(eventType, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, element); |
| 482 | element.dispatchEvent(event); |
| 483 | } |
| 484 | else |
| 485 | throw new Error("The supplied element doesn't support dispatchEvent"); |
| 486 | } else if (useClickWorkaround && element.click) { |
| 487 | element.click(); |
| 488 | } else if (typeof element.fireEvent != "undefined") { |
| 489 | element.fireEvent("on" + eventType); |
| 490 | } else { |
| 491 | throw new Error("Browser doesn't support triggering events"); |
| 492 | } |
| 493 | }, |
| 494 | |
| 495 | unwrapObservable: function (value) { |
| 496 | return ko.isObservable(value) ? value() : value; |
| 497 | }, |
| 498 | |
| 499 | peekObservable: function (value) { |
| 500 | return ko.isObservable(value) ? value.peek() : value; |
| 501 | }, |
| 502 | |
| 503 | toggleDomNodeCssClass: toggleDomNodeCssClass, |
| 504 | |
| 505 | setTextContent: function(element, textContent) { |
| 506 | var value = ko.utils.unwrapObservable(textContent); |
| 507 | if ((value === null) || (value === undefined)) |
| 508 | value = ""; |
| 509 | |
| 510 | // We need there to be exactly one child: a text node. |
| 511 | // If there are no children, more than one, or if it's not a text node, |
| 512 | // we'll clear everything and create a single text node. |
| 513 | var innerTextNode = ko.virtualElements.firstChild(element); |
| 514 | if (!innerTextNode || innerTextNode.nodeType != 3 || ko.virtualElements.nextSibling(innerTextNode)) { |
| 515 | ko.virtualElements.setDomNodeChildren(element, [element.ownerDocument.createTextNode(value)]); |
| 516 | } else { |
| 517 | innerTextNode.data = value; |
| 518 | } |
| 519 | |
| 520 | ko.utils.forceRefresh(element); |
| 521 | }, |
| 522 | |
| 523 | setElementName: function(element, name) { |
| 524 | element.name = name; |
| 525 | |
| 526 | // Workaround IE 6/7 issue |
| 527 | // - https://github.com/SteveSanderson/knockout/issues/197 |
| 528 | // - http://www.matts411.com/post/setting_the_name_attribute_in_ie_dom/ |
| 529 | if (ieVersion <= 7) { |
| 530 | try { |
| 531 | var escapedName = element.name.replace(/[&<>'"]/g, function(r){ return "&#" + r.charCodeAt(0) + ";"; }); |
| 532 | element.mergeAttributes(document.createElement("<input name='" + escapedName + "'/>"), false); |
| 533 | } |
| 534 | catch(e) {} // For IE9 with doc mode "IE9 Standards" and browser mode "IE9 Compatibility View" |
| 535 | } |
| 536 | }, |
| 537 | |
| 538 | forceRefresh: function(node) { |
| 539 | // Workaround for an IE9 rendering bug - https://github.com/SteveSanderson/knockout/issues/209 |
| 540 | if (ieVersion >= 9) { |
| 541 | // For text nodes and comment nodes (most likely virtual elements), we will have to refresh the container |
| 542 | var elem = node.nodeType == 1 ? node : node.parentNode; |
| 543 | if (elem.style) |
| 544 | elem.style.zoom = elem.style.zoom; |
| 545 | } |
| 546 | }, |
| 547 | |
| 548 | ensureSelectElementIsRenderedCorrectly: function(selectElement) { |
| 549 | // 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. |
| 550 | // (See https://github.com/SteveSanderson/knockout/issues/312, http://stackoverflow.com/questions/5908494/select-only-shows-first-char-of-selected-option) |
| 551 | // Also fixes IE7 and IE8 bug that causes selects to be zero width if enclosed by 'if' or 'with'. (See issue #839) |
| 552 | if (ieVersion) { |
| 553 | var originalWidth = selectElement.style.width; |
| 554 | selectElement.style.width = 0; |
| 555 | selectElement.style.width = originalWidth; |
| 556 | } |
| 557 | }, |
| 558 | |
| 559 | range: function (min, max) { |
| 560 | min = ko.utils.unwrapObservable(min); |
| 561 | max = ko.utils.unwrapObservable(max); |
| 562 | var result = []; |
| 563 | for (var i = min; i <= max; i++) |
| 564 | result.push(i); |
| 565 | return result; |
| 566 | }, |
| 567 | |
| 568 | makeArray: function(arrayLikeObject) { |
| 569 | var result = []; |
| 570 | for (var i = 0, j = arrayLikeObject.length; i < j; i++) { |
| 571 | result.push(arrayLikeObject[i]); |
| 572 | }; |
| 573 | return result; |
| 574 | }, |
| 575 | |
| 576 | createSymbolOrString: function(identifier) { |
| 577 | return canUseSymbols ? Symbol(identifier) : identifier; |
| 578 | }, |
| 579 | |
| 580 | isIe6 : isIe6, |
| 581 | isIe7 : isIe7, |
| 582 | ieVersion : ieVersion, |
| 583 | |
| 584 | getFormFields: function(form, fieldName) { |
| 585 | var fields = ko.utils.makeArray(form.getElementsByTagName("input")).concat(ko.utils.makeArray(form.getElementsByTagName("textarea"))); |
| 586 | var isMatchingField = (typeof fieldName == 'string') |
| 587 | ? function(field) { return field.name === fieldName } |
| 588 | : function(field) { return fieldName.test(field.name) }; // Treat fieldName as regex or object containing predicate |
| 589 | var matches = []; |
| 590 | for (var i = fields.length - 1; i >= 0; i--) { |
| 591 | if (isMatchingField(fields[i])) |
| 592 | matches.push(fields[i]); |
| 593 | }; |
| 594 | return matches; |
| 595 | }, |
| 596 | |
| 597 | parseJson: function (jsonString) { |
| 598 | if (typeof jsonString == "string") { |
| 599 | jsonString = ko.utils.stringTrim(jsonString); |
| 600 | if (jsonString) { |
| 601 | if (JSON && JSON.parse) // Use native parsing where available |
| 602 | return JSON.parse(jsonString); |
| 603 | return (new Function("return " + jsonString))(); // Fallback on less safe parsing for older browsers |
| 604 | } |
| 605 | } |
| 606 | return null; |
| 607 | }, |
| 608 | |
| 609 | stringifyJson: function (data, replacer, space) { // replacer and space are optional |
| 610 | if (!JSON || !JSON.stringify) |
| 611 | 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"); |
| 612 | return JSON.stringify(ko.utils.unwrapObservable(data), replacer, space); |
| 613 | }, |
| 614 | |
| 615 | postJson: function (urlOrForm, data, options) { |
| 616 | options = options || {}; |
| 617 | var params = options['params'] || {}; |
| 618 | var includeFields = options['includeFields'] || this.fieldsIncludedWithJsonPost; |
| 619 | var url = urlOrForm; |
| 620 | |
| 621 | // If we were given a form, use its 'action' URL and pick out any requested field values |
| 622 | if((typeof urlOrForm == 'object') && (ko.utils.tagNameLower(urlOrForm) === "form")) { |
| 623 | var originalForm = urlOrForm; |
| 624 | url = originalForm.action; |
| 625 | for (var i = includeFields.length - 1; i >= 0; i--) { |
| 626 | var fields = ko.utils.getFormFields(originalForm, includeFields[i]); |
| 627 | for (var j = fields.length - 1; j >= 0; j--) |
| 628 | params[fields[j].name] = fields[j].value; |
| 629 | } |
| 630 | } |
| 631 | |
| 632 | data = ko.utils.unwrapObservable(data); |
| 633 | var form = document.createElement("form"); |
| 634 | form.style.display = "none"; |
| 635 | form.action = url; |
| 636 | form.method = "post"; |
| 637 | for (var key in data) { |
| 638 | // Since 'data' this is a model object, we include all properties including those inherited from its prototype |
| 639 | var input = document.createElement("input"); |
| 640 | input.type = "hidden"; |
| 641 | input.name = key; |
| 642 | input.value = ko.utils.stringifyJson(ko.utils.unwrapObservable(data[key])); |
| 643 | form.appendChild(input); |
| 644 | } |
| 645 | objectForEach(params, function(key, value) { |
| 646 | var input = document.createElement("input"); |
| 647 | input.type = "hidden"; |
| 648 | input.name = key; |
| 649 | input.value = value; |
| 650 | form.appendChild(input); |
| 651 | }); |
| 652 | document.body.appendChild(form); |
| 653 | options['submitter'] ? options['submitter'](form) : form.submit(); |
| 654 | setTimeout(function () { form.parentNode.removeChild(form); }, 0); |
| 655 | } |
| 656 | } |
| 657 | }()); |
| 658 | |
| 659 | ko.exportSymbol('utils', ko.utils); |
| 660 | ko.exportSymbol('utils.arrayForEach', ko.utils.arrayForEach); |
| 661 | ko.exportSymbol('utils.arrayFirst', ko.utils.arrayFirst); |
| 662 | ko.exportSymbol('utils.arrayFilter', ko.utils.arrayFilter); |
| 663 | ko.exportSymbol('utils.arrayGetDistinctValues', ko.utils.arrayGetDistinctValues); |
| 664 | ko.exportSymbol('utils.arrayIndexOf', ko.utils.arrayIndexOf); |
| 665 | ko.exportSymbol('utils.arrayMap', ko.utils.arrayMap); |
| 666 | ko.exportSymbol('utils.arrayPushAll', ko.utils.arrayPushAll); |
| 667 | ko.exportSymbol('utils.arrayRemoveItem', ko.utils.arrayRemoveItem); |
| 668 | ko.exportSymbol('utils.cloneNodes', ko.utils.cloneNodes); |
| 669 | ko.exportSymbol('utils.createSymbolOrString', ko.utils.createSymbolOrString); |
| 670 | ko.exportSymbol('utils.extend', ko.utils.extend); |
| 671 | ko.exportSymbol('utils.fieldsIncludedWithJsonPost', ko.utils.fieldsIncludedWithJsonPost); |
| 672 | ko.exportSymbol('utils.getFormFields', ko.utils.getFormFields); |
| 673 | ko.exportSymbol('utils.objectMap', ko.utils.objectMap); |
| 674 | ko.exportSymbol('utils.peekObservable', ko.utils.peekObservable); |
| 675 | ko.exportSymbol('utils.postJson', ko.utils.postJson); |
| 676 | ko.exportSymbol('utils.parseJson', ko.utils.parseJson); |
| 677 | ko.exportSymbol('utils.registerEventHandler', ko.utils.registerEventHandler); |
| 678 | ko.exportSymbol('utils.stringifyJson', ko.utils.stringifyJson); |
| 679 | ko.exportSymbol('utils.range', ko.utils.range); |
| 680 | ko.exportSymbol('utils.toggleDomNodeCssClass', ko.utils.toggleDomNodeCssClass); |
| 681 | ko.exportSymbol('utils.triggerEvent', ko.utils.triggerEvent); |
| 682 | ko.exportSymbol('utils.unwrapObservable', ko.utils.unwrapObservable); |
| 683 | ko.exportSymbol('utils.objectForEach', ko.utils.objectForEach); |
| 684 | ko.exportSymbol('utils.addOrRemoveItem', ko.utils.addOrRemoveItem); |
| 685 | ko.exportSymbol('utils.setTextContent', ko.utils.setTextContent); |
| 686 | ko.exportSymbol('unwrap', ko.utils.unwrapObservable); // Convenient shorthand, because this is used so commonly |
| 687 | |
| 688 | if (!Function.prototype['bind']) { |
| 689 | // 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) |
| 690 | // In case the browser doesn't implement it natively, provide a JavaScript implementation. This implementation is based on the one in prototype.js |
| 691 | Function.prototype['bind'] = function (object) { |
| 692 | var originalFunction = this; |
| 693 | if (arguments.length === 1) { |
| 694 | return function () { |
| 695 | return originalFunction.apply(object, arguments); |
| 696 | }; |
| 697 | } else { |
| 698 | var partialArgs = Array.prototype.slice.call(arguments, 1); |
| 699 | return function () { |
| 700 | var args = partialArgs.slice(0); |
| 701 | args.push.apply(args, arguments); |
| 702 | return originalFunction.apply(object, args); |
| 703 | }; |
| 704 | } |
| 705 | }; |
| 706 | } |
| 707 | |
| 708 | ko.utils.domData = new (function () { |
| 709 | var uniqueId = 0; |
| 710 | var dataStoreKeyExpandoPropertyName = "__ko__" + (new Date).getTime(); |
| 711 | var dataStore = {}; |
| 712 | |
| 713 | var getDataForNode, clear; |
| 714 | if (!ko.utils.ieVersion) { |
| 715 | // We considered using WeakMap, but it has a problem in IE 11 and Edge that prevents using |
| 716 | // it cross-window, so instead we just store the data directly on the node. |
| 717 | // See https://github.com/knockout/knockout/issues/2141 |
| 718 | getDataForNode = function (node, createIfNotFound) { |
| 719 | var dataForNode = node[dataStoreKeyExpandoPropertyName]; |
| 720 | if (!dataForNode && createIfNotFound) { |
| 721 | dataForNode = node[dataStoreKeyExpandoPropertyName] = {}; |
| 722 | } |
| 723 | return dataForNode; |
| 724 | }; |
| 725 | clear = function (node) { |
| 726 | if (node[dataStoreKeyExpandoPropertyName]) { |
| 727 | delete node[dataStoreKeyExpandoPropertyName]; |
| 728 | return true; // Exposing "did clean" flag purely so specs can infer whether things have been cleaned up as intended |
| 729 | } |
| 730 | return false; |
| 731 | }; |
| 732 | } else { |
| 733 | // Old IE versions have memory issues if you store objects on the node, so we use a |
| 734 | // separate data storage and link to it from the node using a string key. |
| 735 | getDataForNode = function (node, createIfNotFound) { |
| 736 | var dataStoreKey = node[dataStoreKeyExpandoPropertyName]; |
| 737 | var hasExistingDataStore = dataStoreKey && (dataStoreKey !== "null") && dataStore[dataStoreKey]; |
| 738 | if (!hasExistingDataStore) { |
| 739 | if (!createIfNotFound) |
| 740 | return undefined; |
| 741 | dataStoreKey = node[dataStoreKeyExpandoPropertyName] = "ko" + uniqueId++; |
| 742 | dataStore[dataStoreKey] = {}; |
| 743 | } |
| 744 | return dataStore[dataStoreKey]; |
| 745 | }; |
| 746 | clear = function (node) { |
| 747 | var dataStoreKey = node[dataStoreKeyExpandoPropertyName]; |
| 748 | if (dataStoreKey) { |
| 749 | delete dataStore[dataStoreKey]; |
| 750 | node[dataStoreKeyExpandoPropertyName] = null; |
| 751 | return true; // Exposing "did clean" flag purely so specs can infer whether things have been cleaned up as intended |
| 752 | } |
| 753 | return false; |
| 754 | }; |
| 755 | } |
| 756 | |
| 757 | return { |
| 758 | get: function (node, key) { |
| 759 | var dataForNode = getDataForNode(node, false); |
| 760 | return dataForNode && dataForNode[key]; |
| 761 | }, |
| 762 | set: function (node, key, value) { |
| 763 | // Make sure we don't actually create a new domData key if we are actually deleting a value |
| 764 | var dataForNode = getDataForNode(node, value !== undefined /* createIfNotFound */); |
| 765 | dataForNode && (dataForNode[key] = value); |
| 766 | }, |
| 767 | getOrSet: function (node, key, value) { |
| 768 | var dataForNode = getDataForNode(node, true /* createIfNotFound */); |
| 769 | return dataForNode[key] || (dataForNode[key] = value); |
| 770 | }, |
| 771 | clear: clear, |
| 772 | |
| 773 | nextKey: function () { |
| 774 | return (uniqueId++) + dataStoreKeyExpandoPropertyName; |
| 775 | } |
| 776 | }; |
| 777 | })(); |
| 778 | |
| 779 | ko.exportSymbol('utils.domData', ko.utils.domData); |
| 780 | ko.exportSymbol('utils.domData.clear', ko.utils.domData.clear); // Exporting only so specs can clear up after themselves fully |
| 781 | |
| 782 | ko.utils.domNodeDisposal = new (function () { |
| 783 | var domDataKey = ko.utils.domData.nextKey(); |
| 784 | var cleanableNodeTypes = { 1: true, 8: true, 9: true }; // Element, Comment, Document |
| 785 | var cleanableNodeTypesWithDescendants = { 1: true, 9: true }; // Element, Document |
| 786 | |
| 787 | function getDisposeCallbacksCollection(node, createIfNotFound) { |
| 788 | var allDisposeCallbacks = ko.utils.domData.get(node, domDataKey); |
| 789 | if ((allDisposeCallbacks === undefined) && createIfNotFound) { |
| 790 | allDisposeCallbacks = []; |
| 791 | ko.utils.domData.set(node, domDataKey, allDisposeCallbacks); |
| 792 | } |
| 793 | return allDisposeCallbacks; |
| 794 | } |
| 795 | function destroyCallbacksCollection(node) { |
| 796 | ko.utils.domData.set(node, domDataKey, undefined); |
| 797 | } |
| 798 | |
| 799 | function cleanSingleNode(node) { |
| 800 | // Run all the dispose callbacks |
| 801 | var callbacks = getDisposeCallbacksCollection(node, false); |
| 802 | if (callbacks) { |
| 803 | callbacks = callbacks.slice(0); // Clone, as the array may be modified during iteration (typically, callbacks will remove themselves) |
| 804 | for (var i = 0; i < callbacks.length; i++) |
| 805 | callbacks[i](node); |
| 806 | } |
| 807 | |
| 808 | // Erase the DOM data |
| 809 | ko.utils.domData.clear(node); |
| 810 | |
| 811 | // Perform cleanup needed by external libraries (currently only jQuery, but can be extended) |
| 812 | ko.utils.domNodeDisposal["cleanExternalData"](node); |
| 813 | |
| 814 | // Clear any immediate-child comment nodes, as these wouldn't have been found by |
| 815 | // node.getElementsByTagName("*") in cleanNode() (comment nodes aren't elements) |
| 816 | if (cleanableNodeTypesWithDescendants[node.nodeType]) { |
| 817 | cleanNodesInList(node.childNodes, true/*onlyComments*/); |
| 818 | } |
| 819 | } |
| 820 | |
| 821 | function cleanNodesInList(nodeList, onlyComments) { |
| 822 | var cleanedNodes = [], lastCleanedNode; |
| 823 | for (var i = 0; i < nodeList.length; i++) { |
| 824 | if (!onlyComments || nodeList[i].nodeType === 8) { |
| 825 | cleanSingleNode(cleanedNodes[cleanedNodes.length] = lastCleanedNode = nodeList[i]); |
| 826 | if (nodeList[i] !== lastCleanedNode) { |
| 827 | while (i-- && ko.utils.arrayIndexOf(cleanedNodes, nodeList[i]) == -1) {} |
| 828 | } |
| 829 | } |
| 830 | } |
| 831 | } |
| 832 | |
| 833 | return { |
| 834 | addDisposeCallback : function(node, callback) { |
| 835 | if (typeof callback != "function") |
| 836 | throw new Error("Callback must be a function"); |
| 837 | getDisposeCallbacksCollection(node, true).push(callback); |
| 838 | }, |
| 839 | |
| 840 | removeDisposeCallback : function(node, callback) { |
| 841 | var callbacksCollection = getDisposeCallbacksCollection(node, false); |
| 842 | if (callbacksCollection) { |
| 843 | ko.utils.arrayRemoveItem(callbacksCollection, callback); |
| 844 | if (callbacksCollection.length == 0) |
| 845 | destroyCallbacksCollection(node); |
| 846 | } |
| 847 | }, |
| 848 | |
| 849 | cleanNode : function(node) { |
| 850 | // First clean this node, where applicable |
| 851 | if (cleanableNodeTypes[node.nodeType]) { |
| 852 | cleanSingleNode(node); |
| 853 | |
| 854 | // ... then its descendants, where applicable |
| 855 | if (cleanableNodeTypesWithDescendants[node.nodeType]) { |
| 856 | cleanNodesInList(node.getElementsByTagName("*")); |
| 857 | } |
| 858 | } |
| 859 | return node; |
| 860 | }, |
| 861 | |
| 862 | removeNode : function(node) { |
| 863 | ko.cleanNode(node); |
| 864 | if (node.parentNode) |
| 865 | node.parentNode.removeChild(node); |
| 866 | }, |
| 867 | |
| 868 | "cleanExternalData" : function (node) { |
| 869 | // Special support for jQuery here because it's so commonly used. |
| 870 | // Many jQuery plugins (including jquery.tmpl) store data using jQuery's equivalent of domData |
| 871 | // so notify it to tear down any resources associated with the node & descendants here. |
| 872 | if (jQueryInstance && (typeof jQueryInstance['cleanData'] == "function")) |
| 873 | jQueryInstance['cleanData']([node]); |
| 874 | } |
| 875 | }; |
| 876 | })(); |
| 877 | ko.cleanNode = ko.utils.domNodeDisposal.cleanNode; // Shorthand name for convenience |
| 878 | ko.removeNode = ko.utils.domNodeDisposal.removeNode; // Shorthand name for convenience |
| 879 | ko.exportSymbol('cleanNode', ko.cleanNode); |
| 880 | ko.exportSymbol('removeNode', ko.removeNode); |
| 881 | ko.exportSymbol('utils.domNodeDisposal', ko.utils.domNodeDisposal); |
| 882 | ko.exportSymbol('utils.domNodeDisposal.addDisposeCallback', ko.utils.domNodeDisposal.addDisposeCallback); |
| 883 | ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeDisposal.removeDisposeCallback); |
| 884 | (function () { |
| 885 | var none = [0, "", ""], |
| 886 | table = [1, "<table>", "</table>"], |
| 887 | tbody = [2, "<table><tbody>", "</tbody></table>"], |
| 888 | tr = [3, "<table><tbody><tr>", "</tr></tbody></table>"], |
| 889 | select = [1, "<select multiple='multiple'>", "</select>"], |
| 890 | lookup = { |
| 891 | 'thead': table, |
| 892 | 'tbody': table, |
| 893 | 'tfoot': table, |
| 894 | 'tr': tbody, |
| 895 | 'td': tr, |
| 896 | 'th': tr, |
| 897 | 'option': select, |
| 898 | 'optgroup': select |
| 899 | }, |
| 900 | |
| 901 | // This is needed for old IE if you're *not* using either jQuery or innerShiv. Doesn't affect other cases. |
| 902 | mayRequireCreateElementHack = ko.utils.ieVersion <= 8; |
| 903 | |
| 904 | function getWrap(tags) { |
| 905 | var m = tags.match(/^(?:<!--.*?-->\s*?)*?<([a-z]+)[\s>]/); |
| 906 | return (m && lookup[m[1]]) || none; |
| 907 | } |
| 908 | |
| 909 | function simpleHtmlParse(html, documentContext) { |
| 910 | documentContext || (documentContext = document); |
| 911 | var windowContext = documentContext['parentWindow'] || documentContext['defaultView'] || window; |
| 912 | |
| 913 | // Based on jQuery's "clean" function, but only accounting for table-related elements. |
| 914 | // If you have referenced jQuery, this won't be used anyway - KO will use jQuery's "clean" function directly |
| 915 | |
| 916 | // Note that there's still an issue in IE < 9 whereby it will discard comment nodes that are the first child of |
| 917 | // a descendant node. For example: "<div><!-- mycomment -->abc</div>" will get parsed as "<div>abc</div>" |
| 918 | // This won't affect anyone who has referenced jQuery, and there's always the workaround of inserting a dummy node |
| 919 | // (possibly a text node) in front of the comment. So, KO does not attempt to workaround this IE issue automatically at present. |
| 920 | |
| 921 | // Trim whitespace, otherwise indexOf won't work as expected |
| 922 | var tags = ko.utils.stringTrim(html).toLowerCase(), div = documentContext.createElement("div"), |
| 923 | wrap = getWrap(tags), |
| 924 | depth = wrap[0]; |
| 925 | |
| 926 | // Go to html and back, then peel off extra wrappers |
| 927 | // Note that we always prefix with some dummy text, because otherwise, IE<9 will strip out leading comment nodes in descendants. Total madness. |
| 928 | var markup = "ignored<div>" + wrap[1] + html + wrap[2] + "</div>"; |
| 929 | if (typeof windowContext['innerShiv'] == "function") { |
| 930 | // Note that innerShiv is deprecated in favour of html5shiv. We should consider adding |
| 931 | // support for html5shiv (except if no explicit support is needed, e.g., if html5shiv |
| 932 | // somehow shims the native APIs so it just works anyway) |
| 933 | div.appendChild(windowContext['innerShiv'](markup)); |
| 934 | } else { |
| 935 | if (mayRequireCreateElementHack) { |
| 936 | // The document.createElement('my-element') trick to enable custom elements in IE6-8 |
| 937 | // only works if we assign innerHTML on an element associated with that document. |
| 938 | documentContext.body.appendChild(div); |
| 939 | } |
| 940 | |
| 941 | div.innerHTML = markup; |
| 942 | |
| 943 | if (mayRequireCreateElementHack) { |
| 944 | div.parentNode.removeChild(div); |
| 945 | } |
| 946 | } |
| 947 | |
| 948 | // Move to the right depth |
| 949 | while (depth--) |
| 950 | div = div.lastChild; |
| 951 | |
| 952 | return ko.utils.makeArray(div.lastChild.childNodes); |
| 953 | } |
| 954 | |
| 955 | function jQueryHtmlParse(html, documentContext) { |
| 956 | // jQuery's "parseHTML" function was introduced in jQuery 1.8.0 and is a documented public API. |
| 957 | if (jQueryInstance['parseHTML']) { |
| 958 | return jQueryInstance['parseHTML'](html, documentContext) || []; // Ensure we always return an array and never null |
| 959 | } else { |
| 960 | // For jQuery < 1.8.0, we fall back on the undocumented internal "clean" function. |
| 961 | var elems = jQueryInstance['clean']([html], documentContext); |
| 962 | |
| 963 | // 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. |
| 964 | // Unfortunately, it never clears the dummy parent nodes from the document fragment, so it leaks memory over time. |
| 965 | // Fix this by finding the top-most dummy parent element, and detaching it from its owner fragment. |
| 966 | if (elems && elems[0]) { |
| 967 | // Find the top-most parent element that's a direct child of a document fragment |
| 968 | var elem = elems[0]; |
| 969 | while (elem.parentNode && elem.parentNode.nodeType !== 11 /* i.e., DocumentFragment */) |
| 970 | elem = elem.parentNode; |
| 971 | // ... then detach it |
| 972 | if (elem.parentNode) |
| 973 | elem.parentNode.removeChild(elem); |
| 974 | } |
| 975 | |
| 976 | return elems; |
| 977 | } |
| 978 | } |
| 979 | |
| 980 | ko.utils.parseHtmlFragment = function(html, documentContext) { |
| 981 | return jQueryInstance ? |
| 982 | jQueryHtmlParse(html, documentContext) : // As below, benefit from jQuery's optimisations where possible |
| 983 | simpleHtmlParse(html, documentContext); // ... otherwise, this simple logic will do in most common cases. |
| 984 | }; |
| 985 | |
| 986 | ko.utils.parseHtmlForTemplateNodes = function(html, documentContext) { |
| 987 | var nodes = ko.utils.parseHtmlFragment(html, documentContext); |
| 988 | return (nodes.length && nodes[0].parentElement) || ko.utils.moveCleanedNodesToContainerElement(nodes); |
| 989 | }; |
| 990 | |
| 991 | ko.utils.setHtml = function(node, html) { |
| 992 | ko.utils.emptyDomNode(node); |
| 993 | |
| 994 | // There's no legitimate reason to display a stringified observable without unwrapping it, so we'll unwrap it |
| 995 | html = ko.utils.unwrapObservable(html); |
| 996 | |
| 997 | if ((html !== null) && (html !== undefined)) { |
| 998 | if (typeof html != 'string') |
| 999 | html = html.toString(); |
| 1000 | |
| 1001 | // jQuery contains a lot of sophisticated code to parse arbitrary HTML fragments, |
| 1002 | // for example <tr> elements which are not normally allowed to exist on their own. |
| 1003 | // If you've referenced jQuery we'll use that rather than duplicating its code. |
| 1004 | if (jQueryInstance) { |
| 1005 | jQueryInstance(node)['html'](html); |
| 1006 | } else { |
| 1007 | // ... otherwise, use KO's own parsing logic. |
| 1008 | var parsedNodes = ko.utils.parseHtmlFragment(html, node.ownerDocument); |
| 1009 | for (var i = 0; i < parsedNodes.length; i++) |
| 1010 | node.appendChild(parsedNodes[i]); |
| 1011 | } |
| 1012 | } |
| 1013 | }; |
| 1014 | })(); |
| 1015 | |
| 1016 | ko.exportSymbol('utils.parseHtmlFragment', ko.utils.parseHtmlFragment); |
| 1017 | ko.exportSymbol('utils.setHtml', ko.utils.setHtml); |
| 1018 | |
| 1019 | ko.memoization = (function () { |
| 1020 | var memos = {}; |
| 1021 | |
| 1022 | function randomMax8HexChars() { |
| 1023 | return (((1 + Math.random()) * 0x100000000) | 0).toString(16).substring(1); |
| 1024 | } |
| 1025 | function generateRandomId() { |
| 1026 | return randomMax8HexChars() + randomMax8HexChars(); |
| 1027 | } |
| 1028 | function findMemoNodes(rootNode, appendToArray) { |
| 1029 | if (!rootNode) |
| 1030 | return; |
| 1031 | if (rootNode.nodeType == 8) { |
| 1032 | var memoId = ko.memoization.parseMemoText(rootNode.nodeValue); |
| 1033 | if (memoId != null) |
| 1034 | appendToArray.push({ domNode: rootNode, memoId: memoId }); |
| 1035 | } else if (rootNode.nodeType == 1) { |
| 1036 | for (var i = 0, childNodes = rootNode.childNodes, j = childNodes.length; i < j; i++) |
| 1037 | findMemoNodes(childNodes[i], appendToArray); |
| 1038 | } |
| 1039 | } |
| 1040 | |
| 1041 | return { |
| 1042 | memoize: function (callback) { |
| 1043 | if (typeof callback != "function") |
| 1044 | throw new Error("You can only pass a function to ko.memoization.memoize()"); |
| 1045 | var memoId = generateRandomId(); |
| 1046 | memos[memoId] = callback; |
| 1047 | return "<!--[ko_memo:" + memoId + "]-->"; |
| 1048 | }, |
| 1049 | |
| 1050 | unmemoize: function (memoId, callbackParams) { |
| 1051 | var callback = memos[memoId]; |
| 1052 | if (callback === undefined) |
| 1053 | throw new Error("Couldn't find any memo with ID " + memoId + ". Perhaps it's already been unmemoized."); |
| 1054 | try { |
| 1055 | callback.apply(null, callbackParams || []); |
| 1056 | return true; |
| 1057 | } |
| 1058 | finally { delete memos[memoId]; } |
| 1059 | }, |
| 1060 | |
| 1061 | unmemoizeDomNodeAndDescendants: function (domNode, extraCallbackParamsArray) { |
| 1062 | var memos = []; |
| 1063 | findMemoNodes(domNode, memos); |
| 1064 | for (var i = 0, j = memos.length; i < j; i++) { |
| 1065 | var node = memos[i].domNode; |
| 1066 | var combinedParams = [node]; |
| 1067 | if (extraCallbackParamsArray) |
| 1068 | ko.utils.arrayPushAll(combinedParams, extraCallbackParamsArray); |
| 1069 | ko.memoization.unmemoize(memos[i].memoId, combinedParams); |
| 1070 | node.nodeValue = ""; // Neuter this node so we don't try to unmemoize it again |
| 1071 | if (node.parentNode) |
| 1072 | 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) |
| 1073 | } |
| 1074 | }, |
| 1075 | |
| 1076 | parseMemoText: function (memoText) { |
| 1077 | var match = memoText.match(/^\[ko_memo\:(.*?)\]$/); |
| 1078 | return match ? match[1] : null; |
| 1079 | } |
| 1080 | }; |
| 1081 | })(); |
| 1082 | |
| 1083 | ko.exportSymbol('memoization', ko.memoization); |
| 1084 | ko.exportSymbol('memoization.memoize', ko.memoization.memoize); |
| 1085 | ko.exportSymbol('memoization.unmemoize', ko.memoization.unmemoize); |
| 1086 | ko.exportSymbol('memoization.parseMemoText', ko.memoization.parseMemoText); |
| 1087 | ko.exportSymbol('memoization.unmemoizeDomNodeAndDescendants', ko.memoization.unmemoizeDomNodeAndDescendants); |
| 1088 | ko.tasks = (function () { |
| 1089 | var scheduler, |
| 1090 | taskQueue = [], |
| 1091 | taskQueueLength = 0, |
| 1092 | nextHandle = 1, |
| 1093 | nextIndexToProcess = 0; |
| 1094 | |
| 1095 | if (window['MutationObserver']) { |
| 1096 | // Chrome 27+, Firefox 14+, IE 11+, Opera 15+, Safari 6.1+ |
| 1097 | // From https://github.com/petkaantonov/bluebird * Copyright (c) 2014 Petka Antonov * License: MIT |
| 1098 | scheduler = (function (callback) { |
| 1099 | var div = document.createElement("div"); |
| 1100 | new MutationObserver(callback).observe(div, {attributes: true}); |
| 1101 | return function () { div.classList.toggle("foo"); }; |
| 1102 | })(scheduledProcess); |
| 1103 | } else if (document && "onreadystatechange" in document.createElement("script")) { |
| 1104 | // IE 6-10 |
| 1105 | // From https://github.com/YuzuJS/setImmediate * Copyright (c) 2012 Barnesandnoble.com, llc, Donavon West, and Domenic Denicola * License: MIT |
| 1106 | scheduler = function (callback) { |
| 1107 | var script = document.createElement("script"); |
| 1108 | script.onreadystatechange = function () { |
| 1109 | script.onreadystatechange = null; |
| 1110 | document.documentElement.removeChild(script); |
| 1111 | script = null; |
| 1112 | callback(); |
| 1113 | }; |
| 1114 | document.documentElement.appendChild(script); |
| 1115 | }; |
| 1116 | } else { |
| 1117 | scheduler = function (callback) { |
| 1118 | setTimeout(callback, 0); |
| 1119 | }; |
| 1120 | } |
| 1121 | |
| 1122 | function processTasks() { |
| 1123 | if (taskQueueLength) { |
| 1124 | // Each mark represents the end of a logical group of tasks and the number of these groups is |
| 1125 | // limited to prevent unchecked recursion. |
| 1126 | var mark = taskQueueLength, countMarks = 0; |
| 1127 | |
| 1128 | // nextIndexToProcess keeps track of where we are in the queue; processTasks can be called recursively without issue |
| 1129 | for (var task; nextIndexToProcess < taskQueueLength; ) { |
| 1130 | if (task = taskQueue[nextIndexToProcess++]) { |
| 1131 | if (nextIndexToProcess > mark) { |
| 1132 | if (++countMarks >= 5000) { |
| 1133 | nextIndexToProcess = taskQueueLength; // skip all tasks remaining in the queue since any of them could be causing the recursion |
| 1134 | ko.utils.deferError(Error("'Too much recursion' after processing " + countMarks + " task groups.")); |
| 1135 | break; |
| 1136 | } |
| 1137 | mark = taskQueueLength; |
| 1138 | } |
| 1139 | try { |
| 1140 | task(); |
| 1141 | } catch (ex) { |
| 1142 | ko.utils.deferError(ex); |
| 1143 | } |
| 1144 | } |
| 1145 | } |
| 1146 | } |
| 1147 | } |
| 1148 | |
| 1149 | function scheduledProcess() { |
| 1150 | processTasks(); |
| 1151 | |
| 1152 | // Reset the queue |
| 1153 | nextIndexToProcess = taskQueueLength = taskQueue.length = 0; |
| 1154 | } |
| 1155 | |
| 1156 | function scheduleTaskProcessing() { |
| 1157 | ko.tasks['scheduler'](scheduledProcess); |
| 1158 | } |
| 1159 | |
| 1160 | var tasks = { |
| 1161 | 'scheduler': scheduler, // Allow overriding the scheduler |
| 1162 | |
| 1163 | schedule: function (func) { |
| 1164 | if (!taskQueueLength) { |
| 1165 | scheduleTaskProcessing(); |
| 1166 | } |
| 1167 | |
| 1168 | taskQueue[taskQueueLength++] = func; |
| 1169 | return nextHandle++; |
| 1170 | }, |
| 1171 | |
| 1172 | cancel: function (handle) { |
| 1173 | var index = handle - (nextHandle - taskQueueLength); |
| 1174 | if (index >= nextIndexToProcess && index < taskQueueLength) { |
| 1175 | taskQueue[index] = null; |
| 1176 | } |
| 1177 | }, |
| 1178 | |
| 1179 | // For testing only: reset the queue and return the previous queue length |
| 1180 | 'resetForTesting': function () { |
| 1181 | var length = taskQueueLength - nextIndexToProcess; |
| 1182 | nextIndexToProcess = taskQueueLength = taskQueue.length = 0; |
| 1183 | return length; |
| 1184 | }, |
| 1185 | |
| 1186 | runEarly: processTasks |
| 1187 | }; |
| 1188 | |
| 1189 | return tasks; |
| 1190 | })(); |
| 1191 | |
| 1192 | ko.exportSymbol('tasks', ko.tasks); |
| 1193 | ko.exportSymbol('tasks.schedule', ko.tasks.schedule); |
| 1194 | //ko.exportSymbol('tasks.cancel', ko.tasks.cancel); "cancel" isn't minified |
| 1195 | ko.exportSymbol('tasks.runEarly', ko.tasks.runEarly); |
| 1196 | ko.extenders = { |
| 1197 | 'throttle': function(target, timeout) { |
| 1198 | // Throttling means two things: |
| 1199 | |
| 1200 | // (1) For dependent observables, we throttle *evaluations* so that, no matter how fast its dependencies |
| 1201 | // notify updates, the target doesn't re-evaluate (and hence doesn't notify) faster than a certain rate |
| 1202 | target['throttleEvaluation'] = timeout; |
| 1203 | |
| 1204 | // (2) For writable targets (observables, or writable dependent observables), we throttle *writes* |
| 1205 | // so the target cannot change value synchronously or faster than a certain rate |
| 1206 | var writeTimeoutInstance = null; |
| 1207 | return ko.dependentObservable({ |
| 1208 | 'read': target, |
| 1209 | 'write': function(value) { |
| 1210 | clearTimeout(writeTimeoutInstance); |
| 1211 | writeTimeoutInstance = ko.utils.setTimeout(function() { |
| 1212 | target(value); |
| 1213 | }, timeout); |
| 1214 | } |
| 1215 | }); |
| 1216 | }, |
| 1217 | |
| 1218 | 'rateLimit': function(target, options) { |
| 1219 | var timeout, method, limitFunction; |
| 1220 | |
| 1221 | if (typeof options == 'number') { |
| 1222 | timeout = options; |
| 1223 | } else { |
| 1224 | timeout = options['timeout']; |
| 1225 | method = options['method']; |
| 1226 | } |
| 1227 | |
| 1228 | // rateLimit supersedes deferred updates |
| 1229 | target._deferUpdates = false; |
| 1230 | |
| 1231 | limitFunction = typeof method == 'function' ? method : method == 'notifyWhenChangesStop' ? debounce : throttle; |
| 1232 | target.limit(function(callback) { |
| 1233 | return limitFunction(callback, timeout, options); |
| 1234 | }); |
| 1235 | }, |
| 1236 | |
| 1237 | 'deferred': function(target, options) { |
| 1238 | if (options !== true) { |
| 1239 | throw new Error('The \'deferred\' extender only accepts the value \'true\', because it is not supported to turn deferral off once enabled.') |
| 1240 | } |
| 1241 | |
| 1242 | if (!target._deferUpdates) { |
| 1243 | target._deferUpdates = true; |
| 1244 | target.limit(function (callback) { |
| 1245 | var handle, |
| 1246 | ignoreUpdates = false; |
| 1247 | return function () { |
| 1248 | if (!ignoreUpdates) { |
| 1249 | ko.tasks.cancel(handle); |
| 1250 | handle = ko.tasks.schedule(callback); |
| 1251 | |
| 1252 | try { |
| 1253 | ignoreUpdates = true; |
| 1254 | target['notifySubscribers'](undefined, 'dirty'); |
| 1255 | } finally { |
| 1256 | ignoreUpdates = false; |
| 1257 | } |
| 1258 | } |
| 1259 | }; |
| 1260 | }); |
| 1261 | } |
| 1262 | }, |
| 1263 | |
| 1264 | 'notify': function(target, notifyWhen) { |
| 1265 | target["equalityComparer"] = notifyWhen == "always" ? |
| 1266 | null : // null equalityComparer means to always notify |
| 1267 | valuesArePrimitiveAndEqual; |
| 1268 | } |
| 1269 | }; |
| 1270 | |
| 1271 | var primitiveTypes = { 'undefined':1, 'boolean':1, 'number':1, 'string':1 }; |
| 1272 | function valuesArePrimitiveAndEqual(a, b) { |
| 1273 | var oldValueIsPrimitive = (a === null) || (typeof(a) in primitiveTypes); |
| 1274 | return oldValueIsPrimitive ? (a === b) : false; |
| 1275 | } |
| 1276 | |
| 1277 | function throttle(callback, timeout) { |
| 1278 | var timeoutInstance; |
| 1279 | return function () { |
| 1280 | if (!timeoutInstance) { |
| 1281 | timeoutInstance = ko.utils.setTimeout(function () { |
| 1282 | timeoutInstance = undefined; |
| 1283 | callback(); |
| 1284 | }, timeout); |
| 1285 | } |
| 1286 | }; |
| 1287 | } |
| 1288 | |
| 1289 | function debounce(callback, timeout) { |
| 1290 | var timeoutInstance; |
| 1291 | return function () { |
| 1292 | clearTimeout(timeoutInstance); |
| 1293 | timeoutInstance = ko.utils.setTimeout(callback, timeout); |
| 1294 | }; |
| 1295 | } |
| 1296 | |
| 1297 | function applyExtenders(requestedExtenders) { |
| 1298 | var target = this; |
| 1299 | if (requestedExtenders) { |
| 1300 | ko.utils.objectForEach(requestedExtenders, function(key, value) { |
| 1301 | var extenderHandler = ko.extenders[key]; |
| 1302 | if (typeof extenderHandler == 'function') { |
| 1303 | target = extenderHandler(target, value) || target; |
| 1304 | } |
| 1305 | }); |
| 1306 | } |
| 1307 | return target; |
| 1308 | } |
| 1309 | |
| 1310 | ko.exportSymbol('extenders', ko.extenders); |
| 1311 | |
| 1312 | ko.subscription = function (target, callback, disposeCallback) { |
| 1313 | this._target = target; |
| 1314 | this._callback = callback; |
| 1315 | this._disposeCallback = disposeCallback; |
| 1316 | this._isDisposed = false; |
| 1317 | this._node = null; |
| 1318 | this._domNodeDisposalCallback = null; |
| 1319 | ko.exportProperty(this, 'dispose', this.dispose); |
| 1320 | ko.exportProperty(this, 'disposeWhenNodeIsRemoved', this.disposeWhenNodeIsRemoved); |
| 1321 | }; |
| 1322 | ko.subscription.prototype.dispose = function () { |
| 1323 | var self = this; |
| 1324 | if (!self._isDisposed) { |
| 1325 | if (self._domNodeDisposalCallback) { |
| 1326 | ko.utils.domNodeDisposal.removeDisposeCallback(self._node, self._domNodeDisposalCallback); |
| 1327 | } |
| 1328 | self._isDisposed = true; |
| 1329 | self._disposeCallback(); |
| 1330 | |
| 1331 | self._target = self._callback = self._disposeCallback = self._node = self._domNodeDisposalCallback = null; |
| 1332 | } |
| 1333 | }; |
| 1334 | ko.subscription.prototype.disposeWhenNodeIsRemoved = function (node) { |
| 1335 | this._node = node; |
| 1336 | ko.utils.domNodeDisposal.addDisposeCallback(node, this._domNodeDisposalCallback = this.dispose.bind(this)); |
| 1337 | }; |
| 1338 | |
| 1339 | ko.subscribable = function () { |
| 1340 | ko.utils.setPrototypeOfOrExtend(this, ko_subscribable_fn); |
| 1341 | ko_subscribable_fn.init(this); |
| 1342 | } |
| 1343 | |
| 1344 | var defaultEvent = "change"; |
| 1345 | |
| 1346 | // Moved out of "limit" to avoid the extra closure |
| 1347 | function limitNotifySubscribers(value, event) { |
| 1348 | if (!event || event === defaultEvent) { |
| 1349 | this._limitChange(value); |
| 1350 | } else if (event === 'beforeChange') { |
| 1351 | this._limitBeforeChange(value); |
| 1352 | } else { |
| 1353 | this._origNotifySubscribers(value, event); |
| 1354 | } |
| 1355 | } |
| 1356 | |
| 1357 | var ko_subscribable_fn = { |
| 1358 | init: function(instance) { |
| 1359 | instance._subscriptions = { "change": [] }; |
| 1360 | instance._versionNumber = 1; |
| 1361 | }, |
| 1362 | |
| 1363 | subscribe: function (callback, callbackTarget, event) { |
| 1364 | var self = this; |
| 1365 | |
| 1366 | event = event || defaultEvent; |
| 1367 | var boundCallback = callbackTarget ? callback.bind(callbackTarget) : callback; |
| 1368 | |
| 1369 | var subscription = new ko.subscription(self, boundCallback, function () { |
| 1370 | ko.utils.arrayRemoveItem(self._subscriptions[event], subscription); |
| 1371 | if (self.afterSubscriptionRemove) |
| 1372 | self.afterSubscriptionRemove(event); |
| 1373 | }); |
| 1374 | |
| 1375 | if (self.beforeSubscriptionAdd) |
| 1376 | self.beforeSubscriptionAdd(event); |
| 1377 | |
| 1378 | if (!self._subscriptions[event]) |
| 1379 | self._subscriptions[event] = []; |
| 1380 | self._subscriptions[event].push(subscription); |
| 1381 | |
| 1382 | return subscription; |
| 1383 | }, |
| 1384 | |
| 1385 | "notifySubscribers": function (valueToNotify, event) { |
| 1386 | event = event || defaultEvent; |
| 1387 | if (event === defaultEvent) { |
| 1388 | this.updateVersion(); |
| 1389 | } |
| 1390 | if (this.hasSubscriptionsForEvent(event)) { |
| 1391 | var subs = event === defaultEvent && this._changeSubscriptions || this._subscriptions[event].slice(0); |
| 1392 | try { |
| 1393 | ko.dependencyDetection.begin(); // Begin suppressing dependency detection (by setting the top frame to undefined) |
| 1394 | for (var i = 0, subscription; subscription = subs[i]; ++i) { |
| 1395 | // In case a subscription was disposed during the arrayForEach cycle, check |
| 1396 | // for isDisposed on each subscription before invoking its callback |
| 1397 | if (!subscription._isDisposed) |
| 1398 | subscription._callback(valueToNotify); |
| 1399 | } |
| 1400 | } finally { |
| 1401 | ko.dependencyDetection.end(); // End suppressing dependency detection |
| 1402 | } |
| 1403 | } |
| 1404 | }, |
| 1405 | |
| 1406 | getVersion: function () { |
| 1407 | return this._versionNumber; |
| 1408 | }, |
| 1409 | |
| 1410 | hasChanged: function (versionToCheck) { |
| 1411 | return this.getVersion() !== versionToCheck; |
| 1412 | }, |
| 1413 | |
| 1414 | updateVersion: function () { |
| 1415 | ++this._versionNumber; |
| 1416 | }, |
| 1417 | |
| 1418 | limit: function(limitFunction) { |
| 1419 | var self = this, selfIsObservable = ko.isObservable(self), |
| 1420 | ignoreBeforeChange, notifyNextChange, previousValue, pendingValue, didUpdate, |
| 1421 | beforeChange = 'beforeChange'; |
| 1422 | |
| 1423 | if (!self._origNotifySubscribers) { |
| 1424 | self._origNotifySubscribers = self["notifySubscribers"]; |
| 1425 | self["notifySubscribers"] = limitNotifySubscribers; |
| 1426 | } |
| 1427 | |
| 1428 | var finish = limitFunction(function() { |
| 1429 | self._notificationIsPending = false; |
| 1430 | |
| 1431 | // If an observable provided a reference to itself, access it to get the latest value. |
| 1432 | // This allows computed observables to delay calculating their value until needed. |
| 1433 | if (selfIsObservable && pendingValue === self) { |
| 1434 | pendingValue = self._evalIfChanged ? self._evalIfChanged() : self(); |
| 1435 | } |
| 1436 | var shouldNotify = notifyNextChange || (didUpdate && self.isDifferent(previousValue, pendingValue)); |
| 1437 | |
| 1438 | didUpdate = notifyNextChange = ignoreBeforeChange = false; |
| 1439 | |
| 1440 | if (shouldNotify) { |
| 1441 | self._origNotifySubscribers(previousValue = pendingValue); |
| 1442 | } |
| 1443 | }); |
| 1444 | |
| 1445 | self._limitChange = function(value, isDirty) { |
| 1446 | if (!isDirty || !self._notificationIsPending) { |
| 1447 | didUpdate = !isDirty; |
| 1448 | } |
| 1449 | self._changeSubscriptions = self._subscriptions[defaultEvent].slice(0); |
| 1450 | self._notificationIsPending = ignoreBeforeChange = true; |
| 1451 | pendingValue = value; |
| 1452 | finish(); |
| 1453 | }; |
| 1454 | self._limitBeforeChange = function(value) { |
| 1455 | if (!ignoreBeforeChange) { |
| 1456 | previousValue = value; |
| 1457 | self._origNotifySubscribers(value, beforeChange); |
| 1458 | } |
| 1459 | }; |
| 1460 | self._recordUpdate = function() { |
| 1461 | didUpdate = true; |
| 1462 | }; |
| 1463 | self._notifyNextChangeIfValueIsDifferent = function() { |
| 1464 | if (self.isDifferent(previousValue, self.peek(true /*evaluate*/))) { |
| 1465 | notifyNextChange = true; |
| 1466 | } |
| 1467 | }; |
| 1468 | }, |
| 1469 | |
| 1470 | hasSubscriptionsForEvent: function(event) { |
| 1471 | return this._subscriptions[event] && this._subscriptions[event].length; |
| 1472 | }, |
| 1473 | |
| 1474 | getSubscriptionsCount: function (event) { |
| 1475 | if (event) { |
| 1476 | return this._subscriptions[event] && this._subscriptions[event].length || 0; |
| 1477 | } else { |
| 1478 | var total = 0; |
| 1479 | ko.utils.objectForEach(this._subscriptions, function(eventName, subscriptions) { |
| 1480 | if (eventName !== 'dirty') |
| 1481 | total += subscriptions.length; |
| 1482 | }); |
| 1483 | return total; |
| 1484 | } |
| 1485 | }, |
| 1486 | |
| 1487 | isDifferent: function(oldValue, newValue) { |
| 1488 | return !this['equalityComparer'] || !this['equalityComparer'](oldValue, newValue); |
| 1489 | }, |
| 1490 | |
| 1491 | toString: function() { |
| 1492 | return '[object Object]' |
| 1493 | }, |
| 1494 | |
| 1495 | extend: applyExtenders |
| 1496 | }; |
| 1497 | |
| 1498 | ko.exportProperty(ko_subscribable_fn, 'init', ko_subscribable_fn.init); |
| 1499 | ko.exportProperty(ko_subscribable_fn, 'subscribe', ko_subscribable_fn.subscribe); |
| 1500 | ko.exportProperty(ko_subscribable_fn, 'extend', ko_subscribable_fn.extend); |
| 1501 | ko.exportProperty(ko_subscribable_fn, 'getSubscriptionsCount', ko_subscribable_fn.getSubscriptionsCount); |
| 1502 | |
| 1503 | // For browsers that support proto assignment, we overwrite the prototype of each |
| 1504 | // observable instance. Since observables are functions, we need Function.prototype |
| 1505 | // to still be in the prototype chain. |
| 1506 | if (ko.utils.canSetPrototype) { |
| 1507 | ko.utils.setPrototypeOf(ko_subscribable_fn, Function.prototype); |
| 1508 | } |
| 1509 | |
| 1510 | ko.subscribable['fn'] = ko_subscribable_fn; |
| 1511 | |
| 1512 | |
| 1513 | ko.isSubscribable = function (instance) { |
| 1514 | return instance != null && typeof instance.subscribe == "function" && typeof instance["notifySubscribers"] == "function"; |
| 1515 | }; |
| 1516 | |
| 1517 | ko.exportSymbol('subscribable', ko.subscribable); |
| 1518 | ko.exportSymbol('isSubscribable', ko.isSubscribable); |
| 1519 | |
| 1520 | ko.computedContext = ko.dependencyDetection = (function () { |
| 1521 | var outerFrames = [], |
| 1522 | currentFrame, |
| 1523 | lastId = 0; |
| 1524 | |
| 1525 | // Return a unique ID that can be assigned to an observable for dependency tracking. |
| 1526 | // Theoretically, you could eventually overflow the number storage size, resulting |
| 1527 | // in duplicate IDs. But in JavaScript, the largest exact integral value is 2^53 |
| 1528 | // or 9,007,199,254,740,992. If you created 1,000,000 IDs per second, it would |
| 1529 | // take over 285 years to reach that number. |
| 1530 | // Reference http://blog.vjeux.com/2010/javascript/javascript-max_int-number-limits.html |
| 1531 | function getId() { |
| 1532 | return ++lastId; |
| 1533 | } |
| 1534 | |
| 1535 | function begin(options) { |
| 1536 | outerFrames.push(currentFrame); |
| 1537 | currentFrame = options; |
| 1538 | } |
| 1539 | |
| 1540 | function end() { |
| 1541 | currentFrame = outerFrames.pop(); |
| 1542 | } |
| 1543 | |
| 1544 | return { |
| 1545 | begin: begin, |
| 1546 | |
| 1547 | end: end, |
| 1548 | |
| 1549 | registerDependency: function (subscribable) { |
| 1550 | if (currentFrame) { |
| 1551 | if (!ko.isSubscribable(subscribable)) |
| 1552 | throw new Error("Only subscribable things can act as dependencies"); |
| 1553 | currentFrame.callback.call(currentFrame.callbackTarget, subscribable, subscribable._id || (subscribable._id = getId())); |
| 1554 | } |
| 1555 | }, |
| 1556 | |
| 1557 | ignore: function (callback, callbackTarget, callbackArgs) { |
| 1558 | try { |
| 1559 | begin(); |
| 1560 | return callback.apply(callbackTarget, callbackArgs || []); |
| 1561 | } finally { |
| 1562 | end(); |
| 1563 | } |
| 1564 | }, |
| 1565 | |
| 1566 | getDependenciesCount: function () { |
| 1567 | if (currentFrame) |
| 1568 | return currentFrame.computed.getDependenciesCount(); |
| 1569 | }, |
| 1570 | |
| 1571 | getDependencies: function () { |
| 1572 | if (currentFrame) |
| 1573 | return currentFrame.computed.getDependencies(); |
| 1574 | }, |
| 1575 | |
| 1576 | isInitial: function() { |
| 1577 | if (currentFrame) |
| 1578 | return currentFrame.isInitial; |
| 1579 | }, |
| 1580 | |
| 1581 | computed: function() { |
| 1582 | if (currentFrame) |
| 1583 | return currentFrame.computed; |
| 1584 | } |
| 1585 | }; |
| 1586 | })(); |
| 1587 | |
| 1588 | ko.exportSymbol('computedContext', ko.computedContext); |
| 1589 | ko.exportSymbol('computedContext.getDependenciesCount', ko.computedContext.getDependenciesCount); |
| 1590 | ko.exportSymbol('computedContext.getDependencies', ko.computedContext.getDependencies); |
| 1591 | ko.exportSymbol('computedContext.isInitial', ko.computedContext.isInitial); |
| 1592 | ko.exportSymbol('computedContext.registerDependency', ko.computedContext.registerDependency); |
| 1593 | |
| 1594 | ko.exportSymbol('ignoreDependencies', ko.ignoreDependencies = ko.dependencyDetection.ignore); |
| 1595 | var observableLatestValue = ko.utils.createSymbolOrString('_latestValue'); |
| 1596 | |
| 1597 | ko.observable = function (initialValue) { |
| 1598 | function observable() { |
| 1599 | if (arguments.length > 0) { |
| 1600 | // Write |
| 1601 | |
| 1602 | // Ignore writes if the value hasn't changed |
| 1603 | if (observable.isDifferent(observable[observableLatestValue], arguments[0])) { |
| 1604 | observable.valueWillMutate(); |
| 1605 | observable[observableLatestValue] = arguments[0]; |
| 1606 | observable.valueHasMutated(); |
| 1607 | } |
| 1608 | return this; // Permits chained assignments |
| 1609 | } |
| 1610 | else { |
| 1611 | // Read |
| 1612 | ko.dependencyDetection.registerDependency(observable); // The caller only needs to be notified of changes if they did a "read" operation |
| 1613 | return observable[observableLatestValue]; |
| 1614 | } |
| 1615 | } |
| 1616 | |
| 1617 | observable[observableLatestValue] = initialValue; |
| 1618 | |
| 1619 | // Inherit from 'subscribable' |
| 1620 | if (!ko.utils.canSetPrototype) { |
| 1621 | // 'subscribable' won't be on the prototype chain unless we put it there directly |
| 1622 | ko.utils.extend(observable, ko.subscribable['fn']); |
| 1623 | } |
| 1624 | ko.subscribable['fn'].init(observable); |
| 1625 | |
| 1626 | // Inherit from 'observable' |
| 1627 | ko.utils.setPrototypeOfOrExtend(observable, observableFn); |
| 1628 | |
| 1629 | if (ko.options['deferUpdates']) { |
| 1630 | ko.extenders['deferred'](observable, true); |
| 1631 | } |
| 1632 | |
| 1633 | return observable; |
| 1634 | } |
| 1635 | |
| 1636 | // Define prototype for observables |
| 1637 | var observableFn = { |
| 1638 | 'equalityComparer': valuesArePrimitiveAndEqual, |
| 1639 | peek: function() { return this[observableLatestValue]; }, |
| 1640 | valueHasMutated: function () { |
| 1641 | this['notifySubscribers'](this[observableLatestValue], 'spectate'); |
| 1642 | this['notifySubscribers'](this[observableLatestValue]); |
| 1643 | }, |
| 1644 | valueWillMutate: function () { this['notifySubscribers'](this[observableLatestValue], 'beforeChange'); } |
| 1645 | }; |
| 1646 | |
| 1647 | // Note that for browsers that don't support proto assignment, the |
| 1648 | // inheritance chain is created manually in the ko.observable constructor |
| 1649 | if (ko.utils.canSetPrototype) { |
| 1650 | ko.utils.setPrototypeOf(observableFn, ko.subscribable['fn']); |
| 1651 | } |
| 1652 | |
| 1653 | var protoProperty = ko.observable.protoProperty = '__ko_proto__'; |
| 1654 | observableFn[protoProperty] = ko.observable; |
| 1655 | |
| 1656 | ko.isObservable = function (instance) { |
| 1657 | var proto = typeof instance == 'function' && instance[protoProperty]; |
| 1658 | if (proto && proto !== observableFn[protoProperty] && proto !== ko.computed['fn'][protoProperty]) { |
| 1659 | throw Error("Invalid object that looks like an observable; possibly from another Knockout instance"); |
| 1660 | } |
| 1661 | return !!proto; |
| 1662 | }; |
| 1663 | |
| 1664 | ko.isWriteableObservable = function (instance) { |
| 1665 | return (typeof instance == 'function' && ( |
| 1666 | (instance[protoProperty] === observableFn[protoProperty]) || // Observable |
| 1667 | (instance[protoProperty] === ko.computed['fn'][protoProperty] && instance.hasWriteFunction))); // Writable computed observable |
| 1668 | }; |
| 1669 | |
| 1670 | ko.exportSymbol('observable', ko.observable); |
| 1671 | ko.exportSymbol('isObservable', ko.isObservable); |
| 1672 | ko.exportSymbol('isWriteableObservable', ko.isWriteableObservable); |
| 1673 | ko.exportSymbol('isWritableObservable', ko.isWriteableObservable); |
| 1674 | ko.exportSymbol('observable.fn', observableFn); |
| 1675 | ko.exportProperty(observableFn, 'peek', observableFn.peek); |
| 1676 | ko.exportProperty(observableFn, 'valueHasMutated', observableFn.valueHasMutated); |
| 1677 | ko.exportProperty(observableFn, 'valueWillMutate', observableFn.valueWillMutate); |
| 1678 | ko.observableArray = function (initialValues) { |
| 1679 | initialValues = initialValues || []; |
| 1680 | |
| 1681 | if (typeof initialValues != 'object' || !('length' in initialValues)) |
| 1682 | throw new Error("The argument passed when initializing an observable array must be an array, or null, or undefined."); |
| 1683 | |
| 1684 | var result = ko.observable(initialValues); |
| 1685 | ko.utils.setPrototypeOfOrExtend(result, ko.observableArray['fn']); |
| 1686 | return result.extend({'trackArrayChanges':true}); |
| 1687 | }; |
| 1688 | |
| 1689 | ko.observableArray['fn'] = { |
| 1690 | 'remove': function (valueOrPredicate) { |
| 1691 | var underlyingArray = this.peek(); |
| 1692 | var removedValues = []; |
| 1693 | var predicate = typeof valueOrPredicate == "function" && !ko.isObservable(valueOrPredicate) ? valueOrPredicate : function (value) { return value === valueOrPredicate; }; |
| 1694 | for (var i = 0; i < underlyingArray.length; i++) { |
| 1695 | var value = underlyingArray[i]; |
| 1696 | if (predicate(value)) { |
| 1697 | if (removedValues.length === 0) { |
| 1698 | this.valueWillMutate(); |
| 1699 | } |
| 1700 | if (underlyingArray[i] !== value) { |
| 1701 | throw Error("Array modified during remove; cannot remove item"); |
| 1702 | } |
| 1703 | removedValues.push(value); |
| 1704 | underlyingArray.splice(i, 1); |
| 1705 | i--; |
| 1706 | } |
| 1707 | } |
| 1708 | if (removedValues.length) { |
| 1709 | this.valueHasMutated(); |
| 1710 | } |
| 1711 | return removedValues; |
| 1712 | }, |
| 1713 | |
| 1714 | 'removeAll': function (arrayOfValues) { |
| 1715 | // If you passed zero args, we remove everything |
| 1716 | if (arrayOfValues === undefined) { |
| 1717 | var underlyingArray = this.peek(); |
| 1718 | var allValues = underlyingArray.slice(0); |
| 1719 | this.valueWillMutate(); |
| 1720 | underlyingArray.splice(0, underlyingArray.length); |
| 1721 | this.valueHasMutated(); |
| 1722 | return allValues; |
| 1723 | } |
| 1724 | // If you passed an arg, we interpret it as an array of entries to remove |
| 1725 | if (!arrayOfValues) |
| 1726 | return []; |
| 1727 | return this['remove'](function (value) { |
| 1728 | return ko.utils.arrayIndexOf(arrayOfValues, value) >= 0; |
| 1729 | }); |
| 1730 | }, |
| 1731 | |
| 1732 | 'destroy': function (valueOrPredicate) { |
| 1733 | var underlyingArray = this.peek(); |
| 1734 | var predicate = typeof valueOrPredicate == "function" && !ko.isObservable(valueOrPredicate) ? valueOrPredicate : function (value) { return value === valueOrPredicate; }; |
| 1735 | this.valueWillMutate(); |
| 1736 | for (var i = underlyingArray.length - 1; i >= 0; i--) { |
| 1737 | var value = underlyingArray[i]; |
| 1738 | if (predicate(value)) |
| 1739 | value["_destroy"] = true; |
| 1740 | } |
| 1741 | this.valueHasMutated(); |
| 1742 | }, |
| 1743 | |
| 1744 | 'destroyAll': function (arrayOfValues) { |
| 1745 | // If you passed zero args, we destroy everything |
| 1746 | if (arrayOfValues === undefined) |
| 1747 | return this['destroy'](function() { return true }); |
| 1748 | |
| 1749 | // If you passed an arg, we interpret it as an array of entries to destroy |
| 1750 | if (!arrayOfValues) |
| 1751 | return []; |
| 1752 | return this['destroy'](function (value) { |
| 1753 | return ko.utils.arrayIndexOf(arrayOfValues, value) >= 0; |
| 1754 | }); |
| 1755 | }, |
| 1756 | |
| 1757 | 'indexOf': function (item) { |
| 1758 | var underlyingArray = this(); |
| 1759 | return ko.utils.arrayIndexOf(underlyingArray, item); |
| 1760 | }, |
| 1761 | |
| 1762 | 'replace': function(oldItem, newItem) { |
| 1763 | var index = this['indexOf'](oldItem); |
| 1764 | if (index >= 0) { |
| 1765 | this.valueWillMutate(); |
| 1766 | this.peek()[index] = newItem; |
| 1767 | this.valueHasMutated(); |
| 1768 | } |
| 1769 | }, |
| 1770 | |
| 1771 | 'sorted': function (compareFunction) { |
| 1772 | var arrayCopy = this().slice(0); |
| 1773 | return compareFunction ? arrayCopy.sort(compareFunction) : arrayCopy.sort(); |
| 1774 | }, |
| 1775 | |
| 1776 | 'reversed': function () { |
| 1777 | return this().slice(0).reverse(); |
| 1778 | } |
| 1779 | }; |
| 1780 | |
| 1781 | // Note that for browsers that don't support proto assignment, the |
| 1782 | // inheritance chain is created manually in the ko.observableArray constructor |
| 1783 | if (ko.utils.canSetPrototype) { |
| 1784 | ko.utils.setPrototypeOf(ko.observableArray['fn'], ko.observable['fn']); |
| 1785 | } |
| 1786 | |
| 1787 | // Populate ko.observableArray.fn with read/write functions from native arrays |
| 1788 | // Important: Do not add any additional functions here that may reasonably be used to *read* data from the array |
| 1789 | // because we'll eval them without causing subscriptions, so ko.computed output could end up getting stale |
| 1790 | ko.utils.arrayForEach(["pop", "push", "reverse", "shift", "sort", "splice", "unshift"], function (methodName) { |
| 1791 | ko.observableArray['fn'][methodName] = function () { |
| 1792 | // Use "peek" to avoid creating a subscription in any computed that we're executing in the context of |
| 1793 | // (for consistency with mutating regular observables) |
| 1794 | var underlyingArray = this.peek(); |
| 1795 | this.valueWillMutate(); |
| 1796 | this.cacheDiffForKnownOperation(underlyingArray, methodName, arguments); |
| 1797 | var methodCallResult = underlyingArray[methodName].apply(underlyingArray, arguments); |
| 1798 | this.valueHasMutated(); |
| 1799 | // The native sort and reverse methods return a reference to the array, but it makes more sense to return the observable array instead. |
| 1800 | return methodCallResult === underlyingArray ? this : methodCallResult; |
| 1801 | }; |
| 1802 | }); |
| 1803 | |
| 1804 | // Populate ko.observableArray.fn with read-only functions from native arrays |
| 1805 | ko.utils.arrayForEach(["slice"], function (methodName) { |
| 1806 | ko.observableArray['fn'][methodName] = function () { |
| 1807 | var underlyingArray = this(); |
| 1808 | return underlyingArray[methodName].apply(underlyingArray, arguments); |
| 1809 | }; |
| 1810 | }); |
| 1811 | |
| 1812 | ko.isObservableArray = function (instance) { |
| 1813 | return ko.isObservable(instance) |
| 1814 | && typeof instance["remove"] == "function" |
| 1815 | && typeof instance["push"] == "function"; |
| 1816 | }; |
| 1817 | |
| 1818 | ko.exportSymbol('observableArray', ko.observableArray); |
| 1819 | ko.exportSymbol('isObservableArray', ko.isObservableArray); |
| 1820 | var arrayChangeEventName = 'arrayChange'; |
| 1821 | ko.extenders['trackArrayChanges'] = function(target, options) { |
| 1822 | // Use the provided options--each call to trackArrayChanges overwrites the previously set options |
| 1823 | target.compareArrayOptions = {}; |
| 1824 | if (options && typeof options == "object") { |
| 1825 | ko.utils.extend(target.compareArrayOptions, options); |
| 1826 | } |
| 1827 | target.compareArrayOptions['sparse'] = true; |
| 1828 | |
| 1829 | // Only modify the target observable once |
| 1830 | if (target.cacheDiffForKnownOperation) { |
| 1831 | return; |
| 1832 | } |
| 1833 | var trackingChanges = false, |
| 1834 | cachedDiff = null, |
| 1835 | arrayChangeSubscription, |
| 1836 | pendingNotifications = 0, |
| 1837 | previousContents, |
| 1838 | underlyingNotifySubscribersFunction, |
| 1839 | underlyingBeforeSubscriptionAddFunction = target.beforeSubscriptionAdd, |
| 1840 | underlyingAfterSubscriptionRemoveFunction = target.afterSubscriptionRemove; |
| 1841 | |
| 1842 | // Watch "subscribe" calls, and for array change events, ensure change tracking is enabled |
| 1843 | target.beforeSubscriptionAdd = function (event) { |
| 1844 | if (underlyingBeforeSubscriptionAddFunction) |
| 1845 | underlyingBeforeSubscriptionAddFunction.call(target, event); |
| 1846 | if (event === arrayChangeEventName) { |
| 1847 | trackChanges(); |
| 1848 | } |
| 1849 | }; |
| 1850 | // Watch "dispose" calls, and for array change events, ensure change tracking is disabled when all are disposed |
| 1851 | target.afterSubscriptionRemove = function (event) { |
| 1852 | if (underlyingAfterSubscriptionRemoveFunction) |
| 1853 | underlyingAfterSubscriptionRemoveFunction.call(target, event); |
| 1854 | if (event === arrayChangeEventName && !target.hasSubscriptionsForEvent(arrayChangeEventName)) { |
| 1855 | if (underlyingNotifySubscribersFunction) { |
| 1856 | target['notifySubscribers'] = underlyingNotifySubscribersFunction; |
| 1857 | underlyingNotifySubscribersFunction = undefined; |
| 1858 | } |
| 1859 | if (arrayChangeSubscription) { |
| 1860 | arrayChangeSubscription.dispose(); |
| 1861 | } |
| 1862 | arrayChangeSubscription = null; |
| 1863 | trackingChanges = false; |
| 1864 | previousContents = undefined; |
| 1865 | } |
| 1866 | }; |
| 1867 | |
| 1868 | function trackChanges() { |
| 1869 | if (trackingChanges) { |
| 1870 | // Whenever there's a new subscription and there are pending notifications, make sure all previous |
| 1871 | // subscriptions are notified of the change so that all subscriptions are in sync. |
| 1872 | notifyChanges(); |
| 1873 | return; |
| 1874 | } |
| 1875 | |
| 1876 | trackingChanges = true; |
| 1877 | |
| 1878 | // Intercept "notifySubscribers" to track how many times it was called. |
| 1879 | underlyingNotifySubscribersFunction = target['notifySubscribers']; |
| 1880 | target['notifySubscribers'] = function(valueToNotify, event) { |
| 1881 | if (!event || event === defaultEvent) { |
| 1882 | ++pendingNotifications; |
| 1883 | } |
| 1884 | return underlyingNotifySubscribersFunction.apply(this, arguments); |
| 1885 | }; |
| 1886 | |
| 1887 | // Each time the array changes value, capture a clone so that on the next |
| 1888 | // change it's possible to produce a diff |
| 1889 | previousContents = [].concat(target.peek() || []); |
| 1890 | cachedDiff = null; |
| 1891 | arrayChangeSubscription = target.subscribe(notifyChanges); |
| 1892 | |
| 1893 | function notifyChanges() { |
| 1894 | if (pendingNotifications) { |
| 1895 | // Make a copy of the current contents and ensure it's an array |
| 1896 | var currentContents = [].concat(target.peek() || []); |
| 1897 | |
| 1898 | // Compute the diff and issue notifications, but only if someone is listening |
| 1899 | if (target.hasSubscriptionsForEvent(arrayChangeEventName)) { |
| 1900 | var changes = getChanges(previousContents, currentContents); |
| 1901 | } |
| 1902 | |
| 1903 | // Eliminate references to the old, removed items, so they can be GCed |
| 1904 | previousContents = currentContents; |
| 1905 | cachedDiff = null; |
| 1906 | pendingNotifications = 0; |
| 1907 | |
| 1908 | if (changes && changes.length) { |
| 1909 | target['notifySubscribers'](changes, arrayChangeEventName); |
| 1910 | } |
| 1911 | } |
| 1912 | } |
| 1913 | } |
| 1914 | |
| 1915 | function getChanges(previousContents, currentContents) { |
| 1916 | // We try to re-use cached diffs. |
| 1917 | // The scenarios where pendingNotifications > 1 are when using rate-limiting or the Deferred Updates |
| 1918 | // plugin, which without this check would not be compatible with arrayChange notifications. Normally, |
| 1919 | // notifications are issued immediately so we wouldn't be queueing up more than one. |
| 1920 | if (!cachedDiff || pendingNotifications > 1) { |
| 1921 | cachedDiff = ko.utils.compareArrays(previousContents, currentContents, target.compareArrayOptions); |
| 1922 | } |
| 1923 | |
| 1924 | return cachedDiff; |
| 1925 | } |
| 1926 | |
| 1927 | target.cacheDiffForKnownOperation = function(rawArray, operationName, args) { |
| 1928 | // Only run if we're currently tracking changes for this observable array |
| 1929 | // and there aren't any pending deferred notifications. |
| 1930 | if (!trackingChanges || pendingNotifications) { |
| 1931 | return; |
| 1932 | } |
| 1933 | var diff = [], |
| 1934 | arrayLength = rawArray.length, |
| 1935 | argsLength = args.length, |
| 1936 | offset = 0; |
| 1937 | |
| 1938 | function pushDiff(status, value, index) { |
| 1939 | return diff[diff.length] = { 'status': status, 'value': value, 'index': index }; |
| 1940 | } |
| 1941 | switch (operationName) { |
| 1942 | case 'push': |
| 1943 | offset = arrayLength; |
| 1944 | case 'unshift': |
| 1945 | for (var index = 0; index < argsLength; index++) { |
| 1946 | pushDiff('added', args[index], offset + index); |
| 1947 | } |
| 1948 | break; |
| 1949 | |
| 1950 | case 'pop': |
| 1951 | offset = arrayLength - 1; |
| 1952 | case 'shift': |
| 1953 | if (arrayLength) { |
| 1954 | pushDiff('deleted', rawArray[offset], offset); |
| 1955 | } |
| 1956 | break; |
| 1957 | |
| 1958 | case 'splice': |
| 1959 | // Negative start index means 'from end of array'. After that we clamp to [0...arrayLength]. |
| 1960 | // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice |
| 1961 | var startIndex = Math.min(Math.max(0, args[0] < 0 ? arrayLength + args[0] : args[0]), arrayLength), |
| 1962 | endDeleteIndex = argsLength === 1 ? arrayLength : Math.min(startIndex + (args[1] || 0), arrayLength), |
| 1963 | endAddIndex = startIndex + argsLength - 2, |
| 1964 | endIndex = Math.max(endDeleteIndex, endAddIndex), |
| 1965 | additions = [], deletions = []; |
| 1966 | for (var index = startIndex, argsIndex = 2; index < endIndex; ++index, ++argsIndex) { |
| 1967 | if (index < endDeleteIndex) |
| 1968 | deletions.push(pushDiff('deleted', rawArray[index], index)); |
| 1969 | if (index < endAddIndex) |
| 1970 | additions.push(pushDiff('added', args[argsIndex], index)); |
| 1971 | } |
| 1972 | ko.utils.findMovesInArrayComparison(deletions, additions); |
| 1973 | break; |
| 1974 | |
| 1975 | default: |
| 1976 | return; |
| 1977 | } |
| 1978 | cachedDiff = diff; |
| 1979 | }; |
| 1980 | }; |
| 1981 | var computedState = ko.utils.createSymbolOrString('_state'); |
| 1982 | |
| 1983 | ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget, options) { |
| 1984 | if (typeof evaluatorFunctionOrOptions === "object") { |
| 1985 | // Single-parameter syntax - everything is on this "options" param |
| 1986 | options = evaluatorFunctionOrOptions; |
| 1987 | } else { |
| 1988 | // Multi-parameter syntax - construct the options according to the params passed |
| 1989 | options = options || {}; |
| 1990 | if (evaluatorFunctionOrOptions) { |
| 1991 | options["read"] = evaluatorFunctionOrOptions; |
| 1992 | } |
| 1993 | } |
| 1994 | if (typeof options["read"] != "function") |
| 1995 | throw Error("Pass a function that returns the value of the ko.computed"); |
| 1996 | |
| 1997 | var writeFunction = options["write"]; |
| 1998 | var state = { |
| 1999 | latestValue: undefined, |
| 2000 | isStale: true, |
| 2001 | isDirty: true, |
| 2002 | isBeingEvaluated: false, |
| 2003 | suppressDisposalUntilDisposeWhenReturnsFalse: false, |
| 2004 | isDisposed: false, |
| 2005 | pure: false, |
| 2006 | isSleeping: false, |
| 2007 | readFunction: options["read"], |
| 2008 | evaluatorFunctionTarget: evaluatorFunctionTarget || options["owner"], |
| 2009 | disposeWhenNodeIsRemoved: options["disposeWhenNodeIsRemoved"] || options.disposeWhenNodeIsRemoved || null, |
| 2010 | disposeWhen: options["disposeWhen"] || options.disposeWhen, |
| 2011 | domNodeDisposalCallback: null, |
| 2012 | dependencyTracking: {}, |
| 2013 | dependenciesCount: 0, |
| 2014 | evaluationTimeoutInstance: null |
| 2015 | }; |
| 2016 | |
| 2017 | function computedObservable() { |
| 2018 | if (arguments.length > 0) { |
| 2019 | if (typeof writeFunction === "function") { |
| 2020 | // Writing a value |
| 2021 | writeFunction.apply(state.evaluatorFunctionTarget, arguments); |
| 2022 | } else { |
| 2023 | throw new Error("Cannot write a value to a ko.computed unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters."); |
| 2024 | } |
| 2025 | return this; // Permits chained assignments |
| 2026 | } else { |
| 2027 | // Reading the value |
| 2028 | if (!state.isDisposed) { |
| 2029 | ko.dependencyDetection.registerDependency(computedObservable); |
| 2030 | } |
| 2031 | if (state.isDirty || (state.isSleeping && computedObservable.haveDependenciesChanged())) { |
| 2032 | computedObservable.evaluateImmediate(); |
| 2033 | } |
| 2034 | return state.latestValue; |
| 2035 | } |
| 2036 | } |
| 2037 | |
| 2038 | computedObservable[computedState] = state; |
| 2039 | computedObservable.hasWriteFunction = typeof writeFunction === "function"; |
| 2040 | |
| 2041 | // Inherit from 'subscribable' |
| 2042 | if (!ko.utils.canSetPrototype) { |
| 2043 | // 'subscribable' won't be on the prototype chain unless we put it there directly |
| 2044 | ko.utils.extend(computedObservable, ko.subscribable['fn']); |
| 2045 | } |
| 2046 | ko.subscribable['fn'].init(computedObservable); |
| 2047 | |
| 2048 | // Inherit from 'computed' |
| 2049 | ko.utils.setPrototypeOfOrExtend(computedObservable, computedFn); |
| 2050 | |
| 2051 | if (options['pure']) { |
| 2052 | state.pure = true; |
| 2053 | state.isSleeping = true; // Starts off sleeping; will awake on the first subscription |
| 2054 | ko.utils.extend(computedObservable, pureComputedOverrides); |
| 2055 | } else if (options['deferEvaluation']) { |
| 2056 | ko.utils.extend(computedObservable, deferEvaluationOverrides); |
| 2057 | } |
| 2058 | |
| 2059 | if (ko.options['deferUpdates']) { |
| 2060 | ko.extenders['deferred'](computedObservable, true); |
| 2061 | } |
| 2062 | |
| 2063 | if (DEBUG) { |
| 2064 | // #1731 - Aid debugging by exposing the computed's options |
| 2065 | computedObservable["_options"] = options; |
| 2066 | } |
| 2067 | |
| 2068 | if (state.disposeWhenNodeIsRemoved) { |
| 2069 | // Since this computed is associated with a DOM node, and we don't want to dispose the computed |
| 2070 | // until the DOM node is *removed* from the document (as opposed to never having been in the document), |
| 2071 | // we'll prevent disposal until "disposeWhen" first returns false. |
| 2072 | state.suppressDisposalUntilDisposeWhenReturnsFalse = true; |
| 2073 | |
| 2074 | // disposeWhenNodeIsRemoved: true can be used to opt into the "only dispose after first false result" |
| 2075 | // behaviour even if there's no specific node to watch. In that case, clear the option so we don't try |
| 2076 | // to watch for a non-node's disposal. This technique is intended for KO's internal use only and shouldn't |
| 2077 | // be documented or used by application code, as it's likely to change in a future version of KO. |
| 2078 | if (!state.disposeWhenNodeIsRemoved.nodeType) { |
| 2079 | state.disposeWhenNodeIsRemoved = null; |
| 2080 | } |
| 2081 | } |
| 2082 | |
| 2083 | // Evaluate, unless sleeping or deferEvaluation is true |
| 2084 | if (!state.isSleeping && !options['deferEvaluation']) { |
| 2085 | computedObservable.evaluateImmediate(); |
| 2086 | } |
| 2087 | |
| 2088 | // Attach a DOM node disposal callback so that the computed will be proactively disposed as soon as the node is |
| 2089 | // removed using ko.removeNode. But skip if isActive is false (there will never be any dependencies to dispose). |
| 2090 | if (state.disposeWhenNodeIsRemoved && computedObservable.isActive()) { |
| 2091 | ko.utils.domNodeDisposal.addDisposeCallback(state.disposeWhenNodeIsRemoved, state.domNodeDisposalCallback = function () { |
| 2092 | computedObservable.dispose(); |
| 2093 | }); |
| 2094 | } |
| 2095 | |
| 2096 | return computedObservable; |
| 2097 | }; |
| 2098 | |
| 2099 | // Utility function that disposes a given dependencyTracking entry |
| 2100 | function computedDisposeDependencyCallback(id, entryToDispose) { |
| 2101 | if (entryToDispose !== null && entryToDispose.dispose) { |
| 2102 | entryToDispose.dispose(); |
| 2103 | } |
| 2104 | } |
| 2105 | |
| 2106 | // This function gets called each time a dependency is detected while evaluating a computed. |
| 2107 | // It's factored out as a shared function to avoid creating unnecessary function instances during evaluation. |
| 2108 | function computedBeginDependencyDetectionCallback(subscribable, id) { |
| 2109 | var computedObservable = this.computedObservable, |
| 2110 | state = computedObservable[computedState]; |
| 2111 | if (!state.isDisposed) { |
| 2112 | if (this.disposalCount && this.disposalCandidates[id]) { |
| 2113 | // Don't want to dispose this subscription, as it's still being used |
| 2114 | computedObservable.addDependencyTracking(id, subscribable, this.disposalCandidates[id]); |
| 2115 | this.disposalCandidates[id] = null; // No need to actually delete the property - disposalCandidates is a transient object anyway |
| 2116 | --this.disposalCount; |
| 2117 | } else if (!state.dependencyTracking[id]) { |
| 2118 | // Brand new subscription - add it |
| 2119 | computedObservable.addDependencyTracking(id, subscribable, state.isSleeping ? { _target: subscribable } : computedObservable.subscribeToDependency(subscribable)); |
| 2120 | } |
| 2121 | // If the observable we've accessed has a pending notification, ensure we get notified of the actual final value (bypass equality checks) |
| 2122 | if (subscribable._notificationIsPending) { |
| 2123 | subscribable._notifyNextChangeIfValueIsDifferent(); |
| 2124 | } |
| 2125 | } |
| 2126 | } |
| 2127 | |
| 2128 | var computedFn = { |
| 2129 | "equalityComparer": valuesArePrimitiveAndEqual, |
| 2130 | getDependenciesCount: function () { |
| 2131 | return this[computedState].dependenciesCount; |
| 2132 | }, |
| 2133 | getDependencies: function () { |
| 2134 | var dependencyTracking = this[computedState].dependencyTracking, dependentObservables = []; |
| 2135 | |
| 2136 | ko.utils.objectForEach(dependencyTracking, function (id, dependency) { |
| 2137 | dependentObservables[dependency._order] = dependency._target; |
| 2138 | }); |
| 2139 | |
| 2140 | return dependentObservables; |
| 2141 | }, |
| 2142 | hasAncestorDependency: function (obs) { |
| 2143 | if (!this[computedState].dependenciesCount) { |
| 2144 | return false; |
| 2145 | } |
| 2146 | var dependencies = this.getDependencies(); |
| 2147 | if (ko.utils.arrayIndexOf(dependencies, obs) !== -1) { |
| 2148 | return true; |
| 2149 | } |
| 2150 | return !!ko.utils.arrayFirst(dependencies, function (dep) { |
| 2151 | return dep.hasAncestorDependency && dep.hasAncestorDependency(obs); |
| 2152 | }); |
| 2153 | }, |
| 2154 | addDependencyTracking: function (id, target, trackingObj) { |
| 2155 | if (this[computedState].pure && target === this) { |
| 2156 | throw Error("A 'pure' computed must not be called recursively"); |
| 2157 | } |
| 2158 | |
| 2159 | this[computedState].dependencyTracking[id] = trackingObj; |
| 2160 | trackingObj._order = this[computedState].dependenciesCount++; |
| 2161 | trackingObj._version = target.getVersion(); |
| 2162 | }, |
| 2163 | haveDependenciesChanged: function () { |
| 2164 | var id, dependency, dependencyTracking = this[computedState].dependencyTracking; |
| 2165 | for (id in dependencyTracking) { |
| 2166 | if (Object.prototype.hasOwnProperty.call(dependencyTracking, id)) { |
| 2167 | dependency = dependencyTracking[id]; |
| 2168 | if ((this._evalDelayed && dependency._target._notificationIsPending) || dependency._target.hasChanged(dependency._version)) { |
| 2169 | return true; |
| 2170 | } |
| 2171 | } |
| 2172 | } |
| 2173 | }, |
| 2174 | markDirty: function () { |
| 2175 | // Process "dirty" events if we can handle delayed notifications |
| 2176 | if (this._evalDelayed && !this[computedState].isBeingEvaluated) { |
| 2177 | this._evalDelayed(false /*isChange*/); |
| 2178 | } |
| 2179 | }, |
| 2180 | isActive: function () { |
| 2181 | var state = this[computedState]; |
| 2182 | return state.isDirty || state.dependenciesCount > 0; |
| 2183 | }, |
| 2184 | respondToChange: function () { |
| 2185 | // Ignore "change" events if we've already scheduled a delayed notification |
| 2186 | if (!this._notificationIsPending) { |
| 2187 | this.evaluatePossiblyAsync(); |
| 2188 | } else if (this[computedState].isDirty) { |
| 2189 | this[computedState].isStale = true; |
| 2190 | } |
| 2191 | }, |
| 2192 | subscribeToDependency: function (target) { |
| 2193 | if (target._deferUpdates) { |
| 2194 | var dirtySub = target.subscribe(this.markDirty, this, 'dirty'), |
| 2195 | changeSub = target.subscribe(this.respondToChange, this); |
| 2196 | return { |
| 2197 | _target: target, |
| 2198 | dispose: function () { |
| 2199 | dirtySub.dispose(); |
| 2200 | changeSub.dispose(); |
| 2201 | } |
| 2202 | }; |
| 2203 | } else { |
| 2204 | return target.subscribe(this.evaluatePossiblyAsync, this); |
| 2205 | } |
| 2206 | }, |
| 2207 | evaluatePossiblyAsync: function () { |
| 2208 | var computedObservable = this, |
| 2209 | throttleEvaluationTimeout = computedObservable['throttleEvaluation']; |
| 2210 | if (throttleEvaluationTimeout && throttleEvaluationTimeout >= 0) { |
| 2211 | clearTimeout(this[computedState].evaluationTimeoutInstance); |
| 2212 | this[computedState].evaluationTimeoutInstance = ko.utils.setTimeout(function () { |
| 2213 | computedObservable.evaluateImmediate(true /*notifyChange*/); |
| 2214 | }, throttleEvaluationTimeout); |
| 2215 | } else if (computedObservable._evalDelayed) { |
| 2216 | computedObservable._evalDelayed(true /*isChange*/); |
| 2217 | } else { |
| 2218 | computedObservable.evaluateImmediate(true /*notifyChange*/); |
| 2219 | } |
| 2220 | }, |
| 2221 | evaluateImmediate: function (notifyChange) { |
| 2222 | var computedObservable = this, |
| 2223 | state = computedObservable[computedState], |
| 2224 | disposeWhen = state.disposeWhen, |
| 2225 | changed = false; |
| 2226 | |
| 2227 | if (state.isBeingEvaluated) { |
| 2228 | // If the evaluation of a ko.computed causes side effects, it's possible that it will trigger its own re-evaluation. |
| 2229 | // This is not desirable (it's hard for a developer to realise a chain of dependencies might cause this, and they almost |
| 2230 | // certainly didn't intend infinite re-evaluations). So, for predictability, we simply prevent ko.computeds from causing |
| 2231 | // their own re-evaluation. Further discussion at https://github.com/SteveSanderson/knockout/pull/387 |
| 2232 | return; |
| 2233 | } |
| 2234 | |
| 2235 | // Do not evaluate (and possibly capture new dependencies) if disposed |
| 2236 | if (state.isDisposed) { |
| 2237 | return; |
| 2238 | } |
| 2239 | |
| 2240 | if (state.disposeWhenNodeIsRemoved && !ko.utils.domNodeIsAttachedToDocument(state.disposeWhenNodeIsRemoved) || disposeWhen && disposeWhen()) { |
| 2241 | // See comment above about suppressDisposalUntilDisposeWhenReturnsFalse |
| 2242 | if (!state.suppressDisposalUntilDisposeWhenReturnsFalse) { |
| 2243 | computedObservable.dispose(); |
| 2244 | return; |
| 2245 | } |
| 2246 | } else { |
| 2247 | // It just did return false, so we can stop suppressing now |
| 2248 | state.suppressDisposalUntilDisposeWhenReturnsFalse = false; |
| 2249 | } |
| 2250 | |
| 2251 | state.isBeingEvaluated = true; |
| 2252 | try { |
| 2253 | changed = this.evaluateImmediate_CallReadWithDependencyDetection(notifyChange); |
| 2254 | } finally { |
| 2255 | state.isBeingEvaluated = false; |
| 2256 | } |
| 2257 | |
| 2258 | return changed; |
| 2259 | }, |
| 2260 | evaluateImmediate_CallReadWithDependencyDetection: function (notifyChange) { |
| 2261 | // This function is really just part of the evaluateImmediate logic. You would never call it from anywhere else. |
| 2262 | // Factoring it out into a separate function means it can be independent of the try/catch block in evaluateImmediate, |
| 2263 | // which contributes to saving about 40% off the CPU overhead of computed evaluation (on V8 at least). |
| 2264 | |
| 2265 | var computedObservable = this, |
| 2266 | state = computedObservable[computedState], |
| 2267 | changed = false; |
| 2268 | |
| 2269 | // Initially, we assume that none of the subscriptions are still being used (i.e., all are candidates for disposal). |
| 2270 | // Then, during evaluation, we cross off any that are in fact still being used. |
| 2271 | var isInitial = state.pure ? undefined : !state.dependenciesCount, // If we're evaluating when there are no previous dependencies, it must be the first time |
| 2272 | dependencyDetectionContext = { |
| 2273 | computedObservable: computedObservable, |
| 2274 | disposalCandidates: state.dependencyTracking, |
| 2275 | disposalCount: state.dependenciesCount |
| 2276 | }; |
| 2277 | |
| 2278 | ko.dependencyDetection.begin({ |
| 2279 | callbackTarget: dependencyDetectionContext, |
| 2280 | callback: computedBeginDependencyDetectionCallback, |
| 2281 | computed: computedObservable, |
| 2282 | isInitial: isInitial |
| 2283 | }); |
| 2284 | |
| 2285 | state.dependencyTracking = {}; |
| 2286 | state.dependenciesCount = 0; |
| 2287 | |
| 2288 | var newValue = this.evaluateImmediate_CallReadThenEndDependencyDetection(state, dependencyDetectionContext); |
| 2289 | |
| 2290 | if (!state.dependenciesCount) { |
| 2291 | computedObservable.dispose(); |
| 2292 | changed = true; // When evaluation causes a disposal, make sure all dependent computeds get notified so they'll see the new state |
| 2293 | } else { |
| 2294 | changed = computedObservable.isDifferent(state.latestValue, newValue); |
| 2295 | } |
| 2296 | |
| 2297 | if (changed) { |
| 2298 | if (!state.isSleeping) { |
| 2299 | computedObservable["notifySubscribers"](state.latestValue, "beforeChange"); |
| 2300 | } else { |
| 2301 | computedObservable.updateVersion(); |
| 2302 | } |
| 2303 | |
| 2304 | state.latestValue = newValue; |
| 2305 | if (DEBUG) computedObservable._latestValue = newValue; |
| 2306 | |
| 2307 | computedObservable["notifySubscribers"](state.latestValue, "spectate"); |
| 2308 | |
| 2309 | if (!state.isSleeping && notifyChange) { |
| 2310 | computedObservable["notifySubscribers"](state.latestValue); |
| 2311 | } |
| 2312 | if (computedObservable._recordUpdate) { |
| 2313 | computedObservable._recordUpdate(); |
| 2314 | } |
| 2315 | } |
| 2316 | |
| 2317 | if (isInitial) { |
| 2318 | computedObservable["notifySubscribers"](state.latestValue, "awake"); |
| 2319 | } |
| 2320 | |
| 2321 | return changed; |
| 2322 | }, |
| 2323 | evaluateImmediate_CallReadThenEndDependencyDetection: function (state, dependencyDetectionContext) { |
| 2324 | // This function is really part of the evaluateImmediate_CallReadWithDependencyDetection logic. |
| 2325 | // You'd never call it from anywhere else. Factoring it out means that evaluateImmediate_CallReadWithDependencyDetection |
| 2326 | // can be independent of try/finally blocks, which contributes to saving about 40% off the CPU |
| 2327 | // overhead of computed evaluation (on V8 at least). |
| 2328 | |
| 2329 | try { |
| 2330 | var readFunction = state.readFunction; |
| 2331 | return state.evaluatorFunctionTarget ? readFunction.call(state.evaluatorFunctionTarget) : readFunction(); |
| 2332 | } finally { |
| 2333 | ko.dependencyDetection.end(); |
| 2334 | |
| 2335 | // For each subscription no longer being used, remove it from the active subscriptions list and dispose it |
| 2336 | if (dependencyDetectionContext.disposalCount && !state.isSleeping) { |
| 2337 | ko.utils.objectForEach(dependencyDetectionContext.disposalCandidates, computedDisposeDependencyCallback); |
| 2338 | } |
| 2339 | |
| 2340 | state.isStale = state.isDirty = false; |
| 2341 | } |
| 2342 | }, |
| 2343 | peek: function (evaluate) { |
| 2344 | // By default, peek won't re-evaluate, except while the computed is sleeping or to get the initial value when "deferEvaluation" is set. |
| 2345 | // Pass in true to evaluate if needed. |
| 2346 | var state = this[computedState]; |
| 2347 | if ((state.isDirty && (evaluate || !state.dependenciesCount)) || (state.isSleeping && this.haveDependenciesChanged())) { |
| 2348 | this.evaluateImmediate(); |
| 2349 | } |
| 2350 | return state.latestValue; |
| 2351 | }, |
| 2352 | limit: function (limitFunction) { |
| 2353 | // Override the limit function with one that delays evaluation as well |
| 2354 | ko.subscribable['fn'].limit.call(this, limitFunction); |
| 2355 | this._evalIfChanged = function () { |
| 2356 | if (!this[computedState].isSleeping) { |
| 2357 | if (this[computedState].isStale) { |
| 2358 | this.evaluateImmediate(); |
| 2359 | } else { |
| 2360 | this[computedState].isDirty = false; |
| 2361 | } |
| 2362 | } |
| 2363 | return this[computedState].latestValue; |
| 2364 | }; |
| 2365 | this._evalDelayed = function (isChange) { |
| 2366 | this._limitBeforeChange(this[computedState].latestValue); |
| 2367 | |
| 2368 | // Mark as dirty |
| 2369 | this[computedState].isDirty = true; |
| 2370 | if (isChange) { |
| 2371 | this[computedState].isStale = true; |
| 2372 | } |
| 2373 | |
| 2374 | // Pass the observable to the "limit" code, which will evaluate it when |
| 2375 | // it's time to do the notification. |
| 2376 | this._limitChange(this, !isChange /* isDirty */); |
| 2377 | }; |
| 2378 | }, |
| 2379 | dispose: function () { |
| 2380 | var state = this[computedState]; |
| 2381 | if (!state.isSleeping && state.dependencyTracking) { |
| 2382 | ko.utils.objectForEach(state.dependencyTracking, function (id, dependency) { |
| 2383 | if (dependency.dispose) |
| 2384 | dependency.dispose(); |
| 2385 | }); |
| 2386 | } |
| 2387 | if (state.disposeWhenNodeIsRemoved && state.domNodeDisposalCallback) { |
| 2388 | ko.utils.domNodeDisposal.removeDisposeCallback(state.disposeWhenNodeIsRemoved, state.domNodeDisposalCallback); |
| 2389 | } |
| 2390 | state.dependencyTracking = undefined; |
| 2391 | state.dependenciesCount = 0; |
| 2392 | state.isDisposed = true; |
| 2393 | state.isStale = false; |
| 2394 | state.isDirty = false; |
| 2395 | state.isSleeping = false; |
| 2396 | state.disposeWhenNodeIsRemoved = undefined; |
| 2397 | state.disposeWhen = undefined; |
| 2398 | state.readFunction = undefined; |
| 2399 | if (!this.hasWriteFunction) { |
| 2400 | state.evaluatorFunctionTarget = undefined; |
| 2401 | } |
| 2402 | } |
| 2403 | }; |
| 2404 | |
| 2405 | var pureComputedOverrides = { |
| 2406 | beforeSubscriptionAdd: function (event) { |
| 2407 | // If asleep, wake up the computed by subscribing to any dependencies. |
| 2408 | var computedObservable = this, |
| 2409 | state = computedObservable[computedState]; |
| 2410 | if (!state.isDisposed && state.isSleeping && event == 'change') { |
| 2411 | state.isSleeping = false; |
| 2412 | if (state.isStale || computedObservable.haveDependenciesChanged()) { |
| 2413 | state.dependencyTracking = null; |
| 2414 | state.dependenciesCount = 0; |
| 2415 | if (computedObservable.evaluateImmediate()) { |
| 2416 | computedObservable.updateVersion(); |
| 2417 | } |
| 2418 | } else { |
| 2419 | // First put the dependencies in order |
| 2420 | var dependenciesOrder = []; |
| 2421 | ko.utils.objectForEach(state.dependencyTracking, function (id, dependency) { |
| 2422 | dependenciesOrder[dependency._order] = id; |
| 2423 | }); |
| 2424 | // Next, subscribe to each one |
| 2425 | ko.utils.arrayForEach(dependenciesOrder, function (id, order) { |
| 2426 | var dependency = state.dependencyTracking[id], |
| 2427 | subscription = computedObservable.subscribeToDependency(dependency._target); |
| 2428 | subscription._order = order; |
| 2429 | subscription._version = dependency._version; |
| 2430 | state.dependencyTracking[id] = subscription; |
| 2431 | }); |
| 2432 | // Waking dependencies may have triggered effects |
| 2433 | if (computedObservable.haveDependenciesChanged()) { |
| 2434 | if (computedObservable.evaluateImmediate()) { |
| 2435 | computedObservable.updateVersion(); |
| 2436 | } |
| 2437 | } |
| 2438 | } |
| 2439 | |
| 2440 | if (!state.isDisposed) { // test since evaluating could trigger disposal |
| 2441 | computedObservable["notifySubscribers"](state.latestValue, "awake"); |
| 2442 | } |
| 2443 | } |
| 2444 | }, |
| 2445 | afterSubscriptionRemove: function (event) { |
| 2446 | var state = this[computedState]; |
| 2447 | if (!state.isDisposed && event == 'change' && !this.hasSubscriptionsForEvent('change')) { |
| 2448 | ko.utils.objectForEach(state.dependencyTracking, function (id, dependency) { |
| 2449 | if (dependency.dispose) { |
| 2450 | state.dependencyTracking[id] = { |
| 2451 | _target: dependency._target, |
| 2452 | _order: dependency._order, |
| 2453 | _version: dependency._version |
| 2454 | }; |
| 2455 | dependency.dispose(); |
| 2456 | } |
| 2457 | }); |
| 2458 | state.isSleeping = true; |
| 2459 | this["notifySubscribers"](undefined, "asleep"); |
| 2460 | } |
| 2461 | }, |
| 2462 | getVersion: function () { |
| 2463 | // Because a pure computed is not automatically updated while it is sleeping, we can't |
| 2464 | // simply return the version number. Instead, we check if any of the dependencies have |
| 2465 | // changed and conditionally re-evaluate the computed observable. |
| 2466 | var state = this[computedState]; |
| 2467 | if (state.isSleeping && (state.isStale || this.haveDependenciesChanged())) { |
| 2468 | this.evaluateImmediate(); |
| 2469 | } |
| 2470 | return ko.subscribable['fn'].getVersion.call(this); |
| 2471 | } |
| 2472 | }; |
| 2473 | |
| 2474 | var deferEvaluationOverrides = { |
| 2475 | beforeSubscriptionAdd: function (event) { |
| 2476 | // This will force a computed with deferEvaluation to evaluate when the first subscription is registered. |
| 2477 | if (event == 'change' || event == 'beforeChange') { |
| 2478 | this.peek(); |
| 2479 | } |
| 2480 | } |
| 2481 | }; |
| 2482 | |
| 2483 | // Note that for browsers that don't support proto assignment, the |
| 2484 | // inheritance chain is created manually in the ko.computed constructor |
| 2485 | if (ko.utils.canSetPrototype) { |
| 2486 | ko.utils.setPrototypeOf(computedFn, ko.subscribable['fn']); |
| 2487 | } |
| 2488 | |
| 2489 | // Set the proto values for ko.computed |
| 2490 | var protoProp = ko.observable.protoProperty; // == "__ko_proto__" |
| 2491 | computedFn[protoProp] = ko.computed; |
| 2492 | |
| 2493 | ko.isComputed = function (instance) { |
| 2494 | return (typeof instance == 'function' && instance[protoProp] === computedFn[protoProp]); |
| 2495 | }; |
| 2496 | |
| 2497 | ko.isPureComputed = function (instance) { |
| 2498 | return ko.isComputed(instance) && instance[computedState] && instance[computedState].pure; |
| 2499 | }; |
| 2500 | |
| 2501 | ko.exportSymbol('computed', ko.computed); |
| 2502 | ko.exportSymbol('dependentObservable', ko.computed); // export ko.dependentObservable for backwards compatibility (1.x) |
| 2503 | ko.exportSymbol('isComputed', ko.isComputed); |
| 2504 | ko.exportSymbol('isPureComputed', ko.isPureComputed); |
| 2505 | ko.exportSymbol('computed.fn', computedFn); |
| 2506 | ko.exportProperty(computedFn, 'peek', computedFn.peek); |
| 2507 | ko.exportProperty(computedFn, 'dispose', computedFn.dispose); |
| 2508 | ko.exportProperty(computedFn, 'isActive', computedFn.isActive); |
| 2509 | ko.exportProperty(computedFn, 'getDependenciesCount', computedFn.getDependenciesCount); |
| 2510 | ko.exportProperty(computedFn, 'getDependencies', computedFn.getDependencies); |
| 2511 | |
| 2512 | ko.pureComputed = function (evaluatorFunctionOrOptions, evaluatorFunctionTarget) { |
| 2513 | if (typeof evaluatorFunctionOrOptions === 'function') { |
| 2514 | return ko.computed(evaluatorFunctionOrOptions, evaluatorFunctionTarget, {'pure':true}); |
| 2515 | } else { |
| 2516 | evaluatorFunctionOrOptions = ko.utils.extend({}, evaluatorFunctionOrOptions); // make a copy of the parameter object |
| 2517 | evaluatorFunctionOrOptions['pure'] = true; |
| 2518 | return ko.computed(evaluatorFunctionOrOptions, evaluatorFunctionTarget); |
| 2519 | } |
| 2520 | } |
| 2521 | ko.exportSymbol('pureComputed', ko.pureComputed); |
| 2522 | |
| 2523 | (function() { |
| 2524 | var maxNestedObservableDepth = 10; // Escape the (unlikely) pathological case where an observable's current value is itself (or similar reference cycle) |
| 2525 | |
| 2526 | ko.toJS = function(rootObject) { |
| 2527 | if (arguments.length == 0) |
| 2528 | throw new Error("When calling ko.toJS, pass the object you want to convert."); |
| 2529 | |
| 2530 | // We just unwrap everything at every level in the object graph |
| 2531 | return mapJsObjectGraph(rootObject, function(valueToMap) { |
| 2532 | // Loop because an observable's value might in turn be another observable wrapper |
| 2533 | for (var i = 0; ko.isObservable(valueToMap) && (i < maxNestedObservableDepth); i++) |
| 2534 | valueToMap = valueToMap(); |
| 2535 | return valueToMap; |
| 2536 | }); |
| 2537 | }; |
| 2538 | |
| 2539 | ko.toJSON = function(rootObject, replacer, space) { // replacer and space are optional |
| 2540 | var plainJavaScriptObject = ko.toJS(rootObject); |
| 2541 | return ko.utils.stringifyJson(plainJavaScriptObject, replacer, space); |
| 2542 | }; |
| 2543 | |
| 2544 | function mapJsObjectGraph(rootObject, mapInputCallback, visitedObjects) { |
| 2545 | visitedObjects = visitedObjects || new objectLookup(); |
| 2546 | |
| 2547 | rootObject = mapInputCallback(rootObject); |
| 2548 | var canHaveProperties = (typeof rootObject == "object") && (rootObject !== null) && (rootObject !== undefined) && (!(rootObject instanceof RegExp)) && (!(rootObject instanceof Date)) && (!(rootObject instanceof String)) && (!(rootObject instanceof Number)) && (!(rootObject instanceof Boolean)); |
| 2549 | if (!canHaveProperties) |
| 2550 | return rootObject; |
| 2551 | |
| 2552 | var outputProperties = rootObject instanceof Array ? [] : {}; |
| 2553 | visitedObjects.save(rootObject, outputProperties); |
| 2554 | |
| 2555 | visitPropertiesOrArrayEntries(rootObject, function(indexer) { |
| 2556 | var propertyValue = mapInputCallback(rootObject[indexer]); |
| 2557 | |
| 2558 | switch (typeof propertyValue) { |
| 2559 | case "boolean": |
| 2560 | case "number": |
| 2561 | case "string": |
| 2562 | case "function": |
| 2563 | outputProperties[indexer] = propertyValue; |
| 2564 | break; |
| 2565 | case "object": |
| 2566 | case "undefined": |
| 2567 | var previouslyMappedValue = visitedObjects.get(propertyValue); |
| 2568 | outputProperties[indexer] = (previouslyMappedValue !== undefined) |
| 2569 | ? previouslyMappedValue |
| 2570 | : mapJsObjectGraph(propertyValue, mapInputCallback, visitedObjects); |
| 2571 | break; |
| 2572 | } |
| 2573 | }); |
| 2574 | |
| 2575 | return outputProperties; |
| 2576 | } |
| 2577 | |
| 2578 | function visitPropertiesOrArrayEntries(rootObject, visitorCallback) { |
| 2579 | if (rootObject instanceof Array) { |
| 2580 | for (var i = 0; i < rootObject.length; i++) |
| 2581 | visitorCallback(i); |
| 2582 | |
| 2583 | // For arrays, also respect toJSON property for custom mappings (fixes #278) |
| 2584 | if (typeof rootObject['toJSON'] == 'function') |
| 2585 | visitorCallback('toJSON'); |
| 2586 | } else { |
| 2587 | for (var propertyName in rootObject) { |
| 2588 | visitorCallback(propertyName); |
| 2589 | } |
| 2590 | } |
| 2591 | }; |
| 2592 | |
| 2593 | function objectLookup() { |
| 2594 | this.keys = []; |
| 2595 | this.values = []; |
| 2596 | }; |
| 2597 | |
| 2598 | objectLookup.prototype = { |
| 2599 | constructor: objectLookup, |
| 2600 | save: function(key, value) { |
| 2601 | var existingIndex = ko.utils.arrayIndexOf(this.keys, key); |
| 2602 | if (existingIndex >= 0) |
| 2603 | this.values[existingIndex] = value; |
| 2604 | else { |
| 2605 | this.keys.push(key); |
| 2606 | this.values.push(value); |
| 2607 | } |
| 2608 | }, |
| 2609 | get: function(key) { |
| 2610 | var existingIndex = ko.utils.arrayIndexOf(this.keys, key); |
| 2611 | return (existingIndex >= 0) ? this.values[existingIndex] : undefined; |
| 2612 | } |
| 2613 | }; |
| 2614 | })(); |
| 2615 | |
| 2616 | ko.exportSymbol('toJS', ko.toJS); |
| 2617 | ko.exportSymbol('toJSON', ko.toJSON); |
| 2618 | ko.when = function(predicate, callback, context) { |
| 2619 | function kowhen (resolve) { |
| 2620 | var observable = ko.pureComputed(predicate, context).extend({notify:'always'}); |
| 2621 | var subscription = observable.subscribe(function(value) { |
| 2622 | if (value) { |
| 2623 | subscription.dispose(); |
| 2624 | resolve(value); |
| 2625 | } |
| 2626 | }); |
| 2627 | // In case the initial value is true, process it right away |
| 2628 | observable['notifySubscribers'](observable.peek()); |
| 2629 | |
| 2630 | return subscription; |
| 2631 | } |
| 2632 | if (typeof Promise === "function" && !callback) { |
| 2633 | return new Promise(kowhen); |
| 2634 | } else { |
| 2635 | return kowhen(callback.bind(context)); |
| 2636 | } |
| 2637 | }; |
| 2638 | |
| 2639 | ko.exportSymbol('when', ko.when); |
| 2640 | (function () { |
| 2641 | var hasDomDataExpandoProperty = '__ko__hasDomDataOptionValue__'; |
| 2642 | |
| 2643 | // Normally, SELECT elements and their OPTIONs can only take value of type 'string' (because the values |
| 2644 | // are stored on DOM attributes). ko.selectExtensions provides a way for SELECTs/OPTIONs to have values |
| 2645 | // that are arbitrary objects. This is very convenient when implementing things like cascading dropdowns. |
| 2646 | ko.selectExtensions = { |
| 2647 | readValue : function(element) { |
| 2648 | switch (ko.utils.tagNameLower(element)) { |
| 2649 | case 'option': |
| 2650 | if (element[hasDomDataExpandoProperty] === true) |
| 2651 | return ko.utils.domData.get(element, ko.bindingHandlers.options.optionValueDomDataKey); |
| 2652 | return ko.utils.ieVersion <= 7 |
| 2653 | ? (element.getAttributeNode('value') && element.getAttributeNode('value').specified ? element.value : element.text) |
| 2654 | : element.value; |
| 2655 | case 'select': |
| 2656 | return element.selectedIndex >= 0 ? ko.selectExtensions.readValue(element.options[element.selectedIndex]) : undefined; |
| 2657 | default: |
| 2658 | return element.value; |
| 2659 | } |
| 2660 | }, |
| 2661 | |
| 2662 | writeValue: function(element, value, allowUnset) { |
| 2663 | switch (ko.utils.tagNameLower(element)) { |
| 2664 | case 'option': |
| 2665 | if (typeof value === "string") { |
| 2666 | ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, undefined); |
| 2667 | if (hasDomDataExpandoProperty in element) { // IE <= 8 throws errors if you delete non-existent properties from a DOM node |
| 2668 | delete element[hasDomDataExpandoProperty]; |
| 2669 | } |
| 2670 | element.value = value; |
| 2671 | } |
| 2672 | else { |
| 2673 | // Store arbitrary object using DomData |
| 2674 | ko.utils.domData.set(element, ko.bindingHandlers.options.optionValueDomDataKey, value); |
| 2675 | element[hasDomDataExpandoProperty] = true; |
| 2676 | |
| 2677 | // Special treatment of numbers is just for backward compatibility. KO 1.2.1 wrote numerical values to element.value. |
| 2678 | element.value = typeof value === "number" ? value : ""; |
| 2679 | } |
| 2680 | break; |
| 2681 | case 'select': |
| 2682 | if (value === "" || value === null) // A blank string or null value will select the caption |
| 2683 | value = undefined; |
| 2684 | var selection = -1; |
| 2685 | for (var i = 0, n = element.options.length, optionValue; i < n; ++i) { |
| 2686 | optionValue = ko.selectExtensions.readValue(element.options[i]); |
| 2687 | // Include special check to handle selecting a caption with a blank string value |
| 2688 | if (optionValue == value || (optionValue === "" && value === undefined)) { |
| 2689 | selection = i; |
| 2690 | break; |
| 2691 | } |
| 2692 | } |
| 2693 | if (allowUnset || selection >= 0 || (value === undefined && element.size > 1)) { |
| 2694 | element.selectedIndex = selection; |
| 2695 | if (ko.utils.ieVersion === 6) { |
| 2696 | // Workaround for IE6 bug: It won't reliably apply values to SELECT nodes during the same execution thread |
| 2697 | // right after you've changed the set of OPTION nodes on it. So for that node type, we'll schedule a second thread |
| 2698 | // to apply the value as well. |
| 2699 | ko.utils.setTimeout(function () { |
| 2700 | element.selectedIndex = selection; |
| 2701 | }, 0); |
| 2702 | } |
| 2703 | } |
| 2704 | break; |
| 2705 | default: |
| 2706 | if ((value === null) || (value === undefined)) |
| 2707 | value = ""; |
| 2708 | element.value = value; |
| 2709 | break; |
| 2710 | } |
| 2711 | } |
| 2712 | }; |
| 2713 | })(); |
| 2714 | |
| 2715 | ko.exportSymbol('selectExtensions', ko.selectExtensions); |
| 2716 | ko.exportSymbol('selectExtensions.readValue', ko.selectExtensions.readValue); |
| 2717 | ko.exportSymbol('selectExtensions.writeValue', ko.selectExtensions.writeValue); |
| 2718 | ko.expressionRewriting = (function () { |
| 2719 | var javaScriptReservedWords = ["true", "false", "null", "undefined"]; |
| 2720 | |
| 2721 | // Matches something that can be assigned to--either an isolated identifier or something ending with a property accessor |
| 2722 | // This is designed to be simple and avoid false negatives, but could produce false positives (e.g., a+b.c). |
| 2723 | // This also will not properly handle nested brackets (e.g., obj1[obj2['prop']]; see #911). |
| 2724 | var javaScriptAssignmentTarget = /^(?:[$_a-z][$\w]*|(.+)(\.\s*[$_a-z][$\w]*|\[.+\]))$/i; |
| 2725 | |
| 2726 | function getWriteableValue(expression) { |
| 2727 | if (ko.utils.arrayIndexOf(javaScriptReservedWords, expression) >= 0) |
| 2728 | return false; |
| 2729 | var match = expression.match(javaScriptAssignmentTarget); |
| 2730 | return match === null ? false : match[1] ? ('Object(' + match[1] + ')' + match[2]) : expression; |
| 2731 | } |
| 2732 | |
| 2733 | // The following regular expressions will be used to split an object-literal string into tokens |
| 2734 | |
| 2735 | var specials = ',"\'`{}()/:[\\]', // These characters have special meaning to the parser and must not appear in the middle of a token, except as part of a string. |
| 2736 | // Create the actual regular expression by or-ing the following regex strings. The order is important. |
| 2737 | bindingToken = RegExp([ |
| 2738 | // These match strings, either with double quotes, single quotes, or backticks |
| 2739 | '"(?:\\\\.|[^"])*"', |
| 2740 | "'(?:\\\\.|[^'])*'", |
| 2741 | "`(?:\\\\.|[^`])*`", |
| 2742 | // Match C style comments |
| 2743 | "/\\*(?:[^*]|\\*+[^*/])*\\*+/", |
| 2744 | // Match C++ style comments |
| 2745 | "//.*\n", |
| 2746 | // Match a regular expression (text enclosed by slashes), but will also match sets of divisions |
| 2747 | // as a regular expression (this is handled by the parsing loop below). |
| 2748 | '/(?:\\\\.|[^/])+/\w*', |
| 2749 | // Match text (at least two characters) that does not contain any of the above special characters, |
| 2750 | // although some of the special characters are allowed to start it (all but the colon and comma). |
| 2751 | // The text can contain spaces, but leading or trailing spaces are skipped. |
| 2752 | '[^\\s:,/][^' + specials + ']*[^\\s' + specials + ']', |
| 2753 | // Match any non-space character not matched already. This will match colons and commas, since they're |
| 2754 | // not matched by "everyThingElse", but will also match any other single character that wasn't already |
| 2755 | // matched (for example: in "a: 1, b: 2", each of the non-space characters will be matched by oneNotSpace). |
| 2756 | '[^\\s]' |
| 2757 | ].join('|'), 'g'), |
| 2758 | |
| 2759 | // Match end of previous token to determine whether a slash is a division or regex. |
| 2760 | divisionLookBehind = /[\])"'A-Za-z0-9_$]+$/, |
| 2761 | keywordRegexLookBehind = {'in':1,'return':1,'typeof':1}; |
| 2762 | |
| 2763 | function parseObjectLiteral(objectLiteralString) { |
| 2764 | // Trim leading and trailing spaces from the string |
| 2765 | var str = ko.utils.stringTrim(objectLiteralString); |
| 2766 | |
| 2767 | // Trim braces '{' surrounding the whole object literal |
| 2768 | if (str.charCodeAt(0) === 123) str = str.slice(1, -1); |
| 2769 | |
| 2770 | // Add a newline to correctly match a C++ style comment at the end of the string and |
| 2771 | // add a comma so that we don't need a separate code block to deal with the last item |
| 2772 | str += "\n,"; |
| 2773 | |
| 2774 | // Split into tokens |
| 2775 | var result = [], toks = str.match(bindingToken), key, values = [], depth = 0; |
| 2776 | |
| 2777 | if (toks.length > 1) { |
| 2778 | for (var i = 0, tok; tok = toks[i]; ++i) { |
| 2779 | var c = tok.charCodeAt(0); |
| 2780 | // A comma signals the end of a key/value pair if depth is zero |
| 2781 | if (c === 44) { // "," |
| 2782 | if (depth <= 0) { |
| 2783 | result.push((key && values.length) ? {key: key, value: values.join('')} : {'unknown': key || values.join('')}); |
| 2784 | key = depth = 0; |
| 2785 | values = []; |
| 2786 | continue; |
| 2787 | } |
| 2788 | // Simply skip the colon that separates the name and value |
| 2789 | } else if (c === 58) { // ":" |
| 2790 | if (!depth && !key && values.length === 1) { |
| 2791 | key = values.pop(); |
| 2792 | continue; |
| 2793 | } |
| 2794 | // Comments: skip them |
| 2795 | } else if (c === 47 && tok.length > 1 && (tok.charCodeAt(1) === 47 || tok.charCodeAt(1) === 42)) { // "//" or "/*" |
| 2796 | continue; |
| 2797 | // A set of slashes is initially matched as a regular expression, but could be division |
| 2798 | } else if (c === 47 && i && tok.length > 1) { // "/" |
| 2799 | // Look at the end of the previous token to determine if the slash is actually division |
| 2800 | var match = toks[i-1].match(divisionLookBehind); |
| 2801 | if (match && !keywordRegexLookBehind[match[0]]) { |
| 2802 | // The slash is actually a division punctuator; re-parse the remainder of the string (not including the slash) |
| 2803 | str = str.substr(str.indexOf(tok) + 1); |
| 2804 | toks = str.match(bindingToken); |
| 2805 | i = -1; |
| 2806 | // Continue with just the slash |
| 2807 | tok = '/'; |
| 2808 | } |
| 2809 | // Increment depth for parentheses, braces, and brackets so that interior commas are ignored |
| 2810 | } else if (c === 40 || c === 123 || c === 91) { // '(', '{', '[' |
| 2811 | ++depth; |
| 2812 | } else if (c === 41 || c === 125 || c === 93) { // ')', '}', ']' |
| 2813 | --depth; |
| 2814 | // The key will be the first token; if it's a string, trim the quotes |
| 2815 | } else if (!key && !values.length && (c === 34 || c === 39)) { // '"', "'" |
| 2816 | tok = tok.slice(1, -1); |
| 2817 | } |
| 2818 | values.push(tok); |
| 2819 | } |
| 2820 | if (depth > 0) { |
| 2821 | throw Error("Unbalanced parentheses, braces, or brackets"); |
| 2822 | } |
| 2823 | } |
| 2824 | return result; |
| 2825 | } |
| 2826 | |
| 2827 | // Two-way bindings include a write function that allow the handler to update the value even if it's not an observable. |
| 2828 | var twoWayBindings = {}; |
| 2829 | |
| 2830 | function preProcessBindings(bindingsStringOrKeyValueArray, bindingOptions) { |
| 2831 | bindingOptions = bindingOptions || {}; |
| 2832 | |
| 2833 | function processKeyValue(key, val) { |
| 2834 | var writableVal; |
| 2835 | function callPreprocessHook(obj) { |
| 2836 | return (obj && obj['preprocess']) ? (val = obj['preprocess'](val, key, processKeyValue)) : true; |
| 2837 | } |
| 2838 | if (!bindingParams) { |
| 2839 | if (!callPreprocessHook(ko['getBindingHandler'](key))) |
| 2840 | return; |
| 2841 | |
| 2842 | if (twoWayBindings[key] && (writableVal = getWriteableValue(val))) { |
| 2843 | // For two-way bindings, provide a write method in case the value |
| 2844 | // isn't a writable observable. |
| 2845 | var writeKey = typeof twoWayBindings[key] == 'string' ? twoWayBindings[key] : key; |
| 2846 | propertyAccessorResultStrings.push("'" + writeKey + "':function(_z){" + writableVal + "=_z}"); |
| 2847 | } |
| 2848 | } |
| 2849 | // Values are wrapped in a function so that each value can be accessed independently |
| 2850 | if (makeValueAccessors) { |
| 2851 | val = 'function(){return ' + val + ' }'; |
| 2852 | } |
| 2853 | resultStrings.push("'" + key + "':" + val); |
| 2854 | } |
| 2855 | |
| 2856 | var resultStrings = [], |
| 2857 | propertyAccessorResultStrings = [], |
| 2858 | makeValueAccessors = bindingOptions['valueAccessors'], |
| 2859 | bindingParams = bindingOptions['bindingParams'], |
| 2860 | keyValueArray = typeof bindingsStringOrKeyValueArray === "string" ? |
| 2861 | parseObjectLiteral(bindingsStringOrKeyValueArray) : bindingsStringOrKeyValueArray; |
| 2862 | |
| 2863 | ko.utils.arrayForEach(keyValueArray, function(keyValue) { |
| 2864 | processKeyValue(keyValue.key || keyValue['unknown'], keyValue.value); |
| 2865 | }); |
| 2866 | |
| 2867 | if (propertyAccessorResultStrings.length) |
| 2868 | processKeyValue('_ko_property_writers', "{" + propertyAccessorResultStrings.join(",") + " }"); |
| 2869 | |
| 2870 | return resultStrings.join(","); |
| 2871 | } |
| 2872 | |
| 2873 | return { |
| 2874 | bindingRewriteValidators: [], |
| 2875 | |
| 2876 | twoWayBindings: twoWayBindings, |
| 2877 | |
| 2878 | parseObjectLiteral: parseObjectLiteral, |
| 2879 | |
| 2880 | preProcessBindings: preProcessBindings, |
| 2881 | |
| 2882 | keyValueArrayContainsKey: function(keyValueArray, key) { |
| 2883 | for (var i = 0; i < keyValueArray.length; i++) |
| 2884 | if (keyValueArray[i]['key'] == key) |
| 2885 | return true; |
| 2886 | return false; |
| 2887 | }, |
| 2888 | |
| 2889 | // Internal, private KO utility for updating model properties from within bindings |
| 2890 | // property: If the property being updated is (or might be) an observable, pass it here |
| 2891 | // If it turns out to be a writable observable, it will be written to directly |
| 2892 | // allBindings: An object with a get method to retrieve bindings in the current execution context. |
| 2893 | // This will be searched for a '_ko_property_writers' property in case you're writing to a non-observable |
| 2894 | // key: The key identifying the property to be written. Example: for { hasFocus: myValue }, write to 'myValue' by specifying the key 'hasFocus' |
| 2895 | // value: The value to be written |
| 2896 | // checkIfDifferent: If true, and if the property being written is a writable observable, the value will only be written if |
| 2897 | // it is !== existing value on that writable observable |
| 2898 | writeValueToProperty: function(property, allBindings, key, value, checkIfDifferent) { |
| 2899 | if (!property || !ko.isObservable(property)) { |
| 2900 | var propWriters = allBindings.get('_ko_property_writers'); |
| 2901 | if (propWriters && propWriters[key]) |
| 2902 | propWriters[key](value); |
| 2903 | } else if (ko.isWriteableObservable(property) && (!checkIfDifferent || property.peek() !== value)) { |
| 2904 | property(value); |
| 2905 | } |
| 2906 | } |
| 2907 | }; |
| 2908 | })(); |
| 2909 | |
| 2910 | ko.exportSymbol('expressionRewriting', ko.expressionRewriting); |
| 2911 | ko.exportSymbol('expressionRewriting.bindingRewriteValidators', ko.expressionRewriting.bindingRewriteValidators); |
| 2912 | ko.exportSymbol('expressionRewriting.parseObjectLiteral', ko.expressionRewriting.parseObjectLiteral); |
| 2913 | ko.exportSymbol('expressionRewriting.preProcessBindings', ko.expressionRewriting.preProcessBindings); |
| 2914 | |
| 2915 | // Making bindings explicitly declare themselves as "two way" isn't ideal in the long term (it would be better if |
| 2916 | // all bindings could use an official 'property writer' API without needing to declare that they might). However, |
| 2917 | // since this is not, and has never been, a public API (_ko_property_writers was never documented), it's acceptable |
| 2918 | // as an internal implementation detail in the short term. |
| 2919 | // For those developers who rely on _ko_property_writers in their custom bindings, we expose _twoWayBindings as an |
| 2920 | // undocumented feature that makes it relatively easy to upgrade to KO 3.0. However, this is still not an official |
| 2921 | // public API, and we reserve the right to remove it at any time if we create a real public property writers API. |
| 2922 | ko.exportSymbol('expressionRewriting._twoWayBindings', ko.expressionRewriting.twoWayBindings); |
| 2923 | |
| 2924 | // For backward compatibility, define the following aliases. (Previously, these function names were misleading because |
| 2925 | // they referred to JSON specifically, even though they actually work with arbitrary JavaScript object literal expressions.) |
| 2926 | ko.exportSymbol('jsonExpressionRewriting', ko.expressionRewriting); |
| 2927 | ko.exportSymbol('jsonExpressionRewriting.insertPropertyAccessorsIntoJson', ko.expressionRewriting.preProcessBindings); |
| 2928 | (function() { |
| 2929 | // "Virtual elements" is an abstraction on top of the usual DOM API which understands the notion that comment nodes |
| 2930 | // may be used to represent hierarchy (in addition to the DOM's natural hierarchy). |
| 2931 | // If you call the DOM-manipulating functions on ko.virtualElements, you will be able to read and write the state |
| 2932 | // of that virtual hierarchy |
| 2933 | // |
| 2934 | // The point of all this is to support containerless templates (e.g., <!-- ko foreach:someCollection -->blah<!-- /ko -->) |
| 2935 | // without having to scatter special cases all over the binding and templating code. |
| 2936 | |
| 2937 | // IE 9 cannot reliably read the "nodeValue" property of a comment node (see https://github.com/SteveSanderson/knockout/issues/186) |
| 2938 | // but it does give them a nonstandard alternative property called "text" that it can read reliably. Other browsers don't have that property. |
| 2939 | // So, use node.text where available, and node.nodeValue elsewhere |
| 2940 | var commentNodesHaveTextProperty = document && document.createComment("test").text === "<!--test-->"; |
| 2941 | |
| 2942 | var startCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*ko(?:\s+([\s\S]+))?\s*-->$/ : /^\s*ko(?:\s+([\s\S]+))?\s*$/; |
| 2943 | var endCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*\/ko\s*-->$/ : /^\s*\/ko\s*$/; |
| 2944 | var htmlTagsWithOptionallyClosingChildren = { 'ul': true, 'ol': true }; |
| 2945 | |
| 2946 | function isStartComment(node) { |
| 2947 | return (node.nodeType == 8) && startCommentRegex.test(commentNodesHaveTextProperty ? node.text : node.nodeValue); |
| 2948 | } |
| 2949 | |
| 2950 | function isEndComment(node) { |
| 2951 | return (node.nodeType == 8) && endCommentRegex.test(commentNodesHaveTextProperty ? node.text : node.nodeValue); |
| 2952 | } |
| 2953 | |
| 2954 | function isUnmatchedEndComment(node) { |
| 2955 | return isEndComment(node) && !(ko.utils.domData.get(node, matchedEndCommentDataKey)); |
| 2956 | } |
| 2957 | |
| 2958 | var matchedEndCommentDataKey = "__ko_matchedEndComment__" |
| 2959 | |
| 2960 | function getVirtualChildren(startComment, allowUnbalanced) { |
| 2961 | var currentNode = startComment; |
| 2962 | var depth = 1; |
| 2963 | var children = []; |
| 2964 | while (currentNode = currentNode.nextSibling) { |
| 2965 | if (isEndComment(currentNode)) { |
| 2966 | ko.utils.domData.set(currentNode, matchedEndCommentDataKey, true); |
| 2967 | depth--; |
| 2968 | if (depth === 0) |
| 2969 | return children; |
| 2970 | } |
| 2971 | |
| 2972 | children.push(currentNode); |
| 2973 | |
| 2974 | if (isStartComment(currentNode)) |
| 2975 | depth++; |
| 2976 | } |
| 2977 | if (!allowUnbalanced) |
| 2978 | throw new Error("Cannot find closing comment tag to match: " + startComment.nodeValue); |
| 2979 | return null; |
| 2980 | } |
| 2981 | |
| 2982 | function getMatchingEndComment(startComment, allowUnbalanced) { |
| 2983 | var allVirtualChildren = getVirtualChildren(startComment, allowUnbalanced); |
| 2984 | if (allVirtualChildren) { |
| 2985 | if (allVirtualChildren.length > 0) |
| 2986 | return allVirtualChildren[allVirtualChildren.length - 1].nextSibling; |
| 2987 | return startComment.nextSibling; |
| 2988 | } else |
| 2989 | return null; // Must have no matching end comment, and allowUnbalanced is true |
| 2990 | } |
| 2991 | |
| 2992 | function getUnbalancedChildTags(node) { |
| 2993 | // e.g., from <div>OK</div><!-- ko blah --><span>Another</span>, returns: <!-- ko blah --><span>Another</span> |
| 2994 | // from <div>OK</div><!-- /ko --><!-- /ko -->, returns: <!-- /ko --><!-- /ko --> |
| 2995 | var childNode = node.firstChild, captureRemaining = null; |
| 2996 | if (childNode) { |
| 2997 | do { |
| 2998 | if (captureRemaining) // We already hit an unbalanced node and are now just scooping up all subsequent nodes |
| 2999 | captureRemaining.push(childNode); |
| 3000 | else if (isStartComment(childNode)) { |
| 3001 | var matchingEndComment = getMatchingEndComment(childNode, /* allowUnbalanced: */ true); |
| 3002 | if (matchingEndComment) // It's a balanced tag, so skip immediately to the end of this virtual set |
| 3003 | childNode = matchingEndComment; |
| 3004 | else |
| 3005 | captureRemaining = [childNode]; // It's unbalanced, so start capturing from this point |
| 3006 | } else if (isEndComment(childNode)) { |
| 3007 | captureRemaining = [childNode]; // It's unbalanced (if it wasn't, we'd have skipped over it already), so start capturing |
| 3008 | } |
| 3009 | } while (childNode = childNode.nextSibling); |
| 3010 | } |
| 3011 | return captureRemaining; |
| 3012 | } |
| 3013 | |
| 3014 | ko.virtualElements = { |
| 3015 | allowedBindings: {}, |
| 3016 | |
| 3017 | childNodes: function(node) { |
| 3018 | return isStartComment(node) ? getVirtualChildren(node) : node.childNodes; |
| 3019 | }, |
| 3020 | |
| 3021 | emptyNode: function(node) { |
| 3022 | if (!isStartComment(node)) |
| 3023 | ko.utils.emptyDomNode(node); |
| 3024 | else { |
| 3025 | var virtualChildren = ko.virtualElements.childNodes(node); |
| 3026 | for (var i = 0, j = virtualChildren.length; i < j; i++) |
| 3027 | ko.removeNode(virtualChildren[i]); |
| 3028 | } |
| 3029 | }, |
| 3030 | |
| 3031 | setDomNodeChildren: function(node, childNodes) { |
| 3032 | if (!isStartComment(node)) |
| 3033 | ko.utils.setDomNodeChildren(node, childNodes); |
| 3034 | else { |
| 3035 | ko.virtualElements.emptyNode(node); |
| 3036 | var endCommentNode = node.nextSibling; // Must be the next sibling, as we just emptied the children |
| 3037 | for (var i = 0, j = childNodes.length; i < j; i++) |
| 3038 | endCommentNode.parentNode.insertBefore(childNodes[i], endCommentNode); |
| 3039 | } |
| 3040 | }, |
| 3041 | |
| 3042 | prepend: function(containerNode, nodeToPrepend) { |
| 3043 | if (!isStartComment(containerNode)) { |
| 3044 | if (containerNode.firstChild) |
| 3045 | containerNode.insertBefore(nodeToPrepend, containerNode.firstChild); |
| 3046 | else |
| 3047 | containerNode.appendChild(nodeToPrepend); |
| 3048 | } else { |
| 3049 | // Start comments must always have a parent and at least one following sibling (the end comment) |
| 3050 | containerNode.parentNode.insertBefore(nodeToPrepend, containerNode.nextSibling); |
| 3051 | } |
| 3052 | }, |
| 3053 | |
| 3054 | insertAfter: function(containerNode, nodeToInsert, insertAfterNode) { |
| 3055 | if (!insertAfterNode) { |
| 3056 | ko.virtualElements.prepend(containerNode, nodeToInsert); |
| 3057 | } else if (!isStartComment(containerNode)) { |
| 3058 | // Insert after insertion point |
| 3059 | if (insertAfterNode.nextSibling) |
| 3060 | containerNode.insertBefore(nodeToInsert, insertAfterNode.nextSibling); |
| 3061 | else |
| 3062 | containerNode.appendChild(nodeToInsert); |
| 3063 | } else { |
| 3064 | // Children of start comments must always have a parent and at least one following sibling (the end comment) |
| 3065 | containerNode.parentNode.insertBefore(nodeToInsert, insertAfterNode.nextSibling); |
| 3066 | } |
| 3067 | }, |
| 3068 | |
| 3069 | firstChild: function(node) { |
| 3070 | if (!isStartComment(node)) { |
| 3071 | if (node.firstChild && isEndComment(node.firstChild)) { |
| 3072 | throw new Error("Found invalid end comment, as the first child of " + node); |
| 3073 | } |
| 3074 | return node.firstChild; |
| 3075 | } else if (!node.nextSibling || isEndComment(node.nextSibling)) { |
| 3076 | return null; |
| 3077 | } else { |
| 3078 | return node.nextSibling; |
| 3079 | } |
| 3080 | }, |
| 3081 | |
| 3082 | nextSibling: function(node) { |
| 3083 | if (isStartComment(node)) { |
| 3084 | node = getMatchingEndComment(node); |
| 3085 | } |
| 3086 | |
| 3087 | if (node.nextSibling && isEndComment(node.nextSibling)) { |
| 3088 | if (isUnmatchedEndComment(node.nextSibling)) { |
| 3089 | throw Error("Found end comment without a matching opening comment, as child of " + node); |
| 3090 | } else { |
| 3091 | return null; |
| 3092 | } |
| 3093 | } else { |
| 3094 | return node.nextSibling; |
| 3095 | } |
| 3096 | }, |
| 3097 | |
| 3098 | hasBindingValue: isStartComment, |
| 3099 | |
| 3100 | virtualNodeBindingValue: function(node) { |
| 3101 | var regexMatch = (commentNodesHaveTextProperty ? node.text : node.nodeValue).match(startCommentRegex); |
| 3102 | return regexMatch ? regexMatch[1] : null; |
| 3103 | }, |
| 3104 | |
| 3105 | normaliseVirtualElementDomStructure: function(elementVerified) { |
| 3106 | // Workaround for https://github.com/SteveSanderson/knockout/issues/155 |
| 3107 | // (IE <= 8 or IE 9 quirks mode parses your HTML weirdly, treating closing </li> tags as if they don't exist, thereby moving comment nodes |
| 3108 | // that are direct descendants of <ul> into the preceding <li>) |
| 3109 | if (!htmlTagsWithOptionallyClosingChildren[ko.utils.tagNameLower(elementVerified)]) |
| 3110 | return; |
| 3111 | |
| 3112 | // Scan immediate children to see if they contain unbalanced comment tags. If they do, those comment tags |
| 3113 | // must be intended to appear *after* that child, so move them there. |
| 3114 | var childNode = elementVerified.firstChild; |
| 3115 | if (childNode) { |
| 3116 | do { |
| 3117 | if (childNode.nodeType === 1) { |
| 3118 | var unbalancedTags = getUnbalancedChildTags(childNode); |
| 3119 | if (unbalancedTags) { |
| 3120 | // Fix up the DOM by moving the unbalanced tags to where they most likely were intended to be placed - *after* the child |
| 3121 | var nodeToInsertBefore = childNode.nextSibling; |
| 3122 | for (var i = 0; i < unbalancedTags.length; i++) { |
| 3123 | if (nodeToInsertBefore) |
| 3124 | elementVerified.insertBefore(unbalancedTags[i], nodeToInsertBefore); |
| 3125 | else |
| 3126 | elementVerified.appendChild(unbalancedTags[i]); |
| 3127 | } |
| 3128 | } |
| 3129 | } |
| 3130 | } while (childNode = childNode.nextSibling); |
| 3131 | } |
| 3132 | } |
| 3133 | }; |
| 3134 | })(); |
| 3135 | ko.exportSymbol('virtualElements', ko.virtualElements); |
| 3136 | ko.exportSymbol('virtualElements.allowedBindings', ko.virtualElements.allowedBindings); |
| 3137 | ko.exportSymbol('virtualElements.emptyNode', ko.virtualElements.emptyNode); |
| 3138 | //ko.exportSymbol('virtualElements.firstChild', ko.virtualElements.firstChild); // firstChild is not minified |
| 3139 | ko.exportSymbol('virtualElements.insertAfter', ko.virtualElements.insertAfter); |
| 3140 | //ko.exportSymbol('virtualElements.nextSibling', ko.virtualElements.nextSibling); // nextSibling is not minified |
| 3141 | ko.exportSymbol('virtualElements.prepend', ko.virtualElements.prepend); |
| 3142 | ko.exportSymbol('virtualElements.setDomNodeChildren', ko.virtualElements.setDomNodeChildren); |
| 3143 | (function() { |
| 3144 | var defaultBindingAttributeName = "data-bind"; |
| 3145 | |
| 3146 | ko.bindingProvider = function() { |
| 3147 | this.bindingCache = {}; |
| 3148 | }; |
| 3149 | |
| 3150 | ko.utils.extend(ko.bindingProvider.prototype, { |
| 3151 | 'nodeHasBindings': function(node) { |
| 3152 | switch (node.nodeType) { |
| 3153 | case 1: // Element |
| 3154 | return node.getAttribute(defaultBindingAttributeName) != null |
| 3155 | || ko.components['getComponentNameForNode'](node); |
| 3156 | case 8: // Comment node |
| 3157 | return ko.virtualElements.hasBindingValue(node); |
| 3158 | default: return false; |
| 3159 | } |
| 3160 | }, |
| 3161 | |
| 3162 | 'getBindings': function(node, bindingContext) { |
| 3163 | var bindingsString = this['getBindingsString'](node, bindingContext), |
| 3164 | parsedBindings = bindingsString ? this['parseBindingsString'](bindingsString, bindingContext, node) : null; |
| 3165 | return ko.components.addBindingsForCustomElement(parsedBindings, node, bindingContext, /* valueAccessors */ false); |
| 3166 | }, |
| 3167 | |
| 3168 | 'getBindingAccessors': function(node, bindingContext) { |
| 3169 | var bindingsString = this['getBindingsString'](node, bindingContext), |
| 3170 | parsedBindings = bindingsString ? this['parseBindingsString'](bindingsString, bindingContext, node, { 'valueAccessors': true }) : null; |
| 3171 | return ko.components.addBindingsForCustomElement(parsedBindings, node, bindingContext, /* valueAccessors */ true); |
| 3172 | }, |
| 3173 | |
| 3174 | // The following function is only used internally by this default provider. |
| 3175 | // It's not part of the interface definition for a general binding provider. |
| 3176 | 'getBindingsString': function(node, bindingContext) { |
| 3177 | switch (node.nodeType) { |
| 3178 | case 1: return node.getAttribute(defaultBindingAttributeName); // Element |
| 3179 | case 8: return ko.virtualElements.virtualNodeBindingValue(node); // Comment node |
| 3180 | default: return null; |
| 3181 | } |
| 3182 | }, |
| 3183 | |
| 3184 | // The following function is only used internally by this default provider. |
| 3185 | // It's not part of the interface definition for a general binding provider. |
| 3186 | 'parseBindingsString': function(bindingsString, bindingContext, node, options) { |
| 3187 | try { |
| 3188 | var bindingFunction = createBindingsStringEvaluatorViaCache(bindingsString, this.bindingCache, options); |
| 3189 | return bindingFunction(bindingContext, node); |
| 3190 | } catch (ex) { |
| 3191 | ex.message = "Unable to parse bindings.\nBindings value: " + bindingsString + "\nMessage: " + ex.message; |
| 3192 | throw ex; |
| 3193 | } |
| 3194 | } |
| 3195 | }); |
| 3196 | |
| 3197 | ko.bindingProvider['instance'] = new ko.bindingProvider(); |
| 3198 | |
| 3199 | function createBindingsStringEvaluatorViaCache(bindingsString, cache, options) { |
| 3200 | var cacheKey = bindingsString + (options && options['valueAccessors'] || ''); |
| 3201 | return cache[cacheKey] |
| 3202 | || (cache[cacheKey] = createBindingsStringEvaluator(bindingsString, options)); |
| 3203 | } |
| 3204 | |
| 3205 | function createBindingsStringEvaluator(bindingsString, options) { |
| 3206 | // Build the source for a function that evaluates "expression" |
| 3207 | // For each scope variable, add an extra level of "with" nesting |
| 3208 | // Example result: with(sc1) { with(sc0) { return (expression) } } |
| 3209 | var rewrittenBindings = ko.expressionRewriting.preProcessBindings(bindingsString, options), |
| 3210 | functionBody = "with($context){with($data||{}){return{" + rewrittenBindings + "}}}"; |
| 3211 | return new Function("$context", "$element", functionBody); |
| 3212 | } |
| 3213 | })(); |
| 3214 | |
| 3215 | ko.exportSymbol('bindingProvider', ko.bindingProvider); |
| 3216 | (function () { |
| 3217 | // Hide or don't minify context properties, see https://github.com/knockout/knockout/issues/2294 |
| 3218 | var contextSubscribable = ko.utils.createSymbolOrString('_subscribable'); |
| 3219 | var contextAncestorBindingInfo = ko.utils.createSymbolOrString('_ancestorBindingInfo'); |
| 3220 | var contextDataDependency = ko.utils.createSymbolOrString('_dataDependency'); |
| 3221 | |
| 3222 | ko.bindingHandlers = {}; |
| 3223 | |
| 3224 | // The following element types will not be recursed into during binding. |
| 3225 | var bindingDoesNotRecurseIntoElementTypes = { |
| 3226 | // Don't want bindings that operate on text nodes to mutate <script> and <textarea> contents, |
| 3227 | // because it's unexpected and a potential XSS issue. |
| 3228 | // Also bindings should not operate on <template> elements since this breaks in Internet Explorer |
| 3229 | // and because such elements' contents are always intended to be bound in a different context |
| 3230 | // from where they appear in the document. |
| 3231 | 'script': true, |
| 3232 | 'textarea': true, |
| 3233 | 'template': true |
| 3234 | }; |
| 3235 | |
| 3236 | // Use an overridable method for retrieving binding handlers so that plugins may support dynamically created handlers |
| 3237 | ko['getBindingHandler'] = function(bindingKey) { |
| 3238 | return ko.bindingHandlers[bindingKey]; |
| 3239 | }; |
| 3240 | |
| 3241 | var inheritParentVm = {}; |
| 3242 | |
| 3243 | // The ko.bindingContext constructor is only called directly to create the root context. For child |
| 3244 | // contexts, use bindingContext.createChildContext or bindingContext.extend. |
| 3245 | ko.bindingContext = function(dataItemOrAccessor, parentContext, dataItemAlias, extendCallback, options) { |
| 3246 | |
| 3247 | // The binding context object includes static properties for the current, parent, and root view models. |
| 3248 | // If a view model is actually stored in an observable, the corresponding binding context object, and |
| 3249 | // any child contexts, must be updated when the view model is changed. |
| 3250 | function updateContext() { |
| 3251 | // Most of the time, the context will directly get a view model object, but if a function is given, |
| 3252 | // we call the function to retrieve the view model. If the function accesses any observables or returns |
| 3253 | // an observable, the dependency is tracked, and those observables can later cause the binding |
| 3254 | // context to be updated. |
| 3255 | var dataItemOrObservable = isFunc ? realDataItemOrAccessor() : realDataItemOrAccessor, |
| 3256 | dataItem = ko.utils.unwrapObservable(dataItemOrObservable); |
| 3257 | |
| 3258 | if (parentContext) { |
| 3259 | // Copy $root and any custom properties from the parent context |
| 3260 | ko.utils.extend(self, parentContext); |
| 3261 | |
| 3262 | // Copy Symbol properties |
| 3263 | if (contextAncestorBindingInfo in parentContext) { |
| 3264 | self[contextAncestorBindingInfo] = parentContext[contextAncestorBindingInfo]; |
| 3265 | } |
| 3266 | } else { |
| 3267 | self['$parents'] = []; |
| 3268 | self['$root'] = dataItem; |
| 3269 | |
| 3270 | // Export 'ko' in the binding context so it will be available in bindings and templates |
| 3271 | // even if 'ko' isn't exported as a global, such as when using an AMD loader. |
| 3272 | // See https://github.com/SteveSanderson/knockout/issues/490 |
| 3273 | self['ko'] = ko; |
| 3274 | } |
| 3275 | |
| 3276 | self[contextSubscribable] = subscribable; |
| 3277 | |
| 3278 | if (shouldInheritData) { |
| 3279 | dataItem = self['$data']; |
| 3280 | } else { |
| 3281 | self['$rawData'] = dataItemOrObservable; |
| 3282 | self['$data'] = dataItem; |
| 3283 | } |
| 3284 | |
| 3285 | if (dataItemAlias) |
| 3286 | self[dataItemAlias] = dataItem; |
| 3287 | |
| 3288 | // The extendCallback function is provided when creating a child context or extending a context. |
| 3289 | // It handles the specific actions needed to finish setting up the binding context. Actions in this |
| 3290 | // function could also add dependencies to this binding context. |
| 3291 | if (extendCallback) |
| 3292 | extendCallback(self, parentContext, dataItem); |
| 3293 | |
| 3294 | // When a "parent" context is given and we don't already have a dependency on its context, register a dependency on it. |
| 3295 | // Thus whenever the parent context is updated, this context will also be updated. |
| 3296 | if (parentContext && parentContext[contextSubscribable] && !ko.computedContext.computed().hasAncestorDependency(parentContext[contextSubscribable])) { |
| 3297 | parentContext[contextSubscribable](); |
| 3298 | } |
| 3299 | |
| 3300 | if (dataDependency) { |
| 3301 | self[contextDataDependency] = dataDependency; |
| 3302 | } |
| 3303 | |
| 3304 | return self['$data']; |
| 3305 | } |
| 3306 | |
| 3307 | var self = this, |
| 3308 | shouldInheritData = dataItemOrAccessor === inheritParentVm, |
| 3309 | realDataItemOrAccessor = shouldInheritData ? undefined : dataItemOrAccessor, |
| 3310 | isFunc = typeof(realDataItemOrAccessor) == "function" && !ko.isObservable(realDataItemOrAccessor), |
| 3311 | nodes, |
| 3312 | subscribable, |
| 3313 | dataDependency = options && options['dataDependency']; |
| 3314 | |
| 3315 | if (options && options['exportDependencies']) { |
| 3316 | // The "exportDependencies" option means that the calling code will track any dependencies and re-create |
| 3317 | // the binding context when they change. |
| 3318 | updateContext(); |
| 3319 | } else { |
| 3320 | subscribable = ko.pureComputed(updateContext); |
| 3321 | subscribable.peek(); |
| 3322 | |
| 3323 | // At this point, the binding context has been initialized, and the "subscribable" computed observable is |
| 3324 | // subscribed to any observables that were accessed in the process. If there is nothing to track, the |
| 3325 | // computed will be inactive, and we can safely throw it away. If it's active, the computed is stored in |
| 3326 | // the context object. |
| 3327 | if (subscribable.isActive()) { |
| 3328 | // Always notify because even if the model ($data) hasn't changed, other context properties might have changed |
| 3329 | subscribable['equalityComparer'] = null; |
| 3330 | } else { |
| 3331 | self[contextSubscribable] = undefined; |
| 3332 | } |
| 3333 | } |
| 3334 | } |
| 3335 | |
| 3336 | // Extend the binding context hierarchy with a new view model object. If the parent context is watching |
| 3337 | // any observables, the new child context will automatically get a dependency on the parent context. |
| 3338 | // But this does not mean that the $data value of the child context will also get updated. If the child |
| 3339 | // view model also depends on the parent view model, you must provide a function that returns the correct |
| 3340 | // view model on each update. |
| 3341 | ko.bindingContext.prototype['createChildContext'] = function (dataItemOrAccessor, dataItemAlias, extendCallback, options) { |
| 3342 | if (!options && dataItemAlias && typeof dataItemAlias == "object") { |
| 3343 | options = dataItemAlias; |
| 3344 | dataItemAlias = options['as']; |
| 3345 | extendCallback = options['extend']; |
| 3346 | } |
| 3347 | |
| 3348 | if (dataItemAlias && options && options['noChildContext']) { |
| 3349 | var isFunc = typeof(dataItemOrAccessor) == "function" && !ko.isObservable(dataItemOrAccessor); |
| 3350 | return new ko.bindingContext(inheritParentVm, this, null, function (self) { |
| 3351 | if (extendCallback) |
| 3352 | extendCallback(self); |
| 3353 | self[dataItemAlias] = isFunc ? dataItemOrAccessor() : dataItemOrAccessor; |
| 3354 | }, options); |
| 3355 | } |
| 3356 | |
| 3357 | return new ko.bindingContext(dataItemOrAccessor, this, dataItemAlias, function (self, parentContext) { |
| 3358 | // Extend the context hierarchy by setting the appropriate pointers |
| 3359 | self['$parentContext'] = parentContext; |
| 3360 | self['$parent'] = parentContext['$data']; |
| 3361 | self['$parents'] = (parentContext['$parents'] || []).slice(0); |
| 3362 | self['$parents'].unshift(self['$parent']); |
| 3363 | if (extendCallback) |
| 3364 | extendCallback(self); |
| 3365 | }, options); |
| 3366 | }; |
| 3367 | |
| 3368 | // Extend the binding context with new custom properties. This doesn't change the context hierarchy. |
| 3369 | // Similarly to "child" contexts, provide a function here to make sure that the correct values are set |
| 3370 | // when an observable view model is updated. |
| 3371 | ko.bindingContext.prototype['extend'] = function(properties, options) { |
| 3372 | return new ko.bindingContext(inheritParentVm, this, null, function(self, parentContext) { |
| 3373 | ko.utils.extend(self, typeof(properties) == "function" ? properties(self) : properties); |
| 3374 | }, options); |
| 3375 | }; |
| 3376 | |
| 3377 | var boundElementDomDataKey = ko.utils.domData.nextKey(); |
| 3378 | |
| 3379 | function asyncContextDispose(node) { |
| 3380 | var bindingInfo = ko.utils.domData.get(node, boundElementDomDataKey), |
| 3381 | asyncContext = bindingInfo && bindingInfo.asyncContext; |
| 3382 | if (asyncContext) { |
| 3383 | bindingInfo.asyncContext = null; |
| 3384 | asyncContext.notifyAncestor(); |
| 3385 | } |
| 3386 | } |
| 3387 | function AsyncCompleteContext(node, bindingInfo, ancestorBindingInfo) { |
| 3388 | this.node = node; |
| 3389 | this.bindingInfo = bindingInfo; |
| 3390 | this.asyncDescendants = []; |
| 3391 | this.childrenComplete = false; |
| 3392 | |
| 3393 | if (!bindingInfo.asyncContext) { |
| 3394 | ko.utils.domNodeDisposal.addDisposeCallback(node, asyncContextDispose); |
| 3395 | } |
| 3396 | |
| 3397 | if (ancestorBindingInfo && ancestorBindingInfo.asyncContext) { |
| 3398 | ancestorBindingInfo.asyncContext.asyncDescendants.push(node); |
| 3399 | this.ancestorBindingInfo = ancestorBindingInfo; |
| 3400 | } |
| 3401 | } |
| 3402 | AsyncCompleteContext.prototype.notifyAncestor = function () { |
| 3403 | if (this.ancestorBindingInfo && this.ancestorBindingInfo.asyncContext) { |
| 3404 | this.ancestorBindingInfo.asyncContext.descendantComplete(this.node); |
| 3405 | } |
| 3406 | }; |
| 3407 | AsyncCompleteContext.prototype.descendantComplete = function (node) { |
| 3408 | ko.utils.arrayRemoveItem(this.asyncDescendants, node); |
| 3409 | if (!this.asyncDescendants.length && this.childrenComplete) { |
| 3410 | this.completeChildren(); |
| 3411 | } |
| 3412 | }; |
| 3413 | AsyncCompleteContext.prototype.completeChildren = function () { |
| 3414 | this.childrenComplete = true; |
| 3415 | if (this.bindingInfo.asyncContext && !this.asyncDescendants.length) { |
| 3416 | this.bindingInfo.asyncContext = null; |
| 3417 | ko.utils.domNodeDisposal.removeDisposeCallback(this.node, asyncContextDispose); |
| 3418 | ko.bindingEvent.notify(this.node, ko.bindingEvent.descendantsComplete); |
| 3419 | this.notifyAncestor(); |
| 3420 | } |
| 3421 | }; |
| 3422 | |
| 3423 | ko.bindingEvent = { |
| 3424 | childrenComplete: "childrenComplete", |
| 3425 | descendantsComplete : "descendantsComplete", |
| 3426 | |
| 3427 | subscribe: function (node, event, callback, context) { |
| 3428 | var bindingInfo = ko.utils.domData.getOrSet(node, boundElementDomDataKey, {}); |
| 3429 | if (!bindingInfo.eventSubscribable) { |
| 3430 | bindingInfo.eventSubscribable = new ko.subscribable; |
| 3431 | } |
| 3432 | return bindingInfo.eventSubscribable.subscribe(callback, context, event); |
| 3433 | }, |
| 3434 | |
| 3435 | notify: function (node, event) { |
| 3436 | var bindingInfo = ko.utils.domData.get(node, boundElementDomDataKey); |
| 3437 | if (bindingInfo) { |
| 3438 | if (bindingInfo.eventSubscribable) { |
| 3439 | bindingInfo.eventSubscribable['notifySubscribers'](node, event); |
| 3440 | } |
| 3441 | if (event == ko.bindingEvent.childrenComplete) { |
| 3442 | if (bindingInfo.asyncContext) { |
| 3443 | bindingInfo.asyncContext.completeChildren(); |
| 3444 | } else if (bindingInfo.asyncContext === undefined && bindingInfo.eventSubscribable && bindingInfo.eventSubscribable.hasSubscriptionsForEvent(ko.bindingEvent.descendantsComplete)) { |
| 3445 | // It's currently an error to register a descendantsComplete handler for a node that was never registered as completing asynchronously. |
| 3446 | // That's because without the asyncContext, we don't have a way to know that all descendants have completed. |
| 3447 | throw new Error("descendantsComplete event not supported for bindings on this node"); |
| 3448 | } |
| 3449 | } |
| 3450 | } |
| 3451 | }, |
| 3452 | |
| 3453 | startPossiblyAsyncContentBinding: function (node, bindingContext) { |
| 3454 | var bindingInfo = ko.utils.domData.getOrSet(node, boundElementDomDataKey, {}); |
| 3455 | |
| 3456 | if (!bindingInfo.asyncContext) { |
| 3457 | bindingInfo.asyncContext = new AsyncCompleteContext(node, bindingInfo, bindingContext[contextAncestorBindingInfo]); |
| 3458 | } |
| 3459 | |
| 3460 | // If the provided context was already extended with this node's binding info, just return the extended context |
| 3461 | if (bindingContext[contextAncestorBindingInfo] == bindingInfo) { |
| 3462 | return bindingContext; |
| 3463 | } |
| 3464 | |
| 3465 | return bindingContext['extend'](function (ctx) { |
| 3466 | ctx[contextAncestorBindingInfo] = bindingInfo; |
| 3467 | }); |
| 3468 | } |
| 3469 | }; |
| 3470 | |
| 3471 | // Returns the valueAccessor function for a binding value |
| 3472 | function makeValueAccessor(value) { |
| 3473 | return function() { |
| 3474 | return value; |
| 3475 | }; |
| 3476 | } |
| 3477 | |
| 3478 | // Returns the value of a valueAccessor function |
| 3479 | function evaluateValueAccessor(valueAccessor) { |
| 3480 | return valueAccessor(); |
| 3481 | } |
| 3482 | |
| 3483 | // Given a function that returns bindings, create and return a new object that contains |
| 3484 | // binding value-accessors functions. Each accessor function calls the original function |
| 3485 | // so that it always gets the latest value and all dependencies are captured. This is used |
| 3486 | // by ko.applyBindingsToNode and getBindingsAndMakeAccessors. |
| 3487 | function makeAccessorsFromFunction(callback) { |
| 3488 | return ko.utils.objectMap(ko.dependencyDetection.ignore(callback), function(value, key) { |
| 3489 | return function() { |
| 3490 | return callback()[key]; |
| 3491 | }; |
| 3492 | }); |
| 3493 | } |
| 3494 | |
| 3495 | // Given a bindings function or object, create and return a new object that contains |
| 3496 | // binding value-accessors functions. This is used by ko.applyBindingsToNode. |
| 3497 | function makeBindingAccessors(bindings, context, node) { |
| 3498 | if (typeof bindings === 'function') { |
| 3499 | return makeAccessorsFromFunction(bindings.bind(null, context, node)); |
| 3500 | } else { |
| 3501 | return ko.utils.objectMap(bindings, makeValueAccessor); |
| 3502 | } |
| 3503 | } |
| 3504 | |
| 3505 | // This function is used if the binding provider doesn't include a getBindingAccessors function. |
| 3506 | // It must be called with 'this' set to the provider instance. |
| 3507 | function getBindingsAndMakeAccessors(node, context) { |
| 3508 | return makeAccessorsFromFunction(this['getBindings'].bind(this, node, context)); |
| 3509 | } |
| 3510 | |
| 3511 | function validateThatBindingIsAllowedForVirtualElements(bindingName) { |
| 3512 | var validator = ko.virtualElements.allowedBindings[bindingName]; |
| 3513 | if (!validator) |
| 3514 | throw new Error("The binding '" + bindingName + "' cannot be used with virtual elements") |
| 3515 | } |
| 3516 | |
| 3517 | function applyBindingsToDescendantsInternal(bindingContext, elementOrVirtualElement) { |
| 3518 | var nextInQueue = ko.virtualElements.firstChild(elementOrVirtualElement); |
| 3519 | |
| 3520 | if (nextInQueue) { |
| 3521 | var currentChild, |
| 3522 | provider = ko.bindingProvider['instance'], |
| 3523 | preprocessNode = provider['preprocessNode']; |
| 3524 | |
| 3525 | // Preprocessing allows a binding provider to mutate a node before bindings are applied to it. For example it's |
| 3526 | // possible to insert new siblings after it, and/or replace the node with a different one. This can be used to |
| 3527 | // implement custom binding syntaxes, such as {{ value }} for string interpolation, or custom element types that |
| 3528 | // trigger insertion of <template> contents at that point in the document. |
| 3529 | if (preprocessNode) { |
| 3530 | while (currentChild = nextInQueue) { |
| 3531 | nextInQueue = ko.virtualElements.nextSibling(currentChild); |
| 3532 | preprocessNode.call(provider, currentChild); |
| 3533 | } |
| 3534 | // Reset nextInQueue for the next loop |
| 3535 | nextInQueue = ko.virtualElements.firstChild(elementOrVirtualElement); |
| 3536 | } |
| 3537 | |
| 3538 | while (currentChild = nextInQueue) { |
| 3539 | // Keep a record of the next child *before* applying bindings, in case the binding removes the current child from its position |
| 3540 | nextInQueue = ko.virtualElements.nextSibling(currentChild); |
| 3541 | applyBindingsToNodeAndDescendantsInternal(bindingContext, currentChild); |
| 3542 | } |
| 3543 | } |
| 3544 | ko.bindingEvent.notify(elementOrVirtualElement, ko.bindingEvent.childrenComplete); |
| 3545 | } |
| 3546 | |
| 3547 | function applyBindingsToNodeAndDescendantsInternal(bindingContext, nodeVerified) { |
| 3548 | var bindingContextForDescendants = bindingContext; |
| 3549 | |
| 3550 | var isElement = (nodeVerified.nodeType === 1); |
| 3551 | if (isElement) // Workaround IE <= 8 HTML parsing weirdness |
| 3552 | ko.virtualElements.normaliseVirtualElementDomStructure(nodeVerified); |
| 3553 | |
| 3554 | // Perf optimisation: Apply bindings only if... |
| 3555 | // (1) We need to store the binding info for the node (all element nodes) |
| 3556 | // (2) It might have bindings (e.g., it has a data-bind attribute, or it's a marker for a containerless template) |
| 3557 | var shouldApplyBindings = isElement || ko.bindingProvider['instance']['nodeHasBindings'](nodeVerified); |
| 3558 | if (shouldApplyBindings) |
| 3559 | bindingContextForDescendants = applyBindingsToNodeInternal(nodeVerified, null, bindingContext)['bindingContextForDescendants']; |
| 3560 | |
| 3561 | if (bindingContextForDescendants && !bindingDoesNotRecurseIntoElementTypes[ko.utils.tagNameLower(nodeVerified)]) { |
| 3562 | applyBindingsToDescendantsInternal(bindingContextForDescendants, nodeVerified); |
| 3563 | } |
| 3564 | } |
| 3565 | |
| 3566 | function topologicalSortBindings(bindings) { |
| 3567 | // Depth-first sort |
| 3568 | var result = [], // The list of key/handler pairs that we will return |
| 3569 | bindingsConsidered = {}, // A temporary record of which bindings are already in 'result' |
| 3570 | cyclicDependencyStack = []; // Keeps track of a depth-search so that, if there's a cycle, we know which bindings caused it |
| 3571 | ko.utils.objectForEach(bindings, function pushBinding(bindingKey) { |
| 3572 | if (!bindingsConsidered[bindingKey]) { |
| 3573 | var binding = ko['getBindingHandler'](bindingKey); |
| 3574 | if (binding) { |
| 3575 | // First add dependencies (if any) of the current binding |
| 3576 | if (binding['after']) { |
| 3577 | cyclicDependencyStack.push(bindingKey); |
| 3578 | ko.utils.arrayForEach(binding['after'], function(bindingDependencyKey) { |
| 3579 | if (bindings[bindingDependencyKey]) { |
| 3580 | if (ko.utils.arrayIndexOf(cyclicDependencyStack, bindingDependencyKey) !== -1) { |
| 3581 | throw Error("Cannot combine the following bindings, because they have a cyclic dependency: " + cyclicDependencyStack.join(", ")); |
| 3582 | } else { |
| 3583 | pushBinding(bindingDependencyKey); |
| 3584 | } |
| 3585 | } |
| 3586 | }); |
| 3587 | cyclicDependencyStack.length--; |
| 3588 | } |
| 3589 | // Next add the current binding |
| 3590 | result.push({ key: bindingKey, handler: binding }); |
| 3591 | } |
| 3592 | bindingsConsidered[bindingKey] = true; |
| 3593 | } |
| 3594 | }); |
| 3595 | |
| 3596 | return result; |
| 3597 | } |
| 3598 | |
| 3599 | function applyBindingsToNodeInternal(node, sourceBindings, bindingContext) { |
| 3600 | var bindingInfo = ko.utils.domData.getOrSet(node, boundElementDomDataKey, {}); |
| 3601 | |
| 3602 | // Prevent multiple applyBindings calls for the same node, except when a binding value is specified |
| 3603 | var alreadyBound = bindingInfo.alreadyBound; |
| 3604 | if (!sourceBindings) { |
| 3605 | if (alreadyBound) { |
| 3606 | throw Error("You cannot apply bindings multiple times to the same element."); |
| 3607 | } |
| 3608 | bindingInfo.alreadyBound = true; |
| 3609 | } |
| 3610 | if (!alreadyBound) { |
| 3611 | bindingInfo.context = bindingContext; |
| 3612 | } |
| 3613 | |
| 3614 | // Use bindings if given, otherwise fall back on asking the bindings provider to give us some bindings |
| 3615 | var bindings; |
| 3616 | if (sourceBindings && typeof sourceBindings !== 'function') { |
| 3617 | bindings = sourceBindings; |
| 3618 | } else { |
| 3619 | var provider = ko.bindingProvider['instance'], |
| 3620 | getBindings = provider['getBindingAccessors'] || getBindingsAndMakeAccessors; |
| 3621 | |
| 3622 | // Get the binding from the provider within a computed observable so that we can update the bindings whenever |
| 3623 | // the binding context is updated or if the binding provider accesses observables. |
| 3624 | var bindingsUpdater = ko.dependentObservable( |
| 3625 | function() { |
| 3626 | bindings = sourceBindings ? sourceBindings(bindingContext, node) : getBindings.call(provider, node, bindingContext); |
| 3627 | // Register a dependency on the binding context to support observable view models. |
| 3628 | if (bindings) { |
| 3629 | if (bindingContext[contextSubscribable]) { |
| 3630 | bindingContext[contextSubscribable](); |
| 3631 | } |
| 3632 | if (bindingContext[contextDataDependency]) { |
| 3633 | bindingContext[contextDataDependency](); |
| 3634 | } |
| 3635 | } |
| 3636 | return bindings; |
| 3637 | }, |
| 3638 | null, { disposeWhenNodeIsRemoved: node } |
| 3639 | ); |
| 3640 | |
| 3641 | if (!bindings || !bindingsUpdater.isActive()) |
| 3642 | bindingsUpdater = null; |
| 3643 | } |
| 3644 | |
| 3645 | var contextToExtend = bindingContext; |
| 3646 | var bindingHandlerThatControlsDescendantBindings; |
| 3647 | if (bindings) { |
| 3648 | // Return the value accessor for a given binding. When bindings are static (won't be updated because of a binding |
| 3649 | // context update), just return the value accessor from the binding. Otherwise, return a function that always gets |
| 3650 | // the latest binding value and registers a dependency on the binding updater. |
| 3651 | var getValueAccessor = bindingsUpdater |
| 3652 | ? function(bindingKey) { |
| 3653 | return function() { |
| 3654 | return evaluateValueAccessor(bindingsUpdater()[bindingKey]); |
| 3655 | }; |
| 3656 | } : function(bindingKey) { |
| 3657 | return bindings[bindingKey]; |
| 3658 | }; |
| 3659 | |
| 3660 | // Use of allBindings as a function is maintained for backwards compatibility, but its use is deprecated |
| 3661 | function allBindings() { |
| 3662 | return ko.utils.objectMap(bindingsUpdater ? bindingsUpdater() : bindings, evaluateValueAccessor); |
| 3663 | } |
| 3664 | // The following is the 3.x allBindings API |
| 3665 | allBindings['get'] = function(key) { |
| 3666 | return bindings[key] && evaluateValueAccessor(getValueAccessor(key)); |
| 3667 | }; |
| 3668 | allBindings['has'] = function(key) { |
| 3669 | return key in bindings; |
| 3670 | }; |
| 3671 | |
| 3672 | if (ko.bindingEvent.childrenComplete in bindings) { |
| 3673 | ko.bindingEvent.subscribe(node, ko.bindingEvent.childrenComplete, function () { |
| 3674 | var callback = evaluateValueAccessor(bindings[ko.bindingEvent.childrenComplete]); |
| 3675 | if (callback) { |
| 3676 | var nodes = ko.virtualElements.childNodes(node); |
| 3677 | if (nodes.length) { |
| 3678 | callback(nodes, ko.dataFor(nodes[0])); |
| 3679 | } |
| 3680 | } |
| 3681 | }); |
| 3682 | } |
| 3683 | |
| 3684 | if (ko.bindingEvent.descendantsComplete in bindings) { |
| 3685 | contextToExtend = ko.bindingEvent.startPossiblyAsyncContentBinding(node, bindingContext); |
| 3686 | ko.bindingEvent.subscribe(node, ko.bindingEvent.descendantsComplete, function () { |
| 3687 | var callback = evaluateValueAccessor(bindings[ko.bindingEvent.descendantsComplete]); |
| 3688 | if (callback && ko.virtualElements.firstChild(node)) { |
| 3689 | callback(node); |
| 3690 | } |
| 3691 | }); |
| 3692 | } |
| 3693 | |
| 3694 | // First put the bindings into the right order |
| 3695 | var orderedBindings = topologicalSortBindings(bindings); |
| 3696 | |
| 3697 | // Go through the sorted bindings, calling init and update for each |
| 3698 | ko.utils.arrayForEach(orderedBindings, function(bindingKeyAndHandler) { |
| 3699 | // Note that topologicalSortBindings has already filtered out any nonexistent binding handlers, |
| 3700 | // so bindingKeyAndHandler.handler will always be nonnull. |
| 3701 | var handlerInitFn = bindingKeyAndHandler.handler["init"], |
| 3702 | handlerUpdateFn = bindingKeyAndHandler.handler["update"], |
| 3703 | bindingKey = bindingKeyAndHandler.key; |
| 3704 | |
| 3705 | if (node.nodeType === 8) { |
| 3706 | validateThatBindingIsAllowedForVirtualElements(bindingKey); |
| 3707 | } |
| 3708 | |
| 3709 | try { |
| 3710 | // Run init, ignoring any dependencies |
| 3711 | if (typeof handlerInitFn == "function") { |
| 3712 | ko.dependencyDetection.ignore(function() { |
| 3713 | var initResult = handlerInitFn(node, getValueAccessor(bindingKey), allBindings, contextToExtend['$data'], contextToExtend); |
| 3714 | |
| 3715 | // If this binding handler claims to control descendant bindings, make a note of this |
| 3716 | if (initResult && initResult['controlsDescendantBindings']) { |
| 3717 | if (bindingHandlerThatControlsDescendantBindings !== undefined) |
| 3718 | throw new Error("Multiple bindings (" + bindingHandlerThatControlsDescendantBindings + " and " + bindingKey + ") are trying to control descendant bindings of the same element. You cannot use these bindings together on the same element."); |
| 3719 | bindingHandlerThatControlsDescendantBindings = bindingKey; |
| 3720 | } |
| 3721 | }); |
| 3722 | } |
| 3723 | |
| 3724 | // Run update in its own computed wrapper |
| 3725 | if (typeof handlerUpdateFn == "function") { |
| 3726 | ko.dependentObservable( |
| 3727 | function() { |
| 3728 | handlerUpdateFn(node, getValueAccessor(bindingKey), allBindings, contextToExtend['$data'], contextToExtend); |
| 3729 | }, |
| 3730 | null, |
| 3731 | { disposeWhenNodeIsRemoved: node } |
| 3732 | ); |
| 3733 | } |
| 3734 | } catch (ex) { |
| 3735 | ex.message = "Unable to process binding \"" + bindingKey + ": " + bindings[bindingKey] + "\"\nMessage: " + ex.message; |
| 3736 | throw ex; |
| 3737 | } |
| 3738 | }); |
| 3739 | } |
| 3740 | |
| 3741 | var shouldBindDescendants = bindingHandlerThatControlsDescendantBindings === undefined; |
| 3742 | return { |
| 3743 | 'shouldBindDescendants': shouldBindDescendants, |
| 3744 | 'bindingContextForDescendants': shouldBindDescendants && contextToExtend |
| 3745 | }; |
| 3746 | }; |
| 3747 | |
| 3748 | ko.storedBindingContextForNode = function (node) { |
| 3749 | var bindingInfo = ko.utils.domData.get(node, boundElementDomDataKey); |
| 3750 | return bindingInfo && bindingInfo.context; |
| 3751 | } |
| 3752 | |
| 3753 | function getBindingContext(viewModelOrBindingContext, extendContextCallback) { |
| 3754 | return viewModelOrBindingContext && (viewModelOrBindingContext instanceof ko.bindingContext) |
| 3755 | ? viewModelOrBindingContext |
| 3756 | : new ko.bindingContext(viewModelOrBindingContext, undefined, undefined, extendContextCallback); |
| 3757 | } |
| 3758 | |
| 3759 | ko.applyBindingAccessorsToNode = function (node, bindings, viewModelOrBindingContext) { |
| 3760 | if (node.nodeType === 1) // If it's an element, workaround IE <= 8 HTML parsing weirdness |
| 3761 | ko.virtualElements.normaliseVirtualElementDomStructure(node); |
| 3762 | return applyBindingsToNodeInternal(node, bindings, getBindingContext(viewModelOrBindingContext)); |
| 3763 | }; |
| 3764 | |
| 3765 | ko.applyBindingsToNode = function (node, bindings, viewModelOrBindingContext) { |
| 3766 | var context = getBindingContext(viewModelOrBindingContext); |
| 3767 | return ko.applyBindingAccessorsToNode(node, makeBindingAccessors(bindings, context, node), context); |
| 3768 | }; |
| 3769 | |
| 3770 | ko.applyBindingsToDescendants = function(viewModelOrBindingContext, rootNode) { |
| 3771 | if (rootNode.nodeType === 1 || rootNode.nodeType === 8) |
| 3772 | applyBindingsToDescendantsInternal(getBindingContext(viewModelOrBindingContext), rootNode); |
| 3773 | }; |
| 3774 | |
| 3775 | ko.applyBindings = function (viewModelOrBindingContext, rootNode, extendContextCallback) { |
| 3776 | // If jQuery is loaded after Knockout, we won't initially have access to it. So save it here. |
| 3777 | if (!jQueryInstance && window['jQuery']) { |
| 3778 | jQueryInstance = window['jQuery']; |
| 3779 | } |
| 3780 | |
| 3781 | if (arguments.length < 2) { |
| 3782 | rootNode = document.body; |
| 3783 | if (!rootNode) { |
| 3784 | throw Error("ko.applyBindings: could not find document.body; has the document been loaded?"); |
| 3785 | } |
| 3786 | } else if (!rootNode || (rootNode.nodeType !== 1 && rootNode.nodeType !== 8)) { |
| 3787 | throw Error("ko.applyBindings: first parameter should be your view model; second parameter should be a DOM node"); |
| 3788 | } |
| 3789 | |
| 3790 | applyBindingsToNodeAndDescendantsInternal(getBindingContext(viewModelOrBindingContext, extendContextCallback), rootNode); |
| 3791 | }; |
| 3792 | |
| 3793 | // Retrieving binding context from arbitrary nodes |
| 3794 | ko.contextFor = function(node) { |
| 3795 | // We can only do something meaningful for elements and comment nodes (in particular, not text nodes, as IE can't store domdata for them) |
| 3796 | if (node && (node.nodeType === 1 || node.nodeType === 8)) { |
| 3797 | return ko.storedBindingContextForNode(node); |
| 3798 | } |
| 3799 | return undefined; |
| 3800 | }; |
| 3801 | ko.dataFor = function(node) { |
| 3802 | var context = ko.contextFor(node); |
| 3803 | return context ? context['$data'] : undefined; |
| 3804 | }; |
| 3805 | |
| 3806 | ko.exportSymbol('bindingHandlers', ko.bindingHandlers); |
| 3807 | ko.exportSymbol('bindingEvent', ko.bindingEvent); |
| 3808 | ko.exportSymbol('bindingEvent.subscribe', ko.bindingEvent.subscribe); |
| 3809 | ko.exportSymbol('bindingEvent.startPossiblyAsyncContentBinding', ko.bindingEvent.startPossiblyAsyncContentBinding); |
| 3810 | ko.exportSymbol('applyBindings', ko.applyBindings); |
| 3811 | ko.exportSymbol('applyBindingsToDescendants', ko.applyBindingsToDescendants); |
| 3812 | ko.exportSymbol('applyBindingAccessorsToNode', ko.applyBindingAccessorsToNode); |
| 3813 | ko.exportSymbol('applyBindingsToNode', ko.applyBindingsToNode); |
| 3814 | ko.exportSymbol('contextFor', ko.contextFor); |
| 3815 | ko.exportSymbol('dataFor', ko.dataFor); |
| 3816 | })(); |
| 3817 | (function(undefined) { |
| 3818 | var loadingSubscribablesCache = {}, // Tracks component loads that are currently in flight |
| 3819 | loadedDefinitionsCache = {}; // Tracks component loads that have already completed |
| 3820 | |
| 3821 | ko.components = { |
| 3822 | get: function(componentName, callback) { |
| 3823 | var cachedDefinition = getObjectOwnProperty(loadedDefinitionsCache, componentName); |
| 3824 | if (cachedDefinition) { |
| 3825 | // It's already loaded and cached. Reuse the same definition object. |
| 3826 | // Note that for API consistency, even cache hits complete asynchronously by default. |
| 3827 | // You can bypass this by putting synchronous:true on your component config. |
| 3828 | if (cachedDefinition.isSynchronousComponent) { |
| 3829 | ko.dependencyDetection.ignore(function() { // See comment in loaderRegistryBehaviors.js for reasoning |
| 3830 | callback(cachedDefinition.definition); |
| 3831 | }); |
| 3832 | } else { |
| 3833 | ko.tasks.schedule(function() { callback(cachedDefinition.definition); }); |
| 3834 | } |
| 3835 | } else { |
| 3836 | // Join the loading process that is already underway, or start a new one. |
| 3837 | loadComponentAndNotify(componentName, callback); |
| 3838 | } |
| 3839 | }, |
| 3840 | |
| 3841 | clearCachedDefinition: function(componentName) { |
| 3842 | delete loadedDefinitionsCache[componentName]; |
| 3843 | }, |
| 3844 | |
| 3845 | _getFirstResultFromLoaders: getFirstResultFromLoaders |
| 3846 | }; |
| 3847 | |
| 3848 | function getObjectOwnProperty(obj, propName) { |
| 3849 | return Object.prototype.hasOwnProperty.call(obj, propName) ? obj[propName] : undefined; |
| 3850 | } |
| 3851 | |
| 3852 | function loadComponentAndNotify(componentName, callback) { |
| 3853 | var subscribable = getObjectOwnProperty(loadingSubscribablesCache, componentName), |
| 3854 | completedAsync; |
| 3855 | if (!subscribable) { |
| 3856 | // It's not started loading yet. Start loading, and when it's done, move it to loadedDefinitionsCache. |
| 3857 | subscribable = loadingSubscribablesCache[componentName] = new ko.subscribable(); |
| 3858 | subscribable.subscribe(callback); |
| 3859 | |
| 3860 | beginLoadingComponent(componentName, function(definition, config) { |
| 3861 | var isSynchronousComponent = !!(config && config['synchronous']); |
| 3862 | loadedDefinitionsCache[componentName] = { definition: definition, isSynchronousComponent: isSynchronousComponent }; |
| 3863 | delete loadingSubscribablesCache[componentName]; |
| 3864 | |
| 3865 | // For API consistency, all loads complete asynchronously. However we want to avoid |
| 3866 | // adding an extra task schedule if it's unnecessary (i.e., the completion is already |
| 3867 | // async). |
| 3868 | // |
| 3869 | // You can bypass the 'always asynchronous' feature by putting the synchronous:true |
| 3870 | // flag on your component configuration when you register it. |
| 3871 | if (completedAsync || isSynchronousComponent) { |
| 3872 | // Note that notifySubscribers ignores any dependencies read within the callback. |
| 3873 | // See comment in loaderRegistryBehaviors.js for reasoning |
| 3874 | subscribable['notifySubscribers'](definition); |
| 3875 | } else { |
| 3876 | ko.tasks.schedule(function() { |
| 3877 | subscribable['notifySubscribers'](definition); |
| 3878 | }); |
| 3879 | } |
| 3880 | }); |
| 3881 | completedAsync = true; |
| 3882 | } else { |
| 3883 | subscribable.subscribe(callback); |
| 3884 | } |
| 3885 | } |
| 3886 | |
| 3887 | function beginLoadingComponent(componentName, callback) { |
| 3888 | getFirstResultFromLoaders('getConfig', [componentName], function(config) { |
| 3889 | if (config) { |
| 3890 | // We have a config, so now load its definition |
| 3891 | getFirstResultFromLoaders('loadComponent', [componentName, config], function(definition) { |
| 3892 | callback(definition, config); |
| 3893 | }); |
| 3894 | } else { |
| 3895 | // The component has no config - it's unknown to all the loaders. |
| 3896 | // Note that this is not an error (e.g., a module loading error) - that would abort the |
| 3897 | // process and this callback would not run. For this callback to run, all loaders must |
| 3898 | // have confirmed they don't know about this component. |
| 3899 | callback(null, null); |
| 3900 | } |
| 3901 | }); |
| 3902 | } |
| 3903 | |
| 3904 | function getFirstResultFromLoaders(methodName, argsExceptCallback, callback, candidateLoaders) { |
| 3905 | // On the first call in the stack, start with the full set of loaders |
| 3906 | if (!candidateLoaders) { |
| 3907 | candidateLoaders = ko.components['loaders'].slice(0); // Use a copy, because we'll be mutating this array |
| 3908 | } |
| 3909 | |
| 3910 | // Try the next candidate |
| 3911 | var currentCandidateLoader = candidateLoaders.shift(); |
| 3912 | if (currentCandidateLoader) { |
| 3913 | var methodInstance = currentCandidateLoader[methodName]; |
| 3914 | if (methodInstance) { |
| 3915 | var wasAborted = false, |
| 3916 | synchronousReturnValue = methodInstance.apply(currentCandidateLoader, argsExceptCallback.concat(function(result) { |
| 3917 | if (wasAborted) { |
| 3918 | callback(null); |
| 3919 | } else if (result !== null) { |
| 3920 | // This candidate returned a value. Use it. |
| 3921 | callback(result); |
| 3922 | } else { |
| 3923 | // Try the next candidate |
| 3924 | getFirstResultFromLoaders(methodName, argsExceptCallback, callback, candidateLoaders); |
| 3925 | } |
| 3926 | })); |
| 3927 | |
| 3928 | // Currently, loaders may not return anything synchronously. This leaves open the possibility |
| 3929 | // that we'll extend the API to support synchronous return values in the future. It won't be |
| 3930 | // a breaking change, because currently no loader is allowed to return anything except undefined. |
| 3931 | if (synchronousReturnValue !== undefined) { |
| 3932 | wasAborted = true; |
| 3933 | |
| 3934 | // Method to suppress exceptions will remain undocumented. This is only to keep |
| 3935 | // KO's specs running tidily, since we can observe the loading got aborted without |
| 3936 | // having exceptions cluttering up the console too. |
| 3937 | if (!currentCandidateLoader['suppressLoaderExceptions']) { |
| 3938 | throw new Error('Component loaders must supply values by invoking the callback, not by returning values synchronously.'); |
| 3939 | } |
| 3940 | } |
| 3941 | } else { |
| 3942 | // This candidate doesn't have the relevant handler. Synchronously move on to the next one. |
| 3943 | getFirstResultFromLoaders(methodName, argsExceptCallback, callback, candidateLoaders); |
| 3944 | } |
| 3945 | } else { |
| 3946 | // No candidates returned a value |
| 3947 | callback(null); |
| 3948 | } |
| 3949 | } |
| 3950 | |
| 3951 | // Reference the loaders via string name so it's possible for developers |
| 3952 | // to replace the whole array by assigning to ko.components.loaders |
| 3953 | ko.components['loaders'] = []; |
| 3954 | |
| 3955 | ko.exportSymbol('components', ko.components); |
| 3956 | ko.exportSymbol('components.get', ko.components.get); |
| 3957 | ko.exportSymbol('components.clearCachedDefinition', ko.components.clearCachedDefinition); |
| 3958 | })(); |
| 3959 | (function(undefined) { |
| 3960 | |
| 3961 | // The default loader is responsible for two things: |
| 3962 | // 1. Maintaining the default in-memory registry of component configuration objects |
| 3963 | // (i.e., the thing you're writing to when you call ko.components.register(someName, ...)) |
| 3964 | // 2. Answering requests for components by fetching configuration objects |
| 3965 | // from that default in-memory registry and resolving them into standard |
| 3966 | // component definition objects (of the form { createViewModel: ..., template: ... }) |
| 3967 | // Custom loaders may override either of these facilities, i.e., |
| 3968 | // 1. To supply configuration objects from some other source (e.g., conventions) |
| 3969 | // 2. Or, to resolve configuration objects by loading viewmodels/templates via arbitrary logic. |
| 3970 | |
| 3971 | var defaultConfigRegistry = {}; |
| 3972 | |
| 3973 | ko.components.register = function(componentName, config) { |
| 3974 | if (!config) { |
| 3975 | throw new Error('Invalid configuration for ' + componentName); |
| 3976 | } |
| 3977 | |
| 3978 | if (ko.components.isRegistered(componentName)) { |
| 3979 | throw new Error('Component ' + componentName + ' is already registered'); |
| 3980 | } |
| 3981 | |
| 3982 | defaultConfigRegistry[componentName] = config; |
| 3983 | }; |
| 3984 | |
| 3985 | ko.components.isRegistered = function(componentName) { |
| 3986 | return Object.prototype.hasOwnProperty.call(defaultConfigRegistry, componentName); |
| 3987 | }; |
| 3988 | |
| 3989 | ko.components.unregister = function(componentName) { |
| 3990 | delete defaultConfigRegistry[componentName]; |
| 3991 | ko.components.clearCachedDefinition(componentName); |
| 3992 | }; |
| 3993 | |
| 3994 | ko.components.defaultLoader = { |
| 3995 | 'getConfig': function(componentName, callback) { |
| 3996 | var result = ko.components.isRegistered(componentName) |
| 3997 | ? defaultConfigRegistry[componentName] |
| 3998 | : null; |
| 3999 | callback(result); |
| 4000 | }, |
| 4001 | |
| 4002 | 'loadComponent': function(componentName, config, callback) { |
| 4003 | var errorCallback = makeErrorCallback(componentName); |
| 4004 | possiblyGetConfigFromAmd(errorCallback, config, function(loadedConfig) { |
| 4005 | resolveConfig(componentName, errorCallback, loadedConfig, callback); |
| 4006 | }); |
| 4007 | }, |
| 4008 | |
| 4009 | 'loadTemplate': function(componentName, templateConfig, callback) { |
| 4010 | resolveTemplate(makeErrorCallback(componentName), templateConfig, callback); |
| 4011 | }, |
| 4012 | |
| 4013 | 'loadViewModel': function(componentName, viewModelConfig, callback) { |
| 4014 | resolveViewModel(makeErrorCallback(componentName), viewModelConfig, callback); |
| 4015 | } |
| 4016 | }; |
| 4017 | |
| 4018 | var createViewModelKey = 'createViewModel'; |
| 4019 | |
| 4020 | // Takes a config object of the form { template: ..., viewModel: ... }, and asynchronously convert it |
| 4021 | // into the standard component definition format: |
| 4022 | // { template: <ArrayOfDomNodes>, createViewModel: function(params, componentInfo) { ... } }. |
| 4023 | // Since both template and viewModel may need to be resolved asynchronously, both tasks are performed |
| 4024 | // in parallel, and the results joined when both are ready. We don't depend on any promises infrastructure, |
| 4025 | // so this is implemented manually below. |
| 4026 | function resolveConfig(componentName, errorCallback, config, callback) { |
| 4027 | var result = {}, |
| 4028 | makeCallBackWhenZero = 2, |
| 4029 | tryIssueCallback = function() { |
| 4030 | if (--makeCallBackWhenZero === 0) { |
| 4031 | callback(result); |
| 4032 | } |
| 4033 | }, |
| 4034 | templateConfig = config['template'], |
| 4035 | viewModelConfig = config['viewModel']; |
| 4036 | |
| 4037 | if (templateConfig) { |
| 4038 | possiblyGetConfigFromAmd(errorCallback, templateConfig, function(loadedConfig) { |
| 4039 | ko.components._getFirstResultFromLoaders('loadTemplate', [componentName, loadedConfig], function(resolvedTemplate) { |
| 4040 | result['template'] = resolvedTemplate; |
| 4041 | tryIssueCallback(); |
| 4042 | }); |
| 4043 | }); |
| 4044 | } else { |
| 4045 | tryIssueCallback(); |
| 4046 | } |
| 4047 | |
| 4048 | if (viewModelConfig) { |
| 4049 | possiblyGetConfigFromAmd(errorCallback, viewModelConfig, function(loadedConfig) { |
| 4050 | ko.components._getFirstResultFromLoaders('loadViewModel', [componentName, loadedConfig], function(resolvedViewModel) { |
| 4051 | result[createViewModelKey] = resolvedViewModel; |
| 4052 | tryIssueCallback(); |
| 4053 | }); |
| 4054 | }); |
| 4055 | } else { |
| 4056 | tryIssueCallback(); |
| 4057 | } |
| 4058 | } |
| 4059 | |
| 4060 | function resolveTemplate(errorCallback, templateConfig, callback) { |
| 4061 | if (typeof templateConfig === 'string') { |
| 4062 | // Markup - parse it |
| 4063 | callback(ko.utils.parseHtmlFragment(templateConfig)); |
| 4064 | } else if (templateConfig instanceof Array) { |
| 4065 | // Assume already an array of DOM nodes - pass through unchanged |
| 4066 | callback(templateConfig); |
| 4067 | } else if (isDocumentFragment(templateConfig)) { |
| 4068 | // Document fragment - use its child nodes |
| 4069 | callback(ko.utils.makeArray(templateConfig.childNodes)); |
| 4070 | } else if (templateConfig['element']) { |
| 4071 | var element = templateConfig['element']; |
| 4072 | if (isDomElement(element)) { |
| 4073 | // Element instance - copy its child nodes |
| 4074 | callback(cloneNodesFromTemplateSourceElement(element)); |
| 4075 | } else if (typeof element === 'string') { |
| 4076 | // Element ID - find it, then copy its child nodes |
| 4077 | var elemInstance = document.getElementById(element); |
| 4078 | if (elemInstance) { |
| 4079 | callback(cloneNodesFromTemplateSourceElement(elemInstance)); |
| 4080 | } else { |
| 4081 | errorCallback('Cannot find element with ID ' + element); |
| 4082 | } |
| 4083 | } else { |
| 4084 | errorCallback('Unknown element type: ' + element); |
| 4085 | } |
| 4086 | } else { |
| 4087 | errorCallback('Unknown template value: ' + templateConfig); |
| 4088 | } |
| 4089 | } |
| 4090 | |
| 4091 | function resolveViewModel(errorCallback, viewModelConfig, callback) { |
| 4092 | if (typeof viewModelConfig === 'function') { |
| 4093 | // Constructor - convert to standard factory function format |
| 4094 | // By design, this does *not* supply componentInfo to the constructor, as the intent is that |
| 4095 | // componentInfo contains non-viewmodel data (e.g., the component's element) that should only |
| 4096 | // be used in factory functions, not viewmodel constructors. |
| 4097 | callback(function (params /*, componentInfo */) { |
| 4098 | return new viewModelConfig(params); |
| 4099 | }); |
| 4100 | } else if (typeof viewModelConfig[createViewModelKey] === 'function') { |
| 4101 | // Already a factory function - use it as-is |
| 4102 | callback(viewModelConfig[createViewModelKey]); |
| 4103 | } else if ('instance' in viewModelConfig) { |
| 4104 | // Fixed object instance - promote to createViewModel format for API consistency |
| 4105 | var fixedInstance = viewModelConfig['instance']; |
| 4106 | callback(function (params, componentInfo) { |
| 4107 | return fixedInstance; |
| 4108 | }); |
| 4109 | } else if ('viewModel' in viewModelConfig) { |
| 4110 | // Resolved AMD module whose value is of the form { viewModel: ... } |
| 4111 | resolveViewModel(errorCallback, viewModelConfig['viewModel'], callback); |
| 4112 | } else { |
| 4113 | errorCallback('Unknown viewModel value: ' + viewModelConfig); |
| 4114 | } |
| 4115 | } |
| 4116 | |
| 4117 | function cloneNodesFromTemplateSourceElement(elemInstance) { |
| 4118 | switch (ko.utils.tagNameLower(elemInstance)) { |
| 4119 | case 'script': |
| 4120 | return ko.utils.parseHtmlFragment(elemInstance.text); |
| 4121 | case 'textarea': |
| 4122 | return ko.utils.parseHtmlFragment(elemInstance.value); |
| 4123 | case 'template': |
| 4124 | // For browsers with proper <template> element support (i.e., where the .content property |
| 4125 | // gives a document fragment), use that document fragment. |
| 4126 | if (isDocumentFragment(elemInstance.content)) { |
| 4127 | return ko.utils.cloneNodes(elemInstance.content.childNodes); |
| 4128 | } |
| 4129 | } |
| 4130 | |
| 4131 | // Regular elements such as <div>, and <template> elements on old browsers that don't really |
| 4132 | // understand <template> and just treat it as a regular container |
| 4133 | return ko.utils.cloneNodes(elemInstance.childNodes); |
| 4134 | } |
| 4135 | |
| 4136 | function isDomElement(obj) { |
| 4137 | if (window['HTMLElement']) { |
| 4138 | return obj instanceof HTMLElement; |
| 4139 | } else { |
| 4140 | return obj && obj.tagName && obj.nodeType === 1; |
| 4141 | } |
| 4142 | } |
| 4143 | |
| 4144 | function isDocumentFragment(obj) { |
| 4145 | if (window['DocumentFragment']) { |
| 4146 | return obj instanceof DocumentFragment; |
| 4147 | } else { |
| 4148 | return obj && obj.nodeType === 11; |
| 4149 | } |
| 4150 | } |
| 4151 | |
| 4152 | function possiblyGetConfigFromAmd(errorCallback, config, callback) { |
| 4153 | if (typeof config['require'] === 'string') { |
| 4154 | // The config is the value of an AMD module |
| 4155 | if (amdRequire || window['require']) { |
| 4156 | (amdRequire || window['require'])([config['require']], callback); |
| 4157 | } else { |
| 4158 | errorCallback('Uses require, but no AMD loader is present'); |
| 4159 | } |
| 4160 | } else { |
| 4161 | callback(config); |
| 4162 | } |
| 4163 | } |
| 4164 | |
| 4165 | function makeErrorCallback(componentName) { |
| 4166 | return function (message) { |
| 4167 | throw new Error('Component \'' + componentName + '\': ' + message); |
| 4168 | }; |
| 4169 | } |
| 4170 | |
| 4171 | ko.exportSymbol('components.register', ko.components.register); |
| 4172 | ko.exportSymbol('components.isRegistered', ko.components.isRegistered); |
| 4173 | ko.exportSymbol('components.unregister', ko.components.unregister); |
| 4174 | |
| 4175 | // Expose the default loader so that developers can directly ask it for configuration |
| 4176 | // or to resolve configuration |
| 4177 | ko.exportSymbol('components.defaultLoader', ko.components.defaultLoader); |
| 4178 | |
| 4179 | // By default, the default loader is the only registered component loader |
| 4180 | ko.components['loaders'].push(ko.components.defaultLoader); |
| 4181 | |
| 4182 | // Privately expose the underlying config registry for use in old-IE shim |
| 4183 | ko.components._allRegisteredComponents = defaultConfigRegistry; |
| 4184 | })(); |
| 4185 | (function (undefined) { |
| 4186 | // Overridable API for determining which component name applies to a given node. By overriding this, |
| 4187 | // you can for example map specific tagNames to components that are not preregistered. |
| 4188 | ko.components['getComponentNameForNode'] = function(node) { |
| 4189 | var tagNameLower = ko.utils.tagNameLower(node); |
| 4190 | if (ko.components.isRegistered(tagNameLower)) { |
| 4191 | // Try to determine that this node can be considered a *custom* element; see https://github.com/knockout/knockout/issues/1603 |
| 4192 | if (tagNameLower.indexOf('-') != -1 || ('' + node) == "[object HTMLUnknownElement]" || (ko.utils.ieVersion <= 8 && node.tagName === tagNameLower)) { |
| 4193 | return tagNameLower; |
| 4194 | } |
| 4195 | } |
| 4196 | }; |
| 4197 | |
| 4198 | ko.components.addBindingsForCustomElement = function(allBindings, node, bindingContext, valueAccessors) { |
| 4199 | // Determine if it's really a custom element matching a component |
| 4200 | if (node.nodeType === 1) { |
| 4201 | var componentName = ko.components['getComponentNameForNode'](node); |
| 4202 | if (componentName) { |
| 4203 | // It does represent a component, so add a component binding for it |
| 4204 | allBindings = allBindings || {}; |
| 4205 | |
| 4206 | if (allBindings['component']) { |
| 4207 | // Avoid silently overwriting some other 'component' binding that may already be on the element |
| 4208 | throw new Error('Cannot use the "component" binding on a custom element matching a component'); |
| 4209 | } |
| 4210 | |
| 4211 | var componentBindingValue = { 'name': componentName, 'params': getComponentParamsFromCustomElement(node, bindingContext) }; |
| 4212 | |
| 4213 | allBindings['component'] = valueAccessors |
| 4214 | ? function() { return componentBindingValue; } |
| 4215 | : componentBindingValue; |
| 4216 | } |
| 4217 | } |
| 4218 | |
| 4219 | return allBindings; |
| 4220 | } |
| 4221 | |
| 4222 | var nativeBindingProviderInstance = new ko.bindingProvider(); |
| 4223 | |
| 4224 | function getComponentParamsFromCustomElement(elem, bindingContext) { |
| 4225 | var paramsAttribute = elem.getAttribute('params'); |
| 4226 | |
| 4227 | if (paramsAttribute) { |
| 4228 | var params = nativeBindingProviderInstance['parseBindingsString'](paramsAttribute, bindingContext, elem, { 'valueAccessors': true, 'bindingParams': true }), |
| 4229 | rawParamComputedValues = ko.utils.objectMap(params, function(paramValue, paramName) { |
| 4230 | return ko.computed(paramValue, null, { disposeWhenNodeIsRemoved: elem }); |
| 4231 | }), |
| 4232 | result = ko.utils.objectMap(rawParamComputedValues, function(paramValueComputed, paramName) { |
| 4233 | var paramValue = paramValueComputed.peek(); |
| 4234 | // Does the evaluation of the parameter value unwrap any observables? |
| 4235 | if (!paramValueComputed.isActive()) { |
| 4236 | // No it doesn't, so there's no need for any computed wrapper. Just pass through the supplied value directly. |
| 4237 | // Example: "someVal: firstName, age: 123" (whether or not firstName is an observable/computed) |
| 4238 | return paramValue; |
| 4239 | } else { |
| 4240 | // Yes it does. Supply a computed property that unwraps both the outer (binding expression) |
| 4241 | // level of observability, and any inner (resulting model value) level of observability. |
| 4242 | // This means the component doesn't have to worry about multiple unwrapping. If the value is a |
| 4243 | // writable observable, the computed will also be writable and pass the value on to the observable. |
| 4244 | return ko.computed({ |
| 4245 | 'read': function() { |
| 4246 | return ko.utils.unwrapObservable(paramValueComputed()); |
| 4247 | }, |
| 4248 | 'write': ko.isWriteableObservable(paramValue) && function(value) { |
| 4249 | paramValueComputed()(value); |
| 4250 | }, |
| 4251 | disposeWhenNodeIsRemoved: elem |
| 4252 | }); |
| 4253 | } |
| 4254 | }); |
| 4255 | |
| 4256 | // Give access to the raw computeds, as long as that wouldn't overwrite any custom param also called '$raw' |
| 4257 | // This is in case the developer wants to react to outer (binding) observability separately from inner |
| 4258 | // (model value) observability, or in case the model value observable has subobservables. |
| 4259 | if (!Object.prototype.hasOwnProperty.call(result, '$raw')) { |
| 4260 | result['$raw'] = rawParamComputedValues; |
| 4261 | } |
| 4262 | |
| 4263 | return result; |
| 4264 | } else { |
| 4265 | // For consistency, absence of a "params" attribute is treated the same as the presence of |
| 4266 | // any empty one. Otherwise component viewmodels need special code to check whether or not |
| 4267 | // 'params' or 'params.$raw' is null/undefined before reading subproperties, which is annoying. |
| 4268 | return { '$raw': {} }; |
| 4269 | } |
| 4270 | } |
| 4271 | |
| 4272 | // -------------------------------------------------------------------------------- |
| 4273 | // Compatibility code for older (pre-HTML5) IE browsers |
| 4274 | |
| 4275 | if (ko.utils.ieVersion < 9) { |
| 4276 | // Whenever you preregister a component, enable it as a custom element in the current document |
| 4277 | ko.components['register'] = (function(originalFunction) { |
| 4278 | return function(componentName) { |
| 4279 | document.createElement(componentName); // Allows IE<9 to parse markup containing the custom element |
| 4280 | return originalFunction.apply(this, arguments); |
| 4281 | } |
| 4282 | })(ko.components['register']); |
| 4283 | |
| 4284 | // Whenever you create a document fragment, enable all preregistered component names as custom elements |
| 4285 | // This is needed to make innerShiv/jQuery HTML parsing correctly handle the custom elements |
| 4286 | document.createDocumentFragment = (function(originalFunction) { |
| 4287 | return function() { |
| 4288 | var newDocFrag = originalFunction(), |
| 4289 | allComponents = ko.components._allRegisteredComponents; |
| 4290 | for (var componentName in allComponents) { |
| 4291 | if (Object.prototype.hasOwnProperty.call(allComponents, componentName)) { |
| 4292 | newDocFrag.createElement(componentName); |
| 4293 | } |
| 4294 | } |
| 4295 | return newDocFrag; |
| 4296 | }; |
| 4297 | })(document.createDocumentFragment); |
| 4298 | } |
| 4299 | })();(function(undefined) { |
| 4300 | var componentLoadingOperationUniqueId = 0; |
| 4301 | |
| 4302 | ko.bindingHandlers['component'] = { |
| 4303 | 'init': function(element, valueAccessor, ignored1, ignored2, bindingContext) { |
| 4304 | var currentViewModel, |
| 4305 | currentLoadingOperationId, |
| 4306 | afterRenderSub, |
| 4307 | disposeAssociatedComponentViewModel = function () { |
| 4308 | var currentViewModelDispose = currentViewModel && currentViewModel['dispose']; |
| 4309 | if (typeof currentViewModelDispose === 'function') { |
| 4310 | currentViewModelDispose.call(currentViewModel); |
| 4311 | } |
| 4312 | if (afterRenderSub) { |
| 4313 | afterRenderSub.dispose(); |
| 4314 | } |
| 4315 | afterRenderSub = null; |
| 4316 | currentViewModel = null; |
| 4317 | // Any in-flight loading operation is no longer relevant, so make sure we ignore its completion |
| 4318 | currentLoadingOperationId = null; |
| 4319 | }, |
| 4320 | originalChildNodes = ko.utils.makeArray(ko.virtualElements.childNodes(element)); |
| 4321 | |
| 4322 | ko.virtualElements.emptyNode(element); |
| 4323 | ko.utils.domNodeDisposal.addDisposeCallback(element, disposeAssociatedComponentViewModel); |
| 4324 | |
| 4325 | ko.computed(function () { |
| 4326 | var value = ko.utils.unwrapObservable(valueAccessor()), |
| 4327 | componentName, componentParams; |
| 4328 | |
| 4329 | if (typeof value === 'string') { |
| 4330 | componentName = value; |
| 4331 | } else { |
| 4332 | componentName = ko.utils.unwrapObservable(value['name']); |
| 4333 | componentParams = ko.utils.unwrapObservable(value['params']); |
| 4334 | } |
| 4335 | |
| 4336 | if (!componentName) { |
| 4337 | throw new Error('No component name specified'); |
| 4338 | } |
| 4339 | |
| 4340 | var asyncContext = ko.bindingEvent.startPossiblyAsyncContentBinding(element, bindingContext); |
| 4341 | |
| 4342 | var loadingOperationId = currentLoadingOperationId = ++componentLoadingOperationUniqueId; |
| 4343 | ko.components.get(componentName, function(componentDefinition) { |
| 4344 | // If this is not the current load operation for this element, ignore it. |
| 4345 | if (currentLoadingOperationId !== loadingOperationId) { |
| 4346 | return; |
| 4347 | } |
| 4348 | |
| 4349 | // Clean up previous state |
| 4350 | disposeAssociatedComponentViewModel(); |
| 4351 | |
| 4352 | // Instantiate and bind new component. Implicitly this cleans any old DOM nodes. |
| 4353 | if (!componentDefinition) { |
| 4354 | throw new Error('Unknown component \'' + componentName + '\''); |
| 4355 | } |
| 4356 | cloneTemplateIntoElement(componentName, componentDefinition, element); |
| 4357 | |
| 4358 | var componentInfo = { |
| 4359 | 'element': element, |
| 4360 | 'templateNodes': originalChildNodes |
| 4361 | }; |
| 4362 | |
| 4363 | var componentViewModel = createViewModel(componentDefinition, componentParams, componentInfo), |
| 4364 | childBindingContext = asyncContext['createChildContext'](componentViewModel, { |
| 4365 | 'extend': function(ctx) { |
| 4366 | ctx['$component'] = componentViewModel; |
| 4367 | ctx['$componentTemplateNodes'] = originalChildNodes; |
| 4368 | } |
| 4369 | }); |
| 4370 | |
| 4371 | if (componentViewModel && componentViewModel['koDescendantsComplete']) { |
| 4372 | afterRenderSub = ko.bindingEvent.subscribe(element, ko.bindingEvent.descendantsComplete, componentViewModel['koDescendantsComplete'], componentViewModel); |
| 4373 | } |
| 4374 | |
| 4375 | currentViewModel = componentViewModel; |
| 4376 | ko.applyBindingsToDescendants(childBindingContext, element); |
| 4377 | }); |
| 4378 | }, null, { disposeWhenNodeIsRemoved: element }); |
| 4379 | |
| 4380 | return { 'controlsDescendantBindings': true }; |
| 4381 | } |
| 4382 | }; |
| 4383 | |
| 4384 | ko.virtualElements.allowedBindings['component'] = true; |
| 4385 | |
| 4386 | function cloneTemplateIntoElement(componentName, componentDefinition, element) { |
| 4387 | var template = componentDefinition['template']; |
| 4388 | if (!template) { |
| 4389 | throw new Error('Component \'' + componentName + '\' has no template'); |
| 4390 | } |
| 4391 | |
| 4392 | var clonedNodesArray = ko.utils.cloneNodes(template); |
| 4393 | ko.virtualElements.setDomNodeChildren(element, clonedNodesArray); |
| 4394 | } |
| 4395 | |
| 4396 | function createViewModel(componentDefinition, componentParams, componentInfo) { |
| 4397 | var componentViewModelFactory = componentDefinition['createViewModel']; |
| 4398 | return componentViewModelFactory |
| 4399 | ? componentViewModelFactory.call(componentDefinition, componentParams, componentInfo) |
| 4400 | : componentParams; // Template-only component |
| 4401 | } |
| 4402 | |
| 4403 | })(); |
| 4404 | var attrHtmlToJavaScriptMap = { 'class': 'className', 'for': 'htmlFor' }; |
| 4405 | ko.bindingHandlers['attr'] = { |
| 4406 | 'update': function(element, valueAccessor, allBindings) { |
| 4407 | var value = ko.utils.unwrapObservable(valueAccessor()) || {}; |
| 4408 | ko.utils.objectForEach(value, function(attrName, attrValue) { |
| 4409 | attrValue = ko.utils.unwrapObservable(attrValue); |
| 4410 | |
| 4411 | // Find the namespace of this attribute, if any. |
| 4412 | var prefixLen = attrName.indexOf(':'); |
| 4413 | var namespace = "lookupNamespaceURI" in element && prefixLen > 0 && element.lookupNamespaceURI(attrName.substr(0, prefixLen)); |
| 4414 | |
| 4415 | // To cover cases like "attr: { checked:someProp }", we want to remove the attribute entirely |
| 4416 | // when someProp is a "no value"-like value (strictly null, false, or undefined) |
| 4417 | // (because the absence of the "checked" attr is how to mark an element as not checked, etc.) |
| 4418 | var toRemove = (attrValue === false) || (attrValue === null) || (attrValue === undefined); |
| 4419 | if (toRemove) { |
| 4420 | namespace ? element.removeAttributeNS(namespace, attrName) : element.removeAttribute(attrName); |
| 4421 | } else { |
| 4422 | attrValue = attrValue.toString(); |
| 4423 | } |
| 4424 | |
| 4425 | // In IE <= 7 and IE8 Quirks Mode, you have to use the JavaScript property name instead of the |
| 4426 | // HTML attribute name for certain attributes. IE8 Standards Mode supports the correct behavior, |
| 4427 | // but instead of figuring out the mode, we'll just set the attribute through the JavaScript |
| 4428 | // property for IE <= 8. |
| 4429 | if (ko.utils.ieVersion <= 8 && attrName in attrHtmlToJavaScriptMap) { |
| 4430 | attrName = attrHtmlToJavaScriptMap[attrName]; |
| 4431 | if (toRemove) |
| 4432 | element.removeAttribute(attrName); |
| 4433 | else |
| 4434 | element[attrName] = attrValue; |
| 4435 | } else if (!toRemove) { |
| 4436 | namespace ? element.setAttributeNS(namespace, attrName, attrValue) : element.setAttribute(attrName, attrValue); |
| 4437 | } |
| 4438 | |
| 4439 | // Treat "name" specially - although you can think of it as an attribute, it also needs |
| 4440 | // special handling on older versions of IE (https://github.com/SteveSanderson/knockout/pull/333) |
| 4441 | // Deliberately being case-sensitive here because XHTML would regard "Name" as a different thing |
| 4442 | // entirely, and there's no strong reason to allow for such casing in HTML. |
| 4443 | if (attrName === "name") { |
| 4444 | ko.utils.setElementName(element, toRemove ? "" : attrValue); |
| 4445 | } |
| 4446 | }); |
| 4447 | } |
| 4448 | }; |
| 4449 | (function() { |
| 4450 | |
| 4451 | ko.bindingHandlers['checked'] = { |
| 4452 | 'after': ['value', 'attr'], |
| 4453 | 'init': function (element, valueAccessor, allBindings) { |
| 4454 | var checkedValue = ko.pureComputed(function() { |
| 4455 | // Treat "value" like "checkedValue" when it is included with "checked" binding |
| 4456 | if (allBindings['has']('checkedValue')) { |
| 4457 | return ko.utils.unwrapObservable(allBindings.get('checkedValue')); |
| 4458 | } else if (useElementValue) { |
| 4459 | if (allBindings['has']('value')) { |
| 4460 | return ko.utils.unwrapObservable(allBindings.get('value')); |
| 4461 | } else { |
| 4462 | return element.value; |
| 4463 | } |
| 4464 | } |
| 4465 | }); |
| 4466 | |
| 4467 | function updateModel() { |
| 4468 | // This updates the model value from the view value. |
| 4469 | // It runs in response to DOM events (click) and changes in checkedValue. |
| 4470 | var isChecked = element.checked, |
| 4471 | elemValue = checkedValue(); |
| 4472 | |
| 4473 | // When we're first setting up this computed, don't change any model state. |
| 4474 | if (ko.computedContext.isInitial()) { |
| 4475 | return; |
| 4476 | } |
| 4477 | |
| 4478 | // We can ignore unchecked radio buttons, because some other radio |
| 4479 | // button will be checked, and that one can take care of updating state. |
| 4480 | // Also ignore value changes to an already unchecked checkbox. |
| 4481 | if (!isChecked && (isRadio || ko.computedContext.getDependenciesCount())) { |
| 4482 | return; |
| 4483 | } |
| 4484 | |
| 4485 | var modelValue = ko.dependencyDetection.ignore(valueAccessor); |
| 4486 | if (valueIsArray) { |
| 4487 | var writableValue = rawValueIsNonArrayObservable ? modelValue.peek() : modelValue, |
| 4488 | saveOldValue = oldElemValue; |
| 4489 | oldElemValue = elemValue; |
| 4490 | |
| 4491 | if (saveOldValue !== elemValue) { |
| 4492 | // When we're responding to the checkedValue changing, and the element is |
| 4493 | // currently checked, replace the old elem value with the new elem value |
| 4494 | // in the model array. |
| 4495 | if (isChecked) { |
| 4496 | ko.utils.addOrRemoveItem(writableValue, elemValue, true); |
| 4497 | ko.utils.addOrRemoveItem(writableValue, saveOldValue, false); |
| 4498 | } |
| 4499 | } else { |
| 4500 | // When we're responding to the user having checked/unchecked a checkbox, |
| 4501 | // add/remove the element value to the model array. |
| 4502 | ko.utils.addOrRemoveItem(writableValue, elemValue, isChecked); |
| 4503 | } |
| 4504 | |
| 4505 | if (rawValueIsNonArrayObservable && ko.isWriteableObservable(modelValue)) { |
| 4506 | modelValue(writableValue); |
| 4507 | } |
| 4508 | } else { |
| 4509 | if (isCheckbox) { |
| 4510 | if (elemValue === undefined) { |
| 4511 | elemValue = isChecked; |
| 4512 | } else if (!isChecked) { |
| 4513 | elemValue = undefined; |
| 4514 | } |
| 4515 | } |
| 4516 | ko.expressionRewriting.writeValueToProperty(modelValue, allBindings, 'checked', elemValue, true); |
| 4517 | } |
| 4518 | }; |
| 4519 | |
| 4520 | function updateView() { |
| 4521 | // This updates the view value from the model value. |
| 4522 | // It runs in response to changes in the bound (checked) value. |
| 4523 | var modelValue = ko.utils.unwrapObservable(valueAccessor()), |
| 4524 | elemValue = checkedValue(); |
| 4525 | |
| 4526 | if (valueIsArray) { |
| 4527 | // When a checkbox is bound to an array, being checked represents its value being present in that array |
| 4528 | element.checked = ko.utils.arrayIndexOf(modelValue, elemValue) >= 0; |
| 4529 | oldElemValue = elemValue; |
| 4530 | } else if (isCheckbox && elemValue === undefined) { |
| 4531 | // When a checkbox is bound to any other value (not an array) and "checkedValue" is not defined, |
| 4532 | // being checked represents the value being trueish |
| 4533 | element.checked = !!modelValue; |
| 4534 | } else { |
| 4535 | // Otherwise, being checked means that the checkbox or radio button's value corresponds to the model value |
| 4536 | element.checked = (checkedValue() === modelValue); |
| 4537 | } |
| 4538 | }; |
| 4539 | |
| 4540 | var isCheckbox = element.type == "checkbox", |
| 4541 | isRadio = element.type == "radio"; |
| 4542 | |
| 4543 | // Only bind to check boxes and radio buttons |
| 4544 | if (!isCheckbox && !isRadio) { |
| 4545 | return; |
| 4546 | } |
| 4547 | |
| 4548 | var rawValue = valueAccessor(), |
| 4549 | valueIsArray = isCheckbox && (ko.utils.unwrapObservable(rawValue) instanceof Array), |
| 4550 | rawValueIsNonArrayObservable = !(valueIsArray && rawValue.push && rawValue.splice), |
| 4551 | useElementValue = isRadio || valueIsArray, |
| 4552 | oldElemValue = valueIsArray ? checkedValue() : undefined; |
| 4553 | |
| 4554 | // IE 6 won't allow radio buttons to be selected unless they have a name |
| 4555 | if (isRadio && !element.name) |
| 4556 | ko.bindingHandlers['uniqueName']['init'](element, function() { return true }); |
| 4557 | |
| 4558 | // Set up two computeds to update the binding: |
| 4559 | |
| 4560 | // The first responds to changes in the checkedValue value and to element clicks |
| 4561 | ko.computed(updateModel, null, { disposeWhenNodeIsRemoved: element }); |
| 4562 | ko.utils.registerEventHandler(element, "click", updateModel); |
| 4563 | |
| 4564 | // The second responds to changes in the model value (the one associated with the checked binding) |
| 4565 | ko.computed(updateView, null, { disposeWhenNodeIsRemoved: element }); |
| 4566 | |
| 4567 | rawValue = undefined; |
| 4568 | } |
| 4569 | }; |
| 4570 | ko.expressionRewriting.twoWayBindings['checked'] = true; |
| 4571 | |
| 4572 | ko.bindingHandlers['checkedValue'] = { |
| 4573 | 'update': function (element, valueAccessor) { |
| 4574 | element.value = ko.utils.unwrapObservable(valueAccessor()); |
| 4575 | } |
| 4576 | }; |
| 4577 | |
| 4578 | })();var classesWrittenByBindingKey = '__ko__cssValue'; |
| 4579 | ko.bindingHandlers['class'] = { |
| 4580 | 'update': function (element, valueAccessor) { |
| 4581 | var value = ko.utils.stringTrim(ko.utils.unwrapObservable(valueAccessor())); |
| 4582 | ko.utils.toggleDomNodeCssClass(element, element[classesWrittenByBindingKey], false); |
| 4583 | element[classesWrittenByBindingKey] = value; |
| 4584 | ko.utils.toggleDomNodeCssClass(element, value, true); |
| 4585 | } |
| 4586 | }; |
| 4587 | |
| 4588 | ko.bindingHandlers['css'] = { |
| 4589 | 'update': function (element, valueAccessor) { |
| 4590 | var value = ko.utils.unwrapObservable(valueAccessor()); |
| 4591 | if (value !== null && typeof value == "object") { |
| 4592 | ko.utils.objectForEach(value, function(className, shouldHaveClass) { |
| 4593 | shouldHaveClass = ko.utils.unwrapObservable(shouldHaveClass); |
| 4594 | ko.utils.toggleDomNodeCssClass(element, className, shouldHaveClass); |
| 4595 | }); |
| 4596 | } else { |
| 4597 | ko.bindingHandlers['class']['update'](element, valueAccessor); |
| 4598 | } |
| 4599 | } |
| 4600 | }; |
| 4601 | ko.bindingHandlers['enable'] = { |
| 4602 | 'update': function (element, valueAccessor) { |
| 4603 | var value = ko.utils.unwrapObservable(valueAccessor()); |
| 4604 | if (value && element.disabled) |
| 4605 | element.removeAttribute("disabled"); |
| 4606 | else if ((!value) && (!element.disabled)) |
| 4607 | element.disabled = true; |
| 4608 | } |
| 4609 | }; |
| 4610 | |
| 4611 | ko.bindingHandlers['disable'] = { |
| 4612 | 'update': function (element, valueAccessor) { |
| 4613 | ko.bindingHandlers['enable']['update'](element, function() { return !ko.utils.unwrapObservable(valueAccessor()) }); |
| 4614 | } |
| 4615 | }; |
| 4616 | // For certain common events (currently just 'click'), allow a simplified data-binding syntax |
| 4617 | // e.g. click:handler instead of the usual full-length event:{click:handler} |
| 4618 | function makeEventHandlerShortcut(eventName) { |
| 4619 | ko.bindingHandlers[eventName] = { |
| 4620 | 'init': function(element, valueAccessor, allBindings, viewModel, bindingContext) { |
| 4621 | var newValueAccessor = function () { |
| 4622 | var result = {}; |
| 4623 | result[eventName] = valueAccessor(); |
| 4624 | return result; |
| 4625 | }; |
| 4626 | return ko.bindingHandlers['event']['init'].call(this, element, newValueAccessor, allBindings, viewModel, bindingContext); |
| 4627 | } |
| 4628 | } |
| 4629 | } |
| 4630 | |
| 4631 | ko.bindingHandlers['event'] = { |
| 4632 | 'init' : function (element, valueAccessor, allBindings, viewModel, bindingContext) { |
| 4633 | var eventsToHandle = valueAccessor() || {}; |
| 4634 | ko.utils.objectForEach(eventsToHandle, function(eventName) { |
| 4635 | if (typeof eventName == "string") { |
| 4636 | ko.utils.registerEventHandler(element, eventName, function (event) { |
| 4637 | var handlerReturnValue; |
| 4638 | var handlerFunction = valueAccessor()[eventName]; |
| 4639 | if (!handlerFunction) |
| 4640 | return; |
| 4641 | |
| 4642 | try { |
| 4643 | // Take all the event args, and prefix with the viewmodel |
| 4644 | var argsForHandler = ko.utils.makeArray(arguments); |
| 4645 | viewModel = bindingContext['$data']; |
| 4646 | argsForHandler.unshift(viewModel); |
| 4647 | handlerReturnValue = handlerFunction.apply(viewModel, argsForHandler); |
| 4648 | } finally { |
| 4649 | if (handlerReturnValue !== true) { // Normally we want to prevent default action. Developer can override this be explicitly returning true. |
| 4650 | if (event.preventDefault) |
| 4651 | event.preventDefault(); |
| 4652 | else |
| 4653 | event.returnValue = false; |
| 4654 | } |
| 4655 | } |
| 4656 | |
| 4657 | var bubble = allBindings.get(eventName + 'Bubble') !== false; |
| 4658 | if (!bubble) { |
| 4659 | event.cancelBubble = true; |
| 4660 | if (event.stopPropagation) |
| 4661 | event.stopPropagation(); |
| 4662 | } |
| 4663 | }); |
| 4664 | } |
| 4665 | }); |
| 4666 | } |
| 4667 | }; |
| 4668 | // "foreach: someExpression" is equivalent to "template: { foreach: someExpression }" |
| 4669 | // "foreach: { data: someExpression, afterAdd: myfn }" is equivalent to "template: { foreach: someExpression, afterAdd: myfn }" |
| 4670 | ko.bindingHandlers['foreach'] = { |
| 4671 | makeTemplateValueAccessor: function(valueAccessor) { |
| 4672 | return function() { |
| 4673 | var modelValue = valueAccessor(), |
| 4674 | unwrappedValue = ko.utils.peekObservable(modelValue); // Unwrap without setting a dependency here |
| 4675 | |
| 4676 | // If unwrappedValue is the array, pass in the wrapped value on its own |
| 4677 | // The value will be unwrapped and tracked within the template binding |
| 4678 | // (See https://github.com/SteveSanderson/knockout/issues/523) |
| 4679 | if ((!unwrappedValue) || typeof unwrappedValue.length == "number") |
| 4680 | return { 'foreach': modelValue, 'templateEngine': ko.nativeTemplateEngine.instance }; |
| 4681 | |
| 4682 | // If unwrappedValue.data is the array, preserve all relevant options and unwrap again value so we get updates |
| 4683 | ko.utils.unwrapObservable(modelValue); |
| 4684 | return { |
| 4685 | 'foreach': unwrappedValue['data'], |
| 4686 | 'as': unwrappedValue['as'], |
| 4687 | 'noChildContext': unwrappedValue['noChildContext'], |
| 4688 | 'includeDestroyed': unwrappedValue['includeDestroyed'], |
| 4689 | 'afterAdd': unwrappedValue['afterAdd'], |
| 4690 | 'beforeRemove': unwrappedValue['beforeRemove'], |
| 4691 | 'afterRender': unwrappedValue['afterRender'], |
| 4692 | 'beforeMove': unwrappedValue['beforeMove'], |
| 4693 | 'afterMove': unwrappedValue['afterMove'], |
| 4694 | 'templateEngine': ko.nativeTemplateEngine.instance |
| 4695 | }; |
| 4696 | }; |
| 4697 | }, |
| 4698 | 'init': function(element, valueAccessor, allBindings, viewModel, bindingContext) { |
| 4699 | return ko.bindingHandlers['template']['init'](element, ko.bindingHandlers['foreach'].makeTemplateValueAccessor(valueAccessor)); |
| 4700 | }, |
| 4701 | 'update': function(element, valueAccessor, allBindings, viewModel, bindingContext) { |
| 4702 | return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['foreach'].makeTemplateValueAccessor(valueAccessor), allBindings, viewModel, bindingContext); |
| 4703 | } |
| 4704 | }; |
| 4705 | ko.expressionRewriting.bindingRewriteValidators['foreach'] = false; // Can't rewrite control flow bindings |
| 4706 | ko.virtualElements.allowedBindings['foreach'] = true; |
| 4707 | var hasfocusUpdatingProperty = '__ko_hasfocusUpdating'; |
| 4708 | var hasfocusLastValue = '__ko_hasfocusLastValue'; |
| 4709 | ko.bindingHandlers['hasfocus'] = { |
| 4710 | 'init': function(element, valueAccessor, allBindings) { |
| 4711 | var handleElementFocusChange = function(isFocused) { |
| 4712 | // Where possible, ignore which event was raised and determine focus state using activeElement, |
| 4713 | // as this avoids phantom focus/blur events raised when changing tabs in modern browsers. |
| 4714 | // However, not all KO-targeted browsers (Firefox 2) support activeElement. For those browsers, |
| 4715 | // prevent a loss of focus when changing tabs/windows by setting a flag that prevents hasfocus |
| 4716 | // from calling 'blur()' on the element when it loses focus. |
| 4717 | // Discussion at https://github.com/SteveSanderson/knockout/pull/352 |
| 4718 | element[hasfocusUpdatingProperty] = true; |
| 4719 | var ownerDoc = element.ownerDocument; |
| 4720 | if ("activeElement" in ownerDoc) { |
| 4721 | var active; |
| 4722 | try { |
| 4723 | active = ownerDoc.activeElement; |
| 4724 | } catch(e) { |
| 4725 | // IE9 throws if you access activeElement during page load (see issue #703) |
| 4726 | active = ownerDoc.body; |
| 4727 | } |
| 4728 | isFocused = (active === element); |
| 4729 | } |
| 4730 | var modelValue = valueAccessor(); |
| 4731 | ko.expressionRewriting.writeValueToProperty(modelValue, allBindings, 'hasfocus', isFocused, true); |
| 4732 | |
| 4733 | //cache the latest value, so we can avoid unnecessarily calling focus/blur in the update function |
| 4734 | element[hasfocusLastValue] = isFocused; |
| 4735 | element[hasfocusUpdatingProperty] = false; |
| 4736 | }; |
| 4737 | var handleElementFocusIn = handleElementFocusChange.bind(null, true); |
| 4738 | var handleElementFocusOut = handleElementFocusChange.bind(null, false); |
| 4739 | |
| 4740 | ko.utils.registerEventHandler(element, "focus", handleElementFocusIn); |
| 4741 | ko.utils.registerEventHandler(element, "focusin", handleElementFocusIn); // For IE |
| 4742 | ko.utils.registerEventHandler(element, "blur", handleElementFocusOut); |
| 4743 | ko.utils.registerEventHandler(element, "focusout", handleElementFocusOut); // For IE |
| 4744 | |
| 4745 | // Assume element is not focused (prevents "blur" being called initially) |
| 4746 | element[hasfocusLastValue] = false; |
| 4747 | }, |
| 4748 | 'update': function(element, valueAccessor) { |
| 4749 | var value = !!ko.utils.unwrapObservable(valueAccessor()); |
| 4750 | |
| 4751 | if (!element[hasfocusUpdatingProperty] && element[hasfocusLastValue] !== value) { |
| 4752 | value ? element.focus() : element.blur(); |
| 4753 | |
| 4754 | // In IE, the blur method doesn't always cause the element to lose focus (for example, if the window is not in focus). |
| 4755 | // Setting focus to the body element does seem to be reliable in IE, but should only be used if we know that the current |
| 4756 | // element was focused already. |
| 4757 | if (!value && element[hasfocusLastValue]) { |
| 4758 | element.ownerDocument.body.focus(); |
| 4759 | } |
| 4760 | |
| 4761 | // For IE, which doesn't reliably fire "focus" or "blur" events synchronously |
| 4762 | ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, value ? "focusin" : "focusout"]); |
| 4763 | } |
| 4764 | } |
| 4765 | }; |
| 4766 | ko.expressionRewriting.twoWayBindings['hasfocus'] = true; |
| 4767 | |
| 4768 | ko.bindingHandlers['hasFocus'] = ko.bindingHandlers['hasfocus']; // Make "hasFocus" an alias |
| 4769 | ko.expressionRewriting.twoWayBindings['hasFocus'] = 'hasfocus'; |
| 4770 | ko.bindingHandlers['html'] = { |
| 4771 | 'init': function() { |
| 4772 | // Prevent binding on the dynamically-injected HTML (as developers are unlikely to expect that, and it has security implications) |
| 4773 | return { 'controlsDescendantBindings': true }; |
| 4774 | }, |
| 4775 | 'update': function (element, valueAccessor) { |
| 4776 | // setHtml will unwrap the value if needed |
| 4777 | ko.utils.setHtml(element, valueAccessor()); |
| 4778 | } |
| 4779 | }; |
| 4780 | (function () { |
| 4781 | |
| 4782 | // Makes a binding like with or if |
| 4783 | function makeWithIfBinding(bindingKey, isWith, isNot) { |
| 4784 | ko.bindingHandlers[bindingKey] = { |
| 4785 | 'init': function(element, valueAccessor, allBindings, viewModel, bindingContext) { |
| 4786 | var didDisplayOnLastUpdate, savedNodes, contextOptions = {}, completeOnRender, needAsyncContext, renderOnEveryChange; |
| 4787 | |
| 4788 | if (isWith) { |
| 4789 | var as = allBindings.get('as'), noChildContext = allBindings.get('noChildContext'); |
| 4790 | renderOnEveryChange = !(as && noChildContext); |
| 4791 | contextOptions = { 'as': as, 'noChildContext': noChildContext, 'exportDependencies': renderOnEveryChange }; |
| 4792 | } |
| 4793 | |
| 4794 | completeOnRender = allBindings.get("completeOn") == "render"; |
| 4795 | needAsyncContext = completeOnRender || allBindings['has'](ko.bindingEvent.descendantsComplete); |
| 4796 | |
| 4797 | ko.computed(function() { |
| 4798 | var value = ko.utils.unwrapObservable(valueAccessor()), |
| 4799 | shouldDisplay = !isNot !== !value, // equivalent to isNot ? !value : !!value, |
| 4800 | isInitial = !savedNodes, |
| 4801 | childContext; |
| 4802 | |
| 4803 | if (!renderOnEveryChange && shouldDisplay === didDisplayOnLastUpdate) { |
| 4804 | return; |
| 4805 | } |
| 4806 | |
| 4807 | if (needAsyncContext) { |
| 4808 | bindingContext = ko.bindingEvent.startPossiblyAsyncContentBinding(element, bindingContext); |
| 4809 | } |
| 4810 | |
| 4811 | if (shouldDisplay) { |
| 4812 | if (!isWith || renderOnEveryChange) { |
| 4813 | contextOptions['dataDependency'] = ko.computedContext.computed(); |
| 4814 | } |
| 4815 | |
| 4816 | if (isWith) { |
| 4817 | childContext = bindingContext['createChildContext'](typeof value == "function" ? value : valueAccessor, contextOptions); |
| 4818 | } else if (ko.computedContext.getDependenciesCount()) { |
| 4819 | childContext = bindingContext['extend'](null, contextOptions); |
| 4820 | } else { |
| 4821 | childContext = bindingContext; |
| 4822 | } |
| 4823 | } |
| 4824 | |
| 4825 | // Save a copy of the inner nodes on the initial update, but only if we have dependencies. |
| 4826 | if (isInitial && ko.computedContext.getDependenciesCount()) { |
| 4827 | savedNodes = ko.utils.cloneNodes(ko.virtualElements.childNodes(element), true /* shouldCleanNodes */); |
| 4828 | } |
| 4829 | |
| 4830 | if (shouldDisplay) { |
| 4831 | if (!isInitial) { |
| 4832 | ko.virtualElements.setDomNodeChildren(element, ko.utils.cloneNodes(savedNodes)); |
| 4833 | } |
| 4834 | |
| 4835 | ko.applyBindingsToDescendants(childContext, element); |
| 4836 | } else { |
| 4837 | ko.virtualElements.emptyNode(element); |
| 4838 | |
| 4839 | if (!completeOnRender) { |
| 4840 | ko.bindingEvent.notify(element, ko.bindingEvent.childrenComplete); |
| 4841 | } |
| 4842 | } |
| 4843 | |
| 4844 | didDisplayOnLastUpdate = shouldDisplay; |
| 4845 | |
| 4846 | }, null, { disposeWhenNodeIsRemoved: element }); |
| 4847 | |
| 4848 | return { 'controlsDescendantBindings': true }; |
| 4849 | } |
| 4850 | }; |
| 4851 | ko.expressionRewriting.bindingRewriteValidators[bindingKey] = false; // Can't rewrite control flow bindings |
| 4852 | ko.virtualElements.allowedBindings[bindingKey] = true; |
| 4853 | } |
| 4854 | |
| 4855 | // Construct the actual binding handlers |
| 4856 | makeWithIfBinding('if'); |
| 4857 | makeWithIfBinding('ifnot', false /* isWith */, true /* isNot */); |
| 4858 | makeWithIfBinding('with', true /* isWith */); |
| 4859 | |
| 4860 | })();ko.bindingHandlers['let'] = { |
| 4861 | 'init': function(element, valueAccessor, allBindings, viewModel, bindingContext) { |
| 4862 | // Make a modified binding context, with extra properties, and apply it to descendant elements |
| 4863 | var innerContext = bindingContext['extend'](valueAccessor); |
| 4864 | ko.applyBindingsToDescendants(innerContext, element); |
| 4865 | |
| 4866 | return { 'controlsDescendantBindings': true }; |
| 4867 | } |
| 4868 | }; |
| 4869 | ko.virtualElements.allowedBindings['let'] = true; |
| 4870 | var captionPlaceholder = {}; |
| 4871 | ko.bindingHandlers['options'] = { |
| 4872 | 'init': function(element) { |
| 4873 | if (ko.utils.tagNameLower(element) !== "select") |
| 4874 | throw new Error("options binding applies only to SELECT elements"); |
| 4875 | |
| 4876 | // Remove all existing <option>s. |
| 4877 | while (element.length > 0) { |
| 4878 | element.remove(0); |
| 4879 | } |
| 4880 | |
| 4881 | // Ensures that the binding processor doesn't try to bind the options |
| 4882 | return { 'controlsDescendantBindings': true }; |
| 4883 | }, |
| 4884 | 'update': function (element, valueAccessor, allBindings) { |
| 4885 | function selectedOptions() { |
| 4886 | return ko.utils.arrayFilter(element.options, function (node) { return node.selected; }); |
| 4887 | } |
| 4888 | |
| 4889 | var selectWasPreviouslyEmpty = element.length == 0, |
| 4890 | multiple = element.multiple, |
| 4891 | previousScrollTop = (!selectWasPreviouslyEmpty && multiple) ? element.scrollTop : null, |
| 4892 | unwrappedArray = ko.utils.unwrapObservable(valueAccessor()), |
| 4893 | valueAllowUnset = allBindings.get('valueAllowUnset') && allBindings['has']('value'), |
| 4894 | includeDestroyed = allBindings.get('optionsIncludeDestroyed'), |
| 4895 | arrayToDomNodeChildrenOptions = {}, |
| 4896 | captionValue, |
| 4897 | filteredArray, |
| 4898 | previousSelectedValues = []; |
| 4899 | |
| 4900 | if (!valueAllowUnset) { |
| 4901 | if (multiple) { |
| 4902 | previousSelectedValues = ko.utils.arrayMap(selectedOptions(), ko.selectExtensions.readValue); |
| 4903 | } else if (element.selectedIndex >= 0) { |
| 4904 | previousSelectedValues.push(ko.selectExtensions.readValue(element.options[element.selectedIndex])); |
| 4905 | } |
| 4906 | } |
| 4907 | |
| 4908 | if (unwrappedArray) { |
| 4909 | if (typeof unwrappedArray.length == "undefined") // Coerce single value into array |
| 4910 | unwrappedArray = [unwrappedArray]; |
| 4911 | |
| 4912 | // Filter out any entries marked as destroyed |
| 4913 | filteredArray = ko.utils.arrayFilter(unwrappedArray, function(item) { |
| 4914 | return includeDestroyed || item === undefined || item === null || !ko.utils.unwrapObservable(item['_destroy']); |
| 4915 | }); |
| 4916 | |
| 4917 | // If caption is included, add it to the array |
| 4918 | if (allBindings['has']('optionsCaption')) { |
| 4919 | captionValue = ko.utils.unwrapObservable(allBindings.get('optionsCaption')); |
| 4920 | // If caption value is null or undefined, don't show a caption |
| 4921 | if (captionValue !== null && captionValue !== undefined) { |
| 4922 | filteredArray.unshift(captionPlaceholder); |
| 4923 | } |
| 4924 | } |
| 4925 | } else { |
| 4926 | // If a falsy value is provided (e.g. null), we'll simply empty the select element |
| 4927 | } |
| 4928 | |
| 4929 | function applyToObject(object, predicate, defaultValue) { |
| 4930 | var predicateType = typeof predicate; |
| 4931 | if (predicateType == "function") // Given a function; run it against the data value |
| 4932 | return predicate(object); |
| 4933 | else if (predicateType == "string") // Given a string; treat it as a property name on the data value |
| 4934 | return object[predicate]; |
| 4935 | else // Given no optionsText arg; use the data value itself |
| 4936 | return defaultValue; |
| 4937 | } |
| 4938 | |
| 4939 | // The following functions can run at two different times: |
| 4940 | // The first is when the whole array is being updated directly from this binding handler. |
| 4941 | // The second is when an observable value for a specific array entry is updated. |
| 4942 | // oldOptions will be empty in the first case, but will be filled with the previously generated option in the second. |
| 4943 | var itemUpdate = false; |
| 4944 | function optionForArrayItem(arrayEntry, index, oldOptions) { |
| 4945 | if (oldOptions.length) { |
| 4946 | previousSelectedValues = !valueAllowUnset && oldOptions[0].selected ? [ ko.selectExtensions.readValue(oldOptions[0]) ] : []; |
| 4947 | itemUpdate = true; |
| 4948 | } |
| 4949 | var option = element.ownerDocument.createElement("option"); |
| 4950 | if (arrayEntry === captionPlaceholder) { |
| 4951 | ko.utils.setTextContent(option, allBindings.get('optionsCaption')); |
| 4952 | ko.selectExtensions.writeValue(option, undefined); |
| 4953 | } else { |
| 4954 | // Apply a value to the option element |
| 4955 | var optionValue = applyToObject(arrayEntry, allBindings.get('optionsValue'), arrayEntry); |
| 4956 | ko.selectExtensions.writeValue(option, ko.utils.unwrapObservable(optionValue)); |
| 4957 | |
| 4958 | // Apply some text to the option element |
| 4959 | var optionText = applyToObject(arrayEntry, allBindings.get('optionsText'), optionValue); |
| 4960 | ko.utils.setTextContent(option, optionText); |
| 4961 | } |
| 4962 | return [option]; |
| 4963 | } |
| 4964 | |
| 4965 | // By using a beforeRemove callback, we delay the removal until after new items are added. This fixes a selection |
| 4966 | // problem in IE<=8 and Firefox. See https://github.com/knockout/knockout/issues/1208 |
| 4967 | arrayToDomNodeChildrenOptions['beforeRemove'] = |
| 4968 | function (option) { |
| 4969 | element.removeChild(option); |
| 4970 | }; |
| 4971 | |
| 4972 | function setSelectionCallback(arrayEntry, newOptions) { |
| 4973 | if (itemUpdate && valueAllowUnset) { |
| 4974 | // The model value is authoritative, so make sure its value is the one selected |
| 4975 | // There is no need to use dependencyDetection.ignore since setDomNodeChildrenFromArrayMapping does so already. |
| 4976 | ko.selectExtensions.writeValue(element, ko.utils.unwrapObservable(allBindings.get('value')), true /* allowUnset */); |
| 4977 | } else if (previousSelectedValues.length) { |
| 4978 | // IE6 doesn't like us to assign selection to OPTION nodes before they're added to the document. |
| 4979 | // That's why we first added them without selection. Now it's time to set the selection. |
| 4980 | var isSelected = ko.utils.arrayIndexOf(previousSelectedValues, ko.selectExtensions.readValue(newOptions[0])) >= 0; |
| 4981 | ko.utils.setOptionNodeSelectionState(newOptions[0], isSelected); |
| 4982 | |
| 4983 | // If this option was changed from being selected during a single-item update, notify the change |
| 4984 | if (itemUpdate && !isSelected) { |
| 4985 | ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, "change"]); |
| 4986 | } |
| 4987 | } |
| 4988 | } |
| 4989 | |
| 4990 | var callback = setSelectionCallback; |
| 4991 | if (allBindings['has']('optionsAfterRender') && typeof allBindings.get('optionsAfterRender') == "function") { |
| 4992 | callback = function(arrayEntry, newOptions) { |
| 4993 | setSelectionCallback(arrayEntry, newOptions); |
| 4994 | ko.dependencyDetection.ignore(allBindings.get('optionsAfterRender'), null, [newOptions[0], arrayEntry !== captionPlaceholder ? arrayEntry : undefined]); |
| 4995 | } |
| 4996 | } |
| 4997 | |
| 4998 | ko.utils.setDomNodeChildrenFromArrayMapping(element, filteredArray, optionForArrayItem, arrayToDomNodeChildrenOptions, callback); |
| 4999 | |
| 5000 | ko.dependencyDetection.ignore(function () { |
| 5001 | if (valueAllowUnset) { |
| 5002 | // The model value is authoritative, so make sure its value is the one selected |
| 5003 | ko.selectExtensions.writeValue(element, ko.utils.unwrapObservable(allBindings.get('value')), true /* allowUnset */); |
| 5004 | } else { |
| 5005 | // Determine if the selection has changed as a result of updating the options list |
| 5006 | var selectionChanged; |
| 5007 | if (multiple) { |
| 5008 | // For a multiple-select box, compare the new selection count to the previous one |
| 5009 | // But if nothing was selected before, the selection can't have changed |
| 5010 | selectionChanged = previousSelectedValues.length && selectedOptions().length < previousSelectedValues.length; |
| 5011 | } else { |
| 5012 | // For a single-select box, compare the current value to the previous value |
| 5013 | // But if nothing was selected before or nothing is selected now, just look for a change in selection |
| 5014 | selectionChanged = (previousSelectedValues.length && element.selectedIndex >= 0) |
| 5015 | ? (ko.selectExtensions.readValue(element.options[element.selectedIndex]) !== previousSelectedValues[0]) |
| 5016 | : (previousSelectedValues.length || element.selectedIndex >= 0); |
| 5017 | } |
| 5018 | |
| 5019 | // Ensure consistency between model value and selected option. |
| 5020 | // If the dropdown was changed so that selection is no longer the same, |
| 5021 | // notify the value or selectedOptions binding. |
| 5022 | if (selectionChanged) { |
| 5023 | ko.utils.triggerEvent(element, "change"); |
| 5024 | } |
| 5025 | } |
| 5026 | }); |
| 5027 | |
| 5028 | // Workaround for IE bug |
| 5029 | ko.utils.ensureSelectElementIsRenderedCorrectly(element); |
| 5030 | |
| 5031 | if (previousScrollTop && Math.abs(previousScrollTop - element.scrollTop) > 20) |
| 5032 | element.scrollTop = previousScrollTop; |
| 5033 | } |
| 5034 | }; |
| 5035 | ko.bindingHandlers['options'].optionValueDomDataKey = ko.utils.domData.nextKey(); |
| 5036 | ko.bindingHandlers['selectedOptions'] = { |
| 5037 | 'after': ['options', 'foreach'], |
| 5038 | 'init': function (element, valueAccessor, allBindings) { |
| 5039 | ko.utils.registerEventHandler(element, "change", function () { |
| 5040 | var value = valueAccessor(), valueToWrite = []; |
| 5041 | ko.utils.arrayForEach(element.getElementsByTagName("option"), function(node) { |
| 5042 | if (node.selected) |
| 5043 | valueToWrite.push(ko.selectExtensions.readValue(node)); |
| 5044 | }); |
| 5045 | ko.expressionRewriting.writeValueToProperty(value, allBindings, 'selectedOptions', valueToWrite); |
| 5046 | }); |
| 5047 | }, |
| 5048 | 'update': function (element, valueAccessor) { |
| 5049 | if (ko.utils.tagNameLower(element) != "select") |
| 5050 | throw new Error("values binding applies only to SELECT elements"); |
| 5051 | |
| 5052 | var newValue = ko.utils.unwrapObservable(valueAccessor()), |
| 5053 | previousScrollTop = element.scrollTop; |
| 5054 | |
| 5055 | if (newValue && typeof newValue.length == "number") { |
| 5056 | ko.utils.arrayForEach(element.getElementsByTagName("option"), function(node) { |
| 5057 | var isSelected = ko.utils.arrayIndexOf(newValue, ko.selectExtensions.readValue(node)) >= 0; |
| 5058 | if (node.selected != isSelected) { // This check prevents flashing of the select element in IE |
| 5059 | ko.utils.setOptionNodeSelectionState(node, isSelected); |
| 5060 | } |
| 5061 | }); |
| 5062 | } |
| 5063 | |
| 5064 | element.scrollTop = previousScrollTop; |
| 5065 | } |
| 5066 | }; |
| 5067 | ko.expressionRewriting.twoWayBindings['selectedOptions'] = true; |
| 5068 | ko.bindingHandlers['style'] = { |
| 5069 | 'update': function (element, valueAccessor) { |
| 5070 | var value = ko.utils.unwrapObservable(valueAccessor() || {}); |
| 5071 | ko.utils.objectForEach(value, function(styleName, styleValue) { |
| 5072 | styleValue = ko.utils.unwrapObservable(styleValue); |
| 5073 | |
| 5074 | if (styleValue === null || styleValue === undefined || styleValue === false) { |
| 5075 | // Empty string removes the value, whereas null/undefined have no effect |
| 5076 | styleValue = ""; |
| 5077 | } |
| 5078 | |
| 5079 | if (jQueryInstance) { |
| 5080 | jQueryInstance(element)['css'](styleName, styleValue); |
| 5081 | } else if (/^--/.test(styleName)) { |
| 5082 | // Is styleName a custom CSS property? |
| 5083 | element.style.setProperty(styleName, styleValue); |
| 5084 | } else { |
| 5085 | styleName = styleName.replace(/-(\w)/g, function (all, letter) { |
| 5086 | return letter.toUpperCase(); |
| 5087 | }); |
| 5088 | |
| 5089 | var previousStyle = element.style[styleName]; |
| 5090 | element.style[styleName] = styleValue; |
| 5091 | |
| 5092 | if (styleValue !== previousStyle && element.style[styleName] == previousStyle && !isNaN(styleValue)) { |
| 5093 | element.style[styleName] = styleValue + "px"; |
| 5094 | } |
| 5095 | } |
| 5096 | }); |
| 5097 | } |
| 5098 | }; |
| 5099 | ko.bindingHandlers['submit'] = { |
| 5100 | 'init': function (element, valueAccessor, allBindings, viewModel, bindingContext) { |
| 5101 | if (typeof valueAccessor() != "function") |
| 5102 | throw new Error("The value for a submit binding must be a function"); |
| 5103 | ko.utils.registerEventHandler(element, "submit", function (event) { |
| 5104 | var handlerReturnValue; |
| 5105 | var value = valueAccessor(); |
| 5106 | try { handlerReturnValue = value.call(bindingContext['$data'], element); } |
| 5107 | finally { |
| 5108 | if (handlerReturnValue !== true) { // Normally we want to prevent default action. Developer can override this be explicitly returning true. |
| 5109 | if (event.preventDefault) |
| 5110 | event.preventDefault(); |
| 5111 | else |
| 5112 | event.returnValue = false; |
| 5113 | } |
| 5114 | } |
| 5115 | }); |
| 5116 | } |
| 5117 | }; |
| 5118 | ko.bindingHandlers['text'] = { |
| 5119 | 'init': function() { |
| 5120 | // Prevent binding on the dynamically-injected text node (as developers are unlikely to expect that, and it has security implications). |
| 5121 | // It should also make things faster, as we no longer have to consider whether the text node might be bindable. |
| 5122 | return { 'controlsDescendantBindings': true }; |
| 5123 | }, |
| 5124 | 'update': function (element, valueAccessor) { |
| 5125 | ko.utils.setTextContent(element, valueAccessor()); |
| 5126 | } |
| 5127 | }; |
| 5128 | ko.virtualElements.allowedBindings['text'] = true; |
| 5129 | (function () { |
| 5130 | |
| 5131 | if (window && window.navigator) { |
| 5132 | var parseVersion = function (matches) { |
| 5133 | if (matches) { |
| 5134 | return parseFloat(matches[1]); |
| 5135 | } |
| 5136 | }; |
| 5137 | |
| 5138 | // Detect various browser versions because some old versions don't fully support the 'input' event |
| 5139 | var userAgent = window.navigator.userAgent, |
| 5140 | operaVersion, chromeVersion, safariVersion, firefoxVersion, ieVersion, edgeVersion; |
| 5141 | |
| 5142 | (operaVersion = window.opera && window.opera.version && parseInt(window.opera.version())) |
| 5143 | || (edgeVersion = parseVersion(userAgent.match(/Edge\/([^ ]+)$/))) |
| 5144 | || (chromeVersion = parseVersion(userAgent.match(/Chrome\/([^ ]+)/))) |
| 5145 | || (safariVersion = parseVersion(userAgent.match(/Version\/([^ ]+) Safari/))) |
| 5146 | || (firefoxVersion = parseVersion(userAgent.match(/Firefox\/([^ ]+)/))) |
| 5147 | || (ieVersion = ko.utils.ieVersion || parseVersion(userAgent.match(/MSIE ([^ ]+)/))) // Detects up to IE 10 |
| 5148 | || (ieVersion = parseVersion(userAgent.match(/rv:([^ )]+)/))); // Detects IE 11 |
| 5149 | } |
| 5150 | |
| 5151 | // IE 8 and 9 have bugs that prevent the normal events from firing when the value changes. |
| 5152 | // But it does fire the 'selectionchange' event on many of those, presumably because the |
| 5153 | // cursor is moving and that counts as the selection changing. The 'selectionchange' event is |
| 5154 | // fired at the document level only and doesn't directly indicate which element changed. We |
| 5155 | // set up just one event handler for the document and use 'activeElement' to determine which |
| 5156 | // element was changed. |
| 5157 | if (ieVersion >= 8 && ieVersion < 10) { |
| 5158 | var selectionChangeRegisteredName = ko.utils.domData.nextKey(), |
| 5159 | selectionChangeHandlerName = ko.utils.domData.nextKey(); |
| 5160 | var selectionChangeHandler = function(event) { |
| 5161 | var target = this.activeElement, |
| 5162 | handler = target && ko.utils.domData.get(target, selectionChangeHandlerName); |
| 5163 | if (handler) { |
| 5164 | handler(event); |
| 5165 | } |
| 5166 | }; |
| 5167 | var registerForSelectionChangeEvent = function (element, handler) { |
| 5168 | var ownerDoc = element.ownerDocument; |
| 5169 | if (!ko.utils.domData.get(ownerDoc, selectionChangeRegisteredName)) { |
| 5170 | ko.utils.domData.set(ownerDoc, selectionChangeRegisteredName, true); |
| 5171 | ko.utils.registerEventHandler(ownerDoc, 'selectionchange', selectionChangeHandler); |
| 5172 | } |
| 5173 | ko.utils.domData.set(element, selectionChangeHandlerName, handler); |
| 5174 | }; |
| 5175 | } |
| 5176 | |
| 5177 | ko.bindingHandlers['textInput'] = { |
| 5178 | 'init': function (element, valueAccessor, allBindings) { |
| 5179 | |
| 5180 | var previousElementValue = element.value, |
| 5181 | timeoutHandle, |
| 5182 | elementValueBeforeEvent; |
| 5183 | |
| 5184 | var updateModel = function (event) { |
| 5185 | clearTimeout(timeoutHandle); |
| 5186 | elementValueBeforeEvent = timeoutHandle = undefined; |
| 5187 | |
| 5188 | var elementValue = element.value; |
| 5189 | if (previousElementValue !== elementValue) { |
| 5190 | // Provide a way for tests to know exactly which event was processed |
| 5191 | if (DEBUG && event) element['_ko_textInputProcessedEvent'] = event.type; |
| 5192 | previousElementValue = elementValue; |
| 5193 | ko.expressionRewriting.writeValueToProperty(valueAccessor(), allBindings, 'textInput', elementValue); |
| 5194 | } |
| 5195 | }; |
| 5196 | |
| 5197 | var deferUpdateModel = function (event) { |
| 5198 | if (!timeoutHandle) { |
| 5199 | // The elementValueBeforeEvent variable is set *only* during the brief gap between an |
| 5200 | // event firing and the updateModel function running. This allows us to ignore model |
| 5201 | // updates that are from the previous state of the element, usually due to techniques |
| 5202 | // such as rateLimit. Such updates, if not ignored, can cause keystrokes to be lost. |
| 5203 | elementValueBeforeEvent = element.value; |
| 5204 | var handler = DEBUG ? updateModel.bind(element, {type: event.type}) : updateModel; |
| 5205 | timeoutHandle = ko.utils.setTimeout(handler, 4); |
| 5206 | } |
| 5207 | }; |
| 5208 | |
| 5209 | // IE9 will mess up the DOM if you handle events synchronously which results in DOM changes (such as other bindings); |
| 5210 | // so we'll make sure all updates are asynchronous |
| 5211 | var ieUpdateModel = ko.utils.ieVersion == 9 ? deferUpdateModel : updateModel, |
| 5212 | ourUpdate = false; |
| 5213 | |
| 5214 | var updateView = function () { |
| 5215 | var modelValue = ko.utils.unwrapObservable(valueAccessor()); |
| 5216 | |
| 5217 | if (modelValue === null || modelValue === undefined) { |
| 5218 | modelValue = ''; |
| 5219 | } |
| 5220 | |
| 5221 | if (elementValueBeforeEvent !== undefined && modelValue === elementValueBeforeEvent) { |
| 5222 | ko.utils.setTimeout(updateView, 4); |
| 5223 | return; |
| 5224 | } |
| 5225 | |
| 5226 | // Update the element only if the element and model are different. On some browsers, updating the value |
| 5227 | // will move the cursor to the end of the input, which would be bad while the user is typing. |
| 5228 | if (element.value !== modelValue) { |
| 5229 | ourUpdate = true; // Make sure we ignore events (propertychange) that result from updating the value |
| 5230 | element.value = modelValue; |
| 5231 | ourUpdate = false; |
| 5232 | previousElementValue = element.value; // In case the browser changes the value (see #2281) |
| 5233 | } |
| 5234 | }; |
| 5235 | |
| 5236 | var onEvent = function (event, handler) { |
| 5237 | ko.utils.registerEventHandler(element, event, handler); |
| 5238 | }; |
| 5239 | |
| 5240 | if (DEBUG && ko.bindingHandlers['textInput']['_forceUpdateOn']) { |
| 5241 | // Provide a way for tests to specify exactly which events are bound |
| 5242 | ko.utils.arrayForEach(ko.bindingHandlers['textInput']['_forceUpdateOn'], function(eventName) { |
| 5243 | if (eventName.slice(0,5) == 'after') { |
| 5244 | onEvent(eventName.slice(5), deferUpdateModel); |
| 5245 | } else { |
| 5246 | onEvent(eventName, updateModel); |
| 5247 | } |
| 5248 | }); |
| 5249 | } else { |
| 5250 | if (ieVersion) { |
| 5251 | // All versions (including 11) of Internet Explorer have a bug that they don't generate an input or propertychange event when ESC is pressed |
| 5252 | onEvent('keypress', updateModel); |
| 5253 | } |
| 5254 | if (ieVersion < 11) { |
| 5255 | // Internet Explorer <= 8 doesn't support the 'input' event, but does include 'propertychange' that fires whenever |
| 5256 | // any property of an element changes. Unlike 'input', it also fires if a property is changed from JavaScript code, |
| 5257 | // but that's an acceptable compromise for this binding. IE 9 and 10 support 'input', but since they don't always |
| 5258 | // fire it when using autocomplete, we'll use 'propertychange' for them also. |
| 5259 | onEvent('propertychange', function(event) { |
| 5260 | if (!ourUpdate && event.propertyName === 'value') { |
| 5261 | ieUpdateModel(event); |
| 5262 | } |
| 5263 | }); |
| 5264 | } |
| 5265 | if (ieVersion == 8) { |
| 5266 | // IE 8 has a bug where it fails to fire 'propertychange' on the first update following a value change from |
| 5267 | // JavaScript code. It also doesn't fire if you clear the entire value. To fix this, we bind to the following |
| 5268 | // events too. |
| 5269 | onEvent('keyup', updateModel); // A single keystoke |
| 5270 | onEvent('keydown', updateModel); // The first character when a key is held down |
| 5271 | } |
| 5272 | if (registerForSelectionChangeEvent) { |
| 5273 | // Internet Explorer 9 doesn't fire the 'input' event when deleting text, including using |
| 5274 | // the backspace, delete, or ctrl-x keys, clicking the 'x' to clear the input, dragging text |
| 5275 | // out of the field, and cutting or deleting text using the context menu. 'selectionchange' |
| 5276 | // can detect all of those except dragging text out of the field, for which we use 'dragend'. |
| 5277 | // These are also needed in IE8 because of the bug described above. |
| 5278 | registerForSelectionChangeEvent(element, ieUpdateModel); // 'selectionchange' covers cut, paste, drop, delete, etc. |
| 5279 | onEvent('dragend', deferUpdateModel); |
| 5280 | } |
| 5281 | |
| 5282 | if (!ieVersion || ieVersion >= 9) { |
| 5283 | // All other supported browsers support the 'input' event, which fires whenever the content of the element is changed |
| 5284 | // through the user interface. |
| 5285 | onEvent('input', ieUpdateModel); |
| 5286 | } |
| 5287 | |
| 5288 | if (safariVersion < 5 && ko.utils.tagNameLower(element) === "textarea") { |
| 5289 | // Safari <5 doesn't fire the 'input' event for <textarea> elements (it does fire 'textInput' |
| 5290 | // but only when typing). So we'll just catch as much as we can with keydown, cut, and paste. |
| 5291 | onEvent('keydown', deferUpdateModel); |
| 5292 | onEvent('paste', deferUpdateModel); |
| 5293 | onEvent('cut', deferUpdateModel); |
| 5294 | } else if (operaVersion < 11) { |
| 5295 | // Opera 10 doesn't always fire the 'input' event for cut, paste, undo & drop operations. |
| 5296 | // We can try to catch some of those using 'keydown'. |
| 5297 | onEvent('keydown', deferUpdateModel); |
| 5298 | } else if (firefoxVersion < 4.0) { |
| 5299 | // Firefox <= 3.6 doesn't fire the 'input' event when text is filled in through autocomplete |
| 5300 | onEvent('DOMAutoComplete', updateModel); |
| 5301 | |
| 5302 | // Firefox <=3.5 doesn't fire the 'input' event when text is dropped into the input. |
| 5303 | onEvent('dragdrop', updateModel); // <3.5 |
| 5304 | onEvent('drop', updateModel); // 3.5 |
| 5305 | } else if (edgeVersion && element.type === "number") { |
| 5306 | // Microsoft Edge doesn't fire 'input' or 'change' events for number inputs when |
| 5307 | // the value is changed via the up / down arrow keys |
| 5308 | onEvent('keydown', deferUpdateModel); |
| 5309 | } |
| 5310 | } |
| 5311 | |
| 5312 | // Bind to the change event so that we can catch programmatic updates of the value that fire this event. |
| 5313 | onEvent('change', updateModel); |
| 5314 | |
| 5315 | // To deal with browsers that don't notify any kind of event for some changes (IE, Safari, etc.) |
| 5316 | onEvent('blur', updateModel); |
| 5317 | |
| 5318 | ko.computed(updateView, null, { disposeWhenNodeIsRemoved: element }); |
| 5319 | } |
| 5320 | }; |
| 5321 | ko.expressionRewriting.twoWayBindings['textInput'] = true; |
| 5322 | |
| 5323 | // textinput is an alias for textInput |
| 5324 | ko.bindingHandlers['textinput'] = { |
| 5325 | // preprocess is the only way to set up a full alias |
| 5326 | 'preprocess': function (value, name, addBinding) { |
| 5327 | addBinding('textInput', value); |
| 5328 | } |
| 5329 | }; |
| 5330 | |
| 5331 | })();ko.bindingHandlers['uniqueName'] = { |
| 5332 | 'init': function (element, valueAccessor) { |
| 5333 | if (valueAccessor()) { |
| 5334 | var name = "ko_unique_" + (++ko.bindingHandlers['uniqueName'].currentIndex); |
| 5335 | ko.utils.setElementName(element, name); |
| 5336 | } |
| 5337 | } |
| 5338 | }; |
| 5339 | ko.bindingHandlers['uniqueName'].currentIndex = 0; |
| 5340 | ko.bindingHandlers['using'] = { |
| 5341 | 'init': function(element, valueAccessor, allBindings, viewModel, bindingContext) { |
| 5342 | var options; |
| 5343 | |
| 5344 | if (allBindings['has']('as')) { |
| 5345 | options = { 'as': allBindings.get('as'), 'noChildContext': allBindings.get('noChildContext') }; |
| 5346 | } |
| 5347 | |
| 5348 | var innerContext = bindingContext['createChildContext'](valueAccessor, options); |
| 5349 | ko.applyBindingsToDescendants(innerContext, element); |
| 5350 | |
| 5351 | return { 'controlsDescendantBindings': true }; |
| 5352 | } |
| 5353 | }; |
| 5354 | ko.virtualElements.allowedBindings['using'] = true; |
| 5355 | ko.bindingHandlers['value'] = { |
| 5356 | 'after': ['options', 'foreach'], |
| 5357 | 'init': function (element, valueAccessor, allBindings) { |
| 5358 | var tagName = ko.utils.tagNameLower(element), |
| 5359 | isInputElement = tagName == "input"; |
| 5360 | |
| 5361 | // If the value binding is placed on a radio/checkbox, then just pass through to checkedValue and quit |
| 5362 | if (isInputElement && (element.type == "checkbox" || element.type == "radio")) { |
| 5363 | ko.applyBindingAccessorsToNode(element, { 'checkedValue': valueAccessor }); |
| 5364 | return; |
| 5365 | } |
| 5366 | |
| 5367 | // Always catch "change" event; possibly other events too if asked |
| 5368 | var eventsToCatch = ["change"]; |
| 5369 | var requestedEventsToCatch = allBindings.get("valueUpdate"); |
| 5370 | var propertyChangedFired = false; |
| 5371 | var elementValueBeforeEvent = null; |
| 5372 | |
| 5373 | if (requestedEventsToCatch) { |
| 5374 | if (typeof requestedEventsToCatch == "string") // Allow both individual event names, and arrays of event names |
| 5375 | requestedEventsToCatch = [requestedEventsToCatch]; |
| 5376 | ko.utils.arrayPushAll(eventsToCatch, requestedEventsToCatch); |
| 5377 | eventsToCatch = ko.utils.arrayGetDistinctValues(eventsToCatch); |
| 5378 | } |
| 5379 | |
| 5380 | var valueUpdateHandler = function() { |
| 5381 | elementValueBeforeEvent = null; |
| 5382 | propertyChangedFired = false; |
| 5383 | var modelValue = valueAccessor(); |
| 5384 | var elementValue = ko.selectExtensions.readValue(element); |
| 5385 | ko.expressionRewriting.writeValueToProperty(modelValue, allBindings, 'value', elementValue); |
| 5386 | } |
| 5387 | |
| 5388 | // Workaround for https://github.com/SteveSanderson/knockout/issues/122 |
| 5389 | // IE doesn't fire "change" events on textboxes if the user selects a value from its autocomplete list |
| 5390 | var ieAutoCompleteHackNeeded = ko.utils.ieVersion && isInputElement && element.type == "text" |
| 5391 | && element.autocomplete != "off" && (!element.form || element.form.autocomplete != "off"); |
| 5392 | if (ieAutoCompleteHackNeeded && ko.utils.arrayIndexOf(eventsToCatch, "propertychange") == -1) { |
| 5393 | ko.utils.registerEventHandler(element, "propertychange", function () { propertyChangedFired = true }); |
| 5394 | ko.utils.registerEventHandler(element, "focus", function () { propertyChangedFired = false }); |
| 5395 | ko.utils.registerEventHandler(element, "blur", function() { |
| 5396 | if (propertyChangedFired) { |
| 5397 | valueUpdateHandler(); |
| 5398 | } |
| 5399 | }); |
| 5400 | } |
| 5401 | |
| 5402 | ko.utils.arrayForEach(eventsToCatch, function(eventName) { |
| 5403 | // The syntax "after<eventname>" means "run the handler asynchronously after the event" |
| 5404 | // This is useful, for example, to catch "keydown" events after the browser has updated the control |
| 5405 | // (otherwise, ko.selectExtensions.readValue(this) will receive the control's value *before* the key event) |
| 5406 | var handler = valueUpdateHandler; |
| 5407 | if (ko.utils.stringStartsWith(eventName, "after")) { |
| 5408 | handler = function() { |
| 5409 | // The elementValueBeforeEvent variable is non-null *only* during the brief gap between |
| 5410 | // a keyX event firing and the valueUpdateHandler running, which is scheduled to happen |
| 5411 | // at the earliest asynchronous opportunity. We store this temporary information so that |
| 5412 | // if, between keyX and valueUpdateHandler, the underlying model value changes separately, |
| 5413 | // we can overwrite that model value change with the value the user just typed. Otherwise, |
| 5414 | // techniques like rateLimit can trigger model changes at critical moments that will |
| 5415 | // override the user's inputs, causing keystrokes to be lost. |
| 5416 | elementValueBeforeEvent = ko.selectExtensions.readValue(element); |
| 5417 | ko.utils.setTimeout(valueUpdateHandler, 0); |
| 5418 | }; |
| 5419 | eventName = eventName.substring("after".length); |
| 5420 | } |
| 5421 | ko.utils.registerEventHandler(element, eventName, handler); |
| 5422 | }); |
| 5423 | |
| 5424 | var updateFromModel; |
| 5425 | |
| 5426 | if (isInputElement && element.type == "file") { |
| 5427 | // For file input elements, can only write the empty string |
| 5428 | updateFromModel = function () { |
| 5429 | var newValue = ko.utils.unwrapObservable(valueAccessor()); |
| 5430 | if (newValue === null || newValue === undefined || newValue === "") { |
| 5431 | element.value = ""; |
| 5432 | } else { |
| 5433 | ko.dependencyDetection.ignore(valueUpdateHandler); // reset the model to match the element |
| 5434 | } |
| 5435 | } |
| 5436 | } else { |
| 5437 | updateFromModel = function () { |
| 5438 | var newValue = ko.utils.unwrapObservable(valueAccessor()); |
| 5439 | var elementValue = ko.selectExtensions.readValue(element); |
| 5440 | |
| 5441 | if (elementValueBeforeEvent !== null && newValue === elementValueBeforeEvent) { |
| 5442 | ko.utils.setTimeout(updateFromModel, 0); |
| 5443 | return; |
| 5444 | } |
| 5445 | |
| 5446 | var valueHasChanged = newValue !== elementValue; |
| 5447 | |
| 5448 | if (valueHasChanged || elementValue === undefined) { |
| 5449 | if (tagName === "select") { |
| 5450 | var allowUnset = allBindings.get('valueAllowUnset'); |
| 5451 | ko.selectExtensions.writeValue(element, newValue, allowUnset); |
| 5452 | if (!allowUnset && newValue !== ko.selectExtensions.readValue(element)) { |
| 5453 | // If you try to set a model value that can't be represented in an already-populated dropdown, reject that change, |
| 5454 | // because you're not allowed to have a model value that disagrees with a visible UI selection. |
| 5455 | ko.dependencyDetection.ignore(valueUpdateHandler); |
| 5456 | } |
| 5457 | } else { |
| 5458 | ko.selectExtensions.writeValue(element, newValue); |
| 5459 | } |
| 5460 | } |
| 5461 | }; |
| 5462 | } |
| 5463 | |
| 5464 | ko.computed(updateFromModel, null, { disposeWhenNodeIsRemoved: element }); |
| 5465 | }, |
| 5466 | 'update': function() {} // Keep for backwards compatibility with code that may have wrapped value binding |
| 5467 | }; |
| 5468 | ko.expressionRewriting.twoWayBindings['value'] = true; |
| 5469 | ko.bindingHandlers['visible'] = { |
| 5470 | 'update': function (element, valueAccessor) { |
| 5471 | var value = ko.utils.unwrapObservable(valueAccessor()); |
| 5472 | var isCurrentlyVisible = !(element.style.display == "none"); |
| 5473 | if (value && !isCurrentlyVisible) |
| 5474 | element.style.display = ""; |
| 5475 | else if ((!value) && isCurrentlyVisible) |
| 5476 | element.style.display = "none"; |
| 5477 | } |
| 5478 | }; |
| 5479 | |
| 5480 | ko.bindingHandlers['hidden'] = { |
| 5481 | 'update': function (element, valueAccessor) { |
| 5482 | ko.bindingHandlers['visible']['update'](element, function() { return !ko.utils.unwrapObservable(valueAccessor()) }); |
| 5483 | } |
| 5484 | }; |
| 5485 | // 'click' is just a shorthand for the usual full-length event:{click:handler} |
| 5486 | makeEventHandlerShortcut('click'); |
| 5487 | // If you want to make a custom template engine, |
| 5488 | // |
| 5489 | // [1] Inherit from this class (like ko.nativeTemplateEngine does) |
| 5490 | // [2] Override 'renderTemplateSource', supplying a function with this signature: |
| 5491 | // |
| 5492 | // function (templateSource, bindingContext, options) { |
| 5493 | // // - templateSource.text() is the text of the template you should render |
| 5494 | // // - bindingContext.$data is the data you should pass into the template |
| 5495 | // // - you might also want to make bindingContext.$parent, bindingContext.$parents, |
| 5496 | // // and bindingContext.$root available in the template too |
| 5497 | // // - options gives you access to any other properties set on "data-bind: { template: options }" |
| 5498 | // // - templateDocument is the document object of the template |
| 5499 | // // |
| 5500 | // // Return value: an array of DOM nodes |
| 5501 | // } |
| 5502 | // |
| 5503 | // [3] Override 'createJavaScriptEvaluatorBlock', supplying a function with this signature: |
| 5504 | // |
| 5505 | // function (script) { |
| 5506 | // // Return value: Whatever syntax means "Evaluate the JavaScript statement 'script' and output the result" |
| 5507 | // // For example, the jquery.tmpl template engine converts 'someScript' to '${ someScript }' |
| 5508 | // } |
| 5509 | // |
| 5510 | // This is only necessary if you want to allow data-bind attributes to reference arbitrary template variables. |
| 5511 | // If you don't want to allow that, you can set the property 'allowTemplateRewriting' to false (like ko.nativeTemplateEngine does) |
| 5512 | // and then you don't need to override 'createJavaScriptEvaluatorBlock'. |
| 5513 | |
| 5514 | ko.templateEngine = function () { }; |
| 5515 | |
| 5516 | ko.templateEngine.prototype['renderTemplateSource'] = function (templateSource, bindingContext, options, templateDocument) { |
| 5517 | throw new Error("Override renderTemplateSource"); |
| 5518 | }; |
| 5519 | |
| 5520 | ko.templateEngine.prototype['createJavaScriptEvaluatorBlock'] = function (script) { |
| 5521 | throw new Error("Override createJavaScriptEvaluatorBlock"); |
| 5522 | }; |
| 5523 | |
| 5524 | ko.templateEngine.prototype['makeTemplateSource'] = function(template, templateDocument) { |
| 5525 | // Named template |
| 5526 | if (typeof template == "string") { |
| 5527 | templateDocument = templateDocument || document; |
| 5528 | var elem = templateDocument.getElementById(template); |
| 5529 | if (!elem) |
| 5530 | throw new Error("Cannot find template with ID " + template); |
| 5531 | return new ko.templateSources.domElement(elem); |
| 5532 | } else if ((template.nodeType == 1) || (template.nodeType == 8)) { |
| 5533 | // Anonymous template |
| 5534 | return new ko.templateSources.anonymousTemplate(template); |
| 5535 | } else |
| 5536 | throw new Error("Unknown template type: " + template); |
| 5537 | }; |
| 5538 | |
| 5539 | ko.templateEngine.prototype['renderTemplate'] = function (template, bindingContext, options, templateDocument) { |
| 5540 | var templateSource = this['makeTemplateSource'](template, templateDocument); |
| 5541 | return this['renderTemplateSource'](templateSource, bindingContext, options, templateDocument); |
| 5542 | }; |
| 5543 | |
| 5544 | ko.templateEngine.prototype['isTemplateRewritten'] = function (template, templateDocument) { |
| 5545 | // Skip rewriting if requested |
| 5546 | if (this['allowTemplateRewriting'] === false) |
| 5547 | return true; |
| 5548 | return this['makeTemplateSource'](template, templateDocument)['data']("isRewritten"); |
| 5549 | }; |
| 5550 | |
| 5551 | ko.templateEngine.prototype['rewriteTemplate'] = function (template, rewriterCallback, templateDocument) { |
| 5552 | var templateSource = this['makeTemplateSource'](template, templateDocument); |
| 5553 | var rewritten = rewriterCallback(templateSource['text']()); |
| 5554 | templateSource['text'](rewritten); |
| 5555 | templateSource['data']("isRewritten", true); |
| 5556 | }; |
| 5557 | |
| 5558 | ko.exportSymbol('templateEngine', ko.templateEngine); |
| 5559 | |
| 5560 | ko.templateRewriting = (function () { |
| 5561 | var memoizeDataBindingAttributeSyntaxRegex = /(<([a-z]+\d*)(?:\s+(?!data-bind\s*=\s*)[a-z0-9\-]+(?:=(?:\"[^\"]*\"|\'[^\']*\'|[^>]*))?)*\s+)data-bind\s*=\s*(["'])([\s\S]*?)\3/gi; |
| 5562 | var memoizeVirtualContainerBindingSyntaxRegex = /<!--\s*ko\b\s*([\s\S]*?)\s*-->/g; |
| 5563 | |
| 5564 | function validateDataBindValuesForRewriting(keyValueArray) { |
| 5565 | var allValidators = ko.expressionRewriting.bindingRewriteValidators; |
| 5566 | for (var i = 0; i < keyValueArray.length; i++) { |
| 5567 | var key = keyValueArray[i]['key']; |
| 5568 | if (Object.prototype.hasOwnProperty.call(allValidators, key)) { |
| 5569 | var validator = allValidators[key]; |
| 5570 | |
| 5571 | if (typeof validator === "function") { |
| 5572 | var possibleErrorMessage = validator(keyValueArray[i]['value']); |
| 5573 | if (possibleErrorMessage) |
| 5574 | throw new Error(possibleErrorMessage); |
| 5575 | } else if (!validator) { |
| 5576 | throw new Error("This template engine does not support the '" + key + "' binding within its templates"); |
| 5577 | } |
| 5578 | } |
| 5579 | } |
| 5580 | } |
| 5581 | |
| 5582 | function constructMemoizedTagReplacement(dataBindAttributeValue, tagToRetain, nodeName, templateEngine) { |
| 5583 | var dataBindKeyValueArray = ko.expressionRewriting.parseObjectLiteral(dataBindAttributeValue); |
| 5584 | validateDataBindValuesForRewriting(dataBindKeyValueArray); |
| 5585 | var rewrittenDataBindAttributeValue = ko.expressionRewriting.preProcessBindings(dataBindKeyValueArray, {'valueAccessors':true}); |
| 5586 | |
| 5587 | // For no obvious reason, Opera fails to evaluate rewrittenDataBindAttributeValue unless it's wrapped in an additional |
| 5588 | // anonymous function, even though Opera's built-in debugger can evaluate it anyway. No other browser requires this |
| 5589 | // extra indirection. |
| 5590 | var applyBindingsToNextSiblingScript = |
| 5591 | "ko.__tr_ambtns(function($context,$element){return(function(){return{ " + rewrittenDataBindAttributeValue + " } })()},'" + nodeName.toLowerCase() + "')"; |
| 5592 | return templateEngine['createJavaScriptEvaluatorBlock'](applyBindingsToNextSiblingScript) + tagToRetain; |
| 5593 | } |
| 5594 | |
| 5595 | return { |
| 5596 | ensureTemplateIsRewritten: function (template, templateEngine, templateDocument) { |
| 5597 | if (!templateEngine['isTemplateRewritten'](template, templateDocument)) |
| 5598 | templateEngine['rewriteTemplate'](template, function (htmlString) { |
| 5599 | return ko.templateRewriting.memoizeBindingAttributeSyntax(htmlString, templateEngine); |
| 5600 | }, templateDocument); |
| 5601 | }, |
| 5602 | |
| 5603 | memoizeBindingAttributeSyntax: function (htmlString, templateEngine) { |
| 5604 | return htmlString.replace(memoizeDataBindingAttributeSyntaxRegex, function () { |
| 5605 | return constructMemoizedTagReplacement(/* dataBindAttributeValue: */ arguments[4], /* tagToRetain: */ arguments[1], /* nodeName: */ arguments[2], templateEngine); |
| 5606 | }).replace(memoizeVirtualContainerBindingSyntaxRegex, function() { |
| 5607 | return constructMemoizedTagReplacement(/* dataBindAttributeValue: */ arguments[1], /* tagToRetain: */ "<!-- ko -->", /* nodeName: */ "#comment", templateEngine); |
| 5608 | }); |
| 5609 | }, |
| 5610 | |
| 5611 | applyMemoizedBindingsToNextSibling: function (bindings, nodeName) { |
| 5612 | return ko.memoization.memoize(function (domNode, bindingContext) { |
| 5613 | var nodeToBind = domNode.nextSibling; |
| 5614 | if (nodeToBind && nodeToBind.nodeName.toLowerCase() === nodeName) { |
| 5615 | ko.applyBindingAccessorsToNode(nodeToBind, bindings, bindingContext); |
| 5616 | } |
| 5617 | }); |
| 5618 | } |
| 5619 | } |
| 5620 | })(); |
| 5621 | |
| 5622 | |
| 5623 | // Exported only because it has to be referenced by string lookup from within rewritten template |
| 5624 | ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextSibling); |
| 5625 | (function() { |
| 5626 | // A template source represents a read/write way of accessing a template. This is to eliminate the need for template loading/saving |
| 5627 | // logic to be duplicated in every template engine (and means they can all work with anonymous templates, etc.) |
| 5628 | // |
| 5629 | // Two are provided by default: |
| 5630 | // 1. ko.templateSources.domElement - reads/writes the text content of an arbitrary DOM element |
| 5631 | // 2. ko.templateSources.anonymousElement - uses ko.utils.domData to read/write text *associated* with the DOM element, but |
| 5632 | // without reading/writing the actual element text content, since it will be overwritten |
| 5633 | // with the rendered template output. |
| 5634 | // You can implement your own template source if you want to fetch/store templates somewhere other than in DOM elements. |
| 5635 | // Template sources need to have the following functions: |
| 5636 | // text() - returns the template text from your storage location |
| 5637 | // text(value) - writes the supplied template text to your storage location |
| 5638 | // data(key) - reads values stored using data(key, value) - see below |
| 5639 | // data(key, value) - associates "value" with this template and the key "key". Is used to store information like "isRewritten". |
| 5640 | // |
| 5641 | // Optionally, template sources can also have the following functions: |
| 5642 | // nodes() - returns a DOM element containing the nodes of this template, where available |
| 5643 | // nodes(value) - writes the given DOM element to your storage location |
| 5644 | // If a DOM element is available for a given template source, template engines are encouraged to use it in preference over text() |
| 5645 | // for improved speed. However, all templateSources must supply text() even if they don't supply nodes(). |
| 5646 | // |
| 5647 | // Once you've implemented a templateSource, make your template engine use it by subclassing whatever template engine you were |
| 5648 | // using and overriding "makeTemplateSource" to return an instance of your custom template source. |
| 5649 | |
| 5650 | ko.templateSources = {}; |
| 5651 | |
| 5652 | // ---- ko.templateSources.domElement ----- |
| 5653 | |
| 5654 | // template types |
| 5655 | var templateScript = 1, |
| 5656 | templateTextArea = 2, |
| 5657 | templateTemplate = 3, |
| 5658 | templateElement = 4; |
| 5659 | |
| 5660 | ko.templateSources.domElement = function(element) { |
| 5661 | this.domElement = element; |
| 5662 | |
| 5663 | if (element) { |
| 5664 | var tagNameLower = ko.utils.tagNameLower(element); |
| 5665 | this.templateType = |
| 5666 | tagNameLower === "script" ? templateScript : |
| 5667 | tagNameLower === "textarea" ? templateTextArea : |
| 5668 | // For browsers with proper <template> element support, where the .content property gives a document fragment |
| 5669 | tagNameLower == "template" && element.content && element.content.nodeType === 11 ? templateTemplate : |
| 5670 | templateElement; |
| 5671 | } |
| 5672 | } |
| 5673 | |
| 5674 | ko.templateSources.domElement.prototype['text'] = function(/* valueToWrite */) { |
| 5675 | var elemContentsProperty = this.templateType === templateScript ? "text" |
| 5676 | : this.templateType === templateTextArea ? "value" |
| 5677 | : "innerHTML"; |
| 5678 | |
| 5679 | if (arguments.length == 0) { |
| 5680 | return this.domElement[elemContentsProperty]; |
| 5681 | } else { |
| 5682 | var valueToWrite = arguments[0]; |
| 5683 | if (elemContentsProperty === "innerHTML") |
| 5684 | ko.utils.setHtml(this.domElement, valueToWrite); |
| 5685 | else |
| 5686 | this.domElement[elemContentsProperty] = valueToWrite; |
| 5687 | } |
| 5688 | }; |
| 5689 | |
| 5690 | var dataDomDataPrefix = ko.utils.domData.nextKey() + "_"; |
| 5691 | ko.templateSources.domElement.prototype['data'] = function(key /*, valueToWrite */) { |
| 5692 | if (arguments.length === 1) { |
| 5693 | return ko.utils.domData.get(this.domElement, dataDomDataPrefix + key); |
| 5694 | } else { |
| 5695 | ko.utils.domData.set(this.domElement, dataDomDataPrefix + key, arguments[1]); |
| 5696 | } |
| 5697 | }; |
| 5698 | |
| 5699 | var templatesDomDataKey = ko.utils.domData.nextKey(); |
| 5700 | function getTemplateDomData(element) { |
| 5701 | return ko.utils.domData.get(element, templatesDomDataKey) || {}; |
| 5702 | } |
| 5703 | function setTemplateDomData(element, data) { |
| 5704 | ko.utils.domData.set(element, templatesDomDataKey, data); |
| 5705 | } |
| 5706 | |
| 5707 | ko.templateSources.domElement.prototype['nodes'] = function(/* valueToWrite */) { |
| 5708 | var element = this.domElement; |
| 5709 | if (arguments.length == 0) { |
| 5710 | var templateData = getTemplateDomData(element), |
| 5711 | nodes = templateData.containerData || ( |
| 5712 | this.templateType === templateTemplate ? element.content : |
| 5713 | this.templateType === templateElement ? element : |
| 5714 | undefined); |
| 5715 | if (!nodes || templateData.alwaysCheckText) { |
| 5716 | // If the template is associated with an element that stores the template as text, |
| 5717 | // parse and cache the nodes whenever there's new text content available. This allows |
| 5718 | // the user to update the template content by updating the text of template node. |
| 5719 | var text = this['text'](); |
| 5720 | if (text) { |
| 5721 | nodes = ko.utils.parseHtmlForTemplateNodes(text, element.ownerDocument); |
| 5722 | this['text'](""); // clear the text from the node |
| 5723 | setTemplateDomData(element, {containerData: nodes, alwaysCheckText: true}); |
| 5724 | } |
| 5725 | } |
| 5726 | return nodes; |
| 5727 | } else { |
| 5728 | var valueToWrite = arguments[0]; |
| 5729 | setTemplateDomData(element, {containerData: valueToWrite}); |
| 5730 | } |
| 5731 | }; |
| 5732 | |
| 5733 | // ---- ko.templateSources.anonymousTemplate ----- |
| 5734 | // Anonymous templates are normally saved/retrieved as DOM nodes through "nodes". |
| 5735 | // For compatibility, you can also read "text"; it will be serialized from the nodes on demand. |
| 5736 | // Writing to "text" is still supported, but then the template data will not be available as DOM nodes. |
| 5737 | |
| 5738 | ko.templateSources.anonymousTemplate = function(element) { |
| 5739 | this.domElement = element; |
| 5740 | } |
| 5741 | ko.templateSources.anonymousTemplate.prototype = new ko.templateSources.domElement(); |
| 5742 | ko.templateSources.anonymousTemplate.prototype.constructor = ko.templateSources.anonymousTemplate; |
| 5743 | ko.templateSources.anonymousTemplate.prototype['text'] = function(/* valueToWrite */) { |
| 5744 | if (arguments.length == 0) { |
| 5745 | var templateData = getTemplateDomData(this.domElement); |
| 5746 | if (templateData.textData === undefined && templateData.containerData) |
| 5747 | templateData.textData = templateData.containerData.innerHTML; |
| 5748 | return templateData.textData; |
| 5749 | } else { |
| 5750 | var valueToWrite = arguments[0]; |
| 5751 | setTemplateDomData(this.domElement, {textData: valueToWrite}); |
| 5752 | } |
| 5753 | }; |
| 5754 | |
| 5755 | ko.exportSymbol('templateSources', ko.templateSources); |
| 5756 | ko.exportSymbol('templateSources.domElement', ko.templateSources.domElement); |
| 5757 | ko.exportSymbol('templateSources.anonymousTemplate', ko.templateSources.anonymousTemplate); |
| 5758 | })(); |
| 5759 | (function () { |
| 5760 | var _templateEngine; |
| 5761 | ko.setTemplateEngine = function (templateEngine) { |
| 5762 | if ((templateEngine != undefined) && !(templateEngine instanceof ko.templateEngine)) |
| 5763 | throw new Error("templateEngine must inherit from ko.templateEngine"); |
| 5764 | _templateEngine = templateEngine; |
| 5765 | } |
| 5766 | |
| 5767 | function invokeForEachNodeInContinuousRange(firstNode, lastNode, action) { |
| 5768 | var node, nextInQueue = firstNode, firstOutOfRangeNode = ko.virtualElements.nextSibling(lastNode); |
| 5769 | while (nextInQueue && ((node = nextInQueue) !== firstOutOfRangeNode)) { |
| 5770 | nextInQueue = ko.virtualElements.nextSibling(node); |
| 5771 | action(node, nextInQueue); |
| 5772 | } |
| 5773 | } |
| 5774 | |
| 5775 | function activateBindingsOnContinuousNodeArray(continuousNodeArray, bindingContext) { |
| 5776 | // To be used on any nodes that have been rendered by a template and have been inserted into some parent element |
| 5777 | // Walks through continuousNodeArray (which *must* be continuous, i.e., an uninterrupted sequence of sibling nodes, because |
| 5778 | // the algorithm for walking them relies on this), and for each top-level item in the virtual-element sense, |
| 5779 | // (1) Does a regular "applyBindings" to associate bindingContext with this node and to activate any non-memoized bindings |
| 5780 | // (2) Unmemoizes any memos in the DOM subtree (e.g., to activate bindings that had been memoized during template rewriting) |
| 5781 | |
| 5782 | if (continuousNodeArray.length) { |
| 5783 | var firstNode = continuousNodeArray[0], |
| 5784 | lastNode = continuousNodeArray[continuousNodeArray.length - 1], |
| 5785 | parentNode = firstNode.parentNode, |
| 5786 | provider = ko.bindingProvider['instance'], |
| 5787 | preprocessNode = provider['preprocessNode']; |
| 5788 | |
| 5789 | if (preprocessNode) { |
| 5790 | invokeForEachNodeInContinuousRange(firstNode, lastNode, function(node, nextNodeInRange) { |
| 5791 | var nodePreviousSibling = node.previousSibling; |
| 5792 | var newNodes = preprocessNode.call(provider, node); |
| 5793 | if (newNodes) { |
| 5794 | if (node === firstNode) |
| 5795 | firstNode = newNodes[0] || nextNodeInRange; |
| 5796 | if (node === lastNode) |
| 5797 | lastNode = newNodes[newNodes.length - 1] || nodePreviousSibling; |
| 5798 | } |
| 5799 | }); |
| 5800 | |
| 5801 | // Because preprocessNode can change the nodes, including the first and last nodes, update continuousNodeArray to match. |
| 5802 | // We need the full set, including inner nodes, because the unmemoize step might remove the first node (and so the real |
| 5803 | // first node needs to be in the array). |
| 5804 | continuousNodeArray.length = 0; |
| 5805 | if (!firstNode) { // preprocessNode might have removed all the nodes, in which case there's nothing left to do |
| 5806 | return; |
| 5807 | } |
| 5808 | if (firstNode === lastNode) { |
| 5809 | continuousNodeArray.push(firstNode); |
| 5810 | } else { |
| 5811 | continuousNodeArray.push(firstNode, lastNode); |
| 5812 | ko.utils.fixUpContinuousNodeArray(continuousNodeArray, parentNode); |
| 5813 | } |
| 5814 | } |
| 5815 | |
| 5816 | // Need to applyBindings *before* unmemoziation, because unmemoization might introduce extra nodes (that we don't want to re-bind) |
| 5817 | // whereas a regular applyBindings won't introduce new memoized nodes |
| 5818 | invokeForEachNodeInContinuousRange(firstNode, lastNode, function(node) { |
| 5819 | if (node.nodeType === 1 || node.nodeType === 8) |
| 5820 | ko.applyBindings(bindingContext, node); |
| 5821 | }); |
| 5822 | invokeForEachNodeInContinuousRange(firstNode, lastNode, function(node) { |
| 5823 | if (node.nodeType === 1 || node.nodeType === 8) |
| 5824 | ko.memoization.unmemoizeDomNodeAndDescendants(node, [bindingContext]); |
| 5825 | }); |
| 5826 | |
| 5827 | // Make sure any changes done by applyBindings or unmemoize are reflected in the array |
| 5828 | ko.utils.fixUpContinuousNodeArray(continuousNodeArray, parentNode); |
| 5829 | } |
| 5830 | } |
| 5831 | |
| 5832 | function getFirstNodeFromPossibleArray(nodeOrNodeArray) { |
| 5833 | return nodeOrNodeArray.nodeType ? nodeOrNodeArray |
| 5834 | : nodeOrNodeArray.length > 0 ? nodeOrNodeArray[0] |
| 5835 | : null; |
| 5836 | } |
| 5837 | |
| 5838 | function executeTemplate(targetNodeOrNodeArray, renderMode, template, bindingContext, options) { |
| 5839 | options = options || {}; |
| 5840 | var firstTargetNode = targetNodeOrNodeArray && getFirstNodeFromPossibleArray(targetNodeOrNodeArray); |
| 5841 | var templateDocument = (firstTargetNode || template || {}).ownerDocument; |
| 5842 | var templateEngineToUse = (options['templateEngine'] || _templateEngine); |
| 5843 | ko.templateRewriting.ensureTemplateIsRewritten(template, templateEngineToUse, templateDocument); |
| 5844 | var renderedNodesArray = templateEngineToUse['renderTemplate'](template, bindingContext, options, templateDocument); |
| 5845 | |
| 5846 | // Loosely check result is an array of DOM nodes |
| 5847 | if ((typeof renderedNodesArray.length != "number") || (renderedNodesArray.length > 0 && typeof renderedNodesArray[0].nodeType != "number")) |
| 5848 | throw new Error("Template engine must return an array of DOM nodes"); |
| 5849 | |
| 5850 | var haveAddedNodesToParent = false; |
| 5851 | switch (renderMode) { |
| 5852 | case "replaceChildren": |
| 5853 | ko.virtualElements.setDomNodeChildren(targetNodeOrNodeArray, renderedNodesArray); |
| 5854 | haveAddedNodesToParent = true; |
| 5855 | break; |
| 5856 | case "replaceNode": |
| 5857 | ko.utils.replaceDomNodes(targetNodeOrNodeArray, renderedNodesArray); |
| 5858 | haveAddedNodesToParent = true; |
| 5859 | break; |
| 5860 | case "ignoreTargetNode": break; |
| 5861 | default: |
| 5862 | throw new Error("Unknown renderMode: " + renderMode); |
| 5863 | } |
| 5864 | |
| 5865 | if (haveAddedNodesToParent) { |
| 5866 | activateBindingsOnContinuousNodeArray(renderedNodesArray, bindingContext); |
| 5867 | if (options['afterRender']) { |
| 5868 | ko.dependencyDetection.ignore(options['afterRender'], null, [renderedNodesArray, bindingContext[options['as'] || '$data']]); |
| 5869 | } |
| 5870 | if (renderMode == "replaceChildren") { |
| 5871 | ko.bindingEvent.notify(targetNodeOrNodeArray, ko.bindingEvent.childrenComplete); |
| 5872 | } |
| 5873 | } |
| 5874 | |
| 5875 | return renderedNodesArray; |
| 5876 | } |
| 5877 | |
| 5878 | function resolveTemplateName(template, data, context) { |
| 5879 | // The template can be specified as: |
| 5880 | if (ko.isObservable(template)) { |
| 5881 | // 1. An observable, with string value |
| 5882 | return template(); |
| 5883 | } else if (typeof template === 'function') { |
| 5884 | // 2. A function of (data, context) returning a string |
| 5885 | return template(data, context); |
| 5886 | } else { |
| 5887 | // 3. A string |
| 5888 | return template; |
| 5889 | } |
| 5890 | } |
| 5891 | |
| 5892 | ko.renderTemplate = function (template, dataOrBindingContext, options, targetNodeOrNodeArray, renderMode) { |
| 5893 | options = options || {}; |
| 5894 | if ((options['templateEngine'] || _templateEngine) == undefined) |
| 5895 | throw new Error("Set a template engine before calling renderTemplate"); |
| 5896 | renderMode = renderMode || "replaceChildren"; |
| 5897 | |
| 5898 | if (targetNodeOrNodeArray) { |
| 5899 | var firstTargetNode = getFirstNodeFromPossibleArray(targetNodeOrNodeArray); |
| 5900 | |
| 5901 | var whenToDispose = function () { return (!firstTargetNode) || !ko.utils.domNodeIsAttachedToDocument(firstTargetNode); }; // Passive disposal (on next evaluation) |
| 5902 | var activelyDisposeWhenNodeIsRemoved = (firstTargetNode && renderMode == "replaceNode") ? firstTargetNode.parentNode : firstTargetNode; |
| 5903 | |
| 5904 | return ko.dependentObservable( // So the DOM is automatically updated when any dependency changes |
| 5905 | function () { |
| 5906 | // Ensure we've got a proper binding context to work with |
| 5907 | var bindingContext = (dataOrBindingContext && (dataOrBindingContext instanceof ko.bindingContext)) |
| 5908 | ? dataOrBindingContext |
| 5909 | : new ko.bindingContext(dataOrBindingContext, null, null, null, { "exportDependencies": true }); |
| 5910 | |
| 5911 | var templateName = resolveTemplateName(template, bindingContext['$data'], bindingContext), |
| 5912 | renderedNodesArray = executeTemplate(targetNodeOrNodeArray, renderMode, templateName, bindingContext, options); |
| 5913 | |
| 5914 | if (renderMode == "replaceNode") { |
| 5915 | targetNodeOrNodeArray = renderedNodesArray; |
| 5916 | firstTargetNode = getFirstNodeFromPossibleArray(targetNodeOrNodeArray); |
| 5917 | } |
| 5918 | }, |
| 5919 | null, |
| 5920 | { disposeWhen: whenToDispose, disposeWhenNodeIsRemoved: activelyDisposeWhenNodeIsRemoved } |
| 5921 | ); |
| 5922 | } else { |
| 5923 | // We don't yet have a DOM node to evaluate, so use a memo and render the template later when there is a DOM node |
| 5924 | return ko.memoization.memoize(function (domNode) { |
| 5925 | ko.renderTemplate(template, dataOrBindingContext, options, domNode, "replaceNode"); |
| 5926 | }); |
| 5927 | } |
| 5928 | }; |
| 5929 | |
| 5930 | ko.renderTemplateForEach = function (template, arrayOrObservableArray, options, targetNode, parentBindingContext) { |
| 5931 | // Since setDomNodeChildrenFromArrayMapping always calls executeTemplateForArrayItem and then |
| 5932 | // activateBindingsCallback for added items, we can store the binding context in the former to use in the latter. |
| 5933 | var arrayItemContext, asName = options['as']; |
| 5934 | |
| 5935 | // This will be called by setDomNodeChildrenFromArrayMapping to get the nodes to add to targetNode |
| 5936 | var executeTemplateForArrayItem = function (arrayValue, index) { |
| 5937 | // Support selecting template as a function of the data being rendered |
| 5938 | arrayItemContext = parentBindingContext['createChildContext'](arrayValue, { |
| 5939 | 'as': asName, |
| 5940 | 'noChildContext': options['noChildContext'], |
| 5941 | 'extend': function(context) { |
| 5942 | context['$index'] = index; |
| 5943 | if (asName) { |
| 5944 | context[asName + "Index"] = index; |
| 5945 | } |
| 5946 | } |
| 5947 | }); |
| 5948 | |
| 5949 | var templateName = resolveTemplateName(template, arrayValue, arrayItemContext); |
| 5950 | return executeTemplate(targetNode, "ignoreTargetNode", templateName, arrayItemContext, options); |
| 5951 | }; |
| 5952 | |
| 5953 | // This will be called whenever setDomNodeChildrenFromArrayMapping has added nodes to targetNode |
| 5954 | var activateBindingsCallback = function(arrayValue, addedNodesArray, index) { |
| 5955 | activateBindingsOnContinuousNodeArray(addedNodesArray, arrayItemContext); |
| 5956 | if (options['afterRender']) |
| 5957 | options['afterRender'](addedNodesArray, arrayValue); |
| 5958 | |
| 5959 | // release the "cache" variable, so that it can be collected by |
| 5960 | // the GC when its value isn't used from within the bindings anymore. |
| 5961 | arrayItemContext = null; |
| 5962 | }; |
| 5963 | |
| 5964 | var setDomNodeChildrenFromArrayMapping = function (newArray, changeList) { |
| 5965 | // Call setDomNodeChildrenFromArrayMapping, ignoring any observables unwrapped within (most likely from a callback function). |
| 5966 | // If the array items are observables, though, they will be unwrapped in executeTemplateForArrayItem and managed within setDomNodeChildrenFromArrayMapping. |
| 5967 | ko.dependencyDetection.ignore(ko.utils.setDomNodeChildrenFromArrayMapping, null, [targetNode, newArray, executeTemplateForArrayItem, options, activateBindingsCallback, changeList]); |
| 5968 | ko.bindingEvent.notify(targetNode, ko.bindingEvent.childrenComplete); |
| 5969 | }; |
| 5970 | |
| 5971 | var shouldHideDestroyed = (options['includeDestroyed'] === false) || (ko.options['foreachHidesDestroyed'] && !options['includeDestroyed']); |
| 5972 | |
| 5973 | if (!shouldHideDestroyed && !options['beforeRemove'] && ko.isObservableArray(arrayOrObservableArray)) { |
| 5974 | setDomNodeChildrenFromArrayMapping(arrayOrObservableArray.peek()); |
| 5975 | |
| 5976 | var subscription = arrayOrObservableArray.subscribe(function (changeList) { |
| 5977 | setDomNodeChildrenFromArrayMapping(arrayOrObservableArray(), changeList); |
| 5978 | }, null, "arrayChange"); |
| 5979 | subscription.disposeWhenNodeIsRemoved(targetNode); |
| 5980 | |
| 5981 | return subscription; |
| 5982 | } else { |
| 5983 | return ko.dependentObservable(function () { |
| 5984 | var unwrappedArray = ko.utils.unwrapObservable(arrayOrObservableArray) || []; |
| 5985 | if (typeof unwrappedArray.length == "undefined") // Coerce single value into array |
| 5986 | unwrappedArray = [unwrappedArray]; |
| 5987 | |
| 5988 | if (shouldHideDestroyed) { |
| 5989 | // Filter out any entries marked as destroyed |
| 5990 | unwrappedArray = ko.utils.arrayFilter(unwrappedArray, function(item) { |
| 5991 | return item === undefined || item === null || !ko.utils.unwrapObservable(item['_destroy']); |
| 5992 | }); |
| 5993 | } |
| 5994 | setDomNodeChildrenFromArrayMapping(unwrappedArray); |
| 5995 | |
| 5996 | }, null, { disposeWhenNodeIsRemoved: targetNode }); |
| 5997 | } |
| 5998 | }; |
| 5999 | |
| 6000 | var templateComputedDomDataKey = ko.utils.domData.nextKey(); |
| 6001 | function disposeOldComputedAndStoreNewOne(element, newComputed) { |
| 6002 | var oldComputed = ko.utils.domData.get(element, templateComputedDomDataKey); |
| 6003 | if (oldComputed && (typeof(oldComputed.dispose) == 'function')) |
| 6004 | oldComputed.dispose(); |
| 6005 | ko.utils.domData.set(element, templateComputedDomDataKey, (newComputed && (!newComputed.isActive || newComputed.isActive())) ? newComputed : undefined); |
| 6006 | } |
| 6007 | |
| 6008 | var cleanContainerDomDataKey = ko.utils.domData.nextKey(); |
| 6009 | ko.bindingHandlers['template'] = { |
| 6010 | 'init': function(element, valueAccessor) { |
| 6011 | // Support anonymous templates |
| 6012 | var bindingValue = ko.utils.unwrapObservable(valueAccessor()); |
| 6013 | if (typeof bindingValue == "string" || bindingValue['name']) { |
| 6014 | // It's a named template - clear the element |
| 6015 | ko.virtualElements.emptyNode(element); |
| 6016 | } else if ('nodes' in bindingValue) { |
| 6017 | // We've been given an array of DOM nodes. Save them as the template source. |
| 6018 | // There is no known use case for the node array being an observable array (if the output |
| 6019 | // varies, put that behavior *into* your template - that's what templates are for), and |
| 6020 | // the implementation would be a mess, so assert that it's not observable. |
| 6021 | var nodes = bindingValue['nodes'] || []; |
| 6022 | if (ko.isObservable(nodes)) { |
| 6023 | throw new Error('The "nodes" option must be a plain, non-observable array.'); |
| 6024 | } |
| 6025 | |
| 6026 | // If the nodes are already attached to a KO-generated container, we reuse that container without moving the |
| 6027 | // elements to a new one (we check only the first node, as the nodes are always moved together) |
| 6028 | var container = nodes[0] && nodes[0].parentNode; |
| 6029 | if (!container || !ko.utils.domData.get(container, cleanContainerDomDataKey)) { |
| 6030 | container = ko.utils.moveCleanedNodesToContainerElement(nodes); |
| 6031 | ko.utils.domData.set(container, cleanContainerDomDataKey, true); |
| 6032 | } |
| 6033 | |
| 6034 | new ko.templateSources.anonymousTemplate(element)['nodes'](container); |
| 6035 | } else { |
| 6036 | // It's an anonymous template - store the element contents, then clear the element |
| 6037 | var templateNodes = ko.virtualElements.childNodes(element); |
| 6038 | if (templateNodes.length > 0) { |
| 6039 | var container = ko.utils.moveCleanedNodesToContainerElement(templateNodes); // This also removes the nodes from their current parent |
| 6040 | new ko.templateSources.anonymousTemplate(element)['nodes'](container); |
| 6041 | } else { |
| 6042 | throw new Error("Anonymous template defined, but no template content was provided"); |
| 6043 | } |
| 6044 | } |
| 6045 | return { 'controlsDescendantBindings': true }; |
| 6046 | }, |
| 6047 | 'update': function (element, valueAccessor, allBindings, viewModel, bindingContext) { |
| 6048 | var value = valueAccessor(), |
| 6049 | options = ko.utils.unwrapObservable(value), |
| 6050 | shouldDisplay = true, |
| 6051 | templateComputed = null, |
| 6052 | templateName; |
| 6053 | |
| 6054 | if (typeof options == "string") { |
| 6055 | templateName = value; |
| 6056 | options = {}; |
| 6057 | } else { |
| 6058 | templateName = options['name']; |
| 6059 | |
| 6060 | // Support "if"/"ifnot" conditions |
| 6061 | if ('if' in options) |
| 6062 | shouldDisplay = ko.utils.unwrapObservable(options['if']); |
| 6063 | if (shouldDisplay && 'ifnot' in options) |
| 6064 | shouldDisplay = !ko.utils.unwrapObservable(options['ifnot']); |
| 6065 | } |
| 6066 | |
| 6067 | if ('foreach' in options) { |
| 6068 | // Render once for each data point (treating data set as empty if shouldDisplay==false) |
| 6069 | var dataArray = (shouldDisplay && options['foreach']) || []; |
| 6070 | templateComputed = ko.renderTemplateForEach(templateName || element, dataArray, options, element, bindingContext); |
| 6071 | } else if (!shouldDisplay) { |
| 6072 | ko.virtualElements.emptyNode(element); |
| 6073 | } else { |
| 6074 | // Render once for this single data point (or use the viewModel if no data was provided) |
| 6075 | var innerBindingContext = bindingContext; |
| 6076 | if ('data' in options) { |
| 6077 | innerBindingContext = bindingContext['createChildContext'](options['data'], { |
| 6078 | 'as': options['as'], |
| 6079 | 'noChildContext': options['noChildContext'], |
| 6080 | 'exportDependencies': true |
| 6081 | }); |
| 6082 | } |
| 6083 | templateComputed = ko.renderTemplate(templateName || element, innerBindingContext, options, element); |
| 6084 | } |
| 6085 | |
| 6086 | // It only makes sense to have a single template computed per element (otherwise which one should have its output displayed?) |
| 6087 | disposeOldComputedAndStoreNewOne(element, templateComputed); |
| 6088 | } |
| 6089 | }; |
| 6090 | |
| 6091 | // Anonymous templates can't be rewritten. Give a nice error message if you try to do it. |
| 6092 | ko.expressionRewriting.bindingRewriteValidators['template'] = function(bindingValue) { |
| 6093 | var parsedBindingValue = ko.expressionRewriting.parseObjectLiteral(bindingValue); |
| 6094 | |
| 6095 | if ((parsedBindingValue.length == 1) && parsedBindingValue[0]['unknown']) |
| 6096 | return null; // It looks like a string literal, not an object literal, so treat it as a named template (which is allowed for rewriting) |
| 6097 | |
| 6098 | if (ko.expressionRewriting.keyValueArrayContainsKey(parsedBindingValue, "name")) |
| 6099 | return null; // Named templates can be rewritten, so return "no error" |
| 6100 | return "This template engine does not support anonymous templates nested within its templates"; |
| 6101 | }; |
| 6102 | |
| 6103 | ko.virtualElements.allowedBindings['template'] = true; |
| 6104 | })(); |
| 6105 | |
| 6106 | ko.exportSymbol('setTemplateEngine', ko.setTemplateEngine); |
| 6107 | ko.exportSymbol('renderTemplate', ko.renderTemplate); |
| 6108 | // Go through the items that have been added and deleted and try to find matches between them. |
| 6109 | ko.utils.findMovesInArrayComparison = function (left, right, limitFailedCompares) { |
| 6110 | if (left.length && right.length) { |
| 6111 | var failedCompares, l, r, leftItem, rightItem; |
| 6112 | for (failedCompares = l = 0; (!limitFailedCompares || failedCompares < limitFailedCompares) && (leftItem = left[l]); ++l) { |
| 6113 | for (r = 0; rightItem = right[r]; ++r) { |
| 6114 | if (leftItem['value'] === rightItem['value']) { |
| 6115 | leftItem['moved'] = rightItem['index']; |
| 6116 | rightItem['moved'] = leftItem['index']; |
| 6117 | right.splice(r, 1); // This item is marked as moved; so remove it from right list |
| 6118 | failedCompares = r = 0; // Reset failed compares count because we're checking for consecutive failures |
| 6119 | break; |
| 6120 | } |
| 6121 | } |
| 6122 | failedCompares += r; |
| 6123 | } |
| 6124 | } |
| 6125 | }; |
| 6126 | |
| 6127 | ko.utils.compareArrays = (function () { |
| 6128 | var statusNotInOld = 'added', statusNotInNew = 'deleted'; |
| 6129 | |
| 6130 | // Simple calculation based on Levenshtein distance. |
| 6131 | function compareArrays(oldArray, newArray, options) { |
| 6132 | // For backward compatibility, if the third arg is actually a bool, interpret |
| 6133 | // it as the old parameter 'dontLimitMoves'. Newer code should use { dontLimitMoves: true }. |
| 6134 | options = (typeof options === 'boolean') ? { 'dontLimitMoves': options } : (options || {}); |
| 6135 | oldArray = oldArray || []; |
| 6136 | newArray = newArray || []; |
| 6137 | |
| 6138 | if (oldArray.length < newArray.length) |
| 6139 | return compareSmallArrayToBigArray(oldArray, newArray, statusNotInOld, statusNotInNew, options); |
| 6140 | else |
| 6141 | return compareSmallArrayToBigArray(newArray, oldArray, statusNotInNew, statusNotInOld, options); |
| 6142 | } |
| 6143 | |
| 6144 | function compareSmallArrayToBigArray(smlArray, bigArray, statusNotInSml, statusNotInBig, options) { |
| 6145 | var myMin = Math.min, |
| 6146 | myMax = Math.max, |
| 6147 | editDistanceMatrix = [], |
| 6148 | smlIndex, smlIndexMax = smlArray.length, |
| 6149 | bigIndex, bigIndexMax = bigArray.length, |
| 6150 | compareRange = (bigIndexMax - smlIndexMax) || 1, |
| 6151 | maxDistance = smlIndexMax + bigIndexMax + 1, |
| 6152 | thisRow, lastRow, |
| 6153 | bigIndexMaxForRow, bigIndexMinForRow; |
| 6154 | |
| 6155 | for (smlIndex = 0; smlIndex <= smlIndexMax; smlIndex++) { |
| 6156 | lastRow = thisRow; |
| 6157 | editDistanceMatrix.push(thisRow = []); |
| 6158 | bigIndexMaxForRow = myMin(bigIndexMax, smlIndex + compareRange); |
| 6159 | bigIndexMinForRow = myMax(0, smlIndex - 1); |
| 6160 | for (bigIndex = bigIndexMinForRow; bigIndex <= bigIndexMaxForRow; bigIndex++) { |
| 6161 | if (!bigIndex) |
| 6162 | thisRow[bigIndex] = smlIndex + 1; |
| 6163 | else if (!smlIndex) // Top row - transform empty array into new array via additions |
| 6164 | thisRow[bigIndex] = bigIndex + 1; |
| 6165 | else if (smlArray[smlIndex - 1] === bigArray[bigIndex - 1]) |
| 6166 | thisRow[bigIndex] = lastRow[bigIndex - 1]; // copy value (no edit) |
| 6167 | else { |
| 6168 | var northDistance = lastRow[bigIndex] || maxDistance; // not in big (deletion) |
| 6169 | var westDistance = thisRow[bigIndex - 1] || maxDistance; // not in small (addition) |
| 6170 | thisRow[bigIndex] = myMin(northDistance, westDistance) + 1; |
| 6171 | } |
| 6172 | } |
| 6173 | } |
| 6174 | |
| 6175 | var editScript = [], meMinusOne, notInSml = [], notInBig = []; |
| 6176 | for (smlIndex = smlIndexMax, bigIndex = bigIndexMax; smlIndex || bigIndex;) { |
| 6177 | meMinusOne = editDistanceMatrix[smlIndex][bigIndex] - 1; |
| 6178 | if (bigIndex && meMinusOne === editDistanceMatrix[smlIndex][bigIndex-1]) { |
| 6179 | notInSml.push(editScript[editScript.length] = { // added |
| 6180 | 'status': statusNotInSml, |
| 6181 | 'value': bigArray[--bigIndex], |
| 6182 | 'index': bigIndex }); |
| 6183 | } else if (smlIndex && meMinusOne === editDistanceMatrix[smlIndex - 1][bigIndex]) { |
| 6184 | notInBig.push(editScript[editScript.length] = { // deleted |
| 6185 | 'status': statusNotInBig, |
| 6186 | 'value': smlArray[--smlIndex], |
| 6187 | 'index': smlIndex }); |
| 6188 | } else { |
| 6189 | --bigIndex; |
| 6190 | --smlIndex; |
| 6191 | if (!options['sparse']) { |
| 6192 | editScript.push({ |
| 6193 | 'status': "retained", |
| 6194 | 'value': bigArray[bigIndex] }); |
| 6195 | } |
| 6196 | } |
| 6197 | } |
| 6198 | |
| 6199 | // Set a limit on the number of consecutive non-matching comparisons; having it a multiple of |
| 6200 | // smlIndexMax keeps the time complexity of this algorithm linear. |
| 6201 | ko.utils.findMovesInArrayComparison(notInBig, notInSml, !options['dontLimitMoves'] && smlIndexMax * 10); |
| 6202 | |
| 6203 | return editScript.reverse(); |
| 6204 | } |
| 6205 | |
| 6206 | return compareArrays; |
| 6207 | })(); |
| 6208 | |
| 6209 | ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays); |
| 6210 | (function () { |
| 6211 | // Objective: |
| 6212 | // * Given an input array, a container DOM node, and a function from array elements to arrays of DOM nodes, |
| 6213 | // map the array elements to arrays of DOM nodes, concatenate together all these arrays, and use them to populate the container DOM node |
| 6214 | // * Next time we're given the same combination of things (with the array possibly having mutated), update the container DOM node |
| 6215 | // so that its children is again the concatenation of the mappings of the array elements, but don't re-map any array elements that we |
| 6216 | // previously mapped - retain those nodes, and just insert/delete other ones |
| 6217 | |
| 6218 | // "callbackAfterAddingNodes" will be invoked after any "mapping"-generated nodes are inserted into the container node |
| 6219 | // You can use this, for example, to activate bindings on those nodes. |
| 6220 | |
| 6221 | function mapNodeAndRefreshWhenChanged(containerNode, mapping, valueToMap, callbackAfterAddingNodes, index) { |
| 6222 | // Map this array value inside a dependentObservable so we re-map when any dependency changes |
| 6223 | var mappedNodes = []; |
| 6224 | var dependentObservable = ko.dependentObservable(function() { |
| 6225 | var newMappedNodes = mapping(valueToMap, index, ko.utils.fixUpContinuousNodeArray(mappedNodes, containerNode)) || []; |
| 6226 | |
| 6227 | // On subsequent evaluations, just replace the previously-inserted DOM nodes |
| 6228 | if (mappedNodes.length > 0) { |
| 6229 | ko.utils.replaceDomNodes(mappedNodes, newMappedNodes); |
| 6230 | if (callbackAfterAddingNodes) |
| 6231 | ko.dependencyDetection.ignore(callbackAfterAddingNodes, null, [valueToMap, newMappedNodes, index]); |
| 6232 | } |
| 6233 | |
| 6234 | // Replace the contents of the mappedNodes array, thereby updating the record |
| 6235 | // of which nodes would be deleted if valueToMap was itself later removed |
| 6236 | mappedNodes.length = 0; |
| 6237 | ko.utils.arrayPushAll(mappedNodes, newMappedNodes); |
| 6238 | }, null, { disposeWhenNodeIsRemoved: containerNode, disposeWhen: function() { return !ko.utils.anyDomNodeIsAttachedToDocument(mappedNodes); } }); |
| 6239 | return { mappedNodes : mappedNodes, dependentObservable : (dependentObservable.isActive() ? dependentObservable : undefined) }; |
| 6240 | } |
| 6241 | |
| 6242 | var lastMappingResultDomDataKey = ko.utils.domData.nextKey(), |
| 6243 | deletedItemDummyValue = ko.utils.domData.nextKey(); |
| 6244 | |
| 6245 | ko.utils.setDomNodeChildrenFromArrayMapping = function (domNode, array, mapping, options, callbackAfterAddingNodes, editScript) { |
| 6246 | array = array || []; |
| 6247 | if (typeof array.length == "undefined") // Coerce single value into array |
| 6248 | array = [array]; |
| 6249 | |
| 6250 | options = options || {}; |
| 6251 | var lastMappingResult = ko.utils.domData.get(domNode, lastMappingResultDomDataKey); |
| 6252 | var isFirstExecution = !lastMappingResult; |
| 6253 | |
| 6254 | // Build the new mapping result |
| 6255 | var newMappingResult = []; |
| 6256 | var lastMappingResultIndex = 0; |
| 6257 | var currentArrayIndex = 0; |
| 6258 | |
| 6259 | var nodesToDelete = []; |
| 6260 | var itemsToMoveFirstIndexes = []; |
| 6261 | var itemsForBeforeRemoveCallbacks = []; |
| 6262 | var itemsForMoveCallbacks = []; |
| 6263 | var itemsForAfterAddCallbacks = []; |
| 6264 | var mapData; |
| 6265 | var countWaitingForRemove = 0; |
| 6266 | |
| 6267 | function itemAdded(value) { |
| 6268 | mapData = { arrayEntry: value, indexObservable: ko.observable(currentArrayIndex++) }; |
| 6269 | newMappingResult.push(mapData); |
| 6270 | if (!isFirstExecution) { |
| 6271 | itemsForAfterAddCallbacks.push(mapData); |
| 6272 | } |
| 6273 | } |
| 6274 | |
| 6275 | function itemMovedOrRetained(oldPosition) { |
| 6276 | mapData = lastMappingResult[oldPosition]; |
| 6277 | if (currentArrayIndex !== mapData.indexObservable.peek()) |
| 6278 | itemsForMoveCallbacks.push(mapData); |
| 6279 | // Since updating the index might change the nodes, do so before calling fixUpContinuousNodeArray |
| 6280 | mapData.indexObservable(currentArrayIndex++); |
| 6281 | ko.utils.fixUpContinuousNodeArray(mapData.mappedNodes, domNode); |
| 6282 | newMappingResult.push(mapData); |
| 6283 | } |
| 6284 | |
| 6285 | function callCallback(callback, items) { |
| 6286 | if (callback) { |
| 6287 | for (var i = 0, n = items.length; i < n; i++) { |
| 6288 | ko.utils.arrayForEach(items[i].mappedNodes, function(node) { |
| 6289 | callback(node, i, items[i].arrayEntry); |
| 6290 | }); |
| 6291 | } |
| 6292 | } |
| 6293 | } |
| 6294 | |
| 6295 | if (isFirstExecution) { |
| 6296 | ko.utils.arrayForEach(array, itemAdded); |
| 6297 | } else { |
| 6298 | if (!editScript || (lastMappingResult && lastMappingResult['_countWaitingForRemove'])) { |
| 6299 | // Compare the provided array against the previous one |
| 6300 | var lastArray = ko.utils.arrayMap(lastMappingResult, function (x) { return x.arrayEntry; }), |
| 6301 | compareOptions = { |
| 6302 | 'dontLimitMoves': options['dontLimitMoves'], |
| 6303 | 'sparse': true |
| 6304 | }; |
| 6305 | editScript = ko.utils.compareArrays(lastArray, array, compareOptions); |
| 6306 | } |
| 6307 | |
| 6308 | for (var i = 0, editScriptItem, movedIndex, itemIndex; editScriptItem = editScript[i]; i++) { |
| 6309 | movedIndex = editScriptItem['moved']; |
| 6310 | itemIndex = editScriptItem['index']; |
| 6311 | switch (editScriptItem['status']) { |
| 6312 | case "deleted": |
| 6313 | while (lastMappingResultIndex < itemIndex) { |
| 6314 | itemMovedOrRetained(lastMappingResultIndex++); |
| 6315 | } |
| 6316 | if (movedIndex === undefined) { |
| 6317 | mapData = lastMappingResult[lastMappingResultIndex]; |
| 6318 | |
| 6319 | // Stop tracking changes to the mapping for these nodes |
| 6320 | if (mapData.dependentObservable) { |
| 6321 | mapData.dependentObservable.dispose(); |
| 6322 | mapData.dependentObservable = undefined; |
| 6323 | } |
| 6324 | |
| 6325 | // Queue these nodes for later removal |
| 6326 | if (ko.utils.fixUpContinuousNodeArray(mapData.mappedNodes, domNode).length) { |
| 6327 | if (options['beforeRemove']) { |
| 6328 | newMappingResult.push(mapData); |
| 6329 | countWaitingForRemove++; |
| 6330 | if (mapData.arrayEntry === deletedItemDummyValue) { |
| 6331 | mapData = null; |
| 6332 | } else { |
| 6333 | itemsForBeforeRemoveCallbacks.push(mapData); |
| 6334 | } |
| 6335 | } |
| 6336 | if (mapData) { |
| 6337 | nodesToDelete.push.apply(nodesToDelete, mapData.mappedNodes); |
| 6338 | } |
| 6339 | } |
| 6340 | } |
| 6341 | lastMappingResultIndex++; |
| 6342 | break; |
| 6343 | |
| 6344 | case "added": |
| 6345 | while (currentArrayIndex < itemIndex) { |
| 6346 | itemMovedOrRetained(lastMappingResultIndex++); |
| 6347 | } |
| 6348 | if (movedIndex !== undefined) { |
| 6349 | itemsToMoveFirstIndexes.push(newMappingResult.length); |
| 6350 | itemMovedOrRetained(movedIndex); |
| 6351 | } else { |
| 6352 | itemAdded(editScriptItem['value']); |
| 6353 | } |
| 6354 | break; |
| 6355 | } |
| 6356 | } |
| 6357 | |
| 6358 | while (currentArrayIndex < array.length) { |
| 6359 | itemMovedOrRetained(lastMappingResultIndex++); |
| 6360 | } |
| 6361 | |
| 6362 | // Record that the current view may still contain deleted items |
| 6363 | // because it means we won't be able to use a provided editScript. |
| 6364 | newMappingResult['_countWaitingForRemove'] = countWaitingForRemove; |
| 6365 | } |
| 6366 | |
| 6367 | // Store a copy of the array items we just considered so we can difference it next time |
| 6368 | ko.utils.domData.set(domNode, lastMappingResultDomDataKey, newMappingResult); |
| 6369 | |
| 6370 | // Call beforeMove first before any changes have been made to the DOM |
| 6371 | callCallback(options['beforeMove'], itemsForMoveCallbacks); |
| 6372 | |
| 6373 | // Next remove nodes for deleted items (or just clean if there's a beforeRemove callback) |
| 6374 | ko.utils.arrayForEach(nodesToDelete, options['beforeRemove'] ? ko.cleanNode : ko.removeNode); |
| 6375 | |
| 6376 | var i, j, nextNodeInDom, lastNode, nodeToInsert, mappedNodes, activeElement; |
| 6377 | |
| 6378 | // Since most browsers remove the focus from an element when it's moved to another location, |
| 6379 | // save the focused element and try to restore it later. |
| 6380 | try { |
| 6381 | activeElement = domNode.ownerDocument.activeElement; |
| 6382 | } catch(e) { |
| 6383 | // IE9 throws if you access activeElement during page load (see issue #703) |
| 6384 | } |
| 6385 | |
| 6386 | // Try to reduce overall moved nodes by first moving the ones that were marked as moved by the edit script |
| 6387 | if (itemsToMoveFirstIndexes.length) { |
| 6388 | while ((i = itemsToMoveFirstIndexes.shift()) != undefined) { |
| 6389 | mapData = newMappingResult[i]; |
| 6390 | for (lastNode = undefined; i; ) { |
| 6391 | if ((mappedNodes = newMappingResult[--i].mappedNodes) && mappedNodes.length) { |
| 6392 | lastNode = mappedNodes[mappedNodes.length-1]; |
| 6393 | break; |
| 6394 | } |
| 6395 | } |
| 6396 | for (j = 0; nodeToInsert = mapData.mappedNodes[j]; lastNode = nodeToInsert, j++) { |
| 6397 | ko.virtualElements.insertAfter(domNode, nodeToInsert, lastNode); |
| 6398 | } |
| 6399 | } |
| 6400 | } |
| 6401 | |
| 6402 | // Next add/reorder the remaining items (will include deleted items if there's a beforeRemove callback) |
| 6403 | for (i = 0, nextNodeInDom = ko.virtualElements.firstChild(domNode); mapData = newMappingResult[i]; i++) { |
| 6404 | // Get nodes for newly added items |
| 6405 | if (!mapData.mappedNodes) |
| 6406 | ko.utils.extend(mapData, mapNodeAndRefreshWhenChanged(domNode, mapping, mapData.arrayEntry, callbackAfterAddingNodes, mapData.indexObservable)); |
| 6407 | |
| 6408 | // Put nodes in the right place if they aren't there already |
| 6409 | for (j = 0; nodeToInsert = mapData.mappedNodes[j]; nextNodeInDom = nodeToInsert.nextSibling, lastNode = nodeToInsert, j++) { |
| 6410 | if (nodeToInsert !== nextNodeInDom) |
| 6411 | ko.virtualElements.insertAfter(domNode, nodeToInsert, lastNode); |
| 6412 | } |
| 6413 | |
| 6414 | // Run the callbacks for newly added nodes (for example, to apply bindings, etc.) |
| 6415 | if (!mapData.initialized && callbackAfterAddingNodes) { |
| 6416 | callbackAfterAddingNodes(mapData.arrayEntry, mapData.mappedNodes, mapData.indexObservable); |
| 6417 | mapData.initialized = true; |
| 6418 | lastNode = mapData.mappedNodes[mapData.mappedNodes.length - 1]; // get the last node again since it may have been changed by a preprocessor |
| 6419 | } |
| 6420 | } |
| 6421 | |
| 6422 | // Restore the focused element if it had lost focus |
| 6423 | if (activeElement && domNode.ownerDocument.activeElement != activeElement) { |
| 6424 | activeElement.focus(); |
| 6425 | } |
| 6426 | |
| 6427 | // If there's a beforeRemove callback, call it after reordering. |
| 6428 | // Note that we assume that the beforeRemove callback will usually be used to remove the nodes using |
| 6429 | // some sort of animation, which is why we first reorder the nodes that will be removed. If the |
| 6430 | // callback instead removes the nodes right away, it would be more efficient to skip reordering them. |
| 6431 | // Perhaps we'll make that change in the future if this scenario becomes more common. |
| 6432 | callCallback(options['beforeRemove'], itemsForBeforeRemoveCallbacks); |
| 6433 | |
| 6434 | // Replace the stored values of deleted items with a dummy value. This provides two benefits: it marks this item |
| 6435 | // as already "removed" so we won't call beforeRemove for it again, and it ensures that the item won't match up |
| 6436 | // with an actual item in the array and appear as "retained" or "moved". |
| 6437 | for (i = 0; i < itemsForBeforeRemoveCallbacks.length; ++i) { |
| 6438 | itemsForBeforeRemoveCallbacks[i].arrayEntry = deletedItemDummyValue; |
| 6439 | } |
| 6440 | |
| 6441 | // Finally call afterMove and afterAdd callbacks |
| 6442 | callCallback(options['afterMove'], itemsForMoveCallbacks); |
| 6443 | callCallback(options['afterAdd'], itemsForAfterAddCallbacks); |
| 6444 | } |
| 6445 | })(); |
| 6446 | |
| 6447 | ko.exportSymbol('utils.setDomNodeChildrenFromArrayMapping', ko.utils.setDomNodeChildrenFromArrayMapping); |
| 6448 | ko.nativeTemplateEngine = function () { |
| 6449 | this['allowTemplateRewriting'] = false; |
| 6450 | } |
| 6451 | |
| 6452 | ko.nativeTemplateEngine.prototype = new ko.templateEngine(); |
| 6453 | ko.nativeTemplateEngine.prototype.constructor = ko.nativeTemplateEngine; |
| 6454 | ko.nativeTemplateEngine.prototype['renderTemplateSource'] = function (templateSource, bindingContext, options, templateDocument) { |
| 6455 | var useNodesIfAvailable = !(ko.utils.ieVersion < 9), // IE<9 cloneNode doesn't work properly |
| 6456 | templateNodesFunc = useNodesIfAvailable ? templateSource['nodes'] : null, |
| 6457 | templateNodes = templateNodesFunc ? templateSource['nodes']() : null; |
| 6458 | |
| 6459 | if (templateNodes) { |
| 6460 | return ko.utils.makeArray(templateNodes.cloneNode(true).childNodes); |
| 6461 | } else { |
| 6462 | var templateText = templateSource['text'](); |
| 6463 | return ko.utils.parseHtmlFragment(templateText, templateDocument); |
| 6464 | } |
| 6465 | }; |
| 6466 | |
| 6467 | ko.nativeTemplateEngine.instance = new ko.nativeTemplateEngine(); |
| 6468 | ko.setTemplateEngine(ko.nativeTemplateEngine.instance); |
| 6469 | |
| 6470 | ko.exportSymbol('nativeTemplateEngine', ko.nativeTemplateEngine); |
| 6471 | (function() { |
| 6472 | ko.jqueryTmplTemplateEngine = function () { |
| 6473 | // Detect which version of jquery-tmpl you're using. Unfortunately jquery-tmpl |
| 6474 | // doesn't expose a version number, so we have to infer it. |
| 6475 | // Note that as of Knockout 1.3, we only support jQuery.tmpl 1.0.0pre and later, |
| 6476 | // which KO internally refers to as version "2", so older versions are no longer detected. |
| 6477 | var jQueryTmplVersion = this.jQueryTmplVersion = (function() { |
| 6478 | if (!jQueryInstance || !(jQueryInstance['tmpl'])) |
| 6479 | return 0; |
| 6480 | // Since it exposes no official version number, we use our own numbering system. To be updated as jquery-tmpl evolves. |
| 6481 | try { |
| 6482 | if (jQueryInstance['tmpl']['tag']['tmpl']['open'].toString().indexOf('__') >= 0) { |
| 6483 | // Since 1.0.0pre, custom tags should append markup to an array called "__" |
| 6484 | return 2; // Final version of jquery.tmpl |
| 6485 | } |
| 6486 | } catch(ex) { /* Apparently not the version we were looking for */ } |
| 6487 | |
| 6488 | return 1; // Any older version that we don't support |
| 6489 | })(); |
| 6490 | |
| 6491 | function ensureHasReferencedJQueryTemplates() { |
| 6492 | if (jQueryTmplVersion < 2) |
| 6493 | throw new Error("Your version of jQuery.tmpl is too old. Please upgrade to jQuery.tmpl 1.0.0pre or later."); |
| 6494 | } |
| 6495 | |
| 6496 | function executeTemplate(compiledTemplate, data, jQueryTemplateOptions) { |
| 6497 | return jQueryInstance['tmpl'](compiledTemplate, data, jQueryTemplateOptions); |
| 6498 | } |
| 6499 | |
| 6500 | this['renderTemplateSource'] = function(templateSource, bindingContext, options, templateDocument) { |
| 6501 | templateDocument = templateDocument || document; |
| 6502 | options = options || {}; |
| 6503 | ensureHasReferencedJQueryTemplates(); |
| 6504 | |
| 6505 | // Ensure we have stored a precompiled version of this template (don't want to reparse on every render) |
| 6506 | var precompiled = templateSource['data']('precompiled'); |
| 6507 | if (!precompiled) { |
| 6508 | var templateText = templateSource['text']() || ""; |
| 6509 | // Wrap in "with($whatever.koBindingContext) { ... }" |
| 6510 | templateText = "{{ko_with $item.koBindingContext}}" + templateText + "{{/ko_with}}"; |
| 6511 | |
| 6512 | precompiled = jQueryInstance['template'](null, templateText); |
| 6513 | templateSource['data']('precompiled', precompiled); |
| 6514 | } |
| 6515 | |
| 6516 | var data = [bindingContext['$data']]; // Prewrap the data in an array to stop jquery.tmpl from trying to unwrap any arrays |
| 6517 | var jQueryTemplateOptions = jQueryInstance['extend']({ 'koBindingContext': bindingContext }, options['templateOptions']); |
| 6518 | |
| 6519 | var resultNodes = executeTemplate(precompiled, data, jQueryTemplateOptions); |
| 6520 | resultNodes['appendTo'](templateDocument.createElement("div")); // Using "appendTo" forces jQuery/jQuery.tmpl to perform necessary cleanup work |
| 6521 | |
| 6522 | jQueryInstance['fragments'] = {}; // Clear jQuery's fragment cache to avoid a memory leak after a large number of template renders |
| 6523 | return resultNodes; |
| 6524 | }; |
| 6525 | |
| 6526 | this['createJavaScriptEvaluatorBlock'] = function(script) { |
| 6527 | return "{{ko_code ((function() { return " + script + " })()) }}"; |
| 6528 | }; |
| 6529 | |
| 6530 | this['addTemplate'] = function(templateName, templateMarkup) { |
| 6531 | document.write("<script type='text/html' id='" + templateName + "'>" + templateMarkup + "<" + "/script>"); |
| 6532 | }; |
| 6533 | |
| 6534 | if (jQueryTmplVersion > 0) { |
| 6535 | jQueryInstance['tmpl']['tag']['ko_code'] = { |
| 6536 | open: "__.push($1 || '');" |
| 6537 | }; |
| 6538 | jQueryInstance['tmpl']['tag']['ko_with'] = { |
| 6539 | open: "with($1) {", |
| 6540 | close: "} " |
| 6541 | }; |
| 6542 | } |
| 6543 | }; |
| 6544 | |
| 6545 | ko.jqueryTmplTemplateEngine.prototype = new ko.templateEngine(); |
| 6546 | ko.jqueryTmplTemplateEngine.prototype.constructor = ko.jqueryTmplTemplateEngine; |
| 6547 | |
| 6548 | // Use this one by default *only if jquery.tmpl is referenced* |
| 6549 | var jqueryTmplTemplateEngineInstance = new ko.jqueryTmplTemplateEngine(); |
| 6550 | if (jqueryTmplTemplateEngineInstance.jQueryTmplVersion > 0) |
| 6551 | ko.setTemplateEngine(jqueryTmplTemplateEngineInstance); |
| 6552 | |
| 6553 | ko.exportSymbol('jqueryTmplTemplateEngine', ko.jqueryTmplTemplateEngine); |
| 6554 | })(); |
| 6555 | })); |
| 6556 | }()); |
| 6557 | })(); |
| 6558 |