'use strict'; var defaultDoc = typeof document == 'undefined' ? undefined : document; var specialElHandlers = require('./specialElHandlers'); var morphAttrs = require('../runtime/vdom/VElement').$__morphAttrs; var ELEMENT_NODE = 1; var TEXT_NODE = 3; var COMMENT_NODE = 8; function compareNodeNames(fromEl, toEl) { return fromEl.nodeName === toEl.$__nodeName; } function getElementById(doc, id) { return doc.getElementById(id); } function morphdom( fromNode, toNode, context, onNodeAdded, onBeforeElUpdated, onBeforeNodeDiscarded, onNodeDiscarded, onBeforeElChildrenUpdated ) { var doc = fromNode.ownerDocument || defaultDoc; // This object is used as a lookup to quickly find all keyed elements in the original DOM tree. var removalList = []; var foundKeys = {}; function walkDiscardedChildNodes(node) { onNodeDiscarded(node); var curChild = node.firstChild; while (curChild) { walkDiscardedChildNodes(curChild); curChild = curChild.nextSibling; } } function addVirtualNode(vEl, parentEl) { var realEl = vEl.$__actualize(doc); if (parentEl) { parentEl.appendChild(realEl); } onNodeAdded(realEl, context); var vCurChild = vEl.firstChild; while (vCurChild) { var realCurChild = null; var key = vCurChild.id; if (key) { var unmatchedFromEl = getElementById(doc, key); if (unmatchedFromEl && compareNodeNames(vCurChild, unmatchedFromEl)) { morphEl(unmatchedFromEl, vCurChild, false); realEl.appendChild(realCurChild = unmatchedFromEl); } } if (!realCurChild) { addVirtualNode(vCurChild, realEl); } vCurChild = vCurChild.nextSibling; } if (vEl.$__nodeType === 1) { var elHandler = specialElHandlers[vEl.nodeName]; if (elHandler !== undefined) { elHandler(realEl, vEl); } } return realEl; } function morphEl(fromEl, toEl, childrenOnly) { var toElKey = toEl.id; var nodeName = toEl.$__nodeName; if (childrenOnly === false) { if (toElKey) { // If an element with an ID is being morphed then it is will be in the final // DOM so clear it out of the saved elements collection foundKeys[toElKey] = true; } var constId = toEl.$__constId; if (constId !== undefined) { var otherProps = fromEl._vprops; if (otherProps !== undefined && constId === otherProps.c) { return; } } if (onBeforeElUpdated(fromEl, toElKey, context) === true) { return; } morphAttrs(fromEl, toEl); } if (onBeforeElChildrenUpdated(fromEl, toElKey, context) === true) { return; } if (nodeName !== 'TEXTAREA') { var curToNodeChild = toEl.firstChild; var curFromNodeChild = fromEl.firstChild; var curToNodeKey; var curFromNodeKey; var fromNextSibling; var toNextSibling; var matchingFromEl; outer: while (curToNodeChild) { toNextSibling = curToNodeChild.nextSibling; curToNodeKey = curToNodeChild.id; while (curFromNodeChild) { fromNextSibling = curFromNodeChild.nextSibling; curFromNodeKey = curFromNodeChild.id; var curFromNodeType = curFromNodeChild.nodeType; var isCompatible = undefined; if (curFromNodeType === curToNodeChild.$__nodeType) { if (curFromNodeType === ELEMENT_NODE) { // Both nodes being compared are Element nodes if (curToNodeKey) { // The target node has a key so we want to match it up with the correct element // in the original DOM tree if (curToNodeKey !== curFromNodeKey) { // The current element in the original DOM tree does not have a matching key so // let's check our lookup to see if there is a matching element in the original // DOM tree if ((matchingFromEl = getElementById(doc, curToNodeKey))) { if (curFromNodeChild.nextSibling === matchingFromEl) { // Special case for single element removals. To avoid removing the original // DOM node out of the tree (since that can break CSS transitions, etc.), // we will instead discard the current node and wait until the next // iteration to properly match up the keyed target element with its matching // element in the original tree isCompatible = false; } else { // We found a matching keyed element somewhere in the original DOM tree. // Let's moving the original DOM node into the current position and morph // it. // NOTE: We use insertBefore instead of replaceChild because we want to go through // the `removeNode()` function for the node that is being discarded so that // all lifecycle hooks are correctly invoked fromEl.insertBefore(matchingFromEl, curFromNodeChild); var curToNodeChildNextSibling = curToNodeChild.nextSibling; if (curToNodeChildNextSibling && curToNodeChildNextSibling.id === curFromNodeKey) { fromNextSibling = curFromNodeChild; } else { fromNextSibling = curFromNodeChild.nextSibling; removalList.push(curFromNodeChild); } curFromNodeChild = matchingFromEl; } } else { // The nodes are not compatible since the "to" node has a key and there // is no matching keyed node in the source tree isCompatible = false; } } } else if (curFromNodeKey) { // The original has a key isCompatible = false; } isCompatible = isCompatible !== false && compareNodeNames(curFromNodeChild, curToNodeChild) === true; if (isCompatible === true) { // We found compatible DOM elements so transform // the current "from" node to match the current // target DOM node. morphEl(curFromNodeChild, curToNodeChild, false); } } else if (curFromNodeType === TEXT_NODE || curFromNodeType === COMMENT_NODE) { // Both nodes being compared are Text or Comment nodes isCompatible = true; // Simply update nodeValue on the original node to // change the text value curFromNodeChild.nodeValue = curToNodeChild.nodeValue; } } if (isCompatible === true) { // Advance both the "to" child and the "from" child since we found a match curToNodeChild = toNextSibling; curFromNodeChild = fromNextSibling; continue outer; } // No compatible match so remove the old node from the DOM and continue trying to find a // match in the original DOM. However, we only do this if the from node is not keyed // since it is possible that a keyed node might match up with a node somewhere else in the // target tree and we don't want to discard it just yet since it still might find a // home in the final DOM tree. After everything is done we will remove any keyed nodes // that didn't find a home removalList.push(curFromNodeChild); curFromNodeChild = fromNextSibling; } // If we got this far then we did not find a candidate match for // our "to node" and we exhausted all of the children "from" // nodes. Therefore, we will just append the current "to" node // to the end if (curToNodeKey && (matchingFromEl = getElementById(doc, curToNodeKey)) && compareNodeNames(matchingFromEl, curToNodeChild)) { fromEl.appendChild(matchingFromEl); morphEl(matchingFromEl, curToNodeChild, false); } else { addVirtualNode(curToNodeChild, fromEl); } curToNodeChild = toNextSibling; curFromNodeChild = fromNextSibling; } // We have processed all of the "to nodes". If curFromNodeChild is // non-null then we still have some from nodes left over that need // to be removed while (curFromNodeChild) { removalList.push(curFromNodeChild); curFromNodeChild = curFromNodeChild.nextSibling; } } var specialElHandler = specialElHandlers[nodeName]; if (specialElHandler) { specialElHandler(fromEl, toEl); } } // END: morphEl(...) var morphedNode = fromNode; var fromNodeType = morphedNode.nodeType; var toNodeType = toNode.$__nodeType; var morphChildrenOnly = false; var shouldMorphEl = true; var newNode; // Handle the case where we are given two DOM nodes that are not // compatible (e.g.
--> or
--> TEXT) if (fromNodeType == ELEMENT_NODE) { if (toNodeType == ELEMENT_NODE) { if (!compareNodeNames(fromNode, toNode)) { newNode = toNode.$__actualize(doc); morphChildrenOnly = true; removalList.push(fromNode); } } else { // Going from an element node to a text or comment node removalList.push(fromNode); newNode = toNode.$__actualize(doc); shouldMorphEl = false; } } else if (fromNodeType == TEXT_NODE || fromNodeType == COMMENT_NODE) { // Text or comment node if (toNodeType == fromNodeType) { morphedNode.nodeValue = toNode.nodeValue; return morphedNode; } else { // Text node to something else removalList.push(fromNode); newNode = addVirtualNode(toNode); shouldMorphEl = false; } } if (shouldMorphEl === true) { morphEl(newNode || morphedNode, toNode, morphChildrenOnly); } if (newNode) { if (fromNode.parentNode) { fromNode.parentNode.replaceChild(newNode, fromNode); } } // We now need to loop over any keyed nodes that might need to be // removed. We only do the removal if we know that the keyed node // never found a match. When a keyed node is matched up we remove // it out of fromNodesLookup and we use fromNodesLookup to determine // if a keyed node has been matched up or not for (var i=0, len=removalList.length; i