/** * jQuery add-on used to support context menus. * Here's a list of supported options. * * @author E4J srl * * - trigger string The command that should trigger the popup menu. Accepts the * following values: click|doubleclick|rightclick|hover. * Click will be used by default. * - placement string Where the popup should be displayed in relation to the target. * Accepts the following values: auto|top|right|bottom|left. * Auto will be used by default (at the mouse coordinates). * - class string An optional class to use for individual styling. * - buttons object[] A list of buttons to include within the popup menu. See the options * of the buttons for further details. * - onShow function An optional callback to invoke when the popup menu is displayed. * - onHide function An optional callback to invoke when the popup menu is dismissed. * - darkMode mixed Flag for dark mode layout, which accepts 3 possible values: * true|false|null. Pass true to always force the dark mode, false to * always use the light mode, null to auto-detect the proper mode * according to the preferred theme of the browser. * - clickable boolean Flag used to check whether the root element should prevent the * browser selection by applying specific CSS rules. False by default. * - lockScroll boolean Flag used to prevent the document scroll when the context menu * pops up. True by default. * - hideOnEsc boolean Choose whether the context menu should be closed when ESC key is * pressed. Always true by default. * - formatShortcut mixed An optional callback that can be used to format the shortcut symbols. * * Here's a list of options supported by the buttons. Any other property of the button will * be accessible by the internal methods. * * - icon mixed Either a function, an image URL, an image instance or a font icon * to display before the button text. In case of a function, it will be * used as callback to define an image/icon at runtime. * - text string The text to display for the popup menu button. * - action function The callback to dispatch when the button gets clicked. * - class string An optional class to use for individual styling. * - disabled mixed Either a function or a boolean to check whether the button should * be clicked or not. The button is never disabled by default. * - visible mixed Either a function or a boolean to check whether the button should * be displayed or not. The button is always visible by default. * - separator boolean Flag used to check whether the popup should include a separator after the * button. False by default. * - shortcut mixed An array of commands to represent the shortcut that will trigger the action * via keyboard. The array must contain one and only one character or symbol. * The array may contain one ore more modifiers, which must be specified first. * * List of methods supported by the add-on. * * - show Manually displays the popup menu. * - hide Manually disposes the popup menu. * - destroy Destroys the popup attached to the element. * - config Returns the configuration of the popup. * - buttons Getter/setter of the popup buttons. * * It is possible to update each setting configuration by using the same * name of the property and the related value to set. Leave the set argument * empty to simply access the current property value. In example: * * jQuery(target).vboContextMenu( 'trigger', 'click'); * jQuery(target).vboContextMenu('placement', 'auto'); */ (function($) { /** * Popup menu trigger setup. * * @param object root The selector element. * @param string trigger The trigger to use. * @param mixed prev The previous trigger. * * @return string The trigger event. */ var vikPopupMenuTrigger = function(root, trigger, prev) { // check if the trigger was already registered if (prev) { // detach previous trigger $(root).off(prev.toLowerCase()); } if (!trigger) { // abort in case of missing trigger return false; } // normalize trigger event switch (trigger.toLowerCase()) { case 'mouseover': case 'hover': trigger = 'mouseover'; break; case 'dblclick': case 'doubleclick': case 'double-click': trigger = 'dblclick'; break; case 'contextmenu': case 'rightclick': case 'right-click': trigger = 'contextmenu'; break; default: trigger = 'click'; }; // scan all the registered elements $(root).each(function() { // register new trigger $(this).on(trigger, function(event) { // always prevent default event event.preventDefault(); // open popup vikPopupMenuShow(this, event); }); }); return trigger; }; /** * Popup menu clickable setup. * * @param object root The selector element. * @param boolean flag True to make the root clickable. * @param mixed prev The flag previously set, if any. * * @return self */ var vikPopupMenuClickable = function(root, flag, prev) { if (prev) { // remove CSS class used to disable the selection from root element $(root).removeClass('vik-context-menu-disable-selection'); } if (flag) { // add CSS class to root element to disable the selection $(root).addClass('vik-context-menu-disable-selection'); } return root; }; /** * Initializes the popup menu. * * @param object root The selector element. * @param object options A configuration object. * * @return self */ var vikPopupMenuInit = function(root, options) { // inject the specified options within the default configuration options = $.extend($.vboContextMenu.defaults, options); // register the popup configuration for being used later vikPopupMenuConfig(root, options); // register trigger to show the popup menu options.trigger = vikPopupMenuTrigger(root, options.trigger); // normalize buttons vikPopupMenuButtons(root, options.buttons); // handle clickable property vikPopupMenuClickable(root, options.clickable); // register callback to dispatch the action of a button when its shortcut is pressed $(document).on('keydown.contextmenu.vikbooking', function(event) { // ignore the event with this namespace because it will end up // to catch also the plain keydown event if (event.namespace == 'contextmenu.vikbooking') { return true; } // go ahead only in case the focus is not help by a text field if ($(document.activeElement).is('input,textarea') == true) { // prevent shortcuts from catching typed characters return true; } // retrieve popup configuration var config = vikPopupMenuConfig(root); // in case ESC was pressed, check if we should hide the popup if (config.hideOnEsc && event.keyCode == 27) { // auto-close the context menu vikPopupMenuHide(root); return true; } // iterate all registered buttons $.each(config.buttons, (i, btn) => { // make sure we have a shortcut and an action to execute if (!btn.shortcut || !btn.action) { // nothing to do here, go ahead return true; } // check whether the shortcut is pressed if (event.originalEvent.shortcut(btn.shortcut)) { // launch callback to check whether the button is disabled // or simply rely on the specified boolean var disabled = typeof btn.disabled === 'function' ? btn.disabled(root, config) : btn.disabled; // trigger action only in case the button is not disabled if (!disabled) { // stop event propagation event.preventDefault(); event.stopPropagation(); // dispatch button action btn.action(root, event); } return false; } }); }); return root; }; /** * Getter and setter of the popup configuration. * * @param object root The selector element. * @param mixed data The popup configuration to set. When omitted, * the method will act as a getter. * * @param mixed Returns the configuration when the data argument is * missing. Otherwise itself will be returned. */ var vikPopupMenuConfig = function(root, data) { if (typeof data === 'undefined') { // GETTER: return popup configuration. // Clone the object in order to prevent manual edits to // the configuration properties. return Object.assign({}, $(root).data('popupConfiguration')); } // SETTER: update popup configuration return $(root).data('popupConfiguration', data); }; /** * Creates and shows the popup menu. * * @param object root The selector element. * @param Event event The dispatcher DOM event. * * @return self */ var vikPopupMenuShow = function(root, event) { if ($('.vik-context-menu').length) { // do not go ahead in case a context menu is visible return root; } // retrieve configuration var config = vikPopupMenuConfig(root); // prepare context menu structure var popup = $('
'); // in case of a custom class, add it if (config.class) { popup.addClass(config.class); } // look for dark mode if (config.darkMode === true) { // turn dark mode on popup.addClass('dark-mode'); } else if (config.darkMode === false) { // suppress dark mode popup.addClass('light-mode'); } // iterate registered buttons and append them one by one $.each(config.buttons, function(i, btn) { // launch callback to check whether the button should be displayed // or simply rely on the specified boolean var visible = typeof btn.visible === 'function' ? btn.visible(root, config) : btn.visible; if (!visible) { // skip button and go ahead return true; } // prepare button structure var popupBtn = $(''); if (btn.icon) { var icon; if (typeof btn.icon === 'function') { // we have a function, launch the callback // to extract the image at runtime icon = btn.icon(root, config); } else { // use it plain icon = btn.icon; } if (icon instanceof Image) { // we have an image instance icon = $(icon); } else if (typeof icon === 'string') { if (icon.indexOf('/') !== -1) { // we have an image URL icon = $('').attr('src', icon); } else { // we probably have a font icon icon = $('').addClass(icon); } } // leave as is in case a jQuery instance was passed // wrap icon in a parent and append all to button popupBtn.append($('').append(icon)); } // insert text button popupBtn.append($('').html(btn.text)); // check if the button specified a shortcut if (btn.shortcut) { // map shortcut elements var cmd = btn.shortcut.map(function(k) { var keyCode = k; switch (k) { case 'alt': k = "⌥"; break; case 'ctrl': k = "⌃"; break; case 'shift': k = "⇧"; break; case 'meta': k = "⌘"; break; // backspace case 8: k = ''; break; // enter case 13: k = '⏎'; break; // space case 32: k = 'Space'; break; // arrow up case 37: k = ''; break; // arrow up case 38: k = ''; break; // arrow right case 39: k = ''; break; // arrow down case 40: k = ''; break; // character default: k = typeof k === 'string' ? k.toUpperCase() : ''; } // look for a custom function used to format shortcuts if (typeof config.formatShortcut === 'function') { // launch the callback k = config.formatShortcut(keyCode, k); } return k; }); cmd = cmd.join(''); // wrap the shortcut between parenthesis in case of no modifiers if (cmd.length == 1) { cmd = '(' + cmd + ')'; } // insert shortcut button popupBtn.append($('').html(cmd)); } // launch callback to check whether the button should be disabled // or simply rely on the specified boolean var disabled = typeof btn.disabled === 'function' ? btn.disabled(root, config) : btn.disabled; // check whether the button is disabled if (disabled) { popupBtn.addClass('disabled'); } else { // register button click event popupBtn.on('click', function(event) { // look for an action callback if (btn.action) { // dispatch callback btn.action(root, event); } // always dismiss the popup when a button gets clicked vikPopupMenuHide(root); }); } // in case of a custom class, add it if (btn.class) { popupBtn.addClass(btn.class); } // wrap button within a parent for