knockout-fast-foreach.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. /*!
  2. Knockout Fast Foreach v0.4.1 (2015-07-17T14:06:15.974Z)
  3. By: Brian M Hunt (C) 2015
  4. License: MIT
  5. Adds `fastForEach` to `ko.bindingHandlers`.
  6. */
  7. (function (root, factory) {
  8. if (typeof define === 'function' && define.amd) {
  9. define(['knockout'], factory);
  10. } else if (typeof exports === 'object') {
  11. module.exports = factory(require('knockout'));
  12. } else {
  13. root.KnockoutFastForeach = factory(root.ko);
  14. }
  15. }(this, function (ko) {
  16. "use strict";
  17. // index.js
  18. // --------
  19. // Fast For Each
  20. //
  21. // Employing sound techniques to make a faster Knockout foreach binding.
  22. // --------
  23. // Utilities
  24. // from https://github.com/jonschlinkert/is-plain-object
  25. function isPlainObject(o) {
  26. return !!o && typeof o === 'object' && o.constructor === Object;
  27. }
  28. // From knockout/src/virtualElements.js
  29. var commentNodesHaveTextProperty = document && document.createComment("test").text === "<!--test-->";
  30. var startCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*ko(?:\s+([\s\S]+))?\s*-->$/ : /^\s*ko(?:\s+([\s\S]+))?\s*$/;
  31. var supportsDocumentFragment = document && typeof document.createDocumentFragment === "function";
  32. function isVirtualNode(node) {
  33. return (node.nodeType === 8) && startCommentRegex.test(commentNodesHaveTextProperty ? node.text : node.nodeValue);
  34. }
  35. // Get a copy of the (possibly virtual) child nodes of the given element,
  36. // put them into a container, then empty the given node.
  37. function makeTemplateNode(sourceNode) {
  38. var container = document.createElement("div");
  39. var parentNode;
  40. if (sourceNode.content) {
  41. // For e.g. <template> tags
  42. parentNode = sourceNode.content;
  43. } else if (sourceNode.tagName === 'SCRIPT') {
  44. parentNode = document.createElement("div");
  45. parentNode.innerHTML = sourceNode.text;
  46. } else {
  47. // Anything else e.g. <div>
  48. parentNode = sourceNode;
  49. }
  50. ko.utils.arrayForEach(ko.virtualElements.childNodes(parentNode), function (child) {
  51. // FIXME - This cloneNode could be expensive; we may prefer to iterate over the
  52. // parentNode children in reverse (so as not to foul the indexes as childNodes are
  53. // removed from parentNode when inserted into the container)
  54. if (child) {
  55. container.insertBefore(child.cloneNode(true), null);
  56. }
  57. });
  58. return container;
  59. }
  60. function insertAllAfter(containerNode, nodeOrNodeArrayToInsert, insertAfterNode) {
  61. var frag, len, i;
  62. // poor man's node and array check, should be enough for this
  63. if (typeof nodeOrNodeArrayToInsert.nodeType !== "undefined" && typeof nodeOrNodeArrayToInsert.length === "undefined") {
  64. throw new Error("Expected a single node or a node array");
  65. }
  66. if (typeof nodeOrNodeArrayToInsert.nodeType !== "undefined") {
  67. ko.virtualElements.insertAfter(containerNode, nodeOrNodeArrayToInsert, insertAfterNode);
  68. return;
  69. }
  70. if (nodeOrNodeArrayToInsert.length === 1) {
  71. ko.virtualElements.insertAfter(containerNode, nodeOrNodeArrayToInsert[0], insertAfterNode);
  72. return;
  73. }
  74. if (supportsDocumentFragment) {
  75. frag = document.createDocumentFragment();
  76. for (i = 0, len = nodeOrNodeArrayToInsert.length; i !== len; ++i) {
  77. frag.appendChild(nodeOrNodeArrayToInsert[i]);
  78. }
  79. ko.virtualElements.insertAfter(containerNode, frag, insertAfterNode);
  80. } else {
  81. // Nodes are inserted in reverse order - pushed down immediately after
  82. // the last node for the previous item or as the first node of element.
  83. for (i = nodeOrNodeArrayToInsert.length - 1; i >= 0; --i) {
  84. var child = nodeOrNodeArrayToInsert[i];
  85. if (!child) {
  86. return;
  87. }
  88. ko.virtualElements.insertAfter(containerNode, child, insertAfterNode);
  89. }
  90. }
  91. }
  92. // Mimic a KO change item 'add'
  93. function valueToChangeAddItem(value, index) {
  94. return {
  95. status: 'added',
  96. value: value,
  97. index: index
  98. };
  99. }
  100. function isAdditionAdjacentToLast(changeIndex, arrayChanges) {
  101. return changeIndex > 0 &&
  102. changeIndex < arrayChanges.length &&
  103. arrayChanges[changeIndex].status === "added" &&
  104. arrayChanges[changeIndex - 1].status === "added" &&
  105. arrayChanges[changeIndex - 1].index === arrayChanges[changeIndex].index - 1;
  106. }
  107. function FastForEach(spec) {
  108. this.element = spec.element;
  109. this.container = isVirtualNode(this.element) ?
  110. this.element.parentNode : this.element;
  111. this.$context = spec.$context;
  112. this.data = spec.data;
  113. this.as = spec.as;
  114. this.noContext = spec.noContext;
  115. this.templateNode = makeTemplateNode(
  116. spec.name ? document.getElementById(spec.name).cloneNode(true) : spec.element
  117. );
  118. this.afterQueueFlush = spec.afterQueueFlush;
  119. this.beforeQueueFlush = spec.beforeQueueFlush;
  120. this.changeQueue = [];
  121. this.lastNodesList = [];
  122. this.indexesToDelete = [];
  123. this.rendering_queued = false;
  124. // Remove existing content.
  125. ko.virtualElements.emptyNode(this.element);
  126. // Prime content
  127. var primeData = ko.unwrap(this.data);
  128. if (primeData.map) {
  129. this.onArrayChange(primeData.map(valueToChangeAddItem));
  130. }
  131. // Watch for changes
  132. if (ko.isObservable(this.data)) {
  133. if (!this.data.indexOf) {
  134. // Make sure the observable is trackable.
  135. this.data = this.data.extend({trackArrayChanges: true});
  136. }
  137. this.changeSubs = this.data.subscribe(this.onArrayChange, this, 'arrayChange');
  138. }
  139. }
  140. FastForEach.animateFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame ||
  141. window.mozRequestAnimationFrame || window.msRequestAnimationFrame ||
  142. function(cb) { return window.setTimeout(cb, 1000 / 60); };
  143. FastForEach.prototype.dispose = function () {
  144. if (this.changeSubs) {
  145. this.changeSubs.dispose();
  146. }
  147. };
  148. // If the array changes we register the change.
  149. FastForEach.prototype.onArrayChange = function (changeSet) {
  150. var self = this;
  151. var changeMap = {
  152. added: [],
  153. deleted: []
  154. };
  155. for (var i = 0, len = changeSet.length; i < len; i++) {
  156. // the change is appended to a last change info object when both are 'added' and have indexes next to each other
  157. // 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
  158. if (isAdditionAdjacentToLast(i, changeSet)) {
  159. var batchValues = changeMap.added[changeMap.added.length - 1].values;
  160. if (!batchValues) {
  161. // transform the last addition into a batch addition object
  162. var lastAddition = changeMap.added.pop();
  163. var batchAddition = {
  164. isBatch: true,
  165. status: 'added',
  166. index: lastAddition.index,
  167. values: [lastAddition.value]
  168. };
  169. batchValues = batchAddition.values;
  170. changeMap.added.push(batchAddition);
  171. }
  172. batchValues.push(changeSet[i].value);
  173. } else {
  174. changeMap[changeSet[i].status].push(changeSet[i]);
  175. }
  176. }
  177. if (changeMap.deleted.length > 0) {
  178. this.changeQueue.push.apply(this.changeQueue, changeMap.deleted);
  179. this.changeQueue.push({status: 'clearDeletedIndexes'});
  180. }
  181. this.changeQueue.push.apply(this.changeQueue, changeMap.added);
  182. // Once a change is registered, the ticking count-down starts for the processQueue.
  183. if (this.changeQueue.length > 0 && !this.rendering_queued) {
  184. this.rendering_queued = true;
  185. FastForEach.animateFrame.call(window, function () { self.processQueue(); });
  186. }
  187. };
  188. // Reflect all the changes in the queue in the DOM, then wipe the queue.
  189. FastForEach.prototype.processQueue = function () {
  190. var self = this;
  191. // Callback so folks can do things before the queue flush.
  192. if (typeof this.beforeQueueFlush === 'function') {
  193. this.beforeQueueFlush(this.changeQueue);
  194. }
  195. ko.utils.arrayForEach(this.changeQueue, function (changeItem) {
  196. // console.log(self.data(), "CI", JSON.stringify(changeItem, null, 2), JSON.stringify($(self.element).text()))
  197. self[changeItem.status](changeItem);
  198. // console.log(" ==> ", JSON.stringify($(self.element).text()))
  199. });
  200. this.rendering_queued = false;
  201. // Callback so folks can do things.
  202. if (typeof this.afterQueueFlush === 'function') {
  203. this.afterQueueFlush(this.changeQueue);
  204. }
  205. this.changeQueue = [];
  206. };
  207. // Process a changeItem with {status: 'added', ...}
  208. FastForEach.prototype.added = function (changeItem) {
  209. var index = changeItem.index;
  210. var valuesToAdd = changeItem.isBatch ? changeItem.values : [changeItem.value];
  211. var referenceElement = this.lastNodesList[index - 1] || null;
  212. // gather all childnodes for a possible batch insertion
  213. var allChildNodes = [];
  214. for (var i = 0, len = valuesToAdd.length; i < len; ++i) {
  215. var templateClone = this.templateNode.cloneNode(true);
  216. var childContext;
  217. if (this.noContext) {
  218. childContext = this.$context.extend({
  219. '$item': valuesToAdd[i]
  220. });
  221. } else {
  222. childContext = this.$context.createChildContext(valuesToAdd[i], this.as || null);
  223. }
  224. // apply bindings first, and then process child nodes, because bindings can add childnodes
  225. ko.applyBindingsToDescendants(childContext, templateClone);
  226. var childNodes = ko.virtualElements.childNodes(templateClone);
  227. // Note discussion at https://github.com/angular/angular.js/issues/7851
  228. allChildNodes.push.apply(allChildNodes, Array.prototype.slice.call(childNodes));
  229. this.lastNodesList.splice(index + i, 0, childNodes[childNodes.length - 1]);
  230. }
  231. insertAllAfter(this.element, allChildNodes, referenceElement);
  232. };
  233. // Process a changeItem with {status: 'deleted', ...}
  234. FastForEach.prototype.deleted = function (changeItem) {
  235. var index = changeItem.index;
  236. var ptr = this.lastNodesList[index],
  237. // We use this.element because that will be the last previous node
  238. // for virtual element lists.
  239. lastNode = this.lastNodesList[index - 1] || this.element;
  240. do {
  241. ptr = ptr.previousSibling;
  242. ko.removeNode((ptr && ptr.nextSibling) || ko.virtualElements.firstChild(this.element));
  243. } while (ptr && ptr !== lastNode);
  244. // The "last node" in the DOM from which we begin our delets of the next adjacent node is
  245. // now the sibling that preceded the first node of this item.
  246. this.lastNodesList[index] = this.lastNodesList[index - 1];
  247. this.indexesToDelete.push(index);
  248. };
  249. // We batch our deletion of item indexes in our parallel array.
  250. // See brianmhunt/knockout-fast-foreach#6/#8
  251. FastForEach.prototype.clearDeletedIndexes = function () {
  252. // We iterate in reverse on the presumption (following the unit tests) that KO's diff engine
  253. // processes diffs (esp. deletes) monotonically ascending i.e. from index 0 -> N.
  254. for (var i = this.indexesToDelete.length - 1; i >= 0; --i) {
  255. this.lastNodesList.splice(this.indexesToDelete[i], 1);
  256. }
  257. this.indexesToDelete = [];
  258. };
  259. ko.bindingHandlers.fastForEach = {
  260. // Valid valueAccessors:
  261. // []
  262. // ko.observable([])
  263. // ko.observableArray([])
  264. // ko.computed
  265. // {data: array, name: string, as: string}
  266. init: function init(element, valueAccessor, bindings, vm, context) {
  267. var value = valueAccessor(),
  268. ffe;
  269. if (isPlainObject(value)) {
  270. value.element = value.element || element;
  271. value.$context = context;
  272. ffe = new FastForEach(value);
  273. } else {
  274. ffe = new FastForEach({
  275. element: element,
  276. data: ko.unwrap(context.$rawData) === value ? context.$rawData : value,
  277. $context: context
  278. });
  279. }
  280. ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
  281. ffe.dispose();
  282. });
  283. return {controlsDescendantBindings: true};
  284. },
  285. // Export for testing, debugging, and overloading.
  286. FastForEach: FastForEach
  287. };
  288. ko.virtualElements.allowedBindings.fastForEach = true;
  289. }));