@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.
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +33 -1
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +6 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +20 -1
- package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +148 -12
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +35 -3
- package/dist/EditorTheme.d.ts +3 -0
- package/dist/EditorToolbar.js +11 -6
- package/dist/NativeProseViewer.d.ts +19 -3
- package/dist/NativeProseViewer.js +193 -13
- package/dist/NativeRichTextEditor.js +189 -30
- package/dist/index.d.ts +1 -1
- package/ios/EditorCore.xcframework/Info.plist +5 -5
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/EditorTheme.swift +6 -0
- package/ios/NativeEditorModule.swift +18 -1
- package/ios/NativeProseViewerExpoView.swift +152 -17
- package/ios/RenderBridge.swift +13 -2
- package/package.json +1 -1
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
|
@@ -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({
|
|
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)(() =>
|
|
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 =
|
|
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
|
-
|
|
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
|
-
}, [
|
|
387
|
+
}, [resolvedContentRevision, renderJson, themeJson]);
|
|
231
388
|
const handleContentHeightChange = (0, react_1.useCallback)((event) => {
|
|
232
389
|
const nextHeight = event.nativeEvent.contentHeight;
|
|
233
|
-
if (nextHeight
|
|
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
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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 ||
|
|
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 === "'") &&
|
|
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) ||
|
|
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 &&
|
|
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 &&
|
|
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
|
-
|
|
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
|
-
|
|
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)(
|
|
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:
|
|
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';
|