UNPKG

handsontable

Version:

Handsontable is a JavaScript Data Grid available for React, Angular and Vue.

694 lines (659 loc) • 24.5 kB
"use strict"; exports.__esModule = true; require("core-js/modules/es.error.cause.js"); require("core-js/modules/es.array.push.js"); var _positioner = require("./positioner"); var _navigator2 = require("./navigator"); var _shortcuts = require("./shortcuts"); var _predefinedItems = require("./../predefinedItems"); var _utils = require("./utils"); var _eventManager = _interopRequireDefault(require("../../../eventManager")); var _array = require("../../../helpers/array"); var _browser = require("../../../helpers/browser"); var _element = require("../../../helpers/dom/element"); var _event = require("../../../helpers/dom/event"); var _function = require("../../../helpers/function"); var _mixed = require("../../../helpers/mixed"); var _object = require("../../../helpers/object"); var _localHooks = _interopRequireDefault(require("../../../mixins/localHooks")); var _menuItemRenderer = require("./menuItemRenderer"); var _a11y = require("../../../helpers/a11y"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function _classPrivateFieldInitSpec(e, t, a) { _checkPrivateRedeclaration(e, t), t.set(e, a); } function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } function _classPrivateFieldSet(s, a, r) { return s.set(_assertClassBrand(s, a), r), r; } function _classPrivateFieldGet(s, a) { return s.get(_assertClassBrand(s, a)); } function _assertClassBrand(e, t, n) { if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; throw new TypeError("Private element is not present on this object"); } const MIN_WIDTH = 215; /** * @typedef MenuOptions * @property {Menu} [parent=null] Instance of {@link Menu}. * @property {string} [name=null] Name of the menu. * @property {string} [className=''] Custom class name. * @property {boolean} [keepInViewport=true] Determine if should be kept in viewport. * @property {boolean} [standalone] Enabling closing menu when clicked element is not belongs to menu itself. * @property {number} [minWidth=MIN_WIDTH] The minimum width. * @property {HTMLElement} [container] The container. */ /** * @private * @class Menu */ var _navigator = /*#__PURE__*/new WeakMap(); var _shortcutsCtrl = /*#__PURE__*/new WeakMap(); var _tableBorderWidth = /*#__PURE__*/new WeakMap(); class Menu { /** * Getter for the table border width. * This getter retrieves the border width of the table used in the menu. * * @returns {number} The border width of the table in pixels. */ get tableBorderWidth() { if (_classPrivateFieldGet(_tableBorderWidth, this) === undefined && this.hotMenu) { _classPrivateFieldSet(_tableBorderWidth, this, parseInt(this.hotMenu.rootWindow.getComputedStyle(this.hotMenu.view._wt.wtTable.TABLE).borderWidth, 10)); } return _classPrivateFieldGet(_tableBorderWidth, this); } /** * @param {Core} hotInstance Handsontable instance. * @param {MenuOptions} [options] Menu options. */ constructor(hotInstance, options) { var _this = this; /** * The Handsontable instance. * * @type {Core} */ _defineProperty(this, "hot", void 0); /** * The Menu options. * * @type {object} */ _defineProperty(this, "options", void 0); /** * @type {EventManager} */ _defineProperty(this, "eventManager", new _eventManager.default(this)); /** * The Menu container element. * * @type {HTMLElement} */ _defineProperty(this, "container", void 0); /** * @type {Positioner} */ _defineProperty(this, "positioner", void 0); /** * The instance of the Handsontable that is used as a menu. * * @type {Core} */ _defineProperty(this, "hotMenu", null); /** * The collection of the Handsontable instances that are used as sub-menus. * * @type {object} */ _defineProperty(this, "hotSubMenus", {}); /** * If the menu acts as the sub-menu then this property contains the reference to the parent menu. * * @type {Menu} */ _defineProperty(this, "parentMenu", void 0); /** * The menu items entries. * * @type {object[]} */ _defineProperty(this, "menuItems", null); /** * @type {boolean} */ _defineProperty(this, "origOutsideClickDeselects", null); /** * The controller module that allows modifying the menu item selection positions. * * @type {Paginator} */ _classPrivateFieldInitSpec(this, _navigator, void 0); /** * The controller module that allows extending the keyboard shortcuts for the menu. * * @type {KeyboardShortcutsMenuController} */ _classPrivateFieldInitSpec(this, _shortcutsCtrl, void 0); /** * The border width of the table used in the menu. * * @type {number} */ _classPrivateFieldInitSpec(this, _tableBorderWidth, void 0); this.hot = hotInstance; this.options = options || { parent: null, name: null, className: '', keepInViewport: true, standalone: false, minWidth: MIN_WIDTH, container: this.hot.rootPortalElement }; this.container = this.createContainer(this.options.name); this.positioner = new _positioner.Positioner(this.options.keepInViewport); this.parentMenu = this.options.parent || null; this.registerEvents(); if (this.isSubMenu()) { this.addLocalHook('afterSelectionChange', function () { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return _this.parentMenu.runLocalHooks('afterSelectionChange', ...args); }); } this.hot.addHook('afterSetTheme', (themeName, firstRun) => { if (this.options.container !== this.hot.rootPortalElement) { (0, _element.removeClass)(this.options.container, /ht-theme-.*/g); (0, _element.addClass)(this.options.container, themeName); } if (!firstRun) { this.close(); } }); } /** * Register event listeners. * * @private */ registerEvents() { let frame = this.hot.rootWindow; while (frame) { this.eventManager.addEventListener(frame.document, 'mousedown', event => this.onDocumentMouseDown(event)); this.eventManager.addEventListener(frame.document, 'touchstart', event => this.onDocumentMouseDown(event)); this.eventManager.addEventListener(frame.document, 'contextmenu', event => this.onDocumentContextMenu(event)); frame = (0, _element.getParentWindow)(frame); } } /** * Set array of objects which defines menu items. * * @param {Array} menuItems Menu items to display. */ setMenuItems(menuItems) { this.menuItems = menuItems; } /** * Gets the controller object that allows modifying the the menu item selection. * * @returns {Paginator | undefined} */ getNavigator() { return _classPrivateFieldGet(_navigator, this); } /** * Gets the controller object that allows extending the keyboard shortcuts of the menu. * * @returns {KeyboardShortcutsMenuController | undefined} */ getKeyboardShortcutsCtrl() { return _classPrivateFieldGet(_shortcutsCtrl, this); } /** * Returns currently selected menu item. Returns `null` if no item was selected. * * @returns {object|null} */ getSelectedItem() { return this.hasSelectedItem() ? this.hotMenu.getSourceDataAtRow(this.hotMenu.getSelectedActive()[0]) : null; } /** * Checks if the menu has selected (highlighted) any item from the menu list. * * @returns {boolean} */ hasSelectedItem() { return Array.isArray(this.hotMenu.getSelectedActive()); } /** * Check if menu is using as sub-menu. * * @returns {boolean} */ isSubMenu() { return this.parentMenu !== null; } /** * Open menu. * * @fires Hooks#beforeContextMenuShow * @fires Hooks#afterContextMenuShow */ open() { this.runLocalHooks('beforeOpen'); this.container.removeAttribute('style'); this.container.style.display = 'block'; const delayedOpenSubMenu = (0, _function.debounce)(row => this.openSubMenu(row), 300); const minWidthOfMenu = this.options.minWidth || MIN_WIDTH; let noItemsDefined = false; let filteredItems = (0, _array.arrayFilter)(this.menuItems, item => { if (item.key === _predefinedItems.NO_ITEMS) { noItemsDefined = true; } return (0, _utils.isItemHidden)(item, this.hot); }); if (filteredItems.length < 1 && !noItemsDefined) { filteredItems.push((0, _predefinedItems.predefinedItems)()[_predefinedItems.NO_ITEMS]); } else if (filteredItems.length === 0) { return; } filteredItems = (0, _utils.filterSeparators)(filteredItems, _predefinedItems.SEPARATOR); let shouldAutoCloseMenu = false; const settings = { data: filteredItems, colHeaders: false, autoColumnSize: true, autoWrapRow: false, modifyColWidth(width) { if ((0, _mixed.isDefined)(width) && width < minWidthOfMenu) { return minWidthOfMenu; } return width; }, autoRowSize: false, readOnly: true, editor: false, copyPaste: false, hiddenRows: true, maxCols: 1, columns: [{ data: 'name', renderer: (0, _menuItemRenderer.createMenuItemRenderer)(this.hot) }], renderAllRows: true, fragmentSelection: false, outsideClickDeselects: false, disableVisualSelection: 'area', layoutDirection: this.hot.isRtl() ? 'rtl' : 'ltr', ariaTags: false, themeName: this.hot.getCurrentThemeName(), beforeRefreshDimensions: () => false, beforeOnCellMouseOver: (event, coords) => { if (this.hotMenu.stylesHandler.isClassicTheme()) { _classPrivateFieldGet(_navigator, this).setCurrentPage(coords.row); } else { _classPrivateFieldGet(_navigator, this).setPageCursorAt(coords.row); } }, afterOnCellMouseOver: (event, coords) => { if (this.isAllSubMenusClosed()) { delayedOpenSubMenu(coords.row); } else { this.openSubMenu(coords.row); } }, 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 ((0, _browser.isWindowsOS)() && shouldAutoCloseMenu && this.hasSelectedItem()) { this.close(true); } }, afterSelection: (row, column, row2, column2, preventScrolling) => { // do not scroll the viewport when mouse clicks on partially visible menu item if (this.hotMenu.view.isMouseDown()) { preventScrolling.value = true; } this.runLocalHooks('afterSelectionChange', this.getSelectedItem()); }, beforeOnCellMouseUp: event => { if (this.hasSelectedItem()) { shouldAutoCloseMenu = !this.isCommandPassive(this.getSelectedItem()); this.executeCommand(event); } }, 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 ((!(0, _browser.isWindowsOS)() || !(0, _event.isRightClick)(event)) && shouldAutoCloseMenu && this.hasSelectedItem()) { // The timeout is necessary only for mobile devices. For desktop, the click event that is fired // right after the mouseup event gets the event element target the same as the mouseup event. // For mobile devices, the click event is triggered with native delay (~300ms), so when the mouseup // event hides the tapped element, the click event grabs the element below. As a result, the filter // by condition menu is closed and immediately open on tapping the "None" item. if ((0, _browser.isMobileBrowser)() || (0, _browser.isIpadOS)()) { this.hot._registerTimeout(() => this.close(true), 325); } else { this.close(true); } } }, 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 (!this.hasSelectedItem() && this.isOpened()) { this.hotMenu.listen(); } } }; this.origOutsideClickDeselects = this.hot.getSettings().outsideClickDeselects; this.hot.getSettings().outsideClickDeselects = false; this.hotMenu = new this.hot.constructor(this.container, settings); this.hotMenu.addHook('afterInit', () => this.onAfterInit()); this.hotMenu.init(); _classPrivateFieldSet(_navigator, this, (0, _navigator2.createMenuNavigator)(this.hotMenu)); _classPrivateFieldSet(_shortcutsCtrl, this, (0, _shortcuts.createKeyboardShortcutsCtrl)(this)); _classPrivateFieldGet(_shortcutsCtrl, this).listen(); this.focus(); if (this.isSubMenu()) { this.addLocalHook('afterOpen', () => this.parentMenu.runLocalHooks('afterSubmenuOpen', this)); } this.runLocalHooks('afterOpen', this); } /** * Close menu. * * @param {boolean} [closeParent=false] If `true` try to close parent menu if exists. */ close() { let closeParent = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; if (!this.isOpened()) { return; } if (closeParent && this.isSubMenu()) { this.parentMenu.close(); } else { _classPrivateFieldGet(_navigator, this).clear(); this.closeAllSubMenus(); this.container.style.display = 'none'; this.hotMenu.destroy(); this.hotMenu = null; this.hot.getSettings().outsideClickDeselects = this.origOutsideClickDeselects; this.runLocalHooks('afterClose'); if (this.isSubMenu()) { if (this.hot.getSettings().ariaTags) { const selection = this.parentMenu.hotMenu.getSelectedActive(); if (selection) { const cell = this.parentMenu.hotMenu.getCell(selection[0], 0); (0, _element.setAttribute)(cell, [(0, _a11y.A11Y_EXPANDED)(false)]); } } 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. */ openSubMenu(row) { if (!this.hotMenu) { return false; } const cell = this.hotMenu.getCell(row, 0); this.closeAllSubMenus(); if (!cell || !(0, _utils.hasSubMenu)(cell)) { return false; } const dataItem = this.hotMenu.getSourceDataAtRow(row); const 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; // Update the accessibility tags on the cell being the base for the submenu. if (this.hot.getSettings().ariaTags) { (0, _element.setAttribute)(cell, [(0, _a11y.A11Y_EXPANDED)(true)]); } return subMenu; } /** * Close sub menu at row index. * * @param {number} row Row index. */ closeSubMenu(row) { const dataItem = this.hotMenu.getSourceDataAtRow(row); const menus = this.hotSubMenus[dataItem.key]; if (menus) { menus.destroy(); delete this.hotSubMenus[dataItem.key]; const cell = this.hotMenu.getCell(row, 0); // Update the accessibility tags on the cell being the base for the submenu. if (this.hot.getSettings().ariaTags) { (0, _element.setAttribute)(cell, [(0, _a11y.A11Y_EXPANDED)(false)]); } } } /** * Close all opened sub menus. */ closeAllSubMenus() { (0, _array.arrayEach)(this.hotMenu.getData(), (value, row) => this.closeSubMenu(row)); } /** * Checks if all created and opened sub menus are closed. * * @returns {boolean} */ isAllSubMenusClosed() { return Object.keys(this.hotSubMenus).length === 0; } /** * Focus the menu so all keyboard shortcuts become active. */ focus() { if (this.isOpened()) { this.hotMenu.rootElement.focus({ preventScroll: true }); this.getKeyboardShortcutsCtrl().listen(); this.hotMenu.listen(); } } /** * Destroy instance. */ destroy() { const 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. */ isOpened() { return this.hotMenu !== null; } /** * Execute menu command. * * The `executeCommand()` method works only for selected cells. * * When no cells are selected, `executeCommand()` doesn't do anything. * * @param {Event} [event] The mouse event object. */ executeCommand(event) { if (!this.isOpened() || !this.hasSelectedItem()) { return; } const selectedItem = this.getSelectedItem(); this.runLocalHooks('select', selectedItem, event); if (this.isCommandPassive(selectedItem)) { return; } const selRanges = this.hot.getSelectedRange(); const normalizedSelection = selRanges ? (0, _utils.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} */ isCommandPassive(commandDescriptor) { return commandDescriptor.isCommand === false || (0, _utils.isItemSeparator)(commandDescriptor) || (0, _utils.isItemDisabled)(commandDescriptor, this.hot) || (0, _utils.isItemSubMenu)(commandDescriptor); } /** * 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. */ setOffset(area) { let offset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; this.positioner.setOffset(area, offset); } /** * Set menu position based on dom event or based on literal object. * * @param {Event|object} coords Event or literal Object with coordinates. */ setPosition(coords) { if (this.isSubMenu()) { this.positioner.setParentElement(this.parentMenu.container); } this.positioner.setElement(this.container).updatePosition(coords); } /** * Updates the dimensions of the menu based on its content. * This method calculates the real height of the menu by summing up the heights of its items, * and adjusts the width and height of the menu's holder and hider elements accordingly. */ updateMenuDimensions() { const { wtTable } = this.hotMenu.view._wt; const data = this.hotMenu.getSettings().data; const hiderStyle = wtTable.hider.style; const holderStyle = wtTable.holder.style; const currentHiderWidth = parseInt(hiderStyle.width, 10); const realHeight = (0, _array.arrayReduce)(data, (accumulator, value, index) => { const itemCell = this.hotMenu.getCell(index, 0); const currentRowHeight = itemCell ? (0, _element.outerHeight)(this.hotMenu.getCell(index, 0)) : 0; return accumulator + (value.name === _predefinedItems.SEPARATOR ? 1 : currentRowHeight); }, 0); if (this.hotMenu.stylesHandler.isClassicTheme()) { // Additional 3px to menu's size because of additional border around its `table.htCore`. holderStyle.width = `${currentHiderWidth + 3}px`; holderStyle.height = `${realHeight + 3}px`; } else { holderStyle.width = `${currentHiderWidth}px`; holderStyle.height = `${realHeight}px`; } hiderStyle.height = holderStyle.height; } /** * Create container/wrapper for handsontable. * * @private * @param {string} [name] Class name. * @returns {HTMLElement} */ createContainer() { let name = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; const doc = this.options.container.ownerDocument; let className = name; let container; if (className) { if ((0, _function.isFunction)(className)) { className = className.call(this.hot); if (className === null || (0, _mixed.isUndefined)(className)) { className = ''; } else { className = className.toString(); } } className = className.replace(/[^A-Za-z0-9]/g, '_'); className = `${this.options.className}Sub_${className}`; container = doc.querySelector(`.${this.options.className}.${className}`); } if (!container) { container = doc.createElement('div'); (0, _element.addClass)(container, `htMenu handsontable ${this.options.className}`); if (className) { (0, _element.addClass)(container, className); } this.options.container.appendChild(container); } return container; } /** * On after init listener. * * @private */ onAfterInit() { this.updateMenuDimensions(); // Replace the default accessibility tags with the context menu's if (this.hot.getSettings().ariaTags) { (0, _element.setAttribute)(this.hotMenu.rootElement, [(0, _a11y.A11Y_MENU)(), (0, _a11y.A11Y_TABINDEX)(-1)]); } } /** * Document mouse down listener. * * @private * @param {Event} event The mouse event object. */ onDocumentMouseDown(event) { if (!this.isOpened()) { return; } // Close menu when clicked element is not belongs to menu itself if (this.options.standalone && this.hotMenu && !(0, _element.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()) && !(0, _element.isChildOf)(event.target, '.htMenu')) { this.close(true); } } /** * Document's contextmenu listener. * * @private * @param {MouseEvent} event The mouse event object. */ onDocumentContextMenu(event) { if (!this.isOpened()) { return; } if ((0, _element.hasClass)(event.target, 'htCore') && (0, _element.isChildOf)(event.target, this.hotMenu.rootElement)) { event.preventDefault(); } } } exports.Menu = Menu; (0, _object.mixin)(Menu, _localHooks.default);