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
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
|
"use strict";
(function(F/*fossil object*/){
/**
A collection of HTML DOM utilities to simplify, a bit, using the
DOM API. It is focused on manipulation of the DOM, but one of its
core mantras is "No innerHTML." Using innerHTML in this code, in
particular assigning to it, is absolutely verboten.
*/
const argsToArray = (a)=>Array.prototype.slice.call(a,0);
const isArray = (v)=>v instanceof Array;
const dom = {
create: function(elemType){
return document.createElement(elemType);
},
createElemFactory: function(eType){
return function(){
return document.createElement(eType);
};
},
remove: function(e){
if(e?.forEach){
e.forEach(
(x)=>x?.parentNode?.removeChild(x)
);
}else{
e?.parentNode?.removeChild(e);
}
return e;
},
/**
Removes all child DOM elements from the given element
and returns that element.
If e has a forEach method (is an array or DOM element
collection), this function instead clears each element in the
collection. May be passed any number of arguments, each of
which must be a DOM element or a container of DOM elements with
a forEach() method. Returns its first argument.
*/
clearElement: function f(e){
if(!f.each){
f.each = function(e){
if(e.forEach){
e.forEach((x)=>f(x));
return e;
}
while(e.firstChild) e.removeChild(e.firstChild);
};
}
argsToArray(arguments).forEach(f.each);
return arguments[0];
},
}/* dom object */;
/**
Returns the result of splitting the given str on
a run of spaces of (\s*,\s*).
*/
dom.splitClassList = function f(str){
if(!f.rx){
f.rx = /(\s+|\s*,\s*)/;
}
return str ? str.split(f.rx) : [str];
};
dom.div = dom.createElemFactory('div');
dom.p = dom.createElemFactory('p');
dom.code = dom.createElemFactory('code');
dom.pre = dom.createElemFactory('pre');
dom.header = dom.createElemFactory('header');
dom.footer = dom.createElemFactory('footer');
dom.section = dom.createElemFactory('section');
dom.span = dom.createElemFactory('span');
dom.strong = dom.createElemFactory('strong');
dom.em = dom.createElemFactory('em');
dom.ins = dom.createElemFactory('ins');
dom.del = dom.createElemFactory('del');
/**
Returns a LABEL element. If passed an argument,
it must be an id or an HTMLElement with an id,
and that id is set as the 'for' attribute of the
label. If passed 2 arguments, the 2nd is text or
a DOM element to append to the label.
2026-06: this is a goofy interface. Generally simpler that
dealing with IDs is to embed the target control within the label
element.
*/
dom.label = function(forElem, text){
const rc = document.createElement('label');
if(forElem){
if(forElem instanceof HTMLElement){
forElem = this.attr(forElem, 'id');
}
if(forElem){
dom.attr(rc, 'for', forElem);
}
}
if(text) this.append(rc, text);
return rc;
};
/**
Returns an IMG element with an optional src
attribute value.
*/
dom.img = function(src){
const e = this.create('img');
if(src) e.setAttribute('src',src);
return e;
};
/**
Creates and returns a new anchor element with the given
optional href and label. If label===true then href is used
as the label.
*/
dom.a = function(href,label){
const e = this.create('a');
if(href) e.setAttribute('href',href);
if(label) e.appendChild(dom.text(true===label ? href : label));
return e;
};
dom.hr = dom.createElemFactory('hr');
dom.br = dom.createElemFactory('br');
/** Returns a new TEXT node which contains the text of all of the
arguments appended together. */
dom.text = function(/*...*/){
return document.createTextNode(argsToArray(arguments).join(''));
};
/** Returns a new Button element with the given optional
label and on-click event listener function. */
dom.button = function(label,callback){
const b = this.create('button');
if(label) b.appendChild(this.text(label));
if('function' === typeof callback){
b.addEventListener('click', callback, false);
}
return b;
};
/**
Returns a TEXTAREA element.
Usages:
([boolean readonly = false])
(non-boolean rows[,cols[,readonly=false]])
Each of the rows/cols/readonly attributes is only set if it is
truthy.
*/
dom.textarea = function(){
const rc = this.create('textarea');
let rows, cols, readonly;
if(1===arguments.length){
if('boolean'===typeof arguments[0]){
readonly = !!arguments[0];
}else{
rows = arguments[0];
}
}else if(arguments.length){
rows = arguments[0];
cols = arguments[1];
readonly = arguments[2];
}
if(rows) rc.setAttribute('rows',rows);
if(cols) rc.setAttribute('cols', cols);
if(readonly) rc.setAttribute('readonly', true);
return rc;
};
/**
Returns a new SELECT element.
*/
dom.select = dom.createElemFactory('select');
/**
Returns an OPTION element with the given value and label text
(which defaults to the value).
Usage:
(value[, label])
(selectElement [,value [,label = value]])
Any forms taking a SELECT element append the new element to the
given SELECT element.
If any label is falsy and the value is not then the value is used
as the label. A non-falsy label value may have any type suitable
for passing as the 2nd argument to dom.append().
If the value has the undefined value then it is NOT assigned as
the option element's value and no label is set unless it has a
non-undefined value.
*/
dom.option = function(value,label){
const a = arguments;
var sel;
if(1==a.length){
if(a[0] instanceof HTMLElement){
sel = a[0];
}else{
value = a[0];
}
}else if(2==a.length){
if(a[0] instanceof HTMLElement){
sel = a[0];
value = a[1];
}else{
value = a[0];
label = a[1];
}
}
else if(3===a.length){
sel = a[0];
value = a[1];
label = a[2];
}
const o = this.create('option');
if(undefined !== value){
o.value = value;
this.append(o, this.text(label || value));
}else if(undefined !== label){
this.append(o, label);
}
if(sel) this.append(sel, o);
return o;
};
dom.h = function(level){
return this.create('h'+level);
};
dom.ul = dom.createElemFactory('ul');
/**
Creates and returns a new LI element, appending it to the
given parent argument if it is provided.
*/
dom.li = function(parent){
const li = this.create('li');
if(parent) parent.appendChild(li);
return li;
};
/**
Returns a function which creates a new DOM element of the
given type and accepts an optional parent DOM element
argument. If the function's argument is truthy, the new
child element is appended to the given parent element.
Returns the new child element.
*/
dom.createElemFactoryWithOptionalParent = function(childType){
return function(parent){
const e = this.create(childType);
if(parent) parent.appendChild(e);
return e;
};
};
dom.table = dom.createElemFactory('table');
dom.thead = dom.createElemFactoryWithOptionalParent('thead');
dom.tbody = dom.createElemFactoryWithOptionalParent('tbody');
dom.tfoot = dom.createElemFactoryWithOptionalParent('tfoot');
dom.tr = dom.createElemFactoryWithOptionalParent('tr');
dom.td = dom.createElemFactoryWithOptionalParent('td');
dom.th = dom.createElemFactoryWithOptionalParent('th');
/**
Creates and returns a FIELDSET element, optionaly with a LEGEND
element added to it. If legendText is an HTMLElement then it is
assumed to be a LEGEND and is appended as-is, else it is assumed
(if truthy) to be a value suitable for passing to
dom.append(aLegendElement,...).
*/
dom.fieldset = function(legendText){
const fs = this.create('fieldset');
if(legendText){
this.append(
fs,
(legendText instanceof HTMLElement)
? legendText
: this.append(this.legend(legendText))
);
}
return fs;
};
/**
Returns a new LEGEND legend element. The given argument, if
not falsy, is append()ed to the element (so it may be a string
or DOM element.
*/
dom.legend = function(legendText){
const rc = this.create('legend');
if(legendText) this.append(rc, legendText);
return rc;
};
/**
Appends each argument after the first to the first argument
(a DOM node) and returns the first argument.
- If an argument is a string or number, it is transformed
into a text node.
- If an argument is an array or has a forEach member, this
function appends each element in that list to the target
by calling its forEach() method to pass it (recursively)
to this function.
- Else the argument assumed to be of a type legal
to pass to parent.appendChild().
*/
dom.append = function f(parent/*,...*/){
const a = argsToArray(arguments);
a.shift();
for(let i in a) {
var e = a[i];
if(isArray(e) || e.forEach){
e.forEach((x)=>f.call(this, parent,x));
continue;
}
if('string'===typeof e
|| 'number'===typeof e
|| 'boolean'===typeof e
|| e instanceof Error) e = this.text(e);
parent.appendChild(e);
}
return parent;
};
dom.input = function(type){
return this.attr(this.create('input'), 'type', type);
};
/**
Returns a new CHECKBOX input element.
Usages:
([boolean checked = false])
(non-boolean value [,boolean checked])
*/
dom.checkbox = function(value, checked){
const rc = this.input('checkbox');
if(1===arguments.length && 'boolean'===typeof value){
checked = !!value;
value = undefined;
}
if(undefined !== value) rc.value = value;
if(!!checked) rc.checked = true;
return rc;
};
/**
Returns a new RADIO input element.
([boolean checked = false])
(string name [,boolean checked])
(string name, non-boolean value [,boolean checked])
*/
dom.radio = function(){
const rc = this.input('radio');
let name, value, checked;
if(1===arguments.length && 'boolean'===typeof name){
checked = arguments[0];
name = value = undefined;
}else if(2===arguments.length){
name = arguments[0];
if('boolean'===typeof arguments[1]){
checked = arguments[1];
}else{
value = arguments[1];
checked = undefined;
}
}else if(arguments.length){
name = arguments[0];
value = arguments[1];
checked = arguments[2];
}
if(name) this.attr(rc, 'name', name);
if(undefined!==value) rc.value = value;
if(!!checked) rc.checked = true;
return rc;
};
/**
Internal impl for addClass(), removeClass().
*/
const domAddRemoveClass = function f(action,e){
if(!f.rxSPlus){
f.rxSPlus = /\s+/;
f.applyAction = function(e,a,v){
if(!e || !v
/*silently skip empty strings/falsy
values, for usage convenience*/) return;
else if(e.forEach){
e.forEach((E)=>E.classList[a](v));
}else{
e.classList[a](v);
}
};
}
var i = 2, n = arguments.length;
for( ; i < n; ++i ){
let c = arguments[i];
if(!c) continue;
else if(isArray(c) ||
('string'===typeof c
&& c.indexOf(' ')>=0
&& (c = c.split(f.rxSPlus)))
|| c.forEach
){
c.forEach((k)=>k ? f.applyAction(e, action, k) : false);
// ^^^ we could arguably call f(action,e,k) to recursively
// apply constructs like ['foo bar'] or [['foo'],['bar baz']].
}else if(c){
f.applyAction(e, action, c);
}
}
return e;
};
/**
Adds one or more CSS classes to one or more DOM elements.
The first argument is a target DOM element or a list type of such elements
which has a forEach() method. Each argument
after the first may be a string or array of strings. Each
string may contain spaces, in which case it is treated as a
list of CSS classes.
Returns e.
*/
dom.addClass = function(e,c){
const a = argsToArray(arguments);
a.unshift('add');
return domAddRemoveClass.apply(this, a);
};
/**
The 'remove' counterpart of the addClass() method, taking
the same arguments and returning the same thing.
*/
dom.removeClass = function(e,c){
const a = argsToArray(arguments);
a.unshift('remove');
return domAddRemoveClass.apply(this, a);
};
/**
Toggles CSS class c on e (a single element for forEach-capable
collection of elements). Returns its first argument.
*/
dom.toggleClass = function f(e,c){
if(e.forEach){
e.forEach((x)=>x.classList.toggle(c));
}else{
e.classList.toggle(c);
}
return e;
};
/**
Returns true if DOM element e contains CSS class c, else
false.
*/
dom.hasClass = function(e,c){
return (e && e.classList) ? e.classList.contains(c) : false;
};
/**
Each argument after the first may be a single DOM element or a
container of them with a forEach() method. All such elements are
appended, in the given order, to the dest element using
dom.append(dest,theElement). Thus the 2nd and susequent arguments
may be any type supported as the 2nd argument to that function.
Returns dest.
*/
dom.moveTo = function(dest,e){
const n = arguments.length;
var i = 1;
const self = this;
for( ; i < n; ++i ){
e = arguments[i];
this.append(dest, e);
}
return dest;
};
/**
Each argument after the first may be a single DOM element
or a container of them with a forEach() method. For each
DOM element argument, all children of that DOM element
are moved to dest (via appendChild()). For each list argument,
each entry in the list is assumed to be a DOM element and is
appended to dest.
dest may be an Array, in which case each child is pushed
into the array and removed from its current parent element.
All children are appended in the given order.
Returns dest.
*/
dom.moveChildrenTo = function f(dest,e){
if(!f.mv){
f.mv = function(d,v){
if(d instanceof Array){
d.push(v);
if(v.parentNode) v.parentNode.removeChild(v);
}
else d.appendChild(v);
};
}
const n = arguments.length;
var i = 1;
for( ; i < n; ++i ){
e = arguments[i];
if(!e){
console.warn("Achtung: dom.moveChildrenTo() passed a falsy value at argument",i,"of",
arguments,arguments[i]);
continue;
}
if(e.forEach){
e.forEach((x)=>f.mv(dest, x));
}else{
while(e.firstChild){
f.mv(dest, e.firstChild);
}
}
}
return dest;
};
/**
Adds each argument (DOM Elements) after the first to the
DOM immediately before the first argument (in the order
provided), then removes the first argument from the DOM.
Returns void.
If any argument beyond the first has a forEach method, that
method is used to recursively insert the collection's
contents before removing the first argument from the DOM.
*/
dom.replaceNode = function f(old,nu){
var i = 1, n = arguments.length;
++f.counter;
try {
for( ; i < n; ++i ){
const e = arguments[i];
if(e.forEach){
e.forEach((x)=>f.call(this,old,e));
continue;
}
old.parentNode.insertBefore(e, old);
}
}
finally{
--f.counter;
}
if(!f.counter){
old.parentNode.removeChild(old);
}
};
dom.replaceNode.counter = 0;
/**
Two args == getter: (e,key), returns value
Three or more == setter: (e,key,val[...,keyN,valN]), returns
e. If val===null or val===undefined then the attribute is
removed. If (e) has a forEach method then this routine is applied
to each element of that collection via that method. Each pair of
keys/values is applied to all elements designated by the first
argument.
*/
dom.attr = function f(e){
if(2===arguments.length) return e.getAttribute(arguments[1]);
const a = argsToArray(arguments);
if(e.forEach){ /* Apply to all elements in the collection */
e.forEach(function(x){
a[0] = x;
f.apply(f,a);
});
return e;
}
a.shift(/*element(s)*/);
while(a.length){
const key = a.shift(), val = a.shift();
if(null===val || undefined===val){
e.removeAttribute(key);
}else{
e.setAttribute(key,val);
}
}
return e;
};
/* Impl for dom.enable() and dom.disable(). */
const enableDisable = function f(enable){
var i = 1, n = arguments.length;
for( ; i < n; ++i ){
let e = arguments[i];
if(e.forEach){
e.forEach((x)=>f(enable,x));
}else{
e.disabled = !enable;
}
}
return arguments[1];
};
/**
Enables (by removing the "disabled" attribute) each element
(HTML DOM element or a collection with a forEach method)
and returns the first argument.
*/
dom.enable = function(e){
const args = argsToArray(arguments);
args.unshift(true);
return enableDisable.apply(this,args);
};
/**
Disables (by setting the "disabled" attribute) each element
(HTML DOM element or a collection with a forEach method)
and returns the first argument.
*/
dom.disable = function(e){
const args = argsToArray(arguments);
args.unshift(false);
return enableDisable.apply(this,args);
};
/**
A proxy for document.querySelector() which throws if
selection x is not found. It may optionally be passed an
"origin" object as its 2nd argument, which restricts the
search to that branch of the tree.
*/
dom.selectOne = function(x,origin){
var src = origin || document,
e = src.querySelector(x);
if(!e){
e = new Error("Cannot find DOM element: "+x);
console.error(e, src);
throw e;
}
return e;
};
/**
"Blinks" the given element a single time for the given number of
milliseconds, defaulting (if the 2nd argument is falsy or not a
number) to flashOnce.defaultTimeMs. If a 3rd argument is passed
in, it must be a function, and it gets called at the end of the
asynchronous flashing processes.
This will only activate once per element during that timeframe -
further calls will become no-ops until the blink is
completed. This routine adds a dataset member to the element for
the duration of the blink, to allow it to block multiple blinks.
If passed 2 arguments and the 2nd is a function, it behaves as if
it were called as (arg1, undefined, arg2).
Returns e, noting that the flash itself is asynchronous and may
still be running, or not yet started, when this function returns.
*/
dom.flashOnce = function f(e,howLongMs,afterFlashCallback){
if(e.dataset.isBlinking){
return;
}
if(2===arguments.length && 'function' ===typeof howLongMs){
afterFlashCallback = howLongMs;
howLongMs = f.defaultTimeMs;
}
if(!howLongMs || 'number'!==typeof howLongMs){
howLongMs = f.defaultTimeMs;
}
e.dataset.isBlinking = true;
const transition = e.style.transition;
e.style.transition = "opacity "+howLongMs+"ms ease-in-out";
const opacity = e.style.opacity;
e.style.opacity = 0;
setTimeout(function(){
e.style.transition = transition;
e.style.opacity = opacity;
delete e.dataset.isBlinking;
if(afterFlashCallback) afterFlashCallback();
}, howLongMs);
return e;
};
dom.flashOnce.defaultTimeMs = 400;
/**
A DOM event handler which simply passes event.target
to dom.flashOnce().
*/
dom.flashOnce.eventHandler = (event)=>dom.flashOnce(event.target)
/**
This variant of flashOnce() flashes the element e n times
for a duration of howLongMs milliseconds then calls the
afterFlashCallback() callback. It may also be called with 2
or 3 arguments, in which case:
2 arguments: default flash time and no callback.
3 arguments: 3rd may be a flash delay time or a callback
function.
Returns this object but the flashing is asynchronous.
Depending on system load and related factors, a multi-flash
animation might stutter and look suboptimal.
*/
dom.flashNTimes = function(e,n,howLongMs,afterFlashCallback){
const args = argsToArray(arguments);
args.splice(1,1);
if(arguments.length===3 && 'function'===typeof howLongMs){
afterFlashCallback = howLongMs;
howLongMs = args[1] = this.flashOnce.defaultTimeMs;
}else if(arguments.length<3){
args[1] = this.flashOnce.defaultTimeMs;
}
n = +n;
const self = this;
const cb = args[2] = function f(){
if(--n){
setTimeout(()=>self.flashOnce(e, howLongMs, f),
howLongMs+(howLongMs*0.1)/*we need a slight gap here*/);
}else if(afterFlashCallback){
afterFlashCallback();
}
};
this.flashOnce.apply(this, args);
return this;
};
/**
Adds the given CSS class or array of CSS classes to the given
element or forEach-capable list of elements for howLongMs, then
removes it. If afterCallack is a function, it is called after the
CSS class is removed from all elements. If called with 3
arguments and the 3rd is a function, the 3rd is treated as a
callback and the default time (addClassBriefly.defaultTimeMs) is
used. If called with only 2 arguments, a time of
addClassBriefly.defaultTimeMs is used.
Passing a value of 0 for howLongMs causes the default value
to be applied.
Returns this object but the CSS removal is asynchronous.
*/
dom.addClassBriefly = function f(e, className, howLongMs, afterCallback){
if(arguments.length<4 && 'function'===typeof howLongMs){
afterCallback = howLongMs;
howLongMs = f.defaultTimeMs;
}else if(arguments.length<3 || !+howLongMs){
howLongMs = f.defaultTimeMs;
}
this.addClass(e, className);
setTimeout(function(){
dom.removeClass(e, className);
if(afterCallback) afterCallback();
}, howLongMs);
return this;
};
dom.addClassBriefly.defaultTimeMs = 1000;
/**
Attempts to copy the given text to the system clipboard. Returns
true if it succeeds, else false.
*/
dom.copyTextToClipboard = function(text){
if( window.clipboardData && window.clipboardData.setData ){
window.clipboardData.setData('Text',text);
return true;
}else{
const x = document.createElement("textarea");
x.style.position = 'fixed';
x.value = text;
document.body.appendChild(x);
x.select();
var rc;
try{
document.execCommand('copy');
rc = true;
}catch(err){
rc = false;
}finally{
document.body.removeChild(x);
}
return rc;
}
};
/**
Copies all properties from the 2nd argument (a plain object) into
the style member of the first argument (DOM element or a
forEach-capable list of elements). If the 2nd argument is falsy
or empty, this is a no-op.
Returns its first argument.
*/
dom.copyStyle = function f(e, style){
if(e.forEach){
e.forEach((x)=>f(x, style));
return e;
}
if(style){
let k;
for(k in style){
if(style.hasOwnProperty(k)) e.style[k] = style[k];
}
}
return e;
};
/**
Given a DOM element, this routine measures its "effective
height", which is the bounding top/bottom range of this element
and all of its children, recursively. For some DOM structure
cases, a parent may have a reported height of 0 even though
children have non-0 sizes.
Returns 0 if !e or if the element really has no height.
*/
dom.effectiveHeight = function f(e){
if(!e) return 0;
if(!f.measure){
f.measure = function callee(e, depth){
if(!e) return;
const m = e.getBoundingClientRect();
if(0===depth){
callee.top = m.top;
callee.bottom = m.bottom;
}else{
callee.top = m.top ? Math.min(callee.top, m.top) : callee.top;
callee.bottom = Math.max(callee.bottom, m.bottom);
}
Array.prototype.forEach.call(e.children,(e)=>callee(e,depth+1));
if(0===depth){
//console.debug("measure() height:",e.className, callee.top, callee.bottom, (callee.bottom - callee.top));
f.extra += callee.bottom - callee.top;
}
return f.extra;
};
}
f.extra = 0;
f.measure(e,0);
return f.extra;
};
/**
Parses a string as HTML.
Usages:
Array parseHtml(htmlString)
DOMElement parseHtml(DOMElement target, htmlString)
The first form parses the string as HTML and returns an Array of
all elements parsed from it. If string is falsy then it returns
an empty array.
The second form parses the HTML string and appends all elements
to the given target element using dom.append(), then returns the
first argument.
Caveats:
- It expects a partial HTML document as input, not a full HTML
document with a HEAD and BODY tags. Because of how DOMParser
works, only children of the parsed document's (virtual) body are
acknowledged by this routine.
*/
dom.parseHtml = function(){
let childs, string, tgt;
if(1===arguments.length){
string = arguments[0];
}else if(2==arguments.length){
tgt = arguments[0];
string = arguments[1];
}
if(string){
const newNode = new DOMParser().parseFromString(string, 'text/html');
childs = newNode.documentElement.querySelector('body');
childs = childs ? Array.prototype.slice.call(childs.childNodes, 0) : [];
/* ^^^ we need a clone of the list because reparenting them
modifies a NodeList they're in. */
}else{
childs = [];
}
return tgt ? this.moveTo(tgt, childs) : childs;
};
/**
Sets up pseudo-automatic content preview handling between a
source element (typically a TEXTAREA) and a target rendering
element (typically a DIV). The selector argument must be one of:
- A single DOM element
- A collection of DOM elements with a forEach method.
- A CSS selector
Each element in the collection must have the following data
attributes:
- data-f-preview-from: is either a DOM element id, WITH a leading
'#' prefix, or the name of a method (see below). If it's an ID,
the DOM element must support .value to get the content.
- data-f-preview-to: the DOM element id of the target "previewer"
element, WITH a leading '#', or the name of a method (see below).
- data-f-preview-via: the name of a method (see below).
- OPTIONAL data-f-preview-as-text: a numeric value. Explained below.
Each element gets a click handler added to it which does the
following:
1) Reads the content from its data-f-preview-from element or, if
that property refers to a method, calls the method without
arguments and uses its result as the content.
2) Passes the content to
methodNamespace[f-data-post-via](content,callback). f-data-post-via
is responsible for submitting the preview HTTP request, including
any parameters the request might require. When the response
arrives, it must pass the content of the response to its 2nd
argument, an auto-generated callback installed by this mechanism
which...
3) Assigns the response text to the data-f-preview-to element or
passes it to the function
methodNamespace[f-data-preview-to](content), as appropriate. If
data-f-preview-to is a DOM element and data-f-preview-as-text is
'0' (the default) then the target elements contents are replaced
with the given content as HTML, else the content is assigned to
the target's textContent property. (Note that this routine uses
DOMParser, rather than assignment to innerHTML, to apply
HTML-format content.)
The methodNamespace (2nd argument) defaults to fossil.page, and
any method-name data properties, e.g. data-f-preview-via and
potentially data-f-preview-from/to, must be a single method name,
not a property-access-style string. e.g. "myPreview" is legal but
"foo.myPreview" is not (unless, of course, the method is actually
named "foo.myPreview" (which is legal but would be
unconventional)). All such methods are called with
methodNamespace as their "this".
An example...
First an input button:
<button id='test-preview-connector'
data-f-preview-from='#fileedit-content-editor' // elem ID or method name
data-f-preview-via='myPreview' // method name
data-f-preview-to='#fileedit-tab-preview-wrapper' // elem ID or method name
>Preview update</button>
And a sample data-f-preview-via method:
fossil.page.myPreview = function(content,callback){
const fd = new FormData();
fd.append('foo', ...);
fossil.fetch('preview_forumpost',{
payload: fd,
onload: callback,
onerror: (e)=>{ // only if app-specific handling is needed
fossil.fetch.onerror(e); // default impl
... any app-specific error reporting ...
}
});
};
Then connect the parts with:
fossil.connectPagePreviewers('#test-preview-connector');
Note that the data-f-preview-from, data-f-preview-via, and
data-f-preview-to selector are not resolved until the button is
actually clicked, so they need not exist in the DOM at the
instant when the connection is set up, so long as they can be
resolved when the preview-refreshing element is clicked.
Maintenance reminder: this method is not strictly part of
fossil.dom, but is in its file because it needs access to
dom.parseHtml() to avoid an innerHTML assignment and all code
which uses this routine also needs fossil.dom.
*/
F.connectPagePreviewers = function f(selector,methodNamespace){
if('string'===typeof selector){
selector = document.querySelectorAll(selector);
}else if(!selector.forEach){
selector = [selector];
}
if(!methodNamespace){
methodNamespace = F.page;
}
selector.forEach(function(e){
e.addEventListener(
'click', function(r){
const eTo = '#'===e.dataset.fPreviewTo[0]
? document.querySelector(e.dataset.fPreviewTo)
: methodNamespace[e.dataset.fPreviewTo],
eFrom = '#'===e.dataset.fPreviewFrom[0]
? document.querySelector(e.dataset.fPreviewFrom)
: methodNamespace[e.dataset.fPreviewFrom],
asText = +(e.dataset.fPreviewAsText || 0);
eTo.textContent = "Fetching preview...";
methodNamespace[e.dataset.fPreviewVia](
(eFrom instanceof Function ? eFrom.call(methodNamespace) : eFrom.value),
function(r){
if(eTo instanceof Function) eTo.call(methodNamespace, r||'');
else if(!r){
dom.clearElement(eTo);
}else if(asText){
eTo.textContent = r;
}else{
dom.parseHtml(dom.clearElement(eTo), r);
}
}
);
}, false
);
});
return this;
}/*F.connectPagePreviewers()*/;
return F.dom = dom;
})(window.fossil);
|