UNPKG

marko

Version:

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

742 lines (650 loc) • 25.1 kB
"use strict"; var componentsUtil = require("@internal/components-util"); var existingComponentLookup = componentsUtil.___componentLookup; var destroyNodeRecursive = componentsUtil.___destroyNodeRecursive; var addComponentRootToKeyedElements = componentsUtil.___addComponentRootToKeyedElements; var normalizeComponentKey = componentsUtil.___normalizeComponentKey; var domData = require("../../components/dom-data"); var eventDelegation = require("../../components/event-delegation"); var KeySequence = require("../../components/KeySequence"); var VElement = require("../vdom").___VElement; var fragment = require("./fragment"); var helpers = require("./helpers"); var isTextOnly = require("../is-text-only"); var virtualizeElement = VElement.___virtualize; var morphAttrs = VElement.___morphAttrs; var keysByDOMNode = domData.___keyByDOMNode; var componentByDOMNode = domData.___componentByDOMNode; var vElementByDOMNode = domData.___vElementByDOMNode; var detachedByDOMNode = domData.___detachedByDOMNode; var insertBefore = helpers.___insertBefore; var insertAfter = helpers.___insertAfter; var nextSibling = helpers.___nextSibling; var firstChild = helpers.___firstChild; var removeChild = helpers.___removeChild; var createFragmentNode = fragment.___createFragmentNode; var beginFragmentNode = fragment.___beginFragmentNode; 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.___nodeName === toEl.___nodeName; } function caseInsensitiveCompare(a, b) { return a.toLowerCase() === b.toLowerCase(); } function onNodeAdded(node, componentsContext) { if (node.nodeType === ELEMENT_NODE) { eventDelegation.___handleNodeAttach(node, componentsContext); } } function morphdom(fromNode, toNode, host, componentsContext) { var globalComponentsContext; var isHydrate = false; var keySequences = Object.create(null); if (componentsContext) { globalComponentsContext = componentsContext.___globalContext; isHydrate = globalComponentsContext.___isHydrate; } function insertVirtualNodeBefore( vNode, key, referenceEl, parentEl, ownerComponent, parentComponent, ) { var realNode = vNode.___actualize(host, parentEl.namespaceURI); insertBefore(realNode, referenceEl, parentEl); if ( vNode.___nodeType === ELEMENT_NODE || vNode.___nodeType === FRAGMENT_NODE ) { if (key) { keysByDOMNode.set(realNode, key); (isAutoKey(key) ? parentComponent : ownerComponent).___keyedElements[ key ] = realNode; } if (!isTextOnly(vNode.___nodeName)) { morphChildren(realNode, vNode, parentComponent); } onNodeAdded(realNode, componentsContext); } } function insertVirtualComponentBefore( vComponent, referenceNode, referenceNodeParentEl, component, key, ownerComponent, parentComponent, ) { var rootNode = (component.___rootNode = insertBefore( createFragmentNode(), referenceNode, referenceNodeParentEl, )); componentByDOMNode.set(rootNode, component); if (key && ownerComponent) { key = normalizeComponentKey(key, parentComponent.id); addComponentRootToKeyedElements( ownerComponent.___keyedElements, key, rootNode, component.id, ); keysByDOMNode.set(rootNode, key); } morphComponent(component, vComponent); } function morphComponent(component, vComponent) { morphChildren(component.___rootNode, 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.___firstChild; var curToNodeKey; var curFromNodeKey; var curToNodeType; var fromNextSibling; var toNextSibling; var matchingFromEl; var matchingFromComponent; var curVFromNodeChild; var fromComponent; outer: while (curToNodeChild) { toNextSibling = curToNodeChild.___nextSibling; curToNodeType = curToNodeChild.___nodeType; curToNodeKey = curToNodeChild.___key; // Skip <!doctype> if (curFromNodeChild && curFromNodeChild.nodeType === DOCTYPE_NODE) { curFromNodeChild = nextSibling(curFromNodeChild); } var ownerComponent = curToNodeChild.___ownerComponent || parentComponent; var referenceComponent; if (curToNodeType === COMPONENT_NODE) { var component = curToNodeChild.___component; if ( (matchingFromComponent = existingComponentLookup[component.id]) === undefined ) { if (isHydrate) { var rootNode = beginFragmentNode(curFromNodeChild, fromNode); component.___rootNode = rootNode; componentByDOMNode.set(rootNode, component); if (ownerComponent && curToNodeKey) { curToNodeKey = normalizeComponentKey( curToNodeKey, parentComponent.id, ); addComponentRootToKeyedElements( ownerComponent.___keyedElements, 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.___rootNode !== curFromNodeChild) { if ( curFromNodeChild && (fromComponent = componentByDOMNode.get(curFromNodeChild)) && globalComponentsContext.___renderedComponentsById[ 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.___rootNode); destroyComponent(fromComponent); continue; } // We need to move the existing component into // the correct location insertBefore( matchingFromComponent.___rootNode, curFromNodeChild, fromNode, ); } else { curFromNodeChild = curFromNodeChild && nextSibling(curFromNodeChild); } if (!curToNodeChild.___preserve) { 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()) ).___nextKey(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.___preserve) { // We just skip over the fromNode if it is preserved if ( curVFromNodeChild && curToNodeType === curVFromNodeChild.___nodeType && (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.___keyedElements[curToNodeKey]; if ( matchingFromEl === undefined || matchingFromEl === curFromNodeChild ) { if (isHydrate && curFromNodeChild) { if ( curFromNodeChild.nodeType === ELEMENT_NODE && (curToNodeChild.___preserve || caseInsensitiveCompare( curFromNodeChild.nodeName, curToNodeChild.___nodeName || "", )) ) { curVFromNodeChild = virtualizeElement(curFromNodeChild); curVFromNodeChild.___nodeName = curToNodeChild.___nodeName; keysByDOMNode.set(curFromNodeChild, curToNodeKey); referenceComponent.___keyedElements[curToNodeKey] = curFromNodeChild; if (curToNodeChild.___preserve) { vElementByDOMNode.set(curFromNodeChild, curVFromNodeChild); } else { morphEl( curFromNodeChild, curVFromNodeChild, curToNodeChild, parentComponent, ); } curToNodeChild = toNextSibling; curFromNodeChild = fromNextSibling; continue; } else if ( curToNodeChild.___nodeType === 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.___keyedElements[curToNodeKey] = fragment; removeChild(curFromNodeChild); removeChild(endNode); if (!curToNodeChild.___preserve) { 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.___preserve) { curVFromNodeChild = vElementByDOMNode.get(matchingFromEl); if ( curVFromNodeChild && curToNodeType === curVFromNodeChild.___nodeType && (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.___key === 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.___renderedComponentsById[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.___nodeName, curToNodeChild.___nodeName, ) ) { curVFromNodeChild.___nodeName = curToNodeChild.___nodeName; } } else { // Skip over nodes that don't look like ours... curFromNodeChild = fromNextSibling; continue; } } else if ((curFromNodeKey = curVFromNodeChild.___key)) { // 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.___nodeValue; var curFromNodeValue = curFromNodeChild.nodeValue; if (curFromNodeValue !== curToNodeValue) { if ( isHydrate && toNextSibling && curFromNodeType === TEXT_NODE && toNextSibling.___nodeType === TEXT_NODE && curFromNodeValue.startsWith(curToNodeValue) && curFromNodeValue .slice(curToNodeValue.length) .startsWith(toNextSibling.___nodeValue) ) { // 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.___finishFragment) { // 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.___finishFragment(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.___renderedComponentsById[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.___ownerComponent; } detachNode(curFromNodeChild, fromNode, referenceComponent); curFromNodeChild = fromNextSibling; } } } function morphEl(fromEl, vFromEl, toEl, parentComponent) { var nodeName = toEl.___nodeName; var constId = toEl.___constId; vElementByDOMNode.set(fromEl, toEl); if (constId !== undefined && vFromEl.___constId === constId) { return; } morphAttrs(fromEl, vFromEl, toEl); if (toEl.___preserveBody) { return; } if (isTextOnly(nodeName)) { if (toEl.___textContent !== vFromEl.___textContent) { if (nodeName === "textarea") { fromEl.value = toEl.___textContent; } else { fromEl.textContent = toEl.___textContent; } } } else { morphChildren(fromEl, toEl, parentComponent); } } // END: morphEl(...) morphChildren(fromNode, toNode, toNode.___component); 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.___handleNodeDetach(node) != false) { removeChild(node); } } } }); } module.exports = morphdom;