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;