import { IEmojiData } from "emoji-picker-react"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { deleteMemoResource, upsertMemoResource } from "../helpers/api"; import { UNKNOWN_ID } from "../helpers/consts"; import { editorStateService, locationService, memoService, resourceService } from "../services"; import { useAppSelector } from "../store"; import * as storage from "../helpers/storage"; import Icon from "./Icon"; import toastHelper from "./Toast"; import Editor, { EditorRefActions } from "./Editor/Editor"; import EmojiPicker from "./Editor/EmojiPicker"; import "../less/memo-editor.less"; const getEditorContentCache = (): string => { return storage.get(["editorContentCache"]).editorContentCache ?? ""; }; const setEditorContentCache = (content: string) => { storage.set({ editorContentCache: content, }); }; interface State { fullscreen: boolean; isUploadingResource: boolean; shouldShowEmojiPicker: boolean; resourceList: Resource[]; } const MemoEditor: React.FC = () => { const { t, i18n } = useTranslation(); const user = useAppSelector((state) => state.user.user); const editorState = useAppSelector((state) => state.editor); const tags = useAppSelector((state) => state.memo.tags); const [state, setState] = useState({ isUploadingResource: false, fullscreen: false, shouldShowEmojiPicker: false, resourceList: [], }); const [allowSave, setAllowSave] = useState(false); const prevGlobalStateRef = useRef(editorState); const editorRef = useRef(null); const tagSeletorRef = useRef(null); const editorFontStyle = user?.setting.editorFontStyle || "normal"; const mobileEditorStyle = user?.setting.mobileEditorStyle || "normal"; useEffect(() => { if (editorState.markMemoId && editorState.markMemoId !== UNKNOWN_ID) { const editorCurrentValue = editorRef.current?.getContent(); const memoLinkText = `${editorCurrentValue ? "\n" : ""}Mark: @[MEMO](${editorState.markMemoId})`; editorRef.current?.insertText(memoLinkText); editorStateService.clearMarkMemo(); } }, [editorState.markMemoId]); useEffect(() => { if ( editorState.editMemoId && editorState.editMemoId !== UNKNOWN_ID && editorState.editMemoId !== prevGlobalStateRef.current.editMemoId ) { const memo = memoService.getMemoById(editorState.editMemoId ?? UNKNOWN_ID); if (memo) { setState({ ...state, resourceList: memo.resourceList, }); editorRef.current?.setContent(memo.content ?? ""); editorRef.current?.focus(); } } prevGlobalStateRef.current = editorState; }, [state, editorState.editMemoId]); const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === "Escape") { if (state.fullscreen) { handleFullscreenBtnClick(); } } else if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) { handleSaveBtnClick(); } }; const handleDropEvent = async (event: React.DragEvent) => { if (event.dataTransfer && event.dataTransfer.files.length > 0) { event.preventDefault(); const resourceList: Resource[] = []; for (const file of event.dataTransfer.files) { const resource = await handleUploadResource(file); if (resource) { resourceList.push(resource); if (editorState.editMemoId) { await upsertMemoResource(editorState.editMemoId, resource.id); } } } setState({ ...state, resourceList: [...state.resourceList, ...resourceList], }); } }; const handlePasteEvent = async (event: React.ClipboardEvent) => { if (event.clipboardData && event.clipboardData.files.length > 0) { event.preventDefault(); const file = event.clipboardData.files[0]; const resource = await handleUploadResource(file); if (resource) { setState({ ...state, resourceList: [...state.resourceList, resource], }); } } }; const handleUploadResource = async (file: File) => { setState({ ...state, isUploadingResource: true, }); let resource = undefined; try { resource = await resourceService.upload(file); } catch (error: any) { console.error(error); toastHelper.error(error.response.data.message); } setState({ ...state, isUploadingResource: false, }); return resource; }; const handleSaveBtnClick = async () => { const content = editorRef.current?.getContent(); if (!content) { toastHelper.error(t("editor.cant-empty")); return; } try { const { editMemoId } = editorStateService.getState(); if (editMemoId && editMemoId !== UNKNOWN_ID) { const prevMemo = memoService.getMemoById(editMemoId ?? UNKNOWN_ID); if (prevMemo) { await memoService.patchMemo({ id: prevMemo.id, content, }); } editorStateService.clearEditMemo(); } else { await memoService.createMemo({ content, resourceIdList: state.resourceList.map((resource) => resource.id), }); locationService.clearQuery(); } } catch (error: any) { console.error(error); toastHelper.error(error.response.data.message); } setState({ ...state, fullscreen: false, resourceList: [], }); setEditorContentCache(""); editorRef.current?.setContent(""); }; const handleCancelEditing = () => { setState({ ...state, resourceList: [], }); editorStateService.clearEditMemo(); editorRef.current?.setContent(""); setEditorContentCache(""); }; const handleContentChange = (content: string) => { setAllowSave(content !== ""); setEditorContentCache(content); }; const handleEmojiPickerBtnClick = () => { handleChangeShouldShowEmojiPicker(!state.shouldShowEmojiPicker); }; const handleCheckBoxBtnClick = () => { if (!editorRef.current) { return; } const cursorPosition = editorRef.current.getCursorPosition(); const prevValue = editorRef.current.getContent().slice(0, cursorPosition); if (prevValue === "" || prevValue.endsWith("\n")) { editorRef.current?.insertText("- [ ] "); } else { editorRef.current?.insertText("\n- [ ] "); } }; const handleCodeBlockBtnClick = () => { if (!editorRef.current) { return; } const cursorPosition = editorRef.current.getCursorPosition(); const prevValue = editorRef.current.getContent().slice(0, cursorPosition); if (prevValue === "" || prevValue.endsWith("\n")) { editorRef.current?.insertText("```\n\n```"); } else { editorRef.current?.insertText("\n```\n\n```"); } }; const handleUploadFileBtnClick = () => { const inputEl = document.createElement("input"); inputEl.style.position = "fixed"; inputEl.style.top = "-100vh"; inputEl.style.left = "-100vw"; document.body.appendChild(inputEl); inputEl.type = "file"; inputEl.multiple = true; inputEl.accept = "*"; inputEl.onchange = async () => { if (!inputEl.files || inputEl.files.length === 0) { return; } const resourceList: Resource[] = []; for (const file of inputEl.files) { const resource = await handleUploadResource(file); if (resource) { resourceList.push(resource); if (editorState.editMemoId) { await upsertMemoResource(editorState.editMemoId, resource.id); } } } setState({ ...state, resourceList: [...state.resourceList, ...resourceList], }); document.body.removeChild(inputEl); }; inputEl.click(); }; const handleFullscreenBtnClick = () => { setState({ ...state, fullscreen: !state.fullscreen, }); }; const handleTagSeletorClick = useCallback((event: React.MouseEvent) => { if (tagSeletorRef.current !== event.target && tagSeletorRef.current?.contains(event.target as Node)) { editorRef.current?.insertText(`#${(event.target as HTMLElement).textContent} ` ?? ""); editorRef.current?.focus(); } }, []); const handleChangeShouldShowEmojiPicker = (status: boolean) => { setState({ ...state, shouldShowEmojiPicker: status, }); }; const handleEmojiClick = (_: any, emojiObject: IEmojiData) => { if (!editorRef.current) { return; } editorRef.current.insertText(`${emojiObject.emoji}`); handleChangeShouldShowEmojiPicker(false); }; const handleDeleteResource = async (resourceId: ResourceId) => { setState({ ...state, resourceList: state.resourceList.filter((resource) => resource.id !== resourceId), }); if (editorState.editMemoId) { await deleteMemoResource(editorState.editMemoId, resourceId); } }; const isEditing = Boolean(editorState.editMemoId && editorState.editMemoId !== UNKNOWN_ID); const editorConfig = useMemo( () => ({ className: `memo-editor ${editorFontStyle}`, initialContent: getEditorContentCache(), placeholder: t("editor.placeholder"), fullscreen: state.fullscreen, onContentChange: handleContentChange, }), [state.fullscreen, i18n.language, editorFontStyle] ); return (
{t("editor.editing")}
{tags.length > 0 ? ( tags.map((tag) => { return ( {tag} ); }) ) : (

e.stopPropagation()}> {t("common.null")}

)}
{state.resourceList.length > 0 && (
{state.resourceList.map((resource) => { return (
{resource.type.includes("image") ? : } {resource.filename} handleDeleteResource(resource.id)} />
); })}
)}
); }; export default MemoEditor;