@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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SchemaDefinition } from './schemas';
|
|
1
2
|
export interface NativeEditorModule {
|
|
2
3
|
editorCreate(configJson: string): number;
|
|
3
4
|
editorDestroy(editorId: number): void;
|
|
@@ -17,6 +18,7 @@ export interface NativeEditorModule {
|
|
|
17
18
|
editorGetHtml(editorId: number): string;
|
|
18
19
|
editorSetJson(editorId: number, json: string): string;
|
|
19
20
|
editorGetJson(editorId: number): string;
|
|
21
|
+
editorGetContentSnapshot(editorId: number): string;
|
|
20
22
|
editorReplaceHtml(editorId: number, html: string): string;
|
|
21
23
|
editorReplaceJson(editorId: number, json: string): string;
|
|
22
24
|
editorInsertText(editorId: number, pos: number, text: string): string;
|
|
@@ -30,8 +32,10 @@ export interface NativeEditorModule {
|
|
|
30
32
|
editorSetMark(editorId: number, markName: string, attrsJson: string): string;
|
|
31
33
|
editorUnsetMark(editorId: number, markName: string): string;
|
|
32
34
|
editorToggleBlockquote(editorId: number): string;
|
|
35
|
+
editorToggleHeading(editorId: number, level: number): string;
|
|
33
36
|
editorSetSelection(editorId: number, anchor: number, head: number): void;
|
|
34
37
|
editorGetSelection(editorId: number): string;
|
|
38
|
+
editorGetSelectionState(editorId: number): string;
|
|
35
39
|
editorGetCurrentState(editorId: number): string;
|
|
36
40
|
editorInsertTextScalar(editorId: number, scalarPos: number, text: string): string;
|
|
37
41
|
editorDeleteScalarRange(editorId: number, scalarFrom: number, scalarTo: number): string;
|
|
@@ -43,6 +47,7 @@ export interface NativeEditorModule {
|
|
|
43
47
|
editorSetMarkAtSelectionScalar(editorId: number, scalarAnchor: number, scalarHead: number, markName: string, attrsJson: string): string;
|
|
44
48
|
editorUnsetMarkAtSelectionScalar(editorId: number, scalarAnchor: number, scalarHead: number, markName: string): string;
|
|
45
49
|
editorToggleBlockquoteAtSelectionScalar(editorId: number, scalarAnchor: number, scalarHead: number): string;
|
|
50
|
+
editorToggleHeadingAtSelectionScalar(editorId: number, scalarAnchor: number, scalarHead: number, level: number): string;
|
|
46
51
|
editorWrapInListAtSelectionScalar(editorId: number, scalarAnchor: number, scalarHead: number, listType: string): string;
|
|
47
52
|
editorUnwrapFromListAtSelectionScalar(editorId: number, scalarAnchor: number, scalarHead: number): string;
|
|
48
53
|
editorIndentListItemAtSelectionScalar(editorId: number, scalarAnchor: number, scalarHead: number): string;
|
|
@@ -90,6 +95,11 @@ export interface RenderElement {
|
|
|
90
95
|
attrs?: Record<string, unknown>;
|
|
91
96
|
listContext?: ListContext;
|
|
92
97
|
}
|
|
98
|
+
interface RenderBlocksPatch {
|
|
99
|
+
startIndex: number;
|
|
100
|
+
deleteCount: number;
|
|
101
|
+
renderBlocks: RenderElement[][];
|
|
102
|
+
}
|
|
93
103
|
export interface ActiveState {
|
|
94
104
|
marks: Record<string, boolean>;
|
|
95
105
|
markAttrs: Record<string, Record<string, unknown>>;
|
|
@@ -104,9 +114,16 @@ export interface HistoryState {
|
|
|
104
114
|
}
|
|
105
115
|
export interface EditorUpdate {
|
|
106
116
|
renderElements: RenderElement[];
|
|
117
|
+
renderBlocks?: RenderElement[][];
|
|
118
|
+
renderPatch?: RenderBlocksPatch;
|
|
107
119
|
selection: Selection;
|
|
108
120
|
activeState: ActiveState;
|
|
109
121
|
historyState: HistoryState;
|
|
122
|
+
documentVersion?: number;
|
|
123
|
+
}
|
|
124
|
+
export interface ContentSnapshot {
|
|
125
|
+
html: string;
|
|
126
|
+
json: DocumentJSON;
|
|
110
127
|
}
|
|
111
128
|
export interface DocumentJSON {
|
|
112
129
|
[key: string]: unknown;
|
|
@@ -125,7 +142,7 @@ export interface CollaborationResult {
|
|
|
125
142
|
}
|
|
126
143
|
export type EncodedCollaborationStateInput = Uint8Array | readonly number[] | string;
|
|
127
144
|
export declare function normalizeActiveState(raw: unknown): ActiveState;
|
|
128
|
-
export declare function parseEditorUpdateJson(json: string): EditorUpdate | null;
|
|
145
|
+
export declare function parseEditorUpdateJson(json: string, previousRenderBlocks?: RenderElement[][]): EditorUpdate | null;
|
|
129
146
|
export declare function encodeCollaborationStateBase64(encodedState: EncodedCollaborationStateInput): string;
|
|
130
147
|
export declare function decodeCollaborationStateBase64(base64: string): Uint8Array;
|
|
131
148
|
export declare function parseCollaborationResultJson(json: string): CollaborationResult;
|
|
@@ -135,6 +152,10 @@ export declare class NativeEditorBridge {
|
|
|
135
152
|
private _editorId;
|
|
136
153
|
private _destroyed;
|
|
137
154
|
private _lastSelection;
|
|
155
|
+
private _documentVersion;
|
|
156
|
+
private _cachedHtml;
|
|
157
|
+
private _cachedJsonString;
|
|
158
|
+
private _renderBlocksCache;
|
|
138
159
|
private constructor();
|
|
139
160
|
/** Create a new editor instance backed by the Rust engine. */
|
|
140
161
|
static create(config?: {
|
|
@@ -154,8 +175,14 @@ export declare class NativeEditorBridge {
|
|
|
154
175
|
getHtml(): string;
|
|
155
176
|
/** Set content from ProseMirror JSON. Returns render elements. */
|
|
156
177
|
setJson(doc: DocumentJSON): RenderElement[];
|
|
178
|
+
/** Set content from a serialized ProseMirror JSON string. Returns render elements. */
|
|
179
|
+
setJsonString(jsonString: string): RenderElement[];
|
|
180
|
+
/** Get content as raw ProseMirror JSON string. */
|
|
181
|
+
getJsonString(): string;
|
|
157
182
|
/** Get content as ProseMirror JSON. */
|
|
158
183
|
getJson(): DocumentJSON;
|
|
184
|
+
/** Get both HTML and JSON content in one native roundtrip. */
|
|
185
|
+
getContentSnapshot(): ContentSnapshot;
|
|
159
186
|
/** Insert text at a document position. Returns the full update. */
|
|
160
187
|
insertText(pos: number, text: string): EditorUpdate | null;
|
|
161
188
|
/** Delete a range [from, to). Returns the full update. */
|
|
@@ -170,6 +197,8 @@ export declare class NativeEditorBridge {
|
|
|
170
197
|
unsetMark(markType: string): EditorUpdate | null;
|
|
171
198
|
/** Toggle blockquote wrapping for the current block selection. */
|
|
172
199
|
toggleBlockquote(): EditorUpdate | null;
|
|
200
|
+
/** Toggle a heading level on the current block selection. */
|
|
201
|
+
toggleHeading(level: number): EditorUpdate | null;
|
|
173
202
|
/** Set the document selection by anchor and head positions. */
|
|
174
203
|
setSelection(anchor: number, head: number): void;
|
|
175
204
|
/** Get the current selection from the Rust engine (synchronous native call).
|
|
@@ -180,6 +209,8 @@ export declare class NativeEditorBridge {
|
|
|
180
209
|
updateSelectionFromNative(anchor: number, head: number): void;
|
|
181
210
|
/** Get the current full state from Rust (render elements, selection, etc.). */
|
|
182
211
|
getCurrentState(): EditorUpdate | null;
|
|
212
|
+
/** Get the current selection-related state without render elements. */
|
|
213
|
+
getSelectionState(): EditorUpdate | null;
|
|
183
214
|
/** Split the block at a position (Enter key). */
|
|
184
215
|
splitBlock(pos: number): EditorUpdate | null;
|
|
185
216
|
/** Insert HTML content at the current selection. */
|
|
@@ -192,6 +223,8 @@ export declare class NativeEditorBridge {
|
|
|
192
223
|
replaceHtml(html: string): EditorUpdate | null;
|
|
193
224
|
/** Replace entire document with JSON via transaction (preserves undo history). */
|
|
194
225
|
replaceJson(doc: DocumentJSON): EditorUpdate | null;
|
|
226
|
+
/** Replace entire document with a serialized JSON transaction. */
|
|
227
|
+
replaceJsonString(jsonString: string): EditorUpdate | null;
|
|
195
228
|
/** Undo the last operation. Returns update or null if nothing to undo. */
|
|
196
229
|
undo(): EditorUpdate | null;
|
|
197
230
|
/** Redo the last undone operation. Returns update or null if nothing to redo. */
|
|
@@ -210,6 +243,10 @@ export declare class NativeEditorBridge {
|
|
|
210
243
|
outdentListItem(): EditorUpdate | null;
|
|
211
244
|
/** Insert a void node (e.g. 'horizontalRule') at the current selection. */
|
|
212
245
|
insertNode(nodeType: string): EditorUpdate | null;
|
|
246
|
+
parseUpdateJson(json: string): EditorUpdate | null;
|
|
247
|
+
private noteUpdate;
|
|
248
|
+
private parseAndNoteUpdate;
|
|
249
|
+
private invalidateContentCaches;
|
|
213
250
|
private assertNotDestroyed;
|
|
214
251
|
private currentScalarSelection;
|
|
215
252
|
}
|
|
@@ -220,6 +257,7 @@ export declare class NativeCollaborationBridge {
|
|
|
220
257
|
static create(config?: {
|
|
221
258
|
clientId?: number;
|
|
222
259
|
fragmentName?: string;
|
|
260
|
+
schema?: SchemaDefinition;
|
|
223
261
|
initialDocumentJson?: DocumentJSON;
|
|
224
262
|
initialEncodedState?: EncodedCollaborationStateInput;
|
|
225
263
|
localAwareness?: Record<string, unknown>;
|
|
@@ -240,3 +278,4 @@ export declare class NativeCollaborationBridge {
|
|
|
240
278
|
clearLocalAwareness(): CollaborationResult;
|
|
241
279
|
private assertNotDestroyed;
|
|
242
280
|
}
|
|
281
|
+
export {};
|
|
@@ -46,7 +46,7 @@ function parseRenderElements(json) {
|
|
|
46
46
|
throw new Error(ERR_NATIVE_RESPONSE);
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
function parseEditorUpdateJson(json) {
|
|
49
|
+
function parseEditorUpdateJson(json, previousRenderBlocks) {
|
|
50
50
|
if (!json || json === '')
|
|
51
51
|
return null;
|
|
52
52
|
try {
|
|
@@ -54,14 +54,72 @@ function parseEditorUpdateJson(json) {
|
|
|
54
54
|
if ('error' in parsed) {
|
|
55
55
|
throw new Error(`NativeEditorBridge: ${parsed.error}`);
|
|
56
56
|
}
|
|
57
|
+
const renderBlocks = Array.isArray(parsed.renderBlocks)
|
|
58
|
+
? parsed.renderBlocks
|
|
59
|
+
: applyRenderBlocksPatch(previousRenderBlocks, parsed.renderPatch != null && typeof parsed.renderPatch === 'object'
|
|
60
|
+
? parsed.renderPatch
|
|
61
|
+
: undefined);
|
|
62
|
+
const renderPatch = parsed.renderPatch != null && typeof parsed.renderPatch === 'object'
|
|
63
|
+
? parsed.renderPatch
|
|
64
|
+
: undefined;
|
|
57
65
|
return {
|
|
58
|
-
renderElements: (parsed.renderElements
|
|
66
|
+
renderElements: Array.isArray(parsed.renderElements)
|
|
67
|
+
? parsed.renderElements
|
|
68
|
+
: flattenRenderBlocks(renderBlocks),
|
|
69
|
+
renderBlocks,
|
|
70
|
+
renderPatch,
|
|
59
71
|
selection: (parsed.selection ?? { type: 'text', anchor: 0, head: 0 }),
|
|
60
72
|
activeState: normalizeActiveState(parsed.activeState),
|
|
61
73
|
historyState: (parsed.historyState ?? {
|
|
62
74
|
canUndo: false,
|
|
63
75
|
canRedo: false,
|
|
64
76
|
}),
|
|
77
|
+
documentVersion: typeof parsed.documentVersion === 'number' ? parsed.documentVersion : undefined,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
if (e instanceof Error && e.message.startsWith('NativeEditorBridge:')) {
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
throw new Error(ERR_NATIVE_RESPONSE);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function flattenRenderBlocks(renderBlocks) {
|
|
88
|
+
if (!renderBlocks || renderBlocks.length === 0) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
return renderBlocks.flat();
|
|
92
|
+
}
|
|
93
|
+
function applyRenderBlocksPatch(previousRenderBlocks, renderPatch) {
|
|
94
|
+
if (!previousRenderBlocks || !renderPatch) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
const { startIndex, deleteCount, renderBlocks } = renderPatch;
|
|
98
|
+
if (!Number.isInteger(startIndex) ||
|
|
99
|
+
!Number.isInteger(deleteCount) ||
|
|
100
|
+
startIndex < 0 ||
|
|
101
|
+
deleteCount < 0 ||
|
|
102
|
+
startIndex > previousRenderBlocks.length ||
|
|
103
|
+
startIndex + deleteCount > previousRenderBlocks.length) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return [
|
|
107
|
+
...previousRenderBlocks.slice(0, startIndex),
|
|
108
|
+
...renderBlocks,
|
|
109
|
+
...previousRenderBlocks.slice(startIndex + deleteCount),
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
function parseContentSnapshotJson(json) {
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(json);
|
|
115
|
+
if ('error' in parsed) {
|
|
116
|
+
throw new Error(`NativeEditorBridge: ${parsed.error}`);
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
html: typeof parsed.html === 'string' ? parsed.html : '',
|
|
120
|
+
json: parsed.json != null && typeof parsed.json === 'object'
|
|
121
|
+
? parsed.json
|
|
122
|
+
: {},
|
|
65
123
|
};
|
|
66
124
|
}
|
|
67
125
|
catch (e) {
|
|
@@ -220,6 +278,10 @@ class NativeEditorBridge {
|
|
|
220
278
|
constructor(editorId) {
|
|
221
279
|
this._destroyed = false;
|
|
222
280
|
this._lastSelection = { type: 'text', anchor: 0, head: 0 };
|
|
281
|
+
this._documentVersion = 0;
|
|
282
|
+
this._cachedHtml = null;
|
|
283
|
+
this._cachedJsonString = null;
|
|
284
|
+
this._renderBlocksCache = null;
|
|
223
285
|
this._editorId = editorId;
|
|
224
286
|
}
|
|
225
287
|
/** Create a new editor instance backed by the Rust engine. */
|
|
@@ -254,57 +316,88 @@ class NativeEditorBridge {
|
|
|
254
316
|
if (this._destroyed)
|
|
255
317
|
return;
|
|
256
318
|
this._destroyed = true;
|
|
319
|
+
this._renderBlocksCache = null;
|
|
257
320
|
getNativeModule().editorDestroy(this._editorId);
|
|
258
321
|
}
|
|
259
322
|
/** Set content from HTML. Returns render elements for display. */
|
|
260
323
|
setHtml(html) {
|
|
261
324
|
this.assertNotDestroyed();
|
|
325
|
+
this.invalidateContentCaches();
|
|
326
|
+
this._renderBlocksCache = null;
|
|
262
327
|
const json = getNativeModule().editorSetHtml(this._editorId, html);
|
|
263
328
|
return parseRenderElements(json);
|
|
264
329
|
}
|
|
265
330
|
/** Get content as HTML. */
|
|
266
331
|
getHtml() {
|
|
267
332
|
this.assertNotDestroyed();
|
|
268
|
-
|
|
333
|
+
if (this._cachedHtml?.version === this._documentVersion) {
|
|
334
|
+
return this._cachedHtml.value;
|
|
335
|
+
}
|
|
336
|
+
const html = getNativeModule().editorGetHtml(this._editorId);
|
|
337
|
+
this._cachedHtml = { version: this._documentVersion, value: html };
|
|
338
|
+
return html;
|
|
269
339
|
}
|
|
270
340
|
/** Set content from ProseMirror JSON. Returns render elements. */
|
|
271
341
|
setJson(doc) {
|
|
342
|
+
return this.setJsonString(JSON.stringify(doc));
|
|
343
|
+
}
|
|
344
|
+
/** Set content from a serialized ProseMirror JSON string. Returns render elements. */
|
|
345
|
+
setJsonString(jsonString) {
|
|
272
346
|
this.assertNotDestroyed();
|
|
273
|
-
|
|
347
|
+
this.invalidateContentCaches();
|
|
348
|
+
this._renderBlocksCache = null;
|
|
349
|
+
const json = getNativeModule().editorSetJson(this._editorId, jsonString);
|
|
274
350
|
return parseRenderElements(json);
|
|
275
351
|
}
|
|
352
|
+
/** Get content as raw ProseMirror JSON string. */
|
|
353
|
+
getJsonString() {
|
|
354
|
+
this.assertNotDestroyed();
|
|
355
|
+
if (this._cachedJsonString?.version === this._documentVersion) {
|
|
356
|
+
return this._cachedJsonString.value;
|
|
357
|
+
}
|
|
358
|
+
const json = getNativeModule().editorGetJson(this._editorId);
|
|
359
|
+
this._cachedJsonString = { version: this._documentVersion, value: json };
|
|
360
|
+
return json;
|
|
361
|
+
}
|
|
276
362
|
/** Get content as ProseMirror JSON. */
|
|
277
363
|
getJson() {
|
|
364
|
+
return parseDocumentJSON(this.getJsonString());
|
|
365
|
+
}
|
|
366
|
+
/** Get both HTML and JSON content in one native roundtrip. */
|
|
367
|
+
getContentSnapshot() {
|
|
278
368
|
this.assertNotDestroyed();
|
|
279
|
-
|
|
280
|
-
|
|
369
|
+
if (this._cachedHtml?.version === this._documentVersion &&
|
|
370
|
+
this._cachedJsonString?.version === this._documentVersion) {
|
|
371
|
+
return {
|
|
372
|
+
html: this._cachedHtml.value,
|
|
373
|
+
json: parseDocumentJSON(this._cachedJsonString.value),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
const snapshot = parseContentSnapshotJson(getNativeModule().editorGetContentSnapshot(this._editorId));
|
|
377
|
+
this._cachedHtml = { version: this._documentVersion, value: snapshot.html };
|
|
378
|
+
this._cachedJsonString = {
|
|
379
|
+
version: this._documentVersion,
|
|
380
|
+
value: JSON.stringify(snapshot.json),
|
|
381
|
+
};
|
|
382
|
+
return snapshot;
|
|
281
383
|
}
|
|
282
384
|
/** Insert text at a document position. Returns the full update. */
|
|
283
385
|
insertText(pos, text) {
|
|
284
386
|
this.assertNotDestroyed();
|
|
285
387
|
const json = getNativeModule().editorInsertText(this._editorId, pos, text);
|
|
286
|
-
|
|
287
|
-
if (update)
|
|
288
|
-
this._lastSelection = update.selection;
|
|
289
|
-
return update;
|
|
388
|
+
return this.parseAndNoteUpdate(json);
|
|
290
389
|
}
|
|
291
390
|
/** Delete a range [from, to). Returns the full update. */
|
|
292
391
|
deleteRange(from, to) {
|
|
293
392
|
this.assertNotDestroyed();
|
|
294
393
|
const json = getNativeModule().editorDeleteRange(this._editorId, from, to);
|
|
295
|
-
|
|
296
|
-
if (update)
|
|
297
|
-
this._lastSelection = update.selection;
|
|
298
|
-
return update;
|
|
394
|
+
return this.parseAndNoteUpdate(json);
|
|
299
395
|
}
|
|
300
396
|
/** Replace the current selection with text atomically. */
|
|
301
397
|
replaceSelectionText(text) {
|
|
302
398
|
this.assertNotDestroyed();
|
|
303
399
|
const json = getNativeModule().editorReplaceSelectionText(this._editorId, text);
|
|
304
|
-
|
|
305
|
-
if (update)
|
|
306
|
-
this._lastSelection = update.selection;
|
|
307
|
-
return update;
|
|
400
|
+
return this.parseAndNoteUpdate(json);
|
|
308
401
|
}
|
|
309
402
|
/** Toggle a mark (bold, italic, etc.) on the current selection. */
|
|
310
403
|
toggleMark(markType) {
|
|
@@ -313,10 +406,7 @@ class NativeEditorBridge {
|
|
|
313
406
|
const json = scalarSelection
|
|
314
407
|
? getNativeModule().editorToggleMarkAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head, markType)
|
|
315
408
|
: getNativeModule().editorToggleMark(this._editorId, markType);
|
|
316
|
-
|
|
317
|
-
if (update)
|
|
318
|
-
this._lastSelection = update.selection;
|
|
319
|
-
return update;
|
|
409
|
+
return this.parseAndNoteUpdate(json);
|
|
320
410
|
}
|
|
321
411
|
/** Set a mark with attrs on the current selection. */
|
|
322
412
|
setMark(markType, attrs) {
|
|
@@ -326,10 +416,7 @@ class NativeEditorBridge {
|
|
|
326
416
|
const json = scalarSelection
|
|
327
417
|
? getNativeModule().editorSetMarkAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head, markType, attrsJson)
|
|
328
418
|
: getNativeModule().editorSetMark(this._editorId, markType, attrsJson);
|
|
329
|
-
|
|
330
|
-
if (update)
|
|
331
|
-
this._lastSelection = update.selection;
|
|
332
|
-
return update;
|
|
419
|
+
return this.parseAndNoteUpdate(json);
|
|
333
420
|
}
|
|
334
421
|
/** Remove a mark from the current selection. */
|
|
335
422
|
unsetMark(markType) {
|
|
@@ -338,10 +425,7 @@ class NativeEditorBridge {
|
|
|
338
425
|
const json = scalarSelection
|
|
339
426
|
? getNativeModule().editorUnsetMarkAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head, markType)
|
|
340
427
|
: getNativeModule().editorUnsetMark(this._editorId, markType);
|
|
341
|
-
|
|
342
|
-
if (update)
|
|
343
|
-
this._lastSelection = update.selection;
|
|
344
|
-
return update;
|
|
428
|
+
return this.parseAndNoteUpdate(json);
|
|
345
429
|
}
|
|
346
430
|
/** Toggle blockquote wrapping for the current block selection. */
|
|
347
431
|
toggleBlockquote() {
|
|
@@ -350,10 +434,19 @@ class NativeEditorBridge {
|
|
|
350
434
|
const json = scalarSelection
|
|
351
435
|
? getNativeModule().editorToggleBlockquoteAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head)
|
|
352
436
|
: getNativeModule().editorToggleBlockquote(this._editorId);
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
437
|
+
return this.parseAndNoteUpdate(json);
|
|
438
|
+
}
|
|
439
|
+
/** Toggle a heading level on the current block selection. */
|
|
440
|
+
toggleHeading(level) {
|
|
441
|
+
this.assertNotDestroyed();
|
|
442
|
+
if (!Number.isInteger(level) || level < 1 || level > 6) {
|
|
443
|
+
throw new Error('NativeEditorBridge: invalid heading level');
|
|
444
|
+
}
|
|
445
|
+
const scalarSelection = this.currentScalarSelection();
|
|
446
|
+
const json = scalarSelection
|
|
447
|
+
? getNativeModule().editorToggleHeadingAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head, level)
|
|
448
|
+
: getNativeModule().editorToggleHeading(this._editorId, level);
|
|
449
|
+
return this.parseAndNoteUpdate(json);
|
|
357
450
|
}
|
|
358
451
|
/** Set the document selection by anchor and head positions. */
|
|
359
452
|
setSelection(anchor, head) {
|
|
@@ -387,82 +480,65 @@ class NativeEditorBridge {
|
|
|
387
480
|
getCurrentState() {
|
|
388
481
|
this.assertNotDestroyed();
|
|
389
482
|
const json = getNativeModule().editorGetCurrentState(this._editorId);
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
483
|
+
return this.parseAndNoteUpdate(json);
|
|
484
|
+
}
|
|
485
|
+
/** Get the current selection-related state without render elements. */
|
|
486
|
+
getSelectionState() {
|
|
487
|
+
this.assertNotDestroyed();
|
|
488
|
+
const json = getNativeModule().editorGetSelectionState(this._editorId);
|
|
489
|
+
return this.parseAndNoteUpdate(json);
|
|
394
490
|
}
|
|
395
491
|
/** Split the block at a position (Enter key). */
|
|
396
492
|
splitBlock(pos) {
|
|
397
493
|
this.assertNotDestroyed();
|
|
398
494
|
const json = getNativeModule().editorSplitBlock(this._editorId, pos);
|
|
399
|
-
|
|
400
|
-
if (update)
|
|
401
|
-
this._lastSelection = update.selection;
|
|
402
|
-
return update;
|
|
495
|
+
return this.parseAndNoteUpdate(json);
|
|
403
496
|
}
|
|
404
497
|
/** Insert HTML content at the current selection. */
|
|
405
498
|
insertContentHtml(html) {
|
|
406
499
|
this.assertNotDestroyed();
|
|
407
500
|
const json = getNativeModule().editorInsertContentHtml(this._editorId, html);
|
|
408
|
-
|
|
409
|
-
if (update)
|
|
410
|
-
this._lastSelection = update.selection;
|
|
411
|
-
return update;
|
|
501
|
+
return this.parseAndNoteUpdate(json);
|
|
412
502
|
}
|
|
413
503
|
/** Insert JSON content at the current selection. */
|
|
414
504
|
insertContentJson(doc) {
|
|
415
505
|
this.assertNotDestroyed();
|
|
416
506
|
const json = getNativeModule().editorInsertContentJson(this._editorId, JSON.stringify(doc));
|
|
417
|
-
|
|
418
|
-
if (update)
|
|
419
|
-
this._lastSelection = update.selection;
|
|
420
|
-
return update;
|
|
507
|
+
return this.parseAndNoteUpdate(json);
|
|
421
508
|
}
|
|
422
509
|
/** Insert JSON content at an explicit scalar selection. */
|
|
423
510
|
insertContentJsonAtSelectionScalar(scalarAnchor, scalarHead, doc) {
|
|
424
511
|
this.assertNotDestroyed();
|
|
425
512
|
const json = getNativeModule().editorInsertContentJsonAtSelectionScalar(this._editorId, scalarAnchor, scalarHead, JSON.stringify(doc));
|
|
426
|
-
|
|
427
|
-
if (update)
|
|
428
|
-
this._lastSelection = update.selection;
|
|
429
|
-
return update;
|
|
513
|
+
return this.parseAndNoteUpdate(json);
|
|
430
514
|
}
|
|
431
515
|
/** Replace entire document with HTML via transaction (preserves undo history). */
|
|
432
516
|
replaceHtml(html) {
|
|
433
517
|
this.assertNotDestroyed();
|
|
434
518
|
const json = getNativeModule().editorReplaceHtml(this._editorId, html);
|
|
435
|
-
|
|
436
|
-
if (update)
|
|
437
|
-
this._lastSelection = update.selection;
|
|
438
|
-
return update;
|
|
519
|
+
return this.parseAndNoteUpdate(json);
|
|
439
520
|
}
|
|
440
521
|
/** Replace entire document with JSON via transaction (preserves undo history). */
|
|
441
522
|
replaceJson(doc) {
|
|
523
|
+
return this.replaceJsonString(JSON.stringify(doc));
|
|
524
|
+
}
|
|
525
|
+
/** Replace entire document with a serialized JSON transaction. */
|
|
526
|
+
replaceJsonString(jsonString) {
|
|
442
527
|
this.assertNotDestroyed();
|
|
443
|
-
const json = getNativeModule().editorReplaceJson(this._editorId,
|
|
444
|
-
|
|
445
|
-
if (update)
|
|
446
|
-
this._lastSelection = update.selection;
|
|
447
|
-
return update;
|
|
528
|
+
const json = getNativeModule().editorReplaceJson(this._editorId, jsonString);
|
|
529
|
+
return this.parseAndNoteUpdate(json);
|
|
448
530
|
}
|
|
449
531
|
/** Undo the last operation. Returns update or null if nothing to undo. */
|
|
450
532
|
undo() {
|
|
451
533
|
this.assertNotDestroyed();
|
|
452
534
|
const json = getNativeModule().editorUndo(this._editorId);
|
|
453
|
-
|
|
454
|
-
if (update)
|
|
455
|
-
this._lastSelection = update.selection;
|
|
456
|
-
return update;
|
|
535
|
+
return this.parseAndNoteUpdate(json);
|
|
457
536
|
}
|
|
458
537
|
/** Redo the last undone operation. Returns update or null if nothing to redo. */
|
|
459
538
|
redo() {
|
|
460
539
|
this.assertNotDestroyed();
|
|
461
540
|
const json = getNativeModule().editorRedo(this._editorId);
|
|
462
|
-
|
|
463
|
-
if (update)
|
|
464
|
-
this._lastSelection = update.selection;
|
|
465
|
-
return update;
|
|
541
|
+
return this.parseAndNoteUpdate(json);
|
|
466
542
|
}
|
|
467
543
|
/** Check if undo is available. */
|
|
468
544
|
canUndo() {
|
|
@@ -486,10 +562,7 @@ class NativeEditorBridge {
|
|
|
486
562
|
: scalarSelection
|
|
487
563
|
? getNativeModule().editorWrapInListAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head, listType)
|
|
488
564
|
: getNativeModule().editorWrapInList(this._editorId, listType);
|
|
489
|
-
|
|
490
|
-
if (update)
|
|
491
|
-
this._lastSelection = update.selection;
|
|
492
|
-
return update;
|
|
565
|
+
return this.parseAndNoteUpdate(json);
|
|
493
566
|
}
|
|
494
567
|
/** Unwrap the current list item back to a paragraph. */
|
|
495
568
|
unwrapFromList() {
|
|
@@ -498,10 +571,7 @@ class NativeEditorBridge {
|
|
|
498
571
|
const json = scalarSelection
|
|
499
572
|
? getNativeModule().editorUnwrapFromListAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head)
|
|
500
573
|
: getNativeModule().editorUnwrapFromList(this._editorId);
|
|
501
|
-
|
|
502
|
-
if (update)
|
|
503
|
-
this._lastSelection = update.selection;
|
|
504
|
-
return update;
|
|
574
|
+
return this.parseAndNoteUpdate(json);
|
|
505
575
|
}
|
|
506
576
|
/** Indent the current list item into a nested list. */
|
|
507
577
|
indentListItem() {
|
|
@@ -510,10 +580,7 @@ class NativeEditorBridge {
|
|
|
510
580
|
const json = scalarSelection
|
|
511
581
|
? getNativeModule().editorIndentListItemAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head)
|
|
512
582
|
: getNativeModule().editorIndentListItem(this._editorId);
|
|
513
|
-
|
|
514
|
-
if (update)
|
|
515
|
-
this._lastSelection = update.selection;
|
|
516
|
-
return update;
|
|
583
|
+
return this.parseAndNoteUpdate(json);
|
|
517
584
|
}
|
|
518
585
|
/** Outdent the current list item to the parent list level. */
|
|
519
586
|
outdentListItem() {
|
|
@@ -522,10 +589,7 @@ class NativeEditorBridge {
|
|
|
522
589
|
const json = scalarSelection
|
|
523
590
|
? getNativeModule().editorOutdentListItemAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head)
|
|
524
591
|
: getNativeModule().editorOutdentListItem(this._editorId);
|
|
525
|
-
|
|
526
|
-
if (update)
|
|
527
|
-
this._lastSelection = update.selection;
|
|
528
|
-
return update;
|
|
592
|
+
return this.parseAndNoteUpdate(json);
|
|
529
593
|
}
|
|
530
594
|
/** Insert a void node (e.g. 'horizontalRule') at the current selection. */
|
|
531
595
|
insertNode(nodeType) {
|
|
@@ -534,11 +598,41 @@ class NativeEditorBridge {
|
|
|
534
598
|
const json = scalarSelection
|
|
535
599
|
? getNativeModule().editorInsertNodeAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head, nodeType)
|
|
536
600
|
: getNativeModule().editorInsertNode(this._editorId, nodeType);
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
601
|
+
return this.parseAndNoteUpdate(json);
|
|
602
|
+
}
|
|
603
|
+
parseUpdateJson(json) {
|
|
604
|
+
this.assertNotDestroyed();
|
|
605
|
+
return this.parseAndNoteUpdate(json);
|
|
606
|
+
}
|
|
607
|
+
noteUpdate(update) {
|
|
608
|
+
if (!update) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
this._lastSelection = update.selection;
|
|
612
|
+
if (update.renderBlocks) {
|
|
613
|
+
this._renderBlocksCache = update.renderBlocks;
|
|
614
|
+
}
|
|
615
|
+
if (typeof update.documentVersion !== 'number') {
|
|
616
|
+
this.invalidateContentCaches();
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (update.documentVersion !== this._documentVersion) {
|
|
620
|
+
this._documentVersion = update.documentVersion;
|
|
621
|
+
this.invalidateContentCaches();
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
parseAndNoteUpdate(json) {
|
|
625
|
+
let update = parseEditorUpdateJson(json, this._renderBlocksCache ?? undefined);
|
|
626
|
+
if (update?.renderPatch && !update.renderBlocks) {
|
|
627
|
+
update = parseEditorUpdateJson(getNativeModule().editorGetCurrentState(this._editorId), this._renderBlocksCache ?? undefined);
|
|
628
|
+
}
|
|
629
|
+
this.noteUpdate(update);
|
|
540
630
|
return update;
|
|
541
631
|
}
|
|
632
|
+
invalidateContentCaches() {
|
|
633
|
+
this._cachedHtml = null;
|
|
634
|
+
this._cachedJsonString = null;
|
|
635
|
+
}
|
|
542
636
|
assertNotDestroyed() {
|
|
543
637
|
if (this._destroyed) {
|
|
544
638
|
throw new Error(ERR_DESTROYED);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
3
3
|
import { type ActiveState, type DocumentJSON, type HistoryState, type Selection } from './NativeEditorBridge';
|
|
4
|
-
import { type EditorToolbarItem } from './EditorToolbar';
|
|
4
|
+
import { type EditorToolbarHeadingLevel, type EditorToolbarItem } from './EditorToolbar';
|
|
5
5
|
import { type EditorTheme } from './EditorTheme';
|
|
6
6
|
import { type EditorAddons } from './addons';
|
|
7
7
|
import { type SchemaDefinition } from './schemas';
|
|
@@ -38,6 +38,8 @@ export interface NativeRichTextEditorProps {
|
|
|
38
38
|
value?: string;
|
|
39
39
|
/** Controlled ProseMirror JSON content. Ignored if value is set. */
|
|
40
40
|
valueJSON?: DocumentJSON;
|
|
41
|
+
/** Optional stable revision hint for `valueJSON` to avoid reserializing equal docs on rerender. */
|
|
42
|
+
valueJSONRevision?: string | number;
|
|
41
43
|
/** Schema definition. Defaults to tiptapSchema if not provided. */
|
|
42
44
|
schema?: SchemaDefinition;
|
|
43
45
|
/** Placeholder text shown when editor is empty. */
|
|
@@ -104,6 +106,8 @@ export interface NativeRichTextEditorRef {
|
|
|
104
106
|
unsetLink(): void;
|
|
105
107
|
/** Toggle blockquote wrapping around the current block selection. */
|
|
106
108
|
toggleBlockquote(): void;
|
|
109
|
+
/** Toggle a heading level on the current block selection. */
|
|
110
|
+
toggleHeading(level: EditorToolbarHeadingLevel): void;
|
|
107
111
|
/** Toggle a list type (bulletList or orderedList). */
|
|
108
112
|
toggleList(listType: 'bulletList' | 'orderedList'): void;
|
|
109
113
|
/** Indent the current list item. */
|