331 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			331 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
| /*!
 | |
|   Knockout Fast Foreach v0.4.1 (2015-07-17T14:06:15.974Z)
 | |
|   By: Brian M Hunt (C) 2015
 | |
|   License: MIT
 | |
| 
 | |
|   Adds `fastForEach` to `ko.bindingHandlers`.
 | |
| */
 | |
| (function (root, factory) {
 | |
|   if (typeof define === 'function' && define.amd) {
 | |
|     define(['knockout'], factory);
 | |
|   } else if (typeof exports === 'object') {
 | |
|     module.exports = factory(require('knockout'));
 | |
|   } else {
 | |
|     root.KnockoutFastForeach = factory(root.ko);
 | |
|   }
 | |
| }(this, function (ko) {
 | |
|   "use strict";
 | |
| // index.js
 | |
| // --------
 | |
| // Fast For Each
 | |
| //
 | |
| // Employing sound techniques to make a faster Knockout foreach binding.
 | |
| // --------
 | |
| 
 | |
| //      Utilities
 | |
| 
 | |
| // from https://github.com/jonschlinkert/is-plain-object
 | |
| function isPlainObject(o) {
 | |
|   return !!o && typeof o === 'object' && o.constructor === Object;
 | |
| }
 | |
| 
 | |
| // From knockout/src/virtualElements.js
 | |
| var commentNodesHaveTextProperty = document && document.createComment("test").text === "<!--test-->";
 | |
| var startCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*ko(?:\s+([\s\S]+))?\s*-->$/ : /^\s*ko(?:\s+([\s\S]+))?\s*$/;
 | |
| var supportsDocumentFragment = document && typeof document.createDocumentFragment === "function";
 | |
| function isVirtualNode(node) {
 | |
|   return (node.nodeType === 8) && startCommentRegex.test(commentNodesHaveTextProperty ? node.text : node.nodeValue);
 | |
| }
 | |
| 
 | |
| 
 | |
| // Get a copy of the (possibly virtual) child nodes of the given element,
 | |
| // put them into a container, then empty the given node.
 | |
| function makeTemplateNode(sourceNode) {
 | |
|   var container = document.createElement("div");
 | |
|   var parentNode;
 | |
|   if (sourceNode.content) {
 | |
|     // For e.g. <template> tags
 | |
|     parentNode = sourceNode.content;
 | |
|   } else if (sourceNode.tagName === 'SCRIPT') {
 | |
|     parentNode = document.createElement("div");
 | |
|     parentNode.innerHTML = sourceNode.text;
 | |
|   } else {
 | |
|     // Anything else e.g. <div>
 | |
|     parentNode = sourceNode;
 | |
|   }
 | |
|   ko.utils.arrayForEach(ko.virtualElements.childNodes(parentNode), function (child) {
 | |
|     // FIXME - This cloneNode could be expensive; we may prefer to iterate over the
 | |
|     // parentNode children in reverse (so as not to foul the indexes as childNodes are
 | |
|     // removed from parentNode when inserted into the container)
 | |
|     if (child) {
 | |
|       container.insertBefore(child.cloneNode(true), null);
 | |
|     }
 | |
|   });
 | |
|   return container;
 | |
| }
 | |
| 
 | |
| function insertAllAfter(containerNode, nodeOrNodeArrayToInsert, insertAfterNode) {
 | |
|   var frag, len, i;
 | |
|   // poor man's node and array check, should be enough for this
 | |
|   if (typeof nodeOrNodeArrayToInsert.nodeType !== "undefined" && typeof nodeOrNodeArrayToInsert.length === "undefined") {
 | |
|     throw new Error("Expected a single node or a node array");
 | |
|   }
 | |
| 
 | |
|   if (typeof nodeOrNodeArrayToInsert.nodeType !== "undefined") {
 | |
|     ko.virtualElements.insertAfter(containerNode, nodeOrNodeArrayToInsert, insertAfterNode);
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   if (nodeOrNodeArrayToInsert.length === 1) {
 | |
|     ko.virtualElements.insertAfter(containerNode, nodeOrNodeArrayToInsert[0], insertAfterNode);
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   if (supportsDocumentFragment) {
 | |
|     frag = document.createDocumentFragment();
 | |
| 
 | |
|     for (i = 0, len = nodeOrNodeArrayToInsert.length; i !== len; ++i) {
 | |
|       frag.appendChild(nodeOrNodeArrayToInsert[i]);
 | |
|     }
 | |
|     ko.virtualElements.insertAfter(containerNode, frag, insertAfterNode);
 | |
|   } else {
 | |
|     // Nodes are inserted in reverse order - pushed down immediately after
 | |
|     // the last node for the previous item or as the first node of element.
 | |
|     for (i = nodeOrNodeArrayToInsert.length - 1; i >= 0; --i) {
 | |
|       var child = nodeOrNodeArrayToInsert[i];
 | |
|       if (!child) {
 | |
|         return;
 | |
|       }
 | |
|       ko.virtualElements.insertAfter(containerNode, child, insertAfterNode);
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| // Mimic a KO change item 'add'
 | |
| function valueToChangeAddItem(value, index) {
 | |
|   return {
 | |
|     status: 'added',
 | |
|     value: value,
 | |
|     index: index
 | |
|   };
 | |
| }
 | |
| 
 | |
| function isAdditionAdjacentToLast(changeIndex, arrayChanges) {
 | |
|   return changeIndex > 0 &&
 | |
|     changeIndex < arrayChanges.length &&
 | |
|     arrayChanges[changeIndex].status === "added" &&
 | |
|     arrayChanges[changeIndex - 1].status === "added" &&
 | |
|     arrayChanges[changeIndex - 1].index === arrayChanges[changeIndex].index - 1;
 | |
| }
 | |
| 
 | |
| function FastForEach(spec) {
 | |
|   this.element = spec.element;
 | |
|   this.container = isVirtualNode(this.element) ?
 | |
|                    this.element.parentNode : this.element;
 | |
|   this.$context = spec.$context;
 | |
|   this.data = spec.data;
 | |
|   this.as = spec.as;
 | |
|   this.noContext = spec.noContext;
 | |
|   this.templateNode = makeTemplateNode(
 | |
|     spec.name ? document.getElementById(spec.name).cloneNode(true) : spec.element
 | |
|   );
 | |
|   this.afterQueueFlush = spec.afterQueueFlush;
 | |
|   this.beforeQueueFlush = spec.beforeQueueFlush;
 | |
|   this.changeQueue = [];
 | |
|   this.lastNodesList = [];
 | |
|   this.indexesToDelete = [];
 | |
|   this.rendering_queued = false;
 | |
| 
 | |
|   // Remove existing content.
 | |
|   ko.virtualElements.emptyNode(this.element);
 | |
| 
 | |
|   // Prime content
 | |
|   var primeData = ko.unwrap(this.data);
 | |
|   if (primeData.map) {
 | |
|     this.onArrayChange(primeData.map(valueToChangeAddItem));
 | |
|   }
 | |
| 
 | |
|   // Watch for changes
 | |
|   if (ko.isObservable(this.data)) {
 | |
|     if (!this.data.indexOf) {
 | |
|       // Make sure the observable is trackable.
 | |
|       this.data = this.data.extend({trackArrayChanges: true});
 | |
|     }
 | |
|     this.changeSubs = this.data.subscribe(this.onArrayChange, this, 'arrayChange');
 | |
|   }
 | |
| }
 | |
| 
 | |
| 
 | |
| FastForEach.animateFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame ||
 | |
|   window.mozRequestAnimationFrame || window.msRequestAnimationFrame ||
 | |
|   function(cb) { return window.setTimeout(cb, 1000 / 60); };
 | |
| 
 | |
| 
 | |
| FastForEach.prototype.dispose = function () {
 | |
|   if (this.changeSubs) {
 | |
|     this.changeSubs.dispose();
 | |
|   }
 | |
| };
 | |
| 
 | |
| 
 | |
| // If the array changes we register the change.
 | |
| FastForEach.prototype.onArrayChange = function (changeSet) {
 | |
|   var self = this;
 | |
|   var changeMap = {
 | |
|     added: [],
 | |
|     deleted: []
 | |
|   };
 | |
|   for (var i = 0, len = changeSet.length; i < len; i++) {
 | |
|     // the change is appended to a last change info object when both are 'added' and have indexes next to each other
 | |
|     // here I presume that ko is sending changes in monotonic order (in index variable) which happens to be true, tested with push and splice with multiple pushed values
 | |
|     if (isAdditionAdjacentToLast(i, changeSet)) {
 | |
|       var batchValues = changeMap.added[changeMap.added.length - 1].values;
 | |
|       if (!batchValues) {
 | |
|         // transform the last addition into a batch addition object
 | |
|         var lastAddition = changeMap.added.pop();
 | |
|         var batchAddition = {
 | |
|           isBatch: true,
 | |
|           status: 'added',
 | |
|           index: lastAddition.index,
 | |
|           values: [lastAddition.value]
 | |
|         };
 | |
|         batchValues = batchAddition.values;
 | |
|         changeMap.added.push(batchAddition);
 | |
|       }
 | |
|       batchValues.push(changeSet[i].value);
 | |
|     } else {
 | |
|       changeMap[changeSet[i].status].push(changeSet[i]);
 | |
|     }
 | |
|   }
 | |
|   if (changeMap.deleted.length > 0) {
 | |
|     this.changeQueue.push.apply(this.changeQueue, changeMap.deleted);
 | |
|     this.changeQueue.push({status: 'clearDeletedIndexes'});
 | |
|   }
 | |
|   this.changeQueue.push.apply(this.changeQueue, changeMap.added);
 | |
|   // Once a change is registered, the ticking count-down starts for the processQueue.
 | |
|   if (this.changeQueue.length > 0 && !this.rendering_queued) {
 | |
|     this.rendering_queued = true;
 | |
|     FastForEach.animateFrame.call(window, function () { self.processQueue(); });
 | |
|   }
 | |
| };
 | |
| 
 | |
| 
 | |
| // Reflect all the changes in the queue in the DOM, then wipe the queue.
 | |
| FastForEach.prototype.processQueue = function () {
 | |
|   var self = this;
 | |
| 
 | |
|   // Callback so folks can do things before the queue flush.
 | |
|   if (typeof this.beforeQueueFlush === 'function') {
 | |
|     this.beforeQueueFlush(this.changeQueue);
 | |
|   }
 | |
| 
 | |
|   ko.utils.arrayForEach(this.changeQueue, function (changeItem) {
 | |
|     // console.log(self.data(), "CI", JSON.stringify(changeItem, null, 2), JSON.stringify($(self.element).text()))
 | |
|     self[changeItem.status](changeItem);
 | |
|     // console.log("  ==> ", JSON.stringify($(self.element).text()))
 | |
|   });
 | |
|   this.rendering_queued = false;
 | |
|   // Callback so folks can do things.
 | |
|   if (typeof this.afterQueueFlush === 'function') {
 | |
|     this.afterQueueFlush(this.changeQueue);
 | |
|   }
 | |
|   this.changeQueue = [];
 | |
| };
 | |
| 
 | |
| 
 | |
| // Process a changeItem with {status: 'added', ...}
 | |
| FastForEach.prototype.added = function (changeItem) {
 | |
|   var index = changeItem.index;
 | |
|   var valuesToAdd = changeItem.isBatch ? changeItem.values : [changeItem.value];
 | |
|   var referenceElement = this.lastNodesList[index - 1] || null;
 | |
|   // gather all childnodes for a possible batch insertion
 | |
|   var allChildNodes = [];
 | |
| 
 | |
|   for (var i = 0, len = valuesToAdd.length; i < len; ++i) {
 | |
|     var templateClone = this.templateNode.cloneNode(true);
 | |
|     var childContext;
 | |
| 
 | |
|     if (this.noContext) {
 | |
|       childContext = this.$context.extend({
 | |
|         '$item': valuesToAdd[i]
 | |
|       });
 | |
|     } else {
 | |
|       childContext = this.$context.createChildContext(valuesToAdd[i], this.as || null);
 | |
|     }
 | |
| 
 | |
|     // apply bindings first, and then process child nodes, because bindings can add childnodes
 | |
|     ko.applyBindingsToDescendants(childContext, templateClone);
 | |
| 
 | |
|     var childNodes = ko.virtualElements.childNodes(templateClone);
 | |
|     // Note discussion at https://github.com/angular/angular.js/issues/7851
 | |
|     allChildNodes.push.apply(allChildNodes, Array.prototype.slice.call(childNodes));
 | |
|     this.lastNodesList.splice(index + i, 0, childNodes[childNodes.length - 1]);
 | |
|   }
 | |
| 
 | |
|   insertAllAfter(this.element, allChildNodes, referenceElement);
 | |
| };
 | |
| 
 | |
| 
 | |
| // Process a changeItem with {status: 'deleted', ...}
 | |
| FastForEach.prototype.deleted = function (changeItem) {
 | |
|   var index = changeItem.index;
 | |
|   var ptr = this.lastNodesList[index],
 | |
|       // We use this.element because that will be the last previous node
 | |
|       // for virtual element lists.
 | |
|       lastNode = this.lastNodesList[index - 1] || this.element;
 | |
|   do {
 | |
|     ptr = ptr.previousSibling;
 | |
|     ko.removeNode((ptr && ptr.nextSibling) || ko.virtualElements.firstChild(this.element));
 | |
|   } while (ptr && ptr !== lastNode);
 | |
|   // The "last node" in the DOM from which we begin our delets of the next adjacent node is
 | |
|   // now the sibling that preceded the first node of this item.
 | |
|   this.lastNodesList[index] = this.lastNodesList[index - 1];
 | |
|   this.indexesToDelete.push(index);
 | |
| };
 | |
| 
 | |
| 
 | |
| // We batch our deletion of item indexes in our parallel array.
 | |
| // See brianmhunt/knockout-fast-foreach#6/#8
 | |
| FastForEach.prototype.clearDeletedIndexes = function () {
 | |
|   // We iterate in reverse on the presumption (following the unit tests) that KO's diff engine
 | |
|   // processes diffs (esp. deletes) monotonically ascending i.e. from index 0 -> N.
 | |
|   for (var i = this.indexesToDelete.length - 1; i >= 0; --i) {
 | |
|     this.lastNodesList.splice(this.indexesToDelete[i], 1);
 | |
|   }
 | |
|   this.indexesToDelete = [];
 | |
| };
 | |
| 
 | |
| 
 | |
| ko.bindingHandlers.fastForEach = {
 | |
|   // Valid valueAccessors:
 | |
|   //    []
 | |
|   //    ko.observable([])
 | |
|   //    ko.observableArray([])
 | |
|   //    ko.computed
 | |
|   //    {data: array, name: string, as: string}
 | |
|   init: function init(element, valueAccessor, bindings, vm, context) {
 | |
|     var value = valueAccessor(),
 | |
|         ffe;
 | |
|     if (isPlainObject(value)) {
 | |
|       value.element = value.element || element;
 | |
|       value.$context = context;
 | |
|       ffe = new FastForEach(value);
 | |
|     } else {
 | |
|       ffe = new FastForEach({
 | |
|         element: element,
 | |
|         data: ko.unwrap(context.$rawData) === value ? context.$rawData : value,
 | |
|         $context: context
 | |
|       });
 | |
|     }
 | |
|     ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
 | |
|       ffe.dispose();
 | |
|     });
 | |
|     return {controlsDescendantBindings: true};
 | |
|   },
 | |
| 
 | |
|   // Export for testing, debugging, and overloading.
 | |
|   FastForEach: FastForEach
 | |
| };
 | |
| 
 | |
| ko.virtualElements.allowedBindings.fastForEach = true;
 | |
| })); |