textEditor.js 13 KB

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