Precise DOM morphing
morphing typescript dom
0
fork

Configure Feed

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

New algorithm

+123 -57
+123 -57
src/morphlex.ts
··· 269 269 } 270 270 271 271 private morphChildNodes([from, to]: PairOfMatchingElements<Element>): void { 272 + const parent = from 273 + 272 274 const fromChildNodes = from.childNodes 273 275 const toChildNodes = Array.from(to.childNodes) 274 276 275 - // Process all reference nodes 276 - for (let i = 0; i < toChildNodes.length; i++) { 277 - const fromChildNode = fromChildNodes[i] 278 - const toChildNode = toChildNodes[i]! 277 + const candidates: Set<ChildNode> = new Set(fromChildNodes) 278 + const unmatched: Set<ChildNode> = new Set(toChildNodes) 279 279 280 - if (fromChildNode) { 281 - // Fast path: if nodes are exactly the same, skip morphing 282 - if (fromChildNode.isSameNode?.(toChildNode)) { 283 - continue 284 - } 280 + const matches: Map<ChildNode, ChildNode> = new Map() 285 281 286 - if (isElement(toChildNode)) { 287 - this.searchSiblingsToMorphChildElement(fromChildNode, toChildNode, from) 288 - } else { 289 - this.morphOneToOne(fromChildNode, toChildNode) 290 - } 291 - } else { 292 - // Add new node at the end 293 - if (this.options.beforeNodeAdded?.(toChildNode) ?? true) { 294 - moveBefore(from, toChildNode, null) 295 - this.options.afterNodeAdded?.(toChildNode) 282 + // Match by isEqualNode 283 + for (const node of unmatched) { 284 + for (const candidate of candidates) { 285 + if (candidate.isEqualNode(node)) { 286 + matches.set(node, candidate) 287 + unmatched.delete(node) 288 + candidates.delete(candidate) 296 289 } 297 290 } 298 291 } 299 292 300 - // Remove any excess nodes from the original 301 - // We iterate backwards through excess nodes and attempt to remove each one 302 - for (let i = from.childNodes.length - 1; i >= toChildNodes.length; i--) { 303 - this.removeNode(from.childNodes[i]!) 304 - } 305 - } 293 + // Match by exact id 294 + for (const node of unmatched) { 295 + if (!isElement(node)) continue 296 + const id = node.id 297 + if (id === "") continue 306 298 307 - private searchSiblingsToMorphChildElement(from: ChildNode, to: Element, parent: ParentNode): void { 308 - const id = to.id 309 - const idSet = this.idMap.get(to) 310 - const idSetArray = idSet ? [...idSet] : [] 299 + for (const candidate of candidates) { 300 + if (isElement(candidate) && id === candidate.id) { 301 + matches.set(node, candidate) 302 + unmatched.delete(node) 303 + candidates.delete(candidate) 304 + } 305 + } 306 + } 311 307 312 - let currentNode: ChildNode | null = from 313 - let bestMatch: Element | null = null 314 - let idSetMatches: number = 0 308 + // Match by idSet 309 + for (const node of unmatched) { 310 + if (!isElement(node)) continue 311 + const idSet = this.idMap.get(node) 312 + if (!idSet) continue 313 + const idSetArray = [...idSet] 315 314 316 - while (currentNode) { 317 - if (isElement(currentNode) && currentNode.localName === to.localName) { 318 - // If we found an exact match, this is the best option. 319 - if (id && id !== "" && id === currentNode.id) { 320 - bestMatch = currentNode 321 - break 315 + for (const candidate of candidates) { 316 + if (isElement(candidate)) { 317 + const candidateIdSet = this.idMap.get(candidate) 318 + if (candidateIdSet && idSetArray.some((id) => candidateIdSet.has(id))) { 319 + matches.set(node, candidate) 320 + unmatched.delete(node) 321 + candidates.delete(candidate) 322 + } 322 323 } 324 + } 325 + } 323 326 324 - // Try to find the node with the best idSet match 325 - const currentIdSet = this.idMap.get(currentNode) 326 - if (currentIdSet) { 327 - const numberOfMatches = idSetArray.filter((id) => currentIdSet.has(id)).length 328 - if (numberOfMatches > idSetMatches) { 329 - bestMatch = currentNode 330 - idSetMatches = numberOfMatches 327 + // Match by nodeType 328 + for (const node of unmatched) { 329 + const nodeType = node.nodeType 330 + 331 + if (isElement(node)) { 332 + const localName = node.localName 333 + 334 + for (const candidate of candidates) { 335 + if (isElement(candidate) && localName === candidate.localName) { 336 + matches.set(node, candidate) 337 + unmatched.delete(node) 338 + candidates.delete(candidate) 331 339 } 332 340 } 333 - 334 - // The fallback is to just use the next element with the same localName 335 - if (!bestMatch) { 336 - bestMatch = currentNode 341 + } else { 342 + for (const candidate of candidates) { 343 + if (nodeType === candidate.nodeType && node.nodeValue === candidate.nodeValue) { 344 + matches.set(node, candidate) 345 + unmatched.delete(node) 346 + candidates.delete(candidate) 347 + } 337 348 } 338 349 } 350 + } 339 351 340 - currentNode = currentNode.nextSibling 352 + let prevNode: ChildNode | null = null 353 + // remove remaining nodes in reverse order 354 + for (let i = toChildNodes.length - 1; i >= 0; i--) { 355 + const node = toChildNodes[i]! 356 + const match = matches.get(node) 357 + if (match) { 358 + moveBefore(parent, match, prevNode) 359 + this.morphOneToOne(match, node) 360 + prevNode = match 361 + } else { 362 + moveBefore(parent, node, prevNode) 363 + prevNode = node 364 + } 341 365 } 342 366 343 - if (bestMatch) { 344 - if (bestMatch !== from) { 345 - moveBefore(parent, bestMatch, from) 346 - } 347 - this.morphOneToOne(bestMatch, to) 348 - } else { 349 - this.morphOneToOne(from, to) 367 + const numberOfNodesToRemove = from.childNodes.length - toChildNodes.length 368 + for (let i = fromChildNodes.length - 1; i >= numberOfNodesToRemove; i--) { 369 + this.removeNode(from.childNodes[i]!) 350 370 } 351 371 } 372 + 373 + // private searchSiblingsToMorphChildElement(from: ChildNode, to: Element, parent: ParentNode): void { 374 + // const id = to.id 375 + // const idSet = this.idMap.get(to) 376 + // const idSetArray = idSet ? [...idSet] : [] 377 + 378 + // let currentNode: ChildNode | null = from 379 + // let bestMatch: Element | null = null 380 + // let idSetMatches: number = 0 381 + 382 + // while (currentNode) { 383 + // if (isElement(currentNode) && currentNode.localName === to.localName) { 384 + // // If we found an exact match, this is the best option. 385 + // if (id && id !== "" && id === currentNode.id) { 386 + // bestMatch = currentNode 387 + // break 388 + // } 389 + 390 + // // Try to find the node with the best idSet match 391 + // const currentIdSet = this.idMap.get(currentNode) 392 + // if (currentIdSet) { 393 + // const numberOfMatches = idSetArray.filter((id) => currentIdSet.has(id)).length 394 + // if (numberOfMatches > idSetMatches) { 395 + // bestMatch = currentNode 396 + // idSetMatches = numberOfMatches 397 + // } 398 + // } 399 + 400 + // // The fallback is to just use the next element with the same localName 401 + // if (!bestMatch) { 402 + // bestMatch = currentNode 403 + // } 404 + // } 405 + 406 + // currentNode = currentNode.nextSibling 407 + // } 408 + 409 + // if (bestMatch) { 410 + // if (bestMatch !== from) { 411 + // moveBefore(parent, bestMatch, from) 412 + // } 413 + // this.morphOneToOne(bestMatch, to) 414 + // } else { 415 + // this.morphOneToOne(from, to) 416 + // } 417 + // } 352 418 353 419 private updateProperty<N extends Node, P extends keyof N>(node: N, propertyName: P, newValue: N[P]): void { 354 420 const oldValue = node[propertyName]