import React, { useState, useRef, useImperativeHandle, useCallback, useMemo, } from 'react'; import Dropzone from 'react-dropzone'; import { useTranslation } from 'react-i18next'; import { Modal, ModalHeader, ModalBody, } from 'reactstrap'; import { toastError } from '~/client/util/apiNotification'; import { useDefaultIndentSize } from '~/stores/context'; import { useEditorSettings } from '~/stores/editor'; import { useIsMobile } from '~/stores/ui'; import { IEditorMethods } from '../../interfaces/editor-methods'; import Cheatsheet from './Cheatsheet'; import CodeMirrorEditor from './CodeMirrorEditor'; import pasteHelper from './PasteHelper'; import TextAreaEditor from './TextAreaEditor'; type EditorPropsType = { value?: string, isGfmMode?: boolean, noCdn?: boolean, isUploadable?: boolean, isUploadableFile?: boolean, onChange?: () => void, onUpload?: (file) => void, indentSize?: number, onScrollCursorIntoView?: (line: number) => void, onSave?: () => Promise, onPasteFiles?: (event: Event) => void, onCtrlEnter?: (event: Event) => void, } type DropzoneRef = { open: () => void } const Editor = React.forwardRef((props: EditorPropsType, ref): JSX.Element => { const { onUpload, isUploadable, isUploadableFile, indentSize, isGfmMode = true, } = props; const [dropzoneActive, setDropzoneActive] = useState(false); const [isUploading, setIsUploading] = useState(false); const [isCheatsheetModalShown, setIsCheatsheetModalShown] = useState(false); const { t } = useTranslation(); const { data: editorSettings } = useEditorSettings(); const { data: defaultIndentSize } = useDefaultIndentSize(); const { data: isMobile } = useIsMobile(); const dropzoneRef = useRef(null); const cmEditorRef = useRef(null); const taEditorRef = useRef(null); const editorSubstance = isMobile ? taEditorRef.current : cmEditorRef.current; const methods: Partial = useMemo(() => { return { forceToFocus: () => { if (editorSubstance == null) { return } editorSubstance.forceToFocus(); }, setValue: (newValue: string) => { if (editorSubstance == null) { return } editorSubstance.setValue(newValue); }, setGfmMode: (bool: boolean) => { if (editorSubstance == null) { return } editorSubstance.setGfmMode(bool); }, setCaretLine: (line: number) => { if (editorSubstance == null) { return } editorSubstance.setCaretLine(line); }, setScrollTopByLine: (line: number) => { if (editorSubstance == null) { return } editorSubstance.setScrollTopByLine(line); }, insertText: (text: string) => { if (editorSubstance == null) { return } editorSubstance.insertText(text); }, getNavbarItems: (): JSX.Element[] => { if (editorSubstance == null) { return [] } // concat common items and items specific to CodeMirrorEditor or TextAreaEditor const navbarItems = editorSubstance.getNavbarItems() ?? []; return navbarItems; }, }; }, [editorSubstance]); // methods for ref useImperativeHandle(ref, () => ({ forceToFocus: methods.forceToFocus, setValue: methods.setValue, setGfmMode: methods.setGfmMode, setCaretLine: methods.setCaretLine, setScrollTopByLine: methods.setScrollTopByLine, insertText: methods.insertText, /** * remove overlay and set isUploading to false */ terminateUploadingState: () => { setDropzoneActive(false); setIsUploading(false); }, })); /** * dispatch onUpload event */ const dispatchUpload = useCallback((files) => { if (onUpload != null) { onUpload(files); } }, [onUpload]); /** * get acceptable(uploadable) file type */ const getAcceptableType = useCallback(() => { let accept = 'null'; // reject all if (isUploadable) { if (!isUploadableFile) { accept = 'image/*'; // image only } else { accept = ''; // allow all } } return accept; }, [isUploadable, isUploadableFile]); const pasteFilesHandler = useCallback((event) => { const items = event.clipboardData.items || event.clipboardData.files || []; toastError(t('toaster.file_upload_failed')); // abort if length is not 1 if (items.length < 1) { return; } for (let i = 0; i < items.length; i++) { try { const file = items[i].getAsFile(); // check file type (the same process as Dropzone) if (file != null && pasteHelper.isAcceptableType(file, getAcceptableType())) { dispatchUpload(file); setIsUploading(true); } } catch (e) { toastError(t('toaster.file_upload_failed')); } } }, [dispatchUpload, getAcceptableType, t]); const dragEnterHandler = useCallback((event) => { const dataTransfer = event.dataTransfer; // do nothing if contents is not files if (!dataTransfer.types.includes('Files')) { return; } setDropzoneActive(true); }, []); const dropHandler = useCallback((accepted) => { // rejected if (accepted.length !== 1) { // length should be 0 or 1 because `multiple={false}` is set setDropzoneActive(false); return; } const file = accepted[0]; dispatchUpload(file); setIsUploading(true); }, [dispatchUpload]); const addAttachmentHandler = useCallback(() => { if (dropzoneRef.current == null) { return } dropzoneRef.current.open(); }, []); const getDropzoneClassName = useCallback((isDragAccept: boolean, isDragReject: boolean) => { let className = 'dropzone'; if (!isUploadable) { className += ' dropzone-unuploadable'; } else { className += ' dropzone-uploadable'; if (isUploadableFile) { className += ' dropzone-uploadablefile'; } } // uploading if (isUploading) { className += ' dropzone-uploading'; } if (isDragAccept) { className += ' dropzone-accepted'; } if (isDragReject) { className += ' dropzone-rejected'; } return className; }, [isUploadable, isUploading, isUploadableFile]); const renderDropzoneOverlay = useCallback(() => { return (
{isUploading && (
Uploading...
) } {!isUploading && }
); }, [isUploading]); const renderNavbar = useCallback(() => { return (
    { methods.getNavbarItems?.().map((item, idx) => { // eslint-disable-next-line react/no-array-index-key return
  • {item}
  • ; }) }
); }, [methods]); const renderCheatsheetModal = useCallback(() => { const hideCheatsheetModal = () => { setIsCheatsheetModalShown(false); }; return ( Markdown help ); }, [isCheatsheetModalShown]); if (editorSettings == null) { return <>; } const flexContainer: React.CSSProperties = { height: '100%', display: 'flex', flexDirection: 'column', }; return ( <>
{ setDropzoneActive(false) }} onDrop={dropHandler} > {({ getRootProps, getInputProps, isDragAccept, isDragReject, }) => { return (
{ dropzoneActive && renderDropzoneOverlay() } { renderNavbar() } {/* for PC */} { !isMobile && ( // eslint-disable-next-line arrow-body-style { setIsCheatsheetModalShown(true) }} onAddAttachmentButtonClicked={addAttachmentHandler} editorSettings={editorSettings} isGfmMode={isGfmMode} {...props} /> )} {/* for mobile */} { isMobile && ( )}
); }}
{ isUploadable && ( ) } { renderCheatsheetModal() }
); }); Editor.displayName = 'Editor'; export default Editor;