PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.6.5
VikBooking Hotel Booking Engine & PMS v1.6.5
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
vikbooking / admin / resources / vbocore.js

vbocore.js in VikBooking Hotel Booking Engine & PMS 1.6.5, at admin/resources/vbocore.js

2,697 lines 80.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * VikBooking Core v1.6.5
3 * Copyright (C) 2023 E4J s.r.l. All Rights Reserved.
4 * http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
5 * https://vikwp.com | https://e4j.com | https://e4jconnect.com
6 */
7 (function($, w) {
8 'use strict';
9
10 w['VBOCore'] = class VBOCore {
11
12 /**
13 * Proxy to support static injection of params.
14 */
15 constructor(params) {
16 if (typeof params === 'object') {
17 VBOCore.setOptions(params);
18 }
19 }
20
21 /**
22 * Inject options by overriding default properties.
23 *
24 * @param object params
25 *
26 * @return self
27 */
28 static setOptions(params) {
29 if (typeof params === 'object') {
30 VBOCore.options = Object.assign(VBOCore.options, params);
31 }
32
33 return VBOCore;
34 }
35
36 /**
37 * Getter for admin_widgets private options property.
38 *
39 * @return array
40 */
41 static get admin_widgets() {
42 return VBOCore.options.admin_widgets;
43 }
44
45 /**
46 * Getter for multitask open event private property.
47 *
48 * @return string
49 */
50 static get multitask_open_event() {
51 return VBOCore.options.multitask_open_event;
52 }
53
54 /**
55 * Getter for multitask close event private property.
56 *
57 * @return string
58 */
59 static get multitask_close_event() {
60 return VBOCore.options.multitask_close_event;
61 }
62
63 /**
64 * Getter for multitask shortcut event private property.
65 *
66 * @return string
67 */
68 static get multitask_shortcut_event() {
69 return VBOCore.options.multitask_shortcut_ev;
70 }
71
72 /**
73 * Getter for multitask seach focus shortcut event private property.
74 *
75 * @return string
76 */
77 static get multitask_searchfs_event() {
78 return VBOCore.options.multitask_searchfs_ev;
79 }
80
81 /**
82 * Getter for multitask event widget modal rendered.
83 *
84 * @return string
85 */
86 static get widget_modal_rendered() {
87 return VBOCore.options.widget_modal_rendered;
88 }
89
90 /**
91 * Getter for widget modal dismiss event.
92 *
93 * @return string
94 */
95 static get widget_modal_dismissed() {
96 return VBOCore.options.widget_modal_dismissed;
97 }
98
99 /**
100 * Getter for the service worker file path.
101 *
102 * @return string
103 */
104 static get service_worker_path() {
105 return VBOCore.options.service_worker_path;
106 }
107
108 /**
109 * Getter for the service worker scope.
110 *
111 * @return string
112 */
113 static get service_worker_scope() {
114 return VBOCore.options.service_worker_scope;
115 }
116
117 /**
118 * Getter for the push application key.
119 *
120 * @return string
121 */
122 static get push_application_key() {
123 return VBOCore.options.push.application_key;
124 }
125
126 /**
127 * Getter for the push storage endpoint identifier.
128 *
129 * @return string
130 */
131 static get push_storage_endpoint_id() {
132 return VBOCore.options.push_options.storage_endp_id;
133 }
134
135 /**
136 * Parses an AJAX response error object.
137 *
138 * @param object err
139 *
140 * @return bool
141 */
142 static isConnectionLostError(err) {
143 if (!err || !err.hasOwnProperty('status')) {
144 return false;
145 }
146
147 return (
148 err.statusText == 'error'
149 && err.status == 0
150 && (err.readyState == 0 || err.readyState == 4)
151 && (!err.hasOwnProperty('responseText') || err.responseText == '')
152 );
153 }
154
155 /**
156 * Ensures AJAX requests that fail due to connection errors are retried automatically.
157 *
158 * @param string url
159 * @param object data
160 * @param function success
161 * @param function failure
162 * @param number attempt
163 */
164 static doAjax(url, data, success, failure, attempt) {
165 const AJAX_MAX_ATTEMPTS = 3;
166
167 if (attempt === undefined) {
168 attempt = 1;
169 }
170
171 return $.ajax({
172 type: 'POST',
173 url: url,
174 data: data
175 }).done(function(resp) {
176 if (success !== undefined) {
177 // launch success callback function
178 success(resp);
179 }
180 }).fail(function(err) {
181 /**
182 * If the error is caused by a site connection lost, and if the number
183 * of retries is lower than max attempts, retry the same AJAX request.
184 */
185 if (attempt < AJAX_MAX_ATTEMPTS && VBOCore.isConnectionLostError(err)) {
186 // delay the retry by half second
187 setTimeout(function() {
188 // re-launch same request and increase number of attempts
189 console.log('Retrying previous AJAX request');
190 VBOCore.doAjax(url, data, success, failure, (attempt + 1));
191 }, 500);
192 } else {
193 // launch the failure callback otherwise
194 if (failure !== undefined) {
195 failure(err);
196 }
197 }
198
199 if (!err.status || err.status == 500) {
200 // log the error in console
201 console.error('AJAX request failed' + (err.status == 500 ? ' (' + err.responseText + ')' : ''), err);
202 }
203 });
204 }
205
206 /**
207 * Matches a keyword against a text.
208 *
209 * @param string search the keyword to search.
210 * @param string text the text to compare.
211 *
212 * @return bool
213 */
214 static matchString(search, text) {
215 return ((text + '').indexOf(search) >= 0);
216 }
217
218 /**
219 * Converts a base64 URL for a server key into an array of 8-bit unsigned integers.
220 *
221 * @param string key the base64 encoded URL key.
222 *
223 * @return Uint8Array
224 */
225 static base64ToUint8Array(key) {
226 const padding = ('=').repeat((4 - (key.length % 4)) % 4);
227 const base64 = (key + padding).replace(/\-/g, '+').replace(/_/g, '/');
228
229 const rawData = window.atob(base64);
230 const uint8_arr = new Uint8Array(rawData.length);
231
232 for (let i = 0; i < rawData.length; ++i) {
233 uint8_arr[i] = rawData.charCodeAt(i);
234 }
235
236 return uint8_arr;
237 }
238
239 /**
240 * Builds an object containing the Push subscription details and data.
241 *
242 * @param PushSubscription pushSubscription the registration data.
243 * @param object data the extra data to merge.
244 *
245 * @return object
246 */
247 static buildPushSubscriptionData(pushSubscription, data) {
248 if (!(pushSubscription instanceof PushSubscription)) {
249 return data;
250 }
251
252 let key = pushSubscription.getKey('p256dh');
253 let token = pushSubscription.getKey('auth');
254 let contentEncoding = (PushManager.supportedContentEncodings || ['aesgcm'])[0];
255
256 let details = {
257 endpoint: pushSubscription.endpoint,
258 publicKey: key ? btoa(String.fromCharCode.apply(null, new Uint8Array(key))) : null,
259 authToken: token ? btoa(String.fromCharCode.apply(null, new Uint8Array(token))) : null,
260 encoding: contentEncoding,
261 };
262
263 return Object.assign(details, data);
264 }
265
266 /**
267 * Attempts to install (and eventually update) the VikBooking's Service Worker.
268 *
269 * @param bool update if defined, it will update the Service Worker Registration.
270 *
271 * @return Promise
272 */
273 static installServiceWorker(update) {
274 // check if support for Web App badge is available
275 if (typeof navigator.setAppBadge !== 'undefined') {
276 // always clear the app badge
277 navigator.clearAppBadge();
278 }
279
280 return new Promise((resolve, reject) => {
281 if (!('serviceWorker' in navigator)) {
282 reject('Service workers are not supported by this browser');
283 return;
284 }
285
286 if (!('PushManager' in window)) {
287 reject('Push notifications are not supported by this browser');
288 return;
289 }
290
291 if (!VBOCore.service_worker_path) {
292 reject('Service Worker path not configured');
293 return;
294 }
295
296 if (!VBOCore.notificationsEnabled()) {
297 reject('Notifications are disabled, and so the service worker will not be installed');
298 return;
299 }
300
301 navigator.serviceWorker.register(VBOCore.service_worker_path, {scope: VBOCore.service_worker_scope}).then(
302 (registration) => {
303 if (typeof update !== 'undefined') {
304 // attempt to update the registration for un-caching purposes
305 registration.update().then((updated) => {
306 // all good
307 }).catch((e) => {
308 // updating should never fail after registering with success
309 console.error(`Service worker registration update failed: ${e}`);
310 });
311 }
312
313 // listen to the ServiceWorker messages for the client
314 VBOCore.listenServiceWorkerMessages();
315
316 // resolve the Promise
317 resolve(registration);
318 },
319 (e) => {
320 console.error(`Service worker registration failed: ${e}`);
321 reject('Service worker registration failed');
322 }
323 );
324 });
325 }
326
327 /**
328 * Handles the Push subscription state. To be called after the
329 * Service Worker installation (registration) is ready.
330 *
331 * @param ServiceWorkerRegistration registration sw registration object.
332 *
333 * @return Promise
334 */
335 static handlePushSubscription(registration) {
336 return new Promise((resolve, reject) => {
337 if (!(registration instanceof ServiceWorkerRegistration)) {
338 reject('Missing service worker registration');
339 return;
340 }
341
342 if (!VBOCore.push_application_key) {
343 reject('Missing application key for Push because not supported');
344 return;
345 }
346
347 // check if a push subscription is available
348 registration.pushManager.getSubscription().then((pushSubscription) => {
349 if (pushSubscription) {
350 // we are subscribed to Push
351 let previous_endpoint = VBOCore.storageGetItem(VBOCore.push_storage_endpoint_id);
352 if (previous_endpoint && pushSubscription.endpoint != previous_endpoint) {
353 // update subscription data
354 VBOCore.doAjax(
355 VBOCore.options.push.ajax_url,
356 VBOCore.buildPushSubscriptionData(pushSubscription, {
357 agent: window.navigator.userAgent,
358 type: 'update',
359 })
360 );
361 }
362
363 // resolve the promise with the Push subscription data
364 resolve(pushSubscription);
365 return;
366 }
367
368 // push subscription options
369 const pushOptions = {
370 userVisibleOnly: true,
371 applicationServerKey: VBOCore.base64ToUint8Array(VBOCore.push_application_key),
372 };
373
374 // subscribe to Push
375 registration.pushManager.subscribe(pushOptions).then(
376 (pushSubscription) => {
377 // store subscription data
378 VBOCore.storageSetItem(VBOCore.push_storage_endpoint_id, pushSubscription.endpoint);
379 VBOCore.doAjax(
380 VBOCore.options.push.ajax_url,
381 VBOCore.buildPushSubscriptionData(pushSubscription, {
382 agent: window.navigator.userAgent,
383 type: 'new',
384 })
385 );
386
387 // resolve the promise with the Push subscription data
388 resolve(pushSubscription);
389 },
390 (e) => {
391 console.error(`Error handling the Push subscription state: ${e}`);
392 reject('Error handling the Push subscription state');
393 }
394 );
395 })
396 .catch((e) => {
397 console.error(`Error getting the Push subscription state: ${e}`);
398 reject('Error getting the Push subscription state');
399 });
400 });
401 }
402
403 /**
404 * Starts listening to the "message" events posted by the ServiceWorker.
405 *
406 * @return void
407 */
408 static listenServiceWorkerMessages() {
409 if (!('serviceWorker' in navigator)) {
410 return;
411 }
412
413 try {
414 // add listener to the message event for the data posted by the ServiceWorker to the client
415 navigator.serviceWorker.addEventListener('message', (event) => {
416 VBOCore.handleServiceWorkerMessage(event.data);
417 });
418 } catch(e) {
419 // do nothing
420 }
421 }
422
423 /**
424 * Handles a message data posted by the ServiceWorker upon clicking a Push notification.
425 *
426 * @param object data the event.notification.data object posted by the ServiceWorker.
427 *
428 * @return void
429 */
430 static handleServiceWorkerMessage(data) {
431 let notif_type = data.type || '';
432
433 if (!notif_type || !VBOCore.options.push_options.allowed_notif_types.includes(notif_type)) {
434 console.error(data);
435 throw new Error('Unsupported message type');
436 }
437
438 // determine the proper widget for dispatching the message data
439 let widget_id = 'booking_details';
440 if (notif_type == 'Chat') {
441 widget_id = 'guest_messages';
442 }
443
444 // the raw notification content
445 let raw_content = data.content || {};
446
447 // multitask data options
448 let options = Object.assign({
449 _push: 1,
450 title: data.title || '',
451 message: data.message || '',
452 }, raw_content);
453
454 // dispatch (Push) message data on widget
455 VBOCore.handleDisplayWidgetNotification({widget_id: widget_id}, options, true);
456
457 // update pushed data map for the next watching event in the current browsing context
458 VBOCore.widgets_pushed_data.push(raw_content);
459
460 // post message onto broadcast channel for any other browsing context
461 if (VBOCore.broadcast_push_data) {
462 // the next watch-data interval will receive the pushed data information
463 VBOCore.broadcast_push_data.postMessage(raw_content);
464 }
465 }
466
467 /**
468 * Initializes the multitasking panel for the admin widgets.
469 *
470 * @param object params the panel object params.
471 *
472 * @return bool
473 */
474 static prepareMultitasking(params) {
475 var panel_opts = {
476 selector: "",
477 sclass_l_small: "vbo-sidepanel-right",
478 sclass_l_large: "vbo-sidepanel-large",
479 btn_trigger: "",
480 search_selector: "#vbo-sidepanel-search-input",
481 search_nores: ".vbo-sidepanel-add-widgets-nores",
482 close_selector: ".vbo-sidepanel-dismiss-btn",
483 t_layout_small: ".vbo-sidepanel-layout-small",
484 t_layout_large: ".vbo-sidepanel-layout-large",
485 wclass_base_sel: ".vbo-admin-widgets-widget-output",
486 wclass_l_small: "vbo-admin-widgets-container-small",
487 wclass_l_large: "vbo-admin-widgets-container-large",
488 addws_selector: ".vbo-sidepanel-add-widgets",
489 addw_selector: ".vbo-sidepanel-add-widget",
490 addw_modal_cls: "vbo-widget-render-modal",
491 addwfs_selector: ".vbo-sidepanel-add-widget-focussed",
492 wtags_selector: ".vbo-sidepanel-widget-tags",
493 addw_data_attr: "data-vbowidgetid",
494 actws_selector: ".vbo-sidepanel-active-widgets",
495 editw_selector: ".vbo-sidepanel-edit-widgets-trig",
496 rmwidget_class: "vbo-admin-widgets-widget-remove",
497 rmwidget_icn: "",
498 dtcwidget_class: "vbo-admin-widgets-widget-detach",
499 dtctarget_class: "vbo-admin-widget-head",
500 dtcwidget_icn: "",
501 notif_selector: ".vbo-sidepanel-notifications-btn",
502 notif_on_class: "vbo-sidepanel-notifications-on",
503 notif_off_class: "vbo-sidepanel-notifications-off",
504 open_class: "vbo-sidepanel-open",
505 close_class: "vbo-sidepanel-close",
506 cur_widget_cls: "vbo-admin-widgets-container-small",
507 };
508
509 if (typeof params === 'object') {
510 panel_opts = Object.assign(panel_opts, params);
511 }
512
513 if (!panel_opts.btn_trigger || !panel_opts.selector) {
514 console.error('Got no trigger or selector');
515 return false;
516 }
517
518 // push panel options
519 VBOCore.setOptions({
520 panel_opts: panel_opts,
521 });
522
523 // setup browser notifications
524 VBOCore.setupNotifications();
525
526 // count active widgets on current page
527 var tot_active_widgets = VBOCore.options.admin_widgets.length;
528 if (tot_active_widgets > 0) {
529 // hide add-widgets container
530 $(panel_opts.addws_selector).hide();
531
532 // register listener for input search blur
533 VBOCore.registerSearchWidgetsBlur();
534 }
535
536 // register click event on trigger button
537 $(VBOCore.options.panel_opts.btn_trigger).on('click', function() {
538 var side_panel = $(VBOCore.options.panel_opts.selector);
539 if (side_panel.hasClass(VBOCore.options.panel_opts.open_class)) {
540 // hide panel
541 VBOCore.side_panel_on = false;
542 VBOCore.emitMultitaskEvent(VBOCore.multitask_close_event);
543 side_panel.addClass(VBOCore.options.panel_opts.close_class).removeClass(VBOCore.options.panel_opts.open_class);
544 // always hide add-widgets container
545 $(VBOCore.options.panel_opts.addws_selector).hide();
546 // check if we are currently editing
547 var is_editing = ($('.' + VBOCore.options.panel_opts.editmode_class).length > 0);
548 if (is_editing) {
549 // deactivate editing mode
550 VBOCore.toggleWidgetsPanelEditing(null);
551 }
552 } else {
553 // show panel
554 VBOCore.side_panel_on = true;
555 VBOCore.emitMultitaskEvent(VBOCore.multitask_open_event);
556 side_panel.addClass(VBOCore.options.panel_opts.open_class).removeClass(VBOCore.options.panel_opts.close_class);
557 if (!VBOCore.options.admin_widgets.length) {
558 // set focus on search widgets input with delay for the opening animation
559 setTimeout(function() {
560 $(VBOCore.options.panel_opts.search_selector).focus();
561 }, 300);
562 }
563 }
564 });
565
566 // register close/dismiss button
567 $(VBOCore.options.panel_opts.close_selector).on('click', function() {
568 $(VBOCore.options.panel_opts.btn_trigger).trigger('click');
569 });
570
571 // register toggle layout buttons
572 $(VBOCore.options.panel_opts.t_layout_large).on('click', function() {
573 // large layout
574 $(VBOCore.options.panel_opts.selector).addClass(VBOCore.options.panel_opts.sclass_l_large).removeClass(VBOCore.options.panel_opts.sclass_l_small);
575 $(VBOCore.options.panel_opts.wclass_base_sel).addClass(VBOCore.options.panel_opts.wclass_l_large).removeClass(VBOCore.options.panel_opts.wclass_l_small);
576 VBOCore.options.panel_opts.cur_widget_cls = VBOCore.options.panel_opts.sclass_l_large;
577 });
578 $(VBOCore.options.panel_opts.t_layout_small).on('click', function() {
579 // small layout
580 $(VBOCore.options.panel_opts.selector).addClass(VBOCore.options.panel_opts.sclass_l_small).removeClass(VBOCore.options.panel_opts.sclass_l_large);
581 $(VBOCore.options.panel_opts.wclass_base_sel).addClass(VBOCore.options.panel_opts.wclass_l_small).removeClass(VBOCore.options.panel_opts.wclass_l_large);
582 VBOCore.options.panel_opts.cur_widget_cls = VBOCore.options.panel_opts.sclass_l_small;
583 });
584
585 // register listener for esc key pressed
586 $(document).keyup(function(e) {
587 if (!VBOCore.side_panel_on) {
588 return;
589 }
590 if ((e.key && e.key === "Escape") || (e.keyCode && e.keyCode == 27)) {
591 $(VBOCore.options.panel_opts.btn_trigger).trigger('click');
592 }
593 });
594
595 // register listener for input search focus
596 $(VBOCore.options.panel_opts.search_selector).on('focus', function() {
597 // always show add-widgets container
598 var widget_focus_class = VBOCore.options.panel_opts.addwfs_selector.replace('.', '');
599 $(VBOCore.options.panel_opts.addw_selector).removeClass(widget_focus_class);
600 $(VBOCore.options.panel_opts.addws_selector).show();
601 });
602
603 // register listener on input search widget
604 $(VBOCore.options.panel_opts.search_selector).keyup(function(e) {
605 // get the keyword to look for
606 var keyword = $(this).val();
607 // counting matching widgets
608 var matching = 0;
609 var first_matched = null;
610 var widget_focus_class = VBOCore.options.panel_opts.addwfs_selector.replace('.', '');
611
612 // adjust widgets to be displayed
613 if (!keyword.length) {
614 // show all widgets for selection
615 $(VBOCore.options.panel_opts.addw_selector).show();
616 // hide "no results"
617 $(VBOCore.options.panel_opts.search_nores).hide();
618 // all widgets are matching
619 matching = $(VBOCore.options.panel_opts.addw_selector).length;
620 } else {
621 // make the keyword lowercase
622 keyword = (keyword + '').toLowerCase();
623 // parse all widget's description tags
624 $(VBOCore.options.panel_opts.addw_selector).each(function() {
625 var elem = $(this);
626 var descr = elem.find(VBOCore.options.panel_opts.wtags_selector).text();
627 if (VBOCore.matchString(keyword, descr)) {
628 elem.show();
629 matching++;
630 if (!first_matched) {
631 // store the first widget that matched
632 first_matched = elem.attr(VBOCore.options.panel_opts.addw_data_attr);
633 }
634 } else {
635 elem.hide();
636 }
637 });
638 // check how many widget matched
639 if (matching > 0) {
640 // hide "no results"
641 $(VBOCore.options.panel_opts.search_nores).hide();
642 } else {
643 // show "no results"
644 $(VBOCore.options.panel_opts.search_nores).show();
645 }
646 }
647
648 // check for shortcuts
649 if (!e.key) {
650 return;
651 }
652
653 // handle Enter key press to add a widget
654 if (e.key === "Enter") {
655 // on Enter key pressed, add the first matching widget or the focussed one
656 var load_wid_id = null;
657 var focussed_wid = $(VBOCore.options.panel_opts.addwfs_selector + ':visible').first();
658
659 if (focussed_wid.length) {
660 load_wid_id = focussed_wid.attr(VBOCore.options.panel_opts.addw_data_attr);
661 } else if (first_matched) {
662 load_wid_id = first_matched;
663 }
664
665 if (!load_wid_id) {
666 // no widget to render found
667 return;
668 }
669
670 if (e.shiftKey === true) {
671 // widget modal rendering
672 VBOCore.renderModalWidget(load_wid_id);
673
674 return;
675 }
676
677 // widget multitask panel rendering
678 VBOCore.addWidgetToPanel(load_wid_id);
679 $(VBOCore.options.panel_opts.search_selector).trigger('blur');
680
681 return;
682 }
683
684 // handle arrow keys selection
685 if (matching > 0 && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
686 // on arrow key pressed, select the next or prev widget
687 var addws_element = $(VBOCore.options.panel_opts.addws_selector);
688 var addws_cont_pos = addws_element.offset().top;
689 var addws_otheight = addws_element.outerHeight();
690 var addws_scrolltp = addws_element.scrollTop();
691
692 if (e.key === 'ArrowDown') {
693 var default_widg = $(VBOCore.options.panel_opts.addw_selector + ':visible').first();
694 } else {
695 var default_widg = $(VBOCore.options.panel_opts.addw_selector + ':visible').last();
696 }
697 var focussed_wid = $(VBOCore.options.panel_opts.addwfs_selector + ':visible').first();
698 var addw_height = default_widg.outerHeight();
699 var focussed_pos = default_widg.offset().top + addw_height;
700
701 if (focussed_wid.length) {
702 focussed_wid.removeClass(widget_focus_class);
703 if (e.key === 'ArrowDown') {
704 var goto_wid = focussed_wid.nextAll(VBOCore.options.panel_opts.addw_selector + ':visible').first();
705 } else {
706 var goto_wid = focussed_wid.prevAll(VBOCore.options.panel_opts.addw_selector + ':visible').first();
707 }
708 if (goto_wid.length) {
709 goto_wid.addClass(widget_focus_class);
710 focussed_pos = goto_wid.offset().top + addw_height;
711 } else {
712 default_widg.addClass(widget_focus_class);
713 }
714 } else {
715 default_widg.addClass(widget_focus_class);
716 }
717
718 if (focussed_pos > (addws_cont_pos + addws_otheight)) {
719 addws_element.scrollTop(focussed_pos - addws_cont_pos - addw_height + addws_scrolltp);
720 } else if (focussed_pos < 0) {
721 addws_element.scrollTop(0);
722 }
723 }
724 });
725
726 // register listener for adding widgets
727 $(VBOCore.options.panel_opts.addw_selector).on('click', function(e) {
728 var widget_id = $(this).attr(VBOCore.options.panel_opts.addw_data_attr);
729 if (!widget_id || !widget_id.length) {
730 return false;
731 }
732
733 // determine widget rendering method
734 if (e && e.target) {
735 let cktarget = $(e.target);
736 if (e.shiftKey === true || (cktarget.hasClass(VBOCore.options.panel_opts.addw_modal_cls) || cktarget.parent().hasClass(VBOCore.options.panel_opts.addw_modal_cls))) {
737 // widget modal rendering
738 VBOCore.renderModalWidget(widget_id);
739
740 return;
741 }
742 }
743
744 // widget multitask panel rendering
745 VBOCore.addWidgetToPanel(widget_id);
746 });
747
748 // register listener for updating multitask sidepanel with debounce
749 document.addEventListener(VBOCore.options.multitask_save_event, VBOCore.debounceEvent(VBOCore.saveMultitasking, 1000));
750
751 // subscribe to event for multitask shortcut
752 document.addEventListener(VBOCore.multitask_shortcut_event, function() {
753 // toggle multitask panel
754 $(VBOCore.options.panel_opts.btn_trigger).trigger('click');
755 });
756
757 // subscribe to event for multitask search focus shortcut
758 document.addEventListener(VBOCore.multitask_searchfs_event, function() {
759 // focus search multitask widgets
760 $(VBOCore.options.panel_opts.search_selector).trigger('focus');
761 });
762
763 // register click event on edit widgets button
764 $(VBOCore.options.panel_opts.editw_selector).on('click', function() {
765 VBOCore.toggleWidgetsPanelEditing(null);
766 });
767
768 // register detach widget buttons
769 $('.' + VBOCore.options.panel_opts.dtcwidget_class).each(function() {
770 let widget_wrapper = $(this).parent(VBOCore.options.panel_opts.wclass_base_sel);
771 let detach_widget_id = widget_wrapper.attr(VBOCore.options.panel_opts.addw_data_attr);
772 let detach_to_target = widget_wrapper.find('.' + VBOCore.options.panel_opts.dtctarget_class);
773 if (detach_to_target.length) {
774 // move detach wrapper onto the target (widget head)
775 $(this).prependTo(detach_to_target);
776 }
777
778 if (!detach_widget_id) {
779 return;
780 }
781
782 // register detach action
783 $(this).on('click', function() {
784 // detach widget, meaning do a modal rendering
785 VBOCore.renderModalWidget(detach_widget_id);
786 });
787 });
788 }
789
790 /**
791 * Registers the blur event for the search widgets input.
792 */
793 static registerSearchWidgetsBlur() {
794 if (VBOCore.options.active_listeners.hasOwnProperty('registerSearchWidgetsBlur')) {
795 // listener is already registered
796 return true;
797 }
798
799 $(VBOCore.options.panel_opts.search_selector).on('blur', function(e) {
800 if (e && e.relatedTarget) {
801 if (e.relatedTarget.classList.contains(VBOCore.options.panel_opts.addw_selector.replace('.', ''))) {
802 // add new widget was clicked, abort hiding process or click event won't fire on target element
803 return;
804 }
805 }
806 var keyword = $(this).val();
807 if (!keyword.length) {
808 // hide add-widgets container
809 $(VBOCore.options.panel_opts.addws_selector).hide();
810 }
811 });
812
813 // register flag for listener active
814 VBOCore.options.active_listeners['registerSearchWidgetsBlur'] = 1;
815 }
816
817 /**
818 * Removes the blur event handler for the search widgets input.
819 */
820 static unregisterSearchWidgetsBlur() {
821 if (!VBOCore.options.active_listeners.hasOwnProperty('registerSearchWidgetsBlur')) {
822 // nothing to unregister
823 return true;
824 }
825
826 $(VBOCore.options.panel_opts.search_selector).off('blur');
827
828 // delete flag for listener active
829 delete VBOCore.options.active_listeners['registerSearchWidgetsBlur'];
830 }
831
832 /**
833 * Adds a widget identifier to the multitask panel.
834 *
835 * @param string widget_id the widget identifier string to add.
836 */
837 static addWidgetToPanel(widget_id) {
838 if (!VBOCore.options.widget_ajax_uri || !VBOCore.options.panel_opts || !Object.keys(VBOCore.options.panel_opts).length) {
839 throw new Error('Multitask panel options are missing');
840 }
841 // prepend container to panel
842 var widget_classes = [VBOCore.options.panel_opts.wclass_base_sel.replace('.', ''), VBOCore.options.panel_opts.cur_widget_cls];
843 var widget_div = '<div class="' + widget_classes.join(' ') + '" ' + VBOCore.options.panel_opts.addw_data_attr + '="' + widget_id + '" style="display: none;"></div>';
844 var widget_elem = $(widget_div);
845 $(VBOCore.options.panel_opts.actws_selector).prepend(widget_elem);
846
847 // always hide add-widgets container
848 $(VBOCore.options.panel_opts.addws_selector).hide();
849
850 // trigger debounced map saving event
851 VBOCore.emitMultitaskEvent();
852
853 // register listener for input search blur
854 VBOCore.registerSearchWidgetsBlur();
855
856 // render widget
857 var call_method = 'render';
858 VBOCore.doAjax(
859 VBOCore.options.widget_ajax_uri,
860 {
861 widget_id: widget_id,
862 call: call_method,
863 vbo_page: VBOCore.options.current_page,
864 vbo_uri: VBOCore.options.current_page_uri,
865 multitask: 1,
866 },
867 (response) => {
868 // display widgets editing button
869 VBOCore.toggleWidgetsPanelEditing(true);
870 // parse response
871 try {
872 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
873 if (obj_res.hasOwnProperty(call_method)) {
874 // populate widget HTML content and display it
875 widget_elem.html(obj_res[call_method]).fadeIn();
876
877 // always scroll active widgets list to top
878 $(VBOCore.options.panel_opts.actws_selector).scrollTop(0);
879
880 // register detach widget button
881 setTimeout(() => {
882 let detach_elem = $('<div></div>').addClass(VBOCore.options.panel_opts.dtcwidget_class).html(VBOCore.options.panel_opts.dtcwidget_icn);
883 let detach_to_target = widget_elem.find('.' + VBOCore.options.panel_opts.dtctarget_class);
884 if (detach_to_target.length) {
885 // move detach wrapper onto the target (widget head)
886 detach_elem.prependTo(detach_to_target);
887 }
888 // register detach action
889 detach_elem.on('click', function() {
890 // detach widget, meaning do a modal rendering
891 VBOCore.renderModalWidget(widget_id);
892 });
893 }, 500);
894 } else {
895 console.error('Unexpected JSON response', obj_res);
896 }
897 } catch(err) {
898 console.error('could not parse JSON response', err, response);
899 }
900 },
901 (error) => {
902 console.error(error.responseText);
903 }
904 );
905 }
906
907 /**
908 * Toggles the edit mode of the multitask widgets panel.
909 *
910 * @param bool added true if a widget was just added, false if it was just removed.
911 */
912 static toggleWidgetsPanelEditing(added) {
913 // check if we are currently editing
914 var is_editing = ($('.' + VBOCore.options.panel_opts.editmode_class).length > 0);
915
916 // the triggerer button
917 var triggerer = $(VBOCore.options.panel_opts.editw_selector);
918
919 // check added action status
920 if (added === true) {
921 // show button for edit mode
922 triggerer.show();
923 return;
924 }
925
926 // grab all widgets
927 var editing_widgets = $(VBOCore.options.panel_opts.wclass_base_sel);
928
929 if (added === false) {
930 if (!editing_widgets.length) {
931 // hide button for edit mode after removing the last widget
932 triggerer.removeClass(VBOCore.options.panel_opts.editw_selector.substr(1) + '-active').hide();
933 }
934 return;
935 }
936
937 if (is_editing) {
938 // deactivate editing mode
939 editing_widgets.removeClass(VBOCore.options.panel_opts.editmode_class);
940 $('.' + VBOCore.options.panel_opts.rmwidget_class).remove();
941 // toggle triggerer button active class
942 triggerer.removeClass(VBOCore.options.panel_opts.editw_selector.substr(1) + '-active');
943 } else {
944 // activate editing mode by looping through all widgets
945 editing_widgets.each(function() {
946 // build remove-widget element
947 var rm_widget = $('<div></div>').addClass(VBOCore.options.panel_opts.rmwidget_class).on('click', function() {
948 VBOCore.removeWidgetFromPanel(this);
949 }).html(VBOCore.options.panel_opts.rmwidget_icn);
950 // add editing class and prepend removing element
951 $(this).addClass(VBOCore.options.panel_opts.editmode_class).prepend(rm_widget);
952 });
953 // toggle triggerer button active class
954 triggerer.addClass(VBOCore.options.panel_opts.editw_selector.substr(1) + '-active');
955 }
956 }
957
958 /**
959 * Handles the removal of a widget from the multitask panel.
960 *
961 * @param object element
962 */
963 static removeWidgetFromPanel(element) {
964 if (!element) {
965 console.error('Invalid widget element to remove', element);
966 return false;
967 }
968 var widget_cont = $(element).parent(VBOCore.options.panel_opts.wclass_base_sel);
969 if (!widget_cont || !widget_cont.length) {
970 console.error('Could not find widget container to remove', element);
971 return false;
972 }
973 var widget_id = widget_cont.attr(VBOCore.options.panel_opts.addw_data_attr);
974 if (!widget_id || !widget_id.length) {
975 console.error('Empty widget id to remove', element);
976 return false;
977 }
978 // find the index of the widget to remove in the panel
979 var widget_index = $(VBOCore.options.panel_opts.wclass_base_sel).index(widget_cont);
980 if (widget_index < 0) {
981 console.error('Empty widget index to remove', widget_cont);
982 return false;
983 }
984 // make sure the index in the array matches the id
985 if (!VBOCore.options.admin_widgets.hasOwnProperty(widget_index) || VBOCore.options.admin_widgets[widget_index]['id'] != widget_id) {
986 console.error('Unmatching widget index or id', VBOCore.options.admin_widgets, widget_index, widget_id);
987 return false;
988 }
989 // remove this widget from the array
990 VBOCore.options.admin_widgets.splice(widget_index, 1);
991
992 // remove element from document
993 widget_cont.remove();
994
995 // check widgets editing button status
996 VBOCore.toggleWidgetsPanelEditing(false);
997
998 // trigger debounced map saving event
999 VBOCore.emitMultitaskEvent();
1000
1001 if (!VBOCore.options.admin_widgets.length) {
1002 // unregister listener for input search blur
1003 VBOCore.unregisterSearchWidgetsBlur();
1004 }
1005 }
1006
1007 /**
1008 * Emits an event related to the multitask features or a custom event, with optional data.
1009 */
1010 static emitMultitaskEvent(ev_name, ev_data) {
1011 var def_ev_name = VBOCore.options.multitask_save_event;
1012 if (typeof ev_name === 'string') {
1013 def_ev_name = ev_name;
1014 }
1015
1016 if (typeof ev_data !== 'undefined' && ev_data) {
1017 // trigger the custom event
1018 document.dispatchEvent(new CustomEvent(def_ev_name, {bubbles: true, detail: ev_data}));
1019 return;
1020 }
1021
1022 // trigger the event
1023 document.dispatchEvent(new Event(def_ev_name));
1024 }
1025
1026 /**
1027 * Proxy for dispatching an event to the document with optional data.
1028 */
1029 static emitEvent(ev_name, ev_data) {
1030 if (typeof ev_name !== 'string' || !ev_name.length) {
1031 return;
1032 }
1033
1034 return VBOCore.emitMultitaskEvent(ev_name, ev_data);
1035 }
1036
1037 /**
1038 * Attempts to save the multitask widgets for this page.
1039 */
1040 static saveMultitasking() {
1041 // gather the list of active widgets
1042 var active_widgets = [];
1043 var cur_admin_widgets = [];
1044 $(VBOCore.options.panel_opts.actws_selector).find(VBOCore.options.panel_opts.wclass_base_sel).each(function() {
1045 var widget_id = $(this).attr(VBOCore.options.panel_opts.addw_data_attr);
1046 if (widget_id && widget_id.length) {
1047 // push id in list
1048 active_widgets.push(widget_id);
1049 // push object with dummy name for global widgets
1050 cur_admin_widgets.push({
1051 id: widget_id,
1052 name: widget_id,
1053 });
1054 }
1055 });
1056
1057 // update multitask widgets map for this page
1058 VBOCore.doAjax(
1059 VBOCore.options.multitask_ajax_uri,
1060 {
1061 call: 'updateMultitaskingMap',
1062 call_args: [
1063 VBOCore.options.current_page,
1064 active_widgets,
1065 0
1066 ],
1067 },
1068 (response) => {
1069 try {
1070 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1071 if (obj_res.hasOwnProperty('result') && obj_res['result']) {
1072 // set current widgets
1073 VBOCore.setOptions({
1074 admin_widgets: cur_admin_widgets
1075 });
1076 } else {
1077 console.error('Unexpected or invalid JSON response', response);
1078 }
1079 } catch(err) {
1080 console.error('could not parse JSON response', err, response);
1081 }
1082 },
1083 (error) => {
1084 console.error(error.responseText);
1085 }
1086 );
1087 }
1088
1089 /**
1090 * Sets up the browser notifications within the multitask panel, if supported.
1091 */
1092 static setupNotifications() {
1093 if (!('Notification' in window)) {
1094 // browser does not support notifications
1095 $(VBOCore.options.panel_opts.notif_selector).hide();
1096 return false;
1097 }
1098
1099 if (Notification.permission && Notification.permission === 'granted') {
1100 // permissions were granted already
1101 $(VBOCore.options.panel_opts.notif_selector)
1102 .addClass(VBOCore.options.panel_opts.notif_on_class)
1103 .attr('title', VBOCore.options.tn_texts.notifs_enabled)
1104 .on('click', function() {
1105 // show congratulations notification
1106 VBOCore.dispatchCongratulations();
1107
1108 // attempt to update the service worker installation
1109 VBOCore.installServiceWorker(true).then(() => {
1110 // all good
1111 }).catch((error) => {
1112 console.error(error);
1113 });
1114 });
1115 return true;
1116 }
1117
1118 // notifications supported, but perms not granted
1119 $(VBOCore.options.panel_opts.notif_selector)
1120 .addClass(VBOCore.options.panel_opts.notif_off_class)
1121 .attr('title', VBOCore.options.tn_texts.notifs_disabled);
1122
1123 // register click-event listener on button to enable notifications
1124 $(VBOCore.options.panel_opts.notif_selector).on('click', function() {
1125 VBOCore.requestNotifPerms();
1126 });
1127
1128 // subscribe to the multitask-panel-open event to show the status of the notifications
1129 document.addEventListener(VBOCore.multitask_open_event, function() {
1130 if (VBOCore.notificationsEnabled() === false) {
1131 // add "shaking" class to notifications button
1132 $(VBOCore.options.panel_opts.notif_selector).addClass('shaking');
1133 }
1134 });
1135
1136 // subscribe to the multitask-panel-close event to update the status of the notifications
1137 document.addEventListener(VBOCore.multitask_close_event, function() {
1138 // always remove "shaking" class from notifications button
1139 $(VBOCore.options.panel_opts.notif_selector).removeClass('shaking');
1140 });
1141 }
1142
1143 /**
1144 * Sets up the browser notifications within the given selector, if supported.
1145 *
1146 * @param string selector the element query selector.
1147 */
1148 static suggestNotifications(selector) {
1149 if (!selector) {
1150 return false;
1151 }
1152
1153 if (!('Notification' in window)) {
1154 // browser does not support notifications
1155 $(selector).hide();
1156 return false;
1157 }
1158
1159 if (Notification.permission && Notification.permission === 'granted') {
1160 // permissions were granted already
1161 $(selector)
1162 .addClass(VBOCore.options.panel_opts.notif_on_class)
1163 .attr('title', VBOCore.options.tn_texts.notifs_enabled)
1164 .on('click', function() {
1165 VBOCore.dispatchCongratulations();
1166 });
1167 return true;
1168 }
1169
1170 // notifications supported, but perms not granted
1171 $(selector)
1172 .addClass(VBOCore.options.panel_opts.notif_off_class)
1173 .attr('title', VBOCore.options.tn_texts.notifs_disabled);
1174
1175 // register click-event listener on button to enable notifications
1176 $(selector).on('click', function() {
1177 VBOCore.requestNotifPerms(selector);
1178 });
1179
1180 // add "shaking" class to make the selector more appealing
1181 $(selector).addClass('shaking');
1182
1183 // remove the "shaking" class after some time
1184 setTimeout(() => {
1185 $(selector).removeClass('shaking');
1186 }, 2000);
1187 }
1188
1189 /**
1190 * Tells whether the notifications are enabled, disabled, not supported.
1191 */
1192 static notificationsEnabled() {
1193 if (!('Notification' in window)) {
1194 // not supported
1195 return null;
1196 }
1197
1198 if (Notification.permission && Notification.permission === 'granted') {
1199 // enabled
1200 return true;
1201 }
1202
1203 // disabled
1204 return false;
1205 }
1206
1207 /**
1208 * Attempts to request the notifications permissions to the browser.
1209 * For security reasons, this should run upon a user gesture (click).
1210 *
1211 * @param string selector optional element query selector.
1212 */
1213 static requestNotifPerms(selector) {
1214 if (!('Notification' in window)) {
1215 // browser does not support notifications
1216 return false;
1217 }
1218
1219 // run permissions request in a try-catch statement to support all browsers
1220 try {
1221 // handle promise-based version to request permissions
1222 Notification.requestPermission().then((permission) => {
1223 VBOCore.handleNotifPerms(permission, selector);
1224 });
1225 } catch(e) {
1226 // run the callback-based version
1227 Notification.requestPermission(function(permission) {
1228 VBOCore.handleNotifPerms(permission, selector);
1229 });
1230 }
1231 }
1232
1233 /**
1234 * Handles the notifications permission response (from callback or promise resolved).
1235 *
1236 * @param any permission permission status string or NotificationPermission object.
1237 * @param string selector optional element query selector.
1238 */
1239 static handleNotifPerms(permission, selector) {
1240 // check the permission status from the Notification object interface
1241 if ((Notification.permission && Notification.permission === 'granted') || (typeof permission === 'string' && permission === 'granted')) {
1242 // permissions granted!
1243 $((selector || VBOCore.options.panel_opts.notif_selector))
1244 .removeClass(VBOCore.options.panel_opts.notif_off_class)
1245 .addClass(VBOCore.options.panel_opts.notif_on_class)
1246 .attr('title', VBOCore.options.tn_texts.notifs_enabled);
1247
1248 // dispatch an immediate notification to congratulate with the activation
1249 VBOCore.dispatchCongratulations();
1250
1251 return true;
1252 } else {
1253 // permissions denied :(
1254 $((selector || VBOCore.options.panel_opts.notif_selector))
1255 .removeClass(VBOCore.options.panel_opts.notif_on_class)
1256 .addClass(VBOCore.options.panel_opts.notif_off_class)
1257 .attr('title', VBOCore.options.tn_texts.notifs_disabled);
1258
1259 // show alert message
1260 console.error('Permission denied for enabling browser notifications', permission);
1261 alert(VBOCore.options.tn_texts.notifs_disabled_help);
1262 }
1263
1264 return false;
1265 }
1266
1267 /**
1268 * Dispatches an immediate notification with a congratulations text.
1269 *
1270 * @return void
1271 */
1272 static dispatchCongratulations() {
1273 try {
1274 let notif = new Notification(VBOCore.options.tn_texts.congrats, {
1275 body: VBOCore.options.tn_texts.notifs_enabled,
1276 tag: 'vbo_notification_congrats',
1277 silent: false
1278 });
1279 } catch(error) {
1280 alert(error);
1281 }
1282 }
1283
1284 /**
1285 * Given a date-time string, returns a Date object representation.
1286 *
1287 * @param string dtime_str the date-time string in "Y-m-d H:i:s" format.
1288 */
1289 static getDateTimeObject(dtime_str) {
1290 // instantiate a new date object
1291 var date_obj = new Date();
1292
1293 // parse date-time string
1294 let dtime_parts = dtime_str.split(' ');
1295 let date_parts = dtime_parts[0].split('-');
1296 if (dtime_parts.length != 2 || date_parts.length != 3) {
1297 // invalid military format
1298 return date_obj;
1299 }
1300 let time_parts = dtime_parts[1].split(':');
1301
1302 // set accurate date-time values
1303 date_obj.setFullYear(date_parts[0]);
1304 date_obj.setMonth((parseInt(date_parts[1]) - 1));
1305 date_obj.setDate(parseInt(date_parts[2]));
1306 date_obj.setHours(parseInt(time_parts[0]));
1307 date_obj.setMinutes(parseInt(time_parts[1]));
1308 date_obj.setSeconds(0);
1309 date_obj.setMilliseconds(0);
1310
1311 // return the accurate date object
1312 return date_obj;
1313 }
1314
1315 /**
1316 * Given a list of schedules, enqueues notifications to watch.
1317 *
1318 * @param array|object schedules list of or one notification object(s).
1319 *
1320 * @return bool
1321 */
1322 static enqueueNotifications(schedules) {
1323 if (!Array.isArray(schedules) || !schedules.length) {
1324 if (typeof schedules === 'object' && schedules.hasOwnProperty('dtime')) {
1325 // convert the single schedule to an array
1326 schedules = [schedules];
1327 } else {
1328 // invalid argument passed
1329 return false;
1330 }
1331 }
1332
1333 for (var i in schedules) {
1334 if (!schedules.hasOwnProperty(i) || typeof schedules[i] !== 'object') {
1335 continue;
1336 }
1337 VBOCore.notifications.push(schedules[i]);
1338 }
1339
1340 // setup the timeouts to schedule the notifications
1341 return VBOCore.scheduleNotifications();
1342 }
1343
1344 /**
1345 * Schedule the trigger timings for each notification.
1346 */
1347 static scheduleNotifications() {
1348 if (!VBOCore.notifications.length) {
1349 // no notifications to be scheduled
1350 return false;
1351 }
1352 if (VBOCore.notificationsEnabled() !== true) {
1353 // notifications not enabled
1354 console.info('Browser notifications disabled or unsupported.');
1355 return false;
1356 }
1357
1358 // gather current date-timing information
1359 const now_date = new Date();
1360 const now_time = now_date.getTime();
1361
1362 // parse all notifications to schedule the timers if not set
1363 for (let i = 0; i < VBOCore.notifications.length; i++) {
1364 let notif = VBOCore.notifications[i];
1365
1366 if (typeof notif !== 'object' || !notif.hasOwnProperty('dtime')) {
1367 // invalid notification object, unset it
1368 VBOCore.notifications.splice(i, 1);
1369 continue;
1370 }
1371
1372 // check if timer has been set
1373 if (!notif.hasOwnProperty('id_timer')) {
1374 // estimate trigger timing
1375 let in_ms = 0;
1376 // check for imminent notifications
1377 if (typeof notif.dtime === 'string' && notif.dtime.indexOf('now') >= 0) {
1378 // imminent ones will be delayed by one second
1379 in_ms = 1000;
1380 } else {
1381 // check overdue date-time (notif.dtime can also be a Date object instance)
1382 let nexp = VBOCore.getDateTimeObject(notif.dtime);
1383 in_ms = nexp.getTime() - now_time;
1384 }
1385 if (in_ms > 0) {
1386 // schedule notification trigger
1387 let id_timer = setTimeout(() => {
1388 VBOCore.dispatchNotification(notif);
1389 }, in_ms);
1390 // set timer on notification object
1391 VBOCore.notifications[i]['id_timer'] = id_timer;
1392 }
1393 }
1394 }
1395
1396 return true;
1397 }
1398
1399 /**
1400 * Deregister all scheduled notifications.
1401 */
1402 static unscheduleNotifications() {
1403 if (!VBOCore.notifications.length) {
1404 // no notifications scheduled
1405 return false;
1406 }
1407
1408 for (let i = 0; i < VBOCore.notifications.length; i++) {
1409 let notif = VBOCore.notifications[i];
1410
1411 if (typeof notif === 'object' && notif.hasOwnProperty('id_timer')) {
1412 // unset timeout for this notification
1413 clearTimeout(notif['id_timer']);
1414 }
1415 }
1416
1417 // reset pool
1418 VBOCore.notifications = [];
1419 }
1420
1421 /**
1422 * Update or delete a previously scheduled notification.
1423 *
1424 * @param object match_props map of properties to match.
1425 * @param string|number newdtime the new date time to schedule (0 for deleting).
1426 *
1427 * @return null|bool true only if a notification matched.
1428 */
1429 static updateNotification(match_props, newdtime) {
1430 if (!VBOCore.notifications.length) {
1431 // no notifications set, terminate
1432 return null;
1433 }
1434
1435 if (typeof match_props !== 'object') {
1436 // no properties to match the notification
1437 return null;
1438 }
1439
1440 // gather current date-timing information
1441 const now_date = new Date();
1442 const now_time = now_date.getTime();
1443
1444 // parse all notifications scheduled
1445 for (let i = 0; i < VBOCore.notifications.length; i++) {
1446 let notif = VBOCore.notifications[i];
1447
1448 let all_matched = true;
1449 let to_matching = false;
1450 for (let prop in match_props) {
1451 if (!match_props.hasOwnProperty(prop)) {
1452 continue;
1453 }
1454 to_matching = true;
1455 if (!notif.hasOwnProperty(prop) || notif[prop] != match_props[prop]) {
1456 all_matched = false;
1457 break;
1458 }
1459 }
1460
1461 if (all_matched && to_matching) {
1462 // notification object found
1463 if (notif.hasOwnProperty('id_timer')) {
1464 // unset previous timeout for this notification
1465 clearTimeout(notif['id_timer']);
1466 }
1467 // update or delete scheduled notification
1468 if (newdtime === 0) {
1469 // delete notification from queue
1470 VBOCore.notifications.splice(i, 1);
1471 } else {
1472 // update timing scheduler
1473 let in_ms = 0;
1474 // check for imminent notifications
1475 if (typeof newdtime === 'string' && newdtime.indexOf('now') >= 0) {
1476 // imminent ones will be delayed by one second
1477 in_ms = 1000;
1478 } else {
1479 // check overdue date-time (newdtime can also be a Date object instance)
1480 let nexp = VBOCore.getDateTimeObject(newdtime);
1481 in_ms = nexp.getTime() - now_time;
1482 }
1483 if (in_ms > 0) {
1484 // schedule notification trigger
1485 let id_timer = setTimeout(() => {
1486 VBOCore.dispatchNotification(notif);
1487 }, in_ms);
1488 // set timer on notification object
1489 VBOCore.notifications[i]['id_timer'] = id_timer;
1490 }
1491 // update date-time value on notification object
1492 VBOCore.notifications[i]['dtime'] = newdtime;
1493 }
1494
1495 // terminate parsing and return true
1496 return true;
1497 }
1498 }
1499
1500 // notification object not found
1501 return false;
1502 }
1503
1504 /**
1505 * Dispatch the notification object.
1506 * Expected notification properties:
1507 *
1508 * {
1509 * id: number
1510 * type: string
1511 * dtime: string|Date
1512 * build_url: string|null
1513 * }
1514 *
1515 * @param object notif the notification object.
1516 */
1517 static dispatchNotification(notif) {
1518 if (typeof notif !== 'object') {
1519 return false;
1520 }
1521
1522 // subscribe to building notification data
1523 VBOCore.buildNotificationData(notif).then((data) => {
1524 // dispatch the notification
1525
1526 // check if the click event should be registered
1527 let func_nodes;
1528 if (data.onclick && typeof data.onclick === 'string') {
1529 let callback_parts = data.onclick.split('.');
1530 while (callback_parts.length) {
1531 // compose window static method string to avoid using eval()
1532 let tmp = callback_parts.shift();
1533 if (!func_nodes) {
1534 func_nodes = window[tmp];
1535 } else {
1536 func_nodes = func_nodes[tmp];
1537 }
1538 }
1539 }
1540
1541 // prepare properties to delete the notification from queue
1542 let match_props = {};
1543 for (let prop in notif) {
1544 if (!notif.hasOwnProperty(prop) || prop == 'id_timer') {
1545 continue;
1546 }
1547 match_props[prop] = notif[prop];
1548 }
1549
1550 // check browser Notifications API
1551 if (VBOCore.notificationsEnabled() !== true) {
1552 // notifications not enabled, fallback to toast message
1553 let toast_notif_data = {
1554 title: data.title,
1555 body: data.message,
1556 icon: data.icon,
1557 delay: {
1558 min: 6000,
1559 max: 20000,
1560 tolerance: 4000,
1561 },
1562 action: () => {
1563 VBOToast.dispose(true);
1564 },
1565 sound: VBOCore.options.notif_audio_url
1566 };
1567 if (func_nodes) {
1568 toast_notif_data.action = function() {
1569 func_nodes(data);
1570 VBOToast.dispose(true);
1571 };
1572 } else if (typeof data.onclick === 'function') {
1573 toast_notif_data.action = function() {
1574 data.onclick.call(data);
1575 VBOToast.dispose(true);
1576 };
1577 }
1578 VBOToast.enqueue(new VBOToastMessage(toast_notif_data));
1579
1580 // delete dispatched notification from queue
1581 VBOCore.updateNotification(match_props, 0);
1582
1583 return;
1584 }
1585
1586 // use the browser's native Notifications API
1587 let browser_notif = new Notification(data.title, {
1588 body: data.message,
1589 icon: data.icon,
1590 tag: 'vbo_notification',
1591 silent: false
1592 });
1593
1594 // check if support for Web App badge is available
1595 if (typeof navigator.setAppBadge !== 'undefined') {
1596 navigator.setAppBadge(1);
1597 }
1598
1599 browser_notif.addEventListener('click', (e) => {
1600 // check if support for Web App badge is available
1601 if (typeof navigator.setAppBadge !== 'undefined') {
1602 navigator.clearAppBadge();
1603 }
1604 // close the notification
1605 e.target.close();
1606 });
1607
1608 if (func_nodes) {
1609 // register notification click event
1610 browser_notif.addEventListener('click', () => {
1611 func_nodes(data);
1612 });
1613 } else if (typeof data.onclick === 'function') {
1614 browser_notif.addEventListener('click', () => {
1615 data.onclick.call(data);
1616 });
1617 }
1618
1619 // delete dispatched notification from queue
1620 VBOCore.updateNotification(match_props, 0);
1621
1622 }).catch((error) => {
1623 console.error(error);
1624 });
1625 }
1626
1627 /**
1628 * Asynchronous build of the notification data object for dispatch.
1629 * Minimum expected notification display data properties:
1630 *
1631 * {
1632 * title: string
1633 * message: string
1634 * icon: string
1635 * onclick: function
1636 * }
1637 *
1638 * @param object notif the scheduled notification object.
1639 *
1640 * @return Promise
1641 */
1642 static buildNotificationData(notif) {
1643 return new Promise((resolve, reject) => {
1644 // notification object validation
1645 if (typeof notif !== 'object') {
1646 reject('Invalid notification object');
1647 return;
1648 }
1649
1650 if (!notif.hasOwnProperty('build_url') || !notif.build_url) {
1651 // building callback not necessary
1652 if (!notif.title && !notif.message) {
1653 reject('Unexpected notification object');
1654 return;
1655 }
1656 // we expect the notification to be built already
1657 resolve(notif);
1658 return;
1659 }
1660
1661 // build the notification data
1662 VBOCore.doAjax(
1663 notif.build_url,
1664 {
1665 payload: JSON.stringify(notif)
1666 },
1667 (response) => {
1668 // parse response
1669 try {
1670 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1671 if (obj_res.hasOwnProperty('title')) {
1672 resolve(obj_res);
1673 } else {
1674 reject('Unexpected JSON response');
1675 }
1676 } catch(err) {
1677 reject('could not parse JSON response');
1678 }
1679 },
1680 (error) => {
1681 reject(error.responseText);
1682 }
1683 );
1684 });
1685 }
1686
1687 /**
1688 * Handle a navigation towards a given URL.
1689 * Common handler for browser notifications click.
1690 *
1691 * @param object data notification display data payload.
1692 */
1693 static handleGoto(data) {
1694 if (typeof data !== 'object' || !data.hasOwnProperty('gotourl') || !data.gotourl) {
1695 console.error('Could not handle the goto operation', data);
1696 return;
1697 }
1698
1699 if (typeof data.openWindow !== 'undefined' || typeof document === 'undefined') {
1700 // open a new window
1701 window.open(data.gotourl);
1702 return;
1703 }
1704
1705 // redirect
1706 document.location.href = data.gotourl;
1707 }
1708
1709 /**
1710 * Opens a new window with the URI to render the Push notification data.
1711 *
1712 * @param object options admin widget rendering options to bind.
1713 *
1714 * @return void
1715 */
1716 static openAdminPushURI(options) {
1717 if (typeof options !== 'object' || !options) {
1718 throw new Error('Unable to build Admin Push URI');
1719 }
1720
1721 let vbo_admin_push_uri = VBOCore.options.root_uri;
1722
1723 if (VBOCore.options.cms === 'wordpress') {
1724 vbo_admin_push_uri += 'wp-admin/admin.php?page=vikbooking';
1725 } else {
1726 vbo_admin_push_uri += 'administrator/index.php?option=com_vikbooking';
1727 }
1728
1729 // build Push notification payload for rendering
1730 let push_data = {
1731 type: options.type || '',
1732 title: options.title || '',
1733 message: options.message || '',
1734 content: options,
1735 };
1736
1737 // open page in a new window
1738 window.open(vbo_admin_push_uri + '&push_notification=' + btoa(JSON.stringify(push_data)), '_blank');
1739 }
1740
1741 /**
1742 * Handle the display of a notification through a widget.
1743 * Common handler for browser notifications displayed through
1744 * a widget modal rendering.
1745 *
1746 * @param object data notification display data payload.
1747 * @param object options optional extra options for the admin widget.
1748 * @param boolean is_push whether we are coming from a Push notification.
1749 *
1750 * @return void
1751 */
1752 static handleDisplayWidgetNotification(data, options, is_push) {
1753 try {
1754 // validate handler
1755 if (!VBOCore.options.is_vbo && !VBOCore.options.is_vcm) {
1756 throw new Error('Unable to handle the notification display from the current page');
1757 }
1758
1759 // validate payload
1760 if (typeof data !== 'object' || !data.hasOwnProperty('widget_id') || !data['widget_id']) {
1761 throw new Error('Invalid widget payload');
1762 }
1763
1764 // parse handler
1765 if (VBOCore.options.is_vcm || (is_push && !VBOCore.options.is_vbo)) {
1766 // the operation is handled by VCM (or by an external admin resource, if it's a Push notification)
1767 VBOCore.loadAdminWidgetAssets(data).then((assets) => {
1768 // append assets to DOM
1769 assets.forEach((asset) => {
1770 if (!$('link#' + asset['id']).length) {
1771 $('head').append('<link rel="stylesheet" id="' + asset['id'] + '" href="' + asset['href'] + '" media="all" />');
1772 }
1773 });
1774 // widget modal rendering handled by VCM (or an external admin resource)
1775 let modal_data = VBOCore.renderModalWidget(data['widget_id'], data, options, false);
1776 if (modal_data.hasOwnProperty('dismissed_event')) {
1777 // register event to unload all assets
1778 document.addEventListener(modal_data['dismissed_event'], () => {
1779 assets.forEach((asset) => {
1780 if ($('link#' + asset['id']).length) {
1781 $('link#' + asset['id']).remove();
1782 }
1783 });
1784 });
1785 }
1786 }).catch((error) => {
1787 throw new Error(error);
1788 });
1789 } else {
1790 // widget modal rendering handled by Vik Booking
1791 VBOCore.renderModalWidget(data['widget_id'], data, options, false);
1792 }
1793 } catch(e) {
1794 if (is_push && options.type) {
1795 // fallback to opening a new window to render the clicked Push notification
1796 VBOCore.openAdminPushURI(options);
1797 } else {
1798 // fallback to a regular link opening, if set
1799 VBOCore.handleGoto(data);
1800 }
1801 }
1802 }
1803
1804 /**
1805 * Asynchronous loading of CSS assets required to render
1806 * an admin widget outside Vik Booking.
1807 *
1808 * @param object data the optional widget payload data.
1809 *
1810 * @return Promise
1811 */
1812 static loadAdminWidgetAssets(data) {
1813 return new Promise((resolve, reject) => {
1814 // the remote assets URI must be set
1815 if (!VBOCore.options.assets_ajax_uri) {
1816 reject('Missing remote assets URI');
1817 return;
1818 }
1819
1820 if (typeof data !== 'object') {
1821 data = {};
1822 }
1823
1824 // make the request
1825 VBOCore.doAjax(
1826 VBOCore.options.assets_ajax_uri,
1827 data,
1828 (response) => {
1829 // parse response
1830 try {
1831 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1832 if (!Array.isArray(obj_res)) {
1833 reject('Unexpected JSON response');
1834 }
1835 resolve(obj_res);
1836 } catch(err) {
1837 reject('could not parse JSON response');
1838 }
1839 },
1840 (error) => {
1841 reject(error.responseText);
1842 }
1843 );
1844 });
1845 }
1846
1847 /**
1848 * Register the latest data to watch for the preloaded admin widgets.
1849 *
1850 * @param object watch_data
1851 */
1852 static registerWatchData(watch_data) {
1853 if (typeof watch_data !== 'object' || watch_data == null) {
1854 VBOCore.widgets_watch_data = null;
1855 return false;
1856 }
1857
1858 // set watch-data map
1859 VBOCore.widgets_watch_data = watch_data;
1860
1861 // schedule watching interval
1862 if (VBOCore.watch_data_interval == null) {
1863 VBOCore.watch_data_interval = window.setInterval(VBOCore.watchWidgetsData, 60000);
1864 }
1865
1866 // set up broadcast channels to connect all browsing contexts
1867 if (typeof BroadcastChannel !== 'undefined') {
1868 // set up watch-data broadcast channel
1869 if (!VBOCore.broadcast_watch_data) {
1870 // start broadcast channel
1871 VBOCore.broadcast_watch_data = new BroadcastChannel('vikbooking_watch_data');
1872
1873 // register to the "on broadcast message received" event
1874 VBOCore.broadcast_watch_data.onmessage = (event) => {
1875 if (event && event.data) {
1876 // update watch data map for next schedule to avoid dispatching duplicate notifications
1877 VBOCore.widgets_watch_data = event.data;
1878 }
1879 };
1880 }
1881
1882 // set up push-data broadcast channel
1883 if (!VBOCore.broadcast_push_data) {
1884 // start broadcast channel
1885 VBOCore.broadcast_push_data = new BroadcastChannel('vikbooking_push_data');
1886
1887 // register to the "on broadcast message received" event
1888 VBOCore.broadcast_push_data.onmessage = (event) => {
1889 if (event && event.data) {
1890 // update pushed data map for the next watching event
1891 VBOCore.widgets_pushed_data.push(event.data);
1892 }
1893 };
1894 }
1895 }
1896 }
1897
1898 /**
1899 * Periodic widgets data watching for new events.
1900 */
1901 static watchWidgetsData() {
1902 if (typeof VBOCore.widgets_watch_data !== 'object' || VBOCore.widgets_watch_data == null) {
1903 return;
1904 }
1905
1906 // call on new events
1907 VBOCore.doAjax(
1908 VBOCore.options.watchdata_ajax_uri,
1909 {
1910 watch_data: JSON.stringify(VBOCore.widgets_watch_data),
1911 pushed_data: JSON.stringify(VBOCore.widgets_pushed_data),
1912 },
1913 (response) => {
1914 try {
1915 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1916 if (obj_res.hasOwnProperty('watch_data')) {
1917 // update watch data map for next schedule
1918 VBOCore.widgets_watch_data = obj_res['watch_data'];
1919
1920 // check for notifications
1921 if (obj_res.hasOwnProperty('notifications') && Array.isArray(obj_res['notifications'])) {
1922 // dispatch notifications
1923 for (var i = 0; i < obj_res['notifications'].length; i++) {
1924 VBOCore.dispatchNotification(obj_res['notifications'][i]);
1925 }
1926
1927 // post message onto broadcast channel for any other browsing context
1928 if (VBOCore.broadcast_watch_data && obj_res['notifications'].length) {
1929 // this will avoid dispatching duplicate notifications
1930 VBOCore.broadcast_watch_data.postMessage(VBOCore.widgets_watch_data);
1931 }
1932 }
1933 } else {
1934 console.error('Unexpected or invalid JSON response', response);
1935 }
1936 } catch(err) {
1937 console.error('could not parse JSON response', err, response);
1938 }
1939 },
1940 (error) => {
1941 console.error(error.responseText);
1942 }
1943 );
1944 }
1945
1946 /**
1947 * Widget modal rendering.
1948 *
1949 * @param string widget_id the widget identifier to render.
1950 * @param any data the optional multitask data to inject.
1951 * @param object options optional list of extra options to render the widget with Multitask data.
1952 * @param bool hide_panel if false, the multitask panel elements will remain unchanged.
1953 *
1954 * @return object the multitask data injected object merged with modal options.
1955 */
1956 static renderModalWidget(widget_id, data, options, hide_panel) {
1957 /**
1958 * Adjust arguments for BC with previous ordering between options
1959 * and hide_panel, which used to be inverted. Useful also to shorten
1960 * the way the method can be invoked in case no options are needed.
1961 */
1962 if (typeof options === 'boolean') {
1963 // switch argument values
1964 hide_panel = options;
1965 options = null;
1966 }
1967
1968 if (typeof data !== 'object' || data == null) {
1969 // always treat data as an object
1970 data = {};
1971 }
1972
1973 if (typeof options !== 'object' || options == null) {
1974 // always treat options as an object
1975 options = data._options || {};
1976 }
1977
1978 // build the default widget payload
1979 let modal_js_id = Math.floor(Math.random() * 100000);
1980 let modal_title = options._push && options.title ? options.title : VBOCore.options.tn_texts.admin_widget;
1981 let widget_data = {
1982 _modalRendering: 1,
1983 _modalJsId: modal_js_id,
1984 _modalTitle: modal_title,
1985 _options: options,
1986 };
1987
1988 // merge default payload options with given options
1989 data = Object.assign(widget_data, data);
1990
1991 // define unique modal event names to avoid conflicts
1992 let dismiss_event = 'vbo-dismiss-widget-modal' + modal_js_id;
1993 let loading_event = 'vbo-loading-widget-modal' + modal_js_id;
1994
1995 // define the modal options
1996 let modal_options = {
1997 suffix: 'widget_modal',
1998 extra_class: 'vbo-modal-widget vbo-modal-rounded vbo-modal-tall vbo-modal-nofooter',
1999 title: data._modalTitle,
2000 body_prepend: true,
2001 lock_scroll: true,
2002 dismiss_event: dismiss_event,
2003 loading_event: loading_event,
2004 loading_body: VBOCore.options.default_loading_body,
2005 dismissed_event: VBOCore.options.widget_modal_dismissed + modal_js_id,
2006 event_data: widget_data,
2007 };
2008
2009 // check if options should rewrite some modal-options
2010 if (options.modal_options) {
2011 modal_options = Object.assign(modal_options, options.modal_options);
2012 }
2013
2014 // display modal
2015 let widget_modal = VBOCore.displayModal(modal_options);
2016
2017 if (hide_panel !== false) {
2018 // blur search widget input, hide multitask panel
2019 $(VBOCore.options.panel_opts.search_selector).trigger('blur');
2020 VBOCore.emitEvent(VBOCore.multitask_shortcut_event);
2021 }
2022
2023 // start loading
2024 VBOCore.emitEvent(loading_event);
2025
2026 // render admin widget
2027 VBOCore.renderAdminWidget(widget_id, data).then((content) => {
2028 // stop loading and append widget content to modal
2029 VBOCore.emitEvent(loading_event);
2030 widget_modal.append(content);
2031 }).catch((error) => {
2032 // dismiss modal and display error
2033 VBOCore.emitEvent(dismiss_event);
2034 alert(error);
2035 });
2036
2037 return Object.assign(data, modal_options);
2038 }
2039
2040 /**
2041 * Renders an admin widget.
2042 *
2043 * @param string widget_id the widget identifier string to add.
2044 * @param any data the optional multitask data to inject.
2045 *
2046 * @return Promise
2047 */
2048 static renderAdminWidget(widget_id, data) {
2049 return new Promise((resolve, reject) => {
2050 if (!VBOCore.options.widget_ajax_uri) {
2051 reject('Could not load admin widget');
2052 return;
2053 }
2054
2055 var call_method = 'render';
2056 VBOCore.doAjax(
2057 VBOCore.options.widget_ajax_uri,
2058 {
2059 widget_id: widget_id,
2060 call: call_method,
2061 vbo_page: VBOCore.options.current_page,
2062 vbo_uri: VBOCore.options.current_page_uri,
2063 multitask: 1,
2064 multitask_data: data,
2065 },
2066 (response) => {
2067 // parse response
2068 try {
2069 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
2070 if (!obj_res.hasOwnProperty(call_method)) {
2071 reject('Unexpected JSON response');
2072 return;
2073 }
2074 resolve(obj_res[call_method]);
2075 } catch(err) {
2076 reject('could not parse JSON response');
2077 }
2078 },
2079 (error) => {
2080 reject(error.responseText);
2081 }
2082 );
2083 });
2084 }
2085
2086 /**
2087 * Helper method used to copy the text of an
2088 * input element within the clipboard.
2089 *
2090 * Clipboard copy will take effect only in case the
2091 * function is handled by a DOM event explicitly
2092 * triggered by the user, such as a "click".
2093 *
2094 * @param any input The input containing the text to copy.
2095 *
2096 * @return Promise
2097 */
2098 static copyToClipboard(input) {
2099 // register and return promise
2100 return new Promise((resolve, reject) => {
2101 // define a fallback function
2102 var fallback = function(input) {
2103 // focus the input
2104 input.focus();
2105 // select the text inside the input
2106 input.select();
2107
2108 try {
2109 // try to copy with shell command
2110 var copy = document.execCommand('copy');
2111
2112 if (copy) {
2113 // copied successfully
2114 resolve(copy);
2115 } else {
2116 // unable to copy
2117 reject(copy);
2118 }
2119 } catch (error) {
2120 // unable to exec the command
2121 reject(error);
2122 }
2123 };
2124
2125 // look for navigator clipboard
2126 if (!navigator || !navigator.clipboard) {
2127 // navigator clipboard not supported, use fallback
2128 fallback(input);
2129 return;
2130 }
2131
2132 // try to copy within the clipboard by using the navigator
2133 navigator.clipboard.writeText((input.val ? input.val() : input.value)).then(() => {
2134 // copied successfully
2135 resolve(true);
2136 }).catch((error) => {
2137 // revert to the fallback
2138 fallback(input);
2139 });
2140 });
2141 }
2142
2143 /**
2144 * Helper method used to display a modal window dinamycally.
2145 *
2146 * @param object options The options to render the modal.
2147 *
2148 * @return object The modal content element wrapper.
2149 */
2150 static displayModal(options) {
2151 var def_options = {
2152 suffix: (Math.floor(Math.random() * 100000)) + '',
2153 extra_class: null,
2154 title: '',
2155 body: '',
2156 body_prepend: false,
2157 lock_scroll: false,
2158 footer_left: null,
2159 footer_right: null,
2160 dismiss_event: null,
2161 dismissed_event: null,
2162 onDismiss: null,
2163 loading_event: null,
2164 loading_body: VBOCore.options.default_loading_body,
2165 event_data: null,
2166 };
2167
2168 // merge default options with given options
2169 options = Object.assign(def_options, options);
2170
2171 // create the modal dismiss function
2172 var modal_dismiss_fn = (e) => {
2173 custom_modal.fadeOut(400, () => {
2174 // invoke callback for onDismiss
2175 if (typeof options.onDismiss === 'function') {
2176 options.onDismiss.call(custom_modal, e);
2177 }
2178
2179 // check if modal did register to the loading event
2180 if (options.loading_event) {
2181 // we can now un-register from the loading event until a new modal is displayed and will register to it again
2182 document.removeEventListener(options.loading_event, modal_handle_loading_event_fn);
2183 }
2184
2185 // check if we should fire the given modal dismissed event
2186 if (options.dismissed_event) {
2187 VBOCore.emitEvent(options.dismissed_event, options.event_data);
2188 }
2189
2190 // check if body scroll lock should be removed
2191 if (options.lock_scroll) {
2192 $('body').removeClass('vbo-modal-lock-scroll');
2193 }
2194
2195 // remove modal from DOM
2196 custom_modal.remove();
2197 });
2198 };
2199
2200 // create the modal loading event handler function
2201 var modal_handle_loading_event_fn = (e) => {
2202 // toggle modal loading
2203 if ($('.vbo-modal-overlay-content-backdrop').length) {
2204 // hide loading
2205 $('.vbo-modal-overlay-content-backdrop').remove();
2206
2207 // do not proceed
2208 return;
2209 }
2210
2211 // show loading
2212 var modal_loading = $('<div></div>').addClass('vbo-modal-overlay-content-backdrop');
2213 var modal_loading_body = $('<div></div>').addClass('vbo-modal-overlay-content-backdrop-body');
2214 if (options.loading_body) {
2215 modal_loading_body.append(options.loading_body);
2216 }
2217 modal_loading.append(modal_loading_body);
2218
2219 // append backdrop loading to modal content
2220 modal_content.prepend(modal_loading);
2221 };
2222
2223 // build modal content
2224 var custom_modal = $('<div></div>').addClass('vbo-modal-overlay-block vbo-modal-overlay-' + options.suffix).css('display', 'block');
2225 var modal_dismiss = $('<a></a>').addClass('vbo-modal-overlay-close');
2226 modal_dismiss.on('click', modal_dismiss_fn);
2227 custom_modal.append(modal_dismiss);
2228
2229 var modal_content = $('<div></div>').addClass('vbo-modal-overlay-content vbo-modal-overlay-content-' + options.suffix);
2230 if (options.extra_class && typeof options.extra_class === 'string') {
2231 modal_content.addClass(options.extra_class);
2232 }
2233
2234 var modal_head = $('<div></div>').addClass('vbo-modal-overlay-content-head');
2235 var modal_head_close = $('<span></span>').addClass('vbo-modal-overlay-close-times').html('&times;');
2236 modal_head_close.on('click', modal_dismiss_fn);
2237 modal_head.append(options.title).append(modal_head_close);
2238
2239 var modal_body = $('<div></div>').addClass('vbo-modal-overlay-content-body vbo-modal-overlay-content-body-scroll');
2240 var modal_content_wrapper = $('<div></div>').addClass('vbo-modal-' + options.suffix + '-wrap');
2241 if (typeof options.body === 'string') {
2242 modal_content_wrapper.html(options.body);
2243 } else {
2244 modal_content_wrapper.append(options.body);
2245 }
2246 modal_body.append(modal_content_wrapper);
2247
2248 // modal footer
2249 var modal_footer = null;
2250 if (options.footer_left || options.footer_right) {
2251 modal_footer = $('<div></div>').addClass('vbo-modal-overlay-content-footer');
2252 if (options.footer_left) {
2253 var modal_footer_left = $('<div></div>').addClass('vbo-modal-overlay-content-footer-left').append(options.footer_left);
2254 modal_footer.append(modal_footer_left);
2255 }
2256 if (options.footer_right) {
2257 var modal_footer_right = $('<div></div>').addClass('vbo-modal-overlay-content-footer-right').append(options.footer_right);
2258 modal_footer.append(modal_footer_right);
2259 }
2260
2261 }
2262
2263 // finalize modal contents
2264 modal_content.append(modal_head).append(modal_body);
2265 if (modal_footer) {
2266 modal_content.append(modal_footer);
2267 }
2268 custom_modal.append(modal_content);
2269
2270 // register to the dismiss event
2271 if (options.dismiss_event) {
2272 // listen to the event that will dismiss the modal
2273 document.addEventListener(options.dismiss_event, function vbo_core_handle_dismiss_event(e) {
2274 // make sure the same event won't propagate again, unless a new modal is displayed (multiple displayModal calls)
2275 e.target.removeEventListener(e.type, vbo_core_handle_dismiss_event);
2276
2277 // invoke the modal dismiss function
2278 modal_dismiss_fn(e);
2279 });
2280
2281 // declare the function to detect the Escape key pressed
2282 const vbo_core_dismiss_event_modal_escape = (e) => {
2283 if (!e.key || e.key != 'Escape') {
2284 return;
2285 }
2286
2287 // immediately unregister from this event once fired
2288 document.removeEventListener(e.type, vbo_core_dismiss_event_modal_escape);
2289
2290 // trigger the actual dismiss event
2291 VBOCore.emitEvent(options.dismiss_event);
2292 };
2293
2294 // listen to the Escape keyup event to dismiss the modal
2295 document.addEventListener('keyup', vbo_core_dismiss_event_modal_escape);
2296 }
2297
2298 // register to the toggle-loading event
2299 if (options.loading_event) {
2300 // let a function handle it so that removing the event listener will be doable
2301 document.addEventListener(options.loading_event, modal_handle_loading_event_fn);
2302 }
2303
2304 // append (or prepend) modal to body
2305 if ($('.vbo-modal-overlay-' + options.suffix).length) {
2306 $('.vbo-modal-overlay-' + options.suffix).remove();
2307 }
2308 if (options.body_prepend) {
2309 // prepend to body
2310 if ($('body > .vbo-modal-overlay-block').length) {
2311 // we've got other modals prepended to the body, so go after the last one
2312 $('body > .vbo-modal-overlay-block').last().after(custom_modal);
2313 } else {
2314 // place the modal right as the first child node of body
2315 $('body').prepend(custom_modal);
2316 }
2317 } else {
2318 // append to body
2319 $('body').append(custom_modal);
2320 }
2321
2322 // check if scroll should be locked on the whole page body for a "sticky" modal
2323 if (options.lock_scroll) {
2324 $('body').addClass('vbo-modal-lock-scroll');
2325 }
2326
2327 // return the content wrapper element of the new modal
2328 return modal_content_wrapper;
2329 }
2330
2331 /**
2332 * Debounce technique to group a flurry of events into one single event.
2333 */
2334 static debounceEvent(func, wait, immediate) {
2335 var timeout;
2336 return function() {
2337 var context = this, args = arguments;
2338 var later = function() {
2339 timeout = null;
2340 if (!immediate) func.apply(context, args);
2341 };
2342 var callNow = immediate && !timeout;
2343 clearTimeout(timeout);
2344 timeout = setTimeout(later, wait);
2345 if (callNow) {
2346 func.apply(context, args);
2347 }
2348 }
2349 }
2350
2351 /**
2352 * Throttle guarantees a constant flow of events at a given time interval.
2353 * Runs immediately when the event takes place, but can be delayed.
2354 */
2355 static throttleEvent(method, delay) {
2356 var time = Date.now();
2357 return function() {
2358 if ((time + delay - Date.now()) < 0) {
2359 method();
2360 time = Date.now();
2361 }
2362 }
2363 }
2364
2365 /**
2366 * Tells whether localStorage is supported.
2367 *
2368 * @return boolean
2369 */
2370 static storageSupported() {
2371 return typeof localStorage !== 'undefined';
2372 }
2373
2374 /**
2375 * Gets an item from localStorage.
2376 *
2377 * @param string keyName the storage key identifier.
2378 *
2379 * @return any
2380 */
2381 static storageGetItem(keyName) {
2382 if (!VBOCore.storageSupported()) {
2383 return null;
2384 }
2385
2386 return localStorage.getItem(keyName);
2387 }
2388
2389 /**
2390 * Sets an item to localStorage.
2391 *
2392 * @param string keyName the storage key identifier.
2393 * @param any value the value to store.
2394 *
2395 * @return boolean
2396 */
2397 static storageSetItem(keyName, value) {
2398 if (!VBOCore.storageSupported()) {
2399 return false;
2400 }
2401
2402 try {
2403 if (typeof value === 'object') {
2404 value = JSON.stringify(value);
2405 }
2406
2407 localStorage.setItem(keyName, value);
2408 } catch(e) {
2409 return false;
2410 }
2411
2412 return true;
2413 }
2414
2415 /**
2416 * Removes an item from localStorage.
2417 *
2418 * @param string keyName the storage key identifier.
2419 *
2420 * @return boolean
2421 */
2422 static storageRemoveItem(keyName) {
2423 if (!VBOCore.storageSupported()) {
2424 return false;
2425 }
2426
2427 localStorage.removeItem(keyName);
2428
2429 return true;
2430 }
2431
2432 /**
2433 * Returns the name of the storage identifier for the given scope.
2434 *
2435 * @param string scope the admin menu scope.
2436 *
2437 * @return string the requested admin menu storage identifier.
2438 */
2439 static getStorageScopeName(scope) {
2440 let storage_scope_name = VBOCore.options.admin_menu_actions_nm;
2441
2442 if (typeof scope === 'string' && scope.length) {
2443 if (scope.indexOf('.') !== 0) {
2444 scope = '.' + scope;
2445 }
2446 storage_scope_name += scope;
2447 }
2448
2449 return storage_scope_name;
2450 }
2451
2452 /**
2453 * Returns a list of admin menu action objects or an empty array.
2454 *
2455 * @param string scope the admin menu scope.
2456 *
2457 * @return Array
2458 */
2459 static getAdminMenuActions(scope) {
2460 let menu_actions = VBOCore.storageGetItem(VBOCore.getStorageScopeName(scope));
2461
2462 if (!menu_actions) {
2463 return [];
2464 }
2465
2466 try {
2467 menu_actions = JSON.parse(menu_actions);
2468 if (!Array.isArray(menu_actions) || !menu_actions.length) {
2469 menu_actions = [];
2470 }
2471 } catch(e) {
2472 return [];
2473 }
2474
2475 return menu_actions;
2476 }
2477
2478 /**
2479 * Builds an admin menu action object with a proper href property.
2480 *
2481 * @param object action the action to build.
2482 *
2483 * @return object
2484 */
2485 static buildAdminMenuAction(action) {
2486 if (typeof action !== 'object' || !action || !action.hasOwnProperty('name')) {
2487 throw new Error('Invalid action object');
2488 }
2489
2490 var action_base = action.hasOwnProperty('href') && typeof action['href'] == 'string' ? action['href'] : window.location.href;
2491 var action_url;
2492
2493 if (action_base.indexOf('http') !== 0) {
2494 // relative URL
2495 action_url = new URL(action_base, window.location.href);
2496 } else {
2497 // absolute URL
2498 action_url = new URL(action_base);
2499 }
2500
2501 // build proper href with a relative URL
2502 action['href'] = action_url.pathname + action_url.search;
2503
2504 return action;
2505 }
2506
2507 /**
2508 * Registers an admin menu action object.
2509 *
2510 * @param object action the action to build.
2511 * @param string scope the admin menu scope.
2512 *
2513 * @return boolean
2514 */
2515 static registerAdminMenuAction(action, scope) {
2516 // build menu action object
2517 let menu_action_entry = VBOCore.buildAdminMenuAction(action);
2518
2519 let menu_actions = VBOCore.getAdminMenuActions(scope);
2520
2521 // make sure we are not pushing a duplicate and count pinned actions
2522 let pinned_actions = 0;
2523 let unpinned_index = [];
2524 for (let i = 0; i < menu_actions.length; i++) {
2525 if (menu_actions[i]['href'] == menu_action_entry['href']) {
2526 return false;
2527 }
2528 if (menu_actions[i].hasOwnProperty('pinned') && menu_actions[i]['pinned']) {
2529 pinned_actions++;
2530 } else {
2531 unpinned_index.push(i);
2532 }
2533 }
2534
2535 if (pinned_actions >= VBOCore.options.admin_menu_maxactions) {
2536 // no more space to register a new menu action for this admin menu
2537 return false;
2538 }
2539
2540 // splice or pop before prepending to keep current indexes
2541 let tot_menu_actions = menu_actions.length;
2542 if (++tot_menu_actions > VBOCore.options.admin_menu_maxactions) {
2543 if (unpinned_index.length) {
2544 menu_actions.splice(unpinned_index[unpinned_index.length - 1], 1);
2545 } else {
2546 menu_actions.pop();
2547 }
2548 }
2549
2550 // prepend new admin menu action
2551 menu_actions.unshift(menu_action_entry);
2552
2553 return VBOCore.storageSetItem(VBOCore.getStorageScopeName(scope), menu_actions);
2554 }
2555
2556 /**
2557 * Updates an existing admin menu action object.
2558 *
2559 * @param object action the action to build.
2560 * @param string scope the admin menu scope.
2561 * @param number index optional menu action index.
2562 *
2563 * @return boolean
2564 */
2565 static updateAdminMenuAction(action, scope, index) {
2566 // build menu action object
2567 let menu_action_entry = VBOCore.buildAdminMenuAction(action);
2568
2569 let menu_actions = VBOCore.getAdminMenuActions(scope);
2570
2571 if (!menu_actions.length) {
2572 return false;
2573 }
2574
2575 if (typeof index === 'undefined') {
2576 // find the proper index to update by href
2577 for (let i = 0; i < menu_actions.length; i++) {
2578 if (menu_actions[i]['href'] == menu_action_entry['href']) {
2579 index = i;
2580 break;
2581 }
2582 }
2583 }
2584
2585 if (isNaN(index) || !(index in menu_actions)) {
2586 // menu entry index not found
2587 return false;
2588 }
2589
2590 menu_actions[index] = menu_action_entry;
2591
2592 return VBOCore.storageSetItem(VBOCore.getStorageScopeName(scope), menu_actions);
2593 }
2594 }
2595
2596 /**
2597 * These used to be private static properties (static #options),
2598 * but they are only supported by quite recent browsers (especially Safari).
2599 * It's too premature, so we decided to keep the class properties public
2600 * without declaring them as static inside the class declaration.
2601 *
2602 * @var object
2603 */
2604 VBOCore.options = {
2605 is_vbo: false,
2606 is_vcm: false,
2607 cms: 'wordpress',
2608 widget_ajax_uri: null,
2609 assets_ajax_uri: null,
2610 multitask_ajax_uri: null,
2611 watchdata_ajax_uri: null,
2612 current_page: null,
2613 current_page_uri: null,
2614 root_uri: '/',
2615 client: 'admin',
2616 panel_opts: {},
2617 admin_widgets: [],
2618 notif_audio_url: '',
2619 active_listeners: {},
2620 tn_texts: {
2621 notifs_enabled: 'Browser notifications are enabled!',
2622 notifs_disabled: 'Browser notifications are disabled',
2623 notifs_disabled_help: "Could not enable browser notifications.\nThis feature is available only in secure contexts (HTTPS).",
2624 admin_widget: 'Admin widget',
2625 congrats: 'Congratulations!',
2626 },
2627 default_loading_body: '....',
2628 multitask_save_event: 'vbo-admin-multitask-save',
2629 multitask_open_event: 'vbo-admin-multitask-open',
2630 multitask_close_event: 'vbo-admin-multitask-close',
2631 multitask_shortcut_ev: 'vbo_multitask_shortcut',
2632 multitask_searchfs_ev: 'vbo_multitask_search_focus',
2633 widget_modal_rendered: 'vbo-admin-widget-modal-rendered',
2634 widget_modal_dismissed: 'vbo-widget-modal-dismissed',
2635 admin_menu_maxactions: 3,
2636 admin_menu_actions_nm: 'vikbooking.admin_menu.actions',
2637 service_worker_path: '',
2638 service_worker_scope: './',
2639 push: {
2640 application_key: '',
2641 ajax_url: '',
2642 },
2643 push_options: {
2644 storage_endp_id: 'vbo_push_subscr_endp',
2645 allowed_notif_types: [
2646 'Book',
2647 'Modify',
2648 'Cancel',
2649 'Request',
2650 'CancelRequest',
2651 'Inquiry',
2652 'CancelInquiry',
2653 'Chat',
2654 'Info',
2655 ],
2656 },
2657 };
2658
2659 /**
2660 * @var bool
2661 */
2662 VBOCore.side_panel_on = false;
2663
2664 /**
2665 * @var array
2666 */
2667 VBOCore.notifications = [];
2668
2669 /**
2670 * @var object
2671 */
2672 VBOCore.widgets_watch_data = null;
2673
2674 /**
2675 * @var number
2676 */
2677 VBOCore.watch_data_interval = null;
2678
2679 /**
2680 * @var object
2681 * @since 1.6.3
2682 */
2683 VBOCore.broadcast_watch_data = null;
2684
2685 /**
2686 * @var object
2687 * @since 1.6.5
2688 */
2689 VBOCore.broadcast_push_data = null;
2690
2691 /**
2692 * @var array<object>
2693 * @since 1.6.5
2694 */
2695 VBOCore.widgets_pushed_data = [];
2696
2697 })(jQuery, window);