@apollohg/react-native-prose-editor 0.5.9 → 0.5.11
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/android/src/main/java/com/apollohg/editor/EditorEditText.kt +71 -36
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +103 -14
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +150 -23
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +4 -0
- package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +2 -1
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +10 -0
- package/dist/EditorToolbar.d.ts +18 -1
- package/dist/EditorToolbar.js +156 -4
- package/dist/NativeRichTextEditor.d.ts +16 -0
- package/dist/NativeRichTextEditor.js +87 -11
- package/dist/index.d.ts +1 -1
- 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/NativeEditorExpoView.swift +70 -8
- package/ios/NativeEditorModule.swift +3 -0
- package/ios/RichTextEditorView.swift +210 -31
- package/package.json +1 -1
- 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/dist/EditorToolbar.js
CHANGED
|
@@ -1,11 +1,95 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DEFAULT_EDITOR_TOOLBAR_ITEMS = void 0;
|
|
4
|
+
exports.isEditorToolbarFocusPreservationActive = isEditorToolbarFocusPreservationActive;
|
|
5
|
+
exports.useEditorToolbarFrames = useEditorToolbarFrames;
|
|
6
|
+
exports._setEditorToolbarFrameForTests = _setEditorToolbarFrameForTests;
|
|
7
|
+
exports._resetEditorToolbarFrameRegistryForTests = _resetEditorToolbarFrameRegistryForTests;
|
|
8
|
+
exports._beginEditorToolbarInteractionForTests = _beginEditorToolbarInteractionForTests;
|
|
9
|
+
exports._endEditorToolbarInteractionForTests = _endEditorToolbarInteractionForTests;
|
|
4
10
|
exports.EditorToolbar = EditorToolbar;
|
|
5
11
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
12
|
const vector_icons_1 = require("@expo/vector-icons");
|
|
7
13
|
const react_1 = require("react");
|
|
8
14
|
const react_native_1 = require("react-native");
|
|
15
|
+
const editorToolbarFrames = new Map();
|
|
16
|
+
const editorToolbarFrameListeners = new Set();
|
|
17
|
+
let nextEditorToolbarRegistrationId = 1;
|
|
18
|
+
let activeEditorToolbarInteractions = 0;
|
|
19
|
+
let editorToolbarFocusPreserveUntil = 0;
|
|
20
|
+
const EDITOR_TOOLBAR_FOCUS_PRESERVE_MS = 750;
|
|
21
|
+
function areToolbarFramesEqual(left, right) {
|
|
22
|
+
return (left?.x === right?.x &&
|
|
23
|
+
left?.y === right?.y &&
|
|
24
|
+
left?.width === right?.width &&
|
|
25
|
+
left?.height === right?.height);
|
|
26
|
+
}
|
|
27
|
+
function notifyEditorToolbarFrameListeners() {
|
|
28
|
+
editorToolbarFrameListeners.forEach((listener) => listener());
|
|
29
|
+
}
|
|
30
|
+
function getEditorToolbarFramesSnapshot() {
|
|
31
|
+
return Array.from(editorToolbarFrames.values());
|
|
32
|
+
}
|
|
33
|
+
function registerEditorToolbarFrame(id, frame) {
|
|
34
|
+
if (frame == null || frame.width <= 0 || frame.height <= 0) {
|
|
35
|
+
if (editorToolbarFrames.delete(id)) {
|
|
36
|
+
notifyEditorToolbarFrameListeners();
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const currentFrame = editorToolbarFrames.get(id);
|
|
41
|
+
if (areToolbarFramesEqual(currentFrame, frame)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
editorToolbarFrames.set(id, frame);
|
|
45
|
+
notifyEditorToolbarFrameListeners();
|
|
46
|
+
}
|
|
47
|
+
function unregisterEditorToolbarFrame(id) {
|
|
48
|
+
if (editorToolbarFrames.delete(id)) {
|
|
49
|
+
notifyEditorToolbarFrameListeners();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function preserveEditorToolbarFocusForNextBlur() {
|
|
53
|
+
editorToolbarFocusPreserveUntil = Date.now() + EDITOR_TOOLBAR_FOCUS_PRESERVE_MS;
|
|
54
|
+
}
|
|
55
|
+
function beginEditorToolbarInteraction() {
|
|
56
|
+
activeEditorToolbarInteractions += 1;
|
|
57
|
+
preserveEditorToolbarFocusForNextBlur();
|
|
58
|
+
}
|
|
59
|
+
function endEditorToolbarInteraction() {
|
|
60
|
+
activeEditorToolbarInteractions = Math.max(0, activeEditorToolbarInteractions - 1);
|
|
61
|
+
preserveEditorToolbarFocusForNextBlur();
|
|
62
|
+
}
|
|
63
|
+
function isEditorToolbarFocusPreservationActive() {
|
|
64
|
+
return activeEditorToolbarInteractions > 0 || Date.now() <= editorToolbarFocusPreserveUntil;
|
|
65
|
+
}
|
|
66
|
+
function useEditorToolbarFrames() {
|
|
67
|
+
const [frames, setFrames] = (0, react_1.useState)(getEditorToolbarFramesSnapshot);
|
|
68
|
+
(0, react_1.useEffect)(() => {
|
|
69
|
+
const listener = () => setFrames(getEditorToolbarFramesSnapshot());
|
|
70
|
+
editorToolbarFrameListeners.add(listener);
|
|
71
|
+
listener();
|
|
72
|
+
return () => {
|
|
73
|
+
editorToolbarFrameListeners.delete(listener);
|
|
74
|
+
};
|
|
75
|
+
}, []);
|
|
76
|
+
return frames;
|
|
77
|
+
}
|
|
78
|
+
function _setEditorToolbarFrameForTests(id, frame) {
|
|
79
|
+
registerEditorToolbarFrame(id, frame);
|
|
80
|
+
}
|
|
81
|
+
function _resetEditorToolbarFrameRegistryForTests() {
|
|
82
|
+
editorToolbarFrames.clear();
|
|
83
|
+
activeEditorToolbarInteractions = 0;
|
|
84
|
+
editorToolbarFocusPreserveUntil = 0;
|
|
85
|
+
notifyEditorToolbarFrameListeners();
|
|
86
|
+
}
|
|
87
|
+
function _beginEditorToolbarInteractionForTests() {
|
|
88
|
+
beginEditorToolbarInteraction();
|
|
89
|
+
}
|
|
90
|
+
function _endEditorToolbarInteractionForTests() {
|
|
91
|
+
endEditorToolbarInteraction();
|
|
92
|
+
}
|
|
9
93
|
function defaultIcon(id) {
|
|
10
94
|
return { type: 'default', id };
|
|
11
95
|
}
|
|
@@ -109,16 +193,22 @@ const DEFAULT_MATERIAL_ICONS = {
|
|
|
109
193
|
undo: 'undo',
|
|
110
194
|
redo: 'redo',
|
|
111
195
|
};
|
|
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, }) {
|
|
196
|
+
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, preserveEditorFocus = true, }) {
|
|
113
197
|
const marks = activeState.marks ?? {};
|
|
114
198
|
const nodes = activeState.nodes ?? {};
|
|
115
199
|
const commands = activeState.commands ?? {};
|
|
116
200
|
const allowedMarks = activeState.allowedMarks ?? [];
|
|
117
201
|
const insertableNodes = activeState.insertableNodes ?? [];
|
|
202
|
+
const rootRef = (0, react_1.useRef)(null);
|
|
118
203
|
const groupButtonRefs = (0, react_1.useRef)(new Map());
|
|
119
204
|
const { width: windowWidth, height: windowHeight } = (0, react_native_1.useWindowDimensions)();
|
|
120
205
|
const [expandedGroupKey, setExpandedGroupKey] = (0, react_1.useState)(null);
|
|
121
206
|
const [menuState, setMenuState] = (0, react_1.useState)(null);
|
|
207
|
+
const toolbarInteractionActiveRef = (0, react_1.useRef)(false);
|
|
208
|
+
const registrationIdRef = (0, react_1.useRef)(null);
|
|
209
|
+
if (registrationIdRef.current == null) {
|
|
210
|
+
registrationIdRef.current = nextEditorToolbarRegistrationId++;
|
|
211
|
+
}
|
|
122
212
|
const isMarkActive = (0, react_1.useCallback)((mark) => !!marks[mark], [marks]);
|
|
123
213
|
const isInList = !!nodes['bulletList'] || !!nodes['orderedList'];
|
|
124
214
|
const canIndentList = isInList && !!commands['indentList'];
|
|
@@ -372,6 +462,56 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
372
462
|
};
|
|
373
463
|
}, [expandedGroupKey, menuState?.groupKey, resolveButton, toolbarItems]);
|
|
374
464
|
const resolvedShowTopBorder = showTopBorder ?? theme?.showTopBorder ?? true;
|
|
465
|
+
const publishToolbarFrame = (0, react_1.useCallback)(() => {
|
|
466
|
+
const registrationId = registrationIdRef.current;
|
|
467
|
+
const toolbar = rootRef.current;
|
|
468
|
+
if (!preserveEditorFocus || registrationId == null || !toolbar) {
|
|
469
|
+
if (registrationId != null) {
|
|
470
|
+
unregisterEditorToolbarFrame(registrationId);
|
|
471
|
+
}
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (typeof toolbar.measureInWindow !== 'function') {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
toolbar.measureInWindow((x, y, width, height) => {
|
|
478
|
+
registerEditorToolbarFrame(registrationId, { x, y, width, height });
|
|
479
|
+
});
|
|
480
|
+
}, [preserveEditorFocus]);
|
|
481
|
+
const handleToolbarLayout = (0, react_1.useCallback)(() => {
|
|
482
|
+
requestAnimationFrame(publishToolbarFrame);
|
|
483
|
+
}, [publishToolbarFrame]);
|
|
484
|
+
(0, react_1.useEffect)(() => {
|
|
485
|
+
if (!preserveEditorFocus) {
|
|
486
|
+
const registrationId = registrationIdRef.current;
|
|
487
|
+
if (registrationId != null) {
|
|
488
|
+
unregisterEditorToolbarFrame(registrationId);
|
|
489
|
+
}
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const frame = requestAnimationFrame(publishToolbarFrame);
|
|
493
|
+
return () => cancelAnimationFrame(frame);
|
|
494
|
+
}, [
|
|
495
|
+
expandedGroupKey,
|
|
496
|
+
menuState?.groupKey,
|
|
497
|
+
preserveEditorFocus,
|
|
498
|
+
publishToolbarFrame,
|
|
499
|
+
renderedItems.length,
|
|
500
|
+
windowHeight,
|
|
501
|
+
windowWidth,
|
|
502
|
+
]);
|
|
503
|
+
(0, react_1.useEffect)(() => {
|
|
504
|
+
const registrationId = registrationIdRef.current;
|
|
505
|
+
return () => {
|
|
506
|
+
if (toolbarInteractionActiveRef.current) {
|
|
507
|
+
toolbarInteractionActiveRef.current = false;
|
|
508
|
+
endEditorToolbarInteraction();
|
|
509
|
+
}
|
|
510
|
+
if (registrationId != null) {
|
|
511
|
+
unregisterEditorToolbarFrame(registrationId);
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
}, []);
|
|
375
515
|
(0, react_1.useEffect)(() => {
|
|
376
516
|
if (expandedGroupKey != null && !groupsByKey.has(expandedGroupKey)) {
|
|
377
517
|
setExpandedGroupKey(null);
|
|
@@ -389,6 +529,18 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
389
529
|
}
|
|
390
530
|
setMenuState(null);
|
|
391
531
|
}, []);
|
|
532
|
+
const handleToolbarPressIn = (0, react_1.useCallback)(() => {
|
|
533
|
+
if (preserveEditorFocus && !toolbarInteractionActiveRef.current) {
|
|
534
|
+
toolbarInteractionActiveRef.current = true;
|
|
535
|
+
beginEditorToolbarInteraction();
|
|
536
|
+
}
|
|
537
|
+
}, [preserveEditorFocus]);
|
|
538
|
+
const handleToolbarPressOut = (0, react_1.useCallback)(() => {
|
|
539
|
+
if (preserveEditorFocus && toolbarInteractionActiveRef.current) {
|
|
540
|
+
toolbarInteractionActiveRef.current = false;
|
|
541
|
+
endEditorToolbarInteraction();
|
|
542
|
+
}
|
|
543
|
+
}, [preserveEditorFocus]);
|
|
392
544
|
const handleGroupPress = (0, react_1.useCallback)((group) => {
|
|
393
545
|
if (group.isDisabled) {
|
|
394
546
|
return;
|
|
@@ -442,7 +594,7 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
442
594
|
else {
|
|
443
595
|
groupButtonRefs.current.delete(anchorGroupKey);
|
|
444
596
|
}
|
|
445
|
-
}, collapsable: false, style: styles.buttonAnchor, children: [(0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { onPress: onPress, disabled: button.isDisabled, style: [
|
|
597
|
+
}, collapsable: false, style: styles.buttonAnchor, children: [(0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { onPressIn: handleToolbarPressIn, onPressOut: handleToolbarPressOut, onPress: onPress, disabled: button.isDisabled, style: [
|
|
446
598
|
styles.button,
|
|
447
599
|
{
|
|
448
600
|
borderRadius: theme?.buttonBorderRadius ?? BUTTON_RADIUS,
|
|
@@ -460,7 +612,7 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
460
612
|
styles.separator,
|
|
461
613
|
theme?.separatorColor != null ? { backgroundColor: theme.separatorColor } : null,
|
|
462
614
|
] }, key));
|
|
463
|
-
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
|
|
615
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { ref: rootRef, collapsable: false, onLayout: handleToolbarLayout, style: [
|
|
464
616
|
styles.container,
|
|
465
617
|
!resolvedShowTopBorder && styles.containerWithoutTopBorder,
|
|
466
618
|
theme?.backgroundColor != null ? { backgroundColor: theme.backgroundColor } : null,
|
|
@@ -512,7 +664,7 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
|
|
|
512
664
|
: button.isDisabled
|
|
513
665
|
? disabledColor
|
|
514
666
|
: defaultColor;
|
|
515
|
-
return ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { onPress: () => handleButtonPress(button), disabled: button.isDisabled, style: ({ pressed }) => [
|
|
667
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { onPressIn: handleToolbarPressIn, onPressOut: handleToolbarPressOut, onPress: () => handleButtonPress(button), disabled: button.isDisabled, style: ({ pressed }) => [
|
|
516
668
|
styles.menuItem,
|
|
517
669
|
button.isActive && {
|
|
518
670
|
backgroundColor: theme?.buttonActiveBackgroundColor ?? ACTIVE_BG,
|
|
@@ -135,6 +135,8 @@ export interface NativeRichTextEditorRef {
|
|
|
135
135
|
getContentJson(): DocumentJSON;
|
|
136
136
|
/** Get the plain text content (no markup). */
|
|
137
137
|
getTextContent(): string;
|
|
138
|
+
/** Get the current caret rectangle in editor-local layout coordinates. */
|
|
139
|
+
getCaretRect(): Promise<NativeRichTextEditorCaretRect | null>;
|
|
138
140
|
/** Undo the last operation. */
|
|
139
141
|
undo(): void;
|
|
140
142
|
/** Redo the last undone operation. */
|
|
@@ -144,4 +146,18 @@ export interface NativeRichTextEditorRef {
|
|
|
144
146
|
/** Check if redo is available. */
|
|
145
147
|
canRedo(): boolean;
|
|
146
148
|
}
|
|
149
|
+
export interface NativeRichTextEditorCaretRect {
|
|
150
|
+
/** Left edge of the caret, relative to the editor root view. */
|
|
151
|
+
x: number;
|
|
152
|
+
/** Top edge of the caret, relative to the editor root view. */
|
|
153
|
+
y: number;
|
|
154
|
+
/** Caret width. */
|
|
155
|
+
width: number;
|
|
156
|
+
/** Caret height. */
|
|
157
|
+
height: number;
|
|
158
|
+
/** Current editor root view width. */
|
|
159
|
+
editorWidth: number;
|
|
160
|
+
/** Current editor root view height. */
|
|
161
|
+
editorHeight: number;
|
|
162
|
+
}
|
|
147
163
|
export declare const NativeRichTextEditor: React.ForwardRefExoticComponent<NativeRichTextEditorProps & React.RefAttributes<NativeRichTextEditorRef>>;
|
|
@@ -423,6 +423,44 @@ function serializeRemoteSelections(remoteSelections) {
|
|
|
423
423
|
}
|
|
424
424
|
return stringifyCachedJson(remoteSelections);
|
|
425
425
|
}
|
|
426
|
+
function areToolbarFramesEqual(left, right) {
|
|
427
|
+
return (left?.x === right?.x &&
|
|
428
|
+
left?.y === right?.y &&
|
|
429
|
+
left?.width === right?.width &&
|
|
430
|
+
left?.height === right?.height);
|
|
431
|
+
}
|
|
432
|
+
function serializeToolbarFrames(frames) {
|
|
433
|
+
if (!frames || frames.length === 0) {
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
return JSON.stringify(frames.length === 1 ? frames[0] : { frames });
|
|
437
|
+
}
|
|
438
|
+
function parseCaretRectJson(raw) {
|
|
439
|
+
if (!raw) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
const parsed = JSON.parse(raw);
|
|
444
|
+
const x = typeof parsed.x === 'number' ? parsed.x : null;
|
|
445
|
+
const y = typeof parsed.y === 'number' ? parsed.y : null;
|
|
446
|
+
const width = typeof parsed.width === 'number' ? parsed.width : null;
|
|
447
|
+
const height = typeof parsed.height === 'number' ? parsed.height : null;
|
|
448
|
+
const editorWidth = typeof parsed.editorWidth === 'number' ? parsed.editorWidth : null;
|
|
449
|
+
const editorHeight = typeof parsed.editorHeight === 'number' ? parsed.editorHeight : null;
|
|
450
|
+
if (x == null ||
|
|
451
|
+
y == null ||
|
|
452
|
+
width == null ||
|
|
453
|
+
height == null ||
|
|
454
|
+
editorWidth == null ||
|
|
455
|
+
editorHeight == null) {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
return { x, y, width, height, editorWidth, editorHeight };
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
426
464
|
const serializedJsonCache = new WeakMap();
|
|
427
465
|
function stringifyCachedJson(value) {
|
|
428
466
|
if (value != null && typeof value === 'object') {
|
|
@@ -463,7 +501,9 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
463
501
|
const [isReady, setIsReady] = (0, react_1.useState)(false);
|
|
464
502
|
const [editorInstanceId, setEditorInstanceId] = (0, react_1.useState)(0);
|
|
465
503
|
const [isFocused, setIsFocused] = (0, react_1.useState)(false);
|
|
466
|
-
const
|
|
504
|
+
const isFocusedRef = (0, react_1.useRef)(false);
|
|
505
|
+
const [inlineToolbarFrame, setInlineToolbarFrame] = (0, react_1.useState)(null);
|
|
506
|
+
const registeredToolbarFrames = (0, EditorToolbar_1.useEditorToolbarFrames)();
|
|
467
507
|
const [pendingNativeUpdate, setPendingNativeUpdate] = (0, react_1.useState)({
|
|
468
508
|
json: undefined,
|
|
469
509
|
revision: 0,
|
|
@@ -745,21 +785,21 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
745
785
|
const updateToolbarFrame = (0, react_1.useCallback)(() => {
|
|
746
786
|
const toolbar = toolbarRef.current;
|
|
747
787
|
if (!toolbar) {
|
|
748
|
-
|
|
788
|
+
setInlineToolbarFrame(null);
|
|
749
789
|
return;
|
|
750
790
|
}
|
|
751
791
|
toolbar.measureInWindow((x, y, width, height) => {
|
|
752
792
|
if (width <= 0 || height <= 0) {
|
|
753
|
-
|
|
793
|
+
setInlineToolbarFrame(null);
|
|
754
794
|
return;
|
|
755
795
|
}
|
|
756
|
-
const
|
|
757
|
-
|
|
796
|
+
const nextFrame = { x, y, width, height };
|
|
797
|
+
setInlineToolbarFrame((prev) => areToolbarFramesEqual(prev, nextFrame) ? prev : nextFrame);
|
|
758
798
|
});
|
|
759
799
|
}, []);
|
|
760
800
|
(0, react_1.useEffect)(() => {
|
|
761
801
|
if (!(showToolbar && toolbarPlacement === 'inline' && isFocused && editable)) {
|
|
762
|
-
|
|
802
|
+
setInlineToolbarFrame(null);
|
|
763
803
|
return;
|
|
764
804
|
}
|
|
765
805
|
const frame = requestAnimationFrame(() => {
|
|
@@ -836,19 +876,40 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
836
876
|
}
|
|
837
877
|
onSelectionChangeRef.current?.(nextSelection);
|
|
838
878
|
}, [syncSelectionStateFromUpdate]);
|
|
879
|
+
const refocusAfterToolbarInteraction = (0, react_1.useCallback)(() => {
|
|
880
|
+
nativeViewRef.current?.focus?.();
|
|
881
|
+
requestAnimationFrame(() => {
|
|
882
|
+
nativeViewRef.current?.focus?.();
|
|
883
|
+
});
|
|
884
|
+
setTimeout(() => {
|
|
885
|
+
nativeViewRef.current?.focus?.();
|
|
886
|
+
}, 50);
|
|
887
|
+
}, []);
|
|
839
888
|
const handleFocusChange = (0, react_1.useCallback)((event) => {
|
|
840
889
|
const { isFocused: focused } = event.nativeEvent;
|
|
890
|
+
if (!focused &&
|
|
891
|
+
editable &&
|
|
892
|
+
isFocusedRef.current &&
|
|
893
|
+
(0, EditorToolbar_1.isEditorToolbarFocusPreservationActive)()) {
|
|
894
|
+
setIsFocused(true);
|
|
895
|
+
refocusAfterToolbarInteraction();
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
const wasFocused = isFocusedRef.current;
|
|
899
|
+
isFocusedRef.current = focused;
|
|
841
900
|
setIsFocused(focused);
|
|
842
901
|
if (!focused) {
|
|
843
902
|
setMentionQueryEvent(null);
|
|
844
903
|
}
|
|
845
904
|
if (focused) {
|
|
846
|
-
|
|
905
|
+
if (!wasFocused) {
|
|
906
|
+
onFocusRef.current?.();
|
|
907
|
+
}
|
|
847
908
|
}
|
|
848
|
-
else {
|
|
909
|
+
else if (wasFocused) {
|
|
849
910
|
onBlurRef.current?.();
|
|
850
911
|
}
|
|
851
|
-
}, []);
|
|
912
|
+
}, [editable, refocusAfterToolbarInteraction]);
|
|
852
913
|
(0, react_1.useEffect)(() => {
|
|
853
914
|
if (addons?.mentions != null) {
|
|
854
915
|
return;
|
|
@@ -1130,6 +1191,13 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
1130
1191
|
return '';
|
|
1131
1192
|
return bridgeRef.current.getHtml().replace(/<[^>]+>/g, '');
|
|
1132
1193
|
},
|
|
1194
|
+
async getCaretRect() {
|
|
1195
|
+
const nativeView = nativeViewRef.current;
|
|
1196
|
+
if (!nativeView?.getCaretRect)
|
|
1197
|
+
return null;
|
|
1198
|
+
const raw = await Promise.resolve(nativeView.getCaretRect());
|
|
1199
|
+
return parseCaretRectJson(raw);
|
|
1200
|
+
},
|
|
1133
1201
|
undo() {
|
|
1134
1202
|
runAndApply(() => bridgeRef.current?.undo() ?? null);
|
|
1135
1203
|
},
|
|
@@ -1219,6 +1287,14 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
1219
1287
|
nativeViewStyleParts.push({ height: autoGrowHeight });
|
|
1220
1288
|
}
|
|
1221
1289
|
const nativeViewStyle = nativeViewStyleParts.length <= 1 ? nativeViewStyleParts[0] : nativeViewStyleParts;
|
|
1290
|
+
const toolbarFrameJson = serializeToolbarFrames(isFocused && editable
|
|
1291
|
+
? [
|
|
1292
|
+
...(toolbarPlacement === 'inline' && inlineToolbarFrame != null
|
|
1293
|
+
? [inlineToolbarFrame]
|
|
1294
|
+
: []),
|
|
1295
|
+
...registeredToolbarFrames,
|
|
1296
|
+
]
|
|
1297
|
+
: undefined);
|
|
1222
1298
|
const jsToolbar = ((0, jsx_runtime_1.jsx)(react_native_1.View, { ref: toolbarRef, testID: 'native-editor-js-toolbar', style: [
|
|
1223
1299
|
styles.inlineToolbar,
|
|
1224
1300
|
{ marginTop: inlineToolbarMarginTop },
|
|
@@ -1270,7 +1346,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
1270
1346
|
'#8E8E93',
|
|
1271
1347
|
},
|
|
1272
1348
|
], children: suggestion.subtitle })) : null] })) }, suggestion.key));
|
|
1273
|
-
}) }) })) : ((0, jsx_runtime_1.jsx)(EditorToolbar_1.EditorToolbar, { activeState: activeState, historyState: historyState, toolbarItems: toolbarItems, theme: theme?.toolbar, showTopBorder: inlineToolbarShowTopBorder, onToggleMark: (mark) => runAndApply(() => bridgeRef.current?.toggleMark(mark) ?? null, {
|
|
1349
|
+
}) }) })) : ((0, jsx_runtime_1.jsx)(EditorToolbar_1.EditorToolbar, { activeState: activeState, historyState: historyState, toolbarItems: toolbarItems, theme: theme?.toolbar, showTopBorder: inlineToolbarShowTopBorder, preserveEditorFocus: false, onToggleMark: (mark) => runAndApply(() => bridgeRef.current?.toggleMark(mark) ?? null, {
|
|
1274
1350
|
skipNativeApplyIfContentUnchanged: true,
|
|
1275
1351
|
}), onToggleListType: (listType) => runAndApply(() => bridgeRef.current?.toggleList(listType) ?? null), onToggleHeading: (level) => runAndApply(() => bridgeRef.current?.toggleHeading(level) ?? null), onToggleBlockquote: () => runAndApply(() => bridgeRef.current?.toggleBlockquote() ?? null), onInsertNodeType: (nodeType) => runAndApply(() => bridgeRef.current?.insertNode(nodeType) ?? null), onRunCommand: (command) => {
|
|
1276
1352
|
switch (command) {
|
|
@@ -1296,7 +1372,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
1296
1372
|
}), onToggleStrike: () => runAndApply(() => bridgeRef.current?.toggleMark('strike') ?? null, {
|
|
1297
1373
|
skipNativeApplyIfContentUnchanged: true,
|
|
1298
1374
|
}), onToggleBulletList: () => runAndApply(() => bridgeRef.current?.toggleList('bulletList') ?? null), onToggleOrderedList: () => runAndApply(() => bridgeRef.current?.toggleList('orderedList') ?? null), onIndentList: () => runAndApply(() => bridgeRef.current?.indentListItem() ?? null), onOutdentList: () => runAndApply(() => bridgeRef.current?.outdentListItem() ?? null), onInsertHorizontalRule: () => runAndApply(() => bridgeRef.current?.insertNode('horizontalRule') ?? null), onInsertLineBreak: () => runAndApply(() => bridgeRef.current?.insertNode('hardBreak') ?? null), onUndo: () => runAndApply(() => bridgeRef.current?.undo() ?? null), onRedo: () => runAndApply(() => bridgeRef.current?.redo() ?? null) })) }));
|
|
1299
|
-
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.container, containerStyle], children: [(0, jsx_runtime_1.jsx)(NativeEditorView, { ref: nativeViewRef, style: nativeViewStyle, editorId: editorInstanceId, placeholder: placeholder, editable: editable, autoFocus: autoFocus, showToolbar: showToolbar, toolbarPlacement: toolbarPlacement, heightBehavior: heightBehavior, allowImageResizing: allowImageResizing, themeJson: themeJson, addonsJson: addonsJson, toolbarItemsJson: toolbarItemsJson, remoteSelectionsJson: remoteSelectionsJson, toolbarFrameJson:
|
|
1375
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.container, containerStyle], children: [(0, jsx_runtime_1.jsx)(NativeEditorView, { ref: nativeViewRef, style: nativeViewStyle, editorId: editorInstanceId, placeholder: placeholder, editable: editable, autoFocus: autoFocus, showToolbar: showToolbar, toolbarPlacement: toolbarPlacement, heightBehavior: heightBehavior, allowImageResizing: allowImageResizing, themeJson: themeJson, addonsJson: addonsJson, toolbarItemsJson: toolbarItemsJson, remoteSelectionsJson: remoteSelectionsJson, toolbarFrameJson: toolbarFrameJson, editorUpdateJson: pendingNativeUpdate.json, editorUpdateRevision: pendingNativeUpdate.revision, onEditorUpdate: handleUpdate, onSelectionChange: handleSelectionChange, onFocusChange: handleFocusChange, onContentHeightChange: handleContentHeightChange, onToolbarAction: handleToolbarAction, onAddonEvent: handleAddonEvent }, DEV_NATIVE_VIEW_KEY), shouldRenderJsToolbar && jsToolbar] }));
|
|
1300
1376
|
});
|
|
1301
1377
|
const styles = react_native_1.StyleSheet.create({
|
|
1302
1378
|
container: {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { NativeRichTextEditor, type NativeRichTextEditorProps, type NativeRichTextEditorRef, type NativeRichTextEditorHeightBehavior, type NativeRichTextEditorToolbarPlacement, type RemoteSelectionDecoration, type LinkRequestContext, type ImageRequestContext, } from './NativeRichTextEditor';
|
|
1
|
+
export { NativeRichTextEditor, type NativeRichTextEditorProps, type NativeRichTextEditorRef, type NativeRichTextEditorCaretRect, type NativeRichTextEditorHeightBehavior, type NativeRichTextEditorToolbarPlacement, type RemoteSelectionDecoration, type LinkRequestContext, type ImageRequestContext, } from './NativeRichTextEditor';
|
|
2
2
|
export { NativeProseViewer, type NativeProseViewerProps, type NativeProseViewerAddons, type NativeProseViewerMentionsAddonConfig, type NativeProseViewerMentionPrefix, type NativeProseViewerLinkPressEvent, type NativeProseViewerMentionRenderContext, type NativeProseViewerMentionPressEvent, } from './NativeProseViewer';
|
|
3
3
|
export { EditorToolbar, DEFAULT_EDITOR_TOOLBAR_ITEMS, type EditorToolbarProps, type EditorToolbarItem, type EditorToolbarLeafItem, type EditorToolbarGroupChildItem, type EditorToolbarGroupItem, type EditorToolbarGroupPresentation, type EditorToolbarIcon, type EditorToolbarDefaultIconId, type EditorToolbarSFSymbolIcon, type EditorToolbarMaterialIcon, type EditorToolbarCommand, type EditorToolbarHeadingLevel, type EditorToolbarListType, } from './EditorToolbar';
|
|
4
4
|
export type { EditorContentInsets, EditorTheme, EditorTextStyle, EditorLinkTheme, EditorHeadingTheme, EditorListTheme, EditorHorizontalRuleTheme, EditorMentionTheme, EditorToolbarTheme, EditorToolbarAppearance, EditorFontStyle, EditorFontWeight, } from './EditorTheme';
|
|
Binary file
|
|
Binary file
|
|
@@ -1626,7 +1626,8 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1626
1626
|
inputViewStyle: .keyboard
|
|
1627
1627
|
)
|
|
1628
1628
|
private let accessoryPlaceholder = EditorAccessoryPlaceholderView(frame: .zero)
|
|
1629
|
-
private var
|
|
1629
|
+
private var toolbarFramesInWindow: [CGRect] = []
|
|
1630
|
+
private var lastToolbarTouchUptime: TimeInterval = -Double.infinity
|
|
1630
1631
|
private var didApplyAutoFocus = false
|
|
1631
1632
|
private var toolbarState = NativeToolbarState.empty
|
|
1632
1633
|
private var toolbarItems: [NativeToolbarItem] = NativeToolbarItem.defaults
|
|
@@ -1855,17 +1856,32 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1855
1856
|
lastToolbarFrameJSON = toolbarFrameJson
|
|
1856
1857
|
guard let toolbarFrameJson,
|
|
1857
1858
|
let data = toolbarFrameJson.data(using: .utf8),
|
|
1858
|
-
let raw = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
1859
|
-
|
|
1859
|
+
let raw = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
1860
|
+
else {
|
|
1861
|
+
toolbarFramesInWindow = []
|
|
1862
|
+
return
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
if let frameDictionaries = raw["frames"] as? [[String: Any]] {
|
|
1866
|
+
toolbarFramesInWindow = frameDictionaries.compactMap(Self.toolbarFrame(from:))
|
|
1867
|
+
return
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
toolbarFramesInWindow = Self.toolbarFrame(from: raw).map { [$0] } ?? []
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
private static func toolbarFrame(from raw: [String: Any]) -> CGRect? {
|
|
1874
|
+
guard let x = (raw["x"] as? NSNumber)?.doubleValue,
|
|
1860
1875
|
let y = (raw["y"] as? NSNumber)?.doubleValue,
|
|
1861
1876
|
let width = (raw["width"] as? NSNumber)?.doubleValue,
|
|
1862
|
-
let height = (raw["height"] as? NSNumber)?.doubleValue
|
|
1877
|
+
let height = (raw["height"] as? NSNumber)?.doubleValue,
|
|
1878
|
+
width > 0,
|
|
1879
|
+
height > 0
|
|
1863
1880
|
else {
|
|
1864
|
-
|
|
1865
|
-
return
|
|
1881
|
+
return nil
|
|
1866
1882
|
}
|
|
1867
1883
|
|
|
1868
|
-
|
|
1884
|
+
return CGRect(x: x, y: y, width: width, height: height)
|
|
1869
1885
|
}
|
|
1870
1886
|
|
|
1871
1887
|
func setPendingEditorUpdateJson(_ editorUpdateJson: String?) {
|
|
@@ -1904,6 +1920,30 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1904
1920
|
richTextView.textView.resignFirstResponder()
|
|
1905
1921
|
}
|
|
1906
1922
|
|
|
1923
|
+
func getCaretRectJson() -> String? {
|
|
1924
|
+
layoutIfNeeded()
|
|
1925
|
+
richTextView.layoutIfNeeded()
|
|
1926
|
+
|
|
1927
|
+
guard let caretRect = richTextView.currentCaretRect() else {
|
|
1928
|
+
return nil
|
|
1929
|
+
}
|
|
1930
|
+
let editorRect = richTextView.convert(caretRect, to: self)
|
|
1931
|
+
let payload: [String: Any] = [
|
|
1932
|
+
"x": editorRect.minX,
|
|
1933
|
+
"y": editorRect.minY,
|
|
1934
|
+
"width": editorRect.width,
|
|
1935
|
+
"height": editorRect.height,
|
|
1936
|
+
"editorWidth": bounds.width,
|
|
1937
|
+
"editorHeight": bounds.height,
|
|
1938
|
+
]
|
|
1939
|
+
guard let data = try? JSONSerialization.data(withJSONObject: payload),
|
|
1940
|
+
let json = String(data: data, encoding: .utf8)
|
|
1941
|
+
else {
|
|
1942
|
+
return nil
|
|
1943
|
+
}
|
|
1944
|
+
return json
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1907
1947
|
// MARK: - Focus Notifications
|
|
1908
1948
|
|
|
1909
1949
|
@objc private func textViewDidBeginEditing(_ notification: Notification) {
|
|
@@ -1914,6 +1954,13 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1914
1954
|
}
|
|
1915
1955
|
|
|
1916
1956
|
@objc private func textViewDidEndEditing(_ notification: Notification) {
|
|
1957
|
+
if shouldPreserveFocusAfterToolbarTouch() {
|
|
1958
|
+
DispatchQueue.main.async { [weak self] in
|
|
1959
|
+
self?.richTextView.textView.becomeFirstResponder()
|
|
1960
|
+
}
|
|
1961
|
+
return
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1917
1964
|
uninstallOutsideTapRecognizer()
|
|
1918
1965
|
richTextView.textView.refreshSelectionVisualState()
|
|
1919
1966
|
clearMentionQueryStateAndHidePopover()
|
|
@@ -1952,6 +1999,9 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1952
1999
|
guard gestureRecognizer === outsideTapGestureRecognizer else { return true }
|
|
1953
2000
|
guard let tapWindow = gestureWindow ?? window else { return true }
|
|
1954
2001
|
let locationInWindow = touch.location(in: tapWindow)
|
|
2002
|
+
if isLocationInStandaloneToolbarFrame(locationInWindow) {
|
|
2003
|
+
markRecentToolbarTouch()
|
|
2004
|
+
}
|
|
1955
2005
|
let result = shouldHandleOutsideTap(
|
|
1956
2006
|
locationInWindow: locationInWindow,
|
|
1957
2007
|
touchedView: touch.view
|
|
@@ -1959,6 +2009,18 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1959
2009
|
return result
|
|
1960
2010
|
}
|
|
1961
2011
|
|
|
2012
|
+
private func markRecentToolbarTouch() {
|
|
2013
|
+
lastToolbarTouchUptime = ProcessInfo.processInfo.systemUptime
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
private func shouldPreserveFocusAfterToolbarTouch() -> Bool {
|
|
2017
|
+
ProcessInfo.processInfo.systemUptime - lastToolbarTouchUptime <= 0.75
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
private func isLocationInStandaloneToolbarFrame(_ locationInWindow: CGPoint) -> Bool {
|
|
2021
|
+
toolbarFramesInWindow.contains(where: { $0.contains(locationInWindow) })
|
|
2022
|
+
}
|
|
2023
|
+
|
|
1962
2024
|
private func shouldHandleOutsideTap(
|
|
1963
2025
|
locationInWindow: CGPoint,
|
|
1964
2026
|
touchedView: UIView?
|
|
@@ -1975,7 +2037,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1975
2037
|
if let touchedView, touchedView.isDescendant(of: accessoryToolbar) {
|
|
1976
2038
|
return false
|
|
1977
2039
|
}
|
|
1978
|
-
if
|
|
2040
|
+
if isLocationInStandaloneToolbarFrame(locationInWindow) {
|
|
1979
2041
|
return false
|
|
1980
2042
|
}
|
|
1981
2043
|
return true
|
|
@@ -386,6 +386,9 @@ public class NativeEditorModule: Module {
|
|
|
386
386
|
AsyncFunction("blur") { (view: NativeEditorExpoView) in
|
|
387
387
|
view.blur()
|
|
388
388
|
}
|
|
389
|
+
AsyncFunction("getCaretRect") { (view: NativeEditorExpoView) -> String? in
|
|
390
|
+
view.getCaretRectJson()
|
|
391
|
+
}
|
|
389
392
|
}
|
|
390
393
|
|
|
391
394
|
View(NativeProseViewerExpoView.self) {
|