@apollohg/react-native-prose-editor 0.1.0
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/LICENSE +160 -0
- package/README.md +143 -0
- package/android/build.gradle +39 -0
- package/android/src/main/assets/editor-icons/MaterialIcons.json +2236 -0
- package/android/src/main/assets/editor-icons/MaterialIcons.ttf +0 -0
- package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +131 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +1057 -0
- package/android/src/main/java/com/apollohg/editor/EditorHeightBehavior.kt +14 -0
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +191 -0
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +325 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +647 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +257 -0
- package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +714 -0
- package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +76 -0
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +1044 -0
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +211 -0
- package/expo-module.config.json +9 -0
- package/ios/EditorAddons.swift +228 -0
- package/ios/EditorCore.xcframework/Info.plist +44 -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 +254 -0
- package/ios/EditorTheme.swift +372 -0
- package/ios/Generated_editor_core.swift +1143 -0
- package/ios/NativeEditorExpoView.swift +1417 -0
- package/ios/NativeEditorModule.swift +263 -0
- package/ios/PositionBridge.swift +278 -0
- package/ios/ReactNativeProseEditor.podspec +49 -0
- package/ios/RenderBridge.swift +825 -0
- package/ios/RichTextEditorView.swift +1559 -0
- package/ios/editor_coreFFI/editor_coreFFI.h +1014 -0
- package/ios/editor_coreFFI/module.modulemap +7 -0
- package/ios/editor_coreFFI.h +904 -0
- package/ios/editor_coreFFI.modulemap +7 -0
- package/package.json +66 -0
- 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 +2014 -0
- package/src/EditorTheme.ts +130 -0
- package/src/EditorToolbar.tsx +620 -0
- package/src/NativeEditorBridge.ts +607 -0
- package/src/NativeRichTextEditor.tsx +951 -0
- package/src/addons.ts +158 -0
- package/src/index.ts +63 -0
- package/src/schemas.ts +153 -0
- package/src/useNativeEditor.ts +173 -0
package/src/addons.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { EditorMentionTheme } from './EditorTheme';
|
|
2
|
+
import type { DocumentJSON } from './NativeEditorBridge';
|
|
3
|
+
import type { SchemaDefinition, NodeSpec } from './schemas';
|
|
4
|
+
|
|
5
|
+
export interface MentionSuggestion {
|
|
6
|
+
key: string;
|
|
7
|
+
title: string;
|
|
8
|
+
subtitle?: string;
|
|
9
|
+
label?: string;
|
|
10
|
+
attrs?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface MentionQueryChangeEvent {
|
|
14
|
+
query: string;
|
|
15
|
+
trigger: string;
|
|
16
|
+
range: {
|
|
17
|
+
anchor: number;
|
|
18
|
+
head: number;
|
|
19
|
+
};
|
|
20
|
+
isActive: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface MentionSelectEvent {
|
|
24
|
+
trigger: string;
|
|
25
|
+
suggestion: MentionSuggestion;
|
|
26
|
+
attrs: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MentionsAddonConfig {
|
|
30
|
+
trigger?: string;
|
|
31
|
+
suggestions?: readonly MentionSuggestion[];
|
|
32
|
+
theme?: EditorMentionTheme;
|
|
33
|
+
onQueryChange?: (event: MentionQueryChangeEvent) => void;
|
|
34
|
+
onSelect?: (event: MentionSelectEvent) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface EditorAddons {
|
|
38
|
+
mentions?: MentionsAddonConfig;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SerializedMentionSuggestion {
|
|
42
|
+
key: string;
|
|
43
|
+
title: string;
|
|
44
|
+
subtitle?: string;
|
|
45
|
+
label: string;
|
|
46
|
+
attrs: Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SerializedMentionsAddonConfig {
|
|
50
|
+
trigger: string;
|
|
51
|
+
theme?: EditorMentionTheme;
|
|
52
|
+
suggestions: SerializedMentionSuggestion[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SerializedEditorAddons {
|
|
56
|
+
mentions?: SerializedMentionsAddonConfig;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type EditorAddonEvent =
|
|
60
|
+
| {
|
|
61
|
+
type: 'mentionsQueryChange';
|
|
62
|
+
query: string;
|
|
63
|
+
trigger: string;
|
|
64
|
+
range: {
|
|
65
|
+
anchor: number;
|
|
66
|
+
head: number;
|
|
67
|
+
};
|
|
68
|
+
isActive: boolean;
|
|
69
|
+
}
|
|
70
|
+
| {
|
|
71
|
+
type: 'mentionsSelect';
|
|
72
|
+
trigger: string;
|
|
73
|
+
suggestionKey: string;
|
|
74
|
+
attrs: Record<string, unknown>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const MENTION_NODE_NAME = 'mention';
|
|
78
|
+
const DEFAULT_MENTION_TRIGGER = '@';
|
|
79
|
+
|
|
80
|
+
export function mentionNodeSpec(): NodeSpec {
|
|
81
|
+
return {
|
|
82
|
+
name: MENTION_NODE_NAME,
|
|
83
|
+
content: '',
|
|
84
|
+
group: 'inline',
|
|
85
|
+
role: 'inline',
|
|
86
|
+
isVoid: true,
|
|
87
|
+
attrs: {
|
|
88
|
+
label: { default: null },
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function withMentionsSchema(schema: SchemaDefinition): SchemaDefinition {
|
|
94
|
+
const hasMentionNode = schema.nodes.some((node) => node.name === MENTION_NODE_NAME);
|
|
95
|
+
if (hasMentionNode) {
|
|
96
|
+
return schema;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
...schema,
|
|
101
|
+
nodes: [...schema.nodes, mentionNodeSpec()],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function normalizeEditorAddons(
|
|
106
|
+
addons?: EditorAddons
|
|
107
|
+
): SerializedEditorAddons | undefined {
|
|
108
|
+
if (!addons?.mentions) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const trigger =
|
|
113
|
+
addons.mentions.trigger?.trim() || DEFAULT_MENTION_TRIGGER;
|
|
114
|
+
const suggestions = (addons.mentions.suggestions ?? []).map((suggestion) => {
|
|
115
|
+
const label = suggestion.label?.trim() || `${trigger}${suggestion.title}`;
|
|
116
|
+
const attrs = {
|
|
117
|
+
label,
|
|
118
|
+
...(suggestion.attrs ?? {}),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
key: suggestion.key,
|
|
123
|
+
title: suggestion.title,
|
|
124
|
+
subtitle: suggestion.subtitle,
|
|
125
|
+
label,
|
|
126
|
+
attrs,
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
mentions: {
|
|
132
|
+
trigger,
|
|
133
|
+
theme: addons.mentions.theme,
|
|
134
|
+
suggestions,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function serializeEditorAddons(addons?: EditorAddons): string | undefined {
|
|
140
|
+
const normalized = normalizeEditorAddons(addons);
|
|
141
|
+
if (!normalized?.mentions) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return JSON.stringify(normalized);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function buildMentionFragmentJson(attrs: Record<string, unknown>): DocumentJSON {
|
|
149
|
+
return {
|
|
150
|
+
type: 'doc',
|
|
151
|
+
content: [
|
|
152
|
+
{
|
|
153
|
+
type: MENTION_NODE_NAME,
|
|
154
|
+
attrs,
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
};
|
|
158
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export {
|
|
2
|
+
NativeRichTextEditor,
|
|
3
|
+
type NativeRichTextEditorProps,
|
|
4
|
+
type NativeRichTextEditorRef,
|
|
5
|
+
type NativeRichTextEditorHeightBehavior,
|
|
6
|
+
type NativeRichTextEditorToolbarPlacement,
|
|
7
|
+
} from './NativeRichTextEditor';
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
EditorToolbar,
|
|
11
|
+
DEFAULT_EDITOR_TOOLBAR_ITEMS,
|
|
12
|
+
type EditorToolbarProps,
|
|
13
|
+
type EditorToolbarItem,
|
|
14
|
+
type EditorToolbarIcon,
|
|
15
|
+
type EditorToolbarDefaultIconId,
|
|
16
|
+
type EditorToolbarSFSymbolIcon,
|
|
17
|
+
type EditorToolbarMaterialIcon,
|
|
18
|
+
type EditorToolbarCommand,
|
|
19
|
+
type EditorToolbarListType,
|
|
20
|
+
} from './EditorToolbar';
|
|
21
|
+
export type {
|
|
22
|
+
EditorContentInsets,
|
|
23
|
+
EditorTheme,
|
|
24
|
+
EditorTextStyle,
|
|
25
|
+
EditorHeadingTheme,
|
|
26
|
+
EditorListTheme,
|
|
27
|
+
EditorHorizontalRuleTheme,
|
|
28
|
+
EditorMentionTheme,
|
|
29
|
+
EditorToolbarTheme,
|
|
30
|
+
EditorFontStyle,
|
|
31
|
+
EditorFontWeight,
|
|
32
|
+
} from './EditorTheme';
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
MENTION_NODE_NAME,
|
|
36
|
+
mentionNodeSpec,
|
|
37
|
+
withMentionsSchema,
|
|
38
|
+
buildMentionFragmentJson,
|
|
39
|
+
type EditorAddons,
|
|
40
|
+
type MentionsAddonConfig,
|
|
41
|
+
type MentionSuggestion,
|
|
42
|
+
type MentionQueryChangeEvent,
|
|
43
|
+
type MentionSelectEvent,
|
|
44
|
+
type EditorAddonEvent,
|
|
45
|
+
} from './addons';
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
tiptapSchema,
|
|
49
|
+
prosemirrorSchema,
|
|
50
|
+
type SchemaDefinition,
|
|
51
|
+
type NodeSpec,
|
|
52
|
+
type MarkSpec,
|
|
53
|
+
type AttrSpec,
|
|
54
|
+
} from './schemas';
|
|
55
|
+
|
|
56
|
+
// Read-only types (no mutation methods)
|
|
57
|
+
export type {
|
|
58
|
+
Selection,
|
|
59
|
+
ActiveState,
|
|
60
|
+
HistoryState,
|
|
61
|
+
EditorUpdate,
|
|
62
|
+
DocumentJSON,
|
|
63
|
+
} from './NativeEditorBridge';
|
package/src/schemas.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
export interface AttrSpec {
|
|
2
|
+
default?: unknown;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface NodeSpec {
|
|
6
|
+
name: string;
|
|
7
|
+
content: string;
|
|
8
|
+
group?: string;
|
|
9
|
+
attrs?: Record<string, AttrSpec>;
|
|
10
|
+
role: string;
|
|
11
|
+
htmlTag?: string;
|
|
12
|
+
isVoid?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface MarkSpec {
|
|
16
|
+
name: string;
|
|
17
|
+
attrs?: Record<string, AttrSpec>;
|
|
18
|
+
excludes?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SchemaDefinition {
|
|
22
|
+
nodes: NodeSpec[];
|
|
23
|
+
marks: MarkSpec[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const MARKS: MarkSpec[] = [
|
|
27
|
+
{ name: 'bold' },
|
|
28
|
+
{ name: 'italic' },
|
|
29
|
+
{ name: 'underline' },
|
|
30
|
+
{ name: 'strike' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export const tiptapSchema: SchemaDefinition = {
|
|
34
|
+
nodes: [
|
|
35
|
+
{
|
|
36
|
+
name: 'doc',
|
|
37
|
+
content: 'block+',
|
|
38
|
+
role: 'doc',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'paragraph',
|
|
42
|
+
content: 'inline*',
|
|
43
|
+
group: 'block',
|
|
44
|
+
role: 'textBlock',
|
|
45
|
+
htmlTag: 'p',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'bulletList',
|
|
49
|
+
content: 'listItem+',
|
|
50
|
+
group: 'block',
|
|
51
|
+
role: 'list',
|
|
52
|
+
htmlTag: 'ul',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'orderedList',
|
|
56
|
+
content: 'listItem+',
|
|
57
|
+
group: 'block',
|
|
58
|
+
attrs: { start: { default: 1 } },
|
|
59
|
+
role: 'list',
|
|
60
|
+
htmlTag: 'ol',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'listItem',
|
|
64
|
+
content: 'paragraph block*',
|
|
65
|
+
role: 'listItem',
|
|
66
|
+
htmlTag: 'li',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'hardBreak',
|
|
70
|
+
content: '',
|
|
71
|
+
group: 'inline',
|
|
72
|
+
role: 'hardBreak',
|
|
73
|
+
htmlTag: 'br',
|
|
74
|
+
isVoid: true,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'horizontalRule',
|
|
78
|
+
content: '',
|
|
79
|
+
group: 'block',
|
|
80
|
+
role: 'block',
|
|
81
|
+
htmlTag: 'hr',
|
|
82
|
+
isVoid: true,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'text',
|
|
86
|
+
content: '',
|
|
87
|
+
group: 'inline',
|
|
88
|
+
role: 'text',
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
marks: MARKS,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const prosemirrorSchema: SchemaDefinition = {
|
|
95
|
+
nodes: [
|
|
96
|
+
{
|
|
97
|
+
name: 'doc',
|
|
98
|
+
content: 'block+',
|
|
99
|
+
role: 'doc',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'paragraph',
|
|
103
|
+
content: 'inline*',
|
|
104
|
+
group: 'block',
|
|
105
|
+
role: 'textBlock',
|
|
106
|
+
htmlTag: 'p',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'bullet_list',
|
|
110
|
+
content: 'list_item+',
|
|
111
|
+
group: 'block',
|
|
112
|
+
role: 'list',
|
|
113
|
+
htmlTag: 'ul',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'ordered_list',
|
|
117
|
+
content: 'list_item+',
|
|
118
|
+
group: 'block',
|
|
119
|
+
attrs: { start: { default: 1 } },
|
|
120
|
+
role: 'list',
|
|
121
|
+
htmlTag: 'ol',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'list_item',
|
|
125
|
+
content: 'paragraph block*',
|
|
126
|
+
role: 'listItem',
|
|
127
|
+
htmlTag: 'li',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'hard_break',
|
|
131
|
+
content: '',
|
|
132
|
+
group: 'inline',
|
|
133
|
+
role: 'hardBreak',
|
|
134
|
+
htmlTag: 'br',
|
|
135
|
+
isVoid: true,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'horizontal_rule',
|
|
139
|
+
content: '',
|
|
140
|
+
group: 'block',
|
|
141
|
+
role: 'block',
|
|
142
|
+
htmlTag: 'hr',
|
|
143
|
+
isVoid: true,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'text',
|
|
147
|
+
content: '',
|
|
148
|
+
group: 'inline',
|
|
149
|
+
role: 'text',
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
marks: MARKS,
|
|
153
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
NativeEditorBridge,
|
|
5
|
+
type ActiveState,
|
|
6
|
+
type EditorUpdate,
|
|
7
|
+
type HistoryState,
|
|
8
|
+
type RenderElement,
|
|
9
|
+
type Selection,
|
|
10
|
+
} from './NativeEditorBridge';
|
|
11
|
+
|
|
12
|
+
export interface UseNativeEditorOptions {
|
|
13
|
+
/** Maximum character length. Omit for no limit. */
|
|
14
|
+
maxLength?: number;
|
|
15
|
+
/** Initial HTML content to load after creation. */
|
|
16
|
+
initialHtml?: string;
|
|
17
|
+
/** Called when content changes after an editing operation. */
|
|
18
|
+
onChange?: (html: string) => void;
|
|
19
|
+
/** Called when selection changes. */
|
|
20
|
+
onSelectionChange?: (selection: Selection) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UseNativeEditorReturn {
|
|
24
|
+
/** The underlying bridge instance, or null before creation. */
|
|
25
|
+
bridge: NativeEditorBridge | null;
|
|
26
|
+
/** Whether the editor has been created and is ready. */
|
|
27
|
+
isReady: boolean;
|
|
28
|
+
/** Current selection state. */
|
|
29
|
+
selection: Selection;
|
|
30
|
+
/** Currently active marks/nodes at the selection. */
|
|
31
|
+
activeState: ActiveState;
|
|
32
|
+
/** Current undo/redo availability. */
|
|
33
|
+
historyState: HistoryState;
|
|
34
|
+
/** Current render elements. */
|
|
35
|
+
renderElements: RenderElement[];
|
|
36
|
+
/** Toggle a mark (e.g. 'bold', 'italic', 'underline'). */
|
|
37
|
+
toggleMark: (markType: string) => void;
|
|
38
|
+
/** Undo the last operation. */
|
|
39
|
+
undo: () => void;
|
|
40
|
+
/** Redo the last undone operation. */
|
|
41
|
+
redo: () => void;
|
|
42
|
+
/** Insert text at a position. */
|
|
43
|
+
insertText: (pos: number, text: string) => void;
|
|
44
|
+
/** Delete a range [from, to). */
|
|
45
|
+
deleteRange: (from: number, to: number) => void;
|
|
46
|
+
/** Get the current HTML content. */
|
|
47
|
+
getHtml: () => string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const DEFAULT_SELECTION: Selection = { type: 'text', anchor: 0, head: 0 };
|
|
51
|
+
const DEFAULT_ACTIVE_STATE: ActiveState = {
|
|
52
|
+
marks: {},
|
|
53
|
+
nodes: {},
|
|
54
|
+
commands: {},
|
|
55
|
+
allowedMarks: [],
|
|
56
|
+
insertableNodes: [],
|
|
57
|
+
};
|
|
58
|
+
const DEFAULT_HISTORY_STATE: HistoryState = { canUndo: false, canRedo: false };
|
|
59
|
+
|
|
60
|
+
export function useNativeEditor(
|
|
61
|
+
options: UseNativeEditorOptions = {}
|
|
62
|
+
): UseNativeEditorReturn {
|
|
63
|
+
const { maxLength, initialHtml, onChange, onSelectionChange } = options;
|
|
64
|
+
|
|
65
|
+
const bridgeRef = useRef<NativeEditorBridge | null>(null);
|
|
66
|
+
const onChangeRef = useRef(onChange);
|
|
67
|
+
onChangeRef.current = onChange;
|
|
68
|
+
const onSelectionChangeRef = useRef(onSelectionChange);
|
|
69
|
+
onSelectionChangeRef.current = onSelectionChange;
|
|
70
|
+
|
|
71
|
+
const [isReady, setIsReady] = useState(false);
|
|
72
|
+
const [selection, setSelection] = useState<Selection>(DEFAULT_SELECTION);
|
|
73
|
+
const [activeState, setActiveState] = useState<ActiveState>(DEFAULT_ACTIVE_STATE);
|
|
74
|
+
const [historyState, setHistoryState] = useState<HistoryState>(DEFAULT_HISTORY_STATE);
|
|
75
|
+
const [renderElements, setRenderElements] = useState<RenderElement[]>([]);
|
|
76
|
+
|
|
77
|
+
const syncStateFromUpdate = useCallback((update: EditorUpdate | null) => {
|
|
78
|
+
if (!update) return;
|
|
79
|
+
setRenderElements(update.renderElements);
|
|
80
|
+
setSelection(update.selection);
|
|
81
|
+
setActiveState(update.activeState);
|
|
82
|
+
setHistoryState(update.historyState);
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
const applyUpdate = useCallback((update: EditorUpdate | null) => {
|
|
86
|
+
if (!update) return;
|
|
87
|
+
syncStateFromUpdate(update);
|
|
88
|
+
onSelectionChangeRef.current?.(update.selection);
|
|
89
|
+
|
|
90
|
+
// Fetch current HTML and notify onChange
|
|
91
|
+
if (onChangeRef.current && bridgeRef.current && !bridgeRef.current.isDestroyed) {
|
|
92
|
+
const html = bridgeRef.current.getHtml();
|
|
93
|
+
onChangeRef.current(html);
|
|
94
|
+
}
|
|
95
|
+
}, [syncStateFromUpdate]);
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
const bridge = NativeEditorBridge.create(maxLength != null ? { maxLength } : undefined);
|
|
99
|
+
bridgeRef.current = bridge;
|
|
100
|
+
|
|
101
|
+
if (initialHtml) {
|
|
102
|
+
bridge.setHtml(initialHtml);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
syncStateFromUpdate(bridge.getCurrentState());
|
|
106
|
+
setIsReady(true);
|
|
107
|
+
|
|
108
|
+
return () => {
|
|
109
|
+
bridge.destroy();
|
|
110
|
+
bridgeRef.current = null;
|
|
111
|
+
setIsReady(false);
|
|
112
|
+
};
|
|
113
|
+
}, [maxLength, initialHtml, syncStateFromUpdate]);
|
|
114
|
+
|
|
115
|
+
const toggleMark = useCallback(
|
|
116
|
+
(markType: string) => {
|
|
117
|
+
if (!bridgeRef.current || bridgeRef.current.isDestroyed) return;
|
|
118
|
+
const update = bridgeRef.current.toggleMark(markType);
|
|
119
|
+
applyUpdate(update);
|
|
120
|
+
},
|
|
121
|
+
[applyUpdate]
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const undo = useCallback(() => {
|
|
125
|
+
if (!bridgeRef.current || bridgeRef.current.isDestroyed) return;
|
|
126
|
+
const update = bridgeRef.current.undo();
|
|
127
|
+
applyUpdate(update);
|
|
128
|
+
}, [applyUpdate]);
|
|
129
|
+
|
|
130
|
+
const redo = useCallback(() => {
|
|
131
|
+
if (!bridgeRef.current || bridgeRef.current.isDestroyed) return;
|
|
132
|
+
const update = bridgeRef.current.redo();
|
|
133
|
+
applyUpdate(update);
|
|
134
|
+
}, [applyUpdate]);
|
|
135
|
+
|
|
136
|
+
const insertText = useCallback(
|
|
137
|
+
(pos: number, text: string) => {
|
|
138
|
+
if (!bridgeRef.current || bridgeRef.current.isDestroyed) return;
|
|
139
|
+
const update = bridgeRef.current.insertText(pos, text);
|
|
140
|
+
applyUpdate(update);
|
|
141
|
+
},
|
|
142
|
+
[applyUpdate]
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const deleteRange = useCallback(
|
|
146
|
+
(from: number, to: number) => {
|
|
147
|
+
if (!bridgeRef.current || bridgeRef.current.isDestroyed) return;
|
|
148
|
+
const update = bridgeRef.current.deleteRange(from, to);
|
|
149
|
+
applyUpdate(update);
|
|
150
|
+
},
|
|
151
|
+
[applyUpdate]
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const getHtml = useCallback((): string => {
|
|
155
|
+
if (!bridgeRef.current || bridgeRef.current.isDestroyed) return '';
|
|
156
|
+
return bridgeRef.current.getHtml();
|
|
157
|
+
}, []);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
bridge: bridgeRef.current,
|
|
161
|
+
isReady,
|
|
162
|
+
selection,
|
|
163
|
+
activeState,
|
|
164
|
+
historyState,
|
|
165
|
+
renderElements,
|
|
166
|
+
toggleMark,
|
|
167
|
+
undo,
|
|
168
|
+
redo,
|
|
169
|
+
insertText,
|
|
170
|
+
deleteRange,
|
|
171
|
+
getHtml,
|
|
172
|
+
};
|
|
173
|
+
}
|