2
0

Editor.jsx 9.3 KB

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