handsontable
Version:
Handsontable is a JavaScript Data Grid available for React, Angular and Vue.
381 lines (367 loc) • 14.1 kB
JavaScript
"use strict";
exports.__esModule = true;
require("core-js/modules/es.error.cause.js");
var _base = require("../base");
var _hooks = require("../../core/hooks");
var _array = require("../../helpers/array");
var _object = require("../../helpers/object");
var _commandExecutor = require("./commandExecutor");
var _itemsFactory = require("./itemsFactory");
var _menu = require("./menu");
var _utils = require("./utils");
var _element = require("../../helpers/dom/element");
var _predefinedItems = require("./predefinedItems");
function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); }
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 _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 PLUGIN_KEY = exports.PLUGIN_KEY = 'contextMenu';
const PLUGIN_PRIORITY = exports.PLUGIN_PRIORITY = 70;
const SHORTCUTS_GROUP = PLUGIN_KEY;
_hooks.Hooks.getSingleton().register('afterContextMenuDefaultOptions');
_hooks.Hooks.getSingleton().register('beforeContextMenuShow');
_hooks.Hooks.getSingleton().register('afterContextMenuShow');
_hooks.Hooks.getSingleton().register('afterContextMenuHide');
_hooks.Hooks.getSingleton().register('afterContextMenuExecute');
/* eslint-disable jsdoc/require-description-complete-sentence */
/**
* @class ContextMenu
* @description
* This plugin creates the Handsontable Context Menu. It allows to create a new row or column at any place in the
* grid among [other features](@/guides/accessories-and-menus/context-menu/context-menu.md#context-menu-with-specific-options).
* Possible values:
* * `true` (to enable default options),
* * `false` (to disable completely)
* * `{ uiContainer: containerDomElement }` (to declare a container for all of the Context Menu's dom elements to be placed in).
* * An array of [the available strings](@/guides/accessories-and-menus/context-menu/context-menu.md#context-menu-with-specific-options)
*
* See [the context menu demo](@/guides/accessories-and-menus/context-menu/context-menu.md) for examples.
*
* @example
* ```js
* // as a boolean
* contextMenu: true
* // as a array
* contextMenu: ['row_above', 'row_below', '---------', 'undo', 'redo']
* ```
*
* @plugin ContextMenu
*/
var _ContextMenu_brand = /*#__PURE__*/new WeakSet();
class ContextMenu extends _base.BasePlugin {
constructor() {
super(...arguments);
/**
* On contextmenu listener.
*
* @param {Event} event The mouse event object.
*/
_classPrivateMethodInitSpec(this, _ContextMenu_brand);
/**
* Instance of {@link CommandExecutor}.
*
* @private
* @type {CommandExecutor}
*/
_defineProperty(this, "commandExecutor", new _commandExecutor.CommandExecutor(this.hot));
/**
* Instance of {@link ItemsFactory}.
*
* @private
* @type {ItemsFactory}
*/
_defineProperty(this, "itemsFactory", null);
/**
* Instance of {@link Menu}.
*
* @private
* @type {Menu}
*/
_defineProperty(this, "menu", null);
}
static get PLUGIN_KEY() {
return PLUGIN_KEY;
}
static get PLUGIN_PRIORITY() {
return PLUGIN_PRIORITY;
}
static get PLUGIN_DEPS() {
return ['plugin:AutoColumnSize'];
}
/**
* Context menu default items order when `contextMenu` options is set as `true`.
*
* @returns {string[]}
*/
static get DEFAULT_ITEMS() {
return [_predefinedItems.ROW_ABOVE, _predefinedItems.ROW_BELOW, _predefinedItems.SEPARATOR, _predefinedItems.COLUMN_LEFT, _predefinedItems.COLUMN_RIGHT, _predefinedItems.SEPARATOR, _predefinedItems.REMOVE_ROW, _predefinedItems.REMOVE_COLUMN, _predefinedItems.SEPARATOR, _predefinedItems.UNDO, _predefinedItems.REDO, _predefinedItems.SEPARATOR, _predefinedItems.READ_ONLY, _predefinedItems.SEPARATOR, _predefinedItems.ALIGNMENT];
}
/**
* Checks if the plugin is enabled in the handsontable settings. This method is executed in {@link Hooks#beforeInit}
* hook and if it returns `true` then the {@link ContextMenu#enablePlugin} method is called.
*
* @returns {boolean}
*/
isEnabled() {
return !!this.hot.getSettings()[PLUGIN_KEY];
}
/**
* Enables the plugin functionality for this Handsontable instance.
*/
enablePlugin() {
var _this = this;
if (this.enabled) {
return;
}
const settings = this.hot.getSettings()[PLUGIN_KEY];
if (typeof settings.callback === 'function') {
this.commandExecutor.setCommonCallback(settings.callback);
}
this.menu = new _menu.Menu(this.hot, {
className: 'htContextMenu',
keepInViewport: true,
container: settings.uiContainer || this.hot.rootPortalElement
});
this.menu.addLocalHook('beforeOpen', () => _assertClassBrand(_ContextMenu_brand, this, _onMenuBeforeOpen).call(this));
this.menu.addLocalHook('afterOpen', () => _assertClassBrand(_ContextMenu_brand, this, _onMenuAfterOpen).call(this));
this.menu.addLocalHook('afterClose', () => _assertClassBrand(_ContextMenu_brand, this, _onMenuAfterClose).call(this));
this.menu.addLocalHook('executeCommand', function () {
for (var _len = arguments.length, params = new Array(_len), _key = 0; _key < _len; _key++) {
params[_key] = arguments[_key];
}
return _this.executeCommand.call(_this, ...params);
});
this.addHook('afterOnCellContextMenu', event => _assertClassBrand(_ContextMenu_brand, this, _onAfterOnCellContextMenu).call(this, event));
this.addHook('beforeDialogShow', () => this.close());
this.registerShortcuts();
super.enablePlugin();
}
/**
* Updates the plugin's state.
*
* This method is executed when [`updateSettings()`](@/api/core.md#updatesettings) is invoked with any of the following configuration options:
* - [`contextMenu`](@/api/options.md#contextmenu)
*/
updatePlugin() {
this.disablePlugin();
this.enablePlugin();
super.updatePlugin();
}
/**
* Disables the plugin functionality for this Handsontable instance.
*/
disablePlugin() {
this.close();
if (this.menu) {
this.menu.destroy();
this.menu = null;
}
this.unregisterShortcuts();
super.disablePlugin();
}
/**
* Register shortcuts responsible for toggling context menu.
*
* @private
*/
registerShortcuts() {
this.hot.getShortcutManager().getContext('grid').addShortcut({
keys: [['Control/Meta', 'Shift', 'Backslash'], ['Shift', 'F10']],
callback: () => {
const {
highlight
} = this.hot.getSelectedRangeActive();
this.hot.scrollToFocusedCell();
const rect = this.hot.getCell(highlight.row, highlight.col, true).getBoundingClientRect();
const offset = (0, _utils.getDocumentOffsetByElement)(this.menu.container, this.hot.rootDocument);
this.open({
left: rect.left + offset.left,
top: rect.top + offset.top - 1 + rect.height
}, {
left: rect.width,
above: -rect.height
});
// Make sure the first item is selected (role=menuitem). Otherwise, screen readers
// will block the Esc key for the whole menu.
this.menu.getNavigator().toFirstItem();
},
runOnlyIf: () => {
var _this$hot$getSelected;
const highlight = (_this$hot$getSelected = this.hot.getSelectedRangeActive()) === null || _this$hot$getSelected === void 0 ? void 0 : _this$hot$getSelected.highlight;
return highlight && this.hot.selection.isCellVisible(highlight) && !this.menu.isOpened();
},
group: SHORTCUTS_GROUP
});
}
/**
* Unregister shortcuts responsible for toggling context menu.
*
* @private
*/
unregisterShortcuts() {
this.hot.getShortcutManager().getContext('grid').removeShortcutsByGroup(SHORTCUTS_GROUP);
}
/**
* Opens menu and re-position it based on the passed coordinates.
*
* @param {{ top: number, left: number }|Event} position An object with `top` and `left` properties
* which contains coordinates relative to the browsers viewport (without included scroll offsets).
* Or if the native event is passed the menu will be positioned based on the `pageX` and `pageY`
* coordinates.
* @param {{ above: number, below: number, left: number, right: number }} offset An object allows applying
* the offset to the menu position.
* @fires Hooks#beforeContextMenuShow
* @fires Hooks#afterContextMenuShow
*/
open(position) {
var _this$menu;
let offset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
above: 0,
below: 0,
left: 0,
right: 0
};
if ((_this$menu = this.menu) !== null && _this$menu !== void 0 && _this$menu.isOpened()) {
return;
}
this.prepareMenuItems();
this.menu.open();
(0, _object.objectEach)(offset, (value, key) => {
this.menu.setOffset(key, value);
});
this.menu.setPosition(position);
}
/**
* Closes the menu.
*/
close() {
var _this$menu2;
(_this$menu2 = this.menu) === null || _this$menu2 === void 0 || _this$menu2.close();
this.itemsFactory = null;
}
/**
* Execute context menu command.
*
* The `executeCommand()` method works only for selected cells.
*
* When no cells are selected, `executeCommand()` doesn't do anything.
*
* You can execute all predefined commands:
* * `'row_above'` - Insert row above
* * `'row_below'` - Insert row below
* * `'col_left'` - Insert column left
* * `'col_right'` - Insert column right
* * `'clear_column'` - Clear selected column
* * `'remove_row'` - Remove row
* * `'remove_col'` - Remove column
* * `'undo'` - Undo last action
* * `'redo'` - Redo last action
* * `'make_read_only'` - Make cell read only
* * `'alignment:left'` - Alignment to the left
* * `'alignment:top'` - Alignment to the top
* * `'alignment:right'` - Alignment to the right
* * `'alignment:bottom'` - Alignment to the bottom
* * `'alignment:middle'` - Alignment to the middle
* * `'alignment:center'` - Alignment to the center (justify).
*
* Or you can execute command registered in settings where `key` is your command name.
*
* @param {string} commandName The command name to be executed.
* @param {*} params Additional parameters passed to command executor module.
*/
executeCommand(commandName) {
if (this.itemsFactory === null) {
this.prepareMenuItems();
}
for (var _len2 = arguments.length, params = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
params[_key2 - 1] = arguments[_key2];
}
this.commandExecutor.execute(commandName, ...params);
}
/**
* Prepares available contextMenu's items list and registers them in commandExecutor.
*
* @private
* @fires Hooks#afterContextMenuDefaultOptions
* @fires Hooks#beforeContextMenuSetItems
*/
prepareMenuItems() {
this.itemsFactory = new _itemsFactory.ItemsFactory(this.hot, ContextMenu.DEFAULT_ITEMS);
const settings = this.hot.getSettings()[PLUGIN_KEY];
const predefinedItems = {
items: this.itemsFactory.getItems(settings)
};
this.hot.runHooks('afterContextMenuDefaultOptions', predefinedItems);
this.itemsFactory.setPredefinedItems(predefinedItems.items);
const menuItems = this.itemsFactory.getItems(settings);
this.hot.runHooks('beforeContextMenuSetItems', menuItems);
this.menu.setMenuItems(menuItems);
// Register all commands. Predefined and added by user or by plugins
(0, _array.arrayEach)(menuItems, command => this.commandExecutor.registerCommand(command.key, command));
}
/**
* Destroys the plugin instance.
*/
destroy() {
this.close();
if (this.menu) {
this.menu.destroy();
}
super.destroy();
}
}
exports.ContextMenu = ContextMenu;
function _onAfterOnCellContextMenu(event) {
const settings = this.hot.getSettings();
const showRowHeaders = settings.rowHeaders;
const showColHeaders = settings.colHeaders;
/**
* @private
* @param {HTMLElement} element The element to validate.
* @returns {boolean}
*/
function isValidElement(element) {
return element.nodeName === 'TD' || element.parentNode.nodeName === 'TD';
}
const element = event.target;
this.close();
if ((0, _element.hasClass)(element, 'handsontableInput')) {
return;
}
event.preventDefault();
event.stopPropagation();
if (!(showRowHeaders || showColHeaders)) {
if (!isValidElement(element) && !((0, _element.hasClass)(element, 'current') && (0, _element.hasClass)(element, 'wtBorder'))) {
return;
}
}
const offset = (0, _utils.getDocumentOffsetByElement)(this.menu.container, this.hot.rootDocument);
this.open({
top: event.clientY + offset.top,
left: event.clientX + offset.left
});
}
/**
* On menu before open listener.
*/
function _onMenuBeforeOpen() {
this.hot.runHooks('beforeContextMenuShow', this);
}
/**
* On menu after open listener.
*/
function _onMenuAfterOpen() {
this.hot.runHooks('afterContextMenuShow', this);
}
/**
* On menu after close listener.
*/
function _onMenuAfterClose() {
this.hot.listen();
this.hot.runHooks('afterContextMenuHide', this);
}
ContextMenu.SEPARATOR = {
name: _predefinedItems.SEPARATOR
};