Editor.jsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import { Subscribe } from 'unstated';
  4. import {
  5. Modal, ModalHeader, ModalBody,
  6. } from 'reactstrap';
  7. import Dropzone from 'react-dropzone';
  8. import EditorContainer from '~/client/services/EditorContainer';
  9. import Cheatsheet from './Cheatsheet';
  10. import AbstractEditor from './AbstractEditor';
  11. import CodeMirrorEditor from './CodeMirrorEditor';
  12. import TextAreaEditor from './TextAreaEditor';
  13. import pasteHelper from './PasteHelper';
  14. export default class Editor extends AbstractEditor {
  15. constructor(props) {
  16. super(props);
  17. this.state = {
  18. isComponentDidMount: false,
  19. dropzoneActive: false,
  20. isUploading: false,
  21. isCheatsheetModalShown: false,
  22. };
  23. this.getEditorSubstance = this.getEditorSubstance.bind(this);
  24. this.pasteFilesHandler = this.pasteFilesHandler.bind(this);
  25. this.dragEnterHandler = this.dragEnterHandler.bind(this);
  26. this.dragLeaveHandler = this.dragLeaveHandler.bind(this);
  27. this.dropHandler = this.dropHandler.bind(this);
  28. this.showMarkdownHelp = this.showMarkdownHelp.bind(this);
  29. this.getAcceptableType = this.getAcceptableType.bind(this);
  30. this.getDropzoneClassName = this.getDropzoneClassName.bind(this);
  31. this.renderDropzoneOverlay = this.renderDropzoneOverlay.bind(this);
  32. }
  33. componentDidMount() {
  34. this.setState({ isComponentDidMount: true });
  35. }
  36. getEditorSubstance() {
  37. return this.props.isMobile
  38. ? this.taEditor
  39. : this.cmEditor;
  40. }
  41. /**
  42. * @inheritDoc
  43. */
  44. forceToFocus() {
  45. this.getEditorSubstance().forceToFocus();
  46. }
  47. /**
  48. * @inheritDoc
  49. */
  50. setValue(newValue) {
  51. this.getEditorSubstance().setValue(newValue);
  52. }
  53. /**
  54. * @inheritDoc
  55. */
  56. setGfmMode(bool) {
  57. this.getEditorSubstance().setGfmMode(bool);
  58. }
  59. /**
  60. * @inheritDoc
  61. */
  62. setCaretLine(line) {
  63. this.getEditorSubstance().setCaretLine(line);
  64. }
  65. /**
  66. * @inheritDoc
  67. */
  68. setScrollTopByLine(line) {
  69. this.getEditorSubstance().setScrollTopByLine(line);
  70. }
  71. /**
  72. * @inheritDoc
  73. */
  74. insertText(text) {
  75. this.getEditorSubstance().insertText(text);
  76. }
  77. /**
  78. * remove overlay and set isUploading to false
  79. */
  80. terminateUploadingState() {
  81. this.setState({
  82. dropzoneActive: false,
  83. isUploading: false,
  84. });
  85. }
  86. /**
  87. * dispatch onUpload event
  88. */
  89. dispatchUpload(files) {
  90. if (this.props.onUpload != null) {
  91. this.props.onUpload(files);
  92. }
  93. }
  94. /**
  95. * get acceptable(uploadable) file type
  96. */
  97. getAcceptableType() {
  98. let accept = 'null'; // reject all
  99. if (this.props.isUploadable) {
  100. if (!this.props.isUploadableFile) {
  101. accept = 'image/*'; // image only
  102. }
  103. else {
  104. accept = ''; // allow all
  105. }
  106. }
  107. return accept;
  108. }
  109. pasteFilesHandler(event) {
  110. const items = event.clipboardData.items || event.clipboardData.files || [];
  111. // abort if length is not 1
  112. if (items.length < 1) {
  113. return;
  114. }
  115. for (let i = 0; i < items.length; i++) {
  116. try {
  117. const file = items[i].getAsFile();
  118. // check file type (the same process as Dropzone)
  119. if (file != null && pasteHelper.isAcceptableType(file, this.getAcceptableType())) {
  120. this.dispatchUpload(file);
  121. this.setState({ isUploading: true });
  122. }
  123. }
  124. catch (e) {
  125. this.logger.error(e);
  126. }
  127. }
  128. }
  129. dragEnterHandler(event) {
  130. const dataTransfer = event.dataTransfer;
  131. // do nothing if contents is not files
  132. if (!dataTransfer.types.includes('Files')) {
  133. return;
  134. }
  135. this.setState({ dropzoneActive: true });
  136. }
  137. dragLeaveHandler() {
  138. this.setState({ dropzoneActive: false });
  139. }
  140. dropHandler(accepted, rejected) {
  141. // rejected
  142. if (accepted.length !== 1) { // length should be 0 or 1 because `multiple={false}` is set
  143. this.setState({ dropzoneActive: false });
  144. return;
  145. }
  146. const file = accepted[0];
  147. this.dispatchUpload(file);
  148. this.setState({ isUploading: true });
  149. }
  150. showMarkdownHelp() {
  151. this.setState({ isCheatsheetModalShown: true });
  152. }
  153. getDropzoneClassName(isDragAccept, isDragReject) {
  154. let className = 'dropzone';
  155. if (!this.props.isUploadable) {
  156. className += ' dropzone-unuploadable';
  157. }
  158. else {
  159. className += ' dropzone-uploadable';
  160. if (this.props.isUploadableFile) {
  161. className += ' dropzone-uploadablefile';
  162. }
  163. }
  164. // uploading
  165. if (this.state.isUploading) {
  166. className += ' dropzone-uploading';
  167. }
  168. if (isDragAccept) {
  169. className += ' dropzone-accepted';
  170. }
  171. if (isDragReject) {
  172. className += ' dropzone-rejected';
  173. }
  174. return className;
  175. }
  176. renderDropzoneOverlay() {
  177. return (
  178. <div className="overlay overlay-dropzone-active">
  179. {this.state.isUploading
  180. && (
  181. <span className="overlay-content">
  182. <div className="speeding-wheel d-inline-block"></div>
  183. <span className="sr-only">Uploading...</span>
  184. </span>
  185. )
  186. }
  187. {!this.state.isUploading && <span className="overlay-content"></span>}
  188. </div>
  189. );
  190. }
  191. renderNavbar() {
  192. return (
  193. <div className="m-0 navbar navbar-default navbar-editor" style={{ minHeight: 'unset' }}>
  194. <ul className="pl-2 nav nav-navbar">
  195. { this.getNavbarItems() != null && this.getNavbarItems().map((item, idx) => {
  196. // eslint-disable-next-line react/no-array-index-key
  197. return <li key={`navbarItem-${idx}`}>{item}</li>;
  198. }) }
  199. </ul>
  200. </div>
  201. );
  202. }
  203. getNavbarItems() {
  204. // set navbar items(react elements) here that are common in CodeMirrorEditor or TextAreaEditor
  205. const navbarItems = [];
  206. // concat common items and items specific to CodeMirrorEditor or TextAreaEditor
  207. return navbarItems.concat(this.getEditorSubstance().getNavbarItems());
  208. }
  209. renderCheatsheetModal() {
  210. const hideCheatsheetModal = () => {
  211. this.setState({ isCheatsheetModalShown: false });
  212. };
  213. return (
  214. <Modal isOpen={this.state.isCheatsheetModalShown} toggle={hideCheatsheetModal} className="modal-gfm-cheatsheet">
  215. <ModalHeader tag="h4" toggle={hideCheatsheetModal} className="bg-primary text-light">
  216. <i className="icon-fw icon-question" />Markdown help
  217. </ModalHeader>
  218. <ModalBody>
  219. <Cheatsheet />
  220. </ModalBody>
  221. </Modal>
  222. );
  223. }
  224. render() {
  225. const flexContainer = {
  226. height: '100%',
  227. display: 'flex',
  228. flexDirection: 'column',
  229. };
  230. const isMobile = this.props.isMobile;
  231. return (
  232. <div style={flexContainer} className="editor-container">
  233. <Dropzone
  234. ref={(c) => { this.dropzone = c }}
  235. accept={this.getAcceptableType()}
  236. noClick
  237. noKeyboard
  238. multiple={false}
  239. onDragLeave={this.dragLeaveHandler}
  240. onDrop={this.dropHandler}
  241. >
  242. {({
  243. getRootProps,
  244. getInputProps,
  245. isDragAccept,
  246. isDragReject,
  247. }) => {
  248. return (
  249. <div className={this.getDropzoneClassName(isDragAccept, isDragReject)} {...getRootProps()}>
  250. { this.state.dropzoneActive && this.renderDropzoneOverlay() }
  251. { this.state.isComponentDidMount && this.renderNavbar() }
  252. {/* for PC */}
  253. { !isMobile && (
  254. <Subscribe to={[EditorContainer]}>
  255. { editorContainer => (
  256. // eslint-disable-next-line arrow-body-style
  257. <CodeMirrorEditor
  258. ref={(c) => { this.cmEditor = c }}
  259. indentSize={editorContainer.state.indentSize}
  260. editorOptions={editorContainer.state.editorOptions}
  261. onPasteFiles={this.pasteFilesHandler}
  262. onDragEnter={this.dragEnterHandler}
  263. onMarkdownHelpButtonClicked={this.showMarkdownHelp}
  264. {...this.props}
  265. />
  266. )}
  267. </Subscribe>
  268. )}
  269. {/* for mobile */}
  270. { isMobile && (
  271. <TextAreaEditor
  272. ref={(c) => { this.taEditor = c }}
  273. onPasteFiles={this.pasteFilesHandler}
  274. onDragEnter={this.dragEnterHandler}
  275. {...this.props}
  276. />
  277. )}
  278. <input {...getInputProps()} />
  279. </div>
  280. );
  281. }}
  282. </Dropzone>
  283. { this.props.isUploadable
  284. && (
  285. <button
  286. type="button"
  287. className="btn btn-outline-secondary btn-block btn-open-dropzone"
  288. onClick={() => { this.dropzone.open() }}
  289. >
  290. <i className="icon-paper-clip" aria-hidden="true"></i>&nbsp;
  291. Attach files
  292. <span className="d-none d-sm-inline">
  293. &nbsp;by dragging &amp; dropping,&nbsp;
  294. <span className="btn-link">selecting them</span>,&nbsp;
  295. or pasting from the clipboard.
  296. </span>
  297. </button>
  298. )
  299. }
  300. { this.renderCheatsheetModal() }
  301. </div>
  302. );
  303. }
  304. }
  305. Editor.propTypes = Object.assign({
  306. noCdn: PropTypes.bool,
  307. isMobile: PropTypes.bool,
  308. isUploadable: PropTypes.bool,
  309. isUploadableFile: PropTypes.bool,
  310. emojiStrategy: PropTypes.object,
  311. onChange: PropTypes.func,
  312. onUpload: PropTypes.func,
  313. }, AbstractEditor.propTypes);