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 +1883 -120 1.6.7 → 1.8.16 View file →
@@ -1,7 +1,7 @@
1 1 /**
2 - * VikBooking Core v1.6.7
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
@@ -461,11 +549,65 @@
461 549 if (VBOCore.broadcast_push_data) {
462 550 // the next watch-data interval will receive the pushed data information
463 551 VBOCore.broadcast_push_data.postMessage(raw_content);
464 552 }
553 +
554 + // let the window reload the badge counter in case it must be updated
555 + VBOCore.emitEvent('vbo-badge-count-reload');
465 556 }
466 557
467 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 + /**
468 610 * Initializes the multitasking panel for the admin widgets.
469 611 *
470 612 * @param object params the panel object params.
471 613 *
@@ -487,13 +629,16 @@
487 629 wclass_l_large: "vbo-admin-widgets-container-large",
488 630 addws_selector: ".vbo-sidepanel-add-widgets",
489 631 addw_selector: ".vbo-sidepanel-add-widget",
490 632 addw_modal_cls: "vbo-widget-render-modal",
633 + addw_def_cls: "vbo-widget-render-regular",
491 634 addwfs_selector: ".vbo-sidepanel-add-widget-focussed",
492 635 wtags_selector: ".vbo-sidepanel-widget-tags",
636 + wname_selector: ".vbo-sidepanel-widget-name",
493 637 addw_data_attr: "data-vbowidgetid",
494 638 actws_selector: ".vbo-sidepanel-active-widgets",
495 639 editw_selector: ".vbo-sidepanel-edit-widgets-trig",
640 + shortc_selector: ".vbo-sidepanel-shortcut",
496 641 rmwidget_class: "vbo-admin-widgets-widget-remove",
497 642 rmwidget_icn: "",
498 643 dtcwidget_class: "vbo-admin-widgets-widget-detach",
499 644 dtctarget_class: "vbo-admin-widget-head",
@@ -503,8 +648,11 @@
503 648 notif_off_class: "vbo-sidepanel-notifications-off",
504 649 open_class: "vbo-sidepanel-open",
505 650 close_class: "vbo-sidepanel-close",
506 651 cur_widget_cls: "vbo-admin-widgets-container-small",
652 + sortable: true,
653 + sort_save_ev: "vbo-admin-widgets-updateposmp",
654 + sorting: null,
507 655 };
508 656
509 657 if (typeof params === 'object') {
510 658 panel_opts = Object.assign(panel_opts, params);
@@ -519,10 +667,12 @@
519 667 VBOCore.setOptions({
520 668 panel_opts: panel_opts,
521 669 });
522 670
523 - // setup browser notifications
524 - VBOCore.setupNotifications();
671 + if (VBOCore.options.is_vbo) {
672 + // setup browser notifications
673 + VBOCore.setupNotifications();
674 + }
525 675
526 676 // count active widgets on current page
527 677 var tot_active_widgets = VBOCore.options.admin_widgets.length;
528 678 if (tot_active_widgets > 0) {
@@ -567,21 +717,23 @@
567 717 $(VBOCore.options.panel_opts.close_selector).on('click', function() {
568 718 $(VBOCore.options.panel_opts.btn_trigger).trigger('click');
569 719 });
570 720
571 - // register toggle layout buttons
572 - $(VBOCore.options.panel_opts.t_layout_large).on('click', function() {
573 - // large layout
574 - $(VBOCore.options.panel_opts.selector).addClass(VBOCore.options.panel_opts.sclass_l_large).removeClass(VBOCore.options.panel_opts.sclass_l_small);
575 - $(VBOCore.options.panel_opts.wclass_base_sel).addClass(VBOCore.options.panel_opts.wclass_l_large).removeClass(VBOCore.options.panel_opts.wclass_l_small);
576 - VBOCore.options.panel_opts.cur_widget_cls = VBOCore.options.panel_opts.sclass_l_large;
577 - });
578 - $(VBOCore.options.panel_opts.t_layout_small).on('click', function() {
579 - // small layout
580 - $(VBOCore.options.panel_opts.selector).addClass(VBOCore.options.panel_opts.sclass_l_small).removeClass(VBOCore.options.panel_opts.sclass_l_large);
581 - $(VBOCore.options.panel_opts.wclass_base_sel).addClass(VBOCore.options.panel_opts.wclass_l_small).removeClass(VBOCore.options.panel_opts.wclass_l_large);
582 - VBOCore.options.panel_opts.cur_widget_cls = VBOCore.options.panel_opts.sclass_l_small;
583 - });
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 + }
584 736
585 737 // register listener for esc key pressed
586 738 $(document).keyup(function(e) {
587 739 if (!VBOCore.side_panel_on) {
@@ -651,9 +803,9 @@
651 803 }
652 804
653 805 // handle Enter key press to add a widget
654 806 if (e.key === "Enter") {
655 - // 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
656 808 var load_wid_id = null;
657 809 var focussed_wid = $(VBOCore.options.panel_opts.addwfs_selector + ':visible').first();
658 810
659 811 if (focussed_wid.length) {
@@ -666,19 +818,31 @@
666 818 // no widget to render found
667 819 return;
668 820 }
669 821
670 - if (e.shiftKey === true) {
671 - // widget modal rendering
672 - 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');
673 826
674 827 return;
675 828 }
676 829
677 - // widget multitask panel rendering
678 - VBOCore.addWidgetToPanel(load_wid_id);
679 - $(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);
680 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 +
681 845 return;
682 846 }
683 847
684 848 // handle arrow keys selection
@@ -729,15 +893,33 @@
729 893 if (!widget_id || !widget_id.length) {
730 894 return false;
731 895 }
732 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 +
733 902 // determine widget rendering method
734 903 if (e && e.target) {
735 904 let cktarget = $(e.target);
736 - 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))) {
737 907 // widget modal rendering
738 - 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);
739 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 +
740 922 return;
741 923 }
742 924 }
743 925
@@ -744,10 +926,12 @@
744 926 // widget multitask panel rendering
745 927 VBOCore.addWidgetToPanel(widget_id);
746 928 });
747 929
748 - // register listener for updating multitask sidepanel with debounce
749 - 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 + }
750 934
751 935 // subscribe to event for multitask shortcut
752 936 document.addEventListener(VBOCore.multitask_shortcut_event, function() {
753 937 // toggle multitask panel
@@ -759,36 +943,116 @@
759 943 // focus search multitask widgets
760 944 $(VBOCore.options.panel_opts.search_selector).trigger('focus');
761 945 });
762 946
763 - // register click event on edit widgets button
764 - $(VBOCore.options.panel_opts.editw_selector).on('click', function() {
765 - VBOCore.toggleWidgetsPanelEditing(null);
766 - });
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 + });
767 952
768 - // register detach widget buttons
769 - $('.' + VBOCore.options.panel_opts.dtcwidget_class).each(function() {
770 - let widget_wrapper = $(this).parent(VBOCore.options.panel_opts.wclass_base_sel);
771 - let detach_widget_id = widget_wrapper.attr(VBOCore.options.panel_opts.addw_data_attr);
772 - let detach_to_target = widget_wrapper.find('.' + VBOCore.options.panel_opts.dtctarget_class);
773 - 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 + }
774 961 // move detach wrapper onto the target (widget head)
775 962 $(this).prependTo(detach_to_target);
776 - }
777 963
778 - if (!detach_widget_id) {
779 - return;
780 - }
964 + if (!detach_widget_id) {
965 + return;
966 + }
781 967
782 - // register detach action
783 - $(this).on('click', function() {
784 - // detach widget, meaning do a modal rendering
785 - 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 + });
786 973 });
787 - });
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 + }
788 1025 }
789 1026
790 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 + /**
791 1055 * Registers the blur event for the search widgets input.
792 1056 */
793 1057 static registerSearchWidgetsBlur() {
794 1058 if (VBOCore.options.active_listeners.hasOwnProperty('registerSearchWidgetsBlur')) {
@@ -880,12 +1144,13 @@
880 1144 // register detach widget button
881 1145 setTimeout(() => {
882 1146 let detach_elem = $('<div></div>').addClass(VBOCore.options.panel_opts.dtcwidget_class).html(VBOCore.options.panel_opts.dtcwidget_icn);
883 1147 let detach_to_target = widget_elem.find('.' + VBOCore.options.panel_opts.dtctarget_class);
884 - if (detach_to_target.length) {
885 - // move detach wrapper onto the target (widget head)
886 - detach_elem.prependTo(detach_to_target);
1148 + if (!detach_to_target.length) {
1149 + detach_to_target = widget_elem;
887 1150 }
1151 + // move detach wrapper onto the target (widget head)
1152 + detach_elem.prependTo(detach_to_target);
888 1153 // register detach action
889 1154 detach_elem.on('click', function() {
890 1155 // detach widget, meaning do a modal rendering
891 1156 VBOCore.renderModalWidget(widget_id);
@@ -919,8 +1184,10 @@
919 1184 // check added action status
920 1185 if (added === true) {
921 1186 // show button for edit mode
922 1187 triggerer.show();
1188 + // hide shortcut element
1189 + $(VBOCore.options.panel_opts.shortc_selector).hide();
923 1190 return;
924 1191 }
925 1192
926 1193 // grab all widgets
@@ -929,8 +1196,10 @@
929 1196 if (added === false) {
930 1197 if (!editing_widgets.length) {
931 1198 // hide button for edit mode after removing the last widget
932 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();
933 1202 }
934 1203 return;
935 1204 }
936 1205
@@ -1771,11 +2040,12 @@
1771 2040 $('head').append('<link rel="stylesheet" id="' + asset['id'] + '" href="' + asset['href'] + '" media="all" />');
1772 2041 }
1773 2042 });
1774 2043 // widget modal rendering handled by VCM (or an external admin resource)
1775 - let modal_data = VBOCore.renderModalWidget(data['widget_id'], data, options, false);
1776 - if (modal_data.hasOwnProperty('dismissed_event')) {
1777 - // register event to unload all assets
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)
1778 2048 document.addEventListener(modal_data['dismissed_event'], () => {
1779 2049 assets.forEach((asset) => {
1780 2050 if ($('link#' + asset['id']).length) {
1781 2051 $('link#' + asset['id']).remove();
@@ -1891,8 +2161,29 @@
1891 2161 VBOCore.widgets_pushed_data.push(event.data);
1892 2162 }
1893 2163 };
1894 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 + }
1895 2186 }
1896 2187 }
1897 2188
1898 2189 /**
@@ -1929,8 +2220,33 @@
1929 2220 // this will avoid dispatching duplicate notifications
1930 2221 VBOCore.broadcast_watch_data.postMessage(VBOCore.widgets_watch_data);
1931 2222 }
1932 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 + }
1933 2249 } else {
1934 2250 console.error('Unexpected or invalid JSON response', response);
1935 2251 }
1936 2252 } catch(err) {
@@ -1990,8 +2306,9 @@
1990 2306
1991 2307 // define unique modal event names to avoid conflicts
1992 2308 let dismiss_event = 'vbo-dismiss-widget-modal' + modal_js_id;
1993 2309 let loading_event = 'vbo-loading-widget-modal' + modal_js_id;
2310 + let resize_event = 'vbo-resize-widget-modal' + modal_js_id;
1994 2311
1995 2312 // define the modal options
1996 2313 let modal_options = {
1997 2314 suffix: 'widget_modal',
@@ -1999,8 +2316,11 @@
1999 2316 title: data._modalTitle,
2000 2317 body_prepend: true,
2001 2318 lock_scroll: true,
2002 2319 draggable: true,
2320 + enlargeable: true,
2321 + minimizeable: VBOCore.options.is_vbo,
2322 + resize_event: resize_event,
2003 2323 dismiss_event: dismiss_event,
2004 2324 loading_event: loading_event,
2005 2325 loading_body: VBOCore.options.default_loading_body,
2006 2326 dismissed_event: VBOCore.options.widget_modal_dismissed + modal_js_id,
@@ -2031,16 +2351,45 @@
2031 2351 widget_modal.append(content);
2032 2352
2033 2353 // register an admin menu action for the rendered widget
2034 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
2035 2384 try {
2036 2385 // work on Local Storage to register the widget data
2037 2386 VBOCore.registerAdminMenuAction({
2038 - name: details['name'],
2387 + name: details.name,
2039 2388 href: 'JavaScript: void(0);',
2040 2389 widget: widget_id,
2041 - icon: details['icon'],
2042 - style: details['style'],
2390 + icon: details.icon,
2391 + style: details.style,
2043 2392 }, 'widgets');
2044 2393 } catch(e) {
2045 2394 console.error(e);
2046 2395 }
@@ -2102,8 +2451,42 @@
2102 2451 });
2103 2452 }
2104 2453
2105 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 + /**
2106 2489 * Fetches the details for a specific widget.
2107 2490 *
2108 2491 * @param string widget_id the widget identifier string to fetch.
2109 2492 *
@@ -2205,65 +2588,90 @@
2205 2588
2206 2589 /**
2207 2590 * Helper method used to display a modal window dinamycally.
2208 2591 *
2209 - * @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.
2210 2594 *
2211 - * @return object The modal content element wrapper.
2595 + * @return object The modal content element wrapper.
2212 2596 */
2213 - static displayModal(options) {
2597 + static displayModal(options, bindDetails) {
2214 2598 var def_options = {
2215 - suffix: (Math.floor(Math.random() * 100000)) + '',
2599 + suffix: (Math.floor(Math.random() * 100000)) + '',
2216 2600 extra_class: null,
2217 - title: '',
2218 - body: '',
2601 + header: true,
2602 + title: '',
2603 + body: '',
2219 2604 body_prepend: false,
2220 2605 lock_scroll: false,
2221 - draggable: false,
2606 + draggable: false,
2607 + enlargeable: false,
2608 + minimizeable: false,
2609 + escape_dismiss: true,
2222 2610 footer_left: null,
2223 2611 footer_right: null,
2612 + footer_center: null,
2613 + resize_event: null,
2224 2614 dismiss_event: null,
2225 - dismissed_event: null,
2226 - onDismiss: null,
2615 + dismissed_event: 'vbo-modal-dismissed',
2616 + onDismiss: null,
2617 + onMinimize: null,
2618 + onRestore: null,
2227 2619 loading_event: null,
2228 2620 loading_body: VBOCore.options.default_loading_body,
2229 - event_data: null,
2621 + progress_event: null,
2622 + event_data: null,
2230 2623 };
2231 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 +
2232 2630 // merge default options with given options
2233 2631 options = Object.assign(def_options, options);
2234 2632
2235 - // create the modal dismiss function
2236 - var modal_dismiss_fn = (e) => {
2237 - custom_modal.fadeOut(400, () => {
2238 - // invoke callback for onDismiss
2239 - if (typeof options.onDismiss === 'function') {
2240 - 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);
2241 2647 }
2648 + }
2242 2649
2243 - // check if modal did register to the loading event
2244 - if (options.loading_event) {
2245 - // we can now un-register from the loading event until a new modal is displayed and will register to it again
2246 - document.removeEventListener(options.loading_event, modal_handle_loading_event_fn);
2247 - }
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 + }
2248 2654
2249 - // check if we should fire the given modal dismissed event
2250 - if (options.dismissed_event) {
2251 - VBOCore.emitEvent(options.dismissed_event, options.event_data);
2252 - }
2655 + // check if body scroll lock should be removed
2656 + if (options.lock_scroll) {
2657 + $('body').removeClass('vbo-modal-lock-scroll');
2658 + }
2253 2659
2254 - // check if body scroll lock should be removed
2255 - if (options.lock_scroll) {
2256 - $('body').removeClass('vbo-modal-lock-scroll');
2257 - }
2660 + // remove modal from DOM
2661 + custom_modal.remove();
2662 + };
2258 2663
2259 - // remove modal from DOM
2260 - 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);
2261 2669 });
2262 2670 };
2263 2671
2264 2672 // create the modal loading event handler function
2265 - var modal_handle_loading_event_fn = (e) => {
2673 + const modal_handle_loading_event_fn = (e) => {
2266 2674 // toggle modal loading
2267 2675 if ($('.vbo-modal-overlay-content-backdrop').length) {
2268 2676 // hide loading
2269 2677 $('.vbo-modal-overlay-content-backdrop').remove();
@@ -2283,13 +2691,101 @@
2283 2691 // append backdrop loading to modal content
2284 2692 modal_content.prepend(modal_loading);
2285 2693 };
2286 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 +
2287 2783 // start the modal position variables
2288 2784 var modal_pos_x = 0, modal_pos_y = 0;
2289 2785
2290 2786 // create the modal drag-start (mousedown) event handler function
2291 - var modal_dragstart_fn = (e) => {
2787 + const modal_dragstart_fn = (e) => {
2292 2788 e = e || window.event;
2293 2789 e.preventDefault();
2294 2790
2295 2791 if (typeof e.clientX === 'undefined' || typeof e.clientY === 'undefined') {
@@ -2296,9 +2792,9 @@
2296 2792 // unsupported
2297 2793 return;
2298 2794 }
2299 2795
2300 - if (e.target) {
2796 + if (e.target && !e.target.matches('.vbo-modal-overlay-cmd')) {
2301 2797 e.target.style.cursor = 'move';
2302 2798 }
2303 2799
2304 2800 // store the initial modal (cursor) position
@@ -2310,12 +2806,12 @@
2310 2806 document.onmousemove = modal_dragmove_fn;
2311 2807 };
2312 2808
2313 2809 // create the modal drag-stop (mouseup) event handler function
2314 - var modal_dragstop_fn = (e) => {
2810 + const modal_dragstop_fn = (e) => {
2315 2811 e = e || window.event;
2316 2812
2317 - if (e.target) {
2813 + if (e.target && !e.target.matches('.vbo-modal-overlay-cmd')) {
2318 2814 e.target.style.cursor = 'auto';
2319 2815 }
2320 2816
2321 2817 // unregister mousemove event
@@ -2329,9 +2825,9 @@
2329 2825 }
2330 2826 };
2331 2827
2332 2828 // create the modal drag-move (mousemove) event handler function
2333 - var modal_dragmove_fn = (e) => {
2829 + const modal_dragmove_fn = (e) => {
2334 2830 e = e || window.event;
2335 2831 e.preventDefault();
2336 2832
2337 2833 if (typeof e.clientX === 'undefined' || typeof e.clientY === 'undefined') {
@@ -2347,9 +2843,9 @@
2347 2843 modal_pos_x = e.clientX;
2348 2844 modal_pos_y = e.clientY;
2349 2845
2350 2846 // find the modal element
2351 - let modal_element = e.target.closest('.vbo-modal-overlay-content');
2847 + let modal_element = e?.target?.closest('.vbo-modal-overlay-content');
2352 2848 if (!modal_element) {
2353 2849 return;
2354 2850 }
2355 2851
@@ -2358,32 +2854,61 @@
2358 2854 modal_element.style.left = (modal_element.offsetLeft - new_modal_pos_x) + 'px';
2359 2855 };
2360 2856
2361 2857 // build modal content
2362 - 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');
2363 2859 var modal_dismiss = $('<a></a>').addClass('vbo-modal-overlay-close');
2364 2860 modal_dismiss.on('click', modal_dismiss_fn);
2365 2861 custom_modal.append(modal_dismiss);
2366 2862
2367 - 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);
2368 2864 if (options.extra_class && typeof options.extra_class === 'string') {
2369 2865 modal_content.addClass(options.extra_class);
2370 2866 }
2371 2867
2372 - var modal_head = $('<div></div>').addClass('vbo-modal-overlay-content-head');
2373 - 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;');
2374 2879 modal_head_close.on('click', modal_dismiss_fn);
2375 - 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);
2376 2895
2377 2896 // check if the modal head should be draggable
2378 2897 if (options.draggable) {
2379 - // register the event to allow dragging
2898 + // register the event(s) to allow dragging
2380 2899 modal_head.addClass('vbo-modal-head-draggable');
2900 + modal_head.on('contextmenu', (e) => {
2901 + e.preventDefault();
2902 + });
2381 2903 modal_head.on('mousedown', modal_dragstart_fn);
2382 2904 }
2383 2905
2384 - var modal_body = $('<div></div>').addClass('vbo-modal-overlay-content-body vbo-modal-overlay-content-body-scroll');
2385 - 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 + }
2386 2911 if (typeof options.body === 'string') {
2387 2912 modal_content_wrapper.html(options.body);
2388 2913 } else {
2389 2914 modal_content_wrapper.append(options.body);
@@ -2390,25 +2915,34 @@
2390 2915 }
2391 2916 modal_body.append(modal_content_wrapper);
2392 2917
2393 2918 // modal footer
2394 - var modal_footer = null;
2395 - if (options.footer_left || options.footer_right) {
2919 + let modal_footer = null;
2920 + if (options.footer_left || options.footer_right || options.footer_center) {
2396 2921 modal_footer = $('<div></div>').addClass('vbo-modal-overlay-content-footer');
2397 2922 if (options.footer_left) {
2398 - 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);
2399 2924 modal_footer.append(modal_footer_left);
2400 2925 }
2401 2926 if (options.footer_right) {
2402 - 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);
2403 2928 modal_footer.append(modal_footer_right);
2404 2929 }
2405 -
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 + }
2406 2934 }
2407 2935
2408 2936 // finalize modal contents
2409 - 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);
2410 2943 if (modal_footer) {
2944 + // append footer
2411 2945 modal_content.append(modal_footer);
2412 2946 }
2413 2947 custom_modal.append(modal_content);
2414 2948
@@ -2429,16 +2963,19 @@
2429 2963 return;
2430 2964 }
2431 2965
2432 2966 // immediately unregister from this event once fired
2433 - document.removeEventListener(e.type, vbo_core_dismiss_event_modal_escape);
2967 + window.removeEventListener(e.type, vbo_core_dismiss_event_modal_escape);
2434 2968
2435 2969 // trigger the actual dismiss event
2436 2970 VBOCore.emitEvent(options.dismiss_event);
2437 2971 };
2438 2972
2439 - // listen to the Escape keyup event to dismiss the modal
2440 - document.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 + }
2441 2978 }
2442 2979
2443 2980 // register to the toggle-loading event
2444 2981 if (options.loading_event) {
@@ -2443,8 +2980,12 @@
2443 2980 // register to the toggle-loading event
2444 2981 if (options.loading_event) {
2445 2982 // let a function handle it so that removing the event listener will be doable
2446 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 + }
2447 2988 }
2448 2989
2449 2990 // append (or prepend) modal to body
2450 2991 if ($('.vbo-modal-overlay-' + options.suffix).length) {
@@ -2468,8 +3009,17 @@
2468 3009 if (options.lock_scroll) {
2469 3010 $('body').addClass('vbo-modal-lock-scroll');
2470 3011 }
2471 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 +
2472 3022 // return the content wrapper element of the new modal
2473 3023 return modal_content_wrapper;
2474 3024 }
2475 3025
@@ -2507,8 +3057,36 @@
2507 3057 }
2508 3058 }
2509 3059
2510 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 + /**
2511 3089 * Tells whether localStorage is supported.
2512 3090 *
2513 3091 * @return boolean
2514 3092 */
@@ -2550,8 +3128,9 @@
2550 3128 }
2551 3129
2552 3130 localStorage.setItem(keyName, value);
2553 3131 } catch(e) {
3132 + console.error(e);
2554 3133 return false;
2555 3134 }
2556 3135
2557 3136 return true;
@@ -2676,9 +3255,9 @@
2676 3255 if (!menu_action_entry.hasOwnProperty('widget') && menu_actions[i]['href'] == menu_action_entry['href']) {
2677 3256 // duplicate link
2678 3257 return false;
2679 3258 }
2680 - if (menu_action_entry.hasOwnProperty('widget') && menu_actions[i].hasOwnProperty('widget') && menu_actions[i]['href'] == menu_action_entry['widget']) {
3259 + if (menu_action_entry.hasOwnProperty('widget') && menu_actions[i].hasOwnProperty('widget') && menu_actions[i]['widget'] == menu_action_entry['widget']) {
2681 3260 // duplicate widget
2682 3261 return false;
2683 3262 }
2684 3263 if (menu_actions[i].hasOwnProperty('pinned') && menu_actions[i]['pinned']) {
@@ -2798,8 +3377,101 @@
2798 3377 }
2799 3378
2800 3379 return menu_actions;
2801 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 + }
2802 3474 }
2803 3475
2804 3476 /**
2805 3477 * These used to be private static properties (static #options),
@@ -2808,9 +3480,9 @@
2808 3480 * without declaring them as static inside the class declaration.
2809 3481 *
2810 3482 * @var object
2811 3483 */
2812 - VBOCore.options = {
3484 + VBOCore.options = CORE_OPTIONS || {
2813 3485 is_vbo: false,
2814 3486 is_vcm: false,
2815 3487 cms: 'wordpress',
2816 3488 widget_ajax_uri: null,
@@ -2822,8 +3494,9 @@
2822 3494 root_uri: '/',
2823 3495 client: 'admin',
2824 3496 panel_opts: {},
2825 3497 admin_widgets: [],
3498 + events_delegated: [],
2826 3499 notif_audio_url: '',
2827 3500 active_listeners: {},
2828 3501 tn_texts: {
2829 3502 notifs_enabled: 'Browser notifications are enabled!',
@@ -2869,8 +3542,14 @@
2869 3542 */
2870 3543 VBOCore.side_panel_on = false;
2871 3544
2872 3545 /**
3546 + * @var bool
3547 + * @since 1.6.8
3548 + */
3549 + VBOCore.throttle_timer = false;
3550 +
3551 + /**
2873 3552 * @var array
2874 3553 */
2875 3554 VBOCore.notifications = [];
2876 3555
@@ -2896,10 +3575,1094 @@
2896 3575 */
2897 3576 VBOCore.broadcast_push_data = null;
2898 3577
2899 3578 /**
3579 + * @var object
3580 + * @since 1.6.8
3581 + */
3582 + VBOCore.broadcast_watch_events = null;
3583 +
3584 + /**
2900 3585 * @var array<object>
2901 3586 * @since 1.6.5
2902 3587 */
2903 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 + }
2904 4667
2905 4668 })(jQuery, window);