Precise DOM morphing
morphing typescript dom
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Update morphlex.js

+325 -358
+325 -358
dist/morphlex.js
··· 1 1 export function morph(node, reference, options = {}) { 2 - if (typeof reference === "string") { 3 - const template = document.createElement("template"); 4 - template.innerHTML = reference.trim(); 5 - reference = template.content.firstChild; 6 - if (!reference) { 7 - throw new Error("The provided string did not contain any nodes."); 8 - } 9 - } 10 - if (isElement(node)) { 11 - node.ariaBusy = "true"; 12 - new Morph(options).morph([node, reference]); 13 - node.ariaBusy = null; 14 - } 15 - else { 16 - new Morph(options).morph([node, reference]); 17 - } 2 + if (typeof reference === "string") { 3 + const template = document.createElement("template"); 4 + template.innerHTML = reference.trim(); 5 + reference = template.content.firstChild; 6 + if (!reference) { 7 + throw new Error("The provided string did not contain any nodes."); 8 + } 9 + } 10 + if (isElement(node)) { 11 + node.ariaBusy = "true"; 12 + new Morph(options).morph([node, reference]); 13 + node.ariaBusy = null; 14 + } else { 15 + new Morph(options).morph([node, reference]); 16 + } 18 17 } 19 18 class Morph { 20 - #idMap; 21 - #sensivityMap; 22 - #ignoreActiveValue; 23 - #preserveModifiedValues; 24 - #beforeNodeMorphed; 25 - #afterNodeMorphed; 26 - #beforeNodeAdded; 27 - #afterNodeAdded; 28 - #beforeNodeRemoved; 29 - #afterNodeRemoved; 30 - #beforeAttributeUpdated; 31 - #afterAttributeUpdated; 32 - #beforePropertyUpdated; 33 - #afterPropertyUpdated; 34 - constructor(options = {}) { 35 - this.#idMap = new WeakMap(); 36 - this.#sensivityMap = new WeakMap(); 37 - this.#ignoreActiveValue = options.ignoreActiveValue || false; 38 - this.#preserveModifiedValues = options.preserveModifiedValues || false; 39 - this.#beforeNodeMorphed = options.beforeNodeMorphed; 40 - this.#afterNodeMorphed = options.afterNodeMorphed; 41 - this.#beforeNodeAdded = options.beforeNodeAdded; 42 - this.#afterNodeAdded = options.afterNodeAdded; 43 - this.#beforeNodeRemoved = options.beforeNodeRemoved; 44 - this.#afterNodeRemoved = options.afterNodeRemoved; 45 - this.#beforeAttributeUpdated = options.beforeAttributeUpdated; 46 - this.#afterAttributeUpdated = options.afterAttributeUpdated; 47 - this.#beforePropertyUpdated = options.beforePropertyUpdated; 48 - this.#afterPropertyUpdated = options.afterPropertyUpdated; 49 - Object.freeze(this); 50 - } 51 - morph(pair) { 52 - if (isParentNodePair(pair)) 53 - this.#buildMaps(pair); 54 - this.#morphNode(pair); 55 - } 56 - morphInner(pair) { 57 - this.#buildMaps(pair); 58 - if (isMatchingElementPair(pair)) { 59 - this.#morphMatchingElementContent(pair); 60 - } 61 - else { 62 - throw new Error("You can only do an inner morph with matching elements."); 63 - } 64 - } 65 - #buildMaps([node, reference]) { 66 - this.#mapIdSets(node); 67 - this.#mapIdSets(reference); 68 - this.#mapSensivity(node); 69 - Object.freeze(this.#idMap); 70 - Object.freeze(this.#sensivityMap); 71 - } 72 - #mapSensivity(node) { 73 - const sensitiveElements = node.querySelectorAll("audio,canvas,embed,iframe,input,object,textarea,video"); 74 - const sensitiveElementsLength = sensitiveElements.length; 75 - for (let i = 0; i < sensitiveElementsLength; i++) { 76 - const sensitiveElement = sensitiveElements[i]; 77 - let sensivity = 0; 78 - if (isInput(sensitiveElement) || isTextArea(sensitiveElement)) { 79 - sensivity++; 80 - if (sensitiveElement.value !== sensitiveElement.defaultValue) 81 - sensivity++; 82 - if (sensitiveElement === document.activeElement) 83 - sensivity++; 84 - } 85 - else { 86 - sensivity += 3; 87 - if (isMedia(sensitiveElement) && !sensitiveElement.ended) { 88 - if (!sensitiveElement.paused) 89 - sensivity++; 90 - if (sensitiveElement.currentTime > 0) 91 - sensivity++; 92 - } 93 - } 94 - let current = sensitiveElement; 95 - while (current) { 96 - this.#sensivityMap.set(current, (this.#sensivityMap.get(current) || 0) + sensivity); 97 - if (current === node) 98 - break; 99 - current = current.parentElement; 100 - } 101 - } 102 - } 103 - // For each node with an ID, push that ID into the IdSet on the IdMap, for each of its parent elements. 104 - #mapIdSets(node) { 105 - const elementsWithIds = node.querySelectorAll("[id]"); 106 - const elementsWithIdsLength = elementsWithIds.length; 107 - for (let i = 0; i < elementsWithIdsLength; i++) { 108 - const elementWithId = elementsWithIds[i]; 109 - const id = elementWithId.id; 110 - // Ignore empty IDs 111 - if (id === "") 112 - continue; 113 - let current = elementWithId; 114 - while (current) { 115 - const idSet = this.#idMap.get(current); 116 - idSet ? idSet.add(id) : this.#idMap.set(current, new Set([id])); 117 - if (current === node) 118 - break; 119 - current = current.parentElement; 120 - } 121 - } 122 - } 123 - // This is where we actually morph the nodes. The `morph` function (above) exists only to set up the `idMap`. 124 - #morphNode(pair) { 125 - if (isMatchingElementPair(pair)) 126 - this.#morphMatchingElementNode(pair); 127 - else 128 - this.#morphOtherNode(pair); 129 - } 130 - #morphMatchingElementNode(pair) { 131 - const [node, reference] = pair; 132 - if (!(this.#beforeNodeMorphed?.(node, writableNode(reference)) ?? true)) 133 - return; 134 - if (node.hasAttributes() || reference.hasAttributes()) 135 - this.#morphAttributes(pair); 136 - // TODO: Should use a branded pair here. 137 - this.#morphMatchingElementContent(pair); 138 - this.#afterNodeMorphed?.(node, writableNode(reference)); 139 - } 140 - #morphOtherNode([node, reference]) { 141 - if (!(this.#beforeNodeMorphed?.(node, writableNode(reference)) ?? true)) 142 - return; 143 - if (node.nodeType === reference.nodeType && node.nodeValue !== null && reference.nodeValue !== null) { 144 - // Handle text nodes, comments, and CDATA sections. 145 - this.#updateProperty(node, "nodeValue", reference.nodeValue); 146 - } 147 - else 148 - this.#replaceNode(node, reference.cloneNode(true)); 149 - this.#afterNodeMorphed?.(node, writableNode(reference)); 150 - } 151 - #morphMatchingElementContent(pair) { 152 - const [node, reference] = pair; 153 - if (isHead(node)) { 154 - // We can pass the reference as a head here becuase we know it's the same as the node. 155 - this.#morphHeadContents(pair); 156 - } 157 - else if (node.hasChildNodes() || reference.hasChildNodes()) 158 - this.#morphChildNodes(pair); 159 - } 160 - #morphHeadContents([node, reference]) { 161 - const refChildNodesMap = new Map(); 162 - // Generate a map of the reference head element’s child nodes, keyed by their outerHTML. 163 - const referenceChildrenLength = reference.children.length; 164 - for (let i = 0; i < referenceChildrenLength; i++) { 165 - const child = reference.children[i]; 166 - refChildNodesMap.set(child.outerHTML, child); 167 - } 168 - const nodeChildrenLength = node.children.length; 169 - for (let i = 0; i < nodeChildrenLength; i++) { 170 - const child = node.children[i]; 171 - const key = child.outerHTML; 172 - const refChild = refChildNodesMap.get(key); 173 - // If the child is in the reference map already, we don’t need to add it later. 174 - // If it’s not in the map, we need to remove it from the node. 175 - refChild ? refChildNodesMap.delete(key) : this.#removeNode(child); 176 - } 177 - // Any remaining nodes in the map should be appended to the head. 178 - for (const refChild of refChildNodesMap.values()) 179 - this.#appendChild(node, refChild.cloneNode(true)); 180 - } 181 - #morphAttributes([element, reference]) { 182 - // Remove any excess attributes from the element that aren’t present in the reference. 183 - for (const { name, value } of element.attributes) { 184 - if (!reference.hasAttribute(name) && (this.#beforeAttributeUpdated?.(element, name, null) ?? true)) { 185 - element.removeAttribute(name); 186 - this.#afterAttributeUpdated?.(element, name, value); 187 - } 188 - } 189 - // Copy attributes from the reference to the element, if they don’t already match. 190 - for (const { name, value } of reference.attributes) { 191 - const previousValue = element.getAttribute(name); 192 - if (previousValue !== value && (this.#beforeAttributeUpdated?.(element, name, value) ?? true)) { 193 - element.setAttribute(name, value); 194 - this.#afterAttributeUpdated?.(element, name, previousValue); 195 - } 196 - } 197 - // For certain types of elements, we need to do some extra work to ensure 198 - // the element’s state matches the reference elements’ state. 199 - if (isInput(element) && isInput(reference)) { 200 - this.#updateProperty(element, "checked", reference.checked); 201 - this.#updateProperty(element, "disabled", reference.disabled); 202 - this.#updateProperty(element, "indeterminate", reference.indeterminate); 203 - if (element.type !== "file" && 204 - !(this.#ignoreActiveValue && document.activeElement === element) && 205 - !(this.#preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue)) { 206 - this.#updateProperty(element, "value", reference.value); 207 - } 208 - } 209 - else if (isOption(element) && isOption(reference)) { 210 - this.#updateProperty(element, "selected", reference.selected); 211 - } 212 - else if (isTextArea(element) && 213 - isTextArea(reference) && 214 - !(this.#ignoreActiveValue && document.activeElement === element) && 215 - !(this.#preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue)) { 216 - this.#updateProperty(element, "value", reference.value); 217 - const text = element.firstElementChild; 218 - if (text) 219 - this.#updateProperty(text, "textContent", reference.value); 220 - } 221 - } 222 - // Iterates over the child nodes of the reference element, morphing the main element’s child nodes to match. 223 - #morphChildNodes(pair) { 224 - const [element, reference] = pair; 225 - const childNodes = element.childNodes; 226 - const refChildNodes = reference.childNodes; 227 - for (let i = 0; i < refChildNodes.length; i++) { 228 - const child = childNodes[i]; 229 - const refChild = refChildNodes[i]; 230 - if (child && refChild) { 231 - const pair = [child, refChild]; 232 - if (isMatchingElementPair(pair)) { 233 - if (isHead(pair[0])) { 234 - this.#morphHeadContents(pair); 235 - } 236 - else { 237 - this.#morphChildElement(pair, element); 238 - } 239 - } 240 - else 241 - this.#morphOtherNode(pair); 242 - } 243 - else if (refChild) { 244 - this.#appendChild(element, refChild.cloneNode(true)); 245 - } 246 - else if (child) { 247 - this.#removeNode(child); 248 - } 249 - } 250 - // Clean up any excess nodes that may be left over 251 - while (childNodes.length > refChildNodes.length) { 252 - const child = element.lastChild; 253 - if (child) 254 - this.#removeNode(child); 255 - } 256 - } 257 - #morphChildElement([child, reference], parent) { 258 - if (!(this.#beforeNodeMorphed?.(child, writableNode(reference)) ?? true)) 259 - return; 260 - const refIdSet = this.#idMap.get(reference); 261 - // Generate the array in advance of the loop 262 - const refSetArray = refIdSet ? [...refIdSet] : []; 263 - let currentNode = child; 264 - let nextMatchByTagName = null; 265 - // Try find a match by idSet, while also looking out for the next best match by tagName. 266 - while (currentNode) { 267 - if (isElement(currentNode)) { 268 - const id = currentNode.id; 269 - if (!nextMatchByTagName && currentNode.localName === reference.localName) { 270 - nextMatchByTagName = currentNode; 271 - } 272 - if (id !== "") { 273 - if (id === reference.id) { 274 - this.#insertBefore(parent, currentNode, child); 275 - return this.#morphNode([currentNode, reference]); 276 - } 277 - else { 278 - const currentIdSet = this.#idMap.get(currentNode); 279 - if (currentIdSet && refSetArray.some((it) => currentIdSet.has(it))) { 280 - this.#insertBefore(parent, currentNode, child); 281 - return this.#morphNode([currentNode, reference]); 282 - } 283 - } 284 - } 285 - } 286 - currentNode = currentNode.nextSibling; 287 - } 288 - if (nextMatchByTagName) { 289 - this.#insertBefore(parent, nextMatchByTagName, child); 290 - this.#morphNode([nextMatchByTagName, reference]); 291 - } 292 - else { 293 - const newNode = reference.cloneNode(true); 294 - if (this.#beforeNodeAdded?.(newNode) ?? true) { 295 - this.#insertBefore(parent, newNode, child); 296 - this.#afterNodeAdded?.(newNode); 297 - } 298 - } 299 - this.#afterNodeMorphed?.(child, writableNode(reference)); 300 - } 301 - #updateProperty(node, propertyName, newValue) { 302 - const previousValue = node[propertyName]; 303 - if (previousValue !== newValue && (this.#beforePropertyUpdated?.(node, propertyName, newValue) ?? true)) { 304 - node[propertyName] = newValue; 305 - this.#afterPropertyUpdated?.(node, propertyName, previousValue); 306 - } 307 - } 308 - #replaceNode(node, newNode) { 309 - if ((this.#beforeNodeRemoved?.(node) ?? true) && (this.#beforeNodeAdded?.(newNode) ?? true)) { 310 - node.replaceWith(newNode); 311 - this.#afterNodeAdded?.(newNode); 312 - this.#afterNodeRemoved?.(node); 313 - } 314 - } 315 - #insertBefore(parent, node, insertionPoint) { 316 - if (node === insertionPoint) 317 - return; 318 - if (isElement(node)) { 319 - const sensitivity = this.#sensivityMap.get(node) ?? 0; 320 - if (sensitivity > 0) { 321 - let previousNode = node.previousSibling; 322 - while (previousNode) { 323 - const previousNodeSensitivity = this.#sensivityMap.get(previousNode) ?? 0; 324 - if (previousNodeSensitivity < sensitivity) { 325 - parent.insertBefore(previousNode, node.nextSibling); 326 - if (previousNode === insertionPoint) 327 - return; 328 - previousNode = node.previousSibling; 329 - } 330 - else 331 - break; 332 - } 333 - } 334 - } 335 - parent.insertBefore(node, insertionPoint); 336 - } 337 - #appendChild(node, newNode) { 338 - if (this.#beforeNodeAdded?.(newNode) ?? true) { 339 - node.appendChild(newNode); 340 - this.#afterNodeAdded?.(newNode); 341 - } 342 - } 343 - #removeNode(node) { 344 - if (this.#beforeNodeRemoved?.(node) ?? true) { 345 - node.remove(); 346 - this.#afterNodeRemoved?.(node); 347 - } 348 - } 19 + #idMap; 20 + #sensivityMap; 21 + #ignoreActiveValue; 22 + #preserveModifiedValues; 23 + #beforeNodeMorphed; 24 + #afterNodeMorphed; 25 + #beforeNodeAdded; 26 + #afterNodeAdded; 27 + #beforeNodeRemoved; 28 + #afterNodeRemoved; 29 + #beforeAttributeUpdated; 30 + #afterAttributeUpdated; 31 + #beforePropertyUpdated; 32 + #afterPropertyUpdated; 33 + constructor(options = {}) { 34 + this.#idMap = new WeakMap(); 35 + this.#sensivityMap = new WeakMap(); 36 + this.#ignoreActiveValue = options.ignoreActiveValue || false; 37 + this.#preserveModifiedValues = options.preserveModifiedValues || false; 38 + this.#beforeNodeMorphed = options.beforeNodeMorphed; 39 + this.#afterNodeMorphed = options.afterNodeMorphed; 40 + this.#beforeNodeAdded = options.beforeNodeAdded; 41 + this.#afterNodeAdded = options.afterNodeAdded; 42 + this.#beforeNodeRemoved = options.beforeNodeRemoved; 43 + this.#afterNodeRemoved = options.afterNodeRemoved; 44 + this.#beforeAttributeUpdated = options.beforeAttributeUpdated; 45 + this.#afterAttributeUpdated = options.afterAttributeUpdated; 46 + this.#beforePropertyUpdated = options.beforePropertyUpdated; 47 + this.#afterPropertyUpdated = options.afterPropertyUpdated; 48 + Object.freeze(this); 49 + } 50 + morph(pair) { 51 + if (isParentNodePair(pair)) this.#buildMaps(pair); 52 + this.#morphNode(pair); 53 + } 54 + morphInner(pair) { 55 + this.#buildMaps(pair); 56 + if (isMatchingElementPair(pair)) { 57 + this.#morphMatchingElementContent(pair); 58 + } else { 59 + throw new Error("You can only do an inner morph with matching elements."); 60 + } 61 + } 62 + #buildMaps([node, reference]) { 63 + this.#mapIdSets(node); 64 + this.#mapIdSets(reference); 65 + this.#mapSensivity(node); 66 + Object.freeze(this.#idMap); 67 + Object.freeze(this.#sensivityMap); 68 + } 69 + #mapSensivity(node) { 70 + const sensitiveElements = node.querySelectorAll("audio,canvas,embed,iframe,input,object,textarea,video"); 71 + const sensitiveElementsLength = sensitiveElements.length; 72 + for (let i = 0; i < sensitiveElementsLength; i++) { 73 + const sensitiveElement = sensitiveElements[i]; 74 + let sensivity = 0; 75 + if (isInput(sensitiveElement) || isTextArea(sensitiveElement)) { 76 + sensivity++; 77 + if (sensitiveElement.value !== sensitiveElement.defaultValue) sensivity++; 78 + if (sensitiveElement === document.activeElement) sensivity++; 79 + } else { 80 + sensivity += 3; 81 + if (isMedia(sensitiveElement) && !sensitiveElement.ended) { 82 + if (!sensitiveElement.paused) sensivity++; 83 + if (sensitiveElement.currentTime > 0) sensivity++; 84 + } 85 + } 86 + let current = sensitiveElement; 87 + while (current) { 88 + this.#sensivityMap.set(current, (this.#sensivityMap.get(current) || 0) + sensivity); 89 + if (current === node) break; 90 + current = current.parentElement; 91 + } 92 + } 93 + } 94 + // For each node with an ID, push that ID into the IdSet on the IdMap, for each of its parent elements. 95 + #mapIdSets(node) { 96 + const elementsWithIds = node.querySelectorAll("[id]"); 97 + const elementsWithIdsLength = elementsWithIds.length; 98 + for (let i = 0; i < elementsWithIdsLength; i++) { 99 + const elementWithId = elementsWithIds[i]; 100 + const id = elementWithId.id; 101 + // Ignore empty IDs 102 + if (id === "") continue; 103 + let current = elementWithId; 104 + while (current) { 105 + const idSet = this.#idMap.get(current); 106 + idSet ? idSet.add(id) : this.#idMap.set(current, new Set([id])); 107 + if (current === node) break; 108 + current = current.parentElement; 109 + } 110 + } 111 + } 112 + // This is where we actually morph the nodes. The `morph` function (above) exists only to set up the `idMap`. 113 + #morphNode(pair) { 114 + if (isMatchingElementPair(pair)) this.#morphMatchingElementNode(pair); 115 + else this.#morphOtherNode(pair); 116 + } 117 + #morphMatchingElementNode(pair) { 118 + const [node, reference] = pair; 119 + if (!(this.#beforeNodeMorphed?.(node, writableNode(reference)) ?? true)) return; 120 + if (node.hasAttributes() || reference.hasAttributes()) this.#morphAttributes(pair); 121 + // TODO: Should use a branded pair here. 122 + this.#morphMatchingElementContent(pair); 123 + this.#afterNodeMorphed?.(node, writableNode(reference)); 124 + } 125 + #morphOtherNode([node, reference]) { 126 + if (!(this.#beforeNodeMorphed?.(node, writableNode(reference)) ?? true)) return; 127 + if (node.nodeType === reference.nodeType && node.nodeValue !== null && reference.nodeValue !== null) { 128 + // Handle text nodes, comments, and CDATA sections. 129 + this.#updateProperty(node, "nodeValue", reference.nodeValue); 130 + } else this.#replaceNode(node, reference.cloneNode(true)); 131 + this.#afterNodeMorphed?.(node, writableNode(reference)); 132 + } 133 + #morphMatchingElementContent(pair) { 134 + const [node, reference] = pair; 135 + if (isHead(node)) { 136 + // We can pass the reference as a head here becuase we know it's the same as the node. 137 + this.#morphHeadContents(pair); 138 + } else if (node.hasChildNodes() || reference.hasChildNodes()) this.#morphChildNodes(pair); 139 + } 140 + #morphHeadContents([node, reference]) { 141 + const refChildNodesMap = new Map(); 142 + // Generate a map of the reference head element’s child nodes, keyed by their outerHTML. 143 + const referenceChildrenLength = reference.children.length; 144 + for (let i = 0; i < referenceChildrenLength; i++) { 145 + const child = reference.children[i]; 146 + refChildNodesMap.set(child.outerHTML, child); 147 + } 148 + const nodeChildrenLength = node.children.length; 149 + for (let i = 0; i < nodeChildrenLength; i++) { 150 + const child = node.children[i]; 151 + const key = child.outerHTML; 152 + const refChild = refChildNodesMap.get(key); 153 + // If the child is in the reference map already, we don’t need to add it later. 154 + // If it’s not in the map, we need to remove it from the node. 155 + refChild ? refChildNodesMap.delete(key) : this.#removeNode(child); 156 + } 157 + // Any remaining nodes in the map should be appended to the head. 158 + for (const refChild of refChildNodesMap.values()) this.#appendChild(node, refChild.cloneNode(true)); 159 + } 160 + #morphAttributes([element, reference]) { 161 + // Remove any excess attributes from the element that aren’t present in the reference. 162 + for (const { name, value } of element.attributes) { 163 + if (!reference.hasAttribute(name) && (this.#beforeAttributeUpdated?.(element, name, null) ?? true)) { 164 + element.removeAttribute(name); 165 + this.#afterAttributeUpdated?.(element, name, value); 166 + } 167 + } 168 + // Copy attributes from the reference to the element, if they don’t already match. 169 + for (const { name, value } of reference.attributes) { 170 + const previousValue = element.getAttribute(name); 171 + if (previousValue !== value && (this.#beforeAttributeUpdated?.(element, name, value) ?? true)) { 172 + element.setAttribute(name, value); 173 + this.#afterAttributeUpdated?.(element, name, previousValue); 174 + } 175 + } 176 + // For certain types of elements, we need to do some extra work to ensure 177 + // the element’s state matches the reference elements’ state. 178 + if (isInput(element) && isInput(reference)) { 179 + this.#updateProperty(element, "checked", reference.checked); 180 + this.#updateProperty(element, "disabled", reference.disabled); 181 + this.#updateProperty(element, "indeterminate", reference.indeterminate); 182 + if ( 183 + element.type !== "file" && 184 + !(this.#ignoreActiveValue && document.activeElement === element) && 185 + !(this.#preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue) 186 + ) { 187 + this.#updateProperty(element, "value", reference.value); 188 + } 189 + } else if (isOption(element) && isOption(reference)) { 190 + this.#updateProperty(element, "selected", reference.selected); 191 + } else if ( 192 + isTextArea(element) && 193 + isTextArea(reference) && 194 + !(this.#ignoreActiveValue && document.activeElement === element) && 195 + !(this.#preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue) 196 + ) { 197 + this.#updateProperty(element, "value", reference.value); 198 + const text = element.firstElementChild; 199 + if (text) this.#updateProperty(text, "textContent", reference.value); 200 + } 201 + } 202 + // Iterates over the child nodes of the reference element, morphing the main element’s child nodes to match. 203 + #morphChildNodes(pair) { 204 + const [element, reference] = pair; 205 + const childNodes = element.childNodes; 206 + const refChildNodes = reference.childNodes; 207 + for (let i = 0; i < refChildNodes.length; i++) { 208 + const child = childNodes[i]; 209 + const refChild = refChildNodes[i]; 210 + if (child && refChild) { 211 + const pair = [child, refChild]; 212 + if (isMatchingElementPair(pair)) { 213 + if (isHead(pair[0])) { 214 + this.#morphHeadContents(pair); 215 + } else { 216 + this.#morphChildElement(pair, element); 217 + } 218 + } else this.#morphOtherNode(pair); 219 + } else if (refChild) { 220 + this.#appendChild(element, refChild.cloneNode(true)); 221 + } else if (child) { 222 + this.#removeNode(child); 223 + } 224 + } 225 + // Clean up any excess nodes that may be left over 226 + while (childNodes.length > refChildNodes.length) { 227 + const child = element.lastChild; 228 + if (child) this.#removeNode(child); 229 + } 230 + } 231 + #morphChildElement([child, reference], parent) { 232 + if (!(this.#beforeNodeMorphed?.(child, writableNode(reference)) ?? true)) return; 233 + const refIdSet = this.#idMap.get(reference); 234 + // Generate the array in advance of the loop 235 + const refSetArray = refIdSet ? [...refIdSet] : []; 236 + let currentNode = child; 237 + let nextMatchByTagName = null; 238 + // Try find a match by idSet, while also looking out for the next best match by tagName. 239 + while (currentNode) { 240 + if (isElement(currentNode)) { 241 + const id = currentNode.id; 242 + if (!nextMatchByTagName && currentNode.localName === reference.localName) { 243 + nextMatchByTagName = currentNode; 244 + } 245 + if (id !== "") { 246 + if (id === reference.id) { 247 + this.#insertBefore(parent, currentNode, child); 248 + return this.#morphNode([currentNode, reference]); 249 + } else { 250 + const currentIdSet = this.#idMap.get(currentNode); 251 + if (currentIdSet && refSetArray.some((it) => currentIdSet.has(it))) { 252 + this.#insertBefore(parent, currentNode, child); 253 + return this.#morphNode([currentNode, reference]); 254 + } 255 + } 256 + } 257 + } 258 + currentNode = currentNode.nextSibling; 259 + } 260 + if (nextMatchByTagName) { 261 + this.#insertBefore(parent, nextMatchByTagName, child); 262 + this.#morphNode([nextMatchByTagName, reference]); 263 + } else { 264 + const newNode = reference.cloneNode(true); 265 + if (this.#beforeNodeAdded?.(newNode) ?? true) { 266 + this.#insertBefore(parent, newNode, child); 267 + this.#afterNodeAdded?.(newNode); 268 + } 269 + } 270 + this.#afterNodeMorphed?.(child, writableNode(reference)); 271 + } 272 + #updateProperty(node, propertyName, newValue) { 273 + const previousValue = node[propertyName]; 274 + if (previousValue !== newValue && (this.#beforePropertyUpdated?.(node, propertyName, newValue) ?? true)) { 275 + node[propertyName] = newValue; 276 + this.#afterPropertyUpdated?.(node, propertyName, previousValue); 277 + } 278 + } 279 + #replaceNode(node, newNode) { 280 + if ((this.#beforeNodeRemoved?.(node) ?? true) && (this.#beforeNodeAdded?.(newNode) ?? true)) { 281 + node.replaceWith(newNode); 282 + this.#afterNodeAdded?.(newNode); 283 + this.#afterNodeRemoved?.(node); 284 + } 285 + } 286 + #insertBefore(parent, node, insertionPoint) { 287 + if (node === insertionPoint) return; 288 + if (isElement(node)) { 289 + const sensitivity = this.#sensivityMap.get(node) ?? 0; 290 + if (sensitivity > 0) { 291 + let previousNode = node.previousSibling; 292 + while (previousNode) { 293 + const previousNodeSensitivity = this.#sensivityMap.get(previousNode) ?? 0; 294 + if (previousNodeSensitivity < sensitivity) { 295 + parent.insertBefore(previousNode, node.nextSibling); 296 + if (previousNode === insertionPoint) return; 297 + previousNode = node.previousSibling; 298 + } else break; 299 + } 300 + } 301 + } 302 + parent.insertBefore(node, insertionPoint); 303 + } 304 + #appendChild(node, newNode) { 305 + if (this.#beforeNodeAdded?.(newNode) ?? true) { 306 + node.appendChild(newNode); 307 + this.#afterNodeAdded?.(newNode); 308 + } 309 + } 310 + #removeNode(node) { 311 + if (this.#beforeNodeRemoved?.(node) ?? true) { 312 + node.remove(); 313 + this.#afterNodeRemoved?.(node); 314 + } 315 + } 349 316 } 350 317 // We cannot use `instanceof` when nodes might be from different documents, 351 318 // so we use type guards instead. This keeps TypeScript happy, while doing 352 319 // the necessary checks at runtime. 353 320 function writableNode(node) { 354 - return node; 321 + return node; 355 322 } 356 323 function isMatchingElementPair(pair) { 357 - const [a, b] = pair; 358 - return isElement(a) && isElement(b) && a.localName === b.localName; 324 + const [a, b] = pair; 325 + return isElement(a) && isElement(b) && a.localName === b.localName; 359 326 } 360 327 function isParentNodePair(pair) { 361 - const [a, b] = pair; 362 - return isParentNode(a) && isParentNode(b); 328 + const [a, b] = pair; 329 + return isParentNode(a) && isParentNode(b); 363 330 } 364 331 function isElement(node) { 365 - return node.nodeType === 1; 332 + return node.nodeType === 1; 366 333 } 367 334 function isMedia(element) { 368 - return element.localName === "video" || element.localName === "audio"; 335 + return element.localName === "video" || element.localName === "audio"; 369 336 } 370 337 function isInput(element) { 371 - return element.localName === "input"; 338 + return element.localName === "input"; 372 339 } 373 340 function isOption(element) { 374 - return element.localName === "option"; 341 + return element.localName === "option"; 375 342 } 376 343 function isTextArea(element) { 377 - return element.localName === "textarea"; 344 + return element.localName === "textarea"; 378 345 } 379 346 function isHead(element) { 380 - return element.localName === "head"; 347 + return element.localName === "head"; 381 348 } 382 349 const parentNodeTypes = new Set([1, 9, 11]); 383 350 function isParentNode(node) { 384 - return parentNodeTypes.has(node.nodeType); 351 + return parentNodeTypes.has(node.nodeType); 385 352 } 386 - //# sourceMappingURL=morphlex.js.map 353 + //# sourceMappingURL=morphlex.js.map