Fossil SCM

fossil-scm / src / fossil.dom.js
Blame History Raw 1025 lines
1
"use strict";
2
(function(F/*fossil object*/){
3
/**
4
A collection of HTML DOM utilities to simplify, a bit, using the
5
DOM API. It is focused on manipulation of the DOM, but one of its
6
core mantras is "No innerHTML." Using innerHTML in this code, in
7
particular assigning to it, is absolutely verboten.
8
*/
9
const argsToArray = (a)=>Array.prototype.slice.call(a,0);
10
const isArray = (v)=>v instanceof Array;
11
12
const dom = {
13
create: function(elemType){
14
return document.createElement(elemType);
15
},
16
createElemFactory: function(eType){
17
return function(){
18
return document.createElement(eType);
19
};
20
},
21
remove: function(e){
22
if(e?.forEach){
23
e.forEach(
24
(x)=>x?.parentNode?.removeChild(x)
25
);
26
}else{
27
e?.parentNode?.removeChild(e);
28
}
29
return e;
30
},
31
/**
32
Removes all child DOM elements from the given element
33
and returns that element.
34
35
If e has a forEach method (is an array or DOM element
36
collection), this function instead clears each element in the
37
collection. May be passed any number of arguments, each of
38
which must be a DOM element or a container of DOM elements with
39
a forEach() method. Returns its first argument.
40
*/
41
clearElement: function f(e){
42
if(!f.each){
43
f.each = function(e){
44
if(e.forEach){
45
e.forEach((x)=>f(x));
46
return e;
47
}
48
while(e.firstChild) e.removeChild(e.firstChild);
49
};
50
}
51
argsToArray(arguments).forEach(f.each);
52
return arguments[0];
53
},
54
}/* dom object */;
55
56
/**
57
Returns the result of splitting the given str on
58
a run of spaces of (\s*,\s*).
59
*/
60
dom.splitClassList = function f(str){
61
if(!f.rx){
62
f.rx = /(\s+|\s*,\s*)/;
63
}
64
return str ? str.split(f.rx) : [str];
65
};
66
67
dom.div = dom.createElemFactory('div');
68
dom.p = dom.createElemFactory('p');
69
dom.code = dom.createElemFactory('code');
70
dom.pre = dom.createElemFactory('pre');
71
dom.header = dom.createElemFactory('header');
72
dom.footer = dom.createElemFactory('footer');
73
dom.section = dom.createElemFactory('section');
74
dom.span = dom.createElemFactory('span');
75
dom.strong = dom.createElemFactory('strong');
76
dom.em = dom.createElemFactory('em');
77
dom.ins = dom.createElemFactory('ins');
78
dom.del = dom.createElemFactory('del');
79
/**
80
Returns a LABEL element. If passed an argument,
81
it must be an id or an HTMLElement with an id,
82
and that id is set as the 'for' attribute of the
83
label. If passed 2 arguments, the 2nd is text or
84
a DOM element to append to the label.
85
*/
86
dom.label = function(forElem, text){
87
const rc = document.createElement('label');
88
if(forElem){
89
if(forElem instanceof HTMLElement){
90
forElem = this.attr(forElem, 'id');
91
}
92
if(forElem){
93
dom.attr(rc, 'for', forElem);
94
}
95
}
96
if(text) this.append(rc, text);
97
return rc;
98
};
99
/**
100
Returns an IMG element with an optional src
101
attribute value.
102
*/
103
dom.img = function(src){
104
const e = this.create('img');
105
if(src) e.setAttribute('src',src);
106
return e;
107
};
108
/**
109
Creates and returns a new anchor element with the given
110
optional href and label. If label===true then href is used
111
as the label.
112
*/
113
dom.a = function(href,label){
114
const e = this.create('a');
115
if(href) e.setAttribute('href',href);
116
if(label) e.appendChild(dom.text(true===label ? href : label));
117
return e;
118
};
119
dom.hr = dom.createElemFactory('hr');
120
dom.br = dom.createElemFactory('br');
121
/** Returns a new TEXT node which contains the text of all of the
122
arguments appended together. */
123
dom.text = function(/*...*/){
124
return document.createTextNode(argsToArray(arguments).join(''));
125
};
126
/** Returns a new Button element with the given optional
127
label and on-click event listener function. */
128
dom.button = function(label,callback){
129
const b = this.create('button');
130
if(label) b.appendChild(this.text(label));
131
if('function' === typeof callback){
132
b.addEventListener('click', callback, false);
133
}
134
return b;
135
};
136
/**
137
Returns a TEXTAREA element.
138
139
Usages:
140
141
([boolean readonly = false])
142
(non-boolean rows[,cols[,readonly=false]])
143
144
Each of the rows/cols/readonly attributes is only set if it is
145
truthy.
146
*/
147
dom.textarea = function(){
148
const rc = this.create('textarea');
149
let rows, cols, readonly;
150
if(1===arguments.length){
151
if('boolean'===typeof arguments[0]){
152
readonly = !!arguments[0];
153
}else{
154
rows = arguments[0];
155
}
156
}else if(arguments.length){
157
rows = arguments[0];
158
cols = arguments[1];
159
readonly = arguments[2];
160
}
161
if(rows) rc.setAttribute('rows',rows);
162
if(cols) rc.setAttribute('cols', cols);
163
if(readonly) rc.setAttribute('readonly', true);
164
return rc;
165
};
166
167
/**
168
Returns a new SELECT element.
169
*/
170
dom.select = dom.createElemFactory('select');
171
172
/**
173
Returns an OPTION element with the given value and label text
174
(which defaults to the value).
175
176
Usage:
177
178
(value[, label])
179
(selectElement [,value [,label = value]])
180
181
Any forms taking a SELECT element append the new element to the
182
given SELECT element.
183
184
If any label is falsy and the value is not then the value is used
185
as the label. A non-falsy label value may have any type suitable
186
for passing as the 2nd argument to dom.append().
187
188
If the value has the undefined value then it is NOT assigned as
189
the option element's value and no label is set unless it has a
190
non-undefined value.
191
*/
192
dom.option = function(value,label){
193
const a = arguments;
194
var sel;
195
if(1==a.length){
196
if(a[0] instanceof HTMLElement){
197
sel = a[0];
198
}else{
199
value = a[0];
200
}
201
}else if(2==a.length){
202
if(a[0] instanceof HTMLElement){
203
sel = a[0];
204
value = a[1];
205
}else{
206
value = a[0];
207
label = a[1];
208
}
209
}
210
else if(3===a.length){
211
sel = a[0];
212
value = a[1];
213
label = a[2];
214
}
215
const o = this.create('option');
216
if(undefined !== value){
217
o.value = value;
218
this.append(o, this.text(label || value));
219
}else if(undefined !== label){
220
this.append(o, label);
221
}
222
if(sel) this.append(sel, o);
223
return o;
224
};
225
dom.h = function(level){
226
return this.create('h'+level);
227
};
228
dom.ul = dom.createElemFactory('ul');
229
/**
230
Creates and returns a new LI element, appending it to the
231
given parent argument if it is provided.
232
*/
233
dom.li = function(parent){
234
const li = this.create('li');
235
if(parent) parent.appendChild(li);
236
return li;
237
};
238
239
/**
240
Returns a function which creates a new DOM element of the
241
given type and accepts an optional parent DOM element
242
argument. If the function's argument is truthy, the new
243
child element is appended to the given parent element.
244
Returns the new child element.
245
*/
246
dom.createElemFactoryWithOptionalParent = function(childType){
247
return function(parent){
248
const e = this.create(childType);
249
if(parent) parent.appendChild(e);
250
return e;
251
};
252
};
253
254
dom.table = dom.createElemFactory('table');
255
dom.thead = dom.createElemFactoryWithOptionalParent('thead');
256
dom.tbody = dom.createElemFactoryWithOptionalParent('tbody');
257
dom.tfoot = dom.createElemFactoryWithOptionalParent('tfoot');
258
dom.tr = dom.createElemFactoryWithOptionalParent('tr');
259
dom.td = dom.createElemFactoryWithOptionalParent('td');
260
dom.th = dom.createElemFactoryWithOptionalParent('th');
261
262
/**
263
Creates and returns a FIELDSET element, optionaly with a LEGEND
264
element added to it. If legendText is an HTMLElement then it is
265
assumed to be a LEGEND and is appended as-is, else it is assumed
266
(if truthy) to be a value suitable for passing to
267
dom.append(aLegendElement,...).
268
*/
269
dom.fieldset = function(legendText){
270
const fs = this.create('fieldset');
271
if(legendText){
272
this.append(
273
fs,
274
(legendText instanceof HTMLElement)
275
? legendText
276
: this.append(this.legend(legendText))
277
);
278
}
279
return fs;
280
};
281
/**
282
Returns a new LEGEND legend element. The given argument, if
283
not falsy, is append()ed to the element (so it may be a string
284
or DOM element.
285
*/
286
dom.legend = function(legendText){
287
const rc = this.create('legend');
288
if(legendText) this.append(rc, legendText);
289
return rc;
290
};
291
292
/**
293
Appends each argument after the first to the first argument
294
(a DOM node) and returns the first argument.
295
296
- If an argument is a string or number, it is transformed
297
into a text node.
298
299
- If an argument is an array or has a forEach member, this
300
function appends each element in that list to the target
301
by calling its forEach() method to pass it (recursively)
302
to this function.
303
304
- Else the argument assumed to be of a type legal
305
to pass to parent.appendChild().
306
*/
307
dom.append = function f(parent/*,...*/){
308
const a = argsToArray(arguments);
309
a.shift();
310
for(let i in a) {
311
var e = a[i];
312
if(isArray(e) || e.forEach){
313
e.forEach((x)=>f.call(this, parent,x));
314
continue;
315
}
316
if('string'===typeof e
317
|| 'number'===typeof e
318
|| 'boolean'===typeof e
319
|| e instanceof Error) e = this.text(e);
320
parent.appendChild(e);
321
}
322
return parent;
323
};
324
325
dom.input = function(type){
326
return this.attr(this.create('input'), 'type', type);
327
};
328
/**
329
Returns a new CHECKBOX input element.
330
331
Usages:
332
333
([boolean checked = false])
334
(non-boolean value [,boolean checked])
335
*/
336
dom.checkbox = function(value, checked){
337
const rc = this.input('checkbox');
338
if(1===arguments.length && 'boolean'===typeof value){
339
checked = !!value;
340
value = undefined;
341
}
342
if(undefined !== value) rc.value = value;
343
if(!!checked) rc.checked = true;
344
return rc;
345
};
346
/**
347
Returns a new RADIO input element.
348
349
([boolean checked = false])
350
(string name [,boolean checked])
351
(string name, non-boolean value [,boolean checked])
352
*/
353
dom.radio = function(){
354
const rc = this.input('radio');
355
let name, value, checked;
356
if(1===arguments.length && 'boolean'===typeof name){
357
checked = arguments[0];
358
name = value = undefined;
359
}else if(2===arguments.length){
360
name = arguments[0];
361
if('boolean'===typeof arguments[1]){
362
checked = arguments[1];
363
}else{
364
value = arguments[1];
365
checked = undefined;
366
}
367
}else if(arguments.length){
368
name = arguments[0];
369
value = arguments[1];
370
checked = arguments[2];
371
}
372
if(name) this.attr(rc, 'name', name);
373
if(undefined!==value) rc.value = value;
374
if(!!checked) rc.checked = true;
375
return rc;
376
};
377
378
/**
379
Internal impl for addClass(), removeClass().
380
*/
381
const domAddRemoveClass = function f(action,e){
382
if(!f.rxSPlus){
383
f.rxSPlus = /\s+/;
384
f.applyAction = function(e,a,v){
385
if(!e || !v
386
/*silently skip empty strings/falsy
387
values, for usage convenience*/) return;
388
else if(e.forEach){
389
e.forEach((E)=>E.classList[a](v));
390
}else{
391
e.classList[a](v);
392
}
393
};
394
}
395
var i = 2, n = arguments.length;
396
for( ; i < n; ++i ){
397
let c = arguments[i];
398
if(!c) continue;
399
else if(isArray(c) ||
400
('string'===typeof c
401
&& c.indexOf(' ')>=0
402
&& (c = c.split(f.rxSPlus)))
403
|| c.forEach
404
){
405
c.forEach((k)=>k ? f.applyAction(e, action, k) : false);
406
// ^^^ we could arguably call f(action,e,k) to recursively
407
// apply constructs like ['foo bar'] or [['foo'],['bar baz']].
408
}else if(c){
409
f.applyAction(e, action, c);
410
}
411
}
412
return e;
413
};
414
415
/**
416
Adds one or more CSS classes to one or more DOM elements.
417
418
The first argument is a target DOM element or a list type of such elements
419
which has a forEach() method. Each argument
420
after the first may be a string or array of strings. Each
421
string may contain spaces, in which case it is treated as a
422
list of CSS classes.
423
424
Returns e.
425
*/
426
dom.addClass = function(e,c){
427
const a = argsToArray(arguments);
428
a.unshift('add');
429
return domAddRemoveClass.apply(this, a);
430
};
431
/**
432
The 'remove' counterpart of the addClass() method, taking
433
the same arguments and returning the same thing.
434
*/
435
dom.removeClass = function(e,c){
436
const a = argsToArray(arguments);
437
a.unshift('remove');
438
return domAddRemoveClass.apply(this, a);
439
};
440
441
/**
442
Toggles CSS class c on e (a single element for forEach-capable
443
collection of elements). Returns its first argument.
444
*/
445
dom.toggleClass = function f(e,c){
446
if(e.forEach){
447
e.forEach((x)=>x.classList.toggle(c));
448
}else{
449
e.classList.toggle(c);
450
}
451
return e;
452
};
453
454
/**
455
Returns true if DOM element e contains CSS class c, else
456
false.
457
*/
458
dom.hasClass = function(e,c){
459
return (e && e.classList) ? e.classList.contains(c) : false;
460
};
461
462
/**
463
Each argument after the first may be a single DOM element or a
464
container of them with a forEach() method. All such elements are
465
appended, in the given order, to the dest element using
466
dom.append(dest,theElement). Thus the 2nd and susequent arguments
467
may be any type supported as the 2nd argument to that function.
468
469
Returns dest.
470
*/
471
dom.moveTo = function(dest,e){
472
const n = arguments.length;
473
var i = 1;
474
const self = this;
475
for( ; i < n; ++i ){
476
e = arguments[i];
477
this.append(dest, e);
478
}
479
return dest;
480
};
481
/**
482
Each argument after the first may be a single DOM element
483
or a container of them with a forEach() method. For each
484
DOM element argument, all children of that DOM element
485
are moved to dest (via appendChild()). For each list argument,
486
each entry in the list is assumed to be a DOM element and is
487
appended to dest.
488
489
dest may be an Array, in which case each child is pushed
490
into the array and removed from its current parent element.
491
492
All children are appended in the given order.
493
494
Returns dest.
495
*/
496
dom.moveChildrenTo = function f(dest,e){
497
if(!f.mv){
498
f.mv = function(d,v){
499
if(d instanceof Array){
500
d.push(v);
501
if(v.parentNode) v.parentNode.removeChild(v);
502
}
503
else d.appendChild(v);
504
};
505
}
506
const n = arguments.length;
507
var i = 1;
508
for( ; i < n; ++i ){
509
e = arguments[i];
510
if(!e){
511
console.warn("Achtung: dom.moveChildrenTo() passed a falsy value at argument",i,"of",
512
arguments,arguments[i]);
513
continue;
514
}
515
if(e.forEach){
516
e.forEach((x)=>f.mv(dest, x));
517
}else{
518
while(e.firstChild){
519
f.mv(dest, e.firstChild);
520
}
521
}
522
}
523
return dest;
524
};
525
526
/**
527
Adds each argument (DOM Elements) after the first to the
528
DOM immediately before the first argument (in the order
529
provided), then removes the first argument from the DOM.
530
Returns void.
531
532
If any argument beyond the first has a forEach method, that
533
method is used to recursively insert the collection's
534
contents before removing the first argument from the DOM.
535
*/
536
dom.replaceNode = function f(old,nu){
537
var i = 1, n = arguments.length;
538
++f.counter;
539
try {
540
for( ; i < n; ++i ){
541
const e = arguments[i];
542
if(e.forEach){
543
e.forEach((x)=>f.call(this,old,e));
544
continue;
545
}
546
old.parentNode.insertBefore(e, old);
547
}
548
}
549
finally{
550
--f.counter;
551
}
552
if(!f.counter){
553
old.parentNode.removeChild(old);
554
}
555
};
556
dom.replaceNode.counter = 0;
557
/**
558
Two args == getter: (e,key), returns value
559
560
Three or more == setter: (e,key,val[...,keyN,valN]), returns
561
e. If val===null or val===undefined then the attribute is
562
removed. If (e) has a forEach method then this routine is applied
563
to each element of that collection via that method. Each pair of
564
keys/values is applied to all elements designated by the first
565
argument.
566
*/
567
dom.attr = function f(e){
568
if(2===arguments.length) return e.getAttribute(arguments[1]);
569
const a = argsToArray(arguments);
570
if(e.forEach){ /* Apply to all elements in the collection */
571
e.forEach(function(x){
572
a[0] = x;
573
f.apply(f,a);
574
});
575
return e;
576
}
577
a.shift(/*element(s)*/);
578
while(a.length){
579
const key = a.shift(), val = a.shift();
580
if(null===val || undefined===val){
581
e.removeAttribute(key);
582
}else{
583
e.setAttribute(key,val);
584
}
585
}
586
return e;
587
};
588
589
/* Impl for dom.enable() and dom.disable(). */
590
const enableDisable = function f(enable){
591
var i = 1, n = arguments.length;
592
for( ; i < n; ++i ){
593
let e = arguments[i];
594
if(e.forEach){
595
e.forEach((x)=>f(enable,x));
596
}else{
597
e.disabled = !enable;
598
}
599
}
600
return arguments[1];
601
};
602
603
/**
604
Enables (by removing the "disabled" attribute) each element
605
(HTML DOM element or a collection with a forEach method)
606
and returns the first argument.
607
*/
608
dom.enable = function(e){
609
const args = argsToArray(arguments);
610
args.unshift(true);
611
return enableDisable.apply(this,args);
612
};
613
/**
614
Disables (by setting the "disabled" attribute) each element
615
(HTML DOM element or a collection with a forEach method)
616
and returns the first argument.
617
*/
618
dom.disable = function(e){
619
const args = argsToArray(arguments);
620
args.unshift(false);
621
return enableDisable.apply(this,args);
622
};
623
624
/**
625
A proxy for document.querySelector() which throws if
626
selection x is not found. It may optionally be passed an
627
"origin" object as its 2nd argument, which restricts the
628
search to that branch of the tree.
629
*/
630
dom.selectOne = function(x,origin){
631
var src = origin || document,
632
e = src.querySelector(x);
633
if(!e){
634
e = new Error("Cannot find DOM element: "+x);
635
console.error(e, src);
636
throw e;
637
}
638
return e;
639
};
640
641
/**
642
"Blinks" the given element a single time for the given number of
643
milliseconds, defaulting (if the 2nd argument is falsy or not a
644
number) to flashOnce.defaultTimeMs. If a 3rd argument is passed
645
in, it must be a function, and it gets called at the end of the
646
asynchronous flashing processes.
647
648
This will only activate once per element during that timeframe -
649
further calls will become no-ops until the blink is
650
completed. This routine adds a dataset member to the element for
651
the duration of the blink, to allow it to block multiple blinks.
652
653
If passed 2 arguments and the 2nd is a function, it behaves as if
654
it were called as (arg1, undefined, arg2).
655
656
Returns e, noting that the flash itself is asynchronous and may
657
still be running, or not yet started, when this function returns.
658
*/
659
dom.flashOnce = function f(e,howLongMs,afterFlashCallback){
660
if(e.dataset.isBlinking){
661
return;
662
}
663
if(2===arguments.length && 'function' ===typeof howLongMs){
664
afterFlashCallback = howLongMs;
665
howLongMs = f.defaultTimeMs;
666
}
667
if(!howLongMs || 'number'!==typeof howLongMs){
668
howLongMs = f.defaultTimeMs;
669
}
670
e.dataset.isBlinking = true;
671
const transition = e.style.transition;
672
e.style.transition = "opacity "+howLongMs+"ms ease-in-out";
673
const opacity = e.style.opacity;
674
e.style.opacity = 0;
675
setTimeout(function(){
676
e.style.transition = transition;
677
e.style.opacity = opacity;
678
delete e.dataset.isBlinking;
679
if(afterFlashCallback) afterFlashCallback();
680
}, howLongMs);
681
return e;
682
};
683
dom.flashOnce.defaultTimeMs = 400;
684
/**
685
A DOM event handler which simply passes event.target
686
to dom.flashOnce().
687
*/
688
dom.flashOnce.eventHandler = (event)=>dom.flashOnce(event.target)
689
690
/**
691
This variant of flashOnce() flashes the element e n times
692
for a duration of howLongMs milliseconds then calls the
693
afterFlashCallback() callback. It may also be called with 2
694
or 3 arguments, in which case:
695
696
2 arguments: default flash time and no callback.
697
698
3 arguments: 3rd may be a flash delay time or a callback
699
function.
700
701
Returns this object but the flashing is asynchronous.
702
703
Depending on system load and related factors, a multi-flash
704
animation might stutter and look suboptimal.
705
*/
706
dom.flashNTimes = function(e,n,howLongMs,afterFlashCallback){
707
const args = argsToArray(arguments);
708
args.splice(1,1);
709
if(arguments.length===3 && 'function'===typeof howLongMs){
710
afterFlashCallback = howLongMs;
711
howLongMs = args[1] = this.flashOnce.defaultTimeMs;
712
}else if(arguments.length<3){
713
args[1] = this.flashOnce.defaultTimeMs;
714
}
715
n = +n;
716
const self = this;
717
const cb = args[2] = function f(){
718
if(--n){
719
setTimeout(()=>self.flashOnce(e, howLongMs, f),
720
howLongMs+(howLongMs*0.1)/*we need a slight gap here*/);
721
}else if(afterFlashCallback){
722
afterFlashCallback();
723
}
724
};
725
this.flashOnce.apply(this, args);
726
return this;
727
};
728
729
/**
730
Adds the given CSS class or array of CSS classes to the given
731
element or forEach-capable list of elements for howLongMs, then
732
removes it. If afterCallack is a function, it is called after the
733
CSS class is removed from all elements. If called with 3
734
arguments and the 3rd is a function, the 3rd is treated as a
735
callback and the default time (addClassBriefly.defaultTimeMs) is
736
used. If called with only 2 arguments, a time of
737
addClassBriefly.defaultTimeMs is used.
738
739
Passing a value of 0 for howLongMs causes the default value
740
to be applied.
741
742
Returns this object but the CSS removal is asynchronous.
743
*/
744
dom.addClassBriefly = function f(e, className, howLongMs, afterCallback){
745
if(arguments.length<4 && 'function'===typeof howLongMs){
746
afterCallback = howLongMs;
747
howLongMs = f.defaultTimeMs;
748
}else if(arguments.length<3 || !+howLongMs){
749
howLongMs = f.defaultTimeMs;
750
}
751
this.addClass(e, className);
752
setTimeout(function(){
753
dom.removeClass(e, className);
754
if(afterCallback) afterCallback();
755
}, howLongMs);
756
return this;
757
};
758
dom.addClassBriefly.defaultTimeMs = 1000;
759
760
/**
761
Attempts to copy the given text to the system clipboard. Returns
762
true if it succeeds, else false.
763
*/
764
dom.copyTextToClipboard = function(text){
765
if( window.clipboardData && window.clipboardData.setData ){
766
window.clipboardData.setData('Text',text);
767
return true;
768
}else{
769
const x = document.createElement("textarea");
770
x.style.position = 'fixed';
771
x.value = text;
772
document.body.appendChild(x);
773
x.select();
774
var rc;
775
try{
776
document.execCommand('copy');
777
rc = true;
778
}catch(err){
779
rc = false;
780
}finally{
781
document.body.removeChild(x);
782
}
783
return rc;
784
}
785
};
786
787
/**
788
Copies all properties from the 2nd argument (a plain object) into
789
the style member of the first argument (DOM element or a
790
forEach-capable list of elements). If the 2nd argument is falsy
791
or empty, this is a no-op.
792
793
Returns its first argument.
794
*/
795
dom.copyStyle = function f(e, style){
796
if(e.forEach){
797
e.forEach((x)=>f(x, style));
798
return e;
799
}
800
if(style){
801
let k;
802
for(k in style){
803
if(style.hasOwnProperty(k)) e.style[k] = style[k];
804
}
805
}
806
return e;
807
};
808
809
/**
810
Given a DOM element, this routine measures its "effective
811
height", which is the bounding top/bottom range of this element
812
and all of its children, recursively. For some DOM structure
813
cases, a parent may have a reported height of 0 even though
814
children have non-0 sizes.
815
816
Returns 0 if !e or if the element really has no height.
817
*/
818
dom.effectiveHeight = function f(e){
819
if(!e) return 0;
820
if(!f.measure){
821
f.measure = function callee(e, depth){
822
if(!e) return;
823
const m = e.getBoundingClientRect();
824
if(0===depth){
825
callee.top = m.top;
826
callee.bottom = m.bottom;
827
}else{
828
callee.top = m.top ? Math.min(callee.top, m.top) : callee.top;
829
callee.bottom = Math.max(callee.bottom, m.bottom);
830
}
831
Array.prototype.forEach.call(e.children,(e)=>callee(e,depth+1));
832
if(0===depth){
833
//console.debug("measure() height:",e.className, callee.top, callee.bottom, (callee.bottom - callee.top));
834
f.extra += callee.bottom - callee.top;
835
}
836
return f.extra;
837
};
838
}
839
f.extra = 0;
840
f.measure(e,0);
841
return f.extra;
842
};
843
844
/**
845
Parses a string as HTML.
846
847
Usages:
848
849
Array parseHtml(htmlString)
850
DOMElement parseHtml(DOMElement target, htmlString)
851
852
The first form parses the string as HTML and returns an Array of
853
all elements parsed from it. If string is falsy then it returns
854
an empty array.
855
856
The second form parses the HTML string and appends all elements
857
to the given target element using dom.append(), then returns the
858
first argument.
859
860
Caveats:
861
862
- It expects a partial HTML document as input, not a full HTML
863
document with a HEAD and BODY tags. Because of how DOMParser
864
works, only children of the parsed document's (virtual) body are
865
acknowledged by this routine.
866
*/
867
dom.parseHtml = function(){
868
let childs, string, tgt;
869
if(1===arguments.length){
870
string = arguments[0];
871
}else if(2==arguments.length){
872
tgt = arguments[0];
873
string = arguments[1];
874
}
875
if(string){
876
const newNode = new DOMParser().parseFromString(string, 'text/html');
877
childs = newNode.documentElement.querySelector('body');
878
childs = childs ? Array.prototype.slice.call(childs.childNodes, 0) : [];
879
/* ^^^ we need a clone of the list because reparenting them
880
modifies a NodeList they're in. */
881
}else{
882
childs = [];
883
}
884
return tgt ? this.moveTo(tgt, childs) : childs;
885
};
886
887
/**
888
Sets up pseudo-automatic content preview handling between a
889
source element (typically a TEXTAREA) and a target rendering
890
element (typically a DIV). The selector argument must be one of:
891
892
- A single DOM element
893
- A collection of DOM elements with a forEach method.
894
- A CSS selector
895
896
Each element in the collection must have the following data
897
attributes:
898
899
- data-f-preview-from: is either a DOM element id, WITH a leading
900
'#' prefix, or the name of a method (see below). If it's an ID,
901
the DOM element must support .value to get the content.
902
903
- data-f-preview-to: the DOM element id of the target "previewer"
904
element, WITH a leading '#', or the name of a method (see below).
905
906
- data-f-preview-via: the name of a method (see below).
907
908
- OPTIONAL data-f-preview-as-text: a numeric value. Explained below.
909
910
Each element gets a click handler added to it which does the
911
following:
912
913
1) Reads the content from its data-f-preview-from element or, if
914
that property refers to a method, calls the method without
915
arguments and uses its result as the content.
916
917
2) Passes the content to
918
methodNamespace[f-data-post-via](content,callback). f-data-post-via
919
is responsible for submitting the preview HTTP request, including
920
any parameters the request might require. When the response
921
arrives, it must pass the content of the response to its 2nd
922
argument, an auto-generated callback installed by this mechanism
923
which...
924
925
3) Assigns the response text to the data-f-preview-to element or
926
passes it to the function
927
methodNamespace[f-data-preview-to](content), as appropriate. If
928
data-f-preview-to is a DOM element and data-f-preview-as-text is
929
'0' (the default) then the target elements contents are replaced
930
with the given content as HTML, else the content is assigned to
931
the target's textContent property. (Note that this routine uses
932
DOMParser, rather than assignment to innerHTML, to apply
933
HTML-format content.)
934
935
The methodNamespace (2nd argument) defaults to fossil.page, and
936
any method-name data properties, e.g. data-f-preview-via and
937
potentially data-f-preview-from/to, must be a single method name,
938
not a property-access-style string. e.g. "myPreview" is legal but
939
"foo.myPreview" is not (unless, of course, the method is actually
940
named "foo.myPreview" (which is legal but would be
941
unconventional)). All such methods are called with
942
methodNamespace as their "this".
943
944
An example...
945
946
First an input button:
947
948
<button id='test-preview-connector'
949
data-f-preview-from='#fileedit-content-editor' // elem ID or method name
950
data-f-preview-via='myPreview' // method name
951
data-f-preview-to='#fileedit-tab-preview-wrapper' // elem ID or method name
952
>Preview update</button>
953
954
And a sample data-f-preview-via method:
955
956
fossil.page.myPreview = function(content,callback){
957
const fd = new FormData();
958
fd.append('foo', ...);
959
fossil.fetch('preview_forumpost',{
960
payload: fd,
961
onload: callback,
962
onerror: (e)=>{ // only if app-specific handling is needed
963
fossil.fetch.onerror(e); // default impl
964
... any app-specific error reporting ...
965
}
966
});
967
};
968
969
Then connect the parts with:
970
971
fossil.connectPagePreviewers('#test-preview-connector');
972
973
Note that the data-f-preview-from, data-f-preview-via, and
974
data-f-preview-to selector are not resolved until the button is
975
actually clicked, so they need not exist in the DOM at the
976
instant when the connection is set up, so long as they can be
977
resolved when the preview-refreshing element is clicked.
978
979
Maintenance reminder: this method is not strictly part of
980
fossil.dom, but is in its file because it needs access to
981
dom.parseHtml() to avoid an innerHTML assignment and all code
982
which uses this routine also needs fossil.dom.
983
*/
984
F.connectPagePreviewers = function f(selector,methodNamespace){
985
if('string'===typeof selector){
986
selector = document.querySelectorAll(selector);
987
}else if(!selector.forEach){
988
selector = [selector];
989
}
990
if(!methodNamespace){
991
methodNamespace = F.page;
992
}
993
selector.forEach(function(e){
994
e.addEventListener(
995
'click', function(r){
996
const eTo = '#'===e.dataset.fPreviewTo[0]
997
? document.querySelector(e.dataset.fPreviewTo)
998
: methodNamespace[e.dataset.fPreviewTo],
999
eFrom = '#'===e.dataset.fPreviewFrom[0]
1000
? document.querySelector(e.dataset.fPreviewFrom)
1001
: methodNamespace[e.dataset.fPreviewFrom],
1002
asText = +(e.dataset.fPreviewAsText || 0);
1003
eTo.textContent = "Fetching preview...";
1004
methodNamespace[e.dataset.fPreviewVia](
1005
(eFrom instanceof Function ? eFrom.call(methodNamespace) : eFrom.value),
1006
function(r){
1007
if(eTo instanceof Function) eTo.call(methodNamespace, r||'');
1008
else if(!r){
1009
dom.clearElement(eTo);
1010
}else if(asText){
1011
eTo.textContent = r;
1012
}else{
1013
dom.parseHtml(dom.clearElement(eTo), r);
1014
}
1015
}
1016
);
1017
}, false
1018
);
1019
});
1020
return this;
1021
}/*F.connectPagePreviewers()*/;
1022
1023
return F.dom = dom;
1024
})(window.fossil);
1025

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button