Precise DOM morphing
morphing typescript dom

Ops, we lost an optimisation from earlier

+66 -68
+66 -68
src/morphlex.ts
··· 454 if (!(this.#options.beforeChildrenVisited?.(from) ?? true)) return 455 const parent = from 456 457 - const fromChildNodes = from.childNodes 458 const toChildNodes = Array.from(to.childNodes) 459 460 - const candidateNodes: Set<ChildNode> = new Set() 461 - const candidateElements: Set<Element> = new Set() 462 463 const matches: Array<ChildNode | null> = Array.from({ length: toChildNodes.length }, () => null) 464 465 - for (const candidate of fromChildNodes) { 466 - if (isElement(candidate)) candidateElements.add(candidate) 467 - else candidateNodes.add(candidate) 468 } 469 470 - // Match elements by isEqualNode 471 for (let i = 0; i < toChildNodes.length; i++) { 472 - const element = toChildNodes[i]! 473 - if (!isElement(element)) continue 474 475 - for (const candidate of candidateElements) { 476 if (candidate.isEqualNode(element)) { 477 matches[i] = candidate 478 - candidateElements.delete(candidate) 479 break 480 } 481 } 482 } 483 484 // Match by exact id 485 - for (let i = 0; i < toChildNodes.length; i++) { 486 - if (matches[i]) continue 487 - const element = toChildNodes[i]! 488 - if (!isElement(element)) continue 489 490 const id = element.id 491 if (id === "") continue 492 493 - for (const candidate of candidateElements) { 494 if (element.localName === candidate.localName && id === candidate.id) { 495 matches[i] = candidate 496 - candidateElements.delete(candidate) 497 break 498 } 499 } 500 } 501 502 // Match by idSet 503 - for (let i = 0; i < toChildNodes.length; i++) { 504 - if (matches[i]) continue 505 - const element = toChildNodes[i]! 506 - if (!isElement(element)) continue 507 508 const idSet = this.#idMap.get(element) 509 if (!idSet) continue 510 511 - candidateLoop: for (const candidate of candidateElements) { 512 - if (isElement(candidate)) { 513 - const candidateIdSet = this.#idMap.get(candidate) 514 - if (candidateIdSet) { 515 - for (const id of idSet) { 516 - if (candidateIdSet.has(id)) { 517 - matches[i] = candidate 518 - candidateElements.delete(candidate) 519 - break candidateLoop 520 - } 521 } 522 } 523 } ··· 525 } 526 527 // Match by heuristics 528 - for (let i = 0; i < toChildNodes.length; i++) { 529 - if (matches[i]) continue 530 - const element = toChildNodes[i]! 531 - if (!isElement(element)) continue 532 533 const name = element.getAttribute("name") 534 const href = element.getAttribute("href") 535 const src = element.getAttribute("src") 536 537 - for (const candidate of candidateElements) { 538 if ( 539 - isElement(candidate) && 540 element.localName === candidate.localName && 541 ((name && name === candidate.getAttribute("name")) || 542 (href && href === candidate.getAttribute("href")) || 543 (src && src === candidate.getAttribute("src"))) 544 ) { 545 matches[i] = candidate 546 - candidateElements.delete(candidate) 547 break 548 } 549 } 550 } 551 552 // Match by tagName 553 - for (let i = 0; i < toChildNodes.length; i++) { 554 - if (matches[i]) continue 555 - const element = toChildNodes[i]! 556 - if (!isElement(element)) continue 557 558 const localName = element.localName 559 560 - for (const candidate of candidateElements) { 561 if (localName === candidate.localName) { 562 if (isInputElement(candidate) && isInputElement(element) && candidate.type !== element.type) { 563 // Treat inputs with different type as though they are different tags. 564 continue 565 } 566 matches[i] = candidate 567 - candidateElements.delete(candidate) 568 break 569 } 570 } 571 } 572 573 // Match nodes by isEqualNode (skip whitespace-only text nodes) 574 - for (let i = 0; i < toChildNodes.length; i++) { 575 - if (matches[i]) continue 576 const node = toChildNodes[i]! 577 - if (isElement(node)) continue 578 if (isWhitespace(node)) continue 579 580 - for (const candidate of candidateNodes) { 581 if (candidate.isEqualNode(node)) { 582 matches[i] = candidate 583 - candidateNodes.delete(candidate) 584 break 585 } 586 } 587 } 588 589 // Match by nodeType (skip whitespace-only text nodes) 590 - for (let i = 0; i < toChildNodes.length; i++) { 591 - if (matches[i]) continue 592 const node = toChildNodes[i]! 593 - if (isElement(node)) continue 594 if (isWhitespace(node)) continue 595 596 const nodeType = node.nodeType 597 598 - for (const candidate of candidateNodes) { 599 if (nodeType === candidate.nodeType) { 600 matches[i] = candidate 601 - candidateNodes.delete(candidate) 602 break 603 } 604 } 605 } 606 607 - // Remove unmatched nodes from candidate sets (they were matched and should not be removed) 608 - for (const match of matches) { 609 - if (match) { 610 - candidateNodes.delete(match) 611 - if (isElement(match)) { 612 - candidateElements.delete(match) 613 - } 614 - } 615 - } 616 - 617 // Remove any unmatched candidates first, before calculating LIS and repositioning 618 - for (const candidate of candidateNodes) { 619 - this.#removeNode(candidate) 620 } 621 622 - for (const candidate of candidateElements) { 623 - this.#removeNode(candidate) 624 } 625 626 // Build sequence of current indices for LIS calculation (after removals)
··· 454 if (!(this.#options.beforeChildrenVisited?.(from) ?? true)) return 455 const parent = from 456 457 + const fromChildNodes = Array.from(from.childNodes) 458 const toChildNodes = Array.from(to.childNodes) 459 460 + const candidateNodes: Set<number> = new Set() 461 + const candidateElements: Set<number> = new Set() 462 463 const matches: Array<ChildNode | null> = Array.from({ length: toChildNodes.length }, () => null) 464 465 + for (let i = 0; i < fromChildNodes.length; i++) { 466 + const candidate = fromChildNodes[i]! 467 + if (isElement(candidate)) candidateElements.add(i) 468 + else candidateNodes.add(i) 469 } 470 471 + const unmatchedElements: Set<number> = new Set() 472 + const unmatchedNodes: Set<number> = new Set() 473 + 474 for (let i = 0; i < toChildNodes.length; i++) { 475 + const node = toChildNodes[i]! 476 + if (isElement(node)) unmatchedElements.add(i) 477 + else unmatchedNodes.add(i) 478 + } 479 + 480 + // Match elements by isEqualNode 481 + for (const i of unmatchedElements) { 482 + const element = toChildNodes[i] as Element 483 484 + for (const candidateIndex of candidateElements) { 485 + const candidate = fromChildNodes[candidateIndex]! 486 if (candidate.isEqualNode(element)) { 487 matches[i] = candidate 488 + candidateElements.delete(candidateIndex) 489 + unmatchedElements.delete(i) 490 break 491 } 492 } 493 } 494 495 // Match by exact id 496 + for (const i of unmatchedElements) { 497 + const element = toChildNodes[i] as Element 498 499 const id = element.id 500 if (id === "") continue 501 502 + for (const candidateIndex of candidateElements) { 503 + const candidate = fromChildNodes[candidateIndex] as Element 504 if (element.localName === candidate.localName && id === candidate.id) { 505 matches[i] = candidate 506 + candidateElements.delete(candidateIndex) 507 + unmatchedElements.delete(i) 508 break 509 } 510 } 511 } 512 513 // Match by idSet 514 + for (const i of unmatchedElements) { 515 + const element = toChildNodes[i] as Element 516 517 const idSet = this.#idMap.get(element) 518 if (!idSet) continue 519 520 + candidateLoop: for (const candidateIndex of candidateElements) { 521 + const candidate = fromChildNodes[candidateIndex] as Element 522 + const candidateIdSet = this.#idMap.get(candidate) 523 + if (candidateIdSet) { 524 + for (const id of idSet) { 525 + if (candidateIdSet.has(id)) { 526 + matches[i] = candidate 527 + candidateElements.delete(candidateIndex) 528 + unmatchedElements.delete(i) 529 + break candidateLoop 530 } 531 } 532 } ··· 534 } 535 536 // Match by heuristics 537 + for (const i of unmatchedElements) { 538 + const element = toChildNodes[i] as Element 539 540 const name = element.getAttribute("name") 541 const href = element.getAttribute("href") 542 const src = element.getAttribute("src") 543 544 + for (const candidateIndex of candidateElements) { 545 + const candidate = fromChildNodes[candidateIndex] as Element 546 if ( 547 element.localName === candidate.localName && 548 ((name && name === candidate.getAttribute("name")) || 549 (href && href === candidate.getAttribute("href")) || 550 (src && src === candidate.getAttribute("src"))) 551 ) { 552 matches[i] = candidate 553 + candidateElements.delete(candidateIndex) 554 + unmatchedElements.delete(i) 555 break 556 } 557 } 558 } 559 560 // Match by tagName 561 + for (const i of unmatchedElements) { 562 + const element = toChildNodes[i] as Element 563 564 const localName = element.localName 565 566 + for (const candidateIndex of candidateElements) { 567 + const candidate = fromChildNodes[candidateIndex] as Element 568 if (localName === candidate.localName) { 569 if (isInputElement(candidate) && isInputElement(element) && candidate.type !== element.type) { 570 // Treat inputs with different type as though they are different tags. 571 continue 572 } 573 matches[i] = candidate 574 + candidateElements.delete(candidateIndex) 575 + unmatchedElements.delete(i) 576 break 577 } 578 } 579 } 580 581 // Match nodes by isEqualNode (skip whitespace-only text nodes) 582 + for (const i of unmatchedNodes) { 583 const node = toChildNodes[i]! 584 if (isWhitespace(node)) continue 585 586 + for (const candidateIndex of candidateNodes) { 587 + const candidate = fromChildNodes[candidateIndex]! 588 if (candidate.isEqualNode(node)) { 589 matches[i] = candidate 590 + candidateNodes.delete(candidateIndex) 591 + unmatchedNodes.delete(i) 592 break 593 } 594 } 595 } 596 597 // Match by nodeType (skip whitespace-only text nodes) 598 + for (const i of unmatchedNodes) { 599 const node = toChildNodes[i]! 600 if (isWhitespace(node)) continue 601 602 const nodeType = node.nodeType 603 604 + for (const candidateIndex of candidateNodes) { 605 + const candidate = fromChildNodes[candidateIndex]! 606 if (nodeType === candidate.nodeType) { 607 matches[i] = candidate 608 + candidateNodes.delete(candidateIndex) 609 + unmatchedNodes.delete(i) 610 break 611 } 612 } 613 } 614 615 // Remove any unmatched candidates first, before calculating LIS and repositioning 616 + for (const candidateIndex of candidateNodes) { 617 + this.#removeNode(fromChildNodes[candidateIndex]!) 618 } 619 620 + for (const candidateIndex of candidateElements) { 621 + this.#removeNode(fromChildNodes[candidateIndex]!) 622 } 623 624 // Build sequence of current indices for LIS calculation (after removals)