Files
upage-git/app/.client/bridge/index.ts

208 lines
5.3 KiB
TypeScript

import type { ActionState } from '../runtime/action-runner';
interface BridgeContext {
loaded: boolean;
}
export const bridgeContext: BridgeContext = import.meta.hot?.data?.editorBridgeContext ?? {
loaded: false,
};
if (import.meta.hot && import.meta.hot.data) {
import.meta.hot.data.editorBridgeContext = bridgeContext;
}
export type EventPayload = {
pageName: string;
} & Record<string, any>;
type EventHandler = (payload: EventPayload) => void;
type EventMap = Map<string, Set<EventHandler>>;
type Page = {
name: string;
title: string;
actionIds?: string[];
};
type SectionProps = {
id: string;
pageName: string;
content: string;
domId: string;
rootDomId: string;
sort?: number;
};
/**
* 构筑一个无头的 editor 的 bridge。
* 所有操作编辑器的方法,都需要经由此 bridge 进行广播。
* 其内部所保存的 pages 与 sections 均为虚拟数据,与 editor 所需的实际数据有一定差异。
*/
export class EditorBridge {
#pages: Map<string, Page> = new Map();
#sections: Map<string, SectionProps> = new Map();
#events: EventMap = new Map();
#watchHandlers: Set<(event: { type: string; payload: EventPayload }) => void> = new Set();
/**
* 监听事件
* @param event 事件名称
* @param handler 处理函数
*/
on(event: string, handler: EventHandler) {
if (!this.#events.has(event)) {
this.#events.set(event, new Set());
}
this.#events.get(event)?.add(handler);
return this;
}
/**
* 移除事件监听
* @param event 事件名称
* @param handler 处理函数
*/
off(event: string, handler?: EventHandler) {
if (!handler) {
this.#events.delete(event);
} else if (this.#events.has(event)) {
this.#events.get(event)?.delete(handler);
}
return this;
}
/**
* 触发事件
* @param event 事件名称
* @param payload 事件负载对象
*/
#emit(event: string, payload: EventPayload) {
this.#events.get(event)?.forEach((handler) => handler(payload));
// 同时通知所有 watch 监听器
this.#watchHandlers.forEach((handler) => handler({ type: event, payload }));
}
/**
* 创建页面
* @param pageName 页面名称
*/
async createPage(name: string, { title, actionIds }: { title?: string; actionIds?: string[] } = {}) {
const newPage: Page = { name, title: title ?? '未命名页面', actionIds: actionIds ?? [] };
this.#pages.set(name, newPage);
// 发出 add_page 事件,其他地方可通过 editorBridge.on('add_page', (payload) => {}) 监听
this.#emit('add_page', {
pageName: name,
...newPage,
});
}
/**
* 更新页面属性
*
* @param pageName
* @param param1
* @returns
*/
async updatePageAttributes(pageName: string, { title }: { title?: string } = {}) {
const page = this.#pages.get(pageName);
if (!page) {
return;
}
const actionIds = page.actionIds;
this.#emit('upsert_page', {
pageName,
title,
actionIds,
});
}
async removePage(pageName: string) {
this.#pages.delete(pageName);
// 发出 remove_page 事件,其他地方可通过 editorBridge.on('remove_page', (payload) => {}) 监听
this.#emit('remove_page', { pageName });
}
async updateSection(action: ActionState) {
this.#sections.set(action.id, action);
// 发出 add_page_section 事件,其他地方可通过 editorBridge.on('add_page_section', (payload) => {}) 监听
this.#emit('update_section', { pageName: action.pageName, id: action.id, section: action });
}
async upsertPageAction(pageName: string, pageTitle: string, actionId: string) {
const page = this.#pages.get(pageName);
const pageProps = page
? {
...page,
actionIds: [...(page.actionIds ?? []), actionId],
}
: {
name: pageName,
title: pageTitle || '未命名页面',
actionIds: [actionId],
};
pageProps.actionIds = [...new Set(pageProps.actionIds)];
this.#pages.set(pageName, pageProps);
// 发出 update_page 事件,其他地方可通过 editorBridge.on('update_page', (payload) => {}) 监听
this.#emit('upsert_page', {
pageName,
...pageProps,
});
}
// /**
// * 获取页面历史记录
// * @param pageName 页面名称
// * @returns 页面历史
// */
// async getPageHistory(pageName: string): Promise<string> {
// return this.#pages.get(pageName) || '{}';
// }
/**
* 监听所有事件
* @param handler 处理所有事件的函数
*/
watch(handler: (event: { type: string; payload: EventPayload }) => void) {
this.#watchHandlers.add(handler);
return this;
}
/**
* 移除 watch 监听
* @param handler 处理函数
*/
unwatch(handler: (event: { type: string; payload: EventPayload }) => void) {
this.#watchHandlers.delete(handler);
return this;
}
}
export let editorBridge: Promise<EditorBridge> = new Promise(() => {
// noop for ssr
});
if (!import.meta.env.SSR) {
editorBridge =
import.meta.hot?.data?.editorBridge ??
Promise.resolve()
.then(() => {
return new EditorBridge();
})
.then(async (editorBridge) => {
bridgeContext.loaded = true;
return editorBridge;
});
if (import.meta.hot && import.meta.hot.data) {
import.meta.hot.data.editorBridge = editorBridge;
}
}