|
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
|
|