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 +1316 -71 1.7.2 → 1.8.16 View file →
@@ -1,7 +1,7 @@
1 1 /**
2 - * VikBooking Core v1.7.2
3 - * Copyright (C) 2024 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 */
@@ -167,13 +241,23 @@
167 241 if (attempt === undefined) {
168 242 attempt = 1;
169 243 }
170 244
171 - return $.ajax({
245 + let requestOptions = {
172 246 type: 'POST',
173 247 url: url,
174 - data: data
175 - }).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) {
176 260 if (success !== undefined) {
177 261 // launch success callback function
178 262 success(resp);
179 263 }
@@ -191,8 +275,12 @@
191 275 }, 500);
192 276 } else {
193 277 // launch the failure callback otherwise
194 278 if (failure !== undefined) {
279 + if (err.responseText === 'false') {
280 + // make the property empty to rely on others
281 + err.responseText = '';
282 + }
195 283 failure(err);
196 284 }
197 285 }
198 286
@@ -479,8 +567,47 @@
479 567 return true;
480 568 }
481 569
482 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 + /**
483 610 * Initializes the multitasking panel for the admin widgets.
484 611 *
485 612 * @param object params the panel object params.
486 613 *
@@ -774,9 +901,10 @@
774 901
775 902 // determine widget rendering method
776 903 if (e && e.target) {
777 904 let cktarget = $(e.target);
778 - if (e.shiftKey === true || VBOCore.options.is_vcm || (cktarget.hasClass(VBOCore.options.panel_opts.addw_modal_cls) || cktarget.parent().hasClass(VBOCore.options.panel_opts.addw_modal_cls))) {
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))) {
779 907 // widget modal rendering
780 908 if (VBOCore.options.is_vcm) {
781 909 // register loading effect with automatic cancellation
782 910 let orig_name = $(this).find(VBOCore.options.panel_opts.wname_selector).text();
@@ -2178,8 +2306,9 @@
2178 2306
2179 2307 // define unique modal event names to avoid conflicts
2180 2308 let dismiss_event = 'vbo-dismiss-widget-modal' + modal_js_id;
2181 2309 let loading_event = 'vbo-loading-widget-modal' + modal_js_id;
2310 + let resize_event = 'vbo-resize-widget-modal' + modal_js_id;
2182 2311
2183 2312 // define the modal options
2184 2313 let modal_options = {
2185 2314 suffix: 'widget_modal',
@@ -2187,8 +2316,11 @@
2187 2316 title: data._modalTitle,
2188 2317 body_prepend: true,
2189 2318 lock_scroll: true,
2190 2319 draggable: true,
2320 + enlargeable: true,
2321 + minimizeable: VBOCore.options.is_vbo,
2322 + resize_event: resize_event,
2191 2323 dismiss_event: dismiss_event,
2192 2324 loading_event: loading_event,
2193 2325 loading_body: VBOCore.options.default_loading_body,
2194 2326 dismissed_event: VBOCore.options.widget_modal_dismissed + modal_js_id,
@@ -2224,20 +2356,40 @@
2224 2356 if (modal_options.title == VBOCore.options.tn_texts.admin_widget) {
2225 2357 widget_modal
2226 2358 .closest('.vbo-modal-overlay-content')
2227 2359 .find('.vbo-modal-overlay-content-head-title')
2228 - .text(data._modalTitle + ' - ' + details['name']);
2360 + .text(data._modalTitle + ' - ' + details.name);
2229 2361 }
2230 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 +
2231 2383 // register admin menu action
2232 2384 try {
2233 2385 // work on Local Storage to register the widget data
2234 2386 VBOCore.registerAdminMenuAction({
2235 - name: details['name'],
2387 + name: details.name,
2236 2388 href: 'JavaScript: void(0);',
2237 2389 widget: widget_id,
2238 - icon: details['icon'],
2239 - style: details['style'],
2390 + icon: details.icon,
2391 + style: details.style,
2240 2392 }, 'widgets');
2241 2393 } catch(e) {
2242 2394 console.error(e);
2243 2395 }
@@ -2299,8 +2451,42 @@
2299 2451 });
2300 2452 }
2301 2453
2302 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 + /**
2303 2489 * Fetches the details for a specific widget.
2304 2490 *
2305 2491 * @param string widget_id the widget identifier string to fetch.
2306 2492 *
@@ -2402,66 +2588,90 @@
2402 2588
2403 2589 /**
2404 2590 * Helper method used to display a modal window dinamycally.
2405 2591 *
2406 - * @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.
2407 2594 *
2408 - * @return object The modal content element wrapper.
2595 + * @return object The modal content element wrapper.
2409 2596 */
2410 - static displayModal(options) {
2597 + static displayModal(options, bindDetails) {
2411 2598 var def_options = {
2412 - suffix: (Math.floor(Math.random() * 100000)) + '',
2599 + suffix: (Math.floor(Math.random() * 100000)) + '',
2413 2600 extra_class: null,
2414 - header: true,
2415 - title: '',
2416 - body: '',
2601 + header: true,
2602 + title: '',
2603 + body: '',
2417 2604 body_prepend: false,
2418 2605 lock_scroll: false,
2419 - draggable: false,
2606 + draggable: false,
2607 + enlargeable: false,
2608 + minimizeable: false,
2609 + escape_dismiss: true,
2420 2610 footer_left: null,
2421 2611 footer_right: null,
2612 + footer_center: null,
2613 + resize_event: null,
2422 2614 dismiss_event: null,
2423 - dismissed_event: null,
2424 - onDismiss: null,
2615 + dismissed_event: 'vbo-modal-dismissed',
2616 + onDismiss: null,
2617 + onMinimize: null,
2618 + onRestore: null,
2425 2619 loading_event: null,
2426 2620 loading_body: VBOCore.options.default_loading_body,
2427 - event_data: null,
2621 + progress_event: null,
2622 + event_data: null,
2428 2623 };
2429 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 +
2430 2630 // merge default options with given options
2431 2631 options = Object.assign(def_options, options);
2432 2632
2433 - // create the modal dismiss function
2434 - var modal_dismiss_fn = (e) => {
2435 - custom_modal.fadeOut(400, () => {
2436 - // invoke callback for onDismiss
2437 - if (typeof options.onDismiss === 'function') {
2438 - options.onDismiss.call(custom_modal, e);
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);
2439 2647 }
2648 + }
2440 2649
2441 - // check if modal did register to the loading event
2442 - if (options.loading_event) {
2443 - // we can now un-register from the loading event until a new modal is displayed and will register to it again
2444 - document.removeEventListener(options.loading_event, modal_handle_loading_event_fn);
2445 - }
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 + }
2446 2654
2447 - // check if we should fire the given modal dismissed event
2448 - if (options.dismissed_event) {
2449 - VBOCore.emitEvent(options.dismissed_event, options.event_data);
2450 - }
2655 + // check if body scroll lock should be removed
2656 + if (options.lock_scroll) {
2657 + $('body').removeClass('vbo-modal-lock-scroll');
2658 + }
2451 2659
2452 - // check if body scroll lock should be removed
2453 - if (options.lock_scroll) {
2454 - $('body').removeClass('vbo-modal-lock-scroll');
2455 - }
2660 + // remove modal from DOM
2661 + custom_modal.remove();
2662 + };
2456 2663
2457 - // remove modal from DOM
2458 - custom_modal.remove();
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);
2459 2669 });
2460 2670 };
2461 2671
2462 2672 // create the modal loading event handler function
2463 - var modal_handle_loading_event_fn = (e) => {
2673 + const modal_handle_loading_event_fn = (e) => {
2464 2674 // toggle modal loading
2465 2675 if ($('.vbo-modal-overlay-content-backdrop').length) {
2466 2676 // hide loading
2467 2677 $('.vbo-modal-overlay-content-backdrop').remove();
@@ -2481,13 +2691,101 @@
2481 2691 // append backdrop loading to modal content
2482 2692 modal_content.prepend(modal_loading);
2483 2693 };
2484 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 +
2485 2783 // start the modal position variables
2486 2784 var modal_pos_x = 0, modal_pos_y = 0;
2487 2785
2488 2786 // create the modal drag-start (mousedown) event handler function
2489 - var modal_dragstart_fn = (e) => {
2787 + const modal_dragstart_fn = (e) => {
2490 2788 e = e || window.event;
2491 2789 e.preventDefault();
2492 2790
2493 2791 if (typeof e.clientX === 'undefined' || typeof e.clientY === 'undefined') {
@@ -2494,9 +2792,9 @@
2494 2792 // unsupported
2495 2793 return;
2496 2794 }
2497 2795
2498 - if (e.target) {
2796 + if (e.target && !e.target.matches('.vbo-modal-overlay-cmd')) {
2499 2797 e.target.style.cursor = 'move';
2500 2798 }
2501 2799
2502 2800 // store the initial modal (cursor) position
@@ -2508,12 +2806,12 @@
2508 2806 document.onmousemove = modal_dragmove_fn;
2509 2807 };
2510 2808
2511 2809 // create the modal drag-stop (mouseup) event handler function
2512 - var modal_dragstop_fn = (e) => {
2810 + const modal_dragstop_fn = (e) => {
2513 2811 e = e || window.event;
2514 2812
2515 - if (e.target) {
2813 + if (e.target && !e.target.matches('.vbo-modal-overlay-cmd')) {
2516 2814 e.target.style.cursor = 'auto';
2517 2815 }
2518 2816
2519 2817 // unregister mousemove event
@@ -2527,9 +2825,9 @@
2527 2825 }
2528 2826 };
2529 2827
2530 2828 // create the modal drag-move (mousemove) event handler function
2531 - var modal_dragmove_fn = (e) => {
2829 + const modal_dragmove_fn = (e) => {
2532 2830 e = e || window.event;
2533 2831 e.preventDefault();
2534 2832
2535 2833 if (typeof e.clientX === 'undefined' || typeof e.clientY === 'undefined') {
@@ -2545,9 +2843,9 @@
2545 2843 modal_pos_x = e.clientX;
2546 2844 modal_pos_y = e.clientY;
2547 2845
2548 2846 // find the modal element
2549 - let modal_element = e.target.closest('.vbo-modal-overlay-content');
2847 + let modal_element = e?.target?.closest('.vbo-modal-overlay-content');
2550 2848 if (!modal_element) {
2551 2849 return;
2552 2850 }
2553 2851
@@ -2556,26 +2854,45 @@
2556 2854 modal_element.style.left = (modal_element.offsetLeft - new_modal_pos_x) + 'px';
2557 2855 };
2558 2856
2559 2857 // build modal content
2560 - 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');
2561 2859 var modal_dismiss = $('<a></a>').addClass('vbo-modal-overlay-close');
2562 2860 modal_dismiss.on('click', modal_dismiss_fn);
2563 2861 custom_modal.append(modal_dismiss);
2564 2862
2565 - 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);
2566 2864 if (options.extra_class && typeof options.extra_class === 'string') {
2567 2865 modal_content.addClass(options.extra_class);
2568 2866 }
2569 2867
2570 - var modal_head = $('<div></div>').addClass('vbo-modal-overlay-content-head');
2571 - var modal_head_close = $('<span></span>').addClass('vbo-modal-overlay-close-times').html('&times;');
2572 - modal_head_close.on('click', modal_dismiss_fn);
2868 + // modal head and title
2869 + const modal_head = $('<div></div>').addClass('vbo-modal-overlay-content-head');
2573 2870 if (options.title) {
2574 2871 let modal_title = $('<span></span>').addClass('vbo-modal-overlay-content-head-title').html(options.title);
2575 2872 modal_head.append(modal_title);
2873 + } else {
2874 + modal_head.addClass('vbo-modal-head-no-title');
2576 2875 }
2577 - modal_head.append(modal_head_close);
2876 + // modal head commands
2877 + var modal_head_cmds = $('<span></span>').addClass('vbo-modal-overlay-cmds');
2878 + var modal_head_close = $('<span></span>').addClass('vbo-modal-overlay-cmd vbo-modal-overlay-close-times').html('&times;');
2879 + modal_head_close.on('click', modal_dismiss_fn);
2880 + modal_head_cmds.append(modal_head_close);
2881 + if (options.enlargeable) {
2882 + var modal_head_enlarge = $('<span></span>').addClass('vbo-modal-overlay-cmd vbo-modal-overlay-cmd-enlarge').html('&square;');
2883 + modal_head_enlarge.on('click', modal_enlarge_fn);
2884 + modal_head_cmds.append(modal_head_enlarge);
2885 + modal_head.on('dblclick', modal_enlarge_fn);
2886 + }
2887 + if (options.minimizeable && options.event_data) {
2888 + // set up the minimize command
2889 + var modal_head_minimize = $('<span></span>').addClass('vbo-modal-overlay-cmd vbo-modal-overlay-cmd-minimize').html('&minus;');
2890 + modal_head_minimize.on('click', modal_minimize_fn);
2891 + modal_head_cmds.append(modal_head_minimize);
2892 + }
2893 + // set commands
2894 + modal_head.append(modal_head_cmds);
2578 2895
2579 2896 // check if the modal head should be draggable
2580 2897 if (options.draggable) {
2581 2898 // register the event(s) to allow dragging
@@ -2585,10 +2902,13 @@
2585 2902 });
2586 2903 modal_head.on('mousedown', modal_dragstart_fn);
2587 2904 }
2588 2905
2589 - var modal_body = $('<div></div>').addClass('vbo-modal-overlay-content-body vbo-modal-overlay-content-body-scroll');
2590 - var modal_content_wrapper = $('<div></div>').addClass('vbo-modal-' + options.suffix + '-wrap');
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 + }
2591 2911 if (typeof options.body === 'string') {
2592 2912 modal_content_wrapper.html(options.body);
2593 2913 } else {
2594 2914 modal_content_wrapper.append(options.body);
@@ -2595,20 +2915,23 @@
2595 2915 }
2596 2916 modal_body.append(modal_content_wrapper);
2597 2917
2598 2918 // modal footer
2599 - var modal_footer = null;
2600 - if (options.footer_left || options.footer_right) {
2919 + let modal_footer = null;
2920 + if (options.footer_left || options.footer_right || options.footer_center) {
2601 2921 modal_footer = $('<div></div>').addClass('vbo-modal-overlay-content-footer');
2602 2922 if (options.footer_left) {
2603 - 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);
2604 2924 modal_footer.append(modal_footer_left);
2605 2925 }
2606 2926 if (options.footer_right) {
2607 - 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);
2608 2928 modal_footer.append(modal_footer_right);
2609 2929 }
2610 -
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 + }
2611 2934 }
2612 2935
2613 2936 // finalize modal contents
2614 2937 if (options.header) {
@@ -2646,11 +2969,13 @@
2646 2969 // trigger the actual dismiss event
2647 2970 VBOCore.emitEvent(options.dismiss_event);
2648 2971 };
2649 2972
2650 - // listen to the Escape keyup event to dismiss the modal
2651 - // listen on window to allow admin-widgets to prevent the default behavior and stop the propagation
2652 - window.addEventListener('keyup', vbo_core_dismiss_event_modal_escape);
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 + }
2653 2978 }
2654 2979
2655 2980 // register to the toggle-loading event
2656 2981 if (options.loading_event) {
@@ -2655,8 +2980,12 @@
2655 2980 // register to the toggle-loading event
2656 2981 if (options.loading_event) {
2657 2982 // let a function handle it so that removing the event listener will be doable
2658 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 + }
2659 2988 }
2660 2989
2661 2990 // append (or prepend) modal to body
2662 2991 if ($('.vbo-modal-overlay-' + options.suffix).length) {
@@ -2680,8 +3009,17 @@
2680 3009 if (options.lock_scroll) {
2681 3010 $('body').addClass('vbo-modal-lock-scroll');
2682 3011 }
2683 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 +
2684 3022 // return the content wrapper element of the new modal
2685 3023 return modal_content_wrapper;
2686 3024 }
2687 3025
@@ -2790,8 +3128,9 @@
2790 3128 }
2791 3129
2792 3130 localStorage.setItem(keyName, value);
2793 3131 } catch(e) {
3132 + console.error(e);
2794 3133 return false;
2795 3134 }
2796 3135
2797 3136 return true;
@@ -3040,17 +3379,99 @@
3040 3379 return menu_actions;
3041 3380 }
3042 3381
3043 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 + /**
3044 3456 * Proxy to access the VBOCurrency object.
3045 3457 *
3046 3458 * @param object options The currency options.
3047 3459 *
3048 - * @return VBOCurrency
3460 + * @return VBOCurrency
3049 3461 */
3050 3462 static getCurrency(options) {
3051 3463 return VBOCurrency.getInstance(options);
3052 3464 }
3465 +
3466 + /**
3467 + * Proxy to access the VBOAdminDock object.
3468 + *
3469 + * @return VBOAdminDock
3470 + */
3471 + static getAdminDock() {
3472 + return VBOAdminDock.getInstance();
3473 + }
3053 3474 }
3054 3475
3055 3476 /**
3056 3477 * These used to be private static properties (static #options),
@@ -3059,9 +3480,9 @@
3059 3480 * without declaring them as static inside the class declaration.
3060 3481 *
3061 3482 * @var object
3062 3483 */
3063 - VBOCore.options = {
3484 + VBOCore.options = CORE_OPTIONS || {
3064 3485 is_vbo: false,
3065 3486 is_vcm: false,
3066 3487 cms: 'wordpress',
3067 3488 widget_ajax_uri: null,
@@ -3073,8 +3494,9 @@
3073 3494 root_uri: '/',
3074 3495 client: 'admin',
3075 3496 panel_opts: {},
3076 3497 admin_widgets: [],
3498 + events_delegated: [],
3077 3499 notif_audio_url: '',
3078 3500 active_listeners: {},
3079 3501 tn_texts: {
3080 3502 notifs_enabled: 'Browser notifications are enabled!',
@@ -3214,8 +3636,11 @@
3214 3636
3215 3637 return ok;
3216 3638 }
3217 3639
3640 + /**
3641 + * VBOCurrency class implementation.
3642 + */
3218 3643 w['VBOCurrency'] = class VBOCurrency {
3219 3644 /**
3220 3645 * Singleton entry-point.
3221 3646 *
@@ -3266,8 +3691,14 @@
3266 3691 let def_digits = this.digits !== undefined ? this.digits : 2;
3267 3692 let def_no_decimals = this.noDecimals !== undefined ? this.noDecimals : 1;
3268 3693 let def_conv_rate = this.conversionRate !== undefined ? this.conversionRate : 1;
3269 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 +
3270 3701 // set currency options
3271 3702 this.symbol = (options.hasOwnProperty('symbol') ? options.symbol : def_symbol);
3272 3703 this.position = (options.hasOwnProperty('position') ? options.position : def_position);
3273 3704 this.decimals = (options.hasOwnProperty('decimals') ? options.decimals : def_decimals);
@@ -3368,8 +3799,31 @@
3368 3799 return price + (options.position == 2 ? ' ' : '') + options.symbol;
3369 3800 }
3370 3801
3371 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 + /**
3372 3826 * Safely sums 2 prices (a + b).
3373 3827 *
3374 3828 * @param float a
3375 3829 * @param float b
@@ -3416,8 +3870,799 @@
3416 3870 b *= Math.pow(10, this.digits);
3417 3871
3418 3872 // do multiplication and go back to decimal
3419 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 + });
3420 4665 }
3421 4666 }
3422 4667
3423 4668 })(jQuery, window);