PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.7.1
VikBooking Hotel Booking Engine & PMS v1.7.1
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.7.1, at admin/resources/vbocore.js

3,207 lines 95.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * VikBooking Core v1.7.0
3 * Copyright (C) 2024 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 // let the window reload the badge counter in case it must be updated
467 VBOCore.emitEvent('vbo-badge-count-reload');
468 }
469
470 /**
471 * Ensures VBO is ready to support VCM.
472 *
473 * @param any env optional data to evaluate.
474 *
475 * @return bool
476 */
477 static vcmMultitasking(env) {
478 // tell VCM that VBOCore supports it
479 return true;
480 }
481
482 /**
483 * Initializes the multitasking panel for the admin widgets.
484 *
485 * @param object params the panel object params.
486 *
487 * @return bool
488 */
489 static prepareMultitasking(params) {
490 var panel_opts = {
491 selector: "",
492 sclass_l_small: "vbo-sidepanel-right",
493 sclass_l_large: "vbo-sidepanel-large",
494 btn_trigger: "",
495 search_selector: "#vbo-sidepanel-search-input",
496 search_nores: ".vbo-sidepanel-add-widgets-nores",
497 close_selector: ".vbo-sidepanel-dismiss-btn",
498 t_layout_small: ".vbo-sidepanel-layout-small",
499 t_layout_large: ".vbo-sidepanel-layout-large",
500 wclass_base_sel: ".vbo-admin-widgets-widget-output",
501 wclass_l_small: "vbo-admin-widgets-container-small",
502 wclass_l_large: "vbo-admin-widgets-container-large",
503 addws_selector: ".vbo-sidepanel-add-widgets",
504 addw_selector: ".vbo-sidepanel-add-widget",
505 addw_modal_cls: "vbo-widget-render-modal",
506 addw_def_cls: "vbo-widget-render-regular",
507 addwfs_selector: ".vbo-sidepanel-add-widget-focussed",
508 wtags_selector: ".vbo-sidepanel-widget-tags",
509 wname_selector: ".vbo-sidepanel-widget-name",
510 addw_data_attr: "data-vbowidgetid",
511 actws_selector: ".vbo-sidepanel-active-widgets",
512 editw_selector: ".vbo-sidepanel-edit-widgets-trig",
513 shortc_selector: ".vbo-sidepanel-shortcut",
514 rmwidget_class: "vbo-admin-widgets-widget-remove",
515 rmwidget_icn: "",
516 dtcwidget_class: "vbo-admin-widgets-widget-detach",
517 dtctarget_class: "vbo-admin-widget-head",
518 dtcwidget_icn: "",
519 notif_selector: ".vbo-sidepanel-notifications-btn",
520 notif_on_class: "vbo-sidepanel-notifications-on",
521 notif_off_class: "vbo-sidepanel-notifications-off",
522 open_class: "vbo-sidepanel-open",
523 close_class: "vbo-sidepanel-close",
524 cur_widget_cls: "vbo-admin-widgets-container-small",
525 sortable: true,
526 sort_save_ev: "vbo-admin-widgets-updateposmp",
527 sorting: null,
528 };
529
530 if (typeof params === 'object') {
531 panel_opts = Object.assign(panel_opts, params);
532 }
533
534 if (!panel_opts.btn_trigger || !panel_opts.selector) {
535 console.error('Got no trigger or selector');
536 return false;
537 }
538
539 // push panel options
540 VBOCore.setOptions({
541 panel_opts: panel_opts,
542 });
543
544 if (VBOCore.options.is_vbo) {
545 // setup browser notifications
546 VBOCore.setupNotifications();
547 }
548
549 // count active widgets on current page
550 var tot_active_widgets = VBOCore.options.admin_widgets.length;
551 if (tot_active_widgets > 0) {
552 // hide add-widgets container
553 $(panel_opts.addws_selector).hide();
554
555 // register listener for input search blur
556 VBOCore.registerSearchWidgetsBlur();
557 }
558
559 // register click event on trigger button
560 $(VBOCore.options.panel_opts.btn_trigger).on('click', function() {
561 var side_panel = $(VBOCore.options.panel_opts.selector);
562 if (side_panel.hasClass(VBOCore.options.panel_opts.open_class)) {
563 // hide panel
564 VBOCore.side_panel_on = false;
565 VBOCore.emitMultitaskEvent(VBOCore.multitask_close_event);
566 side_panel.addClass(VBOCore.options.panel_opts.close_class).removeClass(VBOCore.options.panel_opts.open_class);
567 // always hide add-widgets container
568 $(VBOCore.options.panel_opts.addws_selector).hide();
569 // check if we are currently editing
570 var is_editing = ($('.' + VBOCore.options.panel_opts.editmode_class).length > 0);
571 if (is_editing) {
572 // deactivate editing mode
573 VBOCore.toggleWidgetsPanelEditing(null);
574 }
575 } else {
576 // show panel
577 VBOCore.side_panel_on = true;
578 VBOCore.emitMultitaskEvent(VBOCore.multitask_open_event);
579 side_panel.addClass(VBOCore.options.panel_opts.open_class).removeClass(VBOCore.options.panel_opts.close_class);
580 if (!VBOCore.options.admin_widgets.length) {
581 // set focus on search widgets input with delay for the opening animation
582 setTimeout(function() {
583 $(VBOCore.options.panel_opts.search_selector).focus();
584 }, 300);
585 }
586 }
587 });
588
589 // register close/dismiss button
590 $(VBOCore.options.panel_opts.close_selector).on('click', function() {
591 $(VBOCore.options.panel_opts.btn_trigger).trigger('click');
592 });
593
594 if (VBOCore.options.is_vbo) {
595 // register toggle layout buttons
596 $(VBOCore.options.panel_opts.t_layout_large).on('click', function() {
597 // large layout
598 $(VBOCore.options.panel_opts.selector).addClass(VBOCore.options.panel_opts.sclass_l_large).removeClass(VBOCore.options.panel_opts.sclass_l_small);
599 $(VBOCore.options.panel_opts.wclass_base_sel).addClass(VBOCore.options.panel_opts.wclass_l_large).removeClass(VBOCore.options.panel_opts.wclass_l_small);
600 VBOCore.options.panel_opts.cur_widget_cls = VBOCore.options.panel_opts.sclass_l_large;
601 });
602 $(VBOCore.options.panel_opts.t_layout_small).on('click', function() {
603 // small layout
604 $(VBOCore.options.panel_opts.selector).addClass(VBOCore.options.panel_opts.sclass_l_small).removeClass(VBOCore.options.panel_opts.sclass_l_large);
605 $(VBOCore.options.panel_opts.wclass_base_sel).addClass(VBOCore.options.panel_opts.wclass_l_small).removeClass(VBOCore.options.panel_opts.wclass_l_large);
606 VBOCore.options.panel_opts.cur_widget_cls = VBOCore.options.panel_opts.sclass_l_small;
607 });
608 }
609
610 // register listener for esc key pressed
611 $(document).keyup(function(e) {
612 if (!VBOCore.side_panel_on) {
613 return;
614 }
615 if ((e.key && e.key === "Escape") || (e.keyCode && e.keyCode == 27)) {
616 $(VBOCore.options.panel_opts.btn_trigger).trigger('click');
617 }
618 });
619
620 // register listener for input search focus
621 $(VBOCore.options.panel_opts.search_selector).on('focus', function() {
622 // always show add-widgets container
623 var widget_focus_class = VBOCore.options.panel_opts.addwfs_selector.replace('.', '');
624 $(VBOCore.options.panel_opts.addw_selector).removeClass(widget_focus_class);
625 $(VBOCore.options.panel_opts.addws_selector).show();
626 });
627
628 // register listener on input search widget
629 $(VBOCore.options.panel_opts.search_selector).keyup(function(e) {
630 // get the keyword to look for
631 var keyword = $(this).val();
632 // counting matching widgets
633 var matching = 0;
634 var first_matched = null;
635 var widget_focus_class = VBOCore.options.panel_opts.addwfs_selector.replace('.', '');
636
637 // adjust widgets to be displayed
638 if (!keyword.length) {
639 // show all widgets for selection
640 $(VBOCore.options.panel_opts.addw_selector).show();
641 // hide "no results"
642 $(VBOCore.options.panel_opts.search_nores).hide();
643 // all widgets are matching
644 matching = $(VBOCore.options.panel_opts.addw_selector).length;
645 } else {
646 // make the keyword lowercase
647 keyword = (keyword + '').toLowerCase();
648 // parse all widget's description tags
649 $(VBOCore.options.panel_opts.addw_selector).each(function() {
650 var elem = $(this);
651 var descr = elem.find(VBOCore.options.panel_opts.wtags_selector).text();
652 if (VBOCore.matchString(keyword, descr)) {
653 elem.show();
654 matching++;
655 if (!first_matched) {
656 // store the first widget that matched
657 first_matched = elem.attr(VBOCore.options.panel_opts.addw_data_attr);
658 }
659 } else {
660 elem.hide();
661 }
662 });
663 // check how many widget matched
664 if (matching > 0) {
665 // hide "no results"
666 $(VBOCore.options.panel_opts.search_nores).hide();
667 } else {
668 // show "no results"
669 $(VBOCore.options.panel_opts.search_nores).show();
670 }
671 }
672
673 // check for shortcuts
674 if (!e.key) {
675 return;
676 }
677
678 // handle Enter key press to add a widget
679 if (e.key === "Enter") {
680 // on Enter key pressed, open/add the first matching widget or the focussed one
681 var load_wid_id = null;
682 var focussed_wid = $(VBOCore.options.panel_opts.addwfs_selector + ':visible').first();
683
684 if (focussed_wid.length) {
685 load_wid_id = focussed_wid.attr(VBOCore.options.panel_opts.addw_data_attr);
686 } else if (first_matched) {
687 load_wid_id = first_matched;
688 }
689
690 if (!load_wid_id) {
691 // no widget to render found
692 return;
693 }
694
695 if (e.shiftKey === true && !VBOCore.options.is_vcm) {
696 // widget multitask panel rendering
697 VBOCore.addWidgetToPanel(load_wid_id);
698 $(VBOCore.options.panel_opts.search_selector).trigger('blur');
699
700 return;
701 }
702
703 // widget modal rendering is the default method
704 if (VBOCore.options.is_vcm) {
705 // register loading effect with automatic cancellation
706 let orig_name = (focussed_wid.length ? focussed_wid : first_matched).find(VBOCore.options.panel_opts.wname_selector).text();
707 (focussed_wid.length ? focussed_wid : first_matched).find(VBOCore.options.panel_opts.wname_selector).html(orig_name + ' ' + VBOCore.options.default_loading_body);
708 setTimeout(() => {
709 (focussed_wid.length ? focussed_wid : first_matched).find(VBOCore.options.panel_opts.wname_selector).text(orig_name);
710 }, 1000);
711
712 // assets will be loaded within VCM before rendering the modal widget
713 VBOCore.handleDisplayWidgetNotification({widget_id: load_wid_id});
714 } else {
715 VBOCore.renderModalWidget(load_wid_id);
716 }
717
718 return;
719 }
720
721 // handle arrow keys selection
722 if (matching > 0 && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
723 // on arrow key pressed, select the next or prev widget
724 var addws_element = $(VBOCore.options.panel_opts.addws_selector);
725 var addws_cont_pos = addws_element.offset().top;
726 var addws_otheight = addws_element.outerHeight();
727 var addws_scrolltp = addws_element.scrollTop();
728
729 if (e.key === 'ArrowDown') {
730 var default_widg = $(VBOCore.options.panel_opts.addw_selector + ':visible').first();
731 } else {
732 var default_widg = $(VBOCore.options.panel_opts.addw_selector + ':visible').last();
733 }
734 var focussed_wid = $(VBOCore.options.panel_opts.addwfs_selector + ':visible').first();
735 var addw_height = default_widg.outerHeight();
736 var focussed_pos = default_widg.offset().top + addw_height;
737
738 if (focussed_wid.length) {
739 focussed_wid.removeClass(widget_focus_class);
740 if (e.key === 'ArrowDown') {
741 var goto_wid = focussed_wid.nextAll(VBOCore.options.panel_opts.addw_selector + ':visible').first();
742 } else {
743 var goto_wid = focussed_wid.prevAll(VBOCore.options.panel_opts.addw_selector + ':visible').first();
744 }
745 if (goto_wid.length) {
746 goto_wid.addClass(widget_focus_class);
747 focussed_pos = goto_wid.offset().top + addw_height;
748 } else {
749 default_widg.addClass(widget_focus_class);
750 }
751 } else {
752 default_widg.addClass(widget_focus_class);
753 }
754
755 if (focussed_pos > (addws_cont_pos + addws_otheight)) {
756 addws_element.scrollTop(focussed_pos - addws_cont_pos - addw_height + addws_scrolltp);
757 } else if (focussed_pos < 0) {
758 addws_element.scrollTop(0);
759 }
760 }
761 });
762
763 // register listener for adding widgets
764 $(VBOCore.options.panel_opts.addw_selector).on('click', function(e) {
765 var widget_id = $(this).attr(VBOCore.options.panel_opts.addw_data_attr);
766 if (!widget_id || !widget_id.length) {
767 return false;
768 }
769
770 if (VBOCore.options.panel_opts.sorting) {
771 // prevent dropped widgets after sorting to be added to the panel
772 return false;
773 }
774
775 // determine widget rendering method
776 if (e && e.target) {
777 let cktarget = $(e.target);
778 if (e.shiftKey === true || VBOCore.options.is_vcm || (cktarget.hasClass(VBOCore.options.panel_opts.addw_modal_cls) || cktarget.parent().hasClass(VBOCore.options.panel_opts.addw_modal_cls))) {
779 // widget modal rendering
780 if (VBOCore.options.is_vcm) {
781 // register loading effect with automatic cancellation
782 let orig_name = $(this).find(VBOCore.options.panel_opts.wname_selector).text();
783 $(this).find(VBOCore.options.panel_opts.wname_selector).html(orig_name + ' ' + VBOCore.options.default_loading_body);
784 setTimeout(() => {
785 $(this).find(VBOCore.options.panel_opts.wname_selector).text(orig_name);
786 }, 1000);
787
788 // assets will be loaded within VCM before rendering the modal widget
789 VBOCore.handleDisplayWidgetNotification({widget_id: widget_id});
790 } else {
791 VBOCore.renderModalWidget(widget_id);
792 }
793
794 return;
795 }
796 }
797
798 // widget multitask panel rendering
799 VBOCore.addWidgetToPanel(widget_id);
800 });
801
802 if (VBOCore.options.is_vbo) {
803 // register listener for updating multitask sidepanel with debounce
804 document.addEventListener(VBOCore.options.multitask_save_event, VBOCore.debounceEvent(VBOCore.saveMultitasking, 1000));
805 }
806
807 // subscribe to event for multitask shortcut
808 document.addEventListener(VBOCore.multitask_shortcut_event, function() {
809 // toggle multitask panel
810 $(VBOCore.options.panel_opts.btn_trigger).trigger('click');
811 });
812
813 // subscribe to event for multitask search focus shortcut
814 document.addEventListener(VBOCore.multitask_searchfs_event, function() {
815 // focus search multitask widgets
816 $(VBOCore.options.panel_opts.search_selector).trigger('focus');
817 });
818
819 if (VBOCore.options.is_vbo) {
820 // register click event on edit widgets button
821 $(VBOCore.options.panel_opts.editw_selector).on('click', function() {
822 VBOCore.toggleWidgetsPanelEditing(null);
823 });
824
825 // register detach widget buttons
826 $('.' + VBOCore.options.panel_opts.dtcwidget_class).each(function() {
827 let widget_wrapper = $(this).parent(VBOCore.options.panel_opts.wclass_base_sel);
828 let detach_widget_id = widget_wrapper.attr(VBOCore.options.panel_opts.addw_data_attr);
829 let detach_to_target = widget_wrapper.find('.' + VBOCore.options.panel_opts.dtctarget_class);
830 if (!detach_to_target.length) {
831 detach_to_target = widget_wrapper;
832 }
833 // move detach wrapper onto the target (widget head)
834 $(this).prependTo(detach_to_target);
835
836 if (!detach_widget_id) {
837 return;
838 }
839
840 // register detach action
841 $(this).on('click', function() {
842 // detach widget, meaning do a modal rendering
843 VBOCore.renderModalWidget(detach_widget_id);
844 });
845 });
846 }
847
848 if (VBOCore.options.panel_opts.sortable && typeof $.fn.sortable !== 'undefined') {
849 // make the admin widgets sortable
850 let sortable_env = VBOCore.options.is_vcm ? 'vcm-sidepanel-add-widgets' : 'vbo-sidepanel-add-widgets';
851 let handle_env = VBOCore.options.is_vcm ? 'vcm-sidepanel-widget-info-det' : 'vbo-sidepanel-widget-info-det';
852 let item_env = VBOCore.options.is_vcm ? 'vcm-sidepanel-add-widget' : 'vbo-sidepanel-add-widget';
853
854 // default sorting flag
855 VBOCore.options.panel_opts.sorting = null;
856
857 // register listener for updating the widget sorting values on the multitask sidepanel with debounce
858 document.addEventListener(VBOCore.options.panel_opts.sort_save_ev, VBOCore.debounceEvent(VBOCore.saveMultitaskingSorting, 500));
859
860 // apply sorting capabilities
861 $('.' + sortable_env).sortable({
862 axix: 'x',
863 cursor: 'move',
864 handle: '.' + handle_env,
865 items: '.' + item_env,
866 containment: 'parent',
867 revert: false,
868 start: function(event, ui) {
869 // update flag
870 VBOCore.options.panel_opts.sorting = $(ui.item).attr('data-vbowidgetid');
871 // set is-sorting class
872 $(ui.item).addClass('is-sorting');
873 },
874 stop: function(event, ui) {
875 // make sure no elements are being sorted
876 $('.' + item_env).removeClass('is-sorting');
877 // register delayed update flag to allow understanding a sorting was just completed
878 setTimeout(() => {
879 // restore default sorting flag
880 VBOCore.options.panel_opts.sorting = null;
881 }, 500);
882 },
883 update: function(event, ui) {
884 // build the current widgets position list
885 let pos_list = {};
886 document.querySelectorAll('.' + item_env).forEach((elem, index) => {
887 let curr_wid = elem.getAttribute('data-vbowidgetid');
888 pos_list[curr_wid] = index;
889 });
890 // trigger the event to update the widget positions
891 VBOCore.emitEvent(VBOCore.options.panel_opts.sort_save_ev, {
892 pos_list: pos_list,
893 });
894 }
895 });
896 }
897 }
898
899 /**
900 * Registers the callback for saving the new widget sorting value in the Multitask panel.
901 */
902 static saveMultitaskingSorting(e) {
903 if (!e || !e.detail || !e.detail.pos_list) {
904 return;
905 }
906
907 // update multitask widget position
908 VBOCore.doAjax(
909 VBOCore.options.multitask_ajax_uri,
910 {
911 call: 'setMultitaskingWidgetPos',
912 call_args: [
913 e.detail.pos_list,
914 ],
915 },
916 (response) => {
917 // do nothing on success
918 },
919 (error) => {
920 // silently log the error
921 console.error(error.responseText);
922 }
923 );
924 }
925
926 /**
927 * Registers the blur event for the search widgets input.
928 */
929 static registerSearchWidgetsBlur() {
930 if (VBOCore.options.active_listeners.hasOwnProperty('registerSearchWidgetsBlur')) {
931 // listener is already registered
932 return true;
933 }
934
935 $(VBOCore.options.panel_opts.search_selector).on('blur', function(e) {
936 if (e && e.relatedTarget) {
937 if (e.relatedTarget.classList.contains(VBOCore.options.panel_opts.addw_selector.replace('.', ''))) {
938 // add new widget was clicked, abort hiding process or click event won't fire on target element
939 return;
940 }
941 }
942 var keyword = $(this).val();
943 if (!keyword.length) {
944 // hide add-widgets container
945 $(VBOCore.options.panel_opts.addws_selector).hide();
946 }
947 });
948
949 // register flag for listener active
950 VBOCore.options.active_listeners['registerSearchWidgetsBlur'] = 1;
951 }
952
953 /**
954 * Removes the blur event handler for the search widgets input.
955 */
956 static unregisterSearchWidgetsBlur() {
957 if (!VBOCore.options.active_listeners.hasOwnProperty('registerSearchWidgetsBlur')) {
958 // nothing to unregister
959 return true;
960 }
961
962 $(VBOCore.options.panel_opts.search_selector).off('blur');
963
964 // delete flag for listener active
965 delete VBOCore.options.active_listeners['registerSearchWidgetsBlur'];
966 }
967
968 /**
969 * Adds a widget identifier to the multitask panel.
970 *
971 * @param string widget_id the widget identifier string to add.
972 */
973 static addWidgetToPanel(widget_id) {
974 if (!VBOCore.options.widget_ajax_uri || !VBOCore.options.panel_opts || !Object.keys(VBOCore.options.panel_opts).length) {
975 throw new Error('Multitask panel options are missing');
976 }
977 // prepend container to panel
978 var widget_classes = [VBOCore.options.panel_opts.wclass_base_sel.replace('.', ''), VBOCore.options.panel_opts.cur_widget_cls];
979 var widget_div = '<div class="' + widget_classes.join(' ') + '" ' + VBOCore.options.panel_opts.addw_data_attr + '="' + widget_id + '" style="display: none;"></div>';
980 var widget_elem = $(widget_div);
981 $(VBOCore.options.panel_opts.actws_selector).prepend(widget_elem);
982
983 // always hide add-widgets container
984 $(VBOCore.options.panel_opts.addws_selector).hide();
985
986 // trigger debounced map saving event
987 VBOCore.emitMultitaskEvent();
988
989 // register listener for input search blur
990 VBOCore.registerSearchWidgetsBlur();
991
992 // render widget
993 var call_method = 'render';
994 VBOCore.doAjax(
995 VBOCore.options.widget_ajax_uri,
996 {
997 widget_id: widget_id,
998 call: call_method,
999 vbo_page: VBOCore.options.current_page,
1000 vbo_uri: VBOCore.options.current_page_uri,
1001 multitask: 1,
1002 },
1003 (response) => {
1004 // display widgets editing button
1005 VBOCore.toggleWidgetsPanelEditing(true);
1006 // parse response
1007 try {
1008 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1009 if (obj_res.hasOwnProperty(call_method)) {
1010 // populate widget HTML content and display it
1011 widget_elem.html(obj_res[call_method]).fadeIn();
1012
1013 // always scroll active widgets list to top
1014 $(VBOCore.options.panel_opts.actws_selector).scrollTop(0);
1015
1016 // register detach widget button
1017 setTimeout(() => {
1018 let detach_elem = $('<div></div>').addClass(VBOCore.options.panel_opts.dtcwidget_class).html(VBOCore.options.panel_opts.dtcwidget_icn);
1019 let detach_to_target = widget_elem.find('.' + VBOCore.options.panel_opts.dtctarget_class);
1020 if (!detach_to_target.length) {
1021 detach_to_target = widget_elem;
1022 }
1023 // move detach wrapper onto the target (widget head)
1024 detach_elem.prependTo(detach_to_target);
1025 // register detach action
1026 detach_elem.on('click', function() {
1027 // detach widget, meaning do a modal rendering
1028 VBOCore.renderModalWidget(widget_id);
1029 });
1030 }, 500);
1031 } else {
1032 console.error('Unexpected JSON response', obj_res);
1033 }
1034 } catch(err) {
1035 console.error('could not parse JSON response', err, response);
1036 }
1037 },
1038 (error) => {
1039 console.error(error.responseText);
1040 }
1041 );
1042 }
1043
1044 /**
1045 * Toggles the edit mode of the multitask widgets panel.
1046 *
1047 * @param bool added true if a widget was just added, false if it was just removed.
1048 */
1049 static toggleWidgetsPanelEditing(added) {
1050 // check if we are currently editing
1051 var is_editing = ($('.' + VBOCore.options.panel_opts.editmode_class).length > 0);
1052
1053 // the triggerer button
1054 var triggerer = $(VBOCore.options.panel_opts.editw_selector);
1055
1056 // check added action status
1057 if (added === true) {
1058 // show button for edit mode
1059 triggerer.show();
1060 // hide shortcut element
1061 $(VBOCore.options.panel_opts.shortc_selector).hide();
1062 return;
1063 }
1064
1065 // grab all widgets
1066 var editing_widgets = $(VBOCore.options.panel_opts.wclass_base_sel);
1067
1068 if (added === false) {
1069 if (!editing_widgets.length) {
1070 // hide button for edit mode after removing the last widget
1071 triggerer.removeClass(VBOCore.options.panel_opts.editw_selector.substr(1) + '-active').hide();
1072 // show shortcut element
1073 $(VBOCore.options.panel_opts.shortc_selector).show();
1074 }
1075 return;
1076 }
1077
1078 if (is_editing) {
1079 // deactivate editing mode
1080 editing_widgets.removeClass(VBOCore.options.panel_opts.editmode_class);
1081 $('.' + VBOCore.options.panel_opts.rmwidget_class).remove();
1082 // toggle triggerer button active class
1083 triggerer.removeClass(VBOCore.options.panel_opts.editw_selector.substr(1) + '-active');
1084 } else {
1085 // activate editing mode by looping through all widgets
1086 editing_widgets.each(function() {
1087 // build remove-widget element
1088 var rm_widget = $('<div></div>').addClass(VBOCore.options.panel_opts.rmwidget_class).on('click', function() {
1089 VBOCore.removeWidgetFromPanel(this);
1090 }).html(VBOCore.options.panel_opts.rmwidget_icn);
1091 // add editing class and prepend removing element
1092 $(this).addClass(VBOCore.options.panel_opts.editmode_class).prepend(rm_widget);
1093 });
1094 // toggle triggerer button active class
1095 triggerer.addClass(VBOCore.options.panel_opts.editw_selector.substr(1) + '-active');
1096 }
1097 }
1098
1099 /**
1100 * Handles the removal of a widget from the multitask panel.
1101 *
1102 * @param object element
1103 */
1104 static removeWidgetFromPanel(element) {
1105 if (!element) {
1106 console.error('Invalid widget element to remove', element);
1107 return false;
1108 }
1109 var widget_cont = $(element).parent(VBOCore.options.panel_opts.wclass_base_sel);
1110 if (!widget_cont || !widget_cont.length) {
1111 console.error('Could not find widget container to remove', element);
1112 return false;
1113 }
1114 var widget_id = widget_cont.attr(VBOCore.options.panel_opts.addw_data_attr);
1115 if (!widget_id || !widget_id.length) {
1116 console.error('Empty widget id to remove', element);
1117 return false;
1118 }
1119 // find the index of the widget to remove in the panel
1120 var widget_index = $(VBOCore.options.panel_opts.wclass_base_sel).index(widget_cont);
1121 if (widget_index < 0) {
1122 console.error('Empty widget index to remove', widget_cont);
1123 return false;
1124 }
1125 // make sure the index in the array matches the id
1126 if (!VBOCore.options.admin_widgets.hasOwnProperty(widget_index) || VBOCore.options.admin_widgets[widget_index]['id'] != widget_id) {
1127 console.error('Unmatching widget index or id', VBOCore.options.admin_widgets, widget_index, widget_id);
1128 return false;
1129 }
1130 // remove this widget from the array
1131 VBOCore.options.admin_widgets.splice(widget_index, 1);
1132
1133 // remove element from document
1134 widget_cont.remove();
1135
1136 // check widgets editing button status
1137 VBOCore.toggleWidgetsPanelEditing(false);
1138
1139 // trigger debounced map saving event
1140 VBOCore.emitMultitaskEvent();
1141
1142 if (!VBOCore.options.admin_widgets.length) {
1143 // unregister listener for input search blur
1144 VBOCore.unregisterSearchWidgetsBlur();
1145 }
1146 }
1147
1148 /**
1149 * Emits an event related to the multitask features or a custom event, with optional data.
1150 */
1151 static emitMultitaskEvent(ev_name, ev_data) {
1152 var def_ev_name = VBOCore.options.multitask_save_event;
1153 if (typeof ev_name === 'string') {
1154 def_ev_name = ev_name;
1155 }
1156
1157 if (typeof ev_data !== 'undefined' && ev_data) {
1158 // trigger the custom event
1159 document.dispatchEvent(new CustomEvent(def_ev_name, {bubbles: true, detail: ev_data}));
1160 return;
1161 }
1162
1163 // trigger the event
1164 document.dispatchEvent(new Event(def_ev_name));
1165 }
1166
1167 /**
1168 * Proxy for dispatching an event to the document with optional data.
1169 */
1170 static emitEvent(ev_name, ev_data) {
1171 if (typeof ev_name !== 'string' || !ev_name.length) {
1172 return;
1173 }
1174
1175 return VBOCore.emitMultitaskEvent(ev_name, ev_data);
1176 }
1177
1178 /**
1179 * Attempts to save the multitask widgets for this page.
1180 */
1181 static saveMultitasking() {
1182 // gather the list of active widgets
1183 var active_widgets = [];
1184 var cur_admin_widgets = [];
1185 $(VBOCore.options.panel_opts.actws_selector).find(VBOCore.options.panel_opts.wclass_base_sel).each(function() {
1186 var widget_id = $(this).attr(VBOCore.options.panel_opts.addw_data_attr);
1187 if (widget_id && widget_id.length) {
1188 // push id in list
1189 active_widgets.push(widget_id);
1190 // push object with dummy name for global widgets
1191 cur_admin_widgets.push({
1192 id: widget_id,
1193 name: widget_id,
1194 });
1195 }
1196 });
1197
1198 // update multitask widgets map for this page
1199 VBOCore.doAjax(
1200 VBOCore.options.multitask_ajax_uri,
1201 {
1202 call: 'updateMultitaskingMap',
1203 call_args: [
1204 VBOCore.options.current_page,
1205 active_widgets,
1206 0
1207 ],
1208 },
1209 (response) => {
1210 try {
1211 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1212 if (obj_res.hasOwnProperty('result') && obj_res['result']) {
1213 // set current widgets
1214 VBOCore.setOptions({
1215 admin_widgets: cur_admin_widgets
1216 });
1217 } else {
1218 console.error('Unexpected or invalid JSON response', response);
1219 }
1220 } catch(err) {
1221 console.error('could not parse JSON response', err, response);
1222 }
1223 },
1224 (error) => {
1225 console.error(error.responseText);
1226 }
1227 );
1228 }
1229
1230 /**
1231 * Sets up the browser notifications within the multitask panel, if supported.
1232 */
1233 static setupNotifications() {
1234 if (!('Notification' in window)) {
1235 // browser does not support notifications
1236 $(VBOCore.options.panel_opts.notif_selector).hide();
1237 return false;
1238 }
1239
1240 if (Notification.permission && Notification.permission === 'granted') {
1241 // permissions were granted already
1242 $(VBOCore.options.panel_opts.notif_selector)
1243 .addClass(VBOCore.options.panel_opts.notif_on_class)
1244 .attr('title', VBOCore.options.tn_texts.notifs_enabled)
1245 .on('click', function() {
1246 // show congratulations notification
1247 VBOCore.dispatchCongratulations();
1248
1249 // attempt to update the service worker installation
1250 VBOCore.installServiceWorker(true).then(() => {
1251 // all good
1252 }).catch((error) => {
1253 console.error(error);
1254 });
1255 });
1256 return true;
1257 }
1258
1259 // notifications supported, but perms not granted
1260 $(VBOCore.options.panel_opts.notif_selector)
1261 .addClass(VBOCore.options.panel_opts.notif_off_class)
1262 .attr('title', VBOCore.options.tn_texts.notifs_disabled);
1263
1264 // register click-event listener on button to enable notifications
1265 $(VBOCore.options.panel_opts.notif_selector).on('click', function() {
1266 VBOCore.requestNotifPerms();
1267 });
1268
1269 // subscribe to the multitask-panel-open event to show the status of the notifications
1270 document.addEventListener(VBOCore.multitask_open_event, function() {
1271 if (VBOCore.notificationsEnabled() === false) {
1272 // add "shaking" class to notifications button
1273 $(VBOCore.options.panel_opts.notif_selector).addClass('shaking');
1274 }
1275 });
1276
1277 // subscribe to the multitask-panel-close event to update the status of the notifications
1278 document.addEventListener(VBOCore.multitask_close_event, function() {
1279 // always remove "shaking" class from notifications button
1280 $(VBOCore.options.panel_opts.notif_selector).removeClass('shaking');
1281 });
1282 }
1283
1284 /**
1285 * Sets up the browser notifications within the given selector, if supported.
1286 *
1287 * @param string selector the element query selector.
1288 */
1289 static suggestNotifications(selector) {
1290 if (!selector) {
1291 return false;
1292 }
1293
1294 if (!('Notification' in window)) {
1295 // browser does not support notifications
1296 $(selector).hide();
1297 return false;
1298 }
1299
1300 if (Notification.permission && Notification.permission === 'granted') {
1301 // permissions were granted already
1302 $(selector)
1303 .addClass(VBOCore.options.panel_opts.notif_on_class)
1304 .attr('title', VBOCore.options.tn_texts.notifs_enabled)
1305 .on('click', function() {
1306 VBOCore.dispatchCongratulations();
1307 });
1308 return true;
1309 }
1310
1311 // notifications supported, but perms not granted
1312 $(selector)
1313 .addClass(VBOCore.options.panel_opts.notif_off_class)
1314 .attr('title', VBOCore.options.tn_texts.notifs_disabled);
1315
1316 // register click-event listener on button to enable notifications
1317 $(selector).on('click', function() {
1318 VBOCore.requestNotifPerms(selector);
1319 });
1320
1321 // add "shaking" class to make the selector more appealing
1322 $(selector).addClass('shaking');
1323
1324 // remove the "shaking" class after some time
1325 setTimeout(() => {
1326 $(selector).removeClass('shaking');
1327 }, 2000);
1328 }
1329
1330 /**
1331 * Tells whether the notifications are enabled, disabled, not supported.
1332 */
1333 static notificationsEnabled() {
1334 if (!('Notification' in window)) {
1335 // not supported
1336 return null;
1337 }
1338
1339 if (Notification.permission && Notification.permission === 'granted') {
1340 // enabled
1341 return true;
1342 }
1343
1344 // disabled
1345 return false;
1346 }
1347
1348 /**
1349 * Attempts to request the notifications permissions to the browser.
1350 * For security reasons, this should run upon a user gesture (click).
1351 *
1352 * @param string selector optional element query selector.
1353 */
1354 static requestNotifPerms(selector) {
1355 if (!('Notification' in window)) {
1356 // browser does not support notifications
1357 return false;
1358 }
1359
1360 // run permissions request in a try-catch statement to support all browsers
1361 try {
1362 // handle promise-based version to request permissions
1363 Notification.requestPermission().then((permission) => {
1364 VBOCore.handleNotifPerms(permission, selector);
1365 });
1366 } catch(e) {
1367 // run the callback-based version
1368 Notification.requestPermission(function(permission) {
1369 VBOCore.handleNotifPerms(permission, selector);
1370 });
1371 }
1372 }
1373
1374 /**
1375 * Handles the notifications permission response (from callback or promise resolved).
1376 *
1377 * @param any permission permission status string or NotificationPermission object.
1378 * @param string selector optional element query selector.
1379 */
1380 static handleNotifPerms(permission, selector) {
1381 // check the permission status from the Notification object interface
1382 if ((Notification.permission && Notification.permission === 'granted') || (typeof permission === 'string' && permission === 'granted')) {
1383 // permissions granted!
1384 $((selector || VBOCore.options.panel_opts.notif_selector))
1385 .removeClass(VBOCore.options.panel_opts.notif_off_class)
1386 .addClass(VBOCore.options.panel_opts.notif_on_class)
1387 .attr('title', VBOCore.options.tn_texts.notifs_enabled);
1388
1389 // dispatch an immediate notification to congratulate with the activation
1390 VBOCore.dispatchCongratulations();
1391
1392 return true;
1393 } else {
1394 // permissions denied :(
1395 $((selector || VBOCore.options.panel_opts.notif_selector))
1396 .removeClass(VBOCore.options.panel_opts.notif_on_class)
1397 .addClass(VBOCore.options.panel_opts.notif_off_class)
1398 .attr('title', VBOCore.options.tn_texts.notifs_disabled);
1399
1400 // show alert message
1401 console.error('Permission denied for enabling browser notifications', permission);
1402 alert(VBOCore.options.tn_texts.notifs_disabled_help);
1403 }
1404
1405 return false;
1406 }
1407
1408 /**
1409 * Dispatches an immediate notification with a congratulations text.
1410 *
1411 * @return void
1412 */
1413 static dispatchCongratulations() {
1414 try {
1415 let notif = new Notification(VBOCore.options.tn_texts.congrats, {
1416 body: VBOCore.options.tn_texts.notifs_enabled,
1417 tag: 'vbo_notification_congrats',
1418 silent: false
1419 });
1420 } catch(error) {
1421 alert(error);
1422 }
1423 }
1424
1425 /**
1426 * Given a date-time string, returns a Date object representation.
1427 *
1428 * @param string dtime_str the date-time string in "Y-m-d H:i:s" format.
1429 */
1430 static getDateTimeObject(dtime_str) {
1431 // instantiate a new date object
1432 var date_obj = new Date();
1433
1434 // parse date-time string
1435 let dtime_parts = dtime_str.split(' ');
1436 let date_parts = dtime_parts[0].split('-');
1437 if (dtime_parts.length != 2 || date_parts.length != 3) {
1438 // invalid military format
1439 return date_obj;
1440 }
1441 let time_parts = dtime_parts[1].split(':');
1442
1443 // set accurate date-time values
1444 date_obj.setFullYear(date_parts[0]);
1445 date_obj.setMonth((parseInt(date_parts[1]) - 1));
1446 date_obj.setDate(parseInt(date_parts[2]));
1447 date_obj.setHours(parseInt(time_parts[0]));
1448 date_obj.setMinutes(parseInt(time_parts[1]));
1449 date_obj.setSeconds(0);
1450 date_obj.setMilliseconds(0);
1451
1452 // return the accurate date object
1453 return date_obj;
1454 }
1455
1456 /**
1457 * Given a list of schedules, enqueues notifications to watch.
1458 *
1459 * @param array|object schedules list of or one notification object(s).
1460 *
1461 * @return bool
1462 */
1463 static enqueueNotifications(schedules) {
1464 if (!Array.isArray(schedules) || !schedules.length) {
1465 if (typeof schedules === 'object' && schedules.hasOwnProperty('dtime')) {
1466 // convert the single schedule to an array
1467 schedules = [schedules];
1468 } else {
1469 // invalid argument passed
1470 return false;
1471 }
1472 }
1473
1474 for (var i in schedules) {
1475 if (!schedules.hasOwnProperty(i) || typeof schedules[i] !== 'object') {
1476 continue;
1477 }
1478 VBOCore.notifications.push(schedules[i]);
1479 }
1480
1481 // setup the timeouts to schedule the notifications
1482 return VBOCore.scheduleNotifications();
1483 }
1484
1485 /**
1486 * Schedule the trigger timings for each notification.
1487 */
1488 static scheduleNotifications() {
1489 if (!VBOCore.notifications.length) {
1490 // no notifications to be scheduled
1491 return false;
1492 }
1493 if (VBOCore.notificationsEnabled() !== true) {
1494 // notifications not enabled
1495 console.info('Browser notifications disabled or unsupported.');
1496 return false;
1497 }
1498
1499 // gather current date-timing information
1500 const now_date = new Date();
1501 const now_time = now_date.getTime();
1502
1503 // parse all notifications to schedule the timers if not set
1504 for (let i = 0; i < VBOCore.notifications.length; i++) {
1505 let notif = VBOCore.notifications[i];
1506
1507 if (typeof notif !== 'object' || !notif.hasOwnProperty('dtime')) {
1508 // invalid notification object, unset it
1509 VBOCore.notifications.splice(i, 1);
1510 continue;
1511 }
1512
1513 // check if timer has been set
1514 if (!notif.hasOwnProperty('id_timer')) {
1515 // estimate trigger timing
1516 let in_ms = 0;
1517 // check for imminent notifications
1518 if (typeof notif.dtime === 'string' && notif.dtime.indexOf('now') >= 0) {
1519 // imminent ones will be delayed by one second
1520 in_ms = 1000;
1521 } else {
1522 // check overdue date-time (notif.dtime can also be a Date object instance)
1523 let nexp = VBOCore.getDateTimeObject(notif.dtime);
1524 in_ms = nexp.getTime() - now_time;
1525 }
1526 if (in_ms > 0) {
1527 // schedule notification trigger
1528 let id_timer = setTimeout(() => {
1529 VBOCore.dispatchNotification(notif);
1530 }, in_ms);
1531 // set timer on notification object
1532 VBOCore.notifications[i]['id_timer'] = id_timer;
1533 }
1534 }
1535 }
1536
1537 return true;
1538 }
1539
1540 /**
1541 * Deregister all scheduled notifications.
1542 */
1543 static unscheduleNotifications() {
1544 if (!VBOCore.notifications.length) {
1545 // no notifications scheduled
1546 return false;
1547 }
1548
1549 for (let i = 0; i < VBOCore.notifications.length; i++) {
1550 let notif = VBOCore.notifications[i];
1551
1552 if (typeof notif === 'object' && notif.hasOwnProperty('id_timer')) {
1553 // unset timeout for this notification
1554 clearTimeout(notif['id_timer']);
1555 }
1556 }
1557
1558 // reset pool
1559 VBOCore.notifications = [];
1560 }
1561
1562 /**
1563 * Update or delete a previously scheduled notification.
1564 *
1565 * @param object match_props map of properties to match.
1566 * @param string|number newdtime the new date time to schedule (0 for deleting).
1567 *
1568 * @return null|bool true only if a notification matched.
1569 */
1570 static updateNotification(match_props, newdtime) {
1571 if (!VBOCore.notifications.length) {
1572 // no notifications set, terminate
1573 return null;
1574 }
1575
1576 if (typeof match_props !== 'object') {
1577 // no properties to match the notification
1578 return null;
1579 }
1580
1581 // gather current date-timing information
1582 const now_date = new Date();
1583 const now_time = now_date.getTime();
1584
1585 // parse all notifications scheduled
1586 for (let i = 0; i < VBOCore.notifications.length; i++) {
1587 let notif = VBOCore.notifications[i];
1588
1589 let all_matched = true;
1590 let to_matching = false;
1591 for (let prop in match_props) {
1592 if (!match_props.hasOwnProperty(prop)) {
1593 continue;
1594 }
1595 to_matching = true;
1596 if (!notif.hasOwnProperty(prop) || notif[prop] != match_props[prop]) {
1597 all_matched = false;
1598 break;
1599 }
1600 }
1601
1602 if (all_matched && to_matching) {
1603 // notification object found
1604 if (notif.hasOwnProperty('id_timer')) {
1605 // unset previous timeout for this notification
1606 clearTimeout(notif['id_timer']);
1607 }
1608 // update or delete scheduled notification
1609 if (newdtime === 0) {
1610 // delete notification from queue
1611 VBOCore.notifications.splice(i, 1);
1612 } else {
1613 // update timing scheduler
1614 let in_ms = 0;
1615 // check for imminent notifications
1616 if (typeof newdtime === 'string' && newdtime.indexOf('now') >= 0) {
1617 // imminent ones will be delayed by one second
1618 in_ms = 1000;
1619 } else {
1620 // check overdue date-time (newdtime can also be a Date object instance)
1621 let nexp = VBOCore.getDateTimeObject(newdtime);
1622 in_ms = nexp.getTime() - now_time;
1623 }
1624 if (in_ms > 0) {
1625 // schedule notification trigger
1626 let id_timer = setTimeout(() => {
1627 VBOCore.dispatchNotification(notif);
1628 }, in_ms);
1629 // set timer on notification object
1630 VBOCore.notifications[i]['id_timer'] = id_timer;
1631 }
1632 // update date-time value on notification object
1633 VBOCore.notifications[i]['dtime'] = newdtime;
1634 }
1635
1636 // terminate parsing and return true
1637 return true;
1638 }
1639 }
1640
1641 // notification object not found
1642 return false;
1643 }
1644
1645 /**
1646 * Dispatch the notification object.
1647 * Expected notification properties:
1648 *
1649 * {
1650 * id: number
1651 * type: string
1652 * dtime: string|Date
1653 * build_url: string|null
1654 * }
1655 *
1656 * @param object notif the notification object.
1657 */
1658 static dispatchNotification(notif) {
1659 if (typeof notif !== 'object') {
1660 return false;
1661 }
1662
1663 // subscribe to building notification data
1664 VBOCore.buildNotificationData(notif).then((data) => {
1665 // dispatch the notification
1666
1667 // check if the click event should be registered
1668 let func_nodes;
1669 if (data.onclick && typeof data.onclick === 'string') {
1670 let callback_parts = data.onclick.split('.');
1671 while (callback_parts.length) {
1672 // compose window static method string to avoid using eval()
1673 let tmp = callback_parts.shift();
1674 if (!func_nodes) {
1675 func_nodes = window[tmp];
1676 } else {
1677 func_nodes = func_nodes[tmp];
1678 }
1679 }
1680 }
1681
1682 // prepare properties to delete the notification from queue
1683 let match_props = {};
1684 for (let prop in notif) {
1685 if (!notif.hasOwnProperty(prop) || prop == 'id_timer') {
1686 continue;
1687 }
1688 match_props[prop] = notif[prop];
1689 }
1690
1691 // check browser Notifications API
1692 if (VBOCore.notificationsEnabled() !== true) {
1693 // notifications not enabled, fallback to toast message
1694 let toast_notif_data = {
1695 title: data.title,
1696 body: data.message,
1697 icon: data.icon,
1698 delay: {
1699 min: 6000,
1700 max: 20000,
1701 tolerance: 4000,
1702 },
1703 action: () => {
1704 VBOToast.dispose(true);
1705 },
1706 sound: VBOCore.options.notif_audio_url
1707 };
1708 if (func_nodes) {
1709 toast_notif_data.action = function() {
1710 func_nodes(data);
1711 VBOToast.dispose(true);
1712 };
1713 } else if (typeof data.onclick === 'function') {
1714 toast_notif_data.action = function() {
1715 data.onclick.call(data);
1716 VBOToast.dispose(true);
1717 };
1718 }
1719 VBOToast.enqueue(new VBOToastMessage(toast_notif_data));
1720
1721 // delete dispatched notification from queue
1722 VBOCore.updateNotification(match_props, 0);
1723
1724 return;
1725 }
1726
1727 // use the browser's native Notifications API
1728 let browser_notif = new Notification(data.title, {
1729 body: data.message,
1730 icon: data.icon,
1731 tag: 'vbo_notification',
1732 silent: false
1733 });
1734
1735 // check if support for Web App badge is available
1736 if (typeof navigator.setAppBadge !== 'undefined') {
1737 navigator.setAppBadge(1);
1738 }
1739
1740 browser_notif.addEventListener('click', (e) => {
1741 // check if support for Web App badge is available
1742 if (typeof navigator.setAppBadge !== 'undefined') {
1743 navigator.clearAppBadge();
1744 }
1745 // close the notification
1746 e.target.close();
1747 });
1748
1749 if (func_nodes) {
1750 // register notification click event
1751 browser_notif.addEventListener('click', () => {
1752 func_nodes(data);
1753 });
1754 } else if (typeof data.onclick === 'function') {
1755 browser_notif.addEventListener('click', () => {
1756 data.onclick.call(data);
1757 });
1758 }
1759
1760 // delete dispatched notification from queue
1761 VBOCore.updateNotification(match_props, 0);
1762
1763 }).catch((error) => {
1764 console.error(error);
1765 });
1766 }
1767
1768 /**
1769 * Asynchronous build of the notification data object for dispatch.
1770 * Minimum expected notification display data properties:
1771 *
1772 * {
1773 * title: string
1774 * message: string
1775 * icon: string
1776 * onclick: function
1777 * }
1778 *
1779 * @param object notif the scheduled notification object.
1780 *
1781 * @return Promise
1782 */
1783 static buildNotificationData(notif) {
1784 return new Promise((resolve, reject) => {
1785 // notification object validation
1786 if (typeof notif !== 'object') {
1787 reject('Invalid notification object');
1788 return;
1789 }
1790
1791 if (!notif.hasOwnProperty('build_url') || !notif.build_url) {
1792 // building callback not necessary
1793 if (!notif.title && !notif.message) {
1794 reject('Unexpected notification object');
1795 return;
1796 }
1797 // we expect the notification to be built already
1798 resolve(notif);
1799 return;
1800 }
1801
1802 // build the notification data
1803 VBOCore.doAjax(
1804 notif.build_url,
1805 {
1806 payload: JSON.stringify(notif)
1807 },
1808 (response) => {
1809 // parse response
1810 try {
1811 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1812 if (obj_res.hasOwnProperty('title')) {
1813 resolve(obj_res);
1814 } else {
1815 reject('Unexpected JSON response');
1816 }
1817 } catch(err) {
1818 reject('could not parse JSON response');
1819 }
1820 },
1821 (error) => {
1822 reject(error.responseText);
1823 }
1824 );
1825 });
1826 }
1827
1828 /**
1829 * Handle a navigation towards a given URL.
1830 * Common handler for browser notifications click.
1831 *
1832 * @param object data notification display data payload.
1833 */
1834 static handleGoto(data) {
1835 if (typeof data !== 'object' || !data.hasOwnProperty('gotourl') || !data.gotourl) {
1836 console.error('Could not handle the goto operation', data);
1837 return;
1838 }
1839
1840 if (typeof data.openWindow !== 'undefined' || typeof document === 'undefined') {
1841 // open a new window
1842 window.open(data.gotourl);
1843 return;
1844 }
1845
1846 // redirect
1847 document.location.href = data.gotourl;
1848 }
1849
1850 /**
1851 * Opens a new window with the URI to render the Push notification data.
1852 *
1853 * @param object options admin widget rendering options to bind.
1854 *
1855 * @return void
1856 */
1857 static openAdminPushURI(options) {
1858 if (typeof options !== 'object' || !options) {
1859 throw new Error('Unable to build Admin Push URI');
1860 }
1861
1862 let vbo_admin_push_uri = VBOCore.options.root_uri;
1863
1864 if (VBOCore.options.cms === 'wordpress') {
1865 vbo_admin_push_uri += 'wp-admin/admin.php?page=vikbooking';
1866 } else {
1867 vbo_admin_push_uri += 'administrator/index.php?option=com_vikbooking';
1868 }
1869
1870 // build Push notification payload for rendering
1871 let push_data = {
1872 type: options.type || '',
1873 title: options.title || '',
1874 message: options.message || '',
1875 content: options,
1876 };
1877
1878 // open page in a new window
1879 window.open(vbo_admin_push_uri + '&push_notification=' + btoa(JSON.stringify(push_data)), '_blank');
1880 }
1881
1882 /**
1883 * Handle the display of a notification through a widget.
1884 * Common handler for browser notifications displayed through
1885 * a widget modal rendering.
1886 *
1887 * @param object data notification display data payload.
1888 * @param object options optional extra options for the admin widget.
1889 * @param boolean is_push whether we are coming from a Push notification.
1890 *
1891 * @return void
1892 */
1893 static handleDisplayWidgetNotification(data, options, is_push) {
1894 try {
1895 // validate handler
1896 if (!VBOCore.options.is_vbo && !VBOCore.options.is_vcm) {
1897 throw new Error('Unable to handle the notification display from the current page');
1898 }
1899
1900 // validate payload
1901 if (typeof data !== 'object' || !data.hasOwnProperty('widget_id') || !data['widget_id']) {
1902 throw new Error('Invalid widget payload');
1903 }
1904
1905 // parse handler
1906 if (VBOCore.options.is_vcm || (is_push && !VBOCore.options.is_vbo)) {
1907 // the operation is handled by VCM (or by an external admin resource, if it's a Push notification)
1908 VBOCore.loadAdminWidgetAssets(data).then((assets) => {
1909 // append assets to DOM
1910 assets.forEach((asset) => {
1911 if (!$('link#' + asset['id']).length) {
1912 $('head').append('<link rel="stylesheet" id="' + asset['id'] + '" href="' + asset['href'] + '" media="all" />');
1913 }
1914 });
1915 // widget modal rendering handled by VCM (or an external admin resource)
1916 let hide_panel = VBOCore.options.is_vcm && $('.' + VBOCore.options.panel_opts.open_class).length ? true : false;
1917 let modal_data = VBOCore.renderModalWidget(data['widget_id'], data, options, hide_panel);
1918 if (modal_data.hasOwnProperty('dismissed_event') && (modal_data['suffix'] || '').indexOf('inner') < 0) {
1919 // register event to unload all assets (only if not from an inner modal)
1920 document.addEventListener(modal_data['dismissed_event'], () => {
1921 assets.forEach((asset) => {
1922 if ($('link#' + asset['id']).length) {
1923 $('link#' + asset['id']).remove();
1924 }
1925 });
1926 });
1927 }
1928 }).catch((error) => {
1929 throw new Error(error);
1930 });
1931 } else {
1932 // widget modal rendering handled by Vik Booking
1933 VBOCore.renderModalWidget(data['widget_id'], data, options, false);
1934 }
1935 } catch(e) {
1936 if (is_push && options.type) {
1937 // fallback to opening a new window to render the clicked Push notification
1938 VBOCore.openAdminPushURI(options);
1939 } else {
1940 // fallback to a regular link opening, if set
1941 VBOCore.handleGoto(data);
1942 }
1943 }
1944 }
1945
1946 /**
1947 * Asynchronous loading of CSS assets required to render
1948 * an admin widget outside Vik Booking.
1949 *
1950 * @param object data the optional widget payload data.
1951 *
1952 * @return Promise
1953 */
1954 static loadAdminWidgetAssets(data) {
1955 return new Promise((resolve, reject) => {
1956 // the remote assets URI must be set
1957 if (!VBOCore.options.assets_ajax_uri) {
1958 reject('Missing remote assets URI');
1959 return;
1960 }
1961
1962 if (typeof data !== 'object') {
1963 data = {};
1964 }
1965
1966 // make the request
1967 VBOCore.doAjax(
1968 VBOCore.options.assets_ajax_uri,
1969 data,
1970 (response) => {
1971 // parse response
1972 try {
1973 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1974 if (!Array.isArray(obj_res)) {
1975 reject('Unexpected JSON response');
1976 }
1977 resolve(obj_res);
1978 } catch(err) {
1979 reject('could not parse JSON response');
1980 }
1981 },
1982 (error) => {
1983 reject(error.responseText);
1984 }
1985 );
1986 });
1987 }
1988
1989 /**
1990 * Register the latest data to watch for the preloaded admin widgets.
1991 *
1992 * @param object watch_data
1993 */
1994 static registerWatchData(watch_data) {
1995 if (typeof watch_data !== 'object' || watch_data == null) {
1996 VBOCore.widgets_watch_data = null;
1997 return false;
1998 }
1999
2000 // set watch-data map
2001 VBOCore.widgets_watch_data = watch_data;
2002
2003 // schedule watching interval
2004 if (VBOCore.watch_data_interval == null) {
2005 VBOCore.watch_data_interval = window.setInterval(VBOCore.watchWidgetsData, 60000);
2006 }
2007
2008 // set up broadcast channels to connect all browsing contexts
2009 if (typeof BroadcastChannel !== 'undefined') {
2010 // set up watch-data broadcast channel
2011 if (!VBOCore.broadcast_watch_data) {
2012 // start broadcast channel
2013 VBOCore.broadcast_watch_data = new BroadcastChannel('vikbooking_watch_data');
2014
2015 // register to the "on broadcast message received" event
2016 VBOCore.broadcast_watch_data.onmessage = (event) => {
2017 if (event && event.data) {
2018 // update watch data map for next schedule to avoid dispatching duplicate notifications
2019 VBOCore.widgets_watch_data = event.data;
2020 }
2021 };
2022 }
2023
2024 // set up push-data broadcast channel
2025 if (!VBOCore.broadcast_push_data) {
2026 // start broadcast channel
2027 VBOCore.broadcast_push_data = new BroadcastChannel('vikbooking_push_data');
2028
2029 // register to the "on broadcast message received" event
2030 VBOCore.broadcast_push_data.onmessage = (event) => {
2031 if (event && event.data) {
2032 // update pushed data map for the next watching event
2033 VBOCore.widgets_pushed_data.push(event.data);
2034 }
2035 };
2036 }
2037
2038 // set up watch-events broadcast channel
2039 if (!VBOCore.broadcast_watch_events) {
2040 // start broadcast channel
2041 VBOCore.broadcast_watch_events = new BroadcastChannel('vikbooking_watch_events');
2042
2043 // register to the "on broadcast message received" event
2044 VBOCore.broadcast_watch_events.onmessage = (event) => {
2045 if (event && event.data && typeof event.data === 'object') {
2046 // scan and dispatch the events data received
2047 let events_data = event.data;
2048 for (let ev_name in events_data) {
2049 if (!events_data.hasOwnProperty(ev_name)) {
2050 continue;
2051 }
2052 // emit the event
2053 VBOCore.emitEvent(ev_name, events_data[ev_name]);
2054 }
2055 }
2056 };
2057 }
2058 }
2059 }
2060
2061 /**
2062 * Periodic widgets data watching for new events.
2063 */
2064 static watchWidgetsData() {
2065 if (typeof VBOCore.widgets_watch_data !== 'object' || VBOCore.widgets_watch_data == null) {
2066 return;
2067 }
2068
2069 // call on new events
2070 VBOCore.doAjax(
2071 VBOCore.options.watchdata_ajax_uri,
2072 {
2073 watch_data: JSON.stringify(VBOCore.widgets_watch_data),
2074 pushed_data: JSON.stringify(VBOCore.widgets_pushed_data),
2075 },
2076 (response) => {
2077 try {
2078 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
2079 if (obj_res.hasOwnProperty('watch_data')) {
2080 // update watch data map for next schedule
2081 VBOCore.widgets_watch_data = obj_res['watch_data'];
2082
2083 // check for notifications
2084 if (obj_res.hasOwnProperty('notifications') && Array.isArray(obj_res['notifications'])) {
2085 // dispatch notifications
2086 for (var i = 0; i < obj_res['notifications'].length; i++) {
2087 VBOCore.dispatchNotification(obj_res['notifications'][i]);
2088 }
2089
2090 // post message onto broadcast channel for any other browsing context
2091 if (VBOCore.broadcast_watch_data && obj_res['notifications'].length) {
2092 // this will avoid dispatching duplicate notifications
2093 VBOCore.broadcast_watch_data.postMessage(VBOCore.widgets_watch_data);
2094 }
2095 }
2096
2097 // check for notification events to dispatch
2098 if (obj_res.hasOwnProperty('events') && Array.isArray(obj_res['events'])) {
2099 // parse and dispatch all the events
2100 obj_res['events'].forEach((events_data) => {
2101 if (typeof events_data !== 'object') {
2102 return;
2103 }
2104
2105 // parse all the events for this widget watched data
2106 for (let ev_name in events_data) {
2107 if (!events_data.hasOwnProperty(ev_name)) {
2108 continue;
2109 }
2110 // emit the event
2111 VBOCore.emitEvent(ev_name, events_data[ev_name]);
2112 }
2113
2114 // post message onto broadcast channel for any other browsing context
2115 if (VBOCore.broadcast_watch_events) {
2116 // this will trigger the events on any other browsing context
2117 VBOCore.broadcast_watch_events.postMessage(events_data);
2118 }
2119 });
2120 }
2121 } else {
2122 console.error('Unexpected or invalid JSON response', response);
2123 }
2124 } catch(err) {
2125 console.error('could not parse JSON response', err, response);
2126 }
2127 },
2128 (error) => {
2129 console.error(error.responseText);
2130 }
2131 );
2132 }
2133
2134 /**
2135 * Widget modal rendering.
2136 *
2137 * @param string widget_id the widget identifier to render.
2138 * @param any data the optional multitask data to inject.
2139 * @param object options optional list of extra options to render the widget with Multitask data.
2140 * @param bool hide_panel if false, the multitask panel elements will remain unchanged.
2141 *
2142 * @return object the multitask data injected object merged with modal options.
2143 */
2144 static renderModalWidget(widget_id, data, options, hide_panel) {
2145 /**
2146 * Adjust arguments for BC with previous ordering between options
2147 * and hide_panel, which used to be inverted. Useful also to shorten
2148 * the way the method can be invoked in case no options are needed.
2149 */
2150 if (typeof options === 'boolean') {
2151 // switch argument values
2152 hide_panel = options;
2153 options = null;
2154 }
2155
2156 if (typeof data !== 'object' || data == null) {
2157 // always treat data as an object
2158 data = {};
2159 }
2160
2161 if (typeof options !== 'object' || options == null) {
2162 // always treat options as an object
2163 options = data._options || {};
2164 }
2165
2166 // build the default widget payload
2167 let modal_js_id = Math.floor(Math.random() * 100000);
2168 let modal_title = options._push && options.title ? options.title : VBOCore.options.tn_texts.admin_widget;
2169 let widget_data = {
2170 _modalRendering: 1,
2171 _modalJsId: modal_js_id,
2172 _modalTitle: modal_title,
2173 _options: options,
2174 };
2175
2176 // merge default payload options with given options
2177 data = Object.assign(widget_data, data);
2178
2179 // define unique modal event names to avoid conflicts
2180 let dismiss_event = 'vbo-dismiss-widget-modal' + modal_js_id;
2181 let loading_event = 'vbo-loading-widget-modal' + modal_js_id;
2182
2183 // define the modal options
2184 let modal_options = {
2185 suffix: 'widget_modal',
2186 extra_class: 'vbo-modal-widget vbo-modal-rounded vbo-modal-tall vbo-modal-nofooter',
2187 title: data._modalTitle,
2188 body_prepend: true,
2189 lock_scroll: true,
2190 draggable: true,
2191 dismiss_event: dismiss_event,
2192 loading_event: loading_event,
2193 loading_body: VBOCore.options.default_loading_body,
2194 dismissed_event: VBOCore.options.widget_modal_dismissed + modal_js_id,
2195 event_data: widget_data,
2196 };
2197
2198 // check if options should rewrite some modal-options
2199 if (options.modal_options) {
2200 modal_options = Object.assign(modal_options, options.modal_options);
2201 }
2202
2203 // display modal
2204 let widget_modal = VBOCore.displayModal(modal_options);
2205
2206 if (hide_panel !== false) {
2207 // blur search widget input, hide multitask panel
2208 $(VBOCore.options.panel_opts.search_selector).trigger('blur');
2209 VBOCore.emitEvent(VBOCore.multitask_shortcut_event);
2210 }
2211
2212 // start loading
2213 VBOCore.emitEvent(loading_event);
2214
2215 // render admin widget
2216 VBOCore.renderAdminWidget(widget_id, data).then((content) => {
2217 // stop loading and append widget content to modal
2218 VBOCore.emitEvent(loading_event);
2219 widget_modal.append(content);
2220
2221 // register an admin menu action for the rendered widget
2222 VBOCore.fetchWidgetDetails(widget_id).then((details) => {
2223 // update modal title, if default title is present
2224 if (modal_options.title == VBOCore.options.tn_texts.admin_widget) {
2225 widget_modal
2226 .closest('.vbo-modal-overlay-content')
2227 .find('.vbo-modal-overlay-content-head-title')
2228 .text(data._modalTitle + ' - ' + details['name']);
2229 }
2230
2231 // register admin menu action
2232 try {
2233 // work on Local Storage to register the widget data
2234 VBOCore.registerAdminMenuAction({
2235 name: details['name'],
2236 href: 'JavaScript: void(0);',
2237 widget: widget_id,
2238 icon: details['icon'],
2239 style: details['style'],
2240 }, 'widgets');
2241 } catch(e) {
2242 console.error(e);
2243 }
2244 }).catch((error) => {
2245 console.error(error);
2246 });
2247 }).catch((error) => {
2248 // dismiss modal and display error
2249 VBOCore.emitEvent(dismiss_event);
2250 alert(error);
2251 });
2252
2253 return Object.assign(data, modal_options);
2254 }
2255
2256 /**
2257 * Renders an admin widget.
2258 *
2259 * @param string widget_id the widget identifier string to add.
2260 * @param any data the optional multitask data to inject.
2261 *
2262 * @return Promise
2263 */
2264 static renderAdminWidget(widget_id, data) {
2265 return new Promise((resolve, reject) => {
2266 if (!VBOCore.options.widget_ajax_uri) {
2267 reject('Could not load admin widget');
2268 return;
2269 }
2270
2271 var call_method = 'render';
2272 VBOCore.doAjax(
2273 VBOCore.options.widget_ajax_uri,
2274 {
2275 widget_id: widget_id,
2276 call: call_method,
2277 vbo_page: VBOCore.options.current_page,
2278 vbo_uri: VBOCore.options.current_page_uri,
2279 multitask: 1,
2280 multitask_data: data,
2281 },
2282 (response) => {
2283 // parse response
2284 try {
2285 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
2286 if (!obj_res.hasOwnProperty(call_method)) {
2287 reject('Unexpected JSON response');
2288 return;
2289 }
2290 resolve(obj_res[call_method]);
2291 } catch(err) {
2292 reject('could not parse JSON response');
2293 }
2294 },
2295 (error) => {
2296 reject(error.responseText);
2297 }
2298 );
2299 });
2300 }
2301
2302 /**
2303 * Fetches the details for a specific widget.
2304 *
2305 * @param string widget_id the widget identifier string to fetch.
2306 *
2307 * @return Promise
2308 *
2309 * @since 1.6.7
2310 */
2311 static fetchWidgetDetails(widget_id) {
2312 return new Promise((resolve, reject) => {
2313 if (!VBOCore.options.widget_ajax_uri) {
2314 reject('Could not load admin widget');
2315 return;
2316 }
2317
2318 var call_method = 'getWidgetDetails';
2319 VBOCore.doAjax(
2320 VBOCore.options.widget_ajax_uri,
2321 {
2322 widget_id: widget_id,
2323 call: call_method,
2324 return: 1,
2325 },
2326 (response) => {
2327 // parse response
2328 try {
2329 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
2330 if (!obj_res.hasOwnProperty(call_method)) {
2331 reject('Unexpected JSON response');
2332 return;
2333 }
2334 resolve(obj_res[call_method]);
2335 } catch(err) {
2336 reject('could not parse JSON response');
2337 }
2338 },
2339 (error) => {
2340 reject(error.responseText);
2341 }
2342 );
2343 });
2344 }
2345
2346 /**
2347 * Helper method used to copy the text of an
2348 * input element within the clipboard.
2349 *
2350 * Clipboard copy will take effect only in case the
2351 * function is handled by a DOM event explicitly
2352 * triggered by the user, such as a "click".
2353 *
2354 * @param any input The input containing the text to copy.
2355 *
2356 * @return Promise
2357 */
2358 static copyToClipboard(input) {
2359 // register and return promise
2360 return new Promise((resolve, reject) => {
2361 // define a fallback function
2362 var fallback = function(input) {
2363 // focus the input
2364 input.focus();
2365 // select the text inside the input
2366 input.select();
2367
2368 try {
2369 // try to copy with shell command
2370 var copy = document.execCommand('copy');
2371
2372 if (copy) {
2373 // copied successfully
2374 resolve(copy);
2375 } else {
2376 // unable to copy
2377 reject(copy);
2378 }
2379 } catch (error) {
2380 // unable to exec the command
2381 reject(error);
2382 }
2383 };
2384
2385 // look for navigator clipboard
2386 if (!navigator || !navigator.clipboard) {
2387 // navigator clipboard not supported, use fallback
2388 fallback(input);
2389 return;
2390 }
2391
2392 // try to copy within the clipboard by using the navigator
2393 navigator.clipboard.writeText((input.val ? input.val() : input.value)).then(() => {
2394 // copied successfully
2395 resolve(true);
2396 }).catch((error) => {
2397 // revert to the fallback
2398 fallback(input);
2399 });
2400 });
2401 }
2402
2403 /**
2404 * Helper method used to display a modal window dinamycally.
2405 *
2406 * @param object options The options to render the modal.
2407 *
2408 * @return object The modal content element wrapper.
2409 */
2410 static displayModal(options) {
2411 var def_options = {
2412 suffix: (Math.floor(Math.random() * 100000)) + '',
2413 extra_class: null,
2414 header: true,
2415 title: '',
2416 body: '',
2417 body_prepend: false,
2418 lock_scroll: false,
2419 draggable: false,
2420 footer_left: null,
2421 footer_right: null,
2422 dismiss_event: null,
2423 dismissed_event: null,
2424 onDismiss: null,
2425 loading_event: null,
2426 loading_body: VBOCore.options.default_loading_body,
2427 event_data: null,
2428 };
2429
2430 // merge default options with given options
2431 options = Object.assign(def_options, options);
2432
2433 // create the modal dismiss function
2434 var modal_dismiss_fn = (e) => {
2435 custom_modal.fadeOut(400, () => {
2436 // invoke callback for onDismiss
2437 if (typeof options.onDismiss === 'function') {
2438 options.onDismiss.call(custom_modal, e);
2439 }
2440
2441 // check if modal did register to the loading event
2442 if (options.loading_event) {
2443 // we can now un-register from the loading event until a new modal is displayed and will register to it again
2444 document.removeEventListener(options.loading_event, modal_handle_loading_event_fn);
2445 }
2446
2447 // check if we should fire the given modal dismissed event
2448 if (options.dismissed_event) {
2449 VBOCore.emitEvent(options.dismissed_event, options.event_data);
2450 }
2451
2452 // check if body scroll lock should be removed
2453 if (options.lock_scroll) {
2454 $('body').removeClass('vbo-modal-lock-scroll');
2455 }
2456
2457 // remove modal from DOM
2458 custom_modal.remove();
2459 });
2460 };
2461
2462 // create the modal loading event handler function
2463 var modal_handle_loading_event_fn = (e) => {
2464 // toggle modal loading
2465 if ($('.vbo-modal-overlay-content-backdrop').length) {
2466 // hide loading
2467 $('.vbo-modal-overlay-content-backdrop').remove();
2468
2469 // do not proceed
2470 return;
2471 }
2472
2473 // show loading
2474 var modal_loading = $('<div></div>').addClass('vbo-modal-overlay-content-backdrop');
2475 var modal_loading_body = $('<div></div>').addClass('vbo-modal-overlay-content-backdrop-body');
2476 if (options.loading_body) {
2477 modal_loading_body.append(options.loading_body);
2478 }
2479 modal_loading.append(modal_loading_body);
2480
2481 // append backdrop loading to modal content
2482 modal_content.prepend(modal_loading);
2483 };
2484
2485 // start the modal position variables
2486 var modal_pos_x = 0, modal_pos_y = 0;
2487
2488 // create the modal drag-start (mousedown) event handler function
2489 var modal_dragstart_fn = (e) => {
2490 e = e || window.event;
2491 e.preventDefault();
2492
2493 if (typeof e.clientX === 'undefined' || typeof e.clientY === 'undefined') {
2494 // unsupported
2495 return;
2496 }
2497
2498 if (e.target) {
2499 e.target.style.cursor = 'move';
2500 }
2501
2502 // store the initial modal (cursor) position
2503 modal_pos_x = e.clientX;
2504 modal_pos_y = e.clientY;
2505
2506 // register mouseup and mousemove events
2507 document.onmouseup = modal_dragstop_fn;
2508 document.onmousemove = modal_dragmove_fn;
2509 };
2510
2511 // create the modal drag-stop (mouseup) event handler function
2512 var modal_dragstop_fn = (e) => {
2513 e = e || window.event;
2514
2515 if (e.target) {
2516 e.target.style.cursor = 'auto';
2517 }
2518
2519 // unregister mousemove event
2520 if (document.onmousemove == modal_dragmove_fn) {
2521 document.onmousemove = null;
2522 }
2523
2524 // unregister mouseup event
2525 if (document.onmouseup == modal_dragstop_fn) {
2526 document.onmouseup = null;
2527 }
2528 };
2529
2530 // create the modal drag-move (mousemove) event handler function
2531 var modal_dragmove_fn = (e) => {
2532 e = e || window.event;
2533 e.preventDefault();
2534
2535 if (typeof e.clientX === 'undefined' || typeof e.clientY === 'undefined') {
2536 // unsupported
2537 return;
2538 }
2539
2540 // calculate the new modal (cursor) position
2541 let new_modal_pos_x = modal_pos_x - e.clientX;
2542 let new_modal_pos_y = modal_pos_y - e.clientY;
2543
2544 // update current modal (cursor) position
2545 modal_pos_x = e.clientX;
2546 modal_pos_y = e.clientY;
2547
2548 // find the modal element
2549 let modal_element = e.target.closest('.vbo-modal-overlay-content');
2550 if (!modal_element) {
2551 return;
2552 }
2553
2554 // set the modal position
2555 modal_element.style.top = (modal_element.offsetTop - new_modal_pos_y) + 'px';
2556 modal_element.style.left = (modal_element.offsetLeft - new_modal_pos_x) + 'px';
2557 };
2558
2559 // build modal content
2560 var custom_modal = $('<div></div>').addClass('vbo-modal-overlay-block vbo-modal-overlay-' + options.suffix).css('display', 'block');
2561 var modal_dismiss = $('<a></a>').addClass('vbo-modal-overlay-close');
2562 modal_dismiss.on('click', modal_dismiss_fn);
2563 custom_modal.append(modal_dismiss);
2564
2565 var modal_content = $('<div></div>').addClass('vbo-modal-overlay-content vbo-modal-overlay-content-' + options.suffix);
2566 if (options.extra_class && typeof options.extra_class === 'string') {
2567 modal_content.addClass(options.extra_class);
2568 }
2569
2570 var modal_head = $('<div></div>').addClass('vbo-modal-overlay-content-head');
2571 var modal_head_close = $('<span></span>').addClass('vbo-modal-overlay-close-times').html('&times;');
2572 modal_head_close.on('click', modal_dismiss_fn);
2573 if (options.title) {
2574 let modal_title = $('<span></span>').addClass('vbo-modal-overlay-content-head-title').html(options.title);
2575 modal_head.append(modal_title);
2576 }
2577 modal_head.append(modal_head_close);
2578
2579 // check if the modal head should be draggable
2580 if (options.draggable) {
2581 // register the event(s) to allow dragging
2582 modal_head.addClass('vbo-modal-head-draggable');
2583 modal_head.on('contextmenu', (e) => {
2584 e.preventDefault();
2585 });
2586 modal_head.on('mousedown', modal_dragstart_fn);
2587 }
2588
2589 var modal_body = $('<div></div>').addClass('vbo-modal-overlay-content-body vbo-modal-overlay-content-body-scroll');
2590 var modal_content_wrapper = $('<div></div>').addClass('vbo-modal-' + options.suffix + '-wrap');
2591 if (typeof options.body === 'string') {
2592 modal_content_wrapper.html(options.body);
2593 } else {
2594 modal_content_wrapper.append(options.body);
2595 }
2596 modal_body.append(modal_content_wrapper);
2597
2598 // modal footer
2599 var modal_footer = null;
2600 if (options.footer_left || options.footer_right) {
2601 modal_footer = $('<div></div>').addClass('vbo-modal-overlay-content-footer');
2602 if (options.footer_left) {
2603 var modal_footer_left = $('<div></div>').addClass('vbo-modal-overlay-content-footer-left').append(options.footer_left);
2604 modal_footer.append(modal_footer_left);
2605 }
2606 if (options.footer_right) {
2607 var modal_footer_right = $('<div></div>').addClass('vbo-modal-overlay-content-footer-right').append(options.footer_right);
2608 modal_footer.append(modal_footer_right);
2609 }
2610
2611 }
2612
2613 // finalize modal contents
2614 if (options.header) {
2615 // append header
2616 modal_content.append(modal_head);
2617 }
2618 // append body
2619 modal_content.append(modal_body);
2620 if (modal_footer) {
2621 // append footer
2622 modal_content.append(modal_footer);
2623 }
2624 custom_modal.append(modal_content);
2625
2626 // register to the dismiss event
2627 if (options.dismiss_event) {
2628 // listen to the event that will dismiss the modal
2629 document.addEventListener(options.dismiss_event, function vbo_core_handle_dismiss_event(e) {
2630 // make sure the same event won't propagate again, unless a new modal is displayed (multiple displayModal calls)
2631 e.target.removeEventListener(e.type, vbo_core_handle_dismiss_event);
2632
2633 // invoke the modal dismiss function
2634 modal_dismiss_fn(e);
2635 });
2636
2637 // declare the function to detect the Escape key pressed
2638 const vbo_core_dismiss_event_modal_escape = (e) => {
2639 if (!e.key || e.key != 'Escape') {
2640 return;
2641 }
2642
2643 // immediately unregister from this event once fired
2644 window.removeEventListener(e.type, vbo_core_dismiss_event_modal_escape);
2645
2646 // trigger the actual dismiss event
2647 VBOCore.emitEvent(options.dismiss_event);
2648 };
2649
2650 // listen to the Escape keyup event to dismiss the modal
2651 // listen on window to allow admin-widgets to prevent the default behavior and stop the propagation
2652 window.addEventListener('keyup', vbo_core_dismiss_event_modal_escape);
2653 }
2654
2655 // register to the toggle-loading event
2656 if (options.loading_event) {
2657 // let a function handle it so that removing the event listener will be doable
2658 document.addEventListener(options.loading_event, modal_handle_loading_event_fn);
2659 }
2660
2661 // append (or prepend) modal to body
2662 if ($('.vbo-modal-overlay-' + options.suffix).length) {
2663 $('.vbo-modal-overlay-' + options.suffix).remove();
2664 }
2665 if (options.body_prepend) {
2666 // prepend to body
2667 if ($('body > .vbo-modal-overlay-block').length) {
2668 // we've got other modals prepended to the body, so go after the last one
2669 $('body > .vbo-modal-overlay-block').last().after(custom_modal);
2670 } else {
2671 // place the modal right as the first child node of body
2672 $('body').prepend(custom_modal);
2673 }
2674 } else {
2675 // append to body
2676 $('body').append(custom_modal);
2677 }
2678
2679 // check if scroll should be locked on the whole page body for a "sticky" modal
2680 if (options.lock_scroll) {
2681 $('body').addClass('vbo-modal-lock-scroll');
2682 }
2683
2684 // return the content wrapper element of the new modal
2685 return modal_content_wrapper;
2686 }
2687
2688 /**
2689 * Debounce technique to group a flurry of events into one single event.
2690 */
2691 static debounceEvent(func, wait, immediate) {
2692 var timeout;
2693 return function() {
2694 var context = this, args = arguments;
2695 var later = function() {
2696 timeout = null;
2697 if (!immediate) func.apply(context, args);
2698 };
2699 var callNow = immediate && !timeout;
2700 clearTimeout(timeout);
2701 timeout = setTimeout(later, wait);
2702 if (callNow) {
2703 func.apply(context, args);
2704 }
2705 }
2706 }
2707
2708 /**
2709 * Throttle guarantees a constant flow of events at a given time interval.
2710 * Runs immediately when the event takes place, but can be delayed.
2711 */
2712 static throttleEvent(method, delay) {
2713 var time = Date.now();
2714 return function() {
2715 if ((time + delay - Date.now()) < 0) {
2716 method();
2717 time = Date.now();
2718 }
2719 }
2720 }
2721
2722 /**
2723 * Alternative throttle technique for event listeners.
2724 *
2725 * @param function callback the callback to invoke.
2726 * @param int time the time (in ms) to throttle.
2727 *
2728 * @return void
2729 *
2730 * @since 1.6.8
2731 */
2732 static throttleTimer(callback, time) {
2733 if (VBOCore.throttle_timer) {
2734 // prevent more executions
2735 return;
2736 }
2737
2738 // turn throttle timer on
2739 VBOCore.throttle_timer = true;
2740
2741 setTimeout(() => {
2742 // run the callback with the given delay
2743 callback();
2744
2745 // turn throttle timer off
2746 VBOCore.throttle_timer = false;
2747 }, time);
2748 }
2749
2750 /**
2751 * Tells whether localStorage is supported.
2752 *
2753 * @return boolean
2754 */
2755 static storageSupported() {
2756 return typeof localStorage !== 'undefined';
2757 }
2758
2759 /**
2760 * Gets an item from localStorage.
2761 *
2762 * @param string keyName the storage key identifier.
2763 *
2764 * @return any
2765 */
2766 static storageGetItem(keyName) {
2767 if (!VBOCore.storageSupported()) {
2768 return null;
2769 }
2770
2771 return localStorage.getItem(keyName);
2772 }
2773
2774 /**
2775 * Sets an item to localStorage.
2776 *
2777 * @param string keyName the storage key identifier.
2778 * @param any value the value to store.
2779 *
2780 * @return boolean
2781 */
2782 static storageSetItem(keyName, value) {
2783 if (!VBOCore.storageSupported()) {
2784 return false;
2785 }
2786
2787 try {
2788 if (typeof value === 'object') {
2789 value = JSON.stringify(value);
2790 }
2791
2792 localStorage.setItem(keyName, value);
2793 } catch(e) {
2794 return false;
2795 }
2796
2797 return true;
2798 }
2799
2800 /**
2801 * Removes an item from localStorage.
2802 *
2803 * @param string keyName the storage key identifier.
2804 *
2805 * @return boolean
2806 */
2807 static storageRemoveItem(keyName) {
2808 if (!VBOCore.storageSupported()) {
2809 return false;
2810 }
2811
2812 localStorage.removeItem(keyName);
2813
2814 return true;
2815 }
2816
2817 /**
2818 * Returns the name of the storage identifier for the given scope.
2819 *
2820 * @param string scope the admin menu scope.
2821 *
2822 * @return string the requested admin menu storage identifier.
2823 */
2824 static getStorageScopeName(scope) {
2825 let storage_scope_name = VBOCore.options.admin_menu_actions_nm;
2826
2827 if (typeof scope === 'string' && scope.length) {
2828 if (scope.indexOf('.') !== 0) {
2829 scope = '.' + scope;
2830 }
2831 storage_scope_name += scope;
2832 }
2833
2834 return storage_scope_name;
2835 }
2836
2837 /**
2838 * Returns a list of admin menu action objects or an empty array.
2839 *
2840 * @param string scope the admin menu scope.
2841 *
2842 * @return Array
2843 */
2844 static getAdminMenuActions(scope) {
2845 let menu_actions = VBOCore.storageGetItem(VBOCore.getStorageScopeName(scope));
2846
2847 if (!menu_actions) {
2848 return [];
2849 }
2850
2851 try {
2852 menu_actions = JSON.parse(menu_actions);
2853 if (!Array.isArray(menu_actions) || !menu_actions.length) {
2854 menu_actions = [];
2855 }
2856 } catch(e) {
2857 return [];
2858 }
2859
2860 return menu_actions;
2861 }
2862
2863 /**
2864 * Builds an admin menu action object with a proper href property.
2865 *
2866 * @param object action the action to build.
2867 *
2868 * @return object
2869 */
2870 static buildAdminMenuAction(action) {
2871 if (typeof action !== 'object' || !action || !action.hasOwnProperty('name')) {
2872 throw new Error('Invalid action object');
2873 }
2874
2875 var action_base = action.hasOwnProperty('href') && typeof action['href'] == 'string' ? action['href'] : window.location.href;
2876 var action_url;
2877
2878 if (action_base.toLowerCase().indexOf('javascript') === 0 && action.hasOwnProperty('widget')) {
2879 // no need to prepare the action URL ("JavaScript: void(0)") as the link will use a JS callback
2880 return action;
2881 }
2882
2883 if (action_base.indexOf('http') !== 0) {
2884 // relative URL
2885 action_url = new URL(action_base, window.location.href);
2886 } else {
2887 // absolute URL
2888 action_url = new URL(action_base);
2889 }
2890
2891 // build proper href with a relative URL
2892 action['href'] = action_url.pathname + action_url.search;
2893
2894 return action;
2895 }
2896
2897 /**
2898 * Registers an admin menu action object.
2899 *
2900 * @param object action the action to build.
2901 * @param string scope the admin menu scope.
2902 *
2903 * @return boolean
2904 */
2905 static registerAdminMenuAction(action, scope) {
2906 // build menu action object
2907 let menu_action_entry = VBOCore.buildAdminMenuAction(action);
2908
2909 let menu_actions = VBOCore.getAdminMenuActions(scope);
2910
2911 // make sure we are not pushing a duplicate and count pinned actions
2912 let pinned_actions = 0;
2913 let unpinned_index = [];
2914 for (let i = 0; i < menu_actions.length; i++) {
2915 // avoid duplicate entries
2916 if (!menu_action_entry.hasOwnProperty('widget') && menu_actions[i]['href'] == menu_action_entry['href']) {
2917 // duplicate link
2918 return false;
2919 }
2920 if (menu_action_entry.hasOwnProperty('widget') && menu_actions[i].hasOwnProperty('widget') && menu_actions[i]['widget'] == menu_action_entry['widget']) {
2921 // duplicate widget
2922 return false;
2923 }
2924 if (menu_actions[i].hasOwnProperty('pinned') && menu_actions[i]['pinned']) {
2925 pinned_actions++;
2926 } else {
2927 unpinned_index.push(i);
2928 }
2929 }
2930
2931 if (pinned_actions >= VBOCore.options.admin_menu_maxactions) {
2932 // no more space to register a new menu action for this admin menu
2933 return false;
2934 }
2935
2936 // splice or pop before prepending to keep current indexes
2937 let tot_menu_actions = menu_actions.length;
2938 if (++tot_menu_actions > VBOCore.options.admin_menu_maxactions) {
2939 if (unpinned_index.length) {
2940 menu_actions.splice(unpinned_index[unpinned_index.length - 1], 1);
2941 } else {
2942 menu_actions.pop();
2943 }
2944 }
2945
2946 // prepend new admin menu action
2947 menu_actions.unshift(menu_action_entry);
2948
2949 return VBOCore.storageSetItem(VBOCore.getStorageScopeName(scope), menu_actions);
2950 }
2951
2952 /**
2953 * Updates an existing admin menu action object.
2954 *
2955 * @param object action the action to build.
2956 * @param string scope the admin menu scope.
2957 * @param number index optional menu action index.
2958 *
2959 * @return boolean
2960 */
2961 static updateAdminMenuAction(action, scope, index) {
2962 // build menu action object
2963 let menu_action_entry = VBOCore.buildAdminMenuAction(action);
2964
2965 let menu_actions = VBOCore.getAdminMenuActions(scope);
2966
2967 if (!menu_actions.length) {
2968 return false;
2969 }
2970
2971 if (typeof index === 'undefined') {
2972 // find the proper index to update by href
2973 for (let i = 0; i < menu_actions.length; i++) {
2974 if (menu_actions[i].hasOwnProperty('widget') && menu_action_entry.hasOwnProperty('widget') && menu_actions[i]['widget'] == menu_action_entry['widget']) {
2975 // existing entry for widget found
2976 index = i;
2977 break;
2978 } else if (!menu_action_entry.hasOwnProperty('widget') && menu_actions[i]['href'] == menu_action_entry['href']) {
2979 // existing entry for link found
2980 index = i;
2981 break;
2982 }
2983 }
2984 }
2985
2986 if (isNaN(index) || !(index in menu_actions)) {
2987 // menu entry index not found
2988 return false;
2989 }
2990
2991 menu_actions[index] = menu_action_entry;
2992
2993 return VBOCore.storageSetItem(VBOCore.getStorageScopeName(scope), menu_actions);
2994 }
2995
2996 /**
2997 * Checks if the current menu actions should be filled with some other actions.
2998 * No update is made over the Local Storage through this method.
2999 *
3000 * @param array menu_actions menu actions to fill.
3001 * @param array widgets_actions default actions to use for filling.
3002 * @param string scope the admin menu scope.
3003 *
3004 * @return array the menu actions eventually filled.
3005 */
3006 static fillAdminMenuActions(menu_actions, widgets_actions, scope) {
3007 if (!Array.isArray(menu_actions) || !Array.isArray(widgets_actions)) {
3008 return [];
3009 }
3010
3011 if (!menu_actions.length || !widgets_actions.length || menu_actions.length >= VBOCore.options.admin_menu_maxactions) {
3012 return menu_actions;
3013 }
3014
3015 var current_widgets = [];
3016 menu_actions.forEach((action, index) => {
3017 current_widgets.push(action['widget']);
3018 });
3019
3020 for (var i = 0; i < widgets_actions.length; i++) {
3021 if (menu_actions.length >= VBOCore.options.admin_menu_maxactions) {
3022 // abort for filling completed
3023 break;
3024 }
3025
3026 if (!widgets_actions[i].hasOwnProperty('widget')) {
3027 // action must be for a widget
3028 continue;
3029 }
3030
3031 if (current_widgets.includes(widgets_actions[i]['widget'])) {
3032 // there cannot be duplicate entries
3033 continue;
3034 }
3035
3036 // fill menu action
3037 menu_actions.push(widgets_actions[i]);
3038 }
3039
3040 return menu_actions;
3041 }
3042 }
3043
3044 /**
3045 * These used to be private static properties (static #options),
3046 * but they are only supported by quite recent browsers (especially Safari).
3047 * It's too premature, so we decided to keep the class properties public
3048 * without declaring them as static inside the class declaration.
3049 *
3050 * @var object
3051 */
3052 VBOCore.options = {
3053 is_vbo: false,
3054 is_vcm: false,
3055 cms: 'wordpress',
3056 widget_ajax_uri: null,
3057 assets_ajax_uri: null,
3058 multitask_ajax_uri: null,
3059 watchdata_ajax_uri: null,
3060 current_page: null,
3061 current_page_uri: null,
3062 root_uri: '/',
3063 client: 'admin',
3064 panel_opts: {},
3065 admin_widgets: [],
3066 notif_audio_url: '',
3067 active_listeners: {},
3068 tn_texts: {
3069 notifs_enabled: 'Browser notifications are enabled!',
3070 notifs_disabled: 'Browser notifications are disabled',
3071 notifs_disabled_help: "Could not enable browser notifications.\nThis feature is available only in secure contexts (HTTPS).",
3072 admin_widget: 'Admin widget',
3073 congrats: 'Congratulations!',
3074 },
3075 default_loading_body: '....',
3076 multitask_save_event: 'vbo-admin-multitask-save',
3077 multitask_open_event: 'vbo-admin-multitask-open',
3078 multitask_close_event: 'vbo-admin-multitask-close',
3079 multitask_shortcut_ev: 'vbo_multitask_shortcut',
3080 multitask_searchfs_ev: 'vbo_multitask_search_focus',
3081 widget_modal_rendered: 'vbo-admin-widget-modal-rendered',
3082 widget_modal_dismissed: 'vbo-widget-modal-dismissed',
3083 admin_menu_maxactions: 3,
3084 admin_menu_actions_nm: 'vikbooking.admin_menu.actions',
3085 service_worker_path: '',
3086 service_worker_scope: './',
3087 push: {
3088 application_key: '',
3089 ajax_url: '',
3090 },
3091 push_options: {
3092 storage_endp_id: 'vbo_push_subscr_endp',
3093 allowed_notif_types: [
3094 'Book',
3095 'Modify',
3096 'Cancel',
3097 'Request',
3098 'CancelRequest',
3099 'Inquiry',
3100 'CancelInquiry',
3101 'Chat',
3102 'Info',
3103 ],
3104 },
3105 };
3106
3107 /**
3108 * @var bool
3109 */
3110 VBOCore.side_panel_on = false;
3111
3112 /**
3113 * @var bool
3114 * @since 1.6.8
3115 */
3116 VBOCore.throttle_timer = false;
3117
3118 /**
3119 * @var array
3120 */
3121 VBOCore.notifications = [];
3122
3123 /**
3124 * @var object
3125 */
3126 VBOCore.widgets_watch_data = null;
3127
3128 /**
3129 * @var number
3130 */
3131 VBOCore.watch_data_interval = null;
3132
3133 /**
3134 * @var object
3135 * @since 1.6.3
3136 */
3137 VBOCore.broadcast_watch_data = null;
3138
3139 /**
3140 * @var object
3141 * @since 1.6.5
3142 */
3143 VBOCore.broadcast_push_data = null;
3144
3145 /**
3146 * @var object
3147 * @since 1.6.8
3148 */
3149 VBOCore.broadcast_watch_events = null;
3150
3151 /**
3152 * @var array<object>
3153 * @since 1.6.5
3154 */
3155 VBOCore.widgets_pushed_data = [];
3156
3157 /**
3158 * Checks if the KeyBoard event matches the given shortcut.
3159 *
3160 * @param array keys The shortcut representation.
3161 *
3162 * @return boolean True if matches, otherwise false.
3163 *
3164 * @since 1.7.0
3165 */
3166 KeyboardEvent.prototype.shortcut = function(keys) {
3167 // get modifiers list
3168 var modifiers = keys.slice(0);
3169 // pop character from modifiers
3170 var keyCode = modifiers.pop();
3171
3172 if (typeof keyCode === 'string') {
3173 // get ASCII
3174 keyCode = keyCode.toUpperCase().charCodeAt(0);
3175 }
3176
3177 // make sure the modifiers are lower case
3178 modifiers = modifiers.map(function(mod) {
3179 return mod.toLowerCase();
3180 });
3181
3182 var ok = false;
3183
3184 // validate key code
3185 if (this.keyCode == keyCode) {
3186 // validate modifiers
3187 ok = true;
3188 var lookup = ['meta', 'shift', 'alt', 'ctrl'];
3189
3190 for (var i = 0; i < lookup.length && ok; i++) {
3191 // check if modifiers is pressed
3192 var mod = this[lookup[i] + 'Key'];
3193
3194 if (mod) {
3195 // if pressed, the shortcut must specify it
3196 ok &= modifiers.indexOf(lookup[i]) !== -1;
3197 } else {
3198 // if not pressed, the shortcut must not include it
3199 ok &= modifiers.indexOf(lookup[i]) === -1;
3200 }
3201 }
3202 }
3203
3204 return ok;
3205 }
3206
3207 })(jQuery, window);