@apollohg/react-native-rich-text-editor 2.0.1 → 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/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/dist/EditorStyleSheetNormalization.js +1 -1
- package/dist/EditorStyleSheetTypes.d.ts +1 -1
- package/ios/EditorCore.xcframework/Info.plist +5 -5
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/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 +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/EditorStyleSheetNormalization.ts +1 -1
- package/src/EditorStyleSheetTypes.ts +1 -1
|
@@ -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,
|
|
@@ -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
|
|