UNPKG

marko

Version:

UI Components + streaming, async, high performance, HTML templating for Node.js and the browser.

741 lines (650 loc) • 23.9 kB
"use strict"; var componentsUtil = require("@internal/components-util"); var existingComponentLookup = componentsUtil._l_; var destroyNodeRecursive = componentsUtil._R_; var addComponentRootToKeyedElements = componentsUtil._m_; var normalizeComponentKey = componentsUtil._U_; var domData = require("../../components/dom-data"); var eventDelegation = require("../../components/event-delegation"); var KeySequence = require("../../components/KeySequence"); var VElement = require("../vdom").bw_; var fragment = require("./fragment"); var helpers = require("./helpers"); var isTextOnly = require("../is-text-only"); var virtualizeElement = VElement.ch_; var morphAttrs = VElement.ci_; var keysByDOMNode = domData._n_; var componentByDOMNode = domData._p_; var vElementByDOMNode = domData._K_; var detachedByDOMNode = domData.aW_; var insertBefore = helpers.bf_; var insertAfter = helpers.bg_; var nextSibling = helpers.cm_; var firstChild = helpers.aA_; var removeChild = helpers.bh_; var createFragmentNode = fragment._k_; var beginFragmentNode = fragment.cq_; var ELEMENT_NODE = 1; var TEXT_NODE = 3; var COMMENT_NODE = 8; var COMPONENT_NODE = 2; var FRAGMENT_NODE = 12; var DOCTYPE_NODE = 10; // var FLAG_SIMPLE_ATTRS = 1; // var FLAG_CUSTOM_ELEMENT = 2; // var FLAG_SPREAD_ATTRS = 4; function isAutoKey(key) { return key[0] !== "@"; } function compareNodeNames(fromEl, toEl) { return fromEl.cd_ === toEl.cd_; } function caseInsensitiveCompare(a, b) { return a.toLowerCase() === b.toLowerCase(); } function onNodeAdded(node, componentsContext) { if (node.nodeType === ELEMENT_NODE) { eventDelegation.aV_(node, componentsContext); } } function morphdom(fromNode, toNode, host, componentsContext) { var globalComponentsContext; var isHydrate = false; var keySequences = Object.create(null); if (componentsContext) { globalComponentsContext = componentsContext.p_; isHydrate = globalComponentsContext._Z_; } function insertVirtualNodeBefore( vNode, key, referenceEl, parentEl, ownerComponent, parentComponent) { var realNode = vNode.bo_(host, parentEl.namespaceURI); insertBefore(realNode, referenceEl, parentEl); if ( vNode.bX_ === ELEMENT_NODE || vNode.bX_ === FRAGMENT_NODE) { if (key) { keysByDOMNode.set(realNode, key); (isAutoKey(key) ? parentComponent : ownerComponent).K_[ key] = realNode; } if (!isTextOnly(vNode.cd_)) { morphChildren(realNode, vNode, parentComponent); } onNodeAdded(realNode, componentsContext); } } function insertVirtualComponentBefore( vComponent, referenceNode, referenceNodeParentEl, component, key, ownerComponent, parentComponent) { var rootNode = component._E_ = insertBefore( createFragmentNode(), referenceNode, referenceNodeParentEl ); componentByDOMNode.set(rootNode, component); if (key && ownerComponent) { key = normalizeComponentKey(key, parentComponent.id); addComponentRootToKeyedElements( ownerComponent.K_, key, rootNode, component.id ); keysByDOMNode.set(rootNode, key); } morphComponent(component, vComponent); } function morphComponent(component, vComponent) { morphChildren(component._E_, vComponent, component); } var detachedNodes = []; function detachNode(node, parentNode, ownerComponent) { if (node.nodeType === ELEMENT_NODE || node.nodeType === FRAGMENT_NODE) { detachedNodes.push(node); detachedByDOMNode.set(node, ownerComponent || true); } else { destroyNodeRecursive(node); removeChild(node); } } function destroyComponent(component) { component.destroy(); } function morphChildren(fromNode, toNode, parentComponent) { var curFromNodeChild = firstChild(fromNode); var curToNodeChild = toNode.aA_; var curToNodeKey; var curFromNodeKey; var curToNodeType; var fromNextSibling; var toNextSibling; var matchingFromEl; var matchingFromComponent; var curVFromNodeChild; var fromComponent; outer: while (curToNodeChild) { toNextSibling = curToNodeChild.cm_; curToNodeType = curToNodeChild.bX_; curToNodeKey = curToNodeChild.bY_; // Skip <!doctype> if (curFromNodeChild && curFromNodeChild.nodeType === DOCTYPE_NODE) { curFromNodeChild = nextSibling(curFromNodeChild); } var ownerComponent = curToNodeChild._M_ || parentComponent; var referenceComponent; if (curToNodeType === COMPONENT_NODE) { var component = curToNodeChild.s_; if ( (matchingFromComponent = existingComponentLookup[component.id]) === undefined) { if (isHydrate) { var rootNode = beginFragmentNode(curFromNodeChild, fromNode); component._E_ = rootNode; componentByDOMNode.set(rootNode, component); if (ownerComponent && curToNodeKey) { curToNodeKey = normalizeComponentKey( curToNodeKey, parentComponent.id ); addComponentRootToKeyedElements( ownerComponent.K_, curToNodeKey, rootNode, component.id ); keysByDOMNode.set(rootNode, curToNodeKey); } morphComponent(component, curToNodeChild); curFromNodeChild = nextSibling(rootNode); } else { insertVirtualComponentBefore( curToNodeChild, curFromNodeChild, fromNode, component, curToNodeKey, ownerComponent, parentComponent ); } } else { if (matchingFromComponent._E_ !== curFromNodeChild) { if ( curFromNodeChild && ( fromComponent = componentByDOMNode.get(curFromNodeChild)) && globalComponentsContext.q_[ fromComponent.id] === undefined) { // The component associated with the current real DOM node was not rendered // so we should just remove it out of the real DOM by destroying it curFromNodeChild = nextSibling(fromComponent._E_); destroyComponent(fromComponent); continue; } // We need to move the existing component into // the correct location insertBefore( matchingFromComponent._E_, curFromNodeChild, fromNode ); } else { curFromNodeChild = curFromNodeChild && nextSibling(curFromNodeChild); } if (!curToNodeChild.ad_) { morphComponent(component, curToNodeChild); } } curToNodeChild = toNextSibling; continue; } else if (curToNodeKey) { curVFromNodeChild = undefined; curFromNodeKey = undefined; var curToNodeKeyOriginal = curToNodeKey; if (isAutoKey(curToNodeKey)) { if (ownerComponent !== parentComponent) { curToNodeKey += ":" + ownerComponent.id; } referenceComponent = parentComponent; } else { referenceComponent = ownerComponent; } // We have a keyed element. This is the fast path for matching // up elements curToNodeKey = ( keySequences[referenceComponent.id] || ( keySequences[referenceComponent.id] = new KeySequence())).aN_( curToNodeKey); if (curFromNodeChild) { curFromNodeKey = keysByDOMNode.get(curFromNodeChild); curVFromNodeChild = vElementByDOMNode.get(curFromNodeChild); fromNextSibling = nextSibling(curFromNodeChild); } if (curFromNodeKey === curToNodeKey) { // Elements line up. Now we just have to make sure they are compatible if (!curToNodeChild.ad_) { // We just skip over the fromNode if it is preserved if ( curVFromNodeChild && curToNodeType === curVFromNodeChild.bX_ && ( curToNodeType !== ELEMENT_NODE || compareNodeNames(curToNodeChild, curVFromNodeChild))) { if (curToNodeType === ELEMENT_NODE) { morphEl( curFromNodeChild, curVFromNodeChild, curToNodeChild, parentComponent ); } else { morphChildren( curFromNodeChild, curToNodeChild, parentComponent ); } } else { // Remove the old node detachNode(curFromNodeChild, fromNode, ownerComponent); // Incompatible nodes. Just move the target VNode into the DOM at this position insertVirtualNodeBefore( curToNodeChild, curToNodeKey, curFromNodeChild, fromNode, ownerComponent, parentComponent ); } } } else { matchingFromEl = referenceComponent.K_[curToNodeKey]; if ( matchingFromEl === undefined || matchingFromEl === curFromNodeChild) { if (isHydrate && curFromNodeChild) { if ( curFromNodeChild.nodeType === ELEMENT_NODE && ( curToNodeChild.ad_ || caseInsensitiveCompare( curFromNodeChild.nodeName, curToNodeChild.cd_ || "" ))) { curVFromNodeChild = virtualizeElement(curFromNodeChild); curVFromNodeChild.cd_ = curToNodeChild.cd_; keysByDOMNode.set(curFromNodeChild, curToNodeKey); referenceComponent.K_[curToNodeKey] = curFromNodeChild; if (curToNodeChild.ad_) { vElementByDOMNode.set(curFromNodeChild, curVFromNodeChild); } else { morphEl( curFromNodeChild, curVFromNodeChild, curToNodeChild, parentComponent ); } curToNodeChild = toNextSibling; curFromNodeChild = fromNextSibling; continue; } else if ( curToNodeChild.bX_ === FRAGMENT_NODE && curFromNodeChild.nodeType === COMMENT_NODE) { var content = curFromNodeChild.nodeValue; if (content == "F#" + curToNodeKeyOriginal) { var endNode = curFromNodeChild.nextSibling; var depth = 0; var nodeValue; while (true) { if (endNode.nodeType === COMMENT_NODE) { nodeValue = endNode.nodeValue; if (nodeValue === "F/") { if (depth === 0) { break; } else { depth--; } } else if (nodeValue.indexOf("F#") === 0) { depth++; } } endNode = endNode.nextSibling; } var fragment = createFragmentNode( curFromNodeChild, endNode.nextSibling, fromNode ); keysByDOMNode.set(fragment, curToNodeKey); vElementByDOMNode.set(fragment, curToNodeChild); referenceComponent.K_[curToNodeKey] = fragment; removeChild(curFromNodeChild); removeChild(endNode); if (!curToNodeChild.ad_) { morphChildren(fragment, curToNodeChild, parentComponent); } curToNodeChild = toNextSibling; curFromNodeChild = fragment.nextSibling; continue; } } } insertVirtualNodeBefore( curToNodeChild, curToNodeKey, curFromNodeChild, fromNode, ownerComponent, parentComponent ); fromNextSibling = curFromNodeChild; } else { if (detachedByDOMNode.get(matchingFromEl) !== undefined) { detachedByDOMNode.set(matchingFromEl, undefined); } if (!curToNodeChild.ad_) { curVFromNodeChild = vElementByDOMNode.get(matchingFromEl); if ( curVFromNodeChild && curToNodeType === curVFromNodeChild.bX_ && ( curToNodeType !== ELEMENT_NODE || compareNodeNames(curVFromNodeChild, curToNodeChild))) { if (fromNextSibling === matchingFromEl) { // Single element removal: // A <-> A // B <-> C <-- We are here // C D // D // // Single element swap: // A <-> A // B <-> C <-- We are here // C B if ( toNextSibling && toNextSibling.bY_ === curFromNodeKey) { // Single element swap // We want to stay on the current real DOM node fromNextSibling = curFromNodeChild; // But move the matching element into place insertBefore(matchingFromEl, curFromNodeChild, fromNode); } else { // Single element removal // We need to remove the current real DOM node // and the matching real DOM node will fall into // place. We will continue diffing with next sibling // after the real DOM node that just fell into place fromNextSibling = nextSibling(fromNextSibling); if (curFromNodeChild) { detachNode(curFromNodeChild, fromNode, ownerComponent); } } } else { // A <-> A // B <-> D <-- We are here // C // D // We need to move the matching node into place insertAfter(matchingFromEl, curFromNodeChild, fromNode); if (curFromNodeChild) { detachNode(curFromNodeChild, fromNode, ownerComponent); } } if (curToNodeType === ELEMENT_NODE) { morphEl( matchingFromEl, curVFromNodeChild, curToNodeChild, parentComponent ); } else { morphChildren( matchingFromEl, curToNodeChild, parentComponent ); } } else { insertVirtualNodeBefore( curToNodeChild, curToNodeKey, curFromNodeChild, fromNode, ownerComponent, parentComponent ); detachNode(matchingFromEl, fromNode, ownerComponent); } } else { // preserve the node // but still we need to diff the current from node insertBefore(matchingFromEl, curFromNodeChild, fromNode); fromNextSibling = curFromNodeChild; } } } curToNodeChild = toNextSibling; curFromNodeChild = fromNextSibling; continue; } // The know the target node is not a VComponent node and we know // it is also not a preserve node. Let's now match up the HTML // element, text node, comment, etc. while (curFromNodeChild) { fromNextSibling = nextSibling(curFromNodeChild); if (fromComponent = componentByDOMNode.get(curFromNodeChild)) { // The current "to" element is not associated with a component, // but the current "from" element is associated with a component // Even if we destroy the current component in the original // DOM or not, we still need to skip over it since it is // not compatible with the current "to" node curFromNodeChild = fromNextSibling; if ( !globalComponentsContext.q_[fromComponent.id]) { destroyComponent(fromComponent); } continue; // Move to the next "from" node } var curFromNodeType = curFromNodeChild.nodeType; var isCompatible = undefined; if (curFromNodeType === curToNodeType) { if (curFromNodeType === ELEMENT_NODE) { // Both nodes being compared are Element nodes curVFromNodeChild = vElementByDOMNode.get(curFromNodeChild); if (curVFromNodeChild === undefined) { if (isHydrate) { curVFromNodeChild = virtualizeElement(curFromNodeChild); if ( caseInsensitiveCompare( curVFromNodeChild.cd_, curToNodeChild.cd_ )) { curVFromNodeChild.cd_ = curToNodeChild.cd_; } } else { // Skip over nodes that don't look like ours... curFromNodeChild = fromNextSibling; continue; } } else if (curFromNodeKey = curVFromNodeChild.bY_) { // We have a keyed element here but our target VDOM node // is not keyed so this not doesn't belong isCompatible = false; } isCompatible = isCompatible !== false && compareNodeNames(curVFromNodeChild, 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, curVFromNodeChild, curToNodeChild, parentComponent ); } } else if ( curFromNodeType === TEXT_NODE || curFromNodeType === COMMENT_NODE) { // Both nodes being compared are Text or Comment nodes isCompatible = true; var curToNodeValue = curToNodeChild.bW_; var curFromNodeValue = curFromNodeChild.nodeValue; if (curFromNodeValue !== curToNodeValue) { if ( isHydrate && toNextSibling && curFromNodeType === TEXT_NODE && toNextSibling.bX_ === TEXT_NODE && curFromNodeValue.startsWith(curToNodeValue) && curFromNodeValue. slice(curToNodeValue.length). startsWith(toNextSibling.bW_)) { // In hydrate mode we can use splitText to more efficiently handle // adjacent text vdom nodes that were merged. fromNextSibling = curFromNodeChild.splitText( curToNodeValue.length ); } else { // Simply update nodeValue on the original node to // change the text value curFromNodeChild.nodeValue = curToNodeValue; } } } } if (isCompatible === true) { // Advance both the "to" child and the "from" child since we found a match curToNodeChild = toNextSibling; curFromNodeChild = fromNextSibling; continue outer; } detachNode(curFromNodeChild, fromNode, ownerComponent); curFromNodeChild = fromNextSibling; } // END: while (curFromNodeChild) // 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 insertVirtualNodeBefore( curToNodeChild, curToNodeKey, curFromNodeChild, fromNode, ownerComponent, parentComponent ); curToNodeChild = toNextSibling; curFromNodeChild = fromNextSibling; } // We have processed all of the "to nodes". if (fromNode.cp_) { // If we are in an unfinished fragment, we have reached the end of the nodes // we were matching up and need to end the fragment fromNode.cp_(curFromNodeChild); } else { // If curFromNodeChild is non-null then we still have some from nodes // left over that need to be removed var fragmentBoundary = fromNode.nodeType === FRAGMENT_NODE ? fromNode.endNode : null; while (curFromNodeChild && curFromNodeChild !== fragmentBoundary) { fromNextSibling = nextSibling(curFromNodeChild); if (fromComponent = componentByDOMNode.get(curFromNodeChild)) { curFromNodeChild = fromNextSibling; if ( !globalComponentsContext.q_[fromComponent.id]) { destroyComponent(fromComponent); } continue; } curVFromNodeChild = vElementByDOMNode.get(curFromNodeChild); curFromNodeKey = keysByDOMNode.get(fromNode); // For transcluded content, we need to check if the element belongs to a different component // context than the current component and ensure it gets removed from its key index. if (!curFromNodeKey || isAutoKey(curFromNodeKey)) { referenceComponent = parentComponent; } else { referenceComponent = curVFromNodeChild && curVFromNodeChild._M_; } detachNode(curFromNodeChild, fromNode, referenceComponent); curFromNodeChild = fromNextSibling; } } } function morphEl(fromEl, vFromEl, toEl, parentComponent) { var nodeName = toEl.cd_; var constId = toEl.cf_; vElementByDOMNode.set(fromEl, toEl); if (constId !== undefined && vFromEl.cf_ === constId) { return; } morphAttrs(fromEl, vFromEl, toEl); if (toEl.ac_) { return; } if (isTextOnly(nodeName)) { if (toEl.ce_ !== vFromEl.ce_) { if (nodeName === "textarea") { fromEl.value = toEl.ce_; } else { fromEl.textContent = toEl.ce_; } } } else { morphChildren(fromEl, toEl, parentComponent); } } // END: morphEl(...) morphChildren(fromNode, toNode, toNode.s_); detachedNodes.forEach(function (node) { var detachedFromComponent = detachedByDOMNode.get(node); if (detachedFromComponent !== undefined) { detachedByDOMNode.set(node, undefined); var componentToDestroy = componentByDOMNode.get(node); if (componentToDestroy) { componentToDestroy.destroy(); } else if (node.parentNode) { destroyNodeRecursive( node, detachedFromComponent !== true && detachedFromComponent ); if (eventDelegation.an_(node) != false) { removeChild(node); } } } }); } module.exports = morphdom;