195 lines
6.4 KiB
TypeScript
195 lines
6.4 KiB
TypeScript
import { atom, computed, type MapStore, map, type WritableAtom } from 'nanostores';
|
|
import type { Section } from '~/types/actions';
|
|
import type { DocumentProperties, Editor } from '~/types/editor';
|
|
import type { PageMap, PagesStore } from './pages';
|
|
|
|
/**
|
|
* 编辑器文档,结构为 <pageName, pageProperties>
|
|
*/
|
|
export type EditorDocuments = Record<string, DocumentProperties>;
|
|
|
|
type SelectedDocument = WritableAtom<string | undefined>;
|
|
|
|
export type EditorSection = Record<string, Section>;
|
|
|
|
// 编辑器命令类型
|
|
export type EditorCommandType = 'scrollToElement' | 'exportToZip';
|
|
|
|
// 编辑器命令接口
|
|
export interface EditorCommand {
|
|
type: EditorCommandType;
|
|
payload: any;
|
|
}
|
|
|
|
// 创建一个用于发送编辑器命令的atom
|
|
export const editorCommands = atom<EditorCommand | null>(null);
|
|
|
|
/**
|
|
* 与 Editor 进行对接的 store。
|
|
* 其内部保存的数据可以直接由 editor 使用与操作,并且当前数据与 editor 实时同步。
|
|
*/
|
|
export class EditorStore {
|
|
private readonly pagesStore: PagesStore;
|
|
|
|
editorInstance: WritableAtom<Editor | null> = import.meta.hot?.data?.editorInstance ?? atom<Editor | null>(null);
|
|
// 编辑器中当前选中的文档。
|
|
selectedDocument: SelectedDocument = import.meta.hot?.data?.selectedPage ?? atom<string | undefined>();
|
|
// 编辑器文档数据,始终是与编辑器所保持的最新数据,但此数据不一定执行了保存。
|
|
editorDocuments: MapStore<EditorDocuments> = import.meta.hot?.data?.documents ?? map({});
|
|
// 当前编辑器文档,基于 editorDocuments 和 selectedDocument 计算而来。始终是与编辑器所保持的最新数据,但此数据不一定执行了保存。
|
|
currentDocument = computed([this.editorDocuments, this.selectedDocument], (documents, selectedDocument) => {
|
|
if (!selectedDocument) {
|
|
return undefined;
|
|
}
|
|
return documents[selectedDocument];
|
|
});
|
|
// 当前编辑器未保存的页面
|
|
unsavedDocuments: WritableAtom<Set<string>> = import.meta.hot?.data?.unsavedDocuments ?? atom(new Set<string>());
|
|
// 编辑器文档最后保存时间
|
|
documentLastSaved: WritableAtom<Record<string, number>> =
|
|
import.meta.hot?.data?.documentLastSaved ?? atom<Record<string, number>>({});
|
|
|
|
constructor(pagesStore: PagesStore) {
|
|
this.pagesStore = pagesStore;
|
|
|
|
if (import.meta.hot && import.meta.hot.data) {
|
|
import.meta.hot.data.unsavedDocuments = this.unsavedDocuments;
|
|
import.meta.hot.data.selectedDocument = this.selectedDocument;
|
|
import.meta.hot.data.editorDocuments = this.editorDocuments;
|
|
import.meta.hot.data.documentLastSaved = this.documentLastSaved;
|
|
}
|
|
|
|
this.setupCoordination();
|
|
}
|
|
|
|
private setupCoordination() {
|
|
// 监听 pagesStore 的 pages 变化
|
|
this.pagesStore.pages.listen((pages) => {
|
|
this.setDocuments(pages);
|
|
});
|
|
|
|
// 监听 pagesStore 的 activePage 变化
|
|
this.pagesStore.activePage.listen((pageName) => {
|
|
this.selectedDocument.set(pageName);
|
|
});
|
|
}
|
|
|
|
setEditorInstance(editor: Editor) {
|
|
this.editorInstance.set(editor);
|
|
}
|
|
|
|
getEditorInstance() {
|
|
return this.editorInstance.get();
|
|
}
|
|
|
|
setDocuments(pages: PageMap, updateContent: boolean = false) {
|
|
const documents = this.editorDocuments.get();
|
|
this.editorDocuments.set(
|
|
Object.fromEntries<DocumentProperties>(
|
|
Object.entries(pages)
|
|
.map(([pageName, page]) => {
|
|
if (page === undefined) {
|
|
return undefined;
|
|
}
|
|
|
|
const oldDocument = documents[pageName];
|
|
if (oldDocument && !updateContent) {
|
|
return [pageName, { ...oldDocument, name: pageName, title: page.title }];
|
|
}
|
|
|
|
return [
|
|
pageName,
|
|
{
|
|
name: pageName,
|
|
title: page.title,
|
|
content: page.content,
|
|
},
|
|
] as [string, DocumentProperties];
|
|
})
|
|
.filter(Boolean) as Array<[string, DocumentProperties]>,
|
|
),
|
|
);
|
|
}
|
|
|
|
updatePageState(pageName: string, page: Omit<DocumentProperties, 'content'>) {
|
|
const documents = this.editorDocuments.get();
|
|
const oldDocumentState = documents[pageName];
|
|
if (!oldDocumentState) {
|
|
return;
|
|
}
|
|
|
|
const content = oldDocumentState.content;
|
|
this.editorDocuments.setKey(pageName, { ...oldDocumentState, ...page, content });
|
|
}
|
|
|
|
updateDocumentContent(pageName: string, newContent: string) {
|
|
const documents = this.editorDocuments.get();
|
|
const oldDocumentState = documents[pageName];
|
|
|
|
if (!oldDocumentState) {
|
|
return;
|
|
}
|
|
|
|
const oldContent = oldDocumentState.content;
|
|
const contentChanged = oldContent !== newContent;
|
|
if (contentChanged) {
|
|
this.editorDocuments.setKey(pageName, {
|
|
...oldDocumentState,
|
|
content: newContent,
|
|
});
|
|
}
|
|
this.updateUnsavedDocuments(pageName, newContent);
|
|
}
|
|
|
|
private updateUnsavedDocuments(pageName: string, newContent: string) {
|
|
const savedContent = this.pagesStore.getPage(pageName)?.content;
|
|
// 是否存在未保存的更改
|
|
const unsavedChanges = savedContent === undefined || savedContent !== newContent;
|
|
const currentDocument = this.currentDocument.get();
|
|
if (!currentDocument) {
|
|
return;
|
|
}
|
|
// 保存数据至未保存中
|
|
const previousUnsavedPages = this.unsavedDocuments.get();
|
|
// 如果已经将此页面标记为未保存,则不进行更新。
|
|
if (unsavedChanges && previousUnsavedPages.has(pageName)) {
|
|
return;
|
|
}
|
|
|
|
const newUnsavedPages = new Set(previousUnsavedPages);
|
|
|
|
// 如果存在未保存的更改,则将此页面标记为未保存。否则,将此页面从未保存中移除。
|
|
if (unsavedChanges) {
|
|
newUnsavedPages.add(pageName);
|
|
} else {
|
|
newUnsavedPages.delete(pageName);
|
|
}
|
|
|
|
this.unsavedDocuments.set(newUnsavedPages);
|
|
}
|
|
|
|
removeUnsavedDocument(pageName: string, saved: boolean = false) {
|
|
const newUnsavedPages = new Set(this.unsavedDocuments.get());
|
|
newUnsavedPages.delete(pageName);
|
|
this.unsavedDocuments.set(newUnsavedPages);
|
|
|
|
if (!saved) {
|
|
return;
|
|
}
|
|
// 记录保存时间
|
|
const currentTime = Date.now();
|
|
const lastSavedTimes = this.documentLastSaved.get();
|
|
this.documentLastSaved.set({
|
|
...lastSavedTimes,
|
|
[pageName]: currentTime,
|
|
});
|
|
}
|
|
|
|
scrollToElement(domId: string) {
|
|
editorCommands.set({
|
|
type: 'scrollToElement',
|
|
payload: { domId },
|
|
});
|
|
}
|
|
}
|