PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.6.8
VikBooking Hotel Booking Engine & PMS v1.6.8
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.8, at admin/resources/vbocore.js

2,995 lines 88.5 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 title: '',
2268 body: '',
2269 body_prepend: false,
2270 lock_scroll: false,
2271 draggable: false,
2272 footer_left: null,
2273 footer_right: null,
2274 dismiss_event: null,
2275 dismissed_event: null,
2276 onDismiss: null,
2277 loading_event: null,
2278 loading_body: VBOCore.options.default_loading_body,
2279 event_data: null,
2280 };
2281
2282 // merge default options with given options
2283 options = Object.assign(def_options, options);
2284
2285 // create the modal dismiss function
2286 var modal_dismiss_fn = (e) => {
2287 custom_modal.fadeOut(400, () => {
2288 // invoke callback for onDismiss
2289 if (typeof options.onDismiss === 'function') {
2290 options.onDismiss.call(custom_modal, e);
2291 }
2292
2293 // check if modal did register to the loading event
2294 if (options.loading_event) {
2295 // we can now un-register from the loading event until a new modal is displayed and will register to it again
2296 document.removeEventListener(options.loading_event, modal_handle_loading_event_fn);
2297 }
2298
2299 // check if we should fire the given modal dismissed event
2300 if (options.dismissed_event) {
2301 VBOCore.emitEvent(options.dismissed_event, options.event_data);
2302 }
2303
2304 // check if body scroll lock should be removed
2305 if (options.lock_scroll) {
2306 $('body').removeClass('vbo-modal-lock-scroll');
2307 }
2308
2309 // remove modal from DOM
2310 custom_modal.remove();
2311 });
2312 };
2313
2314 // create the modal loading event handler function
2315 var modal_handle_loading_event_fn = (e) => {
2316 // toggle modal loading
2317 if ($('.vbo-modal-overlay-content-backdrop').length) {
2318 // hide loading
2319 $('.vbo-modal-overlay-content-backdrop').remove();
2320
2321 // do not proceed
2322 return;
2323 }
2324
2325 // show loading
2326 var modal_loading = $('<div></div>').addClass('vbo-modal-overlay-content-backdrop');
2327 var modal_loading_body = $('<div></div>').addClass('vbo-modal-overlay-content-backdrop-body');
2328 if (options.loading_body) {
2329 modal_loading_body.append(options.loading_body);
2330 }
2331 modal_loading.append(modal_loading_body);
2332
2333 // append backdrop loading to modal content
2334 modal_content.prepend(modal_loading);
2335 };
2336
2337 // start the modal position variables
2338 var modal_pos_x = 0, modal_pos_y = 0;
2339
2340 // create the modal drag-start (mousedown) event handler function
2341 var modal_dragstart_fn = (e) => {
2342 e = e || window.event;
2343 e.preventDefault();
2344
2345 if (typeof e.clientX === 'undefined' || typeof e.clientY === 'undefined') {
2346 // unsupported
2347 return;
2348 }
2349
2350 if (e.target) {
2351 e.target.style.cursor = 'move';
2352 }
2353
2354 // store the initial modal (cursor) position
2355 modal_pos_x = e.clientX;
2356 modal_pos_y = e.clientY;
2357
2358 // register mouseup and mousemove events
2359 document.onmouseup = modal_dragstop_fn;
2360 document.onmousemove = modal_dragmove_fn;
2361 };
2362
2363 // create the modal drag-stop (mouseup) event handler function
2364 var modal_dragstop_fn = (e) => {
2365 e = e || window.event;
2366
2367 if (e.target) {
2368 e.target.style.cursor = 'auto';
2369 }
2370
2371 // unregister mousemove event
2372 if (document.onmousemove == modal_dragmove_fn) {
2373 document.onmousemove = null;
2374 }
2375
2376 // unregister mouseup event
2377 if (document.onmouseup == modal_dragstop_fn) {
2378 document.onmouseup = null;
2379 }
2380 };
2381
2382 // create the modal drag-move (mousemove) event handler function
2383 var modal_dragmove_fn = (e) => {
2384 e = e || window.event;
2385 e.preventDefault();
2386
2387 if (typeof e.clientX === 'undefined' || typeof e.clientY === 'undefined') {
2388 // unsupported
2389 return;
2390 }
2391
2392 // calculate the new modal (cursor) position
2393 let new_modal_pos_x = modal_pos_x - e.clientX;
2394 let new_modal_pos_y = modal_pos_y - e.clientY;
2395
2396 // update current modal (cursor) position
2397 modal_pos_x = e.clientX;
2398 modal_pos_y = e.clientY;
2399
2400 // find the modal element
2401 let modal_element = e.target.closest('.vbo-modal-overlay-content');
2402 if (!modal_element) {
2403 return;
2404 }
2405
2406 // set the modal position
2407 modal_element.style.top = (modal_element.offsetTop - new_modal_pos_y) + 'px';
2408 modal_element.style.left = (modal_element.offsetLeft - new_modal_pos_x) + 'px';
2409 };
2410
2411 // build modal content
2412 var custom_modal = $('<div></div>').addClass('vbo-modal-overlay-block vbo-modal-overlay-' + options.suffix).css('display', 'block');
2413 var modal_dismiss = $('<a></a>').addClass('vbo-modal-overlay-close');
2414 modal_dismiss.on('click', modal_dismiss_fn);
2415 custom_modal.append(modal_dismiss);
2416
2417 var modal_content = $('<div></div>').addClass('vbo-modal-overlay-content vbo-modal-overlay-content-' + options.suffix);
2418 if (options.extra_class && typeof options.extra_class === 'string') {
2419 modal_content.addClass(options.extra_class);
2420 }
2421
2422 var modal_head = $('<div></div>').addClass('vbo-modal-overlay-content-head');
2423 var modal_head_close = $('<span></span>').addClass('vbo-modal-overlay-close-times').html('&times;');
2424 modal_head_close.on('click', modal_dismiss_fn);
2425 modal_head.append(options.title).append(modal_head_close);
2426
2427 // check if the modal head should be draggable
2428 if (options.draggable) {
2429 // register the event to allow dragging
2430 modal_head.addClass('vbo-modal-head-draggable');
2431 modal_head.on('mousedown', modal_dragstart_fn);
2432 }
2433
2434 var modal_body = $('<div></div>').addClass('vbo-modal-overlay-content-body vbo-modal-overlay-content-body-scroll');
2435 var modal_content_wrapper = $('<div></div>').addClass('vbo-modal-' + options.suffix + '-wrap');
2436 if (typeof options.body === 'string') {
2437 modal_content_wrapper.html(options.body);
2438 } else {
2439 modal_content_wrapper.append(options.body);
2440 }
2441 modal_body.append(modal_content_wrapper);
2442
2443 // modal footer
2444 var modal_footer = null;
2445 if (options.footer_left || options.footer_right) {
2446 modal_footer = $('<div></div>').addClass('vbo-modal-overlay-content-footer');
2447 if (options.footer_left) {
2448 var modal_footer_left = $('<div></div>').addClass('vbo-modal-overlay-content-footer-left').append(options.footer_left);
2449 modal_footer.append(modal_footer_left);
2450 }
2451 if (options.footer_right) {
2452 var modal_footer_right = $('<div></div>').addClass('vbo-modal-overlay-content-footer-right').append(options.footer_right);
2453 modal_footer.append(modal_footer_right);
2454 }
2455
2456 }
2457
2458 // finalize modal contents
2459 modal_content.append(modal_head).append(modal_body);
2460 if (modal_footer) {
2461 modal_content.append(modal_footer);
2462 }
2463 custom_modal.append(modal_content);
2464
2465 // register to the dismiss event
2466 if (options.dismiss_event) {
2467 // listen to the event that will dismiss the modal
2468 document.addEventListener(options.dismiss_event, function vbo_core_handle_dismiss_event(e) {
2469 // make sure the same event won't propagate again, unless a new modal is displayed (multiple displayModal calls)
2470 e.target.removeEventListener(e.type, vbo_core_handle_dismiss_event);
2471
2472 // invoke the modal dismiss function
2473 modal_dismiss_fn(e);
2474 });
2475
2476 // declare the function to detect the Escape key pressed
2477 const vbo_core_dismiss_event_modal_escape = (e) => {
2478 if (!e.key || e.key != 'Escape') {
2479 return;
2480 }
2481
2482 // immediately unregister from this event once fired
2483 document.removeEventListener(e.type, vbo_core_dismiss_event_modal_escape);
2484
2485 // trigger the actual dismiss event
2486 VBOCore.emitEvent(options.dismiss_event);
2487 };
2488
2489 // listen to the Escape keyup event to dismiss the modal
2490 document.addEventListener('keyup', vbo_core_dismiss_event_modal_escape);
2491 }
2492
2493 // register to the toggle-loading event
2494 if (options.loading_event) {
2495 // let a function handle it so that removing the event listener will be doable
2496 document.addEventListener(options.loading_event, modal_handle_loading_event_fn);
2497 }
2498
2499 // append (or prepend) modal to body
2500 if ($('.vbo-modal-overlay-' + options.suffix).length) {
2501 $('.vbo-modal-overlay-' + options.suffix).remove();
2502 }
2503 if (options.body_prepend) {
2504 // prepend to body
2505 if ($('body > .vbo-modal-overlay-block').length) {
2506 // we've got other modals prepended to the body, so go after the last one
2507 $('body > .vbo-modal-overlay-block').last().after(custom_modal);
2508 } else {
2509 // place the modal right as the first child node of body
2510 $('body').prepend(custom_modal);
2511 }
2512 } else {
2513 // append to body
2514 $('body').append(custom_modal);
2515 }
2516
2517 // check if scroll should be locked on the whole page body for a "sticky" modal
2518 if (options.lock_scroll) {
2519 $('body').addClass('vbo-modal-lock-scroll');
2520 }
2521
2522 // return the content wrapper element of the new modal
2523 return modal_content_wrapper;
2524 }
2525
2526 /**
2527 * Debounce technique to group a flurry of events into one single event.
2528 */
2529 static debounceEvent(func, wait, immediate) {
2530 var timeout;
2531 return function() {
2532 var context = this, args = arguments;
2533 var later = function() {
2534 timeout = null;
2535 if (!immediate) func.apply(context, args);
2536 };
2537 var callNow = immediate && !timeout;
2538 clearTimeout(timeout);
2539 timeout = setTimeout(later, wait);
2540 if (callNow) {
2541 func.apply(context, args);
2542 }
2543 }
2544 }
2545
2546 /**
2547 * Throttle guarantees a constant flow of events at a given time interval.
2548 * Runs immediately when the event takes place, but can be delayed.
2549 */
2550 static throttleEvent(method, delay) {
2551 var time = Date.now();
2552 return function() {
2553 if ((time + delay - Date.now()) < 0) {
2554 method();
2555 time = Date.now();
2556 }
2557 }
2558 }
2559
2560 /**
2561 * Alternative throttle technique for event listeners.
2562 *
2563 * @param function callback the callback to invoke.
2564 * @param int time the time (in ms) to throttle.
2565 *
2566 * @return void
2567 *
2568 * @since 1.6.8
2569 */
2570 static throttleTimer(callback, time) {
2571 if (VBOCore.throttle_timer) {
2572 // prevent more executions
2573 return;
2574 }
2575
2576 // turn throttle timer on
2577 VBOCore.throttle_timer = true;
2578
2579 setTimeout(() => {
2580 // run the callback with the given delay
2581 callback();
2582
2583 // turn throttle timer off
2584 VBOCore.throttle_timer = false;
2585 }, time);
2586 }
2587
2588 /**
2589 * Tells whether localStorage is supported.
2590 *
2591 * @return boolean
2592 */
2593 static storageSupported() {
2594 return typeof localStorage !== 'undefined';
2595 }
2596
2597 /**
2598 * Gets an item from localStorage.
2599 *
2600 * @param string keyName the storage key identifier.
2601 *
2602 * @return any
2603 */
2604 static storageGetItem(keyName) {
2605 if (!VBOCore.storageSupported()) {
2606 return null;
2607 }
2608
2609 return localStorage.getItem(keyName);
2610 }
2611
2612 /**
2613 * Sets an item to localStorage.
2614 *
2615 * @param string keyName the storage key identifier.
2616 * @param any value the value to store.
2617 *
2618 * @return boolean
2619 */
2620 static storageSetItem(keyName, value) {
2621 if (!VBOCore.storageSupported()) {
2622 return false;
2623 }
2624
2625 try {
2626 if (typeof value === 'object') {
2627 value = JSON.stringify(value);
2628 }
2629
2630 localStorage.setItem(keyName, value);
2631 } catch(e) {
2632 return false;
2633 }
2634
2635 return true;
2636 }
2637
2638 /**
2639 * Removes an item from localStorage.
2640 *
2641 * @param string keyName the storage key identifier.
2642 *
2643 * @return boolean
2644 */
2645 static storageRemoveItem(keyName) {
2646 if (!VBOCore.storageSupported()) {
2647 return false;
2648 }
2649
2650 localStorage.removeItem(keyName);
2651
2652 return true;
2653 }
2654
2655 /**
2656 * Returns the name of the storage identifier for the given scope.
2657 *
2658 * @param string scope the admin menu scope.
2659 *
2660 * @return string the requested admin menu storage identifier.
2661 */
2662 static getStorageScopeName(scope) {
2663 let storage_scope_name = VBOCore.options.admin_menu_actions_nm;
2664
2665 if (typeof scope === 'string' && scope.length) {
2666 if (scope.indexOf('.') !== 0) {
2667 scope = '.' + scope;
2668 }
2669 storage_scope_name += scope;
2670 }
2671
2672 return storage_scope_name;
2673 }
2674
2675 /**
2676 * Returns a list of admin menu action objects or an empty array.
2677 *
2678 * @param string scope the admin menu scope.
2679 *
2680 * @return Array
2681 */
2682 static getAdminMenuActions(scope) {
2683 let menu_actions = VBOCore.storageGetItem(VBOCore.getStorageScopeName(scope));
2684
2685 if (!menu_actions) {
2686 return [];
2687 }
2688
2689 try {
2690 menu_actions = JSON.parse(menu_actions);
2691 if (!Array.isArray(menu_actions) || !menu_actions.length) {
2692 menu_actions = [];
2693 }
2694 } catch(e) {
2695 return [];
2696 }
2697
2698 return menu_actions;
2699 }
2700
2701 /**
2702 * Builds an admin menu action object with a proper href property.
2703 *
2704 * @param object action the action to build.
2705 *
2706 * @return object
2707 */
2708 static buildAdminMenuAction(action) {
2709 if (typeof action !== 'object' || !action || !action.hasOwnProperty('name')) {
2710 throw new Error('Invalid action object');
2711 }
2712
2713 var action_base = action.hasOwnProperty('href') && typeof action['href'] == 'string' ? action['href'] : window.location.href;
2714 var action_url;
2715
2716 if (action_base.toLowerCase().indexOf('javascript') === 0 && action.hasOwnProperty('widget')) {
2717 // no need to prepare the action URL ("JavaScript: void(0)") as the link will use a JS callback
2718 return action;
2719 }
2720
2721 if (action_base.indexOf('http') !== 0) {
2722 // relative URL
2723 action_url = new URL(action_base, window.location.href);
2724 } else {
2725 // absolute URL
2726 action_url = new URL(action_base);
2727 }
2728
2729 // build proper href with a relative URL
2730 action['href'] = action_url.pathname + action_url.search;
2731
2732 return action;
2733 }
2734
2735 /**
2736 * Registers an admin menu action object.
2737 *
2738 * @param object action the action to build.
2739 * @param string scope the admin menu scope.
2740 *
2741 * @return boolean
2742 */
2743 static registerAdminMenuAction(action, scope) {
2744 // build menu action object
2745 let menu_action_entry = VBOCore.buildAdminMenuAction(action);
2746
2747 let menu_actions = VBOCore.getAdminMenuActions(scope);
2748
2749 // make sure we are not pushing a duplicate and count pinned actions
2750 let pinned_actions = 0;
2751 let unpinned_index = [];
2752 for (let i = 0; i < menu_actions.length; i++) {
2753 // avoid duplicate entries
2754 if (!menu_action_entry.hasOwnProperty('widget') && menu_actions[i]['href'] == menu_action_entry['href']) {
2755 // duplicate link
2756 return false;
2757 }
2758 if (menu_action_entry.hasOwnProperty('widget') && menu_actions[i].hasOwnProperty('widget') && menu_actions[i]['href'] == menu_action_entry['widget']) {
2759 // duplicate widget
2760 return false;
2761 }
2762 if (menu_actions[i].hasOwnProperty('pinned') && menu_actions[i]['pinned']) {
2763 pinned_actions++;
2764 } else {
2765 unpinned_index.push(i);
2766 }
2767 }
2768
2769 if (pinned_actions >= VBOCore.options.admin_menu_maxactions) {
2770 // no more space to register a new menu action for this admin menu
2771 return false;
2772 }
2773
2774 // splice or pop before prepending to keep current indexes
2775 let tot_menu_actions = menu_actions.length;
2776 if (++tot_menu_actions > VBOCore.options.admin_menu_maxactions) {
2777 if (unpinned_index.length) {
2778 menu_actions.splice(unpinned_index[unpinned_index.length - 1], 1);
2779 } else {
2780 menu_actions.pop();
2781 }
2782 }
2783
2784 // prepend new admin menu action
2785 menu_actions.unshift(menu_action_entry);
2786
2787 return VBOCore.storageSetItem(VBOCore.getStorageScopeName(scope), menu_actions);
2788 }
2789
2790 /**
2791 * Updates an existing admin menu action object.
2792 *
2793 * @param object action the action to build.
2794 * @param string scope the admin menu scope.
2795 * @param number index optional menu action index.
2796 *
2797 * @return boolean
2798 */
2799 static updateAdminMenuAction(action, scope, index) {
2800 // build menu action object
2801 let menu_action_entry = VBOCore.buildAdminMenuAction(action);
2802
2803 let menu_actions = VBOCore.getAdminMenuActions(scope);
2804
2805 if (!menu_actions.length) {
2806 return false;
2807 }
2808
2809 if (typeof index === 'undefined') {
2810 // find the proper index to update by href
2811 for (let i = 0; i < menu_actions.length; i++) {
2812 if (menu_actions[i].hasOwnProperty('widget') && menu_action_entry.hasOwnProperty('widget') && menu_actions[i]['widget'] == menu_action_entry['widget']) {
2813 // existing entry for widget found
2814 index = i;
2815 break;
2816 } else if (!menu_action_entry.hasOwnProperty('widget') && menu_actions[i]['href'] == menu_action_entry['href']) {
2817 // existing entry for link found
2818 index = i;
2819 break;
2820 }
2821 }
2822 }
2823
2824 if (isNaN(index) || !(index in menu_actions)) {
2825 // menu entry index not found
2826 return false;
2827 }
2828
2829 menu_actions[index] = menu_action_entry;
2830
2831 return VBOCore.storageSetItem(VBOCore.getStorageScopeName(scope), menu_actions);
2832 }
2833
2834 /**
2835 * Checks if the current menu actions should be filled with some other actions.
2836 * No update is made over the Local Storage through this method.
2837 *
2838 * @param array menu_actions menu actions to fill.
2839 * @param array widgets_actions default actions to use for filling.
2840 * @param string scope the admin menu scope.
2841 *
2842 * @return array the menu actions eventually filled.
2843 */
2844 static fillAdminMenuActions(menu_actions, widgets_actions, scope) {
2845 if (!Array.isArray(menu_actions) || !Array.isArray(widgets_actions)) {
2846 return [];
2847 }
2848
2849 if (!menu_actions.length || !widgets_actions.length || menu_actions.length >= VBOCore.options.admin_menu_maxactions) {
2850 return menu_actions;
2851 }
2852
2853 var current_widgets = [];
2854 menu_actions.forEach((action, index) => {
2855 current_widgets.push(action['widget']);
2856 });
2857
2858 for (var i = 0; i < widgets_actions.length; i++) {
2859 if (menu_actions.length >= VBOCore.options.admin_menu_maxactions) {
2860 // abort for filling completed
2861 break;
2862 }
2863
2864 if (!widgets_actions[i].hasOwnProperty('widget')) {
2865 // action must be for a widget
2866 continue;
2867 }
2868
2869 if (current_widgets.includes(widgets_actions[i]['widget'])) {
2870 // there cannot be duplicate entries
2871 continue;
2872 }
2873
2874 // fill menu action
2875 menu_actions.push(widgets_actions[i]);
2876 }
2877
2878 return menu_actions;
2879 }
2880 }
2881
2882 /**
2883 * These used to be private static properties (static #options),
2884 * but they are only supported by quite recent browsers (especially Safari).
2885 * It's too premature, so we decided to keep the class properties public
2886 * without declaring them as static inside the class declaration.
2887 *
2888 * @var object
2889 */
2890 VBOCore.options = {
2891 is_vbo: false,
2892 is_vcm: false,
2893 cms: 'wordpress',
2894 widget_ajax_uri: null,
2895 assets_ajax_uri: null,
2896 multitask_ajax_uri: null,
2897 watchdata_ajax_uri: null,
2898 current_page: null,
2899 current_page_uri: null,
2900 root_uri: '/',
2901 client: 'admin',
2902 panel_opts: {},
2903 admin_widgets: [],
2904 notif_audio_url: '',
2905 active_listeners: {},
2906 tn_texts: {
2907 notifs_enabled: 'Browser notifications are enabled!',
2908 notifs_disabled: 'Browser notifications are disabled',
2909 notifs_disabled_help: "Could not enable browser notifications.\nThis feature is available only in secure contexts (HTTPS).",
2910 admin_widget: 'Admin widget',
2911 congrats: 'Congratulations!',
2912 },
2913 default_loading_body: '....',
2914 multitask_save_event: 'vbo-admin-multitask-save',
2915 multitask_open_event: 'vbo-admin-multitask-open',
2916 multitask_close_event: 'vbo-admin-multitask-close',
2917 multitask_shortcut_ev: 'vbo_multitask_shortcut',
2918 multitask_searchfs_ev: 'vbo_multitask_search_focus',
2919 widget_modal_rendered: 'vbo-admin-widget-modal-rendered',
2920 widget_modal_dismissed: 'vbo-widget-modal-dismissed',
2921 admin_menu_maxactions: 3,
2922 admin_menu_actions_nm: 'vikbooking.admin_menu.actions',
2923 service_worker_path: '',
2924 service_worker_scope: './',
2925 push: {
2926 application_key: '',
2927 ajax_url: '',
2928 },
2929 push_options: {
2930 storage_endp_id: 'vbo_push_subscr_endp',
2931 allowed_notif_types: [
2932 'Book',
2933 'Modify',
2934 'Cancel',
2935 'Request',
2936 'CancelRequest',
2937 'Inquiry',
2938 'CancelInquiry',
2939 'Chat',
2940 'Info',
2941 ],
2942 },
2943 };
2944
2945 /**
2946 * @var bool
2947 */
2948 VBOCore.side_panel_on = false;
2949
2950 /**
2951 * @var bool
2952 * @since 1.6.8
2953 */
2954 VBOCore.throttle_timer = false;
2955
2956 /**
2957 * @var array
2958 */
2959 VBOCore.notifications = [];
2960
2961 /**
2962 * @var object
2963 */
2964 VBOCore.widgets_watch_data = null;
2965
2966 /**
2967 * @var number
2968 */
2969 VBOCore.watch_data_interval = null;
2970
2971 /**
2972 * @var object
2973 * @since 1.6.3
2974 */
2975 VBOCore.broadcast_watch_data = null;
2976
2977 /**
2978 * @var object
2979 * @since 1.6.5
2980 */
2981 VBOCore.broadcast_push_data = null;
2982
2983 /**
2984 * @var object
2985 * @since 1.6.8
2986 */
2987 VBOCore.broadcast_watch_events = null;
2988
2989 /**
2990 * @var array<object>
2991 * @since 1.6.5
2992 */
2993 VBOCore.widgets_pushed_data = [];
2994
2995 })(jQuery, window);