-
Notifications
You must be signed in to change notification settings - Fork 47
/
head-GENERATED.js
4838 lines (4148 loc) · 185 KB
/
head-GENERATED.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* Miscellaneous utility functions. */
/* author: Said Achmiz */
/* license: MIT */
/****************************************************************/
/* Generates integer from a uniform distribution over [1, size].
*/
function rollDie(size) {
return Math.floor(Math.random() * size + 1);
}
/**************************************************************************/
/* Returns array, of given size, of consecutive integers, with given start
value.
*/
function range(start, size) {
return [...Array(size).keys()].map(i => i + start);
}
/*********************************************************/
/* Returns val, or min if val < min, or max if val > max.
(In other words, clamps val to [min,max].)
*/
function valMinMax(val, min, max) {
return Math.max(Math.min(val, max), min);
}
/***********************************************************/
/* The first item of the array (or null if array is empty).
*/
Object.defineProperty(Array.prototype, "first", {
get() {
if (this.length == 0)
return null;
return this[0];
}
});
/**********************************************************/
/* The last item of the array (or null if array is empty).
*/
Object.defineProperty(Array.prototype, "last", {
get() {
if (this.length == 0)
return null;
return this[(this.length - 1)];
}
});
/********************************/
/* Remove given item from array.
*/
Array.prototype.remove = function (item) {
let index = this.indexOf(item);
if (index !== -1)
this.splice(index, 1);
};
/***************************************************************************/
/* Remove from array the first item that passes the provided test function.
The test function should take an array item and return true/false.
*/
Array.prototype.removeIf = function (test) {
let index = this.findIndex(test);
if (index !== -1)
this.splice(index, 1);
};
/******************************************************************************/
/* Insert the given item into the array just before the first item that passes
the provided test function. If no item passes the test function, append the
item to the end of the array.
*/
Array.prototype.insertBefore = function (item, test) {
let index = this.findIndex(test);
if (index === -1) {
this.push(item);
} else {
this.splice(index, 0, item);
}
};
/*********************************************************/
/* Polyfill for findLastIndex, for older browser versions
(Firefox 103 and lower, Chrome 96 and lower).
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findLastIndex
NOTE: Does not support the `thisArg` parameter.
*/
if (Array.prototype.findLastIndex === undefined) {
Array.prototype.findLastIndex = function (test) {
for (let i = this.length - 1; i >= 0; i--) {
if (test(this[i], i, this))
return i;
}
return -1;
}
}
/************************************************************/
/* Returns copy of the array, with duplicate values removed.
*/
Array.prototype.unique = function () {
return this.filter((value, index, array) => array.indexOf(value) == index);
}
/*********************************************/
/* Returns the string with words capitalized.
*/
String.prototype.capitalizeWords = function () {
return this.replace(/\b\w/g, l => l.toUpperCase());
};
/********************************************************/
/* Returns the string trimmed of opening/closing quotes.
*/
String.prototype.trimQuotes = function () {
return this.replace(/^["'“‘]?(.+?)["'”’]?$/, '$1');
};
/********************************************************************/
/* Returns true if the string begins with any of the given prefixes.
*/
String.prototype.startsWithAnyOf = function (prefixes) {
for (prefix of prefixes)
if (this.startsWith(prefix))
return true;
return false;
}
/******************************************************************/
/* Returns true if the string ends with any of the given suffixes.
*/
String.prototype.endsWithAnyOf = function (suffixes) {
for (suffix of suffixes)
if (this.endsWith(suffix))
return true;
return false;
}
/*******************************************************************/
/* Returns true if the string includes any of the given substrings.
*/
String.prototype.includesAnyOf = function (substrings) {
for (substring of substrings)
if (this.includes(substring))
return true
return false;
}
/***************************************************************************/
/* Returns the value of the search param with the given key for a the given
URL object.
*/
URL.prototype.getQueryVariable = function (key) {
return this.searchParams.get(key);
}
/**************************************************************************/
/* Set a URL search parameter with the given key to the given value on the
given URL object.
*/
URL.prototype.setQueryVariable = function (key, value) {
let query = new URLSearchParams(this.search);
query.set(key, value);
this.search = query.toString();
}
/******************************************************************************/
/* Delete a URL search parameter with the given key from the given URL object.
*/
URL.prototype.deleteQueryVariable = function (key) {
let query = new URLSearchParams(this.search);
query.delete(key);
this.search = query.toString();
}
/*****************************************************************************/
/* Returns a URL constructed from either a fully qualified URL string,
or an absolute local URL (pathname starting at root), or a relative URL
(pathname component replacing part of current URL after last slash), or
a hash (URL fragment) only.
(The existing URL() constructor only handles fully qualified URL strings.)
The optional baseURL argument allows for qualifying non-fully-qualified
URL strings relative to a base URL other than the current page location.
*/
function URLFromString(urlString, baseURL = location) {
if ( urlString.startsWith("http://")
|| urlString.startsWith("https://"))
return new URL(urlString);
if (urlString.startsWith("#"))
return new URL(baseURL.origin + baseURL.pathname + urlString);
return (urlString.startsWith("/")
? new URL(baseURL.origin + urlString)
: new URL(baseURL.href.replace(/[^\/]*$/, urlString)));
}
/****************************************************************************/
/* Returns a modified URL constructed from the given URL or URL string, with
the specified modifications in key-value form.
*/
function modifiedURL(url, mods) {
let modURL = typeof url == "string"
? URLFromString(url)
: URLFromString(url.href);
for (let [ key, value ] of Object.entries(mods))
modURL[key] = value;
return modURL;
}
/***************************************************************************/
/* Returns the value of the search param with the given key for a the given
HTMLAnchorElement object.
*/
HTMLAnchorElement.prototype.getQueryVariable = function (key) {
let url = URLFromString(this.href);
return url.searchParams.get(key);
}
/**************************************************************************/
/* Set a URL search parameter with the given key to the given value on the
given HTMLAnchorElement.
*/
HTMLAnchorElement.prototype.setQueryVariable = function (key, value) {
let url = URLFromString(this.href);
url.setQueryVariable(key, value);
this.search = url.search;
}
/******************************************************************/
/* Delete a URL search parameter with the given key from the given
HTMLAnchorElement.
*/
HTMLAnchorElement.prototype.deleteQueryVariable = function (key) {
let url = URLFromString(this.href);
url.deleteQueryVariable(key);
this.search = url.search;
}
/****************************************************************************/
/* Add an event listener to a button (or other clickable element), attaching
it to both ‘click’ and ‘keyup’ events (for use with keyboard navigation).
Available option fields:
includeMouseDown (boolean)
Also attach the listener to the ‘mousedown’ event, making the element
activate on mouse down (rather than only mouse up, as normal).
*/
Element.prototype.addActivateEvent = function(fn, options) {
options = Object.assign({
includeMouseDown: false
}, options);
let ael = this.activateEventListener = (event) => {
if ( event.button === 0
|| event.key === ' ')
fn(event);
};
this.addEventListener("click", ael);
this.addEventListener("keyup", ael);
if (options.includeMouseDown)
this.addEventListener("mousedown", ael);
}
/******************************************************************************/
/* Removes event listener from a clickable element, automatically detaching it
from all relevant event types.
*/
Element.prototype.removeActivateEvent = function() {
let ael = this.activateEventListener;
this.removeEventListener("mousedown", ael);
this.removeEventListener("click", ael);
this.removeEventListener("keyup", ael);
}
/***************************************************************************/
/* Swap classes on the given element.
First argument is an array with two string elements (the classes).
Second argument is 0 or 1 (index of class to add; the other is removed).
*/
Element.prototype.swapClasses = function (classes, whichToAdd) {
this.classList.remove(classes[1 - whichToAdd]);
this.classList.add(classes[whichToAdd]);
};
/******************************************************************************/
/* The first text node of a node or element (or null if an element contains no
text nodes).
*/
Object.defineProperty(Node.prototype, "firstTextNode", {
get() {
if (this.nodeType == Node.TEXT_NODE)
return this;
if (this.childNodes.length == 0)
return null;
for (let i = 0; i < this.childNodes.length; i++) {
let firstTextNodeWithinChildNode = this.childNodes[i].firstTextNode;
if (firstTextNodeWithinChildNode)
return firstTextNodeWithinChildNode;
}
return null;
}
});
/******************************************************************************/
/* The last text node of a node or element (or null if an element contains no
text nodes).
*/
Object.defineProperty(Node.prototype, "lastTextNode", {
get() {
if (this.nodeType == Node.TEXT_NODE)
return this;
if (this.childNodes.length == 0)
return null;
for (let i = this.childNodes.length - 1; i >= 0; i--) {
let lastTextNodeWithinChildNode = this.childNodes[i].lastTextNode;
if (lastTextNodeWithinChildNode)
return lastTextNodeWithinChildNode;
}
return null;
}
});
/**************************************************************************/
/* Returns true if the list contains any of the tokens in the given array.
*/
DOMTokenList.prototype.containsAnyOf = function (tokens) {
for (token of tokens)
if (this.contains(token) == true)
return true;
return false;
}
/**************************************************************************/
/* Returns true if the list contains all of the tokens in the given array.
*/
DOMTokenList.prototype.containsAllOf = function (tokens) {
for (token of tokens)
if (this.contains(token) == false)
return false;
return true;
}
/**************************************************/
/* The obvious equivalent of Element’s .innerHTML.
*/
Object.defineProperty(Document.prototype, "innerHTML", {
get() {
return Array.from(this.childNodes).map(node => {
switch (node.nodeType) {
case Node.ELEMENT_NODE:
return node.outerHTML;
case Node.COMMENT_NODE:
return `<!--${node.nodeValue}-->`;
default:
return node.nodeValue;
}
}).join("");
}
});
/**************************************************/
/* The obvious equivalent of Element’s .innerHTML.
*/
Object.defineProperty(DocumentFragment.prototype, "innerHTML", {
get() {
return Array.from(this.childNodes).map(node => {
switch (node.nodeType) {
case Node.ELEMENT_NODE:
return node.outerHTML;
case Node.COMMENT_NODE:
return `<!--${node.nodeValue}-->`;
default:
return node.nodeValue;
}
}).join("");
}
});
/**************************/
/* Selects the given node.
*/
Selection.prototype.selectNode = function (node) {
let range = new Range();
range.selectNode(node);
this.removeAllRanges();
this.addRange(range);
}
/*************************************************************/
/* Polyfill for crypto.randomUUID, for older browser versions
(Mainly Safari < 15.4)
https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID
*/
if (crypto.randomUUID === undefined) {
crypto.randomUUID = function () {
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
}
/*******************************************************************************/
/* Create and return a new element with the specified tag name, attributes, and
object properties.
*/
function newElement(tagName, attributes, properties) {
attributes = Object.assign({ }, attributes);
properties = Object.assign({ }, properties);
let element = document.createElement(tagName);
for (const attrName in attributes)
if (attributes.hasOwnProperty(attrName))
element.setAttribute(attrName, attributes[attrName]);
for (const propName in properties)
if (properties.hasOwnProperty(propName))
element[propName] = properties[propName];
return element;
}
/*******************************************************************************/
/* Create and return a DocumentFragment containing the given content.
The content can be any of the following (yielding the listed return value):
null
an empty DocumentFragment
a DocumentFragment
a DocumentFragment containing the given DocumentFragment’s child nodes
a string
a DocumentFragment containing the HTML content that results from parsing
the string
a Node
a DocumentFragment containing the node
a NodeList
a DocumentFragment containing the nodes
*/
function newDocument(content) {
let docFrag = new DocumentFragment();
if (content == null)
return docFrag;
if (content instanceof DocumentFragment) {
content = content.childNodes;
} else if (typeof content == "string") {
let wrapper = newElement("DIV");
wrapper.innerHTML = content;
content = wrapper.childNodes;
}
if (content instanceof Node) {
docFrag.append(document.importNode(content, true));
} else if ( content instanceof NodeList
|| content instanceof Array) {
docFrag.append(...(Array.from(content).map(node => document.importNode(node, true))));
}
return docFrag;
}
/************************************************************************/
/* Creates element from HTML string. Returns null if given HTML does not
define one, and only one, element.
*/
function elementFromHTML(elementHTML) {
let doc = newDocument(elementHTML);
if (doc.children.length != 1)
return null;
return doc.firstElementChild;
}
/***************************************************************************/
/* Transfer any of the given CSS classes that the source has to the target.
(If no classes are specified, then transfer all classes the source has.)
*/
function transferClasses(source, target, classes) {
if (classes) {
classes.forEach(cssClass => {
if (source.classList.contains(cssClass)) {
source.classList.remove(cssClass);
target.classList.add(cssClass);
}
});
if (source.className == "")
source.removeAttribute("class");
} else {
target.classList.add(...(source.classList));
source.removeAttribute("class");
}
}
/****************************************/
/* Wrap an element in a wrapper element.
The value of the `wrapperSpec` argument should be in the form "tagName" or
"tagName.class-name-1.class-name-2" (etc.) or ".class-name-1.class-name-2"
(in which case the tag name will default to "div").
(Example: "span.foo-bar.baz-quux", which makes the wrapper
`<span class="foo-bar baz-quux"></span>`.)
Available option fields:
useExistingWrapper (boolean)
If the value of `useExistingWrapper` is `true`, then, if the given
element is already the only element child of an element with the same
tag name as the specified wrapper, then do not inject any additional
wrapper. If wrapper classes are specified, apply them to this existing
wrapper.
moveClasses (boolean|Array)
If the value of `moveClasses` is `true`, then all classes are moved
(not copied!) from the given element to the wrapper. If, instead, the
value of `moveClasses` is an array, then all classes which are in the
array are moved (not copied!) from the given element to the wrapper.
*/
function wrapElement(element, wrapperSpec = "", options) {
options = Object.assign({
useExistingWrapper: false,
moveClasses: false
}, options);
let [ wrapperTagName, ...wrapperClasses ] = wrapperSpec.split(".");
// Default wrapper tag to <div>; capitalize tag name.
wrapperTagName = (wrapperTagName == "" ? "div" : wrapperTagName).toUpperCase();
if ( options.useExistingWrapper
&& element.parentElement?.tagName == wrapperTagName
&& isOnlyChild(element)) {
if (wrapperClasses.length > 0)
element.parentElement.classList.add(...wrapperClasses);
} else {
let wrapper = newElement(wrapperTagName);
if (wrapperClasses.length > 0)
wrapper.classList.add(...wrapperClasses);
element.parentNode.insertBefore(wrapper, element);
wrapper.appendChild(element);
}
if (options.moveClasses === true) {
transferClasses(element, element.parentElement);
} else if (options.moveClasses instanceof Array) {
transferClasses(element, element.parentElement, options.moveClasses);
}
return element.parentElement;
}
/****************************************************************************/
/* Wrap all elements specified by the given selector.
See the wrapElement() function for details on the `wrapperSpec` and
`options` arguments.
NOTE: The `wrapperSpec` argument may be a wrap function, in which case all
option fields that pertain to the wrapElement() function are ignored (as
that function is not called in such a case).
Available option fields:
root (Element|Document|DocumentFragment)
Look for the given selector within the subtree of this node.
*/
function wrapAll(selector, wrapperSpec, options) {
options = Object.assign({
root: document
}, options);
let wrapperFunction = typeof wrapperSpec == "function"
? wrapperSpec
: (element) => { wrapElement(element, wrapperSpec, options); };
options.root.querySelectorAll(selector).forEach(wrapperFunction);
}
/**************************************************************************/
/* Replace an element with its contents. Returns array of unwrapped nodes.
Available option fields:
moveID (boolean)
If the value of this option field is `true`, and the wrapper has only a
single child element, then that element is assigned the `id` attribute
of the wrapper (if any).
moveClasses (boolean|Array)
If the value of this option field is `true`, then all classes are moved
from the wrapper to each unwrapped child element. If, instead, the value
of this option field is an array, then all classes which are in the
array are moved from the wrapper to each element child.
preserveBlockSpacing (boolean)
If the value of this option field is `true`, then the value of the
`--bsm` CSS property of the wrapper (if any) is assigned to the first
child element of the wrapper.
*/
function unwrap(wrapper, options) {
options = Object.assign({
moveID: false,
moveClasses: false,
preserveBlockSpacing: false
}, options);
if (wrapper == null)
return;
if (wrapper.parentNode == null)
return;
let nodes = Array.from(wrapper.childNodes);
// Move ID, if specified.
if ( options.moveID
&& wrapper.id > ""
&& wrapper.children.length == 1) {
wrapper.firstElementChild.id = wrapper.id;
}
// Preserve block spacing, if specified.
if ( options.preserveBlockSpacing
&& wrapper.children.length > 0) {
let bsm = wrapper.style.getPropertyValue("--bsm");
if (bsm > "")
wrapper.firstElementChild.setProperty("--bsm", bsm);
}
while (wrapper.childNodes.length > 0) {
let child = wrapper.firstChild;
wrapper.parentNode.insertBefore(child, wrapper);
if (!(child instanceof Element))
continue;
// Move classes, if specified.
if (options.moveClasses === true) {
transferClasses(wrapper, child);
} else if (options.moveClasses instanceof Array) {
transferClasses(wrapper, child, options.moveClasses);
}
}
wrapper.remove();
return nodes;
}
/******************************************************************************/
/* Wrap element’s contents, then unwrap the element itself and return wrapper.
*/
function rewrapContents(...args) {
let wrapper = wrapElement(...args);
unwrap(args[0]);
return wrapper;
}
/*************************************************************************/
/* Unwrap all elements specified by the given selector.
Available option fields:
root (Element|Document|DocumentFragment)
Look for the given selector within the subtree of this node.
(NOTE: All options used by unwrap() are also supported.)
*/
function unwrapAll(selector, options) {
options = Object.assign({
root: document
}, options);
options.root.querySelectorAll(selector).forEach(element => {
unwrap(element, options);
});
}
/*************************************************************************/
/* Save an element’s inline styles in a .savedStyles DOM object property.
Available option fields:
saveProperties (Array)
Array of property names which should be saved. If this is null, then
all properties are saves.
*/
function saveStyles(element, options) {
options = Object.assign({
saveProperties: null
}, options);
let stylesToSave = { };
for (let i = 0; i < element.style.length; i++) {
let propertyName = element.style.item(i);
if ( options.saveProperties == null
|| options.saveProperties.includes(propertyName)) {
let propertyValue = element.style.getPropertyValue(propertyName);
stylesToSave[propertyName] = propertyValue;
}
}
if (Object.entries(stylesToSave).length > 0)
element.savedStyles = stylesToSave;
}
/******************************************************************************/
/* Restore an element’s inline styles from a .savedStyles DOM object property.
*/
function restoreStyles(element) {
if (element.savedStyles == null)
return;
for (let [ propertyName, propertyValue ] of Object.entries(element.savedStyles))
element.style.setProperty(propertyName, propertyValue);
element.savedStyles = null;
}
/*****************************************************************************/
/* Strip an element’s inline styles, optionally only removing some styles,
optionally keeping some styles.
Available option fields:
removeProperties (Array)
Remove only properties whose names are in this array. If this is null,
then all properties are removed.
saveProperties (Array)
Save the value of these properties; after removing some or all
properties (depending on the value of the `removeProperties` option),
restore these properties to their saved values.
*/
function stripStyles(element, options) {
options = Object.assign({
removeProperties: null,
saveProperties: null
}, options);
if (options.saveProperties)
saveStyles(element, { saveProperties: options.saveProperties });
if (options.removeProperties) {
for (let i = 0; i < element.style.length; i++) {
let propertyName = element.style.item(i);
if (options.removeProperties.includes(propertyName))
element.style.removeProperty(propertyName);
}
} else {
element.removeAttribute("style");
}
if (options.saveProperties)
restoreStyles(element);
if (element.style.length == 0)
element.removeAttribute("style");
}
/**************************************************************************/
/* Call the given function when the given element intersects the viewport.
The `entries` parameter of the IntersectionObserver callback is passed
to the called function (unless called immediately).
Available option fields:
root
See IntersectionObserver documentation.
threshold
See IntersectionObserver documentation.
rootMargin
See IntersectionObserver documentation.
*/
function lazyLoadObserver(f, target, options) {
options = Object.assign({ }, options);
if (target == null)
return;
requestAnimationFrame(() => {
if ( (options.threshold ?? 0) == 0
&& (options.rootMargin ?? "0px").includes("-") == false
&& isWithinRectOf(target, options.root)) {
f();
return;
}
let observer = new IntersectionObserver((entries) => {
if (entries.first.isIntersecting == false)
return;
f(entries);
observer.disconnect();
}, options);
observer.observe(target);
});
}
/***********************************************************************/
/* Call the given function when the given element is resized.
The `entries` parameter of the ResizeObserver callback is passed
to the called function.
If `false` is returned from the function call, disconnects observer.
Otherwise, continues observation.
*/
function resizeObserver(f, target) {
if (target == null)
return;
requestAnimationFrame(() => {
let observer = new ResizeObserver((entries) => {
if (f(entries) == false)
observer.disconnect();
});
observer.observe(target);
});
}
/*******************************************************************************/
/* Returns true if the node’s parent has just one child (i.e., the given node),
or if all siblings are empty nodes. (Note that the given node *itself* being
empty does not prevent this function from returning true!)
*/
function isOnlyChild(node) {
if (node == null)
return undefined;
if (node.parentElement == null)
return undefined;
if (node.parentElement.childNodes.length == 1)
return true;
let nonemptySiblingsExist = false;
node.parentElement.childNodes.forEach(child => {
if ( child != node
&& isNodeEmpty(child) == false)
nonemptySiblingsExist = true;
});
return (nonemptySiblingsExist == false);
}
/******************************************************************************/
/* Returns true if the node contains only whitespace and/or other empty nodes.
Available option fields:
excludePredicate (Node => boolean)
If *and only if* the predicate returns true when called with the node
as argument, always consider it to be non-empty (and thus always
return false, no matter what the node may contain). (NOTE: If this
option is set, all other options are ignored.)
alsoExcludePredicate (Node => boolean)
If the predicate returns true when called with the node as argument,
always consider it to be non-empty (and thus always return false,
no matter what the node may contain). (NOTE: If this option is set,
the options below will still take effect.)
excludeSelector (string)
If the node is an element node, then if *and only if* it matches the
given selector, always consider it to be non-empty (and thus always
return false, no matter what the node may contain). (Note the difference
between this option and `alsoExcludeSelector`, below.)
alsoExcludeSelector (string)
If the node is an element node, then if the node matches the given
selector OR is one of several always-considered-nonempty tag types
(IMG, SVG, VIDEO, AUDIO, IFRAME, OBJECT), always consider the node to be
non-empty (and thus always return false, no matter what the node may
contain). (Note the difference between this option and
`excludeSelector`, above.)
excludeIdentifiedElements (boolean)
If the node is an element node, and has a non-empty value for the `id`
attribute, then always consider the node to be non-empty (and thus
always return false, no matter what the node may contain).
*/
function isNodeEmpty(node, options) {
options = Object.assign({
excludePredicate: null,
excludeSelector: null,
alsoExcludeSelector: null,
excludeIdentifiedElements: false
}, options);
if (node == null)
return undefined;
if (node.nodeType == Node.TEXT_NODE)
return (/^[\s\u2060]*$/.test(node.textContent));
if (options.excludePredicate != null) {
if (options.excludePredicate(node))
return false;
} else if (options.alsoExcludePredicate?.(node)) {
return false;
} else if (node.nodeType == Node.ELEMENT_NODE) {
if ( options.excludeIdentifiedElements
&& node.id > "") {
return false;
} else if ( options.excludeSelector != null
&& node.matches(options.excludeSelector)) {
return false;
} else if ( [ "IMG", "SVG", "VIDEO", "AUDIO", "IFRAME", "OBJECT" ].includes(node.tagName.toUpperCase())
|| ( options.alsoExcludeSelector != null
&& node.matches(options.alsoExcludeSelector))) {
return false;
}
}
if (node.childNodes.length == 0)
return true;
for (childNode of node.childNodes)
if (isNodeEmpty(childNode, options) == false)
return false;
return true;
}
/************************************************************************/
/* Wrap text nodes and inline elements in the given element in <p> tags.
Available option fields:
nodeOmissionOptions (object)
Options to pass to the isNodeEmpty() call that determines whether a
node should be dropped when aggregating nodes into paragraphs.
*/
function paragraphizeTextNodesOfElement(element, options) {
options = Object.assign({
nodeOmissionOptions: {
alsoExcludeSelector: "a, br",
excludeIdentifiedElements: true
}
}, options);
let inlineElementSelector = [
"a",
"em",
"strong",
"i",
"b",
"code",
"sup",
"sub",
"span"
].join(", ");
let nodes = Array.from(element.childNodes);
let nodeSequence = [ ];
let shouldOmitNode = (node) => isNodeEmpty(node, options.nodeOmissionOptions);
let node;
do {
node = nodes.shift();
let omitNode = shouldOmitNode(node);
if ( ( node?.nodeType == Node.TEXT_NODE
|| ( node?.nodeType == Node.ELEMENT_NODE
&& node.matches(inlineElementSelector)))
&& omitNode == false) {
nodeSequence.push(node);
} else if (omitNode) {
node?.remove();
} else {
if (nodeSequence.length > 0) {
// Construct paragraph (<p>) to wrap node sequence.
// (This removes the nodes from the element.)
let graf = newElement("P");
graf.append(...nodeSequence);
// Insert paragraph (with the previously removed nodes).
element.insertBefore(graf, node)
}