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/LICENSE.md +11 -0
- package/README.md +485 -0
- package/css/base.css +2736 -0
- package/css/boilerplate.css +295 -0
- package/css/normalize.css +349 -0
- package/js/base.js +648 -0
- package/js/calendar.js +166 -0
- package/js/datetime.js +233 -0
- package/js/dialog.js +385 -0
- package/js/misc.js +311 -0
- package/js/page.js +1940 -0
- package/js/popover.js +158 -0
- package/js/select.js +845 -0
- package/js/tools.js +1212 -0
- package/js/unscroll.min.js +3 -0
- package/package.json +20 -0
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 + " " + p_text;
|
|
173
|
+
if ((p_amt != 1) && !abbrev) text += "s";
|
|
174
|
+
if (s_amt && !no_secondary) {
|
|
175
|
+
text += ", " + s_amt + " " + 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, "&"); // MUST BE FIRST
|
|
513
|
+
text = text.replace(/</g, "<");
|
|
514
|
+
text = text.replace(/>/g, ">");
|
|
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, "&"); // MUST BE FIRST
|
|
526
|
+
text = text.replace(/</g, "<");
|
|
527
|
+
text = text.replace(/>/g, ">");
|
|
528
|
+
text = text.replace(/\"/g, """);
|
|
529
|
+
text = text.replace(/\'/g, "'");
|
|
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
|
+
}
|