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/base.js ADDED
@@ -0,0 +1,648 @@
1
+ // Base App Framework
2
+
3
+ var app = {
4
+
5
+ username: '',
6
+ cacheBust: 0,
7
+ proto: location.protocol.match(/^https/i) ? 'https://' : 'http://',
8
+ secure: !!location.protocol.match(/^https/i),
9
+ retina: (window.devicePixelRatio > 1),
10
+ mobile: !!navigator.userAgent.match(/(iOS|iPhone|iPad|Android)/),
11
+ base_api_url: '/api',
12
+ plain_text_post: false,
13
+ prefs: {},
14
+ lastClick: {},
15
+
16
+ init: function() {
17
+ // override this in your app.js
18
+ },
19
+
20
+ extend: function(obj) {
21
+ // extend app object with another
22
+ for (var key in obj) this[key] = obj[key];
23
+ },
24
+
25
+ setAPIBaseURL: function(url) {
26
+ // set the API base URL (commands are appended to this)
27
+ this.base_api_url = url;
28
+ },
29
+
30
+ setWindowTitle: function(title) {
31
+ // set the current window title, includes app name
32
+ document.title = title + ' | ' + this.name;
33
+ },
34
+
35
+ setHeaderTitle: function(title) {
36
+ // set header title
37
+ $('.header_title').html( title );
38
+ },
39
+
40
+ setHeaderNav: function(items) {
41
+ // populate header with multiple nav elements
42
+ var html = '<div class="header_nav_cont">';
43
+
44
+ items.forEach( function(item, idx) {
45
+ if (typeof(item) == 'string') {
46
+ if (config.ui.nav[item]) item = config.ui.nav[item];
47
+ else { html += item; return; }
48
+ } // custom
49
+ if (!item.type && (idx > 0)) html += '<div class="header_nav_sep"><i class="mdi mdi-chevron-right"></i></div>';
50
+ if (item.loc) item.type = 'link';
51
+
52
+ switch (item.type) {
53
+ case 'badge':
54
+ html += '<div class="color_label ' + item.color + '">';
55
+ if (item.icon) html += '<i class="mdi mdi-' + item.icon + '">&nbsp;</i>';
56
+ html += item.title + '</div>';
57
+ break;
58
+
59
+ case 'link':
60
+ html += '<a class="header_nav_item" href="' + item.loc + '">';
61
+ if (item.icon) html += '<i class="mdi mdi-' + item.icon + '"></i>';
62
+ html += '<span>' + item.title + '</span></a>';
63
+ break;
64
+
65
+ default:
66
+ html += '<div class="header_nav_item">';
67
+ if (item.icon) html += '<i class="mdi mdi-' + item.icon + '"></i>';
68
+ html += item.title + '</div>';
69
+ break;
70
+ } // switch item.type
71
+ } ); // foreach nav item
72
+
73
+ html += '</div>';
74
+ this.setHeaderTitle(html);
75
+ },
76
+
77
+ showSidebar: function(visible) {
78
+ // show or hide sidebar
79
+ if (visible) $('body').addClass('sidebar');
80
+ else $('body').removeClass('sidebar');
81
+ },
82
+
83
+ highlightTab: function(id) {
84
+ // highlight custom tab in sidebar
85
+ $('.sidebar .section_item').removeClass('active').addClass('inactive');
86
+ $('#tab_' + id).removeClass('inactive').addClass('active');
87
+ },
88
+
89
+ updateHeaderInfo: function() {
90
+ // update top-right display
91
+ // override this function in app
92
+ },
93
+
94
+ getUserAvatarURL: function(size, bust) {
95
+ // get url to current user avatar
96
+ var url = '';
97
+
98
+ // user may have custom avatar
99
+ if (this.user && this.user.avatar) {
100
+ // convert to protocol-less URL
101
+ url = this.user.avatar.replace(/^\w+\:/, '');
102
+ }
103
+ else {
104
+ url = '/api/app/avatar/' + this.username + '.png?size=' + size;
105
+ }
106
+
107
+ if (bust) {
108
+ url += (url.match(/\?/) ? '&' : '?') + 'random=' + Math.random();
109
+ }
110
+
111
+ return url;
112
+ },
113
+
114
+ doMyAccount: function() {
115
+ // nav to the my account page
116
+ Nav.go('MyAccount');
117
+ },
118
+
119
+ isAdmin: function() {
120
+ // return true if user is logged in and admin, false otherwise
121
+ return !!( app.user && app.user.privileges && app.user.privileges.admin );
122
+ },
123
+
124
+ handleResize: function() {
125
+ // called when window resizes
126
+ if (this.page_manager && this.page_manager.current_page_id) {
127
+ var id = this.page_manager.current_page_id;
128
+ var page = this.page_manager.find(id);
129
+ if (page && page.onResize) page.onResize( get_inner_window_size() );
130
+ if (page && page.updateBoxButtonFloaterPosition) page.updateBoxButtonFloaterPosition();
131
+ }
132
+
133
+ // also handle sending resize events at a 250ms delay
134
+ // so some pages can perform a more expensive refresh at a slower interval
135
+ if (!this.resize_timer) {
136
+ this.resize_timer = setTimeout( this.handleResizeDelay.bind(this), 250 );
137
+ }
138
+
139
+ if (Dialog.active) Dialog.autoResize();
140
+ if (CodeEditor.active) CodeEditor.autoResize();
141
+ if (Popover.enabled && !this.mobile) Popover.detach();
142
+ },
143
+
144
+ handleResizeDelay: function() {
145
+ // called 250ms after latest resize event
146
+ this.resize_timer = null;
147
+
148
+ if (this.page_manager && this.page_manager.current_page_id) {
149
+ var id = this.page_manager.current_page_id;
150
+ var page = this.page_manager.find(id);
151
+ if (page && page.onResizeDelay) page.onResizeDelay( get_inner_window_size() );
152
+ }
153
+ },
154
+
155
+ handleKeyDown: function(event) {
156
+ // send keydown event to page if text element isn't current focused
157
+ if (document.activeElement && document.activeElement.tagName.match(/^(INPUT|TEXTAREA)$/)) return;
158
+
159
+ if (this.page_manager && this.page_manager.current_page_id) {
160
+ var id = this.page_manager.current_page_id;
161
+ var page = this.page_manager.find(id);
162
+ if (page && page.onKeyDown) page.onKeyDown(event);
163
+ }
164
+ },
165
+
166
+ handleUnload: function(event) {
167
+ // called just before user navs off
168
+ if (this.page_manager && this.page_manager.current_page_id && $P && $P() && $P().onBeforeUnload) {
169
+ var result = $P().onBeforeUnload();
170
+ if (result) {
171
+ (event || window.event).returnValue = result; //Gecko + IE
172
+ return result; // Webkit, Safari, Chrome etc.
173
+ }
174
+ }
175
+ },
176
+
177
+ doError: function(msg, args) {
178
+ // show an error message at the top of the screen
179
+ // and hide the progress dialog if applicable
180
+ if (config.ui.errors[msg]) msg = config.ui.errors[msg];
181
+ if (args) msg = substitute(msg, args);
182
+
183
+ Debug.trace('error', "ERROR: " + msg);
184
+ this.showMessage( 'error', msg, 0 );
185
+ if (Dialog.progress) Dialog.hideProgress();
186
+ return null;
187
+ },
188
+
189
+ badField: function(id, msg, args) {
190
+ // mark field as bad
191
+ if (!msg) {
192
+ var raw_id = id.replace(/^\#/, '');
193
+ if (config.ui.errors[raw_id]) msg = config.ui.errors[raw_id];
194
+ }
195
+ if (msg && args) msg = substitute(msg, args);
196
+
197
+ if (id.match(/^\w+$/)) id = '#' + id;
198
+ $(id).removeClass('invalid').width(); // trigger reflow to reset css animation
199
+ $(id).addClass('invalid');
200
+ try { $(id).focus(); } catch (e) {;}
201
+
202
+ if (msg) return this.doError(msg);
203
+ else return false;
204
+ },
205
+
206
+ clearError: function() {
207
+ // clear last error
208
+ $('div.toast.error').remove();
209
+ $('.invalid').removeClass('invalid');
210
+ },
211
+
212
+ showMessage: function(type, msg, lifetime, loc) {
213
+ // show success, warning or error message
214
+ // Dialog.hide();
215
+ var icon = '';
216
+ switch (type) {
217
+ case 'success': icon = 'check-circle'; break;
218
+ case 'warning': icon = 'alert-circle'; break;
219
+ case 'error': icon = 'alert-decagram'; break;
220
+ case 'info': icon = 'information-outline'; break;
221
+
222
+ default:
223
+ if (type.match(/^(\w+)\/(.+)$/)) { type = RegExp.$1; icon = RegExp.$2; }
224
+ break;
225
+ }
226
+
227
+ // strip html to prevent script injection
228
+ msg = strip_html(msg);
229
+
230
+ this.toast({ type, icon, msg, lifetime, loc });
231
+ },
232
+
233
+ toast: function(args) {
234
+ // show toast notification given raw html
235
+ var { type, icon, msg, lifetime, loc } = args;
236
+
237
+ var html = '';
238
+ html += '<div class="toast ' + type + '" style="display:none">';
239
+ html += '<i class="mdi mdi-' + icon + '"></i>';
240
+ html += '<span>' + msg + '</span>';
241
+ html += '</div>';
242
+
243
+ var $toast = $(html);
244
+ var timer = null;
245
+ $('#toaster').append( $toast );
246
+
247
+ $toast.fadeIn(250);
248
+ $toast.on('click', function() {
249
+ if (timer) clearTimeout(timer);
250
+ $toast.fadeOut( 250, function() { $(this).remove(); } );
251
+ if (loc) Nav.go(loc);
252
+ } );
253
+
254
+ if ((type == 'success') || (type == 'info') || lifetime) {
255
+ if (!lifetime) lifetime = 8;
256
+ timer = setTimeout( function() {
257
+ $toast.fadeOut( 500, function() { $(this).remove(); } );
258
+ }, lifetime * 1000 );
259
+ }
260
+ },
261
+
262
+ hideMessage: function(animate) {
263
+ // if (animate) $('#d_message').hide(animate);
264
+ // else $('#d_message').hide();
265
+
266
+ if (animate) $('div.toast').fadeOut( animate, function() { $(this).remove(); } );
267
+ else $('div.toast').remove();
268
+ },
269
+
270
+ api: {
271
+
272
+ request: function(url, opts, callback, errorCallback) {
273
+ // send HTTP GET to API endpoint
274
+ Debug.trace('api', "Sending API request: " + url );
275
+
276
+ // default 10 sec timeout
277
+ var timeout = opts.timeout || 10000;
278
+ delete opts.timeout;
279
+
280
+ // retry delay (w/exp backoff)
281
+ var retryDelay = opts.retryDelay || 100;
282
+ delete opts.retryDelay;
283
+
284
+ var timed_out = false;
285
+ var timer = setTimeout( function() {
286
+ timed_out = true;
287
+ timer = null;
288
+ var err = new Error("Timeout");
289
+ Debug.trace('api', "HTTP Error: " + err);
290
+
291
+ if (errorCallback) errorCallback({ code: 'http', description: '' + (err.message || err) });
292
+ else app.doError( "HTTP Error: " + err.message || err );
293
+ }, timeout );
294
+
295
+ window.fetch( url, opts )
296
+ .then( function(res) {
297
+ if (timer) { clearTimeout(timer); timer = null; }
298
+ if (!res.ok) throw new Error("HTTP " + res.status + " " + res.statusText);
299
+ return res.json();
300
+ } )
301
+ .then(function(json) {
302
+ // got response
303
+ if (timed_out) return;
304
+ if (timer) { clearTimeout(timer); timer = null; }
305
+ var text = JSON.stringify(json);
306
+ if (text.length > 8192) text = "(" + text.length + " bytes)";
307
+ Debug.trace('api', "API Response: " + text );
308
+
309
+ // use setTimeout to avoid insanity with the stupid fetch promise
310
+ setTimeout( function() {
311
+ if (('code' in json) && (json.code != 0)) {
312
+ // an error occurred within the JSON response
313
+ // session errors are handled specially
314
+ if (json.code == 'session') app.doUserLogout(true);
315
+ else if (errorCallback) errorCallback(json);
316
+ else app.doError("API Error: " + json.description);
317
+ }
318
+ else if (callback) callback( json );
319
+ }, 1 );
320
+ } )
321
+ .catch( function(err) {
322
+ // HTTP error
323
+ if (timed_out) return;
324
+ if (timer) { clearTimeout(timer); timer = null; }
325
+ Debug.trace('api', "HTTP Error: " + err);
326
+
327
+ // retry network errors
328
+ if (err instanceof TypeError) {
329
+ retryDelay = Math.min( retryDelay * 2, 8000 );
330
+ Debug.trace('api', `Retrying network error in ${retryDelay}ms...`);
331
+ setTimeout( function() { app.api.request(url, { ...opts, timeout, retryDelay }, callback, errorCallback); }, retryDelay );
332
+ return;
333
+ }
334
+
335
+ if (errorCallback) errorCallback({ code: 'http', description: '' + (err.message || err) });
336
+ else app.doError( err.message || err );
337
+ } );
338
+ }, // api.request
339
+
340
+ post: function(cmd, data, callback, errorCallback) {
341
+ // send HTTP POST to API endpoint
342
+ var url = cmd;
343
+ if (!url.match(/^(\w+\:\/\/|\/)/)) url = app.base_api_url + "/" + cmd;
344
+
345
+ var json_raw = JSON.stringify(data);
346
+ Debug.trace( 'api', "Sending HTTP POST to: " + url + ": " + json_raw );
347
+
348
+ app.api.request( url, {
349
+ method: "POST",
350
+ headers: {
351
+ "Content-Type": app.plain_text_post ? 'text/plain' : 'application/json',
352
+ },
353
+ body: json_raw
354
+ }, callback, errorCallback );
355
+ }, // api.post
356
+
357
+ upload: function(cmd, data, callback, errorCallback) {
358
+ // send FormData to API endpoint
359
+ var url = cmd;
360
+ if (!url.match(/^(\w+\:\/\/|\/)/)) url = app.base_api_url + "/" + cmd;
361
+
362
+ Debug.trace( 'api', "Uploading files to: " + url );
363
+
364
+ app.api.request( url, {
365
+ method: "POST",
366
+ body: data,
367
+ timeout: 300 * 1000 // 5 minutes
368
+ }, callback, errorCallback );
369
+ }, // api.post
370
+
371
+ get: function(cmd, query, callback, errorCallback) {
372
+ // send HTTP GET to API endpoint
373
+ var url = cmd;
374
+ if (!url.match(/^(\w+\:\/\/|\/)/)) url = app.base_api_url + "/" + cmd;
375
+
376
+ if (!query) query = {};
377
+ if (app.cacheBust) query.cachebust = app.cacheBust;
378
+ url += compose_query_string(query);
379
+
380
+ Debug.trace( 'api', "Sending HTTP GET to: " + url );
381
+ app.api.request( url, {}, callback, errorCallback );
382
+ } // api.get
383
+
384
+ }, // api
385
+
386
+ initPrefs: function(key) {
387
+ // init prefs, load from localStorage if applicable
388
+ this.prefs = {};
389
+
390
+ if (localStorage.prefs) {
391
+ Debug.trace('prefs', "localStorage.prefs: " + localStorage.prefs );
392
+ try { this.prefs = JSON.parse( localStorage.prefs ); }
393
+ catch (err) {
394
+ Debug.trace('prefs', "ERROR: Failed to load prefs: " + err, localStorage.prefs);
395
+ }
396
+ }
397
+
398
+ // apply defaults
399
+ if (this.default_prefs) {
400
+ for (var key in this.default_prefs) {
401
+ if (!(key in this.prefs)) {
402
+ this.prefs[key] = this.default_prefs[key];
403
+ }
404
+ }
405
+ }
406
+
407
+ Debug.trace('prefs', "Loaded: " + JSON.stringify(this.prefs));
408
+ },
409
+
410
+ getPref: function(key) {
411
+ // get user preference, accepts single key, dot.path or slash/path syntax.
412
+ return get_path( this.prefs, key );
413
+ },
414
+
415
+ setPref: function(key, value) {
416
+ // set user preference, accepts single key, dot.path or slash/path syntax.
417
+ set_path( this.prefs, key, value );
418
+ this.savePrefs();
419
+ },
420
+
421
+ deletePref: function(key) {
422
+ // delete user preference, accepts single key, dot.path or slash/path syntax.
423
+ delete_path( this.prefs, key );
424
+ this.savePrefs();
425
+ },
426
+
427
+ savePrefs: function() {
428
+ // save local pref cache back to localStorage
429
+ localStorage.prefs = JSON.stringify( this.prefs );
430
+ },
431
+
432
+ get_base_url: function() {
433
+ return app.proto + location.hostname + '/';
434
+ },
435
+
436
+ setTheme: function(theme) {
437
+ // set light/dark theme
438
+ var icon = '';
439
+
440
+ this.setPref('theme', theme);
441
+
442
+ switch (theme) {
443
+ case 'light': icon = 'white-balance-sunny'; break; // weather-sunny
444
+ case 'dark': icon = 'moon-waning-crescent'; break; // weather-night
445
+ case 'auto': icon = 'circle-half-full'; break;
446
+ }
447
+
448
+ if (theme == 'auto') {
449
+ if (window.matchMedia('(prefers-color-scheme: dark)').matches) theme = 'dark';
450
+ else theme = 'light';
451
+ }
452
+
453
+ if (this.onThemeChange) this.onThemeChange(theme);
454
+
455
+ if (theme == 'dark') {
456
+ $('body').addClass('dark');
457
+ $('head > meta[name = theme-color]').attr('content', '#222222');
458
+ }
459
+ else {
460
+ $('body').removeClass('dark');
461
+ $('head > meta[name = theme-color]').attr('content', '#3791F5');
462
+ }
463
+
464
+ $('#d_theme_ctrl').html( '<i class="mdi mdi-' + icon + '"></i>' );
465
+ },
466
+
467
+ initTheme: function() {
468
+ // set theme to user's preference
469
+ this.setTheme( this.getPref('theme') || 'auto' );
470
+
471
+ // listen for changes
472
+ var match = window.matchMedia('(prefers-color-scheme: dark)');
473
+ match.addEventListener('change', function(event) {
474
+ if (app.getPref('theme') == 'auto') app.setTheme('auto');
475
+ });
476
+ },
477
+
478
+ toggleTheme: function() {
479
+ // toggle light/dark theme
480
+ if (this.getPref('theme') == 'dark') this.setTheme('light');
481
+ else this.setTheme('dark');
482
+ },
483
+
484
+ getTheme() {
485
+ // get current theme, computed if auto
486
+ var theme = this.getPref('theme') || 'auto';
487
+
488
+ if (theme == 'auto') {
489
+ if (window.matchMedia('(prefers-color-scheme: dark)').matches) theme = 'dark';
490
+ else theme = 'light';
491
+ }
492
+
493
+ return theme;
494
+ },
495
+
496
+ pullSidebar: function() {
497
+ // mobile: pull sidebar over content
498
+ if (!$('div.sidebar').hasClass('force')) {
499
+ $('div.sidebar').addClass('force');
500
+
501
+ if ($('#sidebar_overlay').length) {
502
+ $('#sidebar_overlay').stop().remove();
503
+ }
504
+
505
+ var $overlay = $('<div id="sidebar_overlay"></div>').css('opacity', 0);
506
+ $('body').append($overlay);
507
+ $overlay.fadeTo( 500, 0.5 ).click(function() {
508
+ app.pushSidebar();
509
+ });
510
+ }
511
+ },
512
+
513
+ pushSidebar: function() {
514
+ // mobile: return sidebar to its hidden drawer
515
+ if ($('div.sidebar').hasClass('force')) {
516
+ $('div.sidebar').removeClass('force');
517
+ $('#sidebar_overlay').stop().fadeOut( 500, function() { $(this).remove(); } );
518
+ }
519
+ },
520
+
521
+ notifyUserNav: function(loc) {
522
+ // override in app
523
+ // called by each page nav operation
524
+ }
525
+
526
+ }; // app object
527
+
528
+ function $P(id) {
529
+ // shortcut for page_manager.find(), also defaults to current page
530
+ if (!id) id = app.page_manager.current_page_id;
531
+ var page = app.page_manager.find(id);
532
+ assert( !!page, "Failed to locate page: " + id );
533
+ return page;
534
+ };
535
+
536
+ window.Debug = {
537
+
538
+ enabled: false,
539
+ categories: { all: 1 },
540
+ backlog: [],
541
+
542
+ colors: ["#001F3F", "#0074D9", "#7FDBFF", "#39CCCC", "#3D9970", "#2ECC40", "#01FF70", "#FFDC00", "#FF851B", "#FF4136", "#F012BE", "#B10DC9", "#85144B"],
543
+ nextColorIdx: 0,
544
+ catColors: {},
545
+
546
+ enable: function(cats) {
547
+ // enable debug logging and flush backlog if applicable
548
+ if (cats) this.categories = cats;
549
+ this.enabled = true;
550
+ this._dump();
551
+ },
552
+
553
+ disable: function() {
554
+ // disable debug logging, but keep backlog
555
+ this.enabled = false;
556
+ },
557
+
558
+ trace: function(cat, msg, data) {
559
+ // trace one line to console, or store in backlog
560
+ // allow msg, cat + msg, msg + data, or cat + msg + data
561
+ if (arguments.length == 1) {
562
+ msg = cat;
563
+ cat = 'debug';
564
+ }
565
+ else if ((arguments.length == 2) && (typeof(arguments[arguments.length - 1]) == 'object')) {
566
+ data = msg;
567
+ msg = cat;
568
+ cat = 'debug';
569
+ }
570
+
571
+ var now = new Date();
572
+ var timestamp = '' +
573
+ this._zeroPad( now.getHours(), 2 ) + ':' +
574
+ this._zeroPad( now.getMinutes(), 2 ) + ':' +
575
+ this._zeroPad( now.getSeconds(), 2 ) + '.' +
576
+ this._zeroPad( now.getMilliseconds(), 3 );
577
+
578
+ if (data && (typeof(data) == 'object')) data = JSON.stringify(data);
579
+ if (!data) data = false;
580
+
581
+ if (this.enabled) {
582
+ if ((this.categories.all || this.categories[cat]) && (this.categories[cat] !== false)) {
583
+ this._print(timestamp, cat, msg, data);
584
+ }
585
+ }
586
+ else {
587
+ this.backlog.push([ timestamp, cat, msg, data ]);
588
+ if (this.backlog.length > 1000) this.backlog.shift();
589
+ }
590
+ },
591
+
592
+ _dump: function() {
593
+ // dump backlog to console
594
+ for (var idx = 0, len = this.backlog.length; idx < len; idx++) {
595
+ this._print.apply( this, this.backlog[idx] );
596
+ }
597
+ this.backlog = [];
598
+ },
599
+
600
+ _print: function(timestamp, cat, msg, data) {
601
+ // format and print one message to the console
602
+ var color = this.catColors[cat] || '';
603
+ if (!color) {
604
+ color = this.catColors[cat] = this.colors[this.nextColorIdx];
605
+ this.nextColorIdx = (this.nextColorIdx + 1) % this.colors.length;
606
+ }
607
+
608
+ console.log( timestamp + ' %c[' + cat + ']%c ' + msg, 'color:' + color + '; font-weight:bold', 'color:inherit; font-weight:normal' );
609
+ if (data) console.log(data);
610
+ },
611
+
612
+ _zeroPad: function(value, len) {
613
+ // Pad a number with zeroes to achieve a desired total length (max 10)
614
+ return ('0000000000' + value).slice(0 - len);
615
+ }
616
+ };
617
+
618
+ if (!window.assert) window.assert = function(fact, msg) {
619
+ // very simple assert
620
+ if (!fact) {
621
+ console.error("ASSERT FAILURE: " + msg);
622
+ throw("ASSERT FAILED! " + msg);
623
+ }
624
+ return fact;
625
+ };
626
+
627
+ $(document).ready(function() {
628
+ app.init();
629
+ });
630
+
631
+ window.addEventListener( "click", function(e) {
632
+ app.lastClick = { altKey: e.altKey, ctrlKey: e.ctrlKey, shiftKey: e.shiftKey, metaKey: e.metaKey };
633
+ }, true );
634
+
635
+ window.addEventListener( "keydown", function(event) {
636
+ if (Popover.enabled) Popover.handleKeyDown(event);
637
+ else if (CodeEditor.handleKeyDown) CodeEditor.handleKeyDown(event);
638
+ else if (Dialog.active) Dialog.confirm_key(event);
639
+ else app.handleKeyDown(event);
640
+ }, false );
641
+
642
+ window.addEventListener( "resize", function() {
643
+ app.handleResize();
644
+ }, false );
645
+
646
+ window.addEventListener("beforeunload", function (e) {
647
+ return app.handleUnload(e);
648
+ }, false );