4 // * Given an input array, a container DOM node, and a function from array elements to arrays of DOM nodes,
5 // map the array elements to arrays of DOM nodes, concatenate together all these arrays, and use them to populate the container DOM node
6 // * Next time we're given the same combination of things (with the array possibly having mutated), update the container DOM node
7 // 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
8 // previously mapped - retain those nodes, and just insert/delete other ones
10 // "callbackAfterAddingNodes" will be invoked after any "mapping"-generated nodes are inserted into the container node
11 // You can use this, for example, to activate bindings on those nodes.
13 function fixUpNodesToBeMovedOrRemoved(contiguousNodeArray) {
14 // Before moving, deleting, or replacing a set of nodes that were previously outputted by the "map" function, we have to reconcile
15 // them against what is in the DOM right now. It may be that some of the nodes have already been removed from the document,
16 // or that new nodes might have been inserted in the middle, for example by a binding. Also, there may previously have been
17 // leading comment nodes (created by rewritten string-based templates) that have since been removed during binding.
18 // So, this function translates the old "map" output array into its best guess of what set of current DOM nodes should be removed.
21 // [A] Any leading nodes that aren't in the document any more should be ignored
22 // These most likely correspond to memoization nodes that were already removed during binding
23 // See https://github.com/SteveSanderson/knockout/pull/440
24 // [B] We want to output a contiguous series of nodes that are still in the document. So, ignore any nodes that
25 // have already been removed, and include any nodes that have been inserted among the previous collection
28 while (contiguousNodeArray.length && !ko.utils.domNodeIsAttachedToDocument(contiguousNodeArray[0]))
29 contiguousNodeArray.splice(0, 1);
32 if (contiguousNodeArray.length > 1) {
33 // Build up the actual new contiguous node set
34 var current = contiguousNodeArray[0], last = contiguousNodeArray[contiguousNodeArray.length - 1], newContiguousSet = [current];
35 while (current !== last) {
36 current = current.nextSibling;
37 if (!current) // Won't happen, except if the developer has manually removed some DOM elements (then we're in an undefined scenario)
39 newContiguousSet.push(current);
42 // ... then mutate the input array to match this.
43 // (The following line replaces the contents of contiguousNodeArray with newContiguousSet)
44 Array.prototype.splice.apply(contiguousNodeArray, [0, contiguousNodeArray.length].concat(newContiguousSet));
46 return contiguousNodeArray;
49 function mapNodeAndRefreshWhenChanged(containerNode, mapping, valueToMap, callbackAfterAddingNodes, index) {
50 // Map this array value inside a dependentObservable so we re-map when any dependency changes
52 var dependentObservable = ko.dependentObservable(function() {
53 var newMappedNodes = mapping(valueToMap, index) || [];
55 // On subsequent evaluations, just replace the previously-inserted DOM nodes
56 if (mappedNodes.length > 0) {
57 ko.utils.replaceDomNodes(fixUpNodesToBeMovedOrRemoved(mappedNodes), newMappedNodes);
58 if (callbackAfterAddingNodes)
59 ko.dependencyDetection.ignore(callbackAfterAddingNodes, null, [valueToMap, newMappedNodes, index]);
62 // Replace the contents of the mappedNodes array, thereby updating the record
63 // of which nodes would be deleted if valueToMap was itself later removed
64 mappedNodes.splice(0, mappedNodes.length);
65 ko.utils.arrayPushAll(mappedNodes, newMappedNodes);
66 }, null, { disposeWhenNodeIsRemoved: containerNode, disposeWhen: function() { return (mappedNodes.length == 0) || !ko.utils.domNodeIsAttachedToDocument(mappedNodes[0]) } });
67 return { mappedNodes : mappedNodes, dependentObservable : (dependentObservable.isActive() ? dependentObservable : undefined) };
70 var lastMappingResultDomDataKey = "setDomNodeChildrenFromArrayMapping_lastMappingResult";
72 ko.utils.setDomNodeChildrenFromArrayMapping = function (domNode, array, mapping, options, callbackAfterAddingNodes) {
73 // Compare the provided array against the previous one
75 options = options || {};
76 var isFirstExecution = ko.utils.domData.get(domNode, lastMappingResultDomDataKey) === undefined;
77 var lastMappingResult = ko.utils.domData.get(domNode, lastMappingResultDomDataKey) || [];
78 var lastArray = ko.utils.arrayMap(lastMappingResult, function (x) { return x.arrayEntry; });
79 var editScript = ko.utils.compareArrays(lastArray, array);
81 // Build the new mapping result
82 var newMappingResult = [];
83 var lastMappingResultIndex = 0;
84 var newMappingResultIndex = 0;
86 var nodesToDelete = [];
87 var itemsToProcess = [];
88 var itemsForBeforeRemoveCallbacks = [];
89 var itemsForMoveCallbacks = [];
90 var itemsForAfterAddCallbacks = [];
93 function itemMovedOrRetained(editScriptIndex, oldPosition) {
94 mapData = lastMappingResult[oldPosition];
95 if (newMappingResultIndex !== oldPosition)
96 itemsForMoveCallbacks[editScriptIndex] = mapData;
97 // Since updating the index might change the nodes, do so before calling fixUpNodesToBeMovedOrRemoved
98 mapData.indexObservable(newMappingResultIndex++);
99 fixUpNodesToBeMovedOrRemoved(mapData.mappedNodes);
100 newMappingResult.push(mapData);
101 itemsToProcess.push(mapData);
104 function callCallback(callback, items) {
106 for (var i = 0, n = items.length; i < n; i++) {
108 ko.utils.arrayForEach(items[i].mappedNodes, function(node) {
109 callback(node, i, items[i].arrayEntry);
116 for (var i = 0, editScriptItem, movedIndex; editScriptItem = editScript[i]; i++) {
117 movedIndex = editScriptItem['moved'];
118 switch (editScriptItem['status']) {
120 if (movedIndex === undefined) {
121 mapData = lastMappingResult[lastMappingResultIndex];
123 // Stop tracking changes to the mapping for these nodes
124 if (mapData.dependentObservable)
125 mapData.dependentObservable.dispose();
127 // Queue these nodes for later removal
128 nodesToDelete.push.apply(nodesToDelete, fixUpNodesToBeMovedOrRemoved(mapData.mappedNodes));
129 if (options['beforeRemove']) {
130 itemsForBeforeRemoveCallbacks[i] = mapData;
131 itemsToProcess.push(mapData);
134 lastMappingResultIndex++;
138 itemMovedOrRetained(i, lastMappingResultIndex++);
142 if (movedIndex !== undefined) {
143 itemMovedOrRetained(i, movedIndex);
145 mapData = { arrayEntry: editScriptItem['value'], indexObservable: ko.observable(newMappingResultIndex++) };
146 newMappingResult.push(mapData);
147 itemsToProcess.push(mapData);
148 if (!isFirstExecution)
149 itemsForAfterAddCallbacks[i] = mapData;
155 // Call beforeMove first before any changes have been made to the DOM
156 callCallback(options['beforeMove'], itemsForMoveCallbacks);
158 // Next remove nodes for deleted items (or just clean if there's a beforeRemove callback)
159 ko.utils.arrayForEach(nodesToDelete, options['beforeRemove'] ? ko.cleanNode : ko.removeNode);
161 // Next add/reorder the remaining items (will include deleted items if there's a beforeRemove callback)
162 for (var i = 0, nextNode = ko.virtualElements.firstChild(domNode), lastNode, node; mapData = itemsToProcess[i]; i++) {
163 // Get nodes for newly added items
164 if (!mapData.mappedNodes)
165 ko.utils.extend(mapData, mapNodeAndRefreshWhenChanged(domNode, mapping, mapData.arrayEntry, callbackAfterAddingNodes, mapData.indexObservable));
167 // Put nodes in the right place if they aren't there already
168 for (var j = 0; node = mapData.mappedNodes[j]; nextNode = node.nextSibling, lastNode = node, j++) {
169 if (node !== nextNode)
170 ko.virtualElements.insertAfter(domNode, node, lastNode);
173 // Run the callbacks for newly added nodes (for example, to apply bindings, etc.)
174 if (!mapData.initialized && callbackAfterAddingNodes) {
175 callbackAfterAddingNodes(mapData.arrayEntry, mapData.mappedNodes, mapData.indexObservable);
176 mapData.initialized = true;
180 // If there's a beforeRemove callback, call it after reordering.
181 // Note that we assume that the beforeRemove callback will usually be used to remove the nodes using
182 // some sort of animation, which is why we first reorder the nodes that will be removed. If the
183 // callback instead removes the nodes right away, it would be more efficient to skip reordering them.
184 // Perhaps we'll make that change in the future if this scenario becomes more common.
185 callCallback(options['beforeRemove'], itemsForBeforeRemoveCallbacks);
187 // Finally call afterMove and afterAdd callbacks
188 callCallback(options['afterMove'], itemsForMoveCallbacks);
189 callCallback(options['afterAdd'], itemsForAfterAddCallbacks);
191 // Store a copy of the array items we just considered so we can difference it next time
192 ko.utils.domData.set(domNode, lastMappingResultDomDataKey, newMappingResult);
196 ko.exportSymbol('utils.setDomNodeChildrenFromArrayMapping', ko.utils.setDomNodeChildrenFromArrayMapping);