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

4,668 lines 138.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * VikBooking Core v1.8.8
3 * Copyright (C) 2026 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 /**
11 * AJAX loading of Views may reload the already existing options, so we cache them when available.
12 */
13 var CORE_OPTIONS = typeof VBOCore !== 'undefined' ? VBOCore?.options : null;
14
15 /**
16 * VBOCore class implementation.
17 */
18 w['VBOCore'] = class VBOCore {
19
20 /**
21 * Proxy to support static injection of params.
22 */
23 constructor(params) {
24 VBOCore.setOptions(params);
25 }
26
27 /**
28 * Inject options by overriding default properties.
29 *
30 * @param object params
31 *
32 * @return self
33 */
34 static setOptions(params) {
35 if (params != null && typeof params === 'object') {
36 VBOCore.options = Object.assign(VBOCore.options, params);
37 }
38
39 return VBOCore;
40 }
41
42 /**
43 * Fires a given function when the DOM content has loaded.
44 *
45 * @param function fireFn The function to fire when the document is ready.
46 *
47 * @return undefined
48 */
49 static DOMLoaded(fireFn) {
50 if (typeof fireFn !== 'function') {
51 throw new Error('Invalid argument provided');
52 }
53
54 if (document.readyState === 'loading') {
55 // register event because DOMContentLoaded hasn't finished yet
56 document.addEventListener('DOMContentLoaded', fireFn);
57 } else {
58 // DOMContentLoaded event has fired already
59 fireFn();
60 }
61 }
62
63 /**
64 * Returns a promise that resolves when the specified instance gets defined.
65 *
66 * @param function check The callback to invoke to check whether the instance is ready.
67 * @param mixed threshold An optional threshold to estabilish the max number of attempts.
68 *
69 * @return Promise
70 */
71 static onInstanceReady(check, threshold) {
72 return new Promise((resolve, reject) => {
73 // prepare safe counter
74 let count = 0;
75
76 const callback = function() {
77 // increase counter
78 count++;
79
80 let instance;
81
82 try {
83 // check whether the instance is ready
84 instance = check();
85 } catch (err) {
86 // an error has occurred, manually abort
87 reject();
88 return;
89 }
90
91 if (instance) {
92 // object is now ready
93 resolve(instance);
94 } else {
95 if (!threshold || count < Math.abs(threshold)) {
96 // check again
97 setTimeout(callback, 32 + Math.floor(Math.random() * 128));
98 } else {
99 // instance not ready
100 reject();
101 }
102 }
103 };
104
105 // check
106 callback();
107 });
108 }
109
110 /**
111 * Getter for admin_widgets private options property.
112 *
113 * @return array
114 */
115 static get admin_widgets() {
116 return VBOCore.options.admin_widgets;
117 }
118
119 /**
120 * Getter for multitask open event private property.
121 *
122 * @return string
123 */
124 static get multitask_open_event() {
125 return VBOCore.options.multitask_open_event;
126 }
127
128 /**
129 * Getter for multitask close event private property.
130 *
131 * @return string
132 */
133 static get multitask_close_event() {
134 return VBOCore.options.multitask_close_event;
135 }
136
137 /**
138 * Getter for multitask shortcut event private property.
139 *
140 * @return string
141 */
142 static get multitask_shortcut_event() {
143 return VBOCore.options.multitask_shortcut_ev;
144 }
145
146 /**
147 * Getter for multitask seach focus shortcut event private property.
148 *
149 * @return string
150 */
151 static get multitask_searchfs_event() {
152 return VBOCore.options.multitask_searchfs_ev;
153 }
154
155 /**
156 * Getter for multitask event widget modal rendered.
157 *
158 * @return string
159 */
160 static get widget_modal_rendered() {
161 return VBOCore.options.widget_modal_rendered;
162 }
163
164 /**
165 * Getter for widget modal dismiss event.
166 *
167 * @return string
168 */
169 static get widget_modal_dismissed() {
170 return VBOCore.options.widget_modal_dismissed;
171 }
172
173 /**
174 * Getter for the service worker file path.
175 *
176 * @return string
177 */
178 static get service_worker_path() {
179 return VBOCore.options.service_worker_path;
180 }
181
182 /**
183 * Getter for the service worker scope.
184 *
185 * @return string
186 */
187 static get service_worker_scope() {
188 return VBOCore.options.service_worker_scope;
189 }
190
191 /**
192 * Getter for the push application key.
193 *
194 * @return string
195 */
196 static get push_application_key() {
197 return VBOCore.options.push.application_key;
198 }
199
200 /**
201 * Getter for the push storage endpoint identifier.
202 *
203 * @return string
204 */
205 static get push_storage_endpoint_id() {
206 return VBOCore.options.push_options.storage_endp_id;
207 }
208
209 /**
210 * Parses an AJAX response error object.
211 *
212 * @param object err
213 *
214 * @return bool
215 */
216 static isConnectionLostError(err) {
217 if (!err || !err.hasOwnProperty('status')) {
218 return false;
219 }
220
221 return (
222 err.statusText == 'error'
223 && err.status == 0
224 && (err.readyState == 0 || err.readyState == 4)
225 && (!err.hasOwnProperty('responseText') || err.responseText == '')
226 );
227 }
228
229 /**
230 * Ensures AJAX requests that fail due to connection errors are retried automatically.
231 *
232 * @param string url
233 * @param object data
234 * @param function success
235 * @param function failure
236 * @param number attempt
237 */
238 static doAjax(url, data, success, failure, attempt) {
239 const AJAX_MAX_ATTEMPTS = 3;
240
241 if (attempt === undefined) {
242 attempt = 1;
243 }
244
245 let requestOptions = {
246 type: 'POST',
247 url: url,
248 data: data,
249 };
250
251 if (data instanceof FormData) {
252 // ensure the FormData object is not manipulated
253 requestOptions.processData = false;
254 requestOptions.contentType = false;
255 }
256
257 return $.ajax(
258 requestOptions
259 ).done(function(resp) {
260 if (success !== undefined) {
261 // launch success callback function
262 success(resp);
263 }
264 }).fail(function(err) {
265 /**
266 * If the error is caused by a site connection lost, and if the number
267 * of retries is lower than max attempts, retry the same AJAX request.
268 */
269 if (attempt < AJAX_MAX_ATTEMPTS && VBOCore.isConnectionLostError(err)) {
270 // delay the retry by half second
271 setTimeout(function() {
272 // re-launch same request and increase number of attempts
273 console.log('Retrying previous AJAX request');
274 VBOCore.doAjax(url, data, success, failure, (attempt + 1));
275 }, 500);
276 } else {
277 // launch the failure callback otherwise
278 if (failure !== undefined) {
279 if (err.responseText === 'false') {
280 // make the property empty to rely on others
281 err.responseText = '';
282 }
283 failure(err);
284 }
285 }
286
287 if (!err.status || err.status == 500) {
288 // log the error in console
289 console.error('AJAX request failed' + (err.status == 500 ? ' (' + err.responseText + ')' : ''), err);
290 }
291 });
292 }
293
294 /**
295 * Matches a keyword against a text.
296 *
297 * @param string search the keyword to search.
298 * @param string text the text to compare.
299 *
300 * @return bool
301 */
302 static matchString(search, text) {
303 return ((text + '').indexOf(search) >= 0);
304 }
305
306 /**
307 * Converts a base64 URL for a server key into an array of 8-bit unsigned integers.
308 *
309 * @param string key the base64 encoded URL key.
310 *
311 * @return Uint8Array
312 */
313 static base64ToUint8Array(key) {
314 const padding = ('=').repeat((4 - (key.length % 4)) % 4);
315 const base64 = (key + padding).replace(/\-/g, '+').replace(/_/g, '/');
316
317 const rawData = window.atob(base64);
318 const uint8_arr = new Uint8Array(rawData.length);
319
320 for (let i = 0; i < rawData.length; ++i) {
321 uint8_arr[i] = rawData.charCodeAt(i);
322 }
323
324 return uint8_arr;
325 }
326
327 /**
328 * Builds an object containing the Push subscription details and data.
329 *
330 * @param PushSubscription pushSubscription the registration data.
331 * @param object data the extra data to merge.
332 *
333 * @return object
334 */
335 static buildPushSubscriptionData(pushSubscription, data) {
336 if (!(pushSubscription instanceof PushSubscription)) {
337 return data;
338 }
339
340 let key = pushSubscription.getKey('p256dh');
341 let token = pushSubscription.getKey('auth');
342 let contentEncoding = (PushManager.supportedContentEncodings || ['aesgcm'])[0];
343
344 let details = {
345 endpoint: pushSubscription.endpoint,
346 publicKey: key ? btoa(String.fromCharCode.apply(null, new Uint8Array(key))) : null,
347 authToken: token ? btoa(String.fromCharCode.apply(null, new Uint8Array(token))) : null,
348 encoding: contentEncoding,
349 };
350
351 return Object.assign(details, data);
352 }
353
354 /**
355 * Attempts to install (and eventually update) the VikBooking's Service Worker.
356 *
357 * @param bool update if defined, it will update the Service Worker Registration.
358 *
359 * @return Promise
360 */
361 static installServiceWorker(update) {
362 // check if support for Web App badge is available
363 if (typeof navigator.setAppBadge !== 'undefined') {
364 // always clear the app badge
365 navigator.clearAppBadge();
366 }
367
368 return new Promise((resolve, reject) => {
369 if (!('serviceWorker' in navigator)) {
370 reject('Service workers are not supported by this browser');
371 return;
372 }
373
374 if (!('PushManager' in window)) {
375 reject('Push notifications are not supported by this browser');
376 return;
377 }
378
379 if (!VBOCore.service_worker_path) {
380 reject('Service Worker path not configured');
381 return;
382 }
383
384 if (!VBOCore.notificationsEnabled()) {
385 reject('Notifications are disabled, and so the service worker will not be installed');
386 return;
387 }
388
389 navigator.serviceWorker.register(VBOCore.service_worker_path, {scope: VBOCore.service_worker_scope}).then(
390 (registration) => {
391 if (typeof update !== 'undefined') {
392 // attempt to update the registration for un-caching purposes
393 registration.update().then((updated) => {
394 // all good
395 }).catch((e) => {
396 // updating should never fail after registering with success
397 console.error(`Service worker registration update failed: ${e}`);
398 });
399 }
400
401 // listen to the ServiceWorker messages for the client
402 VBOCore.listenServiceWorkerMessages();
403
404 // resolve the Promise
405 resolve(registration);
406 },
407 (e) => {
408 console.error(`Service worker registration failed: ${e}`);
409 reject('Service worker registration failed');
410 }
411 );
412 });
413 }
414
415 /**
416 * Handles the Push subscription state. To be called after the
417 * Service Worker installation (registration) is ready.
418 *
419 * @param ServiceWorkerRegistration registration sw registration object.
420 *
421 * @return Promise
422 */
423 static handlePushSubscription(registration) {
424 return new Promise((resolve, reject) => {
425 if (!(registration instanceof ServiceWorkerRegistration)) {
426 reject('Missing service worker registration');
427 return;
428 }
429
430 if (!VBOCore.push_application_key) {
431 reject('Missing application key for Push because not supported');
432 return;
433 }
434
435 // check if a push subscription is available
436 registration.pushManager.getSubscription().then((pushSubscription) => {
437 if (pushSubscription) {
438 // we are subscribed to Push
439 let previous_endpoint = VBOCore.storageGetItem(VBOCore.push_storage_endpoint_id);
440 if (previous_endpoint && pushSubscription.endpoint != previous_endpoint) {
441 // update subscription data
442 VBOCore.doAjax(
443 VBOCore.options.push.ajax_url,
444 VBOCore.buildPushSubscriptionData(pushSubscription, {
445 agent: window.navigator.userAgent,
446 type: 'update',
447 })
448 );
449 }
450
451 // resolve the promise with the Push subscription data
452 resolve(pushSubscription);
453 return;
454 }
455
456 // push subscription options
457 const pushOptions = {
458 userVisibleOnly: true,
459 applicationServerKey: VBOCore.base64ToUint8Array(VBOCore.push_application_key),
460 };
461
462 // subscribe to Push
463 registration.pushManager.subscribe(pushOptions).then(
464 (pushSubscription) => {
465 // store subscription data
466 VBOCore.storageSetItem(VBOCore.push_storage_endpoint_id, pushSubscription.endpoint);
467 VBOCore.doAjax(
468 VBOCore.options.push.ajax_url,
469 VBOCore.buildPushSubscriptionData(pushSubscription, {
470 agent: window.navigator.userAgent,
471 type: 'new',
472 })
473 );
474
475 // resolve the promise with the Push subscription data
476 resolve(pushSubscription);
477 },
478 (e) => {
479 console.error(`Error handling the Push subscription state: ${e}`);
480 reject('Error handling the Push subscription state');
481 }
482 );
483 })
484 .catch((e) => {
485 console.error(`Error getting the Push subscription state: ${e}`);
486 reject('Error getting the Push subscription state');
487 });
488 });
489 }
490
491 /**
492 * Starts listening to the "message" events posted by the ServiceWorker.
493 *
494 * @return void
495 */
496 static listenServiceWorkerMessages() {
497 if (!('serviceWorker' in navigator)) {
498 return;
499 }
500
501 try {
502 // add listener to the message event for the data posted by the ServiceWorker to the client
503 navigator.serviceWorker.addEventListener('message', (event) => {
504 VBOCore.handleServiceWorkerMessage(event.data);
505 });
506 } catch(e) {
507 // do nothing
508 }
509 }
510
511 /**
512 * Handles a message data posted by the ServiceWorker upon clicking a Push notification.
513 *
514 * @param object data the event.notification.data object posted by the ServiceWorker.
515 *
516 * @return void
517 */
518 static handleServiceWorkerMessage(data) {
519 let notif_type = data.type || '';
520
521 if (!notif_type || !VBOCore.options.push_options.allowed_notif_types.includes(notif_type)) {
522 console.error(data);
523 throw new Error('Unsupported message type');
524 }
525
526 // determine the proper widget for dispatching the message data
527 let widget_id = 'booking_details';
528 if (notif_type == 'Chat') {
529 widget_id = 'guest_messages';
530 }
531
532 // the raw notification content
533 let raw_content = data.content || {};
534
535 // multitask data options
536 let options = Object.assign({
537 _push: 1,
538 title: data.title || '',
539 message: data.message || '',
540 }, raw_content);
541
542 // dispatch (Push) message data on widget
543 VBOCore.handleDisplayWidgetNotification({widget_id: widget_id}, options, true);
544
545 // update pushed data map for the next watching event in the current browsing context
546 VBOCore.widgets_pushed_data.push(raw_content);
547
548 // post message onto broadcast channel for any other browsing context
549 if (VBOCore.broadcast_push_data) {
550 // the next watch-data interval will receive the pushed data information
551 VBOCore.broadcast_push_data.postMessage(raw_content);
552 }
553
554 // let the window reload the badge counter in case it must be updated
555 VBOCore.emitEvent('vbo-badge-count-reload');
556 }
557
558 /**
559 * Ensures VBO is ready to support VCM.
560 *
561 * @param any env optional data to evaluate.
562 *
563 * @return bool
564 */
565 static vcmMultitasking(env) {
566 // tell VCM that VBOCore supports it
567 return true;
568 }
569
570 /**
571 * Registers an event identifier as delegated.
572 * Useful to flag an event as registered.
573 *
574 * @param string ev The event name.
575 *
576 * @return Array
577 */
578 static setEventDelegated(ev) {
579 VBOCore.options.events_delegated.push(ev);
580 return VBOCore.options.events_delegated;
581 }
582
583 /**
584 * Un-registers an event identifier as delegated.
585 *
586 * @param string ev The event name.
587 *
588 * @return Array
589 */
590 static unsetEventDelegated(ev) {
591 let index = VBOCore.options.events_delegated.indexOf(ev);
592 if (index >= 0) {
593 VBOCore.options.events_delegated.splice(index, 1);
594 }
595 return VBOCore.options.events_delegated;
596 }
597
598 /**
599 * Tells if an event identifier was delegated.
600 *
601 * @param string ev The event name.
602 *
603 * @return bool
604 */
605 static wasEventDelegated(ev) {
606 return VBOCore.options.events_delegated.includes(ev);
607 }
608
609 /**
610 * Initializes the multitasking panel for the admin widgets.
611 *
612 * @param object params the panel object params.
613 *
614 * @return bool
615 */
616 static prepareMultitasking(params) {
617 var panel_opts = {
618 selector: "",
619 sclass_l_small: "vbo-sidepanel-right",
620 sclass_l_large: "vbo-sidepanel-large",
621 btn_trigger: "",
622 search_selector: "#vbo-sidepanel-search-input",
623 search_nores: ".vbo-sidepanel-add-widgets-nores",
624 close_selector: ".vbo-sidepanel-dismiss-btn",
625 t_layout_small: ".vbo-sidepanel-layout-small",
626 t_layout_large: ".vbo-sidepanel-layout-large",
627 wclass_base_sel: ".vbo-admin-widgets-widget-output",
628 wclass_l_small: "vbo-admin-widgets-container-small",
629 wclass_l_large: "vbo-admin-widgets-container-large",
630 addws_selector: ".vbo-sidepanel-add-widgets",
631 addw_selector: ".vbo-sidepanel-add-widget",
632 addw_modal_cls: "vbo-widget-render-modal",
633 addw_def_cls: "vbo-widget-render-regular",
634 addwfs_selector: ".vbo-sidepanel-add-widget-focussed",
635 wtags_selector: ".vbo-sidepanel-widget-tags",
636 wname_selector: ".vbo-sidepanel-widget-name",
637 addw_data_attr: "data-vbowidgetid",
638 actws_selector: ".vbo-sidepanel-active-widgets",
639 editw_selector: ".vbo-sidepanel-edit-widgets-trig",
640 shortc_selector: ".vbo-sidepanel-shortcut",
641 rmwidget_class: "vbo-admin-widgets-widget-remove",
642 rmwidget_icn: "",
643 dtcwidget_class: "vbo-admin-widgets-widget-detach",
644 dtctarget_class: "vbo-admin-widget-head",
645 dtcwidget_icn: "",
646 notif_selector: ".vbo-sidepanel-notifications-btn",
647 notif_on_class: "vbo-sidepanel-notifications-on",
648 notif_off_class: "vbo-sidepanel-notifications-off",
649 open_class: "vbo-sidepanel-open",
650 close_class: "vbo-sidepanel-close",
651 cur_widget_cls: "vbo-admin-widgets-container-small",
652 sortable: true,
653 sort_save_ev: "vbo-admin-widgets-updateposmp",
654 sorting: null,
655 };
656
657 if (typeof params === 'object') {
658 panel_opts = Object.assign(panel_opts, params);
659 }
660
661 if (!panel_opts.btn_trigger || !panel_opts.selector) {
662 console.error('Got no trigger or selector');
663 return false;
664 }
665
666 // push panel options
667 VBOCore.setOptions({
668 panel_opts: panel_opts,
669 });
670
671 if (VBOCore.options.is_vbo) {
672 // setup browser notifications
673 VBOCore.setupNotifications();
674 }
675
676 // count active widgets on current page
677 var tot_active_widgets = VBOCore.options.admin_widgets.length;
678 if (tot_active_widgets > 0) {
679 // hide add-widgets container
680 $(panel_opts.addws_selector).hide();
681
682 // register listener for input search blur
683 VBOCore.registerSearchWidgetsBlur();
684 }
685
686 // register click event on trigger button
687 $(VBOCore.options.panel_opts.btn_trigger).on('click', function() {
688 var side_panel = $(VBOCore.options.panel_opts.selector);
689 if (side_panel.hasClass(VBOCore.options.panel_opts.open_class)) {
690 // hide panel
691 VBOCore.side_panel_on = false;
692 VBOCore.emitMultitaskEvent(VBOCore.multitask_close_event);
693 side_panel.addClass(VBOCore.options.panel_opts.close_class).removeClass(VBOCore.options.panel_opts.open_class);
694 // always hide add-widgets container
695 $(VBOCore.options.panel_opts.addws_selector).hide();
696 // check if we are currently editing
697 var is_editing = ($('.' + VBOCore.options.panel_opts.editmode_class).length > 0);
698 if (is_editing) {
699 // deactivate editing mode
700 VBOCore.toggleWidgetsPanelEditing(null);
701 }
702 } else {
703 // show panel
704 VBOCore.side_panel_on = true;
705 VBOCore.emitMultitaskEvent(VBOCore.multitask_open_event);
706 side_panel.addClass(VBOCore.options.panel_opts.open_class).removeClass(VBOCore.options.panel_opts.close_class);
707 if (!VBOCore.options.admin_widgets.length) {
708 // set focus on search widgets input with delay for the opening animation
709 setTimeout(function() {
710 $(VBOCore.options.panel_opts.search_selector).focus();
711 }, 300);
712 }
713 }
714 });
715
716 // register close/dismiss button
717 $(VBOCore.options.panel_opts.close_selector).on('click', function() {
718 $(VBOCore.options.panel_opts.btn_trigger).trigger('click');
719 });
720
721 if (VBOCore.options.is_vbo) {
722 // register toggle layout buttons
723 $(VBOCore.options.panel_opts.t_layout_large).on('click', function() {
724 // large layout
725 $(VBOCore.options.panel_opts.selector).addClass(VBOCore.options.panel_opts.sclass_l_large).removeClass(VBOCore.options.panel_opts.sclass_l_small);
726 $(VBOCore.options.panel_opts.wclass_base_sel).addClass(VBOCore.options.panel_opts.wclass_l_large).removeClass(VBOCore.options.panel_opts.wclass_l_small);
727 VBOCore.options.panel_opts.cur_widget_cls = VBOCore.options.panel_opts.sclass_l_large;
728 });
729 $(VBOCore.options.panel_opts.t_layout_small).on('click', function() {
730 // small layout
731 $(VBOCore.options.panel_opts.selector).addClass(VBOCore.options.panel_opts.sclass_l_small).removeClass(VBOCore.options.panel_opts.sclass_l_large);
732 $(VBOCore.options.panel_opts.wclass_base_sel).addClass(VBOCore.options.panel_opts.wclass_l_small).removeClass(VBOCore.options.panel_opts.wclass_l_large);
733 VBOCore.options.panel_opts.cur_widget_cls = VBOCore.options.panel_opts.sclass_l_small;
734 });
735 }
736
737 // register listener for esc key pressed
738 $(document).keyup(function(e) {
739 if (!VBOCore.side_panel_on) {
740 return;
741 }
742 if ((e.key && e.key === "Escape") || (e.keyCode && e.keyCode == 27)) {
743 $(VBOCore.options.panel_opts.btn_trigger).trigger('click');
744 }
745 });
746
747 // register listener for input search focus
748 $(VBOCore.options.panel_opts.search_selector).on('focus', function() {
749 // always show add-widgets container
750 var widget_focus_class = VBOCore.options.panel_opts.addwfs_selector.replace('.', '');
751 $(VBOCore.options.panel_opts.addw_selector).removeClass(widget_focus_class);
752 $(VBOCore.options.panel_opts.addws_selector).show();
753 });
754
755 // register listener on input search widget
756 $(VBOCore.options.panel_opts.search_selector).keyup(function(e) {
757 // get the keyword to look for
758 var keyword = $(this).val();
759 // counting matching widgets
760 var matching = 0;
761 var first_matched = null;
762 var widget_focus_class = VBOCore.options.panel_opts.addwfs_selector.replace('.', '');
763
764 // adjust widgets to be displayed
765 if (!keyword.length) {
766 // show all widgets for selection
767 $(VBOCore.options.panel_opts.addw_selector).show();
768 // hide "no results"
769 $(VBOCore.options.panel_opts.search_nores).hide();
770 // all widgets are matching
771 matching = $(VBOCore.options.panel_opts.addw_selector).length;
772 } else {
773 // make the keyword lowercase
774 keyword = (keyword + '').toLowerCase();
775 // parse all widget's description tags
776 $(VBOCore.options.panel_opts.addw_selector).each(function() {
777 var elem = $(this);
778 var descr = elem.find(VBOCore.options.panel_opts.wtags_selector).text();
779 if (VBOCore.matchString(keyword, descr)) {
780 elem.show();
781 matching++;
782 if (!first_matched) {
783 // store the first widget that matched
784 first_matched = elem.attr(VBOCore.options.panel_opts.addw_data_attr);
785 }
786 } else {
787 elem.hide();
788 }
789 });
790 // check how many widget matched
791 if (matching > 0) {
792 // hide "no results"
793 $(VBOCore.options.panel_opts.search_nores).hide();
794 } else {
795 // show "no results"
796 $(VBOCore.options.panel_opts.search_nores).show();
797 }
798 }
799
800 // check for shortcuts
801 if (!e.key) {
802 return;
803 }
804
805 // handle Enter key press to add a widget
806 if (e.key === "Enter") {
807 // on Enter key pressed, open/add the first matching widget or the focussed one
808 var load_wid_id = null;
809 var focussed_wid = $(VBOCore.options.panel_opts.addwfs_selector + ':visible').first();
810
811 if (focussed_wid.length) {
812 load_wid_id = focussed_wid.attr(VBOCore.options.panel_opts.addw_data_attr);
813 } else if (first_matched) {
814 load_wid_id = first_matched;
815 }
816
817 if (!load_wid_id) {
818 // no widget to render found
819 return;
820 }
821
822 if (e.shiftKey === true && !VBOCore.options.is_vcm) {
823 // widget multitask panel rendering
824 VBOCore.addWidgetToPanel(load_wid_id);
825 $(VBOCore.options.panel_opts.search_selector).trigger('blur');
826
827 return;
828 }
829
830 // widget modal rendering is the default method
831 if (VBOCore.options.is_vcm) {
832 // register loading effect with automatic cancellation
833 let orig_name = (focussed_wid.length ? focussed_wid : first_matched).find(VBOCore.options.panel_opts.wname_selector).text();
834 (focussed_wid.length ? focussed_wid : first_matched).find(VBOCore.options.panel_opts.wname_selector).html(orig_name + ' ' + VBOCore.options.default_loading_body);
835 setTimeout(() => {
836 (focussed_wid.length ? focussed_wid : first_matched).find(VBOCore.options.panel_opts.wname_selector).text(orig_name);
837 }, 1000);
838
839 // assets will be loaded within VCM before rendering the modal widget
840 VBOCore.handleDisplayWidgetNotification({widget_id: load_wid_id});
841 } else {
842 VBOCore.renderModalWidget(load_wid_id);
843 }
844
845 return;
846 }
847
848 // handle arrow keys selection
849 if (matching > 0 && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
850 // on arrow key pressed, select the next or prev widget
851 var addws_element = $(VBOCore.options.panel_opts.addws_selector);
852 var addws_cont_pos = addws_element.offset().top;
853 var addws_otheight = addws_element.outerHeight();
854 var addws_scrolltp = addws_element.scrollTop();
855
856 if (e.key === 'ArrowDown') {
857 var default_widg = $(VBOCore.options.panel_opts.addw_selector + ':visible').first();
858 } else {
859 var default_widg = $(VBOCore.options.panel_opts.addw_selector + ':visible').last();
860 }
861 var focussed_wid = $(VBOCore.options.panel_opts.addwfs_selector + ':visible').first();
862 var addw_height = default_widg.outerHeight();
863 var focussed_pos = default_widg.offset().top + addw_height;
864
865 if (focussed_wid.length) {
866 focussed_wid.removeClass(widget_focus_class);
867 if (e.key === 'ArrowDown') {
868 var goto_wid = focussed_wid.nextAll(VBOCore.options.panel_opts.addw_selector + ':visible').first();
869 } else {
870 var goto_wid = focussed_wid.prevAll(VBOCore.options.panel_opts.addw_selector + ':visible').first();
871 }
872 if (goto_wid.length) {
873 goto_wid.addClass(widget_focus_class);
874 focussed_pos = goto_wid.offset().top + addw_height;
875 } else {
876 default_widg.addClass(widget_focus_class);
877 }
878 } else {
879 default_widg.addClass(widget_focus_class);
880 }
881
882 if (focussed_pos > (addws_cont_pos + addws_otheight)) {
883 addws_element.scrollTop(focussed_pos - addws_cont_pos - addw_height + addws_scrolltp);
884 } else if (focussed_pos < 0) {
885 addws_element.scrollTop(0);
886 }
887 }
888 });
889
890 // register listener for adding widgets
891 $(VBOCore.options.panel_opts.addw_selector).on('click', function(e) {
892 var widget_id = $(this).attr(VBOCore.options.panel_opts.addw_data_attr);
893 if (!widget_id || !widget_id.length) {
894 return false;
895 }
896
897 if (VBOCore.options.panel_opts.sorting) {
898 // prevent dropped widgets after sorting to be added to the panel
899 return false;
900 }
901
902 // determine widget rendering method
903 if (e && e.target) {
904 let cktarget = $(e.target);
905 let is_main_elem_clicked = cktarget.length && (cktarget.hasClass('vbo-sidepanel-widget-info-det') || cktarget.hasClass('vbo-sidepanel-widget-name'));
906 if (e.shiftKey === true || VBOCore.options.is_vcm || is_main_elem_clicked || (cktarget.hasClass(VBOCore.options.panel_opts.addw_modal_cls) || cktarget.parent().hasClass(VBOCore.options.panel_opts.addw_modal_cls))) {
907 // widget modal rendering
908 if (VBOCore.options.is_vcm) {
909 // register loading effect with automatic cancellation
910 let orig_name = $(this).find(VBOCore.options.panel_opts.wname_selector).text();
911 $(this).find(VBOCore.options.panel_opts.wname_selector).html(orig_name + ' ' + VBOCore.options.default_loading_body);
912 setTimeout(() => {
913 $(this).find(VBOCore.options.panel_opts.wname_selector).text(orig_name);
914 }, 1000);
915
916 // assets will be loaded within VCM before rendering the modal widget
917 VBOCore.handleDisplayWidgetNotification({widget_id: widget_id});
918 } else {
919 VBOCore.renderModalWidget(widget_id);
920 }
921
922 return;
923 }
924 }
925
926 // widget multitask panel rendering
927 VBOCore.addWidgetToPanel(widget_id);
928 });
929
930 if (VBOCore.options.is_vbo) {
931 // register listener for updating multitask sidepanel with debounce
932 document.addEventListener(VBOCore.options.multitask_save_event, VBOCore.debounceEvent(VBOCore.saveMultitasking, 1000));
933 }
934
935 // subscribe to event for multitask shortcut
936 document.addEventListener(VBOCore.multitask_shortcut_event, function() {
937 // toggle multitask panel
938 $(VBOCore.options.panel_opts.btn_trigger).trigger('click');
939 });
940
941 // subscribe to event for multitask search focus shortcut
942 document.addEventListener(VBOCore.multitask_searchfs_event, function() {
943 // focus search multitask widgets
944 $(VBOCore.options.panel_opts.search_selector).trigger('focus');
945 });
946
947 if (VBOCore.options.is_vbo) {
948 // register click event on edit widgets button
949 $(VBOCore.options.panel_opts.editw_selector).on('click', function() {
950 VBOCore.toggleWidgetsPanelEditing(null);
951 });
952
953 // register detach widget buttons
954 $('.' + VBOCore.options.panel_opts.dtcwidget_class).each(function() {
955 let widget_wrapper = $(this).parent(VBOCore.options.panel_opts.wclass_base_sel);
956 let detach_widget_id = widget_wrapper.attr(VBOCore.options.panel_opts.addw_data_attr);
957 let detach_to_target = widget_wrapper.find('.' + VBOCore.options.panel_opts.dtctarget_class);
958 if (!detach_to_target.length) {
959 detach_to_target = widget_wrapper;
960 }
961 // move detach wrapper onto the target (widget head)
962 $(this).prependTo(detach_to_target);
963
964 if (!detach_widget_id) {
965 return;
966 }
967
968 // register detach action
969 $(this).on('click', function() {
970 // detach widget, meaning do a modal rendering
971 VBOCore.renderModalWidget(detach_widget_id);
972 });
973 });
974 }
975
976 if (VBOCore.options.panel_opts.sortable && typeof $.fn.sortable !== 'undefined') {
977 // make the admin widgets sortable
978 let sortable_env = VBOCore.options.is_vcm ? 'vcm-sidepanel-add-widgets' : 'vbo-sidepanel-add-widgets';
979 let handle_env = VBOCore.options.is_vcm ? 'vcm-sidepanel-widget-info-det' : 'vbo-sidepanel-widget-info-det';
980 let item_env = VBOCore.options.is_vcm ? 'vcm-sidepanel-add-widget' : 'vbo-sidepanel-add-widget';
981
982 // default sorting flag
983 VBOCore.options.panel_opts.sorting = null;
984
985 // register listener for updating the widget sorting values on the multitask sidepanel with debounce
986 document.addEventListener(VBOCore.options.panel_opts.sort_save_ev, VBOCore.debounceEvent(VBOCore.saveMultitaskingSorting, 500));
987
988 // apply sorting capabilities
989 $('.' + sortable_env).sortable({
990 axix: 'x',
991 cursor: 'move',
992 handle: '.' + handle_env,
993 items: '.' + item_env,
994 containment: 'parent',
995 revert: false,
996 start: function(event, ui) {
997 // update flag
998 VBOCore.options.panel_opts.sorting = $(ui.item).attr('data-vbowidgetid');
999 // set is-sorting class
1000 $(ui.item).addClass('is-sorting');
1001 },
1002 stop: function(event, ui) {
1003 // make sure no elements are being sorted
1004 $('.' + item_env).removeClass('is-sorting');
1005 // register delayed update flag to allow understanding a sorting was just completed
1006 setTimeout(() => {
1007 // restore default sorting flag
1008 VBOCore.options.panel_opts.sorting = null;
1009 }, 500);
1010 },
1011 update: function(event, ui) {
1012 // build the current widgets position list
1013 let pos_list = {};
1014 document.querySelectorAll('.' + item_env).forEach((elem, index) => {
1015 let curr_wid = elem.getAttribute('data-vbowidgetid');
1016 pos_list[curr_wid] = index;
1017 });
1018 // trigger the event to update the widget positions
1019 VBOCore.emitEvent(VBOCore.options.panel_opts.sort_save_ev, {
1020 pos_list: pos_list,
1021 });
1022 }
1023 });
1024 }
1025 }
1026
1027 /**
1028 * Registers the callback for saving the new widget sorting value in the Multitask panel.
1029 */
1030 static saveMultitaskingSorting(e) {
1031 if (!e || !e.detail || !e.detail.pos_list) {
1032 return;
1033 }
1034
1035 // update multitask widget position
1036 VBOCore.doAjax(
1037 VBOCore.options.multitask_ajax_uri,
1038 {
1039 call: 'setMultitaskingWidgetPos',
1040 call_args: [
1041 e.detail.pos_list,
1042 ],
1043 },
1044 (response) => {
1045 // do nothing on success
1046 },
1047 (error) => {
1048 // silently log the error
1049 console.error(error.responseText);
1050 }
1051 );
1052 }
1053
1054 /**
1055 * Registers the blur event for the search widgets input.
1056 */
1057 static registerSearchWidgetsBlur() {
1058 if (VBOCore.options.active_listeners.hasOwnProperty('registerSearchWidgetsBlur')) {
1059 // listener is already registered
1060 return true;
1061 }
1062
1063 $(VBOCore.options.panel_opts.search_selector).on('blur', function(e) {
1064 if (e && e.relatedTarget) {
1065 if (e.relatedTarget.classList.contains(VBOCore.options.panel_opts.addw_selector.replace('.', ''))) {
1066 // add new widget was clicked, abort hiding process or click event won't fire on target element
1067 return;
1068 }
1069 }
1070 var keyword = $(this).val();
1071 if (!keyword.length) {
1072 // hide add-widgets container
1073 $(VBOCore.options.panel_opts.addws_selector).hide();
1074 }
1075 });
1076
1077 // register flag for listener active
1078 VBOCore.options.active_listeners['registerSearchWidgetsBlur'] = 1;
1079 }
1080
1081 /**
1082 * Removes the blur event handler for the search widgets input.
1083 */
1084 static unregisterSearchWidgetsBlur() {
1085 if (!VBOCore.options.active_listeners.hasOwnProperty('registerSearchWidgetsBlur')) {
1086 // nothing to unregister
1087 return true;
1088 }
1089
1090 $(VBOCore.options.panel_opts.search_selector).off('blur');
1091
1092 // delete flag for listener active
1093 delete VBOCore.options.active_listeners['registerSearchWidgetsBlur'];
1094 }
1095
1096 /**
1097 * Adds a widget identifier to the multitask panel.
1098 *
1099 * @param string widget_id the widget identifier string to add.
1100 */
1101 static addWidgetToPanel(widget_id) {
1102 if (!VBOCore.options.widget_ajax_uri || !VBOCore.options.panel_opts || !Object.keys(VBOCore.options.panel_opts).length) {
1103 throw new Error('Multitask panel options are missing');
1104 }
1105 // prepend container to panel
1106 var widget_classes = [VBOCore.options.panel_opts.wclass_base_sel.replace('.', ''), VBOCore.options.panel_opts.cur_widget_cls];
1107 var widget_div = '<div class="' + widget_classes.join(' ') + '" ' + VBOCore.options.panel_opts.addw_data_attr + '="' + widget_id + '" style="display: none;"></div>';
1108 var widget_elem = $(widget_div);
1109 $(VBOCore.options.panel_opts.actws_selector).prepend(widget_elem);
1110
1111 // always hide add-widgets container
1112 $(VBOCore.options.panel_opts.addws_selector).hide();
1113
1114 // trigger debounced map saving event
1115 VBOCore.emitMultitaskEvent();
1116
1117 // register listener for input search blur
1118 VBOCore.registerSearchWidgetsBlur();
1119
1120 // render widget
1121 var call_method = 'render';
1122 VBOCore.doAjax(
1123 VBOCore.options.widget_ajax_uri,
1124 {
1125 widget_id: widget_id,
1126 call: call_method,
1127 vbo_page: VBOCore.options.current_page,
1128 vbo_uri: VBOCore.options.current_page_uri,
1129 multitask: 1,
1130 },
1131 (response) => {
1132 // display widgets editing button
1133 VBOCore.toggleWidgetsPanelEditing(true);
1134 // parse response
1135 try {
1136 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1137 if (obj_res.hasOwnProperty(call_method)) {
1138 // populate widget HTML content and display it
1139 widget_elem.html(obj_res[call_method]).fadeIn();
1140
1141 // always scroll active widgets list to top
1142 $(VBOCore.options.panel_opts.actws_selector).scrollTop(0);
1143
1144 // register detach widget button
1145 setTimeout(() => {
1146 let detach_elem = $('<div></div>').addClass(VBOCore.options.panel_opts.dtcwidget_class).html(VBOCore.options.panel_opts.dtcwidget_icn);
1147 let detach_to_target = widget_elem.find('.' + VBOCore.options.panel_opts.dtctarget_class);
1148 if (!detach_to_target.length) {
1149 detach_to_target = widget_elem;
1150 }
1151 // move detach wrapper onto the target (widget head)
1152 detach_elem.prependTo(detach_to_target);
1153 // register detach action
1154 detach_elem.on('click', function() {
1155 // detach widget, meaning do a modal rendering
1156 VBOCore.renderModalWidget(widget_id);
1157 });
1158 }, 500);
1159 } else {
1160 console.error('Unexpected JSON response', obj_res);
1161 }
1162 } catch(err) {
1163 console.error('could not parse JSON response', err, response);
1164 }
1165 },
1166 (error) => {
1167 console.error(error.responseText);
1168 }
1169 );
1170 }
1171
1172 /**
1173 * Toggles the edit mode of the multitask widgets panel.
1174 *
1175 * @param bool added true if a widget was just added, false if it was just removed.
1176 */
1177 static toggleWidgetsPanelEditing(added) {
1178 // check if we are currently editing
1179 var is_editing = ($('.' + VBOCore.options.panel_opts.editmode_class).length > 0);
1180
1181 // the triggerer button
1182 var triggerer = $(VBOCore.options.panel_opts.editw_selector);
1183
1184 // check added action status
1185 if (added === true) {
1186 // show button for edit mode
1187 triggerer.show();
1188 // hide shortcut element
1189 $(VBOCore.options.panel_opts.shortc_selector).hide();
1190 return;
1191 }
1192
1193 // grab all widgets
1194 var editing_widgets = $(VBOCore.options.panel_opts.wclass_base_sel);
1195
1196 if (added === false) {
1197 if (!editing_widgets.length) {
1198 // hide button for edit mode after removing the last widget
1199 triggerer.removeClass(VBOCore.options.panel_opts.editw_selector.substr(1) + '-active').hide();
1200 // show shortcut element
1201 $(VBOCore.options.panel_opts.shortc_selector).show();
1202 }
1203 return;
1204 }
1205
1206 if (is_editing) {
1207 // deactivate editing mode
1208 editing_widgets.removeClass(VBOCore.options.panel_opts.editmode_class);
1209 $('.' + VBOCore.options.panel_opts.rmwidget_class).remove();
1210 // toggle triggerer button active class
1211 triggerer.removeClass(VBOCore.options.panel_opts.editw_selector.substr(1) + '-active');
1212 } else {
1213 // activate editing mode by looping through all widgets
1214 editing_widgets.each(function() {
1215 // build remove-widget element
1216 var rm_widget = $('<div></div>').addClass(VBOCore.options.panel_opts.rmwidget_class).on('click', function() {
1217 VBOCore.removeWidgetFromPanel(this);
1218 }).html(VBOCore.options.panel_opts.rmwidget_icn);
1219 // add editing class and prepend removing element
1220 $(this).addClass(VBOCore.options.panel_opts.editmode_class).prepend(rm_widget);
1221 });
1222 // toggle triggerer button active class
1223 triggerer.addClass(VBOCore.options.panel_opts.editw_selector.substr(1) + '-active');
1224 }
1225 }
1226
1227 /**
1228 * Handles the removal of a widget from the multitask panel.
1229 *
1230 * @param object element
1231 */
1232 static removeWidgetFromPanel(element) {
1233 if (!element) {
1234 console.error('Invalid widget element to remove', element);
1235 return false;
1236 }
1237 var widget_cont = $(element).parent(VBOCore.options.panel_opts.wclass_base_sel);
1238 if (!widget_cont || !widget_cont.length) {
1239 console.error('Could not find widget container to remove', element);
1240 return false;
1241 }
1242 var widget_id = widget_cont.attr(VBOCore.options.panel_opts.addw_data_attr);
1243 if (!widget_id || !widget_id.length) {
1244 console.error('Empty widget id to remove', element);
1245 return false;
1246 }
1247 // find the index of the widget to remove in the panel
1248 var widget_index = $(VBOCore.options.panel_opts.wclass_base_sel).index(widget_cont);
1249 if (widget_index < 0) {
1250 console.error('Empty widget index to remove', widget_cont);
1251 return false;
1252 }
1253 // make sure the index in the array matches the id
1254 if (!VBOCore.options.admin_widgets.hasOwnProperty(widget_index) || VBOCore.options.admin_widgets[widget_index]['id'] != widget_id) {
1255 console.error('Unmatching widget index or id', VBOCore.options.admin_widgets, widget_index, widget_id);
1256 return false;
1257 }
1258 // remove this widget from the array
1259 VBOCore.options.admin_widgets.splice(widget_index, 1);
1260
1261 // remove element from document
1262 widget_cont.remove();
1263
1264 // check widgets editing button status
1265 VBOCore.toggleWidgetsPanelEditing(false);
1266
1267 // trigger debounced map saving event
1268 VBOCore.emitMultitaskEvent();
1269
1270 if (!VBOCore.options.admin_widgets.length) {
1271 // unregister listener for input search blur
1272 VBOCore.unregisterSearchWidgetsBlur();
1273 }
1274 }
1275
1276 /**
1277 * Emits an event related to the multitask features or a custom event, with optional data.
1278 */
1279 static emitMultitaskEvent(ev_name, ev_data) {
1280 var def_ev_name = VBOCore.options.multitask_save_event;
1281 if (typeof ev_name === 'string') {
1282 def_ev_name = ev_name;
1283 }
1284
1285 if (typeof ev_data !== 'undefined' && ev_data) {
1286 // trigger the custom event
1287 document.dispatchEvent(new CustomEvent(def_ev_name, {bubbles: true, detail: ev_data}));
1288 return;
1289 }
1290
1291 // trigger the event
1292 document.dispatchEvent(new Event(def_ev_name));
1293 }
1294
1295 /**
1296 * Proxy for dispatching an event to the document with optional data.
1297 */
1298 static emitEvent(ev_name, ev_data) {
1299 if (typeof ev_name !== 'string' || !ev_name.length) {
1300 return;
1301 }
1302
1303 return VBOCore.emitMultitaskEvent(ev_name, ev_data);
1304 }
1305
1306 /**
1307 * Attempts to save the multitask widgets for this page.
1308 */
1309 static saveMultitasking() {
1310 // gather the list of active widgets
1311 var active_widgets = [];
1312 var cur_admin_widgets = [];
1313 $(VBOCore.options.panel_opts.actws_selector).find(VBOCore.options.panel_opts.wclass_base_sel).each(function() {
1314 var widget_id = $(this).attr(VBOCore.options.panel_opts.addw_data_attr);
1315 if (widget_id && widget_id.length) {
1316 // push id in list
1317 active_widgets.push(widget_id);
1318 // push object with dummy name for global widgets
1319 cur_admin_widgets.push({
1320 id: widget_id,
1321 name: widget_id,
1322 });
1323 }
1324 });
1325
1326 // update multitask widgets map for this page
1327 VBOCore.doAjax(
1328 VBOCore.options.multitask_ajax_uri,
1329 {
1330 call: 'updateMultitaskingMap',
1331 call_args: [
1332 VBOCore.options.current_page,
1333 active_widgets,
1334 0
1335 ],
1336 },
1337 (response) => {
1338 try {
1339 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1340 if (obj_res.hasOwnProperty('result') && obj_res['result']) {
1341 // set current widgets
1342 VBOCore.setOptions({
1343 admin_widgets: cur_admin_widgets
1344 });
1345 } else {
1346 console.error('Unexpected or invalid JSON response', response);
1347 }
1348 } catch(err) {
1349 console.error('could not parse JSON response', err, response);
1350 }
1351 },
1352 (error) => {
1353 console.error(error.responseText);
1354 }
1355 );
1356 }
1357
1358 /**
1359 * Sets up the browser notifications within the multitask panel, if supported.
1360 */
1361 static setupNotifications() {
1362 if (!('Notification' in window)) {
1363 // browser does not support notifications
1364 $(VBOCore.options.panel_opts.notif_selector).hide();
1365 return false;
1366 }
1367
1368 if (Notification.permission && Notification.permission === 'granted') {
1369 // permissions were granted already
1370 $(VBOCore.options.panel_opts.notif_selector)
1371 .addClass(VBOCore.options.panel_opts.notif_on_class)
1372 .attr('title', VBOCore.options.tn_texts.notifs_enabled)
1373 .on('click', function() {
1374 // show congratulations notification
1375 VBOCore.dispatchCongratulations();
1376
1377 // attempt to update the service worker installation
1378 VBOCore.installServiceWorker(true).then(() => {
1379 // all good
1380 }).catch((error) => {
1381 console.error(error);
1382 });
1383 });
1384 return true;
1385 }
1386
1387 // notifications supported, but perms not granted
1388 $(VBOCore.options.panel_opts.notif_selector)
1389 .addClass(VBOCore.options.panel_opts.notif_off_class)
1390 .attr('title', VBOCore.options.tn_texts.notifs_disabled);
1391
1392 // register click-event listener on button to enable notifications
1393 $(VBOCore.options.panel_opts.notif_selector).on('click', function() {
1394 VBOCore.requestNotifPerms();
1395 });
1396
1397 // subscribe to the multitask-panel-open event to show the status of the notifications
1398 document.addEventListener(VBOCore.multitask_open_event, function() {
1399 if (VBOCore.notificationsEnabled() === false) {
1400 // add "shaking" class to notifications button
1401 $(VBOCore.options.panel_opts.notif_selector).addClass('shaking');
1402 }
1403 });
1404
1405 // subscribe to the multitask-panel-close event to update the status of the notifications
1406 document.addEventListener(VBOCore.multitask_close_event, function() {
1407 // always remove "shaking" class from notifications button
1408 $(VBOCore.options.panel_opts.notif_selector).removeClass('shaking');
1409 });
1410 }
1411
1412 /**
1413 * Sets up the browser notifications within the given selector, if supported.
1414 *
1415 * @param string selector the element query selector.
1416 */
1417 static suggestNotifications(selector) {
1418 if (!selector) {
1419 return false;
1420 }
1421
1422 if (!('Notification' in window)) {
1423 // browser does not support notifications
1424 $(selector).hide();
1425 return false;
1426 }
1427
1428 if (Notification.permission && Notification.permission === 'granted') {
1429 // permissions were granted already
1430 $(selector)
1431 .addClass(VBOCore.options.panel_opts.notif_on_class)
1432 .attr('title', VBOCore.options.tn_texts.notifs_enabled)
1433 .on('click', function() {
1434 VBOCore.dispatchCongratulations();
1435 });
1436 return true;
1437 }
1438
1439 // notifications supported, but perms not granted
1440 $(selector)
1441 .addClass(VBOCore.options.panel_opts.notif_off_class)
1442 .attr('title', VBOCore.options.tn_texts.notifs_disabled);
1443
1444 // register click-event listener on button to enable notifications
1445 $(selector).on('click', function() {
1446 VBOCore.requestNotifPerms(selector);
1447 });
1448
1449 // add "shaking" class to make the selector more appealing
1450 $(selector).addClass('shaking');
1451
1452 // remove the "shaking" class after some time
1453 setTimeout(() => {
1454 $(selector).removeClass('shaking');
1455 }, 2000);
1456 }
1457
1458 /**
1459 * Tells whether the notifications are enabled, disabled, not supported.
1460 */
1461 static notificationsEnabled() {
1462 if (!('Notification' in window)) {
1463 // not supported
1464 return null;
1465 }
1466
1467 if (Notification.permission && Notification.permission === 'granted') {
1468 // enabled
1469 return true;
1470 }
1471
1472 // disabled
1473 return false;
1474 }
1475
1476 /**
1477 * Attempts to request the notifications permissions to the browser.
1478 * For security reasons, this should run upon a user gesture (click).
1479 *
1480 * @param string selector optional element query selector.
1481 */
1482 static requestNotifPerms(selector) {
1483 if (!('Notification' in window)) {
1484 // browser does not support notifications
1485 return false;
1486 }
1487
1488 // run permissions request in a try-catch statement to support all browsers
1489 try {
1490 // handle promise-based version to request permissions
1491 Notification.requestPermission().then((permission) => {
1492 VBOCore.handleNotifPerms(permission, selector);
1493 });
1494 } catch(e) {
1495 // run the callback-based version
1496 Notification.requestPermission(function(permission) {
1497 VBOCore.handleNotifPerms(permission, selector);
1498 });
1499 }
1500 }
1501
1502 /**
1503 * Handles the notifications permission response (from callback or promise resolved).
1504 *
1505 * @param any permission permission status string or NotificationPermission object.
1506 * @param string selector optional element query selector.
1507 */
1508 static handleNotifPerms(permission, selector) {
1509 // check the permission status from the Notification object interface
1510 if ((Notification.permission && Notification.permission === 'granted') || (typeof permission === 'string' && permission === 'granted')) {
1511 // permissions granted!
1512 $((selector || VBOCore.options.panel_opts.notif_selector))
1513 .removeClass(VBOCore.options.panel_opts.notif_off_class)
1514 .addClass(VBOCore.options.panel_opts.notif_on_class)
1515 .attr('title', VBOCore.options.tn_texts.notifs_enabled);
1516
1517 // dispatch an immediate notification to congratulate with the activation
1518 VBOCore.dispatchCongratulations();
1519
1520 return true;
1521 } else {
1522 // permissions denied :(
1523 $((selector || VBOCore.options.panel_opts.notif_selector))
1524 .removeClass(VBOCore.options.panel_opts.notif_on_class)
1525 .addClass(VBOCore.options.panel_opts.notif_off_class)
1526 .attr('title', VBOCore.options.tn_texts.notifs_disabled);
1527
1528 // show alert message
1529 console.error('Permission denied for enabling browser notifications', permission);
1530 alert(VBOCore.options.tn_texts.notifs_disabled_help);
1531 }
1532
1533 return false;
1534 }
1535
1536 /**
1537 * Dispatches an immediate notification with a congratulations text.
1538 *
1539 * @return void
1540 */
1541 static dispatchCongratulations() {
1542 try {
1543 let notif = new Notification(VBOCore.options.tn_texts.congrats, {
1544 body: VBOCore.options.tn_texts.notifs_enabled,
1545 tag: 'vbo_notification_congrats',
1546 silent: false
1547 });
1548 } catch(error) {
1549 alert(error);
1550 }
1551 }
1552
1553 /**
1554 * Given a date-time string, returns a Date object representation.
1555 *
1556 * @param string dtime_str the date-time string in "Y-m-d H:i:s" format.
1557 */
1558 static getDateTimeObject(dtime_str) {
1559 // instantiate a new date object
1560 var date_obj = new Date();
1561
1562 // parse date-time string
1563 let dtime_parts = dtime_str.split(' ');
1564 let date_parts = dtime_parts[0].split('-');
1565 if (dtime_parts.length != 2 || date_parts.length != 3) {
1566 // invalid military format
1567 return date_obj;
1568 }
1569 let time_parts = dtime_parts[1].split(':');
1570
1571 // set accurate date-time values
1572 date_obj.setFullYear(date_parts[0]);
1573 date_obj.setMonth((parseInt(date_parts[1]) - 1));
1574 date_obj.setDate(parseInt(date_parts[2]));
1575 date_obj.setHours(parseInt(time_parts[0]));
1576 date_obj.setMinutes(parseInt(time_parts[1]));
1577 date_obj.setSeconds(0);
1578 date_obj.setMilliseconds(0);
1579
1580 // return the accurate date object
1581 return date_obj;
1582 }
1583
1584 /**
1585 * Given a list of schedules, enqueues notifications to watch.
1586 *
1587 * @param array|object schedules list of or one notification object(s).
1588 *
1589 * @return bool
1590 */
1591 static enqueueNotifications(schedules) {
1592 if (!Array.isArray(schedules) || !schedules.length) {
1593 if (typeof schedules === 'object' && schedules.hasOwnProperty('dtime')) {
1594 // convert the single schedule to an array
1595 schedules = [schedules];
1596 } else {
1597 // invalid argument passed
1598 return false;
1599 }
1600 }
1601
1602 for (var i in schedules) {
1603 if (!schedules.hasOwnProperty(i) || typeof schedules[i] !== 'object') {
1604 continue;
1605 }
1606 VBOCore.notifications.push(schedules[i]);
1607 }
1608
1609 // setup the timeouts to schedule the notifications
1610 return VBOCore.scheduleNotifications();
1611 }
1612
1613 /**
1614 * Schedule the trigger timings for each notification.
1615 */
1616 static scheduleNotifications() {
1617 if (!VBOCore.notifications.length) {
1618 // no notifications to be scheduled
1619 return false;
1620 }
1621 if (VBOCore.notificationsEnabled() !== true) {
1622 // notifications not enabled
1623 console.info('Browser notifications disabled or unsupported.');
1624 return false;
1625 }
1626
1627 // gather current date-timing information
1628 const now_date = new Date();
1629 const now_time = now_date.getTime();
1630
1631 // parse all notifications to schedule the timers if not set
1632 for (let i = 0; i < VBOCore.notifications.length; i++) {
1633 let notif = VBOCore.notifications[i];
1634
1635 if (typeof notif !== 'object' || !notif.hasOwnProperty('dtime')) {
1636 // invalid notification object, unset it
1637 VBOCore.notifications.splice(i, 1);
1638 continue;
1639 }
1640
1641 // check if timer has been set
1642 if (!notif.hasOwnProperty('id_timer')) {
1643 // estimate trigger timing
1644 let in_ms = 0;
1645 // check for imminent notifications
1646 if (typeof notif.dtime === 'string' && notif.dtime.indexOf('now') >= 0) {
1647 // imminent ones will be delayed by one second
1648 in_ms = 1000;
1649 } else {
1650 // check overdue date-time (notif.dtime can also be a Date object instance)
1651 let nexp = VBOCore.getDateTimeObject(notif.dtime);
1652 in_ms = nexp.getTime() - now_time;
1653 }
1654 if (in_ms > 0) {
1655 // schedule notification trigger
1656 let id_timer = setTimeout(() => {
1657 VBOCore.dispatchNotification(notif);
1658 }, in_ms);
1659 // set timer on notification object
1660 VBOCore.notifications[i]['id_timer'] = id_timer;
1661 }
1662 }
1663 }
1664
1665 return true;
1666 }
1667
1668 /**
1669 * Deregister all scheduled notifications.
1670 */
1671 static unscheduleNotifications() {
1672 if (!VBOCore.notifications.length) {
1673 // no notifications scheduled
1674 return false;
1675 }
1676
1677 for (let i = 0; i < VBOCore.notifications.length; i++) {
1678 let notif = VBOCore.notifications[i];
1679
1680 if (typeof notif === 'object' && notif.hasOwnProperty('id_timer')) {
1681 // unset timeout for this notification
1682 clearTimeout(notif['id_timer']);
1683 }
1684 }
1685
1686 // reset pool
1687 VBOCore.notifications = [];
1688 }
1689
1690 /**
1691 * Update or delete a previously scheduled notification.
1692 *
1693 * @param object match_props map of properties to match.
1694 * @param string|number newdtime the new date time to schedule (0 for deleting).
1695 *
1696 * @return null|bool true only if a notification matched.
1697 */
1698 static updateNotification(match_props, newdtime) {
1699 if (!VBOCore.notifications.length) {
1700 // no notifications set, terminate
1701 return null;
1702 }
1703
1704 if (typeof match_props !== 'object') {
1705 // no properties to match the notification
1706 return null;
1707 }
1708
1709 // gather current date-timing information
1710 const now_date = new Date();
1711 const now_time = now_date.getTime();
1712
1713 // parse all notifications scheduled
1714 for (let i = 0; i < VBOCore.notifications.length; i++) {
1715 let notif = VBOCore.notifications[i];
1716
1717 let all_matched = true;
1718 let to_matching = false;
1719 for (let prop in match_props) {
1720 if (!match_props.hasOwnProperty(prop)) {
1721 continue;
1722 }
1723 to_matching = true;
1724 if (!notif.hasOwnProperty(prop) || notif[prop] != match_props[prop]) {
1725 all_matched = false;
1726 break;
1727 }
1728 }
1729
1730 if (all_matched && to_matching) {
1731 // notification object found
1732 if (notif.hasOwnProperty('id_timer')) {
1733 // unset previous timeout for this notification
1734 clearTimeout(notif['id_timer']);
1735 }
1736 // update or delete scheduled notification
1737 if (newdtime === 0) {
1738 // delete notification from queue
1739 VBOCore.notifications.splice(i, 1);
1740 } else {
1741 // update timing scheduler
1742 let in_ms = 0;
1743 // check for imminent notifications
1744 if (typeof newdtime === 'string' && newdtime.indexOf('now') >= 0) {
1745 // imminent ones will be delayed by one second
1746 in_ms = 1000;
1747 } else {
1748 // check overdue date-time (newdtime can also be a Date object instance)
1749 let nexp = VBOCore.getDateTimeObject(newdtime);
1750 in_ms = nexp.getTime() - now_time;
1751 }
1752 if (in_ms > 0) {
1753 // schedule notification trigger
1754 let id_timer = setTimeout(() => {
1755 VBOCore.dispatchNotification(notif);
1756 }, in_ms);
1757 // set timer on notification object
1758 VBOCore.notifications[i]['id_timer'] = id_timer;
1759 }
1760 // update date-time value on notification object
1761 VBOCore.notifications[i]['dtime'] = newdtime;
1762 }
1763
1764 // terminate parsing and return true
1765 return true;
1766 }
1767 }
1768
1769 // notification object not found
1770 return false;
1771 }
1772
1773 /**
1774 * Dispatch the notification object.
1775 * Expected notification properties:
1776 *
1777 * {
1778 * id: number
1779 * type: string
1780 * dtime: string|Date
1781 * build_url: string|null
1782 * }
1783 *
1784 * @param object notif the notification object.
1785 */
1786 static dispatchNotification(notif) {
1787 if (typeof notif !== 'object') {
1788 return false;
1789 }
1790
1791 // subscribe to building notification data
1792 VBOCore.buildNotificationData(notif).then((data) => {
1793 // dispatch the notification
1794
1795 // check if the click event should be registered
1796 let func_nodes;
1797 if (data.onclick && typeof data.onclick === 'string') {
1798 let callback_parts = data.onclick.split('.');
1799 while (callback_parts.length) {
1800 // compose window static method string to avoid using eval()
1801 let tmp = callback_parts.shift();
1802 if (!func_nodes) {
1803 func_nodes = window[tmp];
1804 } else {
1805 func_nodes = func_nodes[tmp];
1806 }
1807 }
1808 }
1809
1810 // prepare properties to delete the notification from queue
1811 let match_props = {};
1812 for (let prop in notif) {
1813 if (!notif.hasOwnProperty(prop) || prop == 'id_timer') {
1814 continue;
1815 }
1816 match_props[prop] = notif[prop];
1817 }
1818
1819 // check browser Notifications API
1820 if (VBOCore.notificationsEnabled() !== true) {
1821 // notifications not enabled, fallback to toast message
1822 let toast_notif_data = {
1823 title: data.title,
1824 body: data.message,
1825 icon: data.icon,
1826 delay: {
1827 min: 6000,
1828 max: 20000,
1829 tolerance: 4000,
1830 },
1831 action: () => {
1832 VBOToast.dispose(true);
1833 },
1834 sound: VBOCore.options.notif_audio_url
1835 };
1836 if (func_nodes) {
1837 toast_notif_data.action = function() {
1838 func_nodes(data);
1839 VBOToast.dispose(true);
1840 };
1841 } else if (typeof data.onclick === 'function') {
1842 toast_notif_data.action = function() {
1843 data.onclick.call(data);
1844 VBOToast.dispose(true);
1845 };
1846 }
1847 VBOToast.enqueue(new VBOToastMessage(toast_notif_data));
1848
1849 // delete dispatched notification from queue
1850 VBOCore.updateNotification(match_props, 0);
1851
1852 return;
1853 }
1854
1855 // use the browser's native Notifications API
1856 let browser_notif = new Notification(data.title, {
1857 body: data.message,
1858 icon: data.icon,
1859 tag: 'vbo_notification',
1860 silent: false
1861 });
1862
1863 // check if support for Web App badge is available
1864 if (typeof navigator.setAppBadge !== 'undefined') {
1865 navigator.setAppBadge(1);
1866 }
1867
1868 browser_notif.addEventListener('click', (e) => {
1869 // check if support for Web App badge is available
1870 if (typeof navigator.setAppBadge !== 'undefined') {
1871 navigator.clearAppBadge();
1872 }
1873 // close the notification
1874 e.target.close();
1875 });
1876
1877 if (func_nodes) {
1878 // register notification click event
1879 browser_notif.addEventListener('click', () => {
1880 func_nodes(data);
1881 });
1882 } else if (typeof data.onclick === 'function') {
1883 browser_notif.addEventListener('click', () => {
1884 data.onclick.call(data);
1885 });
1886 }
1887
1888 // delete dispatched notification from queue
1889 VBOCore.updateNotification(match_props, 0);
1890
1891 }).catch((error) => {
1892 console.error(error);
1893 });
1894 }
1895
1896 /**
1897 * Asynchronous build of the notification data object for dispatch.
1898 * Minimum expected notification display data properties:
1899 *
1900 * {
1901 * title: string
1902 * message: string
1903 * icon: string
1904 * onclick: function
1905 * }
1906 *
1907 * @param object notif the scheduled notification object.
1908 *
1909 * @return Promise
1910 */
1911 static buildNotificationData(notif) {
1912 return new Promise((resolve, reject) => {
1913 // notification object validation
1914 if (typeof notif !== 'object') {
1915 reject('Invalid notification object');
1916 return;
1917 }
1918
1919 if (!notif.hasOwnProperty('build_url') || !notif.build_url) {
1920 // building callback not necessary
1921 if (!notif.title && !notif.message) {
1922 reject('Unexpected notification object');
1923 return;
1924 }
1925 // we expect the notification to be built already
1926 resolve(notif);
1927 return;
1928 }
1929
1930 // build the notification data
1931 VBOCore.doAjax(
1932 notif.build_url,
1933 {
1934 payload: JSON.stringify(notif)
1935 },
1936 (response) => {
1937 // parse response
1938 try {
1939 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1940 if (obj_res.hasOwnProperty('title')) {
1941 resolve(obj_res);
1942 } else {
1943 reject('Unexpected JSON response');
1944 }
1945 } catch(err) {
1946 reject('could not parse JSON response');
1947 }
1948 },
1949 (error) => {
1950 reject(error.responseText);
1951 }
1952 );
1953 });
1954 }
1955
1956 /**
1957 * Handle a navigation towards a given URL.
1958 * Common handler for browser notifications click.
1959 *
1960 * @param object data notification display data payload.
1961 */
1962 static handleGoto(data) {
1963 if (typeof data !== 'object' || !data.hasOwnProperty('gotourl') || !data.gotourl) {
1964 console.error('Could not handle the goto operation', data);
1965 return;
1966 }
1967
1968 if (typeof data.openWindow !== 'undefined' || typeof document === 'undefined') {
1969 // open a new window
1970 window.open(data.gotourl);
1971 return;
1972 }
1973
1974 // redirect
1975 document.location.href = data.gotourl;
1976 }
1977
1978 /**
1979 * Opens a new window with the URI to render the Push notification data.
1980 *
1981 * @param object options admin widget rendering options to bind.
1982 *
1983 * @return void
1984 */
1985 static openAdminPushURI(options) {
1986 if (typeof options !== 'object' || !options) {
1987 throw new Error('Unable to build Admin Push URI');
1988 }
1989
1990 let vbo_admin_push_uri = VBOCore.options.root_uri;
1991
1992 if (VBOCore.options.cms === 'wordpress') {
1993 vbo_admin_push_uri += 'wp-admin/admin.php?page=vikbooking';
1994 } else {
1995 vbo_admin_push_uri += 'administrator/index.php?option=com_vikbooking';
1996 }
1997
1998 // build Push notification payload for rendering
1999 let push_data = {
2000 type: options.type || '',
2001 title: options.title || '',
2002 message: options.message || '',
2003 content: options,
2004 };
2005
2006 // open page in a new window
2007 window.open(vbo_admin_push_uri + '&push_notification=' + btoa(JSON.stringify(push_data)), '_blank');
2008 }
2009
2010 /**
2011 * Handle the display of a notification through a widget.
2012 * Common handler for browser notifications displayed through
2013 * a widget modal rendering.
2014 *
2015 * @param object data notification display data payload.
2016 * @param object options optional extra options for the admin widget.
2017 * @param boolean is_push whether we are coming from a Push notification.
2018 *
2019 * @return void
2020 */
2021 static handleDisplayWidgetNotification(data, options, is_push) {
2022 try {
2023 // validate handler
2024 if (!VBOCore.options.is_vbo && !VBOCore.options.is_vcm) {
2025 throw new Error('Unable to handle the notification display from the current page');
2026 }
2027
2028 // validate payload
2029 if (typeof data !== 'object' || !data.hasOwnProperty('widget_id') || !data['widget_id']) {
2030 throw new Error('Invalid widget payload');
2031 }
2032
2033 // parse handler
2034 if (VBOCore.options.is_vcm || (is_push && !VBOCore.options.is_vbo)) {
2035 // the operation is handled by VCM (or by an external admin resource, if it's a Push notification)
2036 VBOCore.loadAdminWidgetAssets(data).then((assets) => {
2037 // append assets to DOM
2038 assets.forEach((asset) => {
2039 if (!$('link#' + asset['id']).length) {
2040 $('head').append('<link rel="stylesheet" id="' + asset['id'] + '" href="' + asset['href'] + '" media="all" />');
2041 }
2042 });
2043 // widget modal rendering handled by VCM (or an external admin resource)
2044 let hide_panel = VBOCore.options.is_vcm && $('.' + VBOCore.options.panel_opts.open_class).length ? true : false;
2045 let modal_data = VBOCore.renderModalWidget(data['widget_id'], data, options, hide_panel);
2046 if (modal_data.hasOwnProperty('dismissed_event') && (modal_data['suffix'] || '').indexOf('inner') < 0) {
2047 // register event to unload all assets (only if not from an inner modal)
2048 document.addEventListener(modal_data['dismissed_event'], () => {
2049 assets.forEach((asset) => {
2050 if ($('link#' + asset['id']).length) {
2051 $('link#' + asset['id']).remove();
2052 }
2053 });
2054 });
2055 }
2056 }).catch((error) => {
2057 throw new Error(error);
2058 });
2059 } else {
2060 // widget modal rendering handled by Vik Booking
2061 VBOCore.renderModalWidget(data['widget_id'], data, options, false);
2062 }
2063 } catch(e) {
2064 if (is_push && options.type) {
2065 // fallback to opening a new window to render the clicked Push notification
2066 VBOCore.openAdminPushURI(options);
2067 } else {
2068 // fallback to a regular link opening, if set
2069 VBOCore.handleGoto(data);
2070 }
2071 }
2072 }
2073
2074 /**
2075 * Asynchronous loading of CSS assets required to render
2076 * an admin widget outside Vik Booking.
2077 *
2078 * @param object data the optional widget payload data.
2079 *
2080 * @return Promise
2081 */
2082 static loadAdminWidgetAssets(data) {
2083 return new Promise((resolve, reject) => {
2084 // the remote assets URI must be set
2085 if (!VBOCore.options.assets_ajax_uri) {
2086 reject('Missing remote assets URI');
2087 return;
2088 }
2089
2090 if (typeof data !== 'object') {
2091 data = {};
2092 }
2093
2094 // make the request
2095 VBOCore.doAjax(
2096 VBOCore.options.assets_ajax_uri,
2097 data,
2098 (response) => {
2099 // parse response
2100 try {
2101 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
2102 if (!Array.isArray(obj_res)) {
2103 reject('Unexpected JSON response');
2104 }
2105 resolve(obj_res);
2106 } catch(err) {
2107 reject('could not parse JSON response');
2108 }
2109 },
2110 (error) => {
2111 reject(error.responseText);
2112 }
2113 );
2114 });
2115 }
2116
2117 /**
2118 * Register the latest data to watch for the preloaded admin widgets.
2119 *
2120 * @param object watch_data
2121 */
2122 static registerWatchData(watch_data) {
2123 if (typeof watch_data !== 'object' || watch_data == null) {
2124 VBOCore.widgets_watch_data = null;
2125 return false;
2126 }
2127
2128 // set watch-data map
2129 VBOCore.widgets_watch_data = watch_data;
2130
2131 // schedule watching interval
2132 if (VBOCore.watch_data_interval == null) {
2133 VBOCore.watch_data_interval = window.setInterval(VBOCore.watchWidgetsData, 60000);
2134 }
2135
2136 // set up broadcast channels to connect all browsing contexts
2137 if (typeof BroadcastChannel !== 'undefined') {
2138 // set up watch-data broadcast channel
2139 if (!VBOCore.broadcast_watch_data) {
2140 // start broadcast channel
2141 VBOCore.broadcast_watch_data = new BroadcastChannel('vikbooking_watch_data');
2142
2143 // register to the "on broadcast message received" event
2144 VBOCore.broadcast_watch_data.onmessage = (event) => {
2145 if (event && event.data) {
2146 // update watch data map for next schedule to avoid dispatching duplicate notifications
2147 VBOCore.widgets_watch_data = event.data;
2148 }
2149 };
2150 }
2151
2152 // set up push-data broadcast channel
2153 if (!VBOCore.broadcast_push_data) {
2154 // start broadcast channel
2155 VBOCore.broadcast_push_data = new BroadcastChannel('vikbooking_push_data');
2156
2157 // register to the "on broadcast message received" event
2158 VBOCore.broadcast_push_data.onmessage = (event) => {
2159 if (event && event.data) {
2160 // update pushed data map for the next watching event
2161 VBOCore.widgets_pushed_data.push(event.data);
2162 }
2163 };
2164 }
2165
2166 // set up watch-events broadcast channel
2167 if (!VBOCore.broadcast_watch_events) {
2168 // start broadcast channel
2169 VBOCore.broadcast_watch_events = new BroadcastChannel('vikbooking_watch_events');
2170
2171 // register to the "on broadcast message received" event
2172 VBOCore.broadcast_watch_events.onmessage = (event) => {
2173 if (event && event.data && typeof event.data === 'object') {
2174 // scan and dispatch the events data received
2175 let events_data = event.data;
2176 for (let ev_name in events_data) {
2177 if (!events_data.hasOwnProperty(ev_name)) {
2178 continue;
2179 }
2180 // emit the event
2181 VBOCore.emitEvent(ev_name, events_data[ev_name]);
2182 }
2183 }
2184 };
2185 }
2186 }
2187 }
2188
2189 /**
2190 * Periodic widgets data watching for new events.
2191 */
2192 static watchWidgetsData() {
2193 if (typeof VBOCore.widgets_watch_data !== 'object' || VBOCore.widgets_watch_data == null) {
2194 return;
2195 }
2196
2197 // call on new events
2198 VBOCore.doAjax(
2199 VBOCore.options.watchdata_ajax_uri,
2200 {
2201 watch_data: JSON.stringify(VBOCore.widgets_watch_data),
2202 pushed_data: JSON.stringify(VBOCore.widgets_pushed_data),
2203 },
2204 (response) => {
2205 try {
2206 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
2207 if (obj_res.hasOwnProperty('watch_data')) {
2208 // update watch data map for next schedule
2209 VBOCore.widgets_watch_data = obj_res['watch_data'];
2210
2211 // check for notifications
2212 if (obj_res.hasOwnProperty('notifications') && Array.isArray(obj_res['notifications'])) {
2213 // dispatch notifications
2214 for (var i = 0; i < obj_res['notifications'].length; i++) {
2215 VBOCore.dispatchNotification(obj_res['notifications'][i]);
2216 }
2217
2218 // post message onto broadcast channel for any other browsing context
2219 if (VBOCore.broadcast_watch_data && obj_res['notifications'].length) {
2220 // this will avoid dispatching duplicate notifications
2221 VBOCore.broadcast_watch_data.postMessage(VBOCore.widgets_watch_data);
2222 }
2223 }
2224
2225 // check for notification events to dispatch
2226 if (obj_res.hasOwnProperty('events') && Array.isArray(obj_res['events'])) {
2227 // parse and dispatch all the events
2228 obj_res['events'].forEach((events_data) => {
2229 if (typeof events_data !== 'object') {
2230 return;
2231 }
2232
2233 // parse all the events for this widget watched data
2234 for (let ev_name in events_data) {
2235 if (!events_data.hasOwnProperty(ev_name)) {
2236 continue;
2237 }
2238 // emit the event
2239 VBOCore.emitEvent(ev_name, events_data[ev_name]);
2240 }
2241
2242 // post message onto broadcast channel for any other browsing context
2243 if (VBOCore.broadcast_watch_events) {
2244 // this will trigger the events on any other browsing context
2245 VBOCore.broadcast_watch_events.postMessage(events_data);
2246 }
2247 });
2248 }
2249 } else {
2250 console.error('Unexpected or invalid JSON response', response);
2251 }
2252 } catch(err) {
2253 console.error('could not parse JSON response', err, response);
2254 }
2255 },
2256 (error) => {
2257 console.error(error.responseText);
2258 }
2259 );
2260 }
2261
2262 /**
2263 * Widget modal rendering.
2264 *
2265 * @param string widget_id the widget identifier to render.
2266 * @param any data the optional multitask data to inject.
2267 * @param object options optional list of extra options to render the widget with Multitask data.
2268 * @param bool hide_panel if false, the multitask panel elements will remain unchanged.
2269 *
2270 * @return object the multitask data injected object merged with modal options.
2271 */
2272 static renderModalWidget(widget_id, data, options, hide_panel) {
2273 /**
2274 * Adjust arguments for BC with previous ordering between options
2275 * and hide_panel, which used to be inverted. Useful also to shorten
2276 * the way the method can be invoked in case no options are needed.
2277 */
2278 if (typeof options === 'boolean') {
2279 // switch argument values
2280 hide_panel = options;
2281 options = null;
2282 }
2283
2284 if (typeof data !== 'object' || data == null) {
2285 // always treat data as an object
2286 data = {};
2287 }
2288
2289 if (typeof options !== 'object' || options == null) {
2290 // always treat options as an object
2291 options = data._options || {};
2292 }
2293
2294 // build the default widget payload
2295 let modal_js_id = Math.floor(Math.random() * 100000);
2296 let modal_title = options._push && options.title ? options.title : VBOCore.options.tn_texts.admin_widget;
2297 let widget_data = {
2298 _modalRendering: 1,
2299 _modalJsId: modal_js_id,
2300 _modalTitle: modal_title,
2301 _options: options,
2302 };
2303
2304 // merge default payload options with given options
2305 data = Object.assign(widget_data, data);
2306
2307 // define unique modal event names to avoid conflicts
2308 let dismiss_event = 'vbo-dismiss-widget-modal' + modal_js_id;
2309 let loading_event = 'vbo-loading-widget-modal' + modal_js_id;
2310 let resize_event = 'vbo-resize-widget-modal' + modal_js_id;
2311
2312 // define the modal options
2313 let modal_options = {
2314 suffix: 'widget_modal',
2315 extra_class: 'vbo-modal-widget vbo-modal-rounded vbo-modal-tall vbo-modal-nofooter',
2316 title: data._modalTitle,
2317 body_prepend: true,
2318 lock_scroll: true,
2319 draggable: true,
2320 enlargeable: true,
2321 minimizeable: VBOCore.options.is_vbo,
2322 resize_event: resize_event,
2323 dismiss_event: dismiss_event,
2324 loading_event: loading_event,
2325 loading_body: VBOCore.options.default_loading_body,
2326 dismissed_event: VBOCore.options.widget_modal_dismissed + modal_js_id,
2327 event_data: widget_data,
2328 };
2329
2330 // check if options should rewrite some modal-options
2331 if (options.modal_options) {
2332 modal_options = Object.assign(modal_options, options.modal_options);
2333 }
2334
2335 // display modal
2336 let widget_modal = VBOCore.displayModal(modal_options);
2337
2338 if (hide_panel !== false) {
2339 // blur search widget input, hide multitask panel
2340 $(VBOCore.options.panel_opts.search_selector).trigger('blur');
2341 VBOCore.emitEvent(VBOCore.multitask_shortcut_event);
2342 }
2343
2344 // start loading
2345 VBOCore.emitEvent(loading_event);
2346
2347 // render admin widget
2348 VBOCore.renderAdminWidget(widget_id, data).then((content) => {
2349 // stop loading and append widget content to modal
2350 VBOCore.emitEvent(loading_event);
2351 widget_modal.append(content);
2352
2353 // register an admin menu action for the rendered widget
2354 VBOCore.fetchWidgetDetails(widget_id).then((details) => {
2355 // update modal title, if default title is present
2356 if (modal_options.title == VBOCore.options.tn_texts.admin_widget) {
2357 widget_modal
2358 .closest('.vbo-modal-overlay-content')
2359 .find('.vbo-modal-overlay-content-head-title')
2360 .text(data._modalTitle + ' - ' + details.name);
2361 }
2362
2363 // check if this widget returned a particular modal setup
2364 if (details?.modal) {
2365 if (details.modal?.add_class) {
2366 // add custom modal class
2367 widget_modal
2368 .closest('.vbo-modal-overlay-content')
2369 .addClass(details.modal.add_class);
2370
2371 // fire the "resize" event with a delay to support the CSS transition
2372 setTimeout(() => {
2373 VBOCore.emitEvent(resize_event, {
2374 content: widget_modal,
2375 });
2376 }, 400);
2377 }
2378 }
2379
2380 // set data for widget details
2381 widget_modal.data('details', JSON.stringify(details));
2382
2383 // register admin menu action
2384 try {
2385 // work on Local Storage to register the widget data
2386 VBOCore.registerAdminMenuAction({
2387 name: details.name,
2388 href: 'JavaScript: void(0);',
2389 widget: widget_id,
2390 icon: details.icon,
2391 style: details.style,
2392 }, 'widgets');
2393 } catch(e) {
2394 console.error(e);
2395 }
2396 }).catch((error) => {
2397 console.error(error);
2398 });
2399 }).catch((error) => {
2400 // dismiss modal and display error
2401 VBOCore.emitEvent(dismiss_event);
2402 alert(error);
2403 });
2404
2405 return Object.assign(data, modal_options);
2406 }
2407
2408 /**
2409 * Renders an admin widget.
2410 *
2411 * @param string widget_id the widget identifier string to add.
2412 * @param any data the optional multitask data to inject.
2413 *
2414 * @return Promise
2415 */
2416 static renderAdminWidget(widget_id, data) {
2417 return new Promise((resolve, reject) => {
2418 if (!VBOCore.options.widget_ajax_uri) {
2419 reject('Could not load admin widget');
2420 return;
2421 }
2422
2423 var call_method = 'render';
2424 VBOCore.doAjax(
2425 VBOCore.options.widget_ajax_uri,
2426 {
2427 widget_id: widget_id,
2428 call: call_method,
2429 vbo_page: VBOCore.options.current_page,
2430 vbo_uri: VBOCore.options.current_page_uri,
2431 multitask: 1,
2432 multitask_data: data,
2433 },
2434 (response) => {
2435 // parse response
2436 try {
2437 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
2438 if (!obj_res.hasOwnProperty(call_method)) {
2439 reject('Unexpected JSON response');
2440 return;
2441 }
2442 resolve(obj_res[call_method]);
2443 } catch(err) {
2444 reject('could not parse JSON response');
2445 }
2446 },
2447 (error) => {
2448 reject(error.responseText);
2449 }
2450 );
2451 });
2452 }
2453
2454 /**
2455 * Renders an admin widget "inline", by loading the necessary assets.
2456 * To be called outside VikBooking for displaying an admin widget.
2457 *
2458 * @param string widget_id the widget identifier string to add.
2459 * @param any data the optional multitask data to inject.
2460 *
2461 * @return Promise
2462 */
2463 static handleDisplayInlineWidget(widget_id, data) {
2464 return new Promise((resolve, reject) => {
2465 if (VBOCore.options.is_vbo) {
2466 reject('Unable to display the inline widget within VikBooking.');
2467 return;
2468 }
2469
2470 VBOCore.loadAdminWidgetAssets(data).then((assets) => {
2471 // append assets to DOM
2472 assets.forEach((asset) => {
2473 if (!$('link#' + asset['id']).length) {
2474 $('head').append('<link rel="stylesheet" id="' + asset['id'] + '" href="' + asset['href'] + '" media="all" />');
2475 }
2476 });
2477 VBOCore.renderAdminWidget(widget_id, data).then((content) => {
2478 resolve(content);
2479 }).catch((error) => {
2480 reject(error);
2481 });
2482 }).catch((error) => {
2483 reject(error);
2484 });
2485 });
2486 }
2487
2488 /**
2489 * Fetches the details for a specific widget.
2490 *
2491 * @param string widget_id the widget identifier string to fetch.
2492 *
2493 * @return Promise
2494 *
2495 * @since 1.6.7
2496 */
2497 static fetchWidgetDetails(widget_id) {
2498 return new Promise((resolve, reject) => {
2499 if (!VBOCore.options.widget_ajax_uri) {
2500 reject('Could not load admin widget');
2501 return;
2502 }
2503
2504 var call_method = 'getWidgetDetails';
2505 VBOCore.doAjax(
2506 VBOCore.options.widget_ajax_uri,
2507 {
2508 widget_id: widget_id,
2509 call: call_method,
2510 return: 1,
2511 },
2512 (response) => {
2513 // parse response
2514 try {
2515 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
2516 if (!obj_res.hasOwnProperty(call_method)) {
2517 reject('Unexpected JSON response');
2518 return;
2519 }
2520 resolve(obj_res[call_method]);
2521 } catch(err) {
2522 reject('could not parse JSON response');
2523 }
2524 },
2525 (error) => {
2526 reject(error.responseText);
2527 }
2528 );
2529 });
2530 }
2531
2532 /**
2533 * Helper method used to copy the text of an
2534 * input element within the clipboard.
2535 *
2536 * Clipboard copy will take effect only in case the
2537 * function is handled by a DOM event explicitly
2538 * triggered by the user, such as a "click".
2539 *
2540 * @param any input The input containing the text to copy.
2541 *
2542 * @return Promise
2543 */
2544 static copyToClipboard(input) {
2545 // register and return promise
2546 return new Promise((resolve, reject) => {
2547 // define a fallback function
2548 var fallback = function(input) {
2549 // focus the input
2550 input.focus();
2551 // select the text inside the input
2552 input.select();
2553
2554 try {
2555 // try to copy with shell command
2556 var copy = document.execCommand('copy');
2557
2558 if (copy) {
2559 // copied successfully
2560 resolve(copy);
2561 } else {
2562 // unable to copy
2563 reject(copy);
2564 }
2565 } catch (error) {
2566 // unable to exec the command
2567 reject(error);
2568 }
2569 };
2570
2571 // look for navigator clipboard
2572 if (!navigator || !navigator.clipboard) {
2573 // navigator clipboard not supported, use fallback
2574 fallback(input);
2575 return;
2576 }
2577
2578 // try to copy within the clipboard by using the navigator
2579 navigator.clipboard.writeText((input.val ? input.val() : input.value)).then(() => {
2580 // copied successfully
2581 resolve(true);
2582 }).catch((error) => {
2583 // revert to the fallback
2584 fallback(input);
2585 });
2586 });
2587 }
2588
2589 /**
2590 * Helper method used to display a modal window dinamycally.
2591 *
2592 * @param object options The options to render the modal.
2593 * @param object bindDetails Optional data to bind to the modal content.
2594 *
2595 * @return object The modal content element wrapper.
2596 */
2597 static displayModal(options, bindDetails) {
2598 var def_options = {
2599 suffix: (Math.floor(Math.random() * 100000)) + '',
2600 extra_class: null,
2601 header: true,
2602 title: '',
2603 body: '',
2604 body_prepend: false,
2605 lock_scroll: false,
2606 draggable: false,
2607 enlargeable: false,
2608 minimizeable: false,
2609 escape_dismiss: true,
2610 footer_left: null,
2611 footer_right: null,
2612 footer_center: null,
2613 resize_event: null,
2614 dismiss_event: null,
2615 dismissed_event: 'vbo-modal-dismissed',
2616 onDismiss: null,
2617 onMinimize: null,
2618 onRestore: null,
2619 loading_event: null,
2620 loading_body: VBOCore.options.default_loading_body,
2621 progress_event: null,
2622 event_data: null,
2623 };
2624
2625 if (options.event_data && typeof options.event_data === 'object') {
2626 // get rid of possible cyclic object references
2627 options.event_data = Object.assign({}, options.event_data);
2628 }
2629
2630 // merge default options with given options
2631 options = Object.assign(def_options, options);
2632
2633 // create the modal destroy function
2634 const modal_destroy_fn = (e) => {
2635 // invoke callback for onDismiss
2636 if (typeof options.onDismiss === 'function') {
2637 options.onDismiss.call(custom_modal, e);
2638 }
2639
2640 // check if modal did register to the loading event
2641 if (options.loading_event) {
2642 // we can now un-register from the loading event until a new modal is displayed and will register to it again
2643 document.removeEventListener(options.loading_event, modal_handle_loading_event_fn);
2644 if (options.progress_event) {
2645 // we can now un-register from the progress event
2646 document.removeEventListener(options.progress_event, modal_handle_progress_event_fn);
2647 }
2648 }
2649
2650 // check if we should fire the given modal dismissed event
2651 if (options.dismissed_event) {
2652 VBOCore.emitEvent(options.dismissed_event, options.event_data);
2653 }
2654
2655 // check if body scroll lock should be removed
2656 if (options.lock_scroll) {
2657 $('body').removeClass('vbo-modal-lock-scroll');
2658 }
2659
2660 // remove modal from DOM
2661 custom_modal.remove();
2662 };
2663
2664 // create the modal dismiss function
2665 const modal_dismiss_fn = (e) => {
2666 custom_modal.fadeOut(400, () => {
2667 // destroy the modal
2668 modal_destroy_fn.call(custom_modal, e);
2669 });
2670 };
2671
2672 // create the modal loading event handler function
2673 const modal_handle_loading_event_fn = (e) => {
2674 // toggle modal loading
2675 if ($('.vbo-modal-overlay-content-backdrop').length) {
2676 // hide loading
2677 $('.vbo-modal-overlay-content-backdrop').remove();
2678
2679 // do not proceed
2680 return;
2681 }
2682
2683 // show loading
2684 var modal_loading = $('<div></div>').addClass('vbo-modal-overlay-content-backdrop');
2685 var modal_loading_body = $('<div></div>').addClass('vbo-modal-overlay-content-backdrop-body');
2686 if (options.loading_body) {
2687 modal_loading_body.append(options.loading_body);
2688 }
2689 modal_loading.append(modal_loading_body);
2690
2691 // append backdrop loading to modal content
2692 modal_content.prepend(modal_loading);
2693 };
2694
2695 // create the modal progress event handler function
2696 const modal_handle_progress_event_fn = (e) => {
2697 if (!e || !e.detail || !e.detail.progress_content) {
2698 // do not proceed
2699 return;
2700 }
2701
2702 // identify the current loading backdrop body
2703 let backdrop_body = modal_content.find('.vbo-modal-overlay-content-backdrop-body');
2704 if (!backdrop_body.length) {
2705 // loading body not found
2706 return;
2707 }
2708
2709 let backdrop_txt_el = $('<div></div>').addClass('vbo-modal-overlay-content-backdrop-text');
2710 if (typeof e.detail.progress_content === 'string') {
2711 backdrop_txt_el.html(e.detail.progress_content);
2712 } else {
2713 backdrop_txt_el.append(e.detail.progress_content);
2714 }
2715
2716 let backdrop_txt_old = backdrop_body.find('.vbo-modal-overlay-content-backdrop-text');
2717 if (backdrop_txt_old.length) {
2718 // remove the previous text element
2719 backdrop_txt_old.remove();
2720 }
2721
2722 // set the loading progress content
2723 backdrop_body.append(backdrop_txt_el);
2724 };
2725
2726 // create the modal enlarge (toggle) function
2727 const modal_enlarge_fn = (e) => {
2728 // toggle modal fullscreen class
2729 modal_content.toggleClass('vbo-modal-fullscreen');
2730
2731 // check if modal did register a resize event
2732 if (options.resize_event) {
2733 // fire the requested event with a delay to support the CSS transition
2734 setTimeout(() => {
2735 VBOCore.emitEvent(options.resize_event, {
2736 content: modal_content,
2737 });
2738 }, 400);
2739 }
2740 };
2741
2742 // create the modal minimize function
2743 const modal_minimize_fn = (e) => {
2744 // invoke callback for onMinimize
2745 if (typeof options.onMinimize === 'function') {
2746 options.onMinimize.call(custom_modal, e);
2747 }
2748
2749 // start minimizing animation
2750 custom_modal.removeClass('vbo-restoring');
2751 modal_content.removeClass('vbo-restoring');
2752 custom_modal.addClass('vbo-minimizing');
2753 modal_content.addClass('vbo-minimizing');
2754
2755 // access the modal body details data, if any
2756 let details_data = modal_content_wrapper.data('details');
2757 try {
2758 if (details_data && typeof details_data === 'string') {
2759 details_data = JSON.parse(details_data);
2760 }
2761 } catch(e) {
2762 details_data = {};
2763 }
2764
2765 // register delayed partial-distruction
2766 setTimeout(() => {
2767 // hide modal
2768 custom_modal.hide();
2769
2770 // check if body scroll lock should be removed
2771 if (options.lock_scroll) {
2772 $('body').removeClass('vbo-modal-lock-scroll');
2773 }
2774
2775 // add the minimized widget to dock
2776 VBOCore.getAdminDock().addWidget(details_data, options, modal_content_wrapper, modal_destroy_fn);
2777
2778 // remove modal from DOM without firing the dismiss events (do not destroy the modal)
2779 custom_modal.remove();
2780 }, 400);
2781 };
2782
2783 // start the modal position variables
2784 var modal_pos_x = 0, modal_pos_y = 0;
2785
2786 // create the modal drag-start (mousedown) event handler function
2787 const modal_dragstart_fn = (e) => {
2788 e = e || window.event;
2789 e.preventDefault();
2790
2791 if (typeof e.clientX === 'undefined' || typeof e.clientY === 'undefined') {
2792 // unsupported
2793 return;
2794 }
2795
2796 if (e.target && !e.target.matches('.vbo-modal-overlay-cmd')) {
2797 e.target.style.cursor = 'move';
2798 }
2799
2800 // store the initial modal (cursor) position
2801 modal_pos_x = e.clientX;
2802 modal_pos_y = e.clientY;
2803
2804 // register mouseup and mousemove events
2805 document.onmouseup = modal_dragstop_fn;
2806 document.onmousemove = modal_dragmove_fn;
2807 };
2808
2809 // create the modal drag-stop (mouseup) event handler function
2810 const modal_dragstop_fn = (e) => {
2811 e = e || window.event;
2812
2813 if (e.target && !e.target.matches('.vbo-modal-overlay-cmd')) {
2814 e.target.style.cursor = 'auto';
2815 }
2816
2817 // unregister mousemove event
2818 if (document.onmousemove == modal_dragmove_fn) {
2819 document.onmousemove = null;
2820 }
2821
2822 // unregister mouseup event
2823 if (document.onmouseup == modal_dragstop_fn) {
2824 document.onmouseup = null;
2825 }
2826 };
2827
2828 // create the modal drag-move (mousemove) event handler function
2829 const modal_dragmove_fn = (e) => {
2830 e = e || window.event;
2831 e.preventDefault();
2832
2833 if (typeof e.clientX === 'undefined' || typeof e.clientY === 'undefined') {
2834 // unsupported
2835 return;
2836 }
2837
2838 // calculate the new modal (cursor) position
2839 let new_modal_pos_x = modal_pos_x - e.clientX;
2840 let new_modal_pos_y = modal_pos_y - e.clientY;
2841
2842 // update current modal (cursor) position
2843 modal_pos_x = e.clientX;
2844 modal_pos_y = e.clientY;
2845
2846 // find the modal element
2847 let modal_element = e?.target?.closest('.vbo-modal-overlay-content');
2848 if (!modal_element) {
2849 return;
2850 }
2851
2852 // set the modal position
2853 modal_element.style.top = (modal_element.offsetTop - new_modal_pos_y) + 'px';
2854 modal_element.style.left = (modal_element.offsetLeft - new_modal_pos_x) + 'px';
2855 };
2856
2857 // build modal content
2858 const custom_modal = $('<div></div>').addClass('vbo-modal-overlay-block vbo-modal-overlay-' + options.suffix).css('display', 'block');
2859 var modal_dismiss = $('<a></a>').addClass('vbo-modal-overlay-close');
2860 modal_dismiss.on('click', modal_dismiss_fn);
2861 custom_modal.append(modal_dismiss);
2862
2863 const modal_content = $('<div></div>').addClass('vbo-modal-overlay-content vbo-modal-overlay-content-' + options.suffix);
2864 if (options.extra_class && typeof options.extra_class === 'string') {
2865 modal_content.addClass(options.extra_class);
2866 }
2867
2868 // modal head and title
2869 const modal_head = $('<div></div>').addClass('vbo-modal-overlay-content-head');
2870 if (options.title) {
2871 let modal_title = $('<span></span>').addClass('vbo-modal-overlay-content-head-title').html(options.title);
2872 modal_head.append(modal_title);
2873 } else {
2874 modal_head.addClass('vbo-modal-head-no-title');
2875 }
2876 // modal head commands
2877 var modal_head_cmds = $('<span></span>').addClass('vbo-modal-overlay-cmds');
2878 var modal_head_close = $('<span></span>').addClass('vbo-modal-overlay-cmd vbo-modal-overlay-close-times').html('&times;');
2879 modal_head_close.on('click', modal_dismiss_fn);
2880 modal_head_cmds.append(modal_head_close);
2881 if (options.enlargeable) {
2882 var modal_head_enlarge = $('<span></span>').addClass('vbo-modal-overlay-cmd vbo-modal-overlay-cmd-enlarge').html('&square;');
2883 modal_head_enlarge.on('click', modal_enlarge_fn);
2884 modal_head_cmds.append(modal_head_enlarge);
2885 modal_head.on('dblclick', modal_enlarge_fn);
2886 }
2887 if (options.minimizeable && options.event_data) {
2888 // set up the minimize command
2889 var modal_head_minimize = $('<span></span>').addClass('vbo-modal-overlay-cmd vbo-modal-overlay-cmd-minimize').html('&minus;');
2890 modal_head_minimize.on('click', modal_minimize_fn);
2891 modal_head_cmds.append(modal_head_minimize);
2892 }
2893 // set commands
2894 modal_head.append(modal_head_cmds);
2895
2896 // check if the modal head should be draggable
2897 if (options.draggable) {
2898 // register the event(s) to allow dragging
2899 modal_head.addClass('vbo-modal-head-draggable');
2900 modal_head.on('contextmenu', (e) => {
2901 e.preventDefault();
2902 });
2903 modal_head.on('mousedown', modal_dragstart_fn);
2904 }
2905
2906 const modal_body = $('<div></div>').addClass('vbo-modal-overlay-content-body vbo-modal-overlay-content-body-scroll');
2907 const modal_content_wrapper = $('<div></div>').addClass('vbo-modal-' + options.suffix + '-wrap');
2908 if (options.suffix != 'widget_modal') {
2909 modal_content_wrapper.addClass('vbo-modal-widget_modal-wrap');
2910 }
2911 if (typeof options.body === 'string') {
2912 modal_content_wrapper.html(options.body);
2913 } else {
2914 modal_content_wrapper.append(options.body);
2915 }
2916 modal_body.append(modal_content_wrapper);
2917
2918 // modal footer
2919 let modal_footer = null;
2920 if (options.footer_left || options.footer_right || options.footer_center) {
2921 modal_footer = $('<div></div>').addClass('vbo-modal-overlay-content-footer');
2922 if (options.footer_left) {
2923 let modal_footer_left = $('<div></div>').addClass('vbo-modal-overlay-content-footer-left').append(options.footer_left);
2924 modal_footer.append(modal_footer_left);
2925 }
2926 if (options.footer_right) {
2927 let modal_footer_right = $('<div></div>').addClass('vbo-modal-overlay-content-footer-right').append(options.footer_right);
2928 modal_footer.append(modal_footer_right);
2929 }
2930 if (options.footer_center) {
2931 let modal_footer_center = $('<div></div>').addClass('vbo-modal-overlay-content-footer-center').append(options.footer_center);
2932 modal_footer.append(modal_footer_center);
2933 }
2934 }
2935
2936 // finalize modal contents
2937 if (options.header) {
2938 // append header
2939 modal_content.append(modal_head);
2940 }
2941 // append body
2942 modal_content.append(modal_body);
2943 if (modal_footer) {
2944 // append footer
2945 modal_content.append(modal_footer);
2946 }
2947 custom_modal.append(modal_content);
2948
2949 // register to the dismiss event
2950 if (options.dismiss_event) {
2951 // listen to the event that will dismiss the modal
2952 document.addEventListener(options.dismiss_event, function vbo_core_handle_dismiss_event(e) {
2953 // make sure the same event won't propagate again, unless a new modal is displayed (multiple displayModal calls)
2954 e.target.removeEventListener(e.type, vbo_core_handle_dismiss_event);
2955
2956 // invoke the modal dismiss function
2957 modal_dismiss_fn(e);
2958 });
2959
2960 // declare the function to detect the Escape key pressed
2961 const vbo_core_dismiss_event_modal_escape = (e) => {
2962 if (!e.key || e.key != 'Escape') {
2963 return;
2964 }
2965
2966 // immediately unregister from this event once fired
2967 window.removeEventListener(e.type, vbo_core_dismiss_event_modal_escape);
2968
2969 // trigger the actual dismiss event
2970 VBOCore.emitEvent(options.dismiss_event);
2971 };
2972
2973 if (options.escape_dismiss) {
2974 // listen to the Escape keyup event to dismiss the modal
2975 // listen on window to allow admin-widgets to prevent the default behavior and stop the propagation
2976 window.addEventListener('keyup', vbo_core_dismiss_event_modal_escape);
2977 }
2978 }
2979
2980 // register to the toggle-loading event
2981 if (options.loading_event) {
2982 // let a function handle it so that removing the event listener will be doable
2983 document.addEventListener(options.loading_event, modal_handle_loading_event_fn);
2984 if (options.progress_event) {
2985 // let a function handle the progress event so that removing the listener will be doable
2986 document.addEventListener(options.progress_event, modal_handle_progress_event_fn);
2987 }
2988 }
2989
2990 // append (or prepend) modal to body
2991 if ($('.vbo-modal-overlay-' + options.suffix).length) {
2992 $('.vbo-modal-overlay-' + options.suffix).remove();
2993 }
2994 if (options.body_prepend) {
2995 // prepend to body
2996 if ($('body > .vbo-modal-overlay-block').length) {
2997 // we've got other modals prepended to the body, so go after the last one
2998 $('body > .vbo-modal-overlay-block').last().after(custom_modal);
2999 } else {
3000 // place the modal right as the first child node of body
3001 $('body').prepend(custom_modal);
3002 }
3003 } else {
3004 // append to body
3005 $('body').append(custom_modal);
3006 }
3007
3008 // check if scroll should be locked on the whole page body for a "sticky" modal
3009 if (options.lock_scroll) {
3010 $('body').addClass('vbo-modal-lock-scroll');
3011 }
3012
3013 if (typeof bindDetails === 'object') {
3014 try {
3015 // bind widget details data
3016 modal_content_wrapper.data('details', JSON.stringify(bindDetails));
3017 } catch(e) {
3018 // do nothing
3019 }
3020 }
3021
3022 // return the content wrapper element of the new modal
3023 return modal_content_wrapper;
3024 }
3025
3026 /**
3027 * Debounce technique to group a flurry of events into one single event.
3028 */
3029 static debounceEvent(func, wait, immediate) {
3030 var timeout;
3031 return function() {
3032 var context = this, args = arguments;
3033 var later = function() {
3034 timeout = null;
3035 if (!immediate) func.apply(context, args);
3036 };
3037 var callNow = immediate && !timeout;
3038 clearTimeout(timeout);
3039 timeout = setTimeout(later, wait);
3040 if (callNow) {
3041 func.apply(context, args);
3042 }
3043 }
3044 }
3045
3046 /**
3047 * Throttle guarantees a constant flow of events at a given time interval.
3048 * Runs immediately when the event takes place, but can be delayed.
3049 */
3050 static throttleEvent(method, delay) {
3051 var time = Date.now();
3052 return function() {
3053 if ((time + delay - Date.now()) < 0) {
3054 method();
3055 time = Date.now();
3056 }
3057 }
3058 }
3059
3060 /**
3061 * Alternative throttle technique for event listeners.
3062 *
3063 * @param function callback the callback to invoke.
3064 * @param int time the time (in ms) to throttle.
3065 *
3066 * @return void
3067 *
3068 * @since 1.6.8
3069 */
3070 static throttleTimer(callback, time) {
3071 if (VBOCore.throttle_timer) {
3072 // prevent more executions
3073 return;
3074 }
3075
3076 // turn throttle timer on
3077 VBOCore.throttle_timer = true;
3078
3079 setTimeout(() => {
3080 // run the callback with the given delay
3081 callback();
3082
3083 // turn throttle timer off
3084 VBOCore.throttle_timer = false;
3085 }, time);
3086 }
3087
3088 /**
3089 * Tells whether localStorage is supported.
3090 *
3091 * @return boolean
3092 */
3093 static storageSupported() {
3094 return typeof localStorage !== 'undefined';
3095 }
3096
3097 /**
3098 * Gets an item from localStorage.
3099 *
3100 * @param string keyName the storage key identifier.
3101 *
3102 * @return any
3103 */
3104 static storageGetItem(keyName) {
3105 if (!VBOCore.storageSupported()) {
3106 return null;
3107 }
3108
3109 return localStorage.getItem(keyName);
3110 }
3111
3112 /**
3113 * Sets an item to localStorage.
3114 *
3115 * @param string keyName the storage key identifier.
3116 * @param any value the value to store.
3117 *
3118 * @return boolean
3119 */
3120 static storageSetItem(keyName, value) {
3121 if (!VBOCore.storageSupported()) {
3122 return false;
3123 }
3124
3125 try {
3126 if (typeof value === 'object') {
3127 value = JSON.stringify(value);
3128 }
3129
3130 localStorage.setItem(keyName, value);
3131 } catch(e) {
3132 console.error(e);
3133 return false;
3134 }
3135
3136 return true;
3137 }
3138
3139 /**
3140 * Removes an item from localStorage.
3141 *
3142 * @param string keyName the storage key identifier.
3143 *
3144 * @return boolean
3145 */
3146 static storageRemoveItem(keyName) {
3147 if (!VBOCore.storageSupported()) {
3148 return false;
3149 }
3150
3151 localStorage.removeItem(keyName);
3152
3153 return true;
3154 }
3155
3156 /**
3157 * Returns the name of the storage identifier for the given scope.
3158 *
3159 * @param string scope the admin menu scope.
3160 *
3161 * @return string the requested admin menu storage identifier.
3162 */
3163 static getStorageScopeName(scope) {
3164 let storage_scope_name = VBOCore.options.admin_menu_actions_nm;
3165
3166 if (typeof scope === 'string' && scope.length) {
3167 if (scope.indexOf('.') !== 0) {
3168 scope = '.' + scope;
3169 }
3170 storage_scope_name += scope;
3171 }
3172
3173 return storage_scope_name;
3174 }
3175
3176 /**
3177 * Returns a list of admin menu action objects or an empty array.
3178 *
3179 * @param string scope the admin menu scope.
3180 *
3181 * @return Array
3182 */
3183 static getAdminMenuActions(scope) {
3184 let menu_actions = VBOCore.storageGetItem(VBOCore.getStorageScopeName(scope));
3185
3186 if (!menu_actions) {
3187 return [];
3188 }
3189
3190 try {
3191 menu_actions = JSON.parse(menu_actions);
3192 if (!Array.isArray(menu_actions) || !menu_actions.length) {
3193 menu_actions = [];
3194 }
3195 } catch(e) {
3196 return [];
3197 }
3198
3199 return menu_actions;
3200 }
3201
3202 /**
3203 * Builds an admin menu action object with a proper href property.
3204 *
3205 * @param object action the action to build.
3206 *
3207 * @return object
3208 */
3209 static buildAdminMenuAction(action) {
3210 if (typeof action !== 'object' || !action || !action.hasOwnProperty('name')) {
3211 throw new Error('Invalid action object');
3212 }
3213
3214 var action_base = action.hasOwnProperty('href') && typeof action['href'] == 'string' ? action['href'] : window.location.href;
3215 var action_url;
3216
3217 if (action_base.toLowerCase().indexOf('javascript') === 0 && action.hasOwnProperty('widget')) {
3218 // no need to prepare the action URL ("JavaScript: void(0)") as the link will use a JS callback
3219 return action;
3220 }
3221
3222 if (action_base.indexOf('http') !== 0) {
3223 // relative URL
3224 action_url = new URL(action_base, window.location.href);
3225 } else {
3226 // absolute URL
3227 action_url = new URL(action_base);
3228 }
3229
3230 // build proper href with a relative URL
3231 action['href'] = action_url.pathname + action_url.search;
3232
3233 return action;
3234 }
3235
3236 /**
3237 * Registers an admin menu action object.
3238 *
3239 * @param object action the action to build.
3240 * @param string scope the admin menu scope.
3241 *
3242 * @return boolean
3243 */
3244 static registerAdminMenuAction(action, scope) {
3245 // build menu action object
3246 let menu_action_entry = VBOCore.buildAdminMenuAction(action);
3247
3248 let menu_actions = VBOCore.getAdminMenuActions(scope);
3249
3250 // make sure we are not pushing a duplicate and count pinned actions
3251 let pinned_actions = 0;
3252 let unpinned_index = [];
3253 for (let i = 0; i < menu_actions.length; i++) {
3254 // avoid duplicate entries
3255 if (!menu_action_entry.hasOwnProperty('widget') && menu_actions[i]['href'] == menu_action_entry['href']) {
3256 // duplicate link
3257 return false;
3258 }
3259 if (menu_action_entry.hasOwnProperty('widget') && menu_actions[i].hasOwnProperty('widget') && menu_actions[i]['widget'] == menu_action_entry['widget']) {
3260 // duplicate widget
3261 return false;
3262 }
3263 if (menu_actions[i].hasOwnProperty('pinned') && menu_actions[i]['pinned']) {
3264 pinned_actions++;
3265 } else {
3266 unpinned_index.push(i);
3267 }
3268 }
3269
3270 if (pinned_actions >= VBOCore.options.admin_menu_maxactions) {
3271 // no more space to register a new menu action for this admin menu
3272 return false;
3273 }
3274
3275 // splice or pop before prepending to keep current indexes
3276 let tot_menu_actions = menu_actions.length;
3277 if (++tot_menu_actions > VBOCore.options.admin_menu_maxactions) {
3278 if (unpinned_index.length) {
3279 menu_actions.splice(unpinned_index[unpinned_index.length - 1], 1);
3280 } else {
3281 menu_actions.pop();
3282 }
3283 }
3284
3285 // prepend new admin menu action
3286 menu_actions.unshift(menu_action_entry);
3287
3288 return VBOCore.storageSetItem(VBOCore.getStorageScopeName(scope), menu_actions);
3289 }
3290
3291 /**
3292 * Updates an existing admin menu action object.
3293 *
3294 * @param object action the action to build.
3295 * @param string scope the admin menu scope.
3296 * @param number index optional menu action index.
3297 *
3298 * @return boolean
3299 */
3300 static updateAdminMenuAction(action, scope, index) {
3301 // build menu action object
3302 let menu_action_entry = VBOCore.buildAdminMenuAction(action);
3303
3304 let menu_actions = VBOCore.getAdminMenuActions(scope);
3305
3306 if (!menu_actions.length) {
3307 return false;
3308 }
3309
3310 if (typeof index === 'undefined') {
3311 // find the proper index to update by href
3312 for (let i = 0; i < menu_actions.length; i++) {
3313 if (menu_actions[i].hasOwnProperty('widget') && menu_action_entry.hasOwnProperty('widget') && menu_actions[i]['widget'] == menu_action_entry['widget']) {
3314 // existing entry for widget found
3315 index = i;
3316 break;
3317 } else if (!menu_action_entry.hasOwnProperty('widget') && menu_actions[i]['href'] == menu_action_entry['href']) {
3318 // existing entry for link found
3319 index = i;
3320 break;
3321 }
3322 }
3323 }
3324
3325 if (isNaN(index) || !(index in menu_actions)) {
3326 // menu entry index not found
3327 return false;
3328 }
3329
3330 menu_actions[index] = menu_action_entry;
3331
3332 return VBOCore.storageSetItem(VBOCore.getStorageScopeName(scope), menu_actions);
3333 }
3334
3335 /**
3336 * Checks if the current menu actions should be filled with some other actions.
3337 * No update is made over the Local Storage through this method.
3338 *
3339 * @param array menu_actions menu actions to fill.
3340 * @param array widgets_actions default actions to use for filling.
3341 * @param string scope the admin menu scope.
3342 *
3343 * @return array the menu actions eventually filled.
3344 */
3345 static fillAdminMenuActions(menu_actions, widgets_actions, scope) {
3346 if (!Array.isArray(menu_actions) || !Array.isArray(widgets_actions)) {
3347 return [];
3348 }
3349
3350 if (!menu_actions.length || !widgets_actions.length || menu_actions.length >= VBOCore.options.admin_menu_maxactions) {
3351 return menu_actions;
3352 }
3353
3354 var current_widgets = [];
3355 menu_actions.forEach((action, index) => {
3356 current_widgets.push(action['widget']);
3357 });
3358
3359 for (var i = 0; i < widgets_actions.length; i++) {
3360 if (menu_actions.length >= VBOCore.options.admin_menu_maxactions) {
3361 // abort for filling completed
3362 break;
3363 }
3364
3365 if (!widgets_actions[i].hasOwnProperty('widget')) {
3366 // action must be for a widget
3367 continue;
3368 }
3369
3370 if (current_widgets.includes(widgets_actions[i]['widget'])) {
3371 // there cannot be duplicate entries
3372 continue;
3373 }
3374
3375 // fill menu action
3376 menu_actions.push(widgets_actions[i]);
3377 }
3378
3379 return menu_actions;
3380 }
3381
3382 /**
3383 * Formats a date object by accepting "Y", "m" and "d".
3384 *
3385 * @param Date date The date object to format.
3386 * @param string format The date format to use.
3387 *
3388 * @return string
3389 */
3390 static formatDate(date, format) {
3391 if (!date instanceof Date) {
3392 throw new Error('Invalid date object given.');
3393 }
3394
3395 if (!format || typeof format !== 'string') {
3396 // default to military format
3397 format = 'Y-m-d';
3398 }
3399
3400 let year = date.getFullYear();
3401 let month = (date.getMonth() + 1) + '';
3402 let mday = (date.getDate()) + '';
3403
3404 month = month.length < 2 ? '0' + month : month;
3405 mday = mday.length < 2 ? '0' + mday : mday;
3406
3407 // use a regex to capture the date separator and format identifiers
3408 const regexp = /^([ymd])([\s\.\-\/])([ymd])([\s\.\-\/])([ymd])$/gi;
3409
3410 // turn the iterable iterator object into an array by using the spread syntax
3411 let formatArray = [...format.matchAll(regexp)];
3412
3413 if (!formatArray.length || !Array.isArray(formatArray[0]) || formatArray[0].length != 6) {
3414 throw new Error('Invalid date format given.');
3415 }
3416
3417 let firstIdentifier = (formatArray[0][1] + '').toLowerCase();
3418 let separatorChar = formatArray[0][2];
3419 let secondIdentifier = (formatArray[0][3] + '').toLowerCase();
3420 let thirdIdentifier = (formatArray[0][5] + '').toLowerCase();
3421
3422 if (!separatorChar) {
3423 throw new Error('Invalid date format separator given.');
3424 }
3425
3426 let dateParts = [];
3427
3428 if (firstIdentifier == 'y') {
3429 dateParts.push(year);
3430 } else if (firstIdentifier == 'm') {
3431 dateParts.push(month);
3432 } else {
3433 dateParts.push(mday);
3434 }
3435
3436 if (secondIdentifier == 'y') {
3437 dateParts.push(year);
3438 } else if (secondIdentifier == 'm') {
3439 dateParts.push(month);
3440 } else {
3441 dateParts.push(mday);
3442 }
3443
3444 if (thirdIdentifier == 'y') {
3445 dateParts.push(year);
3446 } else if (thirdIdentifier == 'm') {
3447 dateParts.push(month);
3448 } else {
3449 dateParts.push(mday);
3450 }
3451
3452 return dateParts.join(separatorChar);
3453 }
3454
3455 /**
3456 * Proxy to access the VBOCurrency object.
3457 *
3458 * @param object options The currency options.
3459 *
3460 * @return VBOCurrency
3461 */
3462 static getCurrency(options) {
3463 return VBOCurrency.getInstance(options);
3464 }
3465
3466 /**
3467 * Proxy to access the VBOAdminDock object.
3468 *
3469 * @return VBOAdminDock
3470 */
3471 static getAdminDock() {
3472 return VBOAdminDock.getInstance();
3473 }
3474 }
3475
3476 /**
3477 * These used to be private static properties (static #options),
3478 * but they are only supported by quite recent browsers (especially Safari).
3479 * It's too premature, so we decided to keep the class properties public
3480 * without declaring them as static inside the class declaration.
3481 *
3482 * @var object
3483 */
3484 VBOCore.options = CORE_OPTIONS || {
3485 is_vbo: false,
3486 is_vcm: false,
3487 cms: 'wordpress',
3488 widget_ajax_uri: null,
3489 assets_ajax_uri: null,
3490 multitask_ajax_uri: null,
3491 watchdata_ajax_uri: null,
3492 current_page: null,
3493 current_page_uri: null,
3494 root_uri: '/',
3495 client: 'admin',
3496 panel_opts: {},
3497 admin_widgets: [],
3498 events_delegated: [],
3499 notif_audio_url: '',
3500 active_listeners: {},
3501 tn_texts: {
3502 notifs_enabled: 'Browser notifications are enabled!',
3503 notifs_disabled: 'Browser notifications are disabled',
3504 notifs_disabled_help: "Could not enable browser notifications.\nThis feature is available only in secure contexts (HTTPS).",
3505 admin_widget: 'Admin widget',
3506 congrats: 'Congratulations!',
3507 },
3508 default_loading_body: '....',
3509 multitask_save_event: 'vbo-admin-multitask-save',
3510 multitask_open_event: 'vbo-admin-multitask-open',
3511 multitask_close_event: 'vbo-admin-multitask-close',
3512 multitask_shortcut_ev: 'vbo_multitask_shortcut',
3513 multitask_searchfs_ev: 'vbo_multitask_search_focus',
3514 widget_modal_rendered: 'vbo-admin-widget-modal-rendered',
3515 widget_modal_dismissed: 'vbo-widget-modal-dismissed',
3516 admin_menu_maxactions: 3,
3517 admin_menu_actions_nm: 'vikbooking.admin_menu.actions',
3518 service_worker_path: '',
3519 service_worker_scope: './',
3520 push: {
3521 application_key: '',
3522 ajax_url: '',
3523 },
3524 push_options: {
3525 storage_endp_id: 'vbo_push_subscr_endp',
3526 allowed_notif_types: [
3527 'Book',
3528 'Modify',
3529 'Cancel',
3530 'Request',
3531 'CancelRequest',
3532 'Inquiry',
3533 'CancelInquiry',
3534 'Chat',
3535 'Info',
3536 ],
3537 },
3538 };
3539
3540 /**
3541 * @var bool
3542 */
3543 VBOCore.side_panel_on = false;
3544
3545 /**
3546 * @var bool
3547 * @since 1.6.8
3548 */
3549 VBOCore.throttle_timer = false;
3550
3551 /**
3552 * @var array
3553 */
3554 VBOCore.notifications = [];
3555
3556 /**
3557 * @var object
3558 */
3559 VBOCore.widgets_watch_data = null;
3560
3561 /**
3562 * @var number
3563 */
3564 VBOCore.watch_data_interval = null;
3565
3566 /**
3567 * @var object
3568 * @since 1.6.3
3569 */
3570 VBOCore.broadcast_watch_data = null;
3571
3572 /**
3573 * @var object
3574 * @since 1.6.5
3575 */
3576 VBOCore.broadcast_push_data = null;
3577
3578 /**
3579 * @var object
3580 * @since 1.6.8
3581 */
3582 VBOCore.broadcast_watch_events = null;
3583
3584 /**
3585 * @var array<object>
3586 * @since 1.6.5
3587 */
3588 VBOCore.widgets_pushed_data = [];
3589
3590 /**
3591 * Checks if the KeyBoard event matches the given shortcut.
3592 *
3593 * @param array keys The shortcut representation.
3594 *
3595 * @return boolean True if matches, otherwise false.
3596 *
3597 * @since 1.7.0
3598 */
3599 KeyboardEvent.prototype.shortcut = function(keys) {
3600 // get modifiers list
3601 var modifiers = keys.slice(0);
3602 // pop character from modifiers
3603 var keyCode = modifiers.pop();
3604
3605 if (typeof keyCode === 'string') {
3606 // get ASCII
3607 keyCode = keyCode.toUpperCase().charCodeAt(0);
3608 }
3609
3610 // make sure the modifiers are lower case
3611 modifiers = modifiers.map(function(mod) {
3612 return mod.toLowerCase();
3613 });
3614
3615 var ok = false;
3616
3617 // validate key code
3618 if (this.keyCode == keyCode) {
3619 // validate modifiers
3620 ok = true;
3621 var lookup = ['meta', 'shift', 'alt', 'ctrl'];
3622
3623 for (var i = 0; i < lookup.length && ok; i++) {
3624 // check if modifiers is pressed
3625 var mod = this[lookup[i] + 'Key'];
3626
3627 if (mod) {
3628 // if pressed, the shortcut must specify it
3629 ok &= modifiers.indexOf(lookup[i]) !== -1;
3630 } else {
3631 // if not pressed, the shortcut must not include it
3632 ok &= modifiers.indexOf(lookup[i]) === -1;
3633 }
3634 }
3635 }
3636
3637 return ok;
3638 }
3639
3640 /**
3641 * VBOCurrency class implementation.
3642 */
3643 w['VBOCurrency'] = class VBOCurrency {
3644 /**
3645 * Singleton entry-point.
3646 *
3647 * @see construct()
3648 */
3649 static getInstance(options) {
3650 if (typeof VBOCurrency.instance === 'undefined') {
3651 VBOCurrency.instance = new VBOCurrency(options);
3652 }
3653
3654 return VBOCurrency.instance;
3655 }
3656
3657 /**
3658 * Class constructor.
3659 *
3660 * @param object options The currency options object:
3661 * - symbol string The currency symbol (such as €, $, £ and so on).
3662 * - position int The position of the currency (1 before, 2 after). In case the amount is negative
3663 * the space between the currency and the amount won't be used.
3664 * - decimals string The decimals separator character ("." or ",").
3665 * - thousands string The thousands separator character ("," or ".").
3666 * - digits int The number of decimal digits.
3667 * - noDecimals int Whether empty decimals should be omitted (1 true, 0 false).
3668 * - conversionRate float The currency conversion rate.
3669 */
3670 constructor(options) {
3671 this.setOptions(options);
3672 }
3673
3674 /**
3675 * Sets the currency options.
3676 *
3677 * @param object options The currency options object.
3678 *
3679 * @see construct()
3680 */
3681 setOptions(options) {
3682 if (options === undefined) {
3683 options = {};
3684 }
3685
3686 // define default currency options for eventually merging with new options
3687 let def_symbol = this.symbol !== undefined ? this.symbol : '$';
3688 let def_position = this.position !== undefined ? this.position : 1;
3689 let def_decimals = this.decimals !== undefined ? this.decimals : '.';
3690 let def_thousands = this.thousands !== undefined ? this.thousands : ',';
3691 let def_digits = this.digits !== undefined ? this.digits : 2;
3692 let def_no_decimals = this.noDecimals !== undefined ? this.noDecimals : 1;
3693 let def_conv_rate = this.conversionRate !== undefined ? this.conversionRate : 1;
3694
3695 // normalize option properties
3696 if (options.position && /^[a-zA-Z]+$/.test(options.position)) {
3697 // convert alphanumeric position to integer
3698 options.position = options.position.toLowerCase() === 'after' ? 2 : 1;
3699 }
3700
3701 // set currency options
3702 this.symbol = (options.hasOwnProperty('symbol') ? options.symbol : def_symbol);
3703 this.position = (options.hasOwnProperty('position') ? options.position : def_position);
3704 this.decimals = (options.hasOwnProperty('decimals') ? options.decimals : def_decimals);
3705 this.thousands = (options.hasOwnProperty('thousands') ? options.thousands : def_thousands);
3706 this.digits = (options.hasOwnProperty('digits') ? parseInt(options.digits) : def_digits);
3707 this.noDecimals = (options.hasOwnProperty('noDecimals') ? parseInt(options.noDecimals) : def_no_decimals);
3708 this.conversionRate = Math.abs((options.hasOwnProperty('conversionRate') ? parseFloat(options.conversionRate) : 1));
3709 }
3710
3711 /**
3712 * Gets the current currency options.
3713 *
3714 * @return object
3715 */
3716 getOptions() {
3717 let options = {};
3718
3719 Object.keys(this).forEach((option) => {
3720 if (this.hasOwnProperty(option)) {
3721 options[option] = this[option];
3722 }
3723 });
3724
3725 return options;
3726 }
3727
3728 /**
3729 * Formats the given price according to the configuration preferences.
3730 *
3731 * @param float price The price to format.
3732 * @param object options Temporarily overrides the currency options.
3733 *
3734 * @return string The formatted price.
3735 */
3736 format(price, options) {
3737 // merge currency settings
3738 options = Object.assign(this.getOptions(), (options || {}));
3739
3740 let no_decimals = options.noDecimals;
3741 let dig = options.digits;
3742
3743 if (no_decimals && parseInt(price) == price) {
3744 // no decimal digits in case of empty decimals
3745 dig = 0;
3746 }
3747
3748 price = parseFloat(price) / options.conversionRate;
3749
3750 // check whether the price is negative
3751 const isNegative = price < 0;
3752
3753 // adjust to given decimals
3754 price = Math.abs(price).toFixed(dig);
3755
3756 let _d = options.decimals;
3757 let _t = options.thousands;
3758
3759 // make sure the decimal separator is a valid character
3760 if (!_d.match(/[.,\s]/)) {
3761 // revert to default one
3762 _d = '.';
3763 }
3764
3765 // make sure the thousands separator is a valid character
3766 if (!_t.match(/[.,\s]/)) {
3767 // revert to default one
3768 _t = ',';
3769 }
3770
3771 // make sure both the separators are not equals
3772 if (_d == _t) {
3773 _t = _d == ',' ? '.' : ',';
3774 }
3775
3776 price = price.split('.');
3777
3778 price[0] = price[0].replace(/./g, function(c, i, a) {
3779 return i > 0 && (a.length - i) % 3 === 0 ? _t + c : c;
3780 });
3781
3782 if (isNegative) {
3783 // re-add negative sign
3784 price[0] = '-' + price[0];
3785 }
3786
3787 if (price.length > 1) {
3788 price = price[0] + _d + price[1];
3789 } else {
3790 price = price[0];
3791 }
3792
3793 if (Math.abs(options.position) == 1) {
3794 // do not use space in case the position is "-1"
3795 return options.symbol + (options.position == 1 ? ' ' : '') + price;
3796 }
3797
3798 // do not use space in case the position is "-2"
3799 return price + (options.position == 2 ? ' ' : '') + options.symbol;
3800 }
3801
3802 /**
3803 * Displays the given (formatted) price according to the currency settings.
3804 *
3805 * @param string price The price to display (expected to be already formatted).
3806 * @param object options Temporarily overrides the currency options.
3807 *
3808 * @return string The currency and price final string to display.
3809 *
3810 * @since 1.8.3
3811 */
3812 display(price, options) {
3813 // merge currency settings
3814 options = Object.assign(this.getOptions(), (options || {}));
3815
3816 if (Math.abs(options.position) == 1) {
3817 // do not use space in case the position is "-1"
3818 return options.symbol + (options.position == 1 ? ' ' : '') + price;
3819 }
3820
3821 // do not use space in case the position is "-2"
3822 return price + (options.position == 2 ? ' ' : '') + options.symbol;
3823 }
3824
3825 /**
3826 * Safely sums 2 prices (a + b).
3827 *
3828 * @param float a
3829 * @param float b
3830 *
3831 * @return The resulting sum.
3832 */
3833 sum(a, b) {
3834 // get rid of decimals for higher precision
3835 a *= Math.pow(10, this.digits);
3836 b *= Math.pow(10, this.digits);
3837
3838 // do sum and go back to decimal
3839 return (Math.round(a) + Math.round(b)) / Math.pow(10, this.digits);
3840 }
3841
3842 /**
3843 * Safely subtracts 2 prices (a - b).
3844 *
3845 * @param float a
3846 * @param float b
3847 *
3848 * @return The resulting difference.
3849 */
3850 diff(a, b) {
3851 // get rid of decimals for higher precision
3852 a *= Math.pow(10, this.digits);
3853 b *= Math.pow(10, this.digits);
3854
3855 // do difference and go back to decimal
3856 return (Math.round(a) - Math.round(b)) / Math.pow(10, this.digits);
3857 }
3858
3859 /**
3860 * Safely multiplies 2 prices (a * b).
3861 *
3862 * @param float a
3863 * @param float b
3864 *
3865 * @return The resulting multiplication.
3866 */
3867 multiply(a, b) {
3868 // get rid of decimals for higher precision
3869 a *= Math.pow(10, this.digits);
3870 b *= Math.pow(10, this.digits);
3871
3872 // do multiplication and go back to decimal
3873 return (Math.round(a) * Math.round(b)) / Math.pow(10, this.digits * 2);
3874 }
3875 }
3876
3877 /**
3878 * VBOAdminDock class implementation.
3879 */
3880 w['VBOAdminDock'] = class VBOAdminDock {
3881 /**
3882 * Singleton entry-point.
3883 *
3884 * @see construct()
3885 */
3886 static getInstance(options) {
3887 if (typeof VBOAdminDock.instance === 'undefined') {
3888 VBOAdminDock.instance = new VBOAdminDock(options);
3889 }
3890
3891 return VBOAdminDock.instance;
3892 }
3893
3894 /**
3895 * Class constructor.
3896 *
3897 * @param object options The admin dock options object.
3898 */
3899 constructor(options) {
3900 // set options
3901 this.setOptions(options);
3902
3903 // start empty dock elements property
3904 this._elements = [];
3905
3906 // set dock storage identifier property
3907 this._storageId = 'vikbooking.admin_dock.elements';
3908
3909 // set event name for updating the dock element badge counters
3910 this._updateBadgeEv = 'vbo-admin-dock-update-badge';
3911
3912 // build dock node
3913 this.buildDockNode();
3914
3915 // load dock elements
3916 this.loadDockElements();
3917 }
3918
3919 /**
3920 * Sets the options.
3921 *
3922 * @param object options The dock options object.
3923 *
3924 * @see construct()
3925 */
3926 setOptions(options) {
3927 if (options === undefined) {
3928 options = {};
3929 }
3930
3931 // default options
3932 let defaultOptions = {
3933 dockDisplayStyle: 'flex',
3934 };
3935
3936 // set options
3937 this._options = Object.assign(defaultOptions, options);
3938 }
3939
3940 /**
3941 * Gets the current dock options.
3942 *
3943 * @return object
3944 */
3945 getOptions() {
3946 let options = {};
3947
3948 Object.keys(this._options).forEach((option) => {
3949 if (this._options.hasOwnProperty(option)) {
3950 options[option] = this._options[option];
3951 }
3952 });
3953
3954 return options;
3955 }
3956
3957 /**
3958 * Builds the admin dock and adds it to the DOM.
3959 *
3960 * @return HTMLElement
3961 */
3962 buildDockNode() {
3963 if (this._dock) {
3964 return;
3965 }
3966
3967 // create element
3968 this._dock = document.createElement('div');
3969 this._dock.classList.add('vbo-admin-dock-wrapper');
3970
3971 // listen to the event for updating the element badge counters
3972 document.addEventListener(this._updateBadgeEv, this.updateElementsBadgeCounter);
3973
3974 // append element to body
3975 document.querySelector('body').appendChild(this._dock);
3976
3977 return this._dock;
3978 }
3979
3980 /**
3981 * Returns the admin dock node.
3982 *
3983 * @return HTMLElement
3984 */
3985 getDockNode() {
3986 return this._dock || this.buildDockNode();
3987 }
3988
3989 /**
3990 * Removes a given element index from the dock and saves on local storage.
3991 * This method should be called after having removed the element from DOM.
3992 *
3993 * @param number index The element's array index to remove.
3994 *
3995 * @return bool True if the localStorage was updated or false.
3996 */
3997 removeDockElement(index) {
3998 // access the previous element
3999 let previousElement = this._elements[index];
4000
4001 // splice the elements list
4002 this._elements.splice(index, 1);
4003
4004 if (!this._elements.length) {
4005 // hide the dock
4006 this.hideDock();
4007 } else {
4008 // reset dock element indexes
4009 let dom_elements = this.getDockNode().querySelectorAll('.vbo-admin-dock-element');
4010 this._elements.forEach((element, new_index) => {
4011 let widget_id = element.id;
4012 if (!dom_elements[new_index] || dom_elements[new_index].getAttribute('data-id') != widget_id) {
4013 return;
4014 }
4015 dom_elements[new_index].setAttribute('data-index', new_index);
4016 });
4017 }
4018
4019 if (previousElement && previousElement.id == '_tmp' && typeof previousElement?.details?.persist_id === 'string') {
4020 // we have removed a temporary dock element with persisting data so try to clear the localStorage
4021 VBOCore.storageRemoveItem(this._storageId + '.' + previousElement.details.persist_id);
4022 }
4023
4024 // update dock elements on localStorage
4025 let updated = VBOCore.storageSetItem(this._storageId, this._elements);
4026
4027 return updated;
4028 }
4029
4030 /**
4031 * Removes the first dock element found from the given ID.
4032 *
4033 * @param number id The dock element type ID.
4034 *
4035 * @return bool True if element was found, removed and updated on localStorage.
4036 */
4037 removeDockElementById(id) {
4038 let remove_index = this.getDockElementById(id);
4039
4040 if (remove_index >= 0) {
4041 return this.removeDockElement(remove_index);
4042 }
4043
4044 return false;
4045 }
4046
4047 /**
4048 * Returns the index of the first dock element found from the given ID.
4049 *
4050 * @param string id The dock element type ID.
4051 *
4052 * @return number The index found or -1 if nothing is found.
4053 */
4054 getDockElementById(id) {
4055 let index_found = -1;
4056
4057 this._elements.forEach((element, index) => {
4058 if (index_found >= 0) {
4059 return;
4060 }
4061
4062 if (element.id == id) {
4063 index_found = index;
4064 return;
4065 }
4066 });
4067
4068 return index_found;
4069 }
4070
4071 /**
4072 * Builds a dock element node.
4073 *
4074 * @param object details The element details.
4075 * @param object data The element data.
4076 * @param number index The element index number.
4077 * @param HTMLElement body Optional widget modal body to restore.
4078 * @param function destroyFn Optional dismiss callback.
4079 *
4080 * @return HTMLElement
4081 */
4082 buildDockElement(details, data, index, body, destroyFn) {
4083 if (!details?.id) {
4084 throw new Error('Unknown widget id');
4085 }
4086
4087 const widgetId = details.id;
4088
4089 const element = document.createElement('div');
4090 element.classList.add('vbo-admin-dock-element');
4091 element.setAttribute('data-id', widgetId);
4092 element.setAttribute('data-index', index);
4093 element.setAttribute('data-badge-count', '');
4094
4095 let content = document.createElement('div');
4096 content.classList.add('vbo-admin-dock-element-cont');
4097 content.addEventListener('click', (e) => {
4098 // get content target
4099 let target = e.target;
4100 if (!target.matches('.vbo-admin-dock-element-cont')) {
4101 target = target.closest('.vbo-admin-dock-element-cont');
4102 }
4103
4104 // reset badge count
4105 target.closest('.vbo-admin-dock-element').setAttribute('data-badge-count', '');
4106
4107 // get widget data, if any
4108 let widgetData = data?.event_data?._options || data || {};
4109
4110 // get modal body if NOT from localStorage
4111 let prevBody = target.querySelector('.vbo-admin-dock-element-modalbody');
4112
4113 if (!Object.keys(widgetData).length && prevBody && prevBody.children && prevBody.children[0] && prevBody.children[0]?.children?.length) {
4114 // unset dock-minimized attribute on main element with class "vbo-modal-widget_modal-wrap"
4115 prevBody.children[0].setAttribute('data-dock-minimized', 0);
4116 // get the previous style attribute
4117 let prevBodyStyle = prevBody.children[0].getAttribute('style');
4118
4119 // restore admin widget body previously minimized
4120 let prevTitle = data?.title || '';
4121 let nameSuffix = ' - ' + (details?.name || '');
4122 let modalRestore = {
4123 title: prevTitle + (prevTitle.indexOf(nameSuffix) < 0 ? nameSuffix : ''),
4124 // do not immediately set the restored modal body to prevBody.children[0] in order
4125 // to avoid getting duplicate nested elements with class "vbo-modal-widget_modal-wrap"
4126 // body: prevBody.children[0],
4127 };
4128
4129 if (details?.modal?.add_class && data?.extra_class && (data.extra_class + '').indexOf((details.modal.add_class) + '') < 0) {
4130 // restore widget custom class
4131 modalRestore.extra_class = data.extra_class + ' ' + details.modal.add_class;
4132 }
4133
4134 // add modal restoring class
4135 data.extra_class = ((data.extra_class || '') + ' vbo-restoring').trim();
4136
4137 // display modal with previous content and details to bind
4138 let restoredBody = VBOCore.displayModal(
4139 Object.assign(data, modalRestore),
4140 Object.assign({}, details)
4141 );
4142
4143 if (prevBodyStyle) {
4144 (restoredBody[0] || restoredBody).setAttribute('style', prevBodyStyle);
4145 }
4146
4147 // iterate all children elements of main modal body element with class "vbo-modal-widget_modal-wrap"
4148 // to append and restore the original child nodes and to avoid duplicate container elements
4149 Array.from(prevBody.children[0].children).forEach((child) => {
4150 // append child node to the restored modal body
4151 (restoredBody[0] || restoredBody).append(child);
4152 });
4153
4154 try {
4155 if (typeof data?.onRestore === 'function') {
4156 // call the restoring function from the original modal
4157 data.onRestore.call(restoredBody, e);
4158 }
4159
4160 // always emit the native restoring event for the current widget
4161 VBOCore.emitEvent('vbo-admin-dock-restore-' + widgetId, {
4162 data: data?.event_data || {},
4163 });
4164 } catch(e) {
4165 // do nothing
4166 }
4167 } else {
4168 // re-render admin widget from scratch
4169 VBOCore.handleDisplayWidgetNotification({
4170 widget_id: widgetId,
4171 }, widgetData);
4172 }
4173
4174 // remove element from DOM
4175 let elementIndex = element.getAttribute('data-index');
4176 element.remove();
4177
4178 // remove element index from dock
4179 this.removeDockElement(elementIndex);
4180 });
4181
4182 let content_icon = document.createElement('span');
4183 content_icon.classList.add('vbo-admin-dock-element-icn');
4184 if (details?.style) {
4185 content_icon.classList.add('vbo-admin-widget-style-' + details.style);
4186 }
4187 if (details?.icon) {
4188 content_icon.innerHTML = details.icon;
4189 }
4190
4191 if (body) {
4192 // create hidden node
4193 let modal_body = document.createElement('div');
4194 modal_body.classList.add('vbo-admin-dock-element-modalbody');
4195 modal_body.style.display = 'none';
4196
4197 try {
4198 // move modal body to hidden node
4199 modal_body.append((body[0] || body));
4200
4201 // set dock-minimized attribute
4202 (body[0] || body).setAttribute('data-dock-minimized', 1);
4203
4204 // always dispatch the native minimize event for the current widget
4205 VBOCore.emitEvent('vbo-admin-dock-minimize-' + widgetId, {
4206 data: data?.event_data || {},
4207 });
4208 } catch(e) {
4209 // do nothing
4210 }
4211
4212 // append hidden node to content
4213 content.appendChild(modal_body);
4214 }
4215
4216 let content_name = document.createElement('span');
4217 content_name.classList.add('vbo-admin-dock-element-name');
4218 content_name.innerText = details?.name || '';
4219
4220 let dismiss = document.createElement('span');
4221 dismiss.classList.add('vbo-admin-dock-element-dismiss');
4222 dismiss.innerHTML = '&times;';
4223 dismiss.addEventListener('click', (e) => {
4224 try {
4225 if (typeof destroyFn === 'function') {
4226 // destroy the original modal by firing any dismiss event
4227 destroyFn.call(body, e);
4228 }
4229 } catch(e) {
4230 // do nothing
4231 }
4232
4233 // remove element from DOM
4234 let elementIndex = element.getAttribute('data-index');
4235 element.remove();
4236
4237 // remove element index from dock
4238 this.removeDockElement(elementIndex);
4239 });
4240
4241 // append to content
4242 content.appendChild(content_icon);
4243 content.appendChild(content_name);
4244
4245 // append content to element
4246 element.appendChild(content);
4247
4248 // append dismiss to element
4249 element.appendChild(dismiss);
4250
4251 // return the HTMLElement object
4252 return element;
4253 }
4254
4255 /**
4256 * Builds a temporary dock element node.
4257 *
4258 * @param object details The element details.
4259 * @param object data The element data, usually an array of objects.
4260 * @param number index The element index number.
4261 * @param function restoreFn The callback for restoring the data.
4262 * @param function destroyFn The callback for destroying the data.
4263 * @param number badge Optional badge number to set.
4264 *
4265 * @return HTMLElement
4266 */
4267 buildDockTemporaryElement(details, data, index, restoreFn, destroyFn, badge) {
4268 if (!details?.id || details.id != '_tmp') {
4269 throw new Error('Invalid temporary element data.');
4270 }
4271
4272 const dataId = '_tmp';
4273
4274 const element = document.createElement('div');
4275 element.classList.add('vbo-admin-dock-element');
4276 element.setAttribute('data-id', dataId);
4277 element.setAttribute('data-index', index);
4278 element.setAttribute('data-badge-count', (badge || ''));
4279
4280 let content = document.createElement('div');
4281 content.classList.add('vbo-admin-dock-element-cont');
4282 content.addEventListener('click', (e) => {
4283 // get content target
4284 let target = e.target;
4285 if (!target.matches('.vbo-admin-dock-element-cont')) {
4286 target = target.closest('.vbo-admin-dock-element-cont');
4287 }
4288
4289 // reset badge count
4290 target.closest('.vbo-admin-dock-element').setAttribute('data-badge-count', '');
4291
4292 if (typeof restoreFn === 'function') {
4293 this._elements.forEach((dockEl) => {
4294 if (dockEl.id == dataId) {
4295 // invoke the callback by passing the current element data
4296 restoreFn(dockEl?.data);
4297
4298 // abort
4299 return;
4300 }
4301 });
4302 }
4303
4304 // remove element from DOM
4305 let elementIndex = element.getAttribute('data-index');
4306 element.remove();
4307
4308 // remove element index from dock
4309 this.removeDockElement(elementIndex);
4310 });
4311
4312 let content_icon = document.createElement('span');
4313 content_icon.classList.add('vbo-admin-dock-element-icn');
4314 if (details?.style) {
4315 content_icon.classList.add('vbo-admin-widget-style-' + details.style);
4316 }
4317 if (details?.icon) {
4318 content_icon.innerHTML = details.icon;
4319 }
4320
4321 let content_name = document.createElement('span');
4322 content_name.classList.add('vbo-admin-dock-element-name');
4323 content_name.innerText = details?.name || '';
4324
4325 let dismiss = document.createElement('span');
4326 dismiss.classList.add('vbo-admin-dock-element-dismiss');
4327 dismiss.innerHTML = '&times;';
4328 dismiss.addEventListener('click', (e) => {
4329 if (typeof destroyFn === 'function') {
4330 this._elements.forEach((dockEl) => {
4331 if (dockEl.id == dataId) {
4332 // invoke the callback by passing the current element data
4333 destroyFn(dockEl?.data);
4334
4335 // abort
4336 return;
4337 }
4338 });
4339 }
4340
4341 // remove element from DOM
4342 let elementIndex = element.getAttribute('data-index');
4343 element.remove();
4344
4345 // remove element index from dock
4346 this.removeDockElement(elementIndex);
4347 });
4348
4349 // append to content
4350 content.appendChild(content_icon);
4351 content.appendChild(content_name);
4352
4353 // append content to element
4354 element.appendChild(content);
4355
4356 // append dismiss to element
4357 element.appendChild(dismiss);
4358
4359 // return the HTMLElement object
4360 return element;
4361 }
4362
4363 /**
4364 * Adds a widget to the dock.
4365 *
4366 * @param object details The widget details.
4367 * @param object data The widget restoring data.
4368 * @param HTMLElement body Optional widget modal body to restore.
4369 * @param function destroyFn Optional dismiss callback.
4370 */
4371 addWidget(details, data, body, destroyFn) {
4372 if (!VBOCore.options.widget_ajax_uri) {
4373 throw new Error('Wrong environment');
4374 }
4375
4376 if (typeof details !== 'object') {
4377 details = {};
4378 }
4379
4380 if (!details?.id) {
4381 throw new Error('Unknown widget id');
4382 }
4383
4384 // add element to dock in the DOM at first
4385 this.getDockNode().appendChild(
4386 this.buildDockElement(details, data, this._elements.length, body, destroyFn)
4387 );
4388
4389 // push dock element after it was added to the DOM
4390 this._elements.push({
4391 id: details.id,
4392 details: Object.assign({}, details),
4393 data: Object.assign({}, (data?.event_data?._options || {})),
4394 });
4395
4396 // ensure dock is visible
4397 this.showDock();
4398
4399 // update dock elements on localStorage at last
4400 VBOCore.storageSetItem(this._storageId, this._elements);
4401 }
4402
4403 /**
4404 * Adds temporary data to the dock. Data will NOT persist on the same localStorage key.
4405 * Originally introduced to handle the rates update requests within a queue.
4406 *
4407 * @param object details The data details.
4408 * @param object data The data to store, usually an array of objects.
4409 * @param function restoreFn The callback for restoring the data.
4410 * @param function destroyFn The callback for destroying the data.
4411 * @param number badge Optional badge number to set.
4412 */
4413 addTemporaryData(details, data, restoreFn, destroyFn, badge) {
4414 if (!VBOCore.options.widget_ajax_uri) {
4415 throw new Error('Wrong environment');
4416 }
4417
4418 if (typeof details !== 'object') {
4419 details = {};
4420 }
4421
4422 // temporary data will always get a private ID to avoid duplicate entries in the dock
4423 details.id = '_tmp';
4424
4425 // check if the same temporary data element type exists
4426 let previousElement = null;
4427 let previousIndex = null;
4428 this.getElements().forEach((element, index) => {
4429 if (previousElement) {
4430 return;
4431 }
4432 if (element.id == details.id) {
4433 // previous temporary data element type found
4434 previousElement = element;
4435 previousIndex = index;
4436 return;
4437 }
4438 });
4439
4440 if (!previousElement) {
4441 // add temporary element to dock in the DOM at first
4442 this.getDockNode().appendChild(
4443 this.buildDockTemporaryElement(details, data, this._elements.length, restoreFn, destroyFn, (badge ? badge : (Array.isArray(data) ? data.length : null)))
4444 );
4445
4446 // push dock element after it was added to the DOM
4447 this._elements.push({
4448 id: details.id,
4449 details: Object.assign({}, details),
4450 data: Array.isArray(data) ? [...data] : Object.assign({}, data),
4451 });
4452 } else {
4453 // update previous temporary data element type
4454 let updatedData = Array.isArray(data) && Array.isArray(previousElement.data) ? [...previousElement.data].concat([...data]) : [...data];
4455 let updatedBadge = badge ? badge : (Array.isArray(updatedData) ? updatedData.length : previousElement.badge);
4456
4457 // update existing dock element
4458 this._elements[previousIndex] = {
4459 id: details.id,
4460 details: Object.assign({}, previousElement.details, details),
4461 data: updatedData,
4462 };
4463
4464 if (updatedBadge) {
4465 // update DOM element
4466 let dom_elements = this.getDockNode().querySelectorAll('.vbo-admin-dock-element');
4467 this._elements.forEach((element, cur_index) => {
4468 if (!dom_elements[cur_index] || dom_elements[cur_index].getAttribute('data-id') != element.id) {
4469 return;
4470 }
4471 if (dom_elements[cur_index].getAttribute('data-id') != details.id) {
4472 return;
4473 }
4474 // update badge attribute
4475 dom_elements[cur_index].setAttribute('data-badge-count', updatedBadge);
4476 });
4477 }
4478 }
4479
4480 // ensure dock is visible
4481 this.showDock();
4482
4483 if (details.persist_id && typeof details.persist_id === 'string') {
4484 // update dock element on a different localStorage key at last
4485 VBOCore.storageSetItem(this._storageId + '.' + details.persist_id, (previousIndex ? this._elements[previousIndex] : this._elements[this._elements.length - 1]));
4486 }
4487 }
4488
4489 /**
4490 * Loads temporary data onto the dock by using an ID and type identifier.
4491 * Originally introduced to handle the rates update requests within a queue.
4492 *
4493 * @param object details The data details to load.
4494 * @param function restoreFn The callback for restoring the data.
4495 * @param function destroyFn The callback for destroying the data.
4496 * @param number badge Optional badge number to set.
4497 *
4498 * @return any Data object loaded on success or null.
4499 */
4500 loadTemporaryData(details, restoreFn, destroyFn, badge) {
4501 if (!VBOCore.options.widget_ajax_uri) {
4502 throw new Error('Wrong environment');
4503 }
4504
4505 if (typeof details !== 'object' || !details.id || !details.persist_id || typeof details.persist_id !== 'string') {
4506 return null;
4507 }
4508
4509 // temporary data will always get a private ID to avoid duplicate entries in the dock
4510 details.id = '_tmp';
4511
4512 // check if the same temporary data element type exists
4513 let previousElement = null;
4514 this.getElements().forEach((element, index) => {
4515 if (previousElement) {
4516 return;
4517 }
4518 if (element.id == details.id) {
4519 // previous temporary data element type found
4520 previousElement = element;
4521 return;
4522 }
4523 });
4524
4525 if (previousElement) {
4526 // temporary data already loaded in dock
4527 return previousElement;
4528 }
4529
4530 // load the requested temporary and persisting data
4531 let storageElement = VBOCore.storageGetItem(this._storageId + '.' + details.persist_id);
4532
4533 try {
4534 if (typeof storageElement === 'string') {
4535 storageElement = JSON.parse(storageElement);
4536 }
4537 } catch(e) {
4538 storageElement = null;
4539 }
4540
4541 if (!storageElement || typeof storageElement !== 'object' || !storageElement.data) {
4542 return null;
4543 }
4544
4545 // add the temporary data just loaded to the dock
4546 this.addTemporaryData(Object.assign({}, (storageElement.details || {}), details), storageElement.data, restoreFn, destroyFn, badge);
4547
4548 // return the data loaded
4549 return storageElement;
4550 }
4551
4552 /**
4553 * Returns the current dock elements.
4554 *
4555 * @return Array
4556 */
4557 getElements() {
4558 return this._elements;
4559 }
4560
4561 /**
4562 * Loads widget dock elements from localStorage and populates them, if any.
4563 */
4564 loadDockElements() {
4565 let storageElements = VBOCore.storageGetItem(this._storageId) || [];
4566
4567 try {
4568 if (typeof storageElements === 'string') {
4569 storageElements = JSON.parse(storageElements);
4570 }
4571 } catch(e) {
4572 storageElements = [];
4573 }
4574
4575 if (Array.isArray(storageElements)) {
4576 // set current elements
4577 this._elements = storageElements;
4578 }
4579
4580 // scan all elements to ensure no temporary data was stored upon deleting or adding new widgets
4581 this._elements.forEach((element, index) => {
4582 if (element?.id == '_tmp') {
4583 // splice the elements list
4584 this._elements.splice(index, 1);
4585
4586 // update dock elements on localStorage by only keeping real widgets
4587 VBOCore.storageSetItem(this._storageId, this._elements);
4588
4589 // abort
4590 return;
4591 }
4592 });
4593
4594 if (this._elements.length) {
4595 // populate dock elements
4596 this.populateDockElements();
4597
4598 // show the dock
4599 this.showDock();
4600 } else {
4601 // hide the dock
4602 this.hideDock();
4603 }
4604 }
4605
4606 /**
4607 * Populates the current dock elements.
4608 */
4609 populateDockElements() {
4610 const dockNode = this.getDockNode();
4611
4612 // empty the dock
4613 dockNode.innerHTML = '';
4614
4615 // scan all elements
4616 this._elements.forEach((element, index) => {
4617 // add element to dock in the DOM at first
4618 dockNode.appendChild(
4619 this.buildDockElement(element?.details, element?.data, index)
4620 );
4621 });
4622 }
4623
4624 /**
4625 * Hides the dock node.
4626 */
4627 hideDock() {
4628 this.getDockNode().style.display = 'none';
4629 }
4630
4631 /**
4632 * Shows the dock node.
4633 */
4634 showDock() {
4635 if (!this.getDockNode().checkVisibility()) {
4636 this.getDockNode().style.display = this._options.dockDisplayStyle;
4637 }
4638 }
4639
4640 /**
4641 * Event callback fired to update the element(s) badge counter.
4642 */
4643 updateElementsBadgeCounter(e) {
4644 if (!e || !e.detail || !e.detail?.widgetId) {
4645 return;
4646 }
4647
4648 let elements = VBOAdminDock.getInstance().getElements();
4649
4650 if (!elements.length) {
4651 return;
4652 }
4653
4654 let widgetId = e.detail.widgetId;
4655 let badgeCount = parseInt(e.detail?.badgeCount || 0);
4656 let badgeValue = badgeCount > 0 ? badgeCount : '';
4657
4658 elements.forEach((element) => {
4659 if (element.id == widgetId) {
4660 document.querySelectorAll('.vbo-admin-dock-element[data-id="' + widgetId + '"]').forEach((elNode) => {
4661 elNode.setAttribute('data-badge-count', badgeValue);
4662 });
4663 }
4664 });
4665 }
4666 }
4667
4668 })(jQuery, window);