editorManager.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. import { CellCoords } from './3rdparty/walkontable/src';
  2. import { KEY_CODES, isMetaKey, isCtrlMetaKey } from './helpers/unicode';
  3. import { stopPropagation, stopImmediatePropagation, isImmediatePropagationStopped } from './helpers/dom/event';
  4. import { getEditorInstance } from './editors';
  5. import EventManager from './eventManager';
  6. import { EditorState } from './editors/_baseEditor';
  7. function EditorManager(instance, priv, selection) {
  8. const _this = this;
  9. const eventManager = new EventManager(instance);
  10. let destroyed = false;
  11. let lock = false;
  12. let activeEditor;
  13. function moveSelectionAfterEnter(shiftKey) {
  14. const enterMoves = typeof priv.settings.enterMoves === 'function' ? priv.settings.enterMoves(event) : priv.settings.enterMoves;
  15. if (shiftKey) {
  16. // move selection up
  17. selection.transformStart(-enterMoves.row, -enterMoves.col);
  18. } else {
  19. // move selection down (add a new row if needed)
  20. selection.transformStart(enterMoves.row, enterMoves.col, true);
  21. }
  22. }
  23. function moveSelectionUp(shiftKey) {
  24. if (shiftKey) {
  25. selection.transformEnd(-1, 0);
  26. } else {
  27. selection.transformStart(-1, 0);
  28. }
  29. }
  30. function moveSelectionDown(shiftKey) {
  31. if (shiftKey) {
  32. // expanding selection down with shift
  33. selection.transformEnd(1, 0);
  34. } else {
  35. selection.transformStart(1, 0);
  36. }
  37. }
  38. function moveSelectionRight(shiftKey) {
  39. if (shiftKey) {
  40. selection.transformEnd(0, 1);
  41. } else {
  42. selection.transformStart(0, 1);
  43. }
  44. }
  45. function moveSelectionLeft(shiftKey) {
  46. if (shiftKey) {
  47. selection.transformEnd(0, -1);
  48. } else {
  49. selection.transformStart(0, -1);
  50. }
  51. }
  52. function onKeyDown(event) {
  53. if (!instance.isListening()) {
  54. return;
  55. }
  56. instance.runHooks('beforeKeyDown', event);
  57. // keyCode 229 aka 'uninitialized' doesn't take into account with editors. This key code is produced when unfinished
  58. // character is entering (using IME editor). It is fired mainly on linux (ubuntu) with installed ibus-pinyin package.
  59. if (destroyed || event.keyCode === 229) {
  60. return;
  61. }
  62. if (isImmediatePropagationStopped(event)) {
  63. return;
  64. }
  65. priv.lastKeyCode = event.keyCode;
  66. if (!selection.isSelected()) {
  67. return;
  68. }
  69. // catch CTRL but not right ALT (which in some systems triggers ALT+CTRL)
  70. const ctrlDown = (event.ctrlKey || event.metaKey) && !event.altKey;
  71. if (activeEditor && !activeEditor.isWaiting()) {
  72. if (!isMetaKey(event.keyCode) && !isCtrlMetaKey(event.keyCode) && !ctrlDown && !_this.isEditorOpened()) {
  73. _this.openEditor('', event);
  74. return;
  75. }
  76. }
  77. const rangeModifier = event.shiftKey ? selection.setRangeEnd : selection.setRangeStart;
  78. let tabMoves;
  79. switch (event.keyCode) {
  80. case KEY_CODES.A:
  81. if (!_this.isEditorOpened() && ctrlDown) {
  82. instance.selectAll();
  83. event.preventDefault();
  84. stopPropagation(event);
  85. }
  86. break;
  87. case KEY_CODES.ARROW_UP:
  88. if (_this.isEditorOpened() && !activeEditor.isWaiting()) {
  89. _this.closeEditorAndSaveChanges(ctrlDown);
  90. }
  91. moveSelectionUp(event.shiftKey);
  92. event.preventDefault();
  93. stopPropagation(event);
  94. break;
  95. case KEY_CODES.ARROW_DOWN:
  96. if (_this.isEditorOpened() && !activeEditor.isWaiting()) {
  97. _this.closeEditorAndSaveChanges(ctrlDown);
  98. }
  99. moveSelectionDown(event.shiftKey);
  100. event.preventDefault();
  101. stopPropagation(event);
  102. break;
  103. case KEY_CODES.ARROW_RIGHT:
  104. if (_this.isEditorOpened() && !activeEditor.isWaiting()) {
  105. _this.closeEditorAndSaveChanges(ctrlDown);
  106. }
  107. moveSelectionRight(event.shiftKey);
  108. event.preventDefault();
  109. stopPropagation(event);
  110. break;
  111. case KEY_CODES.ARROW_LEFT:
  112. if (_this.isEditorOpened() && !activeEditor.isWaiting()) {
  113. _this.closeEditorAndSaveChanges(ctrlDown);
  114. }
  115. moveSelectionLeft(event.shiftKey);
  116. event.preventDefault();
  117. stopPropagation(event);
  118. break;
  119. case KEY_CODES.TAB:
  120. tabMoves = typeof priv.settings.tabMoves === 'function' ? priv.settings.tabMoves(event) : priv.settings.tabMoves;
  121. if (event.shiftKey) {
  122. // move selection left
  123. selection.transformStart(-tabMoves.row, -tabMoves.col);
  124. } else {
  125. // move selection right (add a new column if needed)
  126. selection.transformStart(tabMoves.row, tabMoves.col, true);
  127. }
  128. event.preventDefault();
  129. stopPropagation(event);
  130. break;
  131. case KEY_CODES.BACKSPACE:
  132. case KEY_CODES.DELETE:
  133. instance.emptySelectedCells();
  134. _this.prepareEditor();
  135. event.preventDefault();
  136. break;
  137. case KEY_CODES.F2:
  138. /* F2 */
  139. if (activeEditor) {
  140. activeEditor.enableFullEditMode();
  141. }
  142. _this.openEditor(null, event);
  143. event.preventDefault(); // prevent Opera from opening 'Go to Page dialog'
  144. break;
  145. case KEY_CODES.ENTER:
  146. /* return/enter */
  147. if (_this.isEditorOpened()) {
  148. if (activeEditor && activeEditor.state !== EditorState.WAITING) {
  149. _this.closeEditorAndSaveChanges(ctrlDown);
  150. }
  151. moveSelectionAfterEnter(event.shiftKey);
  152. } else if (instance.getSettings().enterBeginsEditing) {
  153. if (activeEditor) {
  154. activeEditor.enableFullEditMode();
  155. }
  156. _this.openEditor(null, event);
  157. } else {
  158. moveSelectionAfterEnter(event.shiftKey);
  159. }
  160. event.preventDefault(); // don't add newline to field
  161. stopImmediatePropagation(event); // required by HandsontableEditor
  162. break;
  163. case KEY_CODES.ESCAPE:
  164. if (_this.isEditorOpened()) {
  165. _this.closeEditorAndRestoreOriginalValue(ctrlDown);
  166. activeEditor.focus();
  167. }
  168. event.preventDefault();
  169. break;
  170. case KEY_CODES.HOME:
  171. if (event.ctrlKey || event.metaKey) {
  172. rangeModifier.call(selection, new CellCoords(0, selection.selectedRange.current().from.col));
  173. } else {
  174. rangeModifier.call(selection, new CellCoords(selection.selectedRange.current().from.row, 0));
  175. }
  176. event.preventDefault(); // don't scroll the window
  177. stopPropagation(event);
  178. break;
  179. case KEY_CODES.END:
  180. if (event.ctrlKey || event.metaKey) {
  181. rangeModifier.call(selection, new CellCoords(instance.countRows() - 1, selection.selectedRange.current().from.col));
  182. } else {
  183. rangeModifier.call(selection, new CellCoords(selection.selectedRange.current().from.row, instance.countCols() - 1));
  184. }
  185. event.preventDefault(); // don't scroll the window
  186. stopPropagation(event);
  187. break;
  188. case KEY_CODES.PAGE_UP:
  189. selection.transformStart(-instance.countVisibleRows(), 0);
  190. event.preventDefault(); // don't page up the window
  191. stopPropagation(event);
  192. break;
  193. case KEY_CODES.PAGE_DOWN:
  194. selection.transformStart(instance.countVisibleRows(), 0);
  195. event.preventDefault(); // don't page down the window
  196. stopPropagation(event);
  197. break;
  198. default:
  199. break;
  200. }
  201. }
  202. function init() {
  203. instance.addHook('afterDocumentKeyDown', onKeyDown);
  204. eventManager.addEventListener(document.documentElement, 'keydown', (event) => {
  205. if (!destroyed) {
  206. instance.runHooks('afterDocumentKeyDown', event);
  207. }
  208. });
  209. // Open editor when text composition is started (IME editor)
  210. eventManager.addEventListener(document.documentElement, 'compositionstart', (event) => {
  211. if (!destroyed && activeEditor && !activeEditor.isOpened() && instance.isListening()) {
  212. _this.openEditor('', event);
  213. }
  214. });
  215. function onDblClick(event, coords, elem) {
  216. // may be TD or TH
  217. if (elem.nodeName === 'TD') {
  218. if (activeEditor) {
  219. activeEditor.enableFullEditMode();
  220. }
  221. _this.openEditor(null, event);
  222. }
  223. }
  224. instance.view.wt.update('onCellDblClick', onDblClick);
  225. }
  226. /**
  227. * Lock the editor from being prepared and closed. Locking the editor prevents its closing and
  228. * reinitialized after selecting the new cell. This feature is necessary for a mobile editor.
  229. *
  230. * @function lockEditor
  231. * @memberof! Handsontable.EditorManager#
  232. */
  233. this.lockEditor = function() {
  234. lock = true;
  235. };
  236. /**
  237. * Unlock the editor from being prepare d and closed. This method restores the original behavior of
  238. * the editors where for every new selection its instances are closed.
  239. *
  240. * @function unlockEditor
  241. * @memberof! Handsontable.EditorManager#
  242. */
  243. this.unlockEditor = function() {
  244. lock = false;
  245. };
  246. this.getLockState = function() {
  247. return lock;
  248. };
  249. /**
  250. * Destroy current editor, if exists.
  251. *
  252. * @function destroyEditor
  253. * @memberof! Handsontable.EditorManager#
  254. * @param {Boolean} revertOriginal
  255. */
  256. this.destroyEditor = function(revertOriginal) {
  257. if (!lock) {
  258. this.closeEditor(revertOriginal);
  259. }
  260. };
  261. /**
  262. * Get active editor.
  263. *
  264. * @function getActiveEditor
  265. * @memberof! Handsontable.EditorManager#
  266. * @returns {*}
  267. */
  268. this.getActiveEditor = function() {
  269. return activeEditor;
  270. };
  271. /**
  272. * Prepare text input to be displayed at given grid cell.
  273. *
  274. * @function prepareEditor
  275. * @memberof! Handsontable.EditorManager#
  276. */
  277. this.prepareEditor = function() {
  278. if (lock) {
  279. return;
  280. }
  281. if (activeEditor && activeEditor.isWaiting()) {
  282. this.closeEditor(false, false, (dataSaved) => {
  283. if (dataSaved) {
  284. _this.prepareEditor();
  285. }
  286. });
  287. return;
  288. }
  289. const row = instance.selection.selectedRange.current().highlight.row;
  290. const col = instance.selection.selectedRange.current().highlight.col;
  291. const prop = instance.colToProp(col);
  292. const td = instance.getCell(row, col);
  293. const originalValue = instance.getSourceDataAtCell(instance.runHooks('modifyRow', row), col);
  294. const cellProperties = instance.getCellMeta(row, col);
  295. const editorClass = instance.getCellEditor(cellProperties);
  296. if (editorClass) {
  297. activeEditor = getEditorInstance(editorClass, instance);
  298. activeEditor.prepare(row, col, prop, td, originalValue, cellProperties);
  299. } else {
  300. activeEditor = void 0;
  301. }
  302. };
  303. /**
  304. * Check is editor is opened/showed.
  305. *
  306. * @function isEditorOpened
  307. * @memberof! Handsontable.EditorManager#
  308. * @returns {Boolean}
  309. */
  310. this.isEditorOpened = function() {
  311. return activeEditor && activeEditor.isOpened();
  312. };
  313. /**
  314. * Open editor with initial value.
  315. *
  316. * @function openEditor
  317. * @memberof! Handsontable.EditorManager#
  318. * @param {null|String} newInitialValue new value from which editor will start if handled property it's not the `null`.
  319. * @param {DOMEvent} event
  320. */
  321. this.openEditor = function(newInitialValue, event) {
  322. if (!activeEditor) {
  323. return;
  324. }
  325. const readOnly = activeEditor.cellProperties.readOnly;
  326. if (readOnly) {
  327. // move the selection after opening the editor with ENTER key
  328. if (event && event.keyCode === KEY_CODES.ENTER) {
  329. moveSelectionAfterEnter();
  330. }
  331. } else {
  332. activeEditor.beginEditing(newInitialValue, event);
  333. }
  334. };
  335. /**
  336. * Close editor, finish editing cell.
  337. *
  338. * @function closeEditor
  339. * @memberof! Handsontable.EditorManager#
  340. * @param {Boolean} restoreOriginalValue
  341. * @param {Boolean} [ctrlDown]
  342. * @param {Function} [callback]
  343. */
  344. this.closeEditor = function(restoreOriginalValue, ctrlDown, callback) {
  345. if (activeEditor) {
  346. activeEditor.finishEditing(restoreOriginalValue, ctrlDown, callback);
  347. } else if (callback) {
  348. callback(false);
  349. }
  350. };
  351. /**
  352. * Close editor and save changes.
  353. *
  354. * @function closeEditorAndSaveChanges
  355. * @memberof! Handsontable.EditorManager#
  356. * @param {Boolean} ctrlDown
  357. */
  358. this.closeEditorAndSaveChanges = function(ctrlDown) {
  359. return this.closeEditor(false, ctrlDown);
  360. };
  361. /**
  362. * Close editor and restore original value.
  363. *
  364. * @function closeEditorAndRestoreOriginalValue
  365. * @memberof! Handsontable.EditorManager#
  366. * @param {Boolean} ctrlDown
  367. */
  368. this.closeEditorAndRestoreOriginalValue = function(ctrlDown) {
  369. return this.closeEditor(true, ctrlDown);
  370. };
  371. /**
  372. * Destroy the instance.
  373. */
  374. this.destroy = function() {
  375. destroyed = true;
  376. };
  377. init();
  378. }
  379. const instances = new WeakMap();
  380. EditorManager.getInstance = function(hotInstance, hotSettings, selection, datamap) {
  381. let editorManager = instances.get(hotInstance);
  382. if (!editorManager) {
  383. editorManager = new EditorManager(hotInstance, hotSettings, selection, datamap);
  384. instances.set(hotInstance, editorManager);
  385. }
  386. return editorManager;
  387. };
  388. export default EditorManager;