@openeditor/native 0.0.36 → 0.0.38
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 +4 -12
- package/dist/native-editor.js +12 -2
- package/dist/native-module.d.ts +1 -0
- package/dist/native-module.js +7 -0
- package/dist/toolbar-host.js +19 -84
- package/dist/toolbar-model.d.ts +48 -0
- package/dist/toolbar-model.js +83 -0
- package/dist/toolbar-surface-model.d.ts +22 -0
- package/dist/toolbar-surface-model.js +13 -0
- package/dist/toolbar-surface.js +12 -10
- package/expo-module.config.json +6 -0
- package/ios/OpenEditorNative.podspec +21 -0
- package/ios/OpenEditorNativeModule.swift +28 -0
- package/package.json +8 -9
- package/plugin.cjs +0 -79
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
|
-
|
|
30
|
-
dismissal
|
|
31
|
-
|
|
32
|
-
|
|
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
|
package/dist/native-editor.js
CHANGED
|
@@ -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
|
+
};
|
package/dist/toolbar-host.js
CHANGED
|
@@ -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,
|
|
7
|
+
import { memo, useState } from "react";
|
|
8
8
|
import { Keyboard, Modal, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View, } from "react-native";
|
|
9
|
-
import { defaultOpenEditorNativeToolbarItems,
|
|
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 [
|
|
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
|
-
|
|
57
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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:
|
|
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: () =>
|
|
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: () =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|
package/dist/toolbar-surface.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
12
|
-
|
|
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:
|
|
19
|
-
borderColor:
|
|
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,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.
|
|
3
|
+
"version": "0.0.38",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"repository": {
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"dist",
|
|
19
|
-
"
|
|
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/
|
|
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
|
-
"@openeditor/embedded-runtime": "0.0.
|
|
36
|
-
"@openeditor/embedded-surface": "0.0.
|
|
37
|
-
"@openeditor/icons": "0.0.
|
|
33
|
+
"@openeditor/core": "0.0.38",
|
|
34
|
+
"@openeditor/embedded-runtime": "0.0.38",
|
|
35
|
+
"@openeditor/embedded-surface": "0.0.38",
|
|
36
|
+
"@openeditor/icons": "0.0.38"
|
|
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;
|