// Ported from nanomorph 5.4.3 (MIT) — https://github.com/choojs/nanomorph // Morph one DOM tree into another. Single-module consolidation of // index.js + lib/morph.js + lib/events.js. /* eslint-disable eqeqeq */ function equal (a, b, message) { if (a != b) throw new Error(message) } function notEqual (a, b, message) { if (a == b) throw new Error(message) } /* eslint-enable eqeqeq */ const ELEMENT_NODE = 1 const TEXT_NODE = 3 const COMMENT_NODE = 8 const events = [ // attribute events (can be set with attributes) 'onclick', 'ondblclick', 'onmousedown', 'onmouseup', 'onmouseover', 'onmousemove', 'onmouseout', 'onmouseenter', 'onmouseleave', 'ontouchcancel', 'ontouchend', 'ontouchmove', 'ontouchstart', 'ondragstart', 'ondrag', 'ondragenter', 'ondragleave', 'ondragover', 'ondrop', 'ondragend', 'onkeydown', 'onkeypress', 'onkeyup', 'onunload', 'onabort', 'onerror', 'onresize', 'onscroll', 'onselect', 'onchange', 'onsubmit', 'onreset', 'onfocus', 'onblur', 'oninput', 'onanimationend', 'onanimationiteration', 'onanimationstart', // other common events 'oncontextmenu', 'onfocusin', 'onfocusout' ] // Morph one tree into another tree // // no parent // -> same: diff and walk children // -> not same: replace and return // old node doesn't exist // -> insert new node // new node doesn't exist // -> delete old node // nodes are not the same // -> diff nodes and apply patch to old node // nodes are the same // -> walk all child nodes and append to old node export default function nanomorph (oldTree, newTree, options) { equal(typeof oldTree, 'object', 'nanomorph: oldTree should be an object') equal(typeof newTree, 'object', 'nanomorph: newTree should be an object') if (options && options.childrenOnly) { updateChildren(newTree, oldTree) return oldTree } notEqual( newTree.nodeType, 11, 'nanomorph: newTree should have one root node (which is not a DocumentFragment)' ) return walk(newTree, oldTree) } // Walk and morph a dom tree function walk (newNode, oldNode) { if (!oldNode) { return newNode } else if (!newNode) { return null } else if (newNode.isSameNode && newNode.isSameNode(oldNode)) { return oldNode } else if (newNode.tagName !== oldNode.tagName || getComponentId(newNode) !== getComponentId(oldNode)) { return newNode } else { morph(newNode, oldNode) updateChildren(newNode, oldNode) return oldNode } } function getComponentId (node) { return node.dataset ? node.dataset.nanomorphComponentId : undefined } // Update the children of elements function updateChildren (newNode, oldNode) { let oldChild, newChild, morphed, oldMatch // The offset is only ever increased, and used for [i - offset] in the loop let offset = 0 for (let i = 0; ; i++) { oldChild = oldNode.childNodes[i] newChild = newNode.childNodes[i - offset] // Both nodes are empty, do nothing if (!oldChild && !newChild) { break // There is no new child, remove old } else if (!newChild) { oldNode.removeChild(oldChild) i-- // There is no old child, add new } else if (!oldChild) { oldNode.appendChild(newChild) offset++ // Both nodes are the same, morph } else if (same(newChild, oldChild)) { morphed = walk(newChild, oldChild) if (morphed !== oldChild) { oldNode.replaceChild(morphed, oldChild) offset++ } // Both nodes do not share an ID or a placeholder, try reorder } else { oldMatch = null // Try and find a similar node somewhere in the tree for (let j = i; j < oldNode.childNodes.length; j++) { if (same(oldNode.childNodes[j], newChild)) { oldMatch = oldNode.childNodes[j] break } } // If there was a node with the same ID or placeholder in the old list if (oldMatch) { morphed = walk(newChild, oldMatch) if (morphed !== oldMatch) offset++ oldNode.insertBefore(morphed, oldChild) // It's safe to morph two nodes in-place if neither has an ID } else if (!newChild.id && !oldChild.id) { morphed = walk(newChild, oldChild) if (morphed !== oldChild) { oldNode.replaceChild(morphed, oldChild) offset++ } // Insert the node at the index if we couldn't morph or find a matching node } else { oldNode.insertBefore(newChild, oldChild) offset++ } } } } function same (a, b) { if (a.id) return a.id === b.id if (a.isSameNode) return a.isSameNode(b) if (a.tagName !== b.tagName) return false if (a.type === TEXT_NODE) return a.nodeValue === b.nodeValue return false } // diff elements and apply the resulting patch to the old node function morph (newNode, oldNode) { const nodeType = newNode.nodeType const nodeName = newNode.nodeName if (nodeType === ELEMENT_NODE) { copyAttrs(newNode, oldNode) } if (nodeType === TEXT_NODE || nodeType === COMMENT_NODE) { if (oldNode.nodeValue !== newNode.nodeValue) { oldNode.nodeValue = newNode.nodeValue } } // Some DOM nodes are weird // https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js if (nodeName === 'INPUT') updateInput(newNode, oldNode) else if (nodeName === 'OPTION') updateOption(newNode, oldNode) else if (nodeName === 'TEXTAREA') updateTextarea(newNode, oldNode) copyEvents(newNode, oldNode) } function copyAttrs (newNode, oldNode) { const oldAttrs = oldNode.attributes const newAttrs = newNode.attributes let attrNamespaceURI = null let attrValue = null let fromValue = null let attrName = null let attr = null for (let i = newAttrs.length - 1; i >= 0; --i) { attr = newAttrs[i] attrName = attr.name attrNamespaceURI = attr.namespaceURI attrValue = attr.value if (attrNamespaceURI) { attrName = attr.localName || attrName fromValue = oldNode.getAttributeNS(attrNamespaceURI, attrName) if (fromValue !== attrValue) { oldNode.setAttributeNS(attrNamespaceURI, attrName, attrValue) } } else { if (!oldNode.hasAttribute(attrName)) { oldNode.setAttribute(attrName, attrValue) } else { fromValue = oldNode.getAttribute(attrName) if (fromValue !== attrValue) { // apparently values are always cast to strings, ah well if (attrValue === 'null' || attrValue === 'undefined') { oldNode.removeAttribute(attrName) } else { oldNode.setAttribute(attrName, attrValue) } } } } } // Remove any extra attributes found on the original DOM element that // weren't found on the target element. for (let j = oldAttrs.length - 1; j >= 0; --j) { attr = oldAttrs[j] if (attr.specified !== false) { attrName = attr.name attrNamespaceURI = attr.namespaceURI if (attrNamespaceURI) { attrName = attr.localName || attrName if (!newNode.hasAttributeNS(attrNamespaceURI, attrName)) { oldNode.removeAttributeNS(attrNamespaceURI, attrName) } } else { if (!newNode.hasAttributeNS(null, attrName)) { oldNode.removeAttribute(attrName) } } } } } function copyEvents (newNode, oldNode) { for (let i = 0; i < events.length; i++) { const ev = events[i] if (newNode[ev]) { // if new element has a whitelisted attribute oldNode[ev] = newNode[ev] // update existing element } else if (oldNode[ev]) { // if existing element has it and new one doesnt oldNode[ev] = undefined // remove it from existing element } } } function updateOption (newNode, oldNode) { updateAttribute(newNode, oldNode, 'selected') } // The "value" attribute is special for the element since it sets the // initial value. Changing the "value" attribute without changing the "value" // property will have no effect since it is only used to the set the initial // value. Similar for the "checked" attribute, and "disabled". function updateInput (newNode, oldNode) { const newValue = newNode.value const oldValue = oldNode.value updateAttribute(newNode, oldNode, 'checked') updateAttribute(newNode, oldNode, 'disabled') // The "indeterminate" property can not be set using an HTML attribute. // See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox if (newNode.indeterminate !== oldNode.indeterminate) { oldNode.indeterminate = newNode.indeterminate } // Persist file value since file inputs can't be changed programatically if (oldNode.type === 'file') return if (newValue !== oldValue) { oldNode.setAttribute('value', newValue) oldNode.value = newValue } if (newValue === 'null') { oldNode.value = '' oldNode.removeAttribute('value') } if (!newNode.hasAttributeNS(null, 'value')) { oldNode.removeAttribute('value') } else if (oldNode.type === 'range') { // this is so elements like slider move their UI thingy oldNode.value = newValue } } function updateTextarea (newNode, oldNode) { const newValue = newNode.value if (newValue !== oldNode.value) { oldNode.value = newValue } if (oldNode.firstChild && oldNode.firstChild.nodeValue !== newValue) { // Needed for IE. Apparently IE sets the placeholder as the // node value and vise versa. This ignores an empty update. if (newValue === '' && oldNode.firstChild.nodeValue === oldNode.placeholder) { return } oldNode.firstChild.nodeValue = newValue } } function updateAttribute (newNode, oldNode, name) { if (newNode[name] !== oldNode[name]) { oldNode[name] = newNode[name] if (newNode[name]) { oldNode.setAttribute(name, '') } else { oldNode.removeAttribute(name) } } }