@apollohg/react-native-prose-editor 0.4.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 (30) 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 +502 -39
  4. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +56 -28
  5. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +6 -0
  6. package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +57 -27
  7. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +147 -78
  8. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +249 -71
  9. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +7 -6
  10. package/dist/NativeEditorBridge.d.ts +36 -1
  11. package/dist/NativeEditorBridge.js +173 -94
  12. package/dist/NativeRichTextEditor.d.ts +2 -0
  13. package/dist/NativeRichTextEditor.js +160 -53
  14. package/dist/YjsCollaboration.d.ts +2 -0
  15. package/dist/YjsCollaboration.js +142 -20
  16. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  17. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  18. package/ios/EditorLayoutManager.swift +3 -3
  19. package/ios/Generated_editor_core.swift +41 -0
  20. package/ios/NativeEditorExpoView.swift +43 -11
  21. package/ios/NativeEditorModule.swift +6 -0
  22. package/ios/PositionBridge.swift +310 -75
  23. package/ios/RenderBridge.swift +362 -27
  24. package/ios/RichTextEditorView.swift +1983 -187
  25. package/ios/editor_coreFFI/editor_coreFFI.h +33 -0
  26. package/package.json +11 -2
  27. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  28. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  29. package/rust/android/x86_64/libeditor_core.so +0 -0
  30. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +63 -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;
@@ -33,6 +35,7 @@ export interface NativeEditorModule {
33
35
  editorToggleHeading(editorId: number, level: number): string;
34
36
  editorSetSelection(editorId: number, anchor: number, head: number): void;
35
37
  editorGetSelection(editorId: number): string;
38
+ editorGetSelectionState(editorId: number): string;
36
39
  editorGetCurrentState(editorId: number): string;
37
40
  editorInsertTextScalar(editorId: number, scalarPos: number, text: string): string;
38
41
  editorDeleteScalarRange(editorId: number, scalarFrom: number, scalarTo: number): string;
@@ -92,6 +95,11 @@ export interface RenderElement {
92
95
  attrs?: Record<string, unknown>;
93
96
  listContext?: ListContext;
94
97
  }
98
+ interface RenderBlocksPatch {
99
+ startIndex: number;
100
+ deleteCount: number;
101
+ renderBlocks: RenderElement[][];
102
+ }
95
103
  export interface ActiveState {
96
104
  marks: Record<string, boolean>;
97
105
  markAttrs: Record<string, Record<string, unknown>>;
@@ -106,9 +114,16 @@ export interface HistoryState {
106
114
  }
107
115
  export interface EditorUpdate {
108
116
  renderElements: RenderElement[];
117
+ renderBlocks?: RenderElement[][];
118
+ renderPatch?: RenderBlocksPatch;
109
119
  selection: Selection;
110
120
  activeState: ActiveState;
111
121
  historyState: HistoryState;
122
+ documentVersion?: number;
123
+ }
124
+ export interface ContentSnapshot {
125
+ html: string;
126
+ json: DocumentJSON;
112
127
  }
113
128
  export interface DocumentJSON {
114
129
  [key: string]: unknown;
@@ -127,7 +142,7 @@ export interface CollaborationResult {
127
142
  }
128
143
  export type EncodedCollaborationStateInput = Uint8Array | readonly number[] | string;
129
144
  export declare function normalizeActiveState(raw: unknown): ActiveState;
130
- export declare function parseEditorUpdateJson(json: string): EditorUpdate | null;
145
+ export declare function parseEditorUpdateJson(json: string, previousRenderBlocks?: RenderElement[][]): EditorUpdate | null;
131
146
  export declare function encodeCollaborationStateBase64(encodedState: EncodedCollaborationStateInput): string;
132
147
  export declare function decodeCollaborationStateBase64(base64: string): Uint8Array;
133
148
  export declare function parseCollaborationResultJson(json: string): CollaborationResult;
@@ -137,6 +152,10 @@ export declare class NativeEditorBridge {
137
152
  private _editorId;
138
153
  private _destroyed;
139
154
  private _lastSelection;
155
+ private _documentVersion;
156
+ private _cachedHtml;
157
+ private _cachedJsonString;
158
+ private _renderBlocksCache;
140
159
  private constructor();
141
160
  /** Create a new editor instance backed by the Rust engine. */
142
161
  static create(config?: {
@@ -156,8 +175,14 @@ export declare class NativeEditorBridge {
156
175
  getHtml(): string;
157
176
  /** Set content from ProseMirror JSON. Returns render elements. */
158
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;
159
182
  /** Get content as ProseMirror JSON. */
160
183
  getJson(): DocumentJSON;
184
+ /** Get both HTML and JSON content in one native roundtrip. */
185
+ getContentSnapshot(): ContentSnapshot;
161
186
  /** Insert text at a document position. Returns the full update. */
162
187
  insertText(pos: number, text: string): EditorUpdate | null;
163
188
  /** Delete a range [from, to). Returns the full update. */
@@ -184,6 +209,8 @@ export declare class NativeEditorBridge {
184
209
  updateSelectionFromNative(anchor: number, head: number): void;
185
210
  /** Get the current full state from Rust (render elements, selection, etc.). */
186
211
  getCurrentState(): EditorUpdate | null;
212
+ /** Get the current selection-related state without render elements. */
213
+ getSelectionState(): EditorUpdate | null;
187
214
  /** Split the block at a position (Enter key). */
188
215
  splitBlock(pos: number): EditorUpdate | null;
189
216
  /** Insert HTML content at the current selection. */
@@ -196,6 +223,8 @@ export declare class NativeEditorBridge {
196
223
  replaceHtml(html: string): EditorUpdate | null;
197
224
  /** Replace entire document with JSON via transaction (preserves undo history). */
198
225
  replaceJson(doc: DocumentJSON): EditorUpdate | null;
226
+ /** Replace entire document with a serialized JSON transaction. */
227
+ replaceJsonString(jsonString: string): EditorUpdate | null;
199
228
  /** Undo the last operation. Returns update or null if nothing to undo. */
200
229
  undo(): EditorUpdate | null;
201
230
  /** Redo the last undone operation. Returns update or null if nothing to redo. */
@@ -214,6 +243,10 @@ export declare class NativeEditorBridge {
214
243
  outdentListItem(): EditorUpdate | null;
215
244
  /** Insert a void node (e.g. 'horizontalRule') at the current selection. */
216
245
  insertNode(nodeType: string): EditorUpdate | null;
246
+ parseUpdateJson(json: string): EditorUpdate | null;
247
+ private noteUpdate;
248
+ private parseAndNoteUpdate;
249
+ private invalidateContentCaches;
217
250
  private assertNotDestroyed;
218
251
  private currentScalarSelection;
219
252
  }
@@ -224,6 +257,7 @@ export declare class NativeCollaborationBridge {
224
257
  static create(config?: {
225
258
  clientId?: number;
226
259
  fragmentName?: string;
260
+ schema?: SchemaDefinition;
227
261
  initialDocumentJson?: DocumentJSON;
228
262
  initialEncodedState?: EncodedCollaborationStateInput;
229
263
  localAwareness?: Record<string, unknown>;
@@ -244,3 +278,4 @@ export declare class NativeCollaborationBridge {
244
278
  clearLocalAwareness(): CollaborationResult;
245
279
  private assertNotDestroyed;
246
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,7 @@ 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);
357
438
  }
358
439
  /** Toggle a heading level on the current block selection. */
359
440
  toggleHeading(level) {
@@ -365,10 +446,7 @@ class NativeEditorBridge {
365
446
  const json = scalarSelection
366
447
  ? getNativeModule().editorToggleHeadingAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head, level)
367
448
  : getNativeModule().editorToggleHeading(this._editorId, level);
368
- const update = parseEditorUpdateJson(json);
369
- if (update)
370
- this._lastSelection = update.selection;
371
- return update;
449
+ return this.parseAndNoteUpdate(json);
372
450
  }
373
451
  /** Set the document selection by anchor and head positions. */
374
452
  setSelection(anchor, head) {
@@ -402,82 +480,65 @@ class NativeEditorBridge {
402
480
  getCurrentState() {
403
481
  this.assertNotDestroyed();
404
482
  const json = getNativeModule().editorGetCurrentState(this._editorId);
405
- const update = parseEditorUpdateJson(json);
406
- if (update)
407
- this._lastSelection = update.selection;
408
- 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);
409
490
  }
410
491
  /** Split the block at a position (Enter key). */
411
492
  splitBlock(pos) {
412
493
  this.assertNotDestroyed();
413
494
  const json = getNativeModule().editorSplitBlock(this._editorId, pos);
414
- const update = parseEditorUpdateJson(json);
415
- if (update)
416
- this._lastSelection = update.selection;
417
- return update;
495
+ return this.parseAndNoteUpdate(json);
418
496
  }
419
497
  /** Insert HTML content at the current selection. */
420
498
  insertContentHtml(html) {
421
499
  this.assertNotDestroyed();
422
500
  const json = getNativeModule().editorInsertContentHtml(this._editorId, html);
423
- const update = parseEditorUpdateJson(json);
424
- if (update)
425
- this._lastSelection = update.selection;
426
- return update;
501
+ return this.parseAndNoteUpdate(json);
427
502
  }
428
503
  /** Insert JSON content at the current selection. */
429
504
  insertContentJson(doc) {
430
505
  this.assertNotDestroyed();
431
506
  const json = getNativeModule().editorInsertContentJson(this._editorId, JSON.stringify(doc));
432
- const update = parseEditorUpdateJson(json);
433
- if (update)
434
- this._lastSelection = update.selection;
435
- return update;
507
+ return this.parseAndNoteUpdate(json);
436
508
  }
437
509
  /** Insert JSON content at an explicit scalar selection. */
438
510
  insertContentJsonAtSelectionScalar(scalarAnchor, scalarHead, doc) {
439
511
  this.assertNotDestroyed();
440
512
  const json = getNativeModule().editorInsertContentJsonAtSelectionScalar(this._editorId, scalarAnchor, scalarHead, JSON.stringify(doc));
441
- const update = parseEditorUpdateJson(json);
442
- if (update)
443
- this._lastSelection = update.selection;
444
- return update;
513
+ return this.parseAndNoteUpdate(json);
445
514
  }
446
515
  /** Replace entire document with HTML via transaction (preserves undo history). */
447
516
  replaceHtml(html) {
448
517
  this.assertNotDestroyed();
449
518
  const json = getNativeModule().editorReplaceHtml(this._editorId, html);
450
- const update = parseEditorUpdateJson(json);
451
- if (update)
452
- this._lastSelection = update.selection;
453
- return update;
519
+ return this.parseAndNoteUpdate(json);
454
520
  }
455
521
  /** Replace entire document with JSON via transaction (preserves undo history). */
456
522
  replaceJson(doc) {
523
+ return this.replaceJsonString(JSON.stringify(doc));
524
+ }
525
+ /** Replace entire document with a serialized JSON transaction. */
526
+ replaceJsonString(jsonString) {
457
527
  this.assertNotDestroyed();
458
- const json = getNativeModule().editorReplaceJson(this._editorId, JSON.stringify(doc));
459
- const update = parseEditorUpdateJson(json);
460
- if (update)
461
- this._lastSelection = update.selection;
462
- return update;
528
+ const json = getNativeModule().editorReplaceJson(this._editorId, jsonString);
529
+ return this.parseAndNoteUpdate(json);
463
530
  }
464
531
  /** Undo the last operation. Returns update or null if nothing to undo. */
465
532
  undo() {
466
533
  this.assertNotDestroyed();
467
534
  const json = getNativeModule().editorUndo(this._editorId);
468
- const update = parseEditorUpdateJson(json);
469
- if (update)
470
- this._lastSelection = update.selection;
471
- return update;
535
+ return this.parseAndNoteUpdate(json);
472
536
  }
473
537
  /** Redo the last undone operation. Returns update or null if nothing to redo. */
474
538
  redo() {
475
539
  this.assertNotDestroyed();
476
540
  const json = getNativeModule().editorRedo(this._editorId);
477
- const update = parseEditorUpdateJson(json);
478
- if (update)
479
- this._lastSelection = update.selection;
480
- return update;
541
+ return this.parseAndNoteUpdate(json);
481
542
  }
482
543
  /** Check if undo is available. */
483
544
  canUndo() {
@@ -501,10 +562,7 @@ class NativeEditorBridge {
501
562
  : scalarSelection
502
563
  ? getNativeModule().editorWrapInListAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head, listType)
503
564
  : getNativeModule().editorWrapInList(this._editorId, listType);
504
- const update = parseEditorUpdateJson(json);
505
- if (update)
506
- this._lastSelection = update.selection;
507
- return update;
565
+ return this.parseAndNoteUpdate(json);
508
566
  }
509
567
  /** Unwrap the current list item back to a paragraph. */
510
568
  unwrapFromList() {
@@ -513,10 +571,7 @@ class NativeEditorBridge {
513
571
  const json = scalarSelection
514
572
  ? getNativeModule().editorUnwrapFromListAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head)
515
573
  : getNativeModule().editorUnwrapFromList(this._editorId);
516
- const update = parseEditorUpdateJson(json);
517
- if (update)
518
- this._lastSelection = update.selection;
519
- return update;
574
+ return this.parseAndNoteUpdate(json);
520
575
  }
521
576
  /** Indent the current list item into a nested list. */
522
577
  indentListItem() {
@@ -525,10 +580,7 @@ class NativeEditorBridge {
525
580
  const json = scalarSelection
526
581
  ? getNativeModule().editorIndentListItemAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head)
527
582
  : getNativeModule().editorIndentListItem(this._editorId);
528
- const update = parseEditorUpdateJson(json);
529
- if (update)
530
- this._lastSelection = update.selection;
531
- return update;
583
+ return this.parseAndNoteUpdate(json);
532
584
  }
533
585
  /** Outdent the current list item to the parent list level. */
534
586
  outdentListItem() {
@@ -537,10 +589,7 @@ class NativeEditorBridge {
537
589
  const json = scalarSelection
538
590
  ? getNativeModule().editorOutdentListItemAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head)
539
591
  : getNativeModule().editorOutdentListItem(this._editorId);
540
- const update = parseEditorUpdateJson(json);
541
- if (update)
542
- this._lastSelection = update.selection;
543
- return update;
592
+ return this.parseAndNoteUpdate(json);
544
593
  }
545
594
  /** Insert a void node (e.g. 'horizontalRule') at the current selection. */
546
595
  insertNode(nodeType) {
@@ -549,11 +598,41 @@ class NativeEditorBridge {
549
598
  const json = scalarSelection
550
599
  ? getNativeModule().editorInsertNodeAtSelectionScalar(this._editorId, scalarSelection.anchor, scalarSelection.head, nodeType)
551
600
  : getNativeModule().editorInsertNode(this._editorId, nodeType);
552
- const update = parseEditorUpdateJson(json);
553
- if (update)
554
- 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);
555
630
  return update;
556
631
  }
632
+ invalidateContentCaches() {
633
+ this._cachedHtml = null;
634
+ this._cachedJsonString = null;
635
+ }
557
636
  assertNotDestroyed() {
558
637
  if (this._destroyed) {
559
638
  throw new Error(ERR_DESTROYED);
@@ -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. */