mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-28 16:17:02 +00:00
Optimize excalidraw code (#7942)
* optimize code * optimize code * optimize code * optimize code --------- Co-authored-by: 小强 <shuntian@Mac.lan>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { seafileAPI } from '../../utils/seafile-api';
|
import { seafileAPI } from '../../../utils/seafile-api';
|
||||||
import { Utils } from '../../utils/utils';
|
import { Utils } from '../../../utils/utils';
|
||||||
|
|
||||||
const { repoID, filePath, fileName } = window.app.pageOptions;
|
const { repoID, filePath, fileName } = window.app.pageOptions;
|
||||||
let dirPath = Utils.getDirName(filePath);
|
let dirPath = Utils.getDirName(filePath);
|
@@ -1,6 +1,6 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
class ExdrawServerApi {
|
class ExcalidrawServerApi {
|
||||||
|
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
this.server = options.exdrawServer;
|
this.server = options.exdrawServer;
|
||||||
@@ -25,4 +25,4 @@ class ExdrawServerApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ExdrawServerApi;
|
export default ExcalidrawServerApi;
|
@@ -17,3 +17,6 @@ export const langList = {
|
|||||||
export const STORAGE_KEYS = {
|
export const STORAGE_KEYS = {
|
||||||
IDB_LIBRARY: 'excalidraw-library',
|
IDB_LIBRARY: 'excalidraw-library',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DELETED_ELEMENT_TIMEOUT = 24 * 60 * 60 * 1000; // 1 day
|
||||||
|
|
||||||
|
32
frontend/src/pages/excalidraw-editor/context.js
Normal file
32
frontend/src/pages/excalidraw-editor/context.js
Normal file
@@ -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;
|
92
frontend/src/pages/excalidraw-editor/editor/index.js
Normal file
92
frontend/src/pages/excalidraw-editor/editor/index.js
Normal file
@@ -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 (
|
||||||
|
<div className='excali-container'>
|
||||||
|
<Loading />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='excali-container' style={{ height: '100%', width: '100%' }}>
|
||||||
|
<Excalidraw
|
||||||
|
initialData={sceneContent}
|
||||||
|
excalidrawAPI={(api) => setExcalidrawAPI(api)}
|
||||||
|
onChange={handleChange}
|
||||||
|
UIOptions={UIOptions}
|
||||||
|
langCode={langList[window.app.config.lang] || 'en'}
|
||||||
|
>
|
||||||
|
<MainMenu>
|
||||||
|
<MainMenu.DefaultItems.Export />
|
||||||
|
<MainMenu.DefaultItems.SaveAsImage />
|
||||||
|
<MainMenu.DefaultItems.Help />
|
||||||
|
<MainMenu.DefaultItems.ClearCanvas />
|
||||||
|
<MainMenu.DefaultItems.ToggleTheme />
|
||||||
|
<MainMenu.DefaultItems.ChangeCanvasBackground />
|
||||||
|
</MainMenu>
|
||||||
|
</Excalidraw>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SimpleEditor;
|
@@ -1,9 +1,5 @@
|
|||||||
import {
|
import { createStore, get, set } from 'idb-keyval';
|
||||||
createStore,
|
import { STORAGE_KEYS } from '../constants';
|
||||||
get,
|
|
||||||
set
|
|
||||||
} from 'idb-keyval';
|
|
||||||
import { STORAGE_KEYS } from './constants';
|
|
||||||
|
|
||||||
export class LibraryIndexedDBAdapter {
|
export class LibraryIndexedDBAdapter {
|
||||||
/** IndexedDB database and store name */
|
/** IndexedDB database and store name */
|
@@ -1,117 +1,85 @@
|
|||||||
import React, { useCallback, useState, useEffect, useRef } from 'react';
|
import React, { useCallback, useState, useEffect, useRef } from 'react';
|
||||||
import SimpleEditor from './simple-editor';
|
import SimpleEditor from './editor';
|
||||||
import editorApi from './editor-api';
|
|
||||||
import isHotkey from 'is-hotkey';
|
|
||||||
import { gettext } from '../../utils/constants';
|
import { gettext } from '../../utils/constants';
|
||||||
import toaster from '../../components/toast';
|
import toaster from '../../components/toast';
|
||||||
import { SAVE_INTERVAL_TIME } from './constants';
|
import { SAVE_INTERVAL_TIME } from './constants';
|
||||||
import { Utils } from '../../utils/utils';
|
import { updateAppIcon } from './utils/common-utils';
|
||||||
import ExdrawServerApi from './collab/exdraw-server-api';
|
import context from './context';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
const { docUuid, excalidrawServerUrl } = window.app.pageOptions;
|
|
||||||
window.name = `${docUuid}`;
|
|
||||||
|
|
||||||
const ExcaliEditor = () => {
|
const ExcaliEditor = () => {
|
||||||
const [fileContent, setFileContent] = useState(null);
|
|
||||||
const editorRef = useRef(null);
|
const editorRef = useRef(null);
|
||||||
const isChangedRef = useRef(false);
|
const isChangedRef = useRef(false);
|
||||||
const [isFetching, setIsFetching] = useState(true);
|
const [isFetching, setIsFetching] = useState(true);
|
||||||
const exdrawServerConfigRef = useRef({
|
const [fileContent, setFileContent] = useState(null);
|
||||||
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]);
|
|
||||||
|
|
||||||
|
// saved file interval
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const saveInterval = setInterval(() => {
|
const saveInterval = setInterval(() => {
|
||||||
if (isChangedRef.current) {
|
if (isChangedRef.current) {
|
||||||
editorApi.saveContent(JSON.stringify(editorRef.current)).then(res => {
|
context.saveSceneContent(JSON.stringify(editorRef.current)).then(res => {
|
||||||
isChangedRef.current = false;
|
isChangedRef.current = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, SAVE_INTERVAL_TIME);
|
}, SAVE_INTERVAL_TIME);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(saveInterval);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// tips before refresh current page
|
||||||
|
useEffect(() => {
|
||||||
const handleBeforeUnload = (event) => {
|
const handleBeforeUnload = (event) => {
|
||||||
if (isChangedRef.current) {
|
if (isChangedRef.current) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.returnValue = '';
|
event.returnValue = '';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(saveInterval);
|
|
||||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||||
};
|
};
|
||||||
}, [saveSceneContent]);
|
}, []);
|
||||||
|
|
||||||
const onSaveContent = useCallback(() => {
|
useEffect(() => {
|
||||||
saveSceneContent();
|
updateAppIcon();
|
||||||
}, [saveSceneContent]);
|
}, []);
|
||||||
|
|
||||||
|
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) => {
|
const onChangeContent = useCallback((elements) => {
|
||||||
editorRef.current = { elements };
|
editorRef.current = { elements };
|
||||||
isChangedRef.current = true;
|
isChangedRef.current = true;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onSetFavicon = useCallback(() => {
|
|
||||||
const { docName } = window.app.pageOptions;
|
|
||||||
const fileIcon = Utils.getFileIconUrl(docName);
|
|
||||||
document.getElementById('favicon').href = fileIcon;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="file-view-content flex-1 p-0 border-0">
|
<div className="file-view-content flex-1 p-0 border-0">
|
||||||
<SimpleEditor
|
<SimpleEditor
|
||||||
isFetching={isFetching}
|
isFetching={isFetching}
|
||||||
sceneContent={fileContent}
|
sceneContent={fileContent}
|
||||||
onSaveContent={onSaveContent}
|
onSaveContent={saveSceneContent}
|
||||||
onChangeContent={onChangeContent}
|
onChangeContent={onChangeContent}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -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 (
|
|
||||||
<div className='excali-container'>
|
|
||||||
<CodeMirrorLoading />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className='excali-container' style={{ height: '100%', width: '100%' }}>
|
|
||||||
<Excalidraw
|
|
||||||
initialData={sceneContent}
|
|
||||||
excalidrawAPI={(api) => setExcalidrawAPI(api)}
|
|
||||||
onChange={handleChange}
|
|
||||||
UIOptions={UIOptions}
|
|
||||||
langCode={langList[window.app.config.lang] || 'en'}
|
|
||||||
>
|
|
||||||
<MainMenu>
|
|
||||||
<MainMenu.DefaultItems.Export />
|
|
||||||
<MainMenu.DefaultItems.SaveAsImage />
|
|
||||||
<MainMenu.DefaultItems.Help />
|
|
||||||
<MainMenu.DefaultItems.ClearCanvas />
|
|
||||||
<MainMenu.DefaultItems.ToggleTheme />
|
|
||||||
<MainMenu.DefaultItems.ChangeCanvasBackground />
|
|
||||||
</MainMenu>
|
|
||||||
</Excalidraw>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SimpleEditor;
|
|
@@ -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;
|
||||||
|
};
|
Reference in New Issue
Block a user