diff --git a/frontend/src/pages/excalidraw-editor/editor-api.js b/frontend/src/pages/excalidraw-editor/api/editor-api.js similarity index 88% rename from frontend/src/pages/excalidraw-editor/editor-api.js rename to frontend/src/pages/excalidraw-editor/api/editor-api.js index fb92916139..c17e0239e4 100644 --- a/frontend/src/pages/excalidraw-editor/editor-api.js +++ b/frontend/src/pages/excalidraw-editor/api/editor-api.js @@ -1,5 +1,5 @@ -import { seafileAPI } from '../../utils/seafile-api'; -import { Utils } from '../../utils/utils'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { Utils } from '../../../utils/utils'; const { repoID, filePath, fileName } = window.app.pageOptions; let dirPath = Utils.getDirName(filePath); diff --git a/frontend/src/pages/excalidraw-editor/collab/exdraw-server-api.js b/frontend/src/pages/excalidraw-editor/api/index.js similarity index 91% rename from frontend/src/pages/excalidraw-editor/collab/exdraw-server-api.js rename to frontend/src/pages/excalidraw-editor/api/index.js index d01269a55e..b1633bd6f5 100644 --- a/frontend/src/pages/excalidraw-editor/collab/exdraw-server-api.js +++ b/frontend/src/pages/excalidraw-editor/api/index.js @@ -1,6 +1,6 @@ import axios from 'axios'; -class ExdrawServerApi { +class ExcalidrawServerApi { constructor(options) { this.server = options.exdrawServer; @@ -25,4 +25,4 @@ class ExdrawServerApi { } } -export default ExdrawServerApi; +export default ExcalidrawServerApi; diff --git a/frontend/src/pages/excalidraw-editor/constants.js b/frontend/src/pages/excalidraw-editor/constants.js index 2aab0e6017..2e3b506e35 100644 --- a/frontend/src/pages/excalidraw-editor/constants.js +++ b/frontend/src/pages/excalidraw-editor/constants.js @@ -17,3 +17,6 @@ export const langList = { export const STORAGE_KEYS = { IDB_LIBRARY: 'excalidraw-library', }; + +export const DELETED_ELEMENT_TIMEOUT = 24 * 60 * 60 * 1000; // 1 day + diff --git a/frontend/src/pages/excalidraw-editor/context.js b/frontend/src/pages/excalidraw-editor/context.js new file mode 100644 index 0000000000..86fedb1e21 --- /dev/null +++ b/frontend/src/pages/excalidraw-editor/context.js @@ -0,0 +1,32 @@ +import ExcalidrawServerApi from './api'; +import editorApi from './api/editor-api'; + +const { docUuid, excalidrawServerUrl } = window.app.pageOptions; + +class Context { + constructor() { + this.docUuid = ''; + this.exdrawServer = ''; + } + + initSettings = async () => { + this.docUuid = docUuid; + this.exdrawServer = excalidrawServerUrl; + const resResult = await editorApi.getExdrawToken(); + const accessToken = resResult; + this.exdrawApi = new ExcalidrawServerApi({ exdrawUuid: docUuid, exdrawServer: excalidrawServerUrl, accessToken }); + }; + + getSceneContent = () => { + return this.exdrawApi.getSceneContent(); + }; + + saveSceneContent = (content) => { + return this.exdrawApi.saveSceneContent(content); + }; + +} + +const context = new Context(); + +export default context; diff --git a/frontend/src/pages/excalidraw-editor/editor/index.js b/frontend/src/pages/excalidraw-editor/editor/index.js new file mode 100644 index 0000000000..576596deb8 --- /dev/null +++ b/frontend/src/pages/excalidraw-editor/editor/index.js @@ -0,0 +1,92 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Excalidraw, MainMenu, useHandleLibrary } from '@excalidraw/excalidraw'; +import isHotkey from 'is-hotkey'; +import Loading from '../../../components/loading'; +import { langList } from '../constants'; +import { LibraryIndexedDBAdapter } from './library-adapter'; + +import '@excalidraw/excalidraw/index.css'; + +const { docUuid } = window.app.pageOptions; +window.name = `${docUuid}`; + +const UIOptions = { + canvasActions: { + saveToActiveFile: false, + LoadScene: false + }, + tools: { image: false }, +}; + +const hasChanged = (elements, oldElements) => { + if (elements.length !== oldElements.length) return true; + + return elements.some((element, index) => { + return element.version !== oldElements[index]?.version; + }); +}; + +const SimpleEditor = ({ sceneContent = null, onChangeContent, onSaveContent, isFetching }) => { + const [excalidrawAPI, setExcalidrawAPI] = useState(null); + const prevElementsRef = useRef(); + + useHandleLibrary({ excalidrawAPI, adapter: LibraryIndexedDBAdapter }); + + const handleChange = () => { + if (!prevElementsRef.current) { + prevElementsRef.current = sceneContent?.elements || []; + } + + const elements = excalidrawAPI.getSceneElements(); + if (hasChanged(elements, prevElementsRef.current)) { + onChangeContent(elements); + prevElementsRef.current = elements; + } + + }; + + useEffect(() => { + const handleHotkeySave = (event) => { + if (isHotkey('mod+s', event)) { + event.preventDefault(); + onSaveContent(excalidrawAPI.getSceneElements()); + } + }; + + document.addEventListener('keydown', handleHotkeySave, true); + return () => { + document.removeEventListener('keydown', handleHotkeySave, true); + }; + }, [excalidrawAPI, onSaveContent]); + + if (isFetching) { + return ( +
+ +
+ ); + } + + return ( +
+ setExcalidrawAPI(api)} + onChange={handleChange} + UIOptions={UIOptions} + langCode={langList[window.app.config.lang] || 'en'} + > + + + + + + + + + +
+ ); +}; + +export default SimpleEditor; diff --git a/frontend/src/pages/excalidraw-editor/library-adapter.js b/frontend/src/pages/excalidraw-editor/editor/library-adapter.js similarity index 85% rename from frontend/src/pages/excalidraw-editor/library-adapter.js rename to frontend/src/pages/excalidraw-editor/editor/library-adapter.js index a915b17e78..31baa7d69e 100644 --- a/frontend/src/pages/excalidraw-editor/library-adapter.js +++ b/frontend/src/pages/excalidraw-editor/editor/library-adapter.js @@ -1,9 +1,5 @@ -import { - createStore, - get, - set -} from 'idb-keyval'; -import { STORAGE_KEYS } from './constants'; +import { createStore, get, set } from 'idb-keyval'; +import { STORAGE_KEYS } from '../constants'; export class LibraryIndexedDBAdapter { /** IndexedDB database and store name */ diff --git a/frontend/src/pages/excalidraw-editor/index.js b/frontend/src/pages/excalidraw-editor/index.js index 7a6e0cd267..0be3e4c7af 100644 --- a/frontend/src/pages/excalidraw-editor/index.js +++ b/frontend/src/pages/excalidraw-editor/index.js @@ -1,117 +1,85 @@ import React, { useCallback, useState, useEffect, useRef } from 'react'; -import SimpleEditor from './simple-editor'; -import editorApi from './editor-api'; -import isHotkey from 'is-hotkey'; +import SimpleEditor from './editor'; import { gettext } from '../../utils/constants'; import toaster from '../../components/toast'; import { SAVE_INTERVAL_TIME } from './constants'; -import { Utils } from '../../utils/utils'; -import ExdrawServerApi from './collab/exdraw-server-api'; +import { updateAppIcon } from './utils/common-utils'; +import context from './context'; import './index.css'; -const { docUuid, excalidrawServerUrl } = window.app.pageOptions; -window.name = `${docUuid}`; - const ExcaliEditor = () => { - const [fileContent, setFileContent] = useState(null); const editorRef = useRef(null); const isChangedRef = useRef(false); const [isFetching, setIsFetching] = useState(true); - const exdrawServerConfigRef = useRef({ - exdrawServer: '', - exdrawUuid: '', - accessToken: '' - }); - - useEffect(() => { - editorApi.getExdrawToken().then(res => { - exdrawServerConfigRef.current = { - exdrawServer: excalidrawServerUrl, - exdrawUuid: docUuid, - accessToken: res - }; - const exdrawServerApi = new ExdrawServerApi(exdrawServerConfigRef.current); - exdrawServerApi.getSceneContent().then(res => { - setFileContent(res.data); - setIsFetching(false); - }); - }); - onSetFavicon(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const saveSceneContent = useCallback(async () => { - if (isChangedRef.current) { - try { - const exdrawServerApi = new ExdrawServerApi(exdrawServerConfigRef.current); - await exdrawServerApi.saveSceneContent(JSON.stringify(editorRef.current)); - isChangedRef.current = false; - toaster.success(gettext('Successfully saved'), { duration: 2 }); - } catch { - toaster.danger(gettext('Failed to save'), { duration: 2 }); - } - } - }, []); - - useEffect(() => { - const handleHotkeySave = (event) => { - if (isHotkey('mod+s', event)) { - event.preventDefault(); - } - }; - - document.addEventListener('keydown', handleHotkeySave, true); - return () => { - document.removeEventListener('keydown', handleHotkeySave, true); - }; - }, [saveSceneContent]); + const [fileContent, setFileContent] = useState(null); + // saved file interval useEffect(() => { const saveInterval = setInterval(() => { if (isChangedRef.current) { - editorApi.saveContent(JSON.stringify(editorRef.current)).then(res => { + context.saveSceneContent(JSON.stringify(editorRef.current)).then(res => { isChangedRef.current = false; }); } }, SAVE_INTERVAL_TIME); + return () => { + clearInterval(saveInterval); + }; + }, []); + + // tips before refresh current page + useEffect(() => { const handleBeforeUnload = (event) => { if (isChangedRef.current) { event.preventDefault(); event.returnValue = ''; } }; - window.addEventListener('beforeunload', handleBeforeUnload); - return () => { - clearInterval(saveInterval); window.removeEventListener('beforeunload', handleBeforeUnload); }; - }, [saveSceneContent]); + }, []); - const onSaveContent = useCallback(() => { - saveSceneContent(); - }, [saveSceneContent]); + useEffect(() => { + updateAppIcon(); + }, []); + + useEffect(() => { + async function loadFileContent() { + await context.initSettings(); + context.getSceneContent().then(res => { + setFileContent(res.data); + setIsFetching(false); + }); + } + loadFileContent(); + }, []); + + const saveSceneContent = useCallback(async () => { + if (isChangedRef.current) { + context.saveSceneContent(JSON.stringify(editorRef.current)).then(res => { + isChangedRef.current = false; + toaster.success(gettext('Successfully saved'), { duration: 2 }); + }).catch(error => { + toaster.danger(gettext('Failed to save'), { duration: 2 }); + }); + } + }, []); const onChangeContent = useCallback((elements) => { editorRef.current = { elements }; isChangedRef.current = true; }, []); - const onSetFavicon = useCallback(() => { - const { docName } = window.app.pageOptions; - const fileIcon = Utils.getFileIconUrl(docName); - document.getElementById('favicon').href = fileIcon; - }, []); - return (
diff --git a/frontend/src/pages/excalidraw-editor/simple-editor.js b/frontend/src/pages/excalidraw-editor/simple-editor.js deleted file mode 100644 index 4e109089dc..0000000000 --- a/frontend/src/pages/excalidraw-editor/simple-editor.js +++ /dev/null @@ -1,93 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { Excalidraw, MainMenu, useHandleLibrary } from '@excalidraw/excalidraw'; -import isHotkey from 'is-hotkey'; -import CodeMirrorLoading from '../../components/code-mirror-loading'; -import { langList } from './constants'; -import { LibraryIndexedDBAdapter } from './library-adapter'; - -import '@excalidraw/excalidraw/index.css'; - -const SimpleEditor = ({ - sceneContent = null, - onChangeContent, - onSaveContent, - isFetching -}) => { - const [excalidrawAPI, setExcalidrawAPI] = useState(null); - const prevElementsRef = useRef([]); - const UIOptions = { - canvasActions: { - saveToActiveFile: false, - LoadScene: false - }, - tools: { image: false }, - }; - - useHandleLibrary({ - excalidrawAPI, - adapter: LibraryIndexedDBAdapter - }); - - const handleChange = () => { - const elements = excalidrawAPI.getSceneElements(); - if (hasChanged(elements, prevElementsRef.current)) { - onChangeContent(elements); - } - prevElementsRef.current = elements; - }; - - useEffect(() => { - const handleHotkeySave = (event) => { - if (isHotkey('mod+s', event)) { - event.preventDefault(); - onSaveContent(excalidrawAPI.getSceneElements()); - } - }; - - document.addEventListener('keydown', handleHotkeySave, true); - return () => { - document.removeEventListener('keydown', handleHotkeySave, true); - }; - }, [excalidrawAPI, onSaveContent]); - - const hasChanged = (prev, current) => { - if (prev.length !== current.length) return true; - - return current.some((element, index) => { - return element.version !== prev[index]?.version; - }); - }; - - if (isFetching) { - return ( -
- -
- ); - } - - return ( - <> -
- setExcalidrawAPI(api)} - onChange={handleChange} - UIOptions={UIOptions} - langCode={langList[window.app.config.lang] || 'en'} - > - - - - - - - - - -
- - ); -}; - -export default SimpleEditor; diff --git a/frontend/src/pages/excalidraw-editor/utils/common-utils.js b/frontend/src/pages/excalidraw-editor/utils/common-utils.js new file mode 100644 index 0000000000..3d190a01a4 --- /dev/null +++ b/frontend/src/pages/excalidraw-editor/utils/common-utils.js @@ -0,0 +1,7 @@ +import { Utils } from '../../../utils/utils'; + +export const updateAppIcon = () => { + const { docName } = window.app.pageOptions; + const fileIcon = Utils.getFileIconUrl(docName); + document.getElementById('favicon').href = fileIcon; +};