@moloodev/editor-sdk 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +62 -3
- package/src/channel.ts +287 -0
- package/src/chrome-manifest.ts +111 -0
- package/src/commands/clipboard/router.ts +99 -0
- package/src/commands/clipboard/types.ts +20 -0
- package/src/commands/history/stack.ts +53 -0
- package/src/commands/icons/catalog.ts +172 -0
- package/src/commands/index.ts +100 -0
- package/src/commands/keys/display.ts +16 -0
- package/src/commands/keys/normalize.ts +55 -0
- package/src/commands/keys/reserved.ts +62 -0
- package/src/commands/keys/validate.ts +77 -0
- package/src/commands/manifest/configuration.ts +59 -0
- package/src/commands/manifest/contribution-manifest-schema.ts +56 -0
- package/src/commands/manifest/editor-document.ts +501 -0
- package/src/commands/manifest/ids.ts +2 -0
- package/src/commands/manifest/merge.ts +215 -0
- package/src/commands/manifest/types.ts +113 -0
- package/src/commands/manifest/validate-manifest.ts +45 -0
- package/src/commands/react/context.tsx +249 -0
- package/src/commands/react/hotkey-defaults.ts +11 -0
- package/src/commands/react/scope-context.tsx +31 -0
- package/src/commands/react/scope-provider.tsx +123 -0
- package/src/commands/react/store.ts +182 -0
- package/src/commands/registry/create.ts +55 -0
- package/src/commands/registry/types.ts +12 -0
- package/src/commands/when/evaluate.ts +80 -0
- package/src/commands/when/index.ts +19 -0
- package/src/commands/when/parse.ts +276 -0
- package/src/commands/when/types.ts +24 -0
- package/src/commands/when/validate.ts +20 -0
- package/src/crdt/awareness.ts +29 -0
- package/src/crdt/bytes.ts +26 -0
- package/src/crdt/connect.ts +285 -0
- package/src/crdt/diff.ts +86 -0
- package/src/crdt/doc.ts +67 -0
- package/src/crdt/front-matter.ts +45 -0
- package/src/crdt/index.ts +76 -0
- package/src/crdt/persist.ts +40 -0
- package/src/crdt/protocol.ts +119 -0
- package/src/crdt/shared.ts +104 -0
- package/src/crdt/text-body.ts +125 -0
- package/src/crdt/undo.ts +59 -0
- package/src/crdt/vite-plugin.ts +42 -0
- package/src/crdt/yjs.ts +16 -0
- package/src/editor-common.ts +160 -0
- package/src/editor-id.ts +56 -0
- package/src/frame-attributes.ts +9 -0
- package/src/index.ts +229 -0
- package/src/json-schema.ts +13 -0
- package/src/layout/bind-toolbar.ts +60 -0
- package/src/layout/catalog.ts +82 -0
- package/src/layout/chrome/command-affordances.ts +47 -0
- package/src/layout/chrome/command-layout-state.ts +42 -0
- package/src/layout/chrome/editor-menu.tsx +358 -0
- package/src/layout/chrome/editor-shortcut-keys.tsx +27 -0
- package/src/layout/chrome/menu-styles.ts +3 -0
- package/src/layout/chrome/mid-menu-nodes.ts +27 -0
- package/src/layout/chrome/use-editor-command-affordances.ts +33 -0
- package/src/layout/chrome/visible-menu-leaves.ts +15 -0
- package/src/layout/content-error-notice.tsx +31 -0
- package/src/layout/define-editor-layout.ts +60 -0
- package/src/layout/define-editor-layout.types.ts +57 -0
- package/src/layout/doc-undo-bridge.tsx +22 -0
- package/src/layout/dock-zones.tsx +61 -0
- package/src/layout/drop.ts +39 -0
- package/src/layout/editor-layout.tsx +287 -0
- package/src/layout/editor-loading.tsx +23 -0
- package/src/layout/frame-layout-binding.tsx +74 -0
- package/src/layout/frame.ts +23 -0
- package/src/layout/hide.ts +103 -0
- package/src/layout/index.ts +14 -0
- package/src/layout/layout-binding.tsx +42 -0
- package/src/layout/layout-storage-key.ts +6 -0
- package/src/layout/layout-values.ts +48 -0
- package/src/layout/mutate.ts +486 -0
- package/src/layout/nesting-context.tsx +20 -0
- package/src/layout/panel-header-action.tsx +29 -0
- package/src/layout/panel-header.tsx +168 -0
- package/src/layout/panel-hide-menu.tsx +6 -0
- package/src/layout/panel-menu.tsx +85 -0
- package/src/layout/panel-section.tsx +114 -0
- package/src/layout/panel-size.ts +94 -0
- package/src/layout/panel-title.tsx +5 -0
- package/src/layout/registry.tsx +333 -0
- package/src/layout/restore-panel.ts +26 -0
- package/src/layout/slots-context.tsx +189 -0
- package/src/layout/tab-button.tsx +60 -0
- package/src/layout/toolbar-scroll.ts +22 -0
- package/src/layout/toolbar.tsx +168 -0
- package/src/layout/types.ts +41 -0
- package/src/layout/use-collapsed-sections.ts +50 -0
- package/src/layout/validate-layout-spec.ts +143 -0
- package/src/message-samples.ts +377 -0
- package/src/origin-headers.ts +57 -0
- package/src/print/index.ts +10 -0
- package/src/print/inspect.ts +16 -0
- package/src/print/schema.ts +122 -0
- package/src/protocol-entry.ts +19 -0
- package/src/protocol.ts +471 -0
- package/src/render-result.ts +25 -0
- package/src/view/editor-chrome.ts +28 -0
- package/src/view/hooks.ts +260 -0
- package/src/view/mount-editor-view.ts +36 -0
- package/src/view/port-binding.ts +127 -0
- package/src/view/port-commands.ts +36 -0
- package/src/view/port.ts +35 -0
- package/src/view/theme-class.ts +11 -0
- package/src/view.ts +313 -0
- package/README.md +0 -3
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { createContext, createElement, useCallback, useContext, useEffect, useRef, useState, type ReactNode } from "react";
|
|
2
|
+
import { KNOWN_PATTERN_IDS, resolvePatternAppearance } from "@moloodev/platform";
|
|
3
|
+
import type { Doc } from "../crdt";
|
|
4
|
+
import type { Composition, Publication } from "@moloodev/platform";
|
|
5
|
+
import type {
|
|
6
|
+
EditorLayoutValues,
|
|
7
|
+
EditorAsset,
|
|
8
|
+
EditorOperation,
|
|
9
|
+
EditorRevision,
|
|
10
|
+
EditorSession,
|
|
11
|
+
EditorSessionAsset,
|
|
12
|
+
} from "../index";
|
|
13
|
+
|
|
14
|
+
export type { EditorAsset, EditorSessionAsset, EditorLayoutValues } from "../index";
|
|
15
|
+
import { resetLayoutCommandId } from "../editor-common";
|
|
16
|
+
import type { EditorView } from "../view";
|
|
17
|
+
|
|
18
|
+
export type EditorDocProjectInput = {
|
|
19
|
+
doc: Doc | unknown | null;
|
|
20
|
+
data: Record<string, unknown> | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type EditorOpenBrowserArgs = {
|
|
24
|
+
mode?: "pick";
|
|
25
|
+
acceptedTypes?: readonly string[];
|
|
26
|
+
excludeTypes?: readonly string[];
|
|
27
|
+
excludeIds?: readonly string[];
|
|
28
|
+
onPick?: (assets: readonly EditorAsset[]) => void;
|
|
29
|
+
onDismiss?: () => void;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type EditorViewBinding = {
|
|
33
|
+
useTheme(): { theme: "dark" | "light" };
|
|
34
|
+
useKind(type: string): { id: string; label: string; color: string; icon: string };
|
|
35
|
+
useKinds(): { id: string; label: string; color: string; icon: string }[];
|
|
36
|
+
useList(args?: { type?: string }): { data: EditorAsset[] | undefined };
|
|
37
|
+
useComposition(id: string | null): {
|
|
38
|
+
data: Composition | undefined;
|
|
39
|
+
isError: boolean;
|
|
40
|
+
replace(composition: Composition): void;
|
|
41
|
+
fetch(): Promise<Composition>;
|
|
42
|
+
invalidate(): Promise<void>;
|
|
43
|
+
};
|
|
44
|
+
usePublication(id: string | null): {
|
|
45
|
+
data: Publication | undefined;
|
|
46
|
+
replace(publication: Publication): void;
|
|
47
|
+
invalidate(): Promise<void>;
|
|
48
|
+
};
|
|
49
|
+
useAssetClient(): {
|
|
50
|
+
listRevisions(id: string): Promise<EditorRevision[]>;
|
|
51
|
+
confirmBundle(
|
|
52
|
+
id: string,
|
|
53
|
+
input: { message: string; reviewed: string },
|
|
54
|
+
): Promise<{ composition: Composition; revision: number }>;
|
|
55
|
+
putPublication(id: string, publication: Publication): Promise<Publication>;
|
|
56
|
+
};
|
|
57
|
+
useAssetOpener(): ((target: { id: string; type: string }) => void) | null;
|
|
58
|
+
useAssetInfo(): {
|
|
59
|
+
open(options?: { focus?: "revision-message" }): void;
|
|
60
|
+
setCreateAdapter(adapter: {
|
|
61
|
+
canCreate: boolean;
|
|
62
|
+
reason?: string;
|
|
63
|
+
submit(message: string): Promise<boolean>;
|
|
64
|
+
} | null): void;
|
|
65
|
+
};
|
|
66
|
+
useOpenSession(): {
|
|
67
|
+
asset: EditorSessionAsset | null;
|
|
68
|
+
doc: Doc | null;
|
|
69
|
+
mode: EditorSession["mode"];
|
|
70
|
+
data: Record<string, unknown> | null;
|
|
71
|
+
syncStatus: EditorSession["syncStatus"];
|
|
72
|
+
openBrowser(args?: EditorOpenBrowserArgs): void;
|
|
73
|
+
closeBrowser(): void;
|
|
74
|
+
};
|
|
75
|
+
useEditorHost(): {
|
|
76
|
+
canManage: boolean;
|
|
77
|
+
acceptedTypes: readonly string[];
|
|
78
|
+
openStartDialog(): void;
|
|
79
|
+
render(input: EditorOperation["host.render"]["body"]): Promise<EditorOperation["host.render"]["success"]>;
|
|
80
|
+
};
|
|
81
|
+
useEditorDocProjection<T>(options: {
|
|
82
|
+
project: (source: EditorDocProjectInput) => T;
|
|
83
|
+
hydrate: (value: T) => void;
|
|
84
|
+
onProjectError?: () => void;
|
|
85
|
+
}): { ready: boolean };
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const EditorViewContext = createContext<EditorViewBinding | null>(null);
|
|
89
|
+
|
|
90
|
+
export function EditorViewProvider({
|
|
91
|
+
binding,
|
|
92
|
+
children,
|
|
93
|
+
}: {
|
|
94
|
+
binding: EditorViewBinding;
|
|
95
|
+
children: ReactNode;
|
|
96
|
+
}) {
|
|
97
|
+
return createElement(EditorViewContext.Provider, { value: binding, children });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function useBinding(hook: string): EditorViewBinding {
|
|
101
|
+
const binding = useContext(EditorViewContext);
|
|
102
|
+
if (!binding) throw new Error(`${hook} must be used within EditorViewProvider`);
|
|
103
|
+
return binding;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function isEditorContentReady(
|
|
107
|
+
mode: EditorSession["mode"],
|
|
108
|
+
hasDoc: boolean,
|
|
109
|
+
): boolean {
|
|
110
|
+
return (mode === "edit" || mode === "read") && hasDoc;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function shouldShowContentError(
|
|
114
|
+
contentError: boolean,
|
|
115
|
+
mode: EditorSession["mode"],
|
|
116
|
+
hasDoc: boolean,
|
|
117
|
+
): boolean {
|
|
118
|
+
return contentError && isEditorContentReady(mode, hasDoc);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function kindOf(type: string) {
|
|
122
|
+
const entry = resolvePatternAppearance(type);
|
|
123
|
+
return {
|
|
124
|
+
id: type,
|
|
125
|
+
label: entry.label,
|
|
126
|
+
color: entry.color,
|
|
127
|
+
icon: entry.icon,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function kindsOf() {
|
|
132
|
+
return KNOWN_PATTERN_IDS.map(kindOf);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function useTheme() {
|
|
136
|
+
return useBinding("useTheme").useTheme();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function useKind(type: string) {
|
|
140
|
+
return useBinding("useKind").useKind(type);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function useKinds() {
|
|
144
|
+
return useBinding("useKinds").useKinds();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function useList(args: { type?: string } = {}): {
|
|
148
|
+
data: EditorAsset[] | undefined;
|
|
149
|
+
} {
|
|
150
|
+
return useBinding("useList").useList(args);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function useComposition(id: string | null) {
|
|
154
|
+
return useBinding("useComposition").useComposition(id);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function usePublication(id: string | null) {
|
|
158
|
+
return useBinding("usePublication").usePublication(id);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function useAssetClient() {
|
|
162
|
+
return useBinding("useAssetClient").useAssetClient();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function useAssetOpener() {
|
|
166
|
+
return useBinding("useAssetOpener").useAssetOpener();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function useAssetInfo() {
|
|
170
|
+
return useBinding("useAssetInfo").useAssetInfo();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function useOpenSession() {
|
|
174
|
+
return useBinding("useOpenSession").useOpenSession();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function useEditorHost() {
|
|
178
|
+
return useBinding("useEditorHost").useEditorHost();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function useEditorDocProjection<T>(options: {
|
|
182
|
+
project: (source: EditorDocProjectInput) => T;
|
|
183
|
+
hydrate: (value: T) => void;
|
|
184
|
+
onProjectError?: () => void;
|
|
185
|
+
}) {
|
|
186
|
+
return useBinding("useEditorDocProjection").useEditorDocProjection(options);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function useHostLayout(view: EditorView): {
|
|
190
|
+
loading: boolean;
|
|
191
|
+
layout: EditorLayoutValues | null;
|
|
192
|
+
save(values: EditorLayoutValues): void;
|
|
193
|
+
} {
|
|
194
|
+
const [loaded, setLoaded] = useState<{
|
|
195
|
+
view: EditorView;
|
|
196
|
+
layout: EditorLayoutValues | null;
|
|
197
|
+
} | null>(null);
|
|
198
|
+
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
199
|
+
const pending = useRef<EditorLayoutValues | null>(null);
|
|
200
|
+
const ticket = useRef(0);
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
const current = ++ticket.current;
|
|
203
|
+
let active = true;
|
|
204
|
+
void view.handshake.then(() => view.call["layout.load"]({})).then(
|
|
205
|
+
(response) => {
|
|
206
|
+
if (active && current === ticket.current) {
|
|
207
|
+
setLoaded({ view, layout: response.ok ? response.body.layout : null });
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
() => {
|
|
211
|
+
if (active && current === ticket.current) setLoaded({ view, layout: null });
|
|
212
|
+
},
|
|
213
|
+
);
|
|
214
|
+
return () => {
|
|
215
|
+
active = false;
|
|
216
|
+
if (timer.current !== null) clearTimeout(timer.current);
|
|
217
|
+
timer.current = null;
|
|
218
|
+
if (pending.current !== null) {
|
|
219
|
+
const layout = pending.current;
|
|
220
|
+
pending.current = null;
|
|
221
|
+
void view.call["layout.save"]({ layout }).catch(() => {});
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}, [view]);
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
let active = true;
|
|
227
|
+
const stop = view.onCommand((commandId) => {
|
|
228
|
+
if (!active || commandId !== resetLayoutCommandId) return;
|
|
229
|
+
if (timer.current !== null) clearTimeout(timer.current);
|
|
230
|
+
timer.current = null;
|
|
231
|
+
pending.current = null;
|
|
232
|
+
const current = ++ticket.current;
|
|
233
|
+
setLoaded(null);
|
|
234
|
+
void view.call["layout.load"]({}).then(
|
|
235
|
+
(response) => {
|
|
236
|
+
if (!active || current !== ticket.current) return;
|
|
237
|
+
setLoaded({ view, layout: response.ok ? response.body.layout : null });
|
|
238
|
+
},
|
|
239
|
+
() => {
|
|
240
|
+
if (!active || current !== ticket.current) return;
|
|
241
|
+
setLoaded({ view, layout: null });
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
return () => {
|
|
246
|
+
active = false;
|
|
247
|
+
stop();
|
|
248
|
+
};
|
|
249
|
+
}, [view]);
|
|
250
|
+
const save = useCallback((values: EditorLayoutValues) => {
|
|
251
|
+
if (timer.current !== null) clearTimeout(timer.current);
|
|
252
|
+
pending.current = values;
|
|
253
|
+
timer.current = setTimeout(() => {
|
|
254
|
+
timer.current = null;
|
|
255
|
+
pending.current = null;
|
|
256
|
+
void view.call["layout.save"]({ layout: values }).catch(() => {});
|
|
257
|
+
}, 500);
|
|
258
|
+
}, [view]);
|
|
259
|
+
return { loading: loaded?.view !== view, layout: loaded?.view === view ? loaded.layout : null, save };
|
|
260
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createElement, type ComponentType } from "react";
|
|
2
|
+
import type { EditorManifest } from "../commands";
|
|
3
|
+
import { connectEditorView, type EditorView } from "../view";
|
|
4
|
+
import { bindThemeClass } from "./theme-class";
|
|
5
|
+
import { createEditorChrome } from "./editor-chrome";
|
|
6
|
+
|
|
7
|
+
/** Imports outside an iframe stay inert, including server and DOM unit tests. */
|
|
8
|
+
export function mountEditorView(options: {
|
|
9
|
+
manifest: EditorManifest;
|
|
10
|
+
layout: Parameters<typeof createEditorChrome>[1];
|
|
11
|
+
Root: ComponentType<{ view: EditorView }>;
|
|
12
|
+
}): EditorView | null {
|
|
13
|
+
if (typeof window === "undefined") return null;
|
|
14
|
+
const parent = new URLSearchParams(window.location.search).get("parent");
|
|
15
|
+
if (parent === null) return null;
|
|
16
|
+
const view = connectEditorView({
|
|
17
|
+
parent,
|
|
18
|
+
editorId: options.manifest.id,
|
|
19
|
+
createChannel: () => new MessageChannel(),
|
|
20
|
+
postMessage: (message, origin, ports) => window.parent.postMessage(message, origin, [...ports]),
|
|
21
|
+
});
|
|
22
|
+
bindThemeClass(view, document.documentElement);
|
|
23
|
+
const root = document.createElement("div");
|
|
24
|
+
root.id = "root";
|
|
25
|
+
document.body.append(root);
|
|
26
|
+
void import("react-dom/client").then(({ createRoot }) => {
|
|
27
|
+
createRoot(root).render(createElement(options.Root, { view }));
|
|
28
|
+
});
|
|
29
|
+
void view.handshake.then(async (reply) => {
|
|
30
|
+
if (!reply.ok) throw new Error(reply.code);
|
|
31
|
+
const { chromeManifest } = createEditorChrome(options.manifest, options.layout);
|
|
32
|
+
const submitted = await view.call["chrome.submit"](chromeManifest);
|
|
33
|
+
if (!submitted.ok) throw new Error(submitted.code);
|
|
34
|
+
});
|
|
35
|
+
return view;
|
|
36
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createElement, useCallback, useMemo, useSyncExternalStore, type ReactNode } from "react";
|
|
2
|
+
import type { EditorOperation } from "../index";
|
|
3
|
+
import { RenderFailure, renderCode } from "../render-result";
|
|
4
|
+
import type { EditorView } from "../view";
|
|
5
|
+
import { EditorViewProvider, kindOf, type EditorOpenBrowserArgs, type EditorViewBinding } from "./hooks";
|
|
6
|
+
|
|
7
|
+
function createPortBinding(view: EditorView, acceptedTypes: readonly string[]): EditorViewBinding {
|
|
8
|
+
let pending: { args: EditorOpenBrowserArgs } | null = null;
|
|
9
|
+
|
|
10
|
+
function closeBrowser() {
|
|
11
|
+
const previous = pending;
|
|
12
|
+
pending = null;
|
|
13
|
+
previous?.args.onDismiss?.();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function openBrowser(args?: EditorOpenBrowserArgs) {
|
|
17
|
+
closeBrowser();
|
|
18
|
+
if (args?.onPick || args?.onDismiss) {
|
|
19
|
+
const pick = { args };
|
|
20
|
+
pending = pick;
|
|
21
|
+
const body: EditorOperation["host.pick"]["body"] = {};
|
|
22
|
+
if (args.acceptedTypes !== undefined) body.acceptedTypes = [...args.acceptedTypes];
|
|
23
|
+
if (args.excludeTypes !== undefined) body.excludeTypes = [...args.excludeTypes];
|
|
24
|
+
if (args.excludeIds !== undefined) body.excludeIds = [...args.excludeIds];
|
|
25
|
+
void view.call["host.pick"](body).then((response) => {
|
|
26
|
+
if (pending !== pick) return;
|
|
27
|
+
pending = null;
|
|
28
|
+
if (response.ok && !response.body.cancelled) args.onPick?.(response.body.assets);
|
|
29
|
+
else args.onDismiss?.();
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
void view.call["host.openBrowser"](
|
|
34
|
+
args?.excludeIds === undefined ? {} : { excludeIds: args.excludeIds },
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function openStartDialog() {
|
|
39
|
+
void view.call["host.start"]({});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function openAsset(target: { id: string; type: string }) {
|
|
43
|
+
void view.call["host.openAsset"]({ id: target.id, type: target.type });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function setCreateAdapter(adapter: Parameters<ReturnType<EditorViewBinding["useAssetInfo"]>["setCreateAdapter"]>[0]) {
|
|
47
|
+
if (adapter !== null) throw new Error("setCreateAdapter is not available in iframe views");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function useSnapshot() {
|
|
51
|
+
return useSyncExternalStore(view.subscribe, view.getSnapshot, view.getSnapshot);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function unavailable(hook: string): never {
|
|
55
|
+
useSnapshot();
|
|
56
|
+
throw new Error(`${hook} is not available in iframe views`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
useTheme() {
|
|
61
|
+
const theme = useSnapshot().theme ?? "dark";
|
|
62
|
+
return useMemo(() => ({ theme }), [theme]);
|
|
63
|
+
},
|
|
64
|
+
useKind(type) {
|
|
65
|
+
useSnapshot();
|
|
66
|
+
return useMemo(() => kindOf(type), [type]);
|
|
67
|
+
},
|
|
68
|
+
useKinds: () => unavailable("useKinds"),
|
|
69
|
+
useList: () => unavailable("useList"),
|
|
70
|
+
useComposition: () => unavailable("useComposition"),
|
|
71
|
+
usePublication: () => unavailable("usePublication"),
|
|
72
|
+
useAssetClient: () => unavailable("useAssetClient"),
|
|
73
|
+
useEditorDocProjection: () => unavailable("useEditorDocProjection"),
|
|
74
|
+
useOpenSession() {
|
|
75
|
+
const snapshot = useSnapshot();
|
|
76
|
+
const session = snapshot.session;
|
|
77
|
+
const doc = session?.asset ? snapshot.doc : null;
|
|
78
|
+
return useMemo(() => ({
|
|
79
|
+
asset: session?.asset ?? null,
|
|
80
|
+
doc,
|
|
81
|
+
mode: session?.mode ?? "idle",
|
|
82
|
+
data: session?.data ?? null,
|
|
83
|
+
syncStatus: session?.syncStatus ?? "offline",
|
|
84
|
+
openBrowser,
|
|
85
|
+
closeBrowser,
|
|
86
|
+
}), [session, doc, openBrowser, closeBrowser]);
|
|
87
|
+
},
|
|
88
|
+
useEditorHost() {
|
|
89
|
+
const canManage = useSnapshot().access?.canManage ?? false;
|
|
90
|
+
return useMemo(() => ({
|
|
91
|
+
canManage,
|
|
92
|
+
acceptedTypes,
|
|
93
|
+
openStartDialog,
|
|
94
|
+
render(input: EditorOperation["host.render"]["body"]) {
|
|
95
|
+
return view.call["host.render"](input).then((response) => {
|
|
96
|
+
if (response.ok) return response.body;
|
|
97
|
+
const code = renderCode(response.code);
|
|
98
|
+
if (code === undefined) throw new Error(response.code);
|
|
99
|
+
throw new RenderFailure(code);
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
}), [canManage, acceptedTypes, openStartDialog, view]);
|
|
103
|
+
},
|
|
104
|
+
useAssetOpener() {
|
|
105
|
+
useSnapshot();
|
|
106
|
+
return useCallback(openAsset, [openAsset]);
|
|
107
|
+
},
|
|
108
|
+
useAssetInfo() {
|
|
109
|
+
const id = useSnapshot().session?.asset?.id;
|
|
110
|
+
return useMemo(() => ({
|
|
111
|
+
open() {
|
|
112
|
+
if (id !== undefined) void view.call["host.openInfo"]({ id });
|
|
113
|
+
},
|
|
114
|
+
setCreateAdapter,
|
|
115
|
+
}), [id, view, setCreateAdapter]);
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function PortEditorView({ view, acceptedTypes, children }: {
|
|
121
|
+
view: EditorView;
|
|
122
|
+
acceptedTypes: readonly string[];
|
|
123
|
+
children: ReactNode;
|
|
124
|
+
}) {
|
|
125
|
+
const binding = useMemo(() => createPortBinding(view, acceptedTypes), [view, acceptedTypes]);
|
|
126
|
+
return createElement(EditorViewProvider, { binding, children });
|
|
127
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Fragment, createElement, useEffect, useMemo, type ReactNode } from "react";
|
|
2
|
+
import { CommandsProvider, useCommands, type ContributionManifest } from "../commands";
|
|
3
|
+
import { resetLayoutCommandId } from "../editor-common";
|
|
4
|
+
import type { EditorView } from "../view";
|
|
5
|
+
|
|
6
|
+
export function bindViewCommands(view: EditorView, execute: (commandId: string) => Promise<void>): () => void {
|
|
7
|
+
return view.onCommand((commandId) => {
|
|
8
|
+
// useHostLayout reloads the saved layout. The registry has no handler for this id.
|
|
9
|
+
if (commandId === resetLayoutCommandId) return;
|
|
10
|
+
void execute(commandId);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function shortcutForwarder(view: EditorView) {
|
|
15
|
+
return (binding: { key: string; commandId: string }): boolean => view.sendShortcut(binding);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ViewCommands({ view }: { view: EditorView }) {
|
|
19
|
+
const { execute } = useCommands();
|
|
20
|
+
useEffect(() => bindViewCommands(view, execute), [view, execute]);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function PortCommandsProvider({ view, manifests, children }: {
|
|
25
|
+
view: EditorView;
|
|
26
|
+
manifests: ContributionManifest[];
|
|
27
|
+
children: ReactNode;
|
|
28
|
+
}) {
|
|
29
|
+
const forward = useMemo(() => shortcutForwarder(view), [view]);
|
|
30
|
+
return createElement(CommandsProvider, {
|
|
31
|
+
manifests,
|
|
32
|
+
onKeybinding: forward,
|
|
33
|
+
children: createElement(Fragment, null,
|
|
34
|
+
createElement(ViewCommands, { view }), children),
|
|
35
|
+
});
|
|
36
|
+
}
|
package/src/view/port.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { EditorPort } from "../channel";
|
|
2
|
+
|
|
3
|
+
type PortMessage = { readonly data: unknown };
|
|
4
|
+
|
|
5
|
+
/** Adapts a platform MessagePort to the protocol EditorPort. */
|
|
6
|
+
export function editorPort(port: MessagePort): EditorPort {
|
|
7
|
+
const listeners = new Map<
|
|
8
|
+
(event: PortMessage) => void,
|
|
9
|
+
(event: PortMessage) => void
|
|
10
|
+
>();
|
|
11
|
+
return {
|
|
12
|
+
postMessage(data: unknown) {
|
|
13
|
+
port.postMessage(data);
|
|
14
|
+
},
|
|
15
|
+
start() {
|
|
16
|
+
port.start();
|
|
17
|
+
},
|
|
18
|
+
close() {
|
|
19
|
+
port.close();
|
|
20
|
+
},
|
|
21
|
+
addEventListener(_type, listener) {
|
|
22
|
+
const wrapped = (event: PortMessage) => {
|
|
23
|
+
listener({ data: event.data });
|
|
24
|
+
};
|
|
25
|
+
listeners.set(listener, wrapped);
|
|
26
|
+
port.addEventListener("message", wrapped);
|
|
27
|
+
},
|
|
28
|
+
removeEventListener(_type, listener) {
|
|
29
|
+
const wrapped = listeners.get(listener);
|
|
30
|
+
if (!wrapped) return;
|
|
31
|
+
listeners.delete(listener);
|
|
32
|
+
port.removeEventListener("message", wrapped);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { EditorView } from "../view";
|
|
2
|
+
|
|
3
|
+
export function bindThemeClass(view: EditorView, root: {
|
|
4
|
+
classList: { toggle(token: string, force: boolean): unknown };
|
|
5
|
+
}): () => void {
|
|
6
|
+
function update() {
|
|
7
|
+
root.classList.toggle("dark", view.getSnapshot().theme !== "light");
|
|
8
|
+
}
|
|
9
|
+
update();
|
|
10
|
+
return view.subscribe(update);
|
|
11
|
+
}
|