3 // * Given an input array, a container DOM node, and a function from array elements to arrays of DOM nodes,
4 // map the array elements to arrays of DOM nodes, concatenate together all these arrays, and use them to populate the container DOM node
5 // * Next time we're given the same combination of things (with the array possibly having mutated), update the container DOM node
6 // 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
7 // previously mapped - retain those nodes, and just insert/delete other ones
9 // "callbackAfterAddingNodes" will be invoked after any "mapping"-generated nodes are inserted into the container node
10 // You can use this, for example, to activate bindings on those nodes.
12 function mapNodeAndRefreshWhenChanged(containerNode, mapping, valueToMap, callbackAfterAddingNodes, index) {
13 // Map this array value inside a dependentObservable so we re-map when any dependency changes
15 var dependentObservable = ko.dependentObservable(function() {
16 var newMappedNodes = mapping(valueToMap, index, ko.utils.fixUpContinuousNodeArray(mappedNodes, containerNode)) || [];
18 // On subsequent evaluations, just replace the previously-inserted DOM nodes
19 if (mappedNodes.length > 0) {
20 ko.utils.replaceDomNodes(mappedNodes, newMappedNodes);
21 if (callbackAfterAddingNodes)
22 ko.dependencyDetection.ignore(callbackAfterAddingNodes, null, [valueToMap, newMappedNodes, index]);
25 // Replace the contents of the mappedNodes array, thereby updating the record
26 // of which nodes would be deleted if valueToMap was itself later removed
27 mappedNodes.length = 0;
28 ko.utils.arrayPushAll(mappedNodes, newMappedNodes);
29 }, null, { disposeWhenNodeIsRemoved: containerNode, disposeWhen: function() { return !ko.utils.anyDomNodeIsAttachedToDocument(mappedNodes); } });
30 return { mappedNodes : mappedNodes, dependentObservable : (dependentObservable.isActive() ? dependentObservable : undefined) };
33 var lastMappingResultDomDataKey = ko.utils.domData.nextKey(),
34 deletedItemDummyValue = ko.utils.domData.nextKey();
36 ko.utils.setDomNodeChildrenFromArrayMapping = function (domNode, array, mapping, options, callbackAfterAddingNodes) {
37 // Compare the provided array against the previous one
39 options = options || {};
40 var isFirstExecution = ko.utils.domData.get(domNode, lastMappingResultDomDataKey) === undefined;
41 var lastMappingResult = ko.utils.domData.get(domNode, lastMappingResultDomDataKey) || [];
42 var lastArray = ko.utils.arrayMap(lastMappingResult, function (x) { return x.arrayEntry; });
43 var editScript = ko.utils.compareArrays(lastArray, array, options['dontLimitMoves']);
45 // Build the new mapping result
46 var newMappingResult = [];
47 var lastMappingResultIndex = 0;
48 var newMappingResultIndex = 0;
50 var nodesToDelete = [];
51 var itemsToProcess = [];
52 var itemsForBeforeRemoveCallbacks = [];
53 var itemsForMoveCallbacks = [];
54 var itemsForAfterAddCallbacks = [];
57 function itemMovedOrRetained(editScriptIndex, oldPosition) {
58 mapData = lastMappingResult[oldPosition];
59 if (newMappingResultIndex !== oldPosition)
60 itemsForMoveCallbacks[editScriptIndex] = mapData;
61 // Since updating the index might change the nodes, do so before calling fixUpContinuousNodeArray
62 mapData.indexObservable(newMappingResultIndex++);
63 ko.utils.fixUpContinuousNodeArray(mapData.mappedNodes, domNode);
64 newMappingResult.push(mapData);
65 itemsToProcess.push(mapData);
68 function callCallback(callback, items) {
70 for (var i = 0, n = items.length; i < n; i++) {
72 ko.utils.arrayForEach(items[i].mappedNodes, function(node) {
73 callback(node, i, items[i].arrayEntry);
80 for (var i = 0, editScriptItem, movedIndex; editScriptItem = editScript[i]; i++) {
81 movedIndex = editScriptItem['moved'];
82 switch (editScriptItem['status']) {
84 if (movedIndex === undefined) {
85 mapData = lastMappingResult[lastMappingResultIndex];
87 // Stop tracking changes to the mapping for these nodes
88 if (mapData.dependentObservable) {
89 mapData.dependentObservable.dispose();
90 mapData.dependentObservable = undefined;
93 // Queue these nodes for later removal
94 if (ko.utils.fixUpContinuousNodeArray(mapData.mappedNodes, domNode).length) {
95 if (options['beforeRemove']) {
96 newMappingResult.push(mapData);
97 itemsToProcess.push(mapData);
98 if (mapData.arrayEntry === deletedItemDummyValue) {
101 itemsForBeforeRemoveCallbacks[i] = mapData;
105 nodesToDelete.push.apply(nodesToDelete, mapData.mappedNodes);
109 lastMappingResultIndex++;
113 itemMovedOrRetained(i, lastMappingResultIndex++);
117 if (movedIndex !== undefined) {
118 itemMovedOrRetained(i, movedIndex);
120 mapData = { arrayEntry: editScriptItem['value'], indexObservable: ko.observable(newMappingResultIndex++) };
121 newMappingResult.push(mapData);
122 itemsToProcess.push(mapData);
123 if (!isFirstExecution)
124 itemsForAfterAddCallbacks[i] = mapData;
130 // Store a copy of the array items we just considered so we can difference it next time
131 ko.utils.domData.set(domNode, lastMappingResultDomDataKey, newMappingResult);
133 // Call beforeMove first before any changes have been made to the DOM
134 callCallback(options['beforeMove'], itemsForMoveCallbacks);
136 // Next remove nodes for deleted items (or just clean if there's a beforeRemove callback)
137 ko.utils.arrayForEach(nodesToDelete, options['beforeRemove'] ? ko.cleanNode : ko.removeNode);
139 // Next add/reorder the remaining items (will include deleted items if there's a beforeRemove callback)
140 for (var i = 0, nextNode = ko.virtualElements.firstChild(domNode), lastNode, node; mapData = itemsToProcess[i]; i++) {
141 // Get nodes for newly added items
142 if (!mapData.mappedNodes)
143 ko.utils.extend(mapData, mapNodeAndRefreshWhenChanged(domNode, mapping, mapData.arrayEntry, callbackAfterAddingNodes, mapData.indexObservable));
145 // Put nodes in the right place if they aren't there already
146 for (var j = 0; node = mapData.mappedNodes[j]; nextNode = node.nextSibling, lastNode = node, j++) {
147 if (node !== nextNode)
148 ko.virtualElements.insertAfter(domNode, node, lastNode);
151 // Run the callbacks for newly added nodes (for example, to apply bindings, etc.)
152 if (!mapData.initialized && callbackAfterAddingNodes) {
153 callbackAfterAddingNodes(mapData.arrayEntry, mapData.mappedNodes, mapData.indexObservable);
154 mapData.initialized = true;
158 // If there's a beforeRemove callback, call it after reordering.
159 // Note that we assume that the beforeRemove callback will usually be used to remove the nodes using
160 // some sort of animation, which is why we first reorder the nodes that will be removed. If the
161 // callback instead removes the nodes right away, it would be more efficient to skip reordering them.
162 // Perhaps we'll make that change in the future if this scenario becomes more common.
163 callCallback(options['beforeRemove'], itemsForBeforeRemoveCallbacks);
165 // Replace the stored values of deleted items with a dummy value. This provides two benefits: it marks this item
166 // as already "removed" so we won't call beforeRemove for it again, and it ensures that the item won't match up
167 // with an actual item in the array and appear as "retained" or "moved".
168 for (i = 0; i < itemsForBeforeRemoveCallbacks.length; ++i) {
169 if (itemsForBeforeRemoveCallbacks[i]) {
170 itemsForBeforeRemoveCallbacks[i].arrayEntry = deletedItemDummyValue;
174 // Finally call afterMove and afterAdd callbacks
175 callCallback(options['afterMove'], itemsForMoveCallbacks);
176 callCallback(options['afterAdd'], itemsForAfterAddCallbacks);
180 ko.exportSymbol('utils.setDomNodeChildrenFromArrayMapping', ko.utils.setDomNodeChildrenFromArrayMapping);