Files
WRNexusJS/packages/block-editor/src/state.ts
T
Clintchiz 0bd2a6860d
Quality / quality (ubuntu-latest) (push) Failing after 11m26s
Quality / quality (windows-latest) (push) Canceled after 0s
feat(builder): add dynamic blocks email and dnd packages
2026-09-03 14:00:41 +05:30

124 lines
3.4 KiB
TypeScript

import { signal, type Signal } from "@wrnexus/reactive";
import {
applyCommand,
type BlockCommand,
type BlockDocument,
type CommandResult,
type DocumentKindDefinition,
type ReadonlyBlockRegistry,
type Result,
type SelectionState,
} from "@wrnexus/blocks";
export interface EditorSnapshot {
document: BlockDocument;
revision: number;
selection: SelectionState;
dirty: boolean;
status: "idle" | "saving" | "saved" | "conflict" | "error";
}
interface HistoryEntry {
forward: BlockCommand;
inverse: BlockCommand;
}
export interface BlockEditorState extends Signal<EditorSnapshot> {
execute(command: BlockCommand): Result<CommandResult>;
undo(): boolean;
redo(): boolean;
select(ids: string[], primary?: string | null): void;
markSaving(): void;
markSaved(revision: number): void;
markConflict(): void;
replaceFromServer(document: BlockDocument, revision: number): void;
canUndo(): boolean;
canRedo(): boolean;
}
export function createBlockEditorState(options: {
document: BlockDocument;
revision: number;
kind: DocumentKindDefinition;
registry: ReadonlyBlockRegistry;
historyLimit?: number;
}): BlockEditorState {
const current = signal<EditorSnapshot>({
document: options.document,
revision: options.revision,
selection: { primary: null, ordered: [] },
dirty: false,
status: "idle",
});
const past: HistoryEntry[] = [];
const future: HistoryEntry[] = [];
const limit = Math.max(1, options.historyLimit ?? 100);
const apply = (command: BlockCommand, remember: boolean): Result<CommandResult> => {
const result = applyCommand(current.get().document, command, options.kind, options.registry);
if (!result.ok) return result;
if (remember) {
past.push({ forward: command, inverse: result.value.inverse });
if (past.length > limit) past.shift();
future.length = 0;
}
current.set({
...current.get(),
document: result.value.document,
selection: result.value.selection,
dirty: true,
status: "idle",
});
return result;
};
return Object.assign(current, {
execute: (command: BlockCommand) => apply(command, true),
undo() {
const entry = past.pop();
if (!entry) return false;
const result = apply(entry.inverse, false);
if (!result.ok) {
past.push(entry);
return false;
}
future.push(entry);
return true;
},
redo() {
const entry = future.pop();
if (!entry) return false;
const result = apply(entry.forward, false);
if (!result.ok) {
future.push(entry);
return false;
}
past.push(entry);
return true;
},
select(ids: string[], primary: string | null = ids[0] ?? null) {
current.set({ ...current.get(), selection: { primary, ordered: [...new Set(ids)] } });
},
markSaving() {
current.set({ ...current.get(), status: "saving" });
},
markSaved(revision: number) {
current.set({ ...current.get(), revision, dirty: false, status: "saved" });
},
markConflict() {
current.set({ ...current.get(), status: "conflict" });
},
replaceFromServer(document: BlockDocument, revision: number) {
past.length = 0;
future.length = 0;
current.set({
document,
revision,
selection: { primary: null, ordered: [] },
dirty: false,
status: "idle",
});
},
canUndo: () => past.length > 0,
canRedo: () => future.length > 0,
});
}