@openeditor/native 0.0.32 → 0.0.34
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 +47 -65
- package/dist/controller.d.ts +79 -0
- package/dist/controller.js +254 -0
- package/dist/index.d.ts +4 -101
- package/dist/index.js +2 -910
- package/dist/native-editor.d.ts +43 -0
- package/dist/native-editor.js +171 -0
- package/dist/toolbar-host.d.ts +17 -0
- package/dist/toolbar-host.js +206 -0
- package/dist/toolbar-items.d.ts +88 -0
- package/dist/toolbar-items.js +52 -0
- package/package.json +6 -18
- 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/theme.d.ts +0 -2
- package/dist/theme.js +0 -39
- package/dist/toolbar.d.ts +0 -30
- package/dist/toolbar.js +0 -282
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { OpenEditorDocument } from "@openeditor/core";
|
|
2
|
+
import { type OpenEditorRuntimeState, type OpenEditorNativeEffectName, type OpenEditorTheme } from "@openeditor/embedded-runtime";
|
|
3
|
+
import { type ReactNode, type RefAttributes } from "react";
|
|
4
|
+
import { type StyleProp, type ViewStyle } from "react-native";
|
|
5
|
+
import { type WebViewProps } from "react-native-webview";
|
|
6
|
+
import { type OpenEditorNativeController, type OpenEditorNativeEffectHandlers } from "./controller.js";
|
|
7
|
+
import { type OpenEditorNativeToolbarItem } from "./toolbar-host.js";
|
|
8
|
+
export type OpenEditorNativeTheme = Partial<OpenEditorTheme>;
|
|
9
|
+
export type OpenEditorNativeContentInsets = {
|
|
10
|
+
top: number;
|
|
11
|
+
right: number;
|
|
12
|
+
bottom: number;
|
|
13
|
+
left: number;
|
|
14
|
+
};
|
|
15
|
+
type EdgeInsets = OpenEditorNativeContentInsets;
|
|
16
|
+
type OwnedWebViewProps = "source" | "onMessage" | "style" | "scrollEnabled" | "nestedScrollEnabled";
|
|
17
|
+
export type OpenEditorNativeProps = {
|
|
18
|
+
initialDocument: OpenEditorDocument;
|
|
19
|
+
editable?: boolean;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
theme?: OpenEditorNativeTheme;
|
|
22
|
+
contentInsets?: Partial<EdgeInsets>;
|
|
23
|
+
showToolbar?: boolean;
|
|
24
|
+
toolbarItems?: readonly OpenEditorNativeToolbarItem[];
|
|
25
|
+
renderToolbar?: (context: {
|
|
26
|
+
availableNativeEffects: readonly OpenEditorNativeEffectName[];
|
|
27
|
+
controller: OpenEditorNativeController;
|
|
28
|
+
state: OpenEditorRuntimeState;
|
|
29
|
+
}) => ReactNode;
|
|
30
|
+
nativeEffects?: OpenEditorNativeEffectHandlers;
|
|
31
|
+
onDocumentChanged?: (change: {
|
|
32
|
+
documentRevision: number;
|
|
33
|
+
origin: "input" | "command";
|
|
34
|
+
}) => void;
|
|
35
|
+
onReady?: (controller: OpenEditorNativeController) => void;
|
|
36
|
+
onError?: (error: Error) => void;
|
|
37
|
+
keyboardVerticalOffset?: number;
|
|
38
|
+
keyboardAvoidanceEnabled?: boolean;
|
|
39
|
+
style?: StyleProp<ViewStyle>;
|
|
40
|
+
webViewProps?: Omit<WebViewProps, OwnedWebViewProps>;
|
|
41
|
+
};
|
|
42
|
+
export declare const OpenEditorNative: import("react").ForwardRefExoticComponent<OpenEditorNativeProps & RefAttributes<OpenEditorNativeController>>;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { getOpenEditorThemeEntries, openEditorThemeCssName, openEditorThemeTokenNames, } from "@openeditor/embedded-runtime";
|
|
3
|
+
import { openEditorEmbeddedSurfaceHtml } from "@openeditor/embedded-surface/html";
|
|
4
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react";
|
|
5
|
+
import { Keyboard, KeyboardAvoidingView, Platform, StyleSheet, View, } from "react-native";
|
|
6
|
+
import { WebView, } from "react-native-webview";
|
|
7
|
+
import { emptyOpenEditorNativeState, isOpenEditorNativeLifecycleCancellation, OpenEditorNativeBridge, } from "./controller.js";
|
|
8
|
+
import { OpenEditorNativeToolbar, } from "./toolbar-host.js";
|
|
9
|
+
const ZERO_INSETS = { top: 0, right: 0, bottom: 0, left: 0 };
|
|
10
|
+
const HostWebView = WebView;
|
|
11
|
+
export const OpenEditorNative = forwardRef(function OpenEditorNative({ initialDocument, editable = true, placeholder = "Start writing…", theme, contentInsets = ZERO_INSETS, showToolbar = true, toolbarItems, renderToolbar, nativeEffects, onDocumentChanged, onReady, onError, keyboardVerticalOffset = 0, keyboardAvoidanceEnabled = true, style, webViewProps, }, forwardedRef) {
|
|
12
|
+
const webViewRef = useRef(null);
|
|
13
|
+
const sessionIdRef = useRef(`native_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`);
|
|
14
|
+
const callbacksRef = useRef({
|
|
15
|
+
nativeEffects,
|
|
16
|
+
onDocumentChanged,
|
|
17
|
+
onError,
|
|
18
|
+
onReady,
|
|
19
|
+
});
|
|
20
|
+
callbacksRef.current = { nativeEffects, onDocumentChanged, onError, onReady };
|
|
21
|
+
const initialDocumentRef = useRef(initialDocument);
|
|
22
|
+
const initialConfigRef = useRef({ editable, placeholder, theme });
|
|
23
|
+
const initializedRef = useRef(false);
|
|
24
|
+
const [editorState, setEditorState] = useState(emptyOpenEditorNativeState);
|
|
25
|
+
const [keyboardVisible, setKeyboardVisible] = useState(() => Keyboard.isVisible?.() ?? false);
|
|
26
|
+
const [bridge] = useState(() => new OpenEditorNativeBridge({
|
|
27
|
+
sessionId: sessionIdRef.current,
|
|
28
|
+
send: (message) => webViewRef.current?.postMessage(message),
|
|
29
|
+
getEffectHandlers: () => callbacksRef.current.nativeEffects,
|
|
30
|
+
}));
|
|
31
|
+
// The packaged HTML is several megabytes. Keeping the source object stable is
|
|
32
|
+
// essential: passing a fresh inline source through React Native on each editor
|
|
33
|
+
// state update can make WKWebView reload it and can terminate its content
|
|
34
|
+
// process under memory pressure.
|
|
35
|
+
const runtimeSource = useMemo(() => ({
|
|
36
|
+
html: openEditorEmbeddedSurfaceHtml,
|
|
37
|
+
baseUrl: "https://openeditor.local/",
|
|
38
|
+
}), []);
|
|
39
|
+
useImperativeHandle(forwardedRef, () => bridge, [bridge]);
|
|
40
|
+
const resolvedInsets = {
|
|
41
|
+
top: contentInsets.top ?? 0,
|
|
42
|
+
right: contentInsets.right ?? 0,
|
|
43
|
+
bottom: contentInsets.bottom ?? 0,
|
|
44
|
+
left: contentInsets.left ?? 0,
|
|
45
|
+
};
|
|
46
|
+
const availableNativeEffects = Object.keys(nativeEffects ?? {});
|
|
47
|
+
const hostConfig = JSON.stringify({
|
|
48
|
+
availableNativeEffects,
|
|
49
|
+
contentInsets: resolvedInsets,
|
|
50
|
+
placeholder,
|
|
51
|
+
sessionId: sessionIdRef.current,
|
|
52
|
+
theme,
|
|
53
|
+
});
|
|
54
|
+
const injectedConfig = `window.__OPENEDITOR_NATIVE_HOST__=${hostConfig};true;`;
|
|
55
|
+
const themeUpdateScript = `(()=>{const root=document.documentElement;for(const name of ${JSON.stringify(openEditorThemeTokenNames.map(openEditorThemeCssName))})root.style.removeProperty(name);for(const [name,value] of ${JSON.stringify(getOpenEditorThemeEntries(theme ?? {}))})root.style.setProperty(name,value);const background=${JSON.stringify(theme?.surface ?? "")};const text=${JSON.stringify(theme?.text ?? "")};background?root.style.setProperty("--oe-native-background",background):root.style.removeProperty("--oe-native-background");text?root.style.setProperty("--oe-native-text",text):root.style.removeProperty("--oe-native-text");})();true;`;
|
|
56
|
+
const initializeRuntime = useCallback(() => {
|
|
57
|
+
if (!bridge.runtimeReady || initializedRef.current)
|
|
58
|
+
return;
|
|
59
|
+
initializedRef.current = true;
|
|
60
|
+
void bridge
|
|
61
|
+
.initialize(initialDocumentRef.current, initialConfigRef.current.editable)
|
|
62
|
+
.then((result) => {
|
|
63
|
+
bridge.markInitialized();
|
|
64
|
+
setEditorState((state) => ({
|
|
65
|
+
...state,
|
|
66
|
+
documentRevision: result.documentRevision,
|
|
67
|
+
revision: result.stateRevision,
|
|
68
|
+
editable: initialConfigRef.current.editable,
|
|
69
|
+
}));
|
|
70
|
+
callbacksRef.current.onReady?.(bridge);
|
|
71
|
+
})
|
|
72
|
+
.catch((error) => {
|
|
73
|
+
initializedRef.current = false;
|
|
74
|
+
if (isOpenEditorNativeLifecycleCancellation(error))
|
|
75
|
+
return;
|
|
76
|
+
callbacksRef.current.onError?.(error instanceof Error
|
|
77
|
+
? error
|
|
78
|
+
: new Error("OpenEditor runtime initialization failed."));
|
|
79
|
+
});
|
|
80
|
+
}, [bridge]);
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
webViewRef.current?.injectJavaScript(themeUpdateScript);
|
|
83
|
+
}, [themeUpdateScript]);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
const showSubscription = Keyboard.addListener("keyboardDidShow", () => setKeyboardVisible(true));
|
|
86
|
+
const hideSubscription = Keyboard.addListener("keyboardDidHide", () => {
|
|
87
|
+
setKeyboardVisible(false);
|
|
88
|
+
});
|
|
89
|
+
return () => {
|
|
90
|
+
showSubscription.remove();
|
|
91
|
+
hideSubscription.remove();
|
|
92
|
+
};
|
|
93
|
+
}, []);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const unsubscribe = bridge.subscribe((event) => {
|
|
96
|
+
if (event.type === "ready") {
|
|
97
|
+
initializeRuntime();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (event.type === "stateChanged") {
|
|
101
|
+
setEditorState(event.state);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (event.type === "documentChanged") {
|
|
105
|
+
if (bridge.ready) {
|
|
106
|
+
callbacksRef.current.onDocumentChanged?.({
|
|
107
|
+
documentRevision: event.documentRevision,
|
|
108
|
+
origin: event.origin,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (event.type === "error") {
|
|
114
|
+
callbacksRef.current.onError?.(new Error(`${event.code}: ${event.message}`));
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
return () => {
|
|
118
|
+
unsubscribe();
|
|
119
|
+
bridge.dispose();
|
|
120
|
+
};
|
|
121
|
+
}, [bridge, initializeRuntime]);
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
let active = true;
|
|
124
|
+
void bridge
|
|
125
|
+
.command({ type: "setEditable", editable })
|
|
126
|
+
.catch((error) => {
|
|
127
|
+
if (active && !isOpenEditorNativeLifecycleCancellation(error)) {
|
|
128
|
+
callbacksRef.current.onError?.(error instanceof Error
|
|
129
|
+
? error
|
|
130
|
+
: new Error("OpenEditor could not update editability."));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
return () => {
|
|
134
|
+
active = false;
|
|
135
|
+
};
|
|
136
|
+
}, [bridge, editable]);
|
|
137
|
+
const onMessage = (event) => {
|
|
138
|
+
if (!bridge.receive(event.nativeEvent.data)) {
|
|
139
|
+
callbacksRef.current.onError?.(new Error("OpenEditor runtime sent an invalid bridge message."));
|
|
140
|
+
}
|
|
141
|
+
initializeRuntime();
|
|
142
|
+
};
|
|
143
|
+
const shouldRenderToolbar = showToolbar && editable && editorState.focused && keyboardVisible;
|
|
144
|
+
const toolbar = renderToolbar && shouldRenderToolbar ? (renderToolbar({ availableNativeEffects, controller: bridge, state: editorState })) : shouldRenderToolbar ? (_jsx(OpenEditorNativeToolbar, { availableNativeEffects: availableNativeEffects, controller: bridge, items: toolbarItems, onCommandError: (error) => callbacksRef.current.onError?.(error), state: editorState, theme: theme })) : null;
|
|
145
|
+
return (_jsxs(KeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : "height", enabled: keyboardAvoidanceEnabled, keyboardVerticalOffset: keyboardVerticalOffset, style: [
|
|
146
|
+
styles.host,
|
|
147
|
+
{ backgroundColor: theme?.surface ?? "#ffffff" },
|
|
148
|
+
style,
|
|
149
|
+
], children: [_jsx(View, { style: styles.webViewContainer, children: _jsx(HostWebView, { ...webViewProps, allowsBackForwardNavigationGestures: false, bounces: webViewProps?.bounces ?? true, injectedJavaScriptBeforeContentLoaded: injectedConfig, javaScriptEnabled: true, hideKeyboardAccessoryView: webViewProps?.hideKeyboardAccessoryView ?? true, nestedScrollEnabled: true, onLoadStart: (event) => {
|
|
150
|
+
initializedRef.current = false;
|
|
151
|
+
bridge.resetRuntime();
|
|
152
|
+
webViewProps?.onLoadStart?.(event);
|
|
153
|
+
}, onContentProcessDidTerminate: (event) => {
|
|
154
|
+
callbacksRef.current.onError?.(new Error("OpenEditor WebView content process terminated unexpectedly."));
|
|
155
|
+
webViewProps?.onContentProcessDidTerminate?.(event);
|
|
156
|
+
}, onMessage: onMessage, originWhitelist: webViewProps?.originWhitelist ?? [
|
|
157
|
+
"https://openeditor.local",
|
|
158
|
+
"file://*",
|
|
159
|
+
"about:blank",
|
|
160
|
+
], ref: webViewRef, scrollEnabled: true, source: runtimeSource, style: [
|
|
161
|
+
styles.webView,
|
|
162
|
+
{
|
|
163
|
+
backgroundColor: theme?.surface ?? "#ffffff",
|
|
164
|
+
},
|
|
165
|
+
] }) }), toolbar] }));
|
|
166
|
+
});
|
|
167
|
+
const styles = StyleSheet.create({
|
|
168
|
+
host: { flex: 1, minHeight: 0 },
|
|
169
|
+
webViewContainer: { flex: 1, minHeight: 0, overflow: "hidden" },
|
|
170
|
+
webView: { flex: 1 },
|
|
171
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { OpenEditorNativeEffectName, OpenEditorRuntimeState } from "@openeditor/embedded-runtime";
|
|
2
|
+
import { type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
+
import type { OpenEditorNativeController } from "./controller.js";
|
|
4
|
+
import type { OpenEditorNativeTheme } from "./native-editor.js";
|
|
5
|
+
import { type OpenEditorNativeToolbarItem } from "./toolbar-items.js";
|
|
6
|
+
export type { OpenEditorNativeToolbarItem } from "./toolbar-items.js";
|
|
7
|
+
export { defaultOpenEditorNativeToolbarItems, openEditorNativeBlockPickerItems, } from "./toolbar-items.js";
|
|
8
|
+
export type OpenEditorNativeToolbarProps = {
|
|
9
|
+
availableNativeEffects?: readonly OpenEditorNativeEffectName[];
|
|
10
|
+
controller: OpenEditorNativeController;
|
|
11
|
+
state: OpenEditorRuntimeState;
|
|
12
|
+
items?: readonly OpenEditorNativeToolbarItem[];
|
|
13
|
+
theme?: OpenEditorNativeTheme;
|
|
14
|
+
style?: StyleProp<ViewStyle>;
|
|
15
|
+
onCommandError?: (error: Error) => void;
|
|
16
|
+
};
|
|
17
|
+
export declare const OpenEditorNativeToolbar: import("react").MemoExoticComponent<({ availableNativeEffects, controller, state, items, theme, style, onCommandError, }: OpenEditorNativeToolbarProps) => import("react").JSX.Element>;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useRef, useState } from "react";
|
|
3
|
+
import { Keyboard, Modal, Pressable, ScrollView, StyleSheet, Text, TextInput, View, } from "react-native";
|
|
4
|
+
import { defaultOpenEditorNativeToolbarItems, openEditorNativeBlockPickerItems, openEditorNativeTableToolbarItems, } from "./toolbar-items.js";
|
|
5
|
+
export { defaultOpenEditorNativeToolbarItems, openEditorNativeBlockPickerItems, } from "./toolbar-items.js";
|
|
6
|
+
const isItemActive = (item, state) => (item.activeWhen?.block !== undefined &&
|
|
7
|
+
state.activeNodes.includes(item.activeWhen.block) &&
|
|
8
|
+
(item.activeWhen?.headingLevel === undefined ||
|
|
9
|
+
state.headingLevel === item.activeWhen.headingLevel)) ||
|
|
10
|
+
(item.activeWhen?.mark !== undefined &&
|
|
11
|
+
state.activeMarks.includes(item.activeWhen.mark));
|
|
12
|
+
const isItemDisabled = (item, state) => !state.editable ||
|
|
13
|
+
(item.disabledWhen === "cannotUndo" && !state.canUndo) ||
|
|
14
|
+
(item.disabledWhen === "cannotRedo" && !state.canRedo);
|
|
15
|
+
export const OpenEditorNativeToolbar = memo(function OpenEditorNativeToolbar({ availableNativeEffects = [], controller, state, items = defaultOpenEditorNativeToolbarItems, theme, style, onCommandError, }) {
|
|
16
|
+
const [blockPickerOpen, setBlockPickerOpen] = useState(false);
|
|
17
|
+
const [linkEditorOpen, setLinkEditorOpen] = useState(false);
|
|
18
|
+
const [linkValue, setLinkValue] = useState("");
|
|
19
|
+
const pendingBlockRef = useRef(null);
|
|
20
|
+
const pickerSelectionRef = useRef(undefined);
|
|
21
|
+
const runCommand = (command) => {
|
|
22
|
+
void command.catch((cause) => {
|
|
23
|
+
onCommandError?.(cause instanceof Error
|
|
24
|
+
? cause
|
|
25
|
+
: new Error("OpenEditor command failed."));
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
const colors = {
|
|
29
|
+
background: theme?.surface ?? "#ffffff",
|
|
30
|
+
border: theme?.border ?? "#e5e7eb",
|
|
31
|
+
text: theme?.text ?? "#171717",
|
|
32
|
+
muted: theme?.muted ?? "#737373",
|
|
33
|
+
accent: theme?.accent ?? "#171717",
|
|
34
|
+
accentText: theme?.accentText ?? "#ffffff",
|
|
35
|
+
};
|
|
36
|
+
const blockPickerItems = openEditorNativeBlockPickerItems.filter((item) => {
|
|
37
|
+
if (item.key === "image")
|
|
38
|
+
return availableNativeEffects.includes("pickImage");
|
|
39
|
+
if (item.key === "attachment")
|
|
40
|
+
return availableNativeEffects.includes("pickAttachment");
|
|
41
|
+
if (item.key === "page")
|
|
42
|
+
return availableNativeEffects.includes("createPage");
|
|
43
|
+
return true;
|
|
44
|
+
});
|
|
45
|
+
return (_jsxs(View, { style: [
|
|
46
|
+
styles.root,
|
|
47
|
+
{ backgroundColor: colors.background, borderColor: colors.border },
|
|
48
|
+
style,
|
|
49
|
+
], children: [_jsx(ScrollView, { contentContainerStyle: styles.content, horizontal: true, keyboardShouldPersistTaps: "always", showsHorizontalScrollIndicator: false, children: state.activeNodes.includes("table")
|
|
50
|
+
? openEditorNativeTableToolbarItems.map((item) => {
|
|
51
|
+
const disabled = isItemDisabled(item, state);
|
|
52
|
+
return (_jsx(Pressable, { accessibilityLabel: item.label, accessibilityRole: "button", accessibilityState: { disabled }, disabled: disabled, onPress: () => {
|
|
53
|
+
if (!item.command) {
|
|
54
|
+
Keyboard.dismiss();
|
|
55
|
+
runCommand(controller.blur());
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
runCommand(controller.command(item.command));
|
|
59
|
+
}, style: ({ pressed }) => [
|
|
60
|
+
styles.button,
|
|
61
|
+
{ borderColor: colors.border },
|
|
62
|
+
pressed && styles.pressed,
|
|
63
|
+
disabled && styles.disabled,
|
|
64
|
+
], children: _jsx(Text, { style: [
|
|
65
|
+
styles.label,
|
|
66
|
+
{ color: disabled ? colors.muted : colors.text },
|
|
67
|
+
], children: item.label }) }, item.key));
|
|
68
|
+
})
|
|
69
|
+
: items.map((item) => {
|
|
70
|
+
const active = isItemActive(item, state);
|
|
71
|
+
const disabled = isItemDisabled(item, state);
|
|
72
|
+
return (_jsx(Pressable, { accessibilityLabel: item.label, accessibilityRole: "button", accessibilityState: { disabled, selected: active }, disabled: disabled, onPress: () => {
|
|
73
|
+
if (item.action === "openBlockPicker") {
|
|
74
|
+
pickerSelectionRef.current =
|
|
75
|
+
state.selection.type === "text"
|
|
76
|
+
? {
|
|
77
|
+
anchor: state.selection.anchor,
|
|
78
|
+
head: state.selection.head,
|
|
79
|
+
}
|
|
80
|
+
: undefined;
|
|
81
|
+
setBlockPickerOpen(true);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (item.action === "openLinkEditor") {
|
|
85
|
+
setLinkValue("");
|
|
86
|
+
setLinkEditorOpen(true);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (!item.command) {
|
|
90
|
+
Keyboard.dismiss();
|
|
91
|
+
runCommand(controller.blur());
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
runCommand(controller.command(item.command));
|
|
95
|
+
}, style: ({ pressed }) => [
|
|
96
|
+
styles.button,
|
|
97
|
+
{ borderColor: active ? colors.accent : colors.border },
|
|
98
|
+
active && { backgroundColor: colors.accent },
|
|
99
|
+
pressed && styles.pressed,
|
|
100
|
+
disabled && styles.disabled,
|
|
101
|
+
], children: _jsx(Text, { style: [
|
|
102
|
+
styles.label,
|
|
103
|
+
{
|
|
104
|
+
color: active
|
|
105
|
+
? colors.accentText
|
|
106
|
+
: disabled
|
|
107
|
+
? colors.muted
|
|
108
|
+
: colors.text,
|
|
109
|
+
},
|
|
110
|
+
], children: item.label }) }, item.key));
|
|
111
|
+
}) }), _jsx(Modal, { animationType: "slide", onDismiss: () => {
|
|
112
|
+
const block = pendingBlockRef.current;
|
|
113
|
+
pendingBlockRef.current = null;
|
|
114
|
+
pickerSelectionRef.current = undefined;
|
|
115
|
+
if (block)
|
|
116
|
+
runCommand(controller.command(block));
|
|
117
|
+
}, onRequestClose: () => setBlockPickerOpen(false), presentationStyle: "pageSheet", visible: blockPickerOpen, children: _jsxs(View, { style: [styles.sheet, { backgroundColor: colors.background }], children: [_jsxs(View, { style: [styles.sheetHeader, { borderBottomColor: colors.border }], children: [_jsx(Text, { accessibilityRole: "header", style: [styles.sheetTitle, { color: colors.text }], children: "Insert block" }), _jsx(Pressable, { accessibilityLabel: "Close block picker", accessibilityRole: "button", onPress: () => setBlockPickerOpen(false), children: _jsx(Text, { style: [styles.sheetClose, { color: colors.accent }], children: "Done" }) })] }), _jsx(ScrollView, { contentContainerStyle: styles.sheetContent, children: blockPickerItems.map((item, index) => {
|
|
118
|
+
const previousGroup = index > 0
|
|
119
|
+
? blockPickerItems[index - 1]?.group
|
|
120
|
+
: undefined;
|
|
121
|
+
return (_jsxs(View, { children: [item.group !== previousGroup ? (_jsx(Text, { style: [styles.groupLabel, { color: colors.muted }], children: item.group })) : null, _jsx(Pressable, { accessibilityLabel: `Insert ${item.label}`, accessibilityRole: "button", onPress: () => {
|
|
122
|
+
pendingBlockRef.current = {
|
|
123
|
+
type: "insertBlock",
|
|
124
|
+
block: item.key,
|
|
125
|
+
selection: pickerSelectionRef.current,
|
|
126
|
+
};
|
|
127
|
+
setBlockPickerOpen(false);
|
|
128
|
+
}, style: ({ pressed }) => [
|
|
129
|
+
styles.blockPickerItem,
|
|
130
|
+
{ borderBottomColor: colors.border },
|
|
131
|
+
pressed && styles.pressed,
|
|
132
|
+
], children: _jsx(Text, { style: [styles.blockPickerLabel, { color: colors.text }], children: item.label }) })] }, item.key));
|
|
133
|
+
}) })] }) }), _jsx(Modal, { animationType: "fade", onRequestClose: () => setLinkEditorOpen(false), presentationStyle: "formSheet", visible: linkEditorOpen, children: _jsxs(View, { style: [styles.linkSheet, { backgroundColor: colors.background }], children: [_jsx(Text, { accessibilityRole: "header", style: [styles.sheetTitle, { color: colors.text }], children: "Edit link" }), _jsx(TextInput, { accessibilityLabel: "Link URL", autoCapitalize: "none", autoCorrect: false, onChangeText: setLinkValue, placeholder: "https://example.com", placeholderTextColor: colors.muted, style: [
|
|
134
|
+
styles.linkInput,
|
|
135
|
+
{ borderColor: colors.border, color: colors.text },
|
|
136
|
+
], value: linkValue }), _jsxs(View, { style: styles.linkActions, children: [_jsx(Pressable, { accessibilityLabel: "Cancel link editing", accessibilityRole: "button", onPress: () => setLinkEditorOpen(false), style: styles.linkAction, children: _jsx(Text, { style: { color: colors.text }, children: "Cancel" }) }), _jsx(Pressable, { accessibilityLabel: "Remove", accessibilityRole: "button", onPress: () => {
|
|
137
|
+
setLinkEditorOpen(false);
|
|
138
|
+
runCommand(controller.command({ type: "setLink", href: null }));
|
|
139
|
+
}, style: styles.linkAction, children: _jsx(Text, { style: { color: colors.text }, children: "Remove" }) }), _jsx(Pressable, { accessibilityLabel: "Apply", accessibilityRole: "button", disabled: !linkValue.trim(), onPress: () => {
|
|
140
|
+
const href = linkValue.trim();
|
|
141
|
+
setLinkEditorOpen(false);
|
|
142
|
+
runCommand(controller.command({ type: "setLink", href }));
|
|
143
|
+
}, style: [styles.linkAction, !linkValue.trim() && styles.disabled], children: _jsx(Text, { style: { color: colors.accent }, children: "Apply" }) })] })] }) })] }));
|
|
144
|
+
});
|
|
145
|
+
const styles = StyleSheet.create({
|
|
146
|
+
root: {
|
|
147
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
148
|
+
minHeight: 52,
|
|
149
|
+
},
|
|
150
|
+
content: {
|
|
151
|
+
alignItems: "center",
|
|
152
|
+
gap: 8,
|
|
153
|
+
paddingHorizontal: 10,
|
|
154
|
+
paddingVertical: 7,
|
|
155
|
+
},
|
|
156
|
+
button: {
|
|
157
|
+
alignItems: "center",
|
|
158
|
+
borderRadius: 8,
|
|
159
|
+
borderWidth: 1,
|
|
160
|
+
justifyContent: "center",
|
|
161
|
+
minHeight: 36,
|
|
162
|
+
minWidth: 40,
|
|
163
|
+
paddingHorizontal: 10,
|
|
164
|
+
},
|
|
165
|
+
label: {
|
|
166
|
+
fontSize: 13,
|
|
167
|
+
fontWeight: "600",
|
|
168
|
+
},
|
|
169
|
+
pressed: { opacity: 0.64 },
|
|
170
|
+
disabled: { opacity: 0.42 },
|
|
171
|
+
sheet: { flex: 1 },
|
|
172
|
+
sheetHeader: {
|
|
173
|
+
alignItems: "center",
|
|
174
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
175
|
+
flexDirection: "row",
|
|
176
|
+
justifyContent: "space-between",
|
|
177
|
+
minHeight: 56,
|
|
178
|
+
paddingHorizontal: 20,
|
|
179
|
+
},
|
|
180
|
+
sheetTitle: { fontSize: 18, fontWeight: "700" },
|
|
181
|
+
sheetClose: { fontSize: 16, fontWeight: "600" },
|
|
182
|
+
sheetContent: { paddingBottom: 32, paddingHorizontal: 20 },
|
|
183
|
+
groupLabel: {
|
|
184
|
+
fontSize: 12,
|
|
185
|
+
fontWeight: "700",
|
|
186
|
+
paddingBottom: 5,
|
|
187
|
+
paddingTop: 20,
|
|
188
|
+
textTransform: "uppercase",
|
|
189
|
+
},
|
|
190
|
+
blockPickerItem: {
|
|
191
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
192
|
+
minHeight: 48,
|
|
193
|
+
justifyContent: "center",
|
|
194
|
+
},
|
|
195
|
+
blockPickerLabel: { fontSize: 16, fontWeight: "500" },
|
|
196
|
+
linkSheet: { flex: 1, gap: 20, justifyContent: "center", padding: 24 },
|
|
197
|
+
linkInput: {
|
|
198
|
+
borderRadius: 10,
|
|
199
|
+
borderWidth: 1,
|
|
200
|
+
fontSize: 16,
|
|
201
|
+
minHeight: 48,
|
|
202
|
+
paddingHorizontal: 14,
|
|
203
|
+
},
|
|
204
|
+
linkActions: { flexDirection: "row", justifyContent: "flex-end", gap: 18 },
|
|
205
|
+
linkAction: { minHeight: 44, justifyContent: "center", paddingHorizontal: 4 },
|
|
206
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { OpenEditorRuntimeCommand } from "@openeditor/embedded-runtime";
|
|
2
|
+
export type OpenEditorNativeToolbarItem = {
|
|
3
|
+
key: string;
|
|
4
|
+
label: string;
|
|
5
|
+
command?: OpenEditorRuntimeCommand;
|
|
6
|
+
action?: "openBlockPicker" | "openLinkEditor" | "dismissKeyboard";
|
|
7
|
+
activeWhen?: {
|
|
8
|
+
block?: string;
|
|
9
|
+
headingLevel?: number;
|
|
10
|
+
mark?: string;
|
|
11
|
+
};
|
|
12
|
+
disabledWhen?: "cannotUndo" | "cannotRedo";
|
|
13
|
+
};
|
|
14
|
+
export declare const defaultOpenEditorNativeToolbarItems: readonly OpenEditorNativeToolbarItem[];
|
|
15
|
+
export declare const openEditorNativeBlockPickerItems: readonly [{
|
|
16
|
+
readonly key: "paragraph";
|
|
17
|
+
readonly label: "Paragraph";
|
|
18
|
+
readonly group: "Text";
|
|
19
|
+
}, {
|
|
20
|
+
readonly key: "heading1";
|
|
21
|
+
readonly label: "Heading 1";
|
|
22
|
+
readonly group: "Text";
|
|
23
|
+
}, {
|
|
24
|
+
readonly key: "heading2";
|
|
25
|
+
readonly label: "Heading 2";
|
|
26
|
+
readonly group: "Text";
|
|
27
|
+
}, {
|
|
28
|
+
readonly key: "heading3";
|
|
29
|
+
readonly label: "Heading 3";
|
|
30
|
+
readonly group: "Text";
|
|
31
|
+
}, {
|
|
32
|
+
readonly key: "bulletList";
|
|
33
|
+
readonly label: "Bullet list";
|
|
34
|
+
readonly group: "Text";
|
|
35
|
+
}, {
|
|
36
|
+
readonly key: "orderedList";
|
|
37
|
+
readonly label: "Numbered list";
|
|
38
|
+
readonly group: "Text";
|
|
39
|
+
}, {
|
|
40
|
+
readonly key: "taskList";
|
|
41
|
+
readonly label: "Task list";
|
|
42
|
+
readonly group: "Text";
|
|
43
|
+
}, {
|
|
44
|
+
readonly key: "toggleList";
|
|
45
|
+
readonly label: "Toggle list";
|
|
46
|
+
readonly group: "Text";
|
|
47
|
+
}, {
|
|
48
|
+
readonly key: "blockquote";
|
|
49
|
+
readonly label: "Quote";
|
|
50
|
+
readonly group: "Text";
|
|
51
|
+
}, {
|
|
52
|
+
readonly key: "codeBlock";
|
|
53
|
+
readonly label: "Code block";
|
|
54
|
+
readonly group: "Text";
|
|
55
|
+
}, {
|
|
56
|
+
readonly key: "divider";
|
|
57
|
+
readonly label: "Divider";
|
|
58
|
+
readonly group: "Structure";
|
|
59
|
+
}, {
|
|
60
|
+
readonly key: "columns";
|
|
61
|
+
readonly label: "Columns";
|
|
62
|
+
readonly group: "Layout";
|
|
63
|
+
}, {
|
|
64
|
+
readonly key: "table";
|
|
65
|
+
readonly label: "Table";
|
|
66
|
+
readonly group: "Layout";
|
|
67
|
+
}, {
|
|
68
|
+
readonly key: "callout";
|
|
69
|
+
readonly label: "Callout";
|
|
70
|
+
readonly group: "Embed";
|
|
71
|
+
}, {
|
|
72
|
+
readonly key: "diagram";
|
|
73
|
+
readonly label: "Diagram";
|
|
74
|
+
readonly group: "Embed";
|
|
75
|
+
}, {
|
|
76
|
+
readonly key: "page";
|
|
77
|
+
readonly label: "Page";
|
|
78
|
+
readonly group: "Embed";
|
|
79
|
+
}, {
|
|
80
|
+
readonly key: "image";
|
|
81
|
+
readonly label: "Image";
|
|
82
|
+
readonly group: "Media";
|
|
83
|
+
}, {
|
|
84
|
+
readonly key: "attachment";
|
|
85
|
+
readonly label: "File";
|
|
86
|
+
readonly group: "Media";
|
|
87
|
+
}];
|
|
88
|
+
export declare const openEditorNativeTableToolbarItems: readonly OpenEditorNativeToolbarItem[];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export const defaultOpenEditorNativeToolbarItems = [
|
|
2
|
+
{ key: "insert", label: "+ Block", action: "openBlockPicker" },
|
|
3
|
+
{ key: "paragraph", label: "Text", command: { type: "setParagraph" }, activeWhen: { block: "paragraph" } },
|
|
4
|
+
{ key: "heading-1", label: "H1", command: { type: "toggleHeading", level: 1 }, activeWhen: { block: "heading", headingLevel: 1 } },
|
|
5
|
+
{ key: "heading-2", label: "H2", command: { type: "toggleHeading", level: 2 }, activeWhen: { block: "heading", headingLevel: 2 } },
|
|
6
|
+
{ key: "bold", label: "B", command: { type: "toggleMark", mark: "bold" }, activeWhen: { mark: "bold" } },
|
|
7
|
+
{ key: "italic", label: "I", command: { type: "toggleMark", mark: "italic" }, activeWhen: { mark: "italic" } },
|
|
8
|
+
{ key: "underline", label: "U", command: { type: "toggleMark", mark: "underline" }, activeWhen: { mark: "underline" } },
|
|
9
|
+
{ key: "strike", label: "S", command: { type: "toggleMark", mark: "strike" }, activeWhen: { mark: "strike" } },
|
|
10
|
+
{ key: "code", label: "Code", command: { type: "toggleMark", mark: "code" }, activeWhen: { mark: "code" } },
|
|
11
|
+
{ key: "link", label: "Link", action: "openLinkEditor", activeWhen: { mark: "link" } },
|
|
12
|
+
{ key: "bullet-list", label: "• List", command: { type: "toggleList", list: "bullet" }, activeWhen: { block: "bulletList" } },
|
|
13
|
+
{ key: "ordered-list", label: "1. List", command: { type: "toggleList", list: "ordered" }, activeWhen: { block: "orderedList" } },
|
|
14
|
+
{ key: "task-list", label: "Tasks", command: { type: "toggleList", list: "task" }, activeWhen: { block: "taskList" } },
|
|
15
|
+
{ key: "quote", label: "Quote", command: { type: "toggleBlockquote" }, activeWhen: { block: "blockquote" } },
|
|
16
|
+
{ key: "code-block", label: "Code block", command: { type: "toggleCodeBlock" }, activeWhen: { block: "codeBlock" } },
|
|
17
|
+
{ key: "undo", label: "Undo", command: { type: "undo" }, disabledWhen: "cannotUndo" },
|
|
18
|
+
{ key: "redo", label: "Redo", command: { type: "redo" }, disabledWhen: "cannotRedo" },
|
|
19
|
+
{ key: "keyboard", label: "Done", action: "dismissKeyboard" },
|
|
20
|
+
];
|
|
21
|
+
export const openEditorNativeBlockPickerItems = [
|
|
22
|
+
{ key: "paragraph", label: "Paragraph", group: "Text" },
|
|
23
|
+
{ key: "heading1", label: "Heading 1", group: "Text" },
|
|
24
|
+
{ key: "heading2", label: "Heading 2", group: "Text" },
|
|
25
|
+
{ key: "heading3", label: "Heading 3", group: "Text" },
|
|
26
|
+
{ key: "bulletList", label: "Bullet list", group: "Text" },
|
|
27
|
+
{ key: "orderedList", label: "Numbered list", group: "Text" },
|
|
28
|
+
{ key: "taskList", label: "Task list", group: "Text" },
|
|
29
|
+
{ key: "toggleList", label: "Toggle list", group: "Text" },
|
|
30
|
+
{ key: "blockquote", label: "Quote", group: "Text" },
|
|
31
|
+
{ key: "codeBlock", label: "Code block", group: "Text" },
|
|
32
|
+
{ key: "divider", label: "Divider", group: "Structure" },
|
|
33
|
+
{ key: "columns", label: "Columns", group: "Layout" },
|
|
34
|
+
{ key: "table", label: "Table", group: "Layout" },
|
|
35
|
+
{ key: "callout", label: "Callout", group: "Embed" },
|
|
36
|
+
{ key: "diagram", label: "Diagram", group: "Embed" },
|
|
37
|
+
{ key: "page", label: "Page", group: "Embed" },
|
|
38
|
+
{ key: "image", label: "Image", group: "Media" },
|
|
39
|
+
{ key: "attachment", label: "File", group: "Media" },
|
|
40
|
+
];
|
|
41
|
+
export const openEditorNativeTableToolbarItems = [
|
|
42
|
+
{ key: "table-row-before", label: "+ Row ↑", command: { type: "addTableRow", after: false } },
|
|
43
|
+
{ key: "table-row-after", label: "+ Row ↓", command: { type: "addTableRow" } },
|
|
44
|
+
{ key: "table-delete-row", label: "− Row", command: { type: "deleteTableRow" } },
|
|
45
|
+
{ key: "table-column-before", label: "+ Col ←", command: { type: "addTableColumn", after: false } },
|
|
46
|
+
{ key: "table-column-after", label: "+ Col →", command: { type: "addTableColumn" } },
|
|
47
|
+
{ key: "table-delete-column", label: "− Col", command: { type: "deleteTableColumn" } },
|
|
48
|
+
{ key: "table-delete", label: "Delete table", command: { type: "deleteTable" } },
|
|
49
|
+
{ key: "table-undo", label: "Undo", command: { type: "undo" }, disabledWhen: "cannotUndo" },
|
|
50
|
+
{ key: "table-redo", label: "Redo", command: { type: "redo" }, disabledWhen: "cannotRedo" },
|
|
51
|
+
{ key: "table-keyboard", label: "Done", action: "dismissKeyboard" },
|
|
52
|
+
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openeditor/native",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.34",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"repository": {
|
|
@@ -26,25 +26,13 @@
|
|
|
26
26
|
"./package.json": "./package.json"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@
|
|
30
|
-
"@
|
|
31
|
-
"@openeditor/
|
|
32
|
-
"@openeditor/exporters": "0.0.32",
|
|
33
|
-
"@openeditor/extensions": "0.0.32",
|
|
34
|
-
"emojibase": "17.0.0",
|
|
35
|
-
"emojibase-data": "16.0.2",
|
|
36
|
-
"react": "19.2.3",
|
|
37
|
-
"react-native": "0.85.3"
|
|
29
|
+
"@openeditor/core": "0.0.34",
|
|
30
|
+
"@openeditor/embedded-runtime": "0.0.34",
|
|
31
|
+
"@openeditor/embedded-surface": "0.0.34"
|
|
38
32
|
},
|
|
39
33
|
"peerDependencies": {
|
|
40
|
-
"@openeditor/react-native-prose-editor": "0.0.32",
|
|
41
|
-
"react-native-keyboard-controller": ">=1.21",
|
|
42
34
|
"react": ">=19",
|
|
43
|
-
"react-native": ">=0.85"
|
|
44
|
-
|
|
45
|
-
"peerDependenciesMeta": {
|
|
46
|
-
"react-native-keyboard-controller": {
|
|
47
|
-
"optional": true
|
|
48
|
-
}
|
|
35
|
+
"react-native": ">=0.85",
|
|
36
|
+
"react-native-webview": ">=13"
|
|
49
37
|
}
|
|
50
38
|
}
|
package/dist/document.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { type OpenEditorDocument, type OpenEditorDocumentMeta, type ProseMirrorDocument, type ProseMirrorNode } from "@openeditor/core";
|
|
2
|
-
export declare const sanitizeNativeEditorDocument: (document: OpenEditorDocument, meta?: OpenEditorDocumentMeta | undefined) => OpenEditorDocument;
|
|
3
|
-
export declare const toNativeEditorNode: (node: ProseMirrorNode) => ProseMirrorNode;
|
|
4
|
-
export declare const fromNativeEditorNode: (node: ProseMirrorNode) => ProseMirrorNode;
|
|
5
|
-
export declare const toNativeEditorDocument: (document: OpenEditorDocument) => ProseMirrorDocument;
|
|
6
|
-
export declare const fromNativeEditorDocument: (document: ProseMirrorDocument, meta?: OpenEditorDocumentMeta) => OpenEditorDocument;
|
|
7
|
-
export declare const createNativeEditorDocument: (content: ProseMirrorNode[], meta?: OpenEditorDocumentMeta) => ProseMirrorDocument;
|