@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
|
@@ -150,6 +150,14 @@ struct EditorStyleBox {
|
|
|
150
150
|
}
|
|
151
151
|
var inset: UIEdgeInsets { padding.adding(borders) }
|
|
152
152
|
var outerInsets: UIEdgeInsets { inset.adding(margin) }
|
|
153
|
+
var mentionInsets: UIEdgeInsets {
|
|
154
|
+
UIEdgeInsets(
|
|
155
|
+
top: number("paddingTop", fallback: number("padding", fallback: 4)),
|
|
156
|
+
left: number("paddingLeft", fallback: number("padding", fallback: 6)),
|
|
157
|
+
bottom: number("paddingBottom", fallback: number("padding", fallback: 4)),
|
|
158
|
+
right: number("paddingRight", fallback: number("padding", fallback: 6))
|
|
159
|
+
).adding(borders)
|
|
160
|
+
}
|
|
153
161
|
private func insets(_ key: String) -> UIEdgeInsets {
|
|
154
162
|
UIEdgeInsets(top: number(key + "Top", fallback: number(key)), left: number(key + "Left", fallback: number(key)), bottom: number(key + "Bottom", fallback: number(key)), right: number(key + "Right", fallback: number(key)))
|
|
155
163
|
}
|
|
@@ -410,15 +418,18 @@ final class EditorMentionRenderedBox: NSObject {
|
|
|
410
418
|
let label: NSAttributedString?
|
|
411
419
|
let size: CGSize
|
|
412
420
|
let padding: UIEdgeInsets
|
|
421
|
+
let baselineOffset: CGFloat
|
|
413
422
|
|
|
414
423
|
init(box: EditorStyleBox, label: NSAttributedString? = nil) {
|
|
415
424
|
self.box = box
|
|
416
425
|
self.label = label
|
|
417
|
-
padding =
|
|
426
|
+
padding = box.mentionInsets
|
|
418
427
|
let measured = label?.size() ?? .zero
|
|
419
428
|
let lineHeight = box.number("lineHeight", fallback: measured.height)
|
|
420
429
|
size = CGSize(width: ceil(measured.width) + padding.left + padding.right,
|
|
421
430
|
height: ceil(max(measured.height, lineHeight)) + padding.top + padding.bottom)
|
|
431
|
+
let font = label.flatMap { $0.length > 0 ? $0.attribute(.font, at: 0, effectiveRange: nil) as? UIFont : nil }
|
|
432
|
+
baselineOffset = padding.top + (size.height - padding.top - padding.bottom - measured.height) / 2 + (font?.ascender ?? 0)
|
|
422
433
|
}
|
|
423
434
|
}
|
|
424
435
|
|
|
@@ -289,7 +289,7 @@ extension EditorTextView {
|
|
|
289
289
|
return (selectionState.docPos, rect)
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
|
|
292
|
+
func blockImageAttachment(docPos: UInt32) -> (range: NSRange, attachment: BlockImageAttachment)? {
|
|
293
293
|
let fullRange = NSRange(location: 0, length: textStorage.length)
|
|
294
294
|
var resolved: (range: NSRange, attachment: BlockImageAttachment)?
|
|
295
295
|
textStorage.enumerateAttribute(
|
|
@@ -309,10 +309,6 @@ extension EditorTextView {
|
|
|
309
309
|
return resolved
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
-
func imagePreviewForDocPos(_ docPos: UInt32) -> UIImage? {
|
|
313
|
-
blockImageAttachment(docPos: docPos)?.attachment.previewImage()
|
|
314
|
-
}
|
|
315
|
-
|
|
316
312
|
func maximumRenderableImageWidth() -> CGFloat {
|
|
317
313
|
let containerWidth: CGFloat
|
|
318
314
|
if bounds.width > 0 {
|
|
@@ -337,14 +333,40 @@ extension EditorTextView {
|
|
|
337
333
|
}
|
|
338
334
|
}
|
|
339
335
|
|
|
340
|
-
func previewResizeImageAtDocPos(_ docPos: UInt32, width: CGFloat
|
|
336
|
+
func previewResizeImageAtDocPos(_ docPos: UInt32, width: CGFloat?, height: CGFloat?) {
|
|
341
337
|
guard let attachmentState = blockImageAttachment(docPos: docPos) else { return }
|
|
342
|
-
attachmentState.attachment.
|
|
343
|
-
|
|
344
|
-
|
|
338
|
+
updateImageResizePreview(attachmentState.attachment, range: attachmentState.range, width: width, height: height)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
func restoreImageResizePreview(_ attachment: BlockImageAttachment, width: CGFloat?, height: CGFloat?) {
|
|
342
|
+
var attachmentRange: NSRange?
|
|
343
|
+
textStorage.enumerateAttribute(.attachment, in: NSRange(location: 0, length: textStorage.length)) { value, range, stop in
|
|
344
|
+
if value as? BlockImageAttachment === attachment {
|
|
345
|
+
attachmentRange = range
|
|
346
|
+
stop.pointee = true
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
guard let attachmentRange else { return }
|
|
350
|
+
updateImageResizePreview(attachment, range: attachmentRange, width: width, height: height)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private func updateImageResizePreview(_ attachment: BlockImageAttachment, range: NSRange, width: CGFloat?, height: CGFloat?) {
|
|
354
|
+
guard attachment.preferredWidth != width || attachment.preferredHeight != height else { return }
|
|
355
|
+
let previousOffset = contentOffset
|
|
356
|
+
attachment.preferredWidth = width
|
|
357
|
+
attachment.preferredHeight = height
|
|
358
|
+
layoutManager.invalidateLayout(forCharacterRange: range, actualCharacterRange: nil)
|
|
359
|
+
layoutManager.invalidateDisplay(forCharacterRange: range)
|
|
345
360
|
textStorage.beginEditing()
|
|
346
|
-
textStorage.edited(.editedAttributes, range:
|
|
361
|
+
textStorage.edited(.editedAttributes, range: range, changeInLength: 0)
|
|
347
362
|
textStorage.endEditing()
|
|
363
|
+
invalidateAutoGrowHeightMeasurement()
|
|
364
|
+
layoutManager.ensureLayout(forCharacterRange: range)
|
|
365
|
+
setNeedsLayout()
|
|
366
|
+
layoutIfNeeded()
|
|
367
|
+
setContentOffset(previousOffset, animated: false)
|
|
368
|
+
notifyHeightChangeIfNeeded()
|
|
369
|
+
onViewportMayChange?()
|
|
348
370
|
}
|
|
349
371
|
|
|
350
372
|
func setImageResizePreviewActive(_ active: Bool) {
|
package/ios/EditorTextView.swift
CHANGED
|
@@ -415,7 +415,7 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate, UITextDragD
|
|
|
415
415
|
height: min(maxPlaceholderHeight, ceil(fittedHeight))
|
|
416
416
|
)
|
|
417
417
|
}
|
|
418
|
-
if heightBehavior == .autoGrow
|
|
418
|
+
if heightBehavior == .autoGrow {
|
|
419
419
|
let currentWidth = ceil(bounds.width)
|
|
420
420
|
if abs(currentWidth - lastAutoGrowMeasuredWidth) > 0.5 {
|
|
421
421
|
autoGrowHeightCheckIsDirty = true
|
|
@@ -150,7 +150,7 @@ extension EditorV2Adapter {
|
|
|
150
150
|
private static func isValidMentionStyle(_ value: Any) -> Bool {
|
|
151
151
|
guard let style = value as? [String: Any] else { return false }
|
|
152
152
|
let colors: Set<String> = ["color", "backgroundColor", "textDecorationColor", "borderColor", "borderTopColor", "borderRightColor", "borderBottomColor", "borderLeftColor"]
|
|
153
|
-
let dimensions: Set<String> = ["fontSize", "lineHeight", "letterSpacing", "borderWidth", "borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth", "borderRadius", "borderTopLeftRadius", "borderTopRightRadius", "borderBottomLeftRadius", "borderBottomRightRadius"]
|
|
153
|
+
let dimensions: Set<String> = ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft", "fontSize", "lineHeight", "letterSpacing", "borderWidth", "borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth", "borderRadius", "borderTopLeftRadius", "borderTopRightRadius", "borderBottomLeftRadius", "borderBottomRightRadius"]
|
|
154
154
|
let strings: Set<String> = ["fontFamily", "fontWeight", "fontStyle", "textDecorationLine", "textDecorationStyle", "borderStyle"]
|
|
155
155
|
guard hasOnlyKeys(style, colors.union(dimensions).union(strings)) else { return false }
|
|
156
156
|
for key in colors where style[key] != nil {
|
|
@@ -159,7 +159,7 @@ extension EditorV2Adapter {
|
|
|
159
159
|
for key in dimensions where style[key] != nil {
|
|
160
160
|
guard let number = finiteNumber(style[key])?.doubleValue else { return false }
|
|
161
161
|
if ["fontSize", "lineHeight"].contains(key), number <= 0 { return false }
|
|
162
|
-
if key.hasPrefix("border"), number < 0 { return false }
|
|
162
|
+
if key.hasPrefix("border") || key.hasPrefix("padding"), number < 0 { return false }
|
|
163
163
|
}
|
|
164
164
|
for key in strings where style[key] != nil {
|
|
165
165
|
guard let value = style[key] as? String else { return false }
|
|
@@ -108,9 +108,7 @@ extension EditorAccessoryToolbarView {
|
|
|
108
108
|
?? (resolvedAppearance == .native ? self.tintColor : .secondaryLabel)
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
button
|
|
112
|
-
button.setTitleColor(tintColor, for: .normal)
|
|
113
|
-
button.setTitleColor(tintColor, for: .disabled)
|
|
111
|
+
applyButtonForeground(to: button, color: tintColor)
|
|
114
112
|
button.tintAdjustmentMode = enabled ? .automatic : .normal
|
|
115
113
|
button.alpha = enabled || resolvedAppearance == .native ? 1 : 0.7
|
|
116
114
|
let inactiveBackgroundColor = buttonStyle?.backgroundColor
|
|
@@ -139,7 +137,20 @@ extension EditorAccessoryToolbarView {
|
|
|
139
137
|
color: backgroundColor,
|
|
140
138
|
cornerRadius: cornerRadius
|
|
141
139
|
)
|
|
142
|
-
applyButtonIconStyle(to: button, item: item)
|
|
140
|
+
applyButtonIconStyle(to: button, item: item, color: tintColor)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// A configured `UIButton` paints disabled content in its own system gray,
|
|
144
|
+
/// ignoring `tintColor`, so the colour must be forced through the
|
|
145
|
+
/// configuration's transformers as well.
|
|
146
|
+
private func applyButtonForeground(to button: UIButton, color: UIColor) {
|
|
147
|
+
button.tintColor = color
|
|
148
|
+
button.setTitleColor(color, for: .normal)
|
|
149
|
+
button.setTitleColor(color, for: .disabled)
|
|
150
|
+
guard #available(iOS 15.0, *), var configuration = button.configuration else { return }
|
|
151
|
+
configuration.baseForegroundColor = color
|
|
152
|
+
configuration.imageColorTransformer = UIConfigurationColorTransformer { _ in color }
|
|
153
|
+
button.configuration = configuration
|
|
143
154
|
}
|
|
144
155
|
|
|
145
156
|
/// Own the configured background to avoid stacking it with UIButton state fills.
|
|
@@ -175,13 +186,14 @@ extension EditorAccessoryToolbarView {
|
|
|
175
186
|
return min(requestedSize, resolvedButtonSize)
|
|
176
187
|
}
|
|
177
188
|
|
|
178
|
-
private func applyButtonIconStyle(to button: UIButton, item: NativeToolbarItem) {
|
|
189
|
+
private func applyButtonIconStyle(to button: UIButton, item: NativeToolbarItem, color: UIColor) {
|
|
179
190
|
let iconSize = resolvedButtonIconSize(for: item)
|
|
180
191
|
let font = UIFont.systemFont(ofSize: iconSize, weight: .semibold)
|
|
181
192
|
if #available(iOS 15.0, *), var configuration = button.configuration {
|
|
182
193
|
configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
|
|
183
194
|
var outgoing = incoming
|
|
184
195
|
outgoing.font = font
|
|
196
|
+
outgoing.foregroundColor = color
|
|
185
197
|
return outgoing
|
|
186
198
|
}
|
|
187
199
|
button.configuration = configuration
|
|
@@ -25,6 +25,21 @@ extension EditorAccessoryToolbarView {
|
|
|
25
25
|
buttonBindings.indices.contains(index) ? buttonBindings[index].button.tintColor : nil
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/// The foreground colour a configured button will actually paint for its
|
|
29
|
+
/// current state, resolved through the configuration transformers rather
|
|
30
|
+
/// than `tintColor`, which UIKit ignores for disabled configured buttons.
|
|
31
|
+
func buttonPaintedForegroundColorForTesting(_ index: Int) -> UIColor? {
|
|
32
|
+
guard buttonBindings.indices.contains(index) else { return nil }
|
|
33
|
+
let button = buttonBindings[index].button
|
|
34
|
+
guard #available(iOS 15.0, *), let configuration = button.configuration else {
|
|
35
|
+
return button.titleColor(for: button.state)
|
|
36
|
+
}
|
|
37
|
+
if button.image(for: .normal) != nil {
|
|
38
|
+
return configuration.imageColorTransformer?(.black)
|
|
39
|
+
}
|
|
40
|
+
return configuration.titleTextAttributesTransformer?(AttributeContainer()).foregroundColor
|
|
41
|
+
}
|
|
42
|
+
|
|
28
43
|
func buttonFontSizeForTesting(_ index: Int) -> CGFloat? {
|
|
29
44
|
buttonBindings.indices.contains(index) ? buttonBindings[index].button.titleLabel?.font.pointSize : nil
|
|
30
45
|
}
|
|
@@ -101,7 +101,6 @@ final class AtomBlockAttachment: NSTextAttachment {
|
|
|
101
101
|
final class BlockImageAttachment: NSTextAttachment {
|
|
102
102
|
var styleBox: EditorStyleBox?
|
|
103
103
|
let source: String
|
|
104
|
-
let placeholderTint: UIColor
|
|
105
104
|
private weak var loadOwner: RenderImageLoadOwner?
|
|
106
105
|
private var loadReceipt: RenderImageLoadOwner.ImageLoadReceipt?
|
|
107
106
|
var preferredWidth: CGFloat?
|
|
@@ -110,12 +109,10 @@ final class BlockImageAttachment: NSTextAttachment {
|
|
|
110
109
|
|
|
111
110
|
init(
|
|
112
111
|
source: String,
|
|
113
|
-
placeholderTint: UIColor,
|
|
114
112
|
preferredWidth: CGFloat?,
|
|
115
113
|
preferredHeight: CGFloat?
|
|
116
114
|
) {
|
|
117
115
|
self.source = source
|
|
118
|
-
self.placeholderTint = placeholderTint
|
|
119
116
|
self.preferredWidth = preferredWidth
|
|
120
117
|
self.preferredHeight = preferredHeight
|
|
121
118
|
self.loadOwner = RenderImageLoadOwner.current
|
|
@@ -131,15 +128,6 @@ final class BlockImageAttachment: NSTextAttachment {
|
|
|
131
128
|
loadReceipt?.cancel()
|
|
132
129
|
}
|
|
133
130
|
|
|
134
|
-
func setPreferredSize(width: CGFloat, height: CGFloat) {
|
|
135
|
-
preferredWidth = width
|
|
136
|
-
preferredHeight = height
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
func previewImage() -> UIImage? {
|
|
140
|
-
loadedImage ?? image
|
|
141
|
-
}
|
|
142
|
-
|
|
143
131
|
override func attachmentBounds(
|
|
144
132
|
for textContainer: NSTextContainer?,
|
|
145
133
|
proposedLineFragment lineFrag: CGRect,
|
|
@@ -193,9 +181,6 @@ final class BlockImageAttachment: NSTextAttachment {
|
|
|
193
181
|
renderer.cgContext.clip(to: imageBounds.inset(by: styleBox.inset))
|
|
194
182
|
if let loadedImage {
|
|
195
183
|
loadedImage.draw(in: styleBox.imageRect(loadedImage.size, in: imageBounds))
|
|
196
|
-
} else {
|
|
197
|
-
let iconBounds = imageBounds.insetBy(dx: imageBounds.width * 0.35, dy: imageBounds.height * 0.35)
|
|
198
|
-
UIImage(systemName: "photo")?.withTintColor(placeholderTint, renderingMode: .alwaysOriginal).draw(in: iconBounds)
|
|
199
184
|
}
|
|
200
185
|
}
|
|
201
186
|
}
|
|
@@ -206,20 +191,6 @@ final class BlockImageAttachment: NSTextAttachment {
|
|
|
206
191
|
let path = UIBezierPath(roundedRect: imageBounds, cornerRadius: 12)
|
|
207
192
|
UIColor.secondarySystemFill.setFill()
|
|
208
193
|
path.fill()
|
|
209
|
-
|
|
210
|
-
let iconSize = min(imageBounds.width, imageBounds.height) * 0.28
|
|
211
|
-
let iconOrigin = CGPoint(
|
|
212
|
-
x: imageBounds.midX - (iconSize / 2),
|
|
213
|
-
y: imageBounds.midY - (iconSize / 2)
|
|
214
|
-
)
|
|
215
|
-
let iconRect = CGRect(origin: iconOrigin, size: CGSize(width: iconSize, height: iconSize))
|
|
216
|
-
|
|
217
|
-
if #available(iOS 13.0, *) {
|
|
218
|
-
let config = UIImage.SymbolConfiguration(pointSize: iconSize, weight: .medium)
|
|
219
|
-
let icon = UIImage(systemName: "photo", withConfiguration: config)?
|
|
220
|
-
.withTintColor(placeholderTint.withAlphaComponent(0.7), renderingMode: .alwaysOriginal)
|
|
221
|
-
icon?.draw(in: iconRect)
|
|
222
|
-
}
|
|
223
194
|
}
|
|
224
195
|
}
|
|
225
196
|
|
|
@@ -684,10 +684,6 @@ final class RichTextEditorView: UIView {
|
|
|
684
684
|
imageTapOverlayView.handleTapForTesting(convert(point, to: imageTapOverlayView))
|
|
685
685
|
}
|
|
686
686
|
|
|
687
|
-
func imageResizePreviewHasImageForTesting() -> Bool {
|
|
688
|
-
imageResizeOverlayView.previewHasImageForTesting
|
|
689
|
-
}
|
|
690
|
-
|
|
691
687
|
func refreshSelectionVisualStateForTesting() {
|
|
692
688
|
textView.refreshSelectionVisualState()
|
|
693
689
|
}
|
|
@@ -708,6 +704,10 @@ final class RichTextEditorView: UIView {
|
|
|
708
704
|
imageResizeOverlayView.simulatePreviewResizeForTesting(width: width, height: height)
|
|
709
705
|
}
|
|
710
706
|
|
|
707
|
+
func previewImageResizeDragForTesting(corner: ImageResizeOverlayView.Corner, translation: CGPoint) {
|
|
708
|
+
imageResizeOverlayView.simulatePreviewDragForTesting(corner: corner, translation: translation)
|
|
709
|
+
}
|
|
710
|
+
|
|
711
711
|
func commitPreviewResizeForTesting() {
|
|
712
712
|
imageResizeOverlayView.commitPreviewResizeForTesting()
|
|
713
713
|
}
|
|
@@ -803,14 +803,6 @@ final class RichTextEditorView: UIView {
|
|
|
803
803
|
textView.setImageResizePreviewActive(active)
|
|
804
804
|
}
|
|
805
805
|
|
|
806
|
-
func imagePreviewForResize(docPos: UInt32) -> UIImage? {
|
|
807
|
-
textView.imagePreviewForDocPos(docPos)
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
func imageResizePreviewBackgroundColor() -> UIColor {
|
|
811
|
-
textView.backgroundColor ?? .systemBackground
|
|
812
|
-
}
|
|
813
|
-
|
|
814
806
|
func maximumImageWidthForResizeGesture() -> CGFloat {
|
|
815
807
|
textView.maximumRenderableImageWidth()
|
|
816
808
|
}
|
|
@@ -79,8 +79,9 @@ extension CoreTextProseLayoutEngine {
|
|
|
79
79
|
let labelWidth = CGFloat(CTLineGetTypographicBounds(labelLine, &labelAscent, &labelDescent, &labelLeading))
|
|
80
80
|
let requestedHeight = EditorTheme.cgFloat(appearance.attributes[editorInlineLineHeightAttribute]) ?? 0
|
|
81
81
|
let extra = max(0, requestedHeight - labelAscent - max(labelDescent, 2)) / 2
|
|
82
|
+
let paddedWidth = labelWidth + appearance.padding.left + appearance.padding.right
|
|
82
83
|
let metrics = PreparedAtomMetrics(
|
|
83
|
-
width: max(paint.font.lineHeight,
|
|
84
|
+
width: nodeType == "mention" ? paddedWidth : max(paint.font.lineHeight, paddedWidth),
|
|
84
85
|
ascent: labelAscent + extra + appearance.padding.top,
|
|
85
86
|
descent: max(labelDescent, 2) + extra + appearance.padding.bottom
|
|
86
87
|
)
|
|
@@ -388,7 +389,7 @@ extension CoreTextProseLayoutEngine {
|
|
|
388
389
|
var boxValues = values
|
|
389
390
|
boxValues["backgroundColor"] = values["backgroundColor"] ?? "#007aff1f"
|
|
390
391
|
let box = EditorStyleBox(boxValues)
|
|
391
|
-
return PreparedAtomAppearance(styleBox: box, attributes: coreTextAttributes(styled), background: box.color("backgroundColor") ?? .clear, borderColor: nil, borderWidth: 0, radius: 0, padding:
|
|
392
|
+
return PreparedAtomAppearance(styleBox: box, attributes: coreTextAttributes(styled), background: box.color("backgroundColor") ?? .clear, borderColor: nil, borderWidth: 0, radius: 0, padding: box.mentionInsets)
|
|
392
393
|
}
|
|
393
394
|
return PreparedAtomAppearance(
|
|
394
395
|
attributes: attributes,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apollohg/react-native-rich-text-editor",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "Native rich text editor with Rust core for React Native",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/apollohg/react-native-rich-text-editor",
|
|
@@ -104,21 +104,29 @@
|
|
|
104
104
|
},
|
|
105
105
|
"devDependencies": {
|
|
106
106
|
"@babel/runtime": "^7.29.7",
|
|
107
|
+
"@eslint/js": "^10.0.1",
|
|
107
108
|
"@expo/vector-icons": "^15.0.3",
|
|
108
109
|
"@react-native/jest-preset": "0.86.2",
|
|
110
|
+
"@stylistic/eslint-plugin": "^5.10.0",
|
|
109
111
|
"@testing-library/react-native": "^13.3.3",
|
|
110
112
|
"@types/jest": "29.5.14",
|
|
111
113
|
"@types/react": "^19.2.14",
|
|
112
114
|
"babel-jest": "^29.7.0",
|
|
113
115
|
"babel-preset-expo": "~57.0.8",
|
|
116
|
+
"eslint": "^10.10.0",
|
|
117
|
+
"eslint-plugin-react": "^7.37.5",
|
|
118
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
119
|
+
"eslint-plugin-react-native": "^5.0.0",
|
|
120
|
+
"eslint-plugin-slop": "^0.1.3",
|
|
114
121
|
"expo": "^57.0.9",
|
|
115
122
|
"expo-modules-core": "~57.0.8",
|
|
116
123
|
"jest": "^29.7.0",
|
|
117
|
-
"
|
|
124
|
+
"jiti": "^2.7.0",
|
|
118
125
|
"react": "19.2.3",
|
|
119
126
|
"react-native": "0.86.2",
|
|
120
127
|
"react-test-renderer": "19.2.3",
|
|
121
|
-
"typescript": "^5.9.3"
|
|
128
|
+
"typescript": "^5.9.3",
|
|
129
|
+
"typescript-eslint": "^8.70.0"
|
|
122
130
|
},
|
|
123
131
|
"files": [
|
|
124
132
|
"README.md",
|
package/react-native.config.js
CHANGED
|
@@ -7,7 +7,7 @@ module.exports = {
|
|
|
7
7
|
// metadata; no nested compatibility podspec exists.
|
|
8
8
|
ios: {},
|
|
9
9
|
android: {
|
|
10
|
-
componentDescriptors: ['PreparedProseViewerComponentDescriptor'],
|
|
10
|
+
componentDescriptors: [ 'PreparedProseViewerComponentDescriptor' ],
|
|
11
11
|
cmakeListsPath: '../android/src/main/jni/CMakeLists.txt',
|
|
12
12
|
packageImportPath: 'import com.apollohg.editor.viewer.PreparedProseViewerPackage;',
|
|
13
13
|
packageInstance: 'new PreparedProseViewerPackage()',
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/AtomHost.tsx
CHANGED
|
@@ -11,9 +11,12 @@ export interface AtomViewport {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function atomIsVisible(y: number, height: number, viewport?: AtomViewport): boolean {
|
|
14
|
-
if (!viewport)
|
|
14
|
+
if (!viewport) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
15
18
|
if (
|
|
16
|
-
![viewport.y, viewport.height, viewport.overscan ?? 200].every(Number.isFinite) ||
|
|
19
|
+
![ viewport.y, viewport.height, viewport.overscan ?? 200 ].every(Number.isFinite) ||
|
|
17
20
|
viewport.height < 0 ||
|
|
18
21
|
(viewport.overscan ?? 200) < 0
|
|
19
22
|
) {
|
|
@@ -21,7 +24,9 @@ export function atomIsVisible(y: number, height: number, viewport?: AtomViewport
|
|
|
21
24
|
'Atom viewport must have finite coordinates and non-negative height and overscan.'
|
|
22
25
|
);
|
|
23
26
|
}
|
|
27
|
+
|
|
24
28
|
const overscan = viewport.overscan ?? 200;
|
|
29
|
+
|
|
25
30
|
return y + height >= viewport.y - overscan && y <= viewport.y + viewport.height + overscan;
|
|
26
31
|
}
|
|
27
32
|
|
|
@@ -49,13 +54,17 @@ class AtomErrorBoundary extends React.Component<
|
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
render() {
|
|
52
|
-
if (!this.state.error)
|
|
57
|
+
if (!this.state.error) {
|
|
58
|
+
return this.props.children;
|
|
59
|
+
}
|
|
60
|
+
|
|
53
61
|
return (
|
|
54
|
-
<View accessibilityRole='alert' style={{ minHeight: 32, padding: 8 }}>
|
|
62
|
+
<View accessibilityRole={'alert'} style={{ minHeight: 32, padding: 8 }}>
|
|
55
63
|
<Text>Unable to display {this.props.nodeType}</Text>
|
|
56
64
|
<Pressable
|
|
57
|
-
accessibilityRole='button'
|
|
58
|
-
onPress={() => this.setState({ error: false })}
|
|
65
|
+
accessibilityRole={'button'}
|
|
66
|
+
onPress={() => this.setState({ error: false })}
|
|
67
|
+
>
|
|
59
68
|
<Text>Retry</Text>
|
|
60
69
|
</Pressable>
|
|
61
70
|
</View>
|
|
@@ -80,58 +89,77 @@ export function AtomHost({
|
|
|
80
89
|
onMeasure?: (event: import('react-native').LayoutChangeEvent) => void;
|
|
81
90
|
nativeID?: string;
|
|
82
91
|
}) {
|
|
83
|
-
const [measurement, setMeasurement] = useState({ width, height: estimatedHeight });
|
|
84
|
-
const [focused, setFocused] = useState(false);
|
|
85
|
-
const [active, setActive] = useState(false);
|
|
86
|
-
const [pending, setPending] = useState(0);
|
|
87
|
-
const [updateError, setUpdateError] = useState<Error | null>(null);
|
|
92
|
+
const [ measurement, setMeasurement ] = useState({ width, height: estimatedHeight });
|
|
93
|
+
const [ focused, setFocused ] = useState(false);
|
|
94
|
+
const [ active, setActive ] = useState(false);
|
|
95
|
+
const [ pending, setPending ] = useState(0);
|
|
96
|
+
const [ updateError, setUpdateError ] = useState<Error | null>(null);
|
|
88
97
|
const latestRequest = useRef(0);
|
|
89
98
|
const applyUpdate = atomProps.updateAttrs;
|
|
90
99
|
const mounted = useRef(true);
|
|
100
|
+
|
|
91
101
|
useEffect(() => {
|
|
92
102
|
mounted.current = true;
|
|
103
|
+
|
|
93
104
|
return () => {
|
|
94
105
|
mounted.current = false;
|
|
95
106
|
};
|
|
96
107
|
}, []);
|
|
108
|
+
|
|
97
109
|
const updateAttrs = useCallback(
|
|
98
|
-
async
|
|
99
|
-
if (!mounted.current)
|
|
110
|
+
async(update: AtomAttrsUpdate) => {
|
|
111
|
+
if (!mounted.current) {
|
|
100
112
|
throw new AtomUpdateAttrsError('not-ready', 'The atom is unmounted.');
|
|
113
|
+
}
|
|
114
|
+
|
|
101
115
|
const request = ++latestRequest.current;
|
|
102
|
-
setPending(
|
|
116
|
+
setPending(count => count + 1);
|
|
103
117
|
setUpdateError(null);
|
|
118
|
+
|
|
104
119
|
try {
|
|
105
120
|
await applyUpdate(update);
|
|
106
121
|
} catch (error) {
|
|
107
|
-
if (mounted.current && latestRequest.current === request)
|
|
122
|
+
if (mounted.current && latestRequest.current === request) {
|
|
108
123
|
setUpdateError(error instanceof Error ? error : new Error(String(error)));
|
|
124
|
+
}
|
|
125
|
+
|
|
109
126
|
throw error;
|
|
110
127
|
} finally {
|
|
111
|
-
if (mounted.current)
|
|
128
|
+
if (mounted.current) {
|
|
129
|
+
setPending(count => count - 1);
|
|
130
|
+
}
|
|
112
131
|
}
|
|
113
132
|
},
|
|
114
|
-
[applyUpdate]
|
|
133
|
+
[ applyUpdate ]
|
|
115
134
|
);
|
|
135
|
+
|
|
116
136
|
const editor = useMemo(() => {
|
|
117
|
-
if (!atomProps.editor)
|
|
118
|
-
|
|
119
|
-
|
|
137
|
+
if (!atomProps.editor) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const guard = (action: () => Promise<void>) => async() => {
|
|
142
|
+
if (!mounted.current) {
|
|
120
143
|
throw new AtomUpdateAttrsError('not-ready', 'The atom is unmounted.');
|
|
144
|
+
}
|
|
145
|
+
|
|
121
146
|
return action();
|
|
122
147
|
};
|
|
148
|
+
|
|
123
149
|
return {
|
|
124
150
|
select: guard(atomProps.editor.select),
|
|
125
151
|
delete: guard(atomProps.editor.delete),
|
|
126
152
|
focusBefore: guard(atomProps.editor.focusBefore),
|
|
127
153
|
focusAfter: guard(atomProps.editor.focusAfter),
|
|
128
154
|
};
|
|
129
|
-
}, [atomProps.editor]);
|
|
155
|
+
}, [ atomProps.editor ]);
|
|
156
|
+
|
|
130
157
|
const retainedHeight = measurement.width === width ? measurement.height : estimatedHeight;
|
|
131
158
|
const show = visible || focused || active || pending > 0 || atomProps.selected;
|
|
159
|
+
|
|
132
160
|
return (
|
|
133
161
|
<View
|
|
134
|
-
testID='atom-host'
|
|
162
|
+
testID={'atom-host'}
|
|
135
163
|
nativeID={nativeID}
|
|
136
164
|
collapsable={false}
|
|
137
165
|
pointerEvents={atomProps.interactive === false ? 'none' : 'box-none'}
|
|
@@ -139,23 +167,24 @@ export function AtomHost({
|
|
|
139
167
|
importantForAccessibility={!show ? 'no-hide-descendants' : 'auto'}
|
|
140
168
|
onFocus={() => setFocused(true)}
|
|
141
169
|
onBlur={() => setFocused(false)}
|
|
142
|
-
onLayout={
|
|
170
|
+
onLayout={event => {
|
|
143
171
|
onMeasure?.(event);
|
|
144
172
|
const size = event.nativeEvent.layout;
|
|
173
|
+
|
|
145
174
|
if (
|
|
146
175
|
show &&
|
|
147
176
|
Number.isFinite(size.height) &&
|
|
148
177
|
size.height >= 0 &&
|
|
149
178
|
Math.abs(size.width - width) <= 1
|
|
150
179
|
) {
|
|
151
|
-
setMeasurement(
|
|
180
|
+
setMeasurement(previous =>
|
|
152
181
|
previous.width === width && previous.height === size.height
|
|
153
182
|
? previous
|
|
154
|
-
: { width, height: size.height }
|
|
155
|
-
);
|
|
183
|
+
: { width, height: size.height });
|
|
156
184
|
}
|
|
157
185
|
}}
|
|
158
|
-
style={show ? undefined : { height: retainedHeight }}
|
|
186
|
+
style={show ? undefined : { height: retainedHeight }}
|
|
187
|
+
>
|
|
159
188
|
{show && (
|
|
160
189
|
<AtomErrorBoundary component={Component} nodeType={atomProps.nodeType}>
|
|
161
190
|
<Suspense
|
|
@@ -163,7 +192,8 @@ export function AtomHost({
|
|
|
163
192
|
<View style={{ height: retainedHeight }}>
|
|
164
193
|
<Text>Loading {atomProps.nodeType}…</Text>
|
|
165
194
|
</View>
|
|
166
|
-
}
|
|
195
|
+
}
|
|
196
|
+
>
|
|
167
197
|
<Component
|
|
168
198
|
{...atomProps}
|
|
169
199
|
editor={editor}
|
package/src/EditorAddon.ts
CHANGED
|
@@ -37,21 +37,31 @@ export type EditorAddonEntry = EditorAddon | false | null | undefined;
|
|
|
37
37
|
export type EditorAddons = readonly EditorAddonEntry[];
|
|
38
38
|
|
|
39
39
|
function copyConfiguration<T>(value: T, ancestors = new Set<object>()): T {
|
|
40
|
-
if (value === null || typeof value !== 'object')
|
|
41
|
-
|
|
40
|
+
if (value === null || typeof value !== 'object') {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (ancestors.has(value)) {
|
|
45
|
+
throw new Error('Addon configuration must not contain cycles.');
|
|
46
|
+
}
|
|
47
|
+
|
|
42
48
|
if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype) {
|
|
43
49
|
throw new Error('Addon configuration must contain only plain objects and arrays.');
|
|
44
50
|
}
|
|
51
|
+
|
|
45
52
|
ancestors.add(value);
|
|
53
|
+
|
|
46
54
|
const copy = Array.isArray(value)
|
|
47
|
-
? value.map((entry) => copyConfiguration(entry, ancestors))
|
|
55
|
+
? value.map((entry: unknown) => copyConfiguration(entry, ancestors))
|
|
48
56
|
: Object.fromEntries(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
Object.entries(value).map(([ key, entry ]) => [
|
|
58
|
+
key,
|
|
59
|
+
copyConfiguration(entry, ancestors),
|
|
60
|
+
])
|
|
61
|
+
);
|
|
62
|
+
|
|
54
63
|
ancestors.delete(value);
|
|
64
|
+
|
|
55
65
|
return Object.freeze(copy) as T;
|
|
56
66
|
}
|
|
57
67
|
|