Skip to content

Commit cd3de91

Browse files
committed
Fix detached node morphing and cover edge cases
1 parent c868c9c commit cd3de91

3 files changed

Lines changed: 382 additions & 72 deletions

File tree

src/morphlex.ts

Lines changed: 47 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,6 @@ const ELEMENT_NODE_TYPE = 1
33
const TEXT_NODE_TYPE = 3
44
const TREE_WALKER_SHOW_ELEMENT = 1
55

6-
const IS_PARENT_NODE_TYPE = [
7-
0, // 0: (unused)
8-
1, // 1: Element
9-
0, // 2: Attribute (deprecated)
10-
0, // 3: Text
11-
0, // 4: CDATASection (deprecated)
12-
0, // 5: EntityReference (deprecated)
13-
0, // 6: Entity (deprecated)
14-
0, // 7: ProcessingInstruction
15-
0, // 8: Comment
16-
1, // 9: Document
17-
0, // 10: DocumentType
18-
1, // 11: DocumentFragment
19-
0, // 12: Notation (deprecated)
20-
]
21-
226
const Operation = {
237
EqualNode: 0,
248
SameElement: 1,
@@ -149,7 +133,7 @@ export function morphDocument(from: Document, to: Document | string, options?: O
149133
export function morph(from: ChildNode, to: ChildNode | NodeListOf<ChildNode> | string, options: Options = {}): void {
150134
if (typeof to === "string") to = parseFragment(to).childNodes
151135

152-
if (isParentNode(from)) flagDirtyInputs(from)
136+
if (isElementNode(from)) flagDirtyInputs(from)
153137
new Morph(options).morph(from, to)
154138
}
155139

@@ -175,34 +159,29 @@ export function morphInner(from: ChildNode, to: ChildNode | string, options: Opt
175159
}
176160
}
177161

178-
if (
179-
from.nodeType === ELEMENT_NODE_TYPE &&
180-
to.nodeType === ELEMENT_NODE_TYPE &&
181-
(from as Element).localName === (to as Element).localName
182-
) {
183-
if (isParentNode(from)) flagDirtyInputs(from)
184-
new Morph(options).visitChildNodes(from as Element, to as Element)
162+
if (isElementNode(from) && isElementNode(to) && from.localName === to.localName) {
163+
flagDirtyInputs(from)
164+
new Morph(options).visitChildNodes(from, to)
185165
} else {
186166
throw new Error("[Morphlex] You can only do an inner morph with matching elements.")
187167
}
188168
}
189169

190-
function flagDirtyInputs(node: ParentNode): void {
191-
if (node.nodeType === ELEMENT_NODE_TYPE) {
192-
const element = node as Element
193-
if (isInputElement(element)) {
194-
if (element.value !== element.defaultValue || element.checked !== element.defaultChecked) {
195-
element.setAttribute("morphlex-dirty", "")
196-
}
197-
} else if (isOptionElement(element)) {
198-
if (element.selected !== element.defaultSelected) {
199-
element.setAttribute("morphlex-dirty", "")
200-
}
201-
} else if (element.localName === "textarea") {
202-
const textarea = element as HTMLTextAreaElement
203-
if (textarea.value !== textarea.defaultValue) {
204-
textarea.setAttribute("morphlex-dirty", "")
205-
}
170+
171+
172+
function flagDirtyInputs(node: Element): void {
173+
if (isInputElement(node)) {
174+
if (node.value !== node.defaultValue || node.checked !== node.defaultChecked) {
175+
node.setAttribute("morphlex-dirty", "")
176+
}
177+
} else if (isOptionElement(node)) {
178+
if (node.selected !== node.defaultSelected) {
179+
node.setAttribute("morphlex-dirty", "")
180+
}
181+
} else if (node.localName === "textarea") {
182+
const textarea = node as HTMLTextAreaElement
183+
if (textarea.value !== textarea.defaultValue) {
184+
textarea.setAttribute("morphlex-dirty", "")
206185
}
207186
}
208187

@@ -238,9 +217,13 @@ function parseDocument(string: string): Document {
238217
}
239218

240219
function moveBefore(parent: ParentNode, node: ChildNode, insertionPoint: ChildNode | null): void {
220+
/* v8 ignore if -- @preserve */
241221
if (node === insertionPoint) return
222+
/* v8 ignore if -- @preserve */
242223
if (node.parentNode === parent) {
224+
/* v8 ignore if -- @preserve */
243225
if (node.nextSibling === insertionPoint) return
226+
/* v8 ignore if -- @preserve */
244227
if (SUPPORTS_MOVE_BEFORE) {
245228
;(parent as NodeWithMoveBefore).moveBefore(node, insertionPoint)
246229
return
@@ -260,15 +243,17 @@ class Morph {
260243
}
261244

262245
morph(from: ChildNode, to: ChildNode | NodeListOf<ChildNode>): void {
263-
if (isParentNode(from)) {
246+
if (isElementNode(from)) {
264247
this.#mapIdSets(from)
265248
}
266249

267250
if (to instanceof NodeList) {
268251
this.#mapIdArraysForEach(to)
269252
this.#morphOneToMany(from, to)
270-
} else if (isParentNode(to)) {
271-
this.#mapIdArrays(to)
253+
} else {
254+
if (isElementNode(to)) {
255+
this.#mapIdArrays(to)
256+
}
272257
this.#morphOneToOne(from, to)
273258
}
274259
}
@@ -280,15 +265,17 @@ class Morph {
280265
this.#removeNode(from)
281266
} else if (length === 1) {
282267
this.#morphOneToOne(from, to[0]!)
283-
} else if (length > 1) {
268+
} else {
284269
const newNodes = [...to]
285270
this.#morphOneToOne(from, newNodes.shift()!)
286271
const insertionPoint = from.nextSibling
287-
const parent = from.parentNode || document
272+
const parent = from.parentNode
273+
const callbackParent = parent || document
288274

289275
for (let i = 0; i < newNodes.length; i++) {
290276
const newNode = newNodes[i]!
291-
if (this.#options.beforeNodeAdded?.(parent, newNode, insertionPoint) ?? true) {
277+
if (this.#options.beforeNodeAdded?.(callbackParent, newNode, insertionPoint) ?? true) {
278+
if (!parent) continue
292279
parent.insertBefore(newNode, insertionPoint)
293280
this.#options.afterNodeAdded?.(newNode)
294281
}
@@ -340,9 +327,7 @@ class Morph {
340327
if (!(this.#options.beforeNodeVisited?.(from, to) ?? true)) return
341328

342329
if (from.nodeType === to.nodeType && from.nodeValue !== null && to.nodeValue !== null) {
343-
if (from.nodeValue !== to.nodeValue) {
344-
from.nodeValue = to.nodeValue
345-
}
330+
from.nodeValue = to.nodeValue
346331
} else {
347332
this.#replaceNode(from, to)
348333
}
@@ -512,7 +497,6 @@ class Morph {
512497
// Match elements by isEqualNode
513498
for (let i = 0; i < unmatchedElementIndices.length; i++) {
514499
const unmatchedIndex = unmatchedElementIndices[i]!
515-
if (!unmatchedElementActive[unmatchedIndex]) continue
516500

517501
const localName = localNameMap[unmatchedIndex]
518502
const element = toChildNodes[unmatchedIndex] as Element
@@ -685,7 +669,6 @@ class Morph {
685669
// Match nodes by isEqualNode (skip whitespace-only text nodes)
686670
for (let i = 0; i < unmatchedNodeIndices.length; i++) {
687671
const unmatchedIndex = unmatchedNodeIndices[i]!
688-
if (!unmatchedNodeActive[unmatchedIndex]) continue
689672

690673
const node = toChildNodes[unmatchedIndex]!
691674
for (let c = 0; c < candidateNodeIndices.length; c++) {
@@ -787,13 +770,15 @@ class Morph {
787770
}
788771

789772
#replaceNode(node: ChildNode, newNode: ChildNode): void {
790-
const parent = node.parentNode || document
773+
const parent = node.parentNode
774+
const callbackParent = parent || document
791775
const insertionPoint = node
792776
// Check if both removal and addition are allowed before starting the replacement
793777
if (
794778
(this.#options.beforeNodeRemoved?.(node) ?? true) &&
795-
(this.#options.beforeNodeAdded?.(parent, newNode, insertionPoint) ?? true)
779+
(this.#options.beforeNodeAdded?.(callbackParent, newNode, insertionPoint) ?? true)
796780
) {
781+
if (!parent) return
797782
parent.insertBefore(newNode, insertionPoint)
798783
this.#options.afterNodeAdded?.(newNode)
799784
node.remove()
@@ -810,21 +795,19 @@ class Morph {
810795

811796
#mapIdArraysForEach(nodeList: NodeList): void {
812797
for (const childNode of nodeList) {
813-
if (isParentNode(childNode)) {
798+
if (isElementNode(childNode)) {
814799
this.#mapIdArrays(childNode)
815800
}
816801
}
817802
}
818803

819804
// For each node with an ID, push that ID into the IdArray on the IdArrayMap, for each of its parent elements.
820-
#mapIdArrays(node: ParentNode): void {
805+
#mapIdArrays(node: Element): void {
821806
const idArrayMap = this.#idArrayMap
822807

823808
forEachDescendantElementWithId(node, (element) => {
824809
const id = element.id
825810

826-
if (id === "") return
827-
828811
let currentElement: Element | null = element
829812

830813
while (currentElement) {
@@ -841,14 +824,12 @@ class Morph {
841824
}
842825

843826
// For each node with an ID, add that ID into the IdSet on the IdSetMap, for each of its parent elements.
844-
#mapIdSets(node: ParentNode): void {
827+
#mapIdSets(node: Element): void {
845828
const idSetMap = this.#idSetMap
846829

847830
forEachDescendantElementWithId(node, (element) => {
848831
const id = element.id
849832

850-
if (id === "") return
851-
852833
let currentElement: Element | null = element
853834

854835
while (currentElement) {
@@ -865,12 +846,8 @@ class Morph {
865846
}
866847
}
867848

868-
function forEachDescendantElementWithId(node: ParentNode, callback: (element: Element) => void): void {
869-
const root = node as Node
870-
const ownerDocument = root.nodeType === 9 ? (root as Document) : root.ownerDocument
871-
if (!ownerDocument) return
872-
873-
const walker = ownerDocument.createTreeWalker(root, TREE_WALKER_SHOW_ELEMENT)
849+
function forEachDescendantElementWithId(node: Element, callback: (element: Element) => void): void {
850+
const walker = node.ownerDocument!.createTreeWalker(node, TREE_WALKER_SHOW_ELEMENT)
874851
let current = walker.nextNode()
875852

876853
while (current) {
@@ -907,6 +884,10 @@ function isWhitespaceTextNode(node: Node): boolean {
907884
return true
908885
}
909886

887+
function isElementNode(node: Node): node is Element {
888+
return node.nodeType === ELEMENT_NODE_TYPE
889+
}
890+
910891
function isInputElement(element: Element): element is HTMLInputElement {
911892
return element.localName === "input"
912893
}
@@ -925,10 +906,6 @@ function isOptionElement(element: Element): element is HTMLOptionElement {
925906
return element.localName === "option"
926907
}
927908

928-
function isParentNode(node: Node): node is ParentNode {
929-
return !!IS_PARENT_NODE_TYPE[node.nodeType]
930-
}
931-
932909
// Find longest increasing subsequence to minimize moves during reordering
933910
// Returns the indices in the sequence that form the LIS
934911
function longestIncreasingSubsequence(sequence: Array<number | undefined>): Array<number> {
@@ -962,8 +939,6 @@ function longestIncreasingSubsequence(sequence: Array<number | undefined>): Arra
962939
if (left === lisLength) lisLength++
963940
}
964941

965-
if (lisLength === 0) return []
966-
967942
const result = new Array<number>(lisLength)
968943
let curr = indices[lisLength - 1]!
969944

0 commit comments

Comments
 (0)