Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
File Manager
/
wp-content
/
plugins
/
vikbooking
/
admin
/
resources
:
contextmenu.js
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
/** * jQuery add-on used to support context menus. * * @version 2.2.4 * @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. * 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 = $('<div class="vik-context-menu"><ul class="buttons-list"></ul></div>'); if (config.search) { // create search input const search = $('<input type="text" />'); 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( $('<li class="no-matches"></li>').append( $('<a class="disabled"></a>').append( $('<span class="button-text"></span>').text(config.searchEmpty) ) ) ); } if (term.length) { searchClear.show(); } else { searchClear.hide(); } }); // create button to clear the text const searchClear = $('<button type="button" class="search-clear"><i class="fas fa-times-circle"></i></button>'); // register event to clear the input searchClear.on('click', () => { search.val('').trigger('keyup'); }).hide(); // create search list item const searchLi = $('<li class="search-box"></li>').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 = $('<a></a>'); 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 = $('<img>').attr('src', icon); } else { // we probably have a font icon icon = $('<i></i>').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($('<span class="button-icon"></span>').append(icon)); } } // insert text button popupBtn.append($('<span class="button-text"></span>').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 = '<i class="fas fa-backspace"></i>'; break; // enter case 13: k = '⏎'; break; // space case 32: k = 'Space'; break; // arrow up case 37: k = '<i class="fas fa-long-arrow-alt-left"></i>'; break; // arrow up case 38: k = '<i class="fas fa-long-arrow-alt-up"></i>'; break; // arrow right case 39: k = '<i class="fas fa-long-arrow-alt-right"></i>'; break; // arrow down case 40: k = '<i class="fas fa-long-arrow-alt-down"></i>'; 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($('<span class="button-shortcut"></span>').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 <ul> compliance const popupItem = $('<li data-id="' + i + '"></li>').append(popupBtn); // in case of a custom class, add it to the li and to the link if (btn.class) { popupItem.addClass(btn.class); popupBtn.addClass(btn.class); } if (!btn.searchable) { popupItem.addClass('not-searchable'); } // in case of a separator, add a specific class if (btn.separator) { popupItem.addClass('separator'); } /** * Register a sub group of buttons to improve individual styling. * * @since 2.1 */ if (btn.group) { // obtain the group element let ulGroup = popup.find('ul.' + btn.group); if (ulGroup.length == 0) { // create now in case it doesn't exist yet ulGroup = $('<ul></ul>').addClass(btn.group); popup.find('ul.buttons-list').append($('<li class="buttons-subgroup separator"></li>').append(ulGroup)); } // append item to the given group ulGroup.append(popupItem); } else { // add button to the default list popup.find('ul.buttons-list').append(popupItem); } }); // hide the popup before appending it popup.hide(); // append button to body $('body').append(popup); // calculate popup position vikPopupMenuCalcPosition(root, popup, event); if (config.lockScroll) { // prevent document from scrolling $('body').addClass('lock-scroll'); } // show popup popup.show(); if (config.search && config.searchFocus) { // auto-focus the search box popup.find('.search-box input').focus(); } // look for a specific callback to be triggered on opening if (config.onShow) { // trigger show callback config.onShow(root, popup, event); } // Register callback to auto dismiss the popup when clicked outside. // Use mousedown event because it will be execured before any other // supported trigger, so that the context menus can be shown on cascade. $(document).on('mousedown.contextmenu.vik', function(event) { // ignore the event with this namespace because it will end up // to catch also the plain mousedown event if (event.namespace == 'contextmenu.vik') { return false; } if (!popup.is(':visible')) { // dialog not visible return false; } // get list of buttons const links = popup.find('a'); // make sure we haven't clicked the popup or a link if (!popup.is(event.target) && popup.has(event.target).length === 0 && !links.is(event.target) && links.has(event.target).length === 0) { // auto close popup when clicked outside vikPopupMenuHide(root); event.stopPropagation(); event.preventDefault(); return false; } }); return root; }; /** * Hides the popup menu. * @param object root The selector element. * * @return self */ const vikPopupMenuHide = function(root) { // get popup configuration const config = vikPopupMenuConfig(root); // go ahead only in case a popup of this element is open if (config.isPopupOpen && $('.vik-context-menu').length) { // remove "open" flag after closing the context menu of this element delete config.isPopupOpen; vikPopupMenuConfig(root, config); // remove the focus from the active element to prevent unexpected scrolls document.activeElement.blur(); // destroy any existing popup menu because we do not support // more than a popup per time $('.vik-context-menu').remove(); // always restore scroll functions $('body').removeClass('lock-scroll'); // turn off proxy $(document).off('mousedown.contextmenu.vik'); // look for a specific callback to be triggered on dismiss if (config.onHide) { // trigger hide callback config.onHide(root); } } return root; }; /** * Destroys the popup menu. * @param object root The selector element. * * @return self */ const vikPopupMenuDestroy = function(root) { // in case the popup was open, close it first vikPopupMenuHide(root); // get popup configuration const config = vikPopupMenuConfig(root); // detach keyboard listener $(document).off('keydown.contextmenu.vik'); // remove CSS class used to disable the selection from root element $(root).removeClass('vik-context-menu-disable-selection'); // detach previous event without attaching a new one vikPopupMenuTrigger(root, null, config.trigger); // detach clickable property vikPopupMenuClickable(root, null, config.clickable); // then destroy the registered data return vikPopupMenuConfig(root, null); }; /** * Getter and setter of the popup buttons. * * @param object root The selector element. * @param mixed data The popup buttons to set. When omitted, * the method will act as a getter. * * @param mixed Returns the buttons list when the data argument is * missing. Otherwise itself will be returned. */ const vikPopupMenuButtons = function(root, data) { // get configuration const config = vikPopupMenuConfig(root); if (typeof data === 'undefined') { // return popup buttons return config.buttons; } // make sure the buttons property is an Array if (!Array.isArray(data)) { throw 'Invalid buttons, an Array was expected.'; } // set specified buttons config.buttons = data; // iterate all buttons for (let i = 0; i < config.buttons.length; i++) { // create default button properties config.buttons[i] = $.extend({ group: '', icon: null, text: '', action: null, shortcut: null, class: '', disabled: false, visible: true, separator: false, searchable: true, }, config.buttons[i]); // make sure we have an array if (!Array.isArray(config.buttons[i].shortcut)) { // invalid shortcut config.buttons[i].shortcut = null; } } // register configuration return vikPopupMenuConfig(root, config); }; /** * Calculates and sets the proper position of the popup. * * @param object root The selector element. * @param mixed popup The popup element. * @param mixed event The dispatcher DOM event. * * @param self */ const vikPopupMenuCalcPosition = function(root, popup, event) { // get popup configuration const config = vikPopupMenuConfig(root); // in case of "auto" placement, we need to make sure // that we own an event to access the mouse coordinates if (config.placement == 'auto' && !event) { // no event was passed, fallback to right config.placement = 'right'; } // calculate root offset let rootOffset = $(root).offset(); // calculate root size let rootWidth = $(root).outerWidth(); let rootHeight = $(root).outerHeight(); // calculate popup size let popupWidth = $(popup).outerWidth(); let popupHeight = $(popup).outerHeight(); let x, y; // display popup above the root if (config.placement == 'top') { x = rootOffset.left + rootWidth / 2 - popupWidth / 2; y = rootOffset.top - popupHeight - 4; } // display popup above the root, to the right else if (config.placement == 'top-right') { x = rootOffset.left + rootWidth - popupWidth; y = rootOffset.top - popupHeight - 4; } // display popup above the root, to the left else if (config.placement == 'top-left') { x = rootOffset.left; y = rootOffset.top - popupHeight - 4; } // display the popup below the root else if (config.placement == 'bottom') { x = rootOffset.left + rootWidth / 2 - popupWidth / 2; y = rootOffset.top + rootHeight + 4; } // display the popup below the root, to the right else if (config.placement == 'bottom-right') { x = rootOffset.left + rootWidth - popupWidth; y = rootOffset.top + rootHeight + 4; } // display the popup below the root, to the left else if (config.placement == 'bottom-left') { x = rootOffset.left; y = rootOffset.top + rootHeight + 4; } // display the popup before the root else if (config.placement == 'left') { x = rootOffset.left - popupWidth - 4; y = rootOffset.top + rootHeight / 2 - popupHeight / 2; } // display the popup after the root else if (config.placement == 'right') { x = rootOffset.left + rootWidth + 4; y = rootOffset.top + rootHeight / 2 - popupHeight / 2; } // display the popup at the mouse coordinates else { x = event.pageX; y = event.pageY; } // calculate screen size let screenWidth = $(window).width(); let screenHeight = $(window).height(); // calculate window scrolls let windowScrollLeft = $(window).scrollLeft(); let windowScrollTop = $(window).scrollTop(); // use 4 pixel as minimum value x = Math.max(4, x); y = Math.max(4, y); // make sure the popup doesn't exceed the screen width if (x + popupWidth + 4 > screenWidth) { x = screenWidth - popupWidth - 4; } // make sure the popup doesn't exceed the screen height if (y + popupHeight + 4 > screenHeight + windowScrollTop) { y = screenHeight - popupHeight - 4 + windowScrollTop; } $(popup).css('top', y).css('left', x); return root; }; // register listener to auto-close the popup when clicked outside $(document).on('mousedown', function() { // we need to propagate the event with a proxy so that we can safely // detach the registered callbacks when the popup gets closed $(document).trigger('mousedown.contextmenu.vik'); }); // register listener to dispatch the actions of the buttons via keyboard $(document).on('keydown', function() { // we need to propagate the event with a proxy so that we can safely // detach the registered callbacks when the popup gets destroyed $(document).trigger('keydown.contextmenu.vik'); }); // register the jQuery callback $.fn.vboContextMenu = function(method, data) { if (!method) { method = {}; } // immediately exit in case of no elements found if ($(this).length == 0) { return this; } // initialize popup events if (typeof method === 'object') { return vikPopupMenuInit(this, method); } // check if we should dismiss the popup else if (typeof method === 'string' && method.match(/^(close|dismiss|hide)$/i)) { return vikPopupMenuHide(this); } // check if we should open the popup else if (typeof method === 'string' && method.match(/^(show|open)$/i)) { return vikPopupMenuShow(this); } // check if we destroy the popup else if (typeof method === 'string' && method.match(/^(destroy)$/i)) { return vikPopupMenuDestroy(this); } // check if we should return the popup configuration else if (typeof method === 'string' && method.match(/^(config|configuration|options)$/i)) { return vikPopupMenuConfig(this); } // check if we should handle with the popup buttons else if (typeof method === 'string' && method.match(/^(buttons)$/i)) { // use getter/setter according to the specified arguments return vikPopupMenuButtons(this, data); } // fallback to configuration setting getter/setter else { // access configuration const config = vikPopupMenuConfig(this); // check if the second argument was passed if (typeof data !== 'undefined') { if (method == 'trigger') { // register trigger before updating the configuration data = vikPopupMenuTrigger(this, data, config.trigger); } else if (method == 'clickable') { // handle clickable property vikPopupMenuClickable(this, data, config.clickable); } // register argument within configuration config[method] = data; // refresh configuration and return self instance return vikPopupMenuConfig(this, config); } // return configuration setting return config[method]; } return this; }; // define the default configuration to use for the context menu $.vboContextMenu = { defaults: { trigger: 'click', placement: 'auto', class: '', buttons: [], onShow: null, onHide: null, clickable: false, lockScroll: true, darkMode: null, hideOnEsc: true, formatShortcut: null, search: false, searchHint: '', searchEmpty: 'No results.', searchClass: 'separator', searchFocus: true, }, }; /** * Checks if the KeyBoard event matches the given shortcut. * * @param array keys The shortcut representation. * * @return boolean True if matches, otherwise false. */ KeyboardEvent.prototype.shortcut = function(keys) { // get modifiers list let modifiers = keys.slice(0); // pop character from modifiers let keyCode = modifiers.pop(); if (typeof keyCode === 'string') { // get ASCII keyCode = keyCode.toUpperCase().charCodeAt(0); } // make sure the modifiers are lower case modifiers = modifiers.map(function(mod) { return mod.toLowerCase(); }); let ok = false; // validate key code if (this.keyCode == keyCode) { // validate modifiers ok = true; const lookup = ['meta', 'shift', 'alt', 'ctrl']; for (let i = 0; i < lookup.length && ok; i++) { // check if modifiers is pressed let mod = this[lookup[i] + 'Key']; if (mod) { // if pressed, the shortcut must specify it ok &= modifiers.indexOf(lookup[i]) !== -1; } else { // if not pressed, the shortcut must not include it ok &= modifiers.indexOf(lookup[i]) === -1; } } } return ok; } })(jQuery);