11d71aa1716d4c88763acad5fb4cc47fa7ca2ff10040324eecd96f84d56808af5e263ac3cccd2d1b8c4a56eb93b4ac802e90e8713bd204ec0b2a604140ccb6 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. import {
  2. addClass,
  3. getCaretPosition,
  4. getComputedStyle,
  5. getCssTransform,
  6. getScrollableElement,
  7. getScrollbarWidth,
  8. innerWidth,
  9. offset,
  10. resetCssTransform,
  11. setCaretPosition,
  12. hasVerticalScrollbar,
  13. hasHorizontalScrollbar
  14. } from './../helpers/dom/element';
  15. import autoResize from './../../lib/autoResize/autoResize';
  16. import BaseEditor, {EditorState} from './_baseEditor';
  17. import EventManager from './../eventManager';
  18. import {KEY_CODES} from './../helpers/unicode';
  19. import {stopPropagation, stopImmediatePropagation, isImmediatePropagationStopped} from './../helpers/dom/event';
  20. const TextEditor = BaseEditor.prototype.extend();
  21. /**
  22. * @private
  23. * @editor TextEditor
  24. * @class TextEditor
  25. * @dependencies autoResize
  26. */
  27. TextEditor.prototype.init = function() {
  28. var that = this;
  29. this.createElements();
  30. this.eventManager = new EventManager(this);
  31. this.bindEvents();
  32. this.autoResize = autoResize();
  33. this.instance.addHook('afterDestroy', () => {
  34. that.destroy();
  35. });
  36. };
  37. TextEditor.prototype.getValue = function() {
  38. return this.TEXTAREA.value;
  39. };
  40. TextEditor.prototype.setValue = function(newValue) {
  41. this.TEXTAREA.value = newValue;
  42. };
  43. var onBeforeKeyDown = function onBeforeKeyDown(event) {
  44. var
  45. instance = this,
  46. that = instance.getActiveEditor(),
  47. ctrlDown;
  48. // catch CTRL but not right ALT (which in some systems triggers ALT+CTRL)
  49. ctrlDown = (event.ctrlKey || event.metaKey) && !event.altKey;
  50. // Process only events that have been fired in the editor
  51. if (event.target !== that.TEXTAREA || isImmediatePropagationStopped(event)) {
  52. return;
  53. }
  54. if (event.keyCode === 17 || event.keyCode === 224 || event.keyCode === 91 || event.keyCode === 93) {
  55. // when CTRL or its equivalent is pressed and cell is edited, don't prepare selectable text in textarea
  56. stopImmediatePropagation(event);
  57. return;
  58. }
  59. switch (event.keyCode) {
  60. case KEY_CODES.ARROW_RIGHT:
  61. if (that.isInFullEditMode()) {
  62. if ((!that.isWaiting() && !that.allowKeyEventPropagation) ||
  63. (!that.isWaiting() && that.allowKeyEventPropagation && !that.allowKeyEventPropagation(event.keyCode))) {
  64. stopImmediatePropagation(event);
  65. }
  66. }
  67. break;
  68. case KEY_CODES.ARROW_LEFT:
  69. if (that.isInFullEditMode()) {
  70. if ((!that.isWaiting() && !that.allowKeyEventPropagation) ||
  71. (!that.isWaiting() && that.allowKeyEventPropagation && !that.allowKeyEventPropagation(event.keyCode))) {
  72. stopImmediatePropagation(event);
  73. }
  74. }
  75. break;
  76. case KEY_CODES.ARROW_UP:
  77. case KEY_CODES.ARROW_DOWN:
  78. if (that.isInFullEditMode()) {
  79. if ((!that.isWaiting() && !that.allowKeyEventPropagation) ||
  80. (!that.isWaiting() && that.allowKeyEventPropagation && !that.allowKeyEventPropagation(event.keyCode))) {
  81. stopImmediatePropagation(event);
  82. }
  83. }
  84. break;
  85. case KEY_CODES.ENTER:
  86. var selected = that.instance.getSelected();
  87. var isMultipleSelection = !(selected[0] === selected[2] && selected[1] === selected[3]);
  88. if ((ctrlDown && !isMultipleSelection) || event.altKey) { // if ctrl+enter or alt+enter, add new line
  89. if (that.isOpened()) {
  90. var
  91. caretPosition = getCaretPosition(that.TEXTAREA),
  92. value = that.getValue();
  93. var newValue = `${value.slice(0, caretPosition)}\n${value.slice(caretPosition)}`;
  94. that.setValue(newValue);
  95. setCaretPosition(that.TEXTAREA, caretPosition + 1);
  96. } else {
  97. that.beginEditing(`${that.originalValue}\n`);
  98. }
  99. stopImmediatePropagation(event);
  100. }
  101. event.preventDefault(); // don't add newline to field
  102. break;
  103. case KEY_CODES.A:
  104. case KEY_CODES.X:
  105. case KEY_CODES.C:
  106. case KEY_CODES.V:
  107. if (ctrlDown) {
  108. stopImmediatePropagation(event); // CTRL+A, CTRL+C, CTRL+V, CTRL+X should only work locally when cell is edited (not in table context)
  109. }
  110. break;
  111. case KEY_CODES.BACKSPACE:
  112. case KEY_CODES.DELETE:
  113. case KEY_CODES.HOME:
  114. case KEY_CODES.END:
  115. stopImmediatePropagation(event); // backspace, delete, home, end should only work locally when cell is edited (not in table context)
  116. break;
  117. default:
  118. break;
  119. }
  120. if ([KEY_CODES.ARROW_UP, KEY_CODES.ARROW_RIGHT, KEY_CODES.ARROW_DOWN, KEY_CODES.ARROW_LEFT].indexOf(event.keyCode) === -1) {
  121. that.autoResize.resize(String.fromCharCode(event.keyCode));
  122. }
  123. };
  124. TextEditor.prototype.open = function() {
  125. this.refreshDimensions(); // need it instantly, to prevent https://github.com/handsontable/handsontable/issues/348
  126. this.instance.addHook('beforeKeyDown', onBeforeKeyDown);
  127. };
  128. TextEditor.prototype.close = function(tdOutside) {
  129. this.textareaParentStyle.display = 'none';
  130. this.autoResize.unObserve();
  131. if (document.activeElement === this.TEXTAREA) {
  132. this.instance.listen(); // don't refocus the table if user focused some cell outside of HT on purpose
  133. }
  134. this.instance.removeHook('beforeKeyDown', onBeforeKeyDown);
  135. };
  136. TextEditor.prototype.focus = function() {
  137. this.TEXTAREA.focus();
  138. setCaretPosition(this.TEXTAREA, this.TEXTAREA.value.length);
  139. };
  140. TextEditor.prototype.createElements = function() {
  141. // this.$body = $(document.body);
  142. this.TEXTAREA = document.createElement('TEXTAREA');
  143. addClass(this.TEXTAREA, 'handsontableInput');
  144. this.textareaStyle = this.TEXTAREA.style;
  145. this.textareaStyle.width = 0;
  146. this.textareaStyle.height = 0;
  147. this.TEXTAREA_PARENT = document.createElement('DIV');
  148. addClass(this.TEXTAREA_PARENT, 'handsontableInputHolder');
  149. this.textareaParentStyle = this.TEXTAREA_PARENT.style;
  150. this.textareaParentStyle.top = 0;
  151. this.textareaParentStyle.left = 0;
  152. this.textareaParentStyle.display = 'none';
  153. this.TEXTAREA_PARENT.appendChild(this.TEXTAREA);
  154. this.instance.rootElement.appendChild(this.TEXTAREA_PARENT);
  155. var that = this;
  156. this.instance._registerTimeout(setTimeout(() => {
  157. that.refreshDimensions();
  158. }, 0));
  159. };
  160. TextEditor.prototype.getEditedCell = function() {
  161. var
  162. editorSection = this.checkEditorSection(),
  163. editedCell;
  164. switch (editorSection) {
  165. case 'top':
  166. editedCell = this.instance.view.wt.wtOverlays.topOverlay.clone.wtTable.getCell({
  167. row: this.row,
  168. col: this.col
  169. });
  170. this.textareaParentStyle.zIndex = 101;
  171. break;
  172. case 'top-left-corner':
  173. editedCell = this.instance.view.wt.wtOverlays.topLeftCornerOverlay.clone.wtTable.getCell({
  174. row: this.row,
  175. col: this.col
  176. });
  177. this.textareaParentStyle.zIndex = 103;
  178. break;
  179. case 'bottom-left-corner':
  180. editedCell = this.instance.view.wt.wtOverlays.bottomLeftCornerOverlay.clone.wtTable.getCell({
  181. row: this.row,
  182. col: this.col
  183. });
  184. this.textareaParentStyle.zIndex = 103;
  185. break;
  186. case 'left':
  187. editedCell = this.instance.view.wt.wtOverlays.leftOverlay.clone.wtTable.getCell({
  188. row: this.row,
  189. col: this.col
  190. });
  191. this.textareaParentStyle.zIndex = 102;
  192. break;
  193. case 'bottom':
  194. editedCell = this.instance.view.wt.wtOverlays.bottomOverlay.clone.wtTable.getCell({
  195. row: this.row,
  196. col: this.col
  197. });
  198. this.textareaParentStyle.zIndex = 102;
  199. break;
  200. default:
  201. editedCell = this.instance.getCell(this.row, this.col);
  202. this.textareaParentStyle.zIndex = '';
  203. break;
  204. }
  205. return editedCell != -1 && editedCell != -2 ? editedCell : void 0;
  206. };
  207. TextEditor.prototype.refreshValue = function() {
  208. let sourceData = this.instance.getSourceDataAtCell(this.row, this.prop);
  209. this.originalValue = sourceData;
  210. this.setValue(sourceData);
  211. this.refreshDimensions();
  212. };
  213. TextEditor.prototype.refreshDimensions = function() {
  214. if (this.state !== EditorState.EDITING) {
  215. return;
  216. }
  217. this.TD = this.getEditedCell();
  218. // TD is outside of the viewport.
  219. if (!this.TD) {
  220. this.close(true);
  221. return;
  222. }
  223. var
  224. currentOffset = offset(this.TD),
  225. containerOffset = offset(this.instance.rootElement),
  226. scrollableContainer = getScrollableElement(this.TD),
  227. totalRowsCount = this.instance.countRows(),
  228. // If colHeaders is disabled, cells in the first row have border-top
  229. editTopModifier = currentOffset.top === containerOffset.top ? 0 : 1,
  230. editTop = currentOffset.top - containerOffset.top - editTopModifier - (scrollableContainer.scrollTop || 0),
  231. editLeft = currentOffset.left - containerOffset.left - 1 - (scrollableContainer.scrollLeft || 0),
  232. settings = this.instance.getSettings(),
  233. rowHeadersCount = this.instance.hasRowHeaders(),
  234. colHeadersCount = this.instance.hasColHeaders(),
  235. editorSection = this.checkEditorSection(),
  236. backgroundColor = this.TD.style.backgroundColor,
  237. cssTransformOffset;
  238. // TODO: Refactor this to the new instance.getCell method (from #ply-59), after 0.12.1 is released
  239. switch (editorSection) {
  240. case 'top':
  241. cssTransformOffset = getCssTransform(this.instance.view.wt.wtOverlays.topOverlay.clone.wtTable.holder.parentNode);
  242. break;
  243. case 'left':
  244. cssTransformOffset = getCssTransform(this.instance.view.wt.wtOverlays.leftOverlay.clone.wtTable.holder.parentNode);
  245. break;
  246. case 'top-left-corner':
  247. cssTransformOffset = getCssTransform(this.instance.view.wt.wtOverlays.topLeftCornerOverlay.clone.wtTable.holder.parentNode);
  248. break;
  249. case 'bottom-left-corner':
  250. cssTransformOffset = getCssTransform(this.instance.view.wt.wtOverlays.bottomLeftCornerOverlay.clone.wtTable.holder.parentNode);
  251. break;
  252. case 'bottom':
  253. cssTransformOffset = getCssTransform(this.instance.view.wt.wtOverlays.bottomOverlay.clone.wtTable.holder.parentNode);
  254. break;
  255. default:
  256. break;
  257. }
  258. if (colHeadersCount && this.instance.getSelected()[0] === 0 ||
  259. (settings.fixedRowsBottom && this.instance.getSelected()[0] === totalRowsCount - settings.fixedRowsBottom)) {
  260. editTop += 1;
  261. }
  262. if (this.instance.getSelected()[1] === 0) {
  263. editLeft += 1;
  264. }
  265. if (cssTransformOffset && cssTransformOffset != -1) {
  266. this.textareaParentStyle[cssTransformOffset[0]] = cssTransformOffset[1];
  267. } else {
  268. resetCssTransform(this.TEXTAREA_PARENT);
  269. }
  270. this.textareaParentStyle.top = `${editTop}px`;
  271. this.textareaParentStyle.left = `${editLeft}px`;
  272. let firstRowOffset = this.instance.view.wt.wtViewport.rowsRenderCalculator.startPosition;
  273. let firstColumnOffset = this.instance.view.wt.wtViewport.columnsRenderCalculator.startPosition;
  274. let horizontalScrollPosition = this.instance.view.wt.wtOverlays.leftOverlay.getScrollPosition();
  275. let verticalScrollPosition = this.instance.view.wt.wtOverlays.topOverlay.getScrollPosition();
  276. let scrollbarWidth = getScrollbarWidth();
  277. let cellTopOffset = this.TD.offsetTop + firstRowOffset - verticalScrollPosition;
  278. let cellLeftOffset = this.TD.offsetLeft + firstColumnOffset - horizontalScrollPosition;
  279. let width = innerWidth(this.TD) - 8;
  280. let actualVerticalScrollbarWidth = hasVerticalScrollbar(scrollableContainer) ? scrollbarWidth : 0;
  281. let actualHorizontalScrollbarWidth = hasHorizontalScrollbar(scrollableContainer) ? scrollbarWidth : 0;
  282. let maxWidth = this.instance.view.maximumVisibleElementWidth(cellLeftOffset) - 9 - actualVerticalScrollbarWidth;
  283. let height = this.TD.scrollHeight + 1;
  284. let maxHeight = Math.max(this.instance.view.maximumVisibleElementHeight(cellTopOffset) - actualHorizontalScrollbarWidth, 23);
  285. const cellComputedStyle = getComputedStyle(this.TD);
  286. this.TEXTAREA.style.fontSize = cellComputedStyle.fontSize;
  287. this.TEXTAREA.style.fontFamily = cellComputedStyle.fontFamily;
  288. this.TEXTAREA.style.backgroundColor = ''; // RESET STYLE
  289. this.TEXTAREA.style.backgroundColor = backgroundColor ? backgroundColor : getComputedStyle(this.TEXTAREA).backgroundColor;
  290. this.autoResize.init(this.TEXTAREA, {
  291. minHeight: Math.min(height, maxHeight),
  292. maxHeight, // TEXTAREA should never be wider than visible part of the viewport (should not cover the scrollbar)
  293. minWidth: Math.min(width, maxWidth),
  294. maxWidth // TEXTAREA should never be wider than visible part of the viewport (should not cover the scrollbar)
  295. }, true);
  296. this.textareaParentStyle.display = 'block';
  297. };
  298. TextEditor.prototype.bindEvents = function() {
  299. var editor = this;
  300. this.eventManager.addEventListener(this.TEXTAREA, 'cut', (event) => {
  301. stopPropagation(event);
  302. });
  303. this.eventManager.addEventListener(this.TEXTAREA, 'paste', (event) => {
  304. stopPropagation(event);
  305. });
  306. this.instance.addHook('afterScrollHorizontally', () => {
  307. editor.refreshDimensions();
  308. });
  309. this.instance.addHook('afterScrollVertically', () => {
  310. editor.refreshDimensions();
  311. });
  312. this.instance.addHook('afterColumnResize', () => {
  313. editor.refreshDimensions();
  314. editor.focus();
  315. });
  316. this.instance.addHook('afterRowResize', () => {
  317. editor.refreshDimensions();
  318. editor.focus();
  319. });
  320. this.instance.addHook('afterDestroy', () => {
  321. editor.eventManager.destroy();
  322. });
  323. };
  324. TextEditor.prototype.destroy = function() {
  325. this.eventManager.destroy();
  326. };
  327. export default TextEditor;