PluginProbe
VikBooking Hotel Booking Engine & PMS / trunk
VikBooking Hotel Booking Engine & PMS vtrunk
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 1.7.3 1.7.4 All 35 releases
vikbooking / admin / resources / contextmenu.js

contextmenu.js in VikBooking Hotel Booking Engine & PMS trunk, at admin/resources/contextmenu.js

1,002 lines 30.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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);