@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.
Files changed (37) hide show
  1. package/README.md +18 -0
  2. package/android/build.gradle +23 -0
  3. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +515 -39
  4. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +58 -28
  5. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +25 -0
  6. package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +232 -62
  7. package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +57 -27
  8. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +147 -78
  9. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +249 -71
  10. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +7 -6
  11. package/dist/EditorToolbar.d.ts +26 -6
  12. package/dist/EditorToolbar.js +299 -65
  13. package/dist/NativeEditorBridge.d.ts +40 -1
  14. package/dist/NativeEditorBridge.js +184 -90
  15. package/dist/NativeRichTextEditor.d.ts +5 -1
  16. package/dist/NativeRichTextEditor.js +201 -78
  17. package/dist/YjsCollaboration.d.ts +2 -0
  18. package/dist/YjsCollaboration.js +142 -20
  19. package/dist/index.d.ts +1 -1
  20. package/dist/schemas.js +12 -0
  21. package/dist/useNativeEditor.d.ts +2 -0
  22. package/dist/useNativeEditor.js +7 -0
  23. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  24. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  25. package/ios/EditorLayoutManager.swift +3 -3
  26. package/ios/Generated_editor_core.swift +87 -0
  27. package/ios/NativeEditorExpoView.swift +488 -178
  28. package/ios/NativeEditorModule.swift +25 -0
  29. package/ios/PositionBridge.swift +310 -75
  30. package/ios/RenderBridge.swift +362 -27
  31. package/ios/RichTextEditorView.swift +2001 -189
  32. package/ios/editor_coreFFI/editor_coreFFI.h +55 -0
  33. package/package.json +11 -2
  34. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  35. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  36. package/rust/android/x86_64/libeditor_core.so +0 -0
  37. 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
- return getNativeModule().editorGetHtml(this._editorId);
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
- const json = getNativeModule().editorSetJson(this._editorId, JSON.stringify(doc));
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
- const json = getNativeModule().editorGetJson(this._editorId);
280
- return parseDocumentJSON(json);
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
- const update = parseEditorUpdateJson(json);
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
- const update = parseEditorUpdateJson(json);
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
- const update = parseEditorUpdateJson(json);
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
- const update = parseEditorUpdateJson(json);
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
- const update = parseEditorUpdateJson(json);
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
- const update = parseEditorUpdateJson(json);
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
- const update = parseEditorUpdateJson(json);
354
- if (update)
355
- this._lastSelection = update.selection;
356
- return update;
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
- const update = parseEditorUpdateJson(json);
391
- if (update)
392
- this._lastSelection = update.selection;
393
- return update;
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
- const update = parseEditorUpdateJson(json);
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
- const update = parseEditorUpdateJson(json);
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
- const update = parseEditorUpdateJson(json);
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
- const update = parseEditorUpdateJson(json);
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
- const update = parseEditorUpdateJson(json);
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, JSON.stringify(doc));
444
- const update = parseEditorUpdateJson(json);
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
- const update = parseEditorUpdateJson(json);
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
- const update = parseEditorUpdateJson(json);
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
- const update = parseEditorUpdateJson(json);
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
- const update = parseEditorUpdateJson(json);
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
- const update = parseEditorUpdateJson(json);
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
- const update = parseEditorUpdateJson(json);
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
- const update = parseEditorUpdateJson(json);
538
- if (update)
539
- this._lastSelection = update.selection;
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. */