PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.6.9
VikBooking Hotel Booking Engine & PMS v1.6.9
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
vikbooking / admin / resources / vbocore.js

vbocore.js in VikBooking Hotel Booking Engine & PMS 1.6.9, at admin/resources/vbocore.js

3,002 lines 88.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * VikBooking Core v1.6.8
3 * Copyright (C) 2024 E4J s.r.l. All Rights Reserved.
4 * http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
5 * https://vikwp.com | https://e4j.com | https://e4jconnect.com
6 */
7 (function($, w) {
8 'use strict';
9
10 w['VBOCore'] = class VBOCore {
11
12 /**
13 * Proxy to support static injection of params.
14 */
15 constructor(params) {
16 if (typeof params === 'object') {
17 VBOCore.setOptions(params);
18 }
19 }
20
21 /**
22 * Inject options by overriding default properties.
23 *
24 * @param object params
25 *
26 * @return self
27 */
28 static setOptions(params) {
29 if (typeof params === 'object') {
30 VBOCore.options = Object.assign(VBOCore.options, params);
31 }
32
33 return VBOCore;
34 }
35
36 /**
37 * Getter for admin_widgets private options property.
38 *
39 * @return array
40 */
41 static get admin_widgets() {
42 return VBOCore.options.admin_widgets;
43 }
44
45 /**
46 * Getter for multitask open event private property.
47 *
48 * @return string
49 */
50 static get multitask_open_event() {
51 return VBOCore.options.multitask_open_event;
52 }
53
54 /**
55 * Getter for multitask close event private property.
56 *
57 * @return string
58 */
59 static get multitask_close_event() {
60 return VBOCore.options.multitask_close_event;
61 }
62
63 /**
64 * Getter for multitask shortcut event private property.
65 *
66 * @return string
67 */
68 static get multitask_shortcut_event() {
69 return VBOCore.options.multitask_shortcut_ev;
70 }
71
72 /**
73 * Getter for multitask seach focus shortcut event private property.
74 *
75 * @return string
76 */
77 static get multitask_searchfs_event() {
78 return VBOCore.options.multitask_searchfs_ev;
79 }
80
81 /**
82 * Getter for multitask event widget modal rendered.
83 *
84 * @return string
85 */
86 static get widget_modal_rendered() {
87 return VBOCore.options.widget_modal_rendered;
88 }
89
90 /**
91 * Getter for widget modal dismiss event.
92 *
93 * @return string
94 */
95 static get widget_modal_dismissed() {
96 return VBOCore.options.widget_modal_dismissed;
97 }
98
99 /**
100 * Getter for the service worker file path.
101 *
102 * @return string
103 */
104 static get service_worker_path() {
105 return VBOCore.options.service_worker_path;
106 }
107
108 /**
109 * Getter for the service worker scope.
110 *
111 * @return string
112 */
113 static get service_worker_scope() {
114 return VBOCore.options.service_worker_scope;
115 }
116
117 /**
118 * Getter for the push application key.
119 *
120 * @return string
121 */
122 static get push_application_key() {
123 return VBOCore.options.push.application_key;
124 }
125
126 /**
127 * Getter for the push storage endpoint identifier.
128 *
129 * @return string
130 */
131 static get push_storage_endpoint_id() {
132 return VBOCore.options.push_options.storage_endp_id;
133 }
134
135 /**
136 * Parses an AJAX response error object.
137 *
138 * @param object err
139 *
140 * @return bool
141 */
142 static isConnectionLostError(err) {
143 if (!err || !err.hasOwnProperty('status')) {
144 return false;
145 }
146
147 return (
148 err.statusText == 'error'
149 && err.status == 0
150 && (err.readyState == 0 || err.readyState == 4)
151 && (!err.hasOwnProperty('responseText') || err.responseText == '')
152 );
153 }
154
155 /**
156 * Ensures AJAX requests that fail due to connection errors are retried automatically.
157 *
158 * @param string url
159 * @param object data
160 * @param function success
161 * @param function failure
162 * @param number attempt
163 */
164 static doAjax(url, data, success, failure, attempt) {
165 const AJAX_MAX_ATTEMPTS = 3;
166
167 if (attempt === undefined) {
168 attempt = 1;
169 }
170
171 return $.ajax({
172 type: 'POST',
173 url: url,
174 data: data
175 }).done(function(resp) {
176 if (success !== undefined) {
177 // launch success callback function
178 success(resp);
179 }
180 }).fail(function(err) {
181 /**
182 * If the error is caused by a site connection lost, and if the number
183 * of retries is lower than max attempts, retry the same AJAX request.
184 */
185 if (attempt < AJAX_MAX_ATTEMPTS && VBOCore.isConnectionLostError(err)) {
186 // delay the retry by half second
187 setTimeout(function() {
188 // re-launch same request and increase number of attempts
189 console.log('Retrying previous AJAX request');
190 VBOCore.doAjax(url, data, success, failure, (attempt + 1));
191 }, 500);
192 } else {
193 // launch the failure callback otherwise
194 if (failure !== undefined) {
195 failure(err);
196 }
197 }
198
199 if (!err.status || err.status == 500) {
200 // log the error in console
201 console.error('AJAX request failed' + (err.status == 500 ? ' (' + err.responseText + ')' : ''), err);
202 }
203 });
204 }
205
206 /**
207 * Matches a keyword against a text.
208 *
209 * @param string search the keyword to search.
210 * @param string text the text to compare.
211 *
212 * @return bool
213 */
214 static matchString(search, text) {
215 return ((text + '').indexOf(search) >= 0);
216 }
217
218 /**
219 * Converts a base64 URL for a server key into an array of 8-bit unsigned integers.
220 *
221 * @param string key the base64 encoded URL key.
222 *
223 * @return Uint8Array
224 */
225 static base64ToUint8Array(key) {
226 const padding = ('=').repeat((4 - (key.length % 4)) % 4);
227 const base64 = (key + padding).replace(/\-/g, '+').replace(/_/g, '/');
228
229 const rawData = window.atob(base64);
230 const uint8_arr = new Uint8Array(rawData.length);
231
232 for (let i = 0; i < rawData.length; ++i) {
233 uint8_arr[i] = rawData.charCodeAt(i);
234 }
235
236 return uint8_arr;
237 }
238
239 /**
240 * Builds an object containing the Push subscription details and data.
241 *
242 * @param PushSubscription pushSubscription the registration data.
243 * @param object data the extra data to merge.
244 *
245 * @return object
246 */
247 static buildPushSubscriptionData(pushSubscription, data) {
248 if (!(pushSubscription instanceof PushSubscription)) {
249 return data;
250 }
251
252 let key = pushSubscription.getKey('p256dh');
253 let token = pushSubscription.getKey('auth');
254 let contentEncoding = (PushManager.supportedContentEncodings || ['aesgcm'])[0];
255
256 let details = {
257 endpoint: pushSubscription.endpoint,
258 publicKey: key ? btoa(String.fromCharCode.apply(null, new Uint8Array(key))) : null,
259 authToken: token ? btoa(String.fromCharCode.apply(null, new Uint8Array(token))) : null,
260 encoding: contentEncoding,
261 };
262
263 return Object.assign(details, data);
264 }
265
266 /**
267 * Attempts to install (and eventually update) the VikBooking's Service Worker.
268 *
269 * @param bool update if defined, it will update the Service Worker Registration.
270 *
271 * @return Promise
272 */
273 static installServiceWorker(update) {
274 // check if support for Web App badge is available
275 if (typeof navigator.setAppBadge !== 'undefined') {
276 // always clear the app badge
277 navigator.clearAppBadge();
278 }
279
280 return new Promise((resolve, reject) => {
281 if (!('serviceWorker' in navigator)) {
282 reject('Service workers are not supported by this browser');
283 return;
284 }
285
286 if (!('PushManager' in window)) {
287 reject('Push notifications are not supported by this browser');
288 return;
289 }
290
291 if (!VBOCore.service_worker_path) {
292 reject('Service Worker path not configured');
293 return;
294 }
295
296 if (!VBOCore.notificationsEnabled()) {
297 reject('Notifications are disabled, and so the service worker will not be installed');
298 return;
299 }
300
301 navigator.serviceWorker.register(VBOCore.service_worker_path, {scope: VBOCore.service_worker_scope}).then(
302 (registration) => {
303 if (typeof update !== 'undefined') {
304 // attempt to update the registration for un-caching purposes
305 registration.update().then((updated) => {
306 // all good
307 }).catch((e) => {
308 // updating should never fail after registering with success
309 console.error(`Service worker registration update failed: ${e}`);
310 });
311 }
312
313 // listen to the ServiceWorker messages for the client
314 VBOCore.listenServiceWorkerMessages();
315
316 // resolve the Promise
317 resolve(registration);
318 },
319 (e) => {
320 console.error(`Service worker registration failed: ${e}`);
321 reject('Service worker registration failed');
322 }
323 );
324 });
325 }
326
327 /**
328 * Handles the Push subscription state. To be called after the
329 * Service Worker installation (registration) is ready.
330 *
331 * @param ServiceWorkerRegistration registration sw registration object.
332 *
333 * @return Promise
334 */
335 static handlePushSubscription(registration) {
336 return new Promise((resolve, reject) => {
337 if (!(registration instanceof ServiceWorkerRegistration)) {
338 reject('Missing service worker registration');
339 return;
340 }
341
342 if (!VBOCore.push_application_key) {
343 reject('Missing application key for Push because not supported');
344 return;
345 }
346
347 // check if a push subscription is available
348 registration.pushManager.getSubscription().then((pushSubscription) => {
349 if (pushSubscription) {
350 // we are subscribed to Push
351 let previous_endpoint = VBOCore.storageGetItem(VBOCore.push_storage_endpoint_id);
352 if (previous_endpoint && pushSubscription.endpoint != previous_endpoint) {
353 // update subscription data
354 VBOCore.doAjax(
355 VBOCore.options.push.ajax_url,
356 VBOCore.buildPushSubscriptionData(pushSubscription, {
357 agent: window.navigator.userAgent,
358 type: 'update',
359 })
360 );
361 }
362
363 // resolve the promise with the Push subscription data
364 resolve(pushSubscription);
365 return;
366 }
367
368 // push subscription options
369 const pushOptions = {
370 userVisibleOnly: true,
371 applicationServerKey: VBOCore.base64ToUint8Array(VBOCore.push_application_key),
372 };
373
374 // subscribe to Push
375 registration.pushManager.subscribe(pushOptions).then(
376 (pushSubscription) => {
377 // store subscription data
378 VBOCore.storageSetItem(VBOCore.push_storage_endpoint_id, pushSubscription.endpoint);
379 VBOCore.doAjax(
380 VBOCore.options.push.ajax_url,
381 VBOCore.buildPushSubscriptionData(pushSubscription, {
382 agent: window.navigator.userAgent,
383 type: 'new',
384 })
385 );
386
387 // resolve the promise with the Push subscription data
388 resolve(pushSubscription);
389 },
390 (e) => {
391 console.error(`Error handling the Push subscription state: ${e}`);
392 reject('Error handling the Push subscription state');
393 }
394 );
395 })
396 .catch((e) => {
397 console.error(`Error getting the Push subscription state: ${e}`);
398 reject('Error getting the Push subscription state');
399 });
400 });
401 }
402
403 /**
404 * Starts listening to the "message" events posted by the ServiceWorker.
405 *
406 * @return void
407 */
408 static listenServiceWorkerMessages() {
409 if (!('serviceWorker' in navigator)) {
410 return;
411 }
412
413 try {
414 // add listener to the message event for the data posted by the ServiceWorker to the client
415 navigator.serviceWorker.addEventListener('message', (event) => {
416 VBOCore.handleServiceWorkerMessage(event.data);
417 });
418 } catch(e) {
419 // do nothing
420 }
421 }
422
423 /**
424 * Handles a message data posted by the ServiceWorker upon clicking a Push notification.
425 *
426 * @param object data the event.notification.data object posted by the ServiceWorker.
427 *
428 * @return void
429 */
430 static handleServiceWorkerMessage(data) {
431 let notif_type = data.type || '';
432
433 if (!notif_type || !VBOCore.options.push_options.allowed_notif_types.includes(notif_type)) {
434 console.error(data);
435 throw new Error('Unsupported message type');
436 }
437
438 // determine the proper widget for dispatching the message data
439 let widget_id = 'booking_details';
440 if (notif_type == 'Chat') {
441 widget_id = 'guest_messages';
442 }
443
444 // the raw notification content
445 let raw_content = data.content || {};
446
447 // multitask data options
448 let options = Object.assign({
449 _push: 1,
450 title: data.title || '',
451 message: data.message || '',
452 }, raw_content);
453
454 // dispatch (Push) message data on widget
455 VBOCore.handleDisplayWidgetNotification({widget_id: widget_id}, options, true);
456
457 // update pushed data map for the next watching event in the current browsing context
458 VBOCore.widgets_pushed_data.push(raw_content);
459
460 // post message onto broadcast channel for any other browsing context
461 if (VBOCore.broadcast_push_data) {
462 // the next watch-data interval will receive the pushed data information
463 VBOCore.broadcast_push_data.postMessage(raw_content);
464 }
465
466 // let the window reload the badge counter in case it must be updated
467 VBOCore.emitEvent('vbo-badge-count-reload');
468 }
469
470 /**
471 * Initializes the multitasking panel for the admin widgets.
472 *
473 * @param object params the panel object params.
474 *
475 * @return bool
476 */
477 static prepareMultitasking(params) {
478 var panel_opts = {
479 selector: "",
480 sclass_l_small: "vbo-sidepanel-right",
481 sclass_l_large: "vbo-sidepanel-large",
482 btn_trigger: "",
483 search_selector: "#vbo-sidepanel-search-input",
484 search_nores: ".vbo-sidepanel-add-widgets-nores",
485 close_selector: ".vbo-sidepanel-dismiss-btn",
486 t_layout_small: ".vbo-sidepanel-layout-small",
487 t_layout_large: ".vbo-sidepanel-layout-large",
488 wclass_base_sel: ".vbo-admin-widgets-widget-output",
489 wclass_l_small: "vbo-admin-widgets-container-small",
490 wclass_l_large: "vbo-admin-widgets-container-large",
491 addws_selector: ".vbo-sidepanel-add-widgets",
492 addw_selector: ".vbo-sidepanel-add-widget",
493 addw_modal_cls: "vbo-widget-render-modal",
494 addw_def_cls: "vbo-widget-render-regular",
495 addwfs_selector: ".vbo-sidepanel-add-widget-focussed",
496 wtags_selector: ".vbo-sidepanel-widget-tags",
497 addw_data_attr: "data-vbowidgetid",
498 actws_selector: ".vbo-sidepanel-active-widgets",
499 editw_selector: ".vbo-sidepanel-edit-widgets-trig",
500 rmwidget_class: "vbo-admin-widgets-widget-remove",
501 rmwidget_icn: "",
502 dtcwidget_class: "vbo-admin-widgets-widget-detach",
503 dtctarget_class: "vbo-admin-widget-head",
504 dtcwidget_icn: "",
505 notif_selector: ".vbo-sidepanel-notifications-btn",
506 notif_on_class: "vbo-sidepanel-notifications-on",
507 notif_off_class: "vbo-sidepanel-notifications-off",
508 open_class: "vbo-sidepanel-open",
509 close_class: "vbo-sidepanel-close",
510 cur_widget_cls: "vbo-admin-widgets-container-small",
511 };
512
513 if (typeof params === 'object') {
514 panel_opts = Object.assign(panel_opts, params);
515 }
516
517 if (!panel_opts.btn_trigger || !panel_opts.selector) {
518 console.error('Got no trigger or selector');
519 return false;
520 }
521
522 // push panel options
523 VBOCore.setOptions({
524 panel_opts: panel_opts,
525 });
526
527 // setup browser notifications
528 VBOCore.setupNotifications();
529
530 // count active widgets on current page
531 var tot_active_widgets = VBOCore.options.admin_widgets.length;
532 if (tot_active_widgets > 0) {
533 // hide add-widgets container
534 $(panel_opts.addws_selector).hide();
535
536 // register listener for input search blur
537 VBOCore.registerSearchWidgetsBlur();
538 }
539
540 // register click event on trigger button
541 $(VBOCore.options.panel_opts.btn_trigger).on('click', function() {
542 var side_panel = $(VBOCore.options.panel_opts.selector);
543 if (side_panel.hasClass(VBOCore.options.panel_opts.open_class)) {
544 // hide panel
545 VBOCore.side_panel_on = false;
546 VBOCore.emitMultitaskEvent(VBOCore.multitask_close_event);
547 side_panel.addClass(VBOCore.options.panel_opts.close_class).removeClass(VBOCore.options.panel_opts.open_class);
548 // always hide add-widgets container
549 $(VBOCore.options.panel_opts.addws_selector).hide();
550 // check if we are currently editing
551 var is_editing = ($('.' + VBOCore.options.panel_opts.editmode_class).length > 0);
552 if (is_editing) {
553 // deactivate editing mode
554 VBOCore.toggleWidgetsPanelEditing(null);
555 }
556 } else {
557 // show panel
558 VBOCore.side_panel_on = true;
559 VBOCore.emitMultitaskEvent(VBOCore.multitask_open_event);
560 side_panel.addClass(VBOCore.options.panel_opts.open_class).removeClass(VBOCore.options.panel_opts.close_class);
561 if (!VBOCore.options.admin_widgets.length) {
562 // set focus on search widgets input with delay for the opening animation
563 setTimeout(function() {
564 $(VBOCore.options.panel_opts.search_selector).focus();
565 }, 300);
566 }
567 }
568 });
569
570 // register close/dismiss button
571 $(VBOCore.options.panel_opts.close_selector).on('click', function() {
572 $(VBOCore.options.panel_opts.btn_trigger).trigger('click');
573 });
574
575 // register toggle layout buttons
576 $(VBOCore.options.panel_opts.t_layout_large).on('click', function() {
577 // large layout
578 $(VBOCore.options.panel_opts.selector).addClass(VBOCore.options.panel_opts.sclass_l_large).removeClass(VBOCore.options.panel_opts.sclass_l_small);
579 $(VBOCore.options.panel_opts.wclass_base_sel).addClass(VBOCore.options.panel_opts.wclass_l_large).removeClass(VBOCore.options.panel_opts.wclass_l_small);
580 VBOCore.options.panel_opts.cur_widget_cls = VBOCore.options.panel_opts.sclass_l_large;
581 });
582 $(VBOCore.options.panel_opts.t_layout_small).on('click', function() {
583 // small layout
584 $(VBOCore.options.panel_opts.selector).addClass(VBOCore.options.panel_opts.sclass_l_small).removeClass(VBOCore.options.panel_opts.sclass_l_large);
585 $(VBOCore.options.panel_opts.wclass_base_sel).addClass(VBOCore.options.panel_opts.wclass_l_small).removeClass(VBOCore.options.panel_opts.wclass_l_large);
586 VBOCore.options.panel_opts.cur_widget_cls = VBOCore.options.panel_opts.sclass_l_small;
587 });
588
589 // register listener for esc key pressed
590 $(document).keyup(function(e) {
591 if (!VBOCore.side_panel_on) {
592 return;
593 }
594 if ((e.key && e.key === "Escape") || (e.keyCode && e.keyCode == 27)) {
595 $(VBOCore.options.panel_opts.btn_trigger).trigger('click');
596 }
597 });
598
599 // register listener for input search focus
600 $(VBOCore.options.panel_opts.search_selector).on('focus', function() {
601 // always show add-widgets container
602 var widget_focus_class = VBOCore.options.panel_opts.addwfs_selector.replace('.', '');
603 $(VBOCore.options.panel_opts.addw_selector).removeClass(widget_focus_class);
604 $(VBOCore.options.panel_opts.addws_selector).show();
605 });
606
607 // register listener on input search widget
608 $(VBOCore.options.panel_opts.search_selector).keyup(function(e) {
609 // get the keyword to look for
610 var keyword = $(this).val();
611 // counting matching widgets
612 var matching = 0;
613 var first_matched = null;
614 var widget_focus_class = VBOCore.options.panel_opts.addwfs_selector.replace('.', '');
615
616 // adjust widgets to be displayed
617 if (!keyword.length) {
618 // show all widgets for selection
619 $(VBOCore.options.panel_opts.addw_selector).show();
620 // hide "no results"
621 $(VBOCore.options.panel_opts.search_nores).hide();
622 // all widgets are matching
623 matching = $(VBOCore.options.panel_opts.addw_selector).length;
624 } else {
625 // make the keyword lowercase
626 keyword = (keyword + '').toLowerCase();
627 // parse all widget's description tags
628 $(VBOCore.options.panel_opts.addw_selector).each(function() {
629 var elem = $(this);
630 var descr = elem.find(VBOCore.options.panel_opts.wtags_selector).text();
631 if (VBOCore.matchString(keyword, descr)) {
632 elem.show();
633 matching++;
634 if (!first_matched) {
635 // store the first widget that matched
636 first_matched = elem.attr(VBOCore.options.panel_opts.addw_data_attr);
637 }
638 } else {
639 elem.hide();
640 }
641 });
642 // check how many widget matched
643 if (matching > 0) {
644 // hide "no results"
645 $(VBOCore.options.panel_opts.search_nores).hide();
646 } else {
647 // show "no results"
648 $(VBOCore.options.panel_opts.search_nores).show();
649 }
650 }
651
652 // check for shortcuts
653 if (!e.key) {
654 return;
655 }
656
657 // handle Enter key press to add a widget
658 if (e.key === "Enter") {
659 // on Enter key pressed, open/add the first matching widget or the focussed one
660 var load_wid_id = null;
661 var focussed_wid = $(VBOCore.options.panel_opts.addwfs_selector + ':visible').first();
662
663 if (focussed_wid.length) {
664 load_wid_id = focussed_wid.attr(VBOCore.options.panel_opts.addw_data_attr);
665 } else if (first_matched) {
666 load_wid_id = first_matched;
667 }
668
669 if (!load_wid_id) {
670 // no widget to render found
671 return;
672 }
673
674 if (e.shiftKey === true) {
675 // widget multitask panel rendering
676 VBOCore.addWidgetToPanel(load_wid_id);
677 $(VBOCore.options.panel_opts.search_selector).trigger('blur');
678
679 return;
680 }
681
682 // widget modal rendering is the default method
683 VBOCore.renderModalWidget(load_wid_id);
684
685 return;
686 }
687
688 // handle arrow keys selection
689 if (matching > 0 && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
690 // on arrow key pressed, select the next or prev widget
691 var addws_element = $(VBOCore.options.panel_opts.addws_selector);
692 var addws_cont_pos = addws_element.offset().top;
693 var addws_otheight = addws_element.outerHeight();
694 var addws_scrolltp = addws_element.scrollTop();
695
696 if (e.key === 'ArrowDown') {
697 var default_widg = $(VBOCore.options.panel_opts.addw_selector + ':visible').first();
698 } else {
699 var default_widg = $(VBOCore.options.panel_opts.addw_selector + ':visible').last();
700 }
701 var focussed_wid = $(VBOCore.options.panel_opts.addwfs_selector + ':visible').first();
702 var addw_height = default_widg.outerHeight();
703 var focussed_pos = default_widg.offset().top + addw_height;
704
705 if (focussed_wid.length) {
706 focussed_wid.removeClass(widget_focus_class);
707 if (e.key === 'ArrowDown') {
708 var goto_wid = focussed_wid.nextAll(VBOCore.options.panel_opts.addw_selector + ':visible').first();
709 } else {
710 var goto_wid = focussed_wid.prevAll(VBOCore.options.panel_opts.addw_selector + ':visible').first();
711 }
712 if (goto_wid.length) {
713 goto_wid.addClass(widget_focus_class);
714 focussed_pos = goto_wid.offset().top + addw_height;
715 } else {
716 default_widg.addClass(widget_focus_class);
717 }
718 } else {
719 default_widg.addClass(widget_focus_class);
720 }
721
722 if (focussed_pos > (addws_cont_pos + addws_otheight)) {
723 addws_element.scrollTop(focussed_pos - addws_cont_pos - addw_height + addws_scrolltp);
724 } else if (focussed_pos < 0) {
725 addws_element.scrollTop(0);
726 }
727 }
728 });
729
730 // register listener for adding widgets
731 $(VBOCore.options.panel_opts.addw_selector).on('click', function(e) {
732 var widget_id = $(this).attr(VBOCore.options.panel_opts.addw_data_attr);
733 if (!widget_id || !widget_id.length) {
734 return false;
735 }
736
737 // determine widget rendering method
738 if (e && e.target) {
739 let cktarget = $(e.target);
740 if (e.shiftKey === true || (cktarget.hasClass(VBOCore.options.panel_opts.addw_modal_cls) || cktarget.parent().hasClass(VBOCore.options.panel_opts.addw_modal_cls))) {
741 // widget modal rendering
742 VBOCore.renderModalWidget(widget_id);
743
744 return;
745 }
746 }
747
748 // widget multitask panel rendering
749 VBOCore.addWidgetToPanel(widget_id);
750 });
751
752 // register listener for updating multitask sidepanel with debounce
753 document.addEventListener(VBOCore.options.multitask_save_event, VBOCore.debounceEvent(VBOCore.saveMultitasking, 1000));
754
755 // subscribe to event for multitask shortcut
756 document.addEventListener(VBOCore.multitask_shortcut_event, function() {
757 // toggle multitask panel
758 $(VBOCore.options.panel_opts.btn_trigger).trigger('click');
759 });
760
761 // subscribe to event for multitask search focus shortcut
762 document.addEventListener(VBOCore.multitask_searchfs_event, function() {
763 // focus search multitask widgets
764 $(VBOCore.options.panel_opts.search_selector).trigger('focus');
765 });
766
767 // register click event on edit widgets button
768 $(VBOCore.options.panel_opts.editw_selector).on('click', function() {
769 VBOCore.toggleWidgetsPanelEditing(null);
770 });
771
772 // register detach widget buttons
773 $('.' + VBOCore.options.panel_opts.dtcwidget_class).each(function() {
774 let widget_wrapper = $(this).parent(VBOCore.options.panel_opts.wclass_base_sel);
775 let detach_widget_id = widget_wrapper.attr(VBOCore.options.panel_opts.addw_data_attr);
776 let detach_to_target = widget_wrapper.find('.' + VBOCore.options.panel_opts.dtctarget_class);
777 if (detach_to_target.length) {
778 // move detach wrapper onto the target (widget head)
779 $(this).prependTo(detach_to_target);
780 }
781
782 if (!detach_widget_id) {
783 return;
784 }
785
786 // register detach action
787 $(this).on('click', function() {
788 // detach widget, meaning do a modal rendering
789 VBOCore.renderModalWidget(detach_widget_id);
790 });
791 });
792 }
793
794 /**
795 * Registers the blur event for the search widgets input.
796 */
797 static registerSearchWidgetsBlur() {
798 if (VBOCore.options.active_listeners.hasOwnProperty('registerSearchWidgetsBlur')) {
799 // listener is already registered
800 return true;
801 }
802
803 $(VBOCore.options.panel_opts.search_selector).on('blur', function(e) {
804 if (e && e.relatedTarget) {
805 if (e.relatedTarget.classList.contains(VBOCore.options.panel_opts.addw_selector.replace('.', ''))) {
806 // add new widget was clicked, abort hiding process or click event won't fire on target element
807 return;
808 }
809 }
810 var keyword = $(this).val();
811 if (!keyword.length) {
812 // hide add-widgets container
813 $(VBOCore.options.panel_opts.addws_selector).hide();
814 }
815 });
816
817 // register flag for listener active
818 VBOCore.options.active_listeners['registerSearchWidgetsBlur'] = 1;
819 }
820
821 /**
822 * Removes the blur event handler for the search widgets input.
823 */
824 static unregisterSearchWidgetsBlur() {
825 if (!VBOCore.options.active_listeners.hasOwnProperty('registerSearchWidgetsBlur')) {
826 // nothing to unregister
827 return true;
828 }
829
830 $(VBOCore.options.panel_opts.search_selector).off('blur');
831
832 // delete flag for listener active
833 delete VBOCore.options.active_listeners['registerSearchWidgetsBlur'];
834 }
835
836 /**
837 * Adds a widget identifier to the multitask panel.
838 *
839 * @param string widget_id the widget identifier string to add.
840 */
841 static addWidgetToPanel(widget_id) {
842 if (!VBOCore.options.widget_ajax_uri || !VBOCore.options.panel_opts || !Object.keys(VBOCore.options.panel_opts).length) {
843 throw new Error('Multitask panel options are missing');
844 }
845 // prepend container to panel
846 var widget_classes = [VBOCore.options.panel_opts.wclass_base_sel.replace('.', ''), VBOCore.options.panel_opts.cur_widget_cls];
847 var widget_div = '<div class="' + widget_classes.join(' ') + '" ' + VBOCore.options.panel_opts.addw_data_attr + '="' + widget_id + '" style="display: none;"></div>';
848 var widget_elem = $(widget_div);
849 $(VBOCore.options.panel_opts.actws_selector).prepend(widget_elem);
850
851 // always hide add-widgets container
852 $(VBOCore.options.panel_opts.addws_selector).hide();
853
854 // trigger debounced map saving event
855 VBOCore.emitMultitaskEvent();
856
857 // register listener for input search blur
858 VBOCore.registerSearchWidgetsBlur();
859
860 // render widget
861 var call_method = 'render';
862 VBOCore.doAjax(
863 VBOCore.options.widget_ajax_uri,
864 {
865 widget_id: widget_id,
866 call: call_method,
867 vbo_page: VBOCore.options.current_page,
868 vbo_uri: VBOCore.options.current_page_uri,
869 multitask: 1,
870 },
871 (response) => {
872 // display widgets editing button
873 VBOCore.toggleWidgetsPanelEditing(true);
874 // parse response
875 try {
876 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
877 if (obj_res.hasOwnProperty(call_method)) {
878 // populate widget HTML content and display it
879 widget_elem.html(obj_res[call_method]).fadeIn();
880
881 // always scroll active widgets list to top
882 $(VBOCore.options.panel_opts.actws_selector).scrollTop(0);
883
884 // register detach widget button
885 setTimeout(() => {
886 let detach_elem = $('<div></div>').addClass(VBOCore.options.panel_opts.dtcwidget_class).html(VBOCore.options.panel_opts.dtcwidget_icn);
887 let detach_to_target = widget_elem.find('.' + VBOCore.options.panel_opts.dtctarget_class);
888 if (detach_to_target.length) {
889 // move detach wrapper onto the target (widget head)
890 detach_elem.prependTo(detach_to_target);
891 }
892 // register detach action
893 detach_elem.on('click', function() {
894 // detach widget, meaning do a modal rendering
895 VBOCore.renderModalWidget(widget_id);
896 });
897 }, 500);
898 } else {
899 console.error('Unexpected JSON response', obj_res);
900 }
901 } catch(err) {
902 console.error('could not parse JSON response', err, response);
903 }
904 },
905 (error) => {
906 console.error(error.responseText);
907 }
908 );
909 }
910
911 /**
912 * Toggles the edit mode of the multitask widgets panel.
913 *
914 * @param bool added true if a widget was just added, false if it was just removed.
915 */
916 static toggleWidgetsPanelEditing(added) {
917 // check if we are currently editing
918 var is_editing = ($('.' + VBOCore.options.panel_opts.editmode_class).length > 0);
919
920 // the triggerer button
921 var triggerer = $(VBOCore.options.panel_opts.editw_selector);
922
923 // check added action status
924 if (added === true) {
925 // show button for edit mode
926 triggerer.show();
927 return;
928 }
929
930 // grab all widgets
931 var editing_widgets = $(VBOCore.options.panel_opts.wclass_base_sel);
932
933 if (added === false) {
934 if (!editing_widgets.length) {
935 // hide button for edit mode after removing the last widget
936 triggerer.removeClass(VBOCore.options.panel_opts.editw_selector.substr(1) + '-active').hide();
937 }
938 return;
939 }
940
941 if (is_editing) {
942 // deactivate editing mode
943 editing_widgets.removeClass(VBOCore.options.panel_opts.editmode_class);
944 $('.' + VBOCore.options.panel_opts.rmwidget_class).remove();
945 // toggle triggerer button active class
946 triggerer.removeClass(VBOCore.options.panel_opts.editw_selector.substr(1) + '-active');
947 } else {
948 // activate editing mode by looping through all widgets
949 editing_widgets.each(function() {
950 // build remove-widget element
951 var rm_widget = $('<div></div>').addClass(VBOCore.options.panel_opts.rmwidget_class).on('click', function() {
952 VBOCore.removeWidgetFromPanel(this);
953 }).html(VBOCore.options.panel_opts.rmwidget_icn);
954 // add editing class and prepend removing element
955 $(this).addClass(VBOCore.options.panel_opts.editmode_class).prepend(rm_widget);
956 });
957 // toggle triggerer button active class
958 triggerer.addClass(VBOCore.options.panel_opts.editw_selector.substr(1) + '-active');
959 }
960 }
961
962 /**
963 * Handles the removal of a widget from the multitask panel.
964 *
965 * @param object element
966 */
967 static removeWidgetFromPanel(element) {
968 if (!element) {
969 console.error('Invalid widget element to remove', element);
970 return false;
971 }
972 var widget_cont = $(element).parent(VBOCore.options.panel_opts.wclass_base_sel);
973 if (!widget_cont || !widget_cont.length) {
974 console.error('Could not find widget container to remove', element);
975 return false;
976 }
977 var widget_id = widget_cont.attr(VBOCore.options.panel_opts.addw_data_attr);
978 if (!widget_id || !widget_id.length) {
979 console.error('Empty widget id to remove', element);
980 return false;
981 }
982 // find the index of the widget to remove in the panel
983 var widget_index = $(VBOCore.options.panel_opts.wclass_base_sel).index(widget_cont);
984 if (widget_index < 0) {
985 console.error('Empty widget index to remove', widget_cont);
986 return false;
987 }
988 // make sure the index in the array matches the id
989 if (!VBOCore.options.admin_widgets.hasOwnProperty(widget_index) || VBOCore.options.admin_widgets[widget_index]['id'] != widget_id) {
990 console.error('Unmatching widget index or id', VBOCore.options.admin_widgets, widget_index, widget_id);
991 return false;
992 }
993 // remove this widget from the array
994 VBOCore.options.admin_widgets.splice(widget_index, 1);
995
996 // remove element from document
997 widget_cont.remove();
998
999 // check widgets editing button status
1000 VBOCore.toggleWidgetsPanelEditing(false);
1001
1002 // trigger debounced map saving event
1003 VBOCore.emitMultitaskEvent();
1004
1005 if (!VBOCore.options.admin_widgets.length) {
1006 // unregister listener for input search blur
1007 VBOCore.unregisterSearchWidgetsBlur();
1008 }
1009 }
1010
1011 /**
1012 * Emits an event related to the multitask features or a custom event, with optional data.
1013 */
1014 static emitMultitaskEvent(ev_name, ev_data) {
1015 var def_ev_name = VBOCore.options.multitask_save_event;
1016 if (typeof ev_name === 'string') {
1017 def_ev_name = ev_name;
1018 }
1019
1020 if (typeof ev_data !== 'undefined' && ev_data) {
1021 // trigger the custom event
1022 document.dispatchEvent(new CustomEvent(def_ev_name, {bubbles: true, detail: ev_data}));
1023 return;
1024 }
1025
1026 // trigger the event
1027 document.dispatchEvent(new Event(def_ev_name));
1028 }
1029
1030 /**
1031 * Proxy for dispatching an event to the document with optional data.
1032 */
1033 static emitEvent(ev_name, ev_data) {
1034 if (typeof ev_name !== 'string' || !ev_name.length) {
1035 return;
1036 }
1037
1038 return VBOCore.emitMultitaskEvent(ev_name, ev_data);
1039 }
1040
1041 /**
1042 * Attempts to save the multitask widgets for this page.
1043 */
1044 static saveMultitasking() {
1045 // gather the list of active widgets
1046 var active_widgets = [];
1047 var cur_admin_widgets = [];
1048 $(VBOCore.options.panel_opts.actws_selector).find(VBOCore.options.panel_opts.wclass_base_sel).each(function() {
1049 var widget_id = $(this).attr(VBOCore.options.panel_opts.addw_data_attr);
1050 if (widget_id && widget_id.length) {
1051 // push id in list
1052 active_widgets.push(widget_id);
1053 // push object with dummy name for global widgets
1054 cur_admin_widgets.push({
1055 id: widget_id,
1056 name: widget_id,
1057 });
1058 }
1059 });
1060
1061 // update multitask widgets map for this page
1062 VBOCore.doAjax(
1063 VBOCore.options.multitask_ajax_uri,
1064 {
1065 call: 'updateMultitaskingMap',
1066 call_args: [
1067 VBOCore.options.current_page,
1068 active_widgets,
1069 0
1070 ],
1071 },
1072 (response) => {
1073 try {
1074 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1075 if (obj_res.hasOwnProperty('result') && obj_res['result']) {
1076 // set current widgets
1077 VBOCore.setOptions({
1078 admin_widgets: cur_admin_widgets
1079 });
1080 } else {
1081 console.error('Unexpected or invalid JSON response', response);
1082 }
1083 } catch(err) {
1084 console.error('could not parse JSON response', err, response);
1085 }
1086 },
1087 (error) => {
1088 console.error(error.responseText);
1089 }
1090 );
1091 }
1092
1093 /**
1094 * Sets up the browser notifications within the multitask panel, if supported.
1095 */
1096 static setupNotifications() {
1097 if (!('Notification' in window)) {
1098 // browser does not support notifications
1099 $(VBOCore.options.panel_opts.notif_selector).hide();
1100 return false;
1101 }
1102
1103 if (Notification.permission && Notification.permission === 'granted') {
1104 // permissions were granted already
1105 $(VBOCore.options.panel_opts.notif_selector)
1106 .addClass(VBOCore.options.panel_opts.notif_on_class)
1107 .attr('title', VBOCore.options.tn_texts.notifs_enabled)
1108 .on('click', function() {
1109 // show congratulations notification
1110 VBOCore.dispatchCongratulations();
1111
1112 // attempt to update the service worker installation
1113 VBOCore.installServiceWorker(true).then(() => {
1114 // all good
1115 }).catch((error) => {
1116 console.error(error);
1117 });
1118 });
1119 return true;
1120 }
1121
1122 // notifications supported, but perms not granted
1123 $(VBOCore.options.panel_opts.notif_selector)
1124 .addClass(VBOCore.options.panel_opts.notif_off_class)
1125 .attr('title', VBOCore.options.tn_texts.notifs_disabled);
1126
1127 // register click-event listener on button to enable notifications
1128 $(VBOCore.options.panel_opts.notif_selector).on('click', function() {
1129 VBOCore.requestNotifPerms();
1130 });
1131
1132 // subscribe to the multitask-panel-open event to show the status of the notifications
1133 document.addEventListener(VBOCore.multitask_open_event, function() {
1134 if (VBOCore.notificationsEnabled() === false) {
1135 // add "shaking" class to notifications button
1136 $(VBOCore.options.panel_opts.notif_selector).addClass('shaking');
1137 }
1138 });
1139
1140 // subscribe to the multitask-panel-close event to update the status of the notifications
1141 document.addEventListener(VBOCore.multitask_close_event, function() {
1142 // always remove "shaking" class from notifications button
1143 $(VBOCore.options.panel_opts.notif_selector).removeClass('shaking');
1144 });
1145 }
1146
1147 /**
1148 * Sets up the browser notifications within the given selector, if supported.
1149 *
1150 * @param string selector the element query selector.
1151 */
1152 static suggestNotifications(selector) {
1153 if (!selector) {
1154 return false;
1155 }
1156
1157 if (!('Notification' in window)) {
1158 // browser does not support notifications
1159 $(selector).hide();
1160 return false;
1161 }
1162
1163 if (Notification.permission && Notification.permission === 'granted') {
1164 // permissions were granted already
1165 $(selector)
1166 .addClass(VBOCore.options.panel_opts.notif_on_class)
1167 .attr('title', VBOCore.options.tn_texts.notifs_enabled)
1168 .on('click', function() {
1169 VBOCore.dispatchCongratulations();
1170 });
1171 return true;
1172 }
1173
1174 // notifications supported, but perms not granted
1175 $(selector)
1176 .addClass(VBOCore.options.panel_opts.notif_off_class)
1177 .attr('title', VBOCore.options.tn_texts.notifs_disabled);
1178
1179 // register click-event listener on button to enable notifications
1180 $(selector).on('click', function() {
1181 VBOCore.requestNotifPerms(selector);
1182 });
1183
1184 // add "shaking" class to make the selector more appealing
1185 $(selector).addClass('shaking');
1186
1187 // remove the "shaking" class after some time
1188 setTimeout(() => {
1189 $(selector).removeClass('shaking');
1190 }, 2000);
1191 }
1192
1193 /**
1194 * Tells whether the notifications are enabled, disabled, not supported.
1195 */
1196 static notificationsEnabled() {
1197 if (!('Notification' in window)) {
1198 // not supported
1199 return null;
1200 }
1201
1202 if (Notification.permission && Notification.permission === 'granted') {
1203 // enabled
1204 return true;
1205 }
1206
1207 // disabled
1208 return false;
1209 }
1210
1211 /**
1212 * Attempts to request the notifications permissions to the browser.
1213 * For security reasons, this should run upon a user gesture (click).
1214 *
1215 * @param string selector optional element query selector.
1216 */
1217 static requestNotifPerms(selector) {
1218 if (!('Notification' in window)) {
1219 // browser does not support notifications
1220 return false;
1221 }
1222
1223 // run permissions request in a try-catch statement to support all browsers
1224 try {
1225 // handle promise-based version to request permissions
1226 Notification.requestPermission().then((permission) => {
1227 VBOCore.handleNotifPerms(permission, selector);
1228 });
1229 } catch(e) {
1230 // run the callback-based version
1231 Notification.requestPermission(function(permission) {
1232 VBOCore.handleNotifPerms(permission, selector);
1233 });
1234 }
1235 }
1236
1237 /**
1238 * Handles the notifications permission response (from callback or promise resolved).
1239 *
1240 * @param any permission permission status string or NotificationPermission object.
1241 * @param string selector optional element query selector.
1242 */
1243 static handleNotifPerms(permission, selector) {
1244 // check the permission status from the Notification object interface
1245 if ((Notification.permission && Notification.permission === 'granted') || (typeof permission === 'string' && permission === 'granted')) {
1246 // permissions granted!
1247 $((selector || VBOCore.options.panel_opts.notif_selector))
1248 .removeClass(VBOCore.options.panel_opts.notif_off_class)
1249 .addClass(VBOCore.options.panel_opts.notif_on_class)
1250 .attr('title', VBOCore.options.tn_texts.notifs_enabled);
1251
1252 // dispatch an immediate notification to congratulate with the activation
1253 VBOCore.dispatchCongratulations();
1254
1255 return true;
1256 } else {
1257 // permissions denied :(
1258 $((selector || VBOCore.options.panel_opts.notif_selector))
1259 .removeClass(VBOCore.options.panel_opts.notif_on_class)
1260 .addClass(VBOCore.options.panel_opts.notif_off_class)
1261 .attr('title', VBOCore.options.tn_texts.notifs_disabled);
1262
1263 // show alert message
1264 console.error('Permission denied for enabling browser notifications', permission);
1265 alert(VBOCore.options.tn_texts.notifs_disabled_help);
1266 }
1267
1268 return false;
1269 }
1270
1271 /**
1272 * Dispatches an immediate notification with a congratulations text.
1273 *
1274 * @return void
1275 */
1276 static dispatchCongratulations() {
1277 try {
1278 let notif = new Notification(VBOCore.options.tn_texts.congrats, {
1279 body: VBOCore.options.tn_texts.notifs_enabled,
1280 tag: 'vbo_notification_congrats',
1281 silent: false
1282 });
1283 } catch(error) {
1284 alert(error);
1285 }
1286 }
1287
1288 /**
1289 * Given a date-time string, returns a Date object representation.
1290 *
1291 * @param string dtime_str the date-time string in "Y-m-d H:i:s" format.
1292 */
1293 static getDateTimeObject(dtime_str) {
1294 // instantiate a new date object
1295 var date_obj = new Date();
1296
1297 // parse date-time string
1298 let dtime_parts = dtime_str.split(' ');
1299 let date_parts = dtime_parts[0].split('-');
1300 if (dtime_parts.length != 2 || date_parts.length != 3) {
1301 // invalid military format
1302 return date_obj;
1303 }
1304 let time_parts = dtime_parts[1].split(':');
1305
1306 // set accurate date-time values
1307 date_obj.setFullYear(date_parts[0]);
1308 date_obj.setMonth((parseInt(date_parts[1]) - 1));
1309 date_obj.setDate(parseInt(date_parts[2]));
1310 date_obj.setHours(parseInt(time_parts[0]));
1311 date_obj.setMinutes(parseInt(time_parts[1]));
1312 date_obj.setSeconds(0);
1313 date_obj.setMilliseconds(0);
1314
1315 // return the accurate date object
1316 return date_obj;
1317 }
1318
1319 /**
1320 * Given a list of schedules, enqueues notifications to watch.
1321 *
1322 * @param array|object schedules list of or one notification object(s).
1323 *
1324 * @return bool
1325 */
1326 static enqueueNotifications(schedules) {
1327 if (!Array.isArray(schedules) || !schedules.length) {
1328 if (typeof schedules === 'object' && schedules.hasOwnProperty('dtime')) {
1329 // convert the single schedule to an array
1330 schedules = [schedules];
1331 } else {
1332 // invalid argument passed
1333 return false;
1334 }
1335 }
1336
1337 for (var i in schedules) {
1338 if (!schedules.hasOwnProperty(i) || typeof schedules[i] !== 'object') {
1339 continue;
1340 }
1341 VBOCore.notifications.push(schedules[i]);
1342 }
1343
1344 // setup the timeouts to schedule the notifications
1345 return VBOCore.scheduleNotifications();
1346 }
1347
1348 /**
1349 * Schedule the trigger timings for each notification.
1350 */
1351 static scheduleNotifications() {
1352 if (!VBOCore.notifications.length) {
1353 // no notifications to be scheduled
1354 return false;
1355 }
1356 if (VBOCore.notificationsEnabled() !== true) {
1357 // notifications not enabled
1358 console.info('Browser notifications disabled or unsupported.');
1359 return false;
1360 }
1361
1362 // gather current date-timing information
1363 const now_date = new Date();
1364 const now_time = now_date.getTime();
1365
1366 // parse all notifications to schedule the timers if not set
1367 for (let i = 0; i < VBOCore.notifications.length; i++) {
1368 let notif = VBOCore.notifications[i];
1369
1370 if (typeof notif !== 'object' || !notif.hasOwnProperty('dtime')) {
1371 // invalid notification object, unset it
1372 VBOCore.notifications.splice(i, 1);
1373 continue;
1374 }
1375
1376 // check if timer has been set
1377 if (!notif.hasOwnProperty('id_timer')) {
1378 // estimate trigger timing
1379 let in_ms = 0;
1380 // check for imminent notifications
1381 if (typeof notif.dtime === 'string' && notif.dtime.indexOf('now') >= 0) {
1382 // imminent ones will be delayed by one second
1383 in_ms = 1000;
1384 } else {
1385 // check overdue date-time (notif.dtime can also be a Date object instance)
1386 let nexp = VBOCore.getDateTimeObject(notif.dtime);
1387 in_ms = nexp.getTime() - now_time;
1388 }
1389 if (in_ms > 0) {
1390 // schedule notification trigger
1391 let id_timer = setTimeout(() => {
1392 VBOCore.dispatchNotification(notif);
1393 }, in_ms);
1394 // set timer on notification object
1395 VBOCore.notifications[i]['id_timer'] = id_timer;
1396 }
1397 }
1398 }
1399
1400 return true;
1401 }
1402
1403 /**
1404 * Deregister all scheduled notifications.
1405 */
1406 static unscheduleNotifications() {
1407 if (!VBOCore.notifications.length) {
1408 // no notifications scheduled
1409 return false;
1410 }
1411
1412 for (let i = 0; i < VBOCore.notifications.length; i++) {
1413 let notif = VBOCore.notifications[i];
1414
1415 if (typeof notif === 'object' && notif.hasOwnProperty('id_timer')) {
1416 // unset timeout for this notification
1417 clearTimeout(notif['id_timer']);
1418 }
1419 }
1420
1421 // reset pool
1422 VBOCore.notifications = [];
1423 }
1424
1425 /**
1426 * Update or delete a previously scheduled notification.
1427 *
1428 * @param object match_props map of properties to match.
1429 * @param string|number newdtime the new date time to schedule (0 for deleting).
1430 *
1431 * @return null|bool true only if a notification matched.
1432 */
1433 static updateNotification(match_props, newdtime) {
1434 if (!VBOCore.notifications.length) {
1435 // no notifications set, terminate
1436 return null;
1437 }
1438
1439 if (typeof match_props !== 'object') {
1440 // no properties to match the notification
1441 return null;
1442 }
1443
1444 // gather current date-timing information
1445 const now_date = new Date();
1446 const now_time = now_date.getTime();
1447
1448 // parse all notifications scheduled
1449 for (let i = 0; i < VBOCore.notifications.length; i++) {
1450 let notif = VBOCore.notifications[i];
1451
1452 let all_matched = true;
1453 let to_matching = false;
1454 for (let prop in match_props) {
1455 if (!match_props.hasOwnProperty(prop)) {
1456 continue;
1457 }
1458 to_matching = true;
1459 if (!notif.hasOwnProperty(prop) || notif[prop] != match_props[prop]) {
1460 all_matched = false;
1461 break;
1462 }
1463 }
1464
1465 if (all_matched && to_matching) {
1466 // notification object found
1467 if (notif.hasOwnProperty('id_timer')) {
1468 // unset previous timeout for this notification
1469 clearTimeout(notif['id_timer']);
1470 }
1471 // update or delete scheduled notification
1472 if (newdtime === 0) {
1473 // delete notification from queue
1474 VBOCore.notifications.splice(i, 1);
1475 } else {
1476 // update timing scheduler
1477 let in_ms = 0;
1478 // check for imminent notifications
1479 if (typeof newdtime === 'string' && newdtime.indexOf('now') >= 0) {
1480 // imminent ones will be delayed by one second
1481 in_ms = 1000;
1482 } else {
1483 // check overdue date-time (newdtime can also be a Date object instance)
1484 let nexp = VBOCore.getDateTimeObject(newdtime);
1485 in_ms = nexp.getTime() - now_time;
1486 }
1487 if (in_ms > 0) {
1488 // schedule notification trigger
1489 let id_timer = setTimeout(() => {
1490 VBOCore.dispatchNotification(notif);
1491 }, in_ms);
1492 // set timer on notification object
1493 VBOCore.notifications[i]['id_timer'] = id_timer;
1494 }
1495 // update date-time value on notification object
1496 VBOCore.notifications[i]['dtime'] = newdtime;
1497 }
1498
1499 // terminate parsing and return true
1500 return true;
1501 }
1502 }
1503
1504 // notification object not found
1505 return false;
1506 }
1507
1508 /**
1509 * Dispatch the notification object.
1510 * Expected notification properties:
1511 *
1512 * {
1513 * id: number
1514 * type: string
1515 * dtime: string|Date
1516 * build_url: string|null
1517 * }
1518 *
1519 * @param object notif the notification object.
1520 */
1521 static dispatchNotification(notif) {
1522 if (typeof notif !== 'object') {
1523 return false;
1524 }
1525
1526 // subscribe to building notification data
1527 VBOCore.buildNotificationData(notif).then((data) => {
1528 // dispatch the notification
1529
1530 // check if the click event should be registered
1531 let func_nodes;
1532 if (data.onclick && typeof data.onclick === 'string') {
1533 let callback_parts = data.onclick.split('.');
1534 while (callback_parts.length) {
1535 // compose window static method string to avoid using eval()
1536 let tmp = callback_parts.shift();
1537 if (!func_nodes) {
1538 func_nodes = window[tmp];
1539 } else {
1540 func_nodes = func_nodes[tmp];
1541 }
1542 }
1543 }
1544
1545 // prepare properties to delete the notification from queue
1546 let match_props = {};
1547 for (let prop in notif) {
1548 if (!notif.hasOwnProperty(prop) || prop == 'id_timer') {
1549 continue;
1550 }
1551 match_props[prop] = notif[prop];
1552 }
1553
1554 // check browser Notifications API
1555 if (VBOCore.notificationsEnabled() !== true) {
1556 // notifications not enabled, fallback to toast message
1557 let toast_notif_data = {
1558 title: data.title,
1559 body: data.message,
1560 icon: data.icon,
1561 delay: {
1562 min: 6000,
1563 max: 20000,
1564 tolerance: 4000,
1565 },
1566 action: () => {
1567 VBOToast.dispose(true);
1568 },
1569 sound: VBOCore.options.notif_audio_url
1570 };
1571 if (func_nodes) {
1572 toast_notif_data.action = function() {
1573 func_nodes(data);
1574 VBOToast.dispose(true);
1575 };
1576 } else if (typeof data.onclick === 'function') {
1577 toast_notif_data.action = function() {
1578 data.onclick.call(data);
1579 VBOToast.dispose(true);
1580 };
1581 }
1582 VBOToast.enqueue(new VBOToastMessage(toast_notif_data));
1583
1584 // delete dispatched notification from queue
1585 VBOCore.updateNotification(match_props, 0);
1586
1587 return;
1588 }
1589
1590 // use the browser's native Notifications API
1591 let browser_notif = new Notification(data.title, {
1592 body: data.message,
1593 icon: data.icon,
1594 tag: 'vbo_notification',
1595 silent: false
1596 });
1597
1598 // check if support for Web App badge is available
1599 if (typeof navigator.setAppBadge !== 'undefined') {
1600 navigator.setAppBadge(1);
1601 }
1602
1603 browser_notif.addEventListener('click', (e) => {
1604 // check if support for Web App badge is available
1605 if (typeof navigator.setAppBadge !== 'undefined') {
1606 navigator.clearAppBadge();
1607 }
1608 // close the notification
1609 e.target.close();
1610 });
1611
1612 if (func_nodes) {
1613 // register notification click event
1614 browser_notif.addEventListener('click', () => {
1615 func_nodes(data);
1616 });
1617 } else if (typeof data.onclick === 'function') {
1618 browser_notif.addEventListener('click', () => {
1619 data.onclick.call(data);
1620 });
1621 }
1622
1623 // delete dispatched notification from queue
1624 VBOCore.updateNotification(match_props, 0);
1625
1626 }).catch((error) => {
1627 console.error(error);
1628 });
1629 }
1630
1631 /**
1632 * Asynchronous build of the notification data object for dispatch.
1633 * Minimum expected notification display data properties:
1634 *
1635 * {
1636 * title: string
1637 * message: string
1638 * icon: string
1639 * onclick: function
1640 * }
1641 *
1642 * @param object notif the scheduled notification object.
1643 *
1644 * @return Promise
1645 */
1646 static buildNotificationData(notif) {
1647 return new Promise((resolve, reject) => {
1648 // notification object validation
1649 if (typeof notif !== 'object') {
1650 reject('Invalid notification object');
1651 return;
1652 }
1653
1654 if (!notif.hasOwnProperty('build_url') || !notif.build_url) {
1655 // building callback not necessary
1656 if (!notif.title && !notif.message) {
1657 reject('Unexpected notification object');
1658 return;
1659 }
1660 // we expect the notification to be built already
1661 resolve(notif);
1662 return;
1663 }
1664
1665 // build the notification data
1666 VBOCore.doAjax(
1667 notif.build_url,
1668 {
1669 payload: JSON.stringify(notif)
1670 },
1671 (response) => {
1672 // parse response
1673 try {
1674 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1675 if (obj_res.hasOwnProperty('title')) {
1676 resolve(obj_res);
1677 } else {
1678 reject('Unexpected JSON response');
1679 }
1680 } catch(err) {
1681 reject('could not parse JSON response');
1682 }
1683 },
1684 (error) => {
1685 reject(error.responseText);
1686 }
1687 );
1688 });
1689 }
1690
1691 /**
1692 * Handle a navigation towards a given URL.
1693 * Common handler for browser notifications click.
1694 *
1695 * @param object data notification display data payload.
1696 */
1697 static handleGoto(data) {
1698 if (typeof data !== 'object' || !data.hasOwnProperty('gotourl') || !data.gotourl) {
1699 console.error('Could not handle the goto operation', data);
1700 return;
1701 }
1702
1703 if (typeof data.openWindow !== 'undefined' || typeof document === 'undefined') {
1704 // open a new window
1705 window.open(data.gotourl);
1706 return;
1707 }
1708
1709 // redirect
1710 document.location.href = data.gotourl;
1711 }
1712
1713 /**
1714 * Opens a new window with the URI to render the Push notification data.
1715 *
1716 * @param object options admin widget rendering options to bind.
1717 *
1718 * @return void
1719 */
1720 static openAdminPushURI(options) {
1721 if (typeof options !== 'object' || !options) {
1722 throw new Error('Unable to build Admin Push URI');
1723 }
1724
1725 let vbo_admin_push_uri = VBOCore.options.root_uri;
1726
1727 if (VBOCore.options.cms === 'wordpress') {
1728 vbo_admin_push_uri += 'wp-admin/admin.php?page=vikbooking';
1729 } else {
1730 vbo_admin_push_uri += 'administrator/index.php?option=com_vikbooking';
1731 }
1732
1733 // build Push notification payload for rendering
1734 let push_data = {
1735 type: options.type || '',
1736 title: options.title || '',
1737 message: options.message || '',
1738 content: options,
1739 };
1740
1741 // open page in a new window
1742 window.open(vbo_admin_push_uri + '&push_notification=' + btoa(JSON.stringify(push_data)), '_blank');
1743 }
1744
1745 /**
1746 * Handle the display of a notification through a widget.
1747 * Common handler for browser notifications displayed through
1748 * a widget modal rendering.
1749 *
1750 * @param object data notification display data payload.
1751 * @param object options optional extra options for the admin widget.
1752 * @param boolean is_push whether we are coming from a Push notification.
1753 *
1754 * @return void
1755 */
1756 static handleDisplayWidgetNotification(data, options, is_push) {
1757 try {
1758 // validate handler
1759 if (!VBOCore.options.is_vbo && !VBOCore.options.is_vcm) {
1760 throw new Error('Unable to handle the notification display from the current page');
1761 }
1762
1763 // validate payload
1764 if (typeof data !== 'object' || !data.hasOwnProperty('widget_id') || !data['widget_id']) {
1765 throw new Error('Invalid widget payload');
1766 }
1767
1768 // parse handler
1769 if (VBOCore.options.is_vcm || (is_push && !VBOCore.options.is_vbo)) {
1770 // the operation is handled by VCM (or by an external admin resource, if it's a Push notification)
1771 VBOCore.loadAdminWidgetAssets(data).then((assets) => {
1772 // append assets to DOM
1773 assets.forEach((asset) => {
1774 if (!$('link#' + asset['id']).length) {
1775 $('head').append('<link rel="stylesheet" id="' + asset['id'] + '" href="' + asset['href'] + '" media="all" />');
1776 }
1777 });
1778 // widget modal rendering handled by VCM (or an external admin resource)
1779 let modal_data = VBOCore.renderModalWidget(data['widget_id'], data, options, false);
1780 if (modal_data.hasOwnProperty('dismissed_event')) {
1781 // register event to unload all assets
1782 document.addEventListener(modal_data['dismissed_event'], () => {
1783 assets.forEach((asset) => {
1784 if ($('link#' + asset['id']).length) {
1785 $('link#' + asset['id']).remove();
1786 }
1787 });
1788 });
1789 }
1790 }).catch((error) => {
1791 throw new Error(error);
1792 });
1793 } else {
1794 // widget modal rendering handled by Vik Booking
1795 VBOCore.renderModalWidget(data['widget_id'], data, options, false);
1796 }
1797 } catch(e) {
1798 if (is_push && options.type) {
1799 // fallback to opening a new window to render the clicked Push notification
1800 VBOCore.openAdminPushURI(options);
1801 } else {
1802 // fallback to a regular link opening, if set
1803 VBOCore.handleGoto(data);
1804 }
1805 }
1806 }
1807
1808 /**
1809 * Asynchronous loading of CSS assets required to render
1810 * an admin widget outside Vik Booking.
1811 *
1812 * @param object data the optional widget payload data.
1813 *
1814 * @return Promise
1815 */
1816 static loadAdminWidgetAssets(data) {
1817 return new Promise((resolve, reject) => {
1818 // the remote assets URI must be set
1819 if (!VBOCore.options.assets_ajax_uri) {
1820 reject('Missing remote assets URI');
1821 return;
1822 }
1823
1824 if (typeof data !== 'object') {
1825 data = {};
1826 }
1827
1828 // make the request
1829 VBOCore.doAjax(
1830 VBOCore.options.assets_ajax_uri,
1831 data,
1832 (response) => {
1833 // parse response
1834 try {
1835 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1836 if (!Array.isArray(obj_res)) {
1837 reject('Unexpected JSON response');
1838 }
1839 resolve(obj_res);
1840 } catch(err) {
1841 reject('could not parse JSON response');
1842 }
1843 },
1844 (error) => {
1845 reject(error.responseText);
1846 }
1847 );
1848 });
1849 }
1850
1851 /**
1852 * Register the latest data to watch for the preloaded admin widgets.
1853 *
1854 * @param object watch_data
1855 */
1856 static registerWatchData(watch_data) {
1857 if (typeof watch_data !== 'object' || watch_data == null) {
1858 VBOCore.widgets_watch_data = null;
1859 return false;
1860 }
1861
1862 // set watch-data map
1863 VBOCore.widgets_watch_data = watch_data;
1864
1865 // schedule watching interval
1866 if (VBOCore.watch_data_interval == null) {
1867 VBOCore.watch_data_interval = window.setInterval(VBOCore.watchWidgetsData, 60000);
1868 }
1869
1870 // set up broadcast channels to connect all browsing contexts
1871 if (typeof BroadcastChannel !== 'undefined') {
1872 // set up watch-data broadcast channel
1873 if (!VBOCore.broadcast_watch_data) {
1874 // start broadcast channel
1875 VBOCore.broadcast_watch_data = new BroadcastChannel('vikbooking_watch_data');
1876
1877 // register to the "on broadcast message received" event
1878 VBOCore.broadcast_watch_data.onmessage = (event) => {
1879 if (event && event.data) {
1880 // update watch data map for next schedule to avoid dispatching duplicate notifications
1881 VBOCore.widgets_watch_data = event.data;
1882 }
1883 };
1884 }
1885
1886 // set up push-data broadcast channel
1887 if (!VBOCore.broadcast_push_data) {
1888 // start broadcast channel
1889 VBOCore.broadcast_push_data = new BroadcastChannel('vikbooking_push_data');
1890
1891 // register to the "on broadcast message received" event
1892 VBOCore.broadcast_push_data.onmessage = (event) => {
1893 if (event && event.data) {
1894 // update pushed data map for the next watching event
1895 VBOCore.widgets_pushed_data.push(event.data);
1896 }
1897 };
1898 }
1899
1900 // set up watch-events broadcast channel
1901 if (!VBOCore.broadcast_watch_events) {
1902 // start broadcast channel
1903 VBOCore.broadcast_watch_events = new BroadcastChannel('vikbooking_watch_events');
1904
1905 // register to the "on broadcast message received" event
1906 VBOCore.broadcast_watch_events.onmessage = (event) => {
1907 if (event && event.data && typeof event.data === 'object') {
1908 // scan and dispatch the events data received
1909 let events_data = event.data;
1910 for (let ev_name in events_data) {
1911 if (!events_data.hasOwnProperty(ev_name)) {
1912 continue;
1913 }
1914 // emit the event
1915 VBOCore.emitEvent(ev_name, events_data[ev_name]);
1916 }
1917 }
1918 };
1919 }
1920 }
1921 }
1922
1923 /**
1924 * Periodic widgets data watching for new events.
1925 */
1926 static watchWidgetsData() {
1927 if (typeof VBOCore.widgets_watch_data !== 'object' || VBOCore.widgets_watch_data == null) {
1928 return;
1929 }
1930
1931 // call on new events
1932 VBOCore.doAjax(
1933 VBOCore.options.watchdata_ajax_uri,
1934 {
1935 watch_data: JSON.stringify(VBOCore.widgets_watch_data),
1936 pushed_data: JSON.stringify(VBOCore.widgets_pushed_data),
1937 },
1938 (response) => {
1939 try {
1940 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1941 if (obj_res.hasOwnProperty('watch_data')) {
1942 // update watch data map for next schedule
1943 VBOCore.widgets_watch_data = obj_res['watch_data'];
1944
1945 // check for notifications
1946 if (obj_res.hasOwnProperty('notifications') && Array.isArray(obj_res['notifications'])) {
1947 // dispatch notifications
1948 for (var i = 0; i < obj_res['notifications'].length; i++) {
1949 VBOCore.dispatchNotification(obj_res['notifications'][i]);
1950 }
1951
1952 // post message onto broadcast channel for any other browsing context
1953 if (VBOCore.broadcast_watch_data && obj_res['notifications'].length) {
1954 // this will avoid dispatching duplicate notifications
1955 VBOCore.broadcast_watch_data.postMessage(VBOCore.widgets_watch_data);
1956 }
1957 }
1958
1959 // check for notification events to dispatch
1960 if (obj_res.hasOwnProperty('events') && Array.isArray(obj_res['events'])) {
1961 // parse and dispatch all the events
1962 obj_res['events'].forEach((events_data) => {
1963 if (typeof events_data !== 'object') {
1964 return;
1965 }
1966
1967 // parse all the events for this widget watched data
1968 for (let ev_name in events_data) {
1969 if (!events_data.hasOwnProperty(ev_name)) {
1970 continue;
1971 }
1972 // emit the event
1973 VBOCore.emitEvent(ev_name, events_data[ev_name]);
1974 }
1975
1976 // post message onto broadcast channel for any other browsing context
1977 if (VBOCore.broadcast_watch_events) {
1978 // this will trigger the events on any other browsing context
1979 VBOCore.broadcast_watch_events.postMessage(events_data);
1980 }
1981 });
1982 }
1983 } else {
1984 console.error('Unexpected or invalid JSON response', response);
1985 }
1986 } catch(err) {
1987 console.error('could not parse JSON response', err, response);
1988 }
1989 },
1990 (error) => {
1991 console.error(error.responseText);
1992 }
1993 );
1994 }
1995
1996 /**
1997 * Widget modal rendering.
1998 *
1999 * @param string widget_id the widget identifier to render.
2000 * @param any data the optional multitask data to inject.
2001 * @param object options optional list of extra options to render the widget with Multitask data.
2002 * @param bool hide_panel if false, the multitask panel elements will remain unchanged.
2003 *
2004 * @return object the multitask data injected object merged with modal options.
2005 */
2006 static renderModalWidget(widget_id, data, options, hide_panel) {
2007 /**
2008 * Adjust arguments for BC with previous ordering between options
2009 * and hide_panel, which used to be inverted. Useful also to shorten
2010 * the way the method can be invoked in case no options are needed.
2011 */
2012 if (typeof options === 'boolean') {
2013 // switch argument values
2014 hide_panel = options;
2015 options = null;
2016 }
2017
2018 if (typeof data !== 'object' || data == null) {
2019 // always treat data as an object
2020 data = {};
2021 }
2022
2023 if (typeof options !== 'object' || options == null) {
2024 // always treat options as an object
2025 options = data._options || {};
2026 }
2027
2028 // build the default widget payload
2029 let modal_js_id = Math.floor(Math.random() * 100000);
2030 let modal_title = options._push && options.title ? options.title : VBOCore.options.tn_texts.admin_widget;
2031 let widget_data = {
2032 _modalRendering: 1,
2033 _modalJsId: modal_js_id,
2034 _modalTitle: modal_title,
2035 _options: options,
2036 };
2037
2038 // merge default payload options with given options
2039 data = Object.assign(widget_data, data);
2040
2041 // define unique modal event names to avoid conflicts
2042 let dismiss_event = 'vbo-dismiss-widget-modal' + modal_js_id;
2043 let loading_event = 'vbo-loading-widget-modal' + modal_js_id;
2044
2045 // define the modal options
2046 let modal_options = {
2047 suffix: 'widget_modal',
2048 extra_class: 'vbo-modal-widget vbo-modal-rounded vbo-modal-tall vbo-modal-nofooter',
2049 title: data._modalTitle,
2050 body_prepend: true,
2051 lock_scroll: true,
2052 draggable: true,
2053 dismiss_event: dismiss_event,
2054 loading_event: loading_event,
2055 loading_body: VBOCore.options.default_loading_body,
2056 dismissed_event: VBOCore.options.widget_modal_dismissed + modal_js_id,
2057 event_data: widget_data,
2058 };
2059
2060 // check if options should rewrite some modal-options
2061 if (options.modal_options) {
2062 modal_options = Object.assign(modal_options, options.modal_options);
2063 }
2064
2065 // display modal
2066 let widget_modal = VBOCore.displayModal(modal_options);
2067
2068 if (hide_panel !== false) {
2069 // blur search widget input, hide multitask panel
2070 $(VBOCore.options.panel_opts.search_selector).trigger('blur');
2071 VBOCore.emitEvent(VBOCore.multitask_shortcut_event);
2072 }
2073
2074 // start loading
2075 VBOCore.emitEvent(loading_event);
2076
2077 // render admin widget
2078 VBOCore.renderAdminWidget(widget_id, data).then((content) => {
2079 // stop loading and append widget content to modal
2080 VBOCore.emitEvent(loading_event);
2081 widget_modal.append(content);
2082
2083 // register an admin menu action for the rendered widget
2084 VBOCore.fetchWidgetDetails(widget_id).then((details) => {
2085 try {
2086 // work on Local Storage to register the widget data
2087 VBOCore.registerAdminMenuAction({
2088 name: details['name'],
2089 href: 'JavaScript: void(0);',
2090 widget: widget_id,
2091 icon: details['icon'],
2092 style: details['style'],
2093 }, 'widgets');
2094 } catch(e) {
2095 console.error(e);
2096 }
2097 }).catch((error) => {
2098 console.error(error);
2099 });
2100 }).catch((error) => {
2101 // dismiss modal and display error
2102 VBOCore.emitEvent(dismiss_event);
2103 alert(error);
2104 });
2105
2106 return Object.assign(data, modal_options);
2107 }
2108
2109 /**
2110 * Renders an admin widget.
2111 *
2112 * @param string widget_id the widget identifier string to add.
2113 * @param any data the optional multitask data to inject.
2114 *
2115 * @return Promise
2116 */
2117 static renderAdminWidget(widget_id, data) {
2118 return new Promise((resolve, reject) => {
2119 if (!VBOCore.options.widget_ajax_uri) {
2120 reject('Could not load admin widget');
2121 return;
2122 }
2123
2124 var call_method = 'render';
2125 VBOCore.doAjax(
2126 VBOCore.options.widget_ajax_uri,
2127 {
2128 widget_id: widget_id,
2129 call: call_method,
2130 vbo_page: VBOCore.options.current_page,
2131 vbo_uri: VBOCore.options.current_page_uri,
2132 multitask: 1,
2133 multitask_data: data,
2134 },
2135 (response) => {
2136 // parse response
2137 try {
2138 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
2139 if (!obj_res.hasOwnProperty(call_method)) {
2140 reject('Unexpected JSON response');
2141 return;
2142 }
2143 resolve(obj_res[call_method]);
2144 } catch(err) {
2145 reject('could not parse JSON response');
2146 }
2147 },
2148 (error) => {
2149 reject(error.responseText);
2150 }
2151 );
2152 });
2153 }
2154
2155 /**
2156 * Fetches the details for a specific widget.
2157 *
2158 * @param string widget_id the widget identifier string to fetch.
2159 *
2160 * @return Promise
2161 *
2162 * @since 1.6.7
2163 */
2164 static fetchWidgetDetails(widget_id) {
2165 return new Promise((resolve, reject) => {
2166 if (!VBOCore.options.widget_ajax_uri) {
2167 reject('Could not load admin widget');
2168 return;
2169 }
2170
2171 var call_method = 'getWidgetDetails';
2172 VBOCore.doAjax(
2173 VBOCore.options.widget_ajax_uri,
2174 {
2175 widget_id: widget_id,
2176 call: call_method,
2177 return: 1,
2178 },
2179 (response) => {
2180 // parse response
2181 try {
2182 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
2183 if (!obj_res.hasOwnProperty(call_method)) {
2184 reject('Unexpected JSON response');
2185 return;
2186 }
2187 resolve(obj_res[call_method]);
2188 } catch(err) {
2189 reject('could not parse JSON response');
2190 }
2191 },
2192 (error) => {
2193 reject(error.responseText);
2194 }
2195 );
2196 });
2197 }
2198
2199 /**
2200 * Helper method used to copy the text of an
2201 * input element within the clipboard.
2202 *
2203 * Clipboard copy will take effect only in case the
2204 * function is handled by a DOM event explicitly
2205 * triggered by the user, such as a "click".
2206 *
2207 * @param any input The input containing the text to copy.
2208 *
2209 * @return Promise
2210 */
2211 static copyToClipboard(input) {
2212 // register and return promise
2213 return new Promise((resolve, reject) => {
2214 // define a fallback function
2215 var fallback = function(input) {
2216 // focus the input
2217 input.focus();
2218 // select the text inside the input
2219 input.select();
2220
2221 try {
2222 // try to copy with shell command
2223 var copy = document.execCommand('copy');
2224
2225 if (copy) {
2226 // copied successfully
2227 resolve(copy);
2228 } else {
2229 // unable to copy
2230 reject(copy);
2231 }
2232 } catch (error) {
2233 // unable to exec the command
2234 reject(error);
2235 }
2236 };
2237
2238 // look for navigator clipboard
2239 if (!navigator || !navigator.clipboard) {
2240 // navigator clipboard not supported, use fallback
2241 fallback(input);
2242 return;
2243 }
2244
2245 // try to copy within the clipboard by using the navigator
2246 navigator.clipboard.writeText((input.val ? input.val() : input.value)).then(() => {
2247 // copied successfully
2248 resolve(true);
2249 }).catch((error) => {
2250 // revert to the fallback
2251 fallback(input);
2252 });
2253 });
2254 }
2255
2256 /**
2257 * Helper method used to display a modal window dinamycally.
2258 *
2259 * @param object options The options to render the modal.
2260 *
2261 * @return object The modal content element wrapper.
2262 */
2263 static displayModal(options) {
2264 var def_options = {
2265 suffix: (Math.floor(Math.random() * 100000)) + '',
2266 extra_class: null,
2267 header: true,
2268 title: '',
2269 body: '',
2270 body_prepend: false,
2271 lock_scroll: false,
2272 draggable: false,
2273 footer_left: null,
2274 footer_right: null,
2275 dismiss_event: null,
2276 dismissed_event: null,
2277 onDismiss: null,
2278 loading_event: null,
2279 loading_body: VBOCore.options.default_loading_body,
2280 event_data: null,
2281 };
2282
2283 // merge default options with given options
2284 options = Object.assign(def_options, options);
2285
2286 // create the modal dismiss function
2287 var modal_dismiss_fn = (e) => {
2288 custom_modal.fadeOut(400, () => {
2289 // invoke callback for onDismiss
2290 if (typeof options.onDismiss === 'function') {
2291 options.onDismiss.call(custom_modal, e);
2292 }
2293
2294 // check if modal did register to the loading event
2295 if (options.loading_event) {
2296 // we can now un-register from the loading event until a new modal is displayed and will register to it again
2297 document.removeEventListener(options.loading_event, modal_handle_loading_event_fn);
2298 }
2299
2300 // check if we should fire the given modal dismissed event
2301 if (options.dismissed_event) {
2302 VBOCore.emitEvent(options.dismissed_event, options.event_data);
2303 }
2304
2305 // check if body scroll lock should be removed
2306 if (options.lock_scroll) {
2307 $('body').removeClass('vbo-modal-lock-scroll');
2308 }
2309
2310 // remove modal from DOM
2311 custom_modal.remove();
2312 });
2313 };
2314
2315 // create the modal loading event handler function
2316 var modal_handle_loading_event_fn = (e) => {
2317 // toggle modal loading
2318 if ($('.vbo-modal-overlay-content-backdrop').length) {
2319 // hide loading
2320 $('.vbo-modal-overlay-content-backdrop').remove();
2321
2322 // do not proceed
2323 return;
2324 }
2325
2326 // show loading
2327 var modal_loading = $('<div></div>').addClass('vbo-modal-overlay-content-backdrop');
2328 var modal_loading_body = $('<div></div>').addClass('vbo-modal-overlay-content-backdrop-body');
2329 if (options.loading_body) {
2330 modal_loading_body.append(options.loading_body);
2331 }
2332 modal_loading.append(modal_loading_body);
2333
2334 // append backdrop loading to modal content
2335 modal_content.prepend(modal_loading);
2336 };
2337
2338 // start the modal position variables
2339 var modal_pos_x = 0, modal_pos_y = 0;
2340
2341 // create the modal drag-start (mousedown) event handler function
2342 var modal_dragstart_fn = (e) => {
2343 e = e || window.event;
2344 e.preventDefault();
2345
2346 if (typeof e.clientX === 'undefined' || typeof e.clientY === 'undefined') {
2347 // unsupported
2348 return;
2349 }
2350
2351 if (e.target) {
2352 e.target.style.cursor = 'move';
2353 }
2354
2355 // store the initial modal (cursor) position
2356 modal_pos_x = e.clientX;
2357 modal_pos_y = e.clientY;
2358
2359 // register mouseup and mousemove events
2360 document.onmouseup = modal_dragstop_fn;
2361 document.onmousemove = modal_dragmove_fn;
2362 };
2363
2364 // create the modal drag-stop (mouseup) event handler function
2365 var modal_dragstop_fn = (e) => {
2366 e = e || window.event;
2367
2368 if (e.target) {
2369 e.target.style.cursor = 'auto';
2370 }
2371
2372 // unregister mousemove event
2373 if (document.onmousemove == modal_dragmove_fn) {
2374 document.onmousemove = null;
2375 }
2376
2377 // unregister mouseup event
2378 if (document.onmouseup == modal_dragstop_fn) {
2379 document.onmouseup = null;
2380 }
2381 };
2382
2383 // create the modal drag-move (mousemove) event handler function
2384 var modal_dragmove_fn = (e) => {
2385 e = e || window.event;
2386 e.preventDefault();
2387
2388 if (typeof e.clientX === 'undefined' || typeof e.clientY === 'undefined') {
2389 // unsupported
2390 return;
2391 }
2392
2393 // calculate the new modal (cursor) position
2394 let new_modal_pos_x = modal_pos_x - e.clientX;
2395 let new_modal_pos_y = modal_pos_y - e.clientY;
2396
2397 // update current modal (cursor) position
2398 modal_pos_x = e.clientX;
2399 modal_pos_y = e.clientY;
2400
2401 // find the modal element
2402 let modal_element = e.target.closest('.vbo-modal-overlay-content');
2403 if (!modal_element) {
2404 return;
2405 }
2406
2407 // set the modal position
2408 modal_element.style.top = (modal_element.offsetTop - new_modal_pos_y) + 'px';
2409 modal_element.style.left = (modal_element.offsetLeft - new_modal_pos_x) + 'px';
2410 };
2411
2412 // build modal content
2413 var custom_modal = $('<div></div>').addClass('vbo-modal-overlay-block vbo-modal-overlay-' + options.suffix).css('display', 'block');
2414 var modal_dismiss = $('<a></a>').addClass('vbo-modal-overlay-close');
2415 modal_dismiss.on('click', modal_dismiss_fn);
2416 custom_modal.append(modal_dismiss);
2417
2418 var modal_content = $('<div></div>').addClass('vbo-modal-overlay-content vbo-modal-overlay-content-' + options.suffix);
2419 if (options.extra_class && typeof options.extra_class === 'string') {
2420 modal_content.addClass(options.extra_class);
2421 }
2422
2423 var modal_head = $('<div></div>').addClass('vbo-modal-overlay-content-head');
2424 var modal_head_close = $('<span></span>').addClass('vbo-modal-overlay-close-times').html('&times;');
2425 modal_head_close.on('click', modal_dismiss_fn);
2426 modal_head.append(options.title).append(modal_head_close);
2427
2428 // check if the modal head should be draggable
2429 if (options.draggable) {
2430 // register the event to allow dragging
2431 modal_head.addClass('vbo-modal-head-draggable');
2432 modal_head.on('mousedown', modal_dragstart_fn);
2433 }
2434
2435 var modal_body = $('<div></div>').addClass('vbo-modal-overlay-content-body vbo-modal-overlay-content-body-scroll');
2436 var modal_content_wrapper = $('<div></div>').addClass('vbo-modal-' + options.suffix + '-wrap');
2437 if (typeof options.body === 'string') {
2438 modal_content_wrapper.html(options.body);
2439 } else {
2440 modal_content_wrapper.append(options.body);
2441 }
2442 modal_body.append(modal_content_wrapper);
2443
2444 // modal footer
2445 var modal_footer = null;
2446 if (options.footer_left || options.footer_right) {
2447 modal_footer = $('<div></div>').addClass('vbo-modal-overlay-content-footer');
2448 if (options.footer_left) {
2449 var modal_footer_left = $('<div></div>').addClass('vbo-modal-overlay-content-footer-left').append(options.footer_left);
2450 modal_footer.append(modal_footer_left);
2451 }
2452 if (options.footer_right) {
2453 var modal_footer_right = $('<div></div>').addClass('vbo-modal-overlay-content-footer-right').append(options.footer_right);
2454 modal_footer.append(modal_footer_right);
2455 }
2456
2457 }
2458
2459 // finalize modal contents
2460 if (options.header) {
2461 // append header
2462 modal_content.append(modal_head);
2463 }
2464 // append body
2465 modal_content.append(modal_body);
2466 if (modal_footer) {
2467 // append footer
2468 modal_content.append(modal_footer);
2469 }
2470 custom_modal.append(modal_content);
2471
2472 // register to the dismiss event
2473 if (options.dismiss_event) {
2474 // listen to the event that will dismiss the modal
2475 document.addEventListener(options.dismiss_event, function vbo_core_handle_dismiss_event(e) {
2476 // make sure the same event won't propagate again, unless a new modal is displayed (multiple displayModal calls)
2477 e.target.removeEventListener(e.type, vbo_core_handle_dismiss_event);
2478
2479 // invoke the modal dismiss function
2480 modal_dismiss_fn(e);
2481 });
2482
2483 // declare the function to detect the Escape key pressed
2484 const vbo_core_dismiss_event_modal_escape = (e) => {
2485 if (!e.key || e.key != 'Escape') {
2486 return;
2487 }
2488
2489 // immediately unregister from this event once fired
2490 document.removeEventListener(e.type, vbo_core_dismiss_event_modal_escape);
2491
2492 // trigger the actual dismiss event
2493 VBOCore.emitEvent(options.dismiss_event);
2494 };
2495
2496 // listen to the Escape keyup event to dismiss the modal
2497 document.addEventListener('keyup', vbo_core_dismiss_event_modal_escape);
2498 }
2499
2500 // register to the toggle-loading event
2501 if (options.loading_event) {
2502 // let a function handle it so that removing the event listener will be doable
2503 document.addEventListener(options.loading_event, modal_handle_loading_event_fn);
2504 }
2505
2506 // append (or prepend) modal to body
2507 if ($('.vbo-modal-overlay-' + options.suffix).length) {
2508 $('.vbo-modal-overlay-' + options.suffix).remove();
2509 }
2510 if (options.body_prepend) {
2511 // prepend to body
2512 if ($('body > .vbo-modal-overlay-block').length) {
2513 // we've got other modals prepended to the body, so go after the last one
2514 $('body > .vbo-modal-overlay-block').last().after(custom_modal);
2515 } else {
2516 // place the modal right as the first child node of body
2517 $('body').prepend(custom_modal);
2518 }
2519 } else {
2520 // append to body
2521 $('body').append(custom_modal);
2522 }
2523
2524 // check if scroll should be locked on the whole page body for a "sticky" modal
2525 if (options.lock_scroll) {
2526 $('body').addClass('vbo-modal-lock-scroll');
2527 }
2528
2529 // return the content wrapper element of the new modal
2530 return modal_content_wrapper;
2531 }
2532
2533 /**
2534 * Debounce technique to group a flurry of events into one single event.
2535 */
2536 static debounceEvent(func, wait, immediate) {
2537 var timeout;
2538 return function() {
2539 var context = this, args = arguments;
2540 var later = function() {
2541 timeout = null;
2542 if (!immediate) func.apply(context, args);
2543 };
2544 var callNow = immediate && !timeout;
2545 clearTimeout(timeout);
2546 timeout = setTimeout(later, wait);
2547 if (callNow) {
2548 func.apply(context, args);
2549 }
2550 }
2551 }
2552
2553 /**
2554 * Throttle guarantees a constant flow of events at a given time interval.
2555 * Runs immediately when the event takes place, but can be delayed.
2556 */
2557 static throttleEvent(method, delay) {
2558 var time = Date.now();
2559 return function() {
2560 if ((time + delay - Date.now()) < 0) {
2561 method();
2562 time = Date.now();
2563 }
2564 }
2565 }
2566
2567 /**
2568 * Alternative throttle technique for event listeners.
2569 *
2570 * @param function callback the callback to invoke.
2571 * @param int time the time (in ms) to throttle.
2572 *
2573 * @return void
2574 *
2575 * @since 1.6.8
2576 */
2577 static throttleTimer(callback, time) {
2578 if (VBOCore.throttle_timer) {
2579 // prevent more executions
2580 return;
2581 }
2582
2583 // turn throttle timer on
2584 VBOCore.throttle_timer = true;
2585
2586 setTimeout(() => {
2587 // run the callback with the given delay
2588 callback();
2589
2590 // turn throttle timer off
2591 VBOCore.throttle_timer = false;
2592 }, time);
2593 }
2594
2595 /**
2596 * Tells whether localStorage is supported.
2597 *
2598 * @return boolean
2599 */
2600 static storageSupported() {
2601 return typeof localStorage !== 'undefined';
2602 }
2603
2604 /**
2605 * Gets an item from localStorage.
2606 *
2607 * @param string keyName the storage key identifier.
2608 *
2609 * @return any
2610 */
2611 static storageGetItem(keyName) {
2612 if (!VBOCore.storageSupported()) {
2613 return null;
2614 }
2615
2616 return localStorage.getItem(keyName);
2617 }
2618
2619 /**
2620 * Sets an item to localStorage.
2621 *
2622 * @param string keyName the storage key identifier.
2623 * @param any value the value to store.
2624 *
2625 * @return boolean
2626 */
2627 static storageSetItem(keyName, value) {
2628 if (!VBOCore.storageSupported()) {
2629 return false;
2630 }
2631
2632 try {
2633 if (typeof value === 'object') {
2634 value = JSON.stringify(value);
2635 }
2636
2637 localStorage.setItem(keyName, value);
2638 } catch(e) {
2639 return false;
2640 }
2641
2642 return true;
2643 }
2644
2645 /**
2646 * Removes an item from localStorage.
2647 *
2648 * @param string keyName the storage key identifier.
2649 *
2650 * @return boolean
2651 */
2652 static storageRemoveItem(keyName) {
2653 if (!VBOCore.storageSupported()) {
2654 return false;
2655 }
2656
2657 localStorage.removeItem(keyName);
2658
2659 return true;
2660 }
2661
2662 /**
2663 * Returns the name of the storage identifier for the given scope.
2664 *
2665 * @param string scope the admin menu scope.
2666 *
2667 * @return string the requested admin menu storage identifier.
2668 */
2669 static getStorageScopeName(scope) {
2670 let storage_scope_name = VBOCore.options.admin_menu_actions_nm;
2671
2672 if (typeof scope === 'string' && scope.length) {
2673 if (scope.indexOf('.') !== 0) {
2674 scope = '.' + scope;
2675 }
2676 storage_scope_name += scope;
2677 }
2678
2679 return storage_scope_name;
2680 }
2681
2682 /**
2683 * Returns a list of admin menu action objects or an empty array.
2684 *
2685 * @param string scope the admin menu scope.
2686 *
2687 * @return Array
2688 */
2689 static getAdminMenuActions(scope) {
2690 let menu_actions = VBOCore.storageGetItem(VBOCore.getStorageScopeName(scope));
2691
2692 if (!menu_actions) {
2693 return [];
2694 }
2695
2696 try {
2697 menu_actions = JSON.parse(menu_actions);
2698 if (!Array.isArray(menu_actions) || !menu_actions.length) {
2699 menu_actions = [];
2700 }
2701 } catch(e) {
2702 return [];
2703 }
2704
2705 return menu_actions;
2706 }
2707
2708 /**
2709 * Builds an admin menu action object with a proper href property.
2710 *
2711 * @param object action the action to build.
2712 *
2713 * @return object
2714 */
2715 static buildAdminMenuAction(action) {
2716 if (typeof action !== 'object' || !action || !action.hasOwnProperty('name')) {
2717 throw new Error('Invalid action object');
2718 }
2719
2720 var action_base = action.hasOwnProperty('href') && typeof action['href'] == 'string' ? action['href'] : window.location.href;
2721 var action_url;
2722
2723 if (action_base.toLowerCase().indexOf('javascript') === 0 && action.hasOwnProperty('widget')) {
2724 // no need to prepare the action URL ("JavaScript: void(0)") as the link will use a JS callback
2725 return action;
2726 }
2727
2728 if (action_base.indexOf('http') !== 0) {
2729 // relative URL
2730 action_url = new URL(action_base, window.location.href);
2731 } else {
2732 // absolute URL
2733 action_url = new URL(action_base);
2734 }
2735
2736 // build proper href with a relative URL
2737 action['href'] = action_url.pathname + action_url.search;
2738
2739 return action;
2740 }
2741
2742 /**
2743 * Registers an admin menu action object.
2744 *
2745 * @param object action the action to build.
2746 * @param string scope the admin menu scope.
2747 *
2748 * @return boolean
2749 */
2750 static registerAdminMenuAction(action, scope) {
2751 // build menu action object
2752 let menu_action_entry = VBOCore.buildAdminMenuAction(action);
2753
2754 let menu_actions = VBOCore.getAdminMenuActions(scope);
2755
2756 // make sure we are not pushing a duplicate and count pinned actions
2757 let pinned_actions = 0;
2758 let unpinned_index = [];
2759 for (let i = 0; i < menu_actions.length; i++) {
2760 // avoid duplicate entries
2761 if (!menu_action_entry.hasOwnProperty('widget') && menu_actions[i]['href'] == menu_action_entry['href']) {
2762 // duplicate link
2763 return false;
2764 }
2765 if (menu_action_entry.hasOwnProperty('widget') && menu_actions[i].hasOwnProperty('widget') && menu_actions[i]['href'] == menu_action_entry['widget']) {
2766 // duplicate widget
2767 return false;
2768 }
2769 if (menu_actions[i].hasOwnProperty('pinned') && menu_actions[i]['pinned']) {
2770 pinned_actions++;
2771 } else {
2772 unpinned_index.push(i);
2773 }
2774 }
2775
2776 if (pinned_actions >= VBOCore.options.admin_menu_maxactions) {
2777 // no more space to register a new menu action for this admin menu
2778 return false;
2779 }
2780
2781 // splice or pop before prepending to keep current indexes
2782 let tot_menu_actions = menu_actions.length;
2783 if (++tot_menu_actions > VBOCore.options.admin_menu_maxactions) {
2784 if (unpinned_index.length) {
2785 menu_actions.splice(unpinned_index[unpinned_index.length - 1], 1);
2786 } else {
2787 menu_actions.pop();
2788 }
2789 }
2790
2791 // prepend new admin menu action
2792 menu_actions.unshift(menu_action_entry);
2793
2794 return VBOCore.storageSetItem(VBOCore.getStorageScopeName(scope), menu_actions);
2795 }
2796
2797 /**
2798 * Updates an existing admin menu action object.
2799 *
2800 * @param object action the action to build.
2801 * @param string scope the admin menu scope.
2802 * @param number index optional menu action index.
2803 *
2804 * @return boolean
2805 */
2806 static updateAdminMenuAction(action, scope, index) {
2807 // build menu action object
2808 let menu_action_entry = VBOCore.buildAdminMenuAction(action);
2809
2810 let menu_actions = VBOCore.getAdminMenuActions(scope);
2811
2812 if (!menu_actions.length) {
2813 return false;
2814 }
2815
2816 if (typeof index === 'undefined') {
2817 // find the proper index to update by href
2818 for (let i = 0; i < menu_actions.length; i++) {
2819 if (menu_actions[i].hasOwnProperty('widget') && menu_action_entry.hasOwnProperty('widget') && menu_actions[i]['widget'] == menu_action_entry['widget']) {
2820 // existing entry for widget found
2821 index = i;
2822 break;
2823 } else if (!menu_action_entry.hasOwnProperty('widget') && menu_actions[i]['href'] == menu_action_entry['href']) {
2824 // existing entry for link found
2825 index = i;
2826 break;
2827 }
2828 }
2829 }
2830
2831 if (isNaN(index) || !(index in menu_actions)) {
2832 // menu entry index not found
2833 return false;
2834 }
2835
2836 menu_actions[index] = menu_action_entry;
2837
2838 return VBOCore.storageSetItem(VBOCore.getStorageScopeName(scope), menu_actions);
2839 }
2840
2841 /**
2842 * Checks if the current menu actions should be filled with some other actions.
2843 * No update is made over the Local Storage through this method.
2844 *
2845 * @param array menu_actions menu actions to fill.
2846 * @param array widgets_actions default actions to use for filling.
2847 * @param string scope the admin menu scope.
2848 *
2849 * @return array the menu actions eventually filled.
2850 */
2851 static fillAdminMenuActions(menu_actions, widgets_actions, scope) {
2852 if (!Array.isArray(menu_actions) || !Array.isArray(widgets_actions)) {
2853 return [];
2854 }
2855
2856 if (!menu_actions.length || !widgets_actions.length || menu_actions.length >= VBOCore.options.admin_menu_maxactions) {
2857 return menu_actions;
2858 }
2859
2860 var current_widgets = [];
2861 menu_actions.forEach((action, index) => {
2862 current_widgets.push(action['widget']);
2863 });
2864
2865 for (var i = 0; i < widgets_actions.length; i++) {
2866 if (menu_actions.length >= VBOCore.options.admin_menu_maxactions) {
2867 // abort for filling completed
2868 break;
2869 }
2870
2871 if (!widgets_actions[i].hasOwnProperty('widget')) {
2872 // action must be for a widget
2873 continue;
2874 }
2875
2876 if (current_widgets.includes(widgets_actions[i]['widget'])) {
2877 // there cannot be duplicate entries
2878 continue;
2879 }
2880
2881 // fill menu action
2882 menu_actions.push(widgets_actions[i]);
2883 }
2884
2885 return menu_actions;
2886 }
2887 }
2888
2889 /**
2890 * These used to be private static properties (static #options),
2891 * but they are only supported by quite recent browsers (especially Safari).
2892 * It's too premature, so we decided to keep the class properties public
2893 * without declaring them as static inside the class declaration.
2894 *
2895 * @var object
2896 */
2897 VBOCore.options = {
2898 is_vbo: false,
2899 is_vcm: false,
2900 cms: 'wordpress',
2901 widget_ajax_uri: null,
2902 assets_ajax_uri: null,
2903 multitask_ajax_uri: null,
2904 watchdata_ajax_uri: null,
2905 current_page: null,
2906 current_page_uri: null,
2907 root_uri: '/',
2908 client: 'admin',
2909 panel_opts: {},
2910 admin_widgets: [],
2911 notif_audio_url: '',
2912 active_listeners: {},
2913 tn_texts: {
2914 notifs_enabled: 'Browser notifications are enabled!',
2915 notifs_disabled: 'Browser notifications are disabled',
2916 notifs_disabled_help: "Could not enable browser notifications.\nThis feature is available only in secure contexts (HTTPS).",
2917 admin_widget: 'Admin widget',
2918 congrats: 'Congratulations!',
2919 },
2920 default_loading_body: '....',
2921 multitask_save_event: 'vbo-admin-multitask-save',
2922 multitask_open_event: 'vbo-admin-multitask-open',
2923 multitask_close_event: 'vbo-admin-multitask-close',
2924 multitask_shortcut_ev: 'vbo_multitask_shortcut',
2925 multitask_searchfs_ev: 'vbo_multitask_search_focus',
2926 widget_modal_rendered: 'vbo-admin-widget-modal-rendered',
2927 widget_modal_dismissed: 'vbo-widget-modal-dismissed',
2928 admin_menu_maxactions: 3,
2929 admin_menu_actions_nm: 'vikbooking.admin_menu.actions',
2930 service_worker_path: '',
2931 service_worker_scope: './',
2932 push: {
2933 application_key: '',
2934 ajax_url: '',
2935 },
2936 push_options: {
2937 storage_endp_id: 'vbo_push_subscr_endp',
2938 allowed_notif_types: [
2939 'Book',
2940 'Modify',
2941 'Cancel',
2942 'Request',
2943 'CancelRequest',
2944 'Inquiry',
2945 'CancelInquiry',
2946 'Chat',
2947 'Info',
2948 ],
2949 },
2950 };
2951
2952 /**
2953 * @var bool
2954 */
2955 VBOCore.side_panel_on = false;
2956
2957 /**
2958 * @var bool
2959 * @since 1.6.8
2960 */
2961 VBOCore.throttle_timer = false;
2962
2963 /**
2964 * @var array
2965 */
2966 VBOCore.notifications = [];
2967
2968 /**
2969 * @var object
2970 */
2971 VBOCore.widgets_watch_data = null;
2972
2973 /**
2974 * @var number
2975 */
2976 VBOCore.watch_data_interval = null;
2977
2978 /**
2979 * @var object
2980 * @since 1.6.3
2981 */
2982 VBOCore.broadcast_watch_data = null;
2983
2984 /**
2985 * @var object
2986 * @since 1.6.5
2987 */
2988 VBOCore.broadcast_push_data = null;
2989
2990 /**
2991 * @var object
2992 * @since 1.6.8
2993 */
2994 VBOCore.broadcast_watch_events = null;
2995
2996 /**
2997 * @var array<object>
2998 * @since 1.6.5
2999 */
3000 VBOCore.widgets_pushed_data = [];
3001
3002 })(jQuery, window);