@apollohg/react-native-prose-editor 0.5.19 → 0.5.21

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.
@@ -64,6 +64,25 @@ function isImageDataUrl(value) {
64
64
  function isRetryableNativeCommandBlock(reason) {
65
65
  return reason === 'composition' || (react_native_1.Platform.OS === 'android' && reason === 'pendingUpdate');
66
66
  }
67
+ function restoreSelectionInBridge(bridge, selection) {
68
+ if (selection.type === 'text') {
69
+ const { anchor, head } = selection;
70
+ if (anchor == null || head == null) {
71
+ return false;
72
+ }
73
+ bridge.setSelection(anchor, head);
74
+ return true;
75
+ }
76
+ if (selection.type === 'node') {
77
+ const { pos } = selection;
78
+ if (pos == null) {
79
+ return false;
80
+ }
81
+ bridge.setSelection(pos, pos);
82
+ return true;
83
+ }
84
+ return false;
85
+ }
67
86
  function isPromiseLike(value) {
68
87
  return (value != null &&
69
88
  typeof value === 'object' &&
@@ -543,23 +562,35 @@ function doesLiveMentionQueryConflictWithNativeSelectRequest(request, currentQue
543
562
  currentQuery.range.anchor !== request.range.anchor ||
544
563
  currentQuery.range.head !== request.range.head);
545
564
  }
546
- exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEditor({ initialContent, initialJSON, value, valueJSON, valueJSONRevision, schema, placeholder, editable = true, maxLength, autoFocus = false, autoCapitalize, autoCorrect, keyboardType, heightBehavior = 'autoGrow', showToolbar = true, toolbarPlacement = 'keyboard', toolbarItems = EditorToolbar_1.DEFAULT_EDITOR_TOOLBAR_ITEMS, onToolbarAction, onRequestLink, onRequestImage, autoDetectLinks = false, onContentChange, onContentChangeJSON, onSelectionChange, onActiveStateChange, onHistoryStateChange, onFocus, onBlur, style, containerStyle, theme, addons, remoteSelections, allowBase64Images = false, allowImageResizing = true, }, ref) {
565
+ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEditor({ initialContent, initialJSON, value, valueJSON, valueJSONRevision, valueJSONUpdateMode = 'replace', preserveSelectionOnValueJSONReset = false, selectionOnValueJSONReset, schema, placeholder, editable = true, maxLength, autoFocus = false, autoCapitalize, autoCorrect, keyboardType, heightBehavior = 'autoGrow', showToolbar = true, toolbarPlacement = 'keyboard', toolbarItems = EditorToolbar_1.DEFAULT_EDITOR_TOOLBAR_ITEMS, onToolbarAction, onRequestLink, onRequestImage, autoDetectLinks = false, onContentChange, onContentChangeJSON, onSelectionChange, onActiveStateChange, onHistoryStateChange, onFocus, onBlur, style, containerStyle, theme, addons, remoteSelections, allowBase64Images = false, allowImageResizing = true, }, ref) {
547
566
  const bridgeRef = (0, react_1.useRef)(null);
548
567
  const nativeViewRef = (0, react_1.useRef)(null);
549
568
  const [isReady, setIsReady] = (0, react_1.useState)(false);
550
569
  const [editorInstanceId, setEditorInstanceId] = (0, react_1.useState)(0);
570
+ const editorInstanceIdRef = (0, react_1.useRef)(0);
571
+ editorInstanceIdRef.current = editorInstanceId;
551
572
  const [isFocused, setIsFocused] = (0, react_1.useState)(false);
552
573
  const isFocusedRef = (0, react_1.useRef)(false);
553
574
  const [inlineToolbarFrame, setInlineToolbarFrame] = (0, react_1.useState)(null);
554
- const registeredToolbarFrames = (0, EditorToolbar_1.useEditorToolbarFrames)();
575
+ const registeredToolbarFrames = (0, EditorToolbar_1.useEditorToolbarFrames)(editorInstanceId);
555
576
  const [pendingNativeUpdate, setPendingNativeUpdate] = (0, react_1.useState)({
556
577
  json: undefined,
557
578
  editorId: undefined,
558
579
  revision: 0,
559
580
  });
560
- const pendingNativeUpdateRef = (0, react_1.useRef)(pendingNativeUpdate);
561
- pendingNativeUpdateRef.current = pendingNativeUpdate;
581
+ const [pendingNativeResetUpdate, setPendingNativeResetUpdate] = (0, react_1.useState)({
582
+ json: undefined,
583
+ editorId: undefined,
584
+ revision: 0,
585
+ });
562
586
  const pendingNativeUpdateInFlightRef = (0, react_1.useRef)(null);
587
+ const pendingNativeResetUpdateInFlightRef = (0, react_1.useRef)(null);
588
+ const nativeUpdateRevisionRef = (0, react_1.useRef)(0);
589
+ const nextNativeUpdateRevision = (0, react_1.useCallback)(() => {
590
+ const revision = nativeUpdateRevisionRef.current + 1;
591
+ nativeUpdateRevisionRef.current = revision;
592
+ return revision;
593
+ }, []);
563
594
  const [blockedNativeCommandRetry, setBlockedNativeCommandRetry] = (0, react_1.useState)(0);
564
595
  const [detachedControlledSyncRetry, setDetachedControlledSyncRetry] = (0, react_1.useState)(0);
565
596
  const [controlledNativeUpdateRetry, setControlledNativeUpdateRetry] = (0, react_1.useState)(0);
@@ -753,19 +784,26 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
753
784
  if (inFlight != null && inFlight.editorId === editorId) {
754
785
  pendingNativeUpdateInFlightRef.current = null;
755
786
  }
756
- setPendingNativeUpdate((current) => {
757
- if (current.json == null && current.editorId === editorId) {
758
- return current;
759
- }
760
- const next = {
761
- json: undefined,
762
- editorId,
763
- revision: current.revision + 1,
764
- };
765
- pendingNativeUpdateRef.current = next;
766
- return next;
767
- });
768
- }, []);
787
+ const next = {
788
+ json: undefined,
789
+ editorId,
790
+ revision: nextNativeUpdateRevision(),
791
+ };
792
+ setPendingNativeUpdate(next);
793
+ }, [nextNativeUpdateRevision]);
794
+ const queuePendingNativeResetUpdate = (0, react_1.useCallback)((updateJson) => {
795
+ if (react_native_1.Platform.OS !== 'android')
796
+ return;
797
+ const editorId = bridgeRef.current?.editorId;
798
+ const revision = nextNativeUpdateRevision();
799
+ const next = {
800
+ json: updateJson,
801
+ editorId,
802
+ revision,
803
+ };
804
+ pendingNativeResetUpdateInFlightRef.current = { editorId, revision };
805
+ setPendingNativeResetUpdate(next);
806
+ }, [nextNativeUpdateRevision]);
769
807
  const consumeBlockedCommandInfoForRetry = (0, react_1.useCallback)((bridge) => {
770
808
  const blockedInfo = bridge.consumeLastCommandBlockedInfo();
771
809
  if (!blockedInfo.blocked)
@@ -819,17 +857,14 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
819
857
  const updateJson = JSON.stringify(update);
820
858
  if (react_native_1.Platform.OS === 'android') {
821
859
  const editorId = bridgeRef.current?.editorId;
822
- setPendingNativeUpdate((current) => {
823
- const revision = current.revision + 1;
824
- const next = {
825
- json: updateJson,
826
- editorId,
827
- revision,
828
- };
829
- pendingNativeUpdateRef.current = next;
830
- pendingNativeUpdateInFlightRef.current = { editorId, revision };
831
- return next;
832
- });
860
+ const revision = nextNativeUpdateRevision();
861
+ const next = {
862
+ json: updateJson,
863
+ editorId,
864
+ revision,
865
+ };
866
+ pendingNativeUpdateInFlightRef.current = { editorId, revision };
867
+ setPendingNativeUpdate(next);
833
868
  }
834
869
  else {
835
870
  try {
@@ -853,7 +888,35 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
853
888
  }
854
889
  }
855
890
  return true;
856
- }, []);
891
+ }, [nextNativeUpdateRevision]);
892
+ const applyResetUpdateToNativeView = (0, react_1.useCallback)((update, previousDocumentVersion) => {
893
+ const updateJson = JSON.stringify(update);
894
+ if (react_native_1.Platform.OS === 'android') {
895
+ clearPendingNativeUpdateForCurrentEditor();
896
+ queuePendingNativeResetUpdate(updateJson);
897
+ const applyResetUpdate = nativeViewRef.current?.applyEditorResetUpdate;
898
+ if (applyResetUpdate) {
899
+ try {
900
+ const applyResult = applyResetUpdate(updateJson);
901
+ if (isPromiseLike(applyResult)) {
902
+ void applyResult.catch(() => {
903
+ // The native view may already be torn down during navigation.
904
+ });
905
+ }
906
+ return;
907
+ }
908
+ catch {
909
+ // Fall through to the regular prop-based apply path.
910
+ }
911
+ }
912
+ return;
913
+ }
914
+ applyUpdateToNativeView(update, previousDocumentVersion);
915
+ }, [
916
+ applyUpdateToNativeView,
917
+ clearPendingNativeUpdateForCurrentEditor,
918
+ queuePendingNativeResetUpdate,
919
+ ]);
857
920
  const maybeApplyAutoDetectedLink = (0, react_1.useCallback)((update, previousDocumentVersion) => {
858
921
  const applyAutoLink = (candidateUpdate, allowPreflightRetry) => {
859
922
  if (!autoDetectLinks ||
@@ -916,10 +979,10 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
916
979
  syncStateFromUpdate(update);
917
980
  onActiveStateChangeRef.current?.(update.activeState);
918
981
  onHistoryStateChangeRef.current?.(update.historyState);
982
+ onSelectionChangeRef.current?.(update.selection);
919
983
  if (!options?.suppressContentCallbacks) {
920
984
  emitContentCallbacksForUpdate(update, previousDocumentVersion);
921
985
  }
922
- onSelectionChangeRef.current?.(update.selection);
923
986
  return update;
924
987
  }, [
925
988
  applyUpdateToNativeView,
@@ -982,10 +1045,10 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
982
1045
  syncStateFromUpdate(update);
983
1046
  onActiveStateChangeRef.current?.(update.activeState);
984
1047
  onHistoryStateChangeRef.current?.(update.historyState);
1048
+ onSelectionChangeRef.current?.(update.selection);
985
1049
  if (!options?.suppressContentCallbacks) {
986
1050
  emitContentCallbacksForUpdate(update, previousDocumentVersion);
987
1051
  }
988
- onSelectionChangeRef.current?.(update.selection);
989
1052
  return update;
990
1053
  }, [
991
1054
  applyUpdateToNativeView,
@@ -1014,6 +1077,40 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
1014
1077
  return (options?.skipWhenContentChanged !== true ||
1015
1078
  !didContentChange(previousDocumentVersion, preflightUpdate));
1016
1079
  }, [consumeBlockedCommandForRetry, syncNativeUpdateFromBridge]);
1080
+ const resetContentJsonString = (0, react_1.useCallback)((jsonString, options) => {
1081
+ const bridge = bridgeRef.current;
1082
+ if (!bridge || bridge.isDestroyed)
1083
+ return null;
1084
+ const previousDocumentVersion = documentVersionRef.current;
1085
+ const preservedSelection = options?.preserveLiveTextSelection === true
1086
+ ? (options.selection ?? selectionRef.current)
1087
+ : null;
1088
+ bridge.setJsonString(jsonString);
1089
+ const update = bridge.getCurrentState();
1090
+ if (!update)
1091
+ return null;
1092
+ if (preservedSelection != null &&
1093
+ restoreSelectionInBridge(bridge, preservedSelection)) {
1094
+ const selectionState = bridge.getSelectionState();
1095
+ if (selectionState) {
1096
+ update.selection = selectionState.selection;
1097
+ update.activeState = selectionState.activeState;
1098
+ update.historyState = selectionState.historyState;
1099
+ if (typeof selectionState.documentVersion === 'number') {
1100
+ update.documentVersion = selectionState.documentVersion;
1101
+ }
1102
+ }
1103
+ }
1104
+ applyResetUpdateToNativeView(update, previousDocumentVersion);
1105
+ syncStateFromUpdate(update);
1106
+ onActiveStateChangeRef.current?.(update.activeState);
1107
+ onHistoryStateChangeRef.current?.(update.historyState);
1108
+ onSelectionChangeRef.current?.(update.selection);
1109
+ if (!options?.suppressContentCallbacks) {
1110
+ emitContentCallbacksForUpdate(update, previousDocumentVersion);
1111
+ }
1112
+ return update;
1113
+ }, [applyResetUpdateToNativeView, emitContentCallbacksForUpdate, syncStateFromUpdate]);
1017
1114
  const syncPreflightUpdateFromNativeEvent = (0, react_1.useCallback)((updateJson) => {
1018
1115
  if (typeof updateJson !== 'string' || updateJson.length === 0) {
1019
1116
  return true;
@@ -1104,6 +1201,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
1104
1201
  bridgeRef.current = null;
1105
1202
  }
1106
1203
  pendingNativeUpdateInFlightRef.current = null;
1204
+ pendingNativeResetUpdateInFlightRef.current = null;
1107
1205
  pendingDetachedControlledSyncRef.current = false;
1108
1206
  pendingControlledSyncAfterNativeUpdateRef.current = false;
1109
1207
  pendingBlockedNativeCommandRetryRef.current = false;
@@ -1113,15 +1211,18 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
1113
1211
  mentionQueryEditorIdRef.current = null;
1114
1212
  clearBlockedNativeCommandRetryTimer();
1115
1213
  setMentionQueryEvent(null);
1116
- setPendingNativeUpdate((current) => {
1117
- const next = {
1118
- json: undefined,
1119
- editorId: undefined,
1120
- revision: current.revision + 1,
1121
- };
1122
- pendingNativeUpdateRef.current = next;
1123
- return next;
1124
- });
1214
+ const clearedNativeUpdate = {
1215
+ json: undefined,
1216
+ editorId: undefined,
1217
+ revision: nextNativeUpdateRevision(),
1218
+ };
1219
+ setPendingNativeUpdate(clearedNativeUpdate);
1220
+ const clearedNativeResetUpdate = {
1221
+ json: undefined,
1222
+ editorId: undefined,
1223
+ revision: nextNativeUpdateRevision(),
1224
+ };
1225
+ setPendingNativeResetUpdate(clearedNativeResetUpdate);
1125
1226
  setEditorInstanceId(0);
1126
1227
  setIsReady(false);
1127
1228
  };
@@ -1132,6 +1233,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
1132
1233
  allowBase64Images,
1133
1234
  serializedSchemaJson,
1134
1235
  clearBlockedNativeCommandRetryTimer,
1236
+ nextNativeUpdateRevision,
1135
1237
  ]);
1136
1238
  (0, react_1.useEffect)(() => {
1137
1239
  if (value == null)
@@ -1203,6 +1305,27 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
1203
1305
  const didControlledValueChange = previousSync.value !== serializedValueJson;
1204
1306
  const didDocumentAdvanceForSameValue = !didControlledValueChange &&
1205
1307
  previousSync.documentVersion !== documentVersionRef.current;
1308
+ if (valueJSONUpdateMode === 'reset') {
1309
+ const currentJson = bridgeRef.current.getJsonString();
1310
+ if (currentJson === serializedValueJson) {
1311
+ clearPendingNativeUpdateForCurrentEditor();
1312
+ controlledJsonSyncRef.current = {
1313
+ value: serializedValueJson,
1314
+ documentVersion: documentVersionRef.current,
1315
+ };
1316
+ return;
1317
+ }
1318
+ resetContentJsonString(serializedValueJson, {
1319
+ suppressContentCallbacks: true,
1320
+ preserveLiveTextSelection: preserveSelectionOnValueJSONReset,
1321
+ selection: selectionOnValueJSONReset,
1322
+ });
1323
+ controlledJsonSyncRef.current = {
1324
+ value: serializedValueJson,
1325
+ documentVersion: documentVersionRef.current,
1326
+ };
1327
+ return;
1328
+ }
1206
1329
  if (!prepareBridgeForExternalContentRead({
1207
1330
  skipWhenContentChanged: !didControlledValueChange,
1208
1331
  })) {
@@ -1248,6 +1371,9 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
1248
1371
  }, [
1249
1372
  serializedValueJson,
1250
1373
  value,
1374
+ valueJSONUpdateMode,
1375
+ preserveSelectionOnValueJSONReset,
1376
+ selectionOnValueJSONReset,
1251
1377
  runAndApply,
1252
1378
  blockedNativeCommandRetry,
1253
1379
  controlledNativeUpdateRetry,
@@ -1255,6 +1381,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
1255
1381
  prepareBridgeForExternalContentRead,
1256
1382
  clearPendingNativeUpdateForCurrentEditor,
1257
1383
  hasPendingNativeUpdateInFlightForCurrentEditor,
1384
+ resetContentJsonString,
1258
1385
  ]);
1259
1386
  const updateToolbarFrame = (0, react_1.useCallback)(() => {
1260
1387
  const toolbar = toolbarRef.current;
@@ -1384,29 +1511,13 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
1384
1511
  }
1385
1512
  onSelectionChangeRef.current?.(nextSelection);
1386
1513
  }, [syncSelectionStateFromUpdate]);
1387
- const refocusAfterToolbarInteraction = (0, react_1.useCallback)(() => {
1388
- nativeViewRef.current?.focus?.();
1389
- requestAnimationFrame(() => {
1390
- nativeViewRef.current?.focus?.();
1391
- });
1392
- setTimeout(() => {
1393
- nativeViewRef.current?.focus?.();
1394
- }, 50);
1395
- }, []);
1396
1514
  const handleFocusChange = (0, react_1.useCallback)((event) => {
1397
1515
  if (!isCurrentNativeEditorEvent(event.nativeEvent, bridgeRef.current))
1398
1516
  return;
1399
1517
  const { isFocused: focused } = event.nativeEvent;
1400
- if (!focused &&
1401
- editable &&
1402
- isFocusedRef.current &&
1403
- (0, EditorToolbar_1.isEditorToolbarFocusPreservationActive)()) {
1404
- setIsFocused(true);
1405
- refocusAfterToolbarInteraction();
1406
- return;
1407
- }
1408
1518
  const wasFocused = isFocusedRef.current;
1409
1519
  isFocusedRef.current = focused;
1520
+ (0, EditorToolbar_1.setActiveEditorToolbarFrameOwnerForEditor)(editorInstanceIdRef.current, focused);
1410
1521
  setIsFocused(focused);
1411
1522
  if (!focused) {
1412
1523
  setMentionQueryEventState(null);
@@ -1419,7 +1530,10 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
1419
1530
  else if (wasFocused) {
1420
1531
  onBlurRef.current?.();
1421
1532
  }
1422
- }, [editable, refocusAfterToolbarInteraction, setMentionQueryEventState]);
1533
+ }, [setMentionQueryEventState]);
1534
+ (0, react_1.useEffect)(() => () => {
1535
+ (0, EditorToolbar_1.setActiveEditorToolbarFrameOwnerForEditor)(editorInstanceId, false);
1536
+ }, [editorInstanceId]);
1423
1537
  (0, react_1.useEffect)(() => {
1424
1538
  if (addons?.mentions != null) {
1425
1539
  return;
@@ -1443,30 +1557,57 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
1443
1557
  const editorId = bridgeRef.current?.editorId;
1444
1558
  const acknowledgedRevision = event.nativeEvent.editorUpdateRevision;
1445
1559
  const inFlight = pendingNativeUpdateInFlightRef.current;
1560
+ const resetInFlight = pendingNativeResetUpdateInFlightRef.current;
1446
1561
  let didClearInFlight = false;
1562
+ let didMatchInFlight = false;
1447
1563
  if (inFlight != null) {
1448
1564
  const matchesRevision = typeof acknowledgedRevision === 'number' &&
1449
1565
  inFlight.editorId === editorId &&
1450
1566
  inFlight.revision === acknowledgedRevision;
1451
- if (!matchesRevision) {
1452
- return;
1567
+ if (matchesRevision) {
1568
+ didMatchInFlight = true;
1569
+ pendingNativeUpdateInFlightRef.current = null;
1570
+ didClearInFlight = true;
1571
+ setPendingNativeUpdate((current) => {
1572
+ if (current.editorId !== editorId ||
1573
+ current.revision !== acknowledgedRevision ||
1574
+ current.json == null) {
1575
+ return current;
1576
+ }
1577
+ const next = {
1578
+ json: undefined,
1579
+ editorId,
1580
+ revision: current.revision,
1581
+ };
1582
+ return next;
1583
+ });
1453
1584
  }
1454
- pendingNativeUpdateInFlightRef.current = null;
1455
- didClearInFlight = true;
1456
- setPendingNativeUpdate((current) => {
1457
- if (current.editorId !== editorId ||
1458
- current.revision !== acknowledgedRevision ||
1459
- current.json == null) {
1460
- return current;
1461
- }
1462
- const next = {
1463
- json: undefined,
1464
- editorId,
1465
- revision: current.revision,
1466
- };
1467
- pendingNativeUpdateRef.current = next;
1468
- return next;
1469
- });
1585
+ }
1586
+ if (resetInFlight != null) {
1587
+ const matchesRevision = typeof acknowledgedRevision === 'number' &&
1588
+ resetInFlight.editorId === editorId &&
1589
+ resetInFlight.revision === acknowledgedRevision;
1590
+ if (matchesRevision) {
1591
+ didMatchInFlight = true;
1592
+ pendingNativeResetUpdateInFlightRef.current = null;
1593
+ didClearInFlight = true;
1594
+ setPendingNativeResetUpdate((current) => {
1595
+ if (current.editorId !== editorId ||
1596
+ current.revision !== acknowledgedRevision ||
1597
+ current.json == null) {
1598
+ return current;
1599
+ }
1600
+ const next = {
1601
+ json: undefined,
1602
+ editorId,
1603
+ revision: current.revision,
1604
+ };
1605
+ return next;
1606
+ });
1607
+ }
1608
+ }
1609
+ if ((inFlight != null || resetInFlight != null) && !didMatchInFlight) {
1610
+ return;
1470
1611
  }
1471
1612
  flushBlockedNativeCommandRetry();
1472
1613
  if (didClearInFlight && pendingControlledSyncAfterNativeUpdateRef.current) {
@@ -1623,6 +1764,24 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
1623
1764
  }
1624
1765
  return { update, queued: didQueueRetry };
1625
1766
  }, [isCommandRetryScopeCurrent, runAndApply]);
1767
+ const runPersistentContentCommand = (0, react_1.useCallback)((mutate, options) => {
1768
+ const requestedEditorId = bridgeRef.current?.editorId;
1769
+ let run = null;
1770
+ const retry = () => {
1771
+ const bridge = bridgeRef.current;
1772
+ if (bridge == null ||
1773
+ bridge.isDestroyed ||
1774
+ bridge.editorId !== requestedEditorId) {
1775
+ return false;
1776
+ }
1777
+ return run?.() != null;
1778
+ };
1779
+ run = () => runAndApply(mutate, {
1780
+ ...options,
1781
+ retryBlockedCommand: retry,
1782
+ });
1783
+ return run();
1784
+ }, [runAndApply]);
1626
1785
  const insertImage = (0, react_1.useCallback)((src, attrs, selection) => {
1627
1786
  const trimmedSrc = src.trim();
1628
1787
  if (!trimmedSrc)
@@ -2069,10 +2228,15 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
2069
2228
  runAndApply(() => bridgeRef.current?.insertContentJson(doc) ?? null);
2070
2229
  },
2071
2230
  setContent(html) {
2072
- runAndApply(() => bridgeRef.current?.replaceHtml(html) ?? null);
2231
+ runPersistentContentCommand(() => bridgeRef.current?.replaceHtml(html) ?? null);
2073
2232
  },
2074
2233
  setContentJson(doc) {
2075
- runAndApply(() => bridgeRef.current?.replaceJson(doc) ?? null);
2234
+ const jsonString = stringifyCachedJson((0, schemas_1.normalizeDocumentJson)(doc, documentSchema));
2235
+ runPersistentContentCommand(() => bridgeRef.current?.replaceJsonString(jsonString) ?? null);
2236
+ },
2237
+ clearContent() {
2238
+ const jsonString = stringifyCachedJson((0, schemas_1.normalizeDocumentJson)({ type: 'doc', content: [] }, documentSchema));
2239
+ resetContentJsonString(jsonString);
2076
2240
  },
2077
2241
  getContent() {
2078
2242
  if (!bridgeRef.current || bridgeRef.current.isDestroyed)
@@ -2121,7 +2285,14 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
2121
2285
  return false;
2122
2286
  return bridgeRef.current.canRedo();
2123
2287
  },
2124
- }), [insertImage, prepareBridgeForExternalContentRead, runAndApply]);
2288
+ }), [
2289
+ documentSchema,
2290
+ insertImage,
2291
+ prepareBridgeForExternalContentRead,
2292
+ runAndApply,
2293
+ runPersistentContentCommand,
2294
+ resetContentJsonString,
2295
+ ]);
2125
2296
  const activeMentionTrigger = mentionQueryEvent?.trigger || resolveMentionTrigger(addons);
2126
2297
  const activeMentionSuggestions = (0, react_1.useMemo)(() => isFocused && mentionQueryEvent != null && addons?.mentions != null
2127
2298
  ? filterMentionSuggestions(addons.mentions.suggestions ?? [], mentionQueryEvent.query, activeMentionTrigger)
@@ -2273,7 +2444,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
2273
2444
  nativeViewStyleParts.push({ height: autoGrowHeight });
2274
2445
  }
2275
2446
  const nativeViewStyle = nativeViewStyleParts.length <= 1 ? nativeViewStyleParts[0] : nativeViewStyleParts;
2276
- const toolbarFrameJson = serializeToolbarFrames(editable
2447
+ const toolbarFrameJson = serializeToolbarFrames(editable && isFocused
2277
2448
  ? [
2278
2449
  ...(toolbarPlacement === 'inline' && inlineToolbarFrame != null
2279
2450
  ? [inlineToolbarFrame]
@@ -2361,7 +2532,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
2361
2532
  }), onToggleStrike: () => runAndApply(() => bridgeRef.current?.toggleMark('strike') ?? null, {
2362
2533
  skipNativeApplyIfContentUnchanged: true,
2363
2534
  }), onToggleBulletList: () => runAndApply(() => bridgeRef.current?.toggleList('bulletList') ?? null), onToggleOrderedList: () => runAndApply(() => bridgeRef.current?.toggleList('orderedList') ?? null), onIndentList: () => runAndApply(() => bridgeRef.current?.indentListItem() ?? null), onOutdentList: () => runAndApply(() => bridgeRef.current?.outdentListItem() ?? null), onInsertHorizontalRule: () => runAndApply(() => bridgeRef.current?.insertNode('horizontalRule') ?? null), onInsertLineBreak: () => runAndApply(() => bridgeRef.current?.insertNode('hardBreak') ?? null), onUndo: () => runAndApply(() => bridgeRef.current?.undo() ?? null), onRedo: () => runAndApply(() => bridgeRef.current?.redo() ?? null) })) }));
2364
- return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.container, containerStyle], children: [(0, jsx_runtime_1.jsx)(NativeEditorView, { ref: nativeViewRef, style: nativeViewStyle, editorId: editorInstanceId, placeholder: placeholder, editable: editable, autoFocus: autoFocus, autoCapitalize: autoCapitalize, autoCorrect: autoCorrect, keyboardType: keyboardType, showToolbar: showToolbar, toolbarPlacement: toolbarPlacement, heightBehavior: heightBehavior, allowImageResizing: allowImageResizing, themeJson: themeJson, addonsJson: addonsJson, toolbarItemsJson: toolbarItemsJson, remoteSelectionsJson: remoteSelectionsJson, toolbarFrameJson: toolbarFrameJson, editorUpdateJson: pendingNativeUpdate.json, editorUpdateEditorId: pendingNativeUpdate.editorId, editorUpdateRevision: pendingNativeUpdate.revision, onEditorUpdate: handleUpdate, onSelectionChange: handleSelectionChange, onFocusChange: handleFocusChange, onContentHeightChange: handleContentHeightChange, ...(react_native_1.Platform.OS === 'android' ? { onEditorReady: handleEditorReady } : {}), onToolbarAction: handleToolbarAction, onAddonEvent: handleAddonEvent }, DEV_NATIVE_VIEW_KEY), shouldRenderJsToolbar && jsToolbar] }));
2535
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.container, containerStyle], children: [(0, jsx_runtime_1.jsx)(NativeEditorView, { ref: nativeViewRef, style: nativeViewStyle, editorId: editorInstanceId, placeholder: placeholder, editable: editable, autoFocus: autoFocus, autoCapitalize: autoCapitalize, autoCorrect: autoCorrect, keyboardType: keyboardType, showToolbar: showToolbar, toolbarPlacement: toolbarPlacement, heightBehavior: heightBehavior, allowImageResizing: allowImageResizing, themeJson: themeJson, addonsJson: addonsJson, toolbarItemsJson: toolbarItemsJson, remoteSelectionsJson: remoteSelectionsJson, toolbarFrameJson: toolbarFrameJson, editorUpdateJson: pendingNativeUpdate.json, editorUpdateEditorId: pendingNativeUpdate.editorId, editorUpdateRevision: pendingNativeUpdate.revision, editorResetUpdateJson: pendingNativeResetUpdate.json, editorResetUpdateEditorId: pendingNativeResetUpdate.editorId, editorResetUpdateRevision: pendingNativeResetUpdate.revision, onEditorUpdate: handleUpdate, onSelectionChange: handleSelectionChange, onFocusChange: handleFocusChange, onContentHeightChange: handleContentHeightChange, ...(react_native_1.Platform.OS === 'android' ? { onEditorReady: handleEditorReady } : {}), onToolbarAction: handleToolbarAction, onAddonEvent: handleAddonEvent }, DEV_NATIVE_VIEW_KEY), shouldRenderJsToolbar && jsToolbar] }));
2365
2536
  });
2366
2537
  const styles = react_native_1.StyleSheet.create({
2367
2538
  container: {
@@ -28,6 +28,8 @@ export interface YjsCollaborationState {
28
28
  status: YjsTransportStatus;
29
29
  isConnected: boolean;
30
30
  documentJson: DocumentJSON;
31
+ /** Local selection remapped through the latest collaboration document update. */
32
+ selectionOnValueJSONReset?: Selection;
31
33
  lastError?: Error;
32
34
  }
33
35
  export interface YjsCollaborationOptions {
@@ -74,6 +76,9 @@ export interface UseYjsCollaborationResult {
74
76
  updateLocalAwareness(partial: Partial<LocalAwarenessState>): void;
75
77
  editorBindings: {
76
78
  valueJSON: DocumentJSON;
79
+ valueJSONUpdateMode: 'reset';
80
+ preserveSelectionOnValueJSONReset: true;
81
+ selectionOnValueJSONReset?: Selection;
77
82
  remoteSelections: RemoteSelectionDecoration[];
78
83
  onContentChangeJSON: (doc: DocumentJSON) => void;
79
84
  onSelectionChange: (selection: Selection) => void;
@@ -177,6 +177,22 @@ function selectionToAwarenessRange(selection) {
177
177
  head: selection.head ?? selection.anchor ?? 0,
178
178
  };
179
179
  }
180
+ function selectionFromPeerState(state) {
181
+ if (!state || typeof state !== 'object')
182
+ return undefined;
183
+ const selection = state.selection;
184
+ if (!selection || typeof selection !== 'object')
185
+ return undefined;
186
+ const anchor = Number(selection.anchor);
187
+ const head = Number(selection.head);
188
+ if (!Number.isFinite(anchor) || !Number.isFinite(head))
189
+ return undefined;
190
+ return { type: 'text', anchor, head };
191
+ }
192
+ function localSelectionFromPeers(peers) {
193
+ const localPeer = peers.find((peer) => peer.isLocal);
194
+ return localPeer ? selectionFromPeerState(localPeer.state) : undefined;
195
+ }
180
196
  function peersToRemoteSelections(peers) {
181
197
  return peers.flatMap((peer) => {
182
198
  if (peer.isLocal || !peer.state || typeof peer.state !== 'object') {
@@ -240,17 +256,24 @@ class YjsCollaborationControllerImpl {
240
256
  localAwareness: awarenessToRecord(this.localAwarenessState),
241
257
  });
242
258
  const nativeDocumentJson = this.bridge.getDocumentJson();
259
+ this._peers = this.bridge.getPeers();
260
+ let initialDocumentJson;
261
+ if (options.initialDocumentJson != null) {
262
+ initialDocumentJson = cloneJsonValue(options.initialDocumentJson);
263
+ }
264
+ else if (shouldUseFallbackForNativeDocument(nativeDocumentJson, options)) {
265
+ initialDocumentJson = defaultEmptyDocument(options.schema);
266
+ }
267
+ else {
268
+ initialDocumentJson = nativeDocumentJson;
269
+ }
243
270
  this._state = {
244
271
  documentId: options.documentId,
245
272
  status: 'idle',
246
273
  isConnected: false,
247
- documentJson: options.initialDocumentJson != null
248
- ? cloneJsonValue(options.initialDocumentJson)
249
- : shouldUseFallbackForNativeDocument(nativeDocumentJson, options)
250
- ? defaultEmptyDocument(options.schema)
251
- : nativeDocumentJson,
274
+ documentJson: initialDocumentJson,
275
+ selectionOnValueJSONReset: localSelectionFromPeers(this._peers),
252
276
  };
253
- this._peers = this.bridge.getPeers();
254
277
  if (options.connect !== false) {
255
278
  this.connect();
256
279
  }
@@ -333,6 +356,9 @@ class YjsCollaborationControllerImpl {
333
356
  return;
334
357
  }
335
358
  try {
359
+ if (this.pendingAwarenessTimer != null) {
360
+ this.commitLocalAwareness();
361
+ }
336
362
  const result = this.bridge.handleMessage(bytes);
337
363
  this.applyResult(result);
338
364
  sendBinaryMessages(socket, result.messages);
@@ -438,6 +464,9 @@ class YjsCollaborationControllerImpl {
438
464
  const result = this.bridge.applyLocalDocumentJson(doc);
439
465
  this.applyResult(result);
440
466
  sendBinaryMessages(this.socket, result.messages);
467
+ if (this.pendingAwarenessTimer != null) {
468
+ this.commitLocalAwareness();
469
+ }
441
470
  }
442
471
  handleSelectionChange(selection) {
443
472
  if (this.destroyed)
@@ -466,13 +495,17 @@ class YjsCollaborationControllerImpl {
466
495
  this.commitLocalAwareness();
467
496
  }
468
497
  applyResult(result) {
498
+ const didPeersChange = result.peersChanged && result.peers != null;
499
+ if (didPeersChange) {
500
+ this._peers = result.peers;
501
+ }
469
502
  if (result.documentChanged && result.documentJson) {
470
503
  this.setState({
471
504
  documentJson: result.documentJson,
505
+ selectionOnValueJSONReset: localSelectionFromPeers(this._peers),
472
506
  });
473
507
  }
474
- if (result.peersChanged && result.peers) {
475
- this._peers = result.peers;
508
+ if (didPeersChange) {
476
509
  this.callbacks.onPeersChange?.(this._peers);
477
510
  }
478
511
  }
@@ -697,6 +730,9 @@ function useYjsCollaboration(options) {
697
730
  updateLocalAwareness: (partial) => controllerRef.current?.updateLocalAwareness(partial),
698
731
  editorBindings: {
699
732
  valueJSON: state.documentJson,
733
+ valueJSONUpdateMode: 'reset',
734
+ preserveSelectionOnValueJSONReset: true,
735
+ selectionOnValueJSONReset: state.selectionOnValueJSONReset,
700
736
  remoteSelections: peersToRemoteSelections(peers),
701
737
  onContentChangeJSON: (doc) => controllerRef.current?.handleLocalDocumentChange(doc),
702
738
  onSelectionChange: (selection) => controllerRef.current?.handleSelectionChange(selection),
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { NativeRichTextEditor, type NativeRichTextEditorProps, type NativeRichTextEditorRef, type NativeRichTextEditorCaretRect, type NativeRichTextEditorHeightBehavior, type NativeRichTextEditorToolbarPlacement, type NativeRichTextEditorAutoCapitalize, type NativeRichTextEditorKeyboardType, type RemoteSelectionDecoration, type LinkRequestContext, type ImageRequestContext, } from './NativeRichTextEditor';
1
+ export { NativeRichTextEditor, type NativeRichTextEditorProps, type NativeRichTextEditorRef, type NativeRichTextEditorCaretRect, type NativeRichTextEditorHeightBehavior, type NativeRichTextEditorToolbarPlacement, type NativeRichTextEditorValueJSONUpdateMode, type NativeRichTextEditorAutoCapitalize, type NativeRichTextEditorKeyboardType, type RemoteSelectionDecoration, type LinkRequestContext, type ImageRequestContext, } from './NativeRichTextEditor';
2
2
  export { NativeProseViewer, type NativeProseViewerProps, type NativeProseViewerAddons, type NativeProseViewerMentionsAddonConfig, type NativeProseViewerMentionPrefix, type NativeProseViewerLinkPressEvent, type NativeProseViewerMentionRenderContext, type NativeProseViewerMentionPressEvent, } from './NativeProseViewer';
3
3
  export { EditorToolbar, DEFAULT_EDITOR_TOOLBAR_ITEMS, type EditorToolbarProps, type EditorToolbarItem, type EditorToolbarLeafItem, type EditorToolbarGroupChildItem, type EditorToolbarGroupItem, type EditorToolbarGroupPresentation, type EditorToolbarIcon, type EditorToolbarDefaultIconId, type EditorToolbarSFSymbolIcon, type EditorToolbarMaterialIcon, type EditorToolbarCommand, type EditorToolbarHeadingLevel, type EditorToolbarListType, } from './EditorToolbar';
4
4
  export type { EditorContentInsets, EditorTheme, EditorTextStyle, EditorLinkTheme, EditorHeadingTheme, EditorListTheme, EditorHorizontalRuleTheme, EditorMentionTheme, EditorToolbarTheme, EditorToolbarAppearance, EditorFontStyle, EditorFontWeight, } from './EditorTheme';