Precise DOM morphing
morphing typescript dom

Ops, we lost an optimisation from earlier

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