marko
Version:
UI Components + streaming, async, high performance, HTML templating for Node.js and the browser.
741 lines (650 loc) • 23.9 kB
JavaScript
"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;