Precise DOM morphing
morphing typescript dom

Better minification with # private properties

+62 -62
+62 -62
src/morphlex.ts
··· 101 101 } 102 102 103 103 class Morph { 104 - private readonly idMap: IdMap = new WeakMap() 105 - private readonly options: Options 104 + readonly #idMap: IdMap = new WeakMap() 105 + readonly #options: Options 106 106 107 107 constructor(options: Options = {}) { 108 - this.options = options 108 + this.#options = options 109 109 } 110 110 111 111 // Find longest increasing subsequence to minimize moves during reordering 112 112 // Returns the indices in the sequence that form the LIS 113 - private longestIncreasingSubsequence(sequence: Array<number>): Array<number> { 113 + #longestIncreasingSubsequence(sequence: Array<number>): Array<number> { 114 114 const n = sequence.length 115 115 if (n === 0) return [] 116 116 ··· 167 167 168 168 morph(from: ChildNode, to: ChildNode | NodeListOf<ChildNode>): void { 169 169 if (isParentNode(from)) { 170 - this.mapIdSets(from) 170 + this.#mapIdSets(from) 171 171 } 172 172 173 173 if (to instanceof NodeList) { 174 - this.mapIdSetsForEach(to) 175 - this.morphOneToMany(from, to) 174 + this.#mapIdSetsForEach(to) 175 + this.#morphOneToMany(from, to) 176 176 } else if (isParentNode(to)) { 177 - this.mapIdSets(to) 178 - this.morphOneToOne(from, to) 177 + this.#mapIdSets(to) 178 + this.#morphOneToOne(from, to) 179 179 } 180 180 } 181 181 182 - private morphOneToMany(from: ChildNode, to: NodeListOf<ChildNode>): void { 182 + #morphOneToMany(from: ChildNode, to: NodeListOf<ChildNode>): void { 183 183 const length = to.length 184 184 185 185 if (length === 0) { 186 - this.removeNode(from) 186 + this.#removeNode(from) 187 187 } else if (length === 1) { 188 - this.morphOneToOne(from, to[0]!) 188 + this.#morphOneToOne(from, to[0]!) 189 189 } else if (length > 1) { 190 190 const newNodes = Array.from(to) 191 - this.morphOneToOne(from, newNodes.shift()!) 191 + this.#morphOneToOne(from, newNodes.shift()!) 192 192 const insertionPoint = from.nextSibling 193 193 const parent = from.parentNode || document 194 194 195 195 for (const newNode of newNodes) { 196 - if (this.options.beforeNodeAdded?.(parent, newNode, insertionPoint) ?? true) { 196 + if (this.#options.beforeNodeAdded?.(parent, newNode, insertionPoint) ?? true) { 197 197 moveBefore(parent, newNode, insertionPoint) 198 - this.options.afterNodeAdded?.(newNode) 198 + this.#options.afterNodeAdded?.(newNode) 199 199 } 200 200 } 201 201 } 202 202 } 203 203 204 - private morphOneToOne(from: ChildNode, to: ChildNode): void { 204 + #morphOneToOne(from: ChildNode, to: ChildNode): void { 205 205 // Fast path: if nodes are exactly the same object, skip morphing 206 206 if (from === to) return 207 207 if (from.isEqualNode?.(to)) return 208 208 209 - if (!(this.options.beforeNodeVisited?.(from, to) ?? true)) return 209 + if (!(this.#options.beforeNodeVisited?.(from, to) ?? true)) return 210 210 211 211 const pair: PairOfNodes<ChildNode> = [from, to] 212 212 213 213 if (isElementPair(pair)) { 214 214 if (isMatchingElementPair(pair)) { 215 - this.morphMatchingElements(pair) 215 + this.#morphMatchingElements(pair) 216 216 } else { 217 - this.morphNonMatchingElements(pair) 217 + this.#morphNonMatchingElements(pair) 218 218 } 219 219 } else { 220 - this.morphOtherNode(pair) 220 + this.#morphOtherNode(pair) 221 221 } 222 222 223 - this.options.afterNodeVisited?.(from, to) 223 + this.#options.afterNodeVisited?.(from, to) 224 224 } 225 225 226 - private morphMatchingElements(pair: PairOfMatchingElements<Element>): void { 226 + #morphMatchingElements(pair: PairOfMatchingElements<Element>): void { 227 227 const [from, to] = pair 228 228 229 229 if (from.hasAttributes() || to.hasAttributes()) { 230 - this.visitAttributes(pair) 230 + this.#visitAttributes(pair) 231 231 } 232 232 233 233 if (isTextAreaElement(from) && isTextAreaElement(to)) { 234 - this.visitTextArea(pair as PairOfMatchingElements<HTMLTextAreaElement>) 234 + this.#visitTextArea(pair as PairOfMatchingElements<HTMLTextAreaElement>) 235 235 } else if (from.hasChildNodes() || to.hasChildNodes()) { 236 236 this.visitChildNodes(pair) 237 237 } 238 238 } 239 239 240 - private morphNonMatchingElements([from, to]: PairOfNodes<Element>): void { 241 - this.replaceNode(from, to) 240 + #morphNonMatchingElements([from, to]: PairOfNodes<Element>): void { 241 + this.#replaceNode(from, to) 242 242 } 243 243 244 - private morphOtherNode([from, to]: PairOfNodes<ChildNode>): void { 244 + #morphOtherNode([from, to]: PairOfNodes<ChildNode>): void { 245 245 if (from.nodeType === to.nodeType && from.nodeValue !== null && to.nodeValue !== null) { 246 246 from.nodeValue = to.nodeValue 247 247 } else { 248 - this.replaceNode(from, to) 248 + this.#replaceNode(from, to) 249 249 } 250 250 } 251 251 252 - private visitAttributes([from, to]: PairOfMatchingElements<Element>): void { 252 + #visitAttributes([from, to]: PairOfMatchingElements<Element>): void { 253 253 if (from.hasAttribute("morphlex-dirty")) { 254 254 from.removeAttribute("morphlex-dirty") 255 255 } ··· 258 258 for (const { name, value } of to.attributes) { 259 259 if (name === "value") { 260 260 if (isInputElement(from) && from.value !== value) { 261 - if (!this.options.preserveModified || from.value === from.defaultValue) { 261 + if (!this.#options.preserveModified || from.value === from.defaultValue) { 262 262 from.value = value 263 263 } 264 264 } ··· 266 266 267 267 if (name === "selected") { 268 268 if (isOptionElement(from) && !from.selected) { 269 - if (!this.options.preserveModified || from.selected === from.defaultSelected) { 269 + if (!this.#options.preserveModified || from.selected === from.defaultSelected) { 270 270 from.selected = true 271 271 } 272 272 } ··· 274 274 275 275 if (name === "checked") { 276 276 if (isInputElement(from) && !from.checked) { 277 - if (!this.options.preserveModified || from.checked === from.defaultChecked) { 277 + if (!this.#options.preserveModified || from.checked === from.defaultChecked) { 278 278 from.checked = true 279 279 } 280 280 } ··· 282 282 283 283 const oldValue = from.getAttribute(name) 284 284 285 - if (oldValue !== value && (this.options.beforeAttributeUpdated?.(from, name, value) ?? true)) { 285 + if (oldValue !== value && (this.#options.beforeAttributeUpdated?.(from, name, value) ?? true)) { 286 286 from.setAttribute(name, value) 287 - this.options.afterAttributeUpdated?.(from, name, oldValue) 287 + this.#options.afterAttributeUpdated?.(from, name, oldValue) 288 288 } 289 289 } 290 290 ··· 297 297 if (!to.hasAttribute(name)) { 298 298 if (name === "selected") { 299 299 if (isOptionElement(from) && from.selected) { 300 - if (!this.options.preserveModified || from.selected === from.defaultSelected) { 300 + if (!this.#options.preserveModified || from.selected === from.defaultSelected) { 301 301 from.selected = false 302 302 } 303 303 } ··· 305 305 306 306 if (name === "checked") { 307 307 if (isInputElement(from) && from.checked) { 308 - if (!this.options.preserveModified || from.checked === from.defaultChecked) { 308 + if (!this.#options.preserveModified || from.checked === from.defaultChecked) { 309 309 from.checked = false 310 310 } 311 311 } 312 312 } 313 313 314 - if (this.options.beforeAttributeUpdated?.(from, name, null) ?? true) { 314 + if (this.#options.beforeAttributeUpdated?.(from, name, null) ?? true) { 315 315 from.removeAttribute(name) 316 - this.options.afterAttributeUpdated?.(from, name, value) 316 + this.#options.afterAttributeUpdated?.(from, name, value) 317 317 } 318 318 } 319 319 } 320 320 } 321 321 322 - private visitTextArea([from, to]: PairOfMatchingElements<HTMLTextAreaElement>): void { 322 + #visitTextArea([from, to]: PairOfMatchingElements<HTMLTextAreaElement>): void { 323 323 const newTextContent = to.textContent || "" 324 324 const isModified = from.value !== from.defaultValue 325 325 ··· 328 328 from.textContent = newTextContent 329 329 } 330 330 331 - if (this.options.preserveModified && isModified) return 331 + if (this.#options.preserveModified && isModified) return 332 332 333 333 from.value = from.defaultValue 334 334 } 335 335 336 336 visitChildNodes([from, to]: PairOfMatchingElements<Element>): void { 337 - if (!(this.options.beforeChildrenVisited?.(from) ?? true)) return 337 + if (!(this.#options.beforeChildrenVisited?.(from) ?? true)) return 338 338 const parent = from 339 339 340 340 const fromChildNodes = from.childNodes ··· 388 388 const element = toChildNodes[i]! 389 389 if (!isElement(element)) continue 390 390 391 - const idSet = this.idMap.get(element) 391 + const idSet = this.#idMap.get(element) 392 392 if (!idSet) continue 393 393 394 394 candidateLoop: for (const candidate of candidateElements) { 395 395 if (isElement(candidate)) { 396 - const candidateIdSet = this.idMap.get(candidate) 396 + const candidateIdSet = this.#idMap.get(candidate) 397 397 if (candidateIdSet) { 398 398 for (const id of idSet) { 399 399 if (candidateIdSet.has(id)) { ··· 500 500 } 501 501 502 502 // Find LIS - these nodes don't need to move 503 - const lisIndices = this.longestIncreasingSubsequence(sequence) 503 + const lisIndices = this.#longestIncreasingSubsequence(sequence) 504 504 const shouldNotMove = new Set<number>() 505 505 for (const idx of lisIndices) { 506 506 shouldNotMove.add(sequence[idx]!) ··· 517 517 if (!shouldNotMove.has(matchIndex)) { 518 518 moveBefore(parent, match, insertionPoint) 519 519 } 520 - this.morphOneToOne(match, node) 520 + this.#morphOneToOne(match, node) 521 521 insertionPoint = match.nextSibling 522 522 // Skip over any nodes that will be removed to avoid unnecessary moves 523 523 while ( ··· 527 527 insertionPoint = insertionPoint.nextSibling 528 528 } 529 529 } else { 530 - if (this.options.beforeNodeAdded?.(parent, node, insertionPoint) ?? true) { 530 + if (this.#options.beforeNodeAdded?.(parent, node, insertionPoint) ?? true) { 531 531 moveBefore(parent, node, insertionPoint) 532 - this.options.afterNodeAdded?.(node) 532 + this.#options.afterNodeAdded?.(node) 533 533 insertionPoint = node.nextSibling 534 534 // Skip over any nodes that will be removed to avoid unnecessary moves 535 535 while ( ··· 544 544 545 545 // Remove any remaining unmatched candidates 546 546 for (const candidate of candidateNodes) { 547 - this.removeNode(candidate) 547 + this.#removeNode(candidate) 548 548 } 549 549 550 550 for (const candidate of candidateElements) { 551 - this.removeNode(candidate) 551 + this.#removeNode(candidate) 552 552 } 553 553 554 - this.options.afterChildrenVisited?.(from) 554 + this.#options.afterChildrenVisited?.(from) 555 555 } 556 556 557 - private replaceNode(node: ChildNode, newNode: ChildNode): void { 557 + #replaceNode(node: ChildNode, newNode: ChildNode): void { 558 558 const parent = node.parentNode || document 559 559 const insertionPoint = node 560 - if (this.options.beforeNodeAdded?.(parent, newNode, insertionPoint) ?? true) { 560 + if (this.#options.beforeNodeAdded?.(parent, newNode, insertionPoint) ?? true) { 561 561 moveBefore(parent, newNode, insertionPoint) 562 - this.options.afterNodeAdded?.(newNode) 563 - this.removeNode(node) 562 + this.#options.afterNodeAdded?.(newNode) 563 + this.#removeNode(node) 564 564 } 565 565 } 566 566 567 - private removeNode(node: ChildNode): void { 568 - if (this.options.beforeNodeRemoved?.(node) ?? true) { 567 + #removeNode(node: ChildNode): void { 568 + if (this.#options.beforeNodeRemoved?.(node) ?? true) { 569 569 node.remove() 570 - this.options.afterNodeRemoved?.(node) 570 + this.#options.afterNodeRemoved?.(node) 571 571 } 572 572 } 573 573 574 - private mapIdSetsForEach(nodeList: NodeList): void { 574 + #mapIdSetsForEach(nodeList: NodeList): void { 575 575 for (const childNode of nodeList) { 576 576 if (isParentNode(childNode)) { 577 - this.mapIdSets(childNode) 577 + this.#mapIdSets(childNode) 578 578 } 579 579 } 580 580 } 581 581 582 582 // For each node with an ID, push that ID into the IdSet on the IdMap, for each of its parent elements. 583 - private mapIdSets(node: ParentNode): void { 583 + #mapIdSets(node: ParentNode): void { 584 584 for (const elementWithId of node.querySelectorAll("[id]")) { 585 585 const id = elementWithId.id 586 586 ··· 589 589 let currentElement: Element | null = elementWithId 590 590 591 591 while (currentElement) { 592 - const idSet: IdSet | undefined = this.idMap.get(currentElement) 592 + const idSet: IdSet | undefined = this.#idMap.get(currentElement) 593 593 if (idSet) idSet.add(id) 594 - else this.idMap.set(currentElement, new Set([id])) 594 + else this.#idMap.set(currentElement, new Set([id])) 595 595 if (currentElement === node) break 596 596 currentElement = currentElement.parentElement 597 597 }