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/page.js ADDED
@@ -0,0 +1,1940 @@
1
+ /**
2
+ * WebApp 1.0 Page Manager
3
+ * Author: Joseph Huckaby
4
+ **/
5
+
6
+ //
7
+ // Page Base Class
8
+ //
9
+
10
+ window.Page = class Page {
11
+ // 'Page' class is the abstract base class for all pages
12
+ // Each web component calls this class daddy
13
+
14
+ // methods
15
+ constructor(config, div) {
16
+ if (!config) return;
17
+
18
+ this.ID = '';
19
+ this.data = null;
20
+ this.active = false;
21
+
22
+ // class constructor, import config into self
23
+ this.data = {};
24
+ if (!config) config = {};
25
+ for (var key in config) this[key] = config[key];
26
+
27
+ this.div = div || $('#page_' + this.ID);
28
+ assert(this.div, "Cannot find page div: page_" + this.ID);
29
+
30
+ this.tab = $('#tab_' + this.ID);
31
+ }
32
+
33
+ onInit() {
34
+ // called with the page is initialized
35
+ }
36
+
37
+ onActivate() {
38
+ // called when page is activated
39
+ return true;
40
+ }
41
+
42
+ onDeactivate() {
43
+ // called when page is deactivated
44
+ return true;
45
+ }
46
+
47
+ show() {
48
+ // show page
49
+ this.div.show();
50
+ }
51
+
52
+ hide() {
53
+ this.div.hide();
54
+ }
55
+
56
+ gosub(anchor) {
57
+ // go to sub-anchor (article section link)
58
+ }
59
+
60
+ getFormRow(args) {
61
+ // render form row using CSS grid elements
62
+ // add localized strings and markdown captions
63
+ var html = '';
64
+
65
+ if (args.id && config.ui.dom[args.id]) {
66
+ // pull in args from localized ui config (for label, caption, etc.)
67
+ // merge_hash_into( args, config.ui.dom[args.id] );
68
+ for (var key in config.ui.dom[args.id]) {
69
+ if (!args[key]) args[key] = config.ui.dom[args.id][key];
70
+ }
71
+ }
72
+
73
+ var label = args.label;
74
+ var content = args.content;
75
+ var suffix = args.suffix;
76
+ var caption = args.caption;
77
+ var extra_classes = args.class || '';
78
+
79
+ delete args.label;
80
+ delete args.content;
81
+ delete args.suffix;
82
+ delete args.caption;
83
+ delete args.class;
84
+
85
+ html += '<div class="form_row ' + extra_classes + '" ' + compose_attribs(args) + '>';
86
+ if (label) html += '<div class="fr_label">' + label + '</div>';
87
+ if (content) html += '<div class="fr_content">' + content + '</div>';
88
+ if (suffix) html += '<div class="fr_suffix">' + suffix + '</div>';
89
+ if (caption) html += '<div class="fr_caption"><span>' + inline_marked(caption) + '</span></div>'; // markdown
90
+ html += '</div>';
91
+
92
+ return html;
93
+ }
94
+
95
+ getFormText(args) {
96
+ // render text field for form
97
+ if (!args.type) args.type = 'text';
98
+ if (args.disabled) args.disabled = "disabled";
99
+ else delete args.disabled;
100
+
101
+ if (args.id && config.ui.dom[args.id]) {
102
+ // pull in args from localized ui config
103
+ merge_hash_into( args, config.ui.dom[args.id] );
104
+ }
105
+
106
+ // stupid hack for safari (autofill bug)
107
+ if (app.safari && (args.autocomplete === 'off')) {
108
+ args.readonly = 'readonly';
109
+ args.onfocus = "this.removeAttribute('readonly')";
110
+ args.onblur = "this.setAttribute('readonly','readonly')";
111
+ }
112
+
113
+ return '<input ' + compose_attribs(args) + '/>';
114
+ }
115
+
116
+ getFormTextarea(args) {
117
+ // render textarea field for form
118
+ var value = ('value' in args) ? args.value : '';
119
+ delete args.value;
120
+
121
+ if (args.id && config.ui.dom[args.id]) {
122
+ // pull in args from localized ui config
123
+ merge_hash_into( args, config.ui.dom[args.id] );
124
+ }
125
+
126
+ return '<textarea ' + compose_attribs(args) + '>' + encode_entities(value) + '</textarea>';
127
+ }
128
+
129
+ getFormCheckbox(args) {
130
+ // render checkbox for form
131
+ var html = '';
132
+
133
+ if (args.id && config.ui.dom[args.id]) {
134
+ // pull in args from localized ui config
135
+ merge_hash_into( args, config.ui.dom[args.id] );
136
+ }
137
+
138
+ var label = args.label || '';
139
+ delete args.label;
140
+
141
+ if (args.auto) {
142
+ args.checked = app.getPref(args.auto);
143
+ args['onChange'] = "app.setPref('" + args.auto + "',$(this).is(':checked'))";
144
+ delete args.auto;
145
+ }
146
+
147
+ if (args.checked) args.checked = "checked";
148
+ else delete args.checked;
149
+
150
+ if (args.disabled) args.disabled = "disabled";
151
+ else delete args.disabled;
152
+
153
+ if (!('value' in args)) args.value = 1;
154
+
155
+ html += '<div class="checkbox_container">';
156
+ html += '<input type="checkbox" ' + compose_attribs(args) + '/>';
157
+ html += '<label>' + label + '</label>';
158
+ html += '</div>';
159
+
160
+ return html;
161
+ }
162
+
163
+ getFormMenu(args) {
164
+ // render menu for form
165
+ var html = '';
166
+ html += '<div class="select_chevron mdi mdi-chevron-down" style="top:7px;"></div>';
167
+
168
+ if (args.id && config.ui.dom[args.id]) {
169
+ // pull in args from localized ui config
170
+ merge_hash_into( args, config.ui.dom[args.id] );
171
+ }
172
+
173
+ var opts = args.options;
174
+ if (isa_hash(args.options)) {
175
+ // convert hash to array
176
+ opts = Object.keys(args.options).map( function(key) {
177
+ return { id: key, title: args.options[key] };
178
+ } );
179
+ }
180
+ delete args.options;
181
+
182
+ var value = args.value || '';
183
+ delete args.value;
184
+
185
+ var auto_add = args.auto_add || false;
186
+ delete args.auto_add;
187
+
188
+ html += '<select ' + compose_attribs(args) + '>';
189
+ html += render_menu_options( opts, value, auto_add );
190
+ html += '</select>';
191
+
192
+ return html;
193
+ }
194
+
195
+ getFormMenuMulti(args) {
196
+ // render multi-select menu for form
197
+ var html = '';
198
+ var opt_values = [];
199
+
200
+ if (args.id && config.ui.dom[args.id]) {
201
+ // pull in args from localized ui config
202
+ merge_hash_into( args, config.ui.dom[args.id] );
203
+ }
204
+
205
+ var opts = deep_copy_object(args.options);
206
+ delete args.options;
207
+
208
+ var values = args.values || [];
209
+ delete args.values;
210
+
211
+ var auto_add = args.auto_add || false;
212
+ delete args.auto_add;
213
+
214
+ if (args.default_icon) {
215
+ opts.forEach( function(item) {
216
+ if (!item.icon) item.icon = args.default_icon;
217
+ } );
218
+ delete args.default_icon;
219
+ }
220
+
221
+ html += '<select multiple ' + compose_attribs(args) + '>';
222
+ for (var idx = 0, len = opts.length; idx < len; idx++) {
223
+ var item = opts[idx];
224
+ var item_name = '';
225
+ var item_value = '';
226
+ var attribs = {};
227
+
228
+ if (isa_hash(item)) {
229
+ if (('label' in item) && ('data' in item)) {
230
+ item_name = item.label;
231
+ item_value = item.data;
232
+ }
233
+ else {
234
+ item_name = item.title;
235
+ item_value = item.id;
236
+ }
237
+ if (item.icon) attribs['data-icon'] = item.icon;
238
+ if (item.abbrev) attribs['data-abbrev'] = item.abbrev;
239
+ if (item.class) attribs['data-class'] = item.class;
240
+ if (item.group) attribs['data-group'] = item.group;
241
+ }
242
+ else if (isa_array(item)) {
243
+ item_value = item[0];
244
+ item_name = item[1];
245
+ }
246
+ else {
247
+ item_name = item_value = item;
248
+ }
249
+
250
+ attribs.value = item_value;
251
+ if (find_in_array(values, item_value)) attribs.selected = 'selected';
252
+ html += '<option ' + compose_attribs(attribs) + '>' + item_name + '</option>';
253
+ opt_values.push( item_value );
254
+ } // foreach opt
255
+
256
+ if (auto_add) {
257
+ values.forEach( function(value) {
258
+ if (!find_in_array(opt_values, value)) {
259
+ html += '<option value="' + encode_attrib_entities(value) + '" selected="selected">' + value + '</option>';
260
+ }
261
+ } );
262
+ } // auto-add
263
+
264
+ html += '</select>';
265
+ return html;
266
+ }
267
+
268
+ getFormMenuSingle(args) {
269
+ // render single-select menu for form
270
+ var html = '';
271
+
272
+ if (args.id && config.ui.dom[args.id]) {
273
+ // pull in args from localized ui config
274
+ merge_hash_into( args, config.ui.dom[args.id] );
275
+ }
276
+
277
+ var opts = deep_copy_object(args.options);
278
+ delete args.options;
279
+
280
+ var value = args.value || '';
281
+ delete args.value;
282
+
283
+ var auto_add = args.auto_add || false;
284
+ delete args.auto_add;
285
+
286
+ if (args.default_icon) {
287
+ opts.forEach( function(item) {
288
+ if (!item.icon) item.icon = args.default_icon;
289
+ } );
290
+ delete args.default_icon;
291
+ }
292
+
293
+ html += '<select ' + compose_attribs(args) + '>';
294
+ html += render_menu_options( opts, value, auto_add );
295
+ html += '</select>';
296
+
297
+ return html;
298
+ }
299
+
300
+ getFormFieldset(args) {
301
+ // get fieldset for form
302
+ if (args.id && config.ui.dom[args.id]) {
303
+ // pull in args from localized ui config
304
+ merge_hash_into( args, config.ui.dom[args.id] );
305
+ }
306
+
307
+ var legend = ('legend' in args) ? args.legend : '';
308
+ delete args.legend;
309
+
310
+ var content = args.content;
311
+ delete args.content;
312
+
313
+ return '<fieldset ' + compose_attribs(args) + '><legend>' + legend + '</legend>' + content + '</fieldset>';
314
+ }
315
+
316
+ getFieldsetInfoGroup(args) {
317
+ // get info group pair for fieldset in form
318
+ var html = '';
319
+
320
+ if (args.id && config.ui.dom[args.id]) {
321
+ // pull in args from localized ui config
322
+ merge_hash_into( args, config.ui.dom[args.id] );
323
+ }
324
+
325
+ if ('label' in args) html += '<div class="info_label">' + args.label + '</div>';
326
+ if ('content' in args) html += '<div class="info_value">' + args.content + '</div>';
327
+ return html;
328
+ }
329
+
330
+ getFormFile(args) {
331
+ // render file field for form
332
+ if (!args.type) args.type = 'file';
333
+
334
+ if (args.id && config.ui.dom[args.id]) {
335
+ // pull in args from localized ui config
336
+ merge_hash_into( args, config.ui.dom[args.id] );
337
+ }
338
+
339
+ return '<input ' + compose_attribs(args) + '/>';
340
+ }
341
+
342
+ getFormDate(args) {
343
+ // render custom date field for form
344
+ // coerce value into epoch
345
+ if (!args.value) args.value = 0;
346
+ else if (!args.value.toString().match(/^\d+$/)) {
347
+ args.value = get_date_args(args.value).epoch;
348
+ }
349
+ if (!args.type) args.type = 'hidden';
350
+
351
+ if (args.id && config.ui.dom[args.id]) {
352
+ // pull in args from localized ui config
353
+ merge_hash_into( args, config.ui.dom[args.id] );
354
+ }
355
+
356
+ return '<input ' + compose_attribs(args) + '/><div class="form_date"></div>';
357
+ }
358
+
359
+ getFormRelativeTime(args) {
360
+ // render custom relative sec/min/hour/day/week/month/year selector
361
+ // value is always seconds, everything else is just UI sugar
362
+ if (!args.value) args.value = 0;
363
+ var adj_value = args.value;
364
+ var unit = 'seconds';
365
+ var html = '';
366
+
367
+ if (args.id && config.ui.dom[args.id]) {
368
+ // pull in args from localized ui config
369
+ merge_hash_into( args, config.ui.dom[args.id] );
370
+ }
371
+
372
+ var units = [
373
+ { id: 'seconds', title: 'Seconds', mult: 1 },
374
+ { id: 'minutes', title: 'Minutes', mult: 60 },
375
+ { id: 'hours', title: 'Hours', mult: 3600 },
376
+ { id: 'days', title: 'Days', mult: 86400 }
377
+ ];
378
+
379
+ if (adj_value && ((adj_value % 86400) == 0)) {
380
+ adj_value = adj_value /= 86400;
381
+ unit = 'days';
382
+ }
383
+ else if (adj_value && ((adj_value % 3600) == 0)) {
384
+ adj_value = adj_value /= 3600;
385
+ unit = 'hours';
386
+ }
387
+ else if (adj_value && ((adj_value % 60) == 0)) {
388
+ adj_value = adj_value /= 60;
389
+ unit = 'minutes';
390
+ }
391
+
392
+ if (!args.type) args.type = 'hidden';
393
+ html += '<input ' + compose_attribs(args) + '/>';
394
+
395
+ html += '<div class="form_row_duo">';
396
+ html += '<div>' + this.getFormText({ id: args.id + '_val', type: 'number', min: 0, value: adj_value }) + '</div>';
397
+ html += '<div>' + this.getFormMenu({ id: args.id + '_mul', options: units, value: unit }) + '</div>';
398
+ html += '</div>';
399
+
400
+ return html;
401
+ }
402
+
403
+ getFormRelativeBytes(args) {
404
+ // render custom relative bytes/kb/mb/gb/tb selector
405
+ // value is always bytes, everything else is just UI sugar
406
+ if (!args.value) args.value = 0;
407
+ var adj_value = args.value;
408
+ var unit = 'b';
409
+ var html = '';
410
+
411
+ if (args.id && config.ui.dom[args.id]) {
412
+ // pull in args from localized ui config
413
+ merge_hash_into( args, config.ui.dom[args.id] );
414
+ }
415
+
416
+ var units = [
417
+ { id: 'b', title: 'Bytes', mult: 1 },
418
+ { id: 'kb', title: 'Kilobytes', mult: 1024 },
419
+ { id: 'mb', title: 'Megabytes', mult: 1048576 },
420
+ { id: 'gb', title: 'Gigabytes', mult: 1073741824 },
421
+ { id: 'tb', title: 'Terabytes', mult: 1099511627776 }
422
+ ];
423
+
424
+ if (adj_value && ((adj_value % 1099511627776) == 0)) {
425
+ adj_value = adj_value /= 1099511627776;
426
+ unit = 'tb';
427
+ }
428
+ else if (adj_value && ((adj_value % 1073741824) == 0)) {
429
+ adj_value = adj_value /= 1073741824;
430
+ unit = 'gb';
431
+ }
432
+ else if (adj_value && ((adj_value % 1048576) == 0)) {
433
+ adj_value = adj_value /= 1048576;
434
+ unit = 'mb';
435
+ }
436
+ else if (adj_value && ((adj_value % 1024) == 0)) {
437
+ adj_value = adj_value /= 1024;
438
+ unit = 'kb';
439
+ }
440
+
441
+ if (!args.type) args.type = 'hidden';
442
+ html += '<input ' + compose_attribs(args) + '/>';
443
+
444
+ html += '<div class="form_row_duo">';
445
+ html += '<div>' + this.getFormText({ id: args.id + '_val', type: 'number', value: adj_value }) + '</div>';
446
+ html += '<div>' + this.getFormMenu({ id: args.id + '_mul', options: units, value: unit }) + '</div>';
447
+ html += '</div>';
448
+
449
+ return html;
450
+ }
451
+
452
+ getFormRange(args) {
453
+ // render custom range slider with visible number value on right side
454
+ if (!args.value) args.value = 0;
455
+ var html = '';
456
+
457
+ if (args.id && config.ui.dom[args.id]) {
458
+ // pull in args from localized ui config
459
+ merge_hash_into( args, config.ui.dom[args.id] );
460
+ }
461
+
462
+ args.onInput = `$P().updateFormRange(this)`;
463
+
464
+ html += '<div class="form_row_range">';
465
+ html += '<div>' + this.getFormText( merge_objects(args, { type: 'range' }) ) + '</div>';
466
+ html += '<div>' + this.getFormText( merge_objects(args, { id: args.id + '_txt', type: 'number' }) ) + '</div>';
467
+ html += '</div>';
468
+
469
+ return html;
470
+ }
471
+
472
+ updateFormRange(elem) {
473
+ // keep range and text field in sync with each other
474
+ var value = elem.value;
475
+ $(elem).closest('.form_row_range').find('input').val(value);
476
+ }
477
+
478
+ getPaginatedTable() {
479
+ // get html for paginated table
480
+ // dual-calling convention: (resp, cols, data_type, callback) or (args)
481
+ var args = null;
482
+ if (arguments.length == 1) {
483
+ // custom args calling convention
484
+ args = arguments[0];
485
+
486
+ // V2 API
487
+ if (!args.resp && args.rows && args.total) {
488
+ args.resp = {
489
+ rows: args.rows,
490
+ list: { length: args.total }
491
+ };
492
+ }
493
+ }
494
+ else {
495
+ // classic calling convention
496
+ args = {
497
+ resp: arguments[0],
498
+ cols: arguments[1],
499
+ data_type: arguments[2],
500
+ callback: arguments[3],
501
+ limit: this.args.limit,
502
+ offset: this.args.offset || 0
503
+ };
504
+ }
505
+
506
+ var resp = args.resp;
507
+ var cols = args.cols;
508
+ var data_type = args.data_type;
509
+ var callback = args.callback;
510
+ var cpl = args.pagination_link || '';
511
+ var html = '';
512
+
513
+ // pagination header
514
+ html += '<div class="pagination">';
515
+ html += '<table cellspacing="0" cellpadding="0" border="0" width="100%"><tr>';
516
+
517
+ var results = {
518
+ limit: args.limit,
519
+ offset: args.offset || 0,
520
+ total: resp.list.length
521
+ };
522
+
523
+ var num_pages = Math.floor( results.total / results.limit ) + 1;
524
+ if (results.total % results.limit == 0) num_pages--;
525
+ var current_page = Math.floor( results.offset / results.limit ) + 1;
526
+
527
+ html += '<td align="left" width="33%">';
528
+ html += commify(results.total) + ' ' + pluralize(data_type, results.total) + ' found';
529
+ html += '</td>';
530
+
531
+ html += '<td align="center" width="34%">';
532
+ if (num_pages > 1) html += 'Page ' + commify(current_page) + ' of ' + commify(num_pages);
533
+ else html += '&nbsp;';
534
+ html += '</td>';
535
+
536
+ html += '<td align="right" width="33%">';
537
+
538
+ if (num_pages > 1) {
539
+ // html += 'Page: ';
540
+ if (current_page > 1) {
541
+ if (cpl) {
542
+ html += '<span class="link" onMouseUp="'+cpl+'('+Math.floor((current_page - 2) * results.limit)+')">&laquo; Prev</span>';
543
+ }
544
+ else {
545
+ html += '<a href="#' + this.ID + compose_query_string(merge_objects(this.args, {
546
+ offset: (current_page - 2) * results.limit
547
+ })) + '">&laquo; Prev</a>';
548
+ }
549
+ }
550
+ html += '&nbsp;&nbsp;&nbsp;';
551
+
552
+ var start_page = current_page - 4;
553
+ var end_page = current_page + 5;
554
+
555
+ if (start_page < 1) {
556
+ end_page += (1 - start_page);
557
+ start_page = 1;
558
+ }
559
+
560
+ if (end_page > num_pages) {
561
+ start_page -= (end_page - num_pages);
562
+ if (start_page < 1) start_page = 1;
563
+ end_page = num_pages;
564
+ }
565
+
566
+ for (var idx = start_page; idx <= end_page; idx++) {
567
+ if (idx == current_page) {
568
+ html += '<b>' + commify(idx) + '</b>';
569
+ }
570
+ else {
571
+ if (cpl) {
572
+ html += '<span class="link" onMouseUp="'+cpl+'('+Math.floor((idx - 1) * results.limit)+')">' + commify(idx) + '</span>';
573
+ }
574
+ else {
575
+ html += '<a href="#' + this.ID + compose_query_string(merge_objects(this.args, {
576
+ offset: (idx - 1) * results.limit
577
+ })) + '">' + commify(idx) + '</a>';
578
+ }
579
+ }
580
+ html += '&nbsp;';
581
+ }
582
+
583
+ html += '&nbsp;&nbsp;';
584
+ if (current_page < num_pages) {
585
+ if (cpl) {
586
+ html += '<span class="link" onMouseUp="'+cpl+'('+Math.floor((current_page + 0) * results.limit)+')">Next &raquo;</span>';
587
+ }
588
+ else {
589
+ html += '<a href="#' + this.ID + compose_query_string(merge_objects(this.args, {
590
+ offset: (current_page + 0) * results.limit
591
+ })) + '">Next &raquo;</a>';
592
+ }
593
+ }
594
+ } // more than one page
595
+ else {
596
+ html += 'Page 1 of 1';
597
+ }
598
+ html += '</td>';
599
+ html += '</tr></table>';
600
+ html += '</div>';
601
+
602
+ html += '<div style="margin-top:5px; overflow-x:auto;">';
603
+
604
+ var tattrs = args.attribs || {};
605
+ if (!tattrs.class) tattrs.class = 'data_table ellip';
606
+ if (!tattrs.width) tattrs.width = '100%';
607
+ html += '<table ' + compose_attribs(tattrs) + '>';
608
+
609
+ html += '<tr><th>' + cols.join('</th><th>').replace(/\s+/g, '&nbsp;') + '</th></tr>';
610
+
611
+ for (var idx = 0, len = resp.rows.length; idx < len; idx++) {
612
+ var row = resp.rows[idx];
613
+ var tds = callback(row, idx);
614
+ if (tds) {
615
+ html += '<tr' + (tds.className ? (' class="'+tds.className+'"') : '') + '>';
616
+ html += '<td>' + tds.join('</td><td>') + '</td>';
617
+ html += '</tr>';
618
+ }
619
+ } // foreach row
620
+
621
+ if (!resp.rows.length) {
622
+ html += '<tr><td colspan="'+cols.length+'" align="center" style="padding-top:10px; padding-bottom:10px; font-weight:bold;">';
623
+ html += 'No '+pluralize(data_type)+' found.';
624
+ html += '</td></tr>';
625
+ }
626
+
627
+ html += '</table>';
628
+ html += '</div>';
629
+
630
+ return html;
631
+ }
632
+
633
+ getPaginatedGrid() {
634
+ // get html for paginated grid
635
+ // multi-calling convention: (resp, cols, data_type, callback), or (args, callback), or (args)
636
+ var args = null;
637
+ if (arguments.length == 1) {
638
+ // custom args calling convention
639
+ args = arguments[0];
640
+
641
+ // V2 API
642
+ if (!args.resp && args.rows && args.total) {
643
+ args.resp = {
644
+ rows: args.rows,
645
+ list: { length: args.total }
646
+ };
647
+ }
648
+ }
649
+ else if (arguments.length == 2) {
650
+ // combo args + callback
651
+ args = arguments[0];
652
+ args.callback = arguments[1];
653
+ }
654
+ else {
655
+ // classic calling convention
656
+ args = {
657
+ resp: arguments[0],
658
+ cols: arguments[1],
659
+ data_type: arguments[2],
660
+ callback: arguments[3],
661
+ limit: this.args.limit,
662
+ offset: this.args.offset || 0
663
+ };
664
+ }
665
+
666
+ var resp = args.resp;
667
+ var rows = resp.rows;
668
+ var cols = args.cols;
669
+ var data_type = args.data_type;
670
+ var callback = args.callback;
671
+ var cpl = args.pagination_link || '';
672
+ var html = '';
673
+
674
+ // pagination header
675
+ html += '<div class="data_grid_pagination">';
676
+
677
+ var results = {
678
+ limit: args.limit,
679
+ offset: args.offset || 0,
680
+ total: resp.list.length
681
+ };
682
+
683
+ var num_pages = Math.floor( results.total / results.limit ) + 1;
684
+ if (results.total % results.limit == 0) num_pages--;
685
+ var current_page = Math.floor( results.offset / results.limit ) + 1;
686
+
687
+ html += '<div style="text-align:left">';
688
+ html += commify(results.total) + ' ' + pluralize(data_type, results.total) + '<span class="sm_hide">&nbsp;found</span>';
689
+ html += '</div>';
690
+
691
+ html += '<div style="text-align:center">';
692
+ if (num_pages > 1) html += 'Page ' + commify(current_page) + ' of ' + commify(num_pages);
693
+ else html += '&nbsp;';
694
+ html += '</div>';
695
+
696
+ html += '<div style="text-align:right">';
697
+
698
+ if (num_pages > 1) {
699
+ // html += 'Page: ';
700
+ if (current_page > 1) {
701
+ if (cpl) {
702
+ html += '<span class="link" onMouseUp="'+cpl+'('+Math.floor((current_page - 2) * results.limit)+')">&laquo; Prev</span>';
703
+ }
704
+ else {
705
+ html += '<a href="#' + this.ID + compose_query_string(merge_objects(this.args, {
706
+ offset: (current_page - 2) * results.limit
707
+ })) + '">&laquo; Prev</a>';
708
+ }
709
+ }
710
+ html += '&nbsp;&nbsp;&nbsp;';
711
+
712
+ var start_page = current_page - 4;
713
+ var end_page = current_page + 5;
714
+
715
+ if (start_page < 1) {
716
+ end_page += (1 - start_page);
717
+ start_page = 1;
718
+ }
719
+
720
+ if (end_page > num_pages) {
721
+ start_page -= (end_page - num_pages);
722
+ if (start_page < 1) start_page = 1;
723
+ end_page = num_pages;
724
+ }
725
+
726
+ html += '<span class="sm_hide">';
727
+ for (var idx = start_page; idx <= end_page; idx++) {
728
+ if (idx == current_page) {
729
+ html += '<b>' + commify(idx) + '</b>';
730
+ }
731
+ else {
732
+ if (cpl) {
733
+ html += '<span class="link" onMouseUp="'+cpl+'('+Math.floor((idx - 1) * results.limit)+')">' + commify(idx) + '</span>';
734
+ }
735
+ else {
736
+ html += '<a href="#' + this.ID + compose_query_string(merge_objects(this.args, {
737
+ offset: (idx - 1) * results.limit
738
+ })) + '">' + commify(idx) + '</a>';
739
+ }
740
+ }
741
+ html += '&nbsp;';
742
+ }
743
+ html += '&nbsp;&nbsp;';
744
+ html += '</span>';
745
+
746
+ if (current_page < num_pages) {
747
+ if (cpl) {
748
+ html += '<span class="link" onMouseUp="'+cpl+'('+Math.floor((current_page + 0) * results.limit)+')">Next &raquo;</span>';
749
+ }
750
+ else {
751
+ html += '<a href="#' + this.ID + compose_query_string(merge_objects(this.args, {
752
+ offset: (current_page + 0) * results.limit
753
+ })) + '">Next &raquo;</a>';
754
+ }
755
+ }
756
+ } // more than one page
757
+ else {
758
+ html += 'Page 1 of 1';
759
+ }
760
+ html += '</div>'; // right 3rd
761
+ html += '</div>'; // pagination
762
+
763
+ html += '<div style="margin-top:5px;">';
764
+
765
+ var tattrs = args.attribs || {};
766
+ if (args.class) tattrs.class = args.class;
767
+ if (!tattrs.class) {
768
+ tattrs.class = 'data_grid';
769
+ if (data_type.match(/^\w+$/)) tattrs.class += ' ' + data_type + '_grid';
770
+ }
771
+ if (!tattrs.style) tattrs.style = '';
772
+
773
+ if (args.grid_template_columns) tattrs.style += 'grid-template-columns: ' + args.grid_template_columns + ';';
774
+ else tattrs.style += 'grid-template-columns: repeat(' + cols.length + ', auto);';
775
+
776
+ html += '<div ' + compose_attribs(tattrs) + '>';
777
+
778
+ html += '<ul class="grid_row_header"><div>' + cols.join('</div><div>') + '</div></ul>';
779
+
780
+ for (var idx = 0, len = rows.length; idx < len; idx++) {
781
+ var row = rows[idx];
782
+ var tds = callback(row, idx);
783
+ if (tds.insertAbove) html += tds.insertAbove;
784
+ html += '<ul class="grid_row ' + (tds.className || '') + '"' + (row.id ? (' data-id="'+row.id+'"') : '') + '>';
785
+ html += '<div>' + tds.join('</div><div>') + '</div>';
786
+ html += '</ul>';
787
+ } // foreach row
788
+
789
+ if (!rows.length) {
790
+ html += '<ul class="grid_row_empty"><div style="grid-column-start: span ' + cols.length + ';">';
791
+ if (args.empty_msg) html += args.empty_msg;
792
+ else html += 'No '+pluralize(data_type)+' found.';
793
+ html += '</div></ul>';
794
+ }
795
+
796
+ if (args.below) html += args.below;
797
+
798
+ html += '</div>'; // scroll wrapper
799
+ html += '</div>'; // grid
800
+
801
+ return html;
802
+ }
803
+
804
+ getBasicTable() {
805
+ // get html for sorted table (fake pagination, for looks only)
806
+ var html = '';
807
+ var args = null;
808
+
809
+ if (arguments.length == 1) {
810
+ // custom args calling convention
811
+ args = arguments[0];
812
+ }
813
+ else {
814
+ // classic calling convention
815
+ args = {
816
+ rows: arguments[0],
817
+ cols: arguments[1],
818
+ data_type: arguments[2],
819
+ callback: arguments[3]
820
+ };
821
+ }
822
+
823
+ var rows = args.rows;
824
+ var cols = args.cols;
825
+ var data_type = args.data_type;
826
+ var callback = args.callback;
827
+
828
+ // pagination
829
+ if (!args.compact) {
830
+ html += '<div class="pagination">';
831
+ html += '<table cellspacing="0" cellpadding="0" border="0" width="100%"><tr>';
832
+
833
+ html += '<td align="left" width="33%">';
834
+ if (cols.headerLeft) html += cols.headerLeft;
835
+ else html += commify(rows.length) + ' ' + pluralize(data_type, rows.length) + '';
836
+ html += '</td>';
837
+
838
+ html += '<td align="center" width="34%">';
839
+ html += cols.headerCenter || '&nbsp;';
840
+ html += '</td>';
841
+
842
+ html += '<td align="right" width="33%">';
843
+ html += cols.headerRight || 'Page 1 of 1';
844
+ html += '</td>';
845
+
846
+ html += '</tr></table>';
847
+ html += '</div>';
848
+ }
849
+
850
+ html += '<div style="margin-top:5px; overflow-x:auto;">';
851
+
852
+ var tattrs = args.attribs || {};
853
+ if (!tattrs.class) tattrs.class = 'data_table ellip';
854
+ if (!tattrs.width) tattrs.width = '100%';
855
+ html += '<table ' + compose_attribs(tattrs) + '>';
856
+
857
+ html += '<tr><th style="white-space:nowrap;">' + cols.join('</th><th style="white-space:nowrap;">') + '</th></tr>';
858
+
859
+ for (var idx = 0, len = rows.length; idx < len; idx++) {
860
+ var row = rows[idx];
861
+ var tds = callback(row, idx);
862
+ if (tds.insertAbove) html += tds.insertAbove;
863
+ html += '<tr' + (tds.className ? (' class="'+tds.className+'"') : '') + (row.id ? (' data-id="'+row.id+'"') : '') + '>';
864
+ html += '<td>' + tds.join('</td><td>') + '</td>';
865
+ html += '</tr>';
866
+ } // foreach row
867
+
868
+ if (!rows.length) {
869
+ html += '<tr><td colspan="'+cols.length+'" align="center" style="padding-top:10px; padding-bottom:10px; font-weight:bold;">';
870
+ if (args.empty_msg) html += args.empty_msg;
871
+ else html += 'No '+pluralize(data_type)+' found.';
872
+ html += '</td></tr>';
873
+ }
874
+
875
+ html += '</table>';
876
+ html += '</div>';
877
+
878
+ return html;
879
+ }
880
+
881
+ getCompactTable(args, callback) {
882
+ // get html for compact table (sans pagination)
883
+ var html = '';
884
+
885
+ var rows = args.rows;
886
+ var cols = args.cols;
887
+ var data_type = args.data_type;
888
+
889
+ html += '<div class="data_table_compact" style="margin-top:5px; overflow-x:auto;">';
890
+
891
+ var tattrs = args.attribs || {};
892
+ if (!tattrs.class) tattrs.class = 'data_table compact';
893
+ if (!tattrs.width) tattrs.width = '100%';
894
+ html += '<table ' + compose_attribs(tattrs) + '>';
895
+
896
+ html += '<tr><th style="white-space:nowrap;">' + cols.join('</th><th style="white-space:nowrap;">') + '</th></tr>';
897
+
898
+ for (var idx = 0, len = rows.length; idx < len; idx++) {
899
+ var row = rows[idx];
900
+ var tds = callback(row, idx);
901
+ if (tds.insertAbove) html += tds.insertAbove;
902
+ html += '<tr' + (tds.className ? (' class="'+tds.className+'"') : '') + (row.id ? (' data-id="'+row.id+'"') : '') + '>';
903
+ html += '<td>' + tds.join('</td><td>') + '</td>';
904
+ html += '</tr>';
905
+ } // foreach row
906
+
907
+ if (!rows.length) {
908
+ html += '<tr><td colspan="'+cols.length+'" align="center" style="padding-top:10px; padding-bottom:10px; font-weight:bold;">';
909
+ if (args.empty_msg) html += args.empty_msg;
910
+ else html += 'No '+pluralize(data_type)+' found.';
911
+ html += '</td></tr>';
912
+ }
913
+ else if (args.append) {
914
+ html += args.append;
915
+ }
916
+
917
+ html += '</table>';
918
+ if (args.below) html += args.below;
919
+ html += '</div>';
920
+
921
+ return html;
922
+ }
923
+
924
+ getCompactGrid(args, callback) {
925
+ // get html for compact grid table (sans pagination)
926
+ // args: { rows, cols, data_type, attribs?, class?, style?, grid_template_columns?, empty_msg?, always_append_empty_msg?, below? }
927
+ var html = '';
928
+ var rows = args.rows;
929
+ var cols = args.cols;
930
+ var data_type = args.data_type;
931
+
932
+ html += '<div class="data_table_compact" style="margin-top:5px;">';
933
+
934
+ var tattrs = args.attribs || {};
935
+ if (args.class) tattrs.class = args.class;
936
+ if (!tattrs.class) {
937
+ tattrs.class = 'data_grid';
938
+ if (data_type.match(/^\w+$/)) tattrs.class += ' ' + data_type + '_grid';
939
+ }
940
+ if (!tattrs.style) tattrs.style = '';
941
+
942
+ if (args.grid_template_columns) tattrs.style += 'grid-template-columns: ' + args.grid_template_columns + ';';
943
+ else tattrs.style += 'grid-template-columns: repeat(' + cols.length + ', auto);';
944
+
945
+ html += '<div ' + compose_attribs(tattrs) + '>';
946
+
947
+ html += '<ul class="grid_row_header"><div>' + cols.join('</div><div>') + '</div></ul>';
948
+
949
+ for (var idx = 0, len = rows.length; idx < len; idx++) {
950
+ var row = rows[idx];
951
+ var tds = callback(row, idx);
952
+ if (tds.insertAbove) html += tds.insertAbove;
953
+ html += '<ul class="grid_row ' + (tds.className || '') + '"' + (row.id ? (' data-id="'+row.id+'"') : '') + '>';
954
+ html += '<div>' + tds.join('</div><div>') + '</div>';
955
+ html += '</ul>';
956
+ } // foreach row
957
+
958
+ if (!rows.length || (args.empty_msg && args.always_append_empty_msg)) {
959
+ html += '<ul class="grid_row_empty"><div style="grid-column-start: span ' + cols.length + ';">';
960
+ if (args.empty_msg) html += args.empty_msg;
961
+ else html += 'No '+pluralize(data_type)+' found.';
962
+ html += '</div></ul>';
963
+ }
964
+
965
+ if (args.below) html += args.below;
966
+
967
+ html += '</div>'; // grid
968
+ html += '</div>'; // data_table_compact
969
+
970
+ return html;
971
+ }
972
+
973
+ getBasicGrid() {
974
+ // get html for sorted grid table (fake pagination, for looks only)
975
+ var html = '';
976
+ var args = null;
977
+
978
+ if (arguments.length == 1) {
979
+ // custom args calling convention
980
+ args = arguments[0];
981
+ }
982
+ else if (arguments.length == 2) {
983
+ // combo args + callback
984
+ args = arguments[0];
985
+ args.callback = arguments[1];
986
+ }
987
+ else {
988
+ // classic calling convention
989
+ args = {
990
+ rows: arguments[0],
991
+ cols: arguments[1],
992
+ data_type: arguments[2],
993
+ callback: arguments[3]
994
+ };
995
+ }
996
+
997
+ var rows = args.rows;
998
+ var cols = args.cols;
999
+ var data_type = args.data_type;
1000
+ var callback = args.callback;
1001
+
1002
+ if (!args.hide_pagination) {
1003
+ // pagination
1004
+ html += '<div class="data_grid_pagination">';
1005
+
1006
+ html += '<div style="text-align:left">';
1007
+ if (cols.headerLeft) html += cols.headerLeft;
1008
+ else html += commify(rows.length) + ' ' + pluralize(data_type, rows.length) + '';
1009
+ html += '</div>';
1010
+
1011
+ html += '<div style="text-align:center">';
1012
+ html += cols.headerCenter || '&nbsp;';
1013
+ html += '</div>';
1014
+
1015
+ html += '<div style="text-align:right">';
1016
+ html += cols.headerRight || 'Page 1 of 1';
1017
+ html += '</div>';
1018
+
1019
+ html += '</div>';
1020
+
1021
+ html += '<div style="margin-top:5px;">';
1022
+ }
1023
+ else {
1024
+ // no pagination
1025
+ html += '<div>';
1026
+ }
1027
+
1028
+ var tattrs = args.attribs || {};
1029
+ if (args.class) tattrs.class = args.class;
1030
+ if (!tattrs.class) {
1031
+ tattrs.class = 'data_grid';
1032
+ if (data_type.match(/^\w+$/)) tattrs.class += ' ' + data_type + '_grid';
1033
+ }
1034
+ if (!tattrs.style) tattrs.style = '';
1035
+
1036
+ if (args.grid_template_columns) tattrs.style += 'grid-template-columns: ' + args.grid_template_columns + ';';
1037
+ else tattrs.style += 'grid-template-columns: repeat(' + cols.length + ', auto);';
1038
+
1039
+ html += '<div ' + compose_attribs(tattrs) + '>';
1040
+
1041
+ html += '<ul class="grid_row_header"><div>' + cols.join('</div><div>') + '</div></ul>';
1042
+
1043
+ for (var idx = 0, len = rows.length; idx < len; idx++) {
1044
+ var row = rows[idx];
1045
+ var tds = callback(row, idx);
1046
+ if (tds.insertAbove) html += tds.insertAbove;
1047
+ html += '<ul class="grid_row ' + (tds.className || '') + '"' + (row.id ? (' data-id="'+row.id+'"') : '') + '>';
1048
+ html += '<div>' + tds.join('</div><div>') + '</div>';
1049
+ html += '</ul>';
1050
+ } // foreach row
1051
+
1052
+ if (!rows.length) {
1053
+ html += '<ul class="grid_row_empty"><div style="grid-column-start: span ' + cols.length + ';">';
1054
+ if (args.empty_msg) html += args.empty_msg;
1055
+ else html += 'No '+pluralize(data_type)+' found.';
1056
+ html += '</div></ul>';
1057
+ }
1058
+
1059
+ if (args.below) html += args.below;
1060
+
1061
+ html += '</div>'; // scroll wrapper
1062
+ html += '</div>'; // grid
1063
+
1064
+ return html;
1065
+ }
1066
+
1067
+ requireLogin(args) {
1068
+ // user must be logged into to continue
1069
+ var self = this;
1070
+
1071
+ if (!app.user) {
1072
+ // require login
1073
+ app.navAfterLogin = this.ID;
1074
+ if (args && num_keys(args)) app.navAfterLogin += compose_query_string(args);
1075
+
1076
+ this.div.hide();
1077
+
1078
+ app.api.post( 'user/resume_session', {}, function(resp) {
1079
+ if (resp.user) {
1080
+ Debug.trace("User Session Resume: " + resp.username);
1081
+ Dialog.hideProgress();
1082
+ app.doUserLogin( resp );
1083
+ Nav.refresh();
1084
+ }
1085
+ else {
1086
+ Debug.trace("User cookie is invalid, redirecting to login page");
1087
+ self.setPref('username', '');
1088
+ setTimeout( function() { Nav.go('Login'); }, 1 );
1089
+ }
1090
+ } );
1091
+
1092
+ return false;
1093
+ }
1094
+ return true;
1095
+ }
1096
+
1097
+ hasPrivilege(priv_id) {
1098
+ // check if user has privilege
1099
+ if (!app.user || !app.user.privileges) return false;
1100
+ if (app.user.privileges.admin) return true;
1101
+ return( !!app.user.privileges[priv_id] );
1102
+ }
1103
+
1104
+ requireAnyPrivilege(...privs) {
1105
+ // check if user has priv, show full page error if not
1106
+ var privs_matched = privs.filter( (priv_id) => this.hasPrivilege(priv_id) ).length;
1107
+ if (!privs_matched) {
1108
+ this.doFullPageError("Your account does not have the required privileges to access this page.");
1109
+ return false;
1110
+ }
1111
+ return true;
1112
+ }
1113
+
1114
+ isAdmin() {
1115
+ // return true if user is logged in and admin, false otherwise
1116
+ // Note: This is used for UI decoration ONLY -- all privileges are checked on the server
1117
+ return( app.user && app.user.privileges && app.user.privileges.admin );
1118
+ }
1119
+
1120
+ getNiceAPIKey(item, link) {
1121
+ if (!item) return 'n/a';
1122
+ var key = item.api_key || item.key;
1123
+ var title = item.api_title || item.title;
1124
+
1125
+ var html = '';
1126
+ var icon = '<i class="mdi mdi-key">&nbsp;</i>';
1127
+ if (link) {
1128
+ if (link === true) link = '#APIKeys?sub=edit&id=' + item.id;
1129
+ html += '<a href="' + link + '" style="text-decoration:none">';
1130
+ html += icon + '<span style="text-decoration:underline">' + title + '</span></a>';
1131
+ }
1132
+ else {
1133
+ html += icon + title;
1134
+ }
1135
+
1136
+ return html;
1137
+ }
1138
+
1139
+ getNiceUsername(user, link) {
1140
+ if (!user) return 'n/a';
1141
+ if ((typeof(user) == 'object') && (user.key || user.api_title)) {
1142
+ return this.getNiceAPIKey(user, link);
1143
+ }
1144
+ var username = user.username ? user.username : user;
1145
+ if (!username || (typeof(username) != 'string')) return 'n/a';
1146
+
1147
+ var html = '';
1148
+ var icon = '<i class="mdi mdi-account">&nbsp;</i>';
1149
+ if (link) {
1150
+ if (link === true) link = '#Users?sub=edit&username=' + username;
1151
+ html += '<a href="' + link + '" style="text-decoration:none">';
1152
+ html += icon + '<span style="text-decoration:underline">' + username + '</span></a>';
1153
+ }
1154
+ else {
1155
+ html += icon + username;
1156
+ }
1157
+
1158
+ return html;
1159
+ }
1160
+
1161
+ getNiceEnvironment(item, link) {
1162
+ // get formatted env with icon, plus optional link
1163
+ if (!item) return '(None)';
1164
+
1165
+ var html = '';
1166
+ var icon = '<i class="mdi mdi-wan">&nbsp;</i>';
1167
+ if (link) {
1168
+ if (link === true) link = '#Environments?sub=edit&id=' + item.id;
1169
+ html += '<a href="' + link + '" style="text-decoration:none">';
1170
+ html += icon + '<span style="text-decoration:underline">' + item.title + '</span></a>';
1171
+ }
1172
+ else {
1173
+ html += icon + item.title;
1174
+ }
1175
+
1176
+ return html;
1177
+ }
1178
+
1179
+ getNiceGroupList(groups, glue, max) {
1180
+ // get formatted group list
1181
+ var self = this;
1182
+ if (!glue) glue = ', ';
1183
+ if (typeof(groups) == 'string') groups = groups.split(/\,\s*/);
1184
+ if (!groups || !groups.length) return '(None)';
1185
+ if (max && (groups.length > max)) {
1186
+ var extras = groups.length - max;
1187
+ groups = groups.slice(0, max);
1188
+ return groups.map( function(group) { return self.getNiceGroup(group); } ).join(glue) + glue + ' and ' + extras + ' more';
1189
+ }
1190
+ return groups.map( function(group) { return self.getNiceGroup(group); } ).join(glue);
1191
+ }
1192
+
1193
+ getNiceGroup(item, link) {
1194
+ // get formatted group with icon, plus optional link
1195
+ if (!item) return '(None)';
1196
+
1197
+ var html = '';
1198
+ var icon = '<i class="mdi mdi-server-network">&nbsp;</i>';
1199
+ if (link) {
1200
+ if (link === true) link = '#Groups?sub=edit&id=' + item.id;
1201
+ html += '<a href="' + link + '" style="text-decoration:none">';
1202
+ html += icon + '<span style="text-decoration:underline">' + item.title + '</span></a>';
1203
+ }
1204
+ else {
1205
+ html += icon + item.title;
1206
+ }
1207
+
1208
+ return html;
1209
+ }
1210
+
1211
+ setGroupVisible(group, visible) {
1212
+ // set web groups of form fields visible or invisible,
1213
+ // according to master checkbox for each section
1214
+ var selector = 'tr.' + group + 'group';
1215
+ if (visible) {
1216
+ if ($(selector).hasClass('collapse')) {
1217
+ $(selector).hide().removeClass('collapse');
1218
+ }
1219
+ $(selector).show(250);
1220
+ }
1221
+ else $(selector).hide(250);
1222
+
1223
+ return this; // for chaining
1224
+ }
1225
+
1226
+ checkUserExists(field) {
1227
+ // check if user exists, update UI checkbox
1228
+ // called after field changes
1229
+ var self = this;
1230
+ var $field = $(field);
1231
+ var username = trim( $field.val().toLowerCase() );
1232
+ var $elem = $field.closest('.form_row').find('.fr_suffix .checker');
1233
+
1234
+ if (username.match(/^[\w\-\.]+$/)) {
1235
+ // check with server
1236
+ app.api.get('app/check_user_exists', { username: username }, function(resp) {
1237
+ if (!self.active) return; // sanity
1238
+
1239
+ if (resp.user_exists) {
1240
+ // username taken
1241
+ $elem.css('color','red').html('<span class="mdi mdi-alert-circle"></span>').attr('title', "Username is taken.");
1242
+ $field.addClass('warning');
1243
+ }
1244
+ else if (resp.user_invalid) {
1245
+ // bad username
1246
+ $elem.css('color', 'red').html('<span class="mdi mdi-alert-decagram"></span>').attr('title', "Username is malformed.");
1247
+ $field.addClass('warning');
1248
+ }
1249
+ else {
1250
+ // username is valid and available!
1251
+ $elem.css('color','green').html('<span class="mdi mdi-check-circle"></span>').attr('title', "Username available!");
1252
+ $field.removeClass('warning');
1253
+ }
1254
+ } );
1255
+ }
1256
+ else if (username.length) {
1257
+ // bad username
1258
+ $elem.css('color','red').html('<span class="mdi mdi-alert-decagram"></span>').attr('title', "Username is malformed.");
1259
+ $field.addClass('warning');
1260
+ }
1261
+ else {
1262
+ // empty
1263
+ $elem.html('').removeAttr('title');
1264
+ $field.removeClass('warning');
1265
+ }
1266
+ }
1267
+
1268
+ checkAddRemoveMe(sel) {
1269
+ // check if user's e-mail is contained in text field or not
1270
+ // expects sel to point to the input
1271
+ var $elem = $(sel);
1272
+ var value = $elem.val().toLowerCase();
1273
+ var email = app.user.email.toLowerCase();
1274
+ var regexp = new RegExp( "\\b" + escape_regexp(email) + "\\b" );
1275
+ return !!value.match(regexp);
1276
+ }
1277
+
1278
+ updateAddRemoveMe(sel) {
1279
+ // update add/remove me text based on if user's e-mail is contained in text field
1280
+ // expects sel to point to the input(s)
1281
+ var self = this;
1282
+
1283
+ $(sel).each( function() {
1284
+ var $elem = $(this);
1285
+ var $suffix = $elem.closest('div.form_row').find('div.form_suffix_icon');
1286
+
1287
+ if (self.checkAddRemoveMe(this)) {
1288
+ $suffix.removeClass('mdi-account-plus').addClass('mdi-account-minus').attr('title', "Remove Me");
1289
+ }
1290
+ else {
1291
+ $suffix.removeClass('mdi-account-minus').addClass('mdi-account-plus').attr('title', "Add Me");
1292
+ }
1293
+ } );
1294
+ }
1295
+
1296
+ addRemoveMe(sel) {
1297
+ // toggle user's e-mail in/out of text field
1298
+ // expects sel to point to the div.form_suffix_icon
1299
+ var $suffix = $(sel);
1300
+ var $elem = $suffix.closest('div.form_row').find('input');
1301
+ var value = trim( $elem.val().replace(/\,\s*\,/g, ',').replace(/^\s*\,\s*/, '').replace(/\s*\,\s*$/, '') );
1302
+
1303
+ if (this.checkAddRemoveMe( $elem[0] )) {
1304
+ // remove e-mail
1305
+ var email = app.user.email.toLowerCase();
1306
+ var regexp = new RegExp( "\\b" + escape_regexp(email) + "\\b", "i" );
1307
+ value = value.replace( regexp, '' ).replace(/\,\s*\,/g, ',').replace(/^\s*\,\s*/, '').replace(/\s*\,\s*$/, '');
1308
+ $elem.val( trim(value) );
1309
+ }
1310
+ else {
1311
+ // add email
1312
+ if (value) value += ', ';
1313
+ $elem.val( value + app.user.email );
1314
+ }
1315
+
1316
+ this.updateAddRemoveMe( $elem[0] );
1317
+ }
1318
+
1319
+ get_custom_combo_unit_box(id, value, items, class_name) {
1320
+ // get HTML for custom combo text/menu, where menu defines units of measurement
1321
+ // items should be array for use in render_menu_options(), with an increasing numerical value
1322
+ if (!class_name) class_name = 'std_combo_unit_table';
1323
+ var units = 0;
1324
+ var value = parseInt( value || 0 );
1325
+
1326
+ for (var idx = items.length - 1; idx >= 0; idx--) {
1327
+ var max = items[idx][0];
1328
+ if ((value >= max) && (value % max == 0)) {
1329
+ units = max;
1330
+ value = Math.floor( value / units );
1331
+ idx = -1;
1332
+ }
1333
+ }
1334
+ if (!units) {
1335
+ // no exact match, so default to first unit in list
1336
+ units = items[0][0];
1337
+ value = Math.floor( value / units );
1338
+ }
1339
+
1340
+ return (
1341
+ '<table cellspacing="0" cellpadding="0" class="'+class_name+'"><tr>' +
1342
+ '<td style="padding:0"><input type="text" id="'+id+'" style="width:30px;" value="'+value+'"/></td>' +
1343
+ '<td style="padding:0"><select id="'+id+'_units">' + render_menu_options(items, units) + '</select></td>' +
1344
+ '</tr></table>'
1345
+ );
1346
+ }
1347
+
1348
+ get_relative_time_combo_box(id, value, class_name, inc_seconds) {
1349
+ // get HTML for combo textfield/menu for a relative time based input
1350
+ // provides Minutes, Hours and Days units
1351
+ var unit_items = [[60,'Minutes'], [3600,'Hours'], [86400,'Days']];
1352
+ if (inc_seconds) unit_items.unshift( [1,'Seconds'] );
1353
+
1354
+ return this.get_custom_combo_unit_box( id, value, unit_items, class_name );
1355
+ }
1356
+
1357
+ get_relative_size_combo_box(id, value, class_name) {
1358
+ // get HTML for combo textfield/menu for a relative size based input
1359
+ // provides MB, GB and TB units
1360
+ var TB = 1024 * 1024 * 1024 * 1024;
1361
+ var GB = 1024 * 1024 * 1024;
1362
+ var MB = 1024 * 1024;
1363
+
1364
+ return this.get_custom_combo_unit_box( id, value, [[MB,'MB'], [GB,'GB'], [TB,'TB']], class_name );
1365
+ }
1366
+
1367
+ setupDraggableTable(args) {
1368
+ // allow table rows to be drag-sorted
1369
+ // args: { table_sel, handle_sel, drag_ghost_sel, drag_ghost_x, drag_ghost_y, callback }
1370
+ var $table = $(args.table_sel);
1371
+ var $rows = $table.find('tr').slice(1); // omit header row
1372
+ var $cur = null;
1373
+
1374
+ var createDropZone = function($tr, idx, pos) {
1375
+ pos.top -= Math.floor( pos.height / 2 );
1376
+
1377
+ $('<div><div class="dz_bar"></div></div>')
1378
+ .addClass('dropzone')
1379
+ .css({
1380
+ left: '' + pos.left + 'px',
1381
+ top: '' + pos.top + 'px',
1382
+ width: '' + pos.width + 'px',
1383
+ height: '' + pos.height + 'px'
1384
+ })
1385
+ .appendTo('body')
1386
+ .on('dragover', function(event) {
1387
+ var e = event.originalEvent;
1388
+ e.preventDefault();
1389
+ e.dataTransfer.effectAllowed = "move";
1390
+ })
1391
+ .on('dragenter', function(event) {
1392
+ var e = event.originalEvent;
1393
+ e.preventDefault();
1394
+ $(this).addClass('drag');
1395
+ })
1396
+ .on('dragleave', function(event) {
1397
+ $(this).removeClass('drag');
1398
+ })
1399
+ .on('drop', function(event) {
1400
+ var e = event.originalEvent;
1401
+ e.preventDefault();
1402
+
1403
+ // make sure we didn't drop on ourselves
1404
+ if (idx == $cur.data('drag_idx')) return false;
1405
+
1406
+ // see if we need to insert above or below target
1407
+ var above = true;
1408
+ var pos = $tr.offset();
1409
+ var height = $tr.height();
1410
+ var y = event.clientY + window.scrollY;
1411
+ if (y > pos.top + (height / 2)) above = false;
1412
+
1413
+ // remove element being dragged
1414
+ $cur.detach();
1415
+
1416
+ // insert at new location
1417
+ if (above) $tr.before( $cur );
1418
+ else $tr.after( $cur );
1419
+
1420
+ // fire callback, pass new sorted collection
1421
+ args.callback( $table.find('tr').slice(1) );
1422
+ });
1423
+ }; // createDropZone
1424
+
1425
+ $rows.each( function(row_idx) {
1426
+ var $handle = $(this).find(args.handle_sel);
1427
+
1428
+ $handle.on('dragstart', function(event) {
1429
+ var e = event.originalEvent;
1430
+ var $tr = $cur = $(this).closest('tr');
1431
+ var $ghost = $tr.find(args.drag_ghost_sel).addClass('dragging');
1432
+ var ghost_x = ('drag_ghost_x' in args) ? args.drag_ghost_x : Math.floor($ghost.width() / 2);
1433
+ var ghost_y = ('drag_ghost_y' in args) ? args.drag_ghost_y : Math.floor($ghost.height() / 2);
1434
+
1435
+ e.dataTransfer.setDragImage( $ghost.get(0), ghost_x, ghost_y );
1436
+ e.dataTransfer.effectAllowed = 'move';
1437
+ e.dataTransfer.setData('text/html', 'blah'); // needed for FF.
1438
+
1439
+ // need to recalc $rows for each drag
1440
+ $rows = $table.find('tr').slice(1);
1441
+
1442
+ $rows.each( function(idx) {
1443
+ var $tr = $(this);
1444
+ $tr.data('drag_idx', idx);
1445
+ });
1446
+
1447
+ // and we need to recalc row_idx too
1448
+ var row_idx = $tr.data('drag_idx');
1449
+
1450
+ // create drop zones for each row
1451
+ // (except those immedately surrounding the row we picked up)
1452
+ $rows.each( function(idx) {
1453
+ var $tr = $(this);
1454
+ if ((idx != row_idx) && (idx != row_idx + 1)) {
1455
+ var pos = $tr.offset();
1456
+ pos.width = $tr.width();
1457
+ pos.height = $tr.height();
1458
+ createDropZone( $tr, idx, pos );
1459
+ }
1460
+ });
1461
+
1462
+ // one final zone below table (possibly)
1463
+ if (row_idx != $rows.length - 1) {
1464
+ var $last_tr = $rows.slice(-1);
1465
+ var pos = $last_tr.offset();
1466
+ pos.width = $last_tr.width();
1467
+ pos.height = $last_tr.height();
1468
+ pos.top += pos.height;
1469
+ createDropZone( $last_tr, $rows.length, pos );
1470
+ }
1471
+ }); // dragstart
1472
+
1473
+ $handle.on('dragend', function(event) {
1474
+ // cleanup drop zones
1475
+ $('div.dropzone').remove();
1476
+ $rows.removeData('drag_idx');
1477
+ $table.find('.dragging').removeClass('dragging');
1478
+ }); // dragend
1479
+
1480
+ } ); // foreach row
1481
+ }
1482
+
1483
+ cancelDrag(table_sel) {
1484
+ // cancel drag operation in progress (well, as best we can)
1485
+ var $table = $(table_sel);
1486
+ if (!$table.length) return;
1487
+
1488
+ var $rows = $table.find('tr').slice(1); // omit header row
1489
+ $('div.dropzone').remove();
1490
+ $rows.removeData('drag_idx');
1491
+ $table.find('.dragging').removeClass('dragging');
1492
+ }
1493
+
1494
+ setupDraggableGrid(args) {
1495
+ // allow grid rows to be drag-sorted
1496
+ // args: { table_sel, handle_sel, drag_ghost_sel, drag_ghost_x, drag_ghost_y, callback }
1497
+ var $table = $(args.table_sel);
1498
+ var $rows = $table.find('ul.grid_row');
1499
+ var $cur = null;
1500
+
1501
+ var createDropZone = function($tr, idx, pos) {
1502
+ pos.top -= Math.floor( pos.height / 2 );
1503
+
1504
+ $('<div><div class="dz_bar"></div></div>')
1505
+ .addClass('dropzone')
1506
+ .css({
1507
+ left: '' + pos.left + 'px',
1508
+ top: '' + pos.top + 'px',
1509
+ width: '' + pos.width + 'px',
1510
+ height: '' + pos.height + 'px'
1511
+ })
1512
+ .appendTo('body')
1513
+ .on('dragover', function(event) {
1514
+ var e = event.originalEvent;
1515
+ e.preventDefault();
1516
+ e.dataTransfer.effectAllowed = "move";
1517
+ })
1518
+ .on('dragenter', function(event) {
1519
+ var e = event.originalEvent;
1520
+ e.preventDefault();
1521
+ $(this).addClass('drag');
1522
+ })
1523
+ .on('dragleave', function(event) {
1524
+ $(this).removeClass('drag');
1525
+ })
1526
+ .on('drop', function(event) {
1527
+ var e = event.originalEvent;
1528
+ e.preventDefault();
1529
+
1530
+ // make sure we didn't drop on ourselves
1531
+ if (idx == $cur.data('drag_idx')) return false;
1532
+
1533
+ // see if we need to insert above or below target
1534
+ var above = true;
1535
+ var bounds = $tr.innerBounds();
1536
+ var y = event.clientY + window.scrollY;
1537
+ if (y > bounds.top + (bounds.height / 2)) above = false;
1538
+
1539
+ // remove element being dragged
1540
+ $cur.detach();
1541
+
1542
+ // insert at new location
1543
+ if (above) $tr.before( $cur );
1544
+ else $tr.after( $cur );
1545
+
1546
+ // fire callback, pass new sorted collection
1547
+ args.callback( $table.find('ul.grid_row') );
1548
+ });
1549
+ }; // createDropZone
1550
+
1551
+ $rows.each( function(row_idx) {
1552
+ var $handle = $(this).find(args.handle_sel);
1553
+
1554
+ $handle.on('dragstart', function(event) {
1555
+ var e = event.originalEvent;
1556
+ var $tr = $cur = $(this).closest('ul');
1557
+ var $ghost = $tr.find(args.drag_ghost_sel).addClass('dragging');
1558
+ var ghost_x = ('drag_ghost_x' in args) ? args.drag_ghost_x : Math.floor($ghost.width() / 2);
1559
+ var ghost_y = ('drag_ghost_y' in args) ? args.drag_ghost_y : Math.floor($ghost.height() / 2);
1560
+
1561
+ e.dataTransfer.setDragImage( $ghost.get(0), ghost_x, ghost_y );
1562
+ e.dataTransfer.effectAllowed = 'move';
1563
+ e.dataTransfer.setData('text/html', 'blah'); // needed for FF.
1564
+
1565
+ // need to recalc $rows for each drag
1566
+ $rows = $table.find('ul.grid_row');
1567
+
1568
+ $rows.each( function(idx) {
1569
+ var $tr = $(this);
1570
+ $tr.data('drag_idx', idx);
1571
+ });
1572
+
1573
+ // and we need to recalc row_idx too
1574
+ var row_idx = $tr.data('drag_idx');
1575
+
1576
+ // create drop zones for each row
1577
+ // (except those immedately surrounding the row we picked up)
1578
+ $rows.each( function(idx) {
1579
+ var $tr = $(this);
1580
+ if ((idx != row_idx) && (idx != row_idx + 1)) {
1581
+ var bounds = $tr.innerBounds();
1582
+ createDropZone( $tr, idx, bounds );
1583
+ }
1584
+ });
1585
+
1586
+ // one final zone below table (possibly)
1587
+ if (row_idx != $rows.length - 1) {
1588
+ var $last_tr = $rows.slice(-1);
1589
+ var bounds = $last_tr.innerBounds();
1590
+ bounds.top += bounds.height;
1591
+ createDropZone( $last_tr, $rows.length, bounds );
1592
+ }
1593
+ }); // dragstart
1594
+
1595
+ $handle.on('dragend', function(event) {
1596
+ // cleanup drop zones
1597
+ $('div.dropzone').remove();
1598
+ $rows.removeData('drag_idx');
1599
+ $table.find('.dragging').removeClass('dragging');
1600
+ }); // dragend
1601
+
1602
+ } ); // foreach row
1603
+ }
1604
+
1605
+ cancelGridDrag(table_sel) {
1606
+ // cancel drag operation in progress (well, as best we can)
1607
+ var $table = $(table_sel);
1608
+ if (!$table.length) return;
1609
+
1610
+ var $rows = $table.find('ul.grid_row');
1611
+ $('div.dropzone').remove();
1612
+ $rows.removeData('drag_idx');
1613
+ $table.find('.dragging').removeClass('dragging');
1614
+ }
1615
+
1616
+ doFullPageError(msg) {
1617
+ // show full page error
1618
+ this.fullPageError({ description: msg });
1619
+ }
1620
+
1621
+ fullPageError(resp) {
1622
+ // show "full page" inline error dialog
1623
+ // suitable for binding to the API errorCallback
1624
+ var html = '';
1625
+ html += '<div style="height:75px;"></div>';
1626
+
1627
+ html += '<div class="box" style="padding:30px">';
1628
+ html += '<div class="box_title error">' + (resp.title || 'An Error Occurred') + '</div>';
1629
+ html += '<div class="box_content" style="font-size:14px;">' + resp.description + '</div>';
1630
+ html += '</div>';
1631
+
1632
+ html += '<div style="height:75px;"></div>';
1633
+ this.div.html(html);
1634
+
1635
+ app.showSidebar(true);
1636
+ app.setWindowTitle( "Error" );
1637
+ app.setHeaderTitle( '<i class="mdi mdi-alert-circle-outline">&nbsp;</i>Error' );
1638
+ }
1639
+
1640
+ }; // class Page
1641
+
1642
+ //
1643
+ // Page Manager
1644
+ //
1645
+
1646
+ window.PageManager = class PageManager {
1647
+ // 'PageManager' class handles all virtual pages in the application
1648
+
1649
+ // methods
1650
+ constructor(page_list) {
1651
+ // class constructor, create all pages
1652
+ // page_list should be array of components from master config
1653
+ // each one should have at least a 'ID' parameter
1654
+ // anything else is copied into object verbatim
1655
+ this.pages = [];
1656
+ this.page_list = page_list;
1657
+ this.current_page_id = '';
1658
+ var $main = $('div.main');
1659
+
1660
+ for (var idx = 0, len = page_list.length; idx < len; idx++) {
1661
+ var page_def = page_list[idx];
1662
+ Debug.trace( 'page', "Initializing page: " + page_def.ID );
1663
+ assert(Page[ page_def.ID ], "Page class not found: Page." + page_def.ID);
1664
+
1665
+ var div = $('<div></div>')
1666
+ .prop('id', 'page_' + page_def.ID)
1667
+ .addClass('page')
1668
+ .css('display', 'none');
1669
+ $main.append(div);
1670
+
1671
+ var page = new Page[ page_def.ID ]( page_def, div );
1672
+ page.args = {};
1673
+ page.onInit();
1674
+ this.pages.push(page);
1675
+
1676
+ // add click handler for tab
1677
+ $('#tab_'+page.ID).click( function(event) {
1678
+ Nav.go( this._page_id );
1679
+ } )[0]._page_id = page.ID;
1680
+ }
1681
+
1682
+ this.initSidebar();
1683
+ }
1684
+
1685
+ find(id) {
1686
+ // locate page by ID (i.e. Plugin Name)
1687
+ var page = find_object( this.pages, { ID: id } );
1688
+ if (!page) Debug.trace('PageManager', "Could not find page: " + id);
1689
+ return page;
1690
+ }
1691
+
1692
+ activate(id, old_id, args) {
1693
+ // send activate event to page by id (i.e. Plugin Name)
1694
+ $('#page_'+id).show();
1695
+ $('.sidebar .section_item').removeClass('active').addClass('inactive');
1696
+ $('#tab_'+id).removeClass('inactive').addClass('active');
1697
+ var page = this.find(id);
1698
+ page.active = true;
1699
+
1700
+ if (!args) args = {};
1701
+
1702
+ // if we are navigating here from a different page, AND the new sub mismatches the old sub, clear the page html
1703
+ var new_sub = args.sub || '';
1704
+ if (old_id && (id != old_id) && (typeof(page._old_sub) != 'undefined') && (new_sub != page._old_sub) && page.div) {
1705
+ page.div.html('');
1706
+ }
1707
+
1708
+ var result = page.onActivate.apply(page, [args]);
1709
+ if (typeof(result) == 'boolean') return result;
1710
+ else throw("Page " + id + " onActivate did not return a boolean!");
1711
+
1712
+ // expand section if applicable -- TODO: unreachable code:
1713
+ var $sect = $('#tab_'+id).parent().prev();
1714
+ if ($sect.length && $sect.hasClass('section_title')) this.expandSidebarGroup( $sect );
1715
+ }
1716
+
1717
+ deactivate(id, new_id) {
1718
+ // send deactivate event to page by id (i.e. Plugin Name)
1719
+ var page = this.find(id);
1720
+ var result = page.onDeactivate(new_id);
1721
+ if (result) {
1722
+ $('#page_'+id).hide();
1723
+ $('#tab_'+id).removeClass('active').addClass('inactive');
1724
+ // $('#d_message').hide();
1725
+ page.active = false;
1726
+
1727
+ // if page has args.sub, save it for clearing html on reactivate, if page AND sub are different
1728
+ if (page.args) page._old_sub = page.args.sub || '';
1729
+ }
1730
+ return result;
1731
+ }
1732
+
1733
+ click(id, args) {
1734
+ // exit current page and enter specified page
1735
+ Debug.trace('page', "Switching pages to: " + id);
1736
+ var old_id = this.current_page_id;
1737
+ if (this.current_page_id) {
1738
+ var result = this.deactivate( this.current_page_id, id );
1739
+ if (!result) return false; // current page said no
1740
+ }
1741
+ this.current_page_id = id;
1742
+ this.old_page_id = old_id;
1743
+
1744
+ window.scrollTo( 0, 0 );
1745
+
1746
+ var result = this.activate(id, old_id, args);
1747
+ if (!result) {
1748
+ // new page has rejected activation, probably because a login is required
1749
+ // un-hide previous page div, but don't call activate on it
1750
+ $('#page_'+id).hide();
1751
+ this.current_page_id = '';
1752
+ // if (old_id) {
1753
+ // $('page_'+old_id).show();
1754
+ // this.current_page_id = old_id;
1755
+ // }
1756
+ }
1757
+
1758
+ return true;
1759
+ }
1760
+
1761
+ // Sidebar
1762
+
1763
+ initSidebar() {
1764
+ // setup sidebar "tabs"
1765
+ var self = this;
1766
+ $('.sidebar .section_title').off('mouseup').on('mouseup', function() {
1767
+ self.toggleSidebarGroup(this);
1768
+ });
1769
+
1770
+ // set initial state
1771
+ $('.sidebar .section_title').each( function() {
1772
+ var $sect = $(this);
1773
+ if (!$sect.hasClass('expanded')) {
1774
+ $sect.next().css('height', 0).scrollTop( $sect.next()[0].scrollHeight );
1775
+ }
1776
+ });
1777
+ }
1778
+
1779
+ toggleSidebarGroup(sect) {
1780
+ // toggle sidebar group open/closed
1781
+ var $sect = $(sect);
1782
+ if ($sect.hasClass('expanded')) this.collapseSidebarGroup(sect);
1783
+ else this.expandSidebarGroup(sect);
1784
+ }
1785
+
1786
+ collapseSidebarGroup(sect) {
1787
+ // collapse tab section in sidebar
1788
+ var $sect = $(sect);
1789
+ if ($sect.hasClass('expanded')) {
1790
+ $sect.removeClass('expanded');
1791
+ $sect.next().stop().animate({
1792
+ scrollTop: $sect.next()[0].scrollHeight,
1793
+ height: 0
1794
+ }, {
1795
+ duration: 500,
1796
+ easing: 'easeOutQuart'
1797
+ });
1798
+ }
1799
+ }
1800
+
1801
+ expandSidebarGroup(sect) {
1802
+ // expand tab section in sidebar
1803
+ var $sect = $(sect);
1804
+ if (!$sect.hasClass('expanded')) {
1805
+ $sect.addClass('expanded');
1806
+ $sect.next().stop().animate({
1807
+ scrollTop: 0,
1808
+ height: $sect.next()[0].scrollHeight
1809
+ }, {
1810
+ duration: 500,
1811
+ easing: 'easeOutQuart'
1812
+ });
1813
+ }
1814
+ }
1815
+
1816
+ }; // class PageManager
1817
+
1818
+ var Nav = {
1819
+
1820
+ /**
1821
+ * Virtual Page Navigation System
1822
+ **/
1823
+
1824
+ loc: '',
1825
+ old_loc: '',
1826
+ inited: false,
1827
+ nodes: [],
1828
+
1829
+ init: function() {
1830
+ // initialize nav system
1831
+ assert( window.config, "window.config not present.");
1832
+
1833
+ if (!this.inited) {
1834
+ this.inited = true;
1835
+ this.loc = 'init';
1836
+ this.monitor();
1837
+
1838
+ window.addEventListener("hashchange", function(event) {
1839
+ Nav.monitor();
1840
+ }, false);
1841
+ }
1842
+ },
1843
+
1844
+ monitor: function() {
1845
+ // monitor browser location and activate handlers as needed
1846
+ var parts = window.location.href.split(/\#/);
1847
+ var anchor = parts[1];
1848
+ if (!anchor) anchor = config.DefaultPage || 'Main';
1849
+
1850
+ var full_anchor = '' + anchor;
1851
+ var sub = '';
1852
+
1853
+ if (anchor.match(/^(.+?)\/(.+)$/)) {
1854
+ // inline section anchor after page name, slash delimited
1855
+ anchor = RegExp.$1;
1856
+ sub = RegExp.$2;
1857
+ }
1858
+
1859
+ if ((anchor != this.loc) && !anchor.match(/^_/)) { // ignore doxter anchors
1860
+ Debug.trace('nav', "Caught navigation anchor: " + full_anchor);
1861
+
1862
+ var page_name = '';
1863
+ var page_args = {};
1864
+ if (full_anchor.match(/^\w+\?.+/)) {
1865
+ parts = full_anchor.split(/\?/);
1866
+ page_name = parts[0];
1867
+ page_args = parse_query_string( parts[1] );
1868
+ }
1869
+ else {
1870
+ parts = full_anchor.split(/\//);
1871
+ page_name = parts[0];
1872
+ page_args = {};
1873
+ if (sub) page_args.sub = sub;
1874
+ }
1875
+
1876
+ Debug.trace('nav', "Calling page: " + page_name + ": " + JSON.stringify(page_args));
1877
+ Dialog.hide();
1878
+ // app.hideMessage();
1879
+ app.pushSidebar();
1880
+
1881
+ var result = app.page_manager.click( page_name, page_args );
1882
+ if (result) {
1883
+ this.old_loc = this.loc;
1884
+ if (this.old_loc == 'init') this.old_loc = config.DefaultPage || 'Main';
1885
+ this.loc = anchor;
1886
+ app.notifyUserNav(this.loc);
1887
+ }
1888
+ else {
1889
+ // current page aborted navigation -- recover current page without refresh
1890
+ this.go( this.loc );
1891
+ }
1892
+ }
1893
+ else if (sub != this.sub_anchor) {
1894
+ Debug.trace('nav', "Caught sub-anchor nav: " + sub);
1895
+ $P().gosub( sub );
1896
+ } // sub-anchor changed
1897
+
1898
+ this.sub_anchor = sub;
1899
+ },
1900
+
1901
+ go: function(anchor, force) {
1902
+ // navigate to page
1903
+ anchor = anchor.replace(/^\#/, '');
1904
+ if (force) {
1905
+ if (anchor == this.loc) {
1906
+ this.loc = 'init';
1907
+ this.monitor();
1908
+ }
1909
+ else {
1910
+ this.loc = 'init';
1911
+ window.location.href = '#' + anchor;
1912
+ }
1913
+ }
1914
+ else {
1915
+ window.location.href = '#' + anchor;
1916
+ }
1917
+ },
1918
+
1919
+ prev: function() {
1920
+ // return to previous page
1921
+ this.go( this.old_loc || config.DefaultPage || 'Main' );
1922
+ },
1923
+
1924
+ refresh: function() {
1925
+ // re-nav to current page
1926
+ this.loc = 'refresh';
1927
+ this.monitor();
1928
+ },
1929
+
1930
+ currentAnchor: function() {
1931
+ // return current page anchor
1932
+ var parts = window.location.href.split(/\#/);
1933
+ var anchor = parts[1] || '';
1934
+
1935
+ anchor = anchor.replace(/\/.+$/, '');
1936
+
1937
+ return anchor;
1938
+ }
1939
+
1940
+ }; // Nav