/** * jQuery add-on used to support context menus. * * @version 2.2.5 * @author E4J srl * * Here's a list of supported options. * * @param trigger string The command that should trigger the popup menu. Accepts the * following values: click|doubleclick|rightclick|hover. * Click will be used by default. * @param placement string Where the popup should be displayed in relation to the target. * Accepts the following values: auto|top|right|bottom|left|center (combined). * Auto will be used by default (at the mouse coordinates). * @param class string An optional class to use for individual styling. * @param buttons object[] A list of buttons to include within the popup menu. See the options * of the buttons for further details. * @param onShow function An optional callback to invoke when the popup menu is displayed. * @param onHide function An optional callback to invoke when the popup menu is dismissed. * @param 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. * @param clickable bool Flag used to check whether the root element should prevent the * browser selection by applying specific CSS rules. False by default. * @param lockScroll bool Flag used to prevent the document scroll when the context menu * pops up. True by default. * @param hideOnEsc bool Choose whether the context menu should be closed when ESC key is * pressed. Always true by default. * @param formatShortcut mixed An optional callback that can be used to format the shortcut symbols. * @param search bool Whether the context menu should display a search box to filter the buttons. * @param searchHint string An optional placeholder to use for the search box. * @param searchEmpty string The string to display in case of no matching results. * @param searchClass string An optional extra class to apply to the search item. * @param searchFocus bool Whether the search bar should grab the focus on show. True by default. * * Here's a list of options supported by the buttons. Any other property of the button will * be accessible by the internal methods. * * @param string group The identifier of the group to which the item belongs (none by default). * @param icon mixed Either a function, a font icon, an image URL, an image instance or an HTML * node to display before the button text. In case of a function, it will be * used as callback to define an image/icon at runtime. * @param text string The text/html to display for the popup menu button. * @param action function The callback to dispatch when the button gets clicked. * @param class string An optional class to use for individual styling. Use "btngroup" to apply a * sort of fieldset title effect. Useful to describe a sub group. * @param 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. * @param 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. * @param separator bool Flag used to check whether the popup should include a separator after the * button. False by default. * @param 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. * @param searchable bool Whether this button can be searched. Ignored in case the search feature is off. * @param keywords string[] A list of keywords to match the searched value. This value is ignored in case * the search feature is disabled. Along with the specified keywords, the system will * keep searching on the button title too. * * List of methods supported by the add-on. * * @method show Manually displays the popup menu. * @method hide Manually disposes the popup menu. * @method destroy Destroys the popup attached to the element. * @method config Returns the configuration of the popup. * @method 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($) { 'use strict'; /** * 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. */ const 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 */ const 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 */ const 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.vik', function(event) { // ignore the event with this namespace because it will end up // to catch also the plain keydown event if (event.namespace == 'contextmenu.vik') { return true; } // retrieve popup configuration const 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; } // go ahead only in case the focus is not held by a text field if ($(document.activeElement).is('input,textarea') == true) { // prevent shortcuts from catching typed characters 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 let 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. */ const 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 */ const vikPopupMenuShow = function(root, event) { if ($('.vik-context-menu').length) { // do not go ahead in case a context menu is visible return root; } // retrieve configuration const config = vikPopupMenuConfig(root); // register a flag to easily check whether the context menu of this root is open config.isPopupOpen = true; vikPopupMenuConfig(root, config); // prepare context menu structure const popup = $('
'); if (config.search) { // create search input const search = $(''); if (config.searchHint) { search.attr('placeholder', config.searchHint); } search.on('keyup', function() { // obtain search term const term = $(search).val().toLowerCase(); // remove "no matches" element popup.find('li.no-matches').remove(); let atLeastOne = false; // scan all the buttons config.buttons.forEach((btn, i) => { const li = popup.find('li[data-id="' + i + '"]'); if (li.length === 0 || li.hasClass('not-searchable')) { // cannot search by this item return; } let btnText = typeof btn.text === 'object' ? $(btn.text).text() : btn.text + ''; // define list of keywords const keywords = [btnText].concat(btn.keywords || []); // check whether the button matches the given search term let match = keywords.some((k) => k.toLowerCase().indexOf(term) !== -1); if (match) { li.show(); atLeastOne = true; } else { li.hide(); } }); /** * Check whether we should completely hide a subgroup because all its children * don't match the specified search. */ popup.find('li.buttons-subgroup ul').each(function() { $(this).parent().show(); if ($(this).children().not('.btngroup').filter(':visible').length === 0) { // all sub-items are hidden, hide the sub-group too $(this).parent().hide(); } }); if (!atLeastOne) { // add "no matches" element in case of no results popup.find('ul.buttons-list').append( $('
  • ').append( $('').append( $('').text(config.searchEmpty) ) ) ); } if (term.length) { searchClear.show(); } else { searchClear.hide(); } }); // create button to clear the text const searchClear = $(''); // register event to clear the input searchClear.on('click', () => { search.val('').trigger('keyup'); }).hide(); // create search list item const searchLi = $('').append(search).append(searchClear); if (config.searchClass) { searchLi.addClass(config.searchClass); } // attach search input to the popup popup.find('ul.buttons-list').append(searchLi); } // 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 let visible = typeof btn.visible === 'function' ? btn.visible(root, config) : btn.visible; if (!visible) { // skip button and go ahead return true; } // prepare button structure const popupBtn = $(''); if (btn.icon) { let 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 if (icon !== null && icon !== undefined) { // 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 let cmd = btn.shortcut.map(function(k) { let 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 let 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); }); } // wrap button within a parent for