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
Simplify <head> morphing
joel.drapper.me
4 months ago
89304e25
667750cc
+46
-60
1 changed file
expand all
collapse all
unified
split
src
morphlex.ts
+46
-60
src/morphlex.ts
···
40
40
41
41
const pair: PairOfNodes<Node> = [from, to]
42
42
if (isElementPair(pair) && isMatchingElementPair(pair)) {
43
43
-
new Morph(options).morphChildren(pair)
43
43
+
new Morph(options).visitChildNodes(pair)
44
44
} else {
45
45
throw new Error("[Morphlex] You can only do an inner morph with matching elements.")
46
46
}
···
132
132
}
133
133
134
134
private morphMatchingElements(pair: PairOfMatchingElements<Element>): void {
135
135
-
this.visitAttributes(pair)
136
136
-
this.morphChildren(pair)
135
135
+
const [from, to] = pair
136
136
+
137
137
+
if (from.hasAttributes() || to.hasAttributes()) {
138
138
+
this.visitAttributes(pair)
139
139
+
}
140
140
+
141
141
+
if (from.hasChildNodes() || to.hasChildNodes()) {
142
142
+
this.visitChildNodes(pair)
143
143
+
}
137
144
}
138
145
139
146
private morphNonMatchingElements([from, to]: PairOfNodes<Element>): void {
···
152
159
153
160
private visitAttributes([from, to]: PairOfMatchingElements<Element>): void {
154
161
const isInput = isInputElement(from) && isInputElement(to)
155
155
-
const isOption = isInput ? false : isOptionElement(from)
162
162
+
const isOption = isOptionElement(from) && isOptionElement(to)
156
163
157
164
const toAttrs = to.attributes
158
165
const fromAttrs = from.attributes
···
164
171
const value = attr.value
165
172
const oldValue = from.getAttribute(name)
166
173
167
167
-
if (isInput && (name === "value" || name === "checked" || name === "indeterminate")) continue
168
168
-
if (isOption && name === "selected") continue
174
174
+
if (isInput) {
175
175
+
if (name === "value" || name === "checked" || name === "indeterminate") {
176
176
+
continue
177
177
+
} else if (name === "morph-value" && (this.options.beforeAttributeUpdated?.(from, name, value) ?? true)) {
178
178
+
from.setAttribute(name, value)
179
179
+
from.value = value
180
180
+
this.options.afterAttributeUpdated?.(from, name, oldValue)
181
181
+
continue
182
182
+
} else if (name === "morph-checked" && (this.options.beforeAttributeUpdated?.(from, name, value) ?? true)) {
183
183
+
from.setAttribute(name, value)
184
184
+
from.checked = value === "true"
185
185
+
this.options.afterAttributeUpdated?.(from, name, oldValue)
186
186
+
continue
187
187
+
} else if (name === "morph-indeterminate" && (this.options.beforeAttributeUpdated?.(from, name, value) ?? true)) {
188
188
+
from.setAttribute(name, value)
189
189
+
from.indeterminate = value === "true"
190
190
+
this.options.afterAttributeUpdated?.(from, name, oldValue)
191
191
+
continue
192
192
+
}
193
193
+
} else if (isOption) {
194
194
+
if (name === "selected") {
195
195
+
continue
196
196
+
} else if (name === "morph-selected") {
197
197
+
from.setAttribute(name, value)
198
198
+
from.selected = value === "true"
199
199
+
this.options.afterAttributeUpdated?.(from, name, oldValue)
200
200
+
}
201
201
+
}
169
202
170
203
if (oldValue !== value && (this.options.beforeAttributeUpdated?.(from, name, value) ?? true)) {
171
204
from.setAttribute(name, value)
···
191
224
}
192
225
}
193
226
194
194
-
morphChildren(pair: PairOfMatchingElements<Element>): void {
195
195
-
const [node, reference] = pair
196
196
-
if (!(this.options.beforeChildrenVisited?.(node) ?? true)) return
197
197
-
198
198
-
if (isHeadElement(node)) {
199
199
-
this.morphHeadChildren(pair as PairOfMatchingElements<HTMLHeadElement>)
200
200
-
} else if (node.hasChildNodes() || reference.hasChildNodes()) {
201
201
-
this.morphChildNodes(pair)
202
202
-
}
203
203
-
204
204
-
this.options.afterChildrenVisited?.(node)
205
205
-
}
206
206
-
207
207
-
// TODO: Review this.
208
208
-
private morphHeadChildren([node, reference]: PairOfMatchingElements<HTMLHeadElement>): void {
209
209
-
const refChildNodesMap: Map<string, Element> = new Map()
210
210
-
211
211
-
// Generate a map of the reference head element’s child nodes, keyed by their outerHTML.
212
212
-
const referenceChildrenLength = reference.children.length
213
213
-
for (let i = 0; i < referenceChildrenLength; i++) {
214
214
-
const child = reference.children[i]!
215
215
-
refChildNodesMap.set(child.outerHTML, child)
216
216
-
}
217
217
-
218
218
-
// Iterate backwards to safely remove children without affecting indices
219
219
-
for (let i = node.children.length - 1; i >= 0; i--) {
220
220
-
const child = node.children[i]!
221
221
-
const key = child.outerHTML
222
222
-
const refChild = refChildNodesMap.get(key)
223
223
-
224
224
-
// If the child is in the reference map already, we don't need to add it later.
225
225
-
// If it's not in the map, we need to remove it from the node.
226
226
-
if (refChild) refChildNodesMap.delete(key)
227
227
-
else this.removeNode(child)
228
228
-
}
229
229
-
230
230
-
// Any remaining nodes in the map should be appended to the head.
231
231
-
for (const refChild of refChildNodesMap.values()) this.appendChild(node, refChild)
232
232
-
}
233
233
-
234
234
-
private morphChildNodes([from, to]: PairOfMatchingElements<Element>): void {
227
227
+
visitChildNodes([from, to]: PairOfMatchingElements<Element>): void {
228
228
+
if (!(this.options.beforeChildrenVisited?.(from) ?? true)) return
235
229
const parent = from
236
230
237
231
const fromChildNodes = from.childNodes
···
298
292
const ariaLabel = node.getAttribute("aria-label")
299
293
const ariaDescription = node.getAttribute("aria-description")
300
294
const href = node.getAttribute("href")
295
295
+
const src = node.getAttribute("src")
301
296
302
297
for (const candidate of candidates) {
303
298
if (
···
307
302
(name !== "" && name === candidate.getAttribute("name")) ||
308
303
(ariaLabel !== "" && ariaLabel === candidate.getAttribute("aria-label")) ||
309
304
(ariaDescription !== "" && ariaDescription === candidate.getAttribute("aria-description")) ||
310
310
-
(href !== "" && href === candidate.getAttribute("href")))
305
305
+
(href !== "" && href === candidate.getAttribute("href")) ||
306
306
+
(src !== "" && src === candidate.getAttribute("src")))
311
307
) {
312
308
matches.set(node, candidate)
313
309
unmatched.delete(node)
···
378
374
for (const candidate of candidates) {
379
375
this.removeNode(candidate)
380
376
}
377
377
+
378
378
+
this.options.afterChildrenVisited?.(from)
381
379
}
382
380
383
381
private replaceNode(node: ChildNode, newNode: ChildNode): void {
···
387
385
moveBefore(parent, newNode, insertionPoint)
388
386
this.options.afterNodeAdded?.(newNode)
389
387
this.removeNode(node)
390
390
-
}
391
391
-
}
392
392
-
393
393
-
private appendChild(parent: ParentNode, newChild: ChildNode): void {
394
394
-
const insertionPoint = null
395
395
-
if (this.options.beforeNodeAdded?.(parent, newChild, insertionPoint) ?? true) {
396
396
-
moveBefore(parent, newChild, insertionPoint)
397
397
-
this.options.afterNodeAdded?.(newChild)
398
388
}
399
389
}
400
390
···
453
443
454
444
function isOptionElement(element: Element): element is HTMLOptionElement {
455
445
return element.localName === "option"
456
456
-
}
457
457
-
458
458
-
function isHeadElement(element: Element): element is HTMLHeadElement {
459
459
-
return element.localName === "head"
460
446
}
461
447
462
448
function isParentNode(node: Node): node is ParentNode {