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 +2605 -145 1.6.4 → 1.8.16 View file →
@@ -1,7 +1,7 @@
1 1 /**
2 - * VikBooking Core v1.6.3
3 - * Copyright (C) 2023 E4J s.r.l. All Rights Reserved.
2 + * VikBooking Core v1.8.8
3 + * Copyright (C) 2026 E4J s.r.l. All Rights Reserved.
4 4 * http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
5 5 * https://vikwp.com | https://e4j.com | https://e4jconnect.com
6 6 */
7 7 (function($, w) {
@@ -6,8 +6,16 @@
6 6 */
7 7 (function($, w) {
8 8 'use strict';
9 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 + */
10 18 w['VBOCore'] = class VBOCore {
11 19
12 20 /**
13 21 * Proxy to support static injection of params.
@@ -12,11 +20,9 @@
12 20 /**
13 21 * Proxy to support static injection of params.
14 22 */
15 23 constructor(params) {
16 - if (typeof params === 'object') {
17 - VBOCore.setOptions(params);
18 - }
24 + VBOCore.setOptions(params);
19 25 }
20 26
21 27 /**
22 28 * Inject options by overriding default properties.
@@ -25,9 +31,9 @@
25 31 *
26 32 * @return self
27 33 */
28 34 static setOptions(params) {
29 - if (typeof params === 'object') {
35 + if (params != null && typeof params === 'object') {
30 36 VBOCore.options = Object.assign(VBOCore.options, params);
31 37 }
32 38
33 39 return VBOCore;
@@ -33,8 +39,76 @@
33 39 return VBOCore;
34 40 }
35 41
36 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 + /**
37 111 * Getter for admin_widgets private options property.
38 112 *
39 113 * @return array
40 114 */
@@ -96,8 +170,44 @@
96 170 return VBOCore.options.widget_modal_dismissed;
97 171 }
98 172
99 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 + /**
100 210 * Parses an AJAX response error object.
101 211 *
102 212 * @param object err
103 213 *
@@ -131,13 +241,23 @@
131 241 if (attempt === undefined) {
132 242 attempt = 1;
133 243 }
134 244
135 - return $.ajax({
245 + let requestOptions = {
136 246 type: 'POST',
137 247 url: url,
138 - data: data
139 - }).done(function(resp) {
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) {
140 260 if (success !== undefined) {
141 261 // launch success callback function
142 262 success(resp);
143 263 }
@@ -155,14 +275,20 @@
155 275 }, 500);
156 276 } else {
157 277 // launch the failure callback otherwise
158 278 if (failure !== undefined) {
279 + if (err.responseText === 'false') {
280 + // make the property empty to rely on others
281 + err.responseText = '';
282 + }
159 283 failure(err);
160 284 }
161 285 }
162 286
163 - // always log the error in console
164 - console.log('AJAX request failed' + (err.status == 500 ? ' (' + err.responseText + ')' : ''), err);
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 + }
165 291 });
166 292 }
167 293
168 294 /**
@@ -177,8 +303,311 @@
177 303 return ((text + '').indexOf(search) >= 0);
178 304 }
179 305
180 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 + /**
181 610 * Initializes the multitasking panel for the admin widgets.
182 611 *
183 612 * @param object params the panel object params.
184 613 *
@@ -200,13 +629,16 @@
200 629 wclass_l_large: "vbo-admin-widgets-container-large",
201 630 addws_selector: ".vbo-sidepanel-add-widgets",
202 631 addw_selector: ".vbo-sidepanel-add-widget",
203 632 addw_modal_cls: "vbo-widget-render-modal",
633 + addw_def_cls: "vbo-widget-render-regular",
204 634 addwfs_selector: ".vbo-sidepanel-add-widget-focussed",
205 635 wtags_selector: ".vbo-sidepanel-widget-tags",
636 + wname_selector: ".vbo-sidepanel-widget-name",
206 637 addw_data_attr: "data-vbowidgetid",
207 638 actws_selector: ".vbo-sidepanel-active-widgets",
208 639 editw_selector: ".vbo-sidepanel-edit-widgets-trig",
640 + shortc_selector: ".vbo-sidepanel-shortcut",
209 641 rmwidget_class: "vbo-admin-widgets-widget-remove",
210 642 rmwidget_icn: "",
211 643 dtcwidget_class: "vbo-admin-widgets-widget-detach",
212 644 dtctarget_class: "vbo-admin-widget-head",
@@ -216,8 +648,11 @@
216 648 notif_off_class: "vbo-sidepanel-notifications-off",
217 649 open_class: "vbo-sidepanel-open",
218 650 close_class: "vbo-sidepanel-close",
219 651 cur_widget_cls: "vbo-admin-widgets-container-small",
652 + sortable: true,
653 + sort_save_ev: "vbo-admin-widgets-updateposmp",
654 + sorting: null,
220 655 };
221 656
222 657 if (typeof params === 'object') {
223 658 panel_opts = Object.assign(panel_opts, params);
@@ -232,10 +667,12 @@
232 667 VBOCore.setOptions({
233 668 panel_opts: panel_opts,
234 669 });
235 670
236 - // setup browser notifications
237 - VBOCore.setupNotifications();
671 + if (VBOCore.options.is_vbo) {
672 + // setup browser notifications
673 + VBOCore.setupNotifications();
674 + }
238 675
239 676 // count active widgets on current page
240 677 var tot_active_widgets = VBOCore.options.admin_widgets.length;
241 678 if (tot_active_widgets > 0) {
@@ -280,21 +717,23 @@
280 717 $(VBOCore.options.panel_opts.close_selector).on('click', function() {
281 718 $(VBOCore.options.panel_opts.btn_trigger).trigger('click');
282 719 });
283 720
284 - // register toggle layout buttons
285 - $(VBOCore.options.panel_opts.t_layout_large).on('click', function() {
286 - // large layout
287 - $(VBOCore.options.panel_opts.selector).addClass(VBOCore.options.panel_opts.sclass_l_large).removeClass(VBOCore.options.panel_opts.sclass_l_small);
288 - $(VBOCore.options.panel_opts.wclass_base_sel).addClass(VBOCore.options.panel_opts.wclass_l_large).removeClass(VBOCore.options.panel_opts.wclass_l_small);
289 - VBOCore.options.panel_opts.cur_widget_cls = VBOCore.options.panel_opts.sclass_l_large;
290 - });
291 - $(VBOCore.options.panel_opts.t_layout_small).on('click', function() {
292 - // small layout
293 - $(VBOCore.options.panel_opts.selector).addClass(VBOCore.options.panel_opts.sclass_l_small).removeClass(VBOCore.options.panel_opts.sclass_l_large);
294 - $(VBOCore.options.panel_opts.wclass_base_sel).addClass(VBOCore.options.panel_opts.wclass_l_small).removeClass(VBOCore.options.panel_opts.wclass_l_large);
295 - VBOCore.options.panel_opts.cur_widget_cls = VBOCore.options.panel_opts.sclass_l_small;
296 - });
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 + }
297 736
298 737 // register listener for esc key pressed
299 738 $(document).keyup(function(e) {
300 739 if (!VBOCore.side_panel_on) {
@@ -364,9 +803,9 @@
364 803 }
365 804
366 805 // handle Enter key press to add a widget
367 806 if (e.key === "Enter") {
368 - // on Enter key pressed, add the first matching widget or the focussed one
807 + // on Enter key pressed, open/add the first matching widget or the focussed one
369 808 var load_wid_id = null;
370 809 var focussed_wid = $(VBOCore.options.panel_opts.addwfs_selector + ':visible').first();
371 810
372 811 if (focussed_wid.length) {
@@ -379,19 +818,31 @@
379 818 // no widget to render found
380 819 return;
381 820 }
382 821
383 - if (e.shiftKey === true) {
384 - // widget modal rendering
385 - VBOCore.renderModalWidget(load_wid_id);
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');
386 826
387 827 return;
388 828 }
389 829
390 - // widget multitask panel rendering
391 - VBOCore.addWidgetToPanel(load_wid_id);
392 - $(VBOCore.options.panel_opts.search_selector).trigger('blur');
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);
393 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 +
394 845 return;
395 846 }
396 847
397 848 // handle arrow keys selection
@@ -442,15 +893,33 @@
442 893 if (!widget_id || !widget_id.length) {
443 894 return false;
444 895 }
445 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 +
446 902 // determine widget rendering method
447 903 if (e && e.target) {
448 904 let cktarget = $(e.target);
449 - if (e.shiftKey === true || (cktarget.hasClass(VBOCore.options.panel_opts.addw_modal_cls) || cktarget.parent().hasClass(VBOCore.options.panel_opts.addw_modal_cls))) {
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))) {
450 907 // widget modal rendering
451 - VBOCore.renderModalWidget(widget_id);
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);
452 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 +
453 922 return;
454 923 }
455 924 }
456 925
@@ -457,10 +926,12 @@
457 926 // widget multitask panel rendering
458 927 VBOCore.addWidgetToPanel(widget_id);
459 928 });
460 929
461 - // register listener for updating multitask sidepanel with debounce
462 - document.addEventListener(VBOCore.options.multitask_save_event, VBOCore.debounceEvent(VBOCore.saveMultitasking, 1000));
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 + }
463 934
464 935 // subscribe to event for multitask shortcut
465 936 document.addEventListener(VBOCore.multitask_shortcut_event, function() {
466 937 // toggle multitask panel
@@ -472,36 +943,116 @@
472 943 // focus search multitask widgets
473 944 $(VBOCore.options.panel_opts.search_selector).trigger('focus');
474 945 });
475 946
476 - // register click event on edit widgets button
477 - $(VBOCore.options.panel_opts.editw_selector).on('click', function() {
478 - VBOCore.toggleWidgetsPanelEditing(null);
479 - });
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 + });
480 952
481 - // register detach widget buttons
482 - $('.' + VBOCore.options.panel_opts.dtcwidget_class).each(function() {
483 - let widget_wrapper = $(this).parent(VBOCore.options.panel_opts.wclass_base_sel);
484 - let detach_widget_id = widget_wrapper.attr(VBOCore.options.panel_opts.addw_data_attr);
485 - let detach_to_target = widget_wrapper.find('.' + VBOCore.options.panel_opts.dtctarget_class);
486 - if (detach_to_target.length) {
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 + }
487 961 // move detach wrapper onto the target (widget head)
488 962 $(this).prependTo(detach_to_target);
489 - }
490 963
491 - if (!detach_widget_id) {
492 - return;
493 - }
964 + if (!detach_widget_id) {
965 + return;
966 + }
494 967
495 - // register detach action
496 - $(this).on('click', function() {
497 - // detach widget, meaning do a modal rendering
498 - VBOCore.renderModalWidget(detach_widget_id);
968 + // register detach action
969 + $(this).on('click', function() {
970 + // detach widget, meaning do a modal rendering
971 + VBOCore.renderModalWidget(detach_widget_id);
972 + });
499 973 });
500 - });
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 + }
501 1025 }
502 1026
503 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 + /**
504 1055 * Registers the blur event for the search widgets input.
505 1056 */
506 1057 static registerSearchWidgetsBlur() {
507 1058 if (VBOCore.options.active_listeners.hasOwnProperty('registerSearchWidgetsBlur')) {
@@ -593,12 +1144,13 @@
593 1144 // register detach widget button
594 1145 setTimeout(() => {
595 1146 let detach_elem = $('<div></div>').addClass(VBOCore.options.panel_opts.dtcwidget_class).html(VBOCore.options.panel_opts.dtcwidget_icn);
596 1147 let detach_to_target = widget_elem.find('.' + VBOCore.options.panel_opts.dtctarget_class);
597 - if (detach_to_target.length) {
598 - // move detach wrapper onto the target (widget head)
599 - detach_elem.prependTo(detach_to_target);
1148 + if (!detach_to_target.length) {
1149 + detach_to_target = widget_elem;
600 1150 }
1151 + // move detach wrapper onto the target (widget head)
1152 + detach_elem.prependTo(detach_to_target);
601 1153 // register detach action
602 1154 detach_elem.on('click', function() {
603 1155 // detach widget, meaning do a modal rendering
604 1156 VBOCore.renderModalWidget(widget_id);
@@ -632,8 +1184,10 @@
632 1184 // check added action status
633 1185 if (added === true) {
634 1186 // show button for edit mode
635 1187 triggerer.show();
1188 + // hide shortcut element
1189 + $(VBOCore.options.panel_opts.shortc_selector).hide();
636 1190 return;
637 1191 }
638 1192
639 1193 // grab all widgets
@@ -642,8 +1196,10 @@
642 1196 if (added === false) {
643 1197 if (!editing_widgets.length) {
644 1198 // hide button for edit mode after removing the last widget
645 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();
646 1202 }
647 1203 return;
648 1204 }
649 1205
@@ -812,9 +1368,20 @@
812 1368 if (Notification.permission && Notification.permission === 'granted') {
813 1369 // permissions were granted already
814 1370 $(VBOCore.options.panel_opts.notif_selector)
815 1371 .addClass(VBOCore.options.panel_opts.notif_on_class)
816 - .attr('title', VBOCore.options.tn_texts.notifs_enabled);
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 + });
817 1384 return true;
818 1385 }
819 1386
820 1387 // notifications supported, but perms not granted
@@ -861,9 +1428,12 @@
861 1428 if (Notification.permission && Notification.permission === 'granted') {
862 1429 // permissions were granted already
863 1430 $(selector)
864 1431 .addClass(VBOCore.options.panel_opts.notif_on_class)
865 - .attr('title', VBOCore.options.tn_texts.notifs_enabled);
1432 + .attr('title', VBOCore.options.tn_texts.notifs_enabled)
1433 + .on('click', function() {
1434 + VBOCore.dispatchCongratulations();
1435 + });
866 1436 return true;
867 1437 }
868 1438
869 1439 // notifications supported, but perms not granted
@@ -931,8 +1501,9 @@
931 1501
932 1502 /**
933 1503 * Handles the notifications permission response (from callback or promise resolved).
934 1504 *
1505 + * @param any permission permission status string or NotificationPermission object.
935 1506 * @param string selector optional element query selector.
936 1507 */
937 1508 static handleNotifPerms(permission, selector) {
938 1509 // check the permission status from the Notification object interface
@@ -943,12 +1514,9 @@
943 1514 .addClass(VBOCore.options.panel_opts.notif_on_class)
944 1515 .attr('title', VBOCore.options.tn_texts.notifs_enabled);
945 1516
946 1517 // dispatch an immediate notification to congratulate with the activation
947 - let notif = new Notification(VBOCore.options.tn_texts.congrats, {
948 - body: VBOCore.options.tn_texts.notifs_enabled,
949 - tag: 'vbo_notification_congrats'
950 - });
1518 + VBOCore.dispatchCongratulations();
951 1519
952 1520 return true;
953 1521 } else {
954 1522 // permissions denied :(
@@ -965,8 +1533,25 @@
965 1533 return false;
966 1534 }
967 1535
968 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 + /**
969 1554 * Given a date-time string, returns a Date object representation.
970 1555 *
971 1556 * @param string dtime_str the date-time string in "Y-m-d H:i:s" format.
972 1557 */
@@ -1035,8 +1620,9 @@
1035 1620 }
1036 1621 if (VBOCore.notificationsEnabled() !== true) {
1037 1622 // notifications not enabled
1038 1623 console.info('Browser notifications disabled or unsupported.');
1624 + return false;
1039 1625 }
1040 1626
1041 1627 // gather current date-timing information
1042 1628 const now_date = new Date();
@@ -1249,12 +1835,14 @@
1249 1835 };
1250 1836 if (func_nodes) {
1251 1837 toast_notif_data.action = function() {
1252 1838 func_nodes(data);
1839 + VBOToast.dispose(true);
1253 1840 };
1254 1841 } else if (typeof data.onclick === 'function') {
1255 1842 toast_notif_data.action = function() {
1256 1843 data.onclick.call(data);
1844 + VBOToast.dispose(true);
1257 1845 };
1258 1846 }
1259 1847 VBOToast.enqueue(new VBOToastMessage(toast_notif_data));
1260 1848
@@ -1267,11 +1855,26 @@
1267 1855 // use the browser's native Notifications API
1268 1856 let browser_notif = new Notification(data.title, {
1269 1857 body: data.message,
1270 1858 icon: data.icon,
1271 - tag: 'vbo_notification'
1859 + tag: 'vbo_notification',
1860 + silent: false
1272 1861 });
1273 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 +
1274 1877 if (func_nodes) {
1275 1878 // register notification click event
1276 1879 browser_notif.addEventListener('click', () => {
1277 1880 func_nodes(data);
@@ -1372,19 +1975,55 @@
1372 1975 document.location.href = data.gotourl;
1373 1976 }
1374 1977
1375 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 + /**
1376 2011 * Handle the display of a notification through a widget.
1377 2012 * Common handler for browser notifications displayed through
1378 2013 * a widget modal rendering.
1379 2014 *
1380 - * @param object data notification display data payload.
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
1381 2020 */
1382 - static handleDisplayWidgetNotification(data) {
2021 + static handleDisplayWidgetNotification(data, options, is_push) {
1383 2022 try {
1384 2023 // validate handler
1385 2024 if (!VBOCore.options.is_vbo && !VBOCore.options.is_vcm) {
1386 - throw new Error('Vik Booking must handle this');
2025 + throw new Error('Unable to handle the notification display from the current page');
1387 2026 }
1388 2027
1389 2028 // validate payload
1390 2029 if (typeof data !== 'object' || !data.hasOwnProperty('widget_id') || !data['widget_id']) {
@@ -1391,10 +2030,10 @@
1391 2030 throw new Error('Invalid widget payload');
1392 2031 }
1393 2032
1394 2033 // parse handler
1395 - if (VBOCore.options.is_vcm) {
1396 - // the operation is handled by Vik Channel Manager
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)
1397 2036 VBOCore.loadAdminWidgetAssets(data).then((assets) => {
1398 2037 // append assets to DOM
1399 2038 assets.forEach((asset) => {
1400 2039 if (!$('link#' + asset['id']).length) {
@@ -1400,12 +2039,13 @@
1400 2039 if (!$('link#' + asset['id']).length) {
1401 2040 $('head').append('<link rel="stylesheet" id="' + asset['id'] + '" href="' + asset['href'] + '" media="all" />');
1402 2041 }
1403 2042 });
1404 - // widget modal rendering handled by Vik Channel Manager
1405 - let modal_data = VBOCore.renderModalWidget(data['widget_id'], data, false);
1406 - if (modal_data.hasOwnProperty('dismissed_event')) {
1407 - // register event to unload all assets
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)
1408 2048 document.addEventListener(modal_data['dismissed_event'], () => {
1409 2049 assets.forEach((asset) => {
1410 2050 if ($('link#' + asset['id']).length) {
1411 2051 $('link#' + asset['id']).remove();
@@ -1417,13 +2057,18 @@
1417 2057 throw new Error(error);
1418 2058 });
1419 2059 } else {
1420 2060 // widget modal rendering handled by Vik Booking
1421 - VBOCore.renderModalWidget(data['widget_id'], data, false);
2061 + VBOCore.renderModalWidget(data['widget_id'], data, options, false);
1422 2062 }
1423 2063 } catch(e) {
1424 - // fallback to a regular link
1425 - VBOCore.handleGoto(data);
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 + }
1426 2071 }
1427 2072 }
1428 2073
1429 2074 /**
@@ -1487,20 +2132,58 @@
1487 2132 if (VBOCore.watch_data_interval == null) {
1488 2133 VBOCore.watch_data_interval = window.setInterval(VBOCore.watchWidgetsData, 60000);
1489 2134 }
1490 2135
1491 - // set up watch-data broadcast channel to connect all browsing contexts
1492 - if (typeof BroadcastChannel !== 'undefined' && !VBOCore.broadcast_watch_data) {
1493 - // start broadcast channel
1494 - VBOCore.broadcast_watch_data = new BroadcastChannel('vikbooking_watch_data');
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');
1495 2142
1496 - // register to the "on broadcast message received" event
1497 - VBOCore.broadcast_watch_data.onmessage = (event) => {
1498 - if (event && event.data) {
1499 - // update watch data map for next schedule to avoid dispatching duplicate notifications
1500 - VBOCore.widgets_watch_data = event.data;
1501 - }
1502 - };
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 + }
1503 2186 }
1504 2187 }
1505 2188
1506 2189 /**
@@ -1514,9 +2197,10 @@
1514 2197 // call on new events
1515 2198 VBOCore.doAjax(
1516 2199 VBOCore.options.watchdata_ajax_uri,
1517 2200 {
1518 - watch_data: JSON.stringify(VBOCore.widgets_watch_data),
2201 + watch_data: JSON.stringify(VBOCore.widgets_watch_data),
2202 + pushed_data: JSON.stringify(VBOCore.widgets_pushed_data),
1519 2203 },
1520 2204 (response) => {
1521 2205 try {
1522 2206 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
@@ -1536,8 +2220,33 @@
1536 2220 // this will avoid dispatching duplicate notifications
1537 2221 VBOCore.broadcast_watch_data.postMessage(VBOCore.widgets_watch_data);
1538 2222 }
1539 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 + }
1540 2249 } else {
1541 2250 console.error('Unexpected or invalid JSON response', response);
1542 2251 }
1543 2252 } catch(err) {
@@ -1554,25 +2263,45 @@
1554 2263 * Widget modal rendering.
1555 2264 *
1556 2265 * @param string widget_id the widget identifier to render.
1557 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.
1558 2268 * @param bool hide_panel if false, the multitask panel elements will remain unchanged.
1559 2269 *
1560 2270 * @return object the multitask data injected object merged with modal options.
1561 2271 */
1562 - static renderModalWidget(widget_id, data, hide_panel) {
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 +
1563 2294 // build the default widget payload
1564 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;
1565 2297 let widget_data = {
1566 2298 _modalRendering: 1,
1567 2299 _modalJsId: modal_js_id,
1568 - _modalTitle: VBOCore.options.tn_texts.admin_widget,
2300 + _modalTitle: modal_title,
2301 + _options: options,
1569 2302 };
1570 2303
1571 - if (typeof data !== 'object') {
1572 - data = {};
1573 - }
1574 -
1575 2304 // merge default payload options with given options
1576 2305 data = Object.assign(widget_data, data);
1577 2306
1578 2307 // define unique modal event names to avoid conflicts
@@ -1577,15 +2306,21 @@
1577 2306
1578 2307 // define unique modal event names to avoid conflicts
1579 2308 let dismiss_event = 'vbo-dismiss-widget-modal' + modal_js_id;
1580 2309 let loading_event = 'vbo-loading-widget-modal' + modal_js_id;
2310 + let resize_event = 'vbo-resize-widget-modal' + modal_js_id;
1581 2311
1582 2312 // define the modal options
1583 2313 let modal_options = {
1584 - suffix: 'widget-modal',
1585 - extra_class: 'vbo-modal-rounded vbo-modal-tall vbo-modal-nofooter',
2314 + suffix: 'widget_modal',
2315 + extra_class: 'vbo-modal-widget vbo-modal-rounded vbo-modal-tall vbo-modal-nofooter',
1586 2316 title: data._modalTitle,
1587 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,
1588 2323 dismiss_event: dismiss_event,
1589 2324 loading_event: loading_event,
1590 2325 loading_body: VBOCore.options.default_loading_body,
1591 2326 dismissed_event: VBOCore.options.widget_modal_dismissed + modal_js_id,
@@ -1591,8 +2326,13 @@
1591 2326 dismissed_event: VBOCore.options.widget_modal_dismissed + modal_js_id,
1592 2327 event_data: widget_data,
1593 2328 };
1594 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 +
1595 2335 // display modal
1596 2336 let widget_modal = VBOCore.displayModal(modal_options);
1597 2337
1598 2338 if (hide_panel !== false) {
@@ -1608,8 +2348,55 @@
1608 2348 VBOCore.renderAdminWidget(widget_id, data).then((content) => {
1609 2349 // stop loading and append widget content to modal
1610 2350 VBOCore.emitEvent(loading_event);
1611 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 + });
1612 2399 }).catch((error) => {
1613 2400 // dismiss modal and display error
1614 2401 VBOCore.emitEvent(dismiss_event);
1615 2402 alert(error);
@@ -1628,9 +2415,9 @@
1628 2415 */
1629 2416 static renderAdminWidget(widget_id, data) {
1630 2417 return new Promise((resolve, reject) => {
1631 2418 if (!VBOCore.options.widget_ajax_uri) {
1632 - reject('Could not add admin widget');
2419 + reject('Could not load admin widget');
1633 2420 return;
1634 2421 }
1635 2422
1636 2423 var call_method = 'render';
@@ -1664,8 +2451,86 @@
1664 2451 });
1665 2452 }
1666 2453
1667 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 + /**
1668 2533 * Helper method used to copy the text of an
1669 2534 * input element within the clipboard.
1670 2535 *
1671 2536 * Clipboard copy will take effect only in case the
@@ -1710,9 +2575,9 @@
1710 2575 return;
1711 2576 }
1712 2577
1713 2578 // try to copy within the clipboard by using the navigator
1714 - navigator.clipboard.writeText(input.value).then(() => {
2579 + navigator.clipboard.writeText((input.val ? input.val() : input.value)).then(() => {
1715 2580 // copied successfully
1716 2581 resolve(true);
1717 2582 }).catch((error) => {
1718 2583 // revert to the fallback
@@ -1723,58 +2588,90 @@
1723 2588
1724 2589 /**
1725 2590 * Helper method used to display a modal window dinamycally.
1726 2591 *
1727 - * @param object options The options to render the modal.
2592 + * @param object options The options to render the modal.
2593 + * @param object bindDetails Optional data to bind to the modal content.
1728 2594 *
1729 - * @return object The modal content element wrapper.
2595 + * @return object The modal content element wrapper.
1730 2596 */
1731 - static displayModal(options) {
2597 + static displayModal(options, bindDetails) {
1732 2598 var def_options = {
1733 - suffix: (Math.floor(Math.random() * 100000)) + '',
2599 + suffix: (Math.floor(Math.random() * 100000)) + '',
1734 2600 extra_class: null,
1735 - title: '',
1736 - body: '',
2601 + header: true,
2602 + title: '',
2603 + body: '',
1737 2604 body_prepend: false,
2605 + lock_scroll: false,
2606 + draggable: false,
2607 + enlargeable: false,
2608 + minimizeable: false,
2609 + escape_dismiss: true,
1738 2610 footer_left: null,
1739 2611 footer_right: null,
2612 + footer_center: null,
2613 + resize_event: null,
1740 2614 dismiss_event: null,
1741 - dismissed_event: null,
1742 - onDismiss: null,
2615 + dismissed_event: 'vbo-modal-dismissed',
2616 + onDismiss: null,
2617 + onMinimize: null,
2618 + onRestore: null,
1743 2619 loading_event: null,
1744 2620 loading_body: VBOCore.options.default_loading_body,
1745 - event_data: null,
2621 + progress_event: null,
2622 + event_data: null,
1746 2623 };
1747 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 +
1748 2630 // merge default options with given options
1749 2631 options = Object.assign(def_options, options);
1750 2632
1751 - // create the modal dismiss function
1752 - var modal_dismiss_fn = (e) => {
1753 - custom_modal.fadeOut(400, () => {
1754 - // invoke callback for onDismiss
1755 - if (typeof options.onDismiss === 'function') {
1756 - options.onDismiss.call(custom_modal, e);
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);
1757 2647 }
2648 + }
1758 2649
1759 - // check if modal did register to the loading event
1760 - if (options.loading_event) {
1761 - // we can now un-register from the loading event until a new modal is displayed and will register to it again
1762 - document.removeEventListener(options.loading_event, modal_handle_loading_event_fn);
1763 - }
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 + }
1764 2654
1765 - // check if we should fire the given modal dismissed event
1766 - if (options.dismissed_event) {
1767 - VBOCore.emitEvent(options.dismissed_event, options.event_data);
1768 - }
2655 + // check if body scroll lock should be removed
2656 + if (options.lock_scroll) {
2657 + $('body').removeClass('vbo-modal-lock-scroll');
2658 + }
1769 2659
1770 - // remove modal from DOM
1771 - custom_modal.remove();
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);
1772 2669 });
1773 2670 };
1774 2671
1775 2672 // create the modal loading event handler function
1776 - var modal_handle_loading_event_fn = (e) => {
2673 + const modal_handle_loading_event_fn = (e) => {
1777 2674 // toggle modal loading
1778 2675 if ($('.vbo-modal-overlay-content-backdrop').length) {
1779 2676 // hide loading
1780 2677 $('.vbo-modal-overlay-content-backdrop').remove();
@@ -1794,26 +2691,224 @@
1794 2691 // append backdrop loading to modal content
1795 2692 modal_content.prepend(modal_loading);
1796 2693 };
1797 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 +
1798 2857 // build modal content
1799 - var custom_modal = $('<div></div>').addClass('vbo-modal-overlay-block vbo-modal-overlay-' + options.suffix).css('display', 'block');
2858 + const custom_modal = $('<div></div>').addClass('vbo-modal-overlay-block vbo-modal-overlay-' + options.suffix).css('display', 'block');
1800 2859 var modal_dismiss = $('<a></a>').addClass('vbo-modal-overlay-close');
1801 2860 modal_dismiss.on('click', modal_dismiss_fn);
1802 2861 custom_modal.append(modal_dismiss);
1803 2862
1804 - var modal_content = $('<div></div>').addClass('vbo-modal-overlay-content vbo-modal-overlay-content-' + options.suffix);
2863 + const modal_content = $('<div></div>').addClass('vbo-modal-overlay-content vbo-modal-overlay-content-' + options.suffix);
1805 2864 if (options.extra_class && typeof options.extra_class === 'string') {
1806 2865 modal_content.addClass(options.extra_class);
1807 2866 }
1808 2867
1809 - var modal_head = $('<div></div>').addClass('vbo-modal-overlay-content-head');
1810 - var modal_head_close = $('<span></span>').addClass('vbo-modal-overlay-close-times').html('&times;');
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;');
1811 2879 modal_head_close.on('click', modal_dismiss_fn);
1812 - modal_head.append(options.title).append(modal_head_close);
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);
1813 2895
1814 - var modal_body = $('<div></div>').addClass('vbo-modal-overlay-content-body vbo-modal-overlay-content-body-scroll');
1815 - var modal_content_wrapper = $('<div></div>').addClass('vbo-modal-' + options.suffix + '-wrap');
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 + }
1816 2911 if (typeof options.body === 'string') {
1817 2912 modal_content_wrapper.html(options.body);
1818 2913 } else {
1819 2914 modal_content_wrapper.append(options.body);
@@ -1820,25 +2915,34 @@
1820 2915 }
1821 2916 modal_body.append(modal_content_wrapper);
1822 2917
1823 2918 // modal footer
1824 - var modal_footer = null;
1825 - if (options.footer_left || options.footer_right) {
2919 + let modal_footer = null;
2920 + if (options.footer_left || options.footer_right || options.footer_center) {
1826 2921 modal_footer = $('<div></div>').addClass('vbo-modal-overlay-content-footer');
1827 2922 if (options.footer_left) {
1828 - var modal_footer_left = $('<div></div>').addClass('vbo-modal-overlay-content-footer-left').append(options.footer_left);
2923 + let modal_footer_left = $('<div></div>').addClass('vbo-modal-overlay-content-footer-left').append(options.footer_left);
1829 2924 modal_footer.append(modal_footer_left);
1830 2925 }
1831 2926 if (options.footer_right) {
1832 - var modal_footer_right = $('<div></div>').addClass('vbo-modal-overlay-content-footer-right').append(options.footer_right);
2927 + let modal_footer_right = $('<div></div>').addClass('vbo-modal-overlay-content-footer-right').append(options.footer_right);
1833 2928 modal_footer.append(modal_footer_right);
1834 2929 }
1835 -
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 + }
1836 2934 }
1837 2935
1838 2936 // finalize modal contents
1839 - modal_content.append(modal_head).append(modal_body);
2937 + if (options.header) {
2938 + // append header
2939 + modal_content.append(modal_head);
2940 + }
2941 + // append body
2942 + modal_content.append(modal_body);
1840 2943 if (modal_footer) {
2944 + // append footer
1841 2945 modal_content.append(modal_footer);
1842 2946 }
1843 2947 custom_modal.append(modal_content);
1844 2948
@@ -1843,8 +2947,9 @@
1843 2947 custom_modal.append(modal_content);
1844 2948
1845 2949 // register to the dismiss event
1846 2950 if (options.dismiss_event) {
2951 + // listen to the event that will dismiss the modal
1847 2952 document.addEventListener(options.dismiss_event, function vbo_core_handle_dismiss_event(e) {
1848 2953 // make sure the same event won't propagate again, unless a new modal is displayed (multiple displayModal calls)
1849 2954 e.target.removeEventListener(e.type, vbo_core_handle_dismiss_event);
1850 2955
@@ -1850,8 +2955,27 @@
1850 2955
1851 2956 // invoke the modal dismiss function
1852 2957 modal_dismiss_fn(e);
1853 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 + }
1854 2978 }
1855 2979
1856 2980 // register to the toggle-loading event
1857 2981 if (options.loading_event) {
@@ -1856,8 +2980,12 @@
1856 2980 // register to the toggle-loading event
1857 2981 if (options.loading_event) {
1858 2982 // let a function handle it so that removing the event listener will be doable
1859 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 + }
1860 2988 }
1861 2989
1862 2990 // append (or prepend) modal to body
1863 2991 if ($('.vbo-modal-overlay-' + options.suffix).length) {
@@ -1863,13 +2991,35 @@
1863 2991 if ($('.vbo-modal-overlay-' + options.suffix).length) {
1864 2992 $('.vbo-modal-overlay-' + options.suffix).remove();
1865 2993 }
1866 2994 if (options.body_prepend) {
1867 - $('body').prepend(custom_modal);
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 + }
1868 3003 } else {
3004 + // append to body
1869 3005 $('body').append(custom_modal);
1870 3006 }
1871 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 +
1872 3022 // return the content wrapper element of the new modal
1873 3023 return modal_content_wrapper;
1874 3024 }
1875 3025
@@ -1907,8 +3057,36 @@
1907 3057 }
1908 3058 }
1909 3059
1910 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 + /**
1911 3089 * Tells whether localStorage is supported.
1912 3090 *
1913 3091 * @return boolean
1914 3092 */
@@ -1950,8 +3128,9 @@
1950 3128 }
1951 3129
1952 3130 localStorage.setItem(keyName, value);
1953 3131 } catch(e) {
3132 + console.error(e);
1954 3133 return false;
1955 3134 }
1956 3135
1957 3136 return true;
@@ -2034,8 +3213,13 @@
2034 3213
2035 3214 var action_base = action.hasOwnProperty('href') && typeof action['href'] == 'string' ? action['href'] : window.location.href;
2036 3215 var action_url;
2037 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 +
2038 3222 if (action_base.indexOf('http') !== 0) {
2039 3223 // relative URL
2040 3224 action_url = new URL(action_base, window.location.href);
2041 3225 } else {
@@ -2066,11 +3250,17 @@
2066 3250 // make sure we are not pushing a duplicate and count pinned actions
2067 3251 let pinned_actions = 0;
2068 3252 let unpinned_index = [];
2069 3253 for (let i = 0; i < menu_actions.length; i++) {
2070 - if (menu_actions[i]['href'] == menu_action_entry['href']) {
3254 + // avoid duplicate entries
3255 + if (!menu_action_entry.hasOwnProperty('widget') && menu_actions[i]['href'] == menu_action_entry['href']) {
3256 + // duplicate link
2071 3257 return false;
2072 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 + }
2073 3263 if (menu_actions[i].hasOwnProperty('pinned') && menu_actions[i]['pinned']) {
2074 3264 pinned_actions++;
2075 3265 } else {
2076 3266 unpinned_index.push(i);
@@ -2119,11 +3309,16 @@
2119 3309
2120 3310 if (typeof index === 'undefined') {
2121 3311 // find the proper index to update by href
2122 3312 for (let i = 0; i < menu_actions.length; i++) {
2123 - if (menu_actions[i]['href'] == menu_action_entry['href']) {
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
2124 3315 index = i;
2125 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;
2126 3321 }
2127 3322 }
2128 3323 }
2129 3324
@@ -2135,21 +3330,162 @@
2135 3330 menu_actions[index] = menu_action_entry;
2136 3331
2137 3332 return VBOCore.storageSetItem(VBOCore.getStorageScopeName(scope), menu_actions);
2138 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 + }
2139 3474 }
2140 3475
2141 3476 /**
2142 3477 * These used to be private static properties (static #options),
2143 3478 * but they are only supported by quite recent browsers (especially Safari).
2144 - * It's too risky, so we decided to keep the class properties public
3479 + * It's too premature, so we decided to keep the class properties public
2145 3480 * without declaring them as static inside the class declaration.
2146 3481 *
2147 3482 * @var object
2148 3483 */
2149 - VBOCore.options = {
3484 + VBOCore.options = CORE_OPTIONS || {
2150 3485 is_vbo: false,
2151 3486 is_vcm: false,
3487 + cms: 'wordpress',
2152 3488 widget_ajax_uri: null,
2153 3489 assets_ajax_uri: null,
2154 3490 multitask_ajax_uri: null,
2155 3491 watchdata_ajax_uri: null,
@@ -2154,11 +3490,13 @@
2154 3490 multitask_ajax_uri: null,
2155 3491 watchdata_ajax_uri: null,
2156 3492 current_page: null,
2157 3493 current_page_uri: null,
3494 + root_uri: '/',
2158 3495 client: 'admin',
2159 3496 panel_opts: {},
2160 3497 admin_widgets: [],
3498 + events_delegated: [],
2161 3499 notif_audio_url: '',
2162 3500 active_listeners: {},
2163 3501 tn_texts: {
2164 3502 notifs_enabled: 'Browser notifications are enabled!',
@@ -2176,8 +3514,28 @@
2176 3514 widget_modal_rendered: 'vbo-admin-widget-modal-rendered',
2177 3515 widget_modal_dismissed: 'vbo-widget-modal-dismissed',
2178 3516 admin_menu_maxactions: 3,
2179 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 + },
2180 3538 };
2181 3539
2182 3540 /**
2183 3541 * @var bool
@@ -2184,8 +3542,14 @@
2184 3542 */
2185 3543 VBOCore.side_panel_on = false;
2186 3544
2187 3545 /**
3546 + * @var bool
3547 + * @since 1.6.8
3548 + */
3549 + VBOCore.throttle_timer = false;
3550 +
3551 + /**
2188 3552 * @var array
2189 3553 */
2190 3554 VBOCore.notifications = [];
2191 3555
@@ -2203,6 +3567,1102 @@
2203 3567 * @var object
2204 3568 * @since 1.6.3
2205 3569 */
2206 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 + }
2207 4667
2208 4668 })(jQuery, window);