@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
|
@@ -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);
|
|
@@ -56,7 +57,21 @@ function normalizeMentionAttrs(node) {
|
|
|
56
57
|
}
|
|
57
58
|
return attrs;
|
|
58
59
|
}
|
|
59
|
-
function
|
|
60
|
+
function baseMentionLabelFromAttrs(attrs) {
|
|
61
|
+
const label = attrs.label;
|
|
62
|
+
return typeof label === 'string' && label.length > 0 ? label : 'mention';
|
|
63
|
+
}
|
|
64
|
+
function resolveMentionPrefix(mentionPrefix, mention) {
|
|
65
|
+
const rawPrefix = typeof mentionPrefix === 'function' ? mentionPrefix(mention) : mentionPrefix;
|
|
66
|
+
return typeof rawPrefix === 'string' && rawPrefix.length > 0 ? rawPrefix : undefined;
|
|
67
|
+
}
|
|
68
|
+
function applyMentionPrefix(label, prefix) {
|
|
69
|
+
if (!prefix || label.startsWith(prefix)) {
|
|
70
|
+
return label;
|
|
71
|
+
}
|
|
72
|
+
return `${prefix}${label}`;
|
|
73
|
+
}
|
|
74
|
+
function collectMentionPayloadsByDocPos(document, mentionPrefix, resolveMentionTheme) {
|
|
60
75
|
const mentions = new Map();
|
|
61
76
|
const visit = (node, pos, isRoot = false) => {
|
|
62
77
|
if (node == null || typeof node !== 'object') {
|
|
@@ -71,8 +86,15 @@ function collectMentionPayloadsByDocPos(document) {
|
|
|
71
86
|
}
|
|
72
87
|
if (nodeType === 'mention') {
|
|
73
88
|
const attrs = normalizeMentionAttrs(nodeRecord);
|
|
74
|
-
const label =
|
|
75
|
-
|
|
89
|
+
const label = baseMentionLabelFromAttrs(attrs);
|
|
90
|
+
const mentionContext = { docPos: pos, label, attrs };
|
|
91
|
+
const renderedLabel = applyMentionPrefix(label, resolveMentionPrefix(mentionPrefix, mentionContext));
|
|
92
|
+
const mentionTheme = resolveMentionTheme?.(mentionContext) ?? undefined;
|
|
93
|
+
mentions.set(pos, {
|
|
94
|
+
...mentionContext,
|
|
95
|
+
renderedLabel,
|
|
96
|
+
mentionTheme,
|
|
97
|
+
});
|
|
76
98
|
}
|
|
77
99
|
if (isRoot && nodeType === 'doc') {
|
|
78
100
|
let nextPos = pos;
|
|
@@ -93,6 +115,132 @@ function collectMentionPayloadsByDocPos(document) {
|
|
|
93
115
|
visit(document, 0, true);
|
|
94
116
|
return mentions;
|
|
95
117
|
}
|
|
118
|
+
function applyResolvedMentionRendering(renderJson, mentionPayloadsByDocPos) {
|
|
119
|
+
if (mentionPayloadsByDocPos.size === 0) {
|
|
120
|
+
return renderJson;
|
|
121
|
+
}
|
|
122
|
+
let parsedElements;
|
|
123
|
+
try {
|
|
124
|
+
parsedElements = JSON.parse(renderJson);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return renderJson;
|
|
128
|
+
}
|
|
129
|
+
if (!Array.isArray(parsedElements)) {
|
|
130
|
+
return renderJson;
|
|
131
|
+
}
|
|
132
|
+
let didChange = false;
|
|
133
|
+
const nextElements = parsedElements.map((element) => {
|
|
134
|
+
if (element == null || typeof element !== 'object' || Array.isArray(element)) {
|
|
135
|
+
return element;
|
|
136
|
+
}
|
|
137
|
+
const renderElement = element;
|
|
138
|
+
if (renderElement.type !== 'opaqueInlineAtom' ||
|
|
139
|
+
renderElement.nodeType !== 'mention' ||
|
|
140
|
+
typeof renderElement.docPos !== 'number') {
|
|
141
|
+
return element;
|
|
142
|
+
}
|
|
143
|
+
const mention = mentionPayloadsByDocPos.get(renderElement.docPos);
|
|
144
|
+
if (!mention) {
|
|
145
|
+
return element;
|
|
146
|
+
}
|
|
147
|
+
let nextElement = renderElement;
|
|
148
|
+
if (renderElement.label !== mention.renderedLabel) {
|
|
149
|
+
nextElement = { ...nextElement, label: mention.renderedLabel };
|
|
150
|
+
didChange = true;
|
|
151
|
+
}
|
|
152
|
+
if (mention.mentionTheme && Object.keys(mention.mentionTheme).length > 0) {
|
|
153
|
+
nextElement =
|
|
154
|
+
nextElement === renderElement ? { ...nextElement } : nextElement;
|
|
155
|
+
nextElement.mentionTheme = mention.mentionTheme;
|
|
156
|
+
didChange = true;
|
|
157
|
+
}
|
|
158
|
+
return nextElement;
|
|
159
|
+
});
|
|
160
|
+
return didChange ? JSON.stringify(nextElements) : renderJson;
|
|
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 isTrailingEmptyParagraphRange(elements, start, endExclusive) {
|
|
172
|
+
const startElement = elements[start];
|
|
173
|
+
const endElement = elements[endExclusive - 1];
|
|
174
|
+
if (startElement?.type !== 'blockStart' ||
|
|
175
|
+
startElement.nodeType !== 'paragraph' ||
|
|
176
|
+
startElement.depth !== 0 ||
|
|
177
|
+
endElement?.type !== 'blockEnd') {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
const innerElements = elements.slice(start + 1, endExclusive - 1);
|
|
181
|
+
return (innerElements.length > 0 &&
|
|
182
|
+
innerElements.every((element) => element.type === 'textRun' &&
|
|
183
|
+
typeof element.text === 'string' &&
|
|
184
|
+
isEmptyParagraphPlaceholderText(element.text)));
|
|
185
|
+
}
|
|
186
|
+
function collapseTrailingEmptyParagraphRenderElements(renderJson) {
|
|
187
|
+
let parsedElements;
|
|
188
|
+
try {
|
|
189
|
+
parsedElements = JSON.parse(renderJson);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return renderJson;
|
|
193
|
+
}
|
|
194
|
+
if (!Array.isArray(parsedElements)) {
|
|
195
|
+
return renderJson;
|
|
196
|
+
}
|
|
197
|
+
const elements = parsedElements;
|
|
198
|
+
const topLevelRanges = [];
|
|
199
|
+
for (let index = 0; index < elements.length; index += 1) {
|
|
200
|
+
const element = elements[index];
|
|
201
|
+
if (!element || typeof element !== 'object' || Array.isArray(element)) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (element.type === 'blockStart' && element.depth === 0) {
|
|
205
|
+
let nestingDepth = 1;
|
|
206
|
+
let cursor = index + 1;
|
|
207
|
+
while (cursor < elements.length && nestingDepth > 0) {
|
|
208
|
+
const current = elements[cursor];
|
|
209
|
+
if (current?.type === 'blockStart') {
|
|
210
|
+
nestingDepth += 1;
|
|
211
|
+
}
|
|
212
|
+
else if (current?.type === 'blockEnd') {
|
|
213
|
+
nestingDepth -= 1;
|
|
214
|
+
}
|
|
215
|
+
cursor += 1;
|
|
216
|
+
}
|
|
217
|
+
if (nestingDepth !== 0) {
|
|
218
|
+
return renderJson;
|
|
219
|
+
}
|
|
220
|
+
topLevelRanges.push({ start: index, endExclusive: cursor });
|
|
221
|
+
index = cursor - 1;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (isTopLevelSingleElementBlock(element)) {
|
|
225
|
+
topLevelRanges.push({ start: index, endExclusive: index + 1 });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (topLevelRanges.length <= 1) {
|
|
229
|
+
return renderJson;
|
|
230
|
+
}
|
|
231
|
+
let trimStart = null;
|
|
232
|
+
for (let rangeIndex = topLevelRanges.length - 1; rangeIndex >= 1; rangeIndex -= 1) {
|
|
233
|
+
const range = topLevelRanges[rangeIndex];
|
|
234
|
+
if (!isTrailingEmptyParagraphRange(elements, range.start, range.endExclusive)) {
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
trimStart = range.start;
|
|
238
|
+
}
|
|
239
|
+
if (trimStart == null) {
|
|
240
|
+
return renderJson;
|
|
241
|
+
}
|
|
242
|
+
return JSON.stringify(elements.slice(0, trimStart));
|
|
243
|
+
}
|
|
96
244
|
function serializeDocumentInput(document, schema) {
|
|
97
245
|
if (typeof document === 'string') {
|
|
98
246
|
try {
|
|
@@ -129,34 +277,73 @@ function extractRenderError(json) {
|
|
|
129
277
|
return null;
|
|
130
278
|
}
|
|
131
279
|
}
|
|
132
|
-
function NativeProseViewer({
|
|
280
|
+
function NativeProseViewer({ ...props }) {
|
|
281
|
+
const { contentRevision, contentJSONRevision, schema, theme, style, allowBase64Images = false, collapseTrailingEmptyParagraphs = true, enableLinkTaps = true, mentionPrefix, resolveMentionTheme, onPressLink, onPressMention, } = props;
|
|
282
|
+
const contentJSON = 'contentJSON' in props ? props.contentJSON : undefined;
|
|
283
|
+
const contentHTML = 'contentHTML' in props ? props.contentHTML : undefined;
|
|
284
|
+
const resolvedContentRevision = contentRevision ?? contentJSONRevision;
|
|
133
285
|
const documentSchema = (0, react_1.useMemo)(() => (0, addons_1.withMentionsSchema)(schema ?? schemas_1.tiptapSchema), [schema]);
|
|
134
|
-
const { normalizedDocument, serializedContentJson } = (0, react_1.useMemo)(() =>
|
|
286
|
+
const { normalizedDocument, serializedContentJson } = (0, react_1.useMemo)(() => {
|
|
287
|
+
if (contentJSON === undefined) {
|
|
288
|
+
return {
|
|
289
|
+
normalizedDocument: null,
|
|
290
|
+
serializedContentJson: null,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return serializeDocumentInput(contentJSON, documentSchema);
|
|
294
|
+
}, [contentJSON, resolvedContentRevision, documentSchema]);
|
|
135
295
|
const themeJson = (0, react_1.useMemo)(() => (0, EditorTheme_1.serializeEditorTheme)(theme), [theme]);
|
|
136
296
|
const mentionPayloadsByDocPos = (0, react_1.useMemo)(() => normalizedDocument == null
|
|
137
297
|
? new Map()
|
|
138
|
-
: collectMentionPayloadsByDocPos(normalizedDocument), [normalizedDocument]);
|
|
298
|
+
: collectMentionPayloadsByDocPos(normalizedDocument, mentionPrefix, resolveMentionTheme), [mentionPrefix, normalizedDocument, resolveMentionTheme]);
|
|
139
299
|
const renderJson = (0, react_1.useMemo)(() => {
|
|
140
300
|
const configJson = JSON.stringify({
|
|
141
301
|
schema: documentSchema,
|
|
142
302
|
...(allowBase64Images ? { allowBase64Images } : {}),
|
|
143
303
|
});
|
|
144
|
-
const nextRenderJson =
|
|
304
|
+
const nextRenderJson = serializedContentJson != null
|
|
305
|
+
? getNativeProseViewerModule().renderDocumentJson(configJson, serializedContentJson)
|
|
306
|
+
: getNativeProseViewerModule().renderDocumentHtml(configJson, contentHTML ?? '');
|
|
145
307
|
const renderError = extractRenderError(nextRenderJson);
|
|
146
308
|
if (renderError != null) {
|
|
147
309
|
console.error(`NativeProseViewer: ${renderError}`);
|
|
148
310
|
return '[]';
|
|
149
311
|
}
|
|
150
312
|
if (looksLikeRenderElementsJson(nextRenderJson)) {
|
|
151
|
-
|
|
313
|
+
const collapsedRenderJson = collapseTrailingEmptyParagraphs
|
|
314
|
+
? collapseTrailingEmptyParagraphRenderElements(nextRenderJson)
|
|
315
|
+
: nextRenderJson;
|
|
316
|
+
return applyResolvedMentionRendering(collapsedRenderJson, mentionPayloadsByDocPos);
|
|
152
317
|
}
|
|
153
318
|
console.error('NativeProseViewer: native renderDocumentJson returned an invalid payload.');
|
|
154
319
|
return '[]';
|
|
155
|
-
}, [
|
|
320
|
+
}, [
|
|
321
|
+
allowBase64Images,
|
|
322
|
+
collapseTrailingEmptyParagraphs,
|
|
323
|
+
contentHTML,
|
|
324
|
+
documentSchema,
|
|
325
|
+
mentionPayloadsByDocPos,
|
|
326
|
+
serializedContentJson,
|
|
327
|
+
]);
|
|
156
328
|
const [contentHeight, setContentHeight] = (0, react_1.useState)(null);
|
|
329
|
+
const allowContentHeightShrinkRef = (0, react_1.useRef)(true);
|
|
330
|
+
(0, react_1.useEffect)(() => {
|
|
331
|
+
allowContentHeightShrinkRef.current = true;
|
|
332
|
+
}, [resolvedContentRevision, renderJson, themeJson]);
|
|
157
333
|
const handleContentHeightChange = (0, react_1.useCallback)((event) => {
|
|
158
334
|
const nextHeight = event.nativeEvent.contentHeight;
|
|
159
|
-
|
|
335
|
+
if (nextHeight <= 0)
|
|
336
|
+
return;
|
|
337
|
+
setContentHeight((currentHeight) => currentHeight == null ||
|
|
338
|
+
nextHeight >= currentHeight ||
|
|
339
|
+
allowContentHeightShrinkRef.current
|
|
340
|
+
? (() => {
|
|
341
|
+
allowContentHeightShrinkRef.current = false;
|
|
342
|
+
return currentHeight === nextHeight
|
|
343
|
+
? currentHeight
|
|
344
|
+
: nextHeight;
|
|
345
|
+
})()
|
|
346
|
+
: currentHeight);
|
|
160
347
|
}, []);
|
|
161
348
|
const handlePressMention = (0, react_1.useCallback)((event) => {
|
|
162
349
|
if (!onPressMention)
|
|
@@ -165,10 +352,22 @@ function NativeProseViewer({ contentJSON, contentJSONRevision, schema, theme, st
|
|
|
165
352
|
const resolvedMention = mentionPayloadsByDocPos.get(docPos);
|
|
166
353
|
onPressMention({
|
|
167
354
|
docPos,
|
|
168
|
-
label: resolvedMention?.
|
|
355
|
+
label: resolvedMention?.renderedLabel ?? label,
|
|
169
356
|
attrs: resolvedMention?.attrs ?? {},
|
|
170
357
|
});
|
|
171
358
|
}, [mentionPayloadsByDocPos, onPressMention]);
|
|
172
|
-
const
|
|
173
|
-
|
|
359
|
+
const handlePressLink = (0, react_1.useCallback)((event) => {
|
|
360
|
+
if (!onPressLink)
|
|
361
|
+
return;
|
|
362
|
+
onPressLink({
|
|
363
|
+
href: event.nativeEvent.href,
|
|
364
|
+
text: event.nativeEvent.text,
|
|
365
|
+
});
|
|
366
|
+
}, [onPressLink]);
|
|
367
|
+
const nativeStyle = (0, react_1.useMemo)(() => [
|
|
368
|
+
{ minHeight: 1 },
|
|
369
|
+
style,
|
|
370
|
+
contentHeight != null ? { minHeight: contentHeight } : null,
|
|
371
|
+
], [contentHeight, style]);
|
|
372
|
+
return ((0, jsx_runtime_1.jsx)(NativeProseViewerView, { style: nativeStyle, renderJson: renderJson, themeJson: themeJson, enableLinkTaps: enableLinkTaps, interceptLinkTaps: typeof onPressLink === 'function', onContentHeightChange: handleContentHeightChange, onPressLink: typeof onPressLink === 'function' ? handlePressLink : undefined, onPressMention: typeof onPressMention === 'function' ? handlePressMention : undefined }));
|
|
174
373
|
}
|
|
@@ -63,6 +63,8 @@ export interface NativeRichTextEditorProps {
|
|
|
63
63
|
onRequestLink?: (context: LinkRequestContext) => void;
|
|
64
64
|
/** Called when a toolbar image item is pressed so the host can choose an image source. */
|
|
65
65
|
onRequestImage?: (context: ImageRequestContext) => void;
|
|
66
|
+
/** Whether plain URLs typed or pasted into the editor should be converted into link marks automatically. */
|
|
67
|
+
autoDetectLinks?: boolean;
|
|
66
68
|
/** Whether `data:image/...` sources are accepted for image insertion and HTML parsing. */
|
|
67
69
|
allowBase64Images?: boolean;
|
|
68
70
|
/** Whether selected images show native resize handles. */
|