marko/morphdom/index.js

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;