migrated knockout asset to bower
[openemr.git] / public / assets / knockout-2-2-1 / src / binding / editDetection / arrayToDomNodeChildren.js
blobd0f8622519099911644bf367c3aa2c9cfb2a1169
2 (function () {
3     // Objective:
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.
19         //
20         // Rules:
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
27         // Rule [A]
28         while (contiguousNodeArray.length && !ko.utils.domNodeIsAttachedToDocument(contiguousNodeArray[0]))
29             contiguousNodeArray.splice(0, 1);
31         // Rule [B]
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)
38                     return;
39                 newContiguousSet.push(current);
40             }
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));
45         }
46         return contiguousNodeArray;
47     }
49     function mapNodeAndRefreshWhenChanged(containerNode, mapping, valueToMap, callbackAfterAddingNodes, index) {
50         // Map this array value inside a dependentObservable so we re-map when any dependency changes
51         var mappedNodes = [];
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]);
60             }
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) };
68     }
70     var lastMappingResultDomDataKey = "setDomNodeChildrenFromArrayMapping_lastMappingResult";
72     ko.utils.setDomNodeChildrenFromArrayMapping = function (domNode, array, mapping, options, callbackAfterAddingNodes) {
73         // Compare the provided array against the previous one
74         array = array || [];
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 = [];
91         var mapData;
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);
102         }
104         function callCallback(callback, items) {
105             if (callback) {
106                 for (var i = 0, n = items.length; i < n; i++) {
107                     if (items[i]) {
108                         ko.utils.arrayForEach(items[i].mappedNodes, function(node) {
109                             callback(node, i, items[i].arrayEntry);
110                         });
111                     }
112                 }
113             }
114         }
116         for (var i = 0, editScriptItem, movedIndex; editScriptItem = editScript[i]; i++) {
117             movedIndex = editScriptItem['moved'];
118             switch (editScriptItem['status']) {
119                 case "deleted":
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);
132                         }
133                     }
134                     lastMappingResultIndex++;
135                     break;
137                 case "retained":
138                     itemMovedOrRetained(i, lastMappingResultIndex++);
139                     break;
141                 case "added":
142                     if (movedIndex !== undefined) {
143                         itemMovedOrRetained(i, movedIndex);
144                     } else {
145                         mapData = { arrayEntry: editScriptItem['value'], indexObservable: ko.observable(newMappingResultIndex++) };
146                         newMappingResult.push(mapData);
147                         itemsToProcess.push(mapData);
148                         if (!isFirstExecution)
149                             itemsForAfterAddCallbacks[i] = mapData;
150                     }
151                     break;
152             }
153         }
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);
171             }
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;
177             }
178         }
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);
193     }
194 })();
196 ko.exportSymbol('utils.setDomNodeChildrenFromArrayMapping', ko.utils.setDomNodeChildrenFromArrayMapping);