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

4,664 lines 137.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * VikBooking Core v1.8.7
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 resize_event: null,
2613 dismiss_event: null,
2614 dismissed_event: 'vbo-modal-dismissed',
2615 onDismiss: null,
2616 onMinimize: null,
2617 onRestore: null,
2618 loading_event: null,
2619 loading_body: VBOCore.options.default_loading_body,
2620 progress_event: null,
2621 event_data: null,
2622 };
2623
2624 if (options.event_data && typeof options.event_data === 'object') {
2625 // get rid of possible cyclic object references
2626 options.event_data = Object.assign({}, options.event_data);
2627 }
2628
2629 // merge default options with given options
2630 options = Object.assign(def_options, options);
2631
2632 // create the modal destroy function
2633 const modal_destroy_fn = (e) => {
2634 // invoke callback for onDismiss
2635 if (typeof options.onDismiss === 'function') {
2636 options.onDismiss.call(custom_modal, e);
2637 }
2638
2639 // check if modal did register to the loading event
2640 if (options.loading_event) {
2641 // we can now un-register from the loading event until a new modal is displayed and will register to it again
2642 document.removeEventListener(options.loading_event, modal_handle_loading_event_fn);
2643 if (options.progress_event) {
2644 // we can now un-register from the progress event
2645 document.removeEventListener(options.progress_event, modal_handle_progress_event_fn);
2646 }
2647 }
2648
2649 // check if we should fire the given modal dismissed event
2650 if (options.dismissed_event) {
2651 VBOCore.emitEvent(options.dismissed_event, options.event_data);
2652 }
2653
2654 // check if body scroll lock should be removed
2655 if (options.lock_scroll) {
2656 $('body').removeClass('vbo-modal-lock-scroll');
2657 }
2658
2659 // remove modal from DOM
2660 custom_modal.remove();
2661 };
2662
2663 // create the modal dismiss function
2664 const modal_dismiss_fn = (e) => {
2665 custom_modal.fadeOut(400, () => {
2666 // destroy the modal
2667 modal_destroy_fn.call(custom_modal, e);
2668 });
2669 };
2670
2671 // create the modal loading event handler function
2672 const modal_handle_loading_event_fn = (e) => {
2673 // toggle modal loading
2674 if ($('.vbo-modal-overlay-content-backdrop').length) {
2675 // hide loading
2676 $('.vbo-modal-overlay-content-backdrop').remove();
2677
2678 // do not proceed
2679 return;
2680 }
2681
2682 // show loading
2683 var modal_loading = $('<div></div>').addClass('vbo-modal-overlay-content-backdrop');
2684 var modal_loading_body = $('<div></div>').addClass('vbo-modal-overlay-content-backdrop-body');
2685 if (options.loading_body) {
2686 modal_loading_body.append(options.loading_body);
2687 }
2688 modal_loading.append(modal_loading_body);
2689
2690 // append backdrop loading to modal content
2691 modal_content.prepend(modal_loading);
2692 };
2693
2694 // create the modal progress event handler function
2695 const modal_handle_progress_event_fn = (e) => {
2696 if (!e || !e.detail || !e.detail.progress_content) {
2697 // do not proceed
2698 return;
2699 }
2700
2701 // identify the current loading backdrop body
2702 let backdrop_body = modal_content.find('.vbo-modal-overlay-content-backdrop-body');
2703 if (!backdrop_body.length) {
2704 // loading body not found
2705 return;
2706 }
2707
2708 let backdrop_txt_el = $('<div></div>').addClass('vbo-modal-overlay-content-backdrop-text');
2709 if (typeof e.detail.progress_content === 'string') {
2710 backdrop_txt_el.html(e.detail.progress_content);
2711 } else {
2712 backdrop_txt_el.append(e.detail.progress_content);
2713 }
2714
2715 let backdrop_txt_old = backdrop_body.find('.vbo-modal-overlay-content-backdrop-text');
2716 if (backdrop_txt_old.length) {
2717 // remove the previous text element
2718 backdrop_txt_old.remove();
2719 }
2720
2721 // set the loading progress content
2722 backdrop_body.append(backdrop_txt_el);
2723 };
2724
2725 // create the modal enlarge (toggle) function
2726 const modal_enlarge_fn = (e) => {
2727 // toggle modal fullscreen class
2728 modal_content.toggleClass('vbo-modal-fullscreen');
2729
2730 // check if modal did register a resize event
2731 if (options.resize_event) {
2732 // fire the requested event with a delay to support the CSS transition
2733 setTimeout(() => {
2734 VBOCore.emitEvent(options.resize_event, {
2735 content: modal_content,
2736 });
2737 }, 400);
2738 }
2739 };
2740
2741 // create the modal minimize function
2742 const modal_minimize_fn = (e) => {
2743 // invoke callback for onMinimize
2744 if (typeof options.onMinimize === 'function') {
2745 options.onMinimize.call(custom_modal, e);
2746 }
2747
2748 // start minimizing animation
2749 custom_modal.removeClass('vbo-restoring');
2750 modal_content.removeClass('vbo-restoring');
2751 custom_modal.addClass('vbo-minimizing');
2752 modal_content.addClass('vbo-minimizing');
2753
2754 // access the modal body details data, if any
2755 let details_data = modal_content_wrapper.data('details');
2756 try {
2757 if (details_data && typeof details_data === 'string') {
2758 details_data = JSON.parse(details_data);
2759 }
2760 } catch(e) {
2761 details_data = {};
2762 }
2763
2764 // register delayed partial-distruction
2765 setTimeout(() => {
2766 // hide modal
2767 custom_modal.hide();
2768
2769 // check if body scroll lock should be removed
2770 if (options.lock_scroll) {
2771 $('body').removeClass('vbo-modal-lock-scroll');
2772 }
2773
2774 // add the minimized widget to dock
2775 VBOCore.getAdminDock().addWidget(details_data, options, modal_content_wrapper, modal_destroy_fn);
2776
2777 // remove modal from DOM without firing the dismiss events (do not destroy the modal)
2778 custom_modal.remove();
2779 }, 400);
2780 };
2781
2782 // start the modal position variables
2783 var modal_pos_x = 0, modal_pos_y = 0;
2784
2785 // create the modal drag-start (mousedown) event handler function
2786 const modal_dragstart_fn = (e) => {
2787 e = e || window.event;
2788 e.preventDefault();
2789
2790 if (typeof e.clientX === 'undefined' || typeof e.clientY === 'undefined') {
2791 // unsupported
2792 return;
2793 }
2794
2795 if (e.target && !e.target.matches('.vbo-modal-overlay-cmd')) {
2796 e.target.style.cursor = 'move';
2797 }
2798
2799 // store the initial modal (cursor) position
2800 modal_pos_x = e.clientX;
2801 modal_pos_y = e.clientY;
2802
2803 // register mouseup and mousemove events
2804 document.onmouseup = modal_dragstop_fn;
2805 document.onmousemove = modal_dragmove_fn;
2806 };
2807
2808 // create the modal drag-stop (mouseup) event handler function
2809 const modal_dragstop_fn = (e) => {
2810 e = e || window.event;
2811
2812 if (e.target && !e.target.matches('.vbo-modal-overlay-cmd')) {
2813 e.target.style.cursor = 'auto';
2814 }
2815
2816 // unregister mousemove event
2817 if (document.onmousemove == modal_dragmove_fn) {
2818 document.onmousemove = null;
2819 }
2820
2821 // unregister mouseup event
2822 if (document.onmouseup == modal_dragstop_fn) {
2823 document.onmouseup = null;
2824 }
2825 };
2826
2827 // create the modal drag-move (mousemove) event handler function
2828 const modal_dragmove_fn = (e) => {
2829 e = e || window.event;
2830 e.preventDefault();
2831
2832 if (typeof e.clientX === 'undefined' || typeof e.clientY === 'undefined') {
2833 // unsupported
2834 return;
2835 }
2836
2837 // calculate the new modal (cursor) position
2838 let new_modal_pos_x = modal_pos_x - e.clientX;
2839 let new_modal_pos_y = modal_pos_y - e.clientY;
2840
2841 // update current modal (cursor) position
2842 modal_pos_x = e.clientX;
2843 modal_pos_y = e.clientY;
2844
2845 // find the modal element
2846 let modal_element = e?.target?.closest('.vbo-modal-overlay-content');
2847 if (!modal_element) {
2848 return;
2849 }
2850
2851 // set the modal position
2852 modal_element.style.top = (modal_element.offsetTop - new_modal_pos_y) + 'px';
2853 modal_element.style.left = (modal_element.offsetLeft - new_modal_pos_x) + 'px';
2854 };
2855
2856 // build modal content
2857 const custom_modal = $('<div></div>').addClass('vbo-modal-overlay-block vbo-modal-overlay-' + options.suffix).css('display', 'block');
2858 var modal_dismiss = $('<a></a>').addClass('vbo-modal-overlay-close');
2859 modal_dismiss.on('click', modal_dismiss_fn);
2860 custom_modal.append(modal_dismiss);
2861
2862 const modal_content = $('<div></div>').addClass('vbo-modal-overlay-content vbo-modal-overlay-content-' + options.suffix);
2863 if (options.extra_class && typeof options.extra_class === 'string') {
2864 modal_content.addClass(options.extra_class);
2865 }
2866
2867 // modal head and title
2868 const modal_head = $('<div></div>').addClass('vbo-modal-overlay-content-head');
2869 if (options.title) {
2870 let modal_title = $('<span></span>').addClass('vbo-modal-overlay-content-head-title').html(options.title);
2871 modal_head.append(modal_title);
2872 } else {
2873 modal_head.addClass('vbo-modal-head-no-title');
2874 }
2875 // modal head commands
2876 var modal_head_cmds = $('<span></span>').addClass('vbo-modal-overlay-cmds');
2877 var modal_head_close = $('<span></span>').addClass('vbo-modal-overlay-cmd vbo-modal-overlay-close-times').html('&times;');
2878 modal_head_close.on('click', modal_dismiss_fn);
2879 modal_head_cmds.append(modal_head_close);
2880 if (options.enlargeable) {
2881 var modal_head_enlarge = $('<span></span>').addClass('vbo-modal-overlay-cmd vbo-modal-overlay-cmd-enlarge').html('&square;');
2882 modal_head_enlarge.on('click', modal_enlarge_fn);
2883 modal_head_cmds.append(modal_head_enlarge);
2884 modal_head.on('dblclick', modal_enlarge_fn);
2885 }
2886 if (options.minimizeable && options.event_data) {
2887 // set up the minimize command
2888 var modal_head_minimize = $('<span></span>').addClass('vbo-modal-overlay-cmd vbo-modal-overlay-cmd-minimize').html('&minus;');
2889 modal_head_minimize.on('click', modal_minimize_fn);
2890 modal_head_cmds.append(modal_head_minimize);
2891 }
2892 // set commands
2893 modal_head.append(modal_head_cmds);
2894
2895 // check if the modal head should be draggable
2896 if (options.draggable) {
2897 // register the event(s) to allow dragging
2898 modal_head.addClass('vbo-modal-head-draggable');
2899 modal_head.on('contextmenu', (e) => {
2900 e.preventDefault();
2901 });
2902 modal_head.on('mousedown', modal_dragstart_fn);
2903 }
2904
2905 const modal_body = $('<div></div>').addClass('vbo-modal-overlay-content-body vbo-modal-overlay-content-body-scroll');
2906 const modal_content_wrapper = $('<div></div>').addClass('vbo-modal-' + options.suffix + '-wrap');
2907 if (options.suffix != 'widget_modal') {
2908 modal_content_wrapper.addClass('vbo-modal-widget_modal-wrap');
2909 }
2910 if (typeof options.body === 'string') {
2911 modal_content_wrapper.html(options.body);
2912 } else {
2913 modal_content_wrapper.append(options.body);
2914 }
2915 modal_body.append(modal_content_wrapper);
2916
2917 // modal footer
2918 let modal_footer = null;
2919 if (options.footer_left || options.footer_right) {
2920 modal_footer = $('<div></div>').addClass('vbo-modal-overlay-content-footer');
2921 if (options.footer_left) {
2922 let modal_footer_left = $('<div></div>').addClass('vbo-modal-overlay-content-footer-left').append(options.footer_left);
2923 modal_footer.append(modal_footer_left);
2924 }
2925 if (options.footer_right) {
2926 let modal_footer_right = $('<div></div>').addClass('vbo-modal-overlay-content-footer-right').append(options.footer_right);
2927 modal_footer.append(modal_footer_right);
2928 }
2929
2930 }
2931
2932 // finalize modal contents
2933 if (options.header) {
2934 // append header
2935 modal_content.append(modal_head);
2936 }
2937 // append body
2938 modal_content.append(modal_body);
2939 if (modal_footer) {
2940 // append footer
2941 modal_content.append(modal_footer);
2942 }
2943 custom_modal.append(modal_content);
2944
2945 // register to the dismiss event
2946 if (options.dismiss_event) {
2947 // listen to the event that will dismiss the modal
2948 document.addEventListener(options.dismiss_event, function vbo_core_handle_dismiss_event(e) {
2949 // make sure the same event won't propagate again, unless a new modal is displayed (multiple displayModal calls)
2950 e.target.removeEventListener(e.type, vbo_core_handle_dismiss_event);
2951
2952 // invoke the modal dismiss function
2953 modal_dismiss_fn(e);
2954 });
2955
2956 // declare the function to detect the Escape key pressed
2957 const vbo_core_dismiss_event_modal_escape = (e) => {
2958 if (!e.key || e.key != 'Escape') {
2959 return;
2960 }
2961
2962 // immediately unregister from this event once fired
2963 window.removeEventListener(e.type, vbo_core_dismiss_event_modal_escape);
2964
2965 // trigger the actual dismiss event
2966 VBOCore.emitEvent(options.dismiss_event);
2967 };
2968
2969 if (options.escape_dismiss) {
2970 // listen to the Escape keyup event to dismiss the modal
2971 // listen on window to allow admin-widgets to prevent the default behavior and stop the propagation
2972 window.addEventListener('keyup', vbo_core_dismiss_event_modal_escape);
2973 }
2974 }
2975
2976 // register to the toggle-loading event
2977 if (options.loading_event) {
2978 // let a function handle it so that removing the event listener will be doable
2979 document.addEventListener(options.loading_event, modal_handle_loading_event_fn);
2980 if (options.progress_event) {
2981 // let a function handle the progress event so that removing the listener will be doable
2982 document.addEventListener(options.progress_event, modal_handle_progress_event_fn);
2983 }
2984 }
2985
2986 // append (or prepend) modal to body
2987 if ($('.vbo-modal-overlay-' + options.suffix).length) {
2988 $('.vbo-modal-overlay-' + options.suffix).remove();
2989 }
2990 if (options.body_prepend) {
2991 // prepend to body
2992 if ($('body > .vbo-modal-overlay-block').length) {
2993 // we've got other modals prepended to the body, so go after the last one
2994 $('body > .vbo-modal-overlay-block').last().after(custom_modal);
2995 } else {
2996 // place the modal right as the first child node of body
2997 $('body').prepend(custom_modal);
2998 }
2999 } else {
3000 // append to body
3001 $('body').append(custom_modal);
3002 }
3003
3004 // check if scroll should be locked on the whole page body for a "sticky" modal
3005 if (options.lock_scroll) {
3006 $('body').addClass('vbo-modal-lock-scroll');
3007 }
3008
3009 if (typeof bindDetails === 'object') {
3010 try {
3011 // bind widget details data
3012 modal_content_wrapper.data('details', JSON.stringify(bindDetails));
3013 } catch(e) {
3014 // do nothing
3015 }
3016 }
3017
3018 // return the content wrapper element of the new modal
3019 return modal_content_wrapper;
3020 }
3021
3022 /**
3023 * Debounce technique to group a flurry of events into one single event.
3024 */
3025 static debounceEvent(func, wait, immediate) {
3026 var timeout;
3027 return function() {
3028 var context = this, args = arguments;
3029 var later = function() {
3030 timeout = null;
3031 if (!immediate) func.apply(context, args);
3032 };
3033 var callNow = immediate && !timeout;
3034 clearTimeout(timeout);
3035 timeout = setTimeout(later, wait);
3036 if (callNow) {
3037 func.apply(context, args);
3038 }
3039 }
3040 }
3041
3042 /**
3043 * Throttle guarantees a constant flow of events at a given time interval.
3044 * Runs immediately when the event takes place, but can be delayed.
3045 */
3046 static throttleEvent(method, delay) {
3047 var time = Date.now();
3048 return function() {
3049 if ((time + delay - Date.now()) < 0) {
3050 method();
3051 time = Date.now();
3052 }
3053 }
3054 }
3055
3056 /**
3057 * Alternative throttle technique for event listeners.
3058 *
3059 * @param function callback the callback to invoke.
3060 * @param int time the time (in ms) to throttle.
3061 *
3062 * @return void
3063 *
3064 * @since 1.6.8
3065 */
3066 static throttleTimer(callback, time) {
3067 if (VBOCore.throttle_timer) {
3068 // prevent more executions
3069 return;
3070 }
3071
3072 // turn throttle timer on
3073 VBOCore.throttle_timer = true;
3074
3075 setTimeout(() => {
3076 // run the callback with the given delay
3077 callback();
3078
3079 // turn throttle timer off
3080 VBOCore.throttle_timer = false;
3081 }, time);
3082 }
3083
3084 /**
3085 * Tells whether localStorage is supported.
3086 *
3087 * @return boolean
3088 */
3089 static storageSupported() {
3090 return typeof localStorage !== 'undefined';
3091 }
3092
3093 /**
3094 * Gets an item from localStorage.
3095 *
3096 * @param string keyName the storage key identifier.
3097 *
3098 * @return any
3099 */
3100 static storageGetItem(keyName) {
3101 if (!VBOCore.storageSupported()) {
3102 return null;
3103 }
3104
3105 return localStorage.getItem(keyName);
3106 }
3107
3108 /**
3109 * Sets an item to localStorage.
3110 *
3111 * @param string keyName the storage key identifier.
3112 * @param any value the value to store.
3113 *
3114 * @return boolean
3115 */
3116 static storageSetItem(keyName, value) {
3117 if (!VBOCore.storageSupported()) {
3118 return false;
3119 }
3120
3121 try {
3122 if (typeof value === 'object') {
3123 value = JSON.stringify(value);
3124 }
3125
3126 localStorage.setItem(keyName, value);
3127 } catch(e) {
3128 console.error(e);
3129 return false;
3130 }
3131
3132 return true;
3133 }
3134
3135 /**
3136 * Removes an item from localStorage.
3137 *
3138 * @param string keyName the storage key identifier.
3139 *
3140 * @return boolean
3141 */
3142 static storageRemoveItem(keyName) {
3143 if (!VBOCore.storageSupported()) {
3144 return false;
3145 }
3146
3147 localStorage.removeItem(keyName);
3148
3149 return true;
3150 }
3151
3152 /**
3153 * Returns the name of the storage identifier for the given scope.
3154 *
3155 * @param string scope the admin menu scope.
3156 *
3157 * @return string the requested admin menu storage identifier.
3158 */
3159 static getStorageScopeName(scope) {
3160 let storage_scope_name = VBOCore.options.admin_menu_actions_nm;
3161
3162 if (typeof scope === 'string' && scope.length) {
3163 if (scope.indexOf('.') !== 0) {
3164 scope = '.' + scope;
3165 }
3166 storage_scope_name += scope;
3167 }
3168
3169 return storage_scope_name;
3170 }
3171
3172 /**
3173 * Returns a list of admin menu action objects or an empty array.
3174 *
3175 * @param string scope the admin menu scope.
3176 *
3177 * @return Array
3178 */
3179 static getAdminMenuActions(scope) {
3180 let menu_actions = VBOCore.storageGetItem(VBOCore.getStorageScopeName(scope));
3181
3182 if (!menu_actions) {
3183 return [];
3184 }
3185
3186 try {
3187 menu_actions = JSON.parse(menu_actions);
3188 if (!Array.isArray(menu_actions) || !menu_actions.length) {
3189 menu_actions = [];
3190 }
3191 } catch(e) {
3192 return [];
3193 }
3194
3195 return menu_actions;
3196 }
3197
3198 /**
3199 * Builds an admin menu action object with a proper href property.
3200 *
3201 * @param object action the action to build.
3202 *
3203 * @return object
3204 */
3205 static buildAdminMenuAction(action) {
3206 if (typeof action !== 'object' || !action || !action.hasOwnProperty('name')) {
3207 throw new Error('Invalid action object');
3208 }
3209
3210 var action_base = action.hasOwnProperty('href') && typeof action['href'] == 'string' ? action['href'] : window.location.href;
3211 var action_url;
3212
3213 if (action_base.toLowerCase().indexOf('javascript') === 0 && action.hasOwnProperty('widget')) {
3214 // no need to prepare the action URL ("JavaScript: void(0)") as the link will use a JS callback
3215 return action;
3216 }
3217
3218 if (action_base.indexOf('http') !== 0) {
3219 // relative URL
3220 action_url = new URL(action_base, window.location.href);
3221 } else {
3222 // absolute URL
3223 action_url = new URL(action_base);
3224 }
3225
3226 // build proper href with a relative URL
3227 action['href'] = action_url.pathname + action_url.search;
3228
3229 return action;
3230 }
3231
3232 /**
3233 * Registers an admin menu action object.
3234 *
3235 * @param object action the action to build.
3236 * @param string scope the admin menu scope.
3237 *
3238 * @return boolean
3239 */
3240 static registerAdminMenuAction(action, scope) {
3241 // build menu action object
3242 let menu_action_entry = VBOCore.buildAdminMenuAction(action);
3243
3244 let menu_actions = VBOCore.getAdminMenuActions(scope);
3245
3246 // make sure we are not pushing a duplicate and count pinned actions
3247 let pinned_actions = 0;
3248 let unpinned_index = [];
3249 for (let i = 0; i < menu_actions.length; i++) {
3250 // avoid duplicate entries
3251 if (!menu_action_entry.hasOwnProperty('widget') && menu_actions[i]['href'] == menu_action_entry['href']) {
3252 // duplicate link
3253 return false;
3254 }
3255 if (menu_action_entry.hasOwnProperty('widget') && menu_actions[i].hasOwnProperty('widget') && menu_actions[i]['widget'] == menu_action_entry['widget']) {
3256 // duplicate widget
3257 return false;
3258 }
3259 if (menu_actions[i].hasOwnProperty('pinned') && menu_actions[i]['pinned']) {
3260 pinned_actions++;
3261 } else {
3262 unpinned_index.push(i);
3263 }
3264 }
3265
3266 if (pinned_actions >= VBOCore.options.admin_menu_maxactions) {
3267 // no more space to register a new menu action for this admin menu
3268 return false;
3269 }
3270
3271 // splice or pop before prepending to keep current indexes
3272 let tot_menu_actions = menu_actions.length;
3273 if (++tot_menu_actions > VBOCore.options.admin_menu_maxactions) {
3274 if (unpinned_index.length) {
3275 menu_actions.splice(unpinned_index[unpinned_index.length - 1], 1);
3276 } else {
3277 menu_actions.pop();
3278 }
3279 }
3280
3281 // prepend new admin menu action
3282 menu_actions.unshift(menu_action_entry);
3283
3284 return VBOCore.storageSetItem(VBOCore.getStorageScopeName(scope), menu_actions);
3285 }
3286
3287 /**
3288 * Updates an existing admin menu action object.
3289 *
3290 * @param object action the action to build.
3291 * @param string scope the admin menu scope.
3292 * @param number index optional menu action index.
3293 *
3294 * @return boolean
3295 */
3296 static updateAdminMenuAction(action, scope, index) {
3297 // build menu action object
3298 let menu_action_entry = VBOCore.buildAdminMenuAction(action);
3299
3300 let menu_actions = VBOCore.getAdminMenuActions(scope);
3301
3302 if (!menu_actions.length) {
3303 return false;
3304 }
3305
3306 if (typeof index === 'undefined') {
3307 // find the proper index to update by href
3308 for (let i = 0; i < menu_actions.length; i++) {
3309 if (menu_actions[i].hasOwnProperty('widget') && menu_action_entry.hasOwnProperty('widget') && menu_actions[i]['widget'] == menu_action_entry['widget']) {
3310 // existing entry for widget found
3311 index = i;
3312 break;
3313 } else if (!menu_action_entry.hasOwnProperty('widget') && menu_actions[i]['href'] == menu_action_entry['href']) {
3314 // existing entry for link found
3315 index = i;
3316 break;
3317 }
3318 }
3319 }
3320
3321 if (isNaN(index) || !(index in menu_actions)) {
3322 // menu entry index not found
3323 return false;
3324 }
3325
3326 menu_actions[index] = menu_action_entry;
3327
3328 return VBOCore.storageSetItem(VBOCore.getStorageScopeName(scope), menu_actions);
3329 }
3330
3331 /**
3332 * Checks if the current menu actions should be filled with some other actions.
3333 * No update is made over the Local Storage through this method.
3334 *
3335 * @param array menu_actions menu actions to fill.
3336 * @param array widgets_actions default actions to use for filling.
3337 * @param string scope the admin menu scope.
3338 *
3339 * @return array the menu actions eventually filled.
3340 */
3341 static fillAdminMenuActions(menu_actions, widgets_actions, scope) {
3342 if (!Array.isArray(menu_actions) || !Array.isArray(widgets_actions)) {
3343 return [];
3344 }
3345
3346 if (!menu_actions.length || !widgets_actions.length || menu_actions.length >= VBOCore.options.admin_menu_maxactions) {
3347 return menu_actions;
3348 }
3349
3350 var current_widgets = [];
3351 menu_actions.forEach((action, index) => {
3352 current_widgets.push(action['widget']);
3353 });
3354
3355 for (var i = 0; i < widgets_actions.length; i++) {
3356 if (menu_actions.length >= VBOCore.options.admin_menu_maxactions) {
3357 // abort for filling completed
3358 break;
3359 }
3360
3361 if (!widgets_actions[i].hasOwnProperty('widget')) {
3362 // action must be for a widget
3363 continue;
3364 }
3365
3366 if (current_widgets.includes(widgets_actions[i]['widget'])) {
3367 // there cannot be duplicate entries
3368 continue;
3369 }
3370
3371 // fill menu action
3372 menu_actions.push(widgets_actions[i]);
3373 }
3374
3375 return menu_actions;
3376 }
3377
3378 /**
3379 * Formats a date object by accepting "Y", "m" and "d".
3380 *
3381 * @param Date date The date object to format.
3382 * @param string format The date format to use.
3383 *
3384 * @return string
3385 */
3386 static formatDate(date, format) {
3387 if (!date instanceof Date) {
3388 throw new Error('Invalid date object given.');
3389 }
3390
3391 if (!format || typeof format !== 'string') {
3392 // default to military format
3393 format = 'Y-m-d';
3394 }
3395
3396 let year = date.getFullYear();
3397 let month = (date.getMonth() + 1) + '';
3398 let mday = (date.getDate()) + '';
3399
3400 month = month.length < 2 ? '0' + month : month;
3401 mday = mday.length < 2 ? '0' + mday : mday;
3402
3403 // use a regex to capture the date separator and format identifiers
3404 const regexp = /^([ymd])([\s\.\-\/])([ymd])([\s\.\-\/])([ymd])$/gi;
3405
3406 // turn the iterable iterator object into an array by using the spread syntax
3407 let formatArray = [...format.matchAll(regexp)];
3408
3409 if (!formatArray.length || !Array.isArray(formatArray[0]) || formatArray[0].length != 6) {
3410 throw new Error('Invalid date format given.');
3411 }
3412
3413 let firstIdentifier = (formatArray[0][1] + '').toLowerCase();
3414 let separatorChar = formatArray[0][2];
3415 let secondIdentifier = (formatArray[0][3] + '').toLowerCase();
3416 let thirdIdentifier = (formatArray[0][5] + '').toLowerCase();
3417
3418 if (!separatorChar) {
3419 throw new Error('Invalid date format separator given.');
3420 }
3421
3422 let dateParts = [];
3423
3424 if (firstIdentifier == 'y') {
3425 dateParts.push(year);
3426 } else if (firstIdentifier == 'm') {
3427 dateParts.push(month);
3428 } else {
3429 dateParts.push(mday);
3430 }
3431
3432 if (secondIdentifier == 'y') {
3433 dateParts.push(year);
3434 } else if (secondIdentifier == 'm') {
3435 dateParts.push(month);
3436 } else {
3437 dateParts.push(mday);
3438 }
3439
3440 if (thirdIdentifier == 'y') {
3441 dateParts.push(year);
3442 } else if (thirdIdentifier == 'm') {
3443 dateParts.push(month);
3444 } else {
3445 dateParts.push(mday);
3446 }
3447
3448 return dateParts.join(separatorChar);
3449 }
3450
3451 /**
3452 * Proxy to access the VBOCurrency object.
3453 *
3454 * @param object options The currency options.
3455 *
3456 * @return VBOCurrency
3457 */
3458 static getCurrency(options) {
3459 return VBOCurrency.getInstance(options);
3460 }
3461
3462 /**
3463 * Proxy to access the VBOAdminDock object.
3464 *
3465 * @return VBOAdminDock
3466 */
3467 static getAdminDock() {
3468 return VBOAdminDock.getInstance();
3469 }
3470 }
3471
3472 /**
3473 * These used to be private static properties (static #options),
3474 * but they are only supported by quite recent browsers (especially Safari).
3475 * It's too premature, so we decided to keep the class properties public
3476 * without declaring them as static inside the class declaration.
3477 *
3478 * @var object
3479 */
3480 VBOCore.options = CORE_OPTIONS || {
3481 is_vbo: false,
3482 is_vcm: false,
3483 cms: 'wordpress',
3484 widget_ajax_uri: null,
3485 assets_ajax_uri: null,
3486 multitask_ajax_uri: null,
3487 watchdata_ajax_uri: null,
3488 current_page: null,
3489 current_page_uri: null,
3490 root_uri: '/',
3491 client: 'admin',
3492 panel_opts: {},
3493 admin_widgets: [],
3494 events_delegated: [],
3495 notif_audio_url: '',
3496 active_listeners: {},
3497 tn_texts: {
3498 notifs_enabled: 'Browser notifications are enabled!',
3499 notifs_disabled: 'Browser notifications are disabled',
3500 notifs_disabled_help: "Could not enable browser notifications.\nThis feature is available only in secure contexts (HTTPS).",
3501 admin_widget: 'Admin widget',
3502 congrats: 'Congratulations!',
3503 },
3504 default_loading_body: '....',
3505 multitask_save_event: 'vbo-admin-multitask-save',
3506 multitask_open_event: 'vbo-admin-multitask-open',
3507 multitask_close_event: 'vbo-admin-multitask-close',
3508 multitask_shortcut_ev: 'vbo_multitask_shortcut',
3509 multitask_searchfs_ev: 'vbo_multitask_search_focus',
3510 widget_modal_rendered: 'vbo-admin-widget-modal-rendered',
3511 widget_modal_dismissed: 'vbo-widget-modal-dismissed',
3512 admin_menu_maxactions: 3,
3513 admin_menu_actions_nm: 'vikbooking.admin_menu.actions',
3514 service_worker_path: '',
3515 service_worker_scope: './',
3516 push: {
3517 application_key: '',
3518 ajax_url: '',
3519 },
3520 push_options: {
3521 storage_endp_id: 'vbo_push_subscr_endp',
3522 allowed_notif_types: [
3523 'Book',
3524 'Modify',
3525 'Cancel',
3526 'Request',
3527 'CancelRequest',
3528 'Inquiry',
3529 'CancelInquiry',
3530 'Chat',
3531 'Info',
3532 ],
3533 },
3534 };
3535
3536 /**
3537 * @var bool
3538 */
3539 VBOCore.side_panel_on = false;
3540
3541 /**
3542 * @var bool
3543 * @since 1.6.8
3544 */
3545 VBOCore.throttle_timer = false;
3546
3547 /**
3548 * @var array
3549 */
3550 VBOCore.notifications = [];
3551
3552 /**
3553 * @var object
3554 */
3555 VBOCore.widgets_watch_data = null;
3556
3557 /**
3558 * @var number
3559 */
3560 VBOCore.watch_data_interval = null;
3561
3562 /**
3563 * @var object
3564 * @since 1.6.3
3565 */
3566 VBOCore.broadcast_watch_data = null;
3567
3568 /**
3569 * @var object
3570 * @since 1.6.5
3571 */
3572 VBOCore.broadcast_push_data = null;
3573
3574 /**
3575 * @var object
3576 * @since 1.6.8
3577 */
3578 VBOCore.broadcast_watch_events = null;
3579
3580 /**
3581 * @var array<object>
3582 * @since 1.6.5
3583 */
3584 VBOCore.widgets_pushed_data = [];
3585
3586 /**
3587 * Checks if the KeyBoard event matches the given shortcut.
3588 *
3589 * @param array keys The shortcut representation.
3590 *
3591 * @return boolean True if matches, otherwise false.
3592 *
3593 * @since 1.7.0
3594 */
3595 KeyboardEvent.prototype.shortcut = function(keys) {
3596 // get modifiers list
3597 var modifiers = keys.slice(0);
3598 // pop character from modifiers
3599 var keyCode = modifiers.pop();
3600
3601 if (typeof keyCode === 'string') {
3602 // get ASCII
3603 keyCode = keyCode.toUpperCase().charCodeAt(0);
3604 }
3605
3606 // make sure the modifiers are lower case
3607 modifiers = modifiers.map(function(mod) {
3608 return mod.toLowerCase();
3609 });
3610
3611 var ok = false;
3612
3613 // validate key code
3614 if (this.keyCode == keyCode) {
3615 // validate modifiers
3616 ok = true;
3617 var lookup = ['meta', 'shift', 'alt', 'ctrl'];
3618
3619 for (var i = 0; i < lookup.length && ok; i++) {
3620 // check if modifiers is pressed
3621 var mod = this[lookup[i] + 'Key'];
3622
3623 if (mod) {
3624 // if pressed, the shortcut must specify it
3625 ok &= modifiers.indexOf(lookup[i]) !== -1;
3626 } else {
3627 // if not pressed, the shortcut must not include it
3628 ok &= modifiers.indexOf(lookup[i]) === -1;
3629 }
3630 }
3631 }
3632
3633 return ok;
3634 }
3635
3636 /**
3637 * VBOCurrency class implementation.
3638 */
3639 w['VBOCurrency'] = class VBOCurrency {
3640 /**
3641 * Singleton entry-point.
3642 *
3643 * @see construct()
3644 */
3645 static getInstance(options) {
3646 if (typeof VBOCurrency.instance === 'undefined') {
3647 VBOCurrency.instance = new VBOCurrency(options);
3648 }
3649
3650 return VBOCurrency.instance;
3651 }
3652
3653 /**
3654 * Class constructor.
3655 *
3656 * @param object options The currency options object:
3657 * - symbol string The currency symbol (such as €, $, £ and so on).
3658 * - position int The position of the currency (1 before, 2 after). In case the amount is negative
3659 * the space between the currency and the amount won't be used.
3660 * - decimals string The decimals separator character ("." or ",").
3661 * - thousands string The thousands separator character ("," or ".").
3662 * - digits int The number of decimal digits.
3663 * - noDecimals int Whether empty decimals should be omitted (1 true, 0 false).
3664 * - conversionRate float The currency conversion rate.
3665 */
3666 constructor(options) {
3667 this.setOptions(options);
3668 }
3669
3670 /**
3671 * Sets the currency options.
3672 *
3673 * @param object options The currency options object.
3674 *
3675 * @see construct()
3676 */
3677 setOptions(options) {
3678 if (options === undefined) {
3679 options = {};
3680 }
3681
3682 // define default currency options for eventually merging with new options
3683 let def_symbol = this.symbol !== undefined ? this.symbol : '$';
3684 let def_position = this.position !== undefined ? this.position : 1;
3685 let def_decimals = this.decimals !== undefined ? this.decimals : '.';
3686 let def_thousands = this.thousands !== undefined ? this.thousands : ',';
3687 let def_digits = this.digits !== undefined ? this.digits : 2;
3688 let def_no_decimals = this.noDecimals !== undefined ? this.noDecimals : 1;
3689 let def_conv_rate = this.conversionRate !== undefined ? this.conversionRate : 1;
3690
3691 // normalize option properties
3692 if (options.position && /^[a-zA-Z]+$/.test(options.position)) {
3693 // convert alphanumeric position to integer
3694 options.position = options.position.toLowerCase() === 'after' ? 2 : 1;
3695 }
3696
3697 // set currency options
3698 this.symbol = (options.hasOwnProperty('symbol') ? options.symbol : def_symbol);
3699 this.position = (options.hasOwnProperty('position') ? options.position : def_position);
3700 this.decimals = (options.hasOwnProperty('decimals') ? options.decimals : def_decimals);
3701 this.thousands = (options.hasOwnProperty('thousands') ? options.thousands : def_thousands);
3702 this.digits = (options.hasOwnProperty('digits') ? parseInt(options.digits) : def_digits);
3703 this.noDecimals = (options.hasOwnProperty('noDecimals') ? parseInt(options.noDecimals) : def_no_decimals);
3704 this.conversionRate = Math.abs((options.hasOwnProperty('conversionRate') ? parseFloat(options.conversionRate) : 1));
3705 }
3706
3707 /**
3708 * Gets the current currency options.
3709 *
3710 * @return object
3711 */
3712 getOptions() {
3713 let options = {};
3714
3715 Object.keys(this).forEach((option) => {
3716 if (this.hasOwnProperty(option)) {
3717 options[option] = this[option];
3718 }
3719 });
3720
3721 return options;
3722 }
3723
3724 /**
3725 * Formats the given price according to the configuration preferences.
3726 *
3727 * @param float price The price to format.
3728 * @param object options Temporarily overrides the currency options.
3729 *
3730 * @return string The formatted price.
3731 */
3732 format(price, options) {
3733 // merge currency settings
3734 options = Object.assign(this.getOptions(), (options || {}));
3735
3736 let no_decimals = options.noDecimals;
3737 let dig = options.digits;
3738
3739 if (no_decimals && parseInt(price) == price) {
3740 // no decimal digits in case of empty decimals
3741 dig = 0;
3742 }
3743
3744 price = parseFloat(price) / options.conversionRate;
3745
3746 // check whether the price is negative
3747 const isNegative = price < 0;
3748
3749 // adjust to given decimals
3750 price = Math.abs(price).toFixed(dig);
3751
3752 let _d = options.decimals;
3753 let _t = options.thousands;
3754
3755 // make sure the decimal separator is a valid character
3756 if (!_d.match(/[.,\s]/)) {
3757 // revert to default one
3758 _d = '.';
3759 }
3760
3761 // make sure the thousands separator is a valid character
3762 if (!_t.match(/[.,\s]/)) {
3763 // revert to default one
3764 _t = ',';
3765 }
3766
3767 // make sure both the separators are not equals
3768 if (_d == _t) {
3769 _t = _d == ',' ? '.' : ',';
3770 }
3771
3772 price = price.split('.');
3773
3774 price[0] = price[0].replace(/./g, function(c, i, a) {
3775 return i > 0 && (a.length - i) % 3 === 0 ? _t + c : c;
3776 });
3777
3778 if (isNegative) {
3779 // re-add negative sign
3780 price[0] = '-' + price[0];
3781 }
3782
3783 if (price.length > 1) {
3784 price = price[0] + _d + price[1];
3785 } else {
3786 price = price[0];
3787 }
3788
3789 if (Math.abs(options.position) == 1) {
3790 // do not use space in case the position is "-1"
3791 return options.symbol + (options.position == 1 ? ' ' : '') + price;
3792 }
3793
3794 // do not use space in case the position is "-2"
3795 return price + (options.position == 2 ? ' ' : '') + options.symbol;
3796 }
3797
3798 /**
3799 * Displays the given (formatted) price according to the currency settings.
3800 *
3801 * @param string price The price to display (expected to be already formatted).
3802 * @param object options Temporarily overrides the currency options.
3803 *
3804 * @return string The currency and price final string to display.
3805 *
3806 * @since 1.8.3
3807 */
3808 display(price, options) {
3809 // merge currency settings
3810 options = Object.assign(this.getOptions(), (options || {}));
3811
3812 if (Math.abs(options.position) == 1) {
3813 // do not use space in case the position is "-1"
3814 return options.symbol + (options.position == 1 ? ' ' : '') + price;
3815 }
3816
3817 // do not use space in case the position is "-2"
3818 return price + (options.position == 2 ? ' ' : '') + options.symbol;
3819 }
3820
3821 /**
3822 * Safely sums 2 prices (a + b).
3823 *
3824 * @param float a
3825 * @param float b
3826 *
3827 * @return The resulting sum.
3828 */
3829 sum(a, b) {
3830 // get rid of decimals for higher precision
3831 a *= Math.pow(10, this.digits);
3832 b *= Math.pow(10, this.digits);
3833
3834 // do sum and go back to decimal
3835 return (Math.round(a) + Math.round(b)) / Math.pow(10, this.digits);
3836 }
3837
3838 /**
3839 * Safely subtracts 2 prices (a - b).
3840 *
3841 * @param float a
3842 * @param float b
3843 *
3844 * @return The resulting difference.
3845 */
3846 diff(a, b) {
3847 // get rid of decimals for higher precision
3848 a *= Math.pow(10, this.digits);
3849 b *= Math.pow(10, this.digits);
3850
3851 // do difference and go back to decimal
3852 return (Math.round(a) - Math.round(b)) / Math.pow(10, this.digits);
3853 }
3854
3855 /**
3856 * Safely multiplies 2 prices (a * b).
3857 *
3858 * @param float a
3859 * @param float b
3860 *
3861 * @return The resulting multiplication.
3862 */
3863 multiply(a, b) {
3864 // get rid of decimals for higher precision
3865 a *= Math.pow(10, this.digits);
3866 b *= Math.pow(10, this.digits);
3867
3868 // do multiplication and go back to decimal
3869 return (Math.round(a) * Math.round(b)) / Math.pow(10, this.digits * 2);
3870 }
3871 }
3872
3873 /**
3874 * VBOAdminDock class implementation.
3875 */
3876 w['VBOAdminDock'] = class VBOAdminDock {
3877 /**
3878 * Singleton entry-point.
3879 *
3880 * @see construct()
3881 */
3882 static getInstance(options) {
3883 if (typeof VBOAdminDock.instance === 'undefined') {
3884 VBOAdminDock.instance = new VBOAdminDock(options);
3885 }
3886
3887 return VBOAdminDock.instance;
3888 }
3889
3890 /**
3891 * Class constructor.
3892 *
3893 * @param object options The admin dock options object.
3894 */
3895 constructor(options) {
3896 // set options
3897 this.setOptions(options);
3898
3899 // start empty dock elements property
3900 this._elements = [];
3901
3902 // set dock storage identifier property
3903 this._storageId = 'vikbooking.admin_dock.elements';
3904
3905 // set event name for updating the dock element badge counters
3906 this._updateBadgeEv = 'vbo-admin-dock-update-badge';
3907
3908 // build dock node
3909 this.buildDockNode();
3910
3911 // load dock elements
3912 this.loadDockElements();
3913 }
3914
3915 /**
3916 * Sets the options.
3917 *
3918 * @param object options The dock options object.
3919 *
3920 * @see construct()
3921 */
3922 setOptions(options) {
3923 if (options === undefined) {
3924 options = {};
3925 }
3926
3927 // default options
3928 let defaultOptions = {
3929 dockDisplayStyle: 'flex',
3930 };
3931
3932 // set options
3933 this._options = Object.assign(defaultOptions, options);
3934 }
3935
3936 /**
3937 * Gets the current dock options.
3938 *
3939 * @return object
3940 */
3941 getOptions() {
3942 let options = {};
3943
3944 Object.keys(this._options).forEach((option) => {
3945 if (this._options.hasOwnProperty(option)) {
3946 options[option] = this._options[option];
3947 }
3948 });
3949
3950 return options;
3951 }
3952
3953 /**
3954 * Builds the admin dock and adds it to the DOM.
3955 *
3956 * @return HTMLElement
3957 */
3958 buildDockNode() {
3959 if (this._dock) {
3960 return;
3961 }
3962
3963 // create element
3964 this._dock = document.createElement('div');
3965 this._dock.classList.add('vbo-admin-dock-wrapper');
3966
3967 // listen to the event for updating the element badge counters
3968 document.addEventListener(this._updateBadgeEv, this.updateElementsBadgeCounter);
3969
3970 // append element to body
3971 document.querySelector('body').appendChild(this._dock);
3972
3973 return this._dock;
3974 }
3975
3976 /**
3977 * Returns the admin dock node.
3978 *
3979 * @return HTMLElement
3980 */
3981 getDockNode() {
3982 return this._dock || this.buildDockNode();
3983 }
3984
3985 /**
3986 * Removes a given element index from the dock and saves on local storage.
3987 * This method should be called after having removed the element from DOM.
3988 *
3989 * @param number index The element's array index to remove.
3990 *
3991 * @return bool True if the localStorage was updated or false.
3992 */
3993 removeDockElement(index) {
3994 // access the previous element
3995 let previousElement = this._elements[index];
3996
3997 // splice the elements list
3998 this._elements.splice(index, 1);
3999
4000 if (!this._elements.length) {
4001 // hide the dock
4002 this.hideDock();
4003 } else {
4004 // reset dock element indexes
4005 let dom_elements = this.getDockNode().querySelectorAll('.vbo-admin-dock-element');
4006 this._elements.forEach((element, new_index) => {
4007 let widget_id = element.id;
4008 if (!dom_elements[new_index] || dom_elements[new_index].getAttribute('data-id') != widget_id) {
4009 return;
4010 }
4011 dom_elements[new_index].setAttribute('data-index', new_index);
4012 });
4013 }
4014
4015 if (previousElement && previousElement.id == '_tmp' && typeof previousElement?.details?.persist_id === 'string') {
4016 // we have removed a temporary dock element with persisting data so try to clear the localStorage
4017 VBOCore.storageRemoveItem(this._storageId + '.' + previousElement.details.persist_id);
4018 }
4019
4020 // update dock elements on localStorage
4021 let updated = VBOCore.storageSetItem(this._storageId, this._elements);
4022
4023 return updated;
4024 }
4025
4026 /**
4027 * Removes the first dock element found from the given ID.
4028 *
4029 * @param number id The dock element type ID.
4030 *
4031 * @return bool True if element was found, removed and updated on localStorage.
4032 */
4033 removeDockElementById(id) {
4034 let remove_index = this.getDockElementById(id);
4035
4036 if (remove_index >= 0) {
4037 return this.removeDockElement(remove_index);
4038 }
4039
4040 return false;
4041 }
4042
4043 /**
4044 * Returns the index of the first dock element found from the given ID.
4045 *
4046 * @param string id The dock element type ID.
4047 *
4048 * @return number The index found or -1 if nothing is found.
4049 */
4050 getDockElementById(id) {
4051 let index_found = -1;
4052
4053 this._elements.forEach((element, index) => {
4054 if (index_found >= 0) {
4055 return;
4056 }
4057
4058 if (element.id == id) {
4059 index_found = index;
4060 return;
4061 }
4062 });
4063
4064 return index_found;
4065 }
4066
4067 /**
4068 * Builds a dock element node.
4069 *
4070 * @param object details The element details.
4071 * @param object data The element data.
4072 * @param number index The element index number.
4073 * @param HTMLElement body Optional widget modal body to restore.
4074 * @param function destroyFn Optional dismiss callback.
4075 *
4076 * @return HTMLElement
4077 */
4078 buildDockElement(details, data, index, body, destroyFn) {
4079 if (!details?.id) {
4080 throw new Error('Unknown widget id');
4081 }
4082
4083 const widgetId = details.id;
4084
4085 const element = document.createElement('div');
4086 element.classList.add('vbo-admin-dock-element');
4087 element.setAttribute('data-id', widgetId);
4088 element.setAttribute('data-index', index);
4089 element.setAttribute('data-badge-count', '');
4090
4091 let content = document.createElement('div');
4092 content.classList.add('vbo-admin-dock-element-cont');
4093 content.addEventListener('click', (e) => {
4094 // get content target
4095 let target = e.target;
4096 if (!target.matches('.vbo-admin-dock-element-cont')) {
4097 target = target.closest('.vbo-admin-dock-element-cont');
4098 }
4099
4100 // reset badge count
4101 target.closest('.vbo-admin-dock-element').setAttribute('data-badge-count', '');
4102
4103 // get widget data, if any
4104 let widgetData = data?.event_data?._options || data || {};
4105
4106 // get modal body if NOT from localStorage
4107 let prevBody = target.querySelector('.vbo-admin-dock-element-modalbody');
4108
4109 if (!Object.keys(widgetData).length && prevBody && prevBody.children && prevBody.children[0] && prevBody.children[0]?.children?.length) {
4110 // unset dock-minimized attribute on main element with class "vbo-modal-widget_modal-wrap"
4111 prevBody.children[0].setAttribute('data-dock-minimized', 0);
4112 // get the previous style attribute
4113 let prevBodyStyle = prevBody.children[0].getAttribute('style');
4114
4115 // restore admin widget body previously minimized
4116 let prevTitle = data?.title || '';
4117 let nameSuffix = ' - ' + (details?.name || '');
4118 let modalRestore = {
4119 title: prevTitle + (prevTitle.indexOf(nameSuffix) < 0 ? nameSuffix : ''),
4120 // do not immediately set the restored modal body to prevBody.children[0] in order
4121 // to avoid getting duplicate nested elements with class "vbo-modal-widget_modal-wrap"
4122 // body: prevBody.children[0],
4123 };
4124
4125 if (details?.modal?.add_class && data?.extra_class && (data.extra_class + '').indexOf((details.modal.add_class) + '') < 0) {
4126 // restore widget custom class
4127 modalRestore.extra_class = data.extra_class + ' ' + details.modal.add_class;
4128 }
4129
4130 // add modal restoring class
4131 data.extra_class = ((data.extra_class || '') + ' vbo-restoring').trim();
4132
4133 // display modal with previous content and details to bind
4134 let restoredBody = VBOCore.displayModal(
4135 Object.assign(data, modalRestore),
4136 Object.assign({}, details)
4137 );
4138
4139 if (prevBodyStyle) {
4140 (restoredBody[0] || restoredBody).setAttribute('style', prevBodyStyle);
4141 }
4142
4143 // iterate all children elements of main modal body element with class "vbo-modal-widget_modal-wrap"
4144 // to append and restore the original child nodes and to avoid duplicate container elements
4145 Array.from(prevBody.children[0].children).forEach((child) => {
4146 // append child node to the restored modal body
4147 (restoredBody[0] || restoredBody).append(child);
4148 });
4149
4150 try {
4151 if (typeof data?.onRestore === 'function') {
4152 // call the restoring function from the original modal
4153 data.onRestore.call(restoredBody, e);
4154 }
4155
4156 // always emit the native restoring event for the current widget
4157 VBOCore.emitEvent('vbo-admin-dock-restore-' + widgetId, {
4158 data: data?.event_data || {},
4159 });
4160 } catch(e) {
4161 // do nothing
4162 }
4163 } else {
4164 // re-render admin widget from scratch
4165 VBOCore.handleDisplayWidgetNotification({
4166 widget_id: widgetId,
4167 }, widgetData);
4168 }
4169
4170 // remove element from DOM
4171 let elementIndex = element.getAttribute('data-index');
4172 element.remove();
4173
4174 // remove element index from dock
4175 this.removeDockElement(elementIndex);
4176 });
4177
4178 let content_icon = document.createElement('span');
4179 content_icon.classList.add('vbo-admin-dock-element-icn');
4180 if (details?.style) {
4181 content_icon.classList.add('vbo-admin-widget-style-' + details.style);
4182 }
4183 if (details?.icon) {
4184 content_icon.innerHTML = details.icon;
4185 }
4186
4187 if (body) {
4188 // create hidden node
4189 let modal_body = document.createElement('div');
4190 modal_body.classList.add('vbo-admin-dock-element-modalbody');
4191 modal_body.style.display = 'none';
4192
4193 try {
4194 // move modal body to hidden node
4195 modal_body.append((body[0] || body));
4196
4197 // set dock-minimized attribute
4198 (body[0] || body).setAttribute('data-dock-minimized', 1);
4199
4200 // always dispatch the native minimize event for the current widget
4201 VBOCore.emitEvent('vbo-admin-dock-minimize-' + widgetId, {
4202 data: data?.event_data || {},
4203 });
4204 } catch(e) {
4205 // do nothing
4206 }
4207
4208 // append hidden node to content
4209 content.appendChild(modal_body);
4210 }
4211
4212 let content_name = document.createElement('span');
4213 content_name.classList.add('vbo-admin-dock-element-name');
4214 content_name.innerText = details?.name || '';
4215
4216 let dismiss = document.createElement('span');
4217 dismiss.classList.add('vbo-admin-dock-element-dismiss');
4218 dismiss.innerHTML = '&times;';
4219 dismiss.addEventListener('click', (e) => {
4220 try {
4221 if (typeof destroyFn === 'function') {
4222 // destroy the original modal by firing any dismiss event
4223 destroyFn.call(body, e);
4224 }
4225 } catch(e) {
4226 // do nothing
4227 }
4228
4229 // remove element from DOM
4230 let elementIndex = element.getAttribute('data-index');
4231 element.remove();
4232
4233 // remove element index from dock
4234 this.removeDockElement(elementIndex);
4235 });
4236
4237 // append to content
4238 content.appendChild(content_icon);
4239 content.appendChild(content_name);
4240
4241 // append content to element
4242 element.appendChild(content);
4243
4244 // append dismiss to element
4245 element.appendChild(dismiss);
4246
4247 // return the HTMLElement object
4248 return element;
4249 }
4250
4251 /**
4252 * Builds a temporary dock element node.
4253 *
4254 * @param object details The element details.
4255 * @param object data The element data, usually an array of objects.
4256 * @param number index The element index number.
4257 * @param function restoreFn The callback for restoring the data.
4258 * @param function destroyFn The callback for destroying the data.
4259 * @param number badge Optional badge number to set.
4260 *
4261 * @return HTMLElement
4262 */
4263 buildDockTemporaryElement(details, data, index, restoreFn, destroyFn, badge) {
4264 if (!details?.id || details.id != '_tmp') {
4265 throw new Error('Invalid temporary element data.');
4266 }
4267
4268 const dataId = '_tmp';
4269
4270 const element = document.createElement('div');
4271 element.classList.add('vbo-admin-dock-element');
4272 element.setAttribute('data-id', dataId);
4273 element.setAttribute('data-index', index);
4274 element.setAttribute('data-badge-count', (badge || ''));
4275
4276 let content = document.createElement('div');
4277 content.classList.add('vbo-admin-dock-element-cont');
4278 content.addEventListener('click', (e) => {
4279 // get content target
4280 let target = e.target;
4281 if (!target.matches('.vbo-admin-dock-element-cont')) {
4282 target = target.closest('.vbo-admin-dock-element-cont');
4283 }
4284
4285 // reset badge count
4286 target.closest('.vbo-admin-dock-element').setAttribute('data-badge-count', '');
4287
4288 if (typeof restoreFn === 'function') {
4289 this._elements.forEach((dockEl) => {
4290 if (dockEl.id == dataId) {
4291 // invoke the callback by passing the current element data
4292 restoreFn(dockEl?.data);
4293
4294 // abort
4295 return;
4296 }
4297 });
4298 }
4299
4300 // remove element from DOM
4301 let elementIndex = element.getAttribute('data-index');
4302 element.remove();
4303
4304 // remove element index from dock
4305 this.removeDockElement(elementIndex);
4306 });
4307
4308 let content_icon = document.createElement('span');
4309 content_icon.classList.add('vbo-admin-dock-element-icn');
4310 if (details?.style) {
4311 content_icon.classList.add('vbo-admin-widget-style-' + details.style);
4312 }
4313 if (details?.icon) {
4314 content_icon.innerHTML = details.icon;
4315 }
4316
4317 let content_name = document.createElement('span');
4318 content_name.classList.add('vbo-admin-dock-element-name');
4319 content_name.innerText = details?.name || '';
4320
4321 let dismiss = document.createElement('span');
4322 dismiss.classList.add('vbo-admin-dock-element-dismiss');
4323 dismiss.innerHTML = '&times;';
4324 dismiss.addEventListener('click', (e) => {
4325 if (typeof destroyFn === 'function') {
4326 this._elements.forEach((dockEl) => {
4327 if (dockEl.id == dataId) {
4328 // invoke the callback by passing the current element data
4329 destroyFn(dockEl?.data);
4330
4331 // abort
4332 return;
4333 }
4334 });
4335 }
4336
4337 // remove element from DOM
4338 let elementIndex = element.getAttribute('data-index');
4339 element.remove();
4340
4341 // remove element index from dock
4342 this.removeDockElement(elementIndex);
4343 });
4344
4345 // append to content
4346 content.appendChild(content_icon);
4347 content.appendChild(content_name);
4348
4349 // append content to element
4350 element.appendChild(content);
4351
4352 // append dismiss to element
4353 element.appendChild(dismiss);
4354
4355 // return the HTMLElement object
4356 return element;
4357 }
4358
4359 /**
4360 * Adds a widget to the dock.
4361 *
4362 * @param object details The widget details.
4363 * @param object data The widget restoring data.
4364 * @param HTMLElement body Optional widget modal body to restore.
4365 * @param function destroyFn Optional dismiss callback.
4366 */
4367 addWidget(details, data, body, destroyFn) {
4368 if (!VBOCore.options.widget_ajax_uri) {
4369 throw new Error('Wrong environment');
4370 }
4371
4372 if (typeof details !== 'object') {
4373 details = {};
4374 }
4375
4376 if (!details?.id) {
4377 throw new Error('Unknown widget id');
4378 }
4379
4380 // add element to dock in the DOM at first
4381 this.getDockNode().appendChild(
4382 this.buildDockElement(details, data, this._elements.length, body, destroyFn)
4383 );
4384
4385 // push dock element after it was added to the DOM
4386 this._elements.push({
4387 id: details.id,
4388 details: Object.assign({}, details),
4389 data: Object.assign({}, (data?.event_data?._options || {})),
4390 });
4391
4392 // ensure dock is visible
4393 this.showDock();
4394
4395 // update dock elements on localStorage at last
4396 VBOCore.storageSetItem(this._storageId, this._elements);
4397 }
4398
4399 /**
4400 * Adds temporary data to the dock. Data will NOT persist on the same localStorage key.
4401 * Originally introduced to handle the rates update requests within a queue.
4402 *
4403 * @param object details The data details.
4404 * @param object data The data to store, usually an array of objects.
4405 * @param function restoreFn The callback for restoring the data.
4406 * @param function destroyFn The callback for destroying the data.
4407 * @param number badge Optional badge number to set.
4408 */
4409 addTemporaryData(details, data, restoreFn, destroyFn, badge) {
4410 if (!VBOCore.options.widget_ajax_uri) {
4411 throw new Error('Wrong environment');
4412 }
4413
4414 if (typeof details !== 'object') {
4415 details = {};
4416 }
4417
4418 // temporary data will always get a private ID to avoid duplicate entries in the dock
4419 details.id = '_tmp';
4420
4421 // check if the same temporary data element type exists
4422 let previousElement = null;
4423 let previousIndex = null;
4424 this.getElements().forEach((element, index) => {
4425 if (previousElement) {
4426 return;
4427 }
4428 if (element.id == details.id) {
4429 // previous temporary data element type found
4430 previousElement = element;
4431 previousIndex = index;
4432 return;
4433 }
4434 });
4435
4436 if (!previousElement) {
4437 // add temporary element to dock in the DOM at first
4438 this.getDockNode().appendChild(
4439 this.buildDockTemporaryElement(details, data, this._elements.length, restoreFn, destroyFn, (badge ? badge : (Array.isArray(data) ? data.length : null)))
4440 );
4441
4442 // push dock element after it was added to the DOM
4443 this._elements.push({
4444 id: details.id,
4445 details: Object.assign({}, details),
4446 data: Array.isArray(data) ? [...data] : Object.assign({}, data),
4447 });
4448 } else {
4449 // update previous temporary data element type
4450 let updatedData = Array.isArray(data) && Array.isArray(previousElement.data) ? [...previousElement.data].concat([...data]) : [...data];
4451 let updatedBadge = badge ? badge : (Array.isArray(updatedData) ? updatedData.length : previousElement.badge);
4452
4453 // update existing dock element
4454 this._elements[previousIndex] = {
4455 id: details.id,
4456 details: Object.assign({}, previousElement.details, details),
4457 data: updatedData,
4458 };
4459
4460 if (updatedBadge) {
4461 // update DOM element
4462 let dom_elements = this.getDockNode().querySelectorAll('.vbo-admin-dock-element');
4463 this._elements.forEach((element, cur_index) => {
4464 if (!dom_elements[cur_index] || dom_elements[cur_index].getAttribute('data-id') != element.id) {
4465 return;
4466 }
4467 if (dom_elements[cur_index].getAttribute('data-id') != details.id) {
4468 return;
4469 }
4470 // update badge attribute
4471 dom_elements[cur_index].setAttribute('data-badge-count', updatedBadge);
4472 });
4473 }
4474 }
4475
4476 // ensure dock is visible
4477 this.showDock();
4478
4479 if (details.persist_id && typeof details.persist_id === 'string') {
4480 // update dock element on a different localStorage key at last
4481 VBOCore.storageSetItem(this._storageId + '.' + details.persist_id, (previousIndex ? this._elements[previousIndex] : this._elements[this._elements.length - 1]));
4482 }
4483 }
4484
4485 /**
4486 * Loads temporary data onto the dock by using an ID and type identifier.
4487 * Originally introduced to handle the rates update requests within a queue.
4488 *
4489 * @param object details The data details to load.
4490 * @param function restoreFn The callback for restoring the data.
4491 * @param function destroyFn The callback for destroying the data.
4492 * @param number badge Optional badge number to set.
4493 *
4494 * @return any Data object loaded on success or null.
4495 */
4496 loadTemporaryData(details, restoreFn, destroyFn, badge) {
4497 if (!VBOCore.options.widget_ajax_uri) {
4498 throw new Error('Wrong environment');
4499 }
4500
4501 if (typeof details !== 'object' || !details.id || !details.persist_id || typeof details.persist_id !== 'string') {
4502 return null;
4503 }
4504
4505 // temporary data will always get a private ID to avoid duplicate entries in the dock
4506 details.id = '_tmp';
4507
4508 // check if the same temporary data element type exists
4509 let previousElement = null;
4510 this.getElements().forEach((element, index) => {
4511 if (previousElement) {
4512 return;
4513 }
4514 if (element.id == details.id) {
4515 // previous temporary data element type found
4516 previousElement = element;
4517 return;
4518 }
4519 });
4520
4521 if (previousElement) {
4522 // temporary data already loaded in dock
4523 return previousElement;
4524 }
4525
4526 // load the requested temporary and persisting data
4527 let storageElement = VBOCore.storageGetItem(this._storageId + '.' + details.persist_id);
4528
4529 try {
4530 if (typeof storageElement === 'string') {
4531 storageElement = JSON.parse(storageElement);
4532 }
4533 } catch(e) {
4534 storageElement = null;
4535 }
4536
4537 if (!storageElement || typeof storageElement !== 'object' || !storageElement.data) {
4538 return null;
4539 }
4540
4541 // add the temporary data just loaded to the dock
4542 this.addTemporaryData(Object.assign({}, (storageElement.details || {}), details), storageElement.data, restoreFn, destroyFn, badge);
4543
4544 // return the data loaded
4545 return storageElement;
4546 }
4547
4548 /**
4549 * Returns the current dock elements.
4550 *
4551 * @return Array
4552 */
4553 getElements() {
4554 return this._elements;
4555 }
4556
4557 /**
4558 * Loads widget dock elements from localStorage and populates them, if any.
4559 */
4560 loadDockElements() {
4561 let storageElements = VBOCore.storageGetItem(this._storageId) || [];
4562
4563 try {
4564 if (typeof storageElements === 'string') {
4565 storageElements = JSON.parse(storageElements);
4566 }
4567 } catch(e) {
4568 storageElements = [];
4569 }
4570
4571 if (Array.isArray(storageElements)) {
4572 // set current elements
4573 this._elements = storageElements;
4574 }
4575
4576 // scan all elements to ensure no temporary data was stored upon deleting or adding new widgets
4577 this._elements.forEach((element, index) => {
4578 if (element?.id == '_tmp') {
4579 // splice the elements list
4580 this._elements.splice(index, 1);
4581
4582 // update dock elements on localStorage by only keeping real widgets
4583 VBOCore.storageSetItem(this._storageId, this._elements);
4584
4585 // abort
4586 return;
4587 }
4588 });
4589
4590 if (this._elements.length) {
4591 // populate dock elements
4592 this.populateDockElements();
4593
4594 // show the dock
4595 this.showDock();
4596 } else {
4597 // hide the dock
4598 this.hideDock();
4599 }
4600 }
4601
4602 /**
4603 * Populates the current dock elements.
4604 */
4605 populateDockElements() {
4606 const dockNode = this.getDockNode();
4607
4608 // empty the dock
4609 dockNode.innerHTML = '';
4610
4611 // scan all elements
4612 this._elements.forEach((element, index) => {
4613 // add element to dock in the DOM at first
4614 dockNode.appendChild(
4615 this.buildDockElement(element?.details, element?.data, index)
4616 );
4617 });
4618 }
4619
4620 /**
4621 * Hides the dock node.
4622 */
4623 hideDock() {
4624 this.getDockNode().style.display = 'none';
4625 }
4626
4627 /**
4628 * Shows the dock node.
4629 */
4630 showDock() {
4631 if (!this.getDockNode().checkVisibility()) {
4632 this.getDockNode().style.display = this._options.dockDisplayStyle;
4633 }
4634 }
4635
4636 /**
4637 * Event callback fired to update the element(s) badge counter.
4638 */
4639 updateElementsBadgeCounter(e) {
4640 if (!e || !e.detail || !e.detail?.widgetId) {
4641 return;
4642 }
4643
4644 let elements = VBOAdminDock.getInstance().getElements();
4645
4646 if (!elements.length) {
4647 return;
4648 }
4649
4650 let widgetId = e.detail.widgetId;
4651 let badgeCount = parseInt(e.detail?.badgeCount || 0);
4652 let badgeValue = badgeCount > 0 ? badgeCount : '';
4653
4654 elements.forEach((element) => {
4655 if (element.id == widgetId) {
4656 document.querySelectorAll('.vbo-admin-dock-element[data-id="' + widgetId + '"]').forEach((elNode) => {
4657 elNode.setAttribute('data-badge-count', badgeValue);
4658 });
4659 }
4660 });
4661 }
4662 }
4663
4664 })(jQuery, window);