tangled
alpha
login
or
join now
yippee.fun
/
morphlex
0
fork
atom
Precise DOM morphing
morphing
typescript
dom
0
fork
atom
overview
issues
pulls
pipelines
Ops, we lost an optimisation from earlier
joel.drapper.me
4 months ago
ca55133d
4b89ad0b
+66
-68
1 changed file
expand all
collapse all
unified
split
src
morphlex.ts
+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)
0
468
}
469
470
-
// Match elements by isEqualNode
0
0
471
for (let i = 0; i < toChildNodes.length; i++) {
472
-
const element = toChildNodes[i]!
473
-
if (!isElement(element)) continue
0
0
0
0
0
0
474
475
-
for (const candidate of candidateElements) {
0
476
if (candidate.isEqualNode(element)) {
477
matches[i] = candidate
478
-
candidateElements.delete(candidate)
0
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) {
0
494
if (element.localName === candidate.localName && id === candidate.id) {
495
matches[i] = candidate
496
-
candidateElements.delete(candidate)
0
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) {
0
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)
0
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) {
0
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)
0
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) {
0
581
if (candidate.isEqualNode(node)) {
582
matches[i] = candidate
583
-
candidateNodes.delete(candidate)
0
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) {
0
599
if (nodeType === candidate.nodeType) {
600
matches[i] = candidate
601
-
candidateNodes.delete(candidate)
0
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
0
0
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
0
0
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
0
0
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 (
0
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
0
0
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) {
0
583
const node = toChildNodes[i]!
0
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) {
0
599
const node = toChildNodes[i]!
0
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
0
0
0
0
0
0
0
0
0
0
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)