Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
File Manager
/
wp-content
/
plugins
/
vikbooking
/
admin
/
resources
:
vbocore.js
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
/** * VikBooking Core v1.8.0 * Copyright (C) 2025 E4J s.r.l. All Rights Reserved. * http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL * https://vikwp.com | https://e4j.com | https://e4jconnect.com */ (function($, w) { 'use strict'; /** * AJAX loading of Views may reload the already existing options, so we cache them when available. */ var CORE_OPTIONS = typeof VBOCore !== 'undefined' ? VBOCore?.options : null; /** * VBOCore class implementation. */ w['VBOCore'] = class VBOCore { /** * Proxy to support static injection of params. */ constructor(params) { VBOCore.setOptions(params); } /** * Inject options by overriding default properties. * * @param object params * * @return self */ static setOptions(params) { if (params != null && typeof params === 'object') { VBOCore.options = Object.assign(VBOCore.options, params); } return VBOCore; } /** * Fires a given function when the DOM content has loaded. * * @param function fireFn The function to fire when the document is ready. * * @return undefined */ static DOMLoaded(fireFn) { if (typeof fireFn !== 'function') { throw new Error('Invalid argument provided'); } if (document.readyState === 'loading') { // register event because DOMContentLoaded hasn't finished yet document.addEventListener('DOMContentLoaded', fireFn); } else { // DOMContentLoaded event has fired already fireFn(); } } /** * Getter for admin_widgets private options property. * * @return array */ static get admin_widgets() { return VBOCore.options.admin_widgets; } /** * Getter for multitask open event private property. * * @return string */ static get multitask_open_event() { return VBOCore.options.multitask_open_event; } /** * Getter for multitask close event private property. * * @return string */ static get multitask_close_event() { return VBOCore.options.multitask_close_event; } /** * Getter for multitask shortcut event private property. * * @return string */ static get multitask_shortcut_event() { return VBOCore.options.multitask_shortcut_ev; } /** * Getter for multitask seach focus shortcut event private property. * * @return string */ static get multitask_searchfs_event() { return VBOCore.options.multitask_searchfs_ev; } /** * Getter for multitask event widget modal rendered. * * @return string */ static get widget_modal_rendered() { return VBOCore.options.widget_modal_rendered; } /** * Getter for widget modal dismiss event. * * @return string */ static get widget_modal_dismissed() { return VBOCore.options.widget_modal_dismissed; } /** * Getter for the service worker file path. * * @return string */ static get service_worker_path() { return VBOCore.options.service_worker_path; } /** * Getter for the service worker scope. * * @return string */ static get service_worker_scope() { return VBOCore.options.service_worker_scope; } /** * Getter for the push application key. * * @return string */ static get push_application_key() { return VBOCore.options.push.application_key; } /** * Getter for the push storage endpoint identifier. * * @return string */ static get push_storage_endpoint_id() { return VBOCore.options.push_options.storage_endp_id; } /** * Parses an AJAX response error object. * * @param object err * * @return bool */ static isConnectionLostError(err) { if (!err || !err.hasOwnProperty('status')) { return false; } return ( err.statusText == 'error' && err.status == 0 && (err.readyState == 0 || err.readyState == 4) && (!err.hasOwnProperty('responseText') || err.responseText == '') ); } /** * Ensures AJAX requests that fail due to connection errors are retried automatically. * * @param string url * @param object data * @param function success * @param function failure * @param number attempt */ static doAjax(url, data, success, failure, attempt) { const AJAX_MAX_ATTEMPTS = 3; if (attempt === undefined) { attempt = 1; } return $.ajax({ type: 'POST', url: url, data: data }).done(function(resp) { if (success !== undefined) { // launch success callback function success(resp); } }).fail(function(err) { /** * If the error is caused by a site connection lost, and if the number * of retries is lower than max attempts, retry the same AJAX request. */ if (attempt < AJAX_MAX_ATTEMPTS && VBOCore.isConnectionLostError(err)) { // delay the retry by half second setTimeout(function() { // re-launch same request and increase number of attempts console.log('Retrying previous AJAX request'); VBOCore.doAjax(url, data, success, failure, (attempt + 1)); }, 500); } else { // launch the failure callback otherwise if (failure !== undefined) { if (err.responseText === 'false') { // make the property empty to rely on others err.responseText = ''; } failure(err); } } if (!err.status || err.status == 500) { // log the error in console console.error('AJAX request failed' + (err.status == 500 ? ' (' + err.responseText + ')' : ''), err); } }); } /** * Matches a keyword against a text. * * @param string search the keyword to search. * @param string text the text to compare. * * @return bool */ static matchString(search, text) { return ((text + '').indexOf(search) >= 0); } /** * Converts a base64 URL for a server key into an array of 8-bit unsigned integers. * * @param string key the base64 encoded URL key. * * @return Uint8Array */ static base64ToUint8Array(key) { const padding = ('=').repeat((4 - (key.length % 4)) % 4); const base64 = (key + padding).replace(/\-/g, '+').replace(/_/g, '/'); const rawData = window.atob(base64); const uint8_arr = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { uint8_arr[i] = rawData.charCodeAt(i); } return uint8_arr; } /** * Builds an object containing the Push subscription details and data. * * @param PushSubscription pushSubscription the registration data. * @param object data the extra data to merge. * * @return object */ static buildPushSubscriptionData(pushSubscription, data) { if (!(pushSubscription instanceof PushSubscription)) { return data; } let key = pushSubscription.getKey('p256dh'); let token = pushSubscription.getKey('auth'); let contentEncoding = (PushManager.supportedContentEncodings || ['aesgcm'])[0]; let details = { endpoint: pushSubscription.endpoint, publicKey: key ? btoa(String.fromCharCode.apply(null, new Uint8Array(key))) : null, authToken: token ? btoa(String.fromCharCode.apply(null, new Uint8Array(token))) : null, encoding: contentEncoding, }; return Object.assign(details, data); } /** * Attempts to install (and eventually update) the VikBooking's Service Worker. * * @param bool update if defined, it will update the Service Worker Registration. * * @return Promise */ static installServiceWorker(update) { // check if support for Web App badge is available if (typeof navigator.setAppBadge !== 'undefined') { // always clear the app badge navigator.clearAppBadge(); } return new Promise((resolve, reject) => { if (!('serviceWorker' in navigator)) { reject('Service workers are not supported by this browser'); return; } if (!('PushManager' in window)) { reject('Push notifications are not supported by this browser'); return; } if (!VBOCore.service_worker_path) { reject('Service Worker path not configured'); return; } if (!VBOCore.notificationsEnabled()) { reject('Notifications are disabled, and so the service worker will not be installed'); return; } navigator.serviceWorker.register(VBOCore.service_worker_path, {scope: VBOCore.service_worker_scope}).then( (registration) => { if (typeof update !== 'undefined') { // attempt to update the registration for un-caching purposes registration.update().then((updated) => { // all good }).catch((e) => { // updating should never fail after registering with success console.error(`Service worker registration update failed: ${e}`); }); } // listen to the ServiceWorker messages for the client VBOCore.listenServiceWorkerMessages(); // resolve the Promise resolve(registration); }, (e) => { console.error(`Service worker registration failed: ${e}`); reject('Service worker registration failed'); } ); }); } /** * Handles the Push subscription state. To be called after the * Service Worker installation (registration) is ready. * * @param ServiceWorkerRegistration registration sw registration object. * * @return Promise */ static handlePushSubscription(registration) { return new Promise((resolve, reject) => { if (!(registration instanceof ServiceWorkerRegistration)) { reject('Missing service worker registration'); return; } if (!VBOCore.push_application_key) { reject('Missing application key for Push because not supported'); return; } // check if a push subscription is available registration.pushManager.getSubscription().then((pushSubscription) => { if (pushSubscription) { // we are subscribed to Push let previous_endpoint = VBOCore.storageGetItem(VBOCore.push_storage_endpoint_id); if (previous_endpoint && pushSubscription.endpoint != previous_endpoint) { // update subscription data VBOCore.doAjax( VBOCore.options.push.ajax_url, VBOCore.buildPushSubscriptionData(pushSubscription, { agent: window.navigator.userAgent, type: 'update', }) ); } // resolve the promise with the Push subscription data resolve(pushSubscription); return; } // push subscription options const pushOptions = { userVisibleOnly: true, applicationServerKey: VBOCore.base64ToUint8Array(VBOCore.push_application_key), }; // subscribe to Push registration.pushManager.subscribe(pushOptions).then( (pushSubscription) => { // store subscription data VBOCore.storageSetItem(VBOCore.push_storage_endpoint_id, pushSubscription.endpoint); VBOCore.doAjax( VBOCore.options.push.ajax_url, VBOCore.buildPushSubscriptionData(pushSubscription, { agent: window.navigator.userAgent, type: 'new', }) ); // resolve the promise with the Push subscription data resolve(pushSubscription); }, (e) => { console.error(`Error handling the Push subscription state: ${e}`); reject('Error handling the Push subscription state'); } ); }) .catch((e) => { console.error(`Error getting the Push subscription state: ${e}`); reject('Error getting the Push subscription state'); }); }); } /** * Starts listening to the "message" events posted by the ServiceWorker. * * @return void */ static listenServiceWorkerMessages() { if (!('serviceWorker' in navigator)) { return; } try { // add listener to the message event for the data posted by the ServiceWorker to the client navigator.serviceWorker.addEventListener('message', (event) => { VBOCore.handleServiceWorkerMessage(event.data); }); } catch(e) { // do nothing } } /** * Handles a message data posted by the ServiceWorker upon clicking a Push notification. * * @param object data the event.notification.data object posted by the ServiceWorker. * * @return void */ static handleServiceWorkerMessage(data) { let notif_type = data.type || ''; if (!notif_type || !VBOCore.options.push_options.allowed_notif_types.includes(notif_type)) { console.error(data); throw new Error('Unsupported message type'); } // determine the proper widget for dispatching the message data let widget_id = 'booking_details'; if (notif_type == 'Chat') { widget_id = 'guest_messages'; } // the raw notification content let raw_content = data.content || {}; // multitask data options let options = Object.assign({ _push: 1, title: data.title || '', message: data.message || '', }, raw_content); // dispatch (Push) message data on widget VBOCore.handleDisplayWidgetNotification({widget_id: widget_id}, options, true); // update pushed data map for the next watching event in the current browsing context VBOCore.widgets_pushed_data.push(raw_content); // post message onto broadcast channel for any other browsing context if (VBOCore.broadcast_push_data) { // the next watch-data interval will receive the pushed data information VBOCore.broadcast_push_data.postMessage(raw_content); } // let the window reload the badge counter in case it must be updated VBOCore.emitEvent('vbo-badge-count-reload'); } /** * Ensures VBO is ready to support VCM. * * @param any env optional data to evaluate. * * @return bool */ static vcmMultitasking(env) { // tell VCM that VBOCore supports it return true; } /** * Initializes the multitasking panel for the admin widgets. * * @param object params the panel object params. * * @return bool */ static prepareMultitasking(params) { var panel_opts = { selector: "", sclass_l_small: "vbo-sidepanel-right", sclass_l_large: "vbo-sidepanel-large", btn_trigger: "", search_selector: "#vbo-sidepanel-search-input", search_nores: ".vbo-sidepanel-add-widgets-nores", close_selector: ".vbo-sidepanel-dismiss-btn", t_layout_small: ".vbo-sidepanel-layout-small", t_layout_large: ".vbo-sidepanel-layout-large", wclass_base_sel: ".vbo-admin-widgets-widget-output", wclass_l_small: "vbo-admin-widgets-container-small", wclass_l_large: "vbo-admin-widgets-container-large", addws_selector: ".vbo-sidepanel-add-widgets", addw_selector: ".vbo-sidepanel-add-widget", addw_modal_cls: "vbo-widget-render-modal", addw_def_cls: "vbo-widget-render-regular", addwfs_selector: ".vbo-sidepanel-add-widget-focussed", wtags_selector: ".vbo-sidepanel-widget-tags", wname_selector: ".vbo-sidepanel-widget-name", addw_data_attr: "data-vbowidgetid", actws_selector: ".vbo-sidepanel-active-widgets", editw_selector: ".vbo-sidepanel-edit-widgets-trig", shortc_selector: ".vbo-sidepanel-shortcut", rmwidget_class: "vbo-admin-widgets-widget-remove", rmwidget_icn: "", dtcwidget_class: "vbo-admin-widgets-widget-detach", dtctarget_class: "vbo-admin-widget-head", dtcwidget_icn: "", notif_selector: ".vbo-sidepanel-notifications-btn", notif_on_class: "vbo-sidepanel-notifications-on", notif_off_class: "vbo-sidepanel-notifications-off", open_class: "vbo-sidepanel-open", close_class: "vbo-sidepanel-close", cur_widget_cls: "vbo-admin-widgets-container-small", sortable: true, sort_save_ev: "vbo-admin-widgets-updateposmp", sorting: null, }; if (typeof params === 'object') { panel_opts = Object.assign(panel_opts, params); } if (!panel_opts.btn_trigger || !panel_opts.selector) { console.error('Got no trigger or selector'); return false; } // push panel options VBOCore.setOptions({ panel_opts: panel_opts, }); if (VBOCore.options.is_vbo) { // setup browser notifications VBOCore.setupNotifications(); } // count active widgets on current page var tot_active_widgets = VBOCore.options.admin_widgets.length; if (tot_active_widgets > 0) { // hide add-widgets container $(panel_opts.addws_selector).hide(); // register listener for input search blur VBOCore.registerSearchWidgetsBlur(); } // register click event on trigger button $(VBOCore.options.panel_opts.btn_trigger).on('click', function() { var side_panel = $(VBOCore.options.panel_opts.selector); if (side_panel.hasClass(VBOCore.options.panel_opts.open_class)) { // hide panel VBOCore.side_panel_on = false; VBOCore.emitMultitaskEvent(VBOCore.multitask_close_event); side_panel.addClass(VBOCore.options.panel_opts.close_class).removeClass(VBOCore.options.panel_opts.open_class); // always hide add-widgets container $(VBOCore.options.panel_opts.addws_selector).hide(); // check if we are currently editing var is_editing = ($('.' + VBOCore.options.panel_opts.editmode_class).length > 0); if (is_editing) { // deactivate editing mode VBOCore.toggleWidgetsPanelEditing(null); } } else { // show panel VBOCore.side_panel_on = true; VBOCore.emitMultitaskEvent(VBOCore.multitask_open_event); side_panel.addClass(VBOCore.options.panel_opts.open_class).removeClass(VBOCore.options.panel_opts.close_class); if (!VBOCore.options.admin_widgets.length) { // set focus on search widgets input with delay for the opening animation setTimeout(function() { $(VBOCore.options.panel_opts.search_selector).focus(); }, 300); } } }); // register close/dismiss button $(VBOCore.options.panel_opts.close_selector).on('click', function() { $(VBOCore.options.panel_opts.btn_trigger).trigger('click'); }); if (VBOCore.options.is_vbo) { // register toggle layout buttons $(VBOCore.options.panel_opts.t_layout_large).on('click', function() { // large layout $(VBOCore.options.panel_opts.selector).addClass(VBOCore.options.panel_opts.sclass_l_large).removeClass(VBOCore.options.panel_opts.sclass_l_small); $(VBOCore.options.panel_opts.wclass_base_sel).addClass(VBOCore.options.panel_opts.wclass_l_large).removeClass(VBOCore.options.panel_opts.wclass_l_small); VBOCore.options.panel_opts.cur_widget_cls = VBOCore.options.panel_opts.sclass_l_large; }); $(VBOCore.options.panel_opts.t_layout_small).on('click', function() { // small layout $(VBOCore.options.panel_opts.selector).addClass(VBOCore.options.panel_opts.sclass_l_small).removeClass(VBOCore.options.panel_opts.sclass_l_large); $(VBOCore.options.panel_opts.wclass_base_sel).addClass(VBOCore.options.panel_opts.wclass_l_small).removeClass(VBOCore.options.panel_opts.wclass_l_large); VBOCore.options.panel_opts.cur_widget_cls = VBOCore.options.panel_opts.sclass_l_small; }); } // register listener for esc key pressed $(document).keyup(function(e) { if (!VBOCore.side_panel_on) { return; } if ((e.key && e.key === "Escape") || (e.keyCode && e.keyCode == 27)) { $(VBOCore.options.panel_opts.btn_trigger).trigger('click'); } }); // register listener for input search focus $(VBOCore.options.panel_opts.search_selector).on('focus', function() { // always show add-widgets container var widget_focus_class = VBOCore.options.panel_opts.addwfs_selector.replace('.', ''); $(VBOCore.options.panel_opts.addw_selector).removeClass(widget_focus_class); $(VBOCore.options.panel_opts.addws_selector).show(); }); // register listener on input search widget $(VBOCore.options.panel_opts.search_selector).keyup(function(e) { // get the keyword to look for var keyword = $(this).val(); // counting matching widgets var matching = 0; var first_matched = null; var widget_focus_class = VBOCore.options.panel_opts.addwfs_selector.replace('.', ''); // adjust widgets to be displayed if (!keyword.length) { // show all widgets for selection $(VBOCore.options.panel_opts.addw_selector).show(); // hide "no results" $(VBOCore.options.panel_opts.search_nores).hide(); // all widgets are matching matching = $(VBOCore.options.panel_opts.addw_selector).length; } else { // make the keyword lowercase keyword = (keyword + '').toLowerCase(); // parse all widget's description tags $(VBOCore.options.panel_opts.addw_selector).each(function() { var elem = $(this); var descr = elem.find(VBOCore.options.panel_opts.wtags_selector).text(); if (VBOCore.matchString(keyword, descr)) { elem.show(); matching++; if (!first_matched) { // store the first widget that matched first_matched = elem.attr(VBOCore.options.panel_opts.addw_data_attr); } } else { elem.hide(); } }); // check how many widget matched if (matching > 0) { // hide "no results" $(VBOCore.options.panel_opts.search_nores).hide(); } else { // show "no results" $(VBOCore.options.panel_opts.search_nores).show(); } } // check for shortcuts if (!e.key) { return; } // handle Enter key press to add a widget if (e.key === "Enter") { // on Enter key pressed, open/add the first matching widget or the focussed one var load_wid_id = null; var focussed_wid = $(VBOCore.options.panel_opts.addwfs_selector + ':visible').first(); if (focussed_wid.length) { load_wid_id = focussed_wid.attr(VBOCore.options.panel_opts.addw_data_attr); } else if (first_matched) { load_wid_id = first_matched; } if (!load_wid_id) { // no widget to render found return; } if (e.shiftKey === true && !VBOCore.options.is_vcm) { // widget multitask panel rendering VBOCore.addWidgetToPanel(load_wid_id); $(VBOCore.options.panel_opts.search_selector).trigger('blur'); return; } // widget modal rendering is the default method if (VBOCore.options.is_vcm) { // register loading effect with automatic cancellation let orig_name = (focussed_wid.length ? focussed_wid : first_matched).find(VBOCore.options.panel_opts.wname_selector).text(); (focussed_wid.length ? focussed_wid : first_matched).find(VBOCore.options.panel_opts.wname_selector).html(orig_name + ' ' + VBOCore.options.default_loading_body); setTimeout(() => { (focussed_wid.length ? focussed_wid : first_matched).find(VBOCore.options.panel_opts.wname_selector).text(orig_name); }, 1000); // assets will be loaded within VCM before rendering the modal widget VBOCore.handleDisplayWidgetNotification({widget_id: load_wid_id}); } else { VBOCore.renderModalWidget(load_wid_id); } return; } // handle arrow keys selection if (matching > 0 && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) { // on arrow key pressed, select the next or prev widget var addws_element = $(VBOCore.options.panel_opts.addws_selector); var addws_cont_pos = addws_element.offset().top; var addws_otheight = addws_element.outerHeight(); var addws_scrolltp = addws_element.scrollTop(); if (e.key === 'ArrowDown') { var default_widg = $(VBOCore.options.panel_opts.addw_selector + ':visible').first(); } else { var default_widg = $(VBOCore.options.panel_opts.addw_selector + ':visible').last(); } var focussed_wid = $(VBOCore.options.panel_opts.addwfs_selector + ':visible').first(); var addw_height = default_widg.outerHeight(); var focussed_pos = default_widg.offset().top + addw_height; if (focussed_wid.length) { focussed_wid.removeClass(widget_focus_class); if (e.key === 'ArrowDown') { var goto_wid = focussed_wid.nextAll(VBOCore.options.panel_opts.addw_selector + ':visible').first(); } else { var goto_wid = focussed_wid.prevAll(VBOCore.options.panel_opts.addw_selector + ':visible').first(); } if (goto_wid.length) { goto_wid.addClass(widget_focus_class); focussed_pos = goto_wid.offset().top + addw_height; } else { default_widg.addClass(widget_focus_class); } } else { default_widg.addClass(widget_focus_class); } if (focussed_pos > (addws_cont_pos + addws_otheight)) { addws_element.scrollTop(focussed_pos - addws_cont_pos - addw_height + addws_scrolltp); } else if (focussed_pos < 0) { addws_element.scrollTop(0); } } }); // register listener for adding widgets $(VBOCore.options.panel_opts.addw_selector).on('click', function(e) { var widget_id = $(this).attr(VBOCore.options.panel_opts.addw_data_attr); if (!widget_id || !widget_id.length) { return false; } if (VBOCore.options.panel_opts.sorting) { // prevent dropped widgets after sorting to be added to the panel return false; } // determine widget rendering method if (e && e.target) { let cktarget = $(e.target); let is_main_elem_clicked = cktarget.length && (cktarget.hasClass('vbo-sidepanel-widget-info-det') || cktarget.hasClass('vbo-sidepanel-widget-name')); 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))) { // widget modal rendering if (VBOCore.options.is_vcm) { // register loading effect with automatic cancellation let orig_name = $(this).find(VBOCore.options.panel_opts.wname_selector).text(); $(this).find(VBOCore.options.panel_opts.wname_selector).html(orig_name + ' ' + VBOCore.options.default_loading_body); setTimeout(() => { $(this).find(VBOCore.options.panel_opts.wname_selector).text(orig_name); }, 1000); // assets will be loaded within VCM before rendering the modal widget VBOCore.handleDisplayWidgetNotification({widget_id: widget_id}); } else { VBOCore.renderModalWidget(widget_id); } return; } } // widget multitask panel rendering VBOCore.addWidgetToPanel(widget_id); }); if (VBOCore.options.is_vbo) { // register listener for updating multitask sidepanel with debounce document.addEventListener(VBOCore.options.multitask_save_event, VBOCore.debounceEvent(VBOCore.saveMultitasking, 1000)); } // subscribe to event for multitask shortcut document.addEventListener(VBOCore.multitask_shortcut_event, function() { // toggle multitask panel $(VBOCore.options.panel_opts.btn_trigger).trigger('click'); }); // subscribe to event for multitask search focus shortcut document.addEventListener(VBOCore.multitask_searchfs_event, function() { // focus search multitask widgets $(VBOCore.options.panel_opts.search_selector).trigger('focus'); }); if (VBOCore.options.is_vbo) { // register click event on edit widgets button $(VBOCore.options.panel_opts.editw_selector).on('click', function() { VBOCore.toggleWidgetsPanelEditing(null); }); // register detach widget buttons $('.' + VBOCore.options.panel_opts.dtcwidget_class).each(function() { let widget_wrapper = $(this).parent(VBOCore.options.panel_opts.wclass_base_sel); let detach_widget_id = widget_wrapper.attr(VBOCore.options.panel_opts.addw_data_attr); let detach_to_target = widget_wrapper.find('.' + VBOCore.options.panel_opts.dtctarget_class); if (!detach_to_target.length) { detach_to_target = widget_wrapper; } // move detach wrapper onto the target (widget head) $(this).prependTo(detach_to_target); if (!detach_widget_id) { return; } // register detach action $(this).on('click', function() { // detach widget, meaning do a modal rendering VBOCore.renderModalWidget(detach_widget_id); }); }); } if (VBOCore.options.panel_opts.sortable && typeof $.fn.sortable !== 'undefined') { // make the admin widgets sortable let sortable_env = VBOCore.options.is_vcm ? 'vcm-sidepanel-add-widgets' : 'vbo-sidepanel-add-widgets'; let handle_env = VBOCore.options.is_vcm ? 'vcm-sidepanel-widget-info-det' : 'vbo-sidepanel-widget-info-det'; let item_env = VBOCore.options.is_vcm ? 'vcm-sidepanel-add-widget' : 'vbo-sidepanel-add-widget'; // default sorting flag VBOCore.options.panel_opts.sorting = null; // register listener for updating the widget sorting values on the multitask sidepanel with debounce document.addEventListener(VBOCore.options.panel_opts.sort_save_ev, VBOCore.debounceEvent(VBOCore.saveMultitaskingSorting, 500)); // apply sorting capabilities $('.' + sortable_env).sortable({ axix: 'x', cursor: 'move', handle: '.' + handle_env, items: '.' + item_env, containment: 'parent', revert: false, start: function(event, ui) { // update flag VBOCore.options.panel_opts.sorting = $(ui.item).attr('data-vbowidgetid'); // set is-sorting class $(ui.item).addClass('is-sorting'); }, stop: function(event, ui) { // make sure no elements are being sorted $('.' + item_env).removeClass('is-sorting'); // register delayed update flag to allow understanding a sorting was just completed setTimeout(() => { // restore default sorting flag VBOCore.options.panel_opts.sorting = null; }, 500); }, update: function(event, ui) { // build the current widgets position list let pos_list = {}; document.querySelectorAll('.' + item_env).forEach((elem, index) => { let curr_wid = elem.getAttribute('data-vbowidgetid'); pos_list[curr_wid] = index; }); // trigger the event to update the widget positions VBOCore.emitEvent(VBOCore.options.panel_opts.sort_save_ev, { pos_list: pos_list, }); } }); } } /** * Registers the callback for saving the new widget sorting value in the Multitask panel. */ static saveMultitaskingSorting(e) { if (!e || !e.detail || !e.detail.pos_list) { return; } // update multitask widget position VBOCore.doAjax( VBOCore.options.multitask_ajax_uri, { call: 'setMultitaskingWidgetPos', call_args: [ e.detail.pos_list, ], }, (response) => { // do nothing on success }, (error) => { // silently log the error console.error(error.responseText); } ); } /** * Registers the blur event for the search widgets input. */ static registerSearchWidgetsBlur() { if (VBOCore.options.active_listeners.hasOwnProperty('registerSearchWidgetsBlur')) { // listener is already registered return true; } $(VBOCore.options.panel_opts.search_selector).on('blur', function(e) { if (e && e.relatedTarget) { if (e.relatedTarget.classList.contains(VBOCore.options.panel_opts.addw_selector.replace('.', ''))) { // add new widget was clicked, abort hiding process or click event won't fire on target element return; } } var keyword = $(this).val(); if (!keyword.length) { // hide add-widgets container $(VBOCore.options.panel_opts.addws_selector).hide(); } }); // register flag for listener active VBOCore.options.active_listeners['registerSearchWidgetsBlur'] = 1; } /** * Removes the blur event handler for the search widgets input. */ static unregisterSearchWidgetsBlur() { if (!VBOCore.options.active_listeners.hasOwnProperty('registerSearchWidgetsBlur')) { // nothing to unregister return true; } $(VBOCore.options.panel_opts.search_selector).off('blur'); // delete flag for listener active delete VBOCore.options.active_listeners['registerSearchWidgetsBlur']; } /** * Adds a widget identifier to the multitask panel. * * @param string widget_id the widget identifier string to add. */ static addWidgetToPanel(widget_id) { if (!VBOCore.options.widget_ajax_uri || !VBOCore.options.panel_opts || !Object.keys(VBOCore.options.panel_opts).length) { throw new Error('Multitask panel options are missing'); } // prepend container to panel var widget_classes = [VBOCore.options.panel_opts.wclass_base_sel.replace('.', ''), VBOCore.options.panel_opts.cur_widget_cls]; var widget_div = '<div class="' + widget_classes.join(' ') + '" ' + VBOCore.options.panel_opts.addw_data_attr + '="' + widget_id + '" style="display: none;"></div>'; var widget_elem = $(widget_div); $(VBOCore.options.panel_opts.actws_selector).prepend(widget_elem); // always hide add-widgets container $(VBOCore.options.panel_opts.addws_selector).hide(); // trigger debounced map saving event VBOCore.emitMultitaskEvent(); // register listener for input search blur VBOCore.registerSearchWidgetsBlur(); // render widget var call_method = 'render'; VBOCore.doAjax( VBOCore.options.widget_ajax_uri, { widget_id: widget_id, call: call_method, vbo_page: VBOCore.options.current_page, vbo_uri: VBOCore.options.current_page_uri, multitask: 1, }, (response) => { // display widgets editing button VBOCore.toggleWidgetsPanelEditing(true); // parse response try { var obj_res = typeof response === 'string' ? JSON.parse(response) : response; if (obj_res.hasOwnProperty(call_method)) { // populate widget HTML content and display it widget_elem.html(obj_res[call_method]).fadeIn(); // always scroll active widgets list to top $(VBOCore.options.panel_opts.actws_selector).scrollTop(0); // register detach widget button setTimeout(() => { let detach_elem = $('<div></div>').addClass(VBOCore.options.panel_opts.dtcwidget_class).html(VBOCore.options.panel_opts.dtcwidget_icn); let detach_to_target = widget_elem.find('.' + VBOCore.options.panel_opts.dtctarget_class); if (!detach_to_target.length) { detach_to_target = widget_elem; } // move detach wrapper onto the target (widget head) detach_elem.prependTo(detach_to_target); // register detach action detach_elem.on('click', function() { // detach widget, meaning do a modal rendering VBOCore.renderModalWidget(widget_id); }); }, 500); } else { console.error('Unexpected JSON response', obj_res); } } catch(err) { console.error('could not parse JSON response', err, response); } }, (error) => { console.error(error.responseText); } ); } /** * Toggles the edit mode of the multitask widgets panel. * * @param bool added true if a widget was just added, false if it was just removed. */ static toggleWidgetsPanelEditing(added) { // check if we are currently editing var is_editing = ($('.' + VBOCore.options.panel_opts.editmode_class).length > 0); // the triggerer button var triggerer = $(VBOCore.options.panel_opts.editw_selector); // check added action status if (added === true) { // show button for edit mode triggerer.show(); // hide shortcut element $(VBOCore.options.panel_opts.shortc_selector).hide(); return; } // grab all widgets var editing_widgets = $(VBOCore.options.panel_opts.wclass_base_sel); if (added === false) { if (!editing_widgets.length) { // hide button for edit mode after removing the last widget triggerer.removeClass(VBOCore.options.panel_opts.editw_selector.substr(1) + '-active').hide(); // show shortcut element $(VBOCore.options.panel_opts.shortc_selector).show(); } return; } if (is_editing) { // deactivate editing mode editing_widgets.removeClass(VBOCore.options.panel_opts.editmode_class); $('.' + VBOCore.options.panel_opts.rmwidget_class).remove(); // toggle triggerer button active class triggerer.removeClass(VBOCore.options.panel_opts.editw_selector.substr(1) + '-active'); } else { // activate editing mode by looping through all widgets editing_widgets.each(function() { // build remove-widget element var rm_widget = $('<div></div>').addClass(VBOCore.options.panel_opts.rmwidget_class).on('click', function() { VBOCore.removeWidgetFromPanel(this); }).html(VBOCore.options.panel_opts.rmwidget_icn); // add editing class and prepend removing element $(this).addClass(VBOCore.options.panel_opts.editmode_class).prepend(rm_widget); }); // toggle triggerer button active class triggerer.addClass(VBOCore.options.panel_opts.editw_selector.substr(1) + '-active'); } } /** * Handles the removal of a widget from the multitask panel. * * @param object element */ static removeWidgetFromPanel(element) { if (!element) { console.error('Invalid widget element to remove', element); return false; } var widget_cont = $(element).parent(VBOCore.options.panel_opts.wclass_base_sel); if (!widget_cont || !widget_cont.length) { console.error('Could not find widget container to remove', element); return false; } var widget_id = widget_cont.attr(VBOCore.options.panel_opts.addw_data_attr); if (!widget_id || !widget_id.length) { console.error('Empty widget id to remove', element); return false; } // find the index of the widget to remove in the panel var widget_index = $(VBOCore.options.panel_opts.wclass_base_sel).index(widget_cont); if (widget_index < 0) { console.error('Empty widget index to remove', widget_cont); return false; } // make sure the index in the array matches the id if (!VBOCore.options.admin_widgets.hasOwnProperty(widget_index) || VBOCore.options.admin_widgets[widget_index]['id'] != widget_id) { console.error('Unmatching widget index or id', VBOCore.options.admin_widgets, widget_index, widget_id); return false; } // remove this widget from the array VBOCore.options.admin_widgets.splice(widget_index, 1); // remove element from document widget_cont.remove(); // check widgets editing button status VBOCore.toggleWidgetsPanelEditing(false); // trigger debounced map saving event VBOCore.emitMultitaskEvent(); if (!VBOCore.options.admin_widgets.length) { // unregister listener for input search blur VBOCore.unregisterSearchWidgetsBlur(); } } /** * Emits an event related to the multitask features or a custom event, with optional data. */ static emitMultitaskEvent(ev_name, ev_data) { var def_ev_name = VBOCore.options.multitask_save_event; if (typeof ev_name === 'string') { def_ev_name = ev_name; } if (typeof ev_data !== 'undefined' && ev_data) { // trigger the custom event document.dispatchEvent(new CustomEvent(def_ev_name, {bubbles: true, detail: ev_data})); return; } // trigger the event document.dispatchEvent(new Event(def_ev_name)); } /** * Proxy for dispatching an event to the document with optional data. */ static emitEvent(ev_name, ev_data) { if (typeof ev_name !== 'string' || !ev_name.length) { return; } return VBOCore.emitMultitaskEvent(ev_name, ev_data); } /** * Attempts to save the multitask widgets for this page. */ static saveMultitasking() { // gather the list of active widgets var active_widgets = []; var cur_admin_widgets = []; $(VBOCore.options.panel_opts.actws_selector).find(VBOCore.options.panel_opts.wclass_base_sel).each(function() { var widget_id = $(this).attr(VBOCore.options.panel_opts.addw_data_attr); if (widget_id && widget_id.length) { // push id in list active_widgets.push(widget_id); // push object with dummy name for global widgets cur_admin_widgets.push({ id: widget_id, name: widget_id, }); } }); // update multitask widgets map for this page VBOCore.doAjax( VBOCore.options.multitask_ajax_uri, { call: 'updateMultitaskingMap', call_args: [ VBOCore.options.current_page, active_widgets, 0 ], }, (response) => { try { var obj_res = typeof response === 'string' ? JSON.parse(response) : response; if (obj_res.hasOwnProperty('result') && obj_res['result']) { // set current widgets VBOCore.setOptions({ admin_widgets: cur_admin_widgets }); } else { console.error('Unexpected or invalid JSON response', response); } } catch(err) { console.error('could not parse JSON response', err, response); } }, (error) => { console.error(error.responseText); } ); } /** * Sets up the browser notifications within the multitask panel, if supported. */ static setupNotifications() { if (!('Notification' in window)) { // browser does not support notifications $(VBOCore.options.panel_opts.notif_selector).hide(); return false; } if (Notification.permission && Notification.permission === 'granted') { // permissions were granted already $(VBOCore.options.panel_opts.notif_selector) .addClass(VBOCore.options.panel_opts.notif_on_class) .attr('title', VBOCore.options.tn_texts.notifs_enabled) .on('click', function() { // show congratulations notification VBOCore.dispatchCongratulations(); // attempt to update the service worker installation VBOCore.installServiceWorker(true).then(() => { // all good }).catch((error) => { console.error(error); }); }); return true; } // notifications supported, but perms not granted $(VBOCore.options.panel_opts.notif_selector) .addClass(VBOCore.options.panel_opts.notif_off_class) .attr('title', VBOCore.options.tn_texts.notifs_disabled); // register click-event listener on button to enable notifications $(VBOCore.options.panel_opts.notif_selector).on('click', function() { VBOCore.requestNotifPerms(); }); // subscribe to the multitask-panel-open event to show the status of the notifications document.addEventListener(VBOCore.multitask_open_event, function() { if (VBOCore.notificationsEnabled() === false) { // add "shaking" class to notifications button $(VBOCore.options.panel_opts.notif_selector).addClass('shaking'); } }); // subscribe to the multitask-panel-close event to update the status of the notifications document.addEventListener(VBOCore.multitask_close_event, function() { // always remove "shaking" class from notifications button $(VBOCore.options.panel_opts.notif_selector).removeClass('shaking'); }); } /** * Sets up the browser notifications within the given selector, if supported. * * @param string selector the element query selector. */ static suggestNotifications(selector) { if (!selector) { return false; } if (!('Notification' in window)) { // browser does not support notifications $(selector).hide(); return false; } if (Notification.permission && Notification.permission === 'granted') { // permissions were granted already $(selector) .addClass(VBOCore.options.panel_opts.notif_on_class) .attr('title', VBOCore.options.tn_texts.notifs_enabled) .on('click', function() { VBOCore.dispatchCongratulations(); }); return true; } // notifications supported, but perms not granted $(selector) .addClass(VBOCore.options.panel_opts.notif_off_class) .attr('title', VBOCore.options.tn_texts.notifs_disabled); // register click-event listener on button to enable notifications $(selector).on('click', function() { VBOCore.requestNotifPerms(selector); }); // add "shaking" class to make the selector more appealing $(selector).addClass('shaking'); // remove the "shaking" class after some time setTimeout(() => { $(selector).removeClass('shaking'); }, 2000); } /** * Tells whether the notifications are enabled, disabled, not supported. */ static notificationsEnabled() { if (!('Notification' in window)) { // not supported return null; } if (Notification.permission && Notification.permission === 'granted') { // enabled return true; } // disabled return false; } /** * Attempts to request the notifications permissions to the browser. * For security reasons, this should run upon a user gesture (click). * * @param string selector optional element query selector. */ static requestNotifPerms(selector) { if (!('Notification' in window)) { // browser does not support notifications return false; } // run permissions request in a try-catch statement to support all browsers try { // handle promise-based version to request permissions Notification.requestPermission().then((permission) => { VBOCore.handleNotifPerms(permission, selector); }); } catch(e) { // run the callback-based version Notification.requestPermission(function(permission) { VBOCore.handleNotifPerms(permission, selector); }); } } /** * Handles the notifications permission response (from callback or promise resolved). * * @param any permission permission status string or NotificationPermission object. * @param string selector optional element query selector. */ static handleNotifPerms(permission, selector) { // check the permission status from the Notification object interface if ((Notification.permission && Notification.permission === 'granted') || (typeof permission === 'string' && permission === 'granted')) { // permissions granted! $((selector || VBOCore.options.panel_opts.notif_selector)) .removeClass(VBOCore.options.panel_opts.notif_off_class) .addClass(VBOCore.options.panel_opts.notif_on_class) .attr('title', VBOCore.options.tn_texts.notifs_enabled); // dispatch an immediate notification to congratulate with the activation VBOCore.dispatchCongratulations(); return true; } else { // permissions denied :( $((selector || VBOCore.options.panel_opts.notif_selector)) .removeClass(VBOCore.options.panel_opts.notif_on_class) .addClass(VBOCore.options.panel_opts.notif_off_class) .attr('title', VBOCore.options.tn_texts.notifs_disabled); // show alert message console.error('Permission denied for enabling browser notifications', permission); alert(VBOCore.options.tn_texts.notifs_disabled_help); } return false; } /** * Dispatches an immediate notification with a congratulations text. * * @return void */ static dispatchCongratulations() { try { let notif = new Notification(VBOCore.options.tn_texts.congrats, { body: VBOCore.options.tn_texts.notifs_enabled, tag: 'vbo_notification_congrats', silent: false }); } catch(error) { alert(error); } } /** * Given a date-time string, returns a Date object representation. * * @param string dtime_str the date-time string in "Y-m-d H:i:s" format. */ static getDateTimeObject(dtime_str) { // instantiate a new date object var date_obj = new Date(); // parse date-time string let dtime_parts = dtime_str.split(' '); let date_parts = dtime_parts[0].split('-'); if (dtime_parts.length != 2 || date_parts.length != 3) { // invalid military format return date_obj; } let time_parts = dtime_parts[1].split(':'); // set accurate date-time values date_obj.setFullYear(date_parts[0]); date_obj.setMonth((parseInt(date_parts[1]) - 1)); date_obj.setDate(parseInt(date_parts[2])); date_obj.setHours(parseInt(time_parts[0])); date_obj.setMinutes(parseInt(time_parts[1])); date_obj.setSeconds(0); date_obj.setMilliseconds(0); // return the accurate date object return date_obj; } /** * Given a list of schedules, enqueues notifications to watch. * * @param array|object schedules list of or one notification object(s). * * @return bool */ static enqueueNotifications(schedules) { if (!Array.isArray(schedules) || !schedules.length) { if (typeof schedules === 'object' && schedules.hasOwnProperty('dtime')) { // convert the single schedule to an array schedules = [schedules]; } else { // invalid argument passed return false; } } for (var i in schedules) { if (!schedules.hasOwnProperty(i) || typeof schedules[i] !== 'object') { continue; } VBOCore.notifications.push(schedules[i]); } // setup the timeouts to schedule the notifications return VBOCore.scheduleNotifications(); } /** * Schedule the trigger timings for each notification. */ static scheduleNotifications() { if (!VBOCore.notifications.length) { // no notifications to be scheduled return false; } if (VBOCore.notificationsEnabled() !== true) { // notifications not enabled console.info('Browser notifications disabled or unsupported.'); return false; } // gather current date-timing information const now_date = new Date(); const now_time = now_date.getTime(); // parse all notifications to schedule the timers if not set for (let i = 0; i < VBOCore.notifications.length; i++) { let notif = VBOCore.notifications[i]; if (typeof notif !== 'object' || !notif.hasOwnProperty('dtime')) { // invalid notification object, unset it VBOCore.notifications.splice(i, 1); continue; } // check if timer has been set if (!notif.hasOwnProperty('id_timer')) { // estimate trigger timing let in_ms = 0; // check for imminent notifications if (typeof notif.dtime === 'string' && notif.dtime.indexOf('now') >= 0) { // imminent ones will be delayed by one second in_ms = 1000; } else { // check overdue date-time (notif.dtime can also be a Date object instance) let nexp = VBOCore.getDateTimeObject(notif.dtime); in_ms = nexp.getTime() - now_time; } if (in_ms > 0) { // schedule notification trigger let id_timer = setTimeout(() => { VBOCore.dispatchNotification(notif); }, in_ms); // set timer on notification object VBOCore.notifications[i]['id_timer'] = id_timer; } } } return true; } /** * Deregister all scheduled notifications. */ static unscheduleNotifications() { if (!VBOCore.notifications.length) { // no notifications scheduled return false; } for (let i = 0; i < VBOCore.notifications.length; i++) { let notif = VBOCore.notifications[i]; if (typeof notif === 'object' && notif.hasOwnProperty('id_timer')) { // unset timeout for this notification clearTimeout(notif['id_timer']); } } // reset pool VBOCore.notifications = []; } /** * Update or delete a previously scheduled notification. * * @param object match_props map of properties to match. * @param string|number newdtime the new date time to schedule (0 for deleting). * * @return null|bool true only if a notification matched. */ static updateNotification(match_props, newdtime) { if (!VBOCore.notifications.length) { // no notifications set, terminate return null; } if (typeof match_props !== 'object') { // no properties to match the notification return null; } // gather current date-timing information const now_date = new Date(); const now_time = now_date.getTime(); // parse all notifications scheduled for (let i = 0; i < VBOCore.notifications.length; i++) { let notif = VBOCore.notifications[i]; let all_matched = true; let to_matching = false; for (let prop in match_props) { if (!match_props.hasOwnProperty(prop)) { continue; } to_matching = true; if (!notif.hasOwnProperty(prop) || notif[prop] != match_props[prop]) { all_matched = false; break; } } if (all_matched && to_matching) { // notification object found if (notif.hasOwnProperty('id_timer')) { // unset previous timeout for this notification clearTimeout(notif['id_timer']); } // update or delete scheduled notification if (newdtime === 0) { // delete notification from queue VBOCore.notifications.splice(i, 1); } else { // update timing scheduler let in_ms = 0; // check for imminent notifications if (typeof newdtime === 'string' && newdtime.indexOf('now') >= 0) { // imminent ones will be delayed by one second in_ms = 1000; } else { // check overdue date-time (newdtime can also be a Date object instance) let nexp = VBOCore.getDateTimeObject(newdtime); in_ms = nexp.getTime() - now_time; } if (in_ms > 0) { // schedule notification trigger let id_timer = setTimeout(() => { VBOCore.dispatchNotification(notif); }, in_ms); // set timer on notification object VBOCore.notifications[i]['id_timer'] = id_timer; } // update date-time value on notification object VBOCore.notifications[i]['dtime'] = newdtime; } // terminate parsing and return true return true; } } // notification object not found return false; } /** * Dispatch the notification object. * Expected notification properties: * * { * id: number * type: string * dtime: string|Date * build_url: string|null * } * * @param object notif the notification object. */ static dispatchNotification(notif) { if (typeof notif !== 'object') { return false; } // subscribe to building notification data VBOCore.buildNotificationData(notif).then((data) => { // dispatch the notification // check if the click event should be registered let func_nodes; if (data.onclick && typeof data.onclick === 'string') { let callback_parts = data.onclick.split('.'); while (callback_parts.length) { // compose window static method string to avoid using eval() let tmp = callback_parts.shift(); if (!func_nodes) { func_nodes = window[tmp]; } else { func_nodes = func_nodes[tmp]; } } } // prepare properties to delete the notification from queue let match_props = {}; for (let prop in notif) { if (!notif.hasOwnProperty(prop) || prop == 'id_timer') { continue; } match_props[prop] = notif[prop]; } // check browser Notifications API if (VBOCore.notificationsEnabled() !== true) { // notifications not enabled, fallback to toast message let toast_notif_data = { title: data.title, body: data.message, icon: data.icon, delay: { min: 6000, max: 20000, tolerance: 4000, }, action: () => { VBOToast.dispose(true); }, sound: VBOCore.options.notif_audio_url }; if (func_nodes) { toast_notif_data.action = function() { func_nodes(data); VBOToast.dispose(true); }; } else if (typeof data.onclick === 'function') { toast_notif_data.action = function() { data.onclick.call(data); VBOToast.dispose(true); }; } VBOToast.enqueue(new VBOToastMessage(toast_notif_data)); // delete dispatched notification from queue VBOCore.updateNotification(match_props, 0); return; } // use the browser's native Notifications API let browser_notif = new Notification(data.title, { body: data.message, icon: data.icon, tag: 'vbo_notification', silent: false }); // check if support for Web App badge is available if (typeof navigator.setAppBadge !== 'undefined') { navigator.setAppBadge(1); } browser_notif.addEventListener('click', (e) => { // check if support for Web App badge is available if (typeof navigator.setAppBadge !== 'undefined') { navigator.clearAppBadge(); } // close the notification e.target.close(); }); if (func_nodes) { // register notification click event browser_notif.addEventListener('click', () => { func_nodes(data); }); } else if (typeof data.onclick === 'function') { browser_notif.addEventListener('click', () => { data.onclick.call(data); }); } // delete dispatched notification from queue VBOCore.updateNotification(match_props, 0); }).catch((error) => { console.error(error); }); } /** * Asynchronous build of the notification data object for dispatch. * Minimum expected notification display data properties: * * { * title: string * message: string * icon: string * onclick: function * } * * @param object notif the scheduled notification object. * * @return Promise */ static buildNotificationData(notif) { return new Promise((resolve, reject) => { // notification object validation if (typeof notif !== 'object') { reject('Invalid notification object'); return; } if (!notif.hasOwnProperty('build_url') || !notif.build_url) { // building callback not necessary if (!notif.title && !notif.message) { reject('Unexpected notification object'); return; } // we expect the notification to be built already resolve(notif); return; } // build the notification data VBOCore.doAjax( notif.build_url, { payload: JSON.stringify(notif) }, (response) => { // parse response try { var obj_res = typeof response === 'string' ? JSON.parse(response) : response; if (obj_res.hasOwnProperty('title')) { resolve(obj_res); } else { reject('Unexpected JSON response'); } } catch(err) { reject('could not parse JSON response'); } }, (error) => { reject(error.responseText); } ); }); } /** * Handle a navigation towards a given URL. * Common handler for browser notifications click. * * @param object data notification display data payload. */ static handleGoto(data) { if (typeof data !== 'object' || !data.hasOwnProperty('gotourl') || !data.gotourl) { console.error('Could not handle the goto operation', data); return; } if (typeof data.openWindow !== 'undefined' || typeof document === 'undefined') { // open a new window window.open(data.gotourl); return; } // redirect document.location.href = data.gotourl; } /** * Opens a new window with the URI to render the Push notification data. * * @param object options admin widget rendering options to bind. * * @return void */ static openAdminPushURI(options) { if (typeof options !== 'object' || !options) { throw new Error('Unable to build Admin Push URI'); } let vbo_admin_push_uri = VBOCore.options.root_uri; if (VBOCore.options.cms === 'wordpress') { vbo_admin_push_uri += 'wp-admin/admin.php?page=vikbooking'; } else { vbo_admin_push_uri += 'administrator/index.php?option=com_vikbooking'; } // build Push notification payload for rendering let push_data = { type: options.type || '', title: options.title || '', message: options.message || '', content: options, }; // open page in a new window window.open(vbo_admin_push_uri + '&push_notification=' + btoa(JSON.stringify(push_data)), '_blank'); } /** * Handle the display of a notification through a widget. * Common handler for browser notifications displayed through * a widget modal rendering. * * @param object data notification display data payload. * @param object options optional extra options for the admin widget. * @param boolean is_push whether we are coming from a Push notification. * * @return void */ static handleDisplayWidgetNotification(data, options, is_push) { try { // validate handler if (!VBOCore.options.is_vbo && !VBOCore.options.is_vcm) { throw new Error('Unable to handle the notification display from the current page'); } // validate payload if (typeof data !== 'object' || !data.hasOwnProperty('widget_id') || !data['widget_id']) { throw new Error('Invalid widget payload'); } // parse handler if (VBOCore.options.is_vcm || (is_push && !VBOCore.options.is_vbo)) { // the operation is handled by VCM (or by an external admin resource, if it's a Push notification) VBOCore.loadAdminWidgetAssets(data).then((assets) => { // append assets to DOM assets.forEach((asset) => { if (!$('link#' + asset['id']).length) { $('head').append('<link rel="stylesheet" id="' + asset['id'] + '" href="' + asset['href'] + '" media="all" />'); } }); // widget modal rendering handled by VCM (or an external admin resource) let hide_panel = VBOCore.options.is_vcm && $('.' + VBOCore.options.panel_opts.open_class).length ? true : false; let modal_data = VBOCore.renderModalWidget(data['widget_id'], data, options, hide_panel); if (modal_data.hasOwnProperty('dismissed_event') && (modal_data['suffix'] || '').indexOf('inner') < 0) { // register event to unload all assets (only if not from an inner modal) document.addEventListener(modal_data['dismissed_event'], () => { assets.forEach((asset) => { if ($('link#' + asset['id']).length) { $('link#' + asset['id']).remove(); } }); }); } }).catch((error) => { throw new Error(error); }); } else { // widget modal rendering handled by Vik Booking VBOCore.renderModalWidget(data['widget_id'], data, options, false); } } catch(e) { if (is_push && options.type) { // fallback to opening a new window to render the clicked Push notification VBOCore.openAdminPushURI(options); } else { // fallback to a regular link opening, if set VBOCore.handleGoto(data); } } } /** * Asynchronous loading of CSS assets required to render * an admin widget outside Vik Booking. * * @param object data the optional widget payload data. * * @return Promise */ static loadAdminWidgetAssets(data) { return new Promise((resolve, reject) => { // the remote assets URI must be set if (!VBOCore.options.assets_ajax_uri) { reject('Missing remote assets URI'); return; } if (typeof data !== 'object') { data = {}; } // make the request VBOCore.doAjax( VBOCore.options.assets_ajax_uri, data, (response) => { // parse response try { var obj_res = typeof response === 'string' ? JSON.parse(response) : response; if (!Array.isArray(obj_res)) { reject('Unexpected JSON response'); } resolve(obj_res); } catch(err) { reject('could not parse JSON response'); } }, (error) => { reject(error.responseText); } ); }); } /** * Register the latest data to watch for the preloaded admin widgets. * * @param object watch_data */ static registerWatchData(watch_data) { if (typeof watch_data !== 'object' || watch_data == null) { VBOCore.widgets_watch_data = null; return false; } // set watch-data map VBOCore.widgets_watch_data = watch_data; // schedule watching interval if (VBOCore.watch_data_interval == null) { VBOCore.watch_data_interval = window.setInterval(VBOCore.watchWidgetsData, 60000); } // set up broadcast channels to connect all browsing contexts if (typeof BroadcastChannel !== 'undefined') { // set up watch-data broadcast channel if (!VBOCore.broadcast_watch_data) { // start broadcast channel VBOCore.broadcast_watch_data = new BroadcastChannel('vikbooking_watch_data'); // register to the "on broadcast message received" event VBOCore.broadcast_watch_data.onmessage = (event) => { if (event && event.data) { // update watch data map for next schedule to avoid dispatching duplicate notifications VBOCore.widgets_watch_data = event.data; } }; } // set up push-data broadcast channel if (!VBOCore.broadcast_push_data) { // start broadcast channel VBOCore.broadcast_push_data = new BroadcastChannel('vikbooking_push_data'); // register to the "on broadcast message received" event VBOCore.broadcast_push_data.onmessage = (event) => { if (event && event.data) { // update pushed data map for the next watching event VBOCore.widgets_pushed_data.push(event.data); } }; } // set up watch-events broadcast channel if (!VBOCore.broadcast_watch_events) { // start broadcast channel VBOCore.broadcast_watch_events = new BroadcastChannel('vikbooking_watch_events'); // register to the "on broadcast message received" event VBOCore.broadcast_watch_events.onmessage = (event) => { if (event && event.data && typeof event.data === 'object') { // scan and dispatch the events data received let events_data = event.data; for (let ev_name in events_data) { if (!events_data.hasOwnProperty(ev_name)) { continue; } // emit the event VBOCore.emitEvent(ev_name, events_data[ev_name]); } } }; } } } /** * Periodic widgets data watching for new events. */ static watchWidgetsData() { if (typeof VBOCore.widgets_watch_data !== 'object' || VBOCore.widgets_watch_data == null) { return; } // call on new events VBOCore.doAjax( VBOCore.options.watchdata_ajax_uri, { watch_data: JSON.stringify(VBOCore.widgets_watch_data), pushed_data: JSON.stringify(VBOCore.widgets_pushed_data), }, (response) => { try { var obj_res = typeof response === 'string' ? JSON.parse(response) : response; if (obj_res.hasOwnProperty('watch_data')) { // update watch data map for next schedule VBOCore.widgets_watch_data = obj_res['watch_data']; // check for notifications if (obj_res.hasOwnProperty('notifications') && Array.isArray(obj_res['notifications'])) { // dispatch notifications for (var i = 0; i < obj_res['notifications'].length; i++) { VBOCore.dispatchNotification(obj_res['notifications'][i]); } // post message onto broadcast channel for any other browsing context if (VBOCore.broadcast_watch_data && obj_res['notifications'].length) { // this will avoid dispatching duplicate notifications VBOCore.broadcast_watch_data.postMessage(VBOCore.widgets_watch_data); } } // check for notification events to dispatch if (obj_res.hasOwnProperty('events') && Array.isArray(obj_res['events'])) { // parse and dispatch all the events obj_res['events'].forEach((events_data) => { if (typeof events_data !== 'object') { return; } // parse all the events for this widget watched data for (let ev_name in events_data) { if (!events_data.hasOwnProperty(ev_name)) { continue; } // emit the event VBOCore.emitEvent(ev_name, events_data[ev_name]); } // post message onto broadcast channel for any other browsing context if (VBOCore.broadcast_watch_events) { // this will trigger the events on any other browsing context VBOCore.broadcast_watch_events.postMessage(events_data); } }); } } else { console.error('Unexpected or invalid JSON response', response); } } catch(err) { console.error('could not parse JSON response', err, response); } }, (error) => { console.error(error.responseText); } ); } /** * Widget modal rendering. * * @param string widget_id the widget identifier to render. * @param any data the optional multitask data to inject. * @param object options optional list of extra options to render the widget with Multitask data. * @param bool hide_panel if false, the multitask panel elements will remain unchanged. * * @return object the multitask data injected object merged with modal options. */ static renderModalWidget(widget_id, data, options, hide_panel) { /** * Adjust arguments for BC with previous ordering between options * and hide_panel, which used to be inverted. Useful also to shorten * the way the method can be invoked in case no options are needed. */ if (typeof options === 'boolean') { // switch argument values hide_panel = options; options = null; } if (typeof data !== 'object' || data == null) { // always treat data as an object data = {}; } if (typeof options !== 'object' || options == null) { // always treat options as an object options = data._options || {}; } // build the default widget payload let modal_js_id = Math.floor(Math.random() * 100000); let modal_title = options._push && options.title ? options.title : VBOCore.options.tn_texts.admin_widget; let widget_data = { _modalRendering: 1, _modalJsId: modal_js_id, _modalTitle: modal_title, _options: options, }; // merge default payload options with given options data = Object.assign(widget_data, data); // define unique modal event names to avoid conflicts let dismiss_event = 'vbo-dismiss-widget-modal' + modal_js_id; let loading_event = 'vbo-loading-widget-modal' + modal_js_id; let resize_event = 'vbo-resize-widget-modal' + modal_js_id; // define the modal options let modal_options = { suffix: 'widget_modal', extra_class: 'vbo-modal-widget vbo-modal-rounded vbo-modal-tall vbo-modal-nofooter', title: data._modalTitle, body_prepend: true, lock_scroll: true, draggable: true, enlargeable: true, minimizeable: VBOCore.options.is_vbo, resize_event: resize_event, dismiss_event: dismiss_event, loading_event: loading_event, loading_body: VBOCore.options.default_loading_body, dismissed_event: VBOCore.options.widget_modal_dismissed + modal_js_id, event_data: widget_data, }; // check if options should rewrite some modal-options if (options.modal_options) { modal_options = Object.assign(modal_options, options.modal_options); } // display modal let widget_modal = VBOCore.displayModal(modal_options); if (hide_panel !== false) { // blur search widget input, hide multitask panel $(VBOCore.options.panel_opts.search_selector).trigger('blur'); VBOCore.emitEvent(VBOCore.multitask_shortcut_event); } // start loading VBOCore.emitEvent(loading_event); // render admin widget VBOCore.renderAdminWidget(widget_id, data).then((content) => { // stop loading and append widget content to modal VBOCore.emitEvent(loading_event); widget_modal.append(content); // register an admin menu action for the rendered widget VBOCore.fetchWidgetDetails(widget_id).then((details) => { // update modal title, if default title is present if (modal_options.title == VBOCore.options.tn_texts.admin_widget) { widget_modal .closest('.vbo-modal-overlay-content') .find('.vbo-modal-overlay-content-head-title') .text(data._modalTitle + ' - ' + details.name); } // check if this widget returned a particular modal setup if (details?.modal) { if (details.modal?.add_class) { // add custom modal class widget_modal .closest('.vbo-modal-overlay-content') .addClass(details.modal.add_class); // fire the "resize" event with a delay to support the CSS transition setTimeout(() => { VBOCore.emitEvent(resize_event, { content: widget_modal, }); }, 400); } } // set data for widget details widget_modal.data('details', JSON.stringify(details)); // register admin menu action try { // work on Local Storage to register the widget data VBOCore.registerAdminMenuAction({ name: details.name, href: 'JavaScript: void(0);', widget: widget_id, icon: details.icon, style: details.style, }, 'widgets'); } catch(e) { console.error(e); } }).catch((error) => { console.error(error); }); }).catch((error) => { // dismiss modal and display error VBOCore.emitEvent(dismiss_event); alert(error); }); return Object.assign(data, modal_options); } /** * Renders an admin widget. * * @param string widget_id the widget identifier string to add. * @param any data the optional multitask data to inject. * * @return Promise */ static renderAdminWidget(widget_id, data) { return new Promise((resolve, reject) => { if (!VBOCore.options.widget_ajax_uri) { reject('Could not load admin widget'); return; } var call_method = 'render'; VBOCore.doAjax( VBOCore.options.widget_ajax_uri, { widget_id: widget_id, call: call_method, vbo_page: VBOCore.options.current_page, vbo_uri: VBOCore.options.current_page_uri, multitask: 1, multitask_data: data, }, (response) => { // parse response try { var obj_res = typeof response === 'string' ? JSON.parse(response) : response; if (!obj_res.hasOwnProperty(call_method)) { reject('Unexpected JSON response'); return; } resolve(obj_res[call_method]); } catch(err) { reject('could not parse JSON response'); } }, (error) => { reject(error.responseText); } ); }); } /** * Fetches the details for a specific widget. * * @param string widget_id the widget identifier string to fetch. * * @return Promise * * @since 1.6.7 */ static fetchWidgetDetails(widget_id) { return new Promise((resolve, reject) => { if (!VBOCore.options.widget_ajax_uri) { reject('Could not load admin widget'); return; } var call_method = 'getWidgetDetails'; VBOCore.doAjax( VBOCore.options.widget_ajax_uri, { widget_id: widget_id, call: call_method, return: 1, }, (response) => { // parse response try { var obj_res = typeof response === 'string' ? JSON.parse(response) : response; if (!obj_res.hasOwnProperty(call_method)) { reject('Unexpected JSON response'); return; } resolve(obj_res[call_method]); } catch(err) { reject('could not parse JSON response'); } }, (error) => { reject(error.responseText); } ); }); } /** * Helper method used to copy the text of an * input element within the clipboard. * * Clipboard copy will take effect only in case the * function is handled by a DOM event explicitly * triggered by the user, such as a "click". * * @param any input The input containing the text to copy. * * @return Promise */ static copyToClipboard(input) { // register and return promise return new Promise((resolve, reject) => { // define a fallback function var fallback = function(input) { // focus the input input.focus(); // select the text inside the input input.select(); try { // try to copy with shell command var copy = document.execCommand('copy'); if (copy) { // copied successfully resolve(copy); } else { // unable to copy reject(copy); } } catch (error) { // unable to exec the command reject(error); } }; // look for navigator clipboard if (!navigator || !navigator.clipboard) { // navigator clipboard not supported, use fallback fallback(input); return; } // try to copy within the clipboard by using the navigator navigator.clipboard.writeText((input.val ? input.val() : input.value)).then(() => { // copied successfully resolve(true); }).catch((error) => { // revert to the fallback fallback(input); }); }); } /** * Helper method used to display a modal window dinamycally. * * @param object options The options to render the modal. * @param object bindDetails Optional data to bind to the modal content. * * @return object The modal content element wrapper. */ static displayModal(options, bindDetails) { var def_options = { suffix: (Math.floor(Math.random() * 100000)) + '', extra_class: null, header: true, title: '', body: '', body_prepend: false, lock_scroll: false, draggable: false, enlargeable: false, minimizeable: false, escape_dismiss: true, footer_left: null, footer_right: null, resize_event: null, dismiss_event: null, dismissed_event: 'vbo-modal-dismissed', onDismiss: null, onMinimize: null, onRestore: null, loading_event: null, loading_body: VBOCore.options.default_loading_body, progress_event: null, event_data: null, }; if (options.event_data && typeof options.event_data === 'object') { // get rid of possible cyclic object references options.event_data = Object.assign({}, options.event_data); } // merge default options with given options options = Object.assign(def_options, options); // create the modal destroy function const modal_destroy_fn = (e) => { // invoke callback for onDismiss if (typeof options.onDismiss === 'function') { options.onDismiss.call(custom_modal, e); } // check if modal did register to the loading event if (options.loading_event) { // we can now un-register from the loading event until a new modal is displayed and will register to it again document.removeEventListener(options.loading_event, modal_handle_loading_event_fn); if (options.progress_event) { // we can now un-register from the progress event document.removeEventListener(options.progress_event, modal_handle_progress_event_fn); } } // check if we should fire the given modal dismissed event if (options.dismissed_event) { VBOCore.emitEvent(options.dismissed_event, options.event_data); } // check if body scroll lock should be removed if (options.lock_scroll) { $('body').removeClass('vbo-modal-lock-scroll'); } // remove modal from DOM custom_modal.remove(); }; // create the modal dismiss function const modal_dismiss_fn = (e) => { custom_modal.fadeOut(400, () => { // destroy the modal modal_destroy_fn.call(custom_modal, e); }); }; // create the modal loading event handler function const modal_handle_loading_event_fn = (e) => { // toggle modal loading if ($('.vbo-modal-overlay-content-backdrop').length) { // hide loading $('.vbo-modal-overlay-content-backdrop').remove(); // do not proceed return; } // show loading var modal_loading = $('<div></div>').addClass('vbo-modal-overlay-content-backdrop'); var modal_loading_body = $('<div></div>').addClass('vbo-modal-overlay-content-backdrop-body'); if (options.loading_body) { modal_loading_body.append(options.loading_body); } modal_loading.append(modal_loading_body); // append backdrop loading to modal content modal_content.prepend(modal_loading); }; // create the modal progress event handler function const modal_handle_progress_event_fn = (e) => { if (!e || !e.detail || !e.detail.progress_content) { // do not proceed return; } // identify the current loading backdrop body let backdrop_body = modal_content.find('.vbo-modal-overlay-content-backdrop-body'); if (!backdrop_body.length) { // loading body not found return; } let backdrop_txt_el = $('<div></div>').addClass('vbo-modal-overlay-content-backdrop-text'); if (typeof e.detail.progress_content === 'string') { backdrop_txt_el.html(e.detail.progress_content); } else { backdrop_txt_el.append(e.detail.progress_content); } let backdrop_txt_old = backdrop_body.find('.vbo-modal-overlay-content-backdrop-text'); if (backdrop_txt_old.length) { // remove the previous text element backdrop_txt_old.remove(); } // set the loading progress content backdrop_body.append(backdrop_txt_el); }; // create the modal enlarge (toggle) function const modal_enlarge_fn = (e) => { // toggle modal fullscreen class modal_content.toggleClass('vbo-modal-fullscreen'); // check if modal did register a resize event if (options.resize_event) { // fire the requested event with a delay to support the CSS transition setTimeout(() => { VBOCore.emitEvent(options.resize_event, { content: modal_content, }); }, 400); } }; // create the modal minimize function const modal_minimize_fn = (e) => { // invoke callback for onMinimize if (typeof options.onMinimize === 'function') { options.onMinimize.call(custom_modal, e); } // start minimizing animation custom_modal.addClass('vbo-minimizing'); modal_content.addClass('vbo-minimizing'); // access the modal body details data, if any let details_data = modal_content_wrapper.data('details'); try { if (details_data && typeof details_data === 'string') { details_data = JSON.parse(details_data); } } catch(e) { details_data = {}; } // register delayed partial-distruction setTimeout(() => { // hide modal custom_modal.hide(); // check if body scroll lock should be removed if (options.lock_scroll) { $('body').removeClass('vbo-modal-lock-scroll'); } // add the minimized widget to dock VBOCore.getAdminDock().addWidget(details_data, options, modal_content_wrapper, modal_destroy_fn); // remove modal from DOM without firing the dismiss events (do not destroy the modal) custom_modal.remove(); }, 400); }; // start the modal position variables var modal_pos_x = 0, modal_pos_y = 0; // create the modal drag-start (mousedown) event handler function const modal_dragstart_fn = (e) => { e = e || window.event; e.preventDefault(); if (typeof e.clientX === 'undefined' || typeof e.clientY === 'undefined') { // unsupported return; } if (e.target && !e.target.matches('.vbo-modal-overlay-cmd')) { e.target.style.cursor = 'move'; } // store the initial modal (cursor) position modal_pos_x = e.clientX; modal_pos_y = e.clientY; // register mouseup and mousemove events document.onmouseup = modal_dragstop_fn; document.onmousemove = modal_dragmove_fn; }; // create the modal drag-stop (mouseup) event handler function const modal_dragstop_fn = (e) => { e = e || window.event; if (e.target && !e.target.matches('.vbo-modal-overlay-cmd')) { e.target.style.cursor = 'auto'; } // unregister mousemove event if (document.onmousemove == modal_dragmove_fn) { document.onmousemove = null; } // unregister mouseup event if (document.onmouseup == modal_dragstop_fn) { document.onmouseup = null; } }; // create the modal drag-move (mousemove) event handler function const modal_dragmove_fn = (e) => { e = e || window.event; e.preventDefault(); if (typeof e.clientX === 'undefined' || typeof e.clientY === 'undefined') { // unsupported return; } // calculate the new modal (cursor) position let new_modal_pos_x = modal_pos_x - e.clientX; let new_modal_pos_y = modal_pos_y - e.clientY; // update current modal (cursor) position modal_pos_x = e.clientX; modal_pos_y = e.clientY; // find the modal element let modal_element = e.target.closest('.vbo-modal-overlay-content'); if (!modal_element) { return; } // set the modal position modal_element.style.top = (modal_element.offsetTop - new_modal_pos_y) + 'px'; modal_element.style.left = (modal_element.offsetLeft - new_modal_pos_x) + 'px'; }; // build modal content const custom_modal = $('<div></div>').addClass('vbo-modal-overlay-block vbo-modal-overlay-' + options.suffix).css('display', 'block'); var modal_dismiss = $('<a></a>').addClass('vbo-modal-overlay-close'); modal_dismiss.on('click', modal_dismiss_fn); custom_modal.append(modal_dismiss); const modal_content = $('<div></div>').addClass('vbo-modal-overlay-content vbo-modal-overlay-content-' + options.suffix); if (options.extra_class && typeof options.extra_class === 'string') { modal_content.addClass(options.extra_class); } // modal head and title const modal_head = $('<div></div>').addClass('vbo-modal-overlay-content-head'); if (options.title) { let modal_title = $('<span></span>').addClass('vbo-modal-overlay-content-head-title').html(options.title); modal_head.append(modal_title); } else { modal_head.addClass('vbo-modal-head-no-title'); } // modal head commands var modal_head_cmds = $('<span></span>').addClass('vbo-modal-overlay-cmds'); var modal_head_close = $('<span></span>').addClass('vbo-modal-overlay-cmd vbo-modal-overlay-close-times').html('×'); modal_head_close.on('click', modal_dismiss_fn); modal_head_cmds.append(modal_head_close); if (options.enlargeable) { var modal_head_enlarge = $('<span></span>').addClass('vbo-modal-overlay-cmd vbo-modal-overlay-cmd-enlarge').html('□'); modal_head_enlarge.on('click', modal_enlarge_fn); modal_head_cmds.append(modal_head_enlarge); modal_head.on('dblclick', modal_enlarge_fn); } if (options.minimizeable && options.event_data) { // set up the minimize command var modal_head_minimize = $('<span></span>').addClass('vbo-modal-overlay-cmd vbo-modal-overlay-cmd-minimize').html('−'); modal_head_minimize.on('click', modal_minimize_fn); modal_head_cmds.append(modal_head_minimize); } // set commands modal_head.append(modal_head_cmds); // check if the modal head should be draggable if (options.draggable) { // register the event(s) to allow dragging modal_head.addClass('vbo-modal-head-draggable'); modal_head.on('contextmenu', (e) => { e.preventDefault(); }); modal_head.on('mousedown', modal_dragstart_fn); } const modal_body = $('<div></div>').addClass('vbo-modal-overlay-content-body vbo-modal-overlay-content-body-scroll'); const modal_content_wrapper = $('<div></div>').addClass('vbo-modal-' + options.suffix + '-wrap'); if (options.suffix != 'widget_modal') { modal_content_wrapper.addClass('vbo-modal-widget_modal-wrap'); } if (typeof options.body === 'string') { modal_content_wrapper.html(options.body); } else { modal_content_wrapper.append(options.body); } modal_body.append(modal_content_wrapper); // modal footer let modal_footer = null; if (options.footer_left || options.footer_right) { modal_footer = $('<div></div>').addClass('vbo-modal-overlay-content-footer'); if (options.footer_left) { let modal_footer_left = $('<div></div>').addClass('vbo-modal-overlay-content-footer-left').append(options.footer_left); modal_footer.append(modal_footer_left); } if (options.footer_right) { let modal_footer_right = $('<div></div>').addClass('vbo-modal-overlay-content-footer-right').append(options.footer_right); modal_footer.append(modal_footer_right); } } // finalize modal contents if (options.header) { // append header modal_content.append(modal_head); } // append body modal_content.append(modal_body); if (modal_footer) { // append footer modal_content.append(modal_footer); } custom_modal.append(modal_content); // register to the dismiss event if (options.dismiss_event) { // listen to the event that will dismiss the modal document.addEventListener(options.dismiss_event, function vbo_core_handle_dismiss_event(e) { // make sure the same event won't propagate again, unless a new modal is displayed (multiple displayModal calls) e.target.removeEventListener(e.type, vbo_core_handle_dismiss_event); // invoke the modal dismiss function modal_dismiss_fn(e); }); // declare the function to detect the Escape key pressed const vbo_core_dismiss_event_modal_escape = (e) => { if (!e.key || e.key != 'Escape') { return; } // immediately unregister from this event once fired window.removeEventListener(e.type, vbo_core_dismiss_event_modal_escape); // trigger the actual dismiss event VBOCore.emitEvent(options.dismiss_event); }; if (options.escape_dismiss) { // listen to the Escape keyup event to dismiss the modal // listen on window to allow admin-widgets to prevent the default behavior and stop the propagation window.addEventListener('keyup', vbo_core_dismiss_event_modal_escape); } } // register to the toggle-loading event if (options.loading_event) { // let a function handle it so that removing the event listener will be doable document.addEventListener(options.loading_event, modal_handle_loading_event_fn); if (options.progress_event) { // let a function handle the progress event so that removing the listener will be doable document.addEventListener(options.progress_event, modal_handle_progress_event_fn); } } // append (or prepend) modal to body if ($('.vbo-modal-overlay-' + options.suffix).length) { $('.vbo-modal-overlay-' + options.suffix).remove(); } if (options.body_prepend) { // prepend to body if ($('body > .vbo-modal-overlay-block').length) { // we've got other modals prepended to the body, so go after the last one $('body > .vbo-modal-overlay-block').last().after(custom_modal); } else { // place the modal right as the first child node of body $('body').prepend(custom_modal); } } else { // append to body $('body').append(custom_modal); } // check if scroll should be locked on the whole page body for a "sticky" modal if (options.lock_scroll) { $('body').addClass('vbo-modal-lock-scroll'); } if (typeof bindDetails === 'object') { try { // bind widget details data modal_content_wrapper.data('details', JSON.stringify(bindDetails)); } catch(e) { // do nothing } } // return the content wrapper element of the new modal return modal_content_wrapper; } /** * Debounce technique to group a flurry of events into one single event. */ static debounceEvent(func, wait, immediate) { var timeout; return function() { var context = this, args = arguments; var later = function() { timeout = null; if (!immediate) func.apply(context, args); }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) { func.apply(context, args); } } } /** * Throttle guarantees a constant flow of events at a given time interval. * Runs immediately when the event takes place, but can be delayed. */ static throttleEvent(method, delay) { var time = Date.now(); return function() { if ((time + delay - Date.now()) < 0) { method(); time = Date.now(); } } } /** * Alternative throttle technique for event listeners. * * @param function callback the callback to invoke. * @param int time the time (in ms) to throttle. * * @return void * * @since 1.6.8 */ static throttleTimer(callback, time) { if (VBOCore.throttle_timer) { // prevent more executions return; } // turn throttle timer on VBOCore.throttle_timer = true; setTimeout(() => { // run the callback with the given delay callback(); // turn throttle timer off VBOCore.throttle_timer = false; }, time); } /** * Tells whether localStorage is supported. * * @return boolean */ static storageSupported() { return typeof localStorage !== 'undefined'; } /** * Gets an item from localStorage. * * @param string keyName the storage key identifier. * * @return any */ static storageGetItem(keyName) { if (!VBOCore.storageSupported()) { return null; } return localStorage.getItem(keyName); } /** * Sets an item to localStorage. * * @param string keyName the storage key identifier. * @param any value the value to store. * * @return boolean */ static storageSetItem(keyName, value) { if (!VBOCore.storageSupported()) { return false; } try { if (typeof value === 'object') { value = JSON.stringify(value); } localStorage.setItem(keyName, value); } catch(e) { console.error(e); return false; } return true; } /** * Removes an item from localStorage. * * @param string keyName the storage key identifier. * * @return boolean */ static storageRemoveItem(keyName) { if (!VBOCore.storageSupported()) { return false; } localStorage.removeItem(keyName); return true; } /** * Returns the name of the storage identifier for the given scope. * * @param string scope the admin menu scope. * * @return string the requested admin menu storage identifier. */ static getStorageScopeName(scope) { let storage_scope_name = VBOCore.options.admin_menu_actions_nm; if (typeof scope === 'string' && scope.length) { if (scope.indexOf('.') !== 0) { scope = '.' + scope; } storage_scope_name += scope; } return storage_scope_name; } /** * Returns a list of admin menu action objects or an empty array. * * @param string scope the admin menu scope. * * @return Array */ static getAdminMenuActions(scope) { let menu_actions = VBOCore.storageGetItem(VBOCore.getStorageScopeName(scope)); if (!menu_actions) { return []; } try { menu_actions = JSON.parse(menu_actions); if (!Array.isArray(menu_actions) || !menu_actions.length) { menu_actions = []; } } catch(e) { return []; } return menu_actions; } /** * Builds an admin menu action object with a proper href property. * * @param object action the action to build. * * @return object */ static buildAdminMenuAction(action) { if (typeof action !== 'object' || !action || !action.hasOwnProperty('name')) { throw new Error('Invalid action object'); } var action_base = action.hasOwnProperty('href') && typeof action['href'] == 'string' ? action['href'] : window.location.href; var action_url; if (action_base.toLowerCase().indexOf('javascript') === 0 && action.hasOwnProperty('widget')) { // no need to prepare the action URL ("JavaScript: void(0)") as the link will use a JS callback return action; } if (action_base.indexOf('http') !== 0) { // relative URL action_url = new URL(action_base, window.location.href); } else { // absolute URL action_url = new URL(action_base); } // build proper href with a relative URL action['href'] = action_url.pathname + action_url.search; return action; } /** * Registers an admin menu action object. * * @param object action the action to build. * @param string scope the admin menu scope. * * @return boolean */ static registerAdminMenuAction(action, scope) { // build menu action object let menu_action_entry = VBOCore.buildAdminMenuAction(action); let menu_actions = VBOCore.getAdminMenuActions(scope); // make sure we are not pushing a duplicate and count pinned actions let pinned_actions = 0; let unpinned_index = []; for (let i = 0; i < menu_actions.length; i++) { // avoid duplicate entries if (!menu_action_entry.hasOwnProperty('widget') && menu_actions[i]['href'] == menu_action_entry['href']) { // duplicate link return false; } if (menu_action_entry.hasOwnProperty('widget') && menu_actions[i].hasOwnProperty('widget') && menu_actions[i]['widget'] == menu_action_entry['widget']) { // duplicate widget return false; } if (menu_actions[i].hasOwnProperty('pinned') && menu_actions[i]['pinned']) { pinned_actions++; } else { unpinned_index.push(i); } } if (pinned_actions >= VBOCore.options.admin_menu_maxactions) { // no more space to register a new menu action for this admin menu return false; } // splice or pop before prepending to keep current indexes let tot_menu_actions = menu_actions.length; if (++tot_menu_actions > VBOCore.options.admin_menu_maxactions) { if (unpinned_index.length) { menu_actions.splice(unpinned_index[unpinned_index.length - 1], 1); } else { menu_actions.pop(); } } // prepend new admin menu action menu_actions.unshift(menu_action_entry); return VBOCore.storageSetItem(VBOCore.getStorageScopeName(scope), menu_actions); } /** * Updates an existing admin menu action object. * * @param object action the action to build. * @param string scope the admin menu scope. * @param number index optional menu action index. * * @return boolean */ static updateAdminMenuAction(action, scope, index) { // build menu action object let menu_action_entry = VBOCore.buildAdminMenuAction(action); let menu_actions = VBOCore.getAdminMenuActions(scope); if (!menu_actions.length) { return false; } if (typeof index === 'undefined') { // find the proper index to update by href for (let i = 0; i < menu_actions.length; i++) { if (menu_actions[i].hasOwnProperty('widget') && menu_action_entry.hasOwnProperty('widget') && menu_actions[i]['widget'] == menu_action_entry['widget']) { // existing entry for widget found index = i; break; } else if (!menu_action_entry.hasOwnProperty('widget') && menu_actions[i]['href'] == menu_action_entry['href']) { // existing entry for link found index = i; break; } } } if (isNaN(index) || !(index in menu_actions)) { // menu entry index not found return false; } menu_actions[index] = menu_action_entry; return VBOCore.storageSetItem(VBOCore.getStorageScopeName(scope), menu_actions); } /** * Checks if the current menu actions should be filled with some other actions. * No update is made over the Local Storage through this method. * * @param array menu_actions menu actions to fill. * @param array widgets_actions default actions to use for filling. * @param string scope the admin menu scope. * * @return array the menu actions eventually filled. */ static fillAdminMenuActions(menu_actions, widgets_actions, scope) { if (!Array.isArray(menu_actions) || !Array.isArray(widgets_actions)) { return []; } if (!menu_actions.length || !widgets_actions.length || menu_actions.length >= VBOCore.options.admin_menu_maxactions) { return menu_actions; } var current_widgets = []; menu_actions.forEach((action, index) => { current_widgets.push(action['widget']); }); for (var i = 0; i < widgets_actions.length; i++) { if (menu_actions.length >= VBOCore.options.admin_menu_maxactions) { // abort for filling completed break; } if (!widgets_actions[i].hasOwnProperty('widget')) { // action must be for a widget continue; } if (current_widgets.includes(widgets_actions[i]['widget'])) { // there cannot be duplicate entries continue; } // fill menu action menu_actions.push(widgets_actions[i]); } return menu_actions; } /** * Formats a date object by accepting "Y", "m" and "d". * * @param Date date The date object to format. * @param string format The date format to use. * * @return string */ static formatDate(date, format) { if (!date instanceof Date) { throw new Error('Invalid date object given.'); } if (!format || typeof format !== 'string') { // default to military format format = 'Y-m-d'; } let year = date.getFullYear(); let month = (date.getMonth() + 1) + ''; let mday = (date.getDate()) + ''; month = month.length < 2 ? '0' + month : month; mday = mday.length < 2 ? '0' + mday : mday; // use a regex to capture the date separator and format identifiers const regexp = /^([ymd])([\s\.\-\/])([ymd])([\s\.\-\/])([ymd])$/gi; // turn the iterable iterator object into an array by using the spread syntax let formatArray = [...format.matchAll(regexp)]; if (!formatArray.length || !Array.isArray(formatArray[0]) || formatArray[0].length != 6) { throw new Error('Invalid date format given.'); } let firstIdentifier = (formatArray[0][1] + '').toLowerCase(); let separatorChar = formatArray[0][2]; let secondIdentifier = (formatArray[0][3] + '').toLowerCase(); let thirdIdentifier = (formatArray[0][5] + '').toLowerCase(); if (!separatorChar) { throw new Error('Invalid date format separator given.'); } let dateParts = []; if (firstIdentifier == 'y') { dateParts.push(year); } else if (firstIdentifier == 'm') { dateParts.push(month); } else { dateParts.push(mday); } if (secondIdentifier == 'y') { dateParts.push(year); } else if (secondIdentifier == 'm') { dateParts.push(month); } else { dateParts.push(mday); } if (thirdIdentifier == 'y') { dateParts.push(year); } else if (thirdIdentifier == 'm') { dateParts.push(month); } else { dateParts.push(mday); } return dateParts.join(separatorChar); } /** * Proxy to access the VBOCurrency object. * * @param object options The currency options. * * @return VBOCurrency */ static getCurrency(options) { return VBOCurrency.getInstance(options); } /** * Proxy to access the VBOAdminDock object. * * @return VBOAdminDock */ static getAdminDock() { return VBOAdminDock.getInstance(); } } /** * These used to be private static properties (static #options), * but they are only supported by quite recent browsers (especially Safari). * It's too premature, so we decided to keep the class properties public * without declaring them as static inside the class declaration. * * @var object */ VBOCore.options = CORE_OPTIONS || { is_vbo: false, is_vcm: false, cms: 'wordpress', widget_ajax_uri: null, assets_ajax_uri: null, multitask_ajax_uri: null, watchdata_ajax_uri: null, current_page: null, current_page_uri: null, root_uri: '/', client: 'admin', panel_opts: {}, admin_widgets: [], notif_audio_url: '', active_listeners: {}, tn_texts: { notifs_enabled: 'Browser notifications are enabled!', notifs_disabled: 'Browser notifications are disabled', notifs_disabled_help: "Could not enable browser notifications.\nThis feature is available only in secure contexts (HTTPS).", admin_widget: 'Admin widget', congrats: 'Congratulations!', }, default_loading_body: '....', multitask_save_event: 'vbo-admin-multitask-save', multitask_open_event: 'vbo-admin-multitask-open', multitask_close_event: 'vbo-admin-multitask-close', multitask_shortcut_ev: 'vbo_multitask_shortcut', multitask_searchfs_ev: 'vbo_multitask_search_focus', widget_modal_rendered: 'vbo-admin-widget-modal-rendered', widget_modal_dismissed: 'vbo-widget-modal-dismissed', admin_menu_maxactions: 3, admin_menu_actions_nm: 'vikbooking.admin_menu.actions', service_worker_path: '', service_worker_scope: './', push: { application_key: '', ajax_url: '', }, push_options: { storage_endp_id: 'vbo_push_subscr_endp', allowed_notif_types: [ 'Book', 'Modify', 'Cancel', 'Request', 'CancelRequest', 'Inquiry', 'CancelInquiry', 'Chat', 'Info', ], }, }; /** * @var bool */ VBOCore.side_panel_on = false; /** * @var bool * @since 1.6.8 */ VBOCore.throttle_timer = false; /** * @var array */ VBOCore.notifications = []; /** * @var object */ VBOCore.widgets_watch_data = null; /** * @var number */ VBOCore.watch_data_interval = null; /** * @var object * @since 1.6.3 */ VBOCore.broadcast_watch_data = null; /** * @var object * @since 1.6.5 */ VBOCore.broadcast_push_data = null; /** * @var object * @since 1.6.8 */ VBOCore.broadcast_watch_events = null; /** * @var array<object> * @since 1.6.5 */ VBOCore.widgets_pushed_data = []; /** * Checks if the KeyBoard event matches the given shortcut. * * @param array keys The shortcut representation. * * @return boolean True if matches, otherwise false. * * @since 1.7.0 */ KeyboardEvent.prototype.shortcut = function(keys) { // get modifiers list var modifiers = keys.slice(0); // pop character from modifiers var keyCode = modifiers.pop(); if (typeof keyCode === 'string') { // get ASCII keyCode = keyCode.toUpperCase().charCodeAt(0); } // make sure the modifiers are lower case modifiers = modifiers.map(function(mod) { return mod.toLowerCase(); }); var ok = false; // validate key code if (this.keyCode == keyCode) { // validate modifiers ok = true; var lookup = ['meta', 'shift', 'alt', 'ctrl']; for (var i = 0; i < lookup.length && ok; i++) { // check if modifiers is pressed var mod = this[lookup[i] + 'Key']; if (mod) { // if pressed, the shortcut must specify it ok &= modifiers.indexOf(lookup[i]) !== -1; } else { // if not pressed, the shortcut must not include it ok &= modifiers.indexOf(lookup[i]) === -1; } } } return ok; } /** * VBOCurrency class implementation. */ w['VBOCurrency'] = class VBOCurrency { /** * Singleton entry-point. * * @see construct() */ static getInstance(options) { if (typeof VBOCurrency.instance === 'undefined') { VBOCurrency.instance = new VBOCurrency(options); } return VBOCurrency.instance; } /** * Class constructor. * * @param object options The currency options object: * - symbol string The currency symbol (such as €, $, £ and so on). * - position int The position of the currency (1 before, 2 after). In case the amount is negative * the space between the currency and the amount won't be used. * - decimals string The decimals separator character ("." or ","). * - thousands string The thousands separator character ("," or "."). * - digits int The number of decimal digits. * - noDecimals int Whether empty decimals should be omitted (1 true, 0 false). * - conversionRate float The currency conversion rate. */ constructor(options) { this.setOptions(options); } /** * Sets the currency options. * * @param object options The currency options object. * * @see construct() */ setOptions(options) { if (options === undefined) { options = {}; } // define default currency options for eventually merging with new options let def_symbol = this.symbol !== undefined ? this.symbol : '$'; let def_position = this.position !== undefined ? this.position : 1; let def_decimals = this.decimals !== undefined ? this.decimals : '.'; let def_thousands = this.thousands !== undefined ? this.thousands : ','; let def_digits = this.digits !== undefined ? this.digits : 2; let def_no_decimals = this.noDecimals !== undefined ? this.noDecimals : 1; let def_conv_rate = this.conversionRate !== undefined ? this.conversionRate : 1; // set currency options this.symbol = (options.hasOwnProperty('symbol') ? options.symbol : def_symbol); this.position = (options.hasOwnProperty('position') ? options.position : def_position); this.decimals = (options.hasOwnProperty('decimals') ? options.decimals : def_decimals); this.thousands = (options.hasOwnProperty('thousands') ? options.thousands : def_thousands); this.digits = (options.hasOwnProperty('digits') ? parseInt(options.digits) : def_digits); this.noDecimals = (options.hasOwnProperty('noDecimals') ? parseInt(options.noDecimals) : def_no_decimals); this.conversionRate = Math.abs((options.hasOwnProperty('conversionRate') ? parseFloat(options.conversionRate) : 1)); } /** * Gets the current currency options. * * @return object */ getOptions() { let options = {}; Object.keys(this).forEach((option) => { if (this.hasOwnProperty(option)) { options[option] = this[option]; } }); return options; } /** * Formats the given price according to the configuration preferences. * * @param float price The price to format. * @param object options Temporarily overrides the currency options. * * @return string The formatted price. */ format(price, options) { // merge currency settings options = Object.assign(this.getOptions(), (options || {})); let no_decimals = options.noDecimals; let dig = options.digits; if (no_decimals && parseInt(price) == price) { // no decimal digits in case of empty decimals dig = 0; } price = parseFloat(price) / options.conversionRate; // check whether the price is negative const isNegative = price < 0; // adjust to given decimals price = Math.abs(price).toFixed(dig); let _d = options.decimals; let _t = options.thousands; // make sure the decimal separator is a valid character if (!_d.match(/[.,\s]/)) { // revert to default one _d = '.'; } // make sure the thousands separator is a valid character if (!_t.match(/[.,\s]/)) { // revert to default one _t = ','; } // make sure both the separators are not equals if (_d == _t) { _t = _d == ',' ? '.' : ','; } price = price.split('.'); price[0] = price[0].replace(/./g, function(c, i, a) { return i > 0 && (a.length - i) % 3 === 0 ? _t + c : c; }); if (isNegative) { // re-add negative sign price[0] = '-' + price[0]; } if (price.length > 1) { price = price[0] + _d + price[1]; } else { price = price[0]; } if (Math.abs(options.position) == 1) { // do not use space in case the position is "-1" return options.symbol + (options.position == 1 ? ' ' : '') + price; } // do not use space in case the position is "-2" return price + (options.position == 2 ? ' ' : '') + options.symbol; } /** * Safely sums 2 prices (a + b). * * @param float a * @param float b * * @return The resulting sum. */ sum(a, b) { // get rid of decimals for higher precision a *= Math.pow(10, this.digits); b *= Math.pow(10, this.digits); // do sum and go back to decimal return (Math.round(a) + Math.round(b)) / Math.pow(10, this.digits); } /** * Safely subtracts 2 prices (a - b). * * @param float a * @param float b * * @return The resulting difference. */ diff(a, b) { // get rid of decimals for higher precision a *= Math.pow(10, this.digits); b *= Math.pow(10, this.digits); // do difference and go back to decimal return (Math.round(a) - Math.round(b)) / Math.pow(10, this.digits); } /** * Safely multiplies 2 prices (a * b). * * @param float a * @param float b * * @return The resulting multiplication. */ multiply(a, b) { // get rid of decimals for higher precision a *= Math.pow(10, this.digits); b *= Math.pow(10, this.digits); // do multiplication and go back to decimal return (Math.round(a) * Math.round(b)) / Math.pow(10, this.digits * 2); } } /** * VBOAdminDock class implementation. */ w['VBOAdminDock'] = class VBOAdminDock { /** * Singleton entry-point. * * @see construct() */ static getInstance(options) { if (typeof VBOAdminDock.instance === 'undefined') { VBOAdminDock.instance = new VBOAdminDock(options); } return VBOAdminDock.instance; } /** * Class constructor. * * @param object options The admin dock options object. */ constructor(options) { // set options this.setOptions(options); // start empty dock elements property this._elements = []; // set dock storage identifier property this._storageId = 'vikbooking.admin_dock.elements'; // set event name for updating the dock element badge counters this._updateBadgeEv = 'vbo-admin-dock-update-badge'; // build dock node this.buildDockNode(); // load dock elements this.loadDockElements(); } /** * Sets the options. * * @param object options The dock options object. * * @see construct() */ setOptions(options) { if (options === undefined) { options = {}; } // default options let defaultOptions = { dockDisplayStyle: 'flex', }; // set options this._options = Object.assign(defaultOptions, options); } /** * Gets the current dock options. * * @return object */ getOptions() { let options = {}; Object.keys(this._options).forEach((option) => { if (this._options.hasOwnProperty(option)) { options[option] = this._options[option]; } }); return options; } /** * Builds the admin dock and adds it to the DOM. * * @return HTMLElement */ buildDockNode() { if (this._dock) { return; } // create element this._dock = document.createElement('div'); this._dock.classList.add('vbo-admin-dock-wrapper'); // listen to the event for updating the element badge counters document.addEventListener(this._updateBadgeEv, this.updateElementsBadgeCounter); // append element to body document.querySelector('body').appendChild(this._dock); return this._dock; } /** * Returns the admin dock node. * * @return HTMLElement */ getDockNode() { return this._dock || this.buildDockNode(); } /** * Removes a given element index from the dock and saves on local storage. * This method should be called after having removed the element from DOM. * * @param number index The element's array index to remove. * * @return bool True if the localStorage was updated or false. */ removeDockElement(index) { // access the previous element let previousElement = this._elements[index]; // splice the elements list this._elements.splice(index, 1); if (!this._elements.length) { // hide the dock this.hideDock(); } else { // reset dock element indexes let dom_elements = this.getDockNode().querySelectorAll('.vbo-admin-dock-element'); this._elements.forEach((element, new_index) => { let widget_id = element.id; if (!dom_elements[new_index] || dom_elements[new_index].getAttribute('data-id') != widget_id) { return; } dom_elements[new_index].setAttribute('data-index', new_index); }); } if (previousElement && previousElement.id == '_tmp' && typeof previousElement?.details?.persist_id === 'string') { // we have removed a temporary dock element with persisting data so try to clear the localStorage VBOCore.storageRemoveItem(this._storageId + '.' + previousElement.details.persist_id); } // update dock elements on localStorage let updated = VBOCore.storageSetItem(this._storageId, this._elements); return updated; } /** * Removes the first dock element found from the given ID. * * @param number id The dock element type ID. * * @return bool True if element was found, removed and updated on localStorage. */ removeDockElementById(id) { let remove_index = this.getDockElementById(id); if (remove_index >= 0) { return this.removeDockElement(remove_index); } return false; } /** * Returns the index of the first dock element found from the given ID. * * @param string id The dock element type ID. * * @return number The index found or -1 if nothing is found. */ getDockElementById(id) { let index_found = -1; this._elements.forEach((element, index) => { if (index_found >= 0) { return; } if (element.id == id) { index_found = index; return; } }); return index_found; } /** * Builds a dock element node. * * @param object details The element details. * @param object data The element data. * @param number index The element index number. * @param HTMLElement body Optional widget modal body to restore. * @param function destroyFn Optional dismiss callback. * * @return HTMLElement */ buildDockElement(details, data, index, body, destroyFn) { if (!details?.id) { throw new Error('Unknown widget id'); } const widgetId = details.id; const element = document.createElement('div'); element.classList.add('vbo-admin-dock-element'); element.setAttribute('data-id', widgetId); element.setAttribute('data-index', index); element.setAttribute('data-badge-count', ''); let content = document.createElement('div'); content.classList.add('vbo-admin-dock-element-cont'); content.addEventListener('click', (e) => { // get content target let target = e.target; if (!target.matches('.vbo-admin-dock-element-cont')) { target = target.closest('.vbo-admin-dock-element-cont'); } // reset badge count target.closest('.vbo-admin-dock-element').setAttribute('data-badge-count', ''); // get widget data, if any let widgetData = data?.event_data?._options || data || {}; // get modal body if NOT from localStorage let prevBody = target.querySelector('.vbo-admin-dock-element-modalbody'); if (!Object.keys(widgetData).length && prevBody && prevBody.children && prevBody.children[0] && prevBody.children[0]?.children?.length) { // unset dock-minimized attribute on main element with class "vbo-modal-widget_modal-wrap" prevBody.children[0].setAttribute('data-dock-minimized', 0); // get the previous style attribute let prevBodyStyle = prevBody.children[0].getAttribute('style'); // restore admin widget body previously minimized let prevTitle = data?.title || ''; let nameSuffix = ' - ' + (details?.name || ''); let modalRestore = { title: prevTitle + (prevTitle.indexOf(nameSuffix) < 0 ? nameSuffix : ''), // do not immediately set the restored modal body to prevBody.children[0] in order // to avoid getting duplicate nested elements with class "vbo-modal-widget_modal-wrap" // body: prevBody.children[0], }; if (details?.modal?.add_class && data?.extra_class && (data.extra_class + '').indexOf((details.modal.add_class) + '') < 0) { // restore widget custom class modalRestore.extra_class = data.extra_class + ' ' + details.modal.add_class; } // display modal with previous content and details to bind let restoredBody = VBOCore.displayModal( Object.assign(data, modalRestore), Object.assign({}, details) ); if (prevBodyStyle) { (restoredBody[0] || restoredBody).setAttribute('style', prevBodyStyle); } // iterate all children elements of main modal body element with class "vbo-modal-widget_modal-wrap" // to append and restore the original child nodes and to avoid duplicate container elements Array.from(prevBody.children[0].children).forEach((child) => { // append child node to the restored modal body (restoredBody[0] || restoredBody).append(child); }); try { if (typeof data?.onRestore === 'function') { // call the restoring function from the original modal data.onRestore.call(restoredBody, e); } // always emit the native restoring event for the current widget VBOCore.emitEvent('vbo-admin-dock-restore-' + widgetId, { data: data?.event_data || {}, }); } catch(e) { // do nothing } } else { // re-render admin widget from scratch VBOCore.handleDisplayWidgetNotification({ widget_id: widgetId, }, widgetData); } // remove element from DOM let elementIndex = element.getAttribute('data-index'); element.remove(); // remove element index from dock this.removeDockElement(elementIndex); }); let content_icon = document.createElement('span'); content_icon.classList.add('vbo-admin-dock-element-icn'); if (details?.style) { content_icon.classList.add('vbo-admin-widget-style-' + details.style); } if (details?.icon) { content_icon.innerHTML = details.icon; } if (body) { // create hidden node let modal_body = document.createElement('div'); modal_body.classList.add('vbo-admin-dock-element-modalbody'); modal_body.style.display = 'none'; try { // move modal body to hidden node modal_body.append((body[0] || body)); // set dock-minimized attribute (body[0] || body).setAttribute('data-dock-minimized', 1); } catch(e) { // do nothing } // append hidden node to content content.appendChild(modal_body); } let content_name = document.createElement('span'); content_name.classList.add('vbo-admin-dock-element-name'); content_name.innerText = details?.name || ''; let dismiss = document.createElement('span'); dismiss.classList.add('vbo-admin-dock-element-dismiss'); dismiss.innerHTML = '×'; dismiss.addEventListener('click', (e) => { try { if (typeof destroyFn === 'function') { // destroy the original modal by firing any dismiss event destroyFn.call(body, e); } } catch(e) { // do nothing } // remove element from DOM let elementIndex = element.getAttribute('data-index'); element.remove(); // remove element index from dock this.removeDockElement(elementIndex); }); // append to content content.appendChild(content_icon); content.appendChild(content_name); // append content to element element.appendChild(content); // append dismiss to element element.appendChild(dismiss); // return the HTMLElement object return element; } /** * Builds a temporary dock element node. * * @param object details The element details. * @param object data The element data, usually an array of objects. * @param number index The element index number. * @param function restoreFn The callback for restoring the data. * @param function destroyFn The callback for destroying the data. * @param number badge Optional badge number to set. * * @return HTMLElement */ buildDockTemporaryElement(details, data, index, restoreFn, destroyFn, badge) { if (!details?.id || details.id != '_tmp') { throw new Error('Invalid temporary element data.'); } const dataId = '_tmp'; const element = document.createElement('div'); element.classList.add('vbo-admin-dock-element'); element.setAttribute('data-id', dataId); element.setAttribute('data-index', index); element.setAttribute('data-badge-count', (badge || '')); let content = document.createElement('div'); content.classList.add('vbo-admin-dock-element-cont'); content.addEventListener('click', (e) => { // get content target let target = e.target; if (!target.matches('.vbo-admin-dock-element-cont')) { target = target.closest('.vbo-admin-dock-element-cont'); } // reset badge count target.closest('.vbo-admin-dock-element').setAttribute('data-badge-count', ''); if (typeof restoreFn === 'function') { this._elements.forEach((dockEl) => { if (dockEl.id == dataId) { // invoke the callback by passing the current element data restoreFn(dockEl?.data); // abort return; } }); } // remove element from DOM let elementIndex = element.getAttribute('data-index'); element.remove(); // remove element index from dock this.removeDockElement(elementIndex); }); let content_icon = document.createElement('span'); content_icon.classList.add('vbo-admin-dock-element-icn'); if (details?.style) { content_icon.classList.add('vbo-admin-widget-style-' + details.style); } if (details?.icon) { content_icon.innerHTML = details.icon; } let content_name = document.createElement('span'); content_name.classList.add('vbo-admin-dock-element-name'); content_name.innerText = details?.name || ''; let dismiss = document.createElement('span'); dismiss.classList.add('vbo-admin-dock-element-dismiss'); dismiss.innerHTML = '×'; dismiss.addEventListener('click', (e) => { if (typeof destroyFn === 'function') { this._elements.forEach((dockEl) => { if (dockEl.id == dataId) { // invoke the callback by passing the current element data destroyFn(dockEl?.data); // abort return; } }); } // remove element from DOM let elementIndex = element.getAttribute('data-index'); element.remove(); // remove element index from dock this.removeDockElement(elementIndex); }); // append to content content.appendChild(content_icon); content.appendChild(content_name); // append content to element element.appendChild(content); // append dismiss to element element.appendChild(dismiss); // return the HTMLElement object return element; } /** * Adds a widget to the dock. * * @param object details The widget details. * @param object data The widget restoring data. * @param HTMLElement body Optional widget modal body to restore. * @param function destroyFn Optional dismiss callback. */ addWidget(details, data, body, destroyFn) { if (!VBOCore.options.widget_ajax_uri) { throw new Error('Wrong environment'); } if (typeof details !== 'object') { details = {}; } if (!details?.id) { throw new Error('Unknown widget id'); } // add element to dock in the DOM at first this.getDockNode().appendChild( this.buildDockElement(details, data, this._elements.length, body, destroyFn) ); // push dock element after it was added to the DOM this._elements.push({ id: details.id, details: Object.assign({}, details), data: Object.assign({}, (data?.event_data?._options || {})), }); // ensure dock is visible this.showDock(); // update dock elements on localStorage at last VBOCore.storageSetItem(this._storageId, this._elements); } /** * Adds temporary data to the dock. Data will NOT persist on the same localStorage key. * Originally introduced to handle the rates update requests within a queue. * * @param object details The data details. * @param object data The data to store, usually an array of objects. * @param function restoreFn The callback for restoring the data. * @param function destroyFn The callback for destroying the data. * @param number badge Optional badge number to set. */ addTemporaryData(details, data, restoreFn, destroyFn, badge) { if (!VBOCore.options.widget_ajax_uri) { throw new Error('Wrong environment'); } if (typeof details !== 'object') { details = {}; } // temporary data will always get a private ID to avoid duplicate entries in the dock details.id = '_tmp'; // check if the same temporary data element type exists let previousElement = null; let previousIndex = null; this.getElements().forEach((element, index) => { if (previousElement) { return; } if (element.id == details.id) { // previous temporary data element type found previousElement = element; previousIndex = index; return; } }); if (!previousElement) { // add temporary element to dock in the DOM at first this.getDockNode().appendChild( this.buildDockTemporaryElement(details, data, this._elements.length, restoreFn, destroyFn, (badge ? badge : (Array.isArray(data) ? data.length : null))) ); // push dock element after it was added to the DOM this._elements.push({ id: details.id, details: Object.assign({}, details), data: Array.isArray(data) ? [...data] : Object.assign({}, data), }); } else { // update previous temporary data element type let updatedData = Array.isArray(data) && Array.isArray(previousElement.data) ? [...previousElement.data].concat([...data]) : [...data]; let updatedBadge = badge ? badge : (Array.isArray(updatedData) ? updatedData.length : previousElement.badge); // update existing dock element this._elements[previousIndex] = { id: details.id, details: Object.assign({}, previousElement.details, details), data: updatedData, }; if (updatedBadge) { // update DOM element let dom_elements = this.getDockNode().querySelectorAll('.vbo-admin-dock-element'); this._elements.forEach((element, cur_index) => { if (!dom_elements[cur_index] || dom_elements[cur_index].getAttribute('data-id') != element.id) { return; } if (dom_elements[cur_index].getAttribute('data-id') != details.id) { return; } // update badge attribute dom_elements[cur_index].setAttribute('data-badge-count', updatedBadge); }); } } // ensure dock is visible this.showDock(); if (details.persist_id && typeof details.persist_id === 'string') { // update dock element on a different localStorage key at last VBOCore.storageSetItem(this._storageId + '.' + details.persist_id, (previousIndex ? this._elements[previousIndex] : this._elements[this._elements.length - 1])); } } /** * Loads temporary data onto the dock by using an ID and type identifier. * Originally introduced to handle the rates update requests within a queue. * * @param object details The data details to load. * @param function restoreFn The callback for restoring the data. * @param function destroyFn The callback for destroying the data. * @param number badge Optional badge number to set. * * @return any Data object loaded on success or null. */ loadTemporaryData(details, restoreFn, destroyFn, badge) { if (!VBOCore.options.widget_ajax_uri) { throw new Error('Wrong environment'); } if (typeof details !== 'object' || !details.id || !details.persist_id || typeof details.persist_id !== 'string') { return null; } // temporary data will always get a private ID to avoid duplicate entries in the dock details.id = '_tmp'; // check if the same temporary data element type exists let previousElement = null; this.getElements().forEach((element, index) => { if (previousElement) { return; } if (element.id == details.id) { // previous temporary data element type found previousElement = element; return; } }); if (previousElement) { // temporary data already loaded in dock return previousElement; } // load the requested temporary and persisting data let storageElement = VBOCore.storageGetItem(this._storageId + '.' + details.persist_id); try { if (typeof storageElement === 'string') { storageElement = JSON.parse(storageElement); } } catch(e) { storageElement = null; } if (!storageElement || typeof storageElement !== 'object' || !storageElement.data) { return null; } // add the temporary data just loaded to the dock this.addTemporaryData(Object.assign({}, (storageElement.details || {}), details), storageElement.data, restoreFn, destroyFn, badge); // return the data loaded return storageElement; } /** * Returns the current dock elements. * * @return Array */ getElements() { return this._elements; } /** * Loads widget dock elements from localStorage and populates them, if any. */ loadDockElements() { let storageElements = VBOCore.storageGetItem(this._storageId) || []; try { if (typeof storageElements === 'string') { storageElements = JSON.parse(storageElements); } } catch(e) { storageElements = []; } if (Array.isArray(storageElements)) { // set current elements this._elements = storageElements; } // scan all elements to ensure no temporary data was stored upon deleting or adding new widgets this._elements.forEach((element, index) => { if (element?.id == '_tmp') { // splice the elements list this._elements.splice(index, 1); // update dock elements on localStorage by only keeping real widgets VBOCore.storageSetItem(this._storageId, this._elements); // abort return; } }); if (this._elements.length) { // populate dock elements this.populateDockElements(); // show the dock this.showDock(); } else { // hide the dock this.hideDock(); } } /** * Populates the current dock elements. */ populateDockElements() { const dockNode = this.getDockNode(); // empty the dock dockNode.innerHTML = ''; // scan all elements this._elements.forEach((element, index) => { // add element to dock in the DOM at first dockNode.appendChild( this.buildDockElement(element?.details, element?.data, index) ); }); } /** * Hides the dock node. */ hideDock() { this.getDockNode().style.display = 'none'; } /** * Shows the dock node. */ showDock() { if (!this.getDockNode().checkVisibility()) { this.getDockNode().style.display = this._options.dockDisplayStyle; } } /** * Event callback fired to update the element(s) badge counter. */ updateElementsBadgeCounter(e) { if (!e || !e.detail || !e.detail?.widgetId) { return; } let elements = VBOAdminDock.getInstance().getElements(); if (!elements.length) { return; } let widgetId = e.detail.widgetId; let badgeCount = parseInt(e.detail?.badgeCount || 0); let badgeValue = badgeCount > 0 ? badgeCount : ''; elements.forEach((element) => { if (element.id == widgetId) { document.querySelectorAll('.vbo-admin-dock-element[data-id="' + widgetId + '"]').forEach((elNode) => { elNode.setAttribute('data-badge-count', badgeValue); }); } }); } } })(jQuery, window);