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

2,208 lines 65.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * VikBooking Core v1.6.3
3 * Copyright (C) 2023 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 * Parses an AJAX response error object.
101 *
102 * @param object err
103 *
104 * @return bool
105 */
106 static isConnectionLostError(err) {
107 if (!err || !err.hasOwnProperty('status')) {
108 return false;
109 }
110
111 return (
112 err.statusText == 'error'
113 && err.status == 0
114 && (err.readyState == 0 || err.readyState == 4)
115 && (!err.hasOwnProperty('responseText') || err.responseText == '')
116 );
117 }
118
119 /**
120 * Ensures AJAX requests that fail due to connection errors are retried automatically.
121 *
122 * @param string url
123 * @param object data
124 * @param function success
125 * @param function failure
126 * @param number attempt
127 */
128 static doAjax(url, data, success, failure, attempt) {
129 const AJAX_MAX_ATTEMPTS = 3;
130
131 if (attempt === undefined) {
132 attempt = 1;
133 }
134
135 return $.ajax({
136 type: 'POST',
137 url: url,
138 data: data
139 }).done(function(resp) {
140 if (success !== undefined) {
141 // launch success callback function
142 success(resp);
143 }
144 }).fail(function(err) {
145 /**
146 * If the error is caused by a site connection lost, and if the number
147 * of retries is lower than max attempts, retry the same AJAX request.
148 */
149 if (attempt < AJAX_MAX_ATTEMPTS && VBOCore.isConnectionLostError(err)) {
150 // delay the retry by half second
151 setTimeout(function() {
152 // re-launch same request and increase number of attempts
153 console.log('Retrying previous AJAX request');
154 VBOCore.doAjax(url, data, success, failure, (attempt + 1));
155 }, 500);
156 } else {
157 // launch the failure callback otherwise
158 if (failure !== undefined) {
159 failure(err);
160 }
161 }
162
163 // always log the error in console
164 console.log('AJAX request failed' + (err.status == 500 ? ' (' + err.responseText + ')' : ''), err);
165 });
166 }
167
168 /**
169 * Matches a keyword against a text.
170 *
171 * @param string search the keyword to search.
172 * @param string text the text to compare.
173 *
174 * @return bool
175 */
176 static matchString(search, text) {
177 return ((text + '').indexOf(search) >= 0);
178 }
179
180 /**
181 * Initializes the multitasking panel for the admin widgets.
182 *
183 * @param object params the panel object params.
184 *
185 * @return bool
186 */
187 static prepareMultitasking(params) {
188 var panel_opts = {
189 selector: "",
190 sclass_l_small: "vbo-sidepanel-right",
191 sclass_l_large: "vbo-sidepanel-large",
192 btn_trigger: "",
193 search_selector: "#vbo-sidepanel-search-input",
194 search_nores: ".vbo-sidepanel-add-widgets-nores",
195 close_selector: ".vbo-sidepanel-dismiss-btn",
196 t_layout_small: ".vbo-sidepanel-layout-small",
197 t_layout_large: ".vbo-sidepanel-layout-large",
198 wclass_base_sel: ".vbo-admin-widgets-widget-output",
199 wclass_l_small: "vbo-admin-widgets-container-small",
200 wclass_l_large: "vbo-admin-widgets-container-large",
201 addws_selector: ".vbo-sidepanel-add-widgets",
202 addw_selector: ".vbo-sidepanel-add-widget",
203 addw_modal_cls: "vbo-widget-render-modal",
204 addwfs_selector: ".vbo-sidepanel-add-widget-focussed",
205 wtags_selector: ".vbo-sidepanel-widget-tags",
206 addw_data_attr: "data-vbowidgetid",
207 actws_selector: ".vbo-sidepanel-active-widgets",
208 editw_selector: ".vbo-sidepanel-edit-widgets-trig",
209 rmwidget_class: "vbo-admin-widgets-widget-remove",
210 rmwidget_icn: "",
211 dtcwidget_class: "vbo-admin-widgets-widget-detach",
212 dtctarget_class: "vbo-admin-widget-head",
213 dtcwidget_icn: "",
214 notif_selector: ".vbo-sidepanel-notifications-btn",
215 notif_on_class: "vbo-sidepanel-notifications-on",
216 notif_off_class: "vbo-sidepanel-notifications-off",
217 open_class: "vbo-sidepanel-open",
218 close_class: "vbo-sidepanel-close",
219 cur_widget_cls: "vbo-admin-widgets-container-small",
220 };
221
222 if (typeof params === 'object') {
223 panel_opts = Object.assign(panel_opts, params);
224 }
225
226 if (!panel_opts.btn_trigger || !panel_opts.selector) {
227 console.error('Got no trigger or selector');
228 return false;
229 }
230
231 // push panel options
232 VBOCore.setOptions({
233 panel_opts: panel_opts,
234 });
235
236 // setup browser notifications
237 VBOCore.setupNotifications();
238
239 // count active widgets on current page
240 var tot_active_widgets = VBOCore.options.admin_widgets.length;
241 if (tot_active_widgets > 0) {
242 // hide add-widgets container
243 $(panel_opts.addws_selector).hide();
244
245 // register listener for input search blur
246 VBOCore.registerSearchWidgetsBlur();
247 }
248
249 // register click event on trigger button
250 $(VBOCore.options.panel_opts.btn_trigger).on('click', function() {
251 var side_panel = $(VBOCore.options.panel_opts.selector);
252 if (side_panel.hasClass(VBOCore.options.panel_opts.open_class)) {
253 // hide panel
254 VBOCore.side_panel_on = false;
255 VBOCore.emitMultitaskEvent(VBOCore.multitask_close_event);
256 side_panel.addClass(VBOCore.options.panel_opts.close_class).removeClass(VBOCore.options.panel_opts.open_class);
257 // always hide add-widgets container
258 $(VBOCore.options.panel_opts.addws_selector).hide();
259 // check if we are currently editing
260 var is_editing = ($('.' + VBOCore.options.panel_opts.editmode_class).length > 0);
261 if (is_editing) {
262 // deactivate editing mode
263 VBOCore.toggleWidgetsPanelEditing(null);
264 }
265 } else {
266 // show panel
267 VBOCore.side_panel_on = true;
268 VBOCore.emitMultitaskEvent(VBOCore.multitask_open_event);
269 side_panel.addClass(VBOCore.options.panel_opts.open_class).removeClass(VBOCore.options.panel_opts.close_class);
270 if (!VBOCore.options.admin_widgets.length) {
271 // set focus on search widgets input with delay for the opening animation
272 setTimeout(function() {
273 $(VBOCore.options.panel_opts.search_selector).focus();
274 }, 300);
275 }
276 }
277 });
278
279 // register close/dismiss button
280 $(VBOCore.options.panel_opts.close_selector).on('click', function() {
281 $(VBOCore.options.panel_opts.btn_trigger).trigger('click');
282 });
283
284 // register toggle layout buttons
285 $(VBOCore.options.panel_opts.t_layout_large).on('click', function() {
286 // large layout
287 $(VBOCore.options.panel_opts.selector).addClass(VBOCore.options.panel_opts.sclass_l_large).removeClass(VBOCore.options.panel_opts.sclass_l_small);
288 $(VBOCore.options.panel_opts.wclass_base_sel).addClass(VBOCore.options.panel_opts.wclass_l_large).removeClass(VBOCore.options.panel_opts.wclass_l_small);
289 VBOCore.options.panel_opts.cur_widget_cls = VBOCore.options.panel_opts.sclass_l_large;
290 });
291 $(VBOCore.options.panel_opts.t_layout_small).on('click', function() {
292 // small layout
293 $(VBOCore.options.panel_opts.selector).addClass(VBOCore.options.panel_opts.sclass_l_small).removeClass(VBOCore.options.panel_opts.sclass_l_large);
294 $(VBOCore.options.panel_opts.wclass_base_sel).addClass(VBOCore.options.panel_opts.wclass_l_small).removeClass(VBOCore.options.panel_opts.wclass_l_large);
295 VBOCore.options.panel_opts.cur_widget_cls = VBOCore.options.panel_opts.sclass_l_small;
296 });
297
298 // register listener for esc key pressed
299 $(document).keyup(function(e) {
300 if (!VBOCore.side_panel_on) {
301 return;
302 }
303 if ((e.key && e.key === "Escape") || (e.keyCode && e.keyCode == 27)) {
304 $(VBOCore.options.panel_opts.btn_trigger).trigger('click');
305 }
306 });
307
308 // register listener for input search focus
309 $(VBOCore.options.panel_opts.search_selector).on('focus', function() {
310 // always show add-widgets container
311 var widget_focus_class = VBOCore.options.panel_opts.addwfs_selector.replace('.', '');
312 $(VBOCore.options.panel_opts.addw_selector).removeClass(widget_focus_class);
313 $(VBOCore.options.panel_opts.addws_selector).show();
314 });
315
316 // register listener on input search widget
317 $(VBOCore.options.panel_opts.search_selector).keyup(function(e) {
318 // get the keyword to look for
319 var keyword = $(this).val();
320 // counting matching widgets
321 var matching = 0;
322 var first_matched = null;
323 var widget_focus_class = VBOCore.options.panel_opts.addwfs_selector.replace('.', '');
324
325 // adjust widgets to be displayed
326 if (!keyword.length) {
327 // show all widgets for selection
328 $(VBOCore.options.panel_opts.addw_selector).show();
329 // hide "no results"
330 $(VBOCore.options.panel_opts.search_nores).hide();
331 // all widgets are matching
332 matching = $(VBOCore.options.panel_opts.addw_selector).length;
333 } else {
334 // make the keyword lowercase
335 keyword = (keyword + '').toLowerCase();
336 // parse all widget's description tags
337 $(VBOCore.options.panel_opts.addw_selector).each(function() {
338 var elem = $(this);
339 var descr = elem.find(VBOCore.options.panel_opts.wtags_selector).text();
340 if (VBOCore.matchString(keyword, descr)) {
341 elem.show();
342 matching++;
343 if (!first_matched) {
344 // store the first widget that matched
345 first_matched = elem.attr(VBOCore.options.panel_opts.addw_data_attr);
346 }
347 } else {
348 elem.hide();
349 }
350 });
351 // check how many widget matched
352 if (matching > 0) {
353 // hide "no results"
354 $(VBOCore.options.panel_opts.search_nores).hide();
355 } else {
356 // show "no results"
357 $(VBOCore.options.panel_opts.search_nores).show();
358 }
359 }
360
361 // check for shortcuts
362 if (!e.key) {
363 return;
364 }
365
366 // handle Enter key press to add a widget
367 if (e.key === "Enter") {
368 // on Enter key pressed, add the first matching widget or the focussed one
369 var load_wid_id = null;
370 var focussed_wid = $(VBOCore.options.panel_opts.addwfs_selector + ':visible').first();
371
372 if (focussed_wid.length) {
373 load_wid_id = focussed_wid.attr(VBOCore.options.panel_opts.addw_data_attr);
374 } else if (first_matched) {
375 load_wid_id = first_matched;
376 }
377
378 if (!load_wid_id) {
379 // no widget to render found
380 return;
381 }
382
383 if (e.shiftKey === true) {
384 // widget modal rendering
385 VBOCore.renderModalWidget(load_wid_id);
386
387 return;
388 }
389
390 // widget multitask panel rendering
391 VBOCore.addWidgetToPanel(load_wid_id);
392 $(VBOCore.options.panel_opts.search_selector).trigger('blur');
393
394 return;
395 }
396
397 // handle arrow keys selection
398 if (matching > 0 && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
399 // on arrow key pressed, select the next or prev widget
400 var addws_element = $(VBOCore.options.panel_opts.addws_selector);
401 var addws_cont_pos = addws_element.offset().top;
402 var addws_otheight = addws_element.outerHeight();
403 var addws_scrolltp = addws_element.scrollTop();
404
405 if (e.key === 'ArrowDown') {
406 var default_widg = $(VBOCore.options.panel_opts.addw_selector + ':visible').first();
407 } else {
408 var default_widg = $(VBOCore.options.panel_opts.addw_selector + ':visible').last();
409 }
410 var focussed_wid = $(VBOCore.options.panel_opts.addwfs_selector + ':visible').first();
411 var addw_height = default_widg.outerHeight();
412 var focussed_pos = default_widg.offset().top + addw_height;
413
414 if (focussed_wid.length) {
415 focussed_wid.removeClass(widget_focus_class);
416 if (e.key === 'ArrowDown') {
417 var goto_wid = focussed_wid.nextAll(VBOCore.options.panel_opts.addw_selector + ':visible').first();
418 } else {
419 var goto_wid = focussed_wid.prevAll(VBOCore.options.panel_opts.addw_selector + ':visible').first();
420 }
421 if (goto_wid.length) {
422 goto_wid.addClass(widget_focus_class);
423 focussed_pos = goto_wid.offset().top + addw_height;
424 } else {
425 default_widg.addClass(widget_focus_class);
426 }
427 } else {
428 default_widg.addClass(widget_focus_class);
429 }
430
431 if (focussed_pos > (addws_cont_pos + addws_otheight)) {
432 addws_element.scrollTop(focussed_pos - addws_cont_pos - addw_height + addws_scrolltp);
433 } else if (focussed_pos < 0) {
434 addws_element.scrollTop(0);
435 }
436 }
437 });
438
439 // register listener for adding widgets
440 $(VBOCore.options.panel_opts.addw_selector).on('click', function(e) {
441 var widget_id = $(this).attr(VBOCore.options.panel_opts.addw_data_attr);
442 if (!widget_id || !widget_id.length) {
443 return false;
444 }
445
446 // determine widget rendering method
447 if (e && e.target) {
448 let cktarget = $(e.target);
449 if (e.shiftKey === true || (cktarget.hasClass(VBOCore.options.panel_opts.addw_modal_cls) || cktarget.parent().hasClass(VBOCore.options.panel_opts.addw_modal_cls))) {
450 // widget modal rendering
451 VBOCore.renderModalWidget(widget_id);
452
453 return;
454 }
455 }
456
457 // widget multitask panel rendering
458 VBOCore.addWidgetToPanel(widget_id);
459 });
460
461 // register listener for updating multitask sidepanel with debounce
462 document.addEventListener(VBOCore.options.multitask_save_event, VBOCore.debounceEvent(VBOCore.saveMultitasking, 1000));
463
464 // subscribe to event for multitask shortcut
465 document.addEventListener(VBOCore.multitask_shortcut_event, function() {
466 // toggle multitask panel
467 $(VBOCore.options.panel_opts.btn_trigger).trigger('click');
468 });
469
470 // subscribe to event for multitask search focus shortcut
471 document.addEventListener(VBOCore.multitask_searchfs_event, function() {
472 // focus search multitask widgets
473 $(VBOCore.options.panel_opts.search_selector).trigger('focus');
474 });
475
476 // register click event on edit widgets button
477 $(VBOCore.options.panel_opts.editw_selector).on('click', function() {
478 VBOCore.toggleWidgetsPanelEditing(null);
479 });
480
481 // register detach widget buttons
482 $('.' + VBOCore.options.panel_opts.dtcwidget_class).each(function() {
483 let widget_wrapper = $(this).parent(VBOCore.options.panel_opts.wclass_base_sel);
484 let detach_widget_id = widget_wrapper.attr(VBOCore.options.panel_opts.addw_data_attr);
485 let detach_to_target = widget_wrapper.find('.' + VBOCore.options.panel_opts.dtctarget_class);
486 if (detach_to_target.length) {
487 // move detach wrapper onto the target (widget head)
488 $(this).prependTo(detach_to_target);
489 }
490
491 if (!detach_widget_id) {
492 return;
493 }
494
495 // register detach action
496 $(this).on('click', function() {
497 // detach widget, meaning do a modal rendering
498 VBOCore.renderModalWidget(detach_widget_id);
499 });
500 });
501 }
502
503 /**
504 * Registers the blur event for the search widgets input.
505 */
506 static registerSearchWidgetsBlur() {
507 if (VBOCore.options.active_listeners.hasOwnProperty('registerSearchWidgetsBlur')) {
508 // listener is already registered
509 return true;
510 }
511
512 $(VBOCore.options.panel_opts.search_selector).on('blur', function(e) {
513 if (e && e.relatedTarget) {
514 if (e.relatedTarget.classList.contains(VBOCore.options.panel_opts.addw_selector.replace('.', ''))) {
515 // add new widget was clicked, abort hiding process or click event won't fire on target element
516 return;
517 }
518 }
519 var keyword = $(this).val();
520 if (!keyword.length) {
521 // hide add-widgets container
522 $(VBOCore.options.panel_opts.addws_selector).hide();
523 }
524 });
525
526 // register flag for listener active
527 VBOCore.options.active_listeners['registerSearchWidgetsBlur'] = 1;
528 }
529
530 /**
531 * Removes the blur event handler for the search widgets input.
532 */
533 static unregisterSearchWidgetsBlur() {
534 if (!VBOCore.options.active_listeners.hasOwnProperty('registerSearchWidgetsBlur')) {
535 // nothing to unregister
536 return true;
537 }
538
539 $(VBOCore.options.panel_opts.search_selector).off('blur');
540
541 // delete flag for listener active
542 delete VBOCore.options.active_listeners['registerSearchWidgetsBlur'];
543 }
544
545 /**
546 * Adds a widget identifier to the multitask panel.
547 *
548 * @param string widget_id the widget identifier string to add.
549 */
550 static addWidgetToPanel(widget_id) {
551 if (!VBOCore.options.widget_ajax_uri || !VBOCore.options.panel_opts || !Object.keys(VBOCore.options.panel_opts).length) {
552 throw new Error('Multitask panel options are missing');
553 }
554 // prepend container to panel
555 var widget_classes = [VBOCore.options.panel_opts.wclass_base_sel.replace('.', ''), VBOCore.options.panel_opts.cur_widget_cls];
556 var widget_div = '<div class="' + widget_classes.join(' ') + '" ' + VBOCore.options.panel_opts.addw_data_attr + '="' + widget_id + '" style="display: none;"></div>';
557 var widget_elem = $(widget_div);
558 $(VBOCore.options.panel_opts.actws_selector).prepend(widget_elem);
559
560 // always hide add-widgets container
561 $(VBOCore.options.panel_opts.addws_selector).hide();
562
563 // trigger debounced map saving event
564 VBOCore.emitMultitaskEvent();
565
566 // register listener for input search blur
567 VBOCore.registerSearchWidgetsBlur();
568
569 // render widget
570 var call_method = 'render';
571 VBOCore.doAjax(
572 VBOCore.options.widget_ajax_uri,
573 {
574 widget_id: widget_id,
575 call: call_method,
576 vbo_page: VBOCore.options.current_page,
577 vbo_uri: VBOCore.options.current_page_uri,
578 multitask: 1,
579 },
580 (response) => {
581 // display widgets editing button
582 VBOCore.toggleWidgetsPanelEditing(true);
583 // parse response
584 try {
585 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
586 if (obj_res.hasOwnProperty(call_method)) {
587 // populate widget HTML content and display it
588 widget_elem.html(obj_res[call_method]).fadeIn();
589
590 // always scroll active widgets list to top
591 $(VBOCore.options.panel_opts.actws_selector).scrollTop(0);
592
593 // register detach widget button
594 setTimeout(() => {
595 let detach_elem = $('<div></div>').addClass(VBOCore.options.panel_opts.dtcwidget_class).html(VBOCore.options.panel_opts.dtcwidget_icn);
596 let detach_to_target = widget_elem.find('.' + VBOCore.options.panel_opts.dtctarget_class);
597 if (detach_to_target.length) {
598 // move detach wrapper onto the target (widget head)
599 detach_elem.prependTo(detach_to_target);
600 }
601 // register detach action
602 detach_elem.on('click', function() {
603 // detach widget, meaning do a modal rendering
604 VBOCore.renderModalWidget(widget_id);
605 });
606 }, 500);
607 } else {
608 console.error('Unexpected JSON response', obj_res);
609 }
610 } catch(err) {
611 console.error('could not parse JSON response', err, response);
612 }
613 },
614 (error) => {
615 console.error(error.responseText);
616 }
617 );
618 }
619
620 /**
621 * Toggles the edit mode of the multitask widgets panel.
622 *
623 * @param bool added true if a widget was just added, false if it was just removed.
624 */
625 static toggleWidgetsPanelEditing(added) {
626 // check if we are currently editing
627 var is_editing = ($('.' + VBOCore.options.panel_opts.editmode_class).length > 0);
628
629 // the triggerer button
630 var triggerer = $(VBOCore.options.panel_opts.editw_selector);
631
632 // check added action status
633 if (added === true) {
634 // show button for edit mode
635 triggerer.show();
636 return;
637 }
638
639 // grab all widgets
640 var editing_widgets = $(VBOCore.options.panel_opts.wclass_base_sel);
641
642 if (added === false) {
643 if (!editing_widgets.length) {
644 // hide button for edit mode after removing the last widget
645 triggerer.removeClass(VBOCore.options.panel_opts.editw_selector.substr(1) + '-active').hide();
646 }
647 return;
648 }
649
650 if (is_editing) {
651 // deactivate editing mode
652 editing_widgets.removeClass(VBOCore.options.panel_opts.editmode_class);
653 $('.' + VBOCore.options.panel_opts.rmwidget_class).remove();
654 // toggle triggerer button active class
655 triggerer.removeClass(VBOCore.options.panel_opts.editw_selector.substr(1) + '-active');
656 } else {
657 // activate editing mode by looping through all widgets
658 editing_widgets.each(function() {
659 // build remove-widget element
660 var rm_widget = $('<div></div>').addClass(VBOCore.options.panel_opts.rmwidget_class).on('click', function() {
661 VBOCore.removeWidgetFromPanel(this);
662 }).html(VBOCore.options.panel_opts.rmwidget_icn);
663 // add editing class and prepend removing element
664 $(this).addClass(VBOCore.options.panel_opts.editmode_class).prepend(rm_widget);
665 });
666 // toggle triggerer button active class
667 triggerer.addClass(VBOCore.options.panel_opts.editw_selector.substr(1) + '-active');
668 }
669 }
670
671 /**
672 * Handles the removal of a widget from the multitask panel.
673 *
674 * @param object element
675 */
676 static removeWidgetFromPanel(element) {
677 if (!element) {
678 console.error('Invalid widget element to remove', element);
679 return false;
680 }
681 var widget_cont = $(element).parent(VBOCore.options.panel_opts.wclass_base_sel);
682 if (!widget_cont || !widget_cont.length) {
683 console.error('Could not find widget container to remove', element);
684 return false;
685 }
686 var widget_id = widget_cont.attr(VBOCore.options.panel_opts.addw_data_attr);
687 if (!widget_id || !widget_id.length) {
688 console.error('Empty widget id to remove', element);
689 return false;
690 }
691 // find the index of the widget to remove in the panel
692 var widget_index = $(VBOCore.options.panel_opts.wclass_base_sel).index(widget_cont);
693 if (widget_index < 0) {
694 console.error('Empty widget index to remove', widget_cont);
695 return false;
696 }
697 // make sure the index in the array matches the id
698 if (!VBOCore.options.admin_widgets.hasOwnProperty(widget_index) || VBOCore.options.admin_widgets[widget_index]['id'] != widget_id) {
699 console.error('Unmatching widget index or id', VBOCore.options.admin_widgets, widget_index, widget_id);
700 return false;
701 }
702 // remove this widget from the array
703 VBOCore.options.admin_widgets.splice(widget_index, 1);
704
705 // remove element from document
706 widget_cont.remove();
707
708 // check widgets editing button status
709 VBOCore.toggleWidgetsPanelEditing(false);
710
711 // trigger debounced map saving event
712 VBOCore.emitMultitaskEvent();
713
714 if (!VBOCore.options.admin_widgets.length) {
715 // unregister listener for input search blur
716 VBOCore.unregisterSearchWidgetsBlur();
717 }
718 }
719
720 /**
721 * Emits an event related to the multitask features or a custom event, with optional data.
722 */
723 static emitMultitaskEvent(ev_name, ev_data) {
724 var def_ev_name = VBOCore.options.multitask_save_event;
725 if (typeof ev_name === 'string') {
726 def_ev_name = ev_name;
727 }
728
729 if (typeof ev_data !== 'undefined' && ev_data) {
730 // trigger the custom event
731 document.dispatchEvent(new CustomEvent(def_ev_name, {bubbles: true, detail: ev_data}));
732 return;
733 }
734
735 // trigger the event
736 document.dispatchEvent(new Event(def_ev_name));
737 }
738
739 /**
740 * Proxy for dispatching an event to the document with optional data.
741 */
742 static emitEvent(ev_name, ev_data) {
743 if (typeof ev_name !== 'string' || !ev_name.length) {
744 return;
745 }
746
747 return VBOCore.emitMultitaskEvent(ev_name, ev_data);
748 }
749
750 /**
751 * Attempts to save the multitask widgets for this page.
752 */
753 static saveMultitasking() {
754 // gather the list of active widgets
755 var active_widgets = [];
756 var cur_admin_widgets = [];
757 $(VBOCore.options.panel_opts.actws_selector).find(VBOCore.options.panel_opts.wclass_base_sel).each(function() {
758 var widget_id = $(this).attr(VBOCore.options.panel_opts.addw_data_attr);
759 if (widget_id && widget_id.length) {
760 // push id in list
761 active_widgets.push(widget_id);
762 // push object with dummy name for global widgets
763 cur_admin_widgets.push({
764 id: widget_id,
765 name: widget_id,
766 });
767 }
768 });
769
770 // update multitask widgets map for this page
771 VBOCore.doAjax(
772 VBOCore.options.multitask_ajax_uri,
773 {
774 call: 'updateMultitaskingMap',
775 call_args: [
776 VBOCore.options.current_page,
777 active_widgets,
778 0
779 ],
780 },
781 (response) => {
782 try {
783 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
784 if (obj_res.hasOwnProperty('result') && obj_res['result']) {
785 // set current widgets
786 VBOCore.setOptions({
787 admin_widgets: cur_admin_widgets
788 });
789 } else {
790 console.error('Unexpected or invalid JSON response', response);
791 }
792 } catch(err) {
793 console.error('could not parse JSON response', err, response);
794 }
795 },
796 (error) => {
797 console.error(error.responseText);
798 }
799 );
800 }
801
802 /**
803 * Sets up the browser notifications within the multitask panel, if supported.
804 */
805 static setupNotifications() {
806 if (!('Notification' in window)) {
807 // browser does not support notifications
808 $(VBOCore.options.panel_opts.notif_selector).hide();
809 return false;
810 }
811
812 if (Notification.permission && Notification.permission === 'granted') {
813 // permissions were granted already
814 $(VBOCore.options.panel_opts.notif_selector)
815 .addClass(VBOCore.options.panel_opts.notif_on_class)
816 .attr('title', VBOCore.options.tn_texts.notifs_enabled);
817 return true;
818 }
819
820 // notifications supported, but perms not granted
821 $(VBOCore.options.panel_opts.notif_selector)
822 .addClass(VBOCore.options.panel_opts.notif_off_class)
823 .attr('title', VBOCore.options.tn_texts.notifs_disabled);
824
825 // register click-event listener on button to enable notifications
826 $(VBOCore.options.panel_opts.notif_selector).on('click', function() {
827 VBOCore.requestNotifPerms();
828 });
829
830 // subscribe to the multitask-panel-open event to show the status of the notifications
831 document.addEventListener(VBOCore.multitask_open_event, function() {
832 if (VBOCore.notificationsEnabled() === false) {
833 // add "shaking" class to notifications button
834 $(VBOCore.options.panel_opts.notif_selector).addClass('shaking');
835 }
836 });
837
838 // subscribe to the multitask-panel-close event to update the status of the notifications
839 document.addEventListener(VBOCore.multitask_close_event, function() {
840 // always remove "shaking" class from notifications button
841 $(VBOCore.options.panel_opts.notif_selector).removeClass('shaking');
842 });
843 }
844
845 /**
846 * Sets up the browser notifications within the given selector, if supported.
847 *
848 * @param string selector the element query selector.
849 */
850 static suggestNotifications(selector) {
851 if (!selector) {
852 return false;
853 }
854
855 if (!('Notification' in window)) {
856 // browser does not support notifications
857 $(selector).hide();
858 return false;
859 }
860
861 if (Notification.permission && Notification.permission === 'granted') {
862 // permissions were granted already
863 $(selector)
864 .addClass(VBOCore.options.panel_opts.notif_on_class)
865 .attr('title', VBOCore.options.tn_texts.notifs_enabled);
866 return true;
867 }
868
869 // notifications supported, but perms not granted
870 $(selector)
871 .addClass(VBOCore.options.panel_opts.notif_off_class)
872 .attr('title', VBOCore.options.tn_texts.notifs_disabled);
873
874 // register click-event listener on button to enable notifications
875 $(selector).on('click', function() {
876 VBOCore.requestNotifPerms(selector);
877 });
878
879 // add "shaking" class to make the selector more appealing
880 $(selector).addClass('shaking');
881
882 // remove the "shaking" class after some time
883 setTimeout(() => {
884 $(selector).removeClass('shaking');
885 }, 2000);
886 }
887
888 /**
889 * Tells whether the notifications are enabled, disabled, not supported.
890 */
891 static notificationsEnabled() {
892 if (!('Notification' in window)) {
893 // not supported
894 return null;
895 }
896
897 if (Notification.permission && Notification.permission === 'granted') {
898 // enabled
899 return true;
900 }
901
902 // disabled
903 return false;
904 }
905
906 /**
907 * Attempts to request the notifications permissions to the browser.
908 * For security reasons, this should run upon a user gesture (click).
909 *
910 * @param string selector optional element query selector.
911 */
912 static requestNotifPerms(selector) {
913 if (!('Notification' in window)) {
914 // browser does not support notifications
915 return false;
916 }
917
918 // run permissions request in a try-catch statement to support all browsers
919 try {
920 // handle promise-based version to request permissions
921 Notification.requestPermission().then((permission) => {
922 VBOCore.handleNotifPerms(permission, selector);
923 });
924 } catch(e) {
925 // run the callback-based version
926 Notification.requestPermission(function(permission) {
927 VBOCore.handleNotifPerms(permission, selector);
928 });
929 }
930 }
931
932 /**
933 * Handles the notifications permission response (from callback or promise resolved).
934 *
935 * @param string selector optional element query selector.
936 */
937 static handleNotifPerms(permission, selector) {
938 // check the permission status from the Notification object interface
939 if ((Notification.permission && Notification.permission === 'granted') || (typeof permission === 'string' && permission === 'granted')) {
940 // permissions granted!
941 $((selector || VBOCore.options.panel_opts.notif_selector))
942 .removeClass(VBOCore.options.panel_opts.notif_off_class)
943 .addClass(VBOCore.options.panel_opts.notif_on_class)
944 .attr('title', VBOCore.options.tn_texts.notifs_enabled);
945
946 // dispatch an immediate notification to congratulate with the activation
947 let notif = new Notification(VBOCore.options.tn_texts.congrats, {
948 body: VBOCore.options.tn_texts.notifs_enabled,
949 tag: 'vbo_notification_congrats'
950 });
951
952 return true;
953 } else {
954 // permissions denied :(
955 $((selector || VBOCore.options.panel_opts.notif_selector))
956 .removeClass(VBOCore.options.panel_opts.notif_on_class)
957 .addClass(VBOCore.options.panel_opts.notif_off_class)
958 .attr('title', VBOCore.options.tn_texts.notifs_disabled);
959
960 // show alert message
961 console.error('Permission denied for enabling browser notifications', permission);
962 alert(VBOCore.options.tn_texts.notifs_disabled_help);
963 }
964
965 return false;
966 }
967
968 /**
969 * Given a date-time string, returns a Date object representation.
970 *
971 * @param string dtime_str the date-time string in "Y-m-d H:i:s" format.
972 */
973 static getDateTimeObject(dtime_str) {
974 // instantiate a new date object
975 var date_obj = new Date();
976
977 // parse date-time string
978 let dtime_parts = dtime_str.split(' ');
979 let date_parts = dtime_parts[0].split('-');
980 if (dtime_parts.length != 2 || date_parts.length != 3) {
981 // invalid military format
982 return date_obj;
983 }
984 let time_parts = dtime_parts[1].split(':');
985
986 // set accurate date-time values
987 date_obj.setFullYear(date_parts[0]);
988 date_obj.setMonth((parseInt(date_parts[1]) - 1));
989 date_obj.setDate(parseInt(date_parts[2]));
990 date_obj.setHours(parseInt(time_parts[0]));
991 date_obj.setMinutes(parseInt(time_parts[1]));
992 date_obj.setSeconds(0);
993 date_obj.setMilliseconds(0);
994
995 // return the accurate date object
996 return date_obj;
997 }
998
999 /**
1000 * Given a list of schedules, enqueues notifications to watch.
1001 *
1002 * @param array|object schedules list of or one notification object(s).
1003 *
1004 * @return bool
1005 */
1006 static enqueueNotifications(schedules) {
1007 if (!Array.isArray(schedules) || !schedules.length) {
1008 if (typeof schedules === 'object' && schedules.hasOwnProperty('dtime')) {
1009 // convert the single schedule to an array
1010 schedules = [schedules];
1011 } else {
1012 // invalid argument passed
1013 return false;
1014 }
1015 }
1016
1017 for (var i in schedules) {
1018 if (!schedules.hasOwnProperty(i) || typeof schedules[i] !== 'object') {
1019 continue;
1020 }
1021 VBOCore.notifications.push(schedules[i]);
1022 }
1023
1024 // setup the timeouts to schedule the notifications
1025 return VBOCore.scheduleNotifications();
1026 }
1027
1028 /**
1029 * Schedule the trigger timings for each notification.
1030 */
1031 static scheduleNotifications() {
1032 if (!VBOCore.notifications.length) {
1033 // no notifications to be scheduled
1034 return false;
1035 }
1036 if (VBOCore.notificationsEnabled() !== true) {
1037 // notifications not enabled
1038 console.info('Browser notifications disabled or unsupported.');
1039 }
1040
1041 // gather current date-timing information
1042 const now_date = new Date();
1043 const now_time = now_date.getTime();
1044
1045 // parse all notifications to schedule the timers if not set
1046 for (let i = 0; i < VBOCore.notifications.length; i++) {
1047 let notif = VBOCore.notifications[i];
1048
1049 if (typeof notif !== 'object' || !notif.hasOwnProperty('dtime')) {
1050 // invalid notification object, unset it
1051 VBOCore.notifications.splice(i, 1);
1052 continue;
1053 }
1054
1055 // check if timer has been set
1056 if (!notif.hasOwnProperty('id_timer')) {
1057 // estimate trigger timing
1058 let in_ms = 0;
1059 // check for imminent notifications
1060 if (typeof notif.dtime === 'string' && notif.dtime.indexOf('now') >= 0) {
1061 // imminent ones will be delayed by one second
1062 in_ms = 1000;
1063 } else {
1064 // check overdue date-time (notif.dtime can also be a Date object instance)
1065 let nexp = VBOCore.getDateTimeObject(notif.dtime);
1066 in_ms = nexp.getTime() - now_time;
1067 }
1068 if (in_ms > 0) {
1069 // schedule notification trigger
1070 let id_timer = setTimeout(() => {
1071 VBOCore.dispatchNotification(notif);
1072 }, in_ms);
1073 // set timer on notification object
1074 VBOCore.notifications[i]['id_timer'] = id_timer;
1075 }
1076 }
1077 }
1078
1079 return true;
1080 }
1081
1082 /**
1083 * Deregister all scheduled notifications.
1084 */
1085 static unscheduleNotifications() {
1086 if (!VBOCore.notifications.length) {
1087 // no notifications scheduled
1088 return false;
1089 }
1090
1091 for (let i = 0; i < VBOCore.notifications.length; i++) {
1092 let notif = VBOCore.notifications[i];
1093
1094 if (typeof notif === 'object' && notif.hasOwnProperty('id_timer')) {
1095 // unset timeout for this notification
1096 clearTimeout(notif['id_timer']);
1097 }
1098 }
1099
1100 // reset pool
1101 VBOCore.notifications = [];
1102 }
1103
1104 /**
1105 * Update or delete a previously scheduled notification.
1106 *
1107 * @param object match_props map of properties to match.
1108 * @param string|number newdtime the new date time to schedule (0 for deleting).
1109 *
1110 * @return null|bool true only if a notification matched.
1111 */
1112 static updateNotification(match_props, newdtime) {
1113 if (!VBOCore.notifications.length) {
1114 // no notifications set, terminate
1115 return null;
1116 }
1117
1118 if (typeof match_props !== 'object') {
1119 // no properties to match the notification
1120 return null;
1121 }
1122
1123 // gather current date-timing information
1124 const now_date = new Date();
1125 const now_time = now_date.getTime();
1126
1127 // parse all notifications scheduled
1128 for (let i = 0; i < VBOCore.notifications.length; i++) {
1129 let notif = VBOCore.notifications[i];
1130
1131 let all_matched = true;
1132 let to_matching = false;
1133 for (let prop in match_props) {
1134 if (!match_props.hasOwnProperty(prop)) {
1135 continue;
1136 }
1137 to_matching = true;
1138 if (!notif.hasOwnProperty(prop) || notif[prop] != match_props[prop]) {
1139 all_matched = false;
1140 break;
1141 }
1142 }
1143
1144 if (all_matched && to_matching) {
1145 // notification object found
1146 if (notif.hasOwnProperty('id_timer')) {
1147 // unset previous timeout for this notification
1148 clearTimeout(notif['id_timer']);
1149 }
1150 // update or delete scheduled notification
1151 if (newdtime === 0) {
1152 // delete notification from queue
1153 VBOCore.notifications.splice(i, 1);
1154 } else {
1155 // update timing scheduler
1156 let in_ms = 0;
1157 // check for imminent notifications
1158 if (typeof newdtime === 'string' && newdtime.indexOf('now') >= 0) {
1159 // imminent ones will be delayed by one second
1160 in_ms = 1000;
1161 } else {
1162 // check overdue date-time (newdtime can also be a Date object instance)
1163 let nexp = VBOCore.getDateTimeObject(newdtime);
1164 in_ms = nexp.getTime() - now_time;
1165 }
1166 if (in_ms > 0) {
1167 // schedule notification trigger
1168 let id_timer = setTimeout(() => {
1169 VBOCore.dispatchNotification(notif);
1170 }, in_ms);
1171 // set timer on notification object
1172 VBOCore.notifications[i]['id_timer'] = id_timer;
1173 }
1174 // update date-time value on notification object
1175 VBOCore.notifications[i]['dtime'] = newdtime;
1176 }
1177
1178 // terminate parsing and return true
1179 return true;
1180 }
1181 }
1182
1183 // notification object not found
1184 return false;
1185 }
1186
1187 /**
1188 * Dispatch the notification object.
1189 * Expected notification properties:
1190 *
1191 * {
1192 * id: number
1193 * type: string
1194 * dtime: string|Date
1195 * build_url: string|null
1196 * }
1197 *
1198 * @param object notif the notification object.
1199 */
1200 static dispatchNotification(notif) {
1201 if (typeof notif !== 'object') {
1202 return false;
1203 }
1204
1205 // subscribe to building notification data
1206 VBOCore.buildNotificationData(notif).then((data) => {
1207 // dispatch the notification
1208
1209 // check if the click event should be registered
1210 let func_nodes;
1211 if (data.onclick && typeof data.onclick === 'string') {
1212 let callback_parts = data.onclick.split('.');
1213 while (callback_parts.length) {
1214 // compose window static method string to avoid using eval()
1215 let tmp = callback_parts.shift();
1216 if (!func_nodes) {
1217 func_nodes = window[tmp];
1218 } else {
1219 func_nodes = func_nodes[tmp];
1220 }
1221 }
1222 }
1223
1224 // prepare properties to delete the notification from queue
1225 let match_props = {};
1226 for (let prop in notif) {
1227 if (!notif.hasOwnProperty(prop) || prop == 'id_timer') {
1228 continue;
1229 }
1230 match_props[prop] = notif[prop];
1231 }
1232
1233 // check browser Notifications API
1234 if (VBOCore.notificationsEnabled() !== true) {
1235 // notifications not enabled, fallback to toast message
1236 let toast_notif_data = {
1237 title: data.title,
1238 body: data.message,
1239 icon: data.icon,
1240 delay: {
1241 min: 6000,
1242 max: 20000,
1243 tolerance: 4000,
1244 },
1245 action: () => {
1246 VBOToast.dispose(true);
1247 },
1248 sound: VBOCore.options.notif_audio_url
1249 };
1250 if (func_nodes) {
1251 toast_notif_data.action = function() {
1252 func_nodes(data);
1253 };
1254 } else if (typeof data.onclick === 'function') {
1255 toast_notif_data.action = function() {
1256 data.onclick.call(data);
1257 };
1258 }
1259 VBOToast.enqueue(new VBOToastMessage(toast_notif_data));
1260
1261 // delete dispatched notification from queue
1262 VBOCore.updateNotification(match_props, 0);
1263
1264 return;
1265 }
1266
1267 // use the browser's native Notifications API
1268 let browser_notif = new Notification(data.title, {
1269 body: data.message,
1270 icon: data.icon,
1271 tag: 'vbo_notification'
1272 });
1273
1274 if (func_nodes) {
1275 // register notification click event
1276 browser_notif.addEventListener('click', () => {
1277 func_nodes(data);
1278 });
1279 } else if (typeof data.onclick === 'function') {
1280 browser_notif.addEventListener('click', () => {
1281 data.onclick.call(data);
1282 });
1283 }
1284
1285 // delete dispatched notification from queue
1286 VBOCore.updateNotification(match_props, 0);
1287
1288 }).catch((error) => {
1289 console.error(error);
1290 });
1291 }
1292
1293 /**
1294 * Asynchronous build of the notification data object for dispatch.
1295 * Minimum expected notification display data properties:
1296 *
1297 * {
1298 * title: string
1299 * message: string
1300 * icon: string
1301 * onclick: function
1302 * }
1303 *
1304 * @param object notif the scheduled notification object.
1305 *
1306 * @return Promise
1307 */
1308 static buildNotificationData(notif) {
1309 return new Promise((resolve, reject) => {
1310 // notification object validation
1311 if (typeof notif !== 'object') {
1312 reject('Invalid notification object');
1313 return;
1314 }
1315
1316 if (!notif.hasOwnProperty('build_url') || !notif.build_url) {
1317 // building callback not necessary
1318 if (!notif.title && !notif.message) {
1319 reject('Unexpected notification object');
1320 return;
1321 }
1322 // we expect the notification to be built already
1323 resolve(notif);
1324 return;
1325 }
1326
1327 // build the notification data
1328 VBOCore.doAjax(
1329 notif.build_url,
1330 {
1331 payload: JSON.stringify(notif)
1332 },
1333 (response) => {
1334 // parse response
1335 try {
1336 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1337 if (obj_res.hasOwnProperty('title')) {
1338 resolve(obj_res);
1339 } else {
1340 reject('Unexpected JSON response');
1341 }
1342 } catch(err) {
1343 reject('could not parse JSON response');
1344 }
1345 },
1346 (error) => {
1347 reject(error.responseText);
1348 }
1349 );
1350 });
1351 }
1352
1353 /**
1354 * Handle a navigation towards a given URL.
1355 * Common handler for browser notifications click.
1356 *
1357 * @param object data notification display data payload.
1358 */
1359 static handleGoto(data) {
1360 if (typeof data !== 'object' || !data.hasOwnProperty('gotourl') || !data.gotourl) {
1361 console.error('Could not handle the goto operation', data);
1362 return;
1363 }
1364
1365 if (typeof data.openWindow !== 'undefined' || typeof document === 'undefined') {
1366 // open a new window
1367 window.open(data.gotourl);
1368 return;
1369 }
1370
1371 // redirect
1372 document.location.href = data.gotourl;
1373 }
1374
1375 /**
1376 * Handle the display of a notification through a widget.
1377 * Common handler for browser notifications displayed through
1378 * a widget modal rendering.
1379 *
1380 * @param object data notification display data payload.
1381 */
1382 static handleDisplayWidgetNotification(data) {
1383 try {
1384 // validate handler
1385 if (!VBOCore.options.is_vbo && !VBOCore.options.is_vcm) {
1386 throw new Error('Vik Booking must handle this');
1387 }
1388
1389 // validate payload
1390 if (typeof data !== 'object' || !data.hasOwnProperty('widget_id') || !data['widget_id']) {
1391 throw new Error('Invalid widget payload');
1392 }
1393
1394 // parse handler
1395 if (VBOCore.options.is_vcm) {
1396 // the operation is handled by Vik Channel Manager
1397 VBOCore.loadAdminWidgetAssets(data).then((assets) => {
1398 // append assets to DOM
1399 assets.forEach((asset) => {
1400 if (!$('link#' + asset['id']).length) {
1401 $('head').append('<link rel="stylesheet" id="' + asset['id'] + '" href="' + asset['href'] + '" media="all" />');
1402 }
1403 });
1404 // widget modal rendering handled by Vik Channel Manager
1405 let modal_data = VBOCore.renderModalWidget(data['widget_id'], data, false);
1406 if (modal_data.hasOwnProperty('dismissed_event')) {
1407 // register event to unload all assets
1408 document.addEventListener(modal_data['dismissed_event'], () => {
1409 assets.forEach((asset) => {
1410 if ($('link#' + asset['id']).length) {
1411 $('link#' + asset['id']).remove();
1412 }
1413 });
1414 });
1415 }
1416 }).catch((error) => {
1417 throw new Error(error);
1418 });
1419 } else {
1420 // widget modal rendering handled by Vik Booking
1421 VBOCore.renderModalWidget(data['widget_id'], data, false);
1422 }
1423 } catch(e) {
1424 // fallback to a regular link
1425 VBOCore.handleGoto(data);
1426 }
1427 }
1428
1429 /**
1430 * Asynchronous loading of CSS assets required to render
1431 * an admin widget outside Vik Booking.
1432 *
1433 * @param object data the optional widget payload data.
1434 *
1435 * @return Promise
1436 */
1437 static loadAdminWidgetAssets(data) {
1438 return new Promise((resolve, reject) => {
1439 // the remote assets URI must be set
1440 if (!VBOCore.options.assets_ajax_uri) {
1441 reject('Missing remote assets URI');
1442 return;
1443 }
1444
1445 if (typeof data !== 'object') {
1446 data = {};
1447 }
1448
1449 // make the request
1450 VBOCore.doAjax(
1451 VBOCore.options.assets_ajax_uri,
1452 data,
1453 (response) => {
1454 // parse response
1455 try {
1456 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1457 if (!Array.isArray(obj_res)) {
1458 reject('Unexpected JSON response');
1459 }
1460 resolve(obj_res);
1461 } catch(err) {
1462 reject('could not parse JSON response');
1463 }
1464 },
1465 (error) => {
1466 reject(error.responseText);
1467 }
1468 );
1469 });
1470 }
1471
1472 /**
1473 * Register the latest data to watch for the preloaded admin widgets.
1474 *
1475 * @param object watch_data
1476 */
1477 static registerWatchData(watch_data) {
1478 if (typeof watch_data !== 'object' || watch_data == null) {
1479 VBOCore.widgets_watch_data = null;
1480 return false;
1481 }
1482
1483 // set watch-data map
1484 VBOCore.widgets_watch_data = watch_data;
1485
1486 // schedule watching interval
1487 if (VBOCore.watch_data_interval == null) {
1488 VBOCore.watch_data_interval = window.setInterval(VBOCore.watchWidgetsData, 60000);
1489 }
1490
1491 // set up watch-data broadcast channel to connect all browsing contexts
1492 if (typeof BroadcastChannel !== 'undefined' && !VBOCore.broadcast_watch_data) {
1493 // start broadcast channel
1494 VBOCore.broadcast_watch_data = new BroadcastChannel('vikbooking_watch_data');
1495
1496 // register to the "on broadcast message received" event
1497 VBOCore.broadcast_watch_data.onmessage = (event) => {
1498 if (event && event.data) {
1499 // update watch data map for next schedule to avoid dispatching duplicate notifications
1500 VBOCore.widgets_watch_data = event.data;
1501 }
1502 };
1503 }
1504 }
1505
1506 /**
1507 * Periodic widgets data watching for new events.
1508 */
1509 static watchWidgetsData() {
1510 if (typeof VBOCore.widgets_watch_data !== 'object' || VBOCore.widgets_watch_data == null) {
1511 return;
1512 }
1513
1514 // call on new events
1515 VBOCore.doAjax(
1516 VBOCore.options.watchdata_ajax_uri,
1517 {
1518 watch_data: JSON.stringify(VBOCore.widgets_watch_data),
1519 },
1520 (response) => {
1521 try {
1522 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1523 if (obj_res.hasOwnProperty('watch_data')) {
1524 // update watch data map for next schedule
1525 VBOCore.widgets_watch_data = obj_res['watch_data'];
1526
1527 // check for notifications
1528 if (obj_res.hasOwnProperty('notifications') && Array.isArray(obj_res['notifications'])) {
1529 // dispatch notifications
1530 for (var i = 0; i < obj_res['notifications'].length; i++) {
1531 VBOCore.dispatchNotification(obj_res['notifications'][i]);
1532 }
1533
1534 // post message onto broadcast channel for any other browsing context
1535 if (VBOCore.broadcast_watch_data && obj_res['notifications'].length) {
1536 // this will avoid dispatching duplicate notifications
1537 VBOCore.broadcast_watch_data.postMessage(VBOCore.widgets_watch_data);
1538 }
1539 }
1540 } else {
1541 console.error('Unexpected or invalid JSON response', response);
1542 }
1543 } catch(err) {
1544 console.error('could not parse JSON response', err, response);
1545 }
1546 },
1547 (error) => {
1548 console.error(error.responseText);
1549 }
1550 );
1551 }
1552
1553 /**
1554 * Widget modal rendering.
1555 *
1556 * @param string widget_id the widget identifier to render.
1557 * @param any data the optional multitask data to inject.
1558 * @param bool hide_panel if false, the multitask panel elements will remain unchanged.
1559 *
1560 * @return object the multitask data injected object merged with modal options.
1561 */
1562 static renderModalWidget(widget_id, data, hide_panel) {
1563 // build the default widget payload
1564 let modal_js_id = Math.floor(Math.random() * 100000);
1565 let widget_data = {
1566 _modalRendering: 1,
1567 _modalJsId: modal_js_id,
1568 _modalTitle: VBOCore.options.tn_texts.admin_widget,
1569 };
1570
1571 if (typeof data !== 'object') {
1572 data = {};
1573 }
1574
1575 // merge default payload options with given options
1576 data = Object.assign(widget_data, data);
1577
1578 // define unique modal event names to avoid conflicts
1579 let dismiss_event = 'vbo-dismiss-widget-modal' + modal_js_id;
1580 let loading_event = 'vbo-loading-widget-modal' + modal_js_id;
1581
1582 // define the modal options
1583 let modal_options = {
1584 suffix: 'widget-modal',
1585 extra_class: 'vbo-modal-rounded vbo-modal-tall vbo-modal-nofooter',
1586 title: data._modalTitle,
1587 body_prepend: true,
1588 dismiss_event: dismiss_event,
1589 loading_event: loading_event,
1590 loading_body: VBOCore.options.default_loading_body,
1591 dismissed_event: VBOCore.options.widget_modal_dismissed + modal_js_id,
1592 event_data: widget_data,
1593 };
1594
1595 // display modal
1596 let widget_modal = VBOCore.displayModal(modal_options);
1597
1598 if (hide_panel !== false) {
1599 // blur search widget input, hide multitask panel
1600 $(VBOCore.options.panel_opts.search_selector).trigger('blur');
1601 VBOCore.emitEvent(VBOCore.multitask_shortcut_event);
1602 }
1603
1604 // start loading
1605 VBOCore.emitEvent(loading_event);
1606
1607 // render admin widget
1608 VBOCore.renderAdminWidget(widget_id, data).then((content) => {
1609 // stop loading and append widget content to modal
1610 VBOCore.emitEvent(loading_event);
1611 widget_modal.append(content);
1612 }).catch((error) => {
1613 // dismiss modal and display error
1614 VBOCore.emitEvent(dismiss_event);
1615 alert(error);
1616 });
1617
1618 return Object.assign(data, modal_options);
1619 }
1620
1621 /**
1622 * Renders an admin widget.
1623 *
1624 * @param string widget_id the widget identifier string to add.
1625 * @param any data the optional multitask data to inject.
1626 *
1627 * @return Promise
1628 */
1629 static renderAdminWidget(widget_id, data) {
1630 return new Promise((resolve, reject) => {
1631 if (!VBOCore.options.widget_ajax_uri) {
1632 reject('Could not add admin widget');
1633 return;
1634 }
1635
1636 var call_method = 'render';
1637 VBOCore.doAjax(
1638 VBOCore.options.widget_ajax_uri,
1639 {
1640 widget_id: widget_id,
1641 call: call_method,
1642 vbo_page: VBOCore.options.current_page,
1643 vbo_uri: VBOCore.options.current_page_uri,
1644 multitask: 1,
1645 multitask_data: data,
1646 },
1647 (response) => {
1648 // parse response
1649 try {
1650 var obj_res = typeof response === 'string' ? JSON.parse(response) : response;
1651 if (!obj_res.hasOwnProperty(call_method)) {
1652 reject('Unexpected JSON response');
1653 return;
1654 }
1655 resolve(obj_res[call_method]);
1656 } catch(err) {
1657 reject('could not parse JSON response');
1658 }
1659 },
1660 (error) => {
1661 reject(error.responseText);
1662 }
1663 );
1664 });
1665 }
1666
1667 /**
1668 * Helper method used to copy the text of an
1669 * input element within the clipboard.
1670 *
1671 * Clipboard copy will take effect only in case the
1672 * function is handled by a DOM event explicitly
1673 * triggered by the user, such as a "click".
1674 *
1675 * @param any input The input containing the text to copy.
1676 *
1677 * @return Promise
1678 */
1679 static copyToClipboard(input) {
1680 // register and return promise
1681 return new Promise((resolve, reject) => {
1682 // define a fallback function
1683 var fallback = function(input) {
1684 // focus the input
1685 input.focus();
1686 // select the text inside the input
1687 input.select();
1688
1689 try {
1690 // try to copy with shell command
1691 var copy = document.execCommand('copy');
1692
1693 if (copy) {
1694 // copied successfully
1695 resolve(copy);
1696 } else {
1697 // unable to copy
1698 reject(copy);
1699 }
1700 } catch (error) {
1701 // unable to exec the command
1702 reject(error);
1703 }
1704 };
1705
1706 // look for navigator clipboard
1707 if (!navigator || !navigator.clipboard) {
1708 // navigator clipboard not supported, use fallback
1709 fallback(input);
1710 return;
1711 }
1712
1713 // try to copy within the clipboard by using the navigator
1714 navigator.clipboard.writeText(input.value).then(() => {
1715 // copied successfully
1716 resolve(true);
1717 }).catch((error) => {
1718 // revert to the fallback
1719 fallback(input);
1720 });
1721 });
1722 }
1723
1724 /**
1725 * Helper method used to display a modal window dinamycally.
1726 *
1727 * @param object options The options to render the modal.
1728 *
1729 * @return object The modal content element wrapper.
1730 */
1731 static displayModal(options) {
1732 var def_options = {
1733 suffix: (Math.floor(Math.random() * 100000)) + '',
1734 extra_class: null,
1735 title: '',
1736 body: '',
1737 body_prepend: false,
1738 footer_left: null,
1739 footer_right: null,
1740 dismiss_event: null,
1741 dismissed_event: null,
1742 onDismiss: null,
1743 loading_event: null,
1744 loading_body: VBOCore.options.default_loading_body,
1745 event_data: null,
1746 };
1747
1748 // merge default options with given options
1749 options = Object.assign(def_options, options);
1750
1751 // create the modal dismiss function
1752 var modal_dismiss_fn = (e) => {
1753 custom_modal.fadeOut(400, () => {
1754 // invoke callback for onDismiss
1755 if (typeof options.onDismiss === 'function') {
1756 options.onDismiss.call(custom_modal, e);
1757 }
1758
1759 // check if modal did register to the loading event
1760 if (options.loading_event) {
1761 // we can now un-register from the loading event until a new modal is displayed and will register to it again
1762 document.removeEventListener(options.loading_event, modal_handle_loading_event_fn);
1763 }
1764
1765 // check if we should fire the given modal dismissed event
1766 if (options.dismissed_event) {
1767 VBOCore.emitEvent(options.dismissed_event, options.event_data);
1768 }
1769
1770 // remove modal from DOM
1771 custom_modal.remove();
1772 });
1773 };
1774
1775 // create the modal loading event handler function
1776 var modal_handle_loading_event_fn = (e) => {
1777 // toggle modal loading
1778 if ($('.vbo-modal-overlay-content-backdrop').length) {
1779 // hide loading
1780 $('.vbo-modal-overlay-content-backdrop').remove();
1781
1782 // do not proceed
1783 return;
1784 }
1785
1786 // show loading
1787 var modal_loading = $('<div></div>').addClass('vbo-modal-overlay-content-backdrop');
1788 var modal_loading_body = $('<div></div>').addClass('vbo-modal-overlay-content-backdrop-body');
1789 if (options.loading_body) {
1790 modal_loading_body.append(options.loading_body);
1791 }
1792 modal_loading.append(modal_loading_body);
1793
1794 // append backdrop loading to modal content
1795 modal_content.prepend(modal_loading);
1796 };
1797
1798 // build modal content
1799 var custom_modal = $('<div></div>').addClass('vbo-modal-overlay-block vbo-modal-overlay-' + options.suffix).css('display', 'block');
1800 var modal_dismiss = $('<a></a>').addClass('vbo-modal-overlay-close');
1801 modal_dismiss.on('click', modal_dismiss_fn);
1802 custom_modal.append(modal_dismiss);
1803
1804 var modal_content = $('<div></div>').addClass('vbo-modal-overlay-content vbo-modal-overlay-content-' + options.suffix);
1805 if (options.extra_class && typeof options.extra_class === 'string') {
1806 modal_content.addClass(options.extra_class);
1807 }
1808
1809 var modal_head = $('<div></div>').addClass('vbo-modal-overlay-content-head');
1810 var modal_head_close = $('<span></span>').addClass('vbo-modal-overlay-close-times').html('&times;');
1811 modal_head_close.on('click', modal_dismiss_fn);
1812 modal_head.append(options.title).append(modal_head_close);
1813
1814 var modal_body = $('<div></div>').addClass('vbo-modal-overlay-content-body vbo-modal-overlay-content-body-scroll');
1815 var modal_content_wrapper = $('<div></div>').addClass('vbo-modal-' + options.suffix + '-wrap');
1816 if (typeof options.body === 'string') {
1817 modal_content_wrapper.html(options.body);
1818 } else {
1819 modal_content_wrapper.append(options.body);
1820 }
1821 modal_body.append(modal_content_wrapper);
1822
1823 // modal footer
1824 var modal_footer = null;
1825 if (options.footer_left || options.footer_right) {
1826 modal_footer = $('<div></div>').addClass('vbo-modal-overlay-content-footer');
1827 if (options.footer_left) {
1828 var modal_footer_left = $('<div></div>').addClass('vbo-modal-overlay-content-footer-left').append(options.footer_left);
1829 modal_footer.append(modal_footer_left);
1830 }
1831 if (options.footer_right) {
1832 var modal_footer_right = $('<div></div>').addClass('vbo-modal-overlay-content-footer-right').append(options.footer_right);
1833 modal_footer.append(modal_footer_right);
1834 }
1835
1836 }
1837
1838 // finalize modal contents
1839 modal_content.append(modal_head).append(modal_body);
1840 if (modal_footer) {
1841 modal_content.append(modal_footer);
1842 }
1843 custom_modal.append(modal_content);
1844
1845 // register to the dismiss event
1846 if (options.dismiss_event) {
1847 document.addEventListener(options.dismiss_event, function vbo_core_handle_dismiss_event(e) {
1848 // make sure the same event won't propagate again, unless a new modal is displayed (multiple displayModal calls)
1849 e.target.removeEventListener(e.type, vbo_core_handle_dismiss_event);
1850
1851 // invoke the modal dismiss function
1852 modal_dismiss_fn(e);
1853 });
1854 }
1855
1856 // register to the toggle-loading event
1857 if (options.loading_event) {
1858 // let a function handle it so that removing the event listener will be doable
1859 document.addEventListener(options.loading_event, modal_handle_loading_event_fn);
1860 }
1861
1862 // append (or prepend) modal to body
1863 if ($('.vbo-modal-overlay-' + options.suffix).length) {
1864 $('.vbo-modal-overlay-' + options.suffix).remove();
1865 }
1866 if (options.body_prepend) {
1867 $('body').prepend(custom_modal);
1868 } else {
1869 $('body').append(custom_modal);
1870 }
1871
1872 // return the content wrapper element of the new modal
1873 return modal_content_wrapper;
1874 }
1875
1876 /**
1877 * Debounce technique to group a flurry of events into one single event.
1878 */
1879 static debounceEvent(func, wait, immediate) {
1880 var timeout;
1881 return function() {
1882 var context = this, args = arguments;
1883 var later = function() {
1884 timeout = null;
1885 if (!immediate) func.apply(context, args);
1886 };
1887 var callNow = immediate && !timeout;
1888 clearTimeout(timeout);
1889 timeout = setTimeout(later, wait);
1890 if (callNow) {
1891 func.apply(context, args);
1892 }
1893 }
1894 }
1895
1896 /**
1897 * Throttle guarantees a constant flow of events at a given time interval.
1898 * Runs immediately when the event takes place, but can be delayed.
1899 */
1900 static throttleEvent(method, delay) {
1901 var time = Date.now();
1902 return function() {
1903 if ((time + delay - Date.now()) < 0) {
1904 method();
1905 time = Date.now();
1906 }
1907 }
1908 }
1909
1910 /**
1911 * Tells whether localStorage is supported.
1912 *
1913 * @return boolean
1914 */
1915 static storageSupported() {
1916 return typeof localStorage !== 'undefined';
1917 }
1918
1919 /**
1920 * Gets an item from localStorage.
1921 *
1922 * @param string keyName the storage key identifier.
1923 *
1924 * @return any
1925 */
1926 static storageGetItem(keyName) {
1927 if (!VBOCore.storageSupported()) {
1928 return null;
1929 }
1930
1931 return localStorage.getItem(keyName);
1932 }
1933
1934 /**
1935 * Sets an item to localStorage.
1936 *
1937 * @param string keyName the storage key identifier.
1938 * @param any value the value to store.
1939 *
1940 * @return boolean
1941 */
1942 static storageSetItem(keyName, value) {
1943 if (!VBOCore.storageSupported()) {
1944 return false;
1945 }
1946
1947 try {
1948 if (typeof value === 'object') {
1949 value = JSON.stringify(value);
1950 }
1951
1952 localStorage.setItem(keyName, value);
1953 } catch(e) {
1954 return false;
1955 }
1956
1957 return true;
1958 }
1959
1960 /**
1961 * Removes an item from localStorage.
1962 *
1963 * @param string keyName the storage key identifier.
1964 *
1965 * @return boolean
1966 */
1967 static storageRemoveItem(keyName) {
1968 if (!VBOCore.storageSupported()) {
1969 return false;
1970 }
1971
1972 localStorage.removeItem(keyName);
1973
1974 return true;
1975 }
1976
1977 /**
1978 * Returns the name of the storage identifier for the given scope.
1979 *
1980 * @param string scope the admin menu scope.
1981 *
1982 * @return string the requested admin menu storage identifier.
1983 */
1984 static getStorageScopeName(scope) {
1985 let storage_scope_name = VBOCore.options.admin_menu_actions_nm;
1986
1987 if (typeof scope === 'string' && scope.length) {
1988 if (scope.indexOf('.') !== 0) {
1989 scope = '.' + scope;
1990 }
1991 storage_scope_name += scope;
1992 }
1993
1994 return storage_scope_name;
1995 }
1996
1997 /**
1998 * Returns a list of admin menu action objects or an empty array.
1999 *
2000 * @param string scope the admin menu scope.
2001 *
2002 * @return Array
2003 */
2004 static getAdminMenuActions(scope) {
2005 let menu_actions = VBOCore.storageGetItem(VBOCore.getStorageScopeName(scope));
2006
2007 if (!menu_actions) {
2008 return [];
2009 }
2010
2011 try {
2012 menu_actions = JSON.parse(menu_actions);
2013 if (!Array.isArray(menu_actions) || !menu_actions.length) {
2014 menu_actions = [];
2015 }
2016 } catch(e) {
2017 return [];
2018 }
2019
2020 return menu_actions;
2021 }
2022
2023 /**
2024 * Builds an admin menu action object with a proper href property.
2025 *
2026 * @param object action the action to build.
2027 *
2028 * @return object
2029 */
2030 static buildAdminMenuAction(action) {
2031 if (typeof action !== 'object' || !action || !action.hasOwnProperty('name')) {
2032 throw new Error('Invalid action object');
2033 }
2034
2035 var action_base = action.hasOwnProperty('href') && typeof action['href'] == 'string' ? action['href'] : window.location.href;
2036 var action_url;
2037
2038 if (action_base.indexOf('http') !== 0) {
2039 // relative URL
2040 action_url = new URL(action_base, window.location.href);
2041 } else {
2042 // absolute URL
2043 action_url = new URL(action_base);
2044 }
2045
2046 // build proper href with a relative URL
2047 action['href'] = action_url.pathname + action_url.search;
2048
2049 return action;
2050 }
2051
2052 /**
2053 * Registers an admin menu action object.
2054 *
2055 * @param object action the action to build.
2056 * @param string scope the admin menu scope.
2057 *
2058 * @return boolean
2059 */
2060 static registerAdminMenuAction(action, scope) {
2061 // build menu action object
2062 let menu_action_entry = VBOCore.buildAdminMenuAction(action);
2063
2064 let menu_actions = VBOCore.getAdminMenuActions(scope);
2065
2066 // make sure we are not pushing a duplicate and count pinned actions
2067 let pinned_actions = 0;
2068 let unpinned_index = [];
2069 for (let i = 0; i < menu_actions.length; i++) {
2070 if (menu_actions[i]['href'] == menu_action_entry['href']) {
2071 return false;
2072 }
2073 if (menu_actions[i].hasOwnProperty('pinned') && menu_actions[i]['pinned']) {
2074 pinned_actions++;
2075 } else {
2076 unpinned_index.push(i);
2077 }
2078 }
2079
2080 if (pinned_actions >= VBOCore.options.admin_menu_maxactions) {
2081 // no more space to register a new menu action for this admin menu
2082 return false;
2083 }
2084
2085 // splice or pop before prepending to keep current indexes
2086 let tot_menu_actions = menu_actions.length;
2087 if (++tot_menu_actions > VBOCore.options.admin_menu_maxactions) {
2088 if (unpinned_index.length) {
2089 menu_actions.splice(unpinned_index[unpinned_index.length - 1], 1);
2090 } else {
2091 menu_actions.pop();
2092 }
2093 }
2094
2095 // prepend new admin menu action
2096 menu_actions.unshift(menu_action_entry);
2097
2098 return VBOCore.storageSetItem(VBOCore.getStorageScopeName(scope), menu_actions);
2099 }
2100
2101 /**
2102 * Updates an existing admin menu action object.
2103 *
2104 * @param object action the action to build.
2105 * @param string scope the admin menu scope.
2106 * @param number index optional menu action index.
2107 *
2108 * @return boolean
2109 */
2110 static updateAdminMenuAction(action, scope, index) {
2111 // build menu action object
2112 let menu_action_entry = VBOCore.buildAdminMenuAction(action);
2113
2114 let menu_actions = VBOCore.getAdminMenuActions(scope);
2115
2116 if (!menu_actions.length) {
2117 return false;
2118 }
2119
2120 if (typeof index === 'undefined') {
2121 // find the proper index to update by href
2122 for (let i = 0; i < menu_actions.length; i++) {
2123 if (menu_actions[i]['href'] == menu_action_entry['href']) {
2124 index = i;
2125 break;
2126 }
2127 }
2128 }
2129
2130 if (isNaN(index) || !(index in menu_actions)) {
2131 // menu entry index not found
2132 return false;
2133 }
2134
2135 menu_actions[index] = menu_action_entry;
2136
2137 return VBOCore.storageSetItem(VBOCore.getStorageScopeName(scope), menu_actions);
2138 }
2139 }
2140
2141 /**
2142 * These used to be private static properties (static #options),
2143 * but they are only supported by quite recent browsers (especially Safari).
2144 * It's too risky, so we decided to keep the class properties public
2145 * without declaring them as static inside the class declaration.
2146 *
2147 * @var object
2148 */
2149 VBOCore.options = {
2150 is_vbo: false,
2151 is_vcm: false,
2152 widget_ajax_uri: null,
2153 assets_ajax_uri: null,
2154 multitask_ajax_uri: null,
2155 watchdata_ajax_uri: null,
2156 current_page: null,
2157 current_page_uri: null,
2158 client: 'admin',
2159 panel_opts: {},
2160 admin_widgets: [],
2161 notif_audio_url: '',
2162 active_listeners: {},
2163 tn_texts: {
2164 notifs_enabled: 'Browser notifications are enabled!',
2165 notifs_disabled: 'Browser notifications are disabled',
2166 notifs_disabled_help: "Could not enable browser notifications.\nThis feature is available only in secure contexts (HTTPS).",
2167 admin_widget: 'Admin widget',
2168 congrats: 'Congratulations!',
2169 },
2170 default_loading_body: '....',
2171 multitask_save_event: 'vbo-admin-multitask-save',
2172 multitask_open_event: 'vbo-admin-multitask-open',
2173 multitask_close_event: 'vbo-admin-multitask-close',
2174 multitask_shortcut_ev: 'vbo_multitask_shortcut',
2175 multitask_searchfs_ev: 'vbo_multitask_search_focus',
2176 widget_modal_rendered: 'vbo-admin-widget-modal-rendered',
2177 widget_modal_dismissed: 'vbo-widget-modal-dismissed',
2178 admin_menu_maxactions: 3,
2179 admin_menu_actions_nm: 'vikbooking.admin_menu.actions',
2180 };
2181
2182 /**
2183 * @var bool
2184 */
2185 VBOCore.side_panel_on = false;
2186
2187 /**
2188 * @var array
2189 */
2190 VBOCore.notifications = [];
2191
2192 /**
2193 * @var object
2194 */
2195 VBOCore.widgets_watch_data = null;
2196
2197 /**
2198 * @var number
2199 */
2200 VBOCore.watch_data_interval = null;
2201
2202 /**
2203 * @var object
2204 * @since 1.6.3
2205 */
2206 VBOCore.broadcast_watch_data = null;
2207
2208 })(jQuery, window);