@hyperspan/framework 0.0.3 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1278 @@
1
+ /**
2
+ * @typedef {object} ConfigHead
3
+ *
4
+ * @property {'merge' | 'append' | 'morph' | 'none'} [style]
5
+ * @property {boolean} [block]
6
+ * @property {boolean} [ignore]
7
+ * @property {function(Element): boolean} [shouldPreserve]
8
+ * @property {function(Element): boolean} [shouldReAppend]
9
+ * @property {function(Element): boolean} [shouldRemove]
10
+ * @property {function(Element, {added: Node[], kept: Element[], removed: Element[]}): void} [afterHeadMorphed]
11
+ */
12
+
13
+ /**
14
+ * @typedef {object} ConfigCallbacks
15
+ *
16
+ * @property {function(Node): boolean} [beforeNodeAdded]
17
+ * @property {function(Node): void} [afterNodeAdded]
18
+ * @property {function(Element, Node): boolean} [beforeNodeMorphed]
19
+ * @property {function(Element, Node): void} [afterNodeMorphed]
20
+ * @property {function(Element): boolean} [beforeNodeRemoved]
21
+ * @property {function(Element): void} [afterNodeRemoved]
22
+ * @property {function(string, Element, "update" | "remove"): boolean} [beforeAttributeUpdated]
23
+ */
24
+
25
+ /**
26
+ * @typedef {object} Config
27
+ *
28
+ * @property {'outerHTML' | 'innerHTML'} [morphStyle]
29
+ * @property {boolean} [ignoreActive]
30
+ * @property {boolean} [ignoreActiveValue]
31
+ * @property {boolean} [restoreFocus]
32
+ * @property {ConfigCallbacks} [callbacks]
33
+ * @property {ConfigHead} [head]
34
+ */
35
+
36
+ /**
37
+ * @typedef {function} NoOp
38
+ *
39
+ * @returns {void}
40
+ */
41
+
42
+ /**
43
+ * @typedef {object} ConfigHeadInternal
44
+ *
45
+ * @property {'merge' | 'append' | 'morph' | 'none'} style
46
+ * @property {boolean} [block]
47
+ * @property {boolean} [ignore]
48
+ * @property {(function(Element): boolean) | NoOp} shouldPreserve
49
+ * @property {(function(Element): boolean) | NoOp} shouldReAppend
50
+ * @property {(function(Element): boolean) | NoOp} shouldRemove
51
+ * @property {(function(Element, {added: Node[], kept: Element[], removed: Element[]}): void) | NoOp} afterHeadMorphed
52
+ */
53
+
54
+ /**
55
+ * @typedef {object} ConfigCallbacksInternal
56
+ *
57
+ * @property {(function(Node): boolean) | NoOp} beforeNodeAdded
58
+ * @property {(function(Node): void) | NoOp} afterNodeAdded
59
+ * @property {(function(Node, Node): boolean) | NoOp} beforeNodeMorphed
60
+ * @property {(function(Node, Node): void) | NoOp} afterNodeMorphed
61
+ * @property {(function(Node): boolean) | NoOp} beforeNodeRemoved
62
+ * @property {(function(Node): void) | NoOp} afterNodeRemoved
63
+ * @property {(function(string, Element, "update" | "remove"): boolean) | NoOp} beforeAttributeUpdated
64
+ */
65
+
66
+ /**
67
+ * @typedef {object} ConfigInternal
68
+ *
69
+ * @property {'outerHTML' | 'innerHTML'} morphStyle
70
+ * @property {boolean} [ignoreActive]
71
+ * @property {boolean} [ignoreActiveValue]
72
+ * @property {boolean} [restoreFocus]
73
+ * @property {ConfigCallbacksInternal} callbacks
74
+ * @property {ConfigHeadInternal} head
75
+ */
76
+
77
+ /**
78
+ * @typedef {Object} IdSets
79
+ * @property {Set<string>} persistentIds
80
+ * @property {Map<Node, Set<string>>} idMap
81
+ */
82
+
83
+ /**
84
+ * @typedef {Function} Morph
85
+ *
86
+ * @param {Element | Document} oldNode
87
+ * @param {Element | Node | HTMLCollection | Node[] | string | null} newContent
88
+ * @param {Config} [config]
89
+ * @returns {undefined | Node[]}
90
+ */
91
+
92
+ // base IIFE to define idiomorph
93
+ /**
94
+ *
95
+ * @type {{defaults: ConfigInternal, morph: Morph}}
96
+ */
97
+ var Idiomorph = (function () {
98
+ 'use strict';
99
+
100
+ /**
101
+ * @typedef {object} MorphContext
102
+ *
103
+ * @property {Element} target
104
+ * @property {Element} newContent
105
+ * @property {ConfigInternal} config
106
+ * @property {ConfigInternal['morphStyle']} morphStyle
107
+ * @property {ConfigInternal['ignoreActive']} ignoreActive
108
+ * @property {ConfigInternal['ignoreActiveValue']} ignoreActiveValue
109
+ * @property {ConfigInternal['restoreFocus']} restoreFocus
110
+ * @property {Map<Node, Set<string>>} idMap
111
+ * @property {Set<string>} persistentIds
112
+ * @property {ConfigInternal['callbacks']} callbacks
113
+ * @property {ConfigInternal['head']} head
114
+ * @property {HTMLDivElement} pantry
115
+ */
116
+
117
+ //=============================================================================
118
+ // AND NOW IT BEGINS...
119
+ //=============================================================================
120
+
121
+ const noOp = () => {};
122
+ /**
123
+ * Default configuration values, updatable by users now
124
+ * @type {ConfigInternal}
125
+ */
126
+ const defaults = {
127
+ morphStyle: 'outerHTML',
128
+ callbacks: {
129
+ beforeNodeAdded: noOp,
130
+ afterNodeAdded: noOp,
131
+ beforeNodeMorphed: noOp,
132
+ afterNodeMorphed: noOp,
133
+ beforeNodeRemoved: noOp,
134
+ afterNodeRemoved: noOp,
135
+ beforeAttributeUpdated: noOp,
136
+ },
137
+ head: {
138
+ style: 'merge',
139
+ shouldPreserve: (elt) => elt.getAttribute('im-preserve') === 'true',
140
+ shouldReAppend: (elt) => elt.getAttribute('im-re-append') === 'true',
141
+ shouldRemove: noOp,
142
+ afterHeadMorphed: noOp,
143
+ },
144
+ restoreFocus: true,
145
+ };
146
+
147
+ /**
148
+ * Core idiomorph function for morphing one DOM tree to another
149
+ *
150
+ * @param {Element | Document} oldNode
151
+ * @param {Element | Node | HTMLCollection | Node[] | string | null} newContent
152
+ * @param {Config} [config]
153
+ * @returns {Promise<Node[]> | Node[]}
154
+ */
155
+ function morph(oldNode, newContent, config = {}) {
156
+ oldNode = normalizeElement(oldNode);
157
+ const newNode = normalizeParent(newContent);
158
+ const ctx = createMorphContext(oldNode, newNode, config);
159
+
160
+ const morphedNodes = saveAndRestoreFocus(ctx, () => {
161
+ return withHeadBlocking(
162
+ ctx,
163
+ oldNode,
164
+ newNode,
165
+ /** @param {MorphContext} ctx */ (ctx) => {
166
+ if (ctx.morphStyle === 'innerHTML') {
167
+ morphChildren(ctx, oldNode, newNode);
168
+ return Array.from(oldNode.childNodes);
169
+ } else {
170
+ return morphOuterHTML(ctx, oldNode, newNode);
171
+ }
172
+ }
173
+ );
174
+ });
175
+
176
+ ctx.pantry.remove();
177
+ return morphedNodes;
178
+ }
179
+
180
+ /**
181
+ * Morph just the outerHTML of the oldNode to the newContent
182
+ * We have to be careful because the oldNode could have siblings which need to be untouched
183
+ * @param {MorphContext} ctx
184
+ * @param {Element} oldNode
185
+ * @param {Element} newNode
186
+ * @returns {Node[]}
187
+ */
188
+ function morphOuterHTML(ctx, oldNode, newNode) {
189
+ const oldParent = normalizeParent(oldNode);
190
+ morphChildren(
191
+ ctx,
192
+ oldParent,
193
+ newNode,
194
+ // these two optional params are the secret sauce
195
+ oldNode, // start point for iteration
196
+ oldNode.nextSibling // end point for iteration
197
+ );
198
+ // this is safe even with siblings, because normalizeParent returns a SlicedParentNode if needed.
199
+ return Array.from(oldParent.childNodes);
200
+ }
201
+
202
+ /**
203
+ * @param {MorphContext} ctx
204
+ * @param {Function} fn
205
+ * @returns {Promise<Node[]> | Node[]}
206
+ */
207
+ function saveAndRestoreFocus(ctx, fn) {
208
+ if (!ctx.config.restoreFocus) return fn();
209
+ let activeElement = /** @type {HTMLInputElement|HTMLTextAreaElement|null} */ (
210
+ document.activeElement
211
+ );
212
+
213
+ // don't bother if the active element is not an input or textarea
214
+ if (
215
+ !(activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement)
216
+ ) {
217
+ return fn();
218
+ }
219
+
220
+ const { id: activeElementId, selectionStart, selectionEnd } = activeElement;
221
+
222
+ const results = fn();
223
+
224
+ if (activeElementId && activeElementId !== document.activeElement?.id) {
225
+ activeElement = ctx.target.querySelector(`[id="${activeElementId}"]`);
226
+ activeElement?.focus();
227
+ }
228
+ if (activeElement && !activeElement.selectionEnd && selectionEnd) {
229
+ activeElement.setSelectionRange(selectionStart, selectionEnd);
230
+ }
231
+
232
+ return results;
233
+ }
234
+
235
+ const morphChildren = (function () {
236
+ /**
237
+ * This is the core algorithm for matching up children. The idea is to use id sets to try to match up
238
+ * nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but
239
+ * by using id sets, we are able to better match up with content deeper in the DOM.
240
+ *
241
+ * Basic algorithm:
242
+ * - for each node in the new content:
243
+ * - search self and siblings for an id set match, falling back to a soft match
244
+ * - if match found
245
+ * - remove any nodes up to the match:
246
+ * - pantry persistent nodes
247
+ * - delete the rest
248
+ * - morph the match
249
+ * - elsif no match found, and node is persistent
250
+ * - find its match by querying the old root (future) and pantry (past)
251
+ * - move it and its children here
252
+ * - morph it
253
+ * - else
254
+ * - create a new node from scratch as a last result
255
+ *
256
+ * @param {MorphContext} ctx the merge context
257
+ * @param {Element} oldParent the old content that we are merging the new content into
258
+ * @param {Element} newParent the parent element of the new content
259
+ * @param {Node|null} [insertionPoint] the point in the DOM we start morphing at (defaults to first child)
260
+ * @param {Node|null} [endPoint] the point in the DOM we stop morphing at (defaults to after last child)
261
+ */
262
+ function morphChildren(ctx, oldParent, newParent, insertionPoint = null, endPoint = null) {
263
+ // normalize
264
+ if (oldParent instanceof HTMLTemplateElement && newParent instanceof HTMLTemplateElement) {
265
+ // @ts-ignore we can pretend the DocumentFragment is an Element
266
+ oldParent = oldParent.content;
267
+ // @ts-ignore ditto
268
+ newParent = newParent.content;
269
+ }
270
+ insertionPoint ||= oldParent.firstChild;
271
+
272
+ // run through all the new content
273
+ for (const newChild of newParent.childNodes) {
274
+ // once we reach the end of the old parent content skip to the end and insert the rest
275
+ if (insertionPoint && insertionPoint != endPoint) {
276
+ const bestMatch = findBestMatch(ctx, newChild, insertionPoint, endPoint);
277
+ if (bestMatch) {
278
+ // if the node to morph is not at the insertion point then remove/move up to it
279
+ if (bestMatch !== insertionPoint) {
280
+ removeNodesBetween(ctx, insertionPoint, bestMatch);
281
+ }
282
+ morphNode(bestMatch, newChild, ctx);
283
+ insertionPoint = bestMatch.nextSibling;
284
+ continue;
285
+ }
286
+ }
287
+
288
+ // if the matching node is elsewhere in the original content
289
+ if (newChild instanceof Element && ctx.persistentIds.has(newChild.id)) {
290
+ // move it and all its children here and morph
291
+ const movedChild = moveBeforeById(oldParent, newChild.id, insertionPoint, ctx);
292
+ morphNode(movedChild, newChild, ctx);
293
+ insertionPoint = movedChild.nextSibling;
294
+ continue;
295
+ }
296
+
297
+ // last resort: insert the new node from scratch
298
+ const insertedNode = createNode(oldParent, newChild, insertionPoint, ctx);
299
+ // could be null if beforeNodeAdded prevented insertion
300
+ if (insertedNode) {
301
+ insertionPoint = insertedNode.nextSibling;
302
+ }
303
+ }
304
+
305
+ // remove any remaining old nodes that didn't match up with new content
306
+ while (insertionPoint && insertionPoint != endPoint) {
307
+ const tempNode = insertionPoint;
308
+ insertionPoint = insertionPoint.nextSibling;
309
+ removeNode(ctx, tempNode);
310
+ }
311
+ }
312
+
313
+ /**
314
+ * This performs the action of inserting a new node while handling situations where the node contains
315
+ * elements with persistent ids and possible state info we can still preserve by moving in and then morphing
316
+ *
317
+ * @param {Element} oldParent
318
+ * @param {Node} newChild
319
+ * @param {Node|null} insertionPoint
320
+ * @param {MorphContext} ctx
321
+ * @returns {Node|null}
322
+ */
323
+ function createNode(oldParent, newChild, insertionPoint, ctx) {
324
+ if (ctx.callbacks.beforeNodeAdded(newChild) === false) return null;
325
+ if (ctx.idMap.has(newChild)) {
326
+ // node has children with ids with possible state so create a dummy elt of same type and apply full morph algorithm
327
+ const newEmptyChild = document.createElement(/** @type {Element} */ (newChild).tagName);
328
+ oldParent.insertBefore(newEmptyChild, insertionPoint);
329
+ morphNode(newEmptyChild, newChild, ctx);
330
+ ctx.callbacks.afterNodeAdded(newEmptyChild);
331
+ return newEmptyChild;
332
+ } else {
333
+ // optimisation: no id state to preserve so we can just insert a clone of the newChild and its descendants
334
+ const newClonedChild = document.importNode(newChild, true); // importNode to not mutate newParent
335
+ oldParent.insertBefore(newClonedChild, insertionPoint);
336
+ ctx.callbacks.afterNodeAdded(newClonedChild);
337
+ return newClonedChild;
338
+ }
339
+ }
340
+
341
+ //=============================================================================
342
+ // Matching Functions
343
+ //=============================================================================
344
+ const findBestMatch = (function () {
345
+ /**
346
+ * Scans forward from the startPoint to the endPoint looking for a match
347
+ * for the node. It looks for an id set match first, then a soft match.
348
+ * We abort softmatching if we find two future soft matches, to reduce churn.
349
+ * @param {Node} node
350
+ * @param {MorphContext} ctx
351
+ * @param {Node | null} startPoint
352
+ * @param {Node | null} endPoint
353
+ * @returns {Node | null}
354
+ */
355
+ function findBestMatch(ctx, node, startPoint, endPoint) {
356
+ let softMatch = null;
357
+ let nextSibling = node.nextSibling;
358
+ let siblingSoftMatchCount = 0;
359
+
360
+ let cursor = startPoint;
361
+ while (cursor && cursor != endPoint) {
362
+ // soft matching is a prerequisite for id set matching
363
+ if (isSoftMatch(cursor, node)) {
364
+ if (isIdSetMatch(ctx, cursor, node)) {
365
+ return cursor; // found an id set match, we're done!
366
+ }
367
+
368
+ // we haven't yet saved a soft match fallback
369
+ if (softMatch === null) {
370
+ // the current soft match will hard match something else in the future, leave it
371
+ if (!ctx.idMap.has(cursor)) {
372
+ // save this as the fallback if we get through the loop without finding a hard match
373
+ softMatch = cursor;
374
+ }
375
+ }
376
+ }
377
+ if (softMatch === null && nextSibling && isSoftMatch(cursor, nextSibling)) {
378
+ // The next new node has a soft match with this node, so
379
+ // increment the count of future soft matches
380
+ siblingSoftMatchCount++;
381
+ nextSibling = nextSibling.nextSibling;
382
+
383
+ // If there are two future soft matches, block soft matching for this node to allow
384
+ // future siblings to soft match. This is to reduce churn in the DOM when an element
385
+ // is prepended.
386
+ if (siblingSoftMatchCount >= 2) {
387
+ softMatch = undefined;
388
+ }
389
+ }
390
+
391
+ // if the current node contains active element, stop looking for better future matches,
392
+ // because if one is found, this node will be moved to the pantry, reparenting it and thus losing focus
393
+ if (cursor.contains(document.activeElement)) break;
394
+
395
+ cursor = cursor.nextSibling;
396
+ }
397
+
398
+ return softMatch || null;
399
+ }
400
+
401
+ /**
402
+ *
403
+ * @param {MorphContext} ctx
404
+ * @param {Node} oldNode
405
+ * @param {Node} newNode
406
+ * @returns {boolean}
407
+ */
408
+ function isIdSetMatch(ctx, oldNode, newNode) {
409
+ let oldSet = ctx.idMap.get(oldNode);
410
+ let newSet = ctx.idMap.get(newNode);
411
+
412
+ if (!newSet || !oldSet) return false;
413
+
414
+ for (const id of oldSet) {
415
+ // a potential match is an id in the new and old nodes that
416
+ // has not already been merged into the DOM
417
+ // But the newNode content we call this on has not been
418
+ // merged yet and we don't allow duplicate IDs so it is simple
419
+ if (newSet.has(id)) {
420
+ return true;
421
+ }
422
+ }
423
+ return false;
424
+ }
425
+
426
+ /**
427
+ *
428
+ * @param {Node} oldNode
429
+ * @param {Node} newNode
430
+ * @returns {boolean}
431
+ */
432
+ function isSoftMatch(oldNode, newNode) {
433
+ // ok to cast: if one is not element, `id` and `tagName` will be undefined and we'll just compare that.
434
+ const oldElt = /** @type {Element} */ (oldNode);
435
+ const newElt = /** @type {Element} */ (newNode);
436
+
437
+ return (
438
+ oldElt.nodeType === newElt.nodeType &&
439
+ oldElt.tagName === newElt.tagName &&
440
+ // If oldElt has an `id` with possible state and it doesn't match newElt.id then avoid morphing.
441
+ // We'll still match an anonymous node with an IDed newElt, though, because if it got this far,
442
+ // its not persistent, and new nodes can't have any hidden state.
443
+ (!oldElt.id || oldElt.id === newElt.id)
444
+ );
445
+ }
446
+
447
+ return findBestMatch;
448
+ })();
449
+
450
+ //=============================================================================
451
+ // DOM Manipulation Functions
452
+ //=============================================================================
453
+
454
+ /**
455
+ * Gets rid of an unwanted DOM node; strategy depends on nature of its reuse:
456
+ * - Persistent nodes will be moved to the pantry for later reuse
457
+ * - Other nodes will have their hooks called, and then are removed
458
+ * @param {MorphContext} ctx
459
+ * @param {Node} node
460
+ */
461
+ function removeNode(ctx, node) {
462
+ // are we going to id set match this later?
463
+ if (ctx.idMap.has(node)) {
464
+ // skip callbacks and move to pantry
465
+ moveBefore(ctx.pantry, node, null);
466
+ } else {
467
+ // remove for realsies
468
+ if (ctx.callbacks.beforeNodeRemoved(node) === false) return;
469
+ node.parentNode?.removeChild(node);
470
+ ctx.callbacks.afterNodeRemoved(node);
471
+ }
472
+ }
473
+
474
+ /**
475
+ * Remove nodes between the start and end nodes
476
+ * @param {MorphContext} ctx
477
+ * @param {Node} startInclusive
478
+ * @param {Node} endExclusive
479
+ * @returns {Node|null}
480
+ */
481
+ function removeNodesBetween(ctx, startInclusive, endExclusive) {
482
+ /** @type {Node | null} */
483
+ let cursor = startInclusive;
484
+ // remove nodes until the endExclusive node
485
+ while (cursor && cursor !== endExclusive) {
486
+ let tempNode = /** @type {Node} */ (cursor);
487
+ cursor = cursor.nextSibling;
488
+ removeNode(ctx, tempNode);
489
+ }
490
+ return cursor;
491
+ }
492
+
493
+ /**
494
+ * Search for an element by id within the document and pantry, and move it using moveBefore.
495
+ *
496
+ * @param {Element} parentNode - The parent node to which the element will be moved.
497
+ * @param {string} id - The ID of the element to be moved.
498
+ * @param {Node | null} after - The reference node to insert the element before.
499
+ * If `null`, the element is appended as the last child.
500
+ * @param {MorphContext} ctx
501
+ * @returns {Element} The found element
502
+ */
503
+ function moveBeforeById(parentNode, id, after, ctx) {
504
+ const target =
505
+ /** @type {Element} - will always be found */
506
+ (
507
+ (ctx.target.id === id && ctx.target) ||
508
+ ctx.target.querySelector(`[id="${id}"]`) ||
509
+ ctx.pantry.querySelector(`[id="${id}"]`)
510
+ );
511
+ removeElementFromAncestorsIdMaps(target, ctx);
512
+ moveBefore(parentNode, target, after);
513
+ return target;
514
+ }
515
+
516
+ /**
517
+ * Removes an element from its ancestors' id maps. This is needed when an element is moved from the
518
+ * "future" via `moveBeforeId`. Otherwise, its erstwhile ancestors could be mistakenly moved to the
519
+ * pantry rather than being deleted, preventing their removal hooks from being called.
520
+ *
521
+ * @param {Element} element - element to remove from its ancestors' id maps
522
+ * @param {MorphContext} ctx
523
+ */
524
+ function removeElementFromAncestorsIdMaps(element, ctx) {
525
+ const id = element.id;
526
+ /** @ts-ignore - safe to loop in this way **/
527
+ while ((element = element.parentNode)) {
528
+ let idSet = ctx.idMap.get(element);
529
+ if (idSet) {
530
+ idSet.delete(id);
531
+ if (!idSet.size) {
532
+ ctx.idMap.delete(element);
533
+ }
534
+ }
535
+ }
536
+ }
537
+
538
+ /**
539
+ * Moves an element before another element within the same parent.
540
+ * Uses the proposed `moveBefore` API if available (and working), otherwise falls back to `insertBefore`.
541
+ * This is essentialy a forward-compat wrapper.
542
+ *
543
+ * @param {Element} parentNode - The parent node containing the after element.
544
+ * @param {Node} element - The element to be moved.
545
+ * @param {Node | null} after - The reference node to insert `element` before.
546
+ * If `null`, `element` is appended as the last child.
547
+ */
548
+ function moveBefore(parentNode, element, after) {
549
+ // @ts-ignore - use proposed moveBefore feature
550
+ if (parentNode.moveBefore) {
551
+ try {
552
+ // @ts-ignore - use proposed moveBefore feature
553
+ parentNode.moveBefore(element, after);
554
+ } catch (e) {
555
+ // fall back to insertBefore as some browsers may fail on moveBefore when trying to move Dom disconnected nodes to pantry
556
+ parentNode.insertBefore(element, after);
557
+ }
558
+ } else {
559
+ parentNode.insertBefore(element, after);
560
+ }
561
+ }
562
+
563
+ return morphChildren;
564
+ })();
565
+
566
+ //=============================================================================
567
+ // Single Node Morphing Code
568
+ //=============================================================================
569
+ const morphNode = (function () {
570
+ /**
571
+ * @param {Node} oldNode root node to merge content into
572
+ * @param {Node} newContent new content to merge
573
+ * @param {MorphContext} ctx the merge context
574
+ * @returns {Node | null} the element that ended up in the DOM
575
+ */
576
+ function morphNode(oldNode, newContent, ctx) {
577
+ if (ctx.ignoreActive && oldNode === document.activeElement) {
578
+ // don't morph focused element
579
+ return null;
580
+ }
581
+
582
+ if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) {
583
+ return oldNode;
584
+ }
585
+
586
+ if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) {
587
+ // ignore the head element
588
+ } else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== 'morph') {
589
+ // ok to cast: if newContent wasn't also a <head>, it would've got caught in the `!isSoftMatch` branch above
590
+ handleHeadElement(oldNode, /** @type {HTMLHeadElement} */ (newContent), ctx);
591
+ } else {
592
+ morphAttributes(oldNode, newContent, ctx);
593
+ if (!ignoreValueOfActiveElement(oldNode, ctx)) {
594
+ // @ts-ignore newContent can be a node here because .firstChild will be null
595
+ morphChildren(ctx, oldNode, newContent);
596
+ }
597
+ }
598
+ ctx.callbacks.afterNodeMorphed(oldNode, newContent);
599
+ return oldNode;
600
+ }
601
+
602
+ /**
603
+ * syncs the oldNode to the newNode, copying over all attributes and
604
+ * inner element state from the newNode to the oldNode
605
+ *
606
+ * @param {Node} oldNode the node to copy attributes & state to
607
+ * @param {Node} newNode the node to copy attributes & state from
608
+ * @param {MorphContext} ctx the merge context
609
+ */
610
+ function morphAttributes(oldNode, newNode, ctx) {
611
+ let type = newNode.nodeType;
612
+
613
+ // if is an element type, sync the attributes from the
614
+ // new node into the new node
615
+ if (type === 1 /* element type */) {
616
+ const oldElt = /** @type {Element} */ (oldNode);
617
+ const newElt = /** @type {Element} */ (newNode);
618
+
619
+ const oldAttributes = oldElt.attributes;
620
+ const newAttributes = newElt.attributes;
621
+ for (const newAttribute of newAttributes) {
622
+ if (ignoreAttribute(newAttribute.name, oldElt, 'update', ctx)) {
623
+ continue;
624
+ }
625
+ if (oldElt.getAttribute(newAttribute.name) !== newAttribute.value) {
626
+ oldElt.setAttribute(newAttribute.name, newAttribute.value);
627
+ }
628
+ }
629
+ // iterate backwards to avoid skipping over items when a delete occurs
630
+ for (let i = oldAttributes.length - 1; 0 <= i; i--) {
631
+ const oldAttribute = oldAttributes[i];
632
+
633
+ // toAttributes is a live NamedNodeMap, so iteration+mutation is unsafe
634
+ // e.g. custom element attribute callbacks can remove other attributes
635
+ if (!oldAttribute) continue;
636
+
637
+ if (!newElt.hasAttribute(oldAttribute.name)) {
638
+ if (ignoreAttribute(oldAttribute.name, oldElt, 'remove', ctx)) {
639
+ continue;
640
+ }
641
+ oldElt.removeAttribute(oldAttribute.name);
642
+ }
643
+ }
644
+
645
+ if (!ignoreValueOfActiveElement(oldElt, ctx)) {
646
+ syncInputValue(oldElt, newElt, ctx);
647
+ }
648
+ }
649
+
650
+ // sync text nodes
651
+ if (type === 8 /* comment */ || type === 3 /* text */) {
652
+ if (oldNode.nodeValue !== newNode.nodeValue) {
653
+ oldNode.nodeValue = newNode.nodeValue;
654
+ }
655
+ }
656
+ }
657
+
658
+ /**
659
+ * NB: many bothans died to bring us information:
660
+ *
661
+ * https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js
662
+ * https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113
663
+ *
664
+ * @param {Element} oldElement the element to sync the input value to
665
+ * @param {Element} newElement the element to sync the input value from
666
+ * @param {MorphContext} ctx the merge context
667
+ */
668
+ function syncInputValue(oldElement, newElement, ctx) {
669
+ if (
670
+ oldElement instanceof HTMLInputElement &&
671
+ newElement instanceof HTMLInputElement &&
672
+ newElement.type !== 'file'
673
+ ) {
674
+ let newValue = newElement.value;
675
+ let oldValue = oldElement.value;
676
+
677
+ // sync boolean attributes
678
+ syncBooleanAttribute(oldElement, newElement, 'checked', ctx);
679
+ syncBooleanAttribute(oldElement, newElement, 'disabled', ctx);
680
+
681
+ if (!newElement.hasAttribute('value')) {
682
+ if (!ignoreAttribute('value', oldElement, 'remove', ctx)) {
683
+ oldElement.value = '';
684
+ oldElement.removeAttribute('value');
685
+ }
686
+ } else if (oldValue !== newValue) {
687
+ if (!ignoreAttribute('value', oldElement, 'update', ctx)) {
688
+ oldElement.setAttribute('value', newValue);
689
+ oldElement.value = newValue;
690
+ }
691
+ }
692
+ // TODO: QUESTION(1cg): this used to only check `newElement` unlike the other branches -- why?
693
+ // did I break something?
694
+ } else if (
695
+ oldElement instanceof HTMLOptionElement &&
696
+ newElement instanceof HTMLOptionElement
697
+ ) {
698
+ syncBooleanAttribute(oldElement, newElement, 'selected', ctx);
699
+ } else if (
700
+ oldElement instanceof HTMLTextAreaElement &&
701
+ newElement instanceof HTMLTextAreaElement
702
+ ) {
703
+ let newValue = newElement.value;
704
+ let oldValue = oldElement.value;
705
+ if (ignoreAttribute('value', oldElement, 'update', ctx)) {
706
+ return;
707
+ }
708
+ if (newValue !== oldValue) {
709
+ oldElement.value = newValue;
710
+ }
711
+ if (oldElement.firstChild && oldElement.firstChild.nodeValue !== newValue) {
712
+ oldElement.firstChild.nodeValue = newValue;
713
+ }
714
+ }
715
+ }
716
+
717
+ /**
718
+ * @param {Element} oldElement element to write the value to
719
+ * @param {Element} newElement element to read the value from
720
+ * @param {string} attributeName the attribute name
721
+ * @param {MorphContext} ctx the merge context
722
+ */
723
+ function syncBooleanAttribute(oldElement, newElement, attributeName, ctx) {
724
+ // @ts-ignore this function is only used on boolean attrs that are reflected as dom properties
725
+ const newLiveValue = newElement[attributeName],
726
+ // @ts-ignore ditto
727
+ oldLiveValue = oldElement[attributeName];
728
+ if (newLiveValue !== oldLiveValue) {
729
+ const ignoreUpdate = ignoreAttribute(attributeName, oldElement, 'update', ctx);
730
+ if (!ignoreUpdate) {
731
+ // update attribute's associated DOM property
732
+ // @ts-ignore this function is only used on boolean attrs that are reflected as dom properties
733
+ oldElement[attributeName] = newElement[attributeName];
734
+ }
735
+ if (newLiveValue) {
736
+ if (!ignoreUpdate) {
737
+ // https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML
738
+ // this is the correct way to set a boolean attribute to "true"
739
+ oldElement.setAttribute(attributeName, '');
740
+ }
741
+ } else {
742
+ if (!ignoreAttribute(attributeName, oldElement, 'remove', ctx)) {
743
+ oldElement.removeAttribute(attributeName);
744
+ }
745
+ }
746
+ }
747
+ }
748
+
749
+ /**
750
+ * @param {string} attr the attribute to be mutated
751
+ * @param {Element} element the element that is going to be updated
752
+ * @param {"update" | "remove"} updateType
753
+ * @param {MorphContext} ctx the merge context
754
+ * @returns {boolean} true if the attribute should be ignored, false otherwise
755
+ */
756
+ function ignoreAttribute(attr, element, updateType, ctx) {
757
+ if (attr === 'value' && ctx.ignoreActiveValue && element === document.activeElement) {
758
+ return true;
759
+ }
760
+ return ctx.callbacks.beforeAttributeUpdated(attr, element, updateType) === false;
761
+ }
762
+
763
+ /**
764
+ * @param {Node} possibleActiveElement
765
+ * @param {MorphContext} ctx
766
+ * @returns {boolean}
767
+ */
768
+ function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
769
+ return (
770
+ !!ctx.ignoreActiveValue &&
771
+ possibleActiveElement === document.activeElement &&
772
+ possibleActiveElement !== document.body
773
+ );
774
+ }
775
+
776
+ return morphNode;
777
+ })();
778
+
779
+ //=============================================================================
780
+ // Head Management Functions
781
+ //=============================================================================
782
+ /**
783
+ * @param {MorphContext} ctx
784
+ * @param {Element} oldNode
785
+ * @param {Element} newNode
786
+ * @param {function} callback
787
+ * @returns {Node[] | Promise<Node[]>}
788
+ */
789
+ function withHeadBlocking(ctx, oldNode, newNode, callback) {
790
+ if (ctx.head.block) {
791
+ const oldHead = oldNode.querySelector('head');
792
+ const newHead = newNode.querySelector('head');
793
+ if (oldHead && newHead) {
794
+ const promises = handleHeadElement(oldHead, newHead, ctx);
795
+ // when head promises resolve, proceed ignoring the head tag
796
+ return Promise.all(promises).then(() => {
797
+ const newCtx = Object.assign(ctx, {
798
+ head: {
799
+ block: false,
800
+ ignore: true,
801
+ },
802
+ });
803
+ return callback(newCtx);
804
+ });
805
+ }
806
+ }
807
+ // just proceed if we not head blocking
808
+ return callback(ctx);
809
+ }
810
+
811
+ /**
812
+ * The HEAD tag can be handled specially, either w/ a 'merge' or 'append' style
813
+ *
814
+ * @param {Element} oldHead
815
+ * @param {Element} newHead
816
+ * @param {MorphContext} ctx
817
+ * @returns {Promise<void>[]}
818
+ */
819
+ function handleHeadElement(oldHead, newHead, ctx) {
820
+ let added = [];
821
+ let removed = [];
822
+ let preserved = [];
823
+ let nodesToAppend = [];
824
+
825
+ // put all new head elements into a Map, by their outerHTML
826
+ let srcToNewHeadNodes = new Map();
827
+ for (const newHeadChild of newHead.children) {
828
+ srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
829
+ }
830
+
831
+ // for each elt in the current head
832
+ for (const currentHeadElt of oldHead.children) {
833
+ // If the current head element is in the map
834
+ let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
835
+ let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
836
+ let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
837
+ if (inNewContent || isPreserved) {
838
+ if (isReAppended) {
839
+ // remove the current version and let the new version replace it and re-execute
840
+ removed.push(currentHeadElt);
841
+ } else {
842
+ // this element already exists and should not be re-appended, so remove it from
843
+ // the new content map, preserving it in the DOM
844
+ srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
845
+ preserved.push(currentHeadElt);
846
+ }
847
+ } else {
848
+ if (ctx.head.style === 'append') {
849
+ // we are appending and this existing element is not new content
850
+ // so if and only if it is marked for re-append do we do anything
851
+ if (isReAppended) {
852
+ removed.push(currentHeadElt);
853
+ nodesToAppend.push(currentHeadElt);
854
+ }
855
+ } else {
856
+ // if this is a merge, we remove this content since it is not in the new head
857
+ if (ctx.head.shouldRemove(currentHeadElt) !== false) {
858
+ removed.push(currentHeadElt);
859
+ }
860
+ }
861
+ }
862
+ }
863
+
864
+ // Push the remaining new head elements in the Map into the
865
+ // nodes to append to the head tag
866
+ nodesToAppend.push(...srcToNewHeadNodes.values());
867
+
868
+ let promises = [];
869
+ for (const newNode of nodesToAppend) {
870
+ // TODO: This could theoretically be null, based on type
871
+ let newElt = /** @type {ChildNode} */ (
872
+ document.createRange().createContextualFragment(newNode.outerHTML).firstChild
873
+ );
874
+ if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
875
+ if (('href' in newElt && newElt.href) || ('src' in newElt && newElt.src)) {
876
+ /** @type {(result?: any) => void} */ let resolve;
877
+ let promise = new Promise(function (_resolve) {
878
+ resolve = _resolve;
879
+ });
880
+ newElt.addEventListener('load', function () {
881
+ resolve();
882
+ });
883
+ promises.push(promise);
884
+ }
885
+ oldHead.appendChild(newElt);
886
+ ctx.callbacks.afterNodeAdded(newElt);
887
+ added.push(newElt);
888
+ }
889
+ }
890
+
891
+ // remove all removed elements, after we have appended the new elements to avoid
892
+ // additional network requests for things like style sheets
893
+ for (const removedElement of removed) {
894
+ if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
895
+ oldHead.removeChild(removedElement);
896
+ ctx.callbacks.afterNodeRemoved(removedElement);
897
+ }
898
+ }
899
+
900
+ ctx.head.afterHeadMorphed(oldHead, {
901
+ added: added,
902
+ kept: preserved,
903
+ removed: removed,
904
+ });
905
+ return promises;
906
+ }
907
+
908
+ //=============================================================================
909
+ // Create Morph Context Functions
910
+ //=============================================================================
911
+ const createMorphContext = (function () {
912
+ /**
913
+ *
914
+ * @param {Element} oldNode
915
+ * @param {Element} newContent
916
+ * @param {Config} config
917
+ * @returns {MorphContext}
918
+ */
919
+ function createMorphContext(oldNode, newContent, config) {
920
+ const { persistentIds, idMap } = createIdMaps(oldNode, newContent);
921
+
922
+ const mergedConfig = mergeDefaults(config);
923
+ const morphStyle = mergedConfig.morphStyle || 'outerHTML';
924
+ if (!['innerHTML', 'outerHTML'].includes(morphStyle)) {
925
+ throw `Do not understand how to morph style ${morphStyle}`;
926
+ }
927
+
928
+ return {
929
+ target: oldNode,
930
+ newContent: newContent,
931
+ config: mergedConfig,
932
+ morphStyle: morphStyle,
933
+ ignoreActive: mergedConfig.ignoreActive,
934
+ ignoreActiveValue: mergedConfig.ignoreActiveValue,
935
+ restoreFocus: mergedConfig.restoreFocus,
936
+ idMap: idMap,
937
+ persistentIds: persistentIds,
938
+ pantry: createPantry(),
939
+ callbacks: mergedConfig.callbacks,
940
+ head: mergedConfig.head,
941
+ };
942
+ }
943
+
944
+ /**
945
+ * Deep merges the config object and the Idiomorph.defaults object to
946
+ * produce a final configuration object
947
+ * @param {Config} config
948
+ * @returns {ConfigInternal}
949
+ */
950
+ function mergeDefaults(config) {
951
+ let finalConfig = Object.assign({}, defaults);
952
+
953
+ // copy top level stuff into final config
954
+ Object.assign(finalConfig, config);
955
+
956
+ // copy callbacks into final config (do this to deep merge the callbacks)
957
+ finalConfig.callbacks = Object.assign({}, defaults.callbacks, config.callbacks);
958
+
959
+ // copy head config into final config (do this to deep merge the head)
960
+ finalConfig.head = Object.assign({}, defaults.head, config.head);
961
+
962
+ return finalConfig;
963
+ }
964
+
965
+ /**
966
+ * @returns {HTMLDivElement}
967
+ */
968
+ function createPantry() {
969
+ const pantry = document.createElement('div');
970
+ pantry.hidden = true;
971
+ document.body.insertAdjacentElement('afterend', pantry);
972
+ return pantry;
973
+ }
974
+
975
+ /**
976
+ * Returns all elements with an ID contained within the root element and its descendants
977
+ *
978
+ * @param {Element} root
979
+ * @returns {Element[]}
980
+ */
981
+ function findIdElements(root) {
982
+ let elements = Array.from(root.querySelectorAll('[id]'));
983
+ if (root.id) {
984
+ elements.push(root);
985
+ }
986
+ return elements;
987
+ }
988
+
989
+ /**
990
+ * A bottom-up algorithm that populates a map of Element -> IdSet.
991
+ * The idSet for a given element is the set of all IDs contained within its subtree.
992
+ * As an optimzation, we filter these IDs through the given list of persistent IDs,
993
+ * because we don't need to bother considering IDed elements that won't be in the new content.
994
+ *
995
+ * @param {Map<Node, Set<string>>} idMap
996
+ * @param {Set<string>} persistentIds
997
+ * @param {Element} root
998
+ * @param {Element[]} elements
999
+ */
1000
+ function populateIdMapWithTree(idMap, persistentIds, root, elements) {
1001
+ for (const elt of elements) {
1002
+ if (persistentIds.has(elt.id)) {
1003
+ /** @type {Element|null} */
1004
+ let current = elt;
1005
+ // walk up the parent hierarchy of that element, adding the id
1006
+ // of element to the parent's id set
1007
+ while (current) {
1008
+ let idSet = idMap.get(current);
1009
+ // if the id set doesn't exist, create it and insert it in the map
1010
+ if (idSet == null) {
1011
+ idSet = new Set();
1012
+ idMap.set(current, idSet);
1013
+ }
1014
+ idSet.add(elt.id);
1015
+
1016
+ if (current === root) break;
1017
+ current = current.parentElement;
1018
+ }
1019
+ }
1020
+ }
1021
+ }
1022
+
1023
+ /**
1024
+ * This function computes a map of nodes to all ids contained within that node (inclusive of the
1025
+ * node). This map can be used to ask if two nodes have intersecting sets of ids, which allows
1026
+ * for a looser definition of "matching" than tradition id matching, and allows child nodes
1027
+ * to contribute to a parent nodes matching.
1028
+ *
1029
+ * @param {Element} oldContent the old content that will be morphed
1030
+ * @param {Element} newContent the new content to morph to
1031
+ * @returns {IdSets}
1032
+ */
1033
+ function createIdMaps(oldContent, newContent) {
1034
+ const oldIdElements = findIdElements(oldContent);
1035
+ const newIdElements = findIdElements(newContent);
1036
+
1037
+ const persistentIds = createPersistentIds(oldIdElements, newIdElements);
1038
+
1039
+ /** @type {Map<Node, Set<string>>} */
1040
+ let idMap = new Map();
1041
+ populateIdMapWithTree(idMap, persistentIds, oldContent, oldIdElements);
1042
+
1043
+ /** @ts-ignore - if newContent is a duck-typed parent, pass its single child node as the root to halt upwards iteration */
1044
+ const newRoot = newContent.__idiomorphRoot || newContent;
1045
+ populateIdMapWithTree(idMap, persistentIds, newRoot, newIdElements);
1046
+
1047
+ return { persistentIds, idMap };
1048
+ }
1049
+
1050
+ /**
1051
+ * This function computes the set of ids that persist between the two contents excluding duplicates
1052
+ *
1053
+ * @param {Element[]} oldIdElements
1054
+ * @param {Element[]} newIdElements
1055
+ * @returns {Set<string>}
1056
+ */
1057
+ function createPersistentIds(oldIdElements, newIdElements) {
1058
+ let duplicateIds = new Set();
1059
+
1060
+ /** @type {Map<string, string>} */
1061
+ let oldIdTagNameMap = new Map();
1062
+ for (const { id, tagName } of oldIdElements) {
1063
+ if (oldIdTagNameMap.has(id)) {
1064
+ duplicateIds.add(id);
1065
+ } else {
1066
+ oldIdTagNameMap.set(id, tagName);
1067
+ }
1068
+ }
1069
+
1070
+ let persistentIds = new Set();
1071
+ for (const { id, tagName } of newIdElements) {
1072
+ if (persistentIds.has(id)) {
1073
+ duplicateIds.add(id);
1074
+ } else if (oldIdTagNameMap.get(id) === tagName) {
1075
+ persistentIds.add(id);
1076
+ }
1077
+ // skip if tag types mismatch because its not possible to morph one tag into another
1078
+ }
1079
+
1080
+ for (const id of duplicateIds) {
1081
+ persistentIds.delete(id);
1082
+ }
1083
+ return persistentIds;
1084
+ }
1085
+
1086
+ return createMorphContext;
1087
+ })();
1088
+
1089
+ //=============================================================================
1090
+ // HTML Normalization Functions
1091
+ //=============================================================================
1092
+ const { normalizeElement, normalizeParent } = (function () {
1093
+ /** @type {WeakSet<Node>} */
1094
+ const generatedByIdiomorph = new WeakSet();
1095
+
1096
+ /**
1097
+ *
1098
+ * @param {Element | Document} content
1099
+ * @returns {Element}
1100
+ */
1101
+ function normalizeElement(content) {
1102
+ if (content instanceof Document) {
1103
+ return content.documentElement;
1104
+ } else {
1105
+ return content;
1106
+ }
1107
+ }
1108
+
1109
+ /**
1110
+ *
1111
+ * @param {null | string | Node | HTMLCollection | Node[] | Document & {generatedByIdiomorph:boolean}} newContent
1112
+ * @returns {Element}
1113
+ */
1114
+ function normalizeParent(newContent) {
1115
+ if (newContent == null) {
1116
+ return document.createElement('div'); // dummy parent element
1117
+ } else if (typeof newContent === 'string') {
1118
+ return normalizeParent(parseContent(newContent));
1119
+ } else if (generatedByIdiomorph.has(/** @type {Element} */ (newContent))) {
1120
+ // the template tag created by idiomorph parsing can serve as a dummy parent
1121
+ return /** @type {Element} */ (newContent);
1122
+ } else if (newContent instanceof Node) {
1123
+ if (newContent.parentNode) {
1124
+ // we can't use the parent directly because newContent may have siblings
1125
+ // that we don't want in the morph, and reparenting might be expensive (TODO is it?),
1126
+ // so instead we create a fake parent node that only sees a slice of its children.
1127
+ /** @type {Element} */
1128
+ return /** @type {any} */ (new SlicedParentNode(newContent));
1129
+ } else {
1130
+ // a single node is added as a child to a dummy parent
1131
+ const dummyParent = document.createElement('div');
1132
+ dummyParent.append(newContent);
1133
+ return dummyParent;
1134
+ }
1135
+ } else {
1136
+ // all nodes in the array or HTMLElement collection are consolidated under
1137
+ // a single dummy parent element
1138
+ const dummyParent = document.createElement('div');
1139
+ for (const elt of [...newContent]) {
1140
+ dummyParent.append(elt);
1141
+ }
1142
+ return dummyParent;
1143
+ }
1144
+ }
1145
+
1146
+ /**
1147
+ * A fake duck-typed parent element to wrap a single node, without actually reparenting it.
1148
+ * This is useful because the node may have siblings that we don't want in the morph, and it may also be moved
1149
+ * or replaced with one or more elements during the morph. This class effectively allows us a window into
1150
+ * a slice of a node's children.
1151
+ * "If it walks like a duck, and quacks like a duck, then it must be a duck!" -- James Whitcomb Riley (1849–1916)
1152
+ */
1153
+ class SlicedParentNode {
1154
+ /** @param {Node} node */
1155
+ constructor(node) {
1156
+ this.originalNode = node;
1157
+ this.realParentNode = /** @type {Element} */ (node.parentNode);
1158
+ this.previousSibling = node.previousSibling;
1159
+ this.nextSibling = node.nextSibling;
1160
+ }
1161
+
1162
+ /** @returns {Node[]} */
1163
+ get childNodes() {
1164
+ // return slice of realParent's current childNodes, based on previousSibling and nextSibling
1165
+ const nodes = [];
1166
+ let cursor = this.previousSibling
1167
+ ? this.previousSibling.nextSibling
1168
+ : this.realParentNode.firstChild;
1169
+ while (cursor && cursor != this.nextSibling) {
1170
+ nodes.push(cursor);
1171
+ cursor = cursor.nextSibling;
1172
+ }
1173
+ return nodes;
1174
+ }
1175
+
1176
+ /**
1177
+ * @param {string} selector
1178
+ * @returns {Element[]}
1179
+ */
1180
+ querySelectorAll(selector) {
1181
+ return this.childNodes.reduce((results, node) => {
1182
+ if (node instanceof Element) {
1183
+ if (node.matches(selector)) results.push(node);
1184
+ const nodeList = node.querySelectorAll(selector);
1185
+ for (let i = 0; i < nodeList.length; i++) {
1186
+ results.push(nodeList[i]);
1187
+ }
1188
+ }
1189
+ return results;
1190
+ }, /** @type {Element[]} */ ([]));
1191
+ }
1192
+
1193
+ /**
1194
+ * @param {Node} node
1195
+ * @param {Node} referenceNode
1196
+ * @returns {Node}
1197
+ */
1198
+ insertBefore(node, referenceNode) {
1199
+ return this.realParentNode.insertBefore(node, referenceNode);
1200
+ }
1201
+
1202
+ /**
1203
+ * @param {Node} node
1204
+ * @param {Node} referenceNode
1205
+ * @returns {Node}
1206
+ */
1207
+ moveBefore(node, referenceNode) {
1208
+ // @ts-ignore - use new moveBefore feature
1209
+ return this.realParentNode.moveBefore(node, referenceNode);
1210
+ }
1211
+
1212
+ /**
1213
+ * for later use with populateIdMapWithTree to halt upwards iteration
1214
+ * @returns {Node}
1215
+ */
1216
+ get __idiomorphRoot() {
1217
+ return this.originalNode;
1218
+ }
1219
+ }
1220
+
1221
+ /**
1222
+ *
1223
+ * @param {string} newContent
1224
+ * @returns {Node | null | DocumentFragment}
1225
+ */
1226
+ function parseContent(newContent) {
1227
+ let parser = new DOMParser();
1228
+
1229
+ // remove svgs to avoid false-positive matches on head, etc.
1230
+ let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
1231
+
1232
+ // if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping
1233
+ if (
1234
+ contentWithSvgsRemoved.match(/<\/html>/) ||
1235
+ contentWithSvgsRemoved.match(/<\/head>/) ||
1236
+ contentWithSvgsRemoved.match(/<\/body>/)
1237
+ ) {
1238
+ let content = parser.parseFromString(newContent, 'text/html');
1239
+ // if it is a full HTML document, return the document itself as the parent container
1240
+ if (contentWithSvgsRemoved.match(/<\/html>/)) {
1241
+ generatedByIdiomorph.add(content);
1242
+ return content;
1243
+ } else {
1244
+ // otherwise return the html element as the parent container
1245
+ let htmlElement = content.firstChild;
1246
+ if (htmlElement) {
1247
+ generatedByIdiomorph.add(htmlElement);
1248
+ }
1249
+ return htmlElement;
1250
+ }
1251
+ } else {
1252
+ // if it is partial HTML, wrap it in a template tag to provide a parent element and also to help
1253
+ // deal with touchy tags like tr, tbody, etc.
1254
+ let responseDoc = parser.parseFromString(
1255
+ '<body><template>' + newContent + '</template></body>',
1256
+ 'text/html'
1257
+ );
1258
+ let content = /** @type {HTMLTemplateElement} */ (
1259
+ responseDoc.body.querySelector('template')
1260
+ ).content;
1261
+ generatedByIdiomorph.add(content);
1262
+ return content;
1263
+ }
1264
+ }
1265
+
1266
+ return { normalizeElement, normalizeParent };
1267
+ })();
1268
+
1269
+ //=============================================================================
1270
+ // This is what ends up becoming the Idiomorph global object
1271
+ //=============================================================================
1272
+ return {
1273
+ morph,
1274
+ defaults,
1275
+ };
1276
+ })();
1277
+
1278
+ export { Idiomorph };