관리-도구
편집 파일: morphdom.js
// From Caleb: I had to change all the "isSameNode"s to "isEqualNode"s and now everything is working great! /** * I pulled in my own version of morphdom, so I could tweak it as needed. * Here are the tweaks I've made so far: * * 1) Changed all the "isSameNode"s to "isEqualNode"s so that morhing doesn't check by reference, only by equality. * 2) Automatically filter out any non-"ElementNode"s from the lifecycle hooks. * 3) Tagged other changes with "@livewireModification". */ 'use strict'; import specialElHandlers from './specialElHandlers'; import { compareNodeNames, createElementNS, doc, moveChildren, toElement } from './util'; var ELEMENT_NODE = 1; var DOCUMENT_FRAGMENT_NODE = 11; var TEXT_NODE = 3; var COMMENT_NODE = 8; function noop() {} function defaultGetNodeKey(node) { return node.id; } function callHook(hook, ...params) { if (hook.name !== 'getNodeKey' && hook.name !== 'onBeforeElUpdated') { // console.log(hook.name, ...params) } // Don't call hook on non-"DOMElement" elements. if (typeof params[0].hasAttribute !== 'function') return return hook(...params) } export default function morphdomFactory(morphAttrs) { return function morphdom(fromNode, toNode, options) { if (!options) { options = {}; } if (typeof toNode === 'string') { if (fromNode.nodeName === '#document' || fromNode.nodeName === 'HTML') { var toNodeHtml = toNode; toNode = doc.createElement('html'); toNode.innerHTML = toNodeHtml; } else { toNode = toElement(toNode); } } var getNodeKey = options.getNodeKey || defaultGetNodeKey; var onBeforeNodeAdded = options.onBeforeNodeAdded || noop; var onNodeAdded = options.onNodeAdded || noop; var onBeforeElUpdated = options.onBeforeElUpdated || noop; var onElUpdated = options.onElUpdated || noop; var onBeforeNodeDiscarded = options.onBeforeNodeDiscarded || noop; var onNodeDiscarded = options.onNodeDiscarded || noop; var onBeforeElChildrenUpdated = options.onBeforeElChildrenUpdated || noop; var childrenOnly = options.childrenOnly === true; // This object is used as a lookup to quickly find all keyed elements in the original DOM tree. var fromNodesLookup = Object.create(null); var keyedRemovalList = []; function addKeyedRemoval(key) { keyedRemovalList.push(key); } function walkDiscardedChildNodes(node, skipKeyedNodes) { if (node.nodeType === ELEMENT_NODE) { var curChild = node.firstChild; while (curChild) { var key = undefined; if (skipKeyedNodes && (key = callHook(getNodeKey, curChild))) { // If we are skipping keyed nodes then we add the key // to a list so that it can be handled at the very end. addKeyedRemoval(key); } else { // Only report the node as discarded if it is not keyed. We do this because // at the end we loop through all keyed elements that were unmatched // and then discard them in one final pass. callHook(onNodeDiscarded, curChild); if (curChild.firstChild) { walkDiscardedChildNodes(curChild, skipKeyedNodes); } } curChild = curChild.nextSibling; } } } /** * Removes a DOM node out of the original DOM * * @param {Node} node The node to remove * @param {Node} parentNode The nodes parent * @param {Boolean} skipKeyedNodes If true then elements with keys will be skipped and not discarded. * @return {undefined} */ function removeNode(node, parentNode, skipKeyedNodes) { if (callHook(onBeforeNodeDiscarded, node) === false) { return; } if (parentNode) { parentNode.removeChild(node); } callHook(onNodeDiscarded, node); walkDiscardedChildNodes(node, skipKeyedNodes); } function indexTree(node) { if (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE) { var curChild = node.firstChild; while (curChild) { var key = callHook(getNodeKey, curChild); if (key) { fromNodesLookup[key] = curChild; } // Walk recursively indexTree(curChild); curChild = curChild.nextSibling; } } } indexTree(fromNode); function handleNodeAdded(el) { callHook(onNodeAdded, el); if (el.skipAddingChildren) { return } var curChild = el.firstChild; while (curChild) { var nextSibling = curChild.nextSibling; var key = callHook(getNodeKey, curChild); if (key) { var unmatchedFromEl = fromNodesLookup[key]; if (unmatchedFromEl && compareNodeNames(curChild, unmatchedFromEl)) { curChild.parentNode.replaceChild(unmatchedFromEl, curChild); morphEl(unmatchedFromEl, curChild); } else { handleNodeAdded(curChild); } } else { handleNodeAdded(curChild); } curChild = nextSibling; } } function cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey) { // 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) { var fromNextSibling = curFromNodeChild.nextSibling; if ((curFromNodeKey = callHook(getNodeKey, curFromNodeChild))) { // Since the node is keyed it might be matched up later so we defer // the actual removal to later addKeyedRemoval(curFromNodeKey); } else { // NOTE: we skip nested keyed nodes from being removed since there is // still a chance they will be matched up later removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */); } curFromNodeChild = fromNextSibling; } } function morphEl(fromEl, toEl, childrenOnly) { var toElKey = callHook(getNodeKey, toEl); if (toElKey) { // If an element with an ID is being morphed then it will be in the final // DOM so clear it out of the saved elements collection delete fromNodesLookup[toElKey]; } if (!childrenOnly) { if (callHook(onBeforeElUpdated, fromEl, toEl) === false) { return; } // @livewireModification. // I added this check to enable wire:ignore.self to not fire // morphAttrs, but not skip updating children as well. // A task that's currently impossible with the provided hooks. if (! fromEl.skipElUpdatingButStillUpdateChildren) { morphAttrs(fromEl, toEl); } callHook(onElUpdated, fromEl); if (callHook(onBeforeElChildrenUpdated, fromEl, toEl) === false) { return; } } if (fromEl.nodeName !== 'TEXTAREA') { morphChildren(fromEl, toEl); } else { if (fromEl.innerHTML != toEl.innerHTML) { // @livewireModification // Only mess with the "value" of textarea if the new dom has something // inside the <textarea></textarea> tag. specialElHandlers.TEXTAREA(fromEl, toEl); } } } function morphChildren(fromEl, toEl) { var curToNodeChild = toEl.firstChild; var curFromNodeChild = fromEl.firstChild; var curToNodeKey; var curFromNodeKey; var fromNextSibling; var toNextSibling; var matchingFromEl; // walk the children outer: while (curToNodeChild) { toNextSibling = curToNodeChild.nextSibling; curToNodeKey = callHook(getNodeKey, curToNodeChild); // walk the fromNode children all the way through while (curFromNodeChild) { fromNextSibling = curFromNodeChild.nextSibling; if (curToNodeChild.isSameNode && curToNodeChild.isSameNode(curFromNodeChild)) { curToNodeChild = toNextSibling; curFromNodeChild = fromNextSibling; continue outer; } curFromNodeKey = callHook(getNodeKey, curFromNodeChild); var curFromNodeType = curFromNodeChild.nodeType; // this means if the curFromNodeChild doesnt have a match with the curToNodeChild 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 = fromNodesLookup[curToNodeKey])) { if (fromNextSibling === 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 move 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); // fromNextSibling = curFromNodeChild.nextSibling; if (curFromNodeKey) { // Since the node is keyed it might be matched up later so we defer // the actual removal to later addKeyedRemoval(curFromNodeKey); } else { // NOTE: we skip nested keyed nodes from being removed since there is // still a chance they will be matched up later removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */); } 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); if (isCompatible) { // @livewireModification // If the two nodes are different, but the next element is an exact match, // we can assume that the new node is meant to be inserted, instead of // used as a morph target. if ( ! curToNodeChild.isEqualNode(curFromNodeChild) && curToNodeChild.nextElementSibling && curToNodeChild.nextElementSibling.isEqualNode(curFromNodeChild) ) { isCompatible = false } else { // We found compatible DOM elements so transform // the current "from" node to match the current // target DOM node. // MORPH morphEl(curFromNodeChild, curToNodeChild); } } } 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 if (curFromNodeChild.nodeValue !== curToNodeChild.nodeValue) { curFromNodeChild.nodeValue = curToNodeChild.nodeValue; } } } if (isCompatible) { // Advance both the "to" child and the "from" child since we found a match // Nothing else to do as we already recursively called morphChildren above curToNodeChild = toNextSibling; curFromNodeChild = fromNextSibling; continue outer; } // @livewireModification // Before we just remove the original element, let's see if it's the very next // element in the "to" list. If it is, we can assume we can insert the new // element before the original one instead of removing it. This is kind of // a "look-ahead". if (curToNodeChild.nextElementSibling && curToNodeChild.nextElementSibling.isEqualNode(curFromNodeChild)) { const nodeToBeAdded = curToNodeChild.cloneNode(true) fromEl.insertBefore(nodeToBeAdded, curFromNodeChild) handleNodeAdded(nodeToBeAdded) curToNodeChild = curToNodeChild.nextElementSibling.nextSibling; curFromNodeChild = fromNextSibling; continue outer; } else { // 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 if (curFromNodeKey) { // Since the node is keyed it might be matched up later so we defer // the actual removal to later addKeyedRemoval(curFromNodeKey); } else { // NOTE: we skip nested keyed nodes from being removed since there is // still a chance they will be matched up later removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */); } } 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 if (curToNodeKey && (matchingFromEl = fromNodesLookup[curToNodeKey]) && compareNodeNames(matchingFromEl, curToNodeChild)) { fromEl.appendChild(matchingFromEl); // MORPH morphEl(matchingFromEl, curToNodeChild); } else { var onBeforeNodeAddedResult = callHook(onBeforeNodeAdded, curToNodeChild); if (onBeforeNodeAddedResult !== false) { if (onBeforeNodeAddedResult) { curToNodeChild = onBeforeNodeAddedResult; } if (curToNodeChild.actualize) { curToNodeChild = curToNodeChild.actualize(fromEl.ownerDocument || doc); } fromEl.appendChild(curToNodeChild); handleNodeAdded(curToNodeChild); } } curToNodeChild = toNextSibling; curFromNodeChild = fromNextSibling; } cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey); var specialElHandler = specialElHandlers[fromEl.nodeName]; if (specialElHandler && ! fromEl.isLivewireModel) { specialElHandler(fromEl, toEl); } } // END: morphChildren(...) var morphedNode = fromNode; var morphedNodeType = morphedNode.nodeType; var toNodeType = toNode.nodeType; if (!childrenOnly) { // Handle the case where we are given two DOM nodes that are not // compatible (e.g. <div> --> <span> or <div> --> TEXT) if (morphedNodeType === ELEMENT_NODE) { if (toNodeType === ELEMENT_NODE) { if (!compareNodeNames(fromNode, toNode)) { callHook(onNodeDiscarded, fromNode); morphedNode = moveChildren(fromNode, createElementNS(toNode.nodeName, toNode.namespaceURI)); } } else { // Going from an element node to a text node morphedNode = toNode; } } else if (morphedNodeType === TEXT_NODE || morphedNodeType === COMMENT_NODE) { // Text or comment node if (toNodeType === morphedNodeType) { if (morphedNode.nodeValue !== toNode.nodeValue) { morphedNode.nodeValue = toNode.nodeValue; } return morphedNode; } else { // Text node to something else morphedNode = toNode; } } } if (morphedNode === toNode) { // The "to node" was not compatible with the "from node" so we had to // toss out the "from node" and use the "to node" callHook(onNodeDiscarded, fromNode); } else { if (toNode.isSameNode && toNode.isSameNode(morphedNode)) { return; } morphEl(morphedNode, toNode, childrenOnly); // 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 if (keyedRemovalList) { for (var i=0, len=keyedRemovalList.length; i<len; i++) { var elToRemove = fromNodesLookup[keyedRemovalList[i]]; if (elToRemove) { removeNode(elToRemove, elToRemove.parentNode, false); } } } } if (!childrenOnly && morphedNode !== fromNode && fromNode.parentNode) { if (morphedNode.actualize) { morphedNode = morphedNode.actualize(fromNode.ownerDocument || doc); } // If we had to swap out the from node with a new node because the old // node was not compatible with the target node then we need to // replace the old DOM node in the original DOM tree. This is only // possible if the original DOM node was part of a DOM tree which // we know is the case if it has a parent node. fromNode.parentNode.replaceChild(morphedNode, fromNode); } return morphedNode; }; }