@apollohg/react-native-prose-editor 0.5.2 → 0.5.4

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.
@@ -17,6 +17,7 @@ function getNativeProseViewerModule() {
17
17
  return nativeProseViewerModule;
18
18
  }
19
19
  const serializedJsonCache = new WeakMap();
20
+ const EMPTY_TEXT_BLOCK_PLACEHOLDER = '\u200B';
20
21
  function stringifyCachedJson(value) {
21
22
  if (value != null && typeof value === 'object') {
22
23
  const cached = serializedJsonCache.get(value);
@@ -158,6 +159,141 @@ function applyResolvedMentionRendering(renderJson, mentionPayloadsByDocPos) {
158
159
  });
159
160
  return didChange ? JSON.stringify(nextElements) : renderJson;
160
161
  }
162
+ function isTopLevelSingleElementBlock(element) {
163
+ return element.type === 'voidBlock' || element.type === 'opaqueBlockAtom';
164
+ }
165
+ function isEmptyParagraphPlaceholderText(text) {
166
+ if (text.length === 0) {
167
+ return false;
168
+ }
169
+ return Array.from(text).every((char) => char === EMPTY_TEXT_BLOCK_PLACEHOLDER);
170
+ }
171
+ function isCollapsibleEmptyParagraphText(text) {
172
+ return Array.from(text).every((char) => char === EMPTY_TEXT_BLOCK_PLACEHOLDER);
173
+ }
174
+ function renderElementsJsonContainsOnlyEmptyParagraphs(renderJson) {
175
+ let parsedElements;
176
+ try {
177
+ parsedElements = JSON.parse(renderJson);
178
+ }
179
+ catch {
180
+ return false;
181
+ }
182
+ if (!Array.isArray(parsedElements)) {
183
+ return false;
184
+ }
185
+ if (parsedElements.length === 0) {
186
+ return true;
187
+ }
188
+ let hasParagraph = false;
189
+ let paragraphIsOpen = false;
190
+ for (const element of parsedElements) {
191
+ if (element == null || typeof element !== 'object' || Array.isArray(element)) {
192
+ return false;
193
+ }
194
+ const renderElement = element;
195
+ switch (renderElement.type) {
196
+ case 'blockStart':
197
+ if (paragraphIsOpen ||
198
+ renderElement.nodeType !== 'paragraph' ||
199
+ renderElement.depth !== 0) {
200
+ return false;
201
+ }
202
+ paragraphIsOpen = true;
203
+ hasParagraph = true;
204
+ break;
205
+ case 'textRun':
206
+ if (!paragraphIsOpen ||
207
+ typeof renderElement.text !== 'string' ||
208
+ !isCollapsibleEmptyParagraphText(renderElement.text)) {
209
+ return false;
210
+ }
211
+ break;
212
+ case 'blockEnd':
213
+ if (!paragraphIsOpen) {
214
+ return false;
215
+ }
216
+ paragraphIsOpen = false;
217
+ break;
218
+ default:
219
+ return false;
220
+ }
221
+ }
222
+ return hasParagraph && !paragraphIsOpen;
223
+ }
224
+ function isTrailingEmptyParagraphRange(elements, start, endExclusive) {
225
+ const startElement = elements[start];
226
+ const endElement = elements[endExclusive - 1];
227
+ if (startElement?.type !== 'blockStart' ||
228
+ startElement.nodeType !== 'paragraph' ||
229
+ startElement.depth !== 0 ||
230
+ endElement?.type !== 'blockEnd') {
231
+ return false;
232
+ }
233
+ const innerElements = elements.slice(start + 1, endExclusive - 1);
234
+ return (innerElements.length > 0 &&
235
+ innerElements.every((element) => element.type === 'textRun' &&
236
+ typeof element.text === 'string' &&
237
+ isEmptyParagraphPlaceholderText(element.text)));
238
+ }
239
+ function collapseTrailingEmptyParagraphRenderElements(renderJson) {
240
+ let parsedElements;
241
+ try {
242
+ parsedElements = JSON.parse(renderJson);
243
+ }
244
+ catch {
245
+ return renderJson;
246
+ }
247
+ if (!Array.isArray(parsedElements)) {
248
+ return renderJson;
249
+ }
250
+ const elements = parsedElements;
251
+ const topLevelRanges = [];
252
+ for (let index = 0; index < elements.length; index += 1) {
253
+ const element = elements[index];
254
+ if (!element || typeof element !== 'object' || Array.isArray(element)) {
255
+ continue;
256
+ }
257
+ if (element.type === 'blockStart' && element.depth === 0) {
258
+ let nestingDepth = 1;
259
+ let cursor = index + 1;
260
+ while (cursor < elements.length && nestingDepth > 0) {
261
+ const current = elements[cursor];
262
+ if (current?.type === 'blockStart') {
263
+ nestingDepth += 1;
264
+ }
265
+ else if (current?.type === 'blockEnd') {
266
+ nestingDepth -= 1;
267
+ }
268
+ cursor += 1;
269
+ }
270
+ if (nestingDepth !== 0) {
271
+ return renderJson;
272
+ }
273
+ topLevelRanges.push({ start: index, endExclusive: cursor });
274
+ index = cursor - 1;
275
+ continue;
276
+ }
277
+ if (isTopLevelSingleElementBlock(element)) {
278
+ topLevelRanges.push({ start: index, endExclusive: index + 1 });
279
+ }
280
+ }
281
+ if (topLevelRanges.length <= 1) {
282
+ return renderJson;
283
+ }
284
+ let trimStart = null;
285
+ for (let rangeIndex = topLevelRanges.length - 1; rangeIndex >= 1; rangeIndex -= 1) {
286
+ const range = topLevelRanges[rangeIndex];
287
+ if (!isTrailingEmptyParagraphRange(elements, range.start, range.endExclusive)) {
288
+ break;
289
+ }
290
+ trimStart = range.start;
291
+ }
292
+ if (trimStart == null) {
293
+ return renderJson;
294
+ }
295
+ return JSON.stringify(elements.slice(0, trimStart));
296
+ }
161
297
  function serializeDocumentInput(document, schema) {
162
298
  if (typeof document === 'string') {
163
299
  try {
@@ -194,9 +330,21 @@ function extractRenderError(json) {
194
330
  return null;
195
331
  }
196
332
  }
197
- function NativeProseViewer({ contentJSON, contentJSONRevision, schema, theme, style, allowBase64Images = false, mentionPrefix, resolveMentionTheme, onPressMention, }) {
333
+ function NativeProseViewer({ ...props }) {
334
+ const { contentRevision, contentJSONRevision, schema, theme, style, allowBase64Images = false, collapseTrailingEmptyParagraphs = true, enableLinkTaps = true, mentionPrefix, resolveMentionTheme, onPressLink, onPressMention, } = props;
335
+ const contentJSON = 'contentJSON' in props ? props.contentJSON : undefined;
336
+ const contentHTML = 'contentHTML' in props ? props.contentHTML : undefined;
337
+ const resolvedContentRevision = contentRevision ?? contentJSONRevision;
198
338
  const documentSchema = (0, react_1.useMemo)(() => (0, addons_1.withMentionsSchema)(schema ?? schemas_1.tiptapSchema), [schema]);
199
- const { normalizedDocument, serializedContentJson } = (0, react_1.useMemo)(() => serializeDocumentInput(contentJSON, documentSchema), [contentJSON, contentJSONRevision, documentSchema]);
339
+ const { normalizedDocument, serializedContentJson } = (0, react_1.useMemo)(() => {
340
+ if (contentJSON === undefined) {
341
+ return {
342
+ normalizedDocument: null,
343
+ serializedContentJson: null,
344
+ };
345
+ }
346
+ return serializeDocumentInput(contentJSON, documentSchema);
347
+ }, [contentJSON, resolvedContentRevision, documentSchema]);
200
348
  const themeJson = (0, react_1.useMemo)(() => (0, EditorTheme_1.serializeEditorTheme)(theme), [theme]);
201
349
  const mentionPayloadsByDocPos = (0, react_1.useMemo)(() => normalizedDocument == null
202
350
  ? new Map()
@@ -206,32 +354,47 @@ function NativeProseViewer({ contentJSON, contentJSONRevision, schema, theme, st
206
354
  schema: documentSchema,
207
355
  ...(allowBase64Images ? { allowBase64Images } : {}),
208
356
  });
209
- const nextRenderJson = getNativeProseViewerModule().renderDocumentJson(configJson, serializedContentJson);
357
+ const nextRenderJson = serializedContentJson != null
358
+ ? getNativeProseViewerModule().renderDocumentJson(configJson, serializedContentJson)
359
+ : getNativeProseViewerModule().renderDocumentHtml(configJson, contentHTML ?? '');
210
360
  const renderError = extractRenderError(nextRenderJson);
211
361
  if (renderError != null) {
212
362
  console.error(`NativeProseViewer: ${renderError}`);
213
363
  return '[]';
214
364
  }
215
365
  if (looksLikeRenderElementsJson(nextRenderJson)) {
216
- return applyResolvedMentionRendering(nextRenderJson, mentionPayloadsByDocPos);
366
+ const collapsedRenderJson = collapseTrailingEmptyParagraphs
367
+ ? collapseTrailingEmptyParagraphRenderElements(nextRenderJson)
368
+ : nextRenderJson;
369
+ return applyResolvedMentionRendering(collapsedRenderJson, mentionPayloadsByDocPos);
217
370
  }
218
371
  console.error('NativeProseViewer: native renderDocumentJson returned an invalid payload.');
219
372
  return '[]';
220
373
  }, [
221
374
  allowBase64Images,
375
+ collapseTrailingEmptyParagraphs,
376
+ contentHTML,
222
377
  documentSchema,
223
378
  mentionPayloadsByDocPos,
224
379
  serializedContentJson,
225
380
  ]);
381
+ const renderJsonIsCollapsedEmpty = (0, react_1.useMemo)(() => collapseTrailingEmptyParagraphs &&
382
+ renderElementsJsonContainsOnlyEmptyParagraphs(renderJson), [collapseTrailingEmptyParagraphs, renderJson]);
226
383
  const [contentHeight, setContentHeight] = (0, react_1.useState)(null);
227
384
  const allowContentHeightShrinkRef = (0, react_1.useRef)(true);
228
385
  (0, react_1.useEffect)(() => {
229
386
  allowContentHeightShrinkRef.current = true;
230
- }, [contentJSONRevision, renderJson, themeJson]);
387
+ }, [resolvedContentRevision, renderJson, themeJson]);
231
388
  const handleContentHeightChange = (0, react_1.useCallback)((event) => {
232
389
  const nextHeight = event.nativeEvent.contentHeight;
233
- if (nextHeight <= 0)
390
+ if (nextHeight < 0)
234
391
  return;
392
+ if (nextHeight === 0 && !renderJsonIsCollapsedEmpty)
393
+ return;
394
+ if (nextHeight === 0) {
395
+ setContentHeight((currentHeight) => currentHeight === 0 ? currentHeight : 0);
396
+ return;
397
+ }
235
398
  setContentHeight((currentHeight) => currentHeight == null ||
236
399
  nextHeight >= currentHeight ||
237
400
  allowContentHeightShrinkRef.current
@@ -242,7 +405,7 @@ function NativeProseViewer({ contentJSON, contentJSONRevision, schema, theme, st
242
405
  : nextHeight;
243
406
  })()
244
407
  : currentHeight);
245
- }, []);
408
+ }, [renderJsonIsCollapsedEmpty]);
246
409
  const handlePressMention = (0, react_1.useCallback)((event) => {
247
410
  if (!onPressMention)
248
411
  return;
@@ -254,10 +417,27 @@ function NativeProseViewer({ contentJSON, contentJSONRevision, schema, theme, st
254
417
  attrs: resolvedMention?.attrs ?? {},
255
418
  });
256
419
  }, [mentionPayloadsByDocPos, onPressMention]);
257
- const nativeStyle = (0, react_1.useMemo)(() => [
258
- { minHeight: 1 },
259
- style,
260
- contentHeight != null ? { minHeight: contentHeight } : null,
261
- ], [contentHeight, style]);
262
- return ((0, jsx_runtime_1.jsx)(NativeProseViewerView, { style: nativeStyle, renderJson: renderJson, themeJson: themeJson, onContentHeightChange: handleContentHeightChange, onPressMention: typeof onPressMention === 'function' ? handlePressMention : undefined }));
420
+ const handlePressLink = (0, react_1.useCallback)((event) => {
421
+ if (!onPressLink)
422
+ return;
423
+ onPressLink({
424
+ href: event.nativeEvent.href,
425
+ text: event.nativeEvent.text,
426
+ });
427
+ }, [onPressLink]);
428
+ const nativeStyle = (0, react_1.useMemo)(() => {
429
+ let measuredStyle = null;
430
+ if (renderJsonIsCollapsedEmpty) {
431
+ measuredStyle = { height: 0, minHeight: 0 };
432
+ }
433
+ else if (contentHeight != null && contentHeight > 0) {
434
+ measuredStyle = { minHeight: contentHeight };
435
+ }
436
+ return [
437
+ { minHeight: renderJsonIsCollapsedEmpty ? 0 : 1 },
438
+ style,
439
+ measuredStyle,
440
+ ];
441
+ }, [contentHeight, renderJsonIsCollapsedEmpty, style]);
442
+ return ((0, jsx_runtime_1.jsx)(NativeProseViewerView, { style: nativeStyle, renderJson: renderJson, themeJson: themeJson, collapsesWhenEmpty: collapseTrailingEmptyParagraphs, enableLinkTaps: enableLinkTaps, interceptLinkTaps: typeof onPressLink === 'function', onContentHeightChange: handleContentHeightChange, onPressLink: typeof onPressLink === 'function' ? handlePressLink : undefined, onPressMention: typeof onPressMention === 'function' ? handlePressMention : undefined }));
263
443
  }
@@ -16,6 +16,9 @@ const DEV_NATIVE_VIEW_KEY = __DEV__
16
16
  : 'native-editor';
17
17
  const LINK_TOOLBAR_ACTION_KEY = '__native-editor-link__';
18
18
  const IMAGE_TOOLBAR_ACTION_KEY = '__native-editor-image__';
19
+ const DEFAULT_MENTION_TRIGGER = '@';
20
+ const MAX_INLINE_MENTION_SUGGESTIONS = 8;
21
+ const INLINE_TOOLBAR_BORDER_COLOR = '#E5E5EA';
19
22
  function mapToolbarChildForNative(item, activeState, editable, onRequestLink, onRequestImage) {
20
23
  if (item.type === 'link') {
21
24
  return {
@@ -34,7 +37,9 @@ function mapToolbarChildForNative(item, activeState, editable, onRequestLink, on
34
37
  label: item.label,
35
38
  icon: item.icon,
36
39
  isActive: false,
37
- isDisabled: !editable || !onRequestImage || !activeState.insertableNodes.includes(schemas_1.IMAGE_NODE_NAME),
40
+ isDisabled: !editable ||
41
+ !onRequestImage ||
42
+ !activeState.insertableNodes.includes(schemas_1.IMAGE_NODE_NAME),
38
43
  };
39
44
  }
40
45
  return item;
@@ -65,6 +70,34 @@ function isPromiseLike(value) {
65
70
  function isRecord(value) {
66
71
  return value != null && typeof value === 'object' && !Array.isArray(value);
67
72
  }
73
+ function resolveMentionTrigger(addons) {
74
+ return addons?.mentions?.trigger?.trim() || DEFAULT_MENTION_TRIGGER;
75
+ }
76
+ function resolveMentionSuggestionLabel(suggestion, trigger) {
77
+ return suggestion.label?.trim() || `${trigger}${suggestion.title}`;
78
+ }
79
+ function filterMentionSuggestions(suggestions, query, trigger) {
80
+ const normalizedQuery = query.trim().toLowerCase();
81
+ const filtered = normalizedQuery.length === 0
82
+ ? suggestions
83
+ : suggestions.filter((suggestion) => {
84
+ const label = resolveMentionSuggestionLabel(suggestion, trigger);
85
+ return (suggestion.title.toLowerCase().includes(normalizedQuery) ||
86
+ label.toLowerCase().includes(normalizedQuery) ||
87
+ suggestion.subtitle?.toLowerCase().includes(normalizedQuery) === true);
88
+ });
89
+ return filtered.slice(0, MAX_INLINE_MENTION_SUGGESTIONS);
90
+ }
91
+ function resolveMentionSuggestionAttrs(suggestion, trigger) {
92
+ const attrs = { ...(suggestion.attrs ?? {}) };
93
+ if (!('label' in attrs)) {
94
+ attrs.label = resolveMentionSuggestionLabel(suggestion, trigger);
95
+ }
96
+ if (!('mentionSuggestionChar' in attrs)) {
97
+ attrs.mentionSuggestionChar = trigger;
98
+ }
99
+ return attrs;
100
+ }
68
101
  const AUTO_LINK_URL_REGEX = /(?:https?:\/\/|www\.)\S+/giu;
69
102
  const AUTO_LINK_INLINE_PLACEHOLDER = '\uFFFC';
70
103
  const AUTO_LINK_LEADING_BOUNDARY_CHARS = new Set(['(', '[', '{', '<', '"', "'"]);
@@ -142,7 +175,8 @@ function trimAutoLinkTrailingPunctuation(value) {
142
175
  result = chars.join('');
143
176
  continue;
144
177
  }
145
- if ((lastChar === '"' || lastChar === "'") && countOccurrences(result, lastChar) % 2 !== 0) {
178
+ if ((lastChar === '"' || lastChar === "'") &&
179
+ countOccurrences(result, lastChar) % 2 !== 0) {
146
180
  chars.pop();
147
181
  result = chars.join('');
148
182
  continue;
@@ -169,7 +203,9 @@ function isAutoLinkBoundaryChar(char) {
169
203
  if (!char) {
170
204
  return true;
171
205
  }
172
- return /\s/u.test(char) || char === AUTO_LINK_INLINE_PLACEHOLDER || AUTO_LINK_LEADING_BOUNDARY_CHARS.has(char);
206
+ return (/\s/u.test(char) ||
207
+ char === AUTO_LINK_INLINE_PLACEHOLDER ||
208
+ AUTO_LINK_LEADING_BOUNDARY_CHARS.has(char));
173
209
  }
174
210
  function isAutoLinkTrailingDelimiterChar(char) {
175
211
  if (!char) {
@@ -236,13 +272,15 @@ function findAutoLinkCandidateInInlineBlock(block, cursorDocPos) {
236
272
  return null;
237
273
  }
238
274
  let localIndex = 0;
239
- while (localIndex < block.docPositions.length && block.docPositions[localIndex] < cursorDocPos) {
275
+ while (localIndex < block.docPositions.length &&
276
+ block.docPositions[localIndex] < cursorDocPos) {
240
277
  localIndex += 1;
241
278
  }
242
279
  if (localIndex === 0) {
243
280
  return null;
244
281
  }
245
- if (cursorDocPos < block.contentEnd && !isAutoLinkTrailingDelimiterChar(block.chars[localIndex - 1])) {
282
+ if (cursorDocPos < block.contentEnd &&
283
+ !isAutoLinkTrailingDelimiterChar(block.chars[localIndex - 1])) {
246
284
  return null;
247
285
  }
248
286
  const prefixChars = block.chars.slice(0, localIndex);
@@ -444,11 +482,14 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
444
482
  canUndo: false,
445
483
  canRedo: false,
446
484
  });
485
+ const [mentionQueryEvent, setMentionQueryEvent] = (0, react_1.useState)(null);
447
486
  // Selection and rendered text length refs (non-rendering state)
448
487
  const selectionRef = (0, react_1.useRef)({ type: 'text', anchor: 0, head: 0 });
449
488
  const renderedTextLengthRef = (0, react_1.useRef)(0);
450
489
  const documentVersionRef = (0, react_1.useRef)(null);
451
490
  const toolbarRef = (0, react_1.useRef)(null);
491
+ const mentionQueryEventRef = (0, react_1.useRef)(null);
492
+ mentionQueryEventRef.current = mentionQueryEvent;
452
493
  const toolbarItemsSerializationCacheRef = (0, react_1.useRef)(null);
453
494
  // Stable callback refs to avoid re-renders
454
495
  const onContentChangeRef = (0, react_1.useRef)(onContentChange);
@@ -672,12 +713,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
672
713
  setIsReady(false);
673
714
  };
674
715
  // eslint-disable-next-line react-hooks/exhaustive-deps
675
- }, [
676
- maxLength,
677
- syncStateFromUpdate,
678
- allowBase64Images,
679
- serializedSchemaJson,
680
- ]);
716
+ }, [maxLength, syncStateFromUpdate, allowBase64Images, serializedSchemaJson]);
681
717
  (0, react_1.useEffect)(() => {
682
718
  if (value == null)
683
719
  return;
@@ -803,6 +839,9 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
803
839
  const handleFocusChange = (0, react_1.useCallback)((event) => {
804
840
  const { isFocused: focused } = event.nativeEvent;
805
841
  setIsFocused(focused);
842
+ if (!focused) {
843
+ setMentionQueryEvent(null);
844
+ }
806
845
  if (focused) {
807
846
  onFocusRef.current?.();
808
847
  }
@@ -810,6 +849,12 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
810
849
  onBlurRef.current?.();
811
850
  }
812
851
  }, []);
852
+ (0, react_1.useEffect)(() => {
853
+ if (addons?.mentions != null) {
854
+ return;
855
+ }
856
+ setMentionQueryEvent(null);
857
+ }, [addons?.mentions]);
813
858
  const handleContentHeightChange = (0, react_1.useCallback)((event) => {
814
859
  if (heightBehavior !== 'autoGrow')
815
860
  return;
@@ -897,6 +942,46 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
897
942
  }
898
943
  onToolbarAction?.(event.nativeEvent.key);
899
944
  }, [onToolbarAction, openImageRequest, openLinkRequest]);
945
+ const resolveMentionSelectionAttrs = (0, react_1.useCallback)((selectionEvent) => {
946
+ let resolvedAttrs;
947
+ try {
948
+ resolvedAttrs =
949
+ addonsRef.current?.mentions?.resolveSelectionAttrs?.(selectionEvent);
950
+ }
951
+ catch (error) {
952
+ if (__DEV__) {
953
+ console.error('NativeRichTextEditor: mentions.resolveSelectionAttrs threw', error);
954
+ }
955
+ }
956
+ return isRecord(resolvedAttrs)
957
+ ? { ...selectionEvent.attrs, ...resolvedAttrs }
958
+ : selectionEvent.attrs;
959
+ }, []);
960
+ const handleInlineMentionSuggestionPress = (0, react_1.useCallback)((suggestion) => {
961
+ const mentionQuery = mentionQueryEventRef.current;
962
+ const mentions = addonsRef.current?.mentions;
963
+ if (!mentionQuery ||
964
+ !mentions ||
965
+ !bridgeRef.current ||
966
+ bridgeRef.current.isDestroyed) {
967
+ return;
968
+ }
969
+ const attrs = resolveMentionSelectionAttrs({
970
+ trigger: mentionQuery.trigger,
971
+ suggestion,
972
+ attrs: resolveMentionSuggestionAttrs(suggestion, mentionQuery.trigger),
973
+ range: mentionQuery.range,
974
+ });
975
+ const update = runAndApply(() => bridgeRef.current?.insertContentJsonAtSelectionScalar(mentionQuery.range.anchor, mentionQuery.range.head, (0, addons_1.buildMentionFragmentJson)(attrs)) ?? null);
976
+ if (update) {
977
+ setMentionQueryEvent(null);
978
+ mentions.onSelect?.({
979
+ trigger: mentionQuery.trigger,
980
+ suggestion,
981
+ attrs,
982
+ });
983
+ }
984
+ }, [resolveMentionSelectionAttrs, runAndApply]);
900
985
  const handleAddonEvent = (0, react_1.useCallback)((event) => {
901
986
  let parsed = null;
902
987
  try {
@@ -908,11 +993,18 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
908
993
  if (!parsed)
909
994
  return;
910
995
  if (parsed.type === 'mentionsQueryChange') {
911
- addonsRef.current?.mentions?.onQueryChange?.({
996
+ const nextEvent = {
912
997
  query: parsed.query,
913
998
  trigger: parsed.trigger,
914
999
  range: parsed.range,
915
1000
  isActive: parsed.isActive,
1001
+ };
1002
+ setMentionQueryEvent(parsed.isActive ? nextEvent : null);
1003
+ addonsRef.current?.mentions?.onQueryChange?.({
1004
+ query: nextEvent.query,
1005
+ trigger: nextEvent.trigger,
1006
+ range: nextEvent.range,
1007
+ isActive: nextEvent.isActive,
916
1008
  });
917
1009
  return;
918
1010
  }
@@ -926,19 +1018,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
926
1018
  attrs: parsed.attrs,
927
1019
  range: parsed.range,
928
1020
  };
929
- let resolvedAttrs;
930
- try {
931
- resolvedAttrs =
932
- addonsRef.current?.mentions?.resolveSelectionAttrs?.(selectionEvent);
933
- }
934
- catch (error) {
935
- if (__DEV__) {
936
- console.error('NativeRichTextEditor: mentions.resolveSelectionAttrs threw', error);
937
- }
938
- }
939
- const finalAttrs = isRecord(resolvedAttrs)
940
- ? { ...parsed.attrs, ...resolvedAttrs }
941
- : parsed.attrs;
1021
+ const finalAttrs = resolveMentionSelectionAttrs(selectionEvent);
942
1022
  const update = runAndApply(() => bridgeRef.current?.insertContentJsonAtSelectionScalar(parsed.range.anchor, parsed.range.head, (0, addons_1.buildMentionFragmentJson)(finalAttrs)) ?? null);
943
1023
  if (update) {
944
1024
  addonsRef.current?.mentions?.onSelect?.({
@@ -959,7 +1039,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
959
1039
  attrs: parsed.attrs,
960
1040
  });
961
1041
  }
962
- }, [runAndApply]);
1042
+ }, [resolveMentionSelectionAttrs, runAndApply]);
963
1043
  (0, react_1.useImperativeHandle)(ref, () => ({
964
1044
  focus() {
965
1045
  nativeViewRef.current?.focus?.();
@@ -1092,6 +1172,25 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
1092
1172
  borderWidth: theme?.toolbar?.borderWidth,
1093
1173
  borderRadius: theme?.toolbar?.borderRadius,
1094
1174
  };
1175
+ const inlineToolbarMarginTop = theme?.toolbar?.marginTop ?? 8;
1176
+ const inlineToolbarShowTopBorder = theme?.toolbar?.showTopBorder ?? false;
1177
+ const inlineToolbarMentionTheme = theme?.mentions ?? addons?.mentions?.theme;
1178
+ const inlineToolbarContentTopBorderStyle = inlineToolbarShowTopBorder
1179
+ ? {
1180
+ borderTopWidth: theme?.toolbar?.borderWidth ?? react_native_1.StyleSheet.hairlineWidth,
1181
+ borderTopColor: theme?.toolbar?.borderColor ?? INLINE_TOOLBAR_BORDER_COLOR,
1182
+ }
1183
+ : null;
1184
+ const inlineMentionSuggestions = toolbarPlacement === 'inline' &&
1185
+ isFocused &&
1186
+ mentionQueryEvent != null &&
1187
+ addons?.mentions != null
1188
+ ? filterMentionSuggestions(addons.mentions.suggestions ?? [], mentionQueryEvent.query, mentionQueryEvent.trigger || resolveMentionTrigger(addons))
1189
+ : [];
1190
+ const shouldShowInlineMentionSuggestions = shouldRenderJsToolbar &&
1191
+ toolbarPlacement === 'inline' &&
1192
+ isFocused &&
1193
+ inlineMentionSuggestions.length > 0;
1095
1194
  const containerMinHeight = react_native_1.StyleSheet.flatten(containerStyle)?.minHeight;
1096
1195
  const nativeViewStyleParts = [];
1097
1196
  if (containerMinHeight != null) {
@@ -1106,6 +1205,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
1106
1205
  const nativeViewStyle = nativeViewStyleParts.length <= 1 ? nativeViewStyleParts[0] : nativeViewStyleParts;
1107
1206
  const jsToolbar = ((0, jsx_runtime_1.jsx)(react_native_1.View, { ref: toolbarRef, testID: 'native-editor-js-toolbar', style: [
1108
1207
  styles.inlineToolbar,
1208
+ { marginTop: inlineToolbarMarginTop },
1109
1209
  inlineToolbarChrome.backgroundColor != null
1110
1210
  ? { backgroundColor: inlineToolbarChrome.backgroundColor }
1111
1211
  : null,
@@ -1118,7 +1218,43 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
1118
1218
  inlineToolbarChrome.borderRadius != null
1119
1219
  ? { borderRadius: inlineToolbarChrome.borderRadius }
1120
1220
  : null,
1121
- ], 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, {
1221
+ ], onLayout: updateToolbarFrame, children: shouldShowInlineMentionSuggestions ? ((0, jsx_runtime_1.jsx)(react_native_1.View, { testID: 'native-editor-inline-mention-suggestions', style: [
1222
+ styles.inlineMentionSuggestionsContainer,
1223
+ inlineToolbarContentTopBorderStyle,
1224
+ ], children: (0, jsx_runtime_1.jsx)(react_native_1.ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, contentContainerStyle: styles.inlineMentionSuggestionsContent, keyboardShouldPersistTaps: 'always', children: inlineMentionSuggestions.map((suggestion) => {
1225
+ const label = resolveMentionSuggestionLabel(suggestion, mentionQueryEvent?.trigger ?? resolveMentionTrigger(addons));
1226
+ return ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { testID: `native-editor-inline-mention-suggestion-${suggestion.key}`, onPress: () => handleInlineMentionSuggestionPress(suggestion), accessibilityRole: 'button', accessibilityLabel: label, style: ({ pressed }) => [
1227
+ styles.inlineMentionSuggestion,
1228
+ {
1229
+ backgroundColor: pressed
1230
+ ? (inlineToolbarMentionTheme?.optionHighlightedBackgroundColor ??
1231
+ 'rgba(0, 122, 255, 0.12)')
1232
+ : (inlineToolbarMentionTheme?.backgroundColor ??
1233
+ '#F2F2F7'),
1234
+ borderColor: inlineToolbarMentionTheme?.borderColor ??
1235
+ 'transparent',
1236
+ borderWidth: inlineToolbarMentionTheme?.borderWidth ?? 0,
1237
+ borderRadius: inlineToolbarMentionTheme?.borderRadius ?? 12,
1238
+ },
1239
+ ], children: ({ pressed }) => ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { numberOfLines: 1, style: [
1240
+ styles.inlineMentionSuggestionTitle,
1241
+ {
1242
+ color: pressed
1243
+ ? (inlineToolbarMentionTheme?.optionHighlightedTextColor ??
1244
+ inlineToolbarMentionTheme?.optionTextColor ??
1245
+ '#000000')
1246
+ : (inlineToolbarMentionTheme?.optionTextColor ??
1247
+ inlineToolbarMentionTheme?.textColor ??
1248
+ '#000000'),
1249
+ },
1250
+ ], children: label }), suggestion.subtitle ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { numberOfLines: 1, style: [
1251
+ styles.inlineMentionSuggestionSubtitle,
1252
+ {
1253
+ color: inlineToolbarMentionTheme?.optionSecondaryTextColor ??
1254
+ '#8E8E93',
1255
+ },
1256
+ ], children: suggestion.subtitle })) : null] })) }, suggestion.key));
1257
+ }) }) })) : ((0, jsx_runtime_1.jsx)(EditorToolbar_1.EditorToolbar, { activeState: activeState, historyState: historyState, toolbarItems: toolbarItems, theme: theme?.toolbar, showTopBorder: inlineToolbarShowTopBorder, onToggleMark: (mark) => runAndApply(() => bridgeRef.current?.toggleMark(mark) ?? null, {
1122
1258
  skipNativeApplyIfContentUnchanged: true,
1123
1259
  }), 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) => {
1124
1260
  switch (command) {
@@ -1143,7 +1279,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
1143
1279
  skipNativeApplyIfContentUnchanged: true,
1144
1280
  }), onToggleStrike: () => runAndApply(() => bridgeRef.current?.toggleMark('strike') ?? null, {
1145
1281
  skipNativeApplyIfContentUnchanged: true,
1146
- }), 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) }) }));
1282
+ }), 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) })) }));
1147
1283
  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, showToolbar: showToolbar, toolbarPlacement: toolbarPlacement, heightBehavior: heightBehavior, allowImageResizing: allowImageResizing, themeJson: themeJson, addonsJson: addonsJson, toolbarItemsJson: toolbarItemsJson, remoteSelectionsJson: remoteSelectionsJson, toolbarFrameJson: toolbarPlacement === 'inline' && isFocused ? toolbarFrameJson : undefined, editorUpdateJson: pendingNativeUpdate.json, editorUpdateRevision: pendingNativeUpdate.revision, onEditorUpdate: handleUpdate, onSelectionChange: handleSelectionChange, onFocusChange: handleFocusChange, onContentHeightChange: handleContentHeightChange, onToolbarAction: handleToolbarAction, onAddonEvent: handleAddonEvent }, DEV_NATIVE_VIEW_KEY), shouldRenderJsToolbar && jsToolbar] }));
1148
1284
  });
1149
1285
  const styles = react_native_1.StyleSheet.create({
@@ -1151,9 +1287,32 @@ const styles = react_native_1.StyleSheet.create({
1151
1287
  position: 'relative',
1152
1288
  },
1153
1289
  inlineToolbar: {
1154
- marginTop: 8,
1155
1290
  borderWidth: react_native_1.StyleSheet.hairlineWidth,
1156
- borderColor: '#E5E5EA',
1291
+ borderColor: INLINE_TOOLBAR_BORDER_COLOR,
1292
+ overflow: 'hidden',
1293
+ },
1294
+ inlineMentionSuggestionsContainer: {
1157
1295
  overflow: 'hidden',
1158
1296
  },
1297
+ inlineMentionSuggestionsContent: {
1298
+ paddingHorizontal: 12,
1299
+ paddingVertical: 8,
1300
+ alignItems: 'center',
1301
+ },
1302
+ inlineMentionSuggestion: {
1303
+ minWidth: 88,
1304
+ minHeight: 40,
1305
+ marginRight: 8,
1306
+ paddingHorizontal: 12,
1307
+ paddingVertical: 8,
1308
+ justifyContent: 'center',
1309
+ },
1310
+ inlineMentionSuggestionTitle: {
1311
+ fontSize: 14,
1312
+ fontWeight: '600',
1313
+ },
1314
+ inlineMentionSuggestionSubtitle: {
1315
+ marginTop: 1,
1316
+ fontSize: 12,
1317
+ },
1159
1318
  });
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { NativeRichTextEditor, type NativeRichTextEditorProps, type NativeRichTextEditorRef, type NativeRichTextEditorHeightBehavior, type NativeRichTextEditorToolbarPlacement, type RemoteSelectionDecoration, type LinkRequestContext, type ImageRequestContext, } from './NativeRichTextEditor';
2
- export { NativeProseViewer, type NativeProseViewerProps, type NativeProseViewerMentionRenderContext, type NativeProseViewerMentionPressEvent, } from './NativeProseViewer';
2
+ export { NativeProseViewer, type NativeProseViewerProps, 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, EditorHeadingTheme, EditorListTheme, EditorHorizontalRuleTheme, EditorMentionTheme, EditorToolbarTheme, EditorToolbarAppearance, EditorFontStyle, EditorFontWeight, } from './EditorTheme';
5
5
  export { MENTION_NODE_NAME, mentionNodeSpec, withMentionsSchema, buildMentionFragmentJson, type EditorAddons, type MentionsAddonConfig, type MentionSuggestion, type MentionQueryChangeEvent, type MentionSelectionAttrsEvent, type MentionSelectEvent, type EditorAddonEvent, } from './addons';