1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-08-18 15:08:22 +00:00

02 普通文本编辑器

This commit is contained in:
Michael An 2025-03-04 12:00:40 +08:00 committed by Michael An
parent 98f7ca81e7
commit 2684e54aed
4 changed files with 61 additions and 9 deletions

View File

@ -78,19 +78,17 @@ https://github.com/haiwen/seahub/pull/7273
│   ├── editor-api.js 处理文件下载和上传 │   ├── editor-api.js 处理文件下载和上传
│   └── index.js 画板编辑器的套壳,处理快捷键,保存内容等 │   └── index.js 画板编辑器的套壳,处理快捷键,保存内容等
## 02 markdown 普通字符编辑器(刘宏博 2024 重构)
—————————————————————————————————————————————————没有查看—————————————————————————————————————————————————
## markdown 普通字符编辑器(刘宏博 2024
https://github.com/haiwen/seahub/pull/5998 https://github.com/haiwen/seahub/pull/5998
├── plain-markdown-editor ├── plain-markdown-editor
│   ├── code-mirror.css │   ├── code-mirror.js codemirror 代码阅读器定制后效果
│   ├── code-mirror.js │   ├── helper.js 获取文件信息的 API 封装后的函数
│   ├── helper.js │   ├── index.js 普通文本编辑器入口,左侧是格式化编辑代码,右侧显示预览
│   ├── index.js
│   └── style.css
—————————————————————————————————————————————————没有查看—————————————————————————————————————————————————
## markdown 富文本编辑器 ## markdown 富文本编辑器

View File

@ -20,14 +20,20 @@ class SeafileCodeMirror extends React.Component {
componentDidMount() { componentDidMount() {
const { initialValue, autoFocus } = this.props; const { initialValue, autoFocus } = this.props;
// 初始化编辑器
this.view = new EditorView({ this.view = new EditorView({
doc: initialValue, doc: initialValue,
extensions: [ extensions: [
// basicSetup: 一个基本的编辑器配置,包括了光标、选择、滚动条等基本功能
basicSetup, basicSetup,
// markdown: markdown 语言的解析器
// languages: 一个对象key 是语言的名称value 是语言对应的解析器
markdown({ codeLanguages: languages }), markdown({ codeLanguages: languages }),
// EditorView.updateListener: 一个监听器,每当编辑器的状态更新时,会被调用
EditorView.updateListener.of((viewUpdate) => { EditorView.updateListener.of((viewUpdate) => {
this.onValueChanged(viewUpdate); this.onValueChanged(viewUpdate);
}), }),
// EditorView.lineWrapping: 使得编辑器支持自动换行
EditorView.lineWrapping EditorView.lineWrapping
], ],
parent: this.codeMirrorRef, parent: this.codeMirrorRef,
@ -42,7 +48,9 @@ class SeafileCodeMirror extends React.Component {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
// 父节点重新渲染时,初始化变化,重新渲染编辑器
if (!prevProps.initialValue && prevProps.initialValue !== this.props.initialValue) { if (!prevProps.initialValue && prevProps.initialValue !== this.props.initialValue) {
// 用新的值替换全部旧的值
this.view.dispatch({ this.view.dispatch({
changes: { changes: {
from: 0, from: 0,
@ -53,6 +61,7 @@ class SeafileCodeMirror extends React.Component {
} }
} }
// 双向的值的变化
onValueChanged = (viewUpdate) => { onValueChanged = (viewUpdate) => {
const { onChange } = this.props; const { onChange } = this.props;
if (onChange && viewUpdate.docChanged) { if (onChange && viewUpdate.docChanged) {

View File

@ -1,18 +1,21 @@
import { seafileAPI } from '../../utils/seafile-api'; import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils'; import { Utils } from '../../utils/utils';
// API 获取文档信息
const getFileInfo = async (repoID, filePath) => { const getFileInfo = async (repoID, filePath) => {
const fileInfoRes = await seafileAPI.getFileInfo(repoID, filePath); const fileInfoRes = await seafileAPI.getFileInfo(repoID, filePath);
const { mtime, size, starred, permission, last_modifier_name: lastModifier, id } = fileInfoRes.data; const { mtime, size, starred, permission, last_modifier_name: lastModifier, id } = fileInfoRes.data;
return { mtime, size, starred, permission, lastModifier, id }; return { mtime, size, starred, permission, lastModifier, id };
}; };
// API 获取下载链接
const getFileDownloadUrl = async (repoID, filePath) => { const getFileDownloadUrl = async (repoID, filePath) => {
const fileDownloadUrlRes = await seafileAPI.getFileDownloadLink(repoID, filePath); const fileDownloadUrlRes = await seafileAPI.getFileDownloadLink(repoID, filePath);
const downloadUrl = fileDownloadUrlRes.data; const downloadUrl = fileDownloadUrlRes.data;
return downloadUrl; return downloadUrl;
}; };
// API 获取权限
const setPermission = async (permission, repoID) => { const setPermission = async (permission, repoID) => {
let hasPermission = permission === 'rw' || permission === 'cloud-edit'; let hasPermission = permission === 'rw' || permission === 'cloud-edit';
// get custom permission // get custom permission
@ -26,12 +29,14 @@ const setPermission = async (permission, repoID) => {
return hasPermission; return hasPermission;
}; };
// API 获取文档内容
const setFileContent = async (downloadUrl) => { const setFileContent = async (downloadUrl) => {
const fileContentRes = await seafileAPI.getFileContent(downloadUrl); const fileContentRes = await seafileAPI.getFileContent(downloadUrl);
const markdownContent = fileContentRes.data; const markdownContent = fileContentRes.data;
return markdownContent; return markdownContent;
}; };
// 获取全部信息入口
export const getPlainOptions = async ({ fileName, filePath, repoID }) => { export const getPlainOptions = async ({ fileName, filePath, repoID }) => {
const fileIcon = Utils.getFileIconUrl(fileName); const fileIcon = Utils.getFileIconUrl(fileName);
document.getElementById('favicon').href = fileIcon; document.getElementById('favicon').href = fileIcon;
@ -40,5 +45,15 @@ export const getPlainOptions = async ({ fileName, filePath, repoID }) => {
const markdownContent = await setFileContent(downloadUrl); const markdownContent = await setFileContent(downloadUrl);
const hasPermission = await setPermission(fileInfo.permission, repoID); const hasPermission = await setPermission(fileInfo.permission, repoID);
// 早期是多个 await 串行执行,可能消耗较多时间;可以改成多个 API 同时执行,减少网络请求的时间
// const [fileInfo, downloadUrl] = await Promise.all([
// getFileInfo(repoID, filePath),
// getFileDownloadUrl(repoID, filePath),
// ]);
// const [markdownContent, hasPermission] = await Promise.all([
// setFileContent(downloadUrl),
// setPermission(fileInfo.permission, repoID),
// ]);
return { markdownContent, hasPermission, fileInfo }; return { markdownContent, hasPermission, fileInfo };
}; };

View File

@ -56,16 +56,29 @@ const initOptions = {
}; };
const PlainMarkdownEditor = (props) => { const PlainMarkdownEditor = (props) => {
// 编辑器的值
const [editorValue, setEditorValue] = useState(''); const [editorValue, setEditorValue] = useState('');
// 预览组件的值
const [previewValue, setPreviewValue] = useState(''); const [previewValue, setPreviewValue] = useState('');
// 判断鼠标在左侧还是在右侧面板内(决定其他快捷键交互等)
const [isMouseInLeftSide, setIsMouseInLeftSide] = useState(false); const [isMouseInLeftSide, setIsMouseInLeftSide] = useState(false);
const [isMouseInRightSide, setIsMouseInRightSide] = useState(false); const [isMouseInRightSide, setIsMouseInRightSide] = useState(false);
// 滚动位置(左右两个面板同步滚动)
const [scrollPercentage, setScrollPercentage] = useState(0); const [scrollPercentage, setScrollPercentage] = useState(0);
// 获取左右两个滚动面板的 DOM进一步获取位置信息
const leftPanelRef = useRef(null); const leftPanelRef = useRef(null);
const rightPanelRef = useRef(null); const rightPanelRef = useRef(null);
// 编辑器属性(包括文档信息,编辑器模式等信息)
const [options, setOptions] = useState(initOptions); const [options, setOptions] = useState(initOptions);
// 设置保存状态
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
// 保存时设置编辑器信息markdown格式以及转换成 HTML 信息
const setContent = useCallback((markdownContent) => { const setContent = useCallback((markdownContent) => {
setEditorValue(markdownContent); setEditorValue(markdownContent);
processor.process(markdownContent, (error, vfile) => { processor.process(markdownContent, (error, vfile) => {
@ -74,6 +87,7 @@ const PlainMarkdownEditor = (props) => {
}); });
}, []); }, []);
// 更新编辑器属性(界面初始化后,用全局变量更新具体的文档信息-获取文档信息,用户权限,文档下载链接等 API 操作,然后更新到当前状态)
const updateOptions = useCallback(async ({ fileName, filePath, repoID }) => { const updateOptions = useCallback(async ({ fileName, filePath, repoID }) => {
const { markdownContent, hasPermission, fileInfo } = await getPlainOptions({ fileName, filePath, repoID }); const { markdownContent, hasPermission, fileInfo } = await getPlainOptions({ fileName, filePath, repoID });
setContent(markdownContent); setContent(markdownContent);
@ -88,11 +102,13 @@ const PlainMarkdownEditor = (props) => {
}, [options, setContent]); }, [options, setContent]);
// 注意useLayoutEffect 在浏览器绘制 DOM 之后执行,而 useEffect 在浏览器绘制 DOM 之前执行。 // 注意useLayoutEffect 在浏览器绘制 DOM 之后执行,而 useEffect 在浏览器绘制 DOM 之前执行。
// 这里确定首先加载编辑器基本骨架然后网络请求获取内容避免全部白屏Loading情况
useLayoutEffect(() => { useLayoutEffect(() => {
updateOptions({ fileName, filePath, repoID }); updateOptions({ fileName, filePath, repoID });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
// 离开页面前,询问是否保存
const onUnload = useCallback((event) => { const onUnload = useCallback((event) => {
const { contentChanged } = options; const { contentChanged } = options;
if (!contentChanged) return; if (!contentChanged) return;
@ -102,6 +118,7 @@ const PlainMarkdownEditor = (props) => {
return confirmationMessage; return confirmationMessage;
}, [options]); }, [options]);
// 离开页面前,询问是否保存
useEffect(() => { useEffect(() => {
window.addEventListener('beforeunload', onUnload); window.addEventListener('beforeunload', onUnload);
return () => { return () => {
@ -109,11 +126,13 @@ const PlainMarkdownEditor = (props) => {
}; };
}, [onUnload]); }, [onUnload]);
// 在代码编辑器中,更新编辑器内容,这里是回调函数
const updateCode = useCallback((newCode) => { const updateCode = useCallback((newCode) => {
setContent(String(newCode)); setContent(String(newCode));
!options.onContentChanged && setOptions({ ...options, contentChanged: true }); !options.onContentChanged && setOptions({ ...options, contentChanged: true });
}, [options, setContent]); }, [options, setContent]);
// 鼠标事件,滚动事件处理
const onEnterLeftPanel = useCallback(() => { const onEnterLeftPanel = useCallback(() => {
setIsMouseInLeftSide(true); setIsMouseInLeftSide(true);
setIsMouseInRightSide(false); setIsMouseInRightSide(false);
@ -149,17 +168,21 @@ const PlainMarkdownEditor = (props) => {
leftPanelElm.scrollTop = scrollPercentage * leftPanelElm.scrollHeight; leftPanelElm.scrollTop = scrollPercentage * leftPanelElm.scrollHeight;
}, [isMouseInRightSide, scrollPercentage]); }, [isMouseInRightSide, scrollPercentage]);
// 更新文件信息
const updateFileInfoMtime = useCallback((fileInfo) => { const updateFileInfoMtime = useCallback((fileInfo) => {
const { fileInfo: oldFileInfo } = options; const { fileInfo: oldFileInfo } = options;
const newFileInfo = Object.assign({}, oldFileInfo, { mtime: fileInfo.mtime, id: fileInfo.id, lastModifier: fileInfo.last_modifier_name }); const newFileInfo = Object.assign({}, oldFileInfo, { mtime: fileInfo.mtime, id: fileInfo.id, lastModifier: fileInfo.last_modifier_name });
return newFileInfo; return newFileInfo;
}, [options]); }, [options]);
// 保存编辑器内容
const onSaveEditorContent = useCallback(() => { const onSaveEditorContent = useCallback(() => {
setSaving(true); setSaving(true);
let fileInfo = options.fileInfo; let fileInfo = options.fileInfo;
// 先保存内容
editorApi.saveContent(editorValue).then(() => { editorApi.saveContent(editorValue).then(() => {
// 重新获取文件信息(例如上次保存事件)
editorApi.getFileInfo().then((res) => { editorApi.getFileInfo().then((res) => {
fileInfo = updateFileInfoMtime(res.data); fileInfo = updateFileInfoMtime(res.data);
setOptions({ setOptions({
@ -178,6 +201,7 @@ const PlainMarkdownEditor = (props) => {
}); });
}, [editorValue, options, setOptions, updateFileInfoMtime]); }, [editorValue, options, setOptions, updateFileInfoMtime]);
// 快捷键
const onHotKey = useCallback((event) => { const onHotKey = useCallback((event) => {
if (isHotkey('mod+s', event)) { if (isHotkey('mod+s', event)) {
event.preventDefault(); event.preventDefault();
@ -186,6 +210,7 @@ const PlainMarkdownEditor = (props) => {
} }
}, [editorValue, onSaveEditorContent]); }, [editorValue, onSaveEditorContent]);
// 切换收藏
const toggleStar = useCallback(() => { const toggleStar = useCallback(() => {
const starred = options.fileInfo.starred; const starred = options.fileInfo.starred;
const newFileInfo = Object.assign({}, options.fileInfo, { starred: !starred }); const newFileInfo = Object.assign({}, options.fileInfo, { starred: !starred });
@ -201,17 +226,20 @@ const PlainMarkdownEditor = (props) => {
}); });
}, [options]); }, [options]);
// 切换到纯文本编辑器
const setEditorMode = useCallback(() => { const setEditorMode = useCallback(() => {
const { origin, pathname } = window.location; const { origin, pathname } = window.location;
window.location.href = origin + pathname; window.location.href = origin + pathname;
}, []); }, []);
// 空回调函数
const ignoreCallBack = useCallback(() => void 0, []); const ignoreCallBack = useCallback(() => void 0, []);
if (options.loading) return <CodeMirrorLoading />; if (options.loading) return <CodeMirrorLoading />;
return ( return (
<> <>
{/* 普通文本和富文本,公共的工具栏,包括保存,星标,历史入口,分享,锁定等功能 */}
<HeaderToolbar <HeaderToolbar
editorApi={editorApi} editorApi={editorApi}
collabUsers={options.collabUsers} collabUsers={options.collabUsers}
@ -233,6 +261,7 @@ const PlainMarkdownEditor = (props) => {
/> />
<div className='sf-plain-editor'> <div className='sf-plain-editor'>
<div className="sf-plain-editor-main d-flex" onKeyDown={onHotKey}> <div className="sf-plain-editor-main d-flex" onKeyDown={onHotKey}>
{/* 编辑器内部,左侧是编辑器,右侧是预览 */}
<div <div
className="sf-plain-editor-left-panel" className="sf-plain-editor-left-panel"
ref={leftPanelRef} ref={leftPanelRef}
@ -250,6 +279,7 @@ const PlainMarkdownEditor = (props) => {
onScroll={onRightScroll} onScroll={onRightScroll}
> >
<div className="preview"> <div className="preview">
{/* 预览 previewValue */}
<div className="rendered-markdown article" dangerouslySetInnerHTML={{ __html: previewValue }}></div> <div className="rendered-markdown article" dangerouslySetInnerHTML={{ __html: previewValue }}></div>
</div> </div>
</div> </div>