622cba6f9e5db7390dffc7582b21e2f630328ca720ec3eae59edcbc9497c73268083c739e84a3f01cd2b5d4824eac42398cb352cc09912db2abe5055cc8891 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. import BasePlugin from './../_base';
  2. import Hooks from './../../pluginHooks';
  3. import {arrayEach} from './../../helpers/array';
  4. import CommandExecutor from './commandExecutor';
  5. import EventManager from './../../eventManager';
  6. import ItemsFactory from './itemsFactory';
  7. import Menu from './menu';
  8. import {registerPlugin} from './../../plugins';
  9. import {stopPropagation, pageX, pageY} from './../../helpers/dom/event';
  10. import {getWindowScrollLeft, getWindowScrollTop, hasClass} from './../../helpers/dom/element';
  11. import {
  12. ROW_ABOVE,
  13. ROW_BELOW,
  14. COLUMN_LEFT,
  15. COLUMN_RIGHT,
  16. REMOVE_ROW,
  17. REMOVE_COLUMN,
  18. UNDO,
  19. REDO,
  20. READ_ONLY,
  21. ALIGNMENT,
  22. SEPARATOR
  23. } from './predefinedItems';
  24. import './contextMenu.css';
  25. Hooks.getSingleton().register('afterContextMenuDefaultOptions');
  26. Hooks.getSingleton().register('afterContextMenuShow');
  27. Hooks.getSingleton().register('afterContextMenuHide');
  28. Hooks.getSingleton().register('afterContextMenuExecute');
  29. /**
  30. * @description
  31. * This plugin creates the Handsontable Context Menu. It allows to create a new row or
  32. * column at any place in the grid among [other features](http://docs.handsontable.com/demo-context-menu.html).
  33. * Possible values:
  34. * * `true` (to enable default options),
  35. * * `false` (to disable completely)
  36. *
  37. * or array of any available strings:
  38. * * `["row_above", "row_below", "col_left", "col_right",
  39. * "remove_row", "remove_col", "---------", "undo", "redo"]`.
  40. *
  41. * See [the context menu demo](http://docs.handsontable.com/demo-context-menu.html) for examples.
  42. *
  43. * @example
  44. * ```js
  45. * ...
  46. * // as a boolean
  47. * contextMenu: true
  48. * ...
  49. * // as a array
  50. * contextMenu: ['row_above', 'row_below', '---------', 'undo', 'redo']
  51. * ...
  52. * ```
  53. *
  54. * @plugin ContextMenu
  55. */
  56. class ContextMenu extends BasePlugin {
  57. /**
  58. * Default menu items order when `contextMenu` is enabled by `true`.
  59. *
  60. * @returns {Array}
  61. */
  62. static get DEFAULT_ITEMS() {
  63. return [
  64. ROW_ABOVE, ROW_BELOW,
  65. SEPARATOR,
  66. COLUMN_LEFT, COLUMN_RIGHT,
  67. SEPARATOR,
  68. REMOVE_ROW, REMOVE_COLUMN,
  69. SEPARATOR,
  70. UNDO, REDO,
  71. SEPARATOR,
  72. READ_ONLY,
  73. SEPARATOR,
  74. ALIGNMENT,
  75. ];
  76. }
  77. constructor(hotInstance) {
  78. super(hotInstance);
  79. /**
  80. * Instance of {@link EventManager}.
  81. *
  82. * @type {EventManager}
  83. */
  84. this.eventManager = new EventManager(this);
  85. /**
  86. * Instance of {@link CommandExecutor}.
  87. *
  88. * @type {CommandExecutor}
  89. */
  90. this.commandExecutor = new CommandExecutor(this.hot);
  91. /**
  92. * Instance of {@link ItemsFactory}.
  93. *
  94. * @type {ItemsFactory}
  95. */
  96. this.itemsFactory = null;
  97. /**
  98. * Instance of {@link Menu}.
  99. *
  100. * @type {Menu}
  101. */
  102. this.menu = null;
  103. }
  104. /**
  105. * Check if the plugin is enabled in the Handsontable settings.
  106. *
  107. * @returns {Boolean}
  108. */
  109. isEnabled() {
  110. return this.hot.getSettings().contextMenu;
  111. }
  112. /**
  113. * Enable plugin for this Handsontable instance.
  114. */
  115. enablePlugin() {
  116. if (this.enabled) {
  117. return;
  118. }
  119. this.itemsFactory = new ItemsFactory(this.hot, ContextMenu.DEFAULT_ITEMS);
  120. const settings = this.hot.getSettings().contextMenu;
  121. let predefinedItems = {
  122. items: this.itemsFactory.getItems(settings)
  123. };
  124. this.registerEvents();
  125. if (typeof settings.callback === 'function') {
  126. this.commandExecutor.setCommonCallback(settings.callback);
  127. }
  128. super.enablePlugin();
  129. this.callOnPluginsReady(() => {
  130. this.hot.runHooks('afterContextMenuDefaultOptions', predefinedItems);
  131. this.itemsFactory.setPredefinedItems(predefinedItems.items);
  132. let menuItems = this.itemsFactory.getItems(settings);
  133. this.menu = new Menu(this.hot, {
  134. className: 'htContextMenu',
  135. keepInViewport: true
  136. });
  137. this.hot.runHooks('beforeContextMenuSetItems', menuItems);
  138. this.menu.setMenuItems(menuItems);
  139. this.menu.addLocalHook('afterOpen', () => this.onMenuAfterOpen());
  140. this.menu.addLocalHook('afterClose', () => this.onMenuAfterClose());
  141. this.menu.addLocalHook('executeCommand', (...params) => this.executeCommand.apply(this, params));
  142. // Register all commands. Predefined and added by user or by plugins
  143. arrayEach(menuItems, (command) => this.commandExecutor.registerCommand(command.key, command));
  144. });
  145. }
  146. /**
  147. * Updates the plugin to use the latest options you have specified.
  148. */
  149. updatePlugin() {
  150. this.disablePlugin();
  151. this.enablePlugin();
  152. super.updatePlugin();
  153. }
  154. /**
  155. * Disable plugin for this Handsontable instance.
  156. */
  157. disablePlugin() {
  158. this.close();
  159. if (this.menu) {
  160. this.menu.destroy();
  161. this.menu = null;
  162. }
  163. super.disablePlugin();
  164. }
  165. /**
  166. * Register dom listeners.
  167. *
  168. * @private
  169. */
  170. registerEvents() {
  171. this.eventManager.addEventListener(this.hot.rootElement, 'contextmenu', (event) => this.onContextMenu(event));
  172. }
  173. /**
  174. * Open menu and re-position it based on dom event object.
  175. *
  176. * @param {Event} event The event object.
  177. */
  178. open(event) {
  179. if (!this.menu) {
  180. return;
  181. }
  182. this.menu.open();
  183. this.menu.setPosition({
  184. top: parseInt(pageY(event), 10) - getWindowScrollTop(),
  185. left: parseInt(pageX(event), 10) - getWindowScrollLeft(),
  186. });
  187. // ContextMenu is not detected HotTableEnv correctly because is injected outside hot-table
  188. this.menu.hotMenu.isHotTableEnv = this.hot.isHotTableEnv;
  189. // Handsontable.eventManager.isHotTableEnv = this.hot.isHotTableEnv;
  190. }
  191. /**
  192. * Close menu.
  193. */
  194. close() {
  195. if (!this.menu) {
  196. return;
  197. }
  198. this.menu.close();
  199. }
  200. /**
  201. * Execute context menu command.
  202. *
  203. * You can execute all predefined commands:
  204. * * `'row_above'` - Insert row above
  205. * * `'row_below'` - Insert row below
  206. * * `'col_left'` - Insert column on the left
  207. * * `'col_right'` - Insert column on the right
  208. * * `'clear_column'` - Clear selected column
  209. * * `'remove_row'` - Remove row
  210. * * `'remove_col'` - Remove column
  211. * * `'undo'` - Undo last action
  212. * * `'redo'` - Redo last action
  213. * * `'make_read_only'` - Make cell read only
  214. * * `'alignment:left'` - Alignment to the left
  215. * * `'alignment:top'` - Alignment to the top
  216. * * `'alignment:right'` - Alignment to the right
  217. * * `'alignment:bottom'` - Alignment to the bottom
  218. * * `'alignment:middle'` - Alignment to the middle
  219. * * `'alignment:center'` - Alignment to the center (justify)
  220. *
  221. * Or you can execute command registered in settings where `key` is your command name.
  222. *
  223. * @param {String} commandName
  224. * @param {*} params
  225. */
  226. executeCommand(...params) {
  227. this.commandExecutor.execute.apply(this.commandExecutor, params);
  228. }
  229. /**
  230. * On context menu listener.
  231. *
  232. * @private
  233. * @param {Event} event
  234. */
  235. onContextMenu(event) {
  236. let settings = this.hot.getSettings();
  237. let showRowHeaders = settings.rowHeaders;
  238. let showColHeaders = settings.colHeaders;
  239. function isValidElement(element) {
  240. return element.nodeName === 'TD' || element.parentNode.nodeName === 'TD';
  241. }
  242. // if event is from hot-table we must get web component element not element inside him
  243. let element = event.realTarget;
  244. this.close();
  245. if (hasClass(element, 'handsontableInput')) {
  246. return;
  247. }
  248. event.preventDefault();
  249. stopPropagation(event);
  250. if (!(showRowHeaders || showColHeaders)) {
  251. if (!isValidElement(element) && !(hasClass(element, 'current') && hasClass(element, 'wtBorder'))) {
  252. return;
  253. }
  254. }
  255. this.open(event);
  256. }
  257. /**
  258. * On menu after open listener.
  259. *
  260. * @private
  261. */
  262. onMenuAfterOpen() {
  263. this.hot.runHooks('afterContextMenuShow', this);
  264. }
  265. /**
  266. * On menu after close listener.
  267. *
  268. * @private
  269. */
  270. onMenuAfterClose() {
  271. this.hot.listen();
  272. this.hot.runHooks('afterContextMenuHide', this);
  273. }
  274. /**
  275. * Destroy instance.
  276. */
  277. destroy() {
  278. this.close();
  279. if (this.menu) {
  280. this.menu.destroy();
  281. }
  282. super.destroy();
  283. }
  284. }
  285. ContextMenu.SEPARATOR = {
  286. name: SEPARATOR
  287. };
  288. registerPlugin('contextMenu', ContextMenu);
  289. export default ContextMenu;