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 +283 -117 1.6.5 → 1.8.16 View file →
@@ -1,59 +1,72 @@
1 1 /**
2 2 * jQuery add-on used to support context menus.
3 + *
4 + * @version 2.2.5
5 + * @author E4J srl
6 + *
3 7 * Here's a list of supported options.
4 - *
5 - * @author E4J srl
6 8 *
7 - * - trigger string The command that should trigger the popup menu. Accepts the
8 - * following values: click|doubleclick|rightclick|hover.
9 - * Click will be used by default.
10 - * - placement string Where the popup should be displayed in relation to the target.
11 - * Accepts the following values: auto|top|right|bottom|left.
12 - * Auto will be used by default (at the mouse coordinates).
13 - * - class string An optional class to use for individual styling.
14 - * - buttons object[] A list of buttons to include within the popup menu. See the options
15 - * of the buttons for further details.
16 - * - onShow function An optional callback to invoke when the popup menu is displayed.
17 - * - onHide function An optional callback to invoke when the popup menu is dismissed.
18 - * - darkMode mixed Flag for dark mode layout, which accepts 3 possible values:
19 - * true|false|null. Pass true to always force the dark mode, false to
20 - * always use the light mode, null to auto-detect the proper mode
21 - * according to the preferred theme of the browser.
22 - * - clickable boolean Flag used to check whether the root element should prevent the
23 - * browser selection by applying specific CSS rules. False by default.
24 - * - lockScroll boolean Flag used to prevent the document scroll when the context menu
25 - * pops up. True by default.
26 - * - hideOnEsc boolean Choose whether the context menu should be closed when ESC key is
27 - * pressed. Always true by default.
28 - * - formatShortcut mixed An optional callback that can be used to format the shortcut symbols.
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.
29 36 *
30 37 * Here's a list of options supported by the buttons. Any other property of the button will
31 38 * be accessible by the internal methods.
32 39 *
33 - * - icon mixed Either a function, an image URL, an image instance or a font icon
34 - * to display before the button text. In case of a function, it will be
35 - * used as callback to define an image/icon at runtime.
36 - * - text string The text to display for the popup menu button.
37 - * - action function The callback to dispatch when the button gets clicked.
38 - * - class string An optional class to use for individual styling.
39 - * - disabled mixed Either a function or a boolean to check whether the button should
40 - * be clicked or not. The button is never disabled by default.
41 - * - visible mixed Either a function or a boolean to check whether the button should
42 - * be displayed or not. The button is always visible by default.
43 - * - separator boolean Flag used to check whether the popup should include a separator after the
44 - * button. False by default.
45 - * - shortcut mixed An array of commands to represent the shortcut that will trigger the action
46 - * via keyboard. The array must contain one and only one character or symbol.
47 - * The array may contain one ore more modifiers, which must be specified first.
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.
48 61 *
49 62 * List of methods supported by the add-on.
50 63 *
51 - * - show Manually displays the popup menu.
52 - * - hide Manually disposes the popup menu.
53 - * - destroy Destroys the popup attached to the element.
54 - * - config Returns the configuration of the popup.
55 - * - buttons Getter/setter of the popup buttons.
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.
56 69 *
57 70 * It is possible to update each setting configuration by using the same
58 71 * name of the property and the related value to set. Leave the set argument
59 72 * empty to simply access the current property value. In example:
@@ -61,8 +74,10 @@
61 74 * jQuery(target).vboContextMenu( 'trigger', 'click');
62 75 * jQuery(target).vboContextMenu('placement', 'auto');
63 76 */
64 77 (function($) {
78 + 'use strict';
79 +
65 80 /**
66 81 * Popup menu trigger setup.
67 82 *
68 83 * @param object root The selector element.
@@ -70,9 +85,9 @@
70 85 * @param mixed prev The previous trigger.
71 86 *
72 87 * @return string The trigger event.
73 88 */
74 - var vikPopupMenuTrigger = function(root, trigger, prev) {
89 + const vikPopupMenuTrigger = function(root, trigger, prev) {
75 90 // check if the trigger was already registered
76 91 if (prev) {
77 92 // detach previous trigger
78 93 $(root).off(prev.toLowerCase());
@@ -129,9 +144,9 @@
129 144 * @param mixed prev The flag previously set, if any.
130 145 *
131 146 * @return self
132 147 */
133 - var vikPopupMenuClickable = function(root, flag, prev) {
148 + const vikPopupMenuClickable = function(root, flag, prev) {
134 149 if (prev) {
135 150 // remove CSS class used to disable the selection from root element
136 151 $(root).removeClass('vik-context-menu-disable-selection');
137 152 }
@@ -151,11 +166,11 @@
151 166 * @param object options A configuration object.
152 167 *
153 168 * @return self
154 169 */
155 - var vikPopupMenuInit = function(root, options) {
170 + const vikPopupMenuInit = function(root, options) {
156 171 // inject the specified options within the default configuration
157 - options = $.extend($.vboContextMenu.defaults, options);
172 + options = $.extend({}, $.vboContextMenu.defaults, options);
158 173
159 174 // register the popup configuration for being used later
160 175 vikPopupMenuConfig(root, options);
161 176
@@ -168,23 +183,17 @@
168 183 // handle clickable property
169 184 vikPopupMenuClickable(root, options.clickable);
170 185
171 186 // register callback to dispatch the action of a button when its shortcut is pressed
172 - $(document).on('keydown.contextmenu.vikbooking', function(event) {
187 + $(document).on('keydown.contextmenu.vik', function(event) {
173 188 // ignore the event with this namespace because it will end up
174 189 // to catch also the plain keydown event
175 - if (event.namespace == 'contextmenu.vikbooking') {
190 + if (event.namespace == 'contextmenu.vik') {
176 191 return true;
177 192 }
178 193
179 - // go ahead only in case the focus is not help by a text field
180 - if ($(document.activeElement).is('input,textarea') == true) {
181 - // prevent shortcuts from catching typed characters
182 - return true;
183 - }
184 -
185 194 // retrieve popup configuration
186 - var config = vikPopupMenuConfig(root);
195 + const config = vikPopupMenuConfig(root);
187 196
188 197 // in case ESC was pressed, check if we should hide the popup
189 198 if (config.hideOnEsc && event.keyCode == 27) {
190 199 // auto-close the context menu
@@ -191,8 +200,14 @@
191 200 vikPopupMenuHide(root);
192 201 return true;
193 202 }
194 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 +
195 210 // iterate all registered buttons
196 211 $.each(config.buttons, (i, btn) => {
197 212 // make sure we have a shortcut and an action to execute
198 213 if (!btn.shortcut || !btn.action) {
@@ -203,9 +218,9 @@
203 218 // check whether the shortcut is pressed
204 219 if (event.originalEvent.shortcut(btn.shortcut)) {
205 220 // launch callback to check whether the button is disabled
206 221 // or simply rely on the specified boolean
207 - var disabled = typeof btn.disabled === 'function' ? btn.disabled(root, config) : btn.disabled;
222 + let disabled = typeof btn.disabled === 'function' ? btn.disabled(root, config) : btn.disabled;
208 223
209 224 // trigger action only in case the button is not disabled
210 225 if (!disabled) {
211 226 // stop event propagation
@@ -233,9 +248,9 @@
233 248 *
234 249 * @param mixed Returns the configuration when the data argument is
235 250 * missing. Otherwise itself will be returned.
236 251 */
237 - var vikPopupMenuConfig = function(root, data) {
252 + const vikPopupMenuConfig = function(root, data) {
238 253 if (typeof data === 'undefined') {
239 254 // GETTER: return popup configuration.
240 255 // Clone the object in order to prevent manual edits to
241 256 // the configuration properties.
@@ -253,9 +268,9 @@
253 268 * @param Event event The dispatcher DOM event.
254 269 *
255 270 * @return self
256 271 */
257 - var vikPopupMenuShow = function(root, event) {
272 + const vikPopupMenuShow = function(root, event) {
258 273 if ($('.vik-context-menu').length) {
259 274 // do not go ahead in case a context menu is visible
260 275 return root;
261 276 }
@@ -260,13 +275,109 @@
260 275 return root;
261 276 }
262 277
263 278 // retrieve configuration
264 - var config = vikPopupMenuConfig(root);
279 + const config = vikPopupMenuConfig(root);
265 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 +
266 285 // prepare context menu structure
267 - var popup = $('<div class="vik-context-menu"><ul></ul></div>');
286 + const popup = $('<div class="vik-context-menu"><ul class="buttons-list"></ul></div>');
268 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 +
269 380 // in case of a custom class, add it
270 381 if (config.class) {
271 382 popup.addClass(config.class);
272 383 }
@@ -283,9 +394,9 @@
283 394 // iterate registered buttons and append them one by one
284 395 $.each(config.buttons, function(i, btn) {
285 396 // launch callback to check whether the button should be displayed
286 397 // or simply rely on the specified boolean
287 - var visible = typeof btn.visible === 'function' ? btn.visible(root, config) : btn.visible;
398 + let visible = typeof btn.visible === 'function' ? btn.visible(root, config) : btn.visible;
288 399
289 400 if (!visible) {
290 401 // skip button and go ahead
291 402 return true;
@@ -291,12 +402,12 @@
291 402 return true;
292 403 }
293 404
294 405 // prepare button structure
295 - var popupBtn = $('<a></a>');
406 + const popupBtn = $('<a></a>');
296 407
297 408 if (btn.icon) {
298 - var icon;
409 + let icon;
299 410
300 411 if (typeof btn.icon === 'function') {
301 412 // we have a function, launch the callback
302 413 // to extract the image at runtime
@@ -320,10 +431,12 @@
320 431 }
321 432
322 433 // leave as is in case a jQuery instance was passed
323 434
324 - // wrap icon in a parent and append all to button
325 - popupBtn.append($('<span class="button-icon"></span>').append(icon));
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 + }
326 439 }
327 440
328 441 // insert text button
329 442 popupBtn.append($('<span class="button-text"></span>').html(btn.text));
@@ -330,10 +443,10 @@
330 443
331 444 // check if the button specified a shortcut
332 445 if (btn.shortcut) {
333 446 // map shortcut elements
334 - var cmd = btn.shortcut.map(function(k) {
335 - var keyCode = k;
447 + let cmd = btn.shortcut.map(function(k) {
448 + let keyCode = k;
336 449
337 450 switch (k) {
338 451 case 'alt': k = "&#8997;"; break;
339 452 case 'ctrl': k = "&#8963;"; break;
@@ -378,9 +491,9 @@
378 491 }
379 492
380 493 // launch callback to check whether the button should be disabled
381 494 // or simply rely on the specified boolean
382 - var disabled = typeof btn.disabled === 'function' ? btn.disabled(root, config) : btn.disabled;
495 + let disabled = typeof btn.disabled === 'function' ? btn.disabled(root, config) : btn.disabled;
383 496
384 497 // check whether the button is disabled
385 498 if (disabled) {
386 499 popupBtn.addClass('disabled');
@@ -397,15 +510,20 @@
397 510 vikPopupMenuHide(root);
398 511 });
399 512 }
400 513
401 - // in case of a custom class, add it
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
402 518 if (btn.class) {
519 + popupItem.addClass(btn.class);
403 520 popupBtn.addClass(btn.class);
404 521 }
405 522
406 - // wrap button within a parent for <ul> compliance
407 - var popupItem = $('<li></li>').append(popupBtn);
523 + if (!btn.searchable) {
524 + popupItem.addClass('not-searchable');
525 + }
408 526
409 527 // in case of a separator, add a specific class
410 528 if (btn.separator) {
411 529 popupItem.addClass('separator');
@@ -410,10 +528,29 @@
410 528 if (btn.separator) {
411 529 popupItem.addClass('separator');
412 530 }
413 531
414 - // wrap button within a parent and add to popup
415 - popup.find('ul').append(popupItem);
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 + }
416 553 });
417 554
418 555 // hide the popup before appending it
419 556 popup.hide();
@@ -431,21 +568,26 @@
431 568
432 569 // show popup
433 570 popup.show();
434 571
572 + if (config.search && config.searchFocus) {
573 + // auto-focus the search box
574 + popup.find('.search-box input').focus();
575 + }
576 +
435 577 // look for a specific callback to be triggered on opening
436 578 if (config.onShow) {
437 579 // trigger show callback
438 - config.onShow(root, popup);
580 + config.onShow(root, popup, event);
439 581 }
440 582
441 583 // Register callback to auto dismiss the popup when clicked outside.
442 584 // Use mousedown event because it will be execured before any other
443 585 // supported trigger, so that the context menus can be shown on cascade.
444 - $(document).on('mousedown.contextmenu.vikbooking', function(event) {
586 + $(document).on('mousedown.contextmenu.vik', function(event) {
445 587 // ignore the event with this namespace because it will end up
446 588 // to catch also the plain mousedown event
447 - if (event.namespace == 'contextmenu.vikbooking') {
589 + if (event.namespace == 'contextmenu.vik') {
448 590 return false;
449 591 }
450 592
451 593 if (!popup.is(':visible')) {
@@ -453,9 +595,9 @@
453 595 return false;
454 596 }
455 597
456 598 // get list of buttons
457 - var links = popup.find('a');
599 + const links = popup.find('a');
458 600
459 601 // make sure we haven't clicked the popup or a link
460 602 if (!popup.is(event.target) && popup.has(event.target).length === 0
461 603 && !links.is(event.target) && links.has(event.target).length === 0) {
@@ -478,11 +620,21 @@
478 620 * @param object root The selector element.
479 621 *
480 622 * @return self
481 623 */
482 - var vikPopupMenuHide = function(root) {
483 - // go ahead only in case a popup is open
484 - if ($('.vik-context-menu').length) {
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 +
485 637 // destroy any existing popup menu because we do not support
486 638 // more than a popup per time
487 639 $('.vik-context-menu').remove();
488 640
@@ -489,13 +641,10 @@
489 641 // always restore scroll functions
490 642 $('body').removeClass('lock-scroll');
491 643
492 644 // turn off proxy
493 - $(document).off('mousedown.contextmenu.vikbooking');
645 + $(document).off('mousedown.contextmenu.vik');
494 646
495 - // get popup configuration
496 - var config = vikPopupMenuConfig(root);
497 -
498 647 // look for a specific callback to be triggered on dismiss
499 648 if (config.onHide) {
500 649 // trigger hide callback
501 650 config.onHide(root);
@@ -511,21 +660,21 @@
511 660 * @param object root The selector element.
512 661 *
513 662 * @return self
514 663 */
515 - var vikPopupMenuDestroy = function(root) {
664 + const vikPopupMenuDestroy = function(root) {
516 665 // in case the popup was open, close it first
517 666 vikPopupMenuHide(root);
518 667
668 + // get popup configuration
669 + const config = vikPopupMenuConfig(root);
670 +
519 671 // detach keyboard listener
520 - $(document).off('keydown.contextmenu.vikbooking');
672 + $(document).off('keydown.contextmenu.vik');
521 673
522 674 // remove CSS class used to disable the selection from root element
523 675 $(root).removeClass('vik-context-menu-disable-selection');
524 676
525 - // get popup configuration
526 - var config = vikPopupMenuConfig(root);
527 -
528 677 // detach previous event without attaching a new one
529 678 vikPopupMenuTrigger(root, null, config.trigger);
530 679
531 680 // detach clickable property
@@ -544,11 +693,11 @@
544 693 *
545 694 * @param mixed Returns the buttons list when the data argument is
546 695 * missing. Otherwise itself will be returned.
547 696 */
548 - var vikPopupMenuButtons = function(root, data) {
697 + const vikPopupMenuButtons = function(root, data) {
549 698 // get configuration
550 - var config = vikPopupMenuConfig(root);
699 + const config = vikPopupMenuConfig(root);
551 700
552 701 if (typeof data === 'undefined') {
553 702 // return popup buttons
554 703 return config.buttons;
@@ -562,11 +711,12 @@
562 711 // set specified buttons
563 712 config.buttons = data;
564 713
565 714 // iterate all buttons
566 - for (var i = 0; i < config.buttons.length; i++) {
715 + for (let i = 0; i < config.buttons.length; i++) {
567 716 // create default button properties
568 717 config.buttons[i] = $.extend({
718 + group: '',
569 719 icon: null,
570 720 text: '',
571 721 action: null,
572 722 shortcut: null,
@@ -573,8 +723,9 @@
573 723 class: '',
574 724 disabled: false,
575 725 visible: true,
576 726 separator: false,
727 + searchable: true,
577 728 }, config.buttons[i]);
578 729
579 730 // make sure we have an array
580 731 if (!Array.isArray(config.buttons[i].shortcut)) {
@@ -595,11 +746,11 @@
595 746 * @param mixed event The dispatcher DOM event.
596 747 *
597 748 * @param self
598 749 */
599 - var vikPopupMenuCalcPosition = function(root, popup, event) {
750 + const vikPopupMenuCalcPosition = function(root, popup, event) {
600 751 // get popup configuration
601 - var config = vikPopupMenuConfig(root);
752 + const config = vikPopupMenuConfig(root);
602 753
603 754 // in case of "auto" placement, we need to make sure
604 755 // that we own an event to access the mouse coordinates
605 756 if (config.placement == 'auto' && !event) {
@@ -607,19 +758,19 @@
607 758 config.placement = 'right';
608 759 }
609 760
610 761 // calculate root offset
611 - var rootOffset = $(root).offset();
762 + let rootOffset = $(root).offset();
612 763 // calculate root size
613 - var rootWidth = $(root).outerWidth();
614 - var rootHeight = $(root).outerHeight();
764 + let rootWidth = $(root).outerWidth();
765 + let rootHeight = $(root).outerHeight();
615 766 // calculate popup size
616 - var popupWidth = $(popup).outerWidth();
617 - var popupHeight = $(popup).outerHeight();
767 + let popupWidth = $(popup).outerWidth();
768 + let popupHeight = $(popup).outerHeight();
618 769
619 - var x, y;
770 + let x, y;
620 771
621 - // display popup above the root, centered
772 + // display popup above the root
622 773 if (config.placement == 'top') {
623 774 x = rootOffset.left + rootWidth / 2 - popupWidth / 2;
624 775 y = rootOffset.top - popupHeight - 4;
625 776 }
@@ -632,9 +783,14 @@
632 783 else if (config.placement == 'top-left') {
633 784 x = rootOffset.left;
634 785 y = rootOffset.top - popupHeight - 4;
635 786 }
636 - // display the popup below the root, centered
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
637 793 else if (config.placement == 'bottom') {
638 794 x = rootOffset.left + rootWidth / 2 - popupWidth / 2;
639 795 y = rootOffset.top + rootHeight + 4;
640 796 }
@@ -647,8 +803,13 @@
647 803 else if (config.placement == 'bottom-left') {
648 804 x = rootOffset.left;
649 805 y = rootOffset.top + rootHeight + 4;
650 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 + }
651 812 // display the popup before the root
652 813 else if (config.placement == 'left') {
653 814 x = rootOffset.left - popupWidth - 4;
654 815 y = rootOffset.top + rootHeight / 2 - popupHeight / 2;
@@ -664,13 +825,13 @@
664 825 y = event.pageY;
665 826 }
666 827
667 828 // calculate screen size
668 - var screenWidth = $(window).width();
669 - var screenHeight = $(window).height();
829 + let screenWidth = $(window).width();
830 + let screenHeight = $(window).height();
670 831 // calculate window scrolls
671 - var windowScrollLeft = $(window).scrollLeft();
672 - var windowScrollTop = $(window).scrollTop();
832 + let windowScrollLeft = $(window).scrollLeft();
833 + let windowScrollTop = $(window).scrollTop();
673 834
674 835 // use 4 pixel as minimum value
675 836 x = Math.max(4, x);
676 837 y = Math.max(4, y);
@@ -693,9 +854,9 @@
693 854 // register listener to auto-close the popup when clicked outside
694 855 $(document).on('mousedown', function() {
695 856 // we need to propagate the event with a proxy so that we can safely
696 857 // detach the registered callbacks when the popup gets closed
697 - $(document).trigger('mousedown.contextmenu.vikbooking');
858 + $(document).trigger('mousedown.contextmenu.vik');
698 859 });
699 860
700 861 // register listener to dispatch the actions of the buttons via keyboard
701 862 $(document).on('keydown', function() {
@@ -700,9 +861,9 @@
700 861 // register listener to dispatch the actions of the buttons via keyboard
701 862 $(document).on('keydown', function() {
702 863 // we need to propagate the event with a proxy so that we can safely
703 864 // detach the registered callbacks when the popup gets destroyed
704 - $(document).trigger('keydown.contextmenu.vikbooking');
865 + $(document).trigger('keydown.contextmenu.vik');
705 866 });
706 867
707 868 // register the jQuery callback
708 869 $.fn.vboContextMenu = function(method, data) {
@@ -742,9 +903,9 @@
742 903 }
743 904 // fallback to configuration setting getter/setter
744 905 else {
745 906 // access configuration
746 - var config = vikPopupMenuConfig(this);
907 + const config = vikPopupMenuConfig(this);
747 908
748 909 // check if the second argument was passed
749 910 if (typeof data !== 'undefined') {
750 911 if (method == 'trigger') {
@@ -782,8 +943,13 @@
782 943 lockScroll: true,
783 944 darkMode: null,
784 945 hideOnEsc: true,
785 946 formatShortcut: null,
947 + search: false,
948 + searchHint: '',
949 + searchEmpty: 'No results.',
950 + searchClass: 'separator',
951 + searchFocus: true,
786 952 },
787 953 };
788 954
789 955 /**
@@ -794,11 +960,11 @@
794 960 * @return boolean True if matches, otherwise false.
795 961 */
796 962 KeyboardEvent.prototype.shortcut = function(keys) {
797 963 // get modifiers list
798 - var modifiers = keys.slice(0);
964 + let modifiers = keys.slice(0);
799 965 // pop character from modifiers
800 - var keyCode = modifiers.pop();
966 + let keyCode = modifiers.pop();
801 967
802 968 if (typeof keyCode === 'string') {
803 969 // get ASCII
804 970 keyCode = keyCode.toUpperCase().charCodeAt(0);
@@ -808,19 +974,19 @@
808 974 modifiers = modifiers.map(function(mod) {
809 975 return mod.toLowerCase();
810 976 });
811 977
812 - var ok = false;
978 + let ok = false;
813 979
814 980 // validate key code
815 981 if (this.keyCode == keyCode) {
816 982 // validate modifiers
817 983 ok = true;
818 - var lookup = ['meta', 'shift', 'alt', 'ctrl'];
984 + const lookup = ['meta', 'shift', 'alt', 'ctrl'];
819 985
820 - for (var i = 0; i < lookup.length && ok; i++) {
986 + for (let i = 0; i < lookup.length && ok; i++) {
821 987 // check if modifiers is pressed
822 - var mod = this[lookup[i] + 'Key'];
988 + let mod = this[lookup[i] + 'Key'];
823 989
824 990 if (mod) {
825 991 // if pressed, the shortcut must specify it
826 992 ok &= modifiers.indexOf(lookup[i]) !== -1;