pixl-xyapp 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/js/tools.js ADDED
@@ -0,0 +1,1212 @@
1
+ ////
2
+ // Joe's Misc JavaScript Tools
3
+ // Copyright (c) 2004 - 2025 Joseph Huckaby
4
+ // Released under the Sustainable Use License
5
+ ////
6
+
7
+ var months = [
8
+ [ 1, 'January' ], [ 2, 'February' ], [ 3, 'March' ], [ 4, 'April' ],
9
+ [ 5, 'May' ], [ 6, 'June' ], [ 7, 'July' ], [ 8, 'August' ],
10
+ [ 9, 'September' ], [ 10, 'October' ], [ 11, 'November' ],
11
+ [ 12, 'December' ]
12
+ ];
13
+
14
+ function parse_query_string(url) {
15
+ // parse query string into key/value pairs and return as object
16
+ var query = {};
17
+ url.replace(/^.*\?/, '').replace(/([^\=]+)\=([^\&]*)\&?/g, function(match, key, value) {
18
+ query[key] = decodeURIComponent(value);
19
+ if (query[key].match(/^\-?\d+$/)) query[key] = parseInt(query[key]);
20
+ else if (query[key].match(/^\-?\d*\.\d+$/)) query[key] = parseFloat(query[key]);
21
+ return '';
22
+ } );
23
+ return query;
24
+ };
25
+
26
+ function compose_query_string(queryObj) {
27
+ // compose key/value pairs into query string
28
+ // supports duplicate keys (i.e. arrays)
29
+ var qs = '';
30
+ for (var key in queryObj) {
31
+ var values = always_array(queryObj[key]);
32
+ for (var idx = 0, len = values.length; idx < len; idx++) {
33
+ qs += (qs.length ? '&' : '?') + encodeURIComponent(key) + '=' + encodeURIComponent(values[idx]);
34
+ }
35
+ }
36
+ return qs;
37
+ }
38
+
39
+ function get_text_from_bytes_dash(bytes) {
40
+ // get super-appreviated bytes-to-text, for dash widgets
41
+ return get_text_from_bytes(bytes, 1).replace(/\s+/g, '').replace(/bytes/, 'b');
42
+ };
43
+
44
+ function get_text_from_bytes(bytes, precision) {
45
+ // convert raw bytes to english-readable format
46
+ // set precision to 1 for ints, 10 for 1 decimal point (default), 100 for 2, etc.
47
+ bytes = Math.floor(bytes);
48
+ if (!precision) precision = 10;
49
+
50
+ if (bytes >= 1024) {
51
+ bytes = Math.floor( (bytes / 1024) * precision ) / precision;
52
+ if (bytes >= 1024) {
53
+ bytes = Math.floor( (bytes / 1024) * precision ) / precision;
54
+ if (bytes >= 1024) {
55
+ bytes = Math.floor( (bytes / 1024) * precision ) / precision;
56
+ if (bytes >= 1024) {
57
+ bytes = Math.floor( (bytes / 1024) * precision ) / precision;
58
+ return bytes + ' TB';
59
+ }
60
+ else return bytes + ' GB';
61
+ }
62
+ else return bytes + ' MB';
63
+ }
64
+ else return bytes + ' K';
65
+ }
66
+ else return bytes + pluralize(' byte', bytes);
67
+ };
68
+
69
+ function get_bytes_from_text(text) {
70
+ // parse text into raw bytes, e.g. "1 K" --> 1024
71
+ if (text.toString().match(/^\d+$/)) return parseInt(text); // already in bytes
72
+ var multipliers = {
73
+ b: 1,
74
+ k: 1024,
75
+ m: 1024 * 1024,
76
+ g: 1024 * 1024 * 1024,
77
+ t: 1024 * 1024 * 1024 * 1024
78
+ };
79
+ var bytes = 0;
80
+ text = text.toString().replace(/([\d\.]+)\s*(\w)\w*\s*/g, function(m_all, m_g1, m_g2) {
81
+ var mult = multipliers[ m_g2.toLowerCase() ] || 0;
82
+ bytes += (parseFloat(m_g1) * mult);
83
+ return '';
84
+ } );
85
+ return Math.floor(bytes);
86
+ };
87
+
88
+ function ucfirst(text) {
89
+ // capitalize first character only, lower-case rest
90
+ return text.substring(0, 1).toUpperCase() + text.substring(1, text.length).toLowerCase();
91
+ }
92
+
93
+ function commify(number) {
94
+ // add localized commas to integer, e.g. 1,234,567 for US
95
+ return (new Intl.NumberFormat()).format(number || 0);
96
+ }
97
+
98
+ function short_float(value, places) {
99
+ // Shorten floating-point decimal to N places max
100
+ if (!places) places = 2;
101
+ var mult = Math.pow(10, places);
102
+ return( Math.floor(parseFloat(value || 0) * mult) / mult );
103
+ }
104
+
105
+ function pct(count, max, floor) {
106
+ // Return formatted percentage given a number along a sliding scale from 0 to 'max'
107
+ var pct = (count * 100) / (max || 1);
108
+ if (!pct.toString().match(/^\d+(\.\d+)?$/)) { pct = 0; }
109
+ return '' + (floor ? Math.floor(pct) : short_float(pct)) + '%';
110
+ };
111
+
112
+ function crammify(str) {
113
+ // strip non-alpha and lower-case
114
+ return ('' + str).replace(/\W+/g, '').toLowerCase();
115
+ };
116
+
117
+ function get_text_from_seconds(sec, abbrev, no_secondary) {
118
+ // convert raw seconds to human-readable relative time
119
+ var neg = '';
120
+ sec = parseInt(sec, 10);
121
+ if (sec<0) { sec =- sec; neg = '-'; }
122
+
123
+ var p_text = abbrev ? "sec" : "second";
124
+ var p_amt = sec;
125
+ var s_text = "";
126
+ var s_amt = 0;
127
+
128
+ if (sec > 59) {
129
+ var min = parseInt(sec / 60, 10);
130
+ sec = sec % 60;
131
+ s_text = abbrev ? "sec" : "second";
132
+ s_amt = sec;
133
+ p_text = abbrev ? "min" : "minute";
134
+ p_amt = min;
135
+
136
+ if (min > 59) {
137
+ var hour = parseInt(min / 60, 10);
138
+ min = min % 60;
139
+ s_text = abbrev ? "min" : "minute";
140
+ s_amt = min;
141
+ p_text = abbrev ? "hr" : "hour";
142
+ p_amt = hour;
143
+
144
+ if (hour > 23) {
145
+ var day = parseInt(hour / 24, 10);
146
+ hour = hour % 24;
147
+ s_text = abbrev ? "hr" : "hour";
148
+ s_amt = hour;
149
+ p_text = "day";
150
+ p_amt = day;
151
+
152
+ if (day > 29) {
153
+ var month = parseInt(day / 30, 10);
154
+ s_text = "day";
155
+ s_amt = day % 30;
156
+ p_text = abbrev ? "mon" : "month";
157
+ p_amt = month;
158
+
159
+ if (day >= 365) {
160
+ var year = parseInt(day / 365, 10);
161
+ month = month % 12;
162
+ s_text = abbrev ? "mon" : "month";
163
+ s_amt = month;
164
+ p_text = abbrev ? "yr" : "year";
165
+ p_amt = year;
166
+ } // day>=365
167
+ } // day>29
168
+ } // hour>23
169
+ } // min>59
170
+ } // sec>59
171
+
172
+ var text = p_amt + "&nbsp;" + p_text;
173
+ if ((p_amt != 1) && !abbrev) text += "s";
174
+ if (s_amt && !no_secondary) {
175
+ text += ", " + s_amt + "&nbsp;" + s_text;
176
+ if ((s_amt != 1) && !abbrev) text += "s";
177
+ }
178
+
179
+ return(neg + text);
180
+ }
181
+
182
+ function get_text_from_seconds_round(sec, abbrev) {
183
+ // convert raw seconds to human-readable relative time
184
+ // round to nearest instead of floor
185
+ var neg = '';
186
+ sec = Math.round(sec);
187
+ if (sec < 0) { sec =- sec; neg = '-'; }
188
+
189
+ var suffix = abbrev ? "sec" : "second";
190
+ var amt = sec;
191
+
192
+ if (sec > 59) {
193
+ var min = Math.round(sec / 60);
194
+ suffix = abbrev ? "min" : "minute";
195
+ amt = min;
196
+
197
+ if (min > 59) {
198
+ var hour = Math.round(min / 60);
199
+ suffix = abbrev ? "hr" : "hour";
200
+ amt = hour;
201
+
202
+ if (hour > 23) {
203
+ var day = Math.round(hour / 24);
204
+ suffix = "day";
205
+ amt = day;
206
+ } // hour>23
207
+ } // min>59
208
+ } // sec>59
209
+
210
+ if (abbrev === 2) suffix = suffix.substring(0, 1);
211
+
212
+ var text = "" + amt + " " + suffix;
213
+ if ((amt != 1) && !abbrev) text += "s";
214
+ if (abbrev === 2) text = text.replace(/\s+/g, '');
215
+
216
+ return(neg + text);
217
+ };
218
+
219
+ function get_text_from_ms_round(ms, abbrev) {
220
+ // convert raw milliseconds to human-readable relative time
221
+ // round to nearest if 1s or above
222
+ if (Math.abs(ms) >= 1000) return get_text_from_seconds_round(ms / 1000, abbrev);
223
+ var neg = '';
224
+ if (ms < 0) { ms =- ms; neg = '-'; }
225
+ var suffix = abbrev ? "ms" : "millisecond";
226
+ var text = "" + ms + " " + suffix;
227
+ if ((ms != 1) && !abbrev) text += "s";
228
+ if (abbrev === 2) text = text.replace(/\s+/g, '');
229
+ return neg + text;
230
+ };
231
+
232
+ function get_seconds_from_text(text) {
233
+ // parse text into raw seconds, e.g. "1 minute" --> 60
234
+ if (text.toString().match(/^\d+$/)) return parseInt(text); // already in seconds
235
+ var multipliers = {
236
+ s: 1,
237
+ m: 60,
238
+ h: 60 * 60,
239
+ d: 60 * 60 * 24,
240
+ w: 60 * 60 * 24 * 7
241
+ };
242
+ var seconds = 0;
243
+ text = text.toString().replace(/([\d\.]+)\s*(\w)\w*\s*/g, function(m_all, m_g1, m_g2) {
244
+ var mult = multipliers[ m_g2.toLowerCase() ] || 0;
245
+ seconds += (parseFloat(m_g1) * mult);
246
+ return '';
247
+ } );
248
+ return Math.floor(seconds);
249
+ };
250
+
251
+ function get_inner_window_size(dom) {
252
+ // get size of inner window
253
+ if (!dom) dom = window;
254
+ var myWidth = 0, myHeight = 0;
255
+
256
+ if( typeof( dom.innerWidth ) == 'number' ) {
257
+ // Non-IE
258
+ myWidth = dom.innerWidth;
259
+ myHeight = dom.innerHeight;
260
+ }
261
+ else if( dom.document.documentElement && ( dom.document.documentElement.clientWidth || dom.document.documentElement.clientHeight ) ) {
262
+ // IE 6+ in 'standards compliant mode'
263
+ myWidth = dom.document.documentElement.clientWidth;
264
+ myHeight = dom.document.documentElement.clientHeight;
265
+ }
266
+ else if( dom.document.body && ( dom.document.body.clientWidth || dom.document.body.clientHeight ) ) {
267
+ // IE 4 compatible
268
+ myWidth = dom.document.body.clientWidth;
269
+ myHeight = dom.document.body.clientHeight;
270
+ }
271
+ return { width: myWidth, height: myHeight };
272
+ }
273
+
274
+ function get_scroll_xy(dom) {
275
+ // get page scroll X, Y
276
+ if (!dom) dom = window;
277
+ var scrOfX = 0, scrOfY = 0;
278
+ if( typeof( dom.pageYOffset ) == 'number' ) {
279
+ //Netscape compliant
280
+ scrOfY = dom.pageYOffset;
281
+ scrOfX = dom.pageXOffset;
282
+ } else if( dom.document.body && ( dom.document.body.scrollLeft || dom.document.body.scrollTop ) ) {
283
+ //DOM compliant
284
+ scrOfY = dom.document.body.scrollTop;
285
+ scrOfX = dom.document.body.scrollLeft;
286
+ } else if( dom.document.documentElement && ( dom.document.documentElement.scrollLeft || dom.document.documentElement.scrollTop ) ) {
287
+ //IE6 standards compliant mode
288
+ scrOfY = dom.document.documentElement.scrollTop;
289
+ scrOfX = dom.document.documentElement.scrollLeft;
290
+ }
291
+ return { x: scrOfX, y: scrOfY };
292
+ }
293
+
294
+ function get_scroll_max(dom) {
295
+ // get maximum scroll width/height
296
+ if (!dom) dom = window;
297
+ var myWidth = 0, myHeight = 0;
298
+ if (dom.document.body.scrollHeight) {
299
+ myWidth = dom.document.body.scrollWidth;
300
+ myHeight = dom.document.body.scrollHeight;
301
+ }
302
+ else if (dom.document.documentElement.scrollHeight) {
303
+ myWidth = dom.document.documentElement.scrollWidth;
304
+ myHeight = dom.document.documentElement.scrollHeight;
305
+ }
306
+ return { width: myWidth, height: myHeight };
307
+ }
308
+
309
+ function hires_time_now() {
310
+ // return the Epoch seconds for like right now
311
+ var now = new Date();
312
+ return ( now.getTime() / 1000 );
313
+ }
314
+
315
+ function str_value(str) {
316
+ // Get friendly string value for display purposes.
317
+ if (typeof(str) == 'undefined') str = '';
318
+ else if (str === null) str = '';
319
+ return '' + str;
320
+ }
321
+
322
+ function pluralize(word, num) {
323
+ // Pluralize a word using simplified English language rules.
324
+ if (num != 1) {
325
+ if (word.match(/[^e]y$/)) return word.replace(/y$/, '') + 'ies';
326
+ else if (word.match(/s$/)) return word + 'es'; // processes
327
+ else return word + 's';
328
+ }
329
+ else return word;
330
+ }
331
+
332
+ function render_menu_options(items, sel_value, auto_add) {
333
+ // return HTML for menu options
334
+ var html = '';
335
+ var found = false;
336
+
337
+ for (var idx = 0, len = items.length; idx < len; idx++) {
338
+ var item = items[idx];
339
+ var item_name = '';
340
+ var item_value = '';
341
+ var attribs = {};
342
+
343
+ if (isa_hash(item)) {
344
+ if (('label' in item) && ('items' in item)) {
345
+ // optgroup, recurse for items within
346
+ html += '<optgroup label="' + item.label + '">';
347
+ html += render_menu_options( item.items, sel_value, false );
348
+ html += '</optgroup>';
349
+ continue;
350
+ }
351
+ if (('label' in item) && ('data' in item)) {
352
+ item_name = item.label;
353
+ item_value = item.data;
354
+ }
355
+ else {
356
+ item_name = item.title;
357
+ item_value = item.id;
358
+ }
359
+ if (item.icon) attribs['data-icon'] = item.icon;
360
+ if (item.class) attribs['data-class'] = item.class;
361
+ if (item.group) attribs['data-group'] = item.group;
362
+ }
363
+ else if (isa_array(item)) {
364
+ item_value = item[0];
365
+ item_name = item[1];
366
+ }
367
+ else {
368
+ item_name = item_value = item;
369
+ }
370
+
371
+ attribs.value = item_value;
372
+ if (item_value == sel_value) attribs.selected = 'selected';
373
+ html += '<option ' + compose_attribs(attribs) + '>' + item_name + '</option>';
374
+
375
+ if (item_value == sel_value) found = true;
376
+ }
377
+
378
+ if (!found && (str_value(sel_value) != '') && auto_add) {
379
+ html += '<option value="'+sel_value+'" selected="selected">'+sel_value+'</option>';
380
+ }
381
+
382
+ return html;
383
+ }
384
+
385
+ function populate_menu(menu, items, sel_values, auto_add) {
386
+ // repopulate menu, supports multi-select
387
+ if (!sel_values) sel_values = [];
388
+ if (typeof(sel_values) == 'string') sel_values = [sel_values];
389
+ if (!auto_add) auto_add = false;
390
+
391
+ menu.options.length = 0;
392
+
393
+ items.forEach( function(item, idx) {
394
+ var item_name = '';
395
+ var item_value = '';
396
+
397
+ if (isa_hash(item)) {
398
+ if (('label' in item) && ('data' in item)) {
399
+ item_name = item.label;
400
+ item_value = item.data;
401
+ }
402
+ else {
403
+ item_name = item.title;
404
+ item_value = item.id;
405
+ }
406
+ }
407
+ else if (isa_array(item)) {
408
+ item_value = item[0];
409
+ item_name = item[1];
410
+ }
411
+ else {
412
+ item_name = item_value = item;
413
+ }
414
+
415
+ var opt = new Option( item_name, item_value );
416
+ if (sel_values.includes(item_value)) {
417
+ opt.selected = true;
418
+ sel_values.splice( sel_values.indexOf(item_value), 1 );
419
+ }
420
+ menu.options[ menu.options.length ] = opt;
421
+
422
+ if (isa_hash(item) && item.icon && opt.setAttribute) opt.setAttribute('data-icon', item.icon);
423
+ if (isa_hash(item) && item.class && opt.setAttribute) opt.setAttribute('data-class', item.class);
424
+ }); // foreach item
425
+
426
+ if (sel_values.length && auto_add) {
427
+ sel_values.forEach( function(sel_value) {
428
+ var opt = new Option( sel_value, sel_value );
429
+ opt.selected = true;
430
+ menu.options[ menu.options.length ] = opt;
431
+ } ); // foreach selected value
432
+ } // yes add
433
+ };
434
+
435
+ function zeroPad(value, len) {
436
+ // Pad a number with zeroes to achieve a desired total length (max 10)
437
+ return ('0000000000' + value).slice(0 - len);
438
+ };
439
+
440
+ function dirname(path) {
441
+ // return path excluding file at end (same as POSIX function of same name)
442
+ return path.toString().replace(/\/+$/, "").replace(/\/[^\/]+$/, "");
443
+ }
444
+
445
+ function basename(path) {
446
+ // return filename, strip path (same as POSIX function of same name)
447
+ return path.toString().replace(/\/+$/, "").replace(/^(.*)\/([^\/]+)$/, "$2");
448
+ }
449
+
450
+ function strip_ext(path) {
451
+ // strip extension from filename
452
+ return path.toString().replace(/\.\w+$/, "");
453
+ }
454
+
455
+ function load_script(url) {
456
+ // Dynamically load script into DOM.
457
+ Debug.trace( "Loading script: " + url );
458
+ var scr = document.createElement('SCRIPT');
459
+ scr.type = 'text/javascript';
460
+ scr.src = url;
461
+ document.getElementsByTagName('HEAD')[0].appendChild(scr);
462
+ }
463
+
464
+ function compose_attribs(attribs) {
465
+ // compose Key="Value" style attributes for HTML elements
466
+ var html = '';
467
+ var value = '';
468
+
469
+ if (attribs) {
470
+ for (var key in attribs) {
471
+ value = attribs[key];
472
+ if (typeof(value) != 'undefined') {
473
+ if (value === null) value = '';
474
+ html += " " + key + "=\"" + encode_attrib_entities(''+value) + "\"";
475
+ }
476
+ }
477
+ }
478
+
479
+ return html;
480
+ }
481
+
482
+ function compose_style(attribs) {
483
+ // compose key:value; pairs for style (CSS) elements
484
+ var html = '';
485
+
486
+ if (attribs) {
487
+ for (var key in attribs) {
488
+ html += " " + key + ":" + attribs[key] + ";";
489
+ }
490
+ }
491
+
492
+ return html;
493
+ }
494
+
495
+ function trim(text) {
496
+ // strip whitespace from beginning and end of string
497
+ if (text == null) return '';
498
+
499
+ if (text && text.replace) {
500
+ text = text.replace(/^\s+/, "");
501
+ text = text.replace(/\s+$/, "");
502
+ }
503
+
504
+ return text;
505
+ }
506
+
507
+ function encode_entities(text) {
508
+ // Simple entitize function for composing XML
509
+ if (text == null) return '';
510
+
511
+ if (text && text.replace) {
512
+ text = text.replace(/\&/g, "&amp;"); // MUST BE FIRST
513
+ text = text.replace(/</g, "&lt;");
514
+ text = text.replace(/>/g, "&gt;");
515
+ }
516
+
517
+ return text;
518
+ }
519
+
520
+ function encode_attrib_entities(text) {
521
+ // Simple entitize function for composing XML attributes
522
+ if (text == null) return '';
523
+
524
+ if (text && text.replace) {
525
+ text = text.replace(/\&/g, "&amp;"); // MUST BE FIRST
526
+ text = text.replace(/</g, "&lt;");
527
+ text = text.replace(/>/g, "&gt;");
528
+ text = text.replace(/\"/g, "&quot;");
529
+ text = text.replace(/\'/g, "&apos;");
530
+ }
531
+
532
+ return text;
533
+ }
534
+
535
+ function strip_html(text) {
536
+ if (text == null) return '';
537
+
538
+ if (text && text.replace) {
539
+ text = text.replace(/<.+?>/g, '');
540
+ }
541
+
542
+ return text;
543
+ }
544
+
545
+ function truncate_ellipsis(str, len) {
546
+ // simple truncate string with ellipsis if too long
547
+ str = str_value(str);
548
+ if (str.length > len) {
549
+ str = str.substring(0, len - 3) + '...';
550
+ }
551
+ return str;
552
+ }
553
+
554
+ function escape_text_field_value(text) {
555
+ // escape text field value, with stupid IE support
556
+ return encode_attrib_entities( str_value(text) );
557
+ }
558
+
559
+ function expando_text(text, max, link) {
560
+ // if text is longer than max chars, chop with ellipsis and include link to show all
561
+ if (!link) link = 'More';
562
+ text = str_value(text);
563
+ if (text.length <= max) return text;
564
+
565
+ var before = text.substring(0, max);
566
+ var after = text.substring(max);
567
+
568
+ return before +
569
+ '<span>... <a href="javascript:void(0)" onMouseUp="$(this).parent().hide().next().show()">'+link+'</a></span>' +
570
+ '<span style="display:none">' + after + '</span>';
571
+ };
572
+
573
+ function get_int_version(str, pad) {
574
+ // Joe's Fun Multi-Decimal Comparision Trick
575
+ // Example: convert 2.5.1 to 2005001 for numerical comparison against other similar "numbers".
576
+ if (!pad) pad = 3;
577
+ str = str_value(str).replace(/[^\d\.]+/g, '');
578
+ if (!str.match(/\./)) return parseInt(str, 10);
579
+
580
+ var parts = str.split(/\./);
581
+ var output = '';
582
+ for (var idx = 0, len = parts.length; idx < len; idx++) {
583
+ var part = '' + parts[idx];
584
+ while (part.length < pad) part = '0' + part;
585
+ output += part;
586
+ }
587
+ return parseInt( output.replace(/^0+/, '') || "0", 10 );
588
+ };
589
+
590
+ function get_unique_id(len) {
591
+ // Get unique ID using crypto
592
+ if (!len) len = 16;
593
+ var id = '';
594
+ var chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
595
+ while (id.length < len) {
596
+ id += chars[ crypto.getRandomValues(new Uint32Array(1))[0] % chars.length ];
597
+ }
598
+ return id;
599
+ };
600
+
601
+ function get_short_id(prefix = '', len = 10) {
602
+ // Get unique ID using crypto, lower-case only
603
+ var id = '';
604
+ var chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
605
+ while (id.length < len) {
606
+ id += chars[ crypto.getRandomValues(new Uint32Array(1))[0] % chars.length ];
607
+ }
608
+ return prefix + id;
609
+ };
610
+
611
+ function escape_regexp(text) {
612
+ // Escape text for use in a regular expression.
613
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
614
+ };
615
+
616
+ function set_path(target, path, value) {
617
+ // set path using dir/slash/syntax or dot.path.syntax
618
+ // preserve dots and slashes if escaped
619
+ var parts = path.replace(/\\\./g, '__PXDOT__').replace(/\\\//g, '__PXSLASH__').split(/[\.\/]/).map( function(elem) {
620
+ return elem.replace(/__PXDOT__/g, '.').replace(/__PXSLASH__/g, '/');
621
+ } );
622
+
623
+ var key = parts.pop();
624
+
625
+ // traverse path
626
+ while (parts.length) {
627
+ var part = parts.shift();
628
+ if (part) {
629
+ if (!(part in target)) {
630
+ // auto-create nodes
631
+ target[part] = {};
632
+ }
633
+ if (typeof(target[part]) != 'object') {
634
+ // path runs into non-object
635
+ return false;
636
+ }
637
+ target = target[part];
638
+ }
639
+ }
640
+
641
+ target[key] = value;
642
+ return true;
643
+ };
644
+
645
+ function get_path(target, path) {
646
+ // get path using dir/slash/syntax or dot.path.syntax
647
+ // preserve dots and slashes if escaped
648
+ var parts = path.replace(/\\\./g, '__PXDOT__').replace(/\\\//g, '__PXSLASH__').split(/[\.\/]/).map( function(elem) {
649
+ return elem.replace(/__PXDOT__/g, '.').replace(/__PXSLASH__/g, '/');
650
+ } );
651
+
652
+ var key = parts.pop();
653
+
654
+ // traverse path
655
+ while (parts.length) {
656
+ var part = parts.shift();
657
+ if (part) {
658
+ if (typeof(target[part]) != 'object') {
659
+ // path runs into non-object
660
+ return undefined;
661
+ }
662
+ target = target[part];
663
+ }
664
+ }
665
+
666
+ return target[key];
667
+ };
668
+
669
+ function delete_path(target, path) {
670
+ // delete path using dir/slash/syntax or dot.path.syntax
671
+ // preserve dots and slashes if escaped
672
+ var parts = path.replace(/\\\./g, '__PXDOT__').replace(/\\\//g, '__PXSLASH__').split(/[\.\/]/).map( function(elem) {
673
+ return elem.replace(/__PXDOT__/g, '.').replace(/__PXSLASH__/g, '/');
674
+ } );
675
+
676
+ var key = parts.pop();
677
+
678
+ // traverse path
679
+ while (parts.length) {
680
+ var part = parts.shift();
681
+ if (part) {
682
+ if (!(part in target)) {
683
+ // auto-create nodes
684
+ target[part] = {};
685
+ }
686
+ if (typeof(target[part]) != 'object') {
687
+ // path runs into non-object
688
+ return false;
689
+ }
690
+ target = target[part];
691
+ }
692
+ }
693
+
694
+ delete target[key];
695
+ return true;
696
+ };
697
+
698
+ function substitute(text, args, fatal) {
699
+ // perform simple [placeholder] substitution using supplied
700
+ // args object and return transformed text
701
+ var self = this;
702
+ var result = true;
703
+ var value = '';
704
+ if (typeof(text) == 'undefined') text = '';
705
+ text = '' + text;
706
+ if (!args) args = {};
707
+
708
+ text = text.replace(/\[([^\]]+)\]/g, function(m_all, name) {
709
+ value = get_path(args, name);
710
+ if (value === undefined) {
711
+ result = false;
712
+ return m_all;
713
+ }
714
+ else return value;
715
+ } );
716
+
717
+ if (!result && fatal) return null;
718
+ else return text;
719
+ };
720
+
721
+ function find_objects_idx(arr, crit, max) {
722
+ // find idx of all objects that match crit keys/values
723
+ var idxs = [];
724
+ var num_crit = 0;
725
+ for (var a in crit) num_crit++;
726
+
727
+ if (isa_hash(arr)) arr = hash_values_to_array(arr);
728
+
729
+ for (var idx = 0, len = arr.length; idx < len; idx++) {
730
+ var matches = 0;
731
+ for (var key in crit) {
732
+ if (arr[idx][key] == crit[key]) matches++;
733
+ }
734
+ if (matches == num_crit) {
735
+ idxs.push(idx);
736
+ if (max && (idxs.length >= max)) return idxs;
737
+ }
738
+ } // foreach elem
739
+
740
+ return idxs;
741
+ };
742
+
743
+ function find_object_idx(arr, crit) {
744
+ // find idx of first matched object, or -1 if not found
745
+ if (isa_hash(arr)) arr = hash_values_to_array(arr);
746
+ var idxs = find_objects_idx(arr, crit, 1);
747
+ return idxs.length ? idxs[0] : -1;
748
+ };
749
+
750
+ function find_object(arr, crit) {
751
+ // return first found object matching crit keys/values, or null if not found
752
+ if (isa_hash(arr)) arr = hash_values_to_array(arr);
753
+ var idx = find_object_idx(arr, crit);
754
+ return (idx > -1) ? arr[idx] : null;
755
+ };
756
+
757
+ function find_objects(arr, crit) {
758
+ // find and return all objects that match crit keys/values
759
+ if (isa_hash(arr)) arr = hash_values_to_array(arr);
760
+ var idxs = find_objects_idx(arr, crit);
761
+ var objs = [];
762
+ for (var idx = 0, len = idxs.length; idx < len; idx++) {
763
+ objs.push( arr[idxs[idx]] );
764
+ }
765
+ return objs;
766
+ };
767
+
768
+ function delete_object(arr, crit) {
769
+ var idx = find_object_idx(arr, crit);
770
+ if (idx > -1) {
771
+ arr.splice( idx, 1 );
772
+ return true;
773
+ }
774
+ return false;
775
+ };
776
+
777
+ function always_array(obj, key) {
778
+ // if object is not array, return array containing object
779
+ // if key is passed, work like XMLalwaysarray() instead
780
+ // apparently MSIE has weird issues with obj = always_array(obj);
781
+
782
+ if (key) {
783
+ if ((typeof(obj[key]) != 'object') || (typeof(obj[key].length) == 'undefined')) {
784
+ var temp = obj[key];
785
+ delete obj[key];
786
+ obj[key] = new Array();
787
+ obj[key][0] = temp;
788
+ }
789
+ return null;
790
+ }
791
+ else {
792
+ if ((typeof(obj) != 'object') || (typeof(obj.length) == 'undefined')) { return [ obj ]; }
793
+ else return obj;
794
+ }
795
+ };
796
+
797
+ function hash_keys_to_array(hash) {
798
+ // convert hash keys to array (discard values)
799
+ var arr = [];
800
+ for (var key in hash) arr.push( key );
801
+ return arr;
802
+ };
803
+
804
+ function hash_values_to_array(hash) {
805
+ // convert hash values to array (discard keys)
806
+ var arr = [];
807
+ for (var key in hash) arr.push( hash[key] );
808
+ return arr;
809
+ };
810
+
811
+ function obj_array_to_hash(arr, key) {
812
+ // convert array of objects to hash, keyed by specific key
813
+ var hash = {};
814
+ for (var idx = 0, len = arr.length; idx < len; idx++) {
815
+ var item = arr[idx];
816
+ if (key in item) hash[ item[key] ] = item;
817
+ }
818
+ return hash;
819
+ }
820
+
821
+ function merge_objects(a, b) {
822
+ // merge keys from a and b into c and return c
823
+ // b has precedence over a
824
+ if (!a) a = {};
825
+ if (!b) b = {};
826
+ var c = {};
827
+
828
+ for (var key in a) c[key] = a[key];
829
+ for (var key in b) c[key] = b[key];
830
+
831
+ return c;
832
+ };
833
+
834
+ function merge_hash_into(a, b) {
835
+ // shallow-merge keys from b into a
836
+ for (var key in b) a[key] = b[key];
837
+ };
838
+
839
+ function copy_object(obj) {
840
+ // return copy of object (NOT DEEP)
841
+ var new_obj = {};
842
+ for (var key in obj) new_obj[key] = obj[key];
843
+ return new_obj;
844
+ };
845
+
846
+ function deep_copy_object(obj) {
847
+ // recursively copy object and nested objects
848
+ // return new object
849
+ return JSON.parse( JSON.stringify(obj) );
850
+ };
851
+
852
+ function num_keys(hash) {
853
+ // count the number of keys in a hash
854
+ var count = 0;
855
+ for (var a in hash) count++;
856
+ return count;
857
+ };
858
+
859
+ function reverse_hash(a) {
860
+ // reverse hash keys/values
861
+ var c = {};
862
+ for (var key in a) {
863
+ c[ a[key] ] = key;
864
+ }
865
+ return c;
866
+ };
867
+
868
+ function isa_hash(arg) {
869
+ // determine if arg is a hash
870
+ return( !!arg && (typeof(arg) == 'object') && (typeof(arg.length) == 'undefined') );
871
+ };
872
+
873
+ function isa_array(arg) {
874
+ // determine if arg is an array or is array-like
875
+ return( !!arg && (typeof(arg) == 'object') && (typeof(arg.length) != 'undefined') );
876
+ };
877
+
878
+ function first_key(hash) {
879
+ // return first key from hash (unordered)
880
+ for (var key in hash) return key;
881
+ return null; // no keys in hash
882
+ };
883
+
884
+ function rand_array(arr) {
885
+ // return random element from array
886
+ return arr[ parseInt(Math.random() * arr.length, 10) ];
887
+ };
888
+
889
+ function find_in_array(arr, elem) {
890
+ // return true if elem is found in arr, false otherwise
891
+ for (var idx = 0, len = arr.length; idx < len; idx++) {
892
+ if (arr[idx] == elem) return true;
893
+ }
894
+ return false;
895
+ };
896
+
897
+ function array_to_hash_keys(arr, value) {
898
+ // convert array to hash keys, all with the same value
899
+ var hash = {};
900
+ for (var idx = 0, len = arr.length; idx < len; idx++) {
901
+ hash[ arr[idx] ] = value;
902
+ }
903
+ return hash;
904
+ };
905
+
906
+ function short_float_str(num) {
907
+ // force a float (add suffix if int)
908
+ num = '' + short_float(num);
909
+ if (num.match(/^\-?\d+$/)) num += ".0";
910
+ return num;
911
+ };
912
+
913
+ function toTitleCase(str) {
914
+ return str.toLowerCase().replace(/\b\w/g, function (txt) { return txt.toUpperCase(); });
915
+ };
916
+
917
+ function sort_by(orig, key, opts) {
918
+ // sort array of objects by key, asc or desc, and optionally return NEW array
919
+ // opts: { dir, type, copy }
920
+ if (!opts) opts = {};
921
+ if (!opts.dir) opts.dir = 1;
922
+ if (!opts.type) opts.type = 'string';
923
+
924
+ var arr = opts.copy ? Array.from(orig) : orig;
925
+
926
+ arr.sort( function(a, b) {
927
+ switch(opts.type) {
928
+ case 'string':
929
+ return( (''+a[key]).localeCompare(b[key]) * opts.dir );
930
+ break;
931
+
932
+ case 'number':
933
+ return (a[key] - b[key]) * opts.dir;
934
+ break;
935
+ }
936
+ } );
937
+
938
+ return arr;
939
+ };
940
+
941
+ function stableSerialize(node) {
942
+ // deep-serialize JSON with sorted keys, for comparison purposes
943
+ if (node === null) return 'null';
944
+ if (isa_hash(node)) {
945
+ var json = '{';
946
+ Object.keys(node).sort().forEach( function(key, idx) {
947
+ if (idx) json += ',';
948
+ json += JSON.stringify(key) + ":" + stableSerialize(node[key]);
949
+ } );
950
+ json += '}';
951
+ return json;
952
+ }
953
+ else if (isa_array(node)) {
954
+ var json = '[';
955
+ node.forEach( function(item, idx) {
956
+ if (idx) json += ',';
957
+ json += stableSerialize(item);
958
+ } );
959
+ json += ']';
960
+ return json;
961
+ }
962
+ else return JSON.stringify(node);
963
+ };
964
+
965
+ function stablePrettyStringify(node) {
966
+ // generate stable (alphabetized keys) pretty-printed json
967
+ return JSON.stringify( JSON.parse( stableSerialize(node) ), null, "\t" );
968
+ };
969
+
970
+ function inline_marked(md) {
971
+ // render text to markdown, trimming and stripping outer <p> tag
972
+ return marked.parse(md, config.ui.marked_config).trim().replace(/^<p>(.+)<\/p>$/, '$1')
973
+ };
974
+
975
+ // Debounce Function Generator
976
+ // Fires once immediately, then never again until freq ms
977
+ function debounce(func, freq) {
978
+ var timeout = null;
979
+ var requestFire = false;
980
+
981
+ return function() {
982
+ var context = this, args = arguments;
983
+ var later = function() {
984
+ timeout = null;
985
+ if (requestFire) {
986
+ func.apply(context, args);
987
+ requestFire = false;
988
+ }
989
+ };
990
+ if (!timeout) {
991
+ func.apply(context, args);
992
+ timeout = setTimeout(later, freq);
993
+ requestFire = false;
994
+ }
995
+ else {
996
+ requestFire = true;
997
+ }
998
+ };
999
+ };
1000
+
1001
+ // Copy text to clipboard
1002
+ // borrowed from: https://github.com/feross/clipboard-copy (MIT License)
1003
+ function copyToClipboard(text) {
1004
+ // Put the text to copy into a <span>
1005
+ var span = document.createElement('span');
1006
+ span.textContent = text;
1007
+
1008
+ // Preserve consecutive spaces and newlines
1009
+ span.style.whiteSpace = 'pre';
1010
+
1011
+ // Add the <span> to the page
1012
+ document.body.appendChild(span);
1013
+
1014
+ // Make a selection object representing the range of text selected by the user
1015
+ var selection = window.getSelection();
1016
+ var range = window.document.createRange();
1017
+ selection.removeAllRanges();
1018
+ range.selectNode(span);
1019
+ selection.addRange(range);
1020
+
1021
+ // Copy text to the clipboard
1022
+ var success = false;
1023
+ try {
1024
+ success = window.document.execCommand('copy');
1025
+ }
1026
+ catch (err) {
1027
+ console.log('error', err);
1028
+ }
1029
+
1030
+ // Cleanup
1031
+ selection.removeAllRanges();
1032
+ window.document.body.removeChild(span);
1033
+ };
1034
+
1035
+ // Support typing tabs in textarea
1036
+ function captureTabs(input, event) {
1037
+ if (event.keyCode == 9) {
1038
+ event.preventDefault();
1039
+ input.setRangeText("\t", input.selectionStart, input.selectionEnd, "end");
1040
+ return false;
1041
+ }
1042
+ };
1043
+
1044
+ // Relative Bytes Component
1045
+ // create via getFormRelativeBytes()
1046
+
1047
+ var RelativeBytes = {
1048
+
1049
+ mults: {
1050
+ b: 1,
1051
+ kb: 1024,
1052
+ mb: 1048576,
1053
+ gb: 1073741824,
1054
+ tb: 1099511627776
1055
+ },
1056
+
1057
+ init: function(sel) {
1058
+ // initialize all based on selector
1059
+ var self = this;
1060
+
1061
+ $(sel).each( function() {
1062
+ var $this = $(this);
1063
+ var $text = $this.next().find("input");
1064
+ var $menu = $this.next().find("select");
1065
+ var $both = $this.next().find("input, select");
1066
+
1067
+ $both.on('change', function() {
1068
+ var adj_value = parseInt( $text.val() );
1069
+ if (isNaN(adj_value) || (adj_value < 0)) return;
1070
+ var unit = $menu.val();
1071
+ var mult = self.mults[ unit ];
1072
+ var value = adj_value * mult;
1073
+ $this.val( value );
1074
+ });
1075
+ });
1076
+ }
1077
+
1078
+ }; // RelativeBytes
1079
+
1080
+ // Simple Color Utility
1081
+ // Copyright (c) 2025 Joseph Huckaby
1082
+
1083
+ class Color {
1084
+
1085
+ r = 0;
1086
+ g = 0;
1087
+ b = 0;
1088
+ a = 0;
1089
+
1090
+ constructor(color) {
1091
+ // string or object
1092
+ if (color) this.import(color);
1093
+ }
1094
+
1095
+ clone() {
1096
+ // make a copy
1097
+ return new Color(this);
1098
+ }
1099
+
1100
+ import(color) {
1101
+ // import color into class
1102
+ if (typeof(color) == 'string') {
1103
+ color = this.parse(color);
1104
+ if (!color) throw new Error("Failed to parse color: " + str);
1105
+ }
1106
+ for (var key in color) this[key] = color[key];
1107
+ }
1108
+
1109
+ parse(str) {
1110
+ // parse rgb(), rgba() or #hex
1111
+ str = ('' + str).toLowerCase();
1112
+ var color = null;
1113
+
1114
+ if (str.match(/^\#?([0-9a-f]{8})$/)) {
1115
+ // 8-color hex with alpha, e.g. AABBCCFF
1116
+ var hex = RegExp.$1;
1117
+ color = {
1118
+ r: parseInt(hex.substring(0, 2), 16),
1119
+ g: parseInt(hex.substring(2, 4), 16),
1120
+ b: parseInt(hex.substring(4, 6), 16),
1121
+ a: parseInt(hex.substring(6, 8), 16),
1122
+ };
1123
+ }
1124
+ else if (str.match(/^\#?([0-9a-f]{6})$/)) {
1125
+ // 6-color hex standard, e.g. AABBCC
1126
+ var hex = RegExp.$1;
1127
+ color = {
1128
+ r: parseInt(hex.substring(0, 2), 16),
1129
+ g: parseInt(hex.substring(2, 4), 16),
1130
+ b: parseInt(hex.substring(4, 6), 16),
1131
+ a: 255
1132
+ };
1133
+ }
1134
+ else if (str.match(/^\#?([0-9a-f]{3})$/)) {
1135
+ // 3-color hex shorthand, e.g. ABC
1136
+ var hex = RegExp.$1;
1137
+ color = {
1138
+ r: parseInt(hex.substring(0, 1) + hex.substring(0, 1), 16),
1139
+ g: parseInt(hex.substring(1, 2) + hex.substring(1, 2), 16),
1140
+ b: parseInt(hex.substring(2, 3) + hex.substring(2, 3), 16),
1141
+ a: 255
1142
+ };
1143
+ }
1144
+ else if (str.match(/^rgba\(([\d\,\.\s]+)\)$/)) {
1145
+ // CSS rgba syntax, with alpha
1146
+ var csv = RegExp.$1;
1147
+ var parts = csv.split(/\,\s*/);
1148
+ color = {
1149
+ r: Math.min(255, parseInt( parts[0] || 0 )),
1150
+ g: Math.min(255, parseInt( parts[1] || 0 )),
1151
+ b: Math.min(255, parseInt( parts[2] || 0 )),
1152
+ a: Math.min(255, Math.floor( parseFloat( parts[3] || 0 ) * 255 ))
1153
+ };
1154
+ }
1155
+ else if (str.match(/^rgb\(([\d\,\.\s]+)\)$/)) {
1156
+ // CSS rgb syntax, opaque
1157
+ var csv = RegExp.$1;
1158
+ var parts = csv.split(/\,\s*/);
1159
+ color = {
1160
+ r: Math.min(255, parseInt( parts[0] || 0 )),
1161
+ g: Math.min(255, parseInt( parts[1] || 0 )),
1162
+ b: Math.min(255, parseInt( parts[2] || 0 )),
1163
+ a: 255
1164
+ };
1165
+ }
1166
+ else {
1167
+ return null;
1168
+ }
1169
+
1170
+ return color;
1171
+ }
1172
+
1173
+ mix(dest, amount) {
1174
+ // mix our colors with a destination color
1175
+ if (typeof(dest) == 'string') dest = new Color(dest);
1176
+
1177
+ this.r = Math.round( this.r + ((dest.r - this.r) * amount) );
1178
+ this.g = Math.round( this.g + ((dest.g - this.g) * amount) );
1179
+ this.b = Math.round( this.b + ((dest.b - this.b) * amount) );
1180
+ this.a = Math.round( this.a + ((dest.a - this.a) * amount) );
1181
+
1182
+ return this; // for chaining
1183
+ }
1184
+
1185
+ hex() {
1186
+ // output color has 6-char hex string
1187
+ var rr = zeroPad( this.r.toString(16), 2 );
1188
+ var gg = zeroPad( this.g.toString(16), 2 );
1189
+ var bb = zeroPad( this.b.toString(16), 2 );
1190
+ return `#${rr}${gg}${bb}`;
1191
+ }
1192
+
1193
+ hexa() {
1194
+ // output color as 8-char hex with alpha
1195
+ var rr = zeroPad( this.r.toString(16), 2 );
1196
+ var gg = zeroPad( this.g.toString(16), 2 );
1197
+ var bb = zeroPad( this.b.toString(16), 2 );
1198
+ var aa = zeroPad( this.a.toString(16), 2 );
1199
+ return `#${rr}${gg}${bb}${aa}`;
1200
+ }
1201
+
1202
+ rgb() {
1203
+ // output color as CSS rgb syntax
1204
+ return `rgb(${this.r},${this.g},${this.b})`;
1205
+ }
1206
+
1207
+ rgba() {
1208
+ // output color as CSS rgba syntax
1209
+ return `rgba(${this.r},${this.g},${this.b},${short_float(this.a / 255, 3)})`;
1210
+ }
1211
+
1212
+ }