@apollohg/react-native-prose-editor 0.5.1 → 0.5.3
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/README.md +18 -15
- package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +4 -2
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +33 -1
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +23 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +39 -6
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +15 -1
- package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +44 -7
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +24 -4
- package/dist/NativeEditorBridge.d.ts +8 -0
- package/dist/NativeEditorBridge.js +16 -0
- package/dist/NativeProseViewer.d.ts +25 -5
- package/dist/NativeProseViewer.js +212 -13
- package/dist/NativeRichTextEditor.d.ts +2 -0
- package/dist/NativeRichTextEditor.js +417 -31
- package/dist/addons.d.ts +20 -0
- package/dist/addons.js +4 -0
- package/dist/index.d.ts +2 -2
- package/ios/EditorAddons.swift +2 -0
- 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/EditorLayoutManager.swift +10 -1
- package/ios/EditorTheme.swift +25 -0
- package/ios/NativeEditorExpoView.swift +56 -6
- package/ios/NativeEditorModule.swift +14 -1
- package/ios/NativeProseViewerExpoView.swift +62 -11
- package/ios/RenderBridge.swift +40 -16
- package/ios/RichTextEditorView.swift +4 -0
- 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
|
@@ -62,6 +62,291 @@ function isPromiseLike(value) {
|
|
|
62
62
|
'then' in value &&
|
|
63
63
|
typeof value.then === 'function');
|
|
64
64
|
}
|
|
65
|
+
function isRecord(value) {
|
|
66
|
+
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
67
|
+
}
|
|
68
|
+
const AUTO_LINK_URL_REGEX = /(?:https?:\/\/|www\.)\S+/giu;
|
|
69
|
+
const AUTO_LINK_INLINE_PLACEHOLDER = '\uFFFC';
|
|
70
|
+
const AUTO_LINK_LEADING_BOUNDARY_CHARS = new Set(['(', '[', '{', '<', '"', "'"]);
|
|
71
|
+
const AUTO_LINK_TRAILING_DELIMITER_CHARS = new Set([
|
|
72
|
+
'.',
|
|
73
|
+
',',
|
|
74
|
+
'!',
|
|
75
|
+
'?',
|
|
76
|
+
';',
|
|
77
|
+
':',
|
|
78
|
+
')',
|
|
79
|
+
']',
|
|
80
|
+
'}',
|
|
81
|
+
'>',
|
|
82
|
+
'"',
|
|
83
|
+
"'",
|
|
84
|
+
]);
|
|
85
|
+
const AUTO_LINK_ALWAYS_TRIM_CHARS = new Set(['.', ',', '!', '?', ';', ':']);
|
|
86
|
+
const AUTO_LINK_MATCHED_CLOSERS = {
|
|
87
|
+
')': '(',
|
|
88
|
+
']': '[',
|
|
89
|
+
'}': '{',
|
|
90
|
+
};
|
|
91
|
+
function unicodeScalars(text) {
|
|
92
|
+
return Array.from(text);
|
|
93
|
+
}
|
|
94
|
+
function unicodeScalarCount(text) {
|
|
95
|
+
return unicodeScalars(text).length;
|
|
96
|
+
}
|
|
97
|
+
function hasDocumentLinkMark(marks) {
|
|
98
|
+
if (!Array.isArray(marks)) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return marks.some((mark) => isRecord(mark) && typeof mark.type === 'string' && mark.type === 'link');
|
|
102
|
+
}
|
|
103
|
+
function isInlineDocumentNode(node) {
|
|
104
|
+
if (!isRecord(node)) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
if (node.type === 'text') {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return !Array.isArray(node.content);
|
|
111
|
+
}
|
|
112
|
+
function isInlineTextBlockNode(node) {
|
|
113
|
+
const content = Array.isArray(node.content) ? node.content : [];
|
|
114
|
+
return content.length > 0 && content.every((child) => isInlineDocumentNode(child));
|
|
115
|
+
}
|
|
116
|
+
function countOccurrences(text, target) {
|
|
117
|
+
let count = 0;
|
|
118
|
+
for (const char of text) {
|
|
119
|
+
if (char === target) {
|
|
120
|
+
count += 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return count;
|
|
124
|
+
}
|
|
125
|
+
function trimAutoLinkTrailingPunctuation(value) {
|
|
126
|
+
let result = value;
|
|
127
|
+
while (result.length > 0) {
|
|
128
|
+
const chars = unicodeScalars(result);
|
|
129
|
+
const lastChar = chars[chars.length - 1];
|
|
130
|
+
if (!lastChar) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
if (AUTO_LINK_ALWAYS_TRIM_CHARS.has(lastChar)) {
|
|
134
|
+
chars.pop();
|
|
135
|
+
result = chars.join('');
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const matchingOpener = AUTO_LINK_MATCHED_CLOSERS[lastChar];
|
|
139
|
+
if (matchingOpener &&
|
|
140
|
+
countOccurrences(result, lastChar) > countOccurrences(result, matchingOpener)) {
|
|
141
|
+
chars.pop();
|
|
142
|
+
result = chars.join('');
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if ((lastChar === '"' || lastChar === "'") && countOccurrences(result, lastChar) % 2 !== 0) {
|
|
146
|
+
chars.pop();
|
|
147
|
+
result = chars.join('');
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
function normalizeAutoDetectedHref(value) {
|
|
155
|
+
const trimmed = trimAutoLinkTrailingPunctuation(value);
|
|
156
|
+
if (!trimmed) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const normalized = /^www\./iu.test(trimmed) ? `https://${trimmed}` : trimmed;
|
|
160
|
+
try {
|
|
161
|
+
new URL(normalized);
|
|
162
|
+
return normalized;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function isAutoLinkBoundaryChar(char) {
|
|
169
|
+
if (!char) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return /\s/u.test(char) || char === AUTO_LINK_INLINE_PLACEHOLDER || AUTO_LINK_LEADING_BOUNDARY_CHARS.has(char);
|
|
173
|
+
}
|
|
174
|
+
function isAutoLinkTrailingDelimiterChar(char) {
|
|
175
|
+
if (!char) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
return (/\s/u.test(char) ||
|
|
179
|
+
char === AUTO_LINK_INLINE_PLACEHOLDER ||
|
|
180
|
+
AUTO_LINK_TRAILING_DELIMITER_CHARS.has(char));
|
|
181
|
+
}
|
|
182
|
+
function codeUnitBoundariesForScalars(chars) {
|
|
183
|
+
const boundaries = [0];
|
|
184
|
+
let offset = 0;
|
|
185
|
+
for (const char of chars) {
|
|
186
|
+
offset += char.length;
|
|
187
|
+
boundaries.push(offset);
|
|
188
|
+
}
|
|
189
|
+
return boundaries;
|
|
190
|
+
}
|
|
191
|
+
function codeUnitOffsetToScalarIndex(boundaries, offset) {
|
|
192
|
+
let scalarIndex = 0;
|
|
193
|
+
while (scalarIndex + 1 < boundaries.length && boundaries[scalarIndex + 1] <= offset) {
|
|
194
|
+
scalarIndex += 1;
|
|
195
|
+
}
|
|
196
|
+
return scalarIndex;
|
|
197
|
+
}
|
|
198
|
+
function buildAutoLinkInlineBlock(node, pos) {
|
|
199
|
+
const chars = [];
|
|
200
|
+
const docPositions = [];
|
|
201
|
+
const linked = [];
|
|
202
|
+
const content = Array.isArray(node.content) ? node.content : [];
|
|
203
|
+
let nextPos = pos + 1;
|
|
204
|
+
for (const child of content) {
|
|
205
|
+
if (!isRecord(child)) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (child.type === 'text') {
|
|
209
|
+
const childText = typeof child.text === 'string' ? child.text : '';
|
|
210
|
+
const childChars = unicodeScalars(childText);
|
|
211
|
+
const hasLink = hasDocumentLinkMark(child.marks);
|
|
212
|
+
for (const char of childChars) {
|
|
213
|
+
chars.push(char);
|
|
214
|
+
docPositions.push(nextPos);
|
|
215
|
+
linked.push(hasLink);
|
|
216
|
+
nextPos += 1;
|
|
217
|
+
}
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
chars.push(child.type === 'hardBreak' ? '\n' : AUTO_LINK_INLINE_PLACEHOLDER);
|
|
221
|
+
docPositions.push(nextPos);
|
|
222
|
+
linked.push(false);
|
|
223
|
+
nextPos += 1;
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
chars,
|
|
227
|
+
docPositions,
|
|
228
|
+
linked,
|
|
229
|
+
contentStart: pos + 1,
|
|
230
|
+
contentEnd: nextPos,
|
|
231
|
+
nextPos: nextPos + 1,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function findAutoLinkCandidateInInlineBlock(block, cursorDocPos) {
|
|
235
|
+
if (cursorDocPos < block.contentStart || cursorDocPos > block.contentEnd) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
let localIndex = 0;
|
|
239
|
+
while (localIndex < block.docPositions.length && block.docPositions[localIndex] < cursorDocPos) {
|
|
240
|
+
localIndex += 1;
|
|
241
|
+
}
|
|
242
|
+
if (localIndex === 0) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
if (cursorDocPos < block.contentEnd && !isAutoLinkTrailingDelimiterChar(block.chars[localIndex - 1])) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
const prefixChars = block.chars.slice(0, localIndex);
|
|
249
|
+
const prefixText = prefixChars.join('');
|
|
250
|
+
if (!prefixText) {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
const boundaries = codeUnitBoundariesForScalars(prefixChars);
|
|
254
|
+
AUTO_LINK_URL_REGEX.lastIndex = 0;
|
|
255
|
+
let lastMatch = null;
|
|
256
|
+
for (const match of prefixText.matchAll(AUTO_LINK_URL_REGEX)) {
|
|
257
|
+
lastMatch = match;
|
|
258
|
+
}
|
|
259
|
+
if (!lastMatch || typeof lastMatch.index !== 'number') {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
const rawStartScalar = codeUnitOffsetToScalarIndex(boundaries, lastMatch.index);
|
|
263
|
+
const normalizedHref = normalizeAutoDetectedHref(lastMatch[0]);
|
|
264
|
+
const trimmedText = trimAutoLinkTrailingPunctuation(lastMatch[0]);
|
|
265
|
+
if (!normalizedHref || !trimmedText) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
const candidateEndScalar = rawStartScalar + unicodeScalarCount(trimmedText);
|
|
269
|
+
if (candidateEndScalar > prefixChars.length) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
if (!isAutoLinkBoundaryChar(prefixChars[rawStartScalar - 1])) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
for (let index = candidateEndScalar; index < localIndex; index += 1) {
|
|
276
|
+
if (!isAutoLinkTrailingDelimiterChar(prefixChars[index])) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
for (let index = rawStartScalar; index < candidateEndScalar; index += 1) {
|
|
281
|
+
if (block.linked[index]) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const docFrom = block.docPositions[rawStartScalar];
|
|
286
|
+
const docTo = candidateEndScalar < block.docPositions.length
|
|
287
|
+
? block.docPositions[candidateEndScalar]
|
|
288
|
+
: block.contentEnd;
|
|
289
|
+
if (!(docTo > docFrom)) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
docFrom,
|
|
294
|
+
docTo,
|
|
295
|
+
href: normalizedHref,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function findAutoLinkCandidateInDocument(document, cursorDocPos) {
|
|
299
|
+
const visit = (node, pos, isRoot = false) => {
|
|
300
|
+
if (!isRecord(node)) {
|
|
301
|
+
return { candidate: null, nextPos: pos };
|
|
302
|
+
}
|
|
303
|
+
const nodeType = typeof node.type === 'string' ? node.type : '';
|
|
304
|
+
const content = Array.isArray(node.content) ? node.content : [];
|
|
305
|
+
if (nodeType === 'text') {
|
|
306
|
+
const text = typeof node.text === 'string' ? node.text : '';
|
|
307
|
+
return { candidate: null, nextPos: pos + unicodeScalarCount(text) };
|
|
308
|
+
}
|
|
309
|
+
if (isRoot && nodeType === 'doc') {
|
|
310
|
+
let nextPos = pos;
|
|
311
|
+
for (const child of content) {
|
|
312
|
+
const result = visit(child, nextPos);
|
|
313
|
+
if (result.candidate) {
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
nextPos = result.nextPos;
|
|
317
|
+
}
|
|
318
|
+
return { candidate: null, nextPos };
|
|
319
|
+
}
|
|
320
|
+
if (isInlineTextBlockNode(node)) {
|
|
321
|
+
const block = buildAutoLinkInlineBlock(node, pos);
|
|
322
|
+
return {
|
|
323
|
+
candidate: findAutoLinkCandidateInInlineBlock(block, cursorDocPos),
|
|
324
|
+
nextPos: block.nextPos,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
if (content.length === 0) {
|
|
328
|
+
return { candidate: null, nextPos: pos + 1 };
|
|
329
|
+
}
|
|
330
|
+
let nextPos = pos + 1;
|
|
331
|
+
for (const child of content) {
|
|
332
|
+
const result = visit(child, nextPos);
|
|
333
|
+
if (result.candidate) {
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
nextPos = result.nextPos;
|
|
337
|
+
}
|
|
338
|
+
return { candidate: null, nextPos: nextPos + 1 };
|
|
339
|
+
};
|
|
340
|
+
return visit(document, 0, true).candidate;
|
|
341
|
+
}
|
|
342
|
+
function didContentChange(previousDocumentVersion, update) {
|
|
343
|
+
if (!update) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
return (previousDocumentVersion == null ||
|
|
347
|
+
typeof update.documentVersion !== 'number' ||
|
|
348
|
+
update.documentVersion !== previousDocumentVersion);
|
|
349
|
+
}
|
|
65
350
|
function computeRenderedTextLength(elements) {
|
|
66
351
|
let len = 0;
|
|
67
352
|
let blockCount = 0;
|
|
@@ -134,7 +419,7 @@ function useSerializedValue(value, serialize, revision) {
|
|
|
134
419
|
};
|
|
135
420
|
return serialized;
|
|
136
421
|
}
|
|
137
|
-
exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEditor({ initialContent, initialJSON, value, valueJSON, valueJSONRevision, schema, placeholder, editable = true, maxLength, autoFocus = false, heightBehavior = 'autoGrow', showToolbar = true, toolbarPlacement = 'keyboard', toolbarItems = EditorToolbar_1.DEFAULT_EDITOR_TOOLBAR_ITEMS, onToolbarAction, onRequestLink, onRequestImage, onContentChange, onContentChangeJSON, onSelectionChange, onActiveStateChange, onHistoryStateChange, onFocus, onBlur, style, containerStyle, theme, addons, remoteSelections, allowBase64Images = false, allowImageResizing = true, }, ref) {
|
|
422
|
+
exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEditor({ initialContent, initialJSON, value, valueJSON, valueJSONRevision, schema, placeholder, editable = true, maxLength, autoFocus = false, heightBehavior = 'autoGrow', showToolbar = true, toolbarPlacement = 'keyboard', toolbarItems = EditorToolbar_1.DEFAULT_EDITOR_TOOLBAR_ITEMS, onToolbarAction, onRequestLink, onRequestImage, autoDetectLinks = false, onContentChange, onContentChangeJSON, onSelectionChange, onActiveStateChange, onHistoryStateChange, onFocus, onBlur, style, containerStyle, theme, addons, remoteSelections, allowBase64Images = false, allowImageResizing = true, }, ref) {
|
|
138
423
|
const bridgeRef = (0, react_1.useRef)(null);
|
|
139
424
|
const nativeViewRef = (0, react_1.useRef)(null);
|
|
140
425
|
const [isReady, setIsReady] = (0, react_1.useState)(false);
|
|
@@ -241,6 +526,74 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
241
526
|
onContentChangeJSONRef.current?.(bridgeRef.current.getJson());
|
|
242
527
|
}
|
|
243
528
|
}, []);
|
|
529
|
+
const applyUpdateToNativeView = (0, react_1.useCallback)((update, previousDocumentVersion, skipNativeApplyIfContentUnchanged = false) => {
|
|
530
|
+
const contentChanged = didContentChange(previousDocumentVersion, update);
|
|
531
|
+
if (!skipNativeApplyIfContentUnchanged || contentChanged) {
|
|
532
|
+
const updateJson = JSON.stringify(update);
|
|
533
|
+
if (react_native_1.Platform.OS === 'android') {
|
|
534
|
+
setPendingNativeUpdate((current) => ({
|
|
535
|
+
json: updateJson,
|
|
536
|
+
revision: current.revision + 1,
|
|
537
|
+
}));
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
try {
|
|
541
|
+
const applyResult = nativeViewRef.current?.applyEditorUpdate(updateJson);
|
|
542
|
+
if (isPromiseLike(applyResult)) {
|
|
543
|
+
void applyResult.catch(() => {
|
|
544
|
+
// The native view may already be torn down during navigation.
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
// The native view may already be torn down during navigation.
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return contentChanged;
|
|
554
|
+
}, []);
|
|
555
|
+
const maybeApplyAutoDetectedLink = (0, react_1.useCallback)((update, previousDocumentVersion) => {
|
|
556
|
+
if (!autoDetectLinks ||
|
|
557
|
+
!update ||
|
|
558
|
+
!didContentChange(previousDocumentVersion, update) ||
|
|
559
|
+
!bridgeRef.current ||
|
|
560
|
+
bridgeRef.current.isDestroyed ||
|
|
561
|
+
!update.activeState.allowedMarks.includes('link') ||
|
|
562
|
+
update.selection.type !== 'text' ||
|
|
563
|
+
update.selection.anchor == null ||
|
|
564
|
+
update.selection.head == null ||
|
|
565
|
+
update.selection.anchor !== update.selection.head) {
|
|
566
|
+
return update;
|
|
567
|
+
}
|
|
568
|
+
const cursorDocPos = update.selection.head;
|
|
569
|
+
const candidate = findAutoLinkCandidateInDocument(bridgeRef.current.getJson(), cursorDocPos);
|
|
570
|
+
if (!candidate) {
|
|
571
|
+
return update;
|
|
572
|
+
}
|
|
573
|
+
const scalarFrom = bridgeRef.current.docToScalar(candidate.docFrom);
|
|
574
|
+
const scalarTo = bridgeRef.current.docToScalar(candidate.docTo);
|
|
575
|
+
if (!(scalarTo > scalarFrom)) {
|
|
576
|
+
return update;
|
|
577
|
+
}
|
|
578
|
+
const autoLinkUpdate = bridgeRef.current.setMarkAtSelectionScalar(scalarFrom, scalarTo, 'link', { href: candidate.href });
|
|
579
|
+
if (!autoLinkUpdate) {
|
|
580
|
+
return update;
|
|
581
|
+
}
|
|
582
|
+
bridgeRef.current.setSelection(update.selection.anchor, update.selection.head);
|
|
583
|
+
const selectionState = bridgeRef.current.getSelectionState();
|
|
584
|
+
if (selectionState) {
|
|
585
|
+
autoLinkUpdate.selection = selectionState.selection;
|
|
586
|
+
autoLinkUpdate.activeState = selectionState.activeState;
|
|
587
|
+
autoLinkUpdate.historyState = selectionState.historyState;
|
|
588
|
+
if (typeof selectionState.documentVersion === 'number') {
|
|
589
|
+
autoLinkUpdate.documentVersion = selectionState.documentVersion;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
autoLinkUpdate.selection = update.selection;
|
|
594
|
+
}
|
|
595
|
+
return autoLinkUpdate;
|
|
596
|
+
}, [autoDetectLinks]);
|
|
244
597
|
// Warn if both value and valueJSON are set
|
|
245
598
|
if (__DEV__ && value != null && valueJSON != null) {
|
|
246
599
|
console.warn('NativeRichTextEditor: value and valueJSON are mutually exclusive. ' +
|
|
@@ -249,9 +602,15 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
249
602
|
const runAndApply = (0, react_1.useCallback)((mutate, options) => {
|
|
250
603
|
const previousDocumentVersion = documentVersionRef.current;
|
|
251
604
|
const preservedSelection = options?.preserveLiveTextSelection === true ? selectionRef.current : null;
|
|
252
|
-
|
|
605
|
+
let update = mutate();
|
|
253
606
|
if (!update)
|
|
254
607
|
return null;
|
|
608
|
+
if (!options?.skipAutoDetectLinks) {
|
|
609
|
+
update = maybeApplyAutoDetectedLink(update, previousDocumentVersion);
|
|
610
|
+
if (!update) {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
255
614
|
if (preservedSelection?.type === 'text' &&
|
|
256
615
|
typeof preservedSelection.anchor === 'number' &&
|
|
257
616
|
typeof preservedSelection.head === 'number' &&
|
|
@@ -264,31 +623,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
264
623
|
head: preservedSelection.head,
|
|
265
624
|
};
|
|
266
625
|
}
|
|
267
|
-
|
|
268
|
-
typeof update.documentVersion !== 'number' ||
|
|
269
|
-
update.documentVersion !== previousDocumentVersion;
|
|
270
|
-
if (!options?.skipNativeApplyIfContentUnchanged || contentChanged) {
|
|
271
|
-
const updateJson = JSON.stringify(update);
|
|
272
|
-
if (react_native_1.Platform.OS === 'android') {
|
|
273
|
-
setPendingNativeUpdate((current) => ({
|
|
274
|
-
json: updateJson,
|
|
275
|
-
revision: current.revision + 1,
|
|
276
|
-
}));
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
279
|
-
try {
|
|
280
|
-
const applyResult = nativeViewRef.current?.applyEditorUpdate(updateJson);
|
|
281
|
-
if (isPromiseLike(applyResult)) {
|
|
282
|
-
void applyResult.catch(() => {
|
|
283
|
-
// The native view may already be torn down during navigation.
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
catch {
|
|
288
|
-
// The native view may already be torn down during navigation.
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
626
|
+
applyUpdateToNativeView(update, previousDocumentVersion, options?.skipNativeApplyIfContentUnchanged);
|
|
292
627
|
syncStateFromUpdate(update);
|
|
293
628
|
onActiveStateChangeRef.current?.(update.activeState);
|
|
294
629
|
onHistoryStateChangeRef.current?.(update.historyState);
|
|
@@ -297,7 +632,12 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
297
632
|
}
|
|
298
633
|
onSelectionChangeRef.current?.(update.selection);
|
|
299
634
|
return update;
|
|
300
|
-
}, [
|
|
635
|
+
}, [
|
|
636
|
+
applyUpdateToNativeView,
|
|
637
|
+
emitContentCallbacksForUpdate,
|
|
638
|
+
maybeApplyAutoDetectedLink,
|
|
639
|
+
syncStateFromUpdate,
|
|
640
|
+
]);
|
|
301
641
|
(0, react_1.useEffect)(() => {
|
|
302
642
|
const bridgeConfig = maxLength != null || serializedSchemaJson || allowBase64Images
|
|
303
643
|
? {
|
|
@@ -349,6 +689,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
349
689
|
runAndApply(() => bridgeRef.current.replaceHtml(value), {
|
|
350
690
|
suppressContentCallbacks: true,
|
|
351
691
|
preserveLiveTextSelection: true,
|
|
692
|
+
skipAutoDetectLinks: true,
|
|
352
693
|
});
|
|
353
694
|
}, [value, runAndApply]);
|
|
354
695
|
(0, react_1.useEffect)(() => {
|
|
@@ -362,6 +703,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
362
703
|
runAndApply(() => bridgeRef.current.replaceJsonString(serializedValueJson), {
|
|
363
704
|
suppressContentCallbacks: true,
|
|
364
705
|
preserveLiveTextSelection: true,
|
|
706
|
+
skipAutoDetectLinks: true,
|
|
365
707
|
});
|
|
366
708
|
}, [serializedValueJson, value, runAndApply]);
|
|
367
709
|
const updateToolbarFrame = (0, react_1.useCallback)(() => {
|
|
@@ -399,9 +741,15 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
399
741
|
return;
|
|
400
742
|
try {
|
|
401
743
|
const previousDocumentVersion = documentVersionRef.current;
|
|
402
|
-
const
|
|
744
|
+
const nativeUpdate = bridgeRef.current.parseUpdateJson(event.nativeEvent.updateJson);
|
|
745
|
+
if (!nativeUpdate)
|
|
746
|
+
return;
|
|
747
|
+
const update = maybeApplyAutoDetectedLink(nativeUpdate, previousDocumentVersion);
|
|
403
748
|
if (!update)
|
|
404
749
|
return;
|
|
750
|
+
if (update !== nativeUpdate) {
|
|
751
|
+
applyUpdateToNativeView(update, previousDocumentVersion);
|
|
752
|
+
}
|
|
405
753
|
syncStateFromUpdate(update);
|
|
406
754
|
onActiveStateChangeRef.current?.(update.activeState);
|
|
407
755
|
onHistoryStateChangeRef.current?.(update.historyState);
|
|
@@ -411,7 +759,12 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
411
759
|
catch {
|
|
412
760
|
// Invalid JSON from native — skip
|
|
413
761
|
}
|
|
414
|
-
}, [
|
|
762
|
+
}, [
|
|
763
|
+
applyUpdateToNativeView,
|
|
764
|
+
emitContentCallbacksForUpdate,
|
|
765
|
+
maybeApplyAutoDetectedLink,
|
|
766
|
+
syncStateFromUpdate,
|
|
767
|
+
]);
|
|
415
768
|
const handleSelectionChange = (0, react_1.useCallback)((event) => {
|
|
416
769
|
if (!bridgeRef.current || bridgeRef.current.isDestroyed)
|
|
417
770
|
return;
|
|
@@ -563,6 +916,39 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
563
916
|
});
|
|
564
917
|
return;
|
|
565
918
|
}
|
|
919
|
+
if (parsed.type === 'mentionsSelectRequest') {
|
|
920
|
+
const suggestion = mentionSuggestionsByKeyRef.current.get(parsed.suggestionKey);
|
|
921
|
+
if (!suggestion || !bridgeRef.current || bridgeRef.current.isDestroyed)
|
|
922
|
+
return;
|
|
923
|
+
const selectionEvent = {
|
|
924
|
+
trigger: parsed.trigger,
|
|
925
|
+
suggestion,
|
|
926
|
+
attrs: parsed.attrs,
|
|
927
|
+
range: parsed.range,
|
|
928
|
+
};
|
|
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;
|
|
942
|
+
const update = runAndApply(() => bridgeRef.current?.insertContentJsonAtSelectionScalar(parsed.range.anchor, parsed.range.head, (0, addons_1.buildMentionFragmentJson)(finalAttrs)) ?? null);
|
|
943
|
+
if (update) {
|
|
944
|
+
addonsRef.current?.mentions?.onSelect?.({
|
|
945
|
+
trigger: parsed.trigger,
|
|
946
|
+
suggestion,
|
|
947
|
+
attrs: finalAttrs,
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
566
952
|
if (parsed.type === 'mentionsSelect') {
|
|
567
953
|
const suggestion = mentionSuggestionsByKeyRef.current.get(parsed.suggestionKey);
|
|
568
954
|
if (!suggestion)
|
|
@@ -573,7 +959,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
|
|
|
573
959
|
attrs: parsed.attrs,
|
|
574
960
|
});
|
|
575
961
|
}
|
|
576
|
-
}, []);
|
|
962
|
+
}, [runAndApply]);
|
|
577
963
|
(0, react_1.useImperativeHandle)(ref, () => ({
|
|
578
964
|
focus() {
|
|
579
965
|
nativeViewRef.current?.focus?.();
|
package/dist/addons.d.ts
CHANGED
|
@@ -22,10 +22,20 @@ export interface MentionSelectEvent {
|
|
|
22
22
|
suggestion: MentionSuggestion;
|
|
23
23
|
attrs: Record<string, unknown>;
|
|
24
24
|
}
|
|
25
|
+
export interface MentionSelectionAttrsEvent {
|
|
26
|
+
trigger: string;
|
|
27
|
+
suggestion: MentionSuggestion;
|
|
28
|
+
attrs: Record<string, unknown>;
|
|
29
|
+
range: {
|
|
30
|
+
anchor: number;
|
|
31
|
+
head: number;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
25
34
|
export interface MentionsAddonConfig {
|
|
26
35
|
trigger?: string;
|
|
27
36
|
suggestions?: readonly MentionSuggestion[];
|
|
28
37
|
theme?: EditorMentionTheme;
|
|
38
|
+
resolveSelectionAttrs?: (event: MentionSelectionAttrsEvent) => Record<string, unknown> | null | undefined;
|
|
29
39
|
onQueryChange?: (event: MentionQueryChangeEvent) => void;
|
|
30
40
|
onSelect?: (event: MentionSelectEvent) => void;
|
|
31
41
|
}
|
|
@@ -42,6 +52,7 @@ export interface SerializedMentionSuggestion {
|
|
|
42
52
|
export interface SerializedMentionsAddonConfig {
|
|
43
53
|
trigger: string;
|
|
44
54
|
theme?: EditorMentionTheme;
|
|
55
|
+
resolveSelectionAttrs?: boolean;
|
|
45
56
|
suggestions: SerializedMentionSuggestion[];
|
|
46
57
|
}
|
|
47
58
|
export interface SerializedEditorAddons {
|
|
@@ -56,6 +67,15 @@ export type EditorAddonEvent = {
|
|
|
56
67
|
head: number;
|
|
57
68
|
};
|
|
58
69
|
isActive: boolean;
|
|
70
|
+
} | {
|
|
71
|
+
type: 'mentionsSelectRequest';
|
|
72
|
+
trigger: string;
|
|
73
|
+
suggestionKey: string;
|
|
74
|
+
attrs: Record<string, unknown>;
|
|
75
|
+
range: {
|
|
76
|
+
anchor: number;
|
|
77
|
+
head: number;
|
|
78
|
+
};
|
|
59
79
|
} | {
|
|
60
80
|
type: 'mentionsSelect';
|
|
61
81
|
trigger: string;
|
package/dist/addons.js
CHANGED
|
@@ -39,6 +39,7 @@ function normalizeEditorAddons(addons) {
|
|
|
39
39
|
const label = suggestion.label?.trim() || `${trigger}${suggestion.title}`;
|
|
40
40
|
const attrs = {
|
|
41
41
|
label,
|
|
42
|
+
mentionSuggestionChar: trigger,
|
|
42
43
|
...(suggestion.attrs ?? {}),
|
|
43
44
|
};
|
|
44
45
|
return {
|
|
@@ -53,6 +54,9 @@ function normalizeEditorAddons(addons) {
|
|
|
53
54
|
mentions: {
|
|
54
55
|
trigger,
|
|
55
56
|
theme: addons.mentions.theme,
|
|
57
|
+
...(typeof addons.mentions.resolveSelectionAttrs === 'function'
|
|
58
|
+
? { resolveSelectionAttrs: true }
|
|
59
|
+
: {}),
|
|
56
60
|
suggestions,
|
|
57
61
|
},
|
|
58
62
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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 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
|
-
export { MENTION_NODE_NAME, mentionNodeSpec, withMentionsSchema, buildMentionFragmentJson, type EditorAddons, type MentionsAddonConfig, type MentionSuggestion, type MentionQueryChangeEvent, type MentionSelectEvent, type EditorAddonEvent, } from './addons';
|
|
5
|
+
export { MENTION_NODE_NAME, mentionNodeSpec, withMentionsSchema, buildMentionFragmentJson, type EditorAddons, type MentionsAddonConfig, type MentionSuggestion, type MentionQueryChangeEvent, type MentionSelectionAttrsEvent, type MentionSelectEvent, type EditorAddonEvent, } from './addons';
|
|
6
6
|
export { tiptapSchema, prosemirrorSchema, IMAGE_NODE_NAME, imageNodeSpec, withImagesSchema, buildImageFragmentJson, type SchemaDefinition, type NodeSpec, type MarkSpec, type AttrSpec, type ImageNodeAttributes, } from './schemas';
|
|
7
7
|
export { createYjsCollaborationController, useYjsCollaboration, type YjsCollaborationOptions, type YjsCollaborationState, type YjsTransportStatus, type LocalAwarenessState, type LocalAwarenessUser, type UseYjsCollaborationResult, type YjsCollaborationController, } from './YjsCollaboration';
|
|
8
8
|
export type { Selection, ActiveState, HistoryState, EditorUpdate, DocumentJSON, CollaborationPeer, EncodedCollaborationStateInput, } from './NativeEditorBridge';
|
package/ios/EditorAddons.swift
CHANGED
|
@@ -27,11 +27,13 @@ struct NativeMentionsAddonConfig {
|
|
|
27
27
|
let trigger: String
|
|
28
28
|
let suggestions: [NativeMentionSuggestion]
|
|
29
29
|
let theme: EditorMentionTheme?
|
|
30
|
+
let resolveSelectionAttrs: Bool
|
|
30
31
|
|
|
31
32
|
init?(dictionary: [String: Any]) {
|
|
32
33
|
let trigger = (dictionary["trigger"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
33
34
|
self.trigger = (trigger?.isEmpty == false ? trigger : "@") ?? "@"
|
|
34
35
|
self.suggestions = ((dictionary["suggestions"] as? [[String: Any]]) ?? []).compactMap(NativeMentionSuggestion.init(dictionary:))
|
|
36
|
+
self.resolveSelectionAttrs = dictionary["resolveSelectionAttrs"] as? Bool ?? false
|
|
35
37
|
if let theme = dictionary["theme"] as? [String: Any] {
|
|
36
38
|
self.theme = EditorMentionTheme(dictionary: theme)
|
|
37
39
|
} else {
|
|
Binary file
|
|
Binary file
|
|
@@ -140,7 +140,7 @@ final class EditorLayoutManager: NSLayoutManager {
|
|
|
140
140
|
let lineFragmentRect = self.lineFragmentRect(forGlyphAt: glyphIndex, effectiveRange: nil)
|
|
141
141
|
let attrs = textStorage.attributes(at: paragraphStart, effectiveRange: nil)
|
|
142
142
|
|
|
143
|
-
let baseFont =
|
|
143
|
+
let baseFont = Self.markerBaseFont(from: attrs)
|
|
144
144
|
let textColor = attrs[RenderBridgeAttributes.listMarkerColor] as? UIColor
|
|
145
145
|
?? attrs[.foregroundColor] as? UIColor
|
|
146
146
|
?? .label
|
|
@@ -465,6 +465,15 @@ final class EditorLayoutManager: NSLayoutManager {
|
|
|
465
465
|
return baseFont.withSize(baseFont.pointSize * markerScale)
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
+
static func markerBaseFont(
|
|
469
|
+
from attrs: [NSAttributedString.Key: Any],
|
|
470
|
+
fallback fallbackFont: UIFont = .systemFont(ofSize: 16)
|
|
471
|
+
) -> UIFont {
|
|
472
|
+
(attrs[RenderBridgeAttributes.listMarkerBaseFont] as? UIFont)
|
|
473
|
+
?? (attrs[.font] as? UIFont)
|
|
474
|
+
?? fallbackFont
|
|
475
|
+
}
|
|
476
|
+
|
|
468
477
|
private static func unorderedBulletGlyphBounds(for font: UIFont) -> CGRect {
|
|
469
478
|
let ctFont = font as CTFont
|
|
470
479
|
let bullet = UniChar(0x2022)
|
package/ios/EditorTheme.swift
CHANGED
|
@@ -139,6 +139,31 @@ struct EditorMentionTheme {
|
|
|
139
139
|
var optionHighlightedBackgroundColor: UIColor?
|
|
140
140
|
var optionHighlightedTextColor: UIColor?
|
|
141
141
|
|
|
142
|
+
func merged(with override: EditorMentionTheme?) -> EditorMentionTheme {
|
|
143
|
+
guard let override else { return self }
|
|
144
|
+
var merged = self
|
|
145
|
+
merged.textColor = override.textColor ?? merged.textColor
|
|
146
|
+
merged.backgroundColor = override.backgroundColor ?? merged.backgroundColor
|
|
147
|
+
merged.borderColor = override.borderColor ?? merged.borderColor
|
|
148
|
+
merged.borderWidth = override.borderWidth ?? merged.borderWidth
|
|
149
|
+
merged.borderRadius = override.borderRadius ?? merged.borderRadius
|
|
150
|
+
merged.fontWeight = override.fontWeight ?? merged.fontWeight
|
|
151
|
+
merged.popoverBackgroundColor =
|
|
152
|
+
override.popoverBackgroundColor ?? merged.popoverBackgroundColor
|
|
153
|
+
merged.popoverBorderColor = override.popoverBorderColor ?? merged.popoverBorderColor
|
|
154
|
+
merged.popoverBorderWidth = override.popoverBorderWidth ?? merged.popoverBorderWidth
|
|
155
|
+
merged.popoverBorderRadius = override.popoverBorderRadius ?? merged.popoverBorderRadius
|
|
156
|
+
merged.popoverShadowColor = override.popoverShadowColor ?? merged.popoverShadowColor
|
|
157
|
+
merged.optionTextColor = override.optionTextColor ?? merged.optionTextColor
|
|
158
|
+
merged.optionSecondaryTextColor =
|
|
159
|
+
override.optionSecondaryTextColor ?? merged.optionSecondaryTextColor
|
|
160
|
+
merged.optionHighlightedBackgroundColor =
|
|
161
|
+
override.optionHighlightedBackgroundColor ?? merged.optionHighlightedBackgroundColor
|
|
162
|
+
merged.optionHighlightedTextColor =
|
|
163
|
+
override.optionHighlightedTextColor ?? merged.optionHighlightedTextColor
|
|
164
|
+
return merged
|
|
165
|
+
}
|
|
166
|
+
|
|
142
167
|
init(dictionary: [String: Any]) {
|
|
143
168
|
textColor = EditorTheme.color(from: dictionary["textColor"])
|
|
144
169
|
backgroundColor = EditorTheme.color(from: dictionary["backgroundColor"])
|