Precise DOM morphing
morphing typescript dom

Simplify <head> morphing

+46 -60
+46 -60
src/morphlex.ts
··· 40 40 41 41 const pair: PairOfNodes<Node> = [from, to] 42 42 if (isElementPair(pair) && isMatchingElementPair(pair)) { 43 - new Morph(options).morphChildren(pair) 43 + new Morph(options).visitChildNodes(pair) 44 44 } else { 45 45 throw new Error("[Morphlex] You can only do an inner morph with matching elements.") 46 46 } ··· 132 132 } 133 133 134 134 private morphMatchingElements(pair: PairOfMatchingElements<Element>): void { 135 - this.visitAttributes(pair) 136 - this.morphChildren(pair) 135 + const [from, to] = pair 136 + 137 + if (from.hasAttributes() || to.hasAttributes()) { 138 + this.visitAttributes(pair) 139 + } 140 + 141 + if (from.hasChildNodes() || to.hasChildNodes()) { 142 + this.visitChildNodes(pair) 143 + } 137 144 } 138 145 139 146 private morphNonMatchingElements([from, to]: PairOfNodes<Element>): void { ··· 152 159 153 160 private visitAttributes([from, to]: PairOfMatchingElements<Element>): void { 154 161 const isInput = isInputElement(from) && isInputElement(to) 155 - const isOption = isInput ? false : isOptionElement(from) 162 + const isOption = isOptionElement(from) && isOptionElement(to) 156 163 157 164 const toAttrs = to.attributes 158 165 const fromAttrs = from.attributes ··· 164 171 const value = attr.value 165 172 const oldValue = from.getAttribute(name) 166 173 167 - if (isInput && (name === "value" || name === "checked" || name === "indeterminate")) continue 168 - if (isOption && name === "selected") continue 174 + if (isInput) { 175 + if (name === "value" || name === "checked" || name === "indeterminate") { 176 + continue 177 + } else if (name === "morph-value" && (this.options.beforeAttributeUpdated?.(from, name, value) ?? true)) { 178 + from.setAttribute(name, value) 179 + from.value = value 180 + this.options.afterAttributeUpdated?.(from, name, oldValue) 181 + continue 182 + } else if (name === "morph-checked" && (this.options.beforeAttributeUpdated?.(from, name, value) ?? true)) { 183 + from.setAttribute(name, value) 184 + from.checked = value === "true" 185 + this.options.afterAttributeUpdated?.(from, name, oldValue) 186 + continue 187 + } else if (name === "morph-indeterminate" && (this.options.beforeAttributeUpdated?.(from, name, value) ?? true)) { 188 + from.setAttribute(name, value) 189 + from.indeterminate = value === "true" 190 + this.options.afterAttributeUpdated?.(from, name, oldValue) 191 + continue 192 + } 193 + } else if (isOption) { 194 + if (name === "selected") { 195 + continue 196 + } else if (name === "morph-selected") { 197 + from.setAttribute(name, value) 198 + from.selected = value === "true" 199 + this.options.afterAttributeUpdated?.(from, name, oldValue) 200 + } 201 + } 169 202 170 203 if (oldValue !== value && (this.options.beforeAttributeUpdated?.(from, name, value) ?? true)) { 171 204 from.setAttribute(name, value) ··· 191 224 } 192 225 } 193 226 194 - morphChildren(pair: PairOfMatchingElements<Element>): void { 195 - const [node, reference] = pair 196 - if (!(this.options.beforeChildrenVisited?.(node) ?? true)) return 197 - 198 - if (isHeadElement(node)) { 199 - this.morphHeadChildren(pair as PairOfMatchingElements<HTMLHeadElement>) 200 - } else if (node.hasChildNodes() || reference.hasChildNodes()) { 201 - this.morphChildNodes(pair) 202 - } 203 - 204 - this.options.afterChildrenVisited?.(node) 205 - } 206 - 207 - // TODO: Review this. 208 - private morphHeadChildren([node, reference]: PairOfMatchingElements<HTMLHeadElement>): void { 209 - const refChildNodesMap: Map<string, Element> = new Map() 210 - 211 - // Generate a map of the reference head element’s child nodes, keyed by their outerHTML. 212 - const referenceChildrenLength = reference.children.length 213 - for (let i = 0; i < referenceChildrenLength; i++) { 214 - const child = reference.children[i]! 215 - refChildNodesMap.set(child.outerHTML, child) 216 - } 217 - 218 - // Iterate backwards to safely remove children without affecting indices 219 - for (let i = node.children.length - 1; i >= 0; i--) { 220 - const child = node.children[i]! 221 - const key = child.outerHTML 222 - const refChild = refChildNodesMap.get(key) 223 - 224 - // If the child is in the reference map already, we don't need to add it later. 225 - // If it's not in the map, we need to remove it from the node. 226 - if (refChild) refChildNodesMap.delete(key) 227 - else this.removeNode(child) 228 - } 229 - 230 - // Any remaining nodes in the map should be appended to the head. 231 - for (const refChild of refChildNodesMap.values()) this.appendChild(node, refChild) 232 - } 233 - 234 - private morphChildNodes([from, to]: PairOfMatchingElements<Element>): void { 227 + visitChildNodes([from, to]: PairOfMatchingElements<Element>): void { 228 + if (!(this.options.beforeChildrenVisited?.(from) ?? true)) return 235 229 const parent = from 236 230 237 231 const fromChildNodes = from.childNodes ··· 298 292 const ariaLabel = node.getAttribute("aria-label") 299 293 const ariaDescription = node.getAttribute("aria-description") 300 294 const href = node.getAttribute("href") 295 + const src = node.getAttribute("src") 301 296 302 297 for (const candidate of candidates) { 303 298 if ( ··· 307 302 (name !== "" && name === candidate.getAttribute("name")) || 308 303 (ariaLabel !== "" && ariaLabel === candidate.getAttribute("aria-label")) || 309 304 (ariaDescription !== "" && ariaDescription === candidate.getAttribute("aria-description")) || 310 - (href !== "" && href === candidate.getAttribute("href"))) 305 + (href !== "" && href === candidate.getAttribute("href")) || 306 + (src !== "" && src === candidate.getAttribute("src"))) 311 307 ) { 312 308 matches.set(node, candidate) 313 309 unmatched.delete(node) ··· 378 374 for (const candidate of candidates) { 379 375 this.removeNode(candidate) 380 376 } 377 + 378 + this.options.afterChildrenVisited?.(from) 381 379 } 382 380 383 381 private replaceNode(node: ChildNode, newNode: ChildNode): void { ··· 387 385 moveBefore(parent, newNode, insertionPoint) 388 386 this.options.afterNodeAdded?.(newNode) 389 387 this.removeNode(node) 390 - } 391 - } 392 - 393 - private appendChild(parent: ParentNode, newChild: ChildNode): void { 394 - const insertionPoint = null 395 - if (this.options.beforeNodeAdded?.(parent, newChild, insertionPoint) ?? true) { 396 - moveBefore(parent, newChild, insertionPoint) 397 - this.options.afterNodeAdded?.(newChild) 398 388 } 399 389 } 400 390 ··· 453 443 454 444 function isOptionElement(element: Element): element is HTMLOptionElement { 455 445 return element.localName === "option" 456 - } 457 - 458 - function isHeadElement(element: Element): element is HTMLHeadElement { 459 - return element.localName === "head" 460 446 } 461 447 462 448 function isParentNode(node: Node): node is ParentNode {