export function setDomNodeChildrenFromArrayMapping(domNode, array, mapping, options, callbackAfterAddingNodes) { // Compare the provided array against the previous one array = array || []; options = options || {}; var isFirstExecution = domData.get(domNode, lastMappingResultDomDataKey) === undefined; var lastMappingResult = domData.get(domNode, lastMappingResultDomDataKey) || []; var lastArray = arrayMap(lastMappingResult, function (x) { return x.arrayEntry; }); var editScript = compareArrays(lastArray, array, options['dontLimitMoves']); // Build the new mapping result var newMappingResult = []; var lastMappingResultIndex = 0; var newMappingResultIndex = 0; var nodesToDelete = []; var itemsToProcess = []; var itemsForBeforeRemoveCallbacks = []; var itemsForMoveCallbacks = []; var itemsForAfterAddCallbacks = []; var mapData; function itemMovedOrRetained(editScriptIndex, oldPosition) { mapData = lastMappingResult[oldPosition]; if (newMappingResultIndex !== oldPosition) itemsForMoveCallbacks[editScriptIndex] = mapData; // Since updating the index might change the nodes, do so before calling fixUpContinuousNodeArray mapData.indexObservable(newMappingResultIndex++); fixUpContinuousNodeArray(mapData.mappedNodes, domNode); newMappingResult.push(mapData); itemsToProcess.push(mapData); } function callCallback(callback, items) { if (callback) { for (var i = 0, n = items.length; i < n; i++) { if (items[i]) { arrayForEach(items[i].mappedNodes, function(node) { callback(node, i, items[i].arrayEntry); }); } } } } for (var i = 0, editScriptItem, movedIndex; editScriptItem = editScript[i]; i++) { movedIndex = editScriptItem['moved']; switch (editScriptItem['status']) { case "deleted": if (movedIndex === undefined) { mapData = lastMappingResult[lastMappingResultIndex]; // Stop tracking changes to the mapping for these nodes if (mapData.dependentObservable) { mapData.dependentObservable.dispose(); mapData.dependentObservable = undefined; } // Queue these nodes for later removal if (fixUpContinuousNodeArray(mapData.mappedNodes, domNode).length) { if (options['beforeRemove']) { newMappingResult.push(mapData); itemsToProcess.push(mapData); if (mapData.arrayEntry === deletedItemDummyValue) { mapData = null; } else { itemsForBeforeRemoveCallbacks[i] = mapData; } } if (mapData) { nodesToDelete.push.apply(nodesToDelete, mapData.mappedNodes); } } } lastMappingResultIndex++; break; case "retained": itemMovedOrRetained(i, lastMappingResultIndex++); break; case "added": if (movedIndex !== undefined) { itemMovedOrRetained(i, movedIndex); } else { mapData = { arrayEntry: editScriptItem['value'], indexObservable: observable(newMappingResultIndex++) }; newMappingResult.push(mapData); itemsToProcess.push(mapData); if (!isFirstExecution) itemsForAfterAddCallbacks[i] = mapData; } break; } } // Store a copy of the array items we just considered so we can difference it next time domData.set(domNode, lastMappingResultDomDataKey, newMappingResult); // Call beforeMove first before any changes have been made to the DOM callCallback(options['beforeMove'], itemsForMoveCallbacks); // Next remove nodes for deleted items (or just clean if there's a beforeRemove callback) arrayForEach(nodesToDelete, options['beforeRemove'] ? cleanNode : removeNode); // Next add/reorder the remaining items (will include deleted items if there's a beforeRemove callback) i = 0; for (var nextNode = virtualElements.firstChild(domNode), lastNode, node; mapData = itemsToProcess[i]; i++) { // Get nodes for newly added items if (!mapData.mappedNodes) extend(mapData, mapNodeAndRefreshWhenChanged(domNode, mapping, mapData.arrayEntry, callbackAfterAddingNodes, mapData.indexObservable)); // Put nodes in the right place if they aren't there already for (var j = 0; node = mapData.mappedNodes[j]; nextNode = node.nextSibling, lastNode = node, j++) { if (node !== nextNode) virtualElements.insertAfter(domNode, node, lastNode); } // Run the callbacks for newly added nodes (for example, to apply bindings, etc.) if (!mapData.initialized && callbackAfterAddingNodes) { callbackAfterAddingNodes(mapData.arrayEntry, mapData.mappedNodes, mapData.indexObservable); mapData.initialized = true; } } // If there's a beforeRemove callback, call it after reordering. // Note that we assume that the beforeRemove callback will usually be used to remove the nodes using // some sort of animation, which is why we first reorder the nodes that will be removed. If the // callback instead removes the nodes right away, it would be more efficient to skip reordering them. // Perhaps we'll make that change in the future if this scenario becomes more common. callCallback(options['beforeRemove'], itemsForBeforeRemoveCallbacks); // Replace the stored values of deleted items with a dummy value. This provides two benefits: it marks this item // as already "removed" so we won't call beforeRemove for it again, and it ensures that the item won't match up // with an actual item in the array and appear as "retained" or "moved". for (i = 0; i < itemsForBeforeRemoveCallbacks.length; ++i) { if (itemsForBeforeRemoveCallbacks[i]) { itemsForBeforeRemoveCallbacks[i].arrayEntry = deletedItemDummyValue; } } // Finally call afterMove and afterAdd callbacks callCallback(options['afterMove'], itemsForMoveCallbacks); callCallback(options['afterAdd'], itemsForAfterAddCallbacks); }
// On subsequent evaluations, just replace the previously-inserted DOM nodes if (mappedNodes.length > 0) { replaceDomNodes(mappedNodes, newMappedNodes); if (callbackAfterAddingNodes) dependencyDetection.ignore(callbackAfterAddingNodes, null, [valueToMap, newMappedNodes, index]); } // Replace the contents of the mappedNodes array, thereby updating the record // of which nodes would be deleted if valueToMap was itself later removed mappedNodes.length = 0; arrayPushAll(mappedNodes, newMappedNodes); }, null, { disposeWhenNodeIsRemoved: containerNode, disposeWhen: function() { return !anyDomNodeIsAttachedToDocument(mappedNodes); } }); return { mappedNodes : mappedNodes, dependentObservable : (dependentObservable.isActive() ? dependentObservable : undefined) }; } var lastMappingResultDomDataKey = domData.nextKey(), deletedItemDummyValue = domData.nextKey(); export function setDomNodeChildrenFromArrayMapping(domNode, array, mapping, options, callbackAfterAddingNodes) { // Compare the provided array against the previous one array = array || []; options = options || {}; var isFirstExecution = domData.get(domNode, lastMappingResultDomDataKey) === undefined; var lastMappingResult = domData.get(domNode, lastMappingResultDomDataKey) || []; var lastArray = arrayMap(lastMappingResult, function (x) { return x.arrayEntry; }); var editScript = compareArrays(lastArray, array, options['dontLimitMoves']); // Build the new mapping result var newMappingResult = []; var lastMappingResultIndex = 0; var newMappingResultIndex = 0;