import "core-js/modules/es.array.concat"; import "core-js/modules/es.function.name"; import "core-js/modules/es.object.keys"; import "core-js/modules/es.object.to-string"; import "core-js/modules/es.regexp.constructor"; import "core-js/modules/es.regexp.exec"; import "core-js/modules/es.regexp.to-string"; import "core-js/modules/es.string.replace"; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } import Core from './../../core'; import { addClass, empty, fastInnerHTML, getScrollbarWidth, isChildOf, isInput, removeClass, getParentWindow } from './../../helpers/dom/element'; import { arrayEach, arrayFilter, arrayReduce } from './../../helpers/array'; import Cursor from './cursor'; import EventManager from './../../eventManager'; import { mixin, hasOwnProperty } from './../../helpers/object'; import { isUndefined, isDefined } from './../../helpers/mixed'; import { debounce, isFunction } from './../../helpers/function'; import { filterSeparators, hasSubMenu, isDisabled, isItemHidden, isSeparator, isSelectionDisabled, normalizeSelection } from './utils'; import { KEY_CODES } from './../../helpers/unicode'; import localHooks from './../../mixins/localHooks'; import { SEPARATOR, NO_ITEMS, predefinedItems } from './predefinedItems'; import { stopImmediatePropagation, isRightClick } from './../../helpers/dom/event'; import { isWindowsOS } from './../../helpers/browser'; var MIN_WIDTH = 215; /** * @class Menu * @plugin ContextMenu */ var Menu = /*#__PURE__*/ function () { function Menu(hotInstance, options) { _classCallCheck(this, Menu); this.hot = hotInstance; this.options = options || { parent: null, name: null, className: '', keepInViewport: true, standalone: false, minWidth: MIN_WIDTH, container: this.hot.rootDocument.documentElement }; this.eventManager = new EventManager(this); this.container = this.createContainer(this.options.name); this.hotMenu = null; this.hotSubMenus = {}; this.parentMenu = this.options.parent || null; this.menuItems = null; this.origOutsideClickDeselects = null; this.keyEvent = false; this.offset = { above: 0, below: 0, left: 0, right: 0 }; this._afterScrollCallback = null; this.registerEvents(); } /** * Register event listeners. * * @private */ _createClass(Menu, [{ key: "registerEvents", value: function registerEvents() { var _this = this; var frame = this.hot.rootWindow; while (frame) { this.eventManager.addEventListener(frame.document, 'mousedown', function (event) { return _this.onDocumentMouseDown(event); }); frame = getParentWindow(frame); } } /** * Set array of objects which defines menu items. * * @param {Array} menuItems Menu items to display. */ }, { key: "setMenuItems", value: function setMenuItems(menuItems) { this.menuItems = menuItems; } /** * Returns currently selected menu item. Returns `null` if no item was selected. * * @returns {Object|null} */ }, { key: "getSelectedItem", value: function getSelectedItem() { return this.hasSelectedItem() ? this.hotMenu.getSourceDataAtRow(this.hotMenu.getSelectedLast()[0]) : null; } /** * Checks if the menu has selected (highlighted) any item from the menu list. * * @returns {Boolean} */ }, { key: "hasSelectedItem", value: function hasSelectedItem() { return Array.isArray(this.hotMenu.getSelectedLast()); } /** * Set offset menu position for specified area (`above`, `below`, `left` or `right`). * * @param {String} area Specified area name (`above`, `below`, `left` or `right`). * @param {Number} offset Offset value. */ }, { key: "setOffset", value: function setOffset(area) { var offset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; this.offset[area] = offset; } /** * Check if menu is using as sub-menu. * * @returns {Boolean} */ }, { key: "isSubMenu", value: function isSubMenu() { return this.parentMenu !== null; } /** * Open menu. * * @fires Hooks#beforeContextMenuShow * @fires Hooks#afterContextMenuShow */ }, { key: "open", value: function open() { var _this2 = this; this.runLocalHooks('beforeOpen'); this.container.removeAttribute('style'); this.container.style.display = 'block'; var delayedOpenSubMenu = debounce(function (row) { return _this2.openSubMenu(row); }, 300); var minWidthOfMenu = this.options.minWidth || MIN_WIDTH; var noItemsDefined = false; var filteredItems = arrayFilter(this.menuItems, function (item) { if (item.key === NO_ITEMS) { noItemsDefined = true; } return isItemHidden(item, _this2.hot); }); if (filteredItems.length < 1 && !noItemsDefined) { filteredItems.push(predefinedItems()[NO_ITEMS]); } else if (filteredItems.length === 0) { return; } filteredItems = filterSeparators(filteredItems, SEPARATOR); var shouldAutoCloseMenu = false; var settings = { data: filteredItems, colHeaders: false, autoColumnSize: true, autoWrapRow: false, modifyColWidth: function modifyColWidth(width) { if (isDefined(width) && width < minWidthOfMenu) { return minWidthOfMenu; } return width; }, autoRowSize: false, readOnly: true, editor: false, copyPaste: false, columns: [{ data: 'name', renderer: function renderer(hot, TD, row, col, prop, value) { return _this2.menuItemRenderer(hot, TD, row, col, prop, value); } }], renderAllRows: true, fragmentSelection: false, outsideClickDeselects: false, disableVisualSelection: 'area', beforeKeyDown: function beforeKeyDown(event) { return _this2.onBeforeKeyDown(event); }, afterOnCellMouseOver: function afterOnCellMouseOver(event, coords) { if (_this2.isAllSubMenusClosed()) { delayedOpenSubMenu(coords.row); } else { _this2.openSubMenu(coords.row); } }, rowHeights: function rowHeights(row) { return filteredItems[row].name === SEPARATOR ? 1 : 23; }, afterOnCellContextMenu: function afterOnCellContextMenu(event) { event.preventDefault(); // On the Windows platform, the "contextmenu" is triggered after the "mouseup" so that's // why the closing menu is here. (#6507#issuecomment-582392301). if (isWindowsOS() && shouldAutoCloseMenu && _this2.hasSelectedItem()) { _this2.close(true); } }, beforeOnCellMouseUp: function beforeOnCellMouseUp(event) { if (_this2.hasSelectedItem()) { shouldAutoCloseMenu = !_this2.isCommandPassive(_this2.getSelectedItem()); _this2.executeCommand(event); } }, afterOnCellMouseUp: function afterOnCellMouseUp(event) { // If the code runs on the other platform than Windows, the "mouseup" is triggered // after the "contextmenu". So then "mouseup" closes the menu. Otherwise, the closing // menu responsibility is forwarded to "afterOnCellContextMenu" callback (#6507#issuecomment-582392301). if ((!isWindowsOS() || !isRightClick(event)) && shouldAutoCloseMenu && _this2.hasSelectedItem()) { _this2.close(true); } }, afterUnlisten: function afterUnlisten() { // Restore menu focus, fix for `this.instance.unlisten();` call in the tableView.js@260 file. // This prevents losing table responsiveness for keyboard events when filter select menu is closed (#6497). if (!_this2.hasSelectedItem() && _this2.isOpened()) { _this2.hotMenu.listen(false); } } }; this.origOutsideClickDeselects = this.hot.getSettings().outsideClickDeselects; this.hot.getSettings().outsideClickDeselects = false; this.hotMenu = new Core(this.container, settings); this.hotMenu.addHook('afterInit', function () { return _this2.onAfterInit(); }); this.hotMenu.addHook('afterSelection', function () { return _this2.onAfterSelection.apply(_this2, arguments); }); this.hotMenu.init(); this.hotMenu.listen(); this.blockMainTableCallbacks(); this.runLocalHooks('afterOpen'); } /** * Close menu. * * @param {Boolean} [closeParent=false] if `true` try to close parent menu if exists. */ }, { key: "close", value: function close() { var closeParent = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; if (!this.isOpened()) { return; } if (closeParent && this.parentMenu) { this.parentMenu.close(); } else { this.closeAllSubMenus(); this.container.style.display = 'none'; this.releaseMainTableCallbacks(); this.hotMenu.destroy(); this.hotMenu = null; this.hot.getSettings().outsideClickDeselects = this.origOutsideClickDeselects; this.runLocalHooks('afterClose'); if (this.parentMenu) { this.parentMenu.hotMenu.listen(); } } } /** * Open sub menu at the provided row index. * * @param {Number} row Row index. * @returns {Menu|Boolean} Returns created menu or `false` if no one menu was created. */ }, { key: "openSubMenu", value: function openSubMenu(row) { if (!this.hotMenu) { return false; } var cell = this.hotMenu.getCell(row, 0); this.closeAllSubMenus(); if (!cell || !hasSubMenu(cell)) { return false; } var dataItem = this.hotMenu.getSourceDataAtRow(row); var subMenu = new Menu(this.hot, { parent: this, name: dataItem.name, className: this.options.className, keepInViewport: true, container: this.options.container }); subMenu.setMenuItems(dataItem.submenu.items); subMenu.open(); subMenu.setPosition(cell.getBoundingClientRect()); this.hotSubMenus[dataItem.key] = subMenu; return subMenu; } /** * Close sub menu at row index. * * @param {Number} row Row index. */ }, { key: "closeSubMenu", value: function closeSubMenu(row) { var dataItem = this.hotMenu.getSourceDataAtRow(row); var menus = this.hotSubMenus[dataItem.key]; if (menus) { menus.destroy(); delete this.hotSubMenus[dataItem.key]; } } /** * Close all opened sub menus. */ }, { key: "closeAllSubMenus", value: function closeAllSubMenus() { var _this3 = this; arrayEach(this.hotMenu.getData(), function (value, row) { return _this3.closeSubMenu(row); }); } /** * Checks if all created and opened sub menus are closed. * * @returns {Boolean} */ }, { key: "isAllSubMenusClosed", value: function isAllSubMenusClosed() { return Object.keys(this.hotSubMenus).length === 0; } /** * Destroy instance. */ }, { key: "destroy", value: function destroy() { var menuContainerParentElement = this.container.parentNode; this.clearLocalHooks(); this.close(); this.parentMenu = null; this.eventManager.destroy(); if (menuContainerParentElement) { menuContainerParentElement.removeChild(this.container); } } /** * Checks if menu was opened. * * @returns {Boolean} Returns `true` if menu was opened. */ }, { key: "isOpened", value: function isOpened() { return this.hotMenu !== null; } /** * Execute menu command. * * @param {Event} [event] */ }, { key: "executeCommand", value: function executeCommand(event) { if (!this.isOpened() || !this.hasSelectedItem()) { return; } var selectedItem = this.getSelectedItem(); this.runLocalHooks('select', selectedItem, event); if (this.isCommandPassive(selectedItem)) { return; } var selRanges = this.hot.getSelectedRange(); var normalizedSelection = selRanges ? normalizeSelection(selRanges) : []; this.runLocalHooks('executeCommand', selectedItem.key, normalizedSelection, event); if (this.isSubMenu()) { this.parentMenu.runLocalHooks('executeCommand', selectedItem.key, normalizedSelection, event); } } /** * Checks if the passed command is passive or not. The command is passive when it's marked as * disabled, the descriptor object contains `isCommand` property set to `false`, command * is a separator, or the item is recognized as submenu. For passive items the menu is not * closed automatically after the user trigger the command through the UI. * * @param {Object} commandDescriptor Selected menu item from the menu data source. * @returns {Boolean} */ }, { key: "isCommandPassive", value: function isCommandPassive(commandDescriptor) { var isCommand = commandDescriptor.isCommand, commandName = commandDescriptor.name, disabled = commandDescriptor.disabled, submenu = commandDescriptor.submenu; var isItemDisabled = disabled === true || typeof disabled === 'function' && disabled.call(this.hot) === true; return isCommand === false || commandName === SEPARATOR || isItemDisabled === true || submenu; } /** * Set menu position based on dom event or based on literal object. * * @param {Event|Object} coords Event or literal Object with coordinates. */ }, { key: "setPosition", value: function setPosition(coords) { var cursor = new Cursor(coords, this.container.ownerDocument.defaultView); if (this.options.keepInViewport) { if (cursor.fitsBelow(this.container)) { this.setPositionBelowCursor(cursor); } else if (cursor.fitsAbove(this.container)) { this.setPositionAboveCursor(cursor); } else { this.setPositionBelowCursor(cursor); } if (cursor.fitsOnRight(this.container)) { this.setPositionOnRightOfCursor(cursor); } else { this.setPositionOnLeftOfCursor(cursor); } } else { this.setPositionBelowCursor(cursor); this.setPositionOnRightOfCursor(cursor); } } /** * Set menu position above cursor object. * * @param {Cursor} cursor `Cursor` object. */ }, { key: "setPositionAboveCursor", value: function setPositionAboveCursor(cursor) { var top = this.offset.above + cursor.top - this.container.offsetHeight; if (this.isSubMenu()) { top = cursor.top + cursor.cellHeight - this.container.offsetHeight + 3; } this.container.style.top = "".concat(top, "px"); } /** * Set menu position below cursor object. * * @param {Cursor} cursor `Cursor` object. */ }, { key: "setPositionBelowCursor", value: function setPositionBelowCursor(cursor) { var top = this.offset.below + cursor.top; if (this.isSubMenu()) { top = cursor.top - 1; } this.container.style.top = "".concat(top, "px"); } /** * Set menu position on the right of cursor object. * * @param {Cursor} cursor `Cursor` object. */ }, { key: "setPositionOnRightOfCursor", value: function setPositionOnRightOfCursor(cursor) { var left; if (this.isSubMenu()) { left = 1 + cursor.left + cursor.cellWidth; } else { left = this.offset.right + 1 + cursor.left; } this.container.style.left = "".concat(left, "px"); } /** * Set menu position on the left of cursor object. * * @param {Cursor} cursor `Cursor` object. */ }, { key: "setPositionOnLeftOfCursor", value: function setPositionOnLeftOfCursor(cursor) { var left = this.offset.left + cursor.left - this.container.offsetWidth + getScrollbarWidth(this.hot.rootDocument) + 4; this.container.style.left = "".concat(left, "px"); } /** * Select first cell in opened menu. */ }, { key: "selectFirstCell", value: function selectFirstCell() { var cell = this.hotMenu.getCell(0, 0); if (isSeparator(cell) || isDisabled(cell) || isSelectionDisabled(cell)) { this.selectNextCell(0, 0); } else { this.hotMenu.selectCell(0, 0); } } /** * Select last cell in opened menu. */ }, { key: "selectLastCell", value: function selectLastCell() { var lastRow = this.hotMenu.countRows() - 1; var cell = this.hotMenu.getCell(lastRow, 0); if (isSeparator(cell) || isDisabled(cell) || isSelectionDisabled(cell)) { this.selectPrevCell(lastRow, 0); } else { this.hotMenu.selectCell(lastRow, 0); } } /** * Select next cell in opened menu. * * @param {Number} row Row index. * @param {Number} col Column index. */ }, { key: "selectNextCell", value: function selectNextCell(row, col) { var nextRow = row + 1; var cell = nextRow < this.hotMenu.countRows() ? this.hotMenu.getCell(nextRow, col) : null; if (!cell) { return; } if (isSeparator(cell) || isDisabled(cell) || isSelectionDisabled(cell)) { this.selectNextCell(nextRow, col); } else { this.hotMenu.selectCell(nextRow, col); } } /** * Select previous cell in opened menu. * * @param {Number} row Row index. * @param {Number} col Column index. */ }, { key: "selectPrevCell", value: function selectPrevCell(row, col) { var prevRow = row - 1; var cell = prevRow >= 0 ? this.hotMenu.getCell(prevRow, col) : null; if (!cell) { return; } if (isSeparator(cell) || isDisabled(cell) || isSelectionDisabled(cell)) { this.selectPrevCell(prevRow, col); } else { this.hotMenu.selectCell(prevRow, col); } } /** * Menu item renderer. * * @private */ }, { key: "menuItemRenderer", value: function menuItemRenderer(hot, TD, row, col, prop, value) { var _this4 = this; var item = hot.getSourceDataAtRow(row); var wrapper = this.hot.rootDocument.createElement('div'); var isSubMenu = function isSubMenu(itemToTest) { return hasOwnProperty(itemToTest, 'submenu'); }; var itemIsSeparator = function itemIsSeparator(itemToTest) { return new RegExp(SEPARATOR, 'i').test(itemToTest.name); }; var itemIsDisabled = function itemIsDisabled(itemToTest) { return itemToTest.disabled === true || typeof itemToTest.disabled === 'function' && itemToTest.disabled.call(_this4.hot) === true; }; var itemIsSelectionDisabled = function itemIsSelectionDisabled(itemToTest) { return itemToTest.disableSelection; }; var itemValue = value; if (typeof itemValue === 'function') { itemValue = itemValue.call(this.hot); } empty(TD); addClass(wrapper, 'htItemWrapper'); TD.appendChild(wrapper); if (itemIsSeparator(item)) { addClass(TD, 'htSeparator'); } else if (typeof item.renderer === 'function') { addClass(TD, 'htCustomMenuRenderer'); TD.appendChild(item.renderer(hot, wrapper, row, col, prop, itemValue)); } else { fastInnerHTML(wrapper, itemValue); } if (itemIsDisabled(item)) { addClass(TD, 'htDisabled'); this.eventManager.addEventListener(TD, 'mouseenter', function () { return hot.deselectCell(); }); } else if (itemIsSelectionDisabled(item)) { addClass(TD, 'htSelectionDisabled'); this.eventManager.addEventListener(TD, 'mouseenter', function () { return hot.deselectCell(); }); } else if (isSubMenu(item)) { addClass(TD, 'htSubmenu'); if (itemIsSelectionDisabled(item)) { this.eventManager.addEventListener(TD, 'mouseenter', function () { return hot.deselectCell(); }); } else { this.eventManager.addEventListener(TD, 'mouseenter', function () { return hot.selectCell(row, col, void 0, void 0, false, false); }); } } else { removeClass(TD, ['htSubmenu', 'htDisabled']); if (itemIsSelectionDisabled(item)) { this.eventManager.addEventListener(TD, 'mouseenter', function () { return hot.deselectCell(); }); } else { this.eventManager.addEventListener(TD, 'mouseenter', function () { return hot.selectCell(row, col, void 0, void 0, false, false); }); } } } /** * Create container/wrapper for handsontable. * * @private * @param {String} [name] Class name. * @returns {HTMLElement} */ }, { key: "createContainer", value: function createContainer() { var name = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; var doc = this.options.container.ownerDocument; var className = name; var container; if (className) { if (isFunction(className)) { className = className.call(this.hot); if (className === null || isUndefined(className)) { className = ''; } else { className = className.toString(); } } className = className.replace(/[^A-z0-9]/g, '_'); className = "".concat(this.options.className, "Sub_").concat(className); container = doc.querySelector(".".concat(this.options.className, ".").concat(className)); } if (!container) { container = doc.createElement('div'); addClass(container, "htMenu ".concat(this.options.className)); if (className) { addClass(container, className); } this.options.container.appendChild(container); } return container; } /** * @private */ }, { key: "blockMainTableCallbacks", value: function blockMainTableCallbacks() { this._afterScrollCallback = function () {}; this.hot.addHook('afterScrollVertically', this._afterScrollCallback); this.hot.addHook('afterScrollHorizontally', this._afterScrollCallback); } /** * @private */ }, { key: "releaseMainTableCallbacks", value: function releaseMainTableCallbacks() { if (this._afterScrollCallback) { this.hot.removeHook('afterScrollVertically', this._afterScrollCallback); this.hot.removeHook('afterScrollHorizontally', this._afterScrollCallback); this._afterScrollCallback = null; } } /** * On before key down listener. * * @private * @param {Event} event */ }, { key: "onBeforeKeyDown", value: function onBeforeKeyDown(event) { // For input elements, prevent event propagation. It allows entering text into an input // element freely - without steeling the key events from the menu module (#6506, #6549). if (isInput(event.target) && this.container.contains(event.target)) { stopImmediatePropagation(event); return; } var selection = this.hotMenu.getSelectedLast(); var stopEvent = false; this.keyEvent = true; switch (event.keyCode) { case KEY_CODES.ESCAPE: this.close(); stopEvent = true; break; case KEY_CODES.ENTER: if (selection) { if (this.hotMenu.getSourceDataAtRow(selection[0]).submenu) { stopEvent = true; } else { this.executeCommand(event); this.close(true); } } break; case KEY_CODES.ARROW_DOWN: if (selection) { this.selectNextCell(selection[0], selection[1]); } else { this.selectFirstCell(); } stopEvent = true; break; case KEY_CODES.ARROW_UP: if (selection) { this.selectPrevCell(selection[0], selection[1]); } else { this.selectLastCell(); } stopEvent = true; break; case KEY_CODES.ARROW_RIGHT: if (selection) { var menu = this.openSubMenu(selection[0]); if (menu) { menu.selectFirstCell(); } } stopEvent = true; break; case KEY_CODES.ARROW_LEFT: if (selection && this.isSubMenu()) { this.close(); if (this.parentMenu) { this.parentMenu.hotMenu.listen(); } stopEvent = true; } break; default: break; } if (stopEvent) { event.preventDefault(); stopImmediatePropagation(event); } this.keyEvent = false; } /** * On after init listener. * * @private */ }, { key: "onAfterInit", value: function onAfterInit() { var wtTable = this.hotMenu.view.wt.wtTable; var data = this.hotMenu.getSettings().data; var hiderStyle = wtTable.hider.style; var holderStyle = wtTable.holder.style; var currentHiderWidth = parseInt(hiderStyle.width, 10); var realHeight = arrayReduce(data, function (accumulator, value) { return accumulator + (value.name === SEPARATOR ? 1 : 26); }, 0); holderStyle.width = "".concat(currentHiderWidth + 22, "px"); holderStyle.height = "".concat(realHeight + 4, "px"); hiderStyle.height = holderStyle.height; } /** * On after selection listener. * * @param {Number} r Selection start row index. * @param {Number} c Selection start column index. * @param {Number} r2 Selection end row index. * @param {Number} c2 Selection end column index. * @param {Object} preventScrolling Object with `value` property where its value change will be observed. * @param {Number} selectionLayerLevel The number which indicates what selection layer is currently modified. */ }, { key: "onAfterSelection", value: function onAfterSelection(r, c, r2, c2, preventScrolling) { if (this.keyEvent === false) { preventScrolling.value = true; } } /** * Document mouse down listener. * * @private * @param {Event} event */ }, { key: "onDocumentMouseDown", value: function onDocumentMouseDown(event) { if (!this.isOpened()) { return; } // Close menu when clicked element is not belongs to menu itself if (this.options.standalone && this.hotMenu && !isChildOf(event.target, this.hotMenu.rootElement)) { this.close(true); // Automatically close menu when clicked element is not belongs to menu or submenu (not necessarily to itself) } else if ((this.isAllSubMenusClosed() || this.isSubMenu()) && !isChildOf(event.target, '.htMenu') && (isChildOf(event.target, this.container.ownerDocument) || isChildOf(event.target, this.hot.rootDocument))) { this.close(true); } } }]); return Menu; }(); mixin(Menu, localHooks); export default Menu;