/ static / morphdom.js
morphdom.js
  1  'use strict';
  2  
  3  var DOCUMENT_FRAGMENT_NODE = 11;
  4  
  5  function morphAttrs(fromNode, toNode) {
  6      var toNodeAttrs = toNode.attributes;
  7      var attr;
  8      var attrName;
  9      var attrNamespaceURI;
 10      var attrValue;
 11      var fromValue;
 12  
 13      // document-fragments dont have attributes so lets not do anything
 14      if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE || fromNode.nodeType === DOCUMENT_FRAGMENT_NODE) {
 15        return;
 16      }
 17  
 18      // update attributes on original DOM element
 19      for (var i = toNodeAttrs.length - 1; i >= 0; i--) {
 20          attr = toNodeAttrs[i];
 21          attrName = attr.name;
 22          attrNamespaceURI = attr.namespaceURI;
 23          attrValue = attr.value;
 24  
 25          if (attrNamespaceURI) {
 26              attrName = attr.localName || attrName;
 27              fromValue = fromNode.getAttributeNS(attrNamespaceURI, attrName);
 28  
 29              if (fromValue !== attrValue) {
 30                  if (attr.prefix === 'xmlns'){
 31                      attrName = attr.name; // It's not allowed to set an attribute with the XMLNS namespace without specifying the `xmlns` prefix
 32                  }
 33                  fromNode.setAttributeNS(attrNamespaceURI, attrName, attrValue);
 34              }
 35          } else {
 36              fromValue = fromNode.getAttribute(attrName);
 37  
 38              if (fromValue !== attrValue) {
 39                  fromNode.setAttribute(attrName, attrValue);
 40              }
 41          }
 42      }
 43  
 44      // Remove any extra attributes found on the original DOM element that
 45      // weren't found on the target element.
 46      var fromNodeAttrs = fromNode.attributes;
 47  
 48      for (var d = fromNodeAttrs.length - 1; d >= 0; d--) {
 49          attr = fromNodeAttrs[d];
 50          attrName = attr.name;
 51          attrNamespaceURI = attr.namespaceURI;
 52  
 53          if (attrNamespaceURI) {
 54              attrName = attr.localName || attrName;
 55  
 56              if (!toNode.hasAttributeNS(attrNamespaceURI, attrName)) {
 57                  fromNode.removeAttributeNS(attrNamespaceURI, attrName);
 58              }
 59          } else {
 60              if (!toNode.hasAttribute(attrName)) {
 61                  fromNode.removeAttribute(attrName);
 62              }
 63          }
 64      }
 65  }
 66  
 67  var range; // Create a range object for efficently rendering strings to elements.
 68  var NS_XHTML = 'http://www.w3.org/1999/xhtml';
 69  
 70  var doc = typeof document === 'undefined' ? undefined : document;
 71  var HAS_TEMPLATE_SUPPORT = !!doc && 'content' in doc.createElement('template');
 72  var HAS_RANGE_SUPPORT = !!doc && doc.createRange && 'createContextualFragment' in doc.createRange();
 73  
 74  function createFragmentFromTemplate(str) {
 75      var template = doc.createElement('template');
 76      template.innerHTML = str;
 77      return template.content.childNodes[0];
 78  }
 79  
 80  function createFragmentFromRange(str) {
 81      if (!range) {
 82          range = doc.createRange();
 83          range.selectNode(doc.body);
 84      }
 85  
 86      var fragment = range.createContextualFragment(str);
 87      return fragment.childNodes[0];
 88  }
 89  
 90  function createFragmentFromWrap(str) {
 91      var fragment = doc.createElement('body');
 92      fragment.innerHTML = str;
 93      return fragment.childNodes[0];
 94  }
 95  
 96  /**
 97   * This is about the same
 98   * var html = new DOMParser().parseFromString(str, 'text/html');
 99   * return html.body.firstChild;
100   *
101   * @method toElement
102   * @param {String} str
103   */
104  function toElement(str) {
105      str = str.trim();
106      if (HAS_TEMPLATE_SUPPORT) {
107        // avoid restrictions on content for things like `<tr><th>Hi</th></tr>` which
108        // createContextualFragment doesn't support
109        // <template> support not available in IE
110        return createFragmentFromTemplate(str);
111      } else if (HAS_RANGE_SUPPORT) {
112        return createFragmentFromRange(str);
113      }
114  
115      return createFragmentFromWrap(str);
116  }
117  
118  /**
119   * Returns true if two node's names are the same.
120   *
121   * NOTE: We don't bother checking `namespaceURI` because you will never find two HTML elements with the same
122   *       nodeName and different namespace URIs.
123   *
124   * @param {Element} a
125   * @param {Element} b The target element
126   * @return {boolean}
127   */
128  function compareNodeNames(fromEl, toEl) {
129      var fromNodeName = fromEl.nodeName;
130      var toNodeName = toEl.nodeName;
131      var fromCodeStart, toCodeStart;
132  
133      if (fromNodeName === toNodeName) {
134          return true;
135      }
136  
137      fromCodeStart = fromNodeName.charCodeAt(0);
138      toCodeStart = toNodeName.charCodeAt(0);
139  
140      // If the target element is a virtual DOM node or SVG node then we may
141      // need to normalize the tag name before comparing. Normal HTML elements that are
142      // in the "http://www.w3.org/1999/xhtml"
143      // are converted to upper case
144      if (fromCodeStart <= 90 && toCodeStart >= 97) { // from is upper and to is lower
145          return fromNodeName === toNodeName.toUpperCase();
146      } else if (toCodeStart <= 90 && fromCodeStart >= 97) { // to is upper and from is lower
147          return toNodeName === fromNodeName.toUpperCase();
148      } else {
149          return false;
150      }
151  }
152  
153  /**
154   * Create an element, optionally with a known namespace URI.
155   *
156   * @param {string} name the element name, e.g. 'div' or 'svg'
157   * @param {string} [namespaceURI] the element's namespace URI, i.e. the value of
158   * its `xmlns` attribute or its inferred namespace.
159   *
160   * @return {Element}
161   */
162  function createElementNS(name, namespaceURI) {
163      return !namespaceURI || namespaceURI === NS_XHTML ?
164          doc.createElement(name) :
165          doc.createElementNS(namespaceURI, name);
166  }
167  
168  /**
169   * Copies the children of one DOM element to another DOM element
170   */
171  function moveChildren(fromEl, toEl) {
172      var curChild = fromEl.firstChild;
173      while (curChild) {
174          var nextChild = curChild.nextSibling;
175          toEl.appendChild(curChild);
176          curChild = nextChild;
177      }
178      return toEl;
179  }
180  
181  function syncBooleanAttrProp(fromEl, toEl, name) {
182      if (fromEl[name] !== toEl[name]) {
183          fromEl[name] = toEl[name];
184          if (fromEl[name]) {
185              fromEl.setAttribute(name, '');
186          } else {
187              fromEl.removeAttribute(name);
188          }
189      }
190  }
191  
192  var specialElHandlers = {
193      OPTION: function(fromEl, toEl) {
194          var parentNode = fromEl.parentNode;
195          if (parentNode) {
196              var parentName = parentNode.nodeName.toUpperCase();
197              if (parentName === 'OPTGROUP') {
198                  parentNode = parentNode.parentNode;
199                  parentName = parentNode && parentNode.nodeName.toUpperCase();
200              }
201              if (parentName === 'SELECT' && !parentNode.hasAttribute('multiple')) {
202                  if (fromEl.hasAttribute('selected') && !toEl.selected) {
203                      // Workaround for MS Edge bug where the 'selected' attribute can only be
204                      // removed if set to a non-empty value:
205                      // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12087679/
206                      fromEl.setAttribute('selected', 'selected');
207                      fromEl.removeAttribute('selected');
208                  }
209                  // We have to reset select element's selectedIndex to -1, otherwise setting
210                  // fromEl.selected using the syncBooleanAttrProp below has no effect.
211                  // The correct selectedIndex will be set in the SELECT special handler below.
212                  parentNode.selectedIndex = -1;
213              }
214          }
215          syncBooleanAttrProp(fromEl, toEl, 'selected');
216      },
217      /**
218       * The "value" attribute is special for the <input> element since it sets
219       * the initial value. Changing the "value" attribute without changing the
220       * "value" property will have no effect since it is only used to the set the
221       * initial value.  Similar for the "checked" attribute, and "disabled".
222       */
223      INPUT: function(fromEl, toEl) {
224          syncBooleanAttrProp(fromEl, toEl, 'checked');
225          syncBooleanAttrProp(fromEl, toEl, 'disabled');
226  
227          if (fromEl.value !== toEl.value) {
228              fromEl.value = toEl.value;
229          }
230  
231          if (!toEl.hasAttribute('value')) {
232              fromEl.removeAttribute('value');
233          }
234      },
235  
236      TEXTAREA: function(fromEl, toEl) {
237          var newValue = toEl.value;
238          if (fromEl.value !== newValue) {
239              fromEl.value = newValue;
240          }
241  
242          var firstChild = fromEl.firstChild;
243          if (firstChild) {
244              // Needed for IE. Apparently IE sets the placeholder as the
245              // node value and vise versa. This ignores an empty update.
246              var oldValue = firstChild.nodeValue;
247  
248              if (oldValue == newValue || (!newValue && oldValue == fromEl.placeholder)) {
249                  return;
250              }
251  
252              firstChild.nodeValue = newValue;
253          }
254      },
255      SELECT: function(fromEl, toEl) {
256          if (!toEl.hasAttribute('multiple')) {
257              var selectedIndex = -1;
258              var i = 0;
259              // We have to loop through children of fromEl, not toEl since nodes can be moved
260              // from toEl to fromEl directly when morphing.
261              // At the time this special handler is invoked, all children have already been morphed
262              // and appended to / removed from fromEl, so using fromEl here is safe and correct.
263              var curChild = fromEl.firstChild;
264              var optgroup;
265              var nodeName;
266              while(curChild) {
267                  nodeName = curChild.nodeName && curChild.nodeName.toUpperCase();
268                  if (nodeName === 'OPTGROUP') {
269                      optgroup = curChild;
270                      curChild = optgroup.firstChild;
271                  } else {
272                      if (nodeName === 'OPTION') {
273                          if (curChild.hasAttribute('selected')) {
274                              selectedIndex = i;
275                              break;
276                          }
277                          i++;
278                      }
279                      curChild = curChild.nextSibling;
280                      if (!curChild && optgroup) {
281                          curChild = optgroup.nextSibling;
282                          optgroup = null;
283                      }
284                  }
285              }
286  
287              fromEl.selectedIndex = selectedIndex;
288          }
289      }
290  };
291  
292  var ELEMENT_NODE = 1;
293  var DOCUMENT_FRAGMENT_NODE$1 = 11;
294  var TEXT_NODE = 3;
295  var COMMENT_NODE = 8;
296  
297  function noop() {}
298  
299  function defaultGetNodeKey(node) {
300    if (node) {
301      return (node.getAttribute && node.getAttribute('id')) || node.id;
302    }
303  }
304  
305  function morphdomFactory(morphAttrs) {
306  
307    return function morphdom(fromNode, toNode, options) {
308      if (!options) {
309        options = {};
310      }
311  
312      if (typeof toNode === 'string') {
313        if (fromNode.nodeName === '#document' || fromNode.nodeName === 'HTML' || fromNode.nodeName === 'BODY') {
314          var toNodeHtml = toNode;
315          toNode = doc.createElement('html');
316          toNode.innerHTML = toNodeHtml;
317        } else {
318          toNode = toElement(toNode);
319        }
320      } else if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE$1) {
321        toNode = toNode.firstElementChild;
322      }
323  
324      var getNodeKey = options.getNodeKey || defaultGetNodeKey;
325      var onBeforeNodeAdded = options.onBeforeNodeAdded || noop;
326      var onNodeAdded = options.onNodeAdded || noop;
327      var onBeforeElUpdated = options.onBeforeElUpdated || noop;
328      var onElUpdated = options.onElUpdated || noop;
329      var onBeforeNodeDiscarded = options.onBeforeNodeDiscarded || noop;
330      var onNodeDiscarded = options.onNodeDiscarded || noop;
331      var onBeforeElChildrenUpdated = options.onBeforeElChildrenUpdated || noop;
332      var skipFromChildren = options.skipFromChildren || noop;
333      var addChild = options.addChild || function(parent, child){ return parent.appendChild(child); };
334      var childrenOnly = options.childrenOnly === true;
335  
336      // This object is used as a lookup to quickly find all keyed elements in the original DOM tree.
337      var fromNodesLookup = Object.create(null);
338      var keyedRemovalList = [];
339  
340      function addKeyedRemoval(key) {
341        keyedRemovalList.push(key);
342      }
343  
344      function walkDiscardedChildNodes(node, skipKeyedNodes) {
345        if (node.nodeType === ELEMENT_NODE) {
346          var curChild = node.firstChild;
347          while (curChild) {
348  
349            var key = undefined;
350  
351            if (skipKeyedNodes && (key = getNodeKey(curChild))) {
352              // If we are skipping keyed nodes then we add the key
353              // to a list so that it can be handled at the very end.
354              addKeyedRemoval(key);
355            } else {
356              // Only report the node as discarded if it is not keyed. We do this because
357              // at the end we loop through all keyed elements that were unmatched
358              // and then discard them in one final pass.
359              onNodeDiscarded(curChild);
360              if (curChild.firstChild) {
361                walkDiscardedChildNodes(curChild, skipKeyedNodes);
362              }
363            }
364  
365            curChild = curChild.nextSibling;
366          }
367        }
368      }
369  
370      /**
371      * Removes a DOM node out of the original DOM
372      *
373      * @param  {Node} node The node to remove
374      * @param  {Node} parentNode The nodes parent
375      * @param  {Boolean} skipKeyedNodes If true then elements with keys will be skipped and not discarded.
376      * @return {undefined}
377      */
378      function removeNode(node, parentNode, skipKeyedNodes) {
379        if (onBeforeNodeDiscarded(node) === false) {
380          return;
381        }
382  
383        if (parentNode) {
384          parentNode.removeChild(node);
385        }
386  
387        onNodeDiscarded(node);
388        walkDiscardedChildNodes(node, skipKeyedNodes);
389      }
390  
391      // // TreeWalker implementation is no faster, but keeping this around in case this changes in the future
392      // function indexTree(root) {
393      //     var treeWalker = document.createTreeWalker(
394      //         root,
395      //         NodeFilter.SHOW_ELEMENT);
396      //
397      //     var el;
398      //     while((el = treeWalker.nextNode())) {
399      //         var key = getNodeKey(el);
400      //         if (key) {
401      //             fromNodesLookup[key] = el;
402      //         }
403      //     }
404      // }
405  
406      // // NodeIterator implementation is no faster, but keeping this around in case this changes in the future
407      //
408      // function indexTree(node) {
409      //     var nodeIterator = document.createNodeIterator(node, NodeFilter.SHOW_ELEMENT);
410      //     var el;
411      //     while((el = nodeIterator.nextNode())) {
412      //         var key = getNodeKey(el);
413      //         if (key) {
414      //             fromNodesLookup[key] = el;
415      //         }
416      //     }
417      // }
418  
419      function indexTree(node) {
420        if (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE$1) {
421          var curChild = node.firstChild;
422          while (curChild) {
423            var key = getNodeKey(curChild);
424            if (key) {
425              fromNodesLookup[key] = curChild;
426            }
427  
428            // Walk recursively
429            indexTree(curChild);
430  
431            curChild = curChild.nextSibling;
432          }
433        }
434      }
435  
436      indexTree(fromNode);
437  
438      function handleNodeAdded(el) {
439        onNodeAdded(el);
440  
441        var curChild = el.firstChild;
442        while (curChild) {
443          var nextSibling = curChild.nextSibling;
444  
445          var key = getNodeKey(curChild);
446          if (key) {
447            var unmatchedFromEl = fromNodesLookup[key];
448            // if we find a duplicate #id node in cache, replace `el` with cache value
449            // and morph it to the child node.
450            if (unmatchedFromEl && compareNodeNames(curChild, unmatchedFromEl)) {
451              curChild.parentNode.replaceChild(unmatchedFromEl, curChild);
452              morphEl(unmatchedFromEl, curChild);
453            } else {
454              handleNodeAdded(curChild);
455            }
456          } else {
457            // recursively call for curChild and it's children to see if we find something in
458            // fromNodesLookup
459            handleNodeAdded(curChild);
460          }
461  
462          curChild = nextSibling;
463        }
464      }
465  
466      function cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey) {
467        // We have processed all of the "to nodes". If curFromNodeChild is
468        // non-null then we still have some from nodes left over that need
469        // to be removed
470        while (curFromNodeChild) {
471          var fromNextSibling = curFromNodeChild.nextSibling;
472          if ((curFromNodeKey = getNodeKey(curFromNodeChild))) {
473            // Since the node is keyed it might be matched up later so we defer
474            // the actual removal to later
475            addKeyedRemoval(curFromNodeKey);
476          } else {
477            // NOTE: we skip nested keyed nodes from being removed since there is
478            //       still a chance they will be matched up later
479            removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
480          }
481          curFromNodeChild = fromNextSibling;
482        }
483      }
484  
485      function morphEl(fromEl, toEl, childrenOnly) {
486        var toElKey = getNodeKey(toEl);
487  
488        if (toElKey) {
489          // If an element with an ID is being morphed then it will be in the final
490          // DOM so clear it out of the saved elements collection
491          delete fromNodesLookup[toElKey];
492        }
493  
494        if (!childrenOnly) {
495          // optional
496          if (onBeforeElUpdated(fromEl, toEl) === false) {
497            return;
498          }
499  
500          // update attributes on original DOM element first
501          morphAttrs(fromEl, toEl);
502          // optional
503          onElUpdated(fromEl);
504  
505          if (onBeforeElChildrenUpdated(fromEl, toEl) === false) {
506            return;
507          }
508        }
509  
510        if (fromEl.nodeName !== 'TEXTAREA') {
511          morphChildren(fromEl, toEl);
512        } else {
513          specialElHandlers.TEXTAREA(fromEl, toEl);
514        }
515      }
516  
517      function morphChildren(fromEl, toEl) {
518        var skipFrom = skipFromChildren(fromEl, toEl);
519        var curToNodeChild = toEl.firstChild;
520        var curFromNodeChild = fromEl.firstChild;
521        var curToNodeKey;
522        var curFromNodeKey;
523  
524        var fromNextSibling;
525        var toNextSibling;
526        var matchingFromEl;
527  
528        // walk the children
529        outer: while (curToNodeChild) {
530          toNextSibling = curToNodeChild.nextSibling;
531          curToNodeKey = getNodeKey(curToNodeChild);
532  
533          // walk the fromNode children all the way through
534          while (!skipFrom && curFromNodeChild) {
535            fromNextSibling = curFromNodeChild.nextSibling;
536  
537            if (curToNodeChild.isSameNode && curToNodeChild.isSameNode(curFromNodeChild)) {
538              curToNodeChild = toNextSibling;
539              curFromNodeChild = fromNextSibling;
540              continue outer;
541            }
542  
543            curFromNodeKey = getNodeKey(curFromNodeChild);
544  
545            var curFromNodeType = curFromNodeChild.nodeType;
546  
547            // this means if the curFromNodeChild doesnt have a match with the curToNodeChild
548            var isCompatible = undefined;
549  
550            if (curFromNodeType === curToNodeChild.nodeType) {
551              if (curFromNodeType === ELEMENT_NODE) {
552                // Both nodes being compared are Element nodes
553  
554                if (curToNodeKey) {
555                  // The target node has a key so we want to match it up with the correct element
556                  // in the original DOM tree
557                  if (curToNodeKey !== curFromNodeKey) {
558                    // The current element in the original DOM tree does not have a matching key so
559                    // let's check our lookup to see if there is a matching element in the original
560                    // DOM tree
561                    if ((matchingFromEl = fromNodesLookup[curToNodeKey])) {
562                      if (fromNextSibling === matchingFromEl) {
563                        // Special case for single element removals. To avoid removing the original
564                        // DOM node out of the tree (since that can break CSS transitions, etc.),
565                        // we will instead discard the current node and wait until the next
566                        // iteration to properly match up the keyed target element with its matching
567                        // element in the original tree
568                        isCompatible = false;
569                      } else {
570                        // We found a matching keyed element somewhere in the original DOM tree.
571                        // Let's move the original DOM node into the current position and morph
572                        // it.
573  
574                        // NOTE: We use insertBefore instead of replaceChild because we want to go through
575                        // the `removeNode()` function for the node that is being discarded so that
576                        // all lifecycle hooks are correctly invoked
577                        fromEl.insertBefore(matchingFromEl, curFromNodeChild);
578  
579                        // fromNextSibling = curFromNodeChild.nextSibling;
580  
581                        if (curFromNodeKey) {
582                          // Since the node is keyed it might be matched up later so we defer
583                          // the actual removal to later
584                          addKeyedRemoval(curFromNodeKey);
585                        } else {
586                          // NOTE: we skip nested keyed nodes from being removed since there is
587                          //       still a chance they will be matched up later
588                          removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
589                        }
590  
591                        curFromNodeChild = matchingFromEl;
592                        curFromNodeKey = getNodeKey(curFromNodeChild);
593                      }
594                    } else {
595                      // The nodes are not compatible since the "to" node has a key and there
596                      // is no matching keyed node in the source tree
597                      isCompatible = false;
598                    }
599                  }
600                } else if (curFromNodeKey) {
601                  // The original has a key
602                  isCompatible = false;
603                }
604  
605                isCompatible = isCompatible !== false && compareNodeNames(curFromNodeChild, curToNodeChild);
606                if (isCompatible) {
607                  // We found compatible DOM elements so transform
608                  // the current "from" node to match the current
609                  // target DOM node.
610                  // MORPH
611                  morphEl(curFromNodeChild, curToNodeChild);
612                }
613  
614              } else if (curFromNodeType === TEXT_NODE || curFromNodeType == COMMENT_NODE) {
615                // Both nodes being compared are Text or Comment nodes
616                isCompatible = true;
617                // Simply update nodeValue on the original node to
618                // change the text value
619                if (curFromNodeChild.nodeValue !== curToNodeChild.nodeValue) {
620                  curFromNodeChild.nodeValue = curToNodeChild.nodeValue;
621                }
622  
623              }
624            }
625  
626            if (isCompatible) {
627              // Advance both the "to" child and the "from" child since we found a match
628              // Nothing else to do as we already recursively called morphChildren above
629              curToNodeChild = toNextSibling;
630              curFromNodeChild = fromNextSibling;
631              continue outer;
632            }
633  
634            // No compatible match so remove the old node from the DOM and continue trying to find a
635            // match in the original DOM. However, we only do this if the from node is not keyed
636            // since it is possible that a keyed node might match up with a node somewhere else in the
637            // target tree and we don't want to discard it just yet since it still might find a
638            // home in the final DOM tree. After everything is done we will remove any keyed nodes
639            // that didn't find a home
640            if (curFromNodeKey) {
641              // Since the node is keyed it might be matched up later so we defer
642              // the actual removal to later
643              addKeyedRemoval(curFromNodeKey);
644            } else {
645              // NOTE: we skip nested keyed nodes from being removed since there is
646              //       still a chance they will be matched up later
647              removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
648            }
649  
650            curFromNodeChild = fromNextSibling;
651          } // END: while(curFromNodeChild) {}
652  
653          // If we got this far then we did not find a candidate match for
654          // our "to node" and we exhausted all of the children "from"
655          // nodes. Therefore, we will just append the current "to" node
656          // to the end
657          if (curToNodeKey && (matchingFromEl = fromNodesLookup[curToNodeKey]) && compareNodeNames(matchingFromEl, curToNodeChild)) {
658            // MORPH
659            if(!skipFrom){ addChild(fromEl, matchingFromEl); }
660            morphEl(matchingFromEl, curToNodeChild);
661          } else {
662            var onBeforeNodeAddedResult = onBeforeNodeAdded(curToNodeChild);
663            if (onBeforeNodeAddedResult !== false) {
664              if (onBeforeNodeAddedResult) {
665                curToNodeChild = onBeforeNodeAddedResult;
666              }
667  
668              if (curToNodeChild.actualize) {
669                curToNodeChild = curToNodeChild.actualize(fromEl.ownerDocument || doc);
670              }
671              addChild(fromEl, curToNodeChild);
672              handleNodeAdded(curToNodeChild);
673            }
674          }
675  
676          curToNodeChild = toNextSibling;
677          curFromNodeChild = fromNextSibling;
678        }
679  
680        cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey);
681  
682        var specialElHandler = specialElHandlers[fromEl.nodeName];
683        if (specialElHandler) {
684          specialElHandler(fromEl, toEl);
685        }
686      } // END: morphChildren(...)
687  
688      var morphedNode = fromNode;
689      var morphedNodeType = morphedNode.nodeType;
690      var toNodeType = toNode.nodeType;
691  
692      if (!childrenOnly) {
693        // Handle the case where we are given two DOM nodes that are not
694        // compatible (e.g. <div> --> <span> or <div> --> TEXT)
695        if (morphedNodeType === ELEMENT_NODE) {
696          if (toNodeType === ELEMENT_NODE) {
697            if (!compareNodeNames(fromNode, toNode)) {
698              onNodeDiscarded(fromNode);
699              morphedNode = moveChildren(fromNode, createElementNS(toNode.nodeName, toNode.namespaceURI));
700            }
701          } else {
702            // Going from an element node to a text node
703            morphedNode = toNode;
704          }
705        } else if (morphedNodeType === TEXT_NODE || morphedNodeType === COMMENT_NODE) { // Text or comment node
706          if (toNodeType === morphedNodeType) {
707            if (morphedNode.nodeValue !== toNode.nodeValue) {
708              morphedNode.nodeValue = toNode.nodeValue;
709            }
710  
711            return morphedNode;
712          } else {
713            // Text node to something else
714            morphedNode = toNode;
715          }
716        }
717      }
718  
719      if (morphedNode === toNode) {
720        // The "to node" was not compatible with the "from node" so we had to
721        // toss out the "from node" and use the "to node"
722        onNodeDiscarded(fromNode);
723      } else {
724        if (toNode.isSameNode && toNode.isSameNode(morphedNode)) {
725          return;
726        }
727  
728        morphEl(morphedNode, toNode, childrenOnly);
729  
730        // We now need to loop over any keyed nodes that might need to be
731        // removed. We only do the removal if we know that the keyed node
732        // never found a match. When a keyed node is matched up we remove
733        // it out of fromNodesLookup and we use fromNodesLookup to determine
734        // if a keyed node has been matched up or not
735        if (keyedRemovalList) {
736          for (var i=0, len=keyedRemovalList.length; i<len; i++) {
737            var elToRemove = fromNodesLookup[keyedRemovalList[i]];
738            if (elToRemove) {
739              removeNode(elToRemove, elToRemove.parentNode, false);
740            }
741          }
742        }
743      }
744  
745      if (!childrenOnly && morphedNode !== fromNode && fromNode.parentNode) {
746        if (morphedNode.actualize) {
747          morphedNode = morphedNode.actualize(fromNode.ownerDocument || doc);
748        }
749        // If we had to swap out the from node with a new node because the old
750        // node was not compatible with the target node then we need to
751        // replace the old DOM node in the original DOM tree. This is only
752        // possible if the original DOM node was part of a DOM tree which
753        // we know is the case if it has a parent node.
754        fromNode.parentNode.replaceChild(morphedNode, fromNode);
755      }
756  
757      return morphedNode;
758    };
759  }
760  
761  var morphdom = morphdomFactory(morphAttrs);
762  
763  module.exports = morphdom;