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
Move options to `this`
joel.drapper.me
2 years ago
691223b7
e83be73a
+103
-63
3 changed files
expand all
collapse all
unified
split
dist
morphlex.d.ts
morphlex.js
src
morphlex.ts
+2
-1
dist/morphlex.d.ts
···
1
1
-
export interface Options {
1
1
+
interface Options {
2
2
ignoreActiveValue?: boolean;
3
3
preserveModifiedValues?: boolean;
4
4
beforeNodeMorphed?: (node: Node, referenceNode: Node) => boolean;
···
13
13
afterPropertyUpdated?: (node: Node, propertyName: PropertyKey, previousValue: unknown) => void;
14
14
}
15
15
export declare function morph(node: ChildNode, reference: ChildNode | string, options?: Options): void;
16
16
+
export {};
+49
-28
dist/morphlex.js
···
16
16
}
17
17
}
18
18
class Morph {
19
19
-
#options;
20
19
#idMap;
21
20
#sensivityMap;
21
21
+
#ignoreActiveValue;
22
22
+
#preserveModifiedValues;
23
23
+
#beforeNodeMorphed;
24
24
+
#afterNodeMorphed;
25
25
+
#beforeNodeAdded;
26
26
+
#afterNodeAdded;
27
27
+
#beforeNodeRemoved;
28
28
+
#afterNodeRemoved;
29
29
+
#beforeAttributeUpdated;
30
30
+
#afterAttributeUpdated;
31
31
+
#beforePropertyUpdated;
32
32
+
#afterPropertyUpdated;
22
33
constructor(options = {}) {
23
23
-
this.#options = options;
24
34
this.#idMap = new WeakMap();
25
35
this.#sensivityMap = new WeakMap();
26
26
-
Object.freeze(this.#options);
36
36
+
this.#ignoreActiveValue = options.ignoreActiveValue || false;
37
37
+
this.#preserveModifiedValues = options.preserveModifiedValues || false;
38
38
+
this.#beforeNodeMorphed = options.beforeNodeMorphed;
39
39
+
this.#afterNodeMorphed = options.afterNodeMorphed;
40
40
+
this.#beforeNodeAdded = options.beforeNodeAdded;
41
41
+
this.#afterNodeAdded = options.afterNodeAdded;
42
42
+
this.#beforeNodeRemoved = options.beforeNodeRemoved;
43
43
+
this.#afterNodeRemoved = options.afterNodeRemoved;
44
44
+
this.#beforeAttributeUpdated = options.beforeAttributeUpdated;
45
45
+
this.#afterAttributeUpdated = options.afterAttributeUpdated;
46
46
+
this.#beforePropertyUpdated = options.beforePropertyUpdated;
47
47
+
this.#afterPropertyUpdated = options.afterPropertyUpdated;
27
48
Object.freeze(this);
28
49
}
29
50
morph(node, reference) {
···
86
107
}
87
108
}
88
109
#morphMatchingElementNode(node, reference) {
89
89
-
if (!(this.#options.beforeNodeMorphed?.(node, reference) ?? true)) return;
110
110
+
if (!(this.#beforeNodeMorphed?.(node, reference) ?? true)) return;
90
111
if (node.hasAttributes() || reference.hasAttributes()) this.#morphAttributes(node, reference);
91
112
if (isHead(node)) {
92
113
this.#morphHead(node, reference);
93
114
} else if (node.hasChildNodes() || reference.hasChildNodes()) this.#morphChildNodes(node, reference);
94
94
-
this.#options.afterNodeMorphed?.(node, reference);
115
115
+
this.#afterNodeMorphed?.(node, reference);
95
116
}
96
117
#morphOtherNode(node, reference) {
97
97
-
if (!(this.#options.beforeNodeMorphed?.(node, reference) ?? true)) return;
118
118
+
if (!(this.#beforeNodeMorphed?.(node, reference) ?? true)) return;
98
119
if (node.nodeType === reference.nodeType && node.nodeValue !== null && reference.nodeValue !== null) {
99
120
// Handle text nodes, comments, and CDATA sections.
100
121
this.#updateProperty(node, "nodeValue", reference.nodeValue);
101
122
} else this.#replaceNode(node, reference.cloneNode(true));
102
102
-
this.#options.afterNodeMorphed?.(node, reference);
123
123
+
this.#afterNodeMorphed?.(node, reference);
103
124
}
104
125
#morphHead(node, reference) {
105
126
const refChildNodesMap = new Map();
···
118
139
#morphAttributes(element, reference) {
119
140
// Remove any excess attributes from the element that aren’t present in the reference.
120
141
for (const { name, value } of element.attributes) {
121
121
-
if (!reference.hasAttribute(name) && (this.#options.beforeAttributeUpdated?.(element, name, null) ?? true)) {
142
142
+
if (!reference.hasAttribute(name) && (this.#beforeAttributeUpdated?.(element, name, null) ?? true)) {
122
143
element.removeAttribute(name);
123
123
-
this.#options.afterAttributeUpdated?.(element, name, value);
144
144
+
this.#afterAttributeUpdated?.(element, name, value);
124
145
}
125
146
}
126
147
// Copy attributes from the reference to the element, if they don’t already match.
127
148
for (const { name, value } of reference.attributes) {
128
149
const previousValue = element.getAttribute(name);
129
129
-
if (previousValue !== value && (this.#options.beforeAttributeUpdated?.(element, name, value) ?? true)) {
150
150
+
if (previousValue !== value && (this.#beforeAttributeUpdated?.(element, name, value) ?? true)) {
130
151
element.setAttribute(name, value);
131
131
-
this.#options.afterAttributeUpdated?.(element, name, previousValue);
152
152
+
this.#afterAttributeUpdated?.(element, name, previousValue);
132
153
}
133
154
}
134
155
// For certain types of elements, we need to do some extra work to ensure
···
139
160
this.#updateProperty(element, "indeterminate", reference.indeterminate);
140
161
if (
141
162
element.type !== "file" &&
142
142
-
!(this.#options.ignoreActiveValue && document.activeElement === element) &&
143
143
-
!(this.#options.preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue)
163
163
+
!(this.#ignoreActiveValue && document.activeElement === element) &&
164
164
+
!(this.#preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue)
144
165
) {
145
166
this.#updateProperty(element, "value", reference.value);
146
167
}
···
149
170
} else if (
150
171
isTextArea(element) &&
151
172
isTextArea(reference) &&
152
152
-
!(this.#options.ignoreActiveValue && document.activeElement === element) &&
153
153
-
!(this.#options.preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue)
173
173
+
!(this.#ignoreActiveValue && document.activeElement === element) &&
174
174
+
!(this.#preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue)
154
175
) {
155
176
this.#updateProperty(element, "value", reference.value);
156
177
const text = element.firstElementChild;
···
183
204
}
184
205
}
185
206
#morphChildElement(child, reference, parent) {
186
186
-
if (!(this.#options.beforeNodeMorphed?.(child, reference) ?? true)) return;
207
207
+
if (!(this.#beforeNodeMorphed?.(child, reference) ?? true)) return;
187
208
const refIdSet = this.#idMap.get(reference);
188
209
// Generate the array in advance of the loop
189
210
const refSetArray = refIdSet ? [...refIdSet] : [];
···
216
237
this.#morphNode(nextMatchByTagName, reference);
217
238
} else {
218
239
const newNode = reference.cloneNode(true);
219
219
-
if (this.#options.beforeNodeAdded?.(newNode) ?? true) {
240
240
+
if (this.#beforeNodeAdded?.(newNode) ?? true) {
220
241
this.#insertBefore(parent, newNode, child);
221
221
-
this.#options.afterNodeAdded?.(newNode);
242
242
+
this.#afterNodeAdded?.(newNode);
222
243
}
223
244
}
224
224
-
this.#options.afterNodeMorphed?.(child, reference);
245
245
+
this.#afterNodeMorphed?.(child, reference);
225
246
}
226
247
#updateProperty(node, propertyName, newValue) {
227
248
const previousValue = node[propertyName];
228
228
-
if (previousValue !== newValue && (this.#options.beforePropertyUpdated?.(node, propertyName, newValue) ?? true)) {
249
249
+
if (previousValue !== newValue && (this.#beforePropertyUpdated?.(node, propertyName, newValue) ?? true)) {
229
250
node[propertyName] = newValue;
230
230
-
this.#options.afterPropertyUpdated?.(node, propertyName, previousValue);
251
251
+
this.#afterPropertyUpdated?.(node, propertyName, previousValue);
231
252
}
232
253
}
233
254
#replaceNode(node, newNode) {
234
234
-
if ((this.#options.beforeNodeRemoved?.(node) ?? true) && (this.#options.beforeNodeAdded?.(newNode) ?? true)) {
255
255
+
if ((this.#beforeNodeRemoved?.(node) ?? true) && (this.#beforeNodeAdded?.(newNode) ?? true)) {
235
256
node.replaceWith(newNode);
236
236
-
this.#options.afterNodeAdded?.(newNode);
237
237
-
this.#options.afterNodeRemoved?.(node);
257
257
+
this.#afterNodeAdded?.(newNode);
258
258
+
this.#afterNodeRemoved?.(node);
238
259
}
239
260
}
240
261
#insertBefore(parent, node, insertionPoint) {
···
256
277
parent.insertBefore(node, insertionPoint);
257
278
}
258
279
#appendChild(node, newNode) {
259
259
-
if (this.#options.beforeNodeAdded?.(newNode) ?? true) {
280
280
+
if (this.#beforeNodeAdded?.(newNode) ?? true) {
260
281
node.appendChild(newNode);
261
261
-
this.#options.afterNodeAdded?.(newNode);
282
282
+
this.#afterNodeAdded?.(newNode);
262
283
}
263
284
}
264
285
#removeNode(node) {
265
265
-
if (this.#options.beforeNodeRemoved?.(node) ?? true) {
286
286
+
if (this.#beforeNodeRemoved?.(node) ?? true) {
266
287
node.remove();
267
267
-
this.#options.afterNodeRemoved?.(node);
288
288
+
this.#afterNodeRemoved?.(node);
268
289
}
269
290
}
270
291
}
+52
-34
src/morphlex.ts
···
28
28
readonly length: NodeListOf<T>["length"];
29
29
};
30
30
31
31
-
export interface Options {
31
31
+
interface Options {
32
32
ignoreActiveValue?: boolean;
33
33
preserveModifiedValues?: boolean;
34
34
-
35
34
beforeNodeMorphed?: (node: Node, referenceNode: Node) => boolean;
36
35
afterNodeMorphed?: (node: Node, referenceNode: Node) => void;
37
37
-
38
36
beforeNodeAdded?: (node: Node) => boolean;
39
37
afterNodeAdded?: (node: Node) => void;
40
40
-
41
38
beforeNodeRemoved?: (node: Node) => boolean;
42
39
afterNodeRemoved?: (node: Node) => void;
43
43
-
44
40
beforeAttributeUpdated?: (element: Element, attributeName: string, newValue: string | null) => boolean;
45
41
afterAttributeUpdated?: (element: Element, attributeName: string, previousValue: string | null) => void;
46
46
-
47
42
beforePropertyUpdated?: (node: Node, propertyName: PropertyKey, newValue: unknown) => boolean;
48
43
afterPropertyUpdated?: (node: Node, propertyName: PropertyKey, previousValue: unknown) => void;
49
44
}
···
68
63
}
69
64
70
65
class Morph {
71
71
-
readonly #options: Options;
72
66
readonly #idMap: IdMap;
73
67
readonly #sensivityMap: SensivityMap;
74
68
69
69
+
readonly #ignoreActiveValue: boolean;
70
70
+
readonly #preserveModifiedValues: boolean;
71
71
+
readonly #beforeNodeMorphed?: (node: Node, referenceNode: Node) => boolean;
72
72
+
readonly #afterNodeMorphed?: (node: Node, referenceNode: Node) => void;
73
73
+
readonly #beforeNodeAdded?: (node: Node) => boolean;
74
74
+
readonly #afterNodeAdded?: (node: Node) => void;
75
75
+
readonly #beforeNodeRemoved?: (node: Node) => boolean;
76
76
+
readonly #afterNodeRemoved?: (node: Node) => void;
77
77
+
readonly #beforeAttributeUpdated?: (element: Element, attributeName: string, newValue: string | null) => boolean;
78
78
+
readonly #afterAttributeUpdated?: (element: Element, attributeName: string, previousValue: string | null) => void;
79
79
+
readonly #beforePropertyUpdated?: (node: Node, propertyName: PropertyKey, newValue: unknown) => boolean;
80
80
+
readonly #afterPropertyUpdated?: (node: Node, propertyName: PropertyKey, previousValue: unknown) => void;
81
81
+
75
82
constructor(options: Options = {}) {
76
76
-
this.#options = options;
77
83
this.#idMap = new WeakMap();
78
84
this.#sensivityMap = new WeakMap();
79
85
80
80
-
Object.freeze(this.#options);
86
86
+
this.#ignoreActiveValue = options.ignoreActiveValue || false;
87
87
+
this.#preserveModifiedValues = options.preserveModifiedValues || false;
88
88
+
this.#beforeNodeMorphed = options.beforeNodeMorphed;
89
89
+
this.#afterNodeMorphed = options.afterNodeMorphed;
90
90
+
this.#beforeNodeAdded = options.beforeNodeAdded;
91
91
+
this.#afterNodeAdded = options.afterNodeAdded;
92
92
+
this.#beforeNodeRemoved = options.beforeNodeRemoved;
93
93
+
this.#afterNodeRemoved = options.afterNodeRemoved;
94
94
+
this.#beforeAttributeUpdated = options.beforeAttributeUpdated;
95
95
+
this.#afterAttributeUpdated = options.afterAttributeUpdated;
96
96
+
this.#beforePropertyUpdated = options.beforePropertyUpdated;
97
97
+
this.#afterPropertyUpdated = options.afterPropertyUpdated;
98
98
+
81
99
Object.freeze(this);
82
100
}
83
101
···
155
173
}
156
174
157
175
#morphMatchingElementNode(node: Element, reference: ReadonlyNode<Element>): void {
158
158
-
if (!(this.#options.beforeNodeMorphed?.(node, reference as ChildNode) ?? true)) return;
176
176
+
if (!(this.#beforeNodeMorphed?.(node, reference as ChildNode) ?? true)) return;
159
177
160
178
if (node.hasAttributes() || reference.hasAttributes()) this.#morphAttributes(node, reference);
161
179
···
163
181
this.#morphHead(node, reference as ReadonlyNode<HTMLHeadElement>);
164
182
} else if (node.hasChildNodes() || reference.hasChildNodes()) this.#morphChildNodes(node, reference);
165
183
166
166
-
this.#options.afterNodeMorphed?.(node, reference as ChildNode);
184
184
+
this.#afterNodeMorphed?.(node, reference as ChildNode);
167
185
}
168
186
169
187
#morphOtherNode(node: ChildNode, reference: ReadonlyNode<ChildNode>): void {
170
170
-
if (!(this.#options.beforeNodeMorphed?.(node, reference as ChildNode) ?? true)) return;
188
188
+
if (!(this.#beforeNodeMorphed?.(node, reference as ChildNode) ?? true)) return;
171
189
172
190
if (node.nodeType === reference.nodeType && node.nodeValue !== null && reference.nodeValue !== null) {
173
191
// Handle text nodes, comments, and CDATA sections.
174
192
this.#updateProperty(node, "nodeValue", reference.nodeValue);
175
193
} else this.#replaceNode(node, reference.cloneNode(true));
176
194
177
177
-
this.#options.afterNodeMorphed?.(node, reference as ChildNode);
195
195
+
this.#afterNodeMorphed?.(node, reference as ChildNode);
178
196
}
179
197
180
198
#morphHead(node: HTMLHeadElement, reference: ReadonlyNode<HTMLHeadElement>): void {
···
199
217
#morphAttributes(element: Element, reference: ReadonlyNode<Element>): void {
200
218
// Remove any excess attributes from the element that aren’t present in the reference.
201
219
for (const { name, value } of element.attributes) {
202
202
-
if (!reference.hasAttribute(name) && (this.#options.beforeAttributeUpdated?.(element, name, null) ?? true)) {
220
220
+
if (!reference.hasAttribute(name) && (this.#beforeAttributeUpdated?.(element, name, null) ?? true)) {
203
221
element.removeAttribute(name);
204
204
-
this.#options.afterAttributeUpdated?.(element, name, value);
222
222
+
this.#afterAttributeUpdated?.(element, name, value);
205
223
}
206
224
}
207
225
208
226
// Copy attributes from the reference to the element, if they don’t already match.
209
227
for (const { name, value } of reference.attributes) {
210
228
const previousValue = element.getAttribute(name);
211
211
-
if (previousValue !== value && (this.#options.beforeAttributeUpdated?.(element, name, value) ?? true)) {
229
229
+
if (previousValue !== value && (this.#beforeAttributeUpdated?.(element, name, value) ?? true)) {
212
230
element.setAttribute(name, value);
213
213
-
this.#options.afterAttributeUpdated?.(element, name, previousValue);
231
231
+
this.#afterAttributeUpdated?.(element, name, previousValue);
214
232
}
215
233
}
216
234
···
222
240
this.#updateProperty(element, "indeterminate", reference.indeterminate);
223
241
if (
224
242
element.type !== "file" &&
225
225
-
!(this.#options.ignoreActiveValue && document.activeElement === element) &&
226
226
-
!(this.#options.preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue)
243
243
+
!(this.#ignoreActiveValue && document.activeElement === element) &&
244
244
+
!(this.#preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue)
227
245
) {
228
246
this.#updateProperty(element, "value", reference.value);
229
247
}
···
232
250
} else if (
233
251
isTextArea(element) &&
234
252
isTextArea(reference) &&
235
235
-
!(this.#options.ignoreActiveValue && document.activeElement === element) &&
236
236
-
!(this.#options.preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue)
253
253
+
!(this.#ignoreActiveValue && document.activeElement === element) &&
254
254
+
!(this.#preserveModifiedValues && element.name === reference.name && element.value !== element.defaultValue)
237
255
) {
238
256
this.#updateProperty(element, "value", reference.value);
239
257
···
272
290
}
273
291
274
292
#morphChildElement(child: Element, reference: ReadonlyNode<Element>, parent: Element): void {
275
275
-
if (!(this.#options.beforeNodeMorphed?.(child, reference as ChildNode) ?? true)) return;
293
293
+
if (!(this.#beforeNodeMorphed?.(child, reference as ChildNode) ?? true)) return;
276
294
277
295
const refIdSet = this.#idMap.get(reference);
278
296
···
314
332
this.#morphNode(nextMatchByTagName, reference);
315
333
} else {
316
334
const newNode = reference.cloneNode(true);
317
317
-
if (this.#options.beforeNodeAdded?.(newNode) ?? true) {
335
335
+
if (this.#beforeNodeAdded?.(newNode) ?? true) {
318
336
this.#insertBefore(parent, newNode, child);
319
319
-
this.#options.afterNodeAdded?.(newNode);
337
337
+
this.#afterNodeAdded?.(newNode);
320
338
}
321
339
}
322
340
323
323
-
this.#options.afterNodeMorphed?.(child, reference as ChildNode);
341
341
+
this.#afterNodeMorphed?.(child, reference as ChildNode);
324
342
}
325
343
326
344
#updateProperty<N extends Node, P extends keyof N>(node: N, propertyName: P, newValue: N[P]): void {
327
345
const previousValue = node[propertyName];
328
346
329
329
-
if (previousValue !== newValue && (this.#options.beforePropertyUpdated?.(node, propertyName, newValue) ?? true)) {
347
347
+
if (previousValue !== newValue && (this.#beforePropertyUpdated?.(node, propertyName, newValue) ?? true)) {
330
348
node[propertyName] = newValue;
331
331
-
this.#options.afterPropertyUpdated?.(node, propertyName, previousValue);
349
349
+
this.#afterPropertyUpdated?.(node, propertyName, previousValue);
332
350
}
333
351
}
334
352
335
353
#replaceNode(node: ChildNode, newNode: Node): void {
336
336
-
if ((this.#options.beforeNodeRemoved?.(node) ?? true) && (this.#options.beforeNodeAdded?.(newNode) ?? true)) {
354
354
+
if ((this.#beforeNodeRemoved?.(node) ?? true) && (this.#beforeNodeAdded?.(newNode) ?? true)) {
337
355
node.replaceWith(newNode);
338
338
-
this.#options.afterNodeAdded?.(newNode);
339
339
-
this.#options.afterNodeRemoved?.(node);
356
356
+
this.#afterNodeAdded?.(newNode);
357
357
+
this.#afterNodeRemoved?.(node);
340
358
}
341
359
}
342
360
···
366
384
}
367
385
368
386
#appendChild(node: ParentNode, newNode: Node): void {
369
369
-
if (this.#options.beforeNodeAdded?.(newNode) ?? true) {
387
387
+
if (this.#beforeNodeAdded?.(newNode) ?? true) {
370
388
node.appendChild(newNode);
371
371
-
this.#options.afterNodeAdded?.(newNode);
389
389
+
this.#afterNodeAdded?.(newNode);
372
390
}
373
391
}
374
392
375
393
#removeNode(node: ChildNode): void {
376
376
-
if (this.#options.beforeNodeRemoved?.(node) ?? true) {
394
394
+
if (this.#beforeNodeRemoved?.(node) ?? true) {
377
395
node.remove();
378
378
-
this.#options.afterNodeRemoved?.(node);
396
396
+
this.#afterNodeRemoved?.(node);
379
397
}
380
398
}
381
399
}