@apollohg/react-native-rich-text-editor 2.0.0 → 2.0.2
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 +8 -116
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/android/build.gradle +3 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextImages.kt +25 -0
- package/android/src/main/java/com/apollohg/editor/EditorV2RenderValidation.kt +1 -0
- package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +101 -33
- package/android/src/main/java/com/apollohg/editor/RenderMediaSpans.kt +15 -3
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +14 -0
- package/android/src/main/java/com/apollohg/editor/viewer/AndroidProseLayoutEngine.kt +1 -1
- package/app.plugin.js +42 -39
- package/dist/AtomHost.js +18 -11
- package/dist/EditorAddon.js +4 -2
- package/dist/EditorAddonNormalization.js +17 -9
- package/dist/EditorMentionThemeNormalization.js +6 -3
- package/dist/EditorMentionThemeValidation.js +28 -11
- package/dist/EditorStyleSheetNormalization.js +85 -38
- package/dist/EditorStyleSheetTypes.d.ts +1 -1
- package/dist/EditorTheme.d.ts +4 -0
- package/dist/EditorTheme.js +5 -3
- package/dist/EditorToolbarComponent.d.ts +1 -1
- package/dist/EditorToolbarComponent.js +1 -1
- package/dist/EditorToolbarRegistry.js +5 -5
- package/dist/EditorToolbarTypes.d.ts +12 -12
- package/dist/EditorToolbarVisuals.js +1 -1
- package/dist/ExternalTextComposition.js +89 -43
- package/dist/ImageLoadingPolicy.d.ts +1 -1
- package/dist/NativeEditorBoundaryError.d.ts +7 -7
- package/dist/NativeEditorBoundaryError.js +9 -5
- package/dist/NativeEditorCollaborationTransport.d.ts +7 -7
- package/dist/NativeEditorCollaborationTransport.js +28 -15
- package/dist/NativeEditorCreateConfig.js +11 -6
- package/dist/NativeEditorCreateJson.js +51 -25
- package/dist/NativeEditorCreateValidation.js +11 -6
- package/dist/NativeEditorDocumentBridge.d.ts +1 -1
- package/dist/NativeEditorDocumentBridge.js +32 -18
- package/dist/NativeEditorDocumentHandle.d.ts +4 -4
- package/dist/NativeEditorDocumentHandle.js +1 -1
- package/dist/NativeEditorLocalAwareness.js +25 -13
- package/dist/NativeEditorNativeModule.d.ts +8 -1
- package/dist/NativeEditorNativeModule.js +7 -5
- package/dist/NativeEditorRenderNormalization.js +66 -25
- package/dist/NativeEditorResultNormalization.js +53 -29
- package/dist/NativeEditorTypes.d.ts +10 -10
- package/dist/NativeEditorV2Decimal.js +6 -3
- package/dist/NativeProseViewer.js +11 -3
- package/dist/ResourceLimits.d.ts +2 -2
- package/dist/ResourceLimits.js +7 -2
- package/dist/RichTextEditor.js +1 -1
- package/dist/RichTextEditorNativeTypes.d.ts +1 -1
- package/dist/RichTextEditorNativeView.d.ts +1 -3
- package/dist/RichTextEditorSerialization.js +24 -14
- package/dist/RichTextEditorTypes.d.ts +1 -1
- package/dist/YjsCollaboration.d.ts +7 -7
- package/dist/YjsCollaboration.js +42 -26
- package/dist/addons.d.ts +4 -4
- package/dist/addons.js +3 -3
- package/dist/atomInstances.js +6 -3
- package/dist/atomUpdates.js +7 -4
- package/dist/atoms.js +20 -12
- package/dist/attributeValidation.js +55 -24
- package/dist/contentExpression.js +80 -42
- package/dist/listMarkers.d.ts +1 -1
- package/dist/listMarkers.js +1 -1
- package/dist/schemaDefinition.d.ts +1 -1
- package/dist/schemaDefinition.js +23 -14
- package/dist/schemaDocument.js +1 -1
- package/dist/schemaDocumentConstruction.js +25 -14
- package/dist/schemaNormalization.js +51 -29
- package/dist/schemaPresets.d.ts +1 -1
- package/dist/schemaPresets.js +9 -4
- package/dist/schemaResolution.js +46 -25
- package/dist/specs/PreparedProseViewerNativeComponent.d.ts +4 -0
- package/dist/useEditorToolbarInteractions.d.ts +2 -2
- package/dist/useEditorToolbarInteractions.js +45 -23
- package/dist/useEditorToolbarItems.d.ts +1 -1
- package/dist/useEditorToolbarItems.js +4 -3
- package/dist/useEditorToolbarPresentation.d.ts +3 -3
- package/dist/useEditorToolbarPresentation.js +4 -4
- package/dist/useEditorToolbarState.d.ts +1 -1
- package/dist/useEditorToolbarState.js +1 -1
- package/dist/useFocusPreservingFrames.js +11 -8
- package/dist/useNativeEditor.d.ts +1 -1
- package/dist/useNativeEditor.js +21 -11
- package/dist/useRichTextEditorCommands.d.ts +2 -2
- package/dist/useRichTextEditorCommands.js +59 -29
- package/dist/useRichTextEditorEvents.d.ts +3 -3
- package/dist/useRichTextEditorEvents.js +75 -32
- package/dist/useRichTextEditorMentions.d.ts +4 -4
- package/dist/useRichTextEditorMentions.js +48 -29
- package/dist/useRichTextEditorPresentation.d.ts +4 -4
- package/dist/useRichTextEditorPresentation.js +25 -23
- package/dist/useRichTextEditorState.d.ts +3 -3
- package/dist/useRichTextEditorState.js +20 -15
- package/dist/useRichTextEditorUpdates.d.ts +1 -1
- package/dist/useRichTextEditorUpdates.js +99 -41
- package/dist/useViewerAtoms.js +63 -36
- 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/EditorInteractionOverlays.swift +114 -122
- package/ios/EditorLayoutManager.swift +13 -3
- package/ios/EditorStyleSheet.swift +12 -1
- package/ios/EditorTextView+Images.swift +32 -10
- package/ios/EditorTextView.swift +1 -1
- package/ios/EditorV2Adapter+RenderValidation.swift +2 -2
- package/ios/NativeToolbar+ButtonStyle.swift +17 -5
- package/ios/NativeToolbar+Diagnostics.swift +15 -0
- package/ios/RenderBridge+Atoms.swift +0 -1
- package/ios/RenderBridge+Attachments.swift +0 -29
- package/ios/RichTextEditorView.swift +4 -12
- package/ios/Viewer/CoreTextProseLayoutEngine+AttributedText.swift +3 -2
- package/package.json +11 -3
- package/react-native.config.js +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/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/src/AtomHost.tsx +58 -28
- package/src/EditorAddon.ts +18 -8
- package/src/EditorAddonNormalization.ts +40 -11
- package/src/EditorMentionThemeNormalization.ts +15 -4
- package/src/EditorMentionThemeValidation.ts +51 -14
- package/src/EditorStyleSheet.ts +1 -0
- package/src/EditorStyleSheetNormalization.ts +195 -64
- package/src/EditorStyleSheetTypes.ts +1 -1
- package/src/EditorTheme.ts +13 -5
- package/src/EditorToolbarComponent.tsx +2 -1
- package/src/EditorToolbarRegistry.tsx +23 -13
- package/src/EditorToolbarTypes.tsx +12 -12
- package/src/EditorToolbarVisuals.tsx +6 -3
- package/src/EditorUpdateRevision.ts +1 -0
- package/src/ExternalTextComposition.ts +259 -49
- package/src/ImageLoadingPolicy.ts +3 -1
- package/src/NativeEditorBoundaryError.ts +38 -11
- package/src/NativeEditorCollaborationTransport.ts +108 -27
- package/src/NativeEditorCreateConfig.ts +92 -19
- package/src/NativeEditorCreateJson.ts +163 -26
- package/src/NativeEditorCreateValidation.ts +33 -12
- package/src/NativeEditorDocumentBridge.ts +111 -19
- package/src/NativeEditorDocumentHandle.ts +9 -4
- package/src/NativeEditorLocalAwareness.ts +70 -15
- package/src/NativeEditorNativeModule.ts +13 -9
- package/src/NativeEditorRenderNormalization.ts +128 -29
- package/src/NativeEditorResultNormalization.ts +143 -32
- package/src/NativeEditorTypes.ts +12 -11
- package/src/NativeEditorV2Decimal.ts +12 -3
- package/src/NativeProseViewer.tsx +46 -16
- package/src/ResourceLimits.ts +15 -8
- package/src/RichTextEditor.tsx +3 -1
- package/src/RichTextEditorNativeTypes.tsx +1 -1
- package/src/RichTextEditorNativeView.tsx +2 -8
- package/src/RichTextEditorSerialization.tsx +70 -24
- package/src/RichTextEditorTypes.tsx +1 -1
- package/src/YjsCollaboration.ts +143 -47
- package/src/addons.ts +12 -8
- package/src/atomInstances.ts +25 -5
- package/src/atomUpdates.ts +23 -6
- package/src/atoms.ts +85 -18
- package/src/attributeValidation.ts +97 -33
- package/src/contentExpression.ts +218 -63
- package/src/listMarkers.ts +1 -1
- package/src/schemaDefinition.ts +82 -20
- package/src/schemaDocument.ts +15 -1
- package/src/schemaDocumentConstruction.ts +80 -16
- package/src/schemaNormalization.ts +155 -41
- package/src/schemaPresets.ts +37 -31
- package/src/schemaResolution.ts +126 -27
- package/src/specs/PreparedProseViewerNativeComponent.ts +4 -2
- package/src/useEditorToolbarInteractions.tsx +85 -36
- package/src/useEditorToolbarItems.tsx +50 -22
- package/src/useEditorToolbarPresentation.tsx +81 -58
- package/src/useEditorToolbarState.tsx +8 -6
- package/src/useFocusPreservingFrames.ts +43 -15
- package/src/useNativeEditor.ts +111 -51
- package/src/useRichTextEditorCommands.tsx +127 -54
- package/src/useRichTextEditorEvents.tsx +144 -42
- package/src/useRichTextEditorMentions.tsx +118 -37
- package/src/useRichTextEditorPresentation.tsx +128 -114
- package/src/useRichTextEditorState.tsx +76 -40
- package/src/useRichTextEditorUpdates.tsx +207 -56
- package/src/useViewerAtoms.tsx +162 -66
package/dist/useViewerAtoms.js
CHANGED
|
@@ -15,8 +15,9 @@ function isRecord(value) {
|
|
|
15
15
|
}
|
|
16
16
|
function parsePositions(json, definitions, snapshot) {
|
|
17
17
|
const values = JSON.parse(json);
|
|
18
|
-
if (!Array.isArray(values))
|
|
18
|
+
if (!Array.isArray(values)) {
|
|
19
19
|
throw new Error('Atom positions must be an array.');
|
|
20
|
+
}
|
|
20
21
|
const seen = new Set();
|
|
21
22
|
const identities = new Set();
|
|
22
23
|
return values.map((value) => {
|
|
@@ -27,24 +28,27 @@ function parsePositions(json, definitions, snapshot) {
|
|
|
27
28
|
Number(value.docPos) < 0 ||
|
|
28
29
|
Number(value.docPos) > 0xffffffff ||
|
|
29
30
|
typeof value.attrsJson !== 'string' ||
|
|
30
|
-
!['x', 'y', 'width', 'height'].every(
|
|
31
|
+
!['x', 'y', 'width', 'height'].every(key => typeof value[key] === 'number' && Number.isFinite(value[key])) ||
|
|
31
32
|
Number(value.width) <= 0 ||
|
|
32
33
|
Number(value.height) < 0 ||
|
|
33
34
|
seen.has(Number(value.docPos))) {
|
|
34
35
|
throw new Error('Invalid prepared atom position.');
|
|
35
36
|
}
|
|
36
37
|
const attrs = JSON.parse(value.attrsJson);
|
|
37
|
-
if (!isRecord(attrs))
|
|
38
|
+
if (!isRecord(attrs)) {
|
|
38
39
|
throw new Error('Atom attributes must be a JSON object.');
|
|
40
|
+
}
|
|
39
41
|
const idAttribute = definitions.get(value.nodeType)?.idAttribute;
|
|
40
42
|
const atomId = idAttribute == null ? undefined : attrs[idAttribute];
|
|
41
|
-
if (idAttribute != null && (typeof atomId !== 'string' || atomId.length === 0))
|
|
43
|
+
if (idAttribute != null && (typeof atomId !== 'string' || atomId.length === 0)) {
|
|
42
44
|
throw new Error('Atom identity must be a non-empty string.');
|
|
45
|
+
}
|
|
43
46
|
const key = typeof atomId === 'string'
|
|
44
47
|
? JSON.stringify([value.nodeType, atomId])
|
|
45
|
-
: `${snapshot}:${value.docPos}:${value.nodeType}`;
|
|
46
|
-
if (identities.has(key))
|
|
48
|
+
: `${snapshot}:${String(value.docPos)}:${value.nodeType}`;
|
|
49
|
+
if (identities.has(key)) {
|
|
47
50
|
throw new Error('Duplicate atom identity.');
|
|
51
|
+
}
|
|
48
52
|
identities.add(key);
|
|
49
53
|
seen.add(Number(value.docPos));
|
|
50
54
|
return { ...value, attrs, key, atomId };
|
|
@@ -52,18 +56,20 @@ function parsePositions(json, definitions, snapshot) {
|
|
|
52
56
|
}
|
|
53
57
|
function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = true, viewport, onUpdateAtomAttrs, onError, }) {
|
|
54
58
|
const [width, setWidth] = (0, react_1.useState)(null);
|
|
55
|
-
const snapshot = (0, react_1.useMemo)(() => String(++nextGeneration), [identity]);
|
|
59
|
+
const snapshot = (0, react_1.useMemo)(() => ({ identity, value: String(++nextGeneration) }), [identity]).value;
|
|
56
60
|
const serializedAtoms = (0, atoms_1.serializeEditorAtoms)(atoms);
|
|
57
|
-
const identifiers = JSON.stringify((atoms ?? []).map(
|
|
58
|
-
const generation = (0, react_1.useMemo)(() =>
|
|
59
|
-
|
|
61
|
+
const identifiers = JSON.stringify((atoms ?? []).map(atom => [atom.name, atom.idAttribute]));
|
|
62
|
+
const generation = (0, react_1.useMemo)(() => ({
|
|
63
|
+
identity, identifiers, serializedAtoms, width, value: String(++nextGeneration),
|
|
64
|
+
}), [identity, identifiers, serializedAtoms, width]).value;
|
|
65
|
+
const definitions = (0, react_1.useMemo)(() => new Map((atoms ?? []).map(atom => [atom.name, atom])), [atoms]);
|
|
60
66
|
const enabled = definitions.size > 0;
|
|
61
67
|
const [measurementState, setMeasurementState] = (0, react_1.useState)({
|
|
62
68
|
generation,
|
|
63
69
|
revision: 0,
|
|
64
70
|
values: {},
|
|
65
71
|
});
|
|
66
|
-
const measurements = measurementState.generation === generation ? measurementState.values : {};
|
|
72
|
+
const measurements = (0, react_1.useMemo)(() => measurementState.generation === generation ? measurementState.values : {}, [measurementState, generation]);
|
|
67
73
|
const revision = String(measurementState.generation === generation ? measurementState.revision : 0);
|
|
68
74
|
const [layout, setLayout] = (0, react_1.useState)(null);
|
|
69
75
|
const pendingLayout = layout?.generation !== generation;
|
|
@@ -72,8 +78,9 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
|
|
|
72
78
|
const mountedMeasurements = (0, react_1.useRef)(new Map());
|
|
73
79
|
(0, react_1.useLayoutEffect)(() => {
|
|
74
80
|
for (const [key, measured] of mountedMeasurements.current) {
|
|
75
|
-
if (!definitions.has(measured.nodeType))
|
|
81
|
+
if (!definitions.has(measured.nodeType)) {
|
|
76
82
|
mountedMeasurements.current.delete(key);
|
|
83
|
+
}
|
|
77
84
|
}
|
|
78
85
|
}, [definitions]);
|
|
79
86
|
const mounted = (0, react_1.useRef)(false);
|
|
@@ -119,7 +126,12 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
|
|
|
119
126
|
measurements,
|
|
120
127
|
},
|
|
121
128
|
})
|
|
122
|
-
: themeJson, [enabled,
|
|
129
|
+
: themeJson, [enabled,
|
|
130
|
+
themeJson,
|
|
131
|
+
serializedAtoms,
|
|
132
|
+
generation,
|
|
133
|
+
revision,
|
|
134
|
+
measurements]);
|
|
123
135
|
const onAtomLayout = (0, react_1.useCallback)((event) => {
|
|
124
136
|
const value = event.nativeEvent;
|
|
125
137
|
const latest = current.current;
|
|
@@ -128,8 +140,9 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
|
|
|
128
140
|
value.revision !== latest.revision ||
|
|
129
141
|
!Number.isFinite(value.layoutWidth) ||
|
|
130
142
|
value.layoutWidth <= 0 ||
|
|
131
|
-
(latest.width != null && Math.abs(latest.width - value.layoutWidth) > 1))
|
|
143
|
+
(latest.width != null && Math.abs(latest.width - value.layoutWidth) > 1)) {
|
|
132
144
|
return;
|
|
145
|
+
}
|
|
133
146
|
try {
|
|
134
147
|
const next = parsePositions(value.atomsJson, latest.definitions, latest.snapshot);
|
|
135
148
|
const retainedMeasurements = {};
|
|
@@ -143,18 +156,19 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
|
|
|
143
156
|
}
|
|
144
157
|
}
|
|
145
158
|
for (const key of mountedMeasurements.current.keys()) {
|
|
146
|
-
if (!retainedKeys.has(key))
|
|
159
|
+
if (!retainedKeys.has(key)) {
|
|
147
160
|
mountedMeasurements.current.delete(key);
|
|
161
|
+
}
|
|
148
162
|
}
|
|
149
163
|
// Unchanged mounted sizes do not emit another Yoga onLayout event.
|
|
150
|
-
setMeasurementState(
|
|
164
|
+
setMeasurementState(previous => previous.generation === value.generation
|
|
151
165
|
? previous
|
|
152
166
|
: {
|
|
153
167
|
generation: value.generation,
|
|
154
168
|
revision: Object.keys(retainedMeasurements).length > 0 ? 1 : 0,
|
|
155
169
|
values: retainedMeasurements,
|
|
156
170
|
});
|
|
157
|
-
setLayout(
|
|
171
|
+
setLayout(previous => previous?.generation === value.generation && previous.json === value.atomsJson
|
|
158
172
|
? previous
|
|
159
173
|
: { generation: value.generation, json: value.atomsJson, positions: next });
|
|
160
174
|
}
|
|
@@ -171,8 +185,9 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
|
|
|
171
185
|
}, []);
|
|
172
186
|
const onContainerLayout = (0, react_1.useCallback)((event) => {
|
|
173
187
|
const next = event.nativeEvent.layout.width;
|
|
174
|
-
if (Number.isFinite(next) && next > 0)
|
|
175
|
-
setWidth(
|
|
188
|
+
if (Number.isFinite(next) && next > 0) {
|
|
189
|
+
setWidth(previous => (previous === next ? previous : next));
|
|
190
|
+
}
|
|
176
191
|
}, []);
|
|
177
192
|
const updateQueues = (0, react_1.useRef)(new Map());
|
|
178
193
|
const acknowledged = (0, react_1.useRef)(new Map());
|
|
@@ -182,14 +197,16 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
|
|
|
182
197
|
const updateAttrs = (0, react_1.useCallback)((owner, atom, update) => {
|
|
183
198
|
const execute = async () => {
|
|
184
199
|
const latest = current.current;
|
|
185
|
-
if (!mounted.current)
|
|
200
|
+
if (!mounted.current) {
|
|
186
201
|
throw new atomInstances_1.AtomUpdateAttrsError('not-ready', 'The viewer is unmounted.');
|
|
202
|
+
}
|
|
187
203
|
if (owner !== latest.generation ||
|
|
188
|
-
!latest.positions.some(
|
|
204
|
+
!latest.positions.some(position => position.key === atom.key && position.attrsJson === atom.attrsJson)) {
|
|
189
205
|
throw new atomInstances_1.AtomUpdateAttrsError('stale-revision', 'The viewer content has changed.');
|
|
190
206
|
}
|
|
191
|
-
if (latest.readOnly || !latest.onUpdateAtomAttrs)
|
|
207
|
+
if (latest.readOnly || !latest.onUpdateAtomAttrs) {
|
|
192
208
|
throw new atomInstances_1.AtomUpdateAttrsError('not-applicable', 'Viewer updates require readOnly={false} and onUpdateAtomAttrs.');
|
|
209
|
+
}
|
|
193
210
|
const accepted = acknowledged.current.get(atom.key);
|
|
194
211
|
const attrs = accepted?.generation === owner ? accepted.attrs : atom.attrs;
|
|
195
212
|
const partial = (0, atomUpdates_1.resolveAtomAttrsUpdate)(attrs, update);
|
|
@@ -198,8 +215,9 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
|
|
|
198
215
|
(0, attributeValidation_1.validateAttributes)({ ...attrs, ...partial }, definition.attrs ?? {});
|
|
199
216
|
if (definition.idAttribute &&
|
|
200
217
|
Object.prototype.hasOwnProperty.call(partial, definition.idAttribute) &&
|
|
201
|
-
partial[definition.idAttribute] !== attrs[definition.idAttribute])
|
|
218
|
+
partial[definition.idAttribute] !== attrs[definition.idAttribute]) {
|
|
202
219
|
throw new Error('An atom identity cannot be changed.');
|
|
220
|
+
}
|
|
203
221
|
}
|
|
204
222
|
catch (error) {
|
|
205
223
|
throw new atomInstances_1.AtomUpdateAttrsError('not-applicable', error instanceof Error ? error.message : String(error));
|
|
@@ -211,18 +229,21 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
|
|
|
211
229
|
attrs,
|
|
212
230
|
partial,
|
|
213
231
|
});
|
|
214
|
-
if (mounted.current && current.current.generation === owner)
|
|
232
|
+
if (mounted.current && current.current.generation === owner) {
|
|
215
233
|
acknowledged.current.set(atom.key, {
|
|
216
234
|
generation: owner,
|
|
217
235
|
attrs: { ...attrs, ...partial },
|
|
218
236
|
});
|
|
237
|
+
}
|
|
219
238
|
};
|
|
220
239
|
const previous = updateQueues.current.get(atom.key);
|
|
221
|
-
const result = previous ? previous.catch(() => {
|
|
240
|
+
const result = previous ? previous.catch(() => {
|
|
241
|
+
}).then(execute) : execute();
|
|
222
242
|
updateQueues.current.set(atom.key, result);
|
|
223
243
|
const clean = () => {
|
|
224
|
-
if (updateQueues.current.get(atom.key) === result)
|
|
244
|
+
if (updateQueues.current.get(atom.key) === result) {
|
|
225
245
|
updateQueues.current.delete(atom.key);
|
|
246
|
+
}
|
|
226
247
|
};
|
|
227
248
|
void result.then(clean, clean);
|
|
228
249
|
return result;
|
|
@@ -238,22 +259,25 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
|
|
|
238
259
|
!Number.isFinite(measured.height) ||
|
|
239
260
|
measured.height < 0 ||
|
|
240
261
|
Math.abs(measured.width - atom.width) > 1 ||
|
|
241
|
-
!latest.renderedPositions.some(
|
|
262
|
+
!latest.renderedPositions.some(position => position.docPos === atom.docPos &&
|
|
242
263
|
position.nodeType === atom.nodeType &&
|
|
243
264
|
position.width === atom.width &&
|
|
244
|
-
position.attrsJson === atom.attrsJson))
|
|
265
|
+
position.attrsJson === atom.attrsJson)) {
|
|
245
266
|
return;
|
|
267
|
+
}
|
|
246
268
|
mountedMeasurements.current.set(atom.key, {
|
|
247
269
|
nodeType: atom.nodeType,
|
|
248
270
|
size: { width: atom.width, height: measured.height },
|
|
249
271
|
});
|
|
250
|
-
if (latest.pendingLayout)
|
|
272
|
+
if (latest.pendingLayout) {
|
|
251
273
|
return;
|
|
252
|
-
|
|
274
|
+
}
|
|
275
|
+
setMeasurementState(previous => {
|
|
253
276
|
const values = previous.generation === owner ? previous.values : {};
|
|
254
277
|
const existing = values[String(atom.docPos)];
|
|
255
|
-
if (existing?.width === atom.width && existing.height === measured.height)
|
|
278
|
+
if (existing?.width === atom.width && existing.height === measured.height) {
|
|
256
279
|
return previous;
|
|
280
|
+
}
|
|
257
281
|
return {
|
|
258
282
|
generation: owner,
|
|
259
283
|
revision: previous.generation === owner ? previous.revision + 1 : 1,
|
|
@@ -264,25 +288,28 @@ function useViewerAtoms({ atoms, identity, themeJson, readOnly, interactive = tr
|
|
|
264
288
|
};
|
|
265
289
|
});
|
|
266
290
|
}, []);
|
|
267
|
-
const children = renderedPositions.map(
|
|
291
|
+
const children = renderedPositions.map(atom => {
|
|
268
292
|
const Component = definitions.get(atom.nodeType)?.component;
|
|
269
|
-
if (!Component)
|
|
293
|
+
if (!Component) {
|
|
270
294
|
return null;
|
|
295
|
+
}
|
|
271
296
|
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { collapsable: false, pointerEvents: !interactive || pendingLayout ? 'none' : 'box-none', accessibilityElementsHidden: pendingLayout, importantForAccessibility: pendingLayout ? 'no-hide-descendants' : 'auto', style: {
|
|
272
297
|
position: 'absolute',
|
|
273
298
|
left: atom.x,
|
|
274
299
|
top: atom.y,
|
|
275
300
|
width: atom.width,
|
|
276
301
|
opacity: pendingLayout ? 0 : 1,
|
|
277
|
-
}, onLayout:
|
|
302
|
+
}, onLayout: event => measure(generation, atom, Component, event), children: (0, jsx_runtime_1.jsx)(AtomHost_1.AtomHost, { component: Component, width: atom.width, estimatedHeight: atom.height, visible: (0, AtomHost_1.atomIsVisible)(atom.y, atom.height, viewport), onMeasure: event => measure(generation, atom, Component, event), atomProps: {
|
|
278
303
|
attrs: atom.attrs,
|
|
279
304
|
nodeType: atom.nodeType,
|
|
280
305
|
selected: false,
|
|
281
306
|
readOnly,
|
|
282
307
|
interactive: interactive && !pendingLayout,
|
|
283
308
|
isViewer: true,
|
|
284
|
-
updateAttrs:
|
|
309
|
+
updateAttrs: partial => updateAttrs(layout.generation, atom, partial),
|
|
285
310
|
} }) }, atom.key));
|
|
286
311
|
});
|
|
287
|
-
return {
|
|
312
|
+
return {
|
|
313
|
+
enabled, themeJson: configuredThemeJson, onAtomLayout, onContainerLayout, children,
|
|
314
|
+
};
|
|
288
315
|
}
|
|
Binary file
|
|
Binary file
|
|
@@ -408,38 +408,29 @@ final class ImageResizeOverlayView: UIView {
|
|
|
408
408
|
|
|
409
409
|
private struct DragState {
|
|
410
410
|
let corner: Corner
|
|
411
|
-
let
|
|
411
|
+
let originalSize: CGSize
|
|
412
412
|
let docPos: UInt32
|
|
413
413
|
let maximumWidth: CGFloat
|
|
414
|
+
let editorId: UInt64
|
|
415
|
+
let attachment: BlockImageAttachment
|
|
416
|
+
let originalWidth: CGFloat?
|
|
417
|
+
let originalHeight: CGFloat?
|
|
418
|
+
var previewSize: CGSize
|
|
414
419
|
}
|
|
415
420
|
|
|
416
421
|
private weak var editorView: RichTextEditorView?
|
|
417
422
|
private let selectionLayer = CAShapeLayer()
|
|
418
|
-
private let previewBackdropView = UIView()
|
|
419
|
-
private let previewImageView = UIImageView()
|
|
420
423
|
private var handleViews: [Corner: ImageResizeHandleView] = [:]
|
|
421
424
|
private var currentRect: CGRect?
|
|
422
425
|
private var currentDocPos: UInt32?
|
|
423
426
|
private var dragState: DragState?
|
|
424
427
|
private let handleSize: CGFloat = 20
|
|
425
|
-
private let minimumImageSize: CGFloat = 48
|
|
426
428
|
|
|
427
429
|
override init(frame: CGRect) {
|
|
428
430
|
super.init(frame: frame)
|
|
429
431
|
backgroundColor = .clear
|
|
430
432
|
clipsToBounds = true
|
|
431
433
|
|
|
432
|
-
previewBackdropView.isUserInteractionEnabled = false
|
|
433
|
-
previewBackdropView.isHidden = true
|
|
434
|
-
previewBackdropView.layer.zPosition = 1
|
|
435
|
-
addSubview(previewBackdropView)
|
|
436
|
-
|
|
437
|
-
previewImageView.isUserInteractionEnabled = false
|
|
438
|
-
previewImageView.isHidden = true
|
|
439
|
-
previewImageView.contentMode = .scaleToFill
|
|
440
|
-
previewImageView.layer.zPosition = 2
|
|
441
|
-
addSubview(previewImageView)
|
|
442
|
-
|
|
443
434
|
selectionLayer.strokeColor = UIColor.systemBlue.cgColor
|
|
444
435
|
selectionLayer.fillColor = UIColor.clear.cgColor
|
|
445
436
|
selectionLayer.lineWidth = 2
|
|
@@ -467,8 +458,8 @@ final class ImageResizeOverlayView: UIView {
|
|
|
467
458
|
}
|
|
468
459
|
|
|
469
460
|
func refresh() {
|
|
470
|
-
if dragState
|
|
471
|
-
|
|
461
|
+
if let dragState, !isCurrentDrag(dragState) {
|
|
462
|
+
finishPreviewResize(commit: false)
|
|
472
463
|
}
|
|
473
464
|
|
|
474
465
|
guard let editorView,
|
|
@@ -478,7 +469,6 @@ final class ImageResizeOverlayView: UIView {
|
|
|
478
469
|
return
|
|
479
470
|
}
|
|
480
471
|
|
|
481
|
-
hidePreviewLayers()
|
|
482
472
|
applyGeometry(rect: geometry.rect, docPos: geometry.docPos)
|
|
483
473
|
}
|
|
484
474
|
|
|
@@ -488,15 +478,16 @@ final class ImageResizeOverlayView: UIView {
|
|
|
488
478
|
}
|
|
489
479
|
|
|
490
480
|
func simulatePreviewResizeForTesting(width: CGFloat, height: CGFloat) {
|
|
491
|
-
|
|
492
|
-
let
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
481
|
+
if dragState == nil, !beginPreviewResize(from: .bottomRight) { return }
|
|
482
|
+
let size = editorView?.clampedImageSize(
|
|
483
|
+
CGSize(width: width, height: height), maximumWidth: dragState?.maximumWidth
|
|
484
|
+
) ?? CGSize(width: width, height: height)
|
|
485
|
+
updatePreviewSize(size)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
func simulatePreviewDragForTesting(corner: Corner, translation: CGPoint) {
|
|
489
|
+
if dragState == nil, !beginPreviewResize(from: corner) { return }
|
|
490
|
+
updatePreview(translation: translation)
|
|
500
491
|
}
|
|
501
492
|
|
|
502
493
|
func commitPreviewResizeForTesting() {
|
|
@@ -511,10 +502,6 @@ final class ImageResizeOverlayView: UIView {
|
|
|
511
502
|
!isHidden
|
|
512
503
|
}
|
|
513
504
|
|
|
514
|
-
var previewHasImageForTesting: Bool {
|
|
515
|
-
!previewImageView.isHidden && previewImageView.image != nil
|
|
516
|
-
}
|
|
517
|
-
|
|
518
505
|
func interceptsPointForTesting(_ location: CGPoint) -> Bool {
|
|
519
506
|
self.point(inside: location, with: nil)
|
|
520
507
|
}
|
|
@@ -530,8 +517,7 @@ final class ImageResizeOverlayView: UIView {
|
|
|
530
517
|
}
|
|
531
518
|
|
|
532
519
|
private func hideOverlay() {
|
|
533
|
-
|
|
534
|
-
dragState = nil
|
|
520
|
+
if dragState != nil { finishPreviewResize(commit: false) }
|
|
535
521
|
currentRect = nil
|
|
536
522
|
currentDocPos = nil
|
|
537
523
|
selectionLayer.path = nil
|
|
@@ -539,64 +525,94 @@ final class ImageResizeOverlayView: UIView {
|
|
|
539
525
|
}
|
|
540
526
|
|
|
541
527
|
private func applyGeometry(rect: CGRect, docPos: UInt32) {
|
|
542
|
-
|
|
543
|
-
currentRect =
|
|
528
|
+
guard currentRect != rect || currentDocPos != docPos || isHidden else { return }
|
|
529
|
+
currentRect = rect
|
|
544
530
|
currentDocPos = docPos
|
|
545
|
-
|
|
531
|
+
CATransaction.begin()
|
|
532
|
+
CATransaction.setDisableActions(true)
|
|
533
|
+
selectionLayer.path = UIBezierPath(roundedRect: rect, cornerRadius: 8).cgPath
|
|
546
534
|
isHidden = false
|
|
547
|
-
layoutHandleViews(for:
|
|
535
|
+
layoutHandleViews(for: rect)
|
|
536
|
+
CATransaction.commit()
|
|
548
537
|
}
|
|
549
538
|
|
|
550
|
-
private func
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
private func showPreview(docPos: UInt32, originalRect: CGRect) {
|
|
557
|
-
previewBackdropView.backgroundColor = editorView?.imageResizePreviewBackgroundColor() ?? .systemBackground
|
|
558
|
-
previewBackdropView.frame = originalRect
|
|
559
|
-
previewBackdropView.isHidden = false
|
|
560
|
-
|
|
561
|
-
previewImageView.image = editorView?.imagePreviewForResize(docPos: docPos)
|
|
562
|
-
previewImageView.frame = originalRect
|
|
563
|
-
previewImageView.isHidden = previewImageView.image == nil
|
|
539
|
+
private func isCurrentDrag(_ state: DragState) -> Bool {
|
|
540
|
+
guard let editorView, editorView.editorId == state.editorId,
|
|
541
|
+
editorView.textView.selectedImageSelectionState()?.docPos == state.docPos,
|
|
542
|
+
editorView.textView.blockImageAttachment(docPos: state.docPos)?.attachment === state.attachment
|
|
543
|
+
else { return false }
|
|
544
|
+
return true
|
|
564
545
|
}
|
|
565
546
|
|
|
566
547
|
@discardableResult
|
|
567
548
|
private func beginPreviewResize(from corner: Corner) -> Bool {
|
|
568
|
-
guard let currentRect, let currentDocPos
|
|
569
|
-
|
|
570
|
-
|
|
549
|
+
guard let editorView, let currentRect, let currentDocPos,
|
|
550
|
+
let image = editorView.textView.blockImageAttachment(docPos: currentDocPos)
|
|
551
|
+
else { return false }
|
|
552
|
+
editorView.setImageResizePreviewActive(true)
|
|
553
|
+
let inset = image.attachment.styleBox?.inset ?? .zero
|
|
554
|
+
let contentSize = CGSize(
|
|
555
|
+
width: max(1, currentRect.width - inset.left - inset.right),
|
|
556
|
+
height: max(1, currentRect.height - inset.top - inset.bottom)
|
|
557
|
+
)
|
|
571
558
|
dragState = DragState(
|
|
572
559
|
corner: corner,
|
|
573
|
-
|
|
560
|
+
originalSize: contentSize,
|
|
574
561
|
docPos: currentDocPos,
|
|
575
|
-
maximumWidth:
|
|
562
|
+
maximumWidth: max(48, editorView.maximumImageWidthForResizeGesture() - inset.left - inset.right),
|
|
563
|
+
editorId: editorView.editorId,
|
|
564
|
+
attachment: image.attachment,
|
|
565
|
+
originalWidth: image.attachment.preferredWidth,
|
|
566
|
+
originalHeight: image.attachment.preferredHeight,
|
|
567
|
+
previewSize: contentSize
|
|
576
568
|
)
|
|
577
|
-
showPreview(docPos: currentDocPos, originalRect: currentRect)
|
|
578
569
|
return true
|
|
579
570
|
}
|
|
580
571
|
|
|
581
|
-
private func
|
|
582
|
-
guard let
|
|
583
|
-
|
|
584
|
-
|
|
572
|
+
private func updatePreviewSize(_ proposedSize: CGSize) {
|
|
573
|
+
guard var state = dragState, let editorView else { return }
|
|
574
|
+
guard isCurrentDrag(state) else {
|
|
575
|
+
finishPreviewResize(commit: false)
|
|
576
|
+
return
|
|
577
|
+
}
|
|
578
|
+
let scale = window?.screen.scale ?? 1
|
|
579
|
+
let size = proposedSize == state.originalSize ? proposedSize : CGSize(
|
|
580
|
+
width: (proposedSize.width * scale).rounded() / scale,
|
|
581
|
+
height: (proposedSize.height * scale).rounded() / scale
|
|
582
|
+
)
|
|
583
|
+
guard size != state.previewSize else { return }
|
|
584
|
+
state.previewSize = size
|
|
585
|
+
dragState = state
|
|
586
|
+
UIView.performWithoutAnimation {
|
|
587
|
+
editorView.textView.previewResizeImageAtDocPos(
|
|
588
|
+
state.docPos,
|
|
589
|
+
width: size.width,
|
|
590
|
+
height: size.height
|
|
591
|
+
)
|
|
592
|
+
if let geometry = editorView.selectedImageGeometry() {
|
|
593
|
+
applyGeometry(rect: geometry.rect, docPos: geometry.docPos)
|
|
594
|
+
}
|
|
595
|
+
}
|
|
585
596
|
}
|
|
586
597
|
|
|
587
598
|
private func finishPreviewResize(commit: Bool) {
|
|
588
|
-
guard let dragState else { return }
|
|
589
|
-
let
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
editorView?.resizeImage(docPos: dragState.docPos, size: finalSize)
|
|
599
|
+
guard let state = dragState, let editorView else { return }
|
|
600
|
+
let shouldCommit = commit && isCurrentDrag(state)
|
|
601
|
+
dragState = nil
|
|
602
|
+
if shouldCommit, state.previewSize != state.originalSize {
|
|
603
|
+
editorView.resizeImage(docPos: state.docPos, size: state.previewSize)
|
|
594
604
|
} else {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
self?.refresh()
|
|
605
|
+
editorView.textView.restoreImageResizePreview(
|
|
606
|
+
state.attachment, width: state.originalWidth, height: state.originalHeight
|
|
607
|
+
)
|
|
599
608
|
}
|
|
609
|
+
editorView.setImageResizePreviewActive(false)
|
|
610
|
+
refresh()
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
override func didMoveToWindow() {
|
|
614
|
+
super.didMoveToWindow()
|
|
615
|
+
if window == nil { finishPreviewResize(commit: false) }
|
|
600
616
|
}
|
|
601
617
|
|
|
602
618
|
private func layoutHandleViews(for rect: CGRect) {
|
|
@@ -624,50 +640,30 @@ final class ImageResizeOverlayView: UIView {
|
|
|
624
640
|
}
|
|
625
641
|
}
|
|
626
642
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
return CGPoint(x: rect.maxX, y: rect.minY)
|
|
635
|
-
case .bottomRight:
|
|
636
|
-
return CGPoint(x: rect.minX, y: rect.minY)
|
|
637
|
-
}
|
|
643
|
+
static func resizedSize(from size: CGSize, corner: Corner, translation: CGPoint) -> CGSize {
|
|
644
|
+
let dx = (corner == .topRight || corner == .bottomRight) ? translation.x : -translation.x
|
|
645
|
+
let dy = (corner == .bottomLeft || corner == .bottomRight) ? translation.y : -translation.y
|
|
646
|
+
let lengthSquared = max(1, size.width * size.width + size.height * size.height)
|
|
647
|
+
let minimumScale = 48 / max(1, min(size.width, size.height))
|
|
648
|
+
let scale = max(minimumScale, 1 + (dx * size.width + dy * size.height) / lengthSquared)
|
|
649
|
+
return CGSize(width: size.width * scale, height: size.height * scale)
|
|
638
650
|
}
|
|
639
651
|
|
|
640
|
-
private func
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
) -> CGRect {
|
|
646
|
-
let aspectRatio = max(originalRect.width / max(originalRect.height, 1), 0.1)
|
|
647
|
-
let signedDx = (corner == .topRight || corner == .bottomRight) ? translation.x : -translation.x
|
|
648
|
-
let signedDy = (corner == .bottomLeft || corner == .bottomRight) ? translation.y : -translation.y
|
|
649
|
-
let widthScale = (originalRect.width + signedDx) / max(originalRect.width, 1)
|
|
650
|
-
let heightScale = (originalRect.height + signedDy) / max(originalRect.height, 1)
|
|
651
|
-
let scale = max(minimumImageSize / max(originalRect.width, 1), widthScale, heightScale)
|
|
652
|
-
let unclampedSize = CGSize(
|
|
653
|
-
width: max(minimumImageSize, originalRect.width * scale),
|
|
654
|
-
height: max(minimumImageSize / aspectRatio, (max(minimumImageSize, originalRect.width * scale) / aspectRatio))
|
|
655
|
-
)
|
|
656
|
-
let clampedSize = editorView?.clampedImageSize(unclampedSize, maximumWidth: maximumWidth) ?? unclampedSize
|
|
657
|
-
let width = clampedSize.width
|
|
658
|
-
let height = clampedSize.height
|
|
659
|
-
let anchor = anchorPoint(for: corner, in: originalRect)
|
|
660
|
-
|
|
661
|
-
switch corner {
|
|
662
|
-
case .topLeft:
|
|
663
|
-
return CGRect(x: anchor.x - width, y: anchor.y - height, width: width, height: height)
|
|
664
|
-
case .topRight:
|
|
665
|
-
return CGRect(x: anchor.x, y: anchor.y - height, width: width, height: height)
|
|
666
|
-
case .bottomLeft:
|
|
667
|
-
return CGRect(x: anchor.x - width, y: anchor.y, width: width, height: height)
|
|
668
|
-
case .bottomRight:
|
|
669
|
-
return CGRect(x: anchor.x, y: anchor.y, width: width, height: height)
|
|
652
|
+
private func updatePreview(translation: CGPoint) {
|
|
653
|
+
guard let dragState else { return }
|
|
654
|
+
if translation == .zero {
|
|
655
|
+
updatePreviewSize(dragState.originalSize)
|
|
656
|
+
return
|
|
670
657
|
}
|
|
658
|
+
let size = Self.resizedSize(
|
|
659
|
+
from: dragState.originalSize,
|
|
660
|
+
corner: dragState.corner,
|
|
661
|
+
translation: translation
|
|
662
|
+
)
|
|
663
|
+
let clampedSize = editorView?.clampedImageSize(
|
|
664
|
+
size, maximumWidth: dragState.maximumWidth
|
|
665
|
+
) ?? size
|
|
666
|
+
updatePreviewSize(clampedSize)
|
|
671
667
|
}
|
|
672
668
|
|
|
673
669
|
@objc
|
|
@@ -676,22 +672,18 @@ final class ImageResizeOverlayView: UIView {
|
|
|
676
672
|
|
|
677
673
|
switch gesture.state {
|
|
678
674
|
case .began:
|
|
679
|
-
|
|
675
|
+
if beginPreviewResize(from: handleView.corner) {
|
|
676
|
+
updatePreview(translation: gesture.translation(in: window))
|
|
677
|
+
}
|
|
680
678
|
case .changed:
|
|
681
|
-
|
|
682
|
-
let nextRect = resizedRect(
|
|
683
|
-
from: dragState.originalRect,
|
|
684
|
-
corner: dragState.corner,
|
|
685
|
-
translation: gesture.translation(in: self),
|
|
686
|
-
maximumWidth: dragState.maximumWidth
|
|
687
|
-
)
|
|
688
|
-
updatePreviewRect(nextRect)
|
|
679
|
+
updatePreview(translation: gesture.translation(in: window))
|
|
689
680
|
case .ended:
|
|
681
|
+
updatePreview(translation: gesture.translation(in: window))
|
|
690
682
|
finishPreviewResize(commit: true)
|
|
691
683
|
case .cancelled, .failed:
|
|
692
684
|
finishPreviewResize(commit: false)
|
|
693
685
|
default:
|
|
694
|
-
|
|
686
|
+
break
|
|
695
687
|
}
|
|
696
688
|
}
|
|
697
689
|
}
|
|
@@ -54,7 +54,16 @@ final class EditorLayoutManager: NSLayoutManager, NSLayoutManagerDelegate {
|
|
|
54
54
|
storage.enumerateAttribute(editorInlineLineHeightAttribute, in: characters) { value, _, _ in
|
|
55
55
|
height = max(height, EditorTheme.cgFloat(value) ?? 0)
|
|
56
56
|
}
|
|
57
|
+
var ascent = baselineOffset.pointee + (height - lineFragmentUsedRect.pointee.height) / 2
|
|
58
|
+
var descent = height - ascent
|
|
59
|
+
storage.enumerateAttribute(editorMentionBoxAttribute, in: characters) { value, _, _ in
|
|
60
|
+
guard let chip = value as? EditorMentionRenderedBox else { return }
|
|
61
|
+
ascent = max(ascent, chip.baselineOffset)
|
|
62
|
+
descent = max(descent, chip.size.height - chip.baselineOffset)
|
|
63
|
+
}
|
|
64
|
+
height = ascent + descent
|
|
57
65
|
let extra = height - lineFragmentUsedRect.pointee.height
|
|
66
|
+
let baselineAdjustment = ascent - baselineOffset.pointee
|
|
58
67
|
var leading: CGFloat = 0
|
|
59
68
|
var overlap: CGFloat = 0
|
|
60
69
|
if characters.location < storage.length,
|
|
@@ -76,7 +85,7 @@ final class EditorLayoutManager: NSLayoutManager, NSLayoutManagerDelegate {
|
|
|
76
85
|
lineFragmentRect.pointee.size.height += extra + leading
|
|
77
86
|
lineFragmentUsedRect.pointee.size.height = height
|
|
78
87
|
lineFragmentUsedRect.pointee.origin.y += leading + overlap
|
|
79
|
-
baselineOffset.pointee +=
|
|
88
|
+
baselineOffset.pointee += baselineAdjustment + leading
|
|
80
89
|
return true
|
|
81
90
|
}
|
|
82
91
|
|
|
@@ -226,10 +235,11 @@ final class EditorLayoutManager: NSLayoutManager, NSLayoutManagerDelegate {
|
|
|
226
235
|
storage.enumerateAttribute(editorMentionBoxAttribute, in: visible) { value, range, _ in
|
|
227
236
|
guard let box = value as? EditorMentionRenderedBox else { return }
|
|
228
237
|
let glyph = self.glyphIndexForCharacter(at: range.location)
|
|
229
|
-
let line = self.lineFragmentUsedRect(forGlyphAt: glyph, effectiveRange: nil)
|
|
230
238
|
let location = self.location(forGlyphAt: glyph)
|
|
231
239
|
let fragment = self.lineFragmentRect(forGlyphAt: glyph, effectiveRange: nil)
|
|
232
|
-
let rect = CGRect(x: fragment.minX + location.x + origin.x,
|
|
240
|
+
let rect = CGRect(x: fragment.minX + location.x + origin.x,
|
|
241
|
+
y: fragment.minY + location.y + origin.y - box.baselineOffset,
|
|
242
|
+
width: box.size.width, height: box.size.height)
|
|
233
243
|
box.box.draw(in: rect, context: context)
|
|
234
244
|
let labelHeight = box.label?.size().height ?? 0
|
|
235
245
|
box.label?.draw(at: CGPoint(x: rect.minX + box.padding.left,
|