@quantlab/handsontable
Version:
Spreadsheet-like data grid editor that provides copy/paste functionality compatible with Excel/Google Docs
326 lines (289 loc) • 8.55 kB
JavaScript
import BasePlugin from './../_base';
import Hooks from './../../pluginHooks';
import {arrayEach} from './../../helpers/array';
import CommandExecutor from './commandExecutor';
import EventManager from './../../eventManager';
import ItemsFactory from './itemsFactory';
import Menu from './menu';
import {registerPlugin} from './../../plugins';
import {stopPropagation, pageX, pageY} from './../../helpers/dom/event';
import {getWindowScrollLeft, getWindowScrollTop, hasClass} from './../../helpers/dom/element';
import {
ROW_ABOVE,
ROW_BELOW,
COLUMN_LEFT,
COLUMN_RIGHT,
REMOVE_ROW,
REMOVE_COLUMN,
UNDO,
REDO,
READ_ONLY,
ALIGNMENT,
SEPARATOR
} from './predefinedItems';
import './contextMenu.css';
Hooks.getSingleton().register('afterContextMenuDefaultOptions');
Hooks.getSingleton().register('afterContextMenuShow');
Hooks.getSingleton().register('afterContextMenuHide');
Hooks.getSingleton().register('afterContextMenuExecute');
/**
* @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](http://docs.handsontable.com/demo-context-menu.html).
* Possible values:
* * `true` (to enable default options),
* * `false` (to disable completely)
*
* or array of any available strings:
* * `["row_above", "row_below", "col_left", "col_right",
* "remove_row", "remove_col", "---------", "undo", "redo"]`.
*
* See [the context menu demo](http://docs.handsontable.com/demo-context-menu.html) for examples.
*
* @example
* ```js
* ...
* // as a boolean
* contextMenu: true
* ...
* // as a array
* contextMenu: ['row_above', 'row_below', '---------', 'undo', 'redo']
* ...
* ```
*
* @plugin ContextMenu
*/
class ContextMenu extends BasePlugin {
/**
* Default menu items order when `contextMenu` is enabled by `true`.
*
* @returns {Array}
*/
static get DEFAULT_ITEMS() {
return [
ROW_ABOVE, ROW_BELOW,
SEPARATOR,
COLUMN_LEFT, COLUMN_RIGHT,
SEPARATOR,
REMOVE_ROW, REMOVE_COLUMN,
SEPARATOR,
UNDO, REDO,
SEPARATOR,
READ_ONLY,
SEPARATOR,
ALIGNMENT,
];
}
constructor(hotInstance) {
super(hotInstance);
/**
* Instance of {@link EventManager}.
*
* @type {EventManager}
*/
this.eventManager = new EventManager(this);
/**
* Instance of {@link CommandExecutor}.
*
* @type {CommandExecutor}
*/
this.commandExecutor = new CommandExecutor(this.hot);
/**
* Instance of {@link ItemsFactory}.
*
* @type {ItemsFactory}
*/
this.itemsFactory = null;
/**
* Instance of {@link Menu}.
*
* @type {Menu}
*/
this.menu = null;
}
/**
* Check if the plugin is enabled in the Handsontable settings.
*
* @returns {Boolean}
*/
isEnabled() {
return this.hot.getSettings().contextMenu;
}
/**
* Enable plugin for this Handsontable instance.
*/
enablePlugin() {
if (this.enabled) {
return;
}
this.itemsFactory = new ItemsFactory(this.hot, ContextMenu.DEFAULT_ITEMS);
const settings = this.hot.getSettings().contextMenu;
let predefinedItems = {
items: this.itemsFactory.getItems(settings)
};
this.registerEvents();
if (typeof settings.callback === 'function') {
this.commandExecutor.setCommonCallback(settings.callback);
}
super.enablePlugin();
this.callOnPluginsReady(() => {
this.hot.runHooks('afterContextMenuDefaultOptions', predefinedItems);
this.itemsFactory.setPredefinedItems(predefinedItems.items);
let menuItems = this.itemsFactory.getItems(settings);
this.menu = new Menu(this.hot, {
className: 'htContextMenu',
keepInViewport: true
});
this.hot.runHooks('beforeContextMenuSetItems', menuItems);
this.menu.setMenuItems(menuItems);
this.menu.addLocalHook('afterOpen', () => this.onMenuAfterOpen());
this.menu.addLocalHook('afterClose', () => this.onMenuAfterClose());
this.menu.addLocalHook('executeCommand', (...params) => this.executeCommand.apply(this, params));
// Register all commands. Predefined and added by user or by plugins
arrayEach(menuItems, (command) => this.commandExecutor.registerCommand(command.key, command));
});
}
/**
* Updates the plugin to use the latest options you have specified.
*/
updatePlugin() {
this.disablePlugin();
this.enablePlugin();
super.updatePlugin();
}
/**
* Disable plugin for this Handsontable instance.
*/
disablePlugin() {
this.close();
if (this.menu) {
this.menu.destroy();
this.menu = null;
}
super.disablePlugin();
}
/**
* Register dom listeners.
*
* @private
*/
registerEvents() {
this.eventManager.addEventListener(this.hot.rootElement, 'contextmenu', (event) => this.onContextMenu(event));
}
/**
* Open menu and re-position it based on dom event object.
*
* @param {Event} event The event object.
*/
open(event) {
if (!this.menu) {
return;
}
this.menu.open();
this.menu.setPosition({
top: parseInt(pageY(event), 10) - getWindowScrollTop(),
left: parseInt(pageX(event), 10) - getWindowScrollLeft(),
});
// ContextMenu is not detected HotTableEnv correctly because is injected outside hot-table
this.menu.hotMenu.isHotTableEnv = this.hot.isHotTableEnv;
// Handsontable.eventManager.isHotTableEnv = this.hot.isHotTableEnv;
}
/**
* Close menu.
*/
close() {
if (!this.menu) {
return;
}
this.menu.close();
}
/**
* Execute context menu command.
*
* You can execute all predefined commands:
* * `'row_above'` - Insert row above
* * `'row_below'` - Insert row below
* * `'col_left'` - Insert column on the left
* * `'col_right'` - Insert column on the 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
* @param {*} params
*/
executeCommand(...params) {
this.commandExecutor.execute.apply(this.commandExecutor, params);
}
/**
* On context menu listener.
*
* @private
* @param {Event} event
*/
onContextMenu(event) {
let settings = this.hot.getSettings();
let showRowHeaders = settings.rowHeaders;
let showColHeaders = settings.colHeaders;
function isValidElement(element) {
return element.nodeName === 'TD' || element.parentNode.nodeName === 'TD';
}
// if event is from hot-table we must get web component element not element inside him
let element = event.realTarget;
this.close();
if (hasClass(element, 'handsontableInput')) {
return;
}
event.preventDefault();
stopPropagation(event);
if (!(showRowHeaders || showColHeaders)) {
if (!isValidElement(element) && !(hasClass(element, 'current') && hasClass(element, 'wtBorder'))) {
return;
}
}
this.open(event);
}
/**
* On menu after open listener.
*
* @private
*/
onMenuAfterOpen() {
this.hot.runHooks('afterContextMenuShow', this);
}
/**
* On menu after close listener.
*
* @private
*/
onMenuAfterClose() {
this.hot.listen();
this.hot.runHooks('afterContextMenuHide', this);
}
/**
* Destroy instance.
*/
destroy() {
this.close();
if (this.menu) {
this.menu.destroy();
}
super.destroy();
}
}
ContextMenu.SEPARATOR = {
name: SEPARATOR
};
registerPlugin('contextMenu', ContextMenu);
export default ContextMenu;