@openeditor/native 0.0.35 → 0.0.37

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 CHANGED
@@ -26,18 +26,10 @@ Install the native peer dependencies:
26
26
  pnpm add expo-glass-effect react-native-keyboard-controller react-native-reanimated react-native-svg react-native-webview
27
27
  ```
28
28
 
29
- For Expo, install the SDK plugin so iOS WebViews receive interactive keyboard
30
- dismissal:
31
-
32
- ```json
33
- {
34
- "expo": {
35
- "plugins": ["@openeditor/native/plugin"]
36
- }
37
- }
38
- ```
39
-
40
- Rebuild the native application after adding the plugin or a native dependency.
29
+ The package includes an Expo native module that scopes interactive iOS keyboard
30
+ dismissal to the editor's own WebView. Expo autolinking discovers it; no config
31
+ plugin or AppDelegate mutation is required. Rebuild the native application after
32
+ adding the package or changing native dependencies.
41
33
  Mount the SDK provider once at the app root:
42
34
 
43
35
  ```tsx
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { openEditorEmbeddedSurfaceHtml } from "@openeditor/embedded-surface/html";
3
3
  import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react";
4
- import { Keyboard, Platform, StyleSheet, useColorScheme, View, } from "react-native";
4
+ import { Keyboard, findNodeHandle, Platform, StyleSheet, useColorScheme, View, } from "react-native";
5
5
  import { KeyboardStickyView } from "react-native-keyboard-controller";
6
6
  import { WebView, } from "react-native-webview";
7
7
  import { emptyOpenEditorNativeState, isOpenEditorNativeLifecycleCancellation, OpenEditorNativeBridge, } from "./controller.js";
@@ -9,6 +9,7 @@ import { OpenEditorNativeToolbar, } from "./toolbar-host.js";
9
9
  import { OpenEditorNativeToolbarSurface } from "./toolbar-surface.js";
10
10
  import { OPENEDITOR_NATIVE_TOOLBAR_OFFSET, OPENEDITOR_NATIVE_TOOLBAR_CONTENT_INSET, OPENEDITOR_NATIVE_TOOLBAR_HEIGHT, OPENEDITOR_NATIVE_TOOLBAR_RADIUS, resolveOpenEditorNativeContentInsets, } from "./native-layout.js";
11
11
  import { resolveOpenEditorNativeTheme } from "./theme.js";
12
+ import { setOpenEditorInteractiveKeyboardDismissal } from "./native-module.js";
12
13
  const DEFAULT_INSETS = { top: 0, right: 16, bottom: 0, left: 16 };
13
14
  const HostWebView = WebView;
14
15
  export const OpenEditorNative = forwardRef(function OpenEditorNative({ initialDocument, page, editable = true, placeholder = "Start writing…", theme, contentInsets = DEFAULT_INSETS, showToolbar = true, toolbarItems, renderToolbar, nativeEffects, onDocumentChanged, onReady, onError, toolbarContentInset = OPENEDITOR_NATIVE_TOOLBAR_CONTENT_INSET, toolbarOffset = OPENEDITOR_NATIVE_TOOLBAR_OFFSET, toolbarColorScheme, toolbarContainerStyle, style, webViewProps, }, forwardedRef) {
@@ -17,6 +18,12 @@ export const OpenEditorNative = forwardRef(function OpenEditorNative({ initialDo
17
18
  const resolvedTheme = useMemo(() => resolveOpenEditorNativeTheme(theme, resolvedColorScheme), [resolvedColorScheme, theme]);
18
19
  const resolvedToolbarColorScheme = toolbarColorScheme ?? resolvedColorScheme;
19
20
  const webViewRef = useRef(null);
21
+ const webViewHostRef = useRef(null);
22
+ const configureInteractiveKeyboardDismissal = useCallback(() => {
23
+ if (Platform.OS !== "ios")
24
+ return;
25
+ setOpenEditorInteractiveKeyboardDismissal(findNodeHandle(webViewHostRef.current));
26
+ }, []);
20
27
  const sessionIdRef = useRef(`native_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`);
21
28
  const callbacksRef = useRef({
22
29
  nativeEffects,
@@ -174,10 +181,13 @@ export const OpenEditorNative = forwardRef(function OpenEditorNative({ initialDo
174
181
  styles.host,
175
182
  { backgroundColor: resolvedTheme.surface },
176
183
  style,
177
- ], 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) => {
184
+ ], children: [_jsx(View, { ref: webViewHostRef, 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) => {
178
185
  initializedRef.current = false;
179
186
  bridge.resetRuntime();
180
187
  webViewProps?.onLoadStart?.(event);
188
+ }, onLoad: (event) => {
189
+ configureInteractiveKeyboardDismissal();
190
+ webViewProps?.onLoad?.(event);
181
191
  }, onContentProcessDidTerminate: (event) => {
182
192
  callbacksRef.current.onError?.(new Error("OpenEditor WebView content process terminated unexpectedly."));
183
193
  webViewProps?.onContentProcessDidTerminate?.(event);
@@ -0,0 +1 @@
1
+ export declare const setOpenEditorInteractiveKeyboardDismissal: (viewTag: number | null) => void;
@@ -0,0 +1,7 @@
1
+ import { requireOptionalNativeModule } from "expo";
2
+ const nativeModule = requireOptionalNativeModule("OpenEditorNative");
3
+ export const setOpenEditorInteractiveKeyboardDismissal = (viewTag) => {
4
+ if (viewTag === null || !nativeModule)
5
+ return;
6
+ void nativeModule.setInteractiveKeyboardDismissal(viewTag);
7
+ };
@@ -4,23 +4,11 @@ import { BottomSheet as SwiftUIBottomSheet, Group as SwiftUIGroup, Host as Swift
4
4
  import { accessibilityLabel as nativeAccessibilityLabel, frame as nativeFrame, glassEffect, padding as nativePadding, presentationDetents, presentationDragIndicator, } from "@expo/ui/swift-ui/modifiers";
5
5
  import { openEditorIcons } from "@openeditor/icons";
6
6
  import { HugeiconsIcon } from "@hugeicons/react-native";
7
- import { memo, useRef, useState } from "react";
7
+ import { memo, useState } from "react";
8
8
  import { Keyboard, Modal, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View, } from "react-native";
9
- import { defaultOpenEditorNativeToolbarItems, openEditorNativeBlockPickerItems, openEditorNativeTableToolbarItems, } from "./toolbar-items.js";
9
+ import { defaultOpenEditorNativeToolbarItems, } from "./toolbar-items.js";
10
+ import { dismissOpenEditorNativeBlockPicker, EMPTY_OPENEDITOR_NATIVE_BLOCK_PICKER, OPENEDITOR_NATIVE_TOOLBAR_BUTTON_GEOMETRY, openOpenEditorNativeBlockPicker, resolveOpenEditorNativeBlockPickerGroups, resolveOpenEditorNativeToolbarColors, resolveOpenEditorNativeToolbarItemPresentation, resolveOpenEditorNativeToolbarItems, selectOpenEditorNativeBlock, } from "./toolbar-model.js";
10
11
  export { defaultOpenEditorNativeToolbarItems, openEditorNativeBlockPickerItems, } from "./toolbar-items.js";
11
- const isItemActive = (item, state) => (item.activeWhen?.block !== undefined &&
12
- state.activeNodes.includes(item.activeWhen.block) &&
13
- (item.activeWhen?.headingLevel === undefined ||
14
- state.headingLevel === item.activeWhen.headingLevel)) ||
15
- (item.activeWhen?.mark !== undefined &&
16
- state.activeMarks.includes(item.activeWhen.mark)) ||
17
- (item.activeWhen?.table !== undefined &&
18
- state.table?.[item.activeWhen.table] === true);
19
- const isItemDisabled = (item, state) => !state.editable ||
20
- (item.disabledWhen === "cannotUndo" && !state.canUndo) ||
21
- (item.disabledWhen === "cannotRedo" && !state.canRedo) ||
22
- (item.disabledWhen === "cannotMergeCells" && !state.table?.canMergeCells);
23
- const TOOLBAR_BUTTON_SIZE = 32;
24
12
  const IOS_BLOCK_PICKER_PRESENTATION_MODIFIERS = [
25
13
  nativeFrame({ alignment: "topLeading", maxWidth: Infinity }),
26
14
  nativePadding({ leading: 16, top: 16, trailing: 16 }),
@@ -40,11 +28,9 @@ const NativeBlockPickerSheet = ({ children, isPresented, onDismiss, onIsPresente
40
28
  return (_jsx(SwiftUIHost, { pointerEvents: "none", style: { position: "absolute" }, children: _jsx(SwiftUIBottomSheet, { isPresented: isPresented, onDismiss: onDismiss, onIsPresentedChange: onIsPresentedChange, testID: "openeditor-block-picker", children: _jsx(SwiftUIGroup, { modifiers: IOS_BLOCK_PICKER_PRESENTATION_MODIFIERS, children: children }) }) }));
41
29
  };
42
30
  export const OpenEditorNativeToolbar = memo(function OpenEditorNativeToolbar({ availableNativeEffects = [], controller, state, items = defaultOpenEditorNativeToolbarItems, theme, style, onCommandError, visible = true, blockHandlesVisible = false, onBlockHandlesVisibleChange, }) {
43
- const [blockPickerOpen, setBlockPickerOpen] = useState(false);
31
+ const [blockPicker, setBlockPicker] = useState(EMPTY_OPENEDITOR_NATIVE_BLOCK_PICKER);
44
32
  const [linkEditorOpen, setLinkEditorOpen] = useState(false);
45
33
  const [linkValue, setLinkValue] = useState("");
46
- const pendingBlockRef = useRef(null);
47
- const pickerSelectionRef = useRef(undefined);
48
34
  const runCommand = (command) => {
49
35
  void command.catch((cause) => {
50
36
  onCommandError?.(cause instanceof Error
@@ -52,61 +38,18 @@ export const OpenEditorNativeToolbar = memo(function OpenEditorNativeToolbar({ a
52
38
  : new Error("OpenEditor command failed."));
53
39
  });
54
40
  };
55
- const colors = {
56
- background: theme?.surface ?? "#ffffff",
57
- border: theme?.border ?? "#e5e7eb",
58
- text: theme?.text ?? "#171717",
59
- muted: theme?.muted ?? "#737373",
60
- accent: theme?.accent ?? "#171717",
61
- accentText: theme?.accentText ?? "#ffffff",
62
- };
63
- const blockPickerItems = openEditorNativeBlockPickerItems.filter((item) => {
64
- if (item.key === "image")
65
- return availableNativeEffects.includes("pickImage");
66
- if (item.key === "attachment")
67
- return availableNativeEffects.includes("pickAttachment");
68
- if (item.key === "page")
69
- return availableNativeEffects.includes("createPage");
70
- return true;
71
- });
72
- const blockPickerGroups = blockPickerItems.reduce((groups, item) => {
73
- const current = groups.at(-1);
74
- if (current?.label === item.group)
75
- current.items.push(item);
76
- else
77
- groups.push({ label: item.group, items: [item] });
78
- return groups;
79
- }, []);
80
- const visibleToolbarItems = state.activeNodes.includes("table")
81
- ? [
82
- ...defaultOpenEditorNativeToolbarItems.filter((item) => item.action === "toggleBlockHandles"),
83
- ...openEditorNativeTableToolbarItems.filter((item) => item.key === "table-split-cell"
84
- ? state.table?.canSplitCell
85
- : item.key !== "table-merge-cells" || !state.table?.canSplitCell),
86
- ]
87
- : items;
41
+ const colors = resolveOpenEditorNativeToolbarColors(theme);
42
+ const blockPickerGroups = resolveOpenEditorNativeBlockPickerGroups(availableNativeEffects);
43
+ const visibleToolbarItems = resolveOpenEditorNativeToolbarItems(state, items);
88
44
  return (_jsxs(_Fragment, { children: [visible ? (_jsx(View, { style: [styles.root, style], children: _jsx(ScrollView, { contentContainerStyle: styles.content, horizontal: true, keyboardShouldPersistTaps: "always", showsHorizontalScrollIndicator: false, children: visibleToolbarItems.map((item) => {
89
- const active = item.action === "toggleBlockHandles"
90
- ? blockHandlesVisible
91
- : isItemActive(item, state);
92
- const disabled = isItemDisabled(item, state);
93
- const accessibilityLabel = item.action === "toggleBlockHandles" && blockHandlesVisible
94
- ? "Hide block handles"
95
- : item.label;
45
+ const { active, disabled, label: accessibilityLabel } = resolveOpenEditorNativeToolbarItemPresentation(item, state, blockHandlesVisible);
96
46
  return (_jsx(Pressable, { accessibilityLabel: accessibilityLabel, accessibilityRole: "button", accessibilityState: { disabled, selected: active }, disabled: disabled, hitSlop: 6, onPress: () => {
97
47
  if (item.action === "toggleBlockHandles") {
98
48
  onBlockHandlesVisibleChange?.(!blockHandlesVisible);
99
49
  return;
100
50
  }
101
51
  if (item.action === "openBlockPicker") {
102
- pickerSelectionRef.current =
103
- state.selection.type === "text"
104
- ? {
105
- anchor: state.selection.anchor,
106
- head: state.selection.head,
107
- }
108
- : undefined;
109
- setBlockPickerOpen(true);
52
+ setBlockPicker(openOpenEditorNativeBlockPicker(state.selection));
110
53
  return;
111
54
  }
112
55
  if (item.action === "openLinkEditor") {
@@ -130,14 +73,13 @@ export const OpenEditorNativeToolbar = memo(function OpenEditorNativeToolbar({ a
130
73
  : disabled
131
74
  ? colors.muted
132
75
  : colors.text, icon: openEditorIcons[item.icon], size: 20, strokeWidth: 1.8 }) }, item.key));
133
- }) }) })) : null, _jsx(NativeBlockPickerSheet, { isPresented: blockPickerOpen, onDismiss: () => {
134
- const block = pendingBlockRef.current;
135
- pendingBlockRef.current = null;
136
- pickerSelectionRef.current = undefined;
137
- if (block) {
138
- runCommand(controller.command(block).then(() => controller.focus()));
76
+ }) }) })) : null, _jsx(NativeBlockPickerSheet, { isPresented: blockPicker.presented, onDismiss: () => {
77
+ const result = dismissOpenEditorNativeBlockPicker(blockPicker);
78
+ setBlockPicker(result.state);
79
+ if (result.command) {
80
+ runCommand(controller.command(result.command).then(() => controller.focus()));
139
81
  }
140
- }, onIsPresentedChange: setBlockPickerOpen, children: _jsxs(NativeColumn, { alignment: "start", spacing: 0, style: { height: "100%", width: "100%" }, children: [_jsxs(NativeRow, { alignment: "center", spacing: 8, style: { paddingBottom: 10, width: "100%" }, children: [_jsx(NativeButton, { modifiers: Platform.OS === "ios"
82
+ }, onIsPresentedChange: (presented) => setBlockPicker((current) => ({ ...current, presented })), children: _jsxs(NativeColumn, { alignment: "start", spacing: 0, style: { height: "100%", width: "100%" }, children: [_jsxs(NativeRow, { alignment: "center", spacing: 8, style: { paddingBottom: 10, width: "100%" }, children: [_jsx(NativeButton, { modifiers: Platform.OS === "ios"
141
83
  ? [
142
84
  nativeAccessibilityLabel("Cancel"),
143
85
  glassEffect({
@@ -145,7 +87,7 @@ export const OpenEditorNativeToolbar = memo(function OpenEditorNativeToolbar({ a
145
87
  shape: "circle",
146
88
  }),
147
89
  ]
148
- : undefined, onPress: () => setBlockPickerOpen(false), style: { borderRadius: 18, height: 36, width: 36 }, testID: "close-block-picker", variant: "text", children: _jsx(NativeRNHostView, { matchContents: true, children: _jsx(View, { style: { height: 18, width: 18 }, children: _jsx(HugeiconsIcon, { accessible: false, color: colors.text, icon: openEditorIcons.close, size: 18, strokeWidth: 1.8 }) }) }) }), _jsx(NativeSpacer, { flexible: true }), _jsx(NativeText, { testID: "block-picker-title", textStyle: { fontSize: 17, fontWeight: "600" }, children: "Insert block" }), _jsx(NativeSpacer, { flexible: true }), _jsx(NativeButton, { modifiers: Platform.OS === "ios"
90
+ : undefined, onPress: () => setBlockPicker((current) => ({ ...current, presented: false })), style: { borderRadius: 18, height: 36, width: 36 }, testID: "close-block-picker", variant: "text", children: _jsx(NativeRNHostView, { matchContents: true, children: _jsx(View, { style: { height: 18, width: 18 }, children: _jsx(HugeiconsIcon, { accessible: false, color: colors.text, icon: openEditorIcons.close, size: 18, strokeWidth: 1.8 }) }) }) }), _jsx(NativeSpacer, { flexible: true }), _jsx(NativeText, { testID: "block-picker-title", textStyle: { fontSize: 17, fontWeight: "600" }, children: "Insert block" }), _jsx(NativeSpacer, { flexible: true }), _jsx(NativeButton, { modifiers: Platform.OS === "ios"
149
91
  ? [
150
92
  nativeAccessibilityLabel("Done"),
151
93
  glassEffect({
@@ -153,19 +95,14 @@ export const OpenEditorNativeToolbar = memo(function OpenEditorNativeToolbar({ a
153
95
  shape: "circle",
154
96
  }),
155
97
  ]
156
- : undefined, onPress: () => setBlockPickerOpen(false), style: { borderRadius: 18, height: 36, width: 36 }, testID: "done-block-picker", variant: "text", children: _jsx(NativeRNHostView, { matchContents: true, children: _jsx(View, { style: { height: 18, width: 18 }, children: _jsx(HugeiconsIcon, { accessible: false, color: colors.text, icon: openEditorIcons.check, size: 18, strokeWidth: 1.8 }) }) }) })] }), _jsx(NativeScrollView, { showsIndicators: true, style: { height: "100%", width: "100%" }, children: _jsx(NativeColumn, { alignment: "start", spacing: 20, style: { width: "100%" }, children: blockPickerGroups.map((group) => (_jsxs(NativeColumn, { alignment: "start", spacing: 4, style: { width: "100%" }, children: [_jsx(NativeText, { textStyle: {
98
+ : undefined, onPress: () => setBlockPicker((current) => ({ ...current, presented: false })), style: { borderRadius: 18, height: 36, width: 36 }, testID: "done-block-picker", variant: "text", children: _jsx(NativeRNHostView, { matchContents: true, children: _jsx(View, { style: { height: 18, width: 18 }, children: _jsx(HugeiconsIcon, { accessible: false, color: colors.text, icon: openEditorIcons.check, size: 18, strokeWidth: 1.8 }) }) }) })] }), _jsx(NativeScrollView, { showsIndicators: true, style: { height: "100%", width: "100%" }, children: _jsx(NativeColumn, { alignment: "start", spacing: 20, style: { width: "100%" }, children: blockPickerGroups.map((group) => (_jsxs(NativeColumn, { alignment: "start", spacing: 4, style: { width: "100%" }, children: [_jsx(NativeText, { textStyle: {
157
99
  color: colors.muted,
158
100
  fontSize: 12,
159
101
  fontWeight: "600",
160
102
  }, children: group.label }), group.items.map((item) => (_jsx(NativeListItem, { leading: (_jsx(HugeiconsIcon, { accessible: false, color: colors.text, icon: openEditorIcons[item.icon], size: 20, strokeWidth: 1.8 })), modifiers: Platform.OS === "ios"
161
103
  ? [nativePadding({ vertical: 10 })]
162
104
  : undefined, onPress: () => {
163
- pendingBlockRef.current = {
164
- type: "insertBlock",
165
- block: item.key,
166
- selection: pickerSelectionRef.current,
167
- };
168
- setBlockPickerOpen(false);
105
+ setBlockPicker((current) => selectOpenEditorNativeBlock(current, item.key));
169
106
  }, testID: `insert-${item.key}`, trailing: (_jsx(HugeiconsIcon, { accessible: false, color: colors.muted, icon: openEditorIcons.chevronRight, size: 16, strokeWidth: 1.8 })), children: _jsx(NativeText, { children: item.label }, `${item.key}-label`) }, item.key)))] }, group.label))) }) })] }) }), _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: [
170
107
  styles.linkInput,
171
108
  { borderColor: colors.border, color: colors.text },
@@ -190,10 +127,8 @@ const styles = StyleSheet.create({
190
127
  },
191
128
  button: {
192
129
  alignItems: "center",
193
- borderRadius: TOOLBAR_BUTTON_SIZE / 2,
194
- height: TOOLBAR_BUTTON_SIZE,
130
+ ...OPENEDITOR_NATIVE_TOOLBAR_BUTTON_GEOMETRY,
195
131
  justifyContent: "center",
196
- width: TOOLBAR_BUTTON_SIZE,
197
132
  },
198
133
  pressed: { opacity: 0.64 },
199
134
  disabled: { opacity: 0.42 },
@@ -0,0 +1,48 @@
1
+ import type { OpenEditorNativeEffectName, OpenEditorRuntimeCommand, OpenEditorRuntimeState } from "@openeditor/embedded-runtime";
2
+ import type { OpenEditorNativeTheme } from "./native-editor.js";
3
+ import { openEditorNativeBlockPickerItems, type OpenEditorNativeToolbarItem } from "./toolbar-items.js";
4
+ export type OpenEditorNativeToolbarColors = {
5
+ accent: string;
6
+ accentText: string;
7
+ background: string;
8
+ border: string;
9
+ muted: string;
10
+ text: string;
11
+ };
12
+ export declare const OPENEDITOR_NATIVE_TOOLBAR_BUTTON_GEOMETRY: {
13
+ borderRadius: number;
14
+ height: number;
15
+ width: number;
16
+ };
17
+ export declare const resolveOpenEditorNativeToolbarColors: (theme?: OpenEditorNativeTheme) => OpenEditorNativeToolbarColors;
18
+ export declare const isOpenEditorNativeToolbarItemActive: (item: OpenEditorNativeToolbarItem, state: OpenEditorRuntimeState) => boolean;
19
+ export declare const isOpenEditorNativeToolbarItemDisabled: (item: OpenEditorNativeToolbarItem, state: OpenEditorRuntimeState) => boolean;
20
+ export declare const resolveOpenEditorNativeToolbarItemPresentation: (item: OpenEditorNativeToolbarItem, state: OpenEditorRuntimeState, blockHandlesVisible: boolean) => {
21
+ active: boolean;
22
+ disabled: boolean;
23
+ label: string;
24
+ };
25
+ export declare const resolveOpenEditorNativeToolbarItems: (state: OpenEditorRuntimeState, items?: readonly OpenEditorNativeToolbarItem[]) => readonly OpenEditorNativeToolbarItem[];
26
+ export declare const resolveOpenEditorNativeBlockPickerGroups: (availableNativeEffects: readonly OpenEditorNativeEffectName[]) => {
27
+ label: string;
28
+ items: Array<(typeof openEditorNativeBlockPickerItems)[number]>;
29
+ }[];
30
+ export declare const createOpenEditorNativeBlockInsertion: (block: (typeof openEditorNativeBlockPickerItems)[number]["key"], selection: OpenEditorRuntimeState["selection"]) => Extract<OpenEditorRuntimeCommand, {
31
+ type: "insertBlock";
32
+ }>;
33
+ export type OpenEditorNativeBlockPickerState = {
34
+ pending: Extract<OpenEditorRuntimeCommand, {
35
+ type: "insertBlock";
36
+ }> | null;
37
+ presented: boolean;
38
+ selection: OpenEditorRuntimeState["selection"] | null;
39
+ };
40
+ export declare const EMPTY_OPENEDITOR_NATIVE_BLOCK_PICKER: OpenEditorNativeBlockPickerState;
41
+ export declare const openOpenEditorNativeBlockPicker: (selection: OpenEditorRuntimeState["selection"]) => OpenEditorNativeBlockPickerState;
42
+ export declare const selectOpenEditorNativeBlock: (state: OpenEditorNativeBlockPickerState, block: (typeof openEditorNativeBlockPickerItems)[number]["key"]) => OpenEditorNativeBlockPickerState;
43
+ export declare const dismissOpenEditorNativeBlockPicker: (state: OpenEditorNativeBlockPickerState) => {
44
+ command: Extract<OpenEditorRuntimeCommand, {
45
+ type: "insertBlock";
46
+ }> | null;
47
+ state: OpenEditorNativeBlockPickerState;
48
+ };
@@ -0,0 +1,83 @@
1
+ import { defaultOpenEditorNativeToolbarItems, openEditorNativeBlockPickerItems, openEditorNativeTableToolbarItems, } from "./toolbar-items.js";
2
+ export const OPENEDITOR_NATIVE_TOOLBAR_BUTTON_GEOMETRY = {
3
+ borderRadius: 16,
4
+ height: 32,
5
+ width: 32,
6
+ };
7
+ export const resolveOpenEditorNativeToolbarColors = (theme) => ({
8
+ background: theme?.surface ?? "#ffffff",
9
+ border: theme?.border ?? "#e5e7eb",
10
+ text: theme?.text ?? "#171717",
11
+ muted: theme?.muted ?? "#737373",
12
+ accent: theme?.accent ?? "#171717",
13
+ accentText: theme?.accentText ?? "#ffffff",
14
+ });
15
+ export const isOpenEditorNativeToolbarItemActive = (item, state) => (item.activeWhen?.block !== undefined
16
+ && state.activeNodes.includes(item.activeWhen.block)
17
+ && (item.activeWhen.headingLevel === undefined
18
+ || state.headingLevel === item.activeWhen.headingLevel))
19
+ || (item.activeWhen?.mark !== undefined
20
+ && state.activeMarks.includes(item.activeWhen.mark))
21
+ || (item.activeWhen?.table !== undefined
22
+ && state.table?.[item.activeWhen.table] === true);
23
+ export const isOpenEditorNativeToolbarItemDisabled = (item, state) => !state.editable
24
+ || (item.disabledWhen === "cannotUndo" && !state.canUndo)
25
+ || (item.disabledWhen === "cannotRedo" && !state.canRedo)
26
+ || (item.disabledWhen === "cannotMergeCells" && !state.table?.canMergeCells);
27
+ export const resolveOpenEditorNativeToolbarItemPresentation = (item, state, blockHandlesVisible) => {
28
+ const active = item.action === "toggleBlockHandles"
29
+ ? blockHandlesVisible
30
+ : isOpenEditorNativeToolbarItemActive(item, state);
31
+ return {
32
+ active,
33
+ disabled: isOpenEditorNativeToolbarItemDisabled(item, state),
34
+ label: item.action === "toggleBlockHandles" && blockHandlesVisible
35
+ ? "Hide block handles"
36
+ : item.label,
37
+ };
38
+ };
39
+ export const resolveOpenEditorNativeToolbarItems = (state, items = defaultOpenEditorNativeToolbarItems) => state.activeNodes.includes("table")
40
+ ? [
41
+ ...defaultOpenEditorNativeToolbarItems.filter((item) => item.action === "toggleBlockHandles"),
42
+ ...openEditorNativeTableToolbarItems.filter((item) => item.key === "table-split-cell"
43
+ ? state.table?.canSplitCell
44
+ : item.key !== "table-merge-cells" || !state.table?.canSplitCell),
45
+ ]
46
+ : items;
47
+ export const resolveOpenEditorNativeBlockPickerGroups = (availableNativeEffects) => openEditorNativeBlockPickerItems
48
+ .filter((item) => {
49
+ if (item.key === "image")
50
+ return availableNativeEffects.includes("pickImage");
51
+ if (item.key === "attachment")
52
+ return availableNativeEffects.includes("pickAttachment");
53
+ if (item.key === "page")
54
+ return availableNativeEffects.includes("createPage");
55
+ return true;
56
+ })
57
+ .reduce((groups, item) => {
58
+ const current = groups.at(-1);
59
+ if (current?.label === item.group)
60
+ current.items.push(item);
61
+ else
62
+ groups.push({ label: item.group, items: [item] });
63
+ return groups;
64
+ }, []);
65
+ export const createOpenEditorNativeBlockInsertion = (block, selection) => ({
66
+ type: "insertBlock",
67
+ block,
68
+ selection: selection.type === "text"
69
+ ? { anchor: selection.anchor, head: selection.head }
70
+ : undefined,
71
+ });
72
+ export const EMPTY_OPENEDITOR_NATIVE_BLOCK_PICKER = {
73
+ pending: null,
74
+ presented: false,
75
+ selection: null,
76
+ };
77
+ export const openOpenEditorNativeBlockPicker = (selection) => ({ pending: null, presented: true, selection });
78
+ export const selectOpenEditorNativeBlock = (state, block) => ({
79
+ ...state,
80
+ pending: createOpenEditorNativeBlockInsertion(block, state.selection ?? { type: "node", from: 0, to: 0 }),
81
+ presented: false,
82
+ });
83
+ export const dismissOpenEditorNativeBlockPicker = (state) => ({ command: state.pending, state: EMPTY_OPENEDITOR_NATIVE_BLOCK_PICKER });
@@ -0,0 +1,22 @@
1
+ import type { OpenEditorNativeTheme } from "./native-editor.js";
2
+ export declare const OPENEDITOR_NATIVE_TOOLBAR_SURFACE_GEOMETRY: {
3
+ borderRadius: number;
4
+ height: number;
5
+ overflow: "hidden";
6
+ };
7
+ export declare const resolveOpenEditorNativeToolbarMaterial: ({ active, glassAvailable, isIOS, theme, }: {
8
+ active?: boolean;
9
+ glassAvailable: boolean;
10
+ isIOS: boolean;
11
+ theme?: OpenEditorNativeTheme;
12
+ }) => {
13
+ kind: "glass";
14
+ glassEffectStyle: "none" | "regular";
15
+ backgroundColor?: undefined;
16
+ borderColor?: undefined;
17
+ } | {
18
+ kind: "fallback";
19
+ backgroundColor: string;
20
+ borderColor: string;
21
+ glassEffectStyle?: undefined;
22
+ };
@@ -0,0 +1,13 @@
1
+ import { OPENEDITOR_NATIVE_TOOLBAR_HEIGHT, OPENEDITOR_NATIVE_TOOLBAR_RADIUS, } from "./native-layout.js";
2
+ export const OPENEDITOR_NATIVE_TOOLBAR_SURFACE_GEOMETRY = {
3
+ borderRadius: OPENEDITOR_NATIVE_TOOLBAR_RADIUS,
4
+ height: OPENEDITOR_NATIVE_TOOLBAR_HEIGHT,
5
+ overflow: "hidden",
6
+ };
7
+ export const resolveOpenEditorNativeToolbarMaterial = ({ active = true, glassAvailable, isIOS, theme, }) => isIOS && glassAvailable
8
+ ? { kind: "glass", glassEffectStyle: active ? "regular" : "none" }
9
+ : {
10
+ kind: "fallback",
11
+ backgroundColor: theme?.surfaceRaised ?? theme?.surfaceMuted ?? theme?.surface ?? "#ffffff",
12
+ borderColor: theme?.border ?? "#e5e7eb",
13
+ };
@@ -1,32 +1,34 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { GlassView, isGlassEffectAPIAvailable, } from "expo-glass-effect";
3
3
  import { Platform, StyleSheet, View, } from "react-native";
4
- import { OPENEDITOR_NATIVE_TOOLBAR_HEIGHT, OPENEDITOR_NATIVE_TOOLBAR_RADIUS, } from "./native-layout.js";
4
+ import { OPENEDITOR_NATIVE_TOOLBAR_SURFACE_GEOMETRY, resolveOpenEditorNativeToolbarMaterial, } from "./toolbar-surface-model.js";
5
5
  /**
6
6
  * The platform material surrounding OpenEditor's keyboard toolbar.
7
7
  * iOS uses the system glass implementation when available; other runtimes
8
8
  * receive the same geometry with a theme-aware opaque fallback.
9
9
  */
10
10
  export function OpenEditorNativeToolbarSurface({ active = true, children, colorScheme = "auto", style, theme, }) {
11
- if (Platform.OS === "ios" && isGlassEffectAPIAvailable()) {
12
- return (_jsx(GlassView, { colorScheme: colorScheme, glassEffectStyle: active ? "regular" : "none", isInteractive: true, style: [styles.surface, style], children: children }));
11
+ const material = resolveOpenEditorNativeToolbarMaterial({
12
+ active,
13
+ glassAvailable: isGlassEffectAPIAvailable(),
14
+ isIOS: Platform.OS === "ios",
15
+ theme,
16
+ });
17
+ if (material.kind === "glass") {
18
+ return (_jsx(GlassView, { colorScheme: colorScheme, glassEffectStyle: material.glassEffectStyle, isInteractive: true, style: [styles.surface, style], children: children }));
13
19
  }
14
20
  return (_jsx(View, { style: [
15
21
  styles.surface,
16
22
  styles.fallback,
17
23
  {
18
- backgroundColor: theme?.surfaceRaised ?? theme?.surfaceMuted ?? theme?.surface ?? "#ffffff",
19
- borderColor: theme?.border ?? "#e5e7eb",
24
+ backgroundColor: material.backgroundColor,
25
+ borderColor: material.borderColor,
20
26
  },
21
27
  style,
22
28
  ], children: children }));
23
29
  }
24
30
  const styles = StyleSheet.create({
25
- surface: {
26
- borderRadius: OPENEDITOR_NATIVE_TOOLBAR_RADIUS,
27
- height: OPENEDITOR_NATIVE_TOOLBAR_HEIGHT,
28
- overflow: "hidden",
29
- },
31
+ surface: OPENEDITOR_NATIVE_TOOLBAR_SURFACE_GEOMETRY,
30
32
  fallback: {
31
33
  borderWidth: StyleSheet.hairlineWidth,
32
34
  },
@@ -0,0 +1,6 @@
1
+ {
2
+ "platforms": ["apple"],
3
+ "apple": {
4
+ "modules": ["OpenEditorNativeModule"]
5
+ }
6
+ }
@@ -0,0 +1,21 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'OpenEditorNative'
7
+ s.version = package['version']
8
+ s.summary = 'Native platform integration for OpenEditor.'
9
+ s.homepage = 'https://github.com/EasyLink-HQ/openeditor'
10
+ s.license = { :type => 'MIT' }
11
+ s.author = 'EasyLink HQ'
12
+ s.platforms = { :ios => '16.4' }
13
+ s.swift_version = '5.9'
14
+ s.source = { :git => 'https://github.com/EasyLink-HQ/openeditor.git', :tag => s.version.to_s }
15
+ s.static_framework = true
16
+
17
+ s.dependency 'ExpoModulesCore'
18
+ s.frameworks = 'WebKit'
19
+ s.source_files = '**/*.{h,m,mm,swift}'
20
+ s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
21
+ end
@@ -0,0 +1,28 @@
1
+ import ExpoModulesCore
2
+ import WebKit
3
+
4
+ public final class OpenEditorNativeModule: Module {
5
+ public func definition() -> ModuleDefinition {
6
+ Name("OpenEditorNative")
7
+
8
+ AsyncFunction("setInteractiveKeyboardDismissal") { (rootView: UIView) -> Bool in
9
+ guard let webView = Self.findWebView(in: rootView) else {
10
+ return false
11
+ }
12
+ webView.scrollView.keyboardDismissMode = .interactive
13
+ return true
14
+ }.runOnQueue(.main)
15
+ }
16
+
17
+ private static func findWebView(in view: UIView) -> WKWebView? {
18
+ if let webView = view as? WKWebView {
19
+ return webView
20
+ }
21
+ for subview in view.subviews {
22
+ if let webView = findWebView(in: subview) {
23
+ return webView
24
+ }
25
+ }
26
+ return nil
27
+ }
28
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openeditor/native",
3
- "version": "0.0.35",
3
+ "version": "0.0.37",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "repository": {
@@ -16,7 +16,8 @@
16
16
  },
17
17
  "files": [
18
18
  "dist",
19
- "plugin.cjs"
19
+ "expo-module.config.json",
20
+ "ios"
20
21
  ],
21
22
  "exports": {
22
23
  ".": {
@@ -24,17 +25,15 @@
24
25
  "types": "./dist/index.d.ts",
25
26
  "import": "./dist/index.js"
26
27
  },
27
- "./plugin": "./plugin.cjs",
28
28
  "./package.json": "./package.json"
29
29
  },
30
30
  "dependencies": {
31
- "@expo/config-plugins": "57.0.6",
32
- "@expo/ui": "~57.0.7",
31
+ "@expo/ui": "~57.0.9",
33
32
  "@hugeicons/react-native": "^1.0.15",
34
- "@openeditor/core": "0.0.35",
35
- "@openeditor/embedded-runtime": "0.0.35",
36
- "@openeditor/embedded-surface": "0.0.35",
37
- "@openeditor/icons": "0.0.35"
33
+ "@openeditor/core": "0.0.37",
34
+ "@openeditor/embedded-runtime": "0.0.37",
35
+ "@openeditor/embedded-surface": "0.0.37",
36
+ "@openeditor/icons": "0.0.37"
38
37
  },
39
38
  "peerDependencies": {
40
39
  "expo": ">=57",
package/plugin.cjs DELETED
@@ -1,79 +0,0 @@
1
- const { withAppDelegate } = require("@expo/config-plugins");
2
-
3
- const WEBKIT_IMPORT = "import WebKit\n";
4
- const OBSERVER_PROPERTY =
5
- " private var openEditorKeyboardWillShowObserver: NSObjectProtocol?\n";
6
- const OBSERVER_SETUP = ` openEditorKeyboardWillShowObserver = NotificationCenter.default.addObserver(
7
- forName: UIResponder.keyboardWillShowNotification,
8
- object: nil,
9
- queue: .main
10
- ) { [weak self] _ in
11
- guard let rootView = self?.window?.rootViewController?.view else { return }
12
- self?.enableOpenEditorInteractiveKeyboardDismissal(in: rootView)
13
- }
14
-
15
- `;
16
- const DISMISSAL_METHOD = ` deinit {
17
- if let openEditorKeyboardWillShowObserver {
18
- NotificationCenter.default.removeObserver(openEditorKeyboardWillShowObserver)
19
- }
20
- }
21
-
22
- private func enableOpenEditorInteractiveKeyboardDismissal(in view: UIView) {
23
- if let webView = view as? WKWebView {
24
- webView.scrollView.keyboardDismissMode = .interactive
25
- }
26
- view.subviews.forEach { enableOpenEditorInteractiveKeyboardDismissal(in: $0) }
27
- }
28
-
29
- `;
30
-
31
- const applyOpenEditorKeyboardDismissal = (contents) => {
32
- for (const anchor of [
33
- "import React\n",
34
- "class AppDelegate: ExpoAppDelegate {\n",
35
- " return super.application(application, didFinishLaunchingWithOptions: launchOptions)\n",
36
- " // Linking API\n",
37
- ]) {
38
- if (!contents.includes(anchor)) {
39
- throw new Error(
40
- `OpenEditor's Expo plugin could not find ${JSON.stringify(anchor)} in AppDelegate.swift.`,
41
- );
42
- }
43
- }
44
-
45
- let next = contents;
46
- if (!next.includes("import WebKit")) {
47
- next = next.replace("import React\n", `import React\n${WEBKIT_IMPORT}`);
48
- }
49
- if (!next.includes("openEditorKeyboardWillShowObserver")) {
50
- next = next.replace(
51
- "class AppDelegate: ExpoAppDelegate {\n",
52
- `class AppDelegate: ExpoAppDelegate {\n${OBSERVER_PROPERTY}`,
53
- );
54
- next = next.replace(
55
- " return super.application(application, didFinishLaunchingWithOptions: launchOptions)\n",
56
- `${OBSERVER_SETUP} return super.application(application, didFinishLaunchingWithOptions: launchOptions)\n`,
57
- );
58
- next = next.replace(
59
- " // Linking API\n",
60
- `${DISMISSAL_METHOD} // Linking API\n`,
61
- );
62
- }
63
- return next;
64
- };
65
-
66
- const withOpenEditorNative = (config) =>
67
- withAppDelegate(config, (mod) => {
68
- if (mod.modResults.language !== "swift") {
69
- throw new Error("OpenEditor's Expo plugin requires a Swift AppDelegate.");
70
- }
71
- mod.modResults.contents = applyOpenEditorKeyboardDismissal(
72
- mod.modResults.contents,
73
- );
74
- return mod;
75
- });
76
-
77
- module.exports = withOpenEditorNative;
78
- module.exports.applyOpenEditorKeyboardDismissal =
79
- applyOpenEditorKeyboardDismissal;