mirror of
https://github.com/marko-js/marko.git
synced 2025-12-08 19:26:05 +00:00
329 lines
13 KiB
JavaScript
329 lines
13 KiB
JavaScript
'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. <div> --> <span> or <div> --> 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<len; i++) {
|
|
var node = removalList[i];
|
|
var key = node.id;
|
|
if (!key || foundKeys[key] === undefined) {
|
|
if (onBeforeNodeDiscarded(node) == false) {
|
|
continue;
|
|
}
|
|
|
|
var parentNode = node.parentNode;
|
|
if (parentNode) {
|
|
parentNode.removeChild(node);
|
|
}
|
|
|
|
walkDiscardedChildNodes(node);
|
|
}
|
|
}
|
|
|
|
return newNode || morphedNode;
|
|
}
|
|
|
|
module.exports = morphdom;
|