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._n_; var destroyNodeRecursive = componentsUtil._T_; var addComponentRootToKeyedElements = componentsUtil._o_; var normalizeComponentKey = componentsUtil._W_; var domData = require("../../components/dom-data"); var eventDelegation = require("../../components/event-delegation"); var KeySequence = require("../../components/KeySequence"); var VElement = require("../vdom").bz_; var fragment = require("./fragment"); var helpers = require("./helpers"); var isTextOnly = require("../is-text-only"); var virtualizeElement = VElement.ck_; var morphAttrs = VElement.cl_; var keysByDOMNode = domData._p_; var componentByDOMNode = domData._r_; var vElementByDOMNode = domData._M_; var detachedByDOMNode = domData.aY_; var insertBefore = helpers.bh_; var insertAfter = helpers.bi_; var nextSibling = helpers.cp_; var firstChild = helpers.aC_; var removeChild = helpers.bj_; var createFragmentNode = fragment._m_; var beginFragmentNode = fragment.ct_; 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.cg_ === toEl.cg_; } function caseInsensitiveCompare(a, b) { return a.toLowerCase() === b.toLowerCase(); } function onNodeAdded(node, componentsContext) { if (node.nodeType === ELEMENT_NODE) { eventDelegation.aX_(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.aa_; } function insertVirtualNodeBefore( vNode, key, referenceEl, parentEl, ownerComponent, parentComponent) { var realNode = vNode.br_(host, parentEl.namespaceURI); insertBefore(realNode, referenceEl, parentEl); if ( vNode.c__ === ELEMENT_NODE || vNode.c__ === FRAGMENT_NODE) { if (key) { keysByDOMNode.set(realNode, key); (isAutoKey(key) ? parentComponent : ownerComponent).L_[ key] = realNode; } if (!isTextOnly(vNode.cg_)) { morphChildren(realNode, vNode, parentComponent); } onNodeAdded(realNode, componentsContext); } } function insertVirtualComponentBefore( vComponent, referenceNode, referenceNodeParentEl, component, key, ownerComponent, parentComponent) { var rootNode = component._G_ = insertBefore( createFragmentNode(), referenceNode, referenceNodeParentEl ); componentByDOMNode.set(rootNode, component); if (key && ownerComponent) { key = normalizeComponentKey(key, parentComponent.id); addComponentRootToKeyedElements( ownerComponent.L_, key, rootNode, component.id ); keysByDOMNode.set(rootNode, key); } morphComponent(component, vComponent); } function morphComponent(component, vComponent) { morphChildren(component._G_, 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.aC_; var curToNodeKey; var curFromNodeKey; var curToNodeType; var fromNextSibling; var toNextSibling; var matchingFromEl; var matchingFromComponent; var curVFromNodeChild; var fromComponent; outer: while (curToNodeChild) { toNextSibling = curToNodeChild.cp_; curToNodeType = curToNodeChild.c__; curToNodeKey = curToNodeChild.ca_; // Skip <!doctype> if (curFromNodeChild && curFromNodeChild.nodeType === DOCTYPE_NODE) { curFromNodeChild = nextSibling(curFromNodeChild); } var ownerComponent = curToNodeChild._O_ || 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._G_ = rootNode; componentByDOMNode.set(rootNode, component); if (ownerComponent && curToNodeKey) { curToNodeKey = normalizeComponentKey( curToNodeKey, parentComponent.id ); addComponentRootToKeyedElements( ownerComponent.L_, 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._G_ !== 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._G_); destroyComponent(fromComponent); continue; } // We need to move the existing component into // the correct location insertBefore( matchingFromComponent._G_, curFromNodeChild, fromNode ); } else { curFromNodeChild = curFromNodeChild && nextSibling(curFromNodeChild); } if (!curToNodeChild.af_) { 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())).aP_( 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.af_) { // We just skip over the fromNode if it is preserved if ( curVFromNodeChild && curToNodeType === curVFromNodeChild.c__ && ( 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.L_[curToNodeKey]; if ( matchingFromEl === undefined || matchingFromEl === curFromNodeChild) { if (isHydrate && curFromNodeChild) { if ( curFromNodeChild.nodeType === ELEMENT_NODE && ( curToNodeChild.af_ || caseInsensitiveCompare( curFromNodeChild.nodeName, curToNodeChild.cg_ || "" ))) { curVFromNodeChild = virtualizeElement(curFromNodeChild); curVFromNodeChild.cg_ = curToNodeChild.cg_; keysByDOMNode.set(curFromNodeChild, curToNodeKey); referenceComponent.L_[curToNodeKey] = curFromNodeChild; if (curToNodeChild.af_) { vElementByDOMNode.set(curFromNodeChild, curVFromNodeChild); } else { morphEl( curFromNodeChild, curVFromNodeChild, curToNodeChild, parentComponent ); } curToNodeChild = toNextSibling; curFromNodeChild = fromNextSibling; continue; } else if ( curToNodeChild.c__ === 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.L_[curToNodeKey] = fragment; removeChild(curFromNodeChild); removeChild(endNode); if (!curToNodeChild.af_) { 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.af_) { curVFromNodeChild = vElementByDOMNode.get(matchingFromEl); if ( curVFromNodeChild && curToNodeType === curVFromNodeChild.c__ && ( 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.ca_ === 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.cg_, curToNodeChild.cg_ )) { curVFromNodeChild.cg_ = curToNodeChild.cg_; } } else { // Skip over nodes that don't look like ours... curFromNodeChild = fromNextSibling; continue; } } else if (curFromNodeKey = curVFromNodeChild.ca_) { // 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.bZ_; var curFromNodeValue = curFromNodeChild.nodeValue; if (curFromNodeValue !== curToNodeValue) { if ( isHydrate && toNextSibling && curFromNodeType === TEXT_NODE && toNextSibling.c__ === TEXT_NODE && curFromNodeValue.startsWith(curToNodeValue) && curFromNodeValue. slice(curToNodeValue.length). startsWith(toNextSibling.bZ_)) { // 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.cs_) { // 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.cs_(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._O_; } detachNode(curFromNodeChild, fromNode, referenceComponent); curFromNodeChild = fromNextSibling; } } } function morphEl(fromEl, vFromEl, toEl, parentComponent) { var nodeName = toEl.cg_; var constId = toEl.ci_; vElementByDOMNode.set(fromEl, toEl); if (constId !== undefined && vFromEl.ci_ === constId) { return; } morphAttrs(fromEl, vFromEl, toEl); if (toEl.ae_) { return; } if (isTextOnly(nodeName)) { if (toEl.ch_ !== vFromEl.ch_) { if (nodeName === "textarea") { fromEl.value = toEl.ch_; } else { fromEl.textContent = toEl.ch_; } } } 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.ap_(node) != false) { removeChild(node); } } } }); } module.exports = morphdom;