/*!
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;
})); |