mirror of
https://github.com/haiwen/seahub.git
synced 2025-08-18 15:08:22 +00:00
02 普通文本编辑器
This commit is contained in:
parent
98f7ca81e7
commit
2684e54aed
@ -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 富文本编辑器
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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 };
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user