@apollohg/react-native-prose-editor 0.3.0 → 0.4.1
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 +18 -0
- package/android/build.gradle +23 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +515 -39
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +58 -28
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +25 -0
- package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +232 -62
- package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +57 -27
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +147 -78
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +249 -71
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +7 -6
- package/dist/EditorToolbar.d.ts +26 -6
- package/dist/EditorToolbar.js +299 -65
- package/dist/NativeEditorBridge.d.ts +40 -1
- package/dist/NativeEditorBridge.js +184 -90
- package/dist/NativeRichTextEditor.d.ts +5 -1
- package/dist/NativeRichTextEditor.js +201 -78
- package/dist/YjsCollaboration.d.ts +2 -0
- package/dist/YjsCollaboration.js +142 -20
- package/dist/index.d.ts +1 -1
- package/dist/schemas.js +12 -0
- package/dist/useNativeEditor.d.ts +2 -0
- package/dist/useNativeEditor.js +7 -0
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/EditorLayoutManager.swift +3 -3
- package/ios/Generated_editor_core.swift +87 -0
- package/ios/NativeEditorExpoView.swift +488 -178
- package/ios/NativeEditorModule.swift +25 -0
- package/ios/PositionBridge.swift +310 -75
- package/ios/RenderBridge.swift +362 -27
- package/ios/RichTextEditorView.swift +2001 -189
- package/ios/editor_coreFFI/editor_coreFFI.h +55 -0
- package/package.json +11 -2
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +128 -0
package/dist/EditorToolbar.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { ActiveState, HistoryState } from './NativeEditorBridge';
|
|
2
2
|
import type { EditorToolbarTheme } from './EditorTheme';
|
|
3
3
|
export type EditorToolbarListType = 'bulletList' | 'orderedList';
|
|
4
|
+
export type EditorToolbarHeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
4
5
|
export type EditorToolbarCommand = 'indentList' | 'outdentList' | 'undo' | 'redo';
|
|
5
|
-
export type
|
|
6
|
+
export type EditorToolbarGroupPresentation = 'expand' | 'menu';
|
|
7
|
+
export type EditorToolbarDefaultIconId = 'bold' | 'italic' | 'underline' | 'strike' | 'link' | 'image' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'blockquote' | 'bulletList' | 'orderedList' | 'indentList' | 'outdentList' | 'lineBreak' | 'horizontalRule' | 'undo' | 'redo';
|
|
6
8
|
export interface EditorToolbarSFSymbolIcon {
|
|
7
9
|
type: 'sfSymbol';
|
|
8
10
|
name: string;
|
|
@@ -23,7 +25,7 @@ export type EditorToolbarIcon = {
|
|
|
23
25
|
android?: EditorToolbarMaterialIcon;
|
|
24
26
|
fallbackText?: string;
|
|
25
27
|
};
|
|
26
|
-
export type
|
|
28
|
+
export type EditorToolbarLeafItem = {
|
|
27
29
|
type: 'mark';
|
|
28
30
|
mark: string;
|
|
29
31
|
label: string;
|
|
@@ -39,6 +41,12 @@ export type EditorToolbarItem = {
|
|
|
39
41
|
label: string;
|
|
40
42
|
icon: EditorToolbarIcon;
|
|
41
43
|
key?: string;
|
|
44
|
+
} | {
|
|
45
|
+
type: 'heading';
|
|
46
|
+
level: EditorToolbarHeadingLevel;
|
|
47
|
+
label: string;
|
|
48
|
+
icon: EditorToolbarIcon;
|
|
49
|
+
key?: string;
|
|
42
50
|
} | {
|
|
43
51
|
type: 'blockquote';
|
|
44
52
|
label: string;
|
|
@@ -62,9 +70,6 @@ export type EditorToolbarItem = {
|
|
|
62
70
|
label: string;
|
|
63
71
|
icon: EditorToolbarIcon;
|
|
64
72
|
key?: string;
|
|
65
|
-
} | {
|
|
66
|
-
type: 'separator';
|
|
67
|
-
key?: string;
|
|
68
73
|
} | {
|
|
69
74
|
type: 'action';
|
|
70
75
|
key: string;
|
|
@@ -73,6 +78,19 @@ export type EditorToolbarItem = {
|
|
|
73
78
|
isActive?: boolean;
|
|
74
79
|
isDisabled?: boolean;
|
|
75
80
|
};
|
|
81
|
+
export type EditorToolbarGroupChildItem = EditorToolbarLeafItem;
|
|
82
|
+
export interface EditorToolbarGroupItem {
|
|
83
|
+
type: 'group';
|
|
84
|
+
key: string;
|
|
85
|
+
label: string;
|
|
86
|
+
icon: EditorToolbarIcon;
|
|
87
|
+
presentation?: EditorToolbarGroupPresentation;
|
|
88
|
+
items: readonly EditorToolbarGroupChildItem[];
|
|
89
|
+
}
|
|
90
|
+
export type EditorToolbarItem = EditorToolbarLeafItem | EditorToolbarGroupItem | {
|
|
91
|
+
type: 'separator';
|
|
92
|
+
key?: string;
|
|
93
|
+
};
|
|
76
94
|
export declare const DEFAULT_EDITOR_TOOLBAR_ITEMS: readonly EditorToolbarItem[];
|
|
77
95
|
export interface EditorToolbarProps {
|
|
78
96
|
/** Currently active marks and nodes from the Rust engine. */
|
|
@@ -109,6 +127,8 @@ export interface EditorToolbarProps {
|
|
|
109
127
|
onToggleMark?: (mark: string) => void;
|
|
110
128
|
/** Generic list toggle handler used by configurable list buttons. */
|
|
111
129
|
onToggleListType?: (listType: EditorToolbarListType) => void;
|
|
130
|
+
/** Generic heading toggle handler used by configurable heading buttons. */
|
|
131
|
+
onToggleHeading?: (level: EditorToolbarHeadingLevel) => void;
|
|
112
132
|
/** Generic node insertion handler used by configurable node buttons. */
|
|
113
133
|
onInsertNodeType?: (nodeType: string) => void;
|
|
114
134
|
/** Generic command handler used by configurable command buttons. */
|
|
@@ -126,4 +146,4 @@ export interface EditorToolbarProps {
|
|
|
126
146
|
/** Whether to render the built-in top separator line. */
|
|
127
147
|
showTopBorder?: boolean;
|
|
128
148
|
}
|
|
129
|
-
export declare function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic, onToggleUnderline, onToggleStrike, onToggleBulletList, onToggleBlockquote, onToggleOrderedList, onIndentList, onOutdentList, onInsertHorizontalRule, onInsertLineBreak, onUndo, onRedo, onToggleMark, onToggleListType, onInsertNodeType, onRunCommand, onToolbarAction, onRequestLink, onRequestImage, toolbarItems, theme, showTopBorder, }: EditorToolbarProps): import("react/jsx-runtime").JSX.Element;
|
|
149
|
+
export declare function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic, onToggleUnderline, onToggleStrike, onToggleBulletList, onToggleHeading, onToggleBlockquote, onToggleOrderedList, onIndentList, onOutdentList, onInsertHorizontalRule, onInsertLineBreak, onUndo, onRedo, onToggleMark, onToggleListType, onInsertNodeType, onRunCommand, onToolbarAction, onRequestLink, onRequestImage, toolbarItems, theme, showTopBorder, }: EditorToolbarProps): import("react/jsx-runtime").JSX.Element;
|
package/dist/EditorToolbar.js
CHANGED
|
@@ -50,6 +50,8 @@ const BUTTON_HIT = 44;
|
|
|
50
50
|
const BUTTON_VISIBLE = 32;
|
|
51
51
|
const TOOLBAR_PADDING_H = 12;
|
|
52
52
|
const TOOLBAR_PADDING_V = 4;
|
|
53
|
+
const MENU_MARGIN = 8;
|
|
54
|
+
const MENU_WIDTH = 192;
|
|
53
55
|
const ACTIVE_BG = 'rgba(0, 122, 255, 0.12)';
|
|
54
56
|
const ACTIVE_COLOR = '#007AFF';
|
|
55
57
|
const DEFAULT_COLOR = '#666666';
|
|
@@ -59,6 +61,8 @@ const TOOLBAR_BG = '#FFFFFF';
|
|
|
59
61
|
const TOOLBAR_BORDER = '#E5E5EA';
|
|
60
62
|
const TOOLBAR_RADIUS = 0;
|
|
61
63
|
const BUTTON_RADIUS = 6;
|
|
64
|
+
const MENU_BORDER = '#D1D1D6';
|
|
65
|
+
const MENU_SHADOW = '#000000';
|
|
62
66
|
const DEFAULT_GLYPH_ICONS = {
|
|
63
67
|
bold: 'B',
|
|
64
68
|
italic: 'I',
|
|
@@ -67,6 +71,12 @@ const DEFAULT_GLYPH_ICONS = {
|
|
|
67
71
|
link: '🔗',
|
|
68
72
|
image: '🖼',
|
|
69
73
|
blockquote: '❝',
|
|
74
|
+
h1: 'H1',
|
|
75
|
+
h2: 'H2',
|
|
76
|
+
h3: 'H3',
|
|
77
|
+
h4: 'H4',
|
|
78
|
+
h5: 'H5',
|
|
79
|
+
h6: 'H6',
|
|
70
80
|
bulletList: '•≡',
|
|
71
81
|
orderedList: '1.',
|
|
72
82
|
indentList: '→',
|
|
@@ -83,6 +93,12 @@ const DEFAULT_MATERIAL_ICONS = {
|
|
|
83
93
|
strike: 'strikethrough-s',
|
|
84
94
|
link: 'link',
|
|
85
95
|
image: 'image',
|
|
96
|
+
h1: 'title',
|
|
97
|
+
h2: 'title',
|
|
98
|
+
h3: 'title',
|
|
99
|
+
h4: 'title',
|
|
100
|
+
h5: 'title',
|
|
101
|
+
h6: 'title',
|
|
86
102
|
blockquote: 'format-quote',
|
|
87
103
|
bulletList: 'format-list-bulleted',
|
|
88
104
|
orderedList: 'format-list-numbered',
|
|
@@ -93,20 +109,22 @@ const DEFAULT_MATERIAL_ICONS = {
|
|
|
93
109
|
undo: 'undo',
|
|
94
110
|
redo: 'redo',
|
|
95
111
|
};
|
|
96
|
-
function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic, onToggleUnderline, onToggleStrike, onToggleBulletList, onToggleBlockquote, onToggleOrderedList, onIndentList, onOutdentList, onInsertHorizontalRule, onInsertLineBreak, onUndo, onRedo, onToggleMark, onToggleListType, onInsertNodeType, onRunCommand, onToolbarAction, onRequestLink, onRequestImage, toolbarItems = exports.DEFAULT_EDITOR_TOOLBAR_ITEMS, theme, showTopBorder = true, }) {
|
|
112
|
+
function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic, onToggleUnderline, onToggleStrike, onToggleBulletList, onToggleHeading, onToggleBlockquote, onToggleOrderedList, onIndentList, onOutdentList, onInsertHorizontalRule, onInsertLineBreak, onUndo, onRedo, onToggleMark, onToggleListType, onInsertNodeType, onRunCommand, onToolbarAction, onRequestLink, onRequestImage, toolbarItems = exports.DEFAULT_EDITOR_TOOLBAR_ITEMS, theme, showTopBorder = true, }) {
|
|
97
113
|
const marks = activeState.marks ?? {};
|
|
98
114
|
const nodes = activeState.nodes ?? {};
|
|
99
115
|
const commands = activeState.commands ?? {};
|
|
100
116
|
const allowedMarks = activeState.allowedMarks ?? [];
|
|
101
117
|
const insertableNodes = activeState.insertableNodes ?? [];
|
|
118
|
+
const groupButtonRefs = (0, react_1.useRef)(new Map());
|
|
119
|
+
const { width: windowWidth, height: windowHeight } = (0, react_native_1.useWindowDimensions)();
|
|
120
|
+
const [expandedGroupKey, setExpandedGroupKey] = (0, react_1.useState)(null);
|
|
121
|
+
const [menuState, setMenuState] = (0, react_1.useState)(null);
|
|
102
122
|
const isMarkActive = (0, react_1.useCallback)((mark) => !!marks[mark], [marks]);
|
|
103
123
|
const isInList = !!nodes['bulletList'] || !!nodes['orderedList'];
|
|
104
124
|
const canIndentList = isInList && !!commands['indentList'];
|
|
105
125
|
const canOutdentList = isInList && !!commands['outdentList'];
|
|
106
126
|
const getActionForItem = (0, react_1.useCallback)((item) => {
|
|
107
127
|
switch (item.type) {
|
|
108
|
-
case 'separator':
|
|
109
|
-
return null;
|
|
110
128
|
case 'mark':
|
|
111
129
|
if (onToggleMark) {
|
|
112
130
|
return () => onToggleMark(item.mark);
|
|
@@ -134,6 +152,8 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
134
152
|
return onRequestLink ?? null;
|
|
135
153
|
case 'image':
|
|
136
154
|
return onRequestImage ?? null;
|
|
155
|
+
case 'heading':
|
|
156
|
+
return onToggleHeading ? () => onToggleHeading(item.level) : null;
|
|
137
157
|
case 'blockquote':
|
|
138
158
|
return onToggleBlockquote ?? null;
|
|
139
159
|
case 'node':
|
|
@@ -173,49 +193,44 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
173
193
|
onOutdentList,
|
|
174
194
|
onRedo,
|
|
175
195
|
onRunCommand,
|
|
176
|
-
onRequestLink,
|
|
177
196
|
onRequestImage,
|
|
178
|
-
|
|
179
|
-
onToggleBold,
|
|
197
|
+
onRequestLink,
|
|
180
198
|
onToggleBlockquote,
|
|
199
|
+
onToggleBold,
|
|
181
200
|
onToggleBulletList,
|
|
201
|
+
onToggleHeading,
|
|
182
202
|
onToggleItalic,
|
|
183
203
|
onToggleListType,
|
|
184
204
|
onToggleMark,
|
|
185
205
|
onToggleOrderedList,
|
|
186
206
|
onToggleStrike,
|
|
187
207
|
onToggleUnderline,
|
|
208
|
+
onToolbarAction,
|
|
188
209
|
onUndo,
|
|
189
210
|
]);
|
|
190
|
-
const makeButtonKey = (0, react_1.useCallback)((item, index) => item.key
|
|
191
|
-
|
|
192
|
-
|
|
211
|
+
const makeButtonKey = (0, react_1.useCallback)((item, index, prefix = '') => item.key != null
|
|
212
|
+
? `${prefix}${item.key}`
|
|
213
|
+
: item.type === 'mark'
|
|
214
|
+
? `${prefix}mark:${item.mark}:${index}`
|
|
193
215
|
: item.type === 'link'
|
|
194
|
-
?
|
|
216
|
+
? `${prefix}link:${index}`
|
|
195
217
|
: item.type === 'image'
|
|
196
|
-
?
|
|
197
|
-
: item.type === '
|
|
198
|
-
?
|
|
199
|
-
: item.type === '
|
|
200
|
-
?
|
|
201
|
-
: item.type === '
|
|
202
|
-
?
|
|
203
|
-
: item.type === '
|
|
204
|
-
?
|
|
205
|
-
:
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (item.type === 'separator') {
|
|
210
|
-
renderedItems.push({
|
|
211
|
-
type: 'separator',
|
|
212
|
-
key: item.key ?? `separator:${index}`,
|
|
213
|
-
});
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
218
|
+
? `${prefix}image:${index}`
|
|
219
|
+
: item.type === 'heading'
|
|
220
|
+
? `${prefix}heading:${item.level}:${index}`
|
|
221
|
+
: item.type === 'blockquote'
|
|
222
|
+
? `${prefix}blockquote:${index}`
|
|
223
|
+
: item.type === 'list'
|
|
224
|
+
? `${prefix}list:${item.listType}:${index}`
|
|
225
|
+
: item.type === 'command'
|
|
226
|
+
? `${prefix}command:${item.command}:${index}`
|
|
227
|
+
: item.type === 'node'
|
|
228
|
+
? `${prefix}node:${item.nodeType}:${index}`
|
|
229
|
+
: `${prefix}action:${item.key}:${index}`, []);
|
|
230
|
+
const resolveButton = (0, react_1.useCallback)((item, index, prefix = '', groupKey) => {
|
|
216
231
|
const action = getActionForItem(item);
|
|
217
232
|
if (!action) {
|
|
218
|
-
|
|
233
|
+
return null;
|
|
219
234
|
}
|
|
220
235
|
let isActive = false;
|
|
221
236
|
let isDisabled = false;
|
|
@@ -231,6 +246,12 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
231
246
|
case 'image':
|
|
232
247
|
isDisabled = !insertableNodes.includes('image') || !onRequestImage;
|
|
233
248
|
break;
|
|
249
|
+
case 'heading': {
|
|
250
|
+
const headingNodeType = `h${item.level}`;
|
|
251
|
+
isActive = !!nodes[headingNodeType];
|
|
252
|
+
isDisabled = !commands[`toggleHeading${item.level}`];
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
234
255
|
case 'blockquote':
|
|
235
256
|
isActive = !!nodes['blockquote'];
|
|
236
257
|
isDisabled = !commands['toggleBlockquote'];
|
|
@@ -265,46 +286,176 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
265
286
|
isDisabled = !insertableNodes.includes(item.nodeType);
|
|
266
287
|
break;
|
|
267
288
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
289
|
+
return {
|
|
290
|
+
key: makeButtonKey(item, index, prefix),
|
|
291
|
+
label: item.label,
|
|
292
|
+
icon: item.icon,
|
|
293
|
+
action,
|
|
294
|
+
isActive,
|
|
295
|
+
isDisabled,
|
|
296
|
+
groupKey,
|
|
297
|
+
};
|
|
298
|
+
}, [
|
|
299
|
+
allowedMarks,
|
|
300
|
+
canIndentList,
|
|
301
|
+
canOutdentList,
|
|
302
|
+
commands,
|
|
303
|
+
getActionForItem,
|
|
304
|
+
historyState.canRedo,
|
|
305
|
+
historyState.canUndo,
|
|
306
|
+
insertableNodes,
|
|
307
|
+
isMarkActive,
|
|
308
|
+
makeButtonKey,
|
|
309
|
+
nodes,
|
|
310
|
+
onRequestImage,
|
|
311
|
+
onRequestLink,
|
|
312
|
+
onToolbarAction,
|
|
313
|
+
]);
|
|
314
|
+
const { renderedItems, groupsByKey } = (0, react_1.useMemo)(() => {
|
|
315
|
+
const entries = [];
|
|
316
|
+
const nextGroups = new Map();
|
|
317
|
+
for (let index = 0; index < toolbarItems.length; index += 1) {
|
|
318
|
+
const item = toolbarItems[index];
|
|
319
|
+
if (item.type === 'separator') {
|
|
320
|
+
entries.push({
|
|
321
|
+
type: 'separator',
|
|
322
|
+
key: item.key ?? `separator:${index}`,
|
|
323
|
+
});
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (item.type === 'group') {
|
|
327
|
+
const children = item.items
|
|
328
|
+
.map((child, childIndex) => resolveButton(child, childIndex, `${item.key}:`, item.key))
|
|
329
|
+
.filter((child) => child != null);
|
|
330
|
+
if (children.length === 0) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const presentation = item.presentation ?? 'expand';
|
|
334
|
+
const isExpanded = presentation === 'expand' && expandedGroupKey === item.key;
|
|
335
|
+
const isMenuOpen = presentation === 'menu' && menuState?.groupKey === item.key;
|
|
336
|
+
const group = {
|
|
337
|
+
key: item.key,
|
|
338
|
+
label: item.label,
|
|
339
|
+
icon: item.icon,
|
|
340
|
+
presentation,
|
|
341
|
+
children,
|
|
342
|
+
isActive: children.some((child) => child.isActive) || isExpanded || isMenuOpen,
|
|
343
|
+
isDisabled: children.every((child) => child.isDisabled),
|
|
344
|
+
isExpanded,
|
|
345
|
+
isOpen: isExpanded || isMenuOpen,
|
|
346
|
+
};
|
|
347
|
+
nextGroups.set(group.key, group);
|
|
348
|
+
entries.push({ type: 'group', group });
|
|
349
|
+
if (group.isExpanded) {
|
|
350
|
+
entries.push(...children.map((child) => ({ type: 'button', button: child })));
|
|
351
|
+
}
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
const button = resolveButton(item, index);
|
|
355
|
+
if (button) {
|
|
356
|
+
entries.push({ type: 'button', button });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
renderedItems: entries.filter((entry, index, list) => {
|
|
361
|
+
if (entry.type !== 'separator') {
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
const previous = list[index - 1];
|
|
365
|
+
const next = list[index + 1];
|
|
366
|
+
return (previous != null &&
|
|
367
|
+
previous.type !== 'separator' &&
|
|
368
|
+
next != null &&
|
|
369
|
+
next.type !== 'separator');
|
|
370
|
+
}),
|
|
371
|
+
groupsByKey: nextGroups,
|
|
372
|
+
};
|
|
373
|
+
}, [expandedGroupKey, menuState?.groupKey, resolveButton, toolbarItems]);
|
|
374
|
+
(0, react_1.useEffect)(() => {
|
|
375
|
+
if (expandedGroupKey != null && !groupsByKey.has(expandedGroupKey)) {
|
|
376
|
+
setExpandedGroupKey(null);
|
|
377
|
+
}
|
|
378
|
+
}, [expandedGroupKey, groupsByKey]);
|
|
379
|
+
(0, react_1.useEffect)(() => {
|
|
380
|
+
if (menuState != null && !groupsByKey.has(menuState.groupKey)) {
|
|
381
|
+
setMenuState(null);
|
|
382
|
+
}
|
|
383
|
+
}, [groupsByKey, menuState]);
|
|
384
|
+
const handleButtonPress = (0, react_1.useCallback)((button) => {
|
|
385
|
+
button.action();
|
|
386
|
+
if (button.groupKey) {
|
|
387
|
+
setExpandedGroupKey((current) => (current === button.groupKey ? null : current));
|
|
283
388
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
389
|
+
setMenuState(null);
|
|
390
|
+
}, []);
|
|
391
|
+
const handleGroupPress = (0, react_1.useCallback)((group) => {
|
|
392
|
+
if (group.isDisabled) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (group.presentation === 'expand') {
|
|
396
|
+
setMenuState(null);
|
|
397
|
+
setExpandedGroupKey((current) => (current === group.key ? null : group.key));
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const anchor = groupButtonRefs.current.get(group.key);
|
|
401
|
+
if (!anchor) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
anchor.measureInWindow((x, y, width, height) => {
|
|
405
|
+
setExpandedGroupKey(null);
|
|
406
|
+
setMenuState((current) => current?.groupKey === group.key
|
|
407
|
+
? null
|
|
408
|
+
: {
|
|
409
|
+
groupKey: group.key,
|
|
410
|
+
x,
|
|
411
|
+
y,
|
|
412
|
+
width,
|
|
413
|
+
height,
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
}, []);
|
|
417
|
+
const menuGroup = menuState != null ? groupsByKey.get(menuState.groupKey) ?? null : null;
|
|
418
|
+
const menuHeight = menuGroup ? menuGroup.children.length * 40 + 16 : 0;
|
|
419
|
+
const menuTop = menuState == null
|
|
420
|
+
? 0
|
|
421
|
+
: Math.max(MENU_MARGIN, Math.min(menuState.y + menuState.height + 8, windowHeight - menuHeight - MENU_MARGIN));
|
|
422
|
+
const menuLeft = menuState == null
|
|
423
|
+
? 0
|
|
424
|
+
: Math.max(MENU_MARGIN, Math.min(menuState.x + menuState.width - MENU_WIDTH, windowWidth - MENU_WIDTH - MENU_MARGIN));
|
|
425
|
+
const renderButton = (button, onPress, options) => {
|
|
289
426
|
const activeColor = theme?.buttonActiveColor ?? ACTIVE_COLOR;
|
|
290
427
|
const defaultColor = theme?.buttonColor ?? DEFAULT_COLOR;
|
|
291
428
|
const disabledColor = theme?.buttonDisabledColor ?? DISABLED_COLOR;
|
|
292
|
-
const color = isActive ? activeColor : isDisabled ? disabledColor : defaultColor;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
429
|
+
const color = button.isActive ? activeColor : button.isDisabled ? disabledColor : defaultColor;
|
|
430
|
+
const anchorGroupKey = options?.anchorGroupKey;
|
|
431
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { ref: anchorGroupKey == null
|
|
432
|
+
? undefined
|
|
433
|
+
: (node) => {
|
|
434
|
+
if (node) {
|
|
435
|
+
groupButtonRefs.current.set(anchorGroupKey, node);
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
groupButtonRefs.current.delete(anchorGroupKey);
|
|
439
|
+
}
|
|
440
|
+
}, collapsable: false, style: styles.buttonAnchor, children: [(0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { onPress: onPress, disabled: button.isDisabled, style: [
|
|
441
|
+
styles.button,
|
|
442
|
+
{
|
|
443
|
+
borderRadius: theme?.buttonBorderRadius ?? BUTTON_RADIUS,
|
|
444
|
+
},
|
|
445
|
+
button.isActive && {
|
|
446
|
+
backgroundColor: theme?.buttonActiveBackgroundColor ?? ACTIVE_BG,
|
|
447
|
+
},
|
|
448
|
+
], activeOpacity: 0.5, accessibilityRole: 'button', accessibilityLabel: button.label, accessibilityState: {
|
|
449
|
+
selected: button.isActive,
|
|
450
|
+
disabled: button.isDisabled,
|
|
451
|
+
expanded: options?.showsDisclosure ? options.expanded : undefined,
|
|
452
|
+
}, children: (0, jsx_runtime_1.jsx)(react_native_1.View, { children: (0, jsx_runtime_1.jsx)(ToolbarIcon, { icon: button.icon, color: color }) }) }), options?.showsDisclosure ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.groupDisclosure, { color }], children: '\u25BE' })) : null] }, button.key));
|
|
302
453
|
};
|
|
303
454
|
const renderSeparator = (key) => ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
|
|
304
455
|
styles.separator,
|
|
305
456
|
theme?.separatorColor != null ? { backgroundColor: theme.separatorColor } : null,
|
|
306
457
|
] }, key));
|
|
307
|
-
return ((0, jsx_runtime_1.
|
|
458
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
|
|
308
459
|
styles.container,
|
|
309
460
|
!showTopBorder && styles.containerWithoutTopBorder,
|
|
310
461
|
theme?.backgroundColor != null ? { backgroundColor: theme.backgroundColor } : null,
|
|
@@ -321,9 +472,55 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
321
472
|
{
|
|
322
473
|
borderRadius: theme?.borderRadius ?? TOOLBAR_RADIUS,
|
|
323
474
|
},
|
|
324
|
-
], children: (0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, contentContainerStyle: styles.scrollContent, keyboardShouldPersistTaps: 'always', children:
|
|
325
|
-
|
|
326
|
-
|
|
475
|
+
], children: [(0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, contentContainerStyle: styles.scrollContent, keyboardShouldPersistTaps: 'always', onScrollBeginDrag: () => setMenuState(null), children: renderedItems.map((item) => {
|
|
476
|
+
if (item.type === 'separator') {
|
|
477
|
+
return renderSeparator(item.key);
|
|
478
|
+
}
|
|
479
|
+
if (item.type === 'group') {
|
|
480
|
+
return renderButton({
|
|
481
|
+
key: item.group.key,
|
|
482
|
+
label: item.group.label,
|
|
483
|
+
icon: item.group.icon,
|
|
484
|
+
isActive: item.group.isActive,
|
|
485
|
+
isDisabled: item.group.isDisabled,
|
|
486
|
+
}, () => handleGroupPress(item.group), {
|
|
487
|
+
anchorGroupKey: item.group.key,
|
|
488
|
+
showsDisclosure: true,
|
|
489
|
+
expanded: item.group.isOpen,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
return renderButton(item.button, () => handleButtonPress(item.button));
|
|
493
|
+
}) }), menuState != null && menuGroup != null ? ((0, jsx_runtime_1.jsx)(react_native_1.Modal, { transparent: true, visible: true, animationType: 'fade', onRequestClose: () => setMenuState(null), children: (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { style: styles.menuBackdrop, onPress: () => setMenuState(null), children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
|
|
494
|
+
styles.menuCard,
|
|
495
|
+
{
|
|
496
|
+
top: menuTop,
|
|
497
|
+
left: menuLeft,
|
|
498
|
+
backgroundColor: theme?.backgroundColor ?? TOOLBAR_BG,
|
|
499
|
+
borderColor: theme?.borderColor ?? MENU_BORDER,
|
|
500
|
+
},
|
|
501
|
+
], children: menuGroup.children.map((button) => {
|
|
502
|
+
const activeColor = theme?.buttonActiveColor ?? ACTIVE_COLOR;
|
|
503
|
+
const defaultColor = theme?.buttonColor ?? DEFAULT_COLOR;
|
|
504
|
+
const disabledColor = theme?.buttonDisabledColor ?? DISABLED_COLOR;
|
|
505
|
+
const color = button.isActive
|
|
506
|
+
? activeColor
|
|
507
|
+
: button.isDisabled
|
|
508
|
+
? disabledColor
|
|
509
|
+
: defaultColor;
|
|
510
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { onPress: () => handleButtonPress(button), disabled: button.isDisabled, style: ({ pressed }) => [
|
|
511
|
+
styles.menuItem,
|
|
512
|
+
button.isActive && {
|
|
513
|
+
backgroundColor: theme?.buttonActiveBackgroundColor ?? ACTIVE_BG,
|
|
514
|
+
},
|
|
515
|
+
pressed &&
|
|
516
|
+
!button.isDisabled && {
|
|
517
|
+
opacity: 0.75,
|
|
518
|
+
},
|
|
519
|
+
], accessibilityRole: 'button', accessibilityLabel: button.label, accessibilityState: {
|
|
520
|
+
selected: button.isActive,
|
|
521
|
+
disabled: button.isDisabled,
|
|
522
|
+
}, children: [(0, jsx_runtime_1.jsx)(ToolbarIcon, { icon: button.icon, color: color }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.menuLabel, { color }], children: button.label })] }, button.key));
|
|
523
|
+
}) }) }) })) : null] }));
|
|
327
524
|
}
|
|
328
525
|
function ToolbarIcon({ icon, color }) {
|
|
329
526
|
const materialIconName = resolveMaterialIconName(icon);
|
|
@@ -370,6 +567,9 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
370
567
|
paddingHorizontal: TOOLBAR_PADDING_H,
|
|
371
568
|
minWidth: '100%',
|
|
372
569
|
},
|
|
570
|
+
buttonAnchor: {
|
|
571
|
+
position: 'relative',
|
|
572
|
+
},
|
|
373
573
|
button: {
|
|
374
574
|
width: BUTTON_HIT,
|
|
375
575
|
height: BUTTON_VISIBLE,
|
|
@@ -377,6 +577,13 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
377
577
|
alignItems: 'center',
|
|
378
578
|
borderRadius: BUTTON_RADIUS,
|
|
379
579
|
},
|
|
580
|
+
groupDisclosure: {
|
|
581
|
+
position: 'absolute',
|
|
582
|
+
right: 5,
|
|
583
|
+
bottom: 2,
|
|
584
|
+
fontSize: 9,
|
|
585
|
+
fontWeight: '700',
|
|
586
|
+
},
|
|
380
587
|
separator: {
|
|
381
588
|
width: react_native_1.StyleSheet.hairlineWidth,
|
|
382
589
|
height: 20,
|
|
@@ -391,4 +598,31 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
391
598
|
fontSize: 16,
|
|
392
599
|
fontWeight: '600',
|
|
393
600
|
},
|
|
601
|
+
menuBackdrop: {
|
|
602
|
+
flex: 1,
|
|
603
|
+
},
|
|
604
|
+
menuCard: {
|
|
605
|
+
position: 'absolute',
|
|
606
|
+
width: MENU_WIDTH,
|
|
607
|
+
borderRadius: 14,
|
|
608
|
+
borderWidth: react_native_1.StyleSheet.hairlineWidth,
|
|
609
|
+
paddingVertical: 8,
|
|
610
|
+
shadowColor: MENU_SHADOW,
|
|
611
|
+
shadowOpacity: 0.16,
|
|
612
|
+
shadowRadius: 18,
|
|
613
|
+
shadowOffset: { width: 0, height: 8 },
|
|
614
|
+
elevation: 10,
|
|
615
|
+
},
|
|
616
|
+
menuItem: {
|
|
617
|
+
minHeight: 40,
|
|
618
|
+
paddingHorizontal: 12,
|
|
619
|
+
flexDirection: 'row',
|
|
620
|
+
alignItems: 'center',
|
|
621
|
+
},
|
|
622
|
+
menuLabel: {
|
|
623
|
+
flex: 1,
|
|
624
|
+
marginLeft: 10,
|
|
625
|
+
fontSize: 14,
|
|
626
|
+
fontWeight: '500',
|
|
627
|
+
},
|
|
394
628
|
});
|