@openeditor/native 0.0.33 → 0.0.35
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/README.md +81 -58
- package/dist/controller.d.ts +79 -0
- package/dist/controller.js +254 -0
- package/dist/index.d.ts +6 -101
- package/dist/index.js +4 -910
- package/dist/native-editor.d.ts +48 -0
- package/dist/native-editor.js +214 -0
- package/dist/native-layout.d.ts +20 -0
- package/dist/native-layout.js +15 -0
- package/dist/native-provider.d.ts +9 -0
- package/dist/native-provider.js +9 -0
- package/dist/theme.d.ts +32 -2
- package/dist/theme.js +56 -37
- package/dist/toolbar-host.d.ts +20 -0
- package/dist/toolbar-host.js +211 -0
- package/dist/toolbar-items.d.ts +109 -0
- package/dist/toolbar-items.js +57 -0
- package/dist/toolbar-surface.d.ts +17 -0
- package/dist/toolbar-surface.js +33 -0
- package/package.json +18 -19
- package/plugin.cjs +79 -0
- package/dist/document.d.ts +0 -7
- package/dist/document.js +0 -69
- package/dist/emoji-button.d.ts +0 -8
- package/dist/emoji-button.js +0 -14
- package/dist/emoji-data.d.ts +0 -44
- package/dist/emoji-data.js +0 -50
- package/dist/emoji-picker.d.ts +0 -12
- package/dist/emoji-picker.js +0 -67
- package/dist/selection.d.ts +0 -3
- package/dist/selection.js +0 -32
- package/dist/toolbar.d.ts +0 -30
- package/dist/toolbar.js +0 -282
package/README.md
CHANGED
|
@@ -1,78 +1,101 @@
|
|
|
1
1
|
# `@openeditor/native`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React Native host for the offline OpenEditor embedded runtime.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The document, selection, history, and DOM NodeViews live inside one internally
|
|
6
|
+
scrollable WebView. React Native owns the keyboard toolbar and application
|
|
7
|
+
effects such as pickers, sheets, and navigation. The bridge sends commands and
|
|
8
|
+
lightweight state/revision events; document JSON only crosses it when the host
|
|
9
|
+
explicitly calls `getDocument()` or `flushDocument()`.
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
On iOS, the host hides WKWebView's default previous/next/Done accessory bar so
|
|
12
|
+
the OpenEditor toolbar is the only bar above the software keyboard. The SDK
|
|
13
|
+
sticks that toolbar to the keyboard, reserves its document inset, and enables
|
|
14
|
+
interactive keyboard dismissal while the editor scrolls. Consumers can opt
|
|
15
|
+
back into the system bar with
|
|
16
|
+
`webViewProps={{ hideKeyboardAccessoryView: false }}`.
|
|
8
17
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- `OpenEditorNativeViewer` for read-only rendering
|
|
12
|
-
- `OpenEditorNativeEmojiPicker` for standalone native emoji selection
|
|
13
|
-
- native document bridge helpers where renderer adaptation is required
|
|
18
|
+
The `theme` prop accepts the same OpenEditor theme-token contract used by the
|
|
19
|
+
web React surface.
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
opens the native picker and commits the selected Unicode value through editor
|
|
17
|
-
history. Inline emoji continue to use the device keyboard.
|
|
21
|
+
## Host integration
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
Install the native peer dependencies:
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
- `react-native-keyboard-controller` (optional but recommended for keyboard toolbar placement)
|
|
25
|
+
```sh
|
|
26
|
+
pnpm add expo-glass-effect react-native-keyboard-controller react-native-reanimated react-native-svg react-native-webview
|
|
27
|
+
```
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
For Expo, install the SDK plugin so iOS WebViews receive interactive keyboard
|
|
30
|
+
dismissal:
|
|
27
31
|
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const editor = useRef<OpenEditorNativeController>(null);
|
|
34
|
-
return (
|
|
35
|
-
<OpenEditorNative
|
|
36
|
-
ref={editor}
|
|
37
|
-
initialDocument={document}
|
|
38
|
-
onChange={(document) => {
|
|
39
|
-
console.log(document);
|
|
40
|
-
}}
|
|
41
|
-
/>
|
|
42
|
-
);
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"expo": {
|
|
35
|
+
"plugins": ["@openeditor/native/plugin"]
|
|
36
|
+
}
|
|
43
37
|
}
|
|
44
38
|
```
|
|
45
39
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
undoable programmatic replacement. Undo and redo delegate directly to the Rust
|
|
49
|
-
editor engine; the React wrapper does not keep document snapshots.
|
|
50
|
-
|
|
51
|
-
Pass `enabledBlocks` to restrict authoring. Page and attachment controls are
|
|
52
|
-
automatically omitted until their required host runtimes are present, while
|
|
53
|
-
existing nodes remain readable.
|
|
54
|
-
|
|
55
|
-
## Standalone emoji picker
|
|
56
|
-
|
|
57
|
-
The editor wires this automatically for callouts and pages. Hosts can also use
|
|
58
|
-
the same picker for page headers or other product UI:
|
|
40
|
+
Rebuild the native application after adding the plugin or a native dependency.
|
|
41
|
+
Mount the SDK provider once at the app root:
|
|
59
42
|
|
|
60
43
|
```tsx
|
|
61
|
-
import {
|
|
44
|
+
import { OpenEditorNativeProvider } from "@openeditor/native";
|
|
62
45
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
onRequestClose={() => setPickerOpen(false)}
|
|
67
|
-
/>;
|
|
46
|
+
export default function RootLayout() {
|
|
47
|
+
return <OpenEditorNativeProvider>{/* app routes */}</OpenEditorNativeProvider>;
|
|
48
|
+
}
|
|
68
49
|
```
|
|
69
50
|
|
|
70
|
-
The
|
|
71
|
-
|
|
72
|
-
for scrolling, search, and touch handling. Long-press an emoji that supports
|
|
73
|
-
skin tones to choose a variant from the platform-native context menu.
|
|
51
|
+
The self-contained offline runtime is packaged with the component, so Metro
|
|
52
|
+
asset customization is not required:
|
|
74
53
|
|
|
75
|
-
|
|
54
|
+
```tsx
|
|
55
|
+
const editor = useRef<OpenEditorNativeController>(null);
|
|
56
|
+
|
|
57
|
+
<OpenEditorNative
|
|
58
|
+
ref={editor}
|
|
59
|
+
initialDocument={document}
|
|
60
|
+
contentInsets={{ top: transparentHeaderHeight, bottom: 16 }}
|
|
61
|
+
onDocumentChanged={({ documentRevision }) => {
|
|
62
|
+
scheduleAutosave(async () => {
|
|
63
|
+
const snapshot = await editor.current?.getDocument({
|
|
64
|
+
minimumRevision: documentRevision,
|
|
65
|
+
});
|
|
66
|
+
if (snapshot) await save(snapshot.document);
|
|
67
|
+
});
|
|
68
|
+
}}
|
|
69
|
+
nativeEffects={{
|
|
70
|
+
pickAttachment: pickUploadAndReturnDurableAttachment,
|
|
71
|
+
createPage: openCreatePageSheet,
|
|
72
|
+
updatePage: updatePageMetadata,
|
|
73
|
+
openPage: navigateToPage,
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
76
|
+
```
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
The default toolbar is a keyboard-sticky, inset pill that meets the keyboard
|
|
79
|
+
edge without a synthetic spacer. It uses native iOS glass and a theme-aware
|
|
80
|
+
material fallback elsewhere. A custom `renderToolbar` returns only the toolbar
|
|
81
|
+
content; the SDK still owns its glass surface and positioning, so do not add
|
|
82
|
+
another `KeyboardStickyView` or outer background. Use `toolbarContentInset`
|
|
83
|
+
when the custom toolbar needs more than the default 76-point document clearance,
|
|
84
|
+
and `toolbarOffset` only when the keyboard edge itself needs a different gap.
|
|
85
|
+
|
|
86
|
+
The editor host must reach the screen's bottom edge. Do not wrap the editor in
|
|
87
|
+
a bottom-padding `SafeAreaView`; the keyboard owns that edge while editing, and
|
|
88
|
+
reserving the home-indicator inset there creates a visible toolbar gap.
|
|
89
|
+
|
|
90
|
+
The block picker derives its available page, image, and attachment commands
|
|
91
|
+
from the supplied effect handlers. If the application cannot upload media to a
|
|
92
|
+
durable URL, omit the corresponding picker effect rather than returning a
|
|
93
|
+
temporary `file://` URI.
|
|
94
|
+
|
|
95
|
+
Do not wrap the component in a React Native `ScrollView`; give it a bounded
|
|
96
|
+
flex layout so the WebView remains the only document scroll owner. Before a
|
|
97
|
+
screen transition or background save, call `flushDocument()` to commit pending
|
|
98
|
+
composition and receive the current snapshot.
|
|
99
|
+
|
|
100
|
+
For a read-only surface, use
|
|
101
|
+
`<OpenEditorNative initialDocument={document} editable={false} showToolbar={false} />`.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { OpenEditorDocument } from "@openeditor/core";
|
|
2
|
+
import { type OpenEditorCommandAck, type OpenEditorNativeAttachmentResult, type OpenEditorNativeEffectRequest, type OpenEditorNativeEffectResult, type OpenEditorNativeImageResult, type OpenEditorNativePageResult, type OpenEditorRuntimeCommand, type OpenEditorRuntimeMessage, type OpenEditorRuntimeState } from "@openeditor/embedded-runtime";
|
|
3
|
+
export declare class OpenEditorNativeLifecycleCancellationError extends Error {
|
|
4
|
+
readonly code = "OPENEDITOR_NATIVE_LIFECYCLE_CANCELLED";
|
|
5
|
+
constructor(message: string);
|
|
6
|
+
}
|
|
7
|
+
export declare const isOpenEditorNativeLifecycleCancellation: (error: unknown) => error is OpenEditorNativeLifecycleCancellationError;
|
|
8
|
+
export type OpenEditorNativeEffectHandlers = {
|
|
9
|
+
pickImage?: (effect: Extract<OpenEditorNativeEffectRequest["effect"], {
|
|
10
|
+
type: "pickImage";
|
|
11
|
+
}>) => Promise<OpenEditorNativeEffectResult<OpenEditorNativeImageResult>> | OpenEditorNativeEffectResult<OpenEditorNativeImageResult>;
|
|
12
|
+
pickAttachment?: (effect: Extract<OpenEditorNativeEffectRequest["effect"], {
|
|
13
|
+
type: "pickAttachment";
|
|
14
|
+
}>) => Promise<OpenEditorNativeEffectResult<OpenEditorNativeAttachmentResult>> | OpenEditorNativeEffectResult<OpenEditorNativeAttachmentResult>;
|
|
15
|
+
createPage?: (effect: Extract<OpenEditorNativeEffectRequest["effect"], {
|
|
16
|
+
type: "createPage";
|
|
17
|
+
}>) => Promise<OpenEditorNativeEffectResult<OpenEditorNativePageResult>> | OpenEditorNativeEffectResult<OpenEditorNativePageResult>;
|
|
18
|
+
updatePage?: (effect: Extract<OpenEditorNativeEffectRequest["effect"], {
|
|
19
|
+
type: "updatePage";
|
|
20
|
+
}>) => Promise<OpenEditorNativeEffectResult<OpenEditorNativePageResult>> | OpenEditorNativeEffectResult<OpenEditorNativePageResult>;
|
|
21
|
+
openPage?: (effect: Extract<OpenEditorNativeEffectRequest["effect"], {
|
|
22
|
+
type: "openPage";
|
|
23
|
+
}>) => Promise<OpenEditorNativeEffectResult<null>> | OpenEditorNativeEffectResult<null>;
|
|
24
|
+
openAttachment?: (effect: Extract<OpenEditorNativeEffectRequest["effect"], {
|
|
25
|
+
type: "openAttachment";
|
|
26
|
+
}>) => Promise<OpenEditorNativeEffectResult<null>> | OpenEditorNativeEffectResult<null>;
|
|
27
|
+
openUrl?: (effect: Extract<OpenEditorNativeEffectRequest["effect"], {
|
|
28
|
+
type: "openUrl";
|
|
29
|
+
}>) => Promise<OpenEditorNativeEffectResult<null>> | OpenEditorNativeEffectResult<null>;
|
|
30
|
+
};
|
|
31
|
+
export type OpenEditorNativeControllerEvent = Extract<OpenEditorRuntimeMessage, {
|
|
32
|
+
type: "ready" | "stateChanged" | "documentChanged" | "error";
|
|
33
|
+
}>;
|
|
34
|
+
export type OpenEditorDocumentSnapshot = {
|
|
35
|
+
document: OpenEditorDocument;
|
|
36
|
+
revision: number;
|
|
37
|
+
};
|
|
38
|
+
export type OpenEditorNativeController = {
|
|
39
|
+
readonly ready: boolean;
|
|
40
|
+
waitUntilReady: () => Promise<void>;
|
|
41
|
+
getDocument: (options?: {
|
|
42
|
+
minimumRevision?: number;
|
|
43
|
+
}) => Promise<OpenEditorDocumentSnapshot>;
|
|
44
|
+
flushDocument: () => Promise<OpenEditorDocumentSnapshot>;
|
|
45
|
+
setDocument: (document: OpenEditorDocument) => Promise<number>;
|
|
46
|
+
command: (command: OpenEditorRuntimeCommand) => Promise<OpenEditorCommandAck>;
|
|
47
|
+
focus: () => Promise<void>;
|
|
48
|
+
blur: () => Promise<void>;
|
|
49
|
+
subscribe: (listener: (event: OpenEditorNativeControllerEvent) => void) => () => void;
|
|
50
|
+
};
|
|
51
|
+
export type OpenEditorNativeBridgeOptions = {
|
|
52
|
+
sessionId: string;
|
|
53
|
+
requestTimeoutMs?: number;
|
|
54
|
+
send: (message: string) => void;
|
|
55
|
+
getEffectHandlers?: () => OpenEditorNativeEffectHandlers | undefined;
|
|
56
|
+
};
|
|
57
|
+
export declare class OpenEditorNativeBridge implements OpenEditorNativeController {
|
|
58
|
+
#private;
|
|
59
|
+
readonly sessionId: string;
|
|
60
|
+
constructor(options: OpenEditorNativeBridgeOptions);
|
|
61
|
+
get ready(): boolean;
|
|
62
|
+
get runtimeReady(): boolean;
|
|
63
|
+
waitUntilReady: () => Promise<void>;
|
|
64
|
+
getDocument: (options?: {
|
|
65
|
+
minimumRevision?: number;
|
|
66
|
+
}) => Promise<OpenEditorDocumentSnapshot>;
|
|
67
|
+
flushDocument: () => Promise<OpenEditorDocumentSnapshot>;
|
|
68
|
+
setDocument: (document: OpenEditorDocument) => Promise<number>;
|
|
69
|
+
command: (command: OpenEditorRuntimeCommand) => Promise<OpenEditorCommandAck>;
|
|
70
|
+
focus: () => Promise<void>;
|
|
71
|
+
blur: () => Promise<void>;
|
|
72
|
+
subscribe: (listener: (event: OpenEditorNativeControllerEvent) => void) => () => void;
|
|
73
|
+
initialize: (document: OpenEditorDocument, editable: boolean) => Promise<OpenEditorCommandAck>;
|
|
74
|
+
markInitialized: () => void;
|
|
75
|
+
receive: (raw: string) => boolean;
|
|
76
|
+
resetRuntime: () => void;
|
|
77
|
+
dispose: (reason?: string) => void;
|
|
78
|
+
}
|
|
79
|
+
export declare const emptyOpenEditorNativeState: OpenEditorRuntimeState;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { OPENEDITOR_PROTOCOL, OPENEDITOR_PROTOCOL_VERSION, decodeOpenEditorRuntimeMessage, } from "@openeditor/embedded-runtime";
|
|
2
|
+
export class OpenEditorNativeLifecycleCancellationError extends Error {
|
|
3
|
+
code = "OPENEDITOR_NATIVE_LIFECYCLE_CANCELLED";
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "OpenEditorNativeLifecycleCancellationError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export const isOpenEditorNativeLifecycleCancellation = (error) => error instanceof OpenEditorNativeLifecycleCancellationError;
|
|
10
|
+
export class OpenEditorNativeBridge {
|
|
11
|
+
#counter = 0;
|
|
12
|
+
#disposed = false;
|
|
13
|
+
#documentRevision = 0;
|
|
14
|
+
#effectHandlers;
|
|
15
|
+
#listeners = new Set();
|
|
16
|
+
#pending = new Map();
|
|
17
|
+
#ready = false;
|
|
18
|
+
#readyWaiters = new Set();
|
|
19
|
+
#revisionWaiters = new Set();
|
|
20
|
+
#runtimeReady = false;
|
|
21
|
+
#requestTimeoutMs;
|
|
22
|
+
#send;
|
|
23
|
+
sessionId;
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.sessionId = options.sessionId;
|
|
26
|
+
this.#send = options.send;
|
|
27
|
+
this.#effectHandlers = options.getEffectHandlers;
|
|
28
|
+
this.#requestTimeoutMs = options.requestTimeoutMs ?? 10_000;
|
|
29
|
+
}
|
|
30
|
+
get ready() { return this.#ready; }
|
|
31
|
+
get runtimeReady() { return this.#runtimeReady; }
|
|
32
|
+
waitUntilReady = () => this.#waitForReady();
|
|
33
|
+
getDocument = async (options) => {
|
|
34
|
+
await this.#waitForReady();
|
|
35
|
+
if ((options?.minimumRevision ?? 0) > this.#documentRevision)
|
|
36
|
+
await this.#waitForRevision(options.minimumRevision);
|
|
37
|
+
const result = await this.#sendCommand({ type: "requestDocument" }, true);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
flushDocument = async () => {
|
|
41
|
+
await this.blur();
|
|
42
|
+
return this.getDocument();
|
|
43
|
+
};
|
|
44
|
+
setDocument = async (document) => (await this.command({ type: "replaceDocument", document })).documentRevision;
|
|
45
|
+
command = async (command) => {
|
|
46
|
+
await this.#waitForReady();
|
|
47
|
+
return this.#sendCommand(command, false);
|
|
48
|
+
};
|
|
49
|
+
focus = async () => { await this.command({ type: "focus" }); };
|
|
50
|
+
blur = async () => { await this.command({ type: "blur" }); };
|
|
51
|
+
subscribe = (listener) => {
|
|
52
|
+
this.#listeners.add(listener);
|
|
53
|
+
return () => { this.#listeners.delete(listener); };
|
|
54
|
+
};
|
|
55
|
+
initialize = (document, editable) => this.#sendCommand({ type: "initialize", document, editable }, false);
|
|
56
|
+
markInitialized = () => {
|
|
57
|
+
if (this.#ready)
|
|
58
|
+
return;
|
|
59
|
+
this.#ready = true;
|
|
60
|
+
for (const waiter of this.#readyWaiters) {
|
|
61
|
+
clearTimeout(waiter.timer);
|
|
62
|
+
waiter.resolve();
|
|
63
|
+
}
|
|
64
|
+
this.#readyWaiters.clear();
|
|
65
|
+
};
|
|
66
|
+
receive = (raw) => {
|
|
67
|
+
if (this.#disposed)
|
|
68
|
+
return false;
|
|
69
|
+
let message;
|
|
70
|
+
try {
|
|
71
|
+
message = decodeOpenEditorRuntimeMessage(raw);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
if (message.sessionId !== this.sessionId)
|
|
77
|
+
return false;
|
|
78
|
+
if (message.type === "ready") {
|
|
79
|
+
this.#runtimeReady = true;
|
|
80
|
+
this.#updateRevision(message.state.documentRevision);
|
|
81
|
+
this.#emit(message);
|
|
82
|
+
}
|
|
83
|
+
else if (message.type === "ack")
|
|
84
|
+
this.#receiveAck(message);
|
|
85
|
+
else if (message.type === "documentSnapshot")
|
|
86
|
+
this.#receiveSnapshot(message);
|
|
87
|
+
else if (message.type === "nativeEffectRequest")
|
|
88
|
+
void this.#receiveEffectRequest(message);
|
|
89
|
+
else if (message.type === "error")
|
|
90
|
+
this.#receiveError(message);
|
|
91
|
+
else {
|
|
92
|
+
if (message.type === "stateChanged")
|
|
93
|
+
this.#updateRevision(message.state.documentRevision);
|
|
94
|
+
if (message.type === "documentChanged")
|
|
95
|
+
this.#updateRevision(message.documentRevision);
|
|
96
|
+
this.#emit(message);
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
};
|
|
100
|
+
resetRuntime = () => {
|
|
101
|
+
const replacesActiveRuntime = this.#runtimeReady || this.#ready;
|
|
102
|
+
this.#runtimeReady = false;
|
|
103
|
+
this.#ready = false;
|
|
104
|
+
this.#documentRevision = 0;
|
|
105
|
+
// onLoadStart is also emitted for the first, expected WebView load. Commands
|
|
106
|
+
// queued before that first load must remain waiting for initialization.
|
|
107
|
+
if (replacesActiveRuntime) {
|
|
108
|
+
this.#rejectPending(new OpenEditorNativeLifecycleCancellationError("OpenEditor runtime reloaded before responding."));
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
dispose = (reason = "OpenEditor native host was disposed.") => {
|
|
112
|
+
this.#disposed = true;
|
|
113
|
+
this.#runtimeReady = false;
|
|
114
|
+
this.#ready = false;
|
|
115
|
+
this.#rejectPending(new OpenEditorNativeLifecycleCancellationError(reason));
|
|
116
|
+
this.#listeners.clear();
|
|
117
|
+
};
|
|
118
|
+
#waitForReady() {
|
|
119
|
+
if (this.#disposed)
|
|
120
|
+
return Promise.reject(new Error("OpenEditor native host was disposed."));
|
|
121
|
+
if (this.#ready)
|
|
122
|
+
return Promise.resolve();
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
const waiter = {
|
|
125
|
+
resolve,
|
|
126
|
+
reject,
|
|
127
|
+
timer: setTimeout(() => {
|
|
128
|
+
this.#readyWaiters.delete(waiter);
|
|
129
|
+
reject(new Error("OpenEditor runtime did not become ready."));
|
|
130
|
+
}, this.#requestTimeoutMs),
|
|
131
|
+
};
|
|
132
|
+
this.#readyWaiters.add(waiter);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
#waitForRevision(minimum) {
|
|
136
|
+
if (this.#documentRevision >= minimum)
|
|
137
|
+
return Promise.resolve();
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
const waiter = {
|
|
140
|
+
minimum,
|
|
141
|
+
resolve,
|
|
142
|
+
reject,
|
|
143
|
+
timer: setTimeout(() => {
|
|
144
|
+
this.#revisionWaiters.delete(waiter);
|
|
145
|
+
reject(new Error(`OpenEditor document revision ${minimum} was not reached.`));
|
|
146
|
+
}, this.#requestTimeoutMs),
|
|
147
|
+
};
|
|
148
|
+
this.#revisionWaiters.add(waiter);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
#nextId() { this.#counter += 1; return `rn:${this.#counter}`; }
|
|
152
|
+
#sendCommand(command, needsSnapshot) {
|
|
153
|
+
const id = this.#nextId();
|
|
154
|
+
const envelope = { protocol: OPENEDITOR_PROTOCOL, version: OPENEDITOR_PROTOCOL_VERSION, sessionId: this.sessionId, type: "command", id, command };
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
const timer = setTimeout(() => { this.#pending.delete(id); reject(new Error(`OpenEditor runtime command timed out: ${command.type}`)); }, this.#requestTimeoutMs);
|
|
157
|
+
this.#pending.set(id, { command: command.type, resolve: (value) => resolve(value), reject, timer });
|
|
158
|
+
this.#send(JSON.stringify(envelope));
|
|
159
|
+
if (!needsSnapshot)
|
|
160
|
+
return;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
#receiveAck(message) {
|
|
164
|
+
this.#updateRevision(message.documentRevision);
|
|
165
|
+
const pending = this.#pending.get(message.commandId);
|
|
166
|
+
if (!pending)
|
|
167
|
+
return;
|
|
168
|
+
pending.ack = message;
|
|
169
|
+
if (pending.command === "requestDocument")
|
|
170
|
+
this.#finishSnapshot(message.commandId, pending);
|
|
171
|
+
else
|
|
172
|
+
this.#finish(message.commandId, pending, message);
|
|
173
|
+
}
|
|
174
|
+
#receiveSnapshot(message) {
|
|
175
|
+
this.#updateRevision(message.documentRevision);
|
|
176
|
+
const pending = this.#pending.get(message.requestId);
|
|
177
|
+
if (!pending || pending.command !== "requestDocument")
|
|
178
|
+
return;
|
|
179
|
+
pending.snapshot = message;
|
|
180
|
+
this.#finishSnapshot(message.requestId, pending);
|
|
181
|
+
}
|
|
182
|
+
#finishSnapshot(id, pending) {
|
|
183
|
+
if (!pending.ack || !pending.snapshot)
|
|
184
|
+
return;
|
|
185
|
+
this.#finish(id, pending, { document: pending.snapshot.document, revision: pending.snapshot.documentRevision });
|
|
186
|
+
}
|
|
187
|
+
#finish(id, pending, value) {
|
|
188
|
+
this.#pending.delete(id);
|
|
189
|
+
clearTimeout(pending.timer);
|
|
190
|
+
pending.resolve(value);
|
|
191
|
+
}
|
|
192
|
+
#receiveError(message) {
|
|
193
|
+
if (message.commandId) {
|
|
194
|
+
const pending = this.#pending.get(message.commandId);
|
|
195
|
+
if (pending) {
|
|
196
|
+
this.#pending.delete(message.commandId);
|
|
197
|
+
clearTimeout(pending.timer);
|
|
198
|
+
pending.reject(new Error(`${message.code}: ${message.message}`));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
this.#emit(message);
|
|
202
|
+
}
|
|
203
|
+
async #receiveEffectRequest(message) {
|
|
204
|
+
const handler = this.#effectHandlers?.()?.[message.effect.type];
|
|
205
|
+
let result;
|
|
206
|
+
try {
|
|
207
|
+
result = handler ? await handler(message.effect) : { status: "error", code: "missingHandler", message: `No native handler for ${message.effect.type}.` };
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
result = { status: "error", code: "nativeEffectFailed", message: error instanceof Error ? error.message : "Native effect failed." };
|
|
211
|
+
}
|
|
212
|
+
const response = { protocol: OPENEDITOR_PROTOCOL, version: OPENEDITOR_PROTOCOL_VERSION, sessionId: this.sessionId, type: "nativeEffectResponse", id: this.#nextId(), requestId: message.id, effect: message.effect.type, result };
|
|
213
|
+
this.#send(JSON.stringify(response));
|
|
214
|
+
}
|
|
215
|
+
#updateRevision(revision) {
|
|
216
|
+
this.#documentRevision = Math.max(this.#documentRevision, revision);
|
|
217
|
+
for (const waiter of this.#revisionWaiters)
|
|
218
|
+
if (this.#documentRevision >= waiter.minimum) {
|
|
219
|
+
this.#revisionWaiters.delete(waiter);
|
|
220
|
+
clearTimeout(waiter.timer);
|
|
221
|
+
waiter.resolve();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
#emit(message) { for (const listener of this.#listeners)
|
|
225
|
+
listener(message); }
|
|
226
|
+
#rejectPending(error) {
|
|
227
|
+
for (const pending of this.#pending.values()) {
|
|
228
|
+
clearTimeout(pending.timer);
|
|
229
|
+
pending.reject(error);
|
|
230
|
+
}
|
|
231
|
+
this.#pending.clear();
|
|
232
|
+
for (const waiter of this.#readyWaiters) {
|
|
233
|
+
clearTimeout(waiter.timer);
|
|
234
|
+
waiter.reject(error);
|
|
235
|
+
}
|
|
236
|
+
this.#readyWaiters.clear();
|
|
237
|
+
for (const waiter of this.#revisionWaiters) {
|
|
238
|
+
clearTimeout(waiter.timer);
|
|
239
|
+
waiter.reject(error);
|
|
240
|
+
}
|
|
241
|
+
this.#revisionWaiters.clear();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
export const emptyOpenEditorNativeState = {
|
|
245
|
+
revision: 0,
|
|
246
|
+
documentRevision: 0,
|
|
247
|
+
activeMarks: [],
|
|
248
|
+
activeNodes: ["paragraph"],
|
|
249
|
+
canUndo: false,
|
|
250
|
+
canRedo: false,
|
|
251
|
+
editable: true,
|
|
252
|
+
focused: false,
|
|
253
|
+
selection: { type: "text", anchor: 1, head: 1 },
|
|
254
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,101 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
export
|
|
7
|
-
export type OpenEditorNativeProps = OpenEditorAuthoringCapabilities & {
|
|
8
|
-
initialDocument?: OpenEditorDocument;
|
|
9
|
-
editable?: boolean;
|
|
10
|
-
placeholder?: string;
|
|
11
|
-
showToolbar?: boolean;
|
|
12
|
-
theme?: Partial<OpenEditorNativeTheme>;
|
|
13
|
-
toolbarPlacement?: NativeRichTextEditorToolbarPlacement;
|
|
14
|
-
onChange?: (document: OpenEditorDocument) => void;
|
|
15
|
-
pageRuntime?: OpenEditorPageRuntime;
|
|
16
|
-
attachmentRuntime?: OpenEditorAttachmentRuntime<any>;
|
|
17
|
-
};
|
|
18
|
-
export type OpenEditorNativeViewerProps = {
|
|
19
|
-
document: OpenEditorDocument;
|
|
20
|
-
theme?: Partial<OpenEditorNativeTheme>;
|
|
21
|
-
pageRuntime?: OpenEditorPageRuntime;
|
|
22
|
-
attachmentRuntime?: OpenEditorAttachmentRuntime<any>;
|
|
23
|
-
renderers?: Partial<Record<string, OpenEditorNativeViewerRenderer>>;
|
|
24
|
-
};
|
|
25
|
-
export type OpenEditorNativeController = {
|
|
26
|
-
getContent: () => OpenEditorDocument;
|
|
27
|
-
setContent: (document: OpenEditorDocument) => void;
|
|
28
|
-
undo: () => void;
|
|
29
|
-
redo: () => void;
|
|
30
|
-
};
|
|
31
|
-
export type OpenEditorNativeToolbarAction = {
|
|
32
|
-
key: string;
|
|
33
|
-
label: string;
|
|
34
|
-
onPress?: (key: string) => void;
|
|
35
|
-
};
|
|
36
|
-
export type OpenEditorNativeToolbarProps = {
|
|
37
|
-
actions?: readonly OpenEditorNativeToolbarAction[];
|
|
38
|
-
onActionPress?: (key: string) => void;
|
|
39
|
-
theme?: Partial<OpenEditorNativeTheme>;
|
|
40
|
-
};
|
|
41
|
-
export type OpenEditorNativeViewerRenderer = (context: {
|
|
42
|
-
node: ProseMirrorNode;
|
|
43
|
-
children: ReactNode;
|
|
44
|
-
theme: Required<OpenEditorNativeTheme>;
|
|
45
|
-
}) => ReactNode;
|
|
46
|
-
export type OpenEditorNativeToolbarTheme = {
|
|
47
|
-
height?: number;
|
|
48
|
-
};
|
|
49
|
-
export type OpenEditorNativeTheme = {
|
|
50
|
-
background: string;
|
|
51
|
-
surface: string;
|
|
52
|
-
surfaceMuted: string;
|
|
53
|
-
blockSurface?: string;
|
|
54
|
-
text: string;
|
|
55
|
-
textSoft?: string;
|
|
56
|
-
muted: string;
|
|
57
|
-
placeholder?: string;
|
|
58
|
-
border: string;
|
|
59
|
-
borderStrong?: string;
|
|
60
|
-
structuralLine?: string;
|
|
61
|
-
accent: string;
|
|
62
|
-
accentText: string;
|
|
63
|
-
accentStrong?: string;
|
|
64
|
-
primary: string;
|
|
65
|
-
primaryText: string;
|
|
66
|
-
codeBackground?: string;
|
|
67
|
-
codeText?: string;
|
|
68
|
-
debugBackground?: string;
|
|
69
|
-
debugText?: string;
|
|
70
|
-
debugHeading?: string;
|
|
71
|
-
debugBorder?: string;
|
|
72
|
-
debugButtonBackground?: string;
|
|
73
|
-
success?: string;
|
|
74
|
-
shadow?: string;
|
|
75
|
-
backdrop: string;
|
|
76
|
-
toolbar?: OpenEditorNativeToolbarTheme;
|
|
77
|
-
};
|
|
78
|
-
export declare const OpenEditorNativeToolbar: ({ actions, onActionPress, theme, }: OpenEditorNativeToolbarProps) => import("react").JSX.Element;
|
|
79
|
-
export type OpenEditorNativePageHeaderProps = {
|
|
80
|
-
page: OpenEditorPageSnapshot;
|
|
81
|
-
runtime: OpenEditorPageRuntime;
|
|
82
|
-
theme?: Partial<OpenEditorNativeTheme>;
|
|
83
|
-
onPageChange?: (page: OpenEditorPageSnapshot) => void;
|
|
84
|
-
};
|
|
85
|
-
/** Canonical native page-title and page-icon editor for opened page surfaces. */
|
|
86
|
-
export declare const OpenEditorNativePageHeader: ({ page, runtime, theme, onPageChange }: OpenEditorNativePageHeaderProps) => import("react").JSX.Element;
|
|
87
|
-
export declare const OpenEditorNative: import("react").ForwardRefExoticComponent<OpenEditorAuthoringCapabilities & {
|
|
88
|
-
initialDocument?: OpenEditorDocument;
|
|
89
|
-
editable?: boolean;
|
|
90
|
-
placeholder?: string;
|
|
91
|
-
showToolbar?: boolean;
|
|
92
|
-
theme?: Partial<OpenEditorNativeTheme>;
|
|
93
|
-
toolbarPlacement?: NativeRichTextEditorToolbarPlacement;
|
|
94
|
-
onChange?: (document: OpenEditorDocument) => void;
|
|
95
|
-
pageRuntime?: OpenEditorPageRuntime;
|
|
96
|
-
attachmentRuntime?: OpenEditorAttachmentRuntime<any>;
|
|
97
|
-
} & import("react").RefAttributes<OpenEditorNativeController>>;
|
|
98
|
-
export declare const OpenEditorNativeViewer: ({ document, theme, renderers, pageRuntime, attachmentRuntime, }: OpenEditorNativeViewerProps) => import("react").JSX.Element;
|
|
99
|
-
export { createNativeEditorDocument, fromNativeEditorDocument, sanitizeNativeEditorDocument, toNativeEditorDocument, } from "./document.js";
|
|
100
|
-
export { createOpenEditorNativeToolbarItems, defaultDocumentForToolbarAction, nativeToolbarParityCoverage, openEditorNativeToolbarActionKeys, openEditorNativeToolbarItems, } from "./toolbar.js";
|
|
101
|
-
export type { OpenEditorNativeToolbarActionKey } from "./toolbar.js";
|
|
1
|
+
export { OpenEditorNative, type OpenEditorNativeContentInsets, type OpenEditorNativeProps, type OpenEditorNativeTheme, } from "./native-editor.js";
|
|
2
|
+
export { OpenEditorNativeProvider, type OpenEditorNativeProviderProps, } from "./native-provider.js";
|
|
3
|
+
export { type OpenEditorNativeController, type OpenEditorNativeControllerEvent, type OpenEditorNativeEffectHandlers, } from "./controller.js";
|
|
4
|
+
export { OpenEditorNativeToolbar, defaultOpenEditorNativeToolbarItems, openEditorNativeBlockPickerItems, type OpenEditorNativeToolbarItem, type OpenEditorNativeToolbarProps, } from "./toolbar-host.js";
|
|
5
|
+
export { resolveOpenEditorNativeTheme, type OpenEditorNativeColorScheme, type ResolvedOpenEditorNativeTheme, } from "./theme.js";
|
|
6
|
+
export type { OpenEditorRuntimeCommand, OpenEditorRuntimeState } from "@openeditor/embedded-runtime";
|