PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.16
VikBooking Hotel Booking Engine & PMS v1.8.16
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
← All changes | admin/resources/contextmenu.js +1002 -0 1.6.2 → 1.8.16 View file →
@@ -1,0 +1,1002 @@
1 +/**
2 + * jQuery add-on used to support context menus.
3 + *
4 + * @version 2.2.5
5 + * @author E4J srl
6 + *
7 + * Here's a list of supported options.
8 + *
9 + * @param trigger string The command that should trigger the popup menu. Accepts the
10 + * following values: click|doubleclick|rightclick|hover.
11 + * Click will be used by default.
12 + * @param placement string Where the popup should be displayed in relation to the target.
13 + * Accepts the following values: auto|top|right|bottom|left|center (combined).
14 + * Auto will be used by default (at the mouse coordinates).
15 + * @param class string An optional class to use for individual styling.
16 + * @param buttons object[] A list of buttons to include within the popup menu. See the options
17 + * of the buttons for further details.
18 + * @param onShow function An optional callback to invoke when the popup menu is displayed.
19 + * @param onHide function An optional callback to invoke when the popup menu is dismissed.
20 + * @param darkMode mixed Flag for dark mode layout, which accepts 3 possible values:
21 + * true|false|null. Pass true to always force the dark mode, false to
22 + * always use the light mode, null to auto-detect the proper mode
23 + * according to the preferred theme of the browser.
24 + * @param clickable bool Flag used to check whether the root element should prevent the
25 + * browser selection by applying specific CSS rules. False by default.
26 + * @param lockScroll bool Flag used to prevent the document scroll when the context menu
27 + * pops up. True by default.
28 + * @param hideOnEsc bool Choose whether the context menu should be closed when ESC key is
29 + * pressed. Always true by default.
30 + * @param formatShortcut mixed An optional callback that can be used to format the shortcut symbols.
31 + * @param search bool Whether the context menu should display a search box to filter the buttons.
32 + * @param searchHint string An optional placeholder to use for the search box.
33 + * @param searchEmpty string The string to display in case of no matching results.
34 + * @param searchClass string An optional extra class to apply to the search item.
35 + * @param searchFocus bool Whether the search bar should grab the focus on show. True by default.
36 + *
37 + * Here's a list of options supported by the buttons. Any other property of the button will
38 + * be accessible by the internal methods.
39 + *
40 + * @param string group The identifier of the group to which the item belongs (none by default).
41 + * @param icon mixed Either a function, a font icon, an image URL, an image instance or an HTML
42 + * node to display before the button text. In case of a function, it will be
43 + * used as callback to define an image/icon at runtime.
44 + * @param text string The text/html to display for the popup menu button.
45 + * @param action function The callback to dispatch when the button gets clicked.
46 + * @param class string An optional class to use for individual styling. Use "btngroup" to apply a
47 + * sort of fieldset title effect. Useful to describe a sub group.
48 + * @param disabled mixed Either a function or a boolean to check whether the button should
49 + * be clicked or not. The button is never disabled by default.
50 + * @param visible mixed Either a function or a boolean to check whether the button should
51 + * be displayed or not. The button is always visible by default.
52 + * @param separator bool Flag used to check whether the popup should include a separator after the
53 + * button. False by default.
54 + * @param shortcut mixed An array of commands to represent the shortcut that will trigger the action
55 + * via keyboard. The array must contain one and only one character or symbol.
56 + * The array may contain one ore more modifiers, which must be specified first.
57 + * @param searchable bool Whether this button can be searched. Ignored in case the search feature is off.
58 + * @param keywords string[] A list of keywords to match the searched value. This value is ignored in case
59 + * the search feature is disabled. Along with the specified keywords, the system will
60 + * keep searching on the button title too.
61 + *
62 + * List of methods supported by the add-on.
63 + *
64 + * @method show Manually displays the popup menu.
65 + * @method hide Manually disposes the popup menu.
66 + * @method destroy Destroys the popup attached to the element.
67 + * @method config Returns the configuration of the popup.
68 + * @method buttons Getter/setter of the popup buttons.
69 + *
70 + * It is possible to update each setting configuration by using the same
71 + * name of the property and the related value to set. Leave the set argument
72 + * empty to simply access the current property value. In example:
73 + *
74 + * jQuery(target).vboContextMenu( 'trigger', 'click');
75 + * jQuery(target).vboContextMenu('placement', 'auto');
76 + */
77 +(function($) {
78 + 'use strict';
79 +
80 + /**
81 + * Popup menu trigger setup.
82 + *
83 + * @param object root The selector element.
84 + * @param string trigger The trigger to use.
85 + * @param mixed prev The previous trigger.
86 + *
87 + * @return string The trigger event.
88 + */
89 + const vikPopupMenuTrigger = function(root, trigger, prev) {
90 + // check if the trigger was already registered
91 + if (prev) {
92 + // detach previous trigger
93 + $(root).off(prev.toLowerCase());
94 + }
95 +
96 + if (!trigger) {
97 + // abort in case of missing trigger
98 + return false;
99 + }
100 +
101 + // normalize trigger event
102 + switch (trigger.toLowerCase()) {
103 + case 'mouseover':
104 + case 'hover':
105 + trigger = 'mouseover';
106 + break;
107 +
108 + case 'dblclick':
109 + case 'doubleclick':
110 + case 'double-click':
111 + trigger = 'dblclick';
112 + break;
113 +
114 + case 'contextmenu':
115 + case 'rightclick':
116 + case 'right-click':
117 + trigger = 'contextmenu';
118 + break;
119 +
120 + default:
121 + trigger = 'click';
122 + };
123 +
124 + // scan all the registered elements
125 + $(root).each(function() {
126 + // register new trigger
127 + $(this).on(trigger, function(event) {
128 + // always prevent default event
129 + event.preventDefault();
130 +
131 + // open popup
132 + vikPopupMenuShow(this, event);
133 + });
134 + });
135 +
136 + return trigger;
137 + };
138 +
139 + /**
140 + * Popup menu clickable setup.
141 + *
142 + * @param object root The selector element.
143 + * @param boolean flag True to make the root clickable.
144 + * @param mixed prev The flag previously set, if any.
145 + *
146 + * @return self
147 + */
148 + const vikPopupMenuClickable = function(root, flag, prev) {
149 + if (prev) {
150 + // remove CSS class used to disable the selection from root element
151 + $(root).removeClass('vik-context-menu-disable-selection');
152 + }
153 +
154 + if (flag) {
155 + // add CSS class to root element to disable the selection
156 + $(root).addClass('vik-context-menu-disable-selection');
157 + }
158 +
159 + return root;
160 + };
161 +
162 + /**
163 + * Initializes the popup menu.
164 + *
165 + * @param object root The selector element.
166 + * @param object options A configuration object.
167 + *
168 + * @return self
169 + */
170 + const vikPopupMenuInit = function(root, options) {
171 + // inject the specified options within the default configuration
172 + options = $.extend({}, $.vboContextMenu.defaults, options);
173 +
174 + // register the popup configuration for being used later
175 + vikPopupMenuConfig(root, options);
176 +
177 + // register trigger to show the popup menu
178 + options.trigger = vikPopupMenuTrigger(root, options.trigger);
179 +
180 + // normalize buttons
181 + vikPopupMenuButtons(root, options.buttons);
182 +
183 + // handle clickable property
184 + vikPopupMenuClickable(root, options.clickable);
185 +
186 + // register callback to dispatch the action of a button when its shortcut is pressed
187 + $(document).on('keydown.contextmenu.vik', function(event) {
188 + // ignore the event with this namespace because it will end up
189 + // to catch also the plain keydown event
190 + if (event.namespace == 'contextmenu.vik') {
191 + return true;
192 + }
193 +
194 + // retrieve popup configuration
195 + const config = vikPopupMenuConfig(root);
196 +
197 + // in case ESC was pressed, check if we should hide the popup
198 + if (config.hideOnEsc && event.keyCode == 27) {
199 + // auto-close the context menu
200 + vikPopupMenuHide(root);
201 + return true;
202 + }
203 +
204 + // go ahead only in case the focus is not held by a text field
205 + if ($(document.activeElement).is('input,textarea') == true) {
206 + // prevent shortcuts from catching typed characters
207 + return true;
208 + }
209 +
210 + // iterate all registered buttons
211 + $.each(config.buttons, (i, btn) => {
212 + // make sure we have a shortcut and an action to execute
213 + if (!btn.shortcut || !btn.action) {
214 + // nothing to do here, go ahead
215 + return true;
216 + }
217 +
218 + // check whether the shortcut is pressed
219 + if (event.originalEvent.shortcut(btn.shortcut)) {
220 + // launch callback to check whether the button is disabled
221 + // or simply rely on the specified boolean
222 + let disabled = typeof btn.disabled === 'function' ? btn.disabled(root, config) : btn.disabled;
223 +
224 + // trigger action only in case the button is not disabled
225 + if (!disabled) {
226 + // stop event propagation
227 + event.preventDefault();
228 + event.stopPropagation();
229 +
230 + // dispatch button action
231 + btn.action(root, event);
232 + }
233 +
234 + return false;
235 + }
236 + });
237 + });
238 +
239 + return root;
240 + };
241 +
242 + /**
243 + * Getter and setter of the popup configuration.
244 + *
245 + * @param object root The selector element.
246 + * @param mixed data The popup configuration to set. When omitted,
247 + * the method will act as a getter.
248 + *
249 + * @param mixed Returns the configuration when the data argument is
250 + * missing. Otherwise itself will be returned.
251 + */
252 + const vikPopupMenuConfig = function(root, data) {
253 + if (typeof data === 'undefined') {
254 + // GETTER: return popup configuration.
255 + // Clone the object in order to prevent manual edits to
256 + // the configuration properties.
257 + return Object.assign({}, $(root).data('popupConfiguration'));
258 + }
259 +
260 + // SETTER: update popup configuration
261 + return $(root).data('popupConfiguration', data);
262 + };
263 +
264 + /**
265 + * Creates and shows the popup menu.
266 + *
267 + * @param object root The selector element.
268 + * @param Event event The dispatcher DOM event.
269 + *
270 + * @return self
271 + */
272 + const vikPopupMenuShow = function(root, event) {
273 + if ($('.vik-context-menu').length) {
274 + // do not go ahead in case a context menu is visible
275 + return root;
276 + }
277 +
278 + // retrieve configuration
279 + const config = vikPopupMenuConfig(root);
280 +
281 + // register a flag to easily check whether the context menu of this root is open
282 + config.isPopupOpen = true;
283 + vikPopupMenuConfig(root, config);
284 +
285 + // prepare context menu structure
286 + const popup = $('<div class="vik-context-menu"><ul class="buttons-list"></ul></div>');
287 +
288 + if (config.search) {
289 + // create search input
290 + const search = $('<input type="text" />');
291 +
292 + if (config.searchHint) {
293 + search.attr('placeholder', config.searchHint);
294 + }
295 +
296 + search.on('keyup', function() {
297 + // obtain search term
298 + const term = $(search).val().toLowerCase();
299 +
300 + // remove "no matches" element
301 + popup.find('li.no-matches').remove();
302 +
303 + let atLeastOne = false;
304 +
305 + // scan all the buttons
306 + config.buttons.forEach((btn, i) => {
307 + const li = popup.find('li[data-id="' + i + '"]');
308 +
309 + if (li.length === 0 || li.hasClass('not-searchable')) {
310 + // cannot search by this item
311 + return;
312 + }
313 +
314 + let btnText = typeof btn.text === 'object' ? $(btn.text).text() : btn.text + '';
315 +
316 + // define list of keywords
317 + const keywords = [btnText].concat(btn.keywords || []);
318 +
319 + // check whether the button matches the given search term
320 + let match = keywords.some((k) => k.toLowerCase().indexOf(term) !== -1);
321 +
322 + if (match) {
323 + li.show();
324 + atLeastOne = true;
325 + } else {
326 + li.hide();
327 + }
328 + });
329 +
330 + /**
331 + * Check whether we should completely hide a subgroup because all its children
332 + * don't match the specified search.
333 + */
334 + popup.find('li.buttons-subgroup ul').each(function() {
335 + $(this).parent().show();
336 +
337 + if ($(this).children().not('.btngroup').filter(':visible').length === 0) {
338 + // all sub-items are hidden, hide the sub-group too
339 + $(this).parent().hide();
340 + }
341 + });
342 +
343 + if (!atLeastOne) {
344 + // add "no matches" element in case of no results
345 + popup.find('ul.buttons-list').append(
346 + $('<li class="no-matches"></li>').append(
347 + $('<a class="disabled"></a>').append(
348 + $('<span class="button-text"></span>').text(config.searchEmpty)
349 + )
350 + )
351 + );
352 + }
353 +
354 + if (term.length) {
355 + searchClear.show();
356 + } else {
357 + searchClear.hide();
358 + }
359 + });
360 +
361 + // create button to clear the text
362 + const searchClear = $('<button type="button" class="search-clear"><i class="fas fa-times-circle"></i></button>');
363 +
364 + // register event to clear the input
365 + searchClear.on('click', () => {
366 + search.val('').trigger('keyup');
367 + }).hide();
368 +
369 + // create search list item
370 + const searchLi = $('<li class="search-box"></li>').append(search).append(searchClear);
371 +
372 + if (config.searchClass) {
373 + searchLi.addClass(config.searchClass);
374 + }
375 +
376 + // attach search input to the popup
377 + popup.find('ul.buttons-list').append(searchLi);
378 + }
379 +
380 + // in case of a custom class, add it
381 + if (config.class) {
382 + popup.addClass(config.class);
383 + }
384 +
385 + // look for dark mode
386 + if (config.darkMode === true) {
387 + // turn dark mode on
388 + popup.addClass('dark-mode');
389 + } else if (config.darkMode === false) {
390 + // suppress dark mode
391 + popup.addClass('light-mode');
392 + }
393 +
394 + // iterate registered buttons and append them one by one
395 + $.each(config.buttons, function(i, btn) {
396 + // launch callback to check whether the button should be displayed
397 + // or simply rely on the specified boolean
398 + let visible = typeof btn.visible === 'function' ? btn.visible(root, config) : btn.visible;
399 +
400 + if (!visible) {
401 + // skip button and go ahead
402 + return true;
403 + }
404 +
405 + // prepare button structure
406 + const popupBtn = $('<a></a>');
407 +
408 + if (btn.icon) {
409 + let icon;
410 +
411 + if (typeof btn.icon === 'function') {
412 + // we have a function, launch the callback
413 + // to extract the image at runtime
414 + icon = btn.icon(root, config);
415 + } else {
416 + // use it plain
417 + icon = btn.icon;
418 + }
419 +
420 + if (icon instanceof Image) {
421 + // we have an image instance
422 + icon = $(icon);
423 + } else if (typeof icon === 'string') {
424 + if (icon.indexOf('/') !== -1) {
425 + // we have an image URL
426 + icon = $('<img>').attr('src', icon);
427 + } else {
428 + // we probably have a font icon
429 + icon = $('<i></i>').addClass(icon);
430 + }
431 + }
432 +
433 + // leave as is in case a jQuery instance was passed
434 +
435 + if (icon !== null && icon !== undefined) {
436 + // wrap icon in a parent and append all to button
437 + popupBtn.append($('<span class="button-icon"></span>').append(icon));
438 + }
439 + }
440 +
441 + // insert text button
442 + popupBtn.append($('<span class="button-text"></span>').html(btn.text));
443 +
444 + // check if the button specified a shortcut
445 + if (btn.shortcut) {
446 + // map shortcut elements
447 + let cmd = btn.shortcut.map(function(k) {
448 + let keyCode = k;
449 +
450 + switch (k) {
451 + case 'alt': k = "&#8997;"; break;
452 + case 'ctrl': k = "&#8963;"; break;
453 + case 'shift': k = "&#8679;"; break;
454 + case 'meta': k = "&#8984;"; break;
455 + // backspace
456 + case 8: k = '<i class="fas fa-backspace"></i>'; break;
457 + // enter
458 + case 13: k = '&#9166;'; break;
459 + // space
460 + case 32: k = 'Space'; break;
461 + // arrow up
462 + case 37: k = '<i class="fas fa-long-arrow-alt-left"></i>'; break;
463 + // arrow up
464 + case 38: k = '<i class="fas fa-long-arrow-alt-up"></i>'; break;
465 + // arrow right
466 + case 39: k = '<i class="fas fa-long-arrow-alt-right"></i>'; break;
467 + // arrow down
468 + case 40: k = '<i class="fas fa-long-arrow-alt-down"></i>'; break;
469 + // character
470 + default: k = typeof k === 'string' ? k.toUpperCase() : '';
471 + }
472 +
473 + // look for a custom function used to format shortcuts
474 + if (typeof config.formatShortcut === 'function') {
475 + // launch the callback
476 + k = config.formatShortcut(keyCode, k);
477 + }
478 +
479 + return k;
480 + });
481 +
482 + cmd = cmd.join('');
483 +
484 + // wrap the shortcut between parenthesis in case of no modifiers
485 + if (cmd.length == 1) {
486 + cmd = '(' + cmd + ')';
487 + }
488 +
489 + // insert shortcut button
490 + popupBtn.append($('<span class="button-shortcut"></span>').html(cmd));
491 + }
492 +
493 + // launch callback to check whether the button should be disabled
494 + // or simply rely on the specified boolean
495 + let disabled = typeof btn.disabled === 'function' ? btn.disabled(root, config) : btn.disabled;
496 +
497 + // check whether the button is disabled
498 + if (disabled) {
499 + popupBtn.addClass('disabled');
500 + } else {
501 + // register button click event
502 + popupBtn.on('click', function(event) {
503 + // look for an action callback
504 + if (btn.action) {
505 + // dispatch callback
506 + btn.action(root, event);
507 + }
508 +
509 + // always dismiss the popup when a button gets clicked
510 + vikPopupMenuHide(root);
511 + });
512 + }
513 +
514 + // wrap button within a parent for <ul> compliance
515 + const popupItem = $('<li data-id="' + i + '"></li>').append(popupBtn);
516 +
517 + // in case of a custom class, add it to the li and to the link
518 + if (btn.class) {
519 + popupItem.addClass(btn.class);
520 + popupBtn.addClass(btn.class);
521 + }
522 +
523 + if (!btn.searchable) {
524 + popupItem.addClass('not-searchable');
525 + }
526 +
527 + // in case of a separator, add a specific class
528 + if (btn.separator) {
529 + popupItem.addClass('separator');
530 + }
531 +
532 + /**
533 + * Register a sub group of buttons to improve individual styling.
534 + *
535 + * @since 2.1
536 + */
537 + if (btn.group) {
538 + // obtain the group element
539 + let ulGroup = popup.find('ul.' + btn.group);
540 +
541 + if (ulGroup.length == 0) {
542 + // create now in case it doesn't exist yet
543 + ulGroup = $('<ul></ul>').addClass(btn.group);
544 + popup.find('ul.buttons-list').append($('<li class="buttons-subgroup separator"></li>').append(ulGroup));
545 + }
546 +
547 + // append item to the given group
548 + ulGroup.append(popupItem);
549 + } else {
550 + // add button to the default list
551 + popup.find('ul.buttons-list').append(popupItem);
552 + }
553 + });
554 +
555 + // hide the popup before appending it
556 + popup.hide();
557 +
558 + // append button to body
559 + $('body').append(popup);
560 +
561 + // calculate popup position
562 + vikPopupMenuCalcPosition(root, popup, event);
563 +
564 + if (config.lockScroll) {
565 + // prevent document from scrolling
566 + $('body').addClass('lock-scroll');
567 + }
568 +
569 + // show popup
570 + popup.show();
571 +
572 + if (config.search && config.searchFocus) {
573 + // auto-focus the search box
574 + popup.find('.search-box input').focus();
575 + }
576 +
577 + // look for a specific callback to be triggered on opening
578 + if (config.onShow) {
579 + // trigger show callback
580 + config.onShow(root, popup, event);
581 + }
582 +
583 + // Register callback to auto dismiss the popup when clicked outside.
584 + // Use mousedown event because it will be execured before any other
585 + // supported trigger, so that the context menus can be shown on cascade.
586 + $(document).on('mousedown.contextmenu.vik', function(event) {
587 + // ignore the event with this namespace because it will end up
588 + // to catch also the plain mousedown event
589 + if (event.namespace == 'contextmenu.vik') {
590 + return false;
591 + }
592 +
593 + if (!popup.is(':visible')) {
594 + // dialog not visible
595 + return false;
596 + }
597 +
598 + // get list of buttons
599 + const links = popup.find('a');
600 +
601 + // make sure we haven't clicked the popup or a link
602 + if (!popup.is(event.target) && popup.has(event.target).length === 0
603 + && !links.is(event.target) && links.has(event.target).length === 0) {
604 + // auto close popup when clicked outside
605 + vikPopupMenuHide(root);
606 +
607 + event.stopPropagation();
608 + event.preventDefault();
609 +
610 + return false;
611 + }
612 + });
613 +
614 + return root;
615 + };
616 +
617 + /**
618 + * Hides the popup menu.
619 +
620 + * @param object root The selector element.
621 + *
622 + * @return self
623 + */
624 + const vikPopupMenuHide = function(root) {
625 + // get popup configuration
626 + const config = vikPopupMenuConfig(root);
627 +
628 + // go ahead only in case a popup of this element is open
629 + if (config.isPopupOpen && $('.vik-context-menu').length) {
630 + // remove "open" flag after closing the context menu of this element
631 + delete config.isPopupOpen;
632 + vikPopupMenuConfig(root, config);
633 +
634 + // remove the focus from the active element to prevent unexpected scrolls
635 + document.activeElement.blur();
636 +
637 + // destroy any existing popup menu because we do not support
638 + // more than a popup per time
639 + $('.vik-context-menu').remove();
640 +
641 + // always restore scroll functions
642 + $('body').removeClass('lock-scroll');
643 +
644 + // turn off proxy
645 + $(document).off('mousedown.contextmenu.vik');
646 +
647 + // look for a specific callback to be triggered on dismiss
648 + if (config.onHide) {
649 + // trigger hide callback
650 + config.onHide(root);
651 + }
652 + }
653 +
654 + return root;
655 + };
656 +
657 + /**
658 + * Destroys the popup menu.
659 +
660 + * @param object root The selector element.
661 + *
662 + * @return self
663 + */
664 + const vikPopupMenuDestroy = function(root) {
665 + // in case the popup was open, close it first
666 + vikPopupMenuHide(root);
667 +
668 + // get popup configuration
669 + const config = vikPopupMenuConfig(root);
670 +
671 + // detach keyboard listener
672 + $(document).off('keydown.contextmenu.vik');
673 +
674 + // remove CSS class used to disable the selection from root element
675 + $(root).removeClass('vik-context-menu-disable-selection');
676 +
677 + // detach previous event without attaching a new one
678 + vikPopupMenuTrigger(root, null, config.trigger);
679 +
680 + // detach clickable property
681 + vikPopupMenuClickable(root, null, config.clickable);
682 +
683 + // then destroy the registered data
684 + return vikPopupMenuConfig(root, null);
685 + };
686 +
687 + /**
688 + * Getter and setter of the popup buttons.
689 + *
690 + * @param object root The selector element.
691 + * @param mixed data The popup buttons to set. When omitted,
692 + * the method will act as a getter.
693 + *
694 + * @param mixed Returns the buttons list when the data argument is
695 + * missing. Otherwise itself will be returned.
696 + */
697 + const vikPopupMenuButtons = function(root, data) {
698 + // get configuration
699 + const config = vikPopupMenuConfig(root);
700 +
701 + if (typeof data === 'undefined') {
702 + // return popup buttons
703 + return config.buttons;
704 + }
705 +
706 + // make sure the buttons property is an Array
707 + if (!Array.isArray(data)) {
708 + throw 'Invalid buttons, an Array was expected.';
709 + }
710 +
711 + // set specified buttons
712 + config.buttons = data;
713 +
714 + // iterate all buttons
715 + for (let i = 0; i < config.buttons.length; i++) {
716 + // create default button properties
717 + config.buttons[i] = $.extend({
718 + group: '',
719 + icon: null,
720 + text: '',
721 + action: null,
722 + shortcut: null,
723 + class: '',
724 + disabled: false,
725 + visible: true,
726 + separator: false,
727 + searchable: true,
728 + }, config.buttons[i]);
729 +
730 + // make sure we have an array
731 + if (!Array.isArray(config.buttons[i].shortcut)) {
732 + // invalid shortcut
733 + config.buttons[i].shortcut = null;
734 + }
735 + }
736 +
737 + // register configuration
738 + return vikPopupMenuConfig(root, config);
739 + };
740 +
741 + /**
742 + * Calculates and sets the proper position of the popup.
743 + *
744 + * @param object root The selector element.
745 + * @param mixed popup The popup element.
746 + * @param mixed event The dispatcher DOM event.
747 + *
748 + * @param self
749 + */
750 + const vikPopupMenuCalcPosition = function(root, popup, event) {
751 + // get popup configuration
752 + const config = vikPopupMenuConfig(root);
753 +
754 + // in case of "auto" placement, we need to make sure
755 + // that we own an event to access the mouse coordinates
756 + if (config.placement == 'auto' && !event) {
757 + // no event was passed, fallback to right
758 + config.placement = 'right';
759 + }
760 +
761 + // calculate root offset
762 + let rootOffset = $(root).offset();
763 + // calculate root size
764 + let rootWidth = $(root).outerWidth();
765 + let rootHeight = $(root).outerHeight();
766 + // calculate popup size
767 + let popupWidth = $(popup).outerWidth();
768 + let popupHeight = $(popup).outerHeight();
769 +
770 + let x, y;
771 +
772 + // display popup above the root
773 + if (config.placement == 'top') {
774 + x = rootOffset.left + rootWidth / 2 - popupWidth / 2;
775 + y = rootOffset.top - popupHeight - 4;
776 + }
777 + // display popup above the root, to the right
778 + else if (config.placement == 'top-right') {
779 + x = rootOffset.left + rootWidth - popupWidth;
780 + y = rootOffset.top - popupHeight - 4;
781 + }
782 + // display popup above the root, to the left
783 + else if (config.placement == 'top-left') {
784 + x = rootOffset.left;
785 + y = rootOffset.top - popupHeight - 4;
786 + }
787 + // display popup above the root, centered
788 + else if (config.placement == 'top-center') {
789 + x = rootOffset.left + (rootWidth > popupWidth ? ((rootWidth - popupWidth) / 2) : 0);
790 + y = rootOffset.top - popupHeight - 4;
791 + }
792 + // display the popup below the root
793 + else if (config.placement == 'bottom') {
794 + x = rootOffset.left + rootWidth / 2 - popupWidth / 2;
795 + y = rootOffset.top + rootHeight + 4;
796 + }
797 + // display the popup below the root, to the right
798 + else if (config.placement == 'bottom-right') {
799 + x = rootOffset.left + rootWidth - popupWidth;
800 + y = rootOffset.top + rootHeight + 4;
801 + }
802 + // display the popup below the root, to the left
803 + else if (config.placement == 'bottom-left') {
804 + x = rootOffset.left;
805 + y = rootOffset.top + rootHeight + 4;
806 + }
807 + // display the popup below the root, centered
808 + else if (config.placement == 'bottom-center') {
809 + x = rootOffset.left + (rootWidth > popupWidth ? ((rootWidth - popupWidth) / 2) : 0);
810 + y = rootOffset.top + rootHeight + 4;
811 + }
812 + // display the popup before the root
813 + else if (config.placement == 'left') {
814 + x = rootOffset.left - popupWidth - 4;
815 + y = rootOffset.top + rootHeight / 2 - popupHeight / 2;
816 + }
817 + // display the popup after the root
818 + else if (config.placement == 'right') {
819 + x = rootOffset.left + rootWidth + 4;
820 + y = rootOffset.top + rootHeight / 2 - popupHeight / 2;
821 + }
822 + // display the popup at the mouse coordinates
823 + else {
824 + x = event.pageX;
825 + y = event.pageY;
826 + }
827 +
828 + // calculate screen size
829 + let screenWidth = $(window).width();
830 + let screenHeight = $(window).height();
831 + // calculate window scrolls
832 + let windowScrollLeft = $(window).scrollLeft();
833 + let windowScrollTop = $(window).scrollTop();
834 +
835 + // use 4 pixel as minimum value
836 + x = Math.max(4, x);
837 + y = Math.max(4, y);
838 +
839 + // make sure the popup doesn't exceed the screen width
840 + if (x + popupWidth + 4 > screenWidth) {
841 + x = screenWidth - popupWidth - 4;
842 + }
843 +
844 + // make sure the popup doesn't exceed the screen height
845 + if (y + popupHeight + 4 > screenHeight + windowScrollTop) {
846 + y = screenHeight - popupHeight - 4 + windowScrollTop;
847 + }
848 +
849 + $(popup).css('top', y).css('left', x);
850 +
851 + return root;
852 + };
853 +
854 + // register listener to auto-close the popup when clicked outside
855 + $(document).on('mousedown', function() {
856 + // we need to propagate the event with a proxy so that we can safely
857 + // detach the registered callbacks when the popup gets closed
858 + $(document).trigger('mousedown.contextmenu.vik');
859 + });
860 +
861 + // register listener to dispatch the actions of the buttons via keyboard
862 + $(document).on('keydown', function() {
863 + // we need to propagate the event with a proxy so that we can safely
864 + // detach the registered callbacks when the popup gets destroyed
865 + $(document).trigger('keydown.contextmenu.vik');
866 + });
867 +
868 + // register the jQuery callback
869 + $.fn.vboContextMenu = function(method, data) {
870 + if (!method) {
871 + method = {};
872 + }
873 +
874 + // immediately exit in case of no elements found
875 + if ($(this).length == 0) {
876 + return this;
877 + }
878 +
879 + // initialize popup events
880 + if (typeof method === 'object') {
881 + return vikPopupMenuInit(this, method);
882 + }
883 + // check if we should dismiss the popup
884 + else if (typeof method === 'string' && method.match(/^(close|dismiss|hide)$/i)) {
885 + return vikPopupMenuHide(this);
886 + }
887 + // check if we should open the popup
888 + else if (typeof method === 'string' && method.match(/^(show|open)$/i)) {
889 + return vikPopupMenuShow(this);
890 + }
891 + // check if we destroy the popup
892 + else if (typeof method === 'string' && method.match(/^(destroy)$/i)) {
893 + return vikPopupMenuDestroy(this);
894 + }
895 + // check if we should return the popup configuration
896 + else if (typeof method === 'string' && method.match(/^(config|configuration|options)$/i)) {
897 + return vikPopupMenuConfig(this);
898 + }
899 + // check if we should handle with the popup buttons
900 + else if (typeof method === 'string' && method.match(/^(buttons)$/i)) {
901 + // use getter/setter according to the specified arguments
902 + return vikPopupMenuButtons(this, data);
903 + }
904 + // fallback to configuration setting getter/setter
905 + else {
906 + // access configuration
907 + const config = vikPopupMenuConfig(this);
908 +
909 + // check if the second argument was passed
910 + if (typeof data !== 'undefined') {
911 + if (method == 'trigger') {
912 + // register trigger before updating the configuration
913 + data = vikPopupMenuTrigger(this, data, config.trigger);
914 + } else if (method == 'clickable') {
915 + // handle clickable property
916 + vikPopupMenuClickable(this, data, config.clickable);
917 + }
918 +
919 + // register argument within configuration
920 + config[method] = data;
921 +
922 + // refresh configuration and return self instance
923 + return vikPopupMenuConfig(this, config);
924 + }
925 +
926 + // return configuration setting
927 + return config[method];
928 + }
929 +
930 + return this;
931 + };
932 +
933 + // define the default configuration to use for the context menu
934 + $.vboContextMenu = {
935 + defaults: {
936 + trigger: 'click',
937 + placement: 'auto',
938 + class: '',
939 + buttons: [],
940 + onShow: null,
941 + onHide: null,
942 + clickable: false,
943 + lockScroll: true,
944 + darkMode: null,
945 + hideOnEsc: true,
946 + formatShortcut: null,
947 + search: false,
948 + searchHint: '',
949 + searchEmpty: 'No results.',
950 + searchClass: 'separator',
951 + searchFocus: true,
952 + },
953 + };
954 +
955 + /**
956 + * Checks if the KeyBoard event matches the given shortcut.
957 + *
958 + * @param array keys The shortcut representation.
959 + *
960 + * @return boolean True if matches, otherwise false.
961 + */
962 + KeyboardEvent.prototype.shortcut = function(keys) {
963 + // get modifiers list
964 + let modifiers = keys.slice(0);
965 + // pop character from modifiers
966 + let keyCode = modifiers.pop();
967 +
968 + if (typeof keyCode === 'string') {
969 + // get ASCII
970 + keyCode = keyCode.toUpperCase().charCodeAt(0);
971 + }
972 +
973 + // make sure the modifiers are lower case
974 + modifiers = modifiers.map(function(mod) {
975 + return mod.toLowerCase();
976 + });
977 +
978 + let ok = false;
979 +
980 + // validate key code
981 + if (this.keyCode == keyCode) {
982 + // validate modifiers
983 + ok = true;
984 + const lookup = ['meta', 'shift', 'alt', 'ctrl'];
985 +
986 + for (let i = 0; i < lookup.length && ok; i++) {
987 + // check if modifiers is pressed
988 + let mod = this[lookup[i] + 'Key'];
989 +
990 + if (mod) {
991 + // if pressed, the shortcut must specify it
992 + ok &= modifiers.indexOf(lookup[i]) !== -1;
993 + } else {
994 + // if not pressed, the shortcut must not include it
995 + ok &= modifiers.indexOf(lookup[i]) === -1;
996 + }
997 + }
998 + }
999 +
1000 + return ok;
1001 + }
1002 +})(jQuery);