jails-js 5.7.1 → 5.8.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,879 @@
1
+ /**
2
+ * BSD 2-Clause License
3
+ * https://github.com/bigskysoftware/idiomorph
4
+
5
+ Copyright (c) 2022, Big Sky Software
6
+ All rights reserved.
7
+
8
+ Redistribution and use in source and binary forms, with or without
9
+ modification, are permitted provided that the following conditions are met:
10
+
11
+ 1. Redistributions of source code must retain the above copyright notice, this
12
+ list of conditions and the following disclaimer.
13
+
14
+ 2. Redistributions in binary form must reproduce the above copyright notice,
15
+ this list of conditions and the following disclaimer in the documentation
16
+ and/or other materials provided with the distribution.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+ */
29
+
30
+ // base IIFE to define idiomorph
31
+
32
+
33
+ export const Idiomorph = (function () {
34
+ 'use strict';
35
+
36
+ //=============================================================================
37
+ // AND NOW IT BEGINS...
38
+ //=============================================================================
39
+ let EMPTY_SET = new Set();
40
+
41
+ // default configuration values, updatable by users now
42
+ let defaults = {
43
+ morphStyle: "outerHTML",
44
+ callbacks : {
45
+ beforeNodeAdded: noOp,
46
+ afterNodeAdded: noOp,
47
+ beforeNodeMorphed: noOp,
48
+ afterNodeMorphed: noOp,
49
+ beforeNodeRemoved: noOp,
50
+ afterNodeRemoved: noOp,
51
+ beforeAttributeUpdated: noOp,
52
+
53
+ },
54
+ head: {
55
+ style: 'merge',
56
+ shouldPreserve: function (elt) {
57
+ return elt.getAttribute("im-preserve") === "true";
58
+ },
59
+ shouldReAppend: function (elt) {
60
+ return elt.getAttribute("im-re-append") === "true";
61
+ },
62
+ shouldRemove: noOp,
63
+ afterHeadMorphed: noOp,
64
+ }
65
+ };
66
+
67
+ //=============================================================================
68
+ // Core Morphing Algorithm - morph, morphNormalizedContent, morphOldNodeTo, morphChildren
69
+ //=============================================================================
70
+ function morph(oldNode, newContent, config = {}) {
71
+
72
+ if (oldNode instanceof Document) {
73
+ oldNode = oldNode.documentElement;
74
+ }
75
+
76
+ if (typeof newContent === 'string') {
77
+ newContent = parseContent(newContent);
78
+ }
79
+
80
+ let normalizedContent = normalizeContent(newContent);
81
+
82
+ let ctx = createMorphContext(oldNode, normalizedContent, config);
83
+
84
+ return morphNormalizedContent(oldNode, normalizedContent, ctx);
85
+ }
86
+
87
+ function morphNormalizedContent(oldNode, normalizedNewContent, ctx) {
88
+ if (ctx.head.block) {
89
+ let oldHead = oldNode.querySelector('head');
90
+ let newHead = normalizedNewContent.querySelector('head');
91
+ if (oldHead && newHead) {
92
+ let promises = handleHeadElement(newHead, oldHead, ctx);
93
+ // when head promises resolve, call morph again, ignoring the head tag
94
+ Promise.all(promises).then(function () {
95
+ morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, {
96
+ head: {
97
+ block: false,
98
+ ignore: true
99
+ }
100
+ }));
101
+ });
102
+ return;
103
+ }
104
+ }
105
+
106
+ if (ctx.morphStyle === "innerHTML") {
107
+
108
+ // innerHTML, so we are only updating the children
109
+ morphChildren(normalizedNewContent, oldNode, ctx);
110
+ return oldNode.children;
111
+
112
+ } else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) {
113
+ // otherwise find the best element match in the new content, morph that, and merge its siblings
114
+ // into either side of the best match
115
+ let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
116
+
117
+ // stash the siblings that will need to be inserted on either side of the best match
118
+ let previousSibling = bestMatch?.previousSibling;
119
+ let nextSibling = bestMatch?.nextSibling;
120
+
121
+ // morph it
122
+ let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
123
+
124
+ if (bestMatch) {
125
+ // if there was a best match, merge the siblings in too and return the
126
+ // whole bunch
127
+ return insertSiblings(previousSibling, morphedNode, nextSibling);
128
+ } else {
129
+ // otherwise nothing was added to the DOM
130
+ return []
131
+ }
132
+ } else {
133
+ throw "Do not understand how to morph style " + ctx.morphStyle;
134
+ }
135
+ }
136
+
137
+
138
+ /**
139
+ * @param possibleActiveElement
140
+ * @param ctx
141
+ * @returns {boolean}
142
+ */
143
+ function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
144
+ return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement;
145
+ }
146
+
147
+ /**
148
+ * @param oldNode root node to merge content into
149
+ * @param newContent new content to merge
150
+ * @param ctx the merge context
151
+ * @returns {Element} the element that ended up in the DOM
152
+ */
153
+ function morphOldNodeTo(oldNode, newContent, ctx) {
154
+ if (ctx.ignoreActive && oldNode === document.activeElement) {
155
+ // don't morph focused element
156
+ } else if (newContent == null) {
157
+ if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
158
+
159
+ oldNode.remove();
160
+ ctx.callbacks.afterNodeRemoved(oldNode);
161
+ return null;
162
+ } else if (!isSoftMatch(oldNode, newContent)) {
163
+ if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
164
+ if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode;
165
+
166
+ oldNode.parentElement.replaceChild(newContent, oldNode);
167
+ ctx.callbacks.afterNodeAdded(newContent);
168
+ ctx.callbacks.afterNodeRemoved(oldNode);
169
+ return newContent;
170
+ } else {
171
+ if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode;
172
+
173
+ if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) {
174
+ // ignore the head element
175
+ } else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
176
+ handleHeadElement(newContent, oldNode, ctx);
177
+ } else {
178
+ syncNodeFrom(newContent, oldNode, ctx);
179
+ if (!ignoreValueOfActiveElement(oldNode, ctx)) {
180
+ morphChildren(newContent, oldNode, ctx);
181
+ }
182
+ }
183
+ ctx.callbacks.afterNodeMorphed(oldNode, newContent);
184
+ return oldNode;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * This is the core algorithm for matching up children. The idea is to use id sets to try to match up
190
+ * nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but
191
+ * by using id sets, we are able to better match up with content deeper in the DOM.
192
+ *
193
+ * Basic algorithm is, for each node in the new content:
194
+ *
195
+ * - if we have reached the end of the old parent, append the new content
196
+ * - if the new content has an id set match with the current insertion point, morph
197
+ * - search for an id set match
198
+ * - if id set match found, morph
199
+ * - otherwise search for a "soft" match
200
+ * - if a soft match is found, morph
201
+ * - otherwise, prepend the new node before the current insertion point
202
+ *
203
+ * The two search algorithms terminate if competing node matches appear to outweigh what can be achieved
204
+ * with the current node. See findIdSetMatch() and findSoftMatch() for details.
205
+ *
206
+ * @param {Element} newParent the parent element of the new content
207
+ * @param {Element } oldParent the old content that we are merging the new content into
208
+ * @param ctx the merge context
209
+ */
210
+ function morphChildren(newParent, oldParent, ctx) {
211
+
212
+ let nextNewChild = newParent.firstChild;
213
+ let insertionPoint = oldParent.firstChild;
214
+ let newChild;
215
+
216
+ // run through all the new content
217
+ while (nextNewChild) {
218
+
219
+ newChild = nextNewChild;
220
+ nextNewChild = newChild.nextSibling;
221
+
222
+ // if we are at the end of the exiting parent's children, just append
223
+ if (insertionPoint == null) {
224
+ if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
225
+
226
+ oldParent.appendChild(newChild);
227
+ ctx.callbacks.afterNodeAdded(newChild);
228
+ removeIdsFromConsideration(ctx, newChild);
229
+ continue;
230
+ }
231
+
232
+ // if the current node has an id set match then morph
233
+ if (isIdSetMatch(newChild, insertionPoint, ctx)) {
234
+ morphOldNodeTo(insertionPoint, newChild, ctx);
235
+ insertionPoint = insertionPoint.nextSibling;
236
+ removeIdsFromConsideration(ctx, newChild);
237
+ continue;
238
+ }
239
+
240
+ // otherwise search forward in the existing old children for an id set match
241
+ let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx);
242
+
243
+ // if we found a potential match, remove the nodes until that point and morph
244
+ if (idSetMatch) {
245
+ insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx);
246
+ morphOldNodeTo(idSetMatch, newChild, ctx);
247
+ removeIdsFromConsideration(ctx, newChild);
248
+ continue;
249
+ }
250
+
251
+ // no id set match found, so scan forward for a soft match for the current node
252
+ let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx);
253
+
254
+ // if we found a soft match for the current node, morph
255
+ if (softMatch) {
256
+ insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx);
257
+ morphOldNodeTo(softMatch, newChild, ctx);
258
+ removeIdsFromConsideration(ctx, newChild);
259
+ continue;
260
+ }
261
+
262
+ // abandon all hope of morphing, just insert the new child before the insertion point
263
+ // and move on
264
+ if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
265
+
266
+ oldParent.insertBefore(newChild, insertionPoint);
267
+ ctx.callbacks.afterNodeAdded(newChild);
268
+ removeIdsFromConsideration(ctx, newChild);
269
+ }
270
+
271
+ // remove any remaining old nodes that didn't match up with new content
272
+ while (insertionPoint !== null) {
273
+
274
+ let tempNode = insertionPoint;
275
+ insertionPoint = insertionPoint.nextSibling;
276
+ removeNode(tempNode, ctx);
277
+ }
278
+ }
279
+
280
+ //=============================================================================
281
+ // Attribute Syncing Code
282
+ //=============================================================================
283
+
284
+ /**
285
+ * @param attr {String} the attribute to be mutated
286
+ * @param to {Element} the element that is going to be updated
287
+ * @param updateType {("update"|"remove")}
288
+ * @param ctx the merge context
289
+ * @returns {boolean} true if the attribute should be ignored, false otherwise
290
+ */
291
+ function ignoreAttribute(attr, to, updateType, ctx) {
292
+ if(attr === 'value' && ctx.ignoreActiveValue && to === document.activeElement){
293
+ return true;
294
+ }
295
+ return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false;
296
+ }
297
+
298
+ /**
299
+ * syncs a given node with another node, copying over all attributes and
300
+ * inner element state from the 'from' node to the 'to' node
301
+ *
302
+ * @param {Element} from the element to copy attributes & state from
303
+ * @param {Element} to the element to copy attributes & state to
304
+ * @param ctx the merge context
305
+ */
306
+ function syncNodeFrom(from, to, ctx) {
307
+ let type = from.nodeType
308
+
309
+ // if is an element type, sync the attributes from the
310
+ // new node into the new node
311
+ if (type === 1 /* element type */) {
312
+ const fromAttributes = from.attributes;
313
+ const toAttributes = to.attributes;
314
+ for (const fromAttribute of fromAttributes) {
315
+ if (ignoreAttribute(fromAttribute.name, to, 'update', ctx)) {
316
+ continue;
317
+ }
318
+ if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
319
+ to.setAttribute(fromAttribute.name, fromAttribute.value);
320
+ }
321
+ }
322
+ // iterate backwards to avoid skipping over items when a delete occurs
323
+ for (let i = toAttributes.length - 1; 0 <= i; i--) {
324
+ const toAttribute = toAttributes[i];
325
+ if (ignoreAttribute(toAttribute.name, to, 'remove', ctx)) {
326
+ continue;
327
+ }
328
+ if (!from.hasAttribute(toAttribute.name)) {
329
+ to.removeAttribute(toAttribute.name);
330
+ }
331
+ }
332
+ }
333
+
334
+ // sync text nodes
335
+ if (type === 8 /* comment */ || type === 3 /* text */) {
336
+ if (to.nodeValue !== from.nodeValue) {
337
+ to.nodeValue = from.nodeValue;
338
+ }
339
+ }
340
+
341
+ if (!ignoreValueOfActiveElement(to, ctx)) {
342
+ // sync input values
343
+ syncInputValue(from, to, ctx);
344
+ }
345
+ }
346
+
347
+ /**
348
+ * @param from {Element} element to sync the value from
349
+ * @param to {Element} element to sync the value to
350
+ * @param attributeName {String} the attribute name
351
+ * @param ctx the merge context
352
+ */
353
+ function syncBooleanAttribute(from, to, attributeName, ctx) {
354
+ if (from[attributeName] !== to[attributeName]) {
355
+ let ignoreUpdate = ignoreAttribute(attributeName, to, 'update', ctx);
356
+ if (!ignoreUpdate) {
357
+ to[attributeName] = from[attributeName];
358
+ }
359
+ if (from[attributeName]) {
360
+ if (!ignoreUpdate) {
361
+ to.setAttribute(attributeName, from[attributeName]);
362
+ }
363
+ } else {
364
+ if (!ignoreAttribute(attributeName, to, 'remove', ctx)) {
365
+ to.removeAttribute(attributeName);
366
+ }
367
+ }
368
+ }
369
+ }
370
+
371
+ /**
372
+ * NB: many bothans died to bring us information:
373
+ *
374
+ * https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js
375
+ * https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113
376
+ *
377
+ * @param from {Element} the element to sync the input value from
378
+ * @param to {Element} the element to sync the input value to
379
+ * @param ctx the merge context
380
+ */
381
+ function syncInputValue(from, to, ctx) {
382
+ if (from instanceof HTMLInputElement &&
383
+ to instanceof HTMLInputElement &&
384
+ from.type !== 'file') {
385
+
386
+ let fromValue = from.value;
387
+ let toValue = to.value;
388
+
389
+ // sync boolean attributes
390
+ syncBooleanAttribute(from, to, 'checked', ctx);
391
+ syncBooleanAttribute(from, to, 'disabled', ctx);
392
+
393
+ if (!from.hasAttribute('value')) {
394
+ if (!ignoreAttribute('value', to, 'remove', ctx)) {
395
+ to.value = '';
396
+ to.removeAttribute('value');
397
+ }
398
+ } else if (fromValue !== toValue) {
399
+ if (!ignoreAttribute('value', to, 'update', ctx)) {
400
+ to.setAttribute('value', fromValue);
401
+ to.value = fromValue;
402
+ }
403
+ }
404
+ } else if (from instanceof HTMLOptionElement) {
405
+ syncBooleanAttribute(from, to, 'selected', ctx)
406
+ } else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
407
+ let fromValue = from.value;
408
+ let toValue = to.value;
409
+ if (ignoreAttribute('value', to, 'update', ctx)) {
410
+ return;
411
+ }
412
+ if (fromValue !== toValue) {
413
+ to.value = fromValue;
414
+ }
415
+ if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
416
+ to.firstChild.nodeValue = fromValue
417
+ }
418
+ }
419
+ }
420
+
421
+ //=============================================================================
422
+ // the HEAD tag can be handled specially, either w/ a 'merge' or 'append' style
423
+ //=============================================================================
424
+ function handleHeadElement(newHeadTag, currentHead, ctx) {
425
+
426
+ let added = []
427
+ let removed = []
428
+ let preserved = []
429
+ let nodesToAppend = []
430
+
431
+ let headMergeStyle = ctx.head.style;
432
+
433
+ // put all new head elements into a Map, by their outerHTML
434
+ let srcToNewHeadNodes = new Map();
435
+ for (const newHeadChild of newHeadTag.children) {
436
+ srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
437
+ }
438
+
439
+ // for each elt in the current head
440
+ for (const currentHeadElt of currentHead.children) {
441
+
442
+ // If the current head element is in the map
443
+ let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
444
+ let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
445
+ let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
446
+ if (inNewContent || isPreserved) {
447
+ if (isReAppended) {
448
+ // remove the current version and let the new version replace it and re-execute
449
+ removed.push(currentHeadElt);
450
+ } else {
451
+ // this element already exists and should not be re-appended, so remove it from
452
+ // the new content map, preserving it in the DOM
453
+ srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
454
+ preserved.push(currentHeadElt);
455
+ }
456
+ } else {
457
+ if (headMergeStyle === "append") {
458
+ // we are appending and this existing element is not new content
459
+ // so if and only if it is marked for re-append do we do anything
460
+ if (isReAppended) {
461
+ removed.push(currentHeadElt);
462
+ nodesToAppend.push(currentHeadElt);
463
+ }
464
+ } else {
465
+ // if this is a merge, we remove this content since it is not in the new head
466
+ if (ctx.head.shouldRemove(currentHeadElt) !== false) {
467
+ removed.push(currentHeadElt);
468
+ }
469
+ }
470
+ }
471
+ }
472
+
473
+ // Push the remaining new head elements in the Map into the
474
+ // nodes to append to the head tag
475
+ nodesToAppend.push(...srcToNewHeadNodes.values());
476
+ log("to append: ", nodesToAppend);
477
+
478
+ let promises = [];
479
+ for (const newNode of nodesToAppend) {
480
+ log("adding: ", newNode);
481
+ let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
482
+ log(newElt);
483
+ if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
484
+ if (newElt.href || newElt.src) {
485
+ let resolve = null;
486
+ let promise = new Promise(function (_resolve) {
487
+ resolve = _resolve;
488
+ });
489
+ newElt.addEventListener('load', function () {
490
+ resolve();
491
+ });
492
+ promises.push(promise);
493
+ }
494
+ currentHead.appendChild(newElt);
495
+ ctx.callbacks.afterNodeAdded(newElt);
496
+ added.push(newElt);
497
+ }
498
+ }
499
+
500
+ // remove all removed elements, after we have appended the new elements to avoid
501
+ // additional network requests for things like style sheets
502
+ for (const removedElement of removed) {
503
+ if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
504
+ currentHead.removeChild(removedElement);
505
+ ctx.callbacks.afterNodeRemoved(removedElement);
506
+ }
507
+ }
508
+
509
+ ctx.head.afterHeadMorphed(currentHead, {added: added, kept: preserved, removed: removed});
510
+ return promises;
511
+ }
512
+
513
+ //=============================================================================
514
+ // Misc
515
+ //=============================================================================
516
+
517
+ function log() {
518
+ //console.log(arguments);
519
+ }
520
+
521
+ function noOp() {
522
+ }
523
+
524
+ /*
525
+ Deep merges the config object and the Idiomoroph.defaults object to
526
+ produce a final configuration object
527
+ */
528
+ function mergeDefaults(config) {
529
+ let finalConfig = {};
530
+ // copy top level stuff into final config
531
+ Object.assign(finalConfig, defaults);
532
+ Object.assign(finalConfig, config);
533
+
534
+ // copy callbacks into final config (do this to deep merge the callbacks)
535
+ finalConfig.callbacks = {};
536
+ Object.assign(finalConfig.callbacks, defaults.callbacks);
537
+ Object.assign(finalConfig.callbacks, config.callbacks);
538
+
539
+ // copy head config into final config (do this to deep merge the head)
540
+ finalConfig.head = {};
541
+ Object.assign(finalConfig.head, defaults.head);
542
+ Object.assign(finalConfig.head, config.head);
543
+ return finalConfig;
544
+ }
545
+
546
+ function createMorphContext(oldNode, newContent, config) {
547
+ config = mergeDefaults(config);
548
+ return {
549
+ target: oldNode,
550
+ newContent: newContent,
551
+ config: config,
552
+ morphStyle: config.morphStyle,
553
+ ignoreActive: config.ignoreActive,
554
+ ignoreActiveValue: config.ignoreActiveValue,
555
+ idMap: createIdMap(oldNode, newContent),
556
+ deadIds: new Set(),
557
+ callbacks: config.callbacks,
558
+ head: config.head
559
+ }
560
+ }
561
+
562
+ function isIdSetMatch(node1, node2, ctx) {
563
+ if (node1 == null || node2 == null) {
564
+ return false;
565
+ }
566
+ if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) {
567
+ if (node1.id !== "" && node1.id === node2.id) {
568
+ return true;
569
+ } else {
570
+ return getIdIntersectionCount(ctx, node1, node2) > 0;
571
+ }
572
+ }
573
+ return false;
574
+ }
575
+
576
+ function isSoftMatch(node1, node2) {
577
+ if (node1 == null || node2 == null) {
578
+ return false;
579
+ }
580
+ return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName
581
+ }
582
+
583
+ function removeNodesBetween(startInclusive, endExclusive, ctx) {
584
+ while (startInclusive !== endExclusive) {
585
+ let tempNode = startInclusive;
586
+ startInclusive = startInclusive.nextSibling;
587
+ removeNode(tempNode, ctx);
588
+ }
589
+ removeIdsFromConsideration(ctx, endExclusive);
590
+ return endExclusive.nextSibling;
591
+ }
592
+
593
+ //=============================================================================
594
+ // Scans forward from the insertionPoint in the old parent looking for a potential id match
595
+ // for the newChild. We stop if we find a potential id match for the new child OR
596
+ // if the number of potential id matches we are discarding is greater than the
597
+ // potential id matches for the new child
598
+ //=============================================================================
599
+ function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
600
+
601
+ // max id matches we are willing to discard in our search
602
+ let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent);
603
+
604
+ let potentialMatch = null;
605
+
606
+ // only search forward if there is a possibility of an id match
607
+ if (newChildPotentialIdCount > 0) {
608
+ let potentialMatch = insertionPoint;
609
+ // if there is a possibility of an id match, scan forward
610
+ // keep track of the potential id match count we are discarding (the
611
+ // newChildPotentialIdCount must be greater than this to make it likely
612
+ // worth it)
613
+ let otherMatchCount = 0;
614
+ while (potentialMatch != null) {
615
+
616
+ // If we have an id match, return the current potential match
617
+ if (isIdSetMatch(newChild, potentialMatch, ctx)) {
618
+ return potentialMatch;
619
+ }
620
+
621
+ // computer the other potential matches of this new content
622
+ otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
623
+ if (otherMatchCount > newChildPotentialIdCount) {
624
+ // if we have more potential id matches in _other_ content, we
625
+ // do not have a good candidate for an id match, so return null
626
+ return null;
627
+ }
628
+
629
+ // advanced to the next old content child
630
+ potentialMatch = potentialMatch.nextSibling;
631
+ }
632
+ }
633
+ return potentialMatch;
634
+ }
635
+
636
+ //=============================================================================
637
+ // Scans forward from the insertionPoint in the old parent looking for a potential soft match
638
+ // for the newChild. We stop if we find a potential soft match for the new child OR
639
+ // if we find a potential id match in the old parents children OR if we find two
640
+ // potential soft matches for the next two pieces of new content
641
+ //=============================================================================
642
+ function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
643
+
644
+ let potentialSoftMatch = insertionPoint;
645
+ let nextSibling = newChild.nextSibling;
646
+ let siblingSoftMatchCount = 0;
647
+
648
+ while (potentialSoftMatch != null) {
649
+
650
+ if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) {
651
+ // the current potential soft match has a potential id set match with the remaining new
652
+ // content so bail out of looking
653
+ return null;
654
+ }
655
+
656
+ // if we have a soft match with the current node, return it
657
+ if (isSoftMatch(newChild, potentialSoftMatch)) {
658
+ return potentialSoftMatch;
659
+ }
660
+
661
+ if (isSoftMatch(nextSibling, potentialSoftMatch)) {
662
+ // the next new node has a soft match with this node, so
663
+ // increment the count of future soft matches
664
+ siblingSoftMatchCount++;
665
+ nextSibling = nextSibling.nextSibling;
666
+
667
+ // If there are two future soft matches, bail to allow the siblings to soft match
668
+ // so that we don't consume future soft matches for the sake of the current node
669
+ if (siblingSoftMatchCount >= 2) {
670
+ return null;
671
+ }
672
+ }
673
+
674
+ // advanced to the next old content child
675
+ potentialSoftMatch = potentialSoftMatch.nextSibling;
676
+ }
677
+
678
+ return potentialSoftMatch;
679
+ }
680
+
681
+ function parseContent(newContent) {
682
+ let parser = new DOMParser();
683
+
684
+ // remove svgs to avoid false-positive matches on head, etc.
685
+ let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
686
+
687
+ // if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping
688
+ if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
689
+ let content = parser.parseFromString(newContent, "text/html");
690
+ // if it is a full HTML document, return the document itself as the parent container
691
+ if (contentWithSvgsRemoved.match(/<\/html>/)) {
692
+ content.generatedByIdiomorph = true;
693
+ return content;
694
+ } else {
695
+ // otherwise return the html element as the parent container
696
+ let htmlElement = content.firstChild;
697
+ if (htmlElement) {
698
+ htmlElement.generatedByIdiomorph = true;
699
+ return htmlElement;
700
+ } else {
701
+ return null;
702
+ }
703
+ }
704
+ } else {
705
+ // if it is partial HTML, wrap it in a template tag to provide a parent element and also to help
706
+ // deal with touchy tags like tr, tbody, etc.
707
+ let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
708
+ let content = responseDoc.body.querySelector('template').content;
709
+ content.generatedByIdiomorph = true;
710
+ return content
711
+ }
712
+ }
713
+
714
+ function normalizeContent(newContent) {
715
+ if (newContent == null) {
716
+ // noinspection UnnecessaryLocalVariableJS
717
+ const dummyParent = document.createElement('div');
718
+ return dummyParent;
719
+ } else if (newContent.generatedByIdiomorph) {
720
+ // the template tag created by idiomorph parsing can serve as a dummy parent
721
+ return newContent;
722
+ } else if (newContent instanceof Node) {
723
+ // a single node is added as a child to a dummy parent
724
+ const dummyParent = document.createElement('div');
725
+ dummyParent.append(newContent);
726
+ return dummyParent;
727
+ } else {
728
+ // all nodes in the array or HTMLElement collection are consolidated under
729
+ // a single dummy parent element
730
+ const dummyParent = document.createElement('div');
731
+ for (const elt of [...newContent]) {
732
+ dummyParent.append(elt);
733
+ }
734
+ return dummyParent;
735
+ }
736
+ }
737
+
738
+ function insertSiblings(previousSibling, morphedNode, nextSibling) {
739
+ let stack = []
740
+ let added = []
741
+ while (previousSibling != null) {
742
+ stack.push(previousSibling);
743
+ previousSibling = previousSibling.previousSibling;
744
+ }
745
+ while (stack.length > 0) {
746
+ let node = stack.pop();
747
+ added.push(node); // push added preceding siblings on in order and insert
748
+ morphedNode.parentElement.insertBefore(node, morphedNode);
749
+ }
750
+ added.push(morphedNode);
751
+ while (nextSibling != null) {
752
+ stack.push(nextSibling);
753
+ added.push(nextSibling); // here we are going in order, so push on as we scan, rather than add
754
+ nextSibling = nextSibling.nextSibling;
755
+ }
756
+ while (stack.length > 0) {
757
+ morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
758
+ }
759
+ return added;
760
+ }
761
+
762
+ function findBestNodeMatch(newContent, oldNode, ctx) {
763
+ let currentElement;
764
+ currentElement = newContent.firstChild;
765
+ let bestElement = currentElement;
766
+ let score = 0;
767
+ while (currentElement) {
768
+ let newScore = scoreElement(currentElement, oldNode, ctx);
769
+ if (newScore > score) {
770
+ bestElement = currentElement;
771
+ score = newScore;
772
+ }
773
+ currentElement = currentElement.nextSibling;
774
+ }
775
+ return bestElement;
776
+ }
777
+
778
+ function scoreElement(node1, node2, ctx) {
779
+ if (isSoftMatch(node1, node2)) {
780
+ return .5 + getIdIntersectionCount(ctx, node1, node2);
781
+ }
782
+ return 0;
783
+ }
784
+
785
+ function removeNode(tempNode, ctx) {
786
+ removeIdsFromConsideration(ctx, tempNode)
787
+ if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return;
788
+
789
+ tempNode.remove();
790
+ ctx.callbacks.afterNodeRemoved(tempNode);
791
+ }
792
+
793
+ //=============================================================================
794
+ // ID Set Functions
795
+ //=============================================================================
796
+
797
+ function isIdInConsideration(ctx, id) {
798
+ return !ctx.deadIds.has(id);
799
+ }
800
+
801
+ function idIsWithinNode(ctx, id, targetNode) {
802
+ let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
803
+ return idSet.has(id);
804
+ }
805
+
806
+ function removeIdsFromConsideration(ctx, node) {
807
+ let idSet = ctx.idMap.get(node) || EMPTY_SET;
808
+ for (const id of idSet) {
809
+ ctx.deadIds.add(id);
810
+ }
811
+ }
812
+
813
+ function getIdIntersectionCount(ctx, node1, node2) {
814
+ let sourceSet = ctx.idMap.get(node1) || EMPTY_SET;
815
+ let matchCount = 0;
816
+ for (const id of sourceSet) {
817
+ // a potential match is an id in the source and potentialIdsSet, but
818
+ // that has not already been merged into the DOM
819
+ if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) {
820
+ ++matchCount;
821
+ }
822
+ }
823
+ return matchCount;
824
+ }
825
+
826
+ /**
827
+ * A bottom up algorithm that finds all elements with ids inside of the node
828
+ * argument and populates id sets for those nodes and all their parents, generating
829
+ * a set of ids contained within all nodes for the entire hierarchy in the DOM
830
+ *
831
+ * @param node {Element}
832
+ * @param {Map<Node, Set<String>>} idMap
833
+ */
834
+ function populateIdMapForNode(node, idMap) {
835
+ let nodeParent = node.parentElement;
836
+ // find all elements with an id property
837
+ let idElements = node.querySelectorAll('[id]');
838
+ for (const elt of idElements) {
839
+ let current = elt;
840
+ // walk up the parent hierarchy of that element, adding the id
841
+ // of element to the parent's id set
842
+ while (current !== nodeParent && current != null) {
843
+ let idSet = idMap.get(current);
844
+ // if the id set doesn't exist, create it and insert it in the map
845
+ if (idSet == null) {
846
+ idSet = new Set();
847
+ idMap.set(current, idSet);
848
+ }
849
+ idSet.add(elt.id);
850
+ current = current.parentElement;
851
+ }
852
+ }
853
+ }
854
+
855
+ /**
856
+ * This function computes a map of nodes to all ids contained within that node (inclusive of the
857
+ * node). This map can be used to ask if two nodes have intersecting sets of ids, which allows
858
+ * for a looser definition of "matching" than tradition id matching, and allows child nodes
859
+ * to contribute to a parent nodes matching.
860
+ *
861
+ * @param {Element} oldContent the old content that will be morphed
862
+ * @param {Element} newContent the new content to morph to
863
+ * @returns {Map<Node, Set<String>>} a map of nodes to id sets for the
864
+ */
865
+ function createIdMap(oldContent, newContent) {
866
+ let idMap = new Map();
867
+ populateIdMapForNode(oldContent, idMap);
868
+ populateIdMapForNode(newContent, idMap);
869
+ return idMap;
870
+ }
871
+
872
+ //=============================================================================
873
+ // This is what ends up becoming the Idiomorph global object
874
+ //=============================================================================
875
+ return {
876
+ morph,
877
+ defaults
878
+ }
879
+ })();