TextAreaEditor.jsx 6.4 KB

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