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