Files
upage-git/app/lib/stores/editor.ts

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 },
});
}
}