@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
@@ -17,6 +17,43 @@ const DEV_NATIVE_VIEW_KEY = __DEV__
17
17
  : 'native-editor';
18
18
  const LINK_TOOLBAR_ACTION_KEY = '__native-editor-link__';
19
19
  const IMAGE_TOOLBAR_ACTION_KEY = '__native-editor-image__';
20
+ function mapToolbarChildForNative(item, activeState, editable, onRequestLink, onRequestImage) {
21
+ if (item.type === 'link') {
22
+ return {
23
+ type: 'action',
24
+ key: LINK_TOOLBAR_ACTION_KEY,
25
+ label: item.label,
26
+ icon: item.icon,
27
+ isActive: activeState.marks.link === true,
28
+ isDisabled: !editable || !onRequestLink || !activeState.allowedMarks.includes('link'),
29
+ };
30
+ }
31
+ if (item.type === 'image') {
32
+ return {
33
+ type: 'action',
34
+ key: IMAGE_TOOLBAR_ACTION_KEY,
35
+ label: item.label,
36
+ icon: item.icon,
37
+ isActive: false,
38
+ isDisabled: !editable || !onRequestImage || !activeState.insertableNodes.includes(schemas_2.IMAGE_NODE_NAME),
39
+ };
40
+ }
41
+ return item;
42
+ }
43
+ function mapToolbarItemsForNative(items, activeState, editable, onRequestLink, onRequestImage) {
44
+ return items.map((item) => {
45
+ if (item.type === 'group') {
46
+ return {
47
+ ...item,
48
+ items: item.items.map((child) => mapToolbarChildForNative(child, activeState, editable, onRequestLink, onRequestImage)),
49
+ };
50
+ }
51
+ if (item.type === 'separator') {
52
+ return item;
53
+ }
54
+ return mapToolbarChildForNative(item, activeState, editable, onRequestLink, onRequestImage);
55
+ });
56
+ }
20
57
  function isImageDataUrl(value) {
21
58
  return /^data:image\//i.test(value.trim());
22
59
  }
@@ -62,9 +99,43 @@ function serializeRemoteSelections(remoteSelections) {
62
99
  if (!remoteSelections || remoteSelections.length === 0) {
63
100
  return undefined;
64
101
  }
65
- return JSON.stringify(remoteSelections);
102
+ return stringifyCachedJson(remoteSelections);
66
103
  }
67
- exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEditor({ initialContent, initialJSON, value, valueJSON, schema, placeholder, editable = true, maxLength, autoFocus = false, heightBehavior = 'autoGrow', showToolbar = true, toolbarPlacement = 'keyboard', toolbarItems = EditorToolbar_1.DEFAULT_EDITOR_TOOLBAR_ITEMS, onToolbarAction, onRequestLink, onRequestImage, onContentChange, onContentChangeJSON, onSelectionChange, onActiveStateChange, onHistoryStateChange, onFocus, onBlur, style, containerStyle, theme, addons, remoteSelections, allowBase64Images = false, allowImageResizing = true, }, ref) {
104
+ const serializedJsonCache = new WeakMap();
105
+ function stringifyCachedJson(value) {
106
+ if (value != null && typeof value === 'object') {
107
+ const cached = serializedJsonCache.get(value);
108
+ if (cached != null) {
109
+ return cached;
110
+ }
111
+ const serialized = JSON.stringify(value);
112
+ serializedJsonCache.set(value, serialized);
113
+ return serialized;
114
+ }
115
+ return JSON.stringify(value);
116
+ }
117
+ function useSerializedValue(value, serialize, revision) {
118
+ const cacheRef = (0, react_1.useRef)(null);
119
+ const hasRevision = revision !== undefined;
120
+ const cached = cacheRef.current;
121
+ if (cached) {
122
+ if (hasRevision && cached.hasRevision && Object.is(cached.revision, revision)) {
123
+ return cached.serialized;
124
+ }
125
+ if (Object.is(cached.value, value) && cached.hasRevision === hasRevision) {
126
+ return cached.serialized;
127
+ }
128
+ }
129
+ const serialized = value == null ? undefined : serialize(value);
130
+ cacheRef.current = {
131
+ value,
132
+ revision,
133
+ hasRevision,
134
+ serialized,
135
+ };
136
+ return serialized;
137
+ }
138
+ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEditor({ initialContent, initialJSON, value, valueJSON, valueJSONRevision, schema, placeholder, editable = true, maxLength, autoFocus = false, heightBehavior = 'autoGrow', showToolbar = true, toolbarPlacement = 'keyboard', toolbarItems = EditorToolbar_1.DEFAULT_EDITOR_TOOLBAR_ITEMS, onToolbarAction, onRequestLink, onRequestImage, onContentChange, onContentChangeJSON, onSelectionChange, onActiveStateChange, onHistoryStateChange, onFocus, onBlur, style, containerStyle, theme, addons, remoteSelections, allowBase64Images = false, allowImageResizing = true, }, ref) {
68
139
  const bridgeRef = (0, react_1.useRef)(null);
69
140
  const nativeViewRef = (0, react_1.useRef)(null);
70
141
  const [isReady, setIsReady] = (0, react_1.useState)(false);
@@ -92,7 +163,9 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
92
163
  // Selection and rendered text length refs (non-rendering state)
93
164
  const selectionRef = (0, react_1.useRef)({ type: 'text', anchor: 0, head: 0 });
94
165
  const renderedTextLengthRef = (0, react_1.useRef)(0);
166
+ const documentVersionRef = (0, react_1.useRef)(null);
95
167
  const toolbarRef = (0, react_1.useRef)(null);
168
+ const toolbarItemsSerializationCacheRef = (0, react_1.useRef)(null);
96
169
  // Stable callback refs to avoid re-renders
97
170
  const onContentChangeRef = (0, react_1.useRef)(onContentChange);
98
171
  onContentChangeRef.current = onContentChange;
@@ -115,6 +188,12 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
115
188
  : undefined;
116
189
  const mentionSuggestionsByKeyRef = (0, react_1.useRef)(new Map());
117
190
  mentionSuggestionsByKeyRef.current = new Map((addons?.mentions?.suggestions ?? []).map((suggestion) => [suggestion.key, suggestion]));
191
+ const serializedSchemaJson = useSerializedValue(addons?.mentions != null ? (0, addons_1.withMentionsSchema)(schema ?? schemas_1.tiptapSchema) : schema, (nextSchema) => stringifyCachedJson(nextSchema));
192
+ const serializedInitialJson = useSerializedValue(initialJSON, stringifyCachedJson);
193
+ const serializedValueJson = useSerializedValue(valueJSON, stringifyCachedJson, valueJSONRevision);
194
+ const themeJson = useSerializedValue(theme, EditorTheme_1.serializeEditorTheme);
195
+ const addonsJson = useSerializedValue(addons, addons_1.serializeEditorAddons);
196
+ const remoteSelectionsJson = useSerializedValue(remoteSelections, (selections) => serializeRemoteSelections(selections));
118
197
  const syncStateFromUpdate = (0, react_1.useCallback)((update) => {
119
198
  if (!update)
120
199
  return;
@@ -122,6 +201,44 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
122
201
  setHistoryState(update.historyState);
123
202
  selectionRef.current = update.selection;
124
203
  renderedTextLengthRef.current = computeRenderedTextLength(update.renderElements);
204
+ if (typeof update.documentVersion === 'number') {
205
+ documentVersionRef.current = update.documentVersion;
206
+ }
207
+ }, []);
208
+ const syncSelectionStateFromUpdate = (0, react_1.useCallback)((update) => {
209
+ if (!update)
210
+ return;
211
+ setActiveState(update.activeState);
212
+ setHistoryState(update.historyState);
213
+ selectionRef.current = update.selection;
214
+ if (typeof update.documentVersion === 'number') {
215
+ documentVersionRef.current = update.documentVersion;
216
+ }
217
+ }, []);
218
+ const emitContentCallbacksForUpdate = (0, react_1.useCallback)((update, previousDocumentVersion) => {
219
+ if (!update || !bridgeRef.current || bridgeRef.current.isDestroyed)
220
+ return;
221
+ const wantsHtml = typeof onContentChangeRef.current === 'function';
222
+ const wantsJson = typeof onContentChangeJSONRef.current === 'function';
223
+ if (!wantsHtml && !wantsJson)
224
+ return;
225
+ if (previousDocumentVersion != null &&
226
+ typeof update.documentVersion === 'number' &&
227
+ update.documentVersion === previousDocumentVersion) {
228
+ return;
229
+ }
230
+ if (wantsHtml && wantsJson) {
231
+ const snapshot = bridgeRef.current.getContentSnapshot();
232
+ onContentChangeRef.current?.(snapshot.html);
233
+ onContentChangeJSONRef.current?.(snapshot.json);
234
+ return;
235
+ }
236
+ if (wantsHtml) {
237
+ onContentChangeRef.current?.(bridgeRef.current.getHtml());
238
+ }
239
+ if (wantsJson) {
240
+ onContentChangeJSONRef.current?.(bridgeRef.current.getJson());
241
+ }
125
242
  }, []);
126
243
  // Warn if both value and valueJSON are set
127
244
  if (__DEV__ && value != null && valueJSON != null) {
@@ -129,13 +246,8 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
129
246
  'Only value will be used.');
130
247
  }
131
248
  const runAndApply = (0, react_1.useCallback)((mutate, options) => {
249
+ const previousDocumentVersion = documentVersionRef.current;
132
250
  const preservedSelection = options?.preserveLiveTextSelection === true ? selectionRef.current : null;
133
- const shouldCheckForNoopNativeApply = options?.skipNativeApplyIfContentUnchanged === true &&
134
- bridgeRef.current != null &&
135
- !bridgeRef.current.isDestroyed;
136
- const htmlBefore = shouldCheckForNoopNativeApply
137
- ? bridgeRef.current.getHtml()
138
- : null;
139
251
  const update = mutate();
140
252
  if (!update)
141
253
  return null;
@@ -151,10 +263,10 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
151
263
  head: preservedSelection.head,
152
264
  };
153
265
  }
154
- const htmlAfter = shouldCheckForNoopNativeApply
155
- ? bridgeRef.current.getHtml()
156
- : null;
157
- if (!shouldCheckForNoopNativeApply || htmlBefore !== htmlAfter) {
266
+ const contentChanged = previousDocumentVersion == null ||
267
+ typeof update.documentVersion !== 'number' ||
268
+ update.documentVersion !== previousDocumentVersion;
269
+ if (!options?.skipNativeApplyIfContentUnchanged || contentChanged) {
158
270
  const updateJson = JSON.stringify(update);
159
271
  if (react_native_1.Platform.OS === 'android') {
160
272
  setPendingNativeUpdate((current) => ({
@@ -180,23 +292,18 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
180
292
  onActiveStateChangeRef.current?.(update.activeState);
181
293
  onHistoryStateChangeRef.current?.(update.historyState);
182
294
  if (!options?.suppressContentCallbacks) {
183
- if (onContentChangeRef.current && bridgeRef.current) {
184
- onContentChangeRef.current(bridgeRef.current.getHtml());
185
- }
186
- if (onContentChangeJSONRef.current && bridgeRef.current) {
187
- onContentChangeJSONRef.current(bridgeRef.current.getJson());
188
- }
295
+ emitContentCallbacksForUpdate(update, previousDocumentVersion);
189
296
  }
190
297
  onSelectionChangeRef.current?.(update.selection);
191
298
  return update;
192
- }, [syncStateFromUpdate]);
299
+ }, [emitContentCallbacksForUpdate, syncStateFromUpdate]);
193
300
  (0, react_1.useEffect)(() => {
194
- const effectiveSchema = addonsRef.current?.mentions != null
195
- ? (0, addons_1.withMentionsSchema)(schema ?? schemas_1.tiptapSchema)
196
- : schema;
197
- const schemaJson = effectiveSchema ? JSON.stringify(effectiveSchema) : undefined;
198
- const bridgeConfig = maxLength != null || schemaJson || allowBase64Images
199
- ? { maxLength, schemaJson, allowBase64Images }
301
+ const bridgeConfig = maxLength != null || serializedSchemaJson || allowBase64Images
302
+ ? {
303
+ maxLength,
304
+ schemaJson: serializedSchemaJson,
305
+ allowBase64Images,
306
+ }
200
307
  : undefined;
201
308
  const bridge = NativeEditorBridge_1.NativeEditorBridge.create(bridgeConfig);
202
309
  bridgeRef.current = bridge;
@@ -205,11 +312,11 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
205
312
  if (value != null) {
206
313
  bridge.setHtml(value);
207
314
  }
208
- else if (valueJSON != null) {
209
- bridge.setJson(valueJSON);
315
+ else if (serializedValueJson != null) {
316
+ bridge.setJsonString(serializedValueJson);
210
317
  }
211
- else if (initialJSON) {
212
- bridge.setJson(initialJSON);
318
+ else if (serializedInitialJson != null) {
319
+ bridge.setJsonString(serializedInitialJson);
213
320
  }
214
321
  else if (initialContent) {
215
322
  bridge.setHtml(initialContent);
@@ -224,7 +331,12 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
224
331
  setIsReady(false);
225
332
  };
226
333
  // eslint-disable-next-line react-hooks/exhaustive-deps
227
- }, [schema, maxLength, syncStateFromUpdate, Boolean(addons?.mentions), allowBase64Images]);
334
+ }, [
335
+ maxLength,
336
+ syncStateFromUpdate,
337
+ allowBase64Images,
338
+ serializedSchemaJson,
339
+ ]);
228
340
  (0, react_1.useEffect)(() => {
229
341
  if (value == null)
230
342
  return;
@@ -239,19 +351,18 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
239
351
  });
240
352
  }, [value, runAndApply]);
241
353
  (0, react_1.useEffect)(() => {
242
- if (valueJSON == null || value != null)
354
+ if (serializedValueJson == null || value != null)
243
355
  return;
244
356
  if (!bridgeRef.current || bridgeRef.current.isDestroyed)
245
357
  return;
246
- // No-op if JSON content is identical (avoids churning undo history)
247
- const currentJson = bridgeRef.current.getJson();
248
- if (JSON.stringify(currentJson) === JSON.stringify(valueJSON))
358
+ const currentJson = bridgeRef.current.getJsonString();
359
+ if (currentJson === serializedValueJson)
249
360
  return;
250
- runAndApply(() => bridgeRef.current.replaceJson(valueJSON), {
361
+ runAndApply(() => bridgeRef.current.replaceJsonString(serializedValueJson), {
251
362
  suppressContentCallbacks: true,
252
363
  preserveLiveTextSelection: true,
253
364
  });
254
- }, [valueJSON, value, runAndApply]);
365
+ }, [serializedValueJson, value, runAndApply]);
255
366
  const updateToolbarFrame = (0, react_1.useCallback)(() => {
256
367
  const toolbar = toolbarRef.current;
257
368
  if (!toolbar) {
@@ -286,28 +397,24 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
286
397
  if (!bridgeRef.current || bridgeRef.current.isDestroyed)
287
398
  return;
288
399
  try {
289
- const update = (0, NativeEditorBridge_1.parseEditorUpdateJson)(event.nativeEvent.updateJson);
400
+ const previousDocumentVersion = documentVersionRef.current;
401
+ const update = bridgeRef.current.parseUpdateJson(event.nativeEvent.updateJson);
290
402
  if (!update)
291
403
  return;
292
404
  syncStateFromUpdate(update);
293
405
  onActiveStateChangeRef.current?.(update.activeState);
294
406
  onHistoryStateChangeRef.current?.(update.historyState);
295
- if (onContentChangeRef.current) {
296
- onContentChangeRef.current(bridgeRef.current.getHtml());
297
- }
298
- if (onContentChangeJSONRef.current) {
299
- onContentChangeJSONRef.current(bridgeRef.current.getJson());
300
- }
407
+ emitContentCallbacksForUpdate(update, previousDocumentVersion);
301
408
  onSelectionChangeRef.current?.(update.selection);
302
409
  }
303
410
  catch {
304
411
  // Invalid JSON from native — skip
305
412
  }
306
- }, [syncStateFromUpdate]);
413
+ }, [emitContentCallbacksForUpdate, syncStateFromUpdate]);
307
414
  const handleSelectionChange = (0, react_1.useCallback)((event) => {
308
415
  if (!bridgeRef.current || bridgeRef.current.isDestroyed)
309
416
  return;
310
- const { anchor, head } = event.nativeEvent;
417
+ const { anchor, head, stateJson } = event.nativeEvent;
311
418
  let selection;
312
419
  if (anchor === 0 &&
313
420
  head >= renderedTextLengthRef.current &&
@@ -318,8 +425,19 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
318
425
  selection = { type: 'text', anchor, head };
319
426
  }
320
427
  bridgeRef.current.updateSelectionFromNative(anchor, head);
321
- const currentState = bridgeRef.current.getCurrentState();
322
- syncStateFromUpdate(currentState);
428
+ let currentState = null;
429
+ if (typeof stateJson === 'string' && stateJson.length > 0) {
430
+ try {
431
+ currentState = bridgeRef.current.parseUpdateJson(stateJson);
432
+ }
433
+ catch {
434
+ currentState = bridgeRef.current.getSelectionState();
435
+ }
436
+ }
437
+ else {
438
+ currentState = bridgeRef.current.getSelectionState();
439
+ }
440
+ syncSelectionStateFromUpdate(currentState);
323
441
  const nextSelection = selection.type === 'all' ? selection : (currentState?.selection ?? selection);
324
442
  selectionRef.current = nextSelection;
325
443
  if (currentState) {
@@ -327,7 +445,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
327
445
  onHistoryStateChangeRef.current?.(currentState.historyState);
328
446
  }
329
447
  onSelectionChangeRef.current?.(nextSelection);
330
- }, [syncStateFromUpdate]);
448
+ }, [syncSelectionStateFromUpdate]);
331
449
  const handleFocusChange = (0, react_1.useCallback)((event) => {
332
450
  const { isFocused: focused } = event.nativeEvent;
333
451
  setIsFocused(focused);
@@ -481,6 +599,9 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
481
599
  toggleBlockquote() {
482
600
  runAndApply(() => bridgeRef.current?.toggleBlockquote() ?? null);
483
601
  },
602
+ toggleHeading(level) {
603
+ runAndApply(() => bridgeRef.current?.toggleHeading(level) ?? null);
604
+ },
484
605
  toggleList(listType) {
485
606
  runAndApply(() => bridgeRef.current?.toggleList(listType) ?? null);
486
607
  },
@@ -545,35 +666,37 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
545
666
  }), [insertImage, runAndApply]);
546
667
  if (!isReady)
547
668
  return null;
548
- const toolbarItemsForNative = toolbarItems.map((item) => {
549
- if (item.type === 'link') {
550
- return {
551
- type: 'action',
552
- key: LINK_TOOLBAR_ACTION_KEY,
553
- label: item.label,
554
- icon: item.icon,
555
- isActive: activeState.marks.link === true,
556
- isDisabled: !editable || !onRequestLink || !activeState.allowedMarks.includes('link'),
557
- };
558
- }
559
- if (item.type === 'image') {
560
- return {
561
- type: 'action',
562
- key: IMAGE_TOOLBAR_ACTION_KEY,
563
- label: item.label,
564
- icon: item.icon,
565
- isActive: false,
566
- isDisabled: !editable
567
- || !onRequestImage
568
- || !activeState.insertableNodes.includes(schemas_2.IMAGE_NODE_NAME),
569
- };
570
- }
571
- return item;
572
- });
573
- const themeJson = (0, EditorTheme_1.serializeEditorTheme)(theme);
574
- const addonsJson = (0, addons_1.serializeEditorAddons)(addons);
575
- const toolbarItemsJson = JSON.stringify(toolbarItemsForNative);
576
- const remoteSelectionsJson = serializeRemoteSelections(remoteSelections);
669
+ const isLinkActive = activeState.marks.link === true;
670
+ const allowsLink = activeState.allowedMarks.includes('link');
671
+ const canInsertImage = activeState.insertableNodes.includes(schemas_2.IMAGE_NODE_NAME);
672
+ const canRequestLink = typeof onRequestLink === 'function';
673
+ const canRequestImage = typeof onRequestImage === 'function';
674
+ const cachedToolbarItems = toolbarItemsSerializationCacheRef.current;
675
+ let toolbarItemsJson;
676
+ if (cachedToolbarItems?.toolbarItems === toolbarItems &&
677
+ cachedToolbarItems.editable === editable &&
678
+ cachedToolbarItems.isLinkActive === isLinkActive &&
679
+ cachedToolbarItems.allowsLink === allowsLink &&
680
+ cachedToolbarItems.canRequestLink === canRequestLink &&
681
+ cachedToolbarItems.canRequestImage === canRequestImage &&
682
+ cachedToolbarItems.canInsertImage === canInsertImage) {
683
+ toolbarItemsJson = cachedToolbarItems.serialized;
684
+ }
685
+ else {
686
+ const mappedItems = mapToolbarItemsForNative(toolbarItems, activeState, editable, onRequestLink, onRequestImage);
687
+ toolbarItemsJson = stringifyCachedJson(mappedItems);
688
+ toolbarItemsSerializationCacheRef.current = {
689
+ toolbarItems,
690
+ editable,
691
+ isLinkActive,
692
+ allowsLink,
693
+ canRequestLink,
694
+ canRequestImage,
695
+ canInsertImage,
696
+ mappedItems,
697
+ serialized: toolbarItemsJson,
698
+ };
699
+ }
577
700
  const usesNativeKeyboardToolbar = toolbarPlacement === 'keyboard' && (react_native_1.Platform.OS === 'ios' || react_native_1.Platform.OS === 'android');
578
701
  const shouldRenderJsToolbar = !usesNativeKeyboardToolbar && showToolbar && editable;
579
702
  const inlineToolbarChrome = {
@@ -610,7 +733,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
610
733
  : null,
611
734
  ], onLayout: updateToolbarFrame, children: (0, jsx_runtime_1.jsx)(EditorToolbar_1.EditorToolbar, { activeState: activeState, historyState: historyState, toolbarItems: toolbarItems, theme: theme?.toolbar, showTopBorder: false, onToggleMark: (mark) => runAndApply(() => bridgeRef.current?.toggleMark(mark) ?? null, {
612
735
  skipNativeApplyIfContentUnchanged: true,
613
- }), onToggleListType: (listType) => runAndApply(() => bridgeRef.current?.toggleList(listType) ?? null), onToggleBlockquote: () => runAndApply(() => bridgeRef.current?.toggleBlockquote() ?? null), onInsertNodeType: (nodeType) => runAndApply(() => bridgeRef.current?.insertNode(nodeType) ?? null), onRunCommand: (command) => {
736
+ }), onToggleListType: (listType) => runAndApply(() => bridgeRef.current?.toggleList(listType) ?? null), onToggleHeading: (level) => runAndApply(() => bridgeRef.current?.toggleHeading(level) ?? null), onToggleBlockquote: () => runAndApply(() => bridgeRef.current?.toggleBlockquote() ?? null), onInsertNodeType: (nodeType) => runAndApply(() => bridgeRef.current?.insertNode(nodeType) ?? null), onRunCommand: (command) => {
614
737
  switch (command) {
615
738
  case 'indentList':
616
739
  runAndApply(() => bridgeRef.current?.indentListItem() ?? null);
@@ -1,5 +1,6 @@
1
1
  import { type CollaborationPeer, type DocumentJSON, type EncodedCollaborationStateInput, type Selection } from './NativeEditorBridge';
2
2
  import type { RemoteSelectionDecoration } from './NativeRichTextEditor';
3
+ import type { SchemaDefinition } from './schemas';
3
4
  export type YjsTransportStatus = 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error';
4
5
  export interface YjsRetryContext {
5
6
  attempt: number;
@@ -35,6 +36,7 @@ export interface YjsCollaborationOptions {
35
36
  connect?: boolean;
36
37
  retryIntervalMs?: YjsRetryInterval | false;
37
38
  fragmentName?: string;
39
+ schema?: SchemaDefinition;
38
40
  initialDocumentJson?: DocumentJSON;
39
41
  initialEncodedState?: EncodedCollaborationStateInput;
40
42
  localAwareness: LocalAwarenessUser;