TextAreaEditor.jsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import React from 'react';
  2. // import PropTypes from 'prop-types';
  3. import InterceptorManager from '@commons/service/interceptor-manager';
  4. import { Input } from 'reactstrap';
  5. import AbstractEditor from './AbstractEditor';
  6. import pasteHelper from './PasteHelper';
  7. import mlu from './MarkdownListUtil';
  8. import PreventMarkdownListInterceptor from './PreventMarkdownListInterceptor';
  9. export default class TextAreaEditor extends AbstractEditor {
  10. constructor(props) {
  11. super(props);
  12. this.logger = require('@alias/logger')('growi:PageEditor:TextAreaEditor');
  13. this.state = {
  14. value: this.props.value,
  15. isGfmMode: this.props.isGfmMode,
  16. };
  17. this.textarea = React.createRef();
  18. this.init();
  19. this.handleEnterKey = this.handleEnterKey.bind(this);
  20. this.keyPressHandler = this.keyPressHandler.bind(this);
  21. this.pasteHandler = this.pasteHandler.bind(this);
  22. this.dragEnterHandler = this.dragEnterHandler.bind(this);
  23. }
  24. init() {
  25. this.interceptorManager = new InterceptorManager();
  26. this.interceptorManager.addInterceptors([
  27. new PreventMarkdownListInterceptor(),
  28. ]);
  29. }
  30. componentDidMount() {
  31. // initialize caret line
  32. this.setCaretLine(0);
  33. // set event handlers
  34. this.textarea.addEventListener('keypress', this.keyPressHandler);
  35. this.textarea.addEventListener('paste', this.pasteHandler);
  36. this.textarea.addEventListener('dragenter', this.dragEnterHandler);
  37. }
  38. /**
  39. * @inheritDoc
  40. */
  41. forceToFocus() {
  42. setTimeout(() => {
  43. this.textarea.focus();
  44. }, 150);
  45. }
  46. /**
  47. * @inheritDoc
  48. */
  49. setValue(newValue) {
  50. this.setState({ value: newValue });
  51. this.textarea.value = newValue;
  52. }
  53. /**
  54. * @inheritDoc
  55. */
  56. setGfmMode(bool) {
  57. this.setState({
  58. isGfmMode: bool,
  59. });
  60. }
  61. /**
  62. * @inheritDoc
  63. */
  64. setCaretLine(line) {
  65. if (Number.isNaN(line)) {
  66. return;
  67. }
  68. // scroll to bottom
  69. this.textarea.scrollTop = this.textarea.scrollHeight;
  70. const lines = this.textarea.value.split('\n').slice(0, line + 1);
  71. /* eslint-disable no-param-reassign, no-return-assign */
  72. const pos = lines
  73. .map((lineStr) => { return lineStr.length + 1 }) // correct length+1 of each lines
  74. .reduce((a, x) => { return a += x }, 0) // sum
  75. - 1; // -1
  76. /* eslint-enable no-param-reassign, no-return-assign */
  77. this.textarea.setSelectionRange(pos, pos);
  78. }
  79. /**
  80. * @inheritDoc
  81. */
  82. setScrollTopByLine(line) {
  83. // do nothing
  84. }
  85. /**
  86. * @inheritDoc
  87. */
  88. insertText(text) {
  89. const startPos = this.textarea.selectionStart;
  90. const endPos = this.textarea.selectionEnd;
  91. this.replaceValue(text, startPos, endPos);
  92. }
  93. /**
  94. * @inheritDoc
  95. */
  96. getStrFromBol() {
  97. const currentPos = this.textarea.selectionStart;
  98. return this.textarea.value.substring(this.getBolPos(), currentPos);
  99. }
  100. /**
  101. * @inheritDoc
  102. */
  103. getStrToEol() {
  104. const currentPos = this.textarea.selectionStart;
  105. return this.textarea.value.substring(currentPos, this.getEolPos());
  106. }
  107. /**
  108. * @inheritDoc
  109. */
  110. getStrFromBolToSelectedUpperPos() {
  111. const startPos = this.textarea.selectionStart;
  112. const endPos = this.textarea.selectionEnd;
  113. const upperPos = (startPos < endPos) ? startPos : endPos;
  114. return this.textarea.value.substring(this.getBolPos(), upperPos);
  115. }
  116. /**
  117. * @inheritDoc
  118. */
  119. replaceBolToCurrentPos(text) {
  120. const startPos = this.textarea.selectionStart;
  121. const endPos = this.textarea.selectionEnd;
  122. const lowerPos = (startPos < endPos) ? endPos : startPos;
  123. this.replaceValue(text, this.getBolPos(), lowerPos);
  124. }
  125. /**
  126. * @inheritDoc
  127. */
  128. replaceLine(text) {
  129. this.replaceValue(text, this.getBolPos(), this.getEolPos());
  130. }
  131. getBolPos() {
  132. const currentPos = this.textarea.selectionStart;
  133. return this.textarea.value.lastIndexOf('\n', currentPos - 1) + 1;
  134. }
  135. getEolPos() {
  136. const currentPos = this.textarea.selectionStart;
  137. const pos = this.textarea.value.indexOf('\n', currentPos);
  138. if (pos < 0) { // not found but EOF
  139. return this.textarea.value.length;
  140. }
  141. return pos;
  142. }
  143. replaceValue(text, startPos, endPos) {
  144. // create new value
  145. const value = this.textarea.value;
  146. const newValue = value.substring(0, startPos) + text + value.substring(endPos, value.length);
  147. // calculate new position
  148. const newPos = startPos + text.length;
  149. this.textarea.value = newValue;
  150. this.textarea.setSelectionRange(newPos, newPos);
  151. }
  152. /**
  153. * keypress event handler
  154. * @param {string} event
  155. */
  156. keyPressHandler(event) {
  157. const key = event.key.toLowerCase();
  158. if (key === 'enter') {
  159. if (event.ctrlKey || event.altKey || event.metaKey) {
  160. return;
  161. }
  162. this.handleEnterKey(event);
  163. }
  164. }
  165. /**
  166. * handle ENTER key
  167. * @param {string} event
  168. */
  169. handleEnterKey(event) {
  170. if (!this.state.isGfmMode) {
  171. return; // do nothing
  172. }
  173. const context = {
  174. handlers: [], // list of handlers which process enter key
  175. editor: this,
  176. };
  177. const interceptorManager = this.interceptorManager;
  178. interceptorManager.process('preHandleEnter', context)
  179. .then(() => {
  180. event.preventDefault();
  181. if (context.handlers.length === 0) {
  182. mlu.newlineAndIndentContinueMarkdownList(this);
  183. }
  184. });
  185. }
  186. /**
  187. * paste event handler
  188. * @param {any} event
  189. */
  190. pasteHandler(event) {
  191. const types = event.clipboardData.types;
  192. // files
  193. if (types.includes('Files')) {
  194. event.preventDefault();
  195. this.dispatchPasteFiles(event);
  196. }
  197. // text
  198. else if (types.includes('text/plain')) {
  199. pasteHelper.pasteText(this, event);
  200. }
  201. }
  202. dragEnterHandler(event) {
  203. this.dispatchDragEnter(event);
  204. }
  205. dispatchDragEnter(event) {
  206. if (this.props.onDragEnter != null) {
  207. this.props.onDragEnter(event);
  208. }
  209. }
  210. render() {
  211. return (
  212. <React.Fragment>
  213. <Input
  214. type="textarea"
  215. className="textarea-editor shadow-none"
  216. innerRef={(c) => { this.textarea = c }}
  217. defaultValue={this.state.value}
  218. onChange={(e) => {
  219. if (this.props.onChange != null) {
  220. this.props.onChange(e.target.value);
  221. }
  222. }}
  223. />
  224. </React.Fragment>
  225. );
  226. }
  227. }
  228. TextAreaEditor.propTypes = Object.assign({
  229. }, AbstractEditor.propTypes);