Editor.jsx 8.4 KB

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