@apollohg/react-native-prose-editor 0.1.1 → 0.3.0
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 +12 -7
- package/android/build.gradle +7 -2
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +289 -2
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +51 -1
- package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +16 -3
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +82 -1
- package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +403 -45
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +246 -0
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +841 -155
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +125 -8
- package/{src/EditorTheme.ts → dist/EditorTheme.d.ts} +12 -52
- package/dist/EditorTheme.js +29 -0
- package/dist/EditorToolbar.d.ts +129 -0
- package/dist/EditorToolbar.js +394 -0
- package/dist/NativeEditorBridge.d.ts +242 -0
- package/dist/NativeEditorBridge.js +647 -0
- package/dist/NativeRichTextEditor.d.ts +142 -0
- package/dist/NativeRichTextEditor.js +649 -0
- package/dist/YjsCollaboration.d.ts +83 -0
- package/dist/YjsCollaboration.js +585 -0
- package/dist/addons.d.ts +70 -0
- package/dist/addons.js +77 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +26 -0
- package/dist/schemas.d.ts +35 -0
- package/{src/schemas.ts → dist/schemas.js} +62 -27
- package/dist/useNativeEditor.d.ts +40 -0
- package/dist/useNativeEditor.js +117 -0
- package/ios/EditorAddons.swift +26 -3
- 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/EditorLayoutManager.swift +236 -0
- package/ios/EditorTheme.swift +51 -1
- package/ios/Generated_editor_core.swift +270 -2
- package/ios/NativeEditorExpoView.swift +612 -45
- package/ios/NativeEditorModule.swift +81 -0
- package/ios/PositionBridge.swift +22 -0
- package/ios/RenderBridge.swift +427 -39
- package/ios/RichTextEditorView.swift +1342 -18
- package/ios/editor_coreFFI/editor_coreFFI.h +209 -0
- package/package.json +80 -64
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +404 -4
- package/src/EditorToolbar.tsx +0 -620
- package/src/NativeEditorBridge.ts +0 -607
- package/src/NativeRichTextEditor.tsx +0 -951
- package/src/addons.ts +0 -158
- package/src/index.ts +0 -63
- package/src/useNativeEditor.ts +0 -173
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import ExpoModulesCore
|
|
2
2
|
import UIKit
|
|
3
|
-
import os
|
|
4
3
|
|
|
5
4
|
private struct NativeToolbarState {
|
|
6
5
|
let marks: [String: Bool]
|
|
@@ -91,6 +90,9 @@ private enum ToolbarDefaultIconId: String {
|
|
|
91
90
|
case italic
|
|
92
91
|
case underline
|
|
93
92
|
case strike
|
|
93
|
+
case link
|
|
94
|
+
case image
|
|
95
|
+
case blockquote
|
|
94
96
|
case bulletList
|
|
95
97
|
case orderedList
|
|
96
98
|
case indentList
|
|
@@ -103,6 +105,7 @@ private enum ToolbarDefaultIconId: String {
|
|
|
103
105
|
|
|
104
106
|
private enum ToolbarItemKind: String {
|
|
105
107
|
case mark
|
|
108
|
+
case blockquote
|
|
106
109
|
case list
|
|
107
110
|
case command
|
|
108
111
|
case node
|
|
@@ -121,6 +124,9 @@ private struct NativeToolbarIcon {
|
|
|
121
124
|
.italic: "italic",
|
|
122
125
|
.underline: "underline",
|
|
123
126
|
.strike: "strikethrough",
|
|
127
|
+
.link: "link",
|
|
128
|
+
.image: "photo",
|
|
129
|
+
.blockquote: "text.quote",
|
|
124
130
|
.bulletList: "list.bullet",
|
|
125
131
|
.orderedList: "list.number",
|
|
126
132
|
.indentList: "increase.indent",
|
|
@@ -136,6 +142,9 @@ private struct NativeToolbarIcon {
|
|
|
136
142
|
.italic: "I",
|
|
137
143
|
.underline: "U",
|
|
138
144
|
.strike: "S",
|
|
145
|
+
.link: "🔗",
|
|
146
|
+
.image: "🖼",
|
|
147
|
+
.blockquote: "❝",
|
|
139
148
|
.bulletList: "•≡",
|
|
140
149
|
.orderedList: "1.",
|
|
141
150
|
.indentList: "→",
|
|
@@ -240,6 +249,7 @@ private struct NativeToolbarItem {
|
|
|
240
249
|
NativeToolbarItem(type: .mark, key: nil, label: "Italic", icon: .defaultIcon(.italic), mark: "italic", listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false),
|
|
241
250
|
NativeToolbarItem(type: .mark, key: nil, label: "Underline", icon: .defaultIcon(.underline), mark: "underline", listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false),
|
|
242
251
|
NativeToolbarItem(type: .mark, key: nil, label: "Strikethrough", icon: .defaultIcon(.strike), mark: "strike", listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false),
|
|
252
|
+
NativeToolbarItem(type: .blockquote, key: nil, label: "Blockquote", icon: .defaultIcon(.blockquote), mark: nil, listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false),
|
|
243
253
|
NativeToolbarItem(type: .separator, key: nil, label: nil, icon: nil, mark: nil, listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false),
|
|
244
254
|
NativeToolbarItem(type: .list, key: nil, label: "Bullet List", icon: .defaultIcon(.bulletList), mark: nil, listType: .bulletList, command: nil, nodeType: nil, isActive: false, isDisabled: false),
|
|
245
255
|
NativeToolbarItem(type: .list, key: nil, label: "Ordered List", icon: .defaultIcon(.orderedList), mark: nil, listType: .orderedList, command: nil, nodeType: nil, isActive: false, isDisabled: false),
|
|
@@ -301,6 +311,24 @@ private struct NativeToolbarItem {
|
|
|
301
311
|
isActive: false,
|
|
302
312
|
isDisabled: false
|
|
303
313
|
)
|
|
314
|
+
case .blockquote:
|
|
315
|
+
guard let label = rawItem["label"] as? String,
|
|
316
|
+
let icon = NativeToolbarIcon.from(jsonValue: rawItem["icon"])
|
|
317
|
+
else {
|
|
318
|
+
return nil
|
|
319
|
+
}
|
|
320
|
+
return NativeToolbarItem(
|
|
321
|
+
type: .blockquote,
|
|
322
|
+
key: key,
|
|
323
|
+
label: label,
|
|
324
|
+
icon: icon,
|
|
325
|
+
mark: nil,
|
|
326
|
+
listType: nil,
|
|
327
|
+
command: nil,
|
|
328
|
+
nodeType: nil,
|
|
329
|
+
isActive: false,
|
|
330
|
+
isDisabled: false
|
|
331
|
+
)
|
|
304
332
|
case .list:
|
|
305
333
|
guard let listTypeRaw = rawItem["listType"] as? String,
|
|
306
334
|
let listType = ToolbarListType(rawValue: listTypeRaw),
|
|
@@ -392,6 +420,8 @@ private struct NativeToolbarItem {
|
|
|
392
420
|
switch type {
|
|
393
421
|
case .mark:
|
|
394
422
|
return "mark:\(mark ?? ""):\(index)"
|
|
423
|
+
case .blockquote:
|
|
424
|
+
return "blockquote:\(index)"
|
|
395
425
|
case .list:
|
|
396
426
|
return "list:\(listType?.rawValue ?? ""):\(index)"
|
|
397
427
|
case .command:
|
|
@@ -406,7 +436,7 @@ private struct NativeToolbarItem {
|
|
|
406
436
|
}
|
|
407
437
|
}
|
|
408
438
|
|
|
409
|
-
final class EditorAccessoryToolbarView:
|
|
439
|
+
final class EditorAccessoryToolbarView: UIInputView {
|
|
410
440
|
private static let baseHeight: CGFloat = 50
|
|
411
441
|
private static let mentionRowHeight: CGFloat = 52
|
|
412
442
|
private static let contentSpacing: CGFloat = 6
|
|
@@ -418,7 +448,16 @@ final class EditorAccessoryToolbarView: UIView {
|
|
|
418
448
|
let button: UIButton
|
|
419
449
|
}
|
|
420
450
|
|
|
451
|
+
private struct BarButtonBinding {
|
|
452
|
+
let item: NativeToolbarItem
|
|
453
|
+
let button: UIBarButtonItem
|
|
454
|
+
}
|
|
455
|
+
|
|
421
456
|
private let chromeView = UIView()
|
|
457
|
+
private let blurView = UIVisualEffectView(effect: nil)
|
|
458
|
+
private let glassTintView = UIView()
|
|
459
|
+
private let nativeToolbarScrollView = UIScrollView()
|
|
460
|
+
private let nativeToolbarView = UIToolbar()
|
|
422
461
|
private let contentStackView = UIStackView()
|
|
423
462
|
private let mentionScrollView = UIScrollView()
|
|
424
463
|
private let mentionStackView = UIStackView()
|
|
@@ -427,8 +466,11 @@ final class EditorAccessoryToolbarView: UIView {
|
|
|
427
466
|
private var chromeLeadingConstraint: NSLayoutConstraint?
|
|
428
467
|
private var chromeTrailingConstraint: NSLayoutConstraint?
|
|
429
468
|
private var chromeBottomConstraint: NSLayoutConstraint?
|
|
469
|
+
private var nativeToolbarWidthConstraint: NSLayoutConstraint?
|
|
430
470
|
private var mentionRowHeightConstraint: NSLayoutConstraint?
|
|
471
|
+
private var nativeToolbarDidInitializeScrollPosition = false
|
|
431
472
|
private var buttonBindings: [ButtonBinding] = []
|
|
473
|
+
private var barButtonBindings: [BarButtonBinding] = []
|
|
432
474
|
private var separators: [UIView] = []
|
|
433
475
|
private var mentionButtons: [MentionSuggestionChipButton] = []
|
|
434
476
|
private var items: [NativeToolbarItem] = NativeToolbarItem.defaults
|
|
@@ -440,20 +482,68 @@ final class EditorAccessoryToolbarView: UIView {
|
|
|
440
482
|
var isShowingMentionSuggestions: Bool {
|
|
441
483
|
!mentionButtons.isEmpty && !mentionScrollView.isHidden && scrollView.isHidden
|
|
442
484
|
}
|
|
485
|
+
var usesNativeAppearanceForTesting: Bool {
|
|
486
|
+
resolvedAppearance == .native
|
|
487
|
+
}
|
|
488
|
+
var usesUIGlassEffectForTesting: Bool {
|
|
489
|
+
#if compiler(>=6.2)
|
|
490
|
+
if #available(iOS 26.0, *) {
|
|
491
|
+
return blurView.effect is UIGlassEffect
|
|
492
|
+
}
|
|
493
|
+
#endif
|
|
494
|
+
return false
|
|
495
|
+
}
|
|
496
|
+
var chromeBorderWidthForTesting: CGFloat {
|
|
497
|
+
chromeView.layer.borderWidth
|
|
498
|
+
}
|
|
499
|
+
var nativeToolbarVisibleWidthForTesting: CGFloat {
|
|
500
|
+
activeNativeToolbarScrollViewForTesting.bounds.width
|
|
501
|
+
}
|
|
502
|
+
var nativeToolbarContentWidthForTesting: CGFloat {
|
|
503
|
+
if usesNativeBarToolbar {
|
|
504
|
+
return max(nativeToolbarScrollView.contentSize.width, nativeToolbarView.bounds.width)
|
|
505
|
+
}
|
|
506
|
+
return max(scrollView.contentSize.width, stackView.bounds.width)
|
|
507
|
+
}
|
|
508
|
+
var nativeToolbarContentOffsetXForTesting: CGFloat {
|
|
509
|
+
activeNativeToolbarScrollViewForTesting.contentOffset.x
|
|
510
|
+
}
|
|
511
|
+
func setNativeToolbarContentOffsetXForTesting(_ offsetX: CGFloat) {
|
|
512
|
+
activeNativeToolbarScrollViewForTesting.contentOffset.x = offsetX
|
|
513
|
+
}
|
|
514
|
+
var selectedButtonCountForTesting: Int {
|
|
515
|
+
#if compiler(>=6.2)
|
|
516
|
+
if #available(iOS 26.0, *) {
|
|
517
|
+
if usesNativeBarToolbar {
|
|
518
|
+
return barButtonBindings.filter { $0.button.style == .prominent }.count
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
#endif
|
|
522
|
+
return buttonBindings.filter(\.button.isSelected).count
|
|
523
|
+
}
|
|
524
|
+
func mentionButtonAtForTesting(_ index: Int) -> MentionSuggestionChipButton? {
|
|
525
|
+
mentionButtons.indices.contains(index) ? mentionButtons[index] : nil
|
|
526
|
+
}
|
|
443
527
|
|
|
444
528
|
override var intrinsicContentSize: CGSize {
|
|
445
529
|
let contentHeight = mentionButtons.isEmpty ? Self.baseHeight : Self.mentionRowHeight
|
|
446
530
|
return CGSize(
|
|
447
531
|
width: UIView.noIntrinsicMetric,
|
|
448
|
-
height: contentHeight +
|
|
532
|
+
height: contentHeight + resolvedKeyboardOffset
|
|
449
533
|
)
|
|
450
534
|
}
|
|
451
535
|
|
|
452
|
-
|
|
453
|
-
|
|
536
|
+
convenience init(frame: CGRect) {
|
|
537
|
+
self.init(frame: frame, inputViewStyle: .keyboard)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
override init(frame: CGRect, inputViewStyle: UIInputView.Style) {
|
|
541
|
+
super.init(frame: frame, inputViewStyle: inputViewStyle)
|
|
454
542
|
translatesAutoresizingMaskIntoConstraints = false
|
|
455
543
|
autoresizingMask = [.flexibleHeight]
|
|
456
544
|
backgroundColor = .clear
|
|
545
|
+
isOpaque = false
|
|
546
|
+
allowsSelfSizing = true
|
|
457
547
|
setupView()
|
|
458
548
|
rebuildButtons()
|
|
459
549
|
}
|
|
@@ -462,6 +552,21 @@ final class EditorAccessoryToolbarView: UIView {
|
|
|
462
552
|
return nil
|
|
463
553
|
}
|
|
464
554
|
|
|
555
|
+
override func didMoveToSuperview() {
|
|
556
|
+
super.didMoveToSuperview()
|
|
557
|
+
refreshNativeHostTransparencyIfNeeded()
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
override func didMoveToWindow() {
|
|
561
|
+
super.didMoveToWindow()
|
|
562
|
+
refreshNativeHostTransparencyIfNeeded()
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
override func layoutSubviews() {
|
|
566
|
+
super.layoutSubviews()
|
|
567
|
+
updateNativeToolbarMetricsIfNeeded()
|
|
568
|
+
}
|
|
569
|
+
|
|
465
570
|
fileprivate func setItems(_ items: [NativeToolbarItem]) {
|
|
466
571
|
self.items = items
|
|
467
572
|
rebuildButtons()
|
|
@@ -476,24 +581,72 @@ final class EditorAccessoryToolbarView: UIView {
|
|
|
476
581
|
|
|
477
582
|
func apply(theme: EditorToolbarTheme?) {
|
|
478
583
|
self.theme = theme
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
chromeView.
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
584
|
+
let usesNativeAppearance = resolvedAppearance == .native
|
|
585
|
+
let hasFloatingGlassButtons = self.usesFloatingGlassButtons
|
|
586
|
+
let usesBarToolbar = usesNativeBarToolbar
|
|
587
|
+
chromeView.backgroundColor = usesNativeAppearance
|
|
588
|
+
? .clear
|
|
589
|
+
: (theme?.backgroundColor ?? .systemBackground)
|
|
590
|
+
chromeView.tintColor = usesNativeAppearance
|
|
591
|
+
? nil
|
|
592
|
+
: (theme?.buttonColor ?? tintColor)
|
|
593
|
+
chromeView.isOpaque = false
|
|
594
|
+
blurView.isHidden = usesBarToolbar || !usesNativeAppearance
|
|
595
|
+
blurView.effect = usesNativeAppearance ? resolvedBlurEffect() : nil
|
|
596
|
+
blurView.alpha = usesNativeAppearance ? resolvedEffectAlpha : 1
|
|
597
|
+
glassTintView.isHidden = usesBarToolbar || !usesNativeAppearance
|
|
598
|
+
glassTintView.backgroundColor = usesNativeAppearance
|
|
599
|
+
? UIColor.systemBackground.withAlphaComponent(resolvedGlassTintAlpha)
|
|
600
|
+
: .clear
|
|
601
|
+
chromeView.layer.borderColor = resolvedBorderColor.cgColor
|
|
602
|
+
chromeView.layer.borderWidth = usesBarToolbar
|
|
603
|
+
? 0
|
|
604
|
+
: (usesNativeAppearance
|
|
605
|
+
? (1 / UIScreen.main.scale)
|
|
606
|
+
: resolvedBorderWidth)
|
|
607
|
+
chromeView.layer.cornerRadius = resolvedBorderRadius
|
|
608
|
+
if #available(iOS 13.0, *) {
|
|
609
|
+
chromeView.layer.cornerCurve = .continuous
|
|
610
|
+
}
|
|
611
|
+
#if compiler(>=6.2)
|
|
612
|
+
if #available(iOS 26.0, *) {
|
|
613
|
+
let cornerConfig: UICornerConfiguration = usesNativeAppearance
|
|
614
|
+
? .capsule(maximumRadius: 24)
|
|
615
|
+
: .uniformCorners(radius: .fixed(Double(resolvedBorderRadius)))
|
|
616
|
+
chromeView.cornerConfiguration = cornerConfig
|
|
617
|
+
blurView.cornerConfiguration = cornerConfig
|
|
618
|
+
glassTintView.cornerConfiguration = cornerConfig
|
|
619
|
+
}
|
|
620
|
+
#endif
|
|
621
|
+
chromeView.clipsToBounds = (usesNativeAppearance && !hasFloatingGlassButtons && !usesBarToolbar) || resolvedBorderRadius > 0
|
|
622
|
+
chromeView.layer.shadowOpacity = usesNativeAppearance && !hasFloatingGlassButtons && !usesBarToolbar ? 0.08 : 0
|
|
623
|
+
chromeView.layer.shadowRadius = usesNativeAppearance && !hasFloatingGlassButtons && !usesBarToolbar ? 10 : 0
|
|
624
|
+
chromeView.layer.shadowOffset = CGSize(width: 0, height: 2)
|
|
625
|
+
chromeView.layer.shadowColor = UIColor.black.cgColor
|
|
626
|
+
chromeLeadingConstraint?.constant = resolvedHorizontalInset
|
|
627
|
+
chromeTrailingConstraint?.constant = -resolvedHorizontalInset
|
|
628
|
+
chromeBottomConstraint?.constant = -resolvedKeyboardOffset
|
|
629
|
+
nativeToolbarScrollView.isHidden = !(usesBarToolbar && mentionButtons.isEmpty)
|
|
630
|
+
nativeToolbarView.isHidden = !(usesBarToolbar && mentionButtons.isEmpty)
|
|
631
|
+
nativeToolbarView.tintColor = usesNativeAppearance
|
|
632
|
+
? nil
|
|
633
|
+
: (theme?.buttonColor ?? tintColor)
|
|
634
|
+
contentStackView.isHidden = usesBarToolbar && mentionButtons.isEmpty
|
|
487
635
|
invalidateIntrinsicContentSize()
|
|
488
636
|
for separator in separators {
|
|
489
|
-
separator.
|
|
637
|
+
separator.isHidden = hasFloatingGlassButtons
|
|
638
|
+
separator.backgroundColor = usesNativeAppearance
|
|
639
|
+
? UIColor.separator.withAlphaComponent(0.45)
|
|
640
|
+
: (theme?.separatorColor ?? .separator)
|
|
490
641
|
}
|
|
491
642
|
for binding in buttonBindings {
|
|
492
|
-
binding.button.layer.cornerRadius =
|
|
643
|
+
binding.button.layer.cornerRadius = resolvedButtonBorderRadius
|
|
493
644
|
}
|
|
494
645
|
for button in mentionButtons {
|
|
495
|
-
button.apply(theme: mentionTheme)
|
|
646
|
+
button.apply(theme: mentionTheme, toolbarAppearance: resolvedAppearance)
|
|
496
647
|
}
|
|
648
|
+
refreshNativeHostTransparencyIfNeeded()
|
|
649
|
+
updateNativeToolbarMetricsIfNeeded()
|
|
497
650
|
apply(state: currentState)
|
|
498
651
|
}
|
|
499
652
|
|
|
@@ -508,7 +661,11 @@ final class EditorAccessoryToolbarView: UIView {
|
|
|
508
661
|
mentionButtons.removeAll()
|
|
509
662
|
|
|
510
663
|
for suggestion in suggestions.prefix(8) {
|
|
511
|
-
let button = MentionSuggestionChipButton(
|
|
664
|
+
let button = MentionSuggestionChipButton(
|
|
665
|
+
suggestion: suggestion,
|
|
666
|
+
theme: mentionTheme,
|
|
667
|
+
toolbarAppearance: resolvedAppearance
|
|
668
|
+
)
|
|
512
669
|
button.addTarget(self, action: #selector(handleSelectMentionSuggestion(_:)), for: .touchUpInside)
|
|
513
670
|
mentionButtons.append(button)
|
|
514
671
|
mentionStackView.addArrangedSubview(button)
|
|
@@ -528,13 +685,45 @@ final class EditorAccessoryToolbarView: UIView {
|
|
|
528
685
|
for binding in buttonBindings {
|
|
529
686
|
let buttonState = buttonState(for: binding.item, state: state)
|
|
530
687
|
binding.button.isEnabled = buttonState.enabled
|
|
688
|
+
binding.button.isSelected = buttonState.active
|
|
531
689
|
binding.button.accessibilityTraits = buttonState.active ? [.button, .selected] : .button
|
|
532
|
-
updateButtonAppearance(
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
690
|
+
updateButtonAppearance(binding.button, item: binding.item, enabled: buttonState.enabled, active: buttonState.active)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
#if compiler(>=6.2)
|
|
694
|
+
if #available(iOS 26.0, *), usesNativeBarToolbar {
|
|
695
|
+
for binding in barButtonBindings {
|
|
696
|
+
let state = buttonState(for: binding.item, state: currentState)
|
|
697
|
+
binding.button.isEnabled = state.enabled
|
|
698
|
+
binding.button.isSelected = state.active
|
|
699
|
+
binding.button.style = state.active ? .prominent : .plain
|
|
700
|
+
}
|
|
537
701
|
}
|
|
702
|
+
#endif
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
var firstButtonAlphaForTesting: CGFloat {
|
|
706
|
+
buttonBindings.first?.button.alpha ?? 0
|
|
707
|
+
}
|
|
708
|
+
var firstButtonTintColorForTesting: UIColor? {
|
|
709
|
+
buttonBindings.first?.button.tintColor
|
|
710
|
+
}
|
|
711
|
+
var firstButtonTintAdjustmentModeForTesting: UIView.TintAdjustmentMode {
|
|
712
|
+
buttonBindings.first?.button.tintAdjustmentMode ?? .automatic
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
func applyBoldStateForTesting(active: Bool, enabled: Bool) {
|
|
716
|
+
apply(
|
|
717
|
+
state: NativeToolbarState(
|
|
718
|
+
marks: ["bold": active],
|
|
719
|
+
nodes: [:],
|
|
720
|
+
commands: [:],
|
|
721
|
+
allowedMarks: enabled ? ["bold"] : [],
|
|
722
|
+
insertableNodes: [],
|
|
723
|
+
canUndo: false,
|
|
724
|
+
canRedo: false
|
|
725
|
+
)
|
|
726
|
+
)
|
|
538
727
|
}
|
|
539
728
|
|
|
540
729
|
private func setupView() {
|
|
@@ -542,8 +731,37 @@ final class EditorAccessoryToolbarView: UIView {
|
|
|
542
731
|
chromeView.backgroundColor = .systemBackground
|
|
543
732
|
chromeView.layer.borderColor = UIColor.separator.cgColor
|
|
544
733
|
chromeView.layer.borderWidth = 0.5
|
|
734
|
+
chromeView.isOpaque = false
|
|
545
735
|
addSubview(chromeView)
|
|
546
736
|
|
|
737
|
+
blurView.translatesAutoresizingMaskIntoConstraints = false
|
|
738
|
+
blurView.isHidden = true
|
|
739
|
+
blurView.isUserInteractionEnabled = false
|
|
740
|
+
blurView.clipsToBounds = true
|
|
741
|
+
chromeView.addSubview(blurView)
|
|
742
|
+
|
|
743
|
+
glassTintView.translatesAutoresizingMaskIntoConstraints = false
|
|
744
|
+
glassTintView.isHidden = true
|
|
745
|
+
glassTintView.isUserInteractionEnabled = false
|
|
746
|
+
chromeView.addSubview(glassTintView)
|
|
747
|
+
|
|
748
|
+
nativeToolbarScrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
749
|
+
nativeToolbarScrollView.isHidden = true
|
|
750
|
+
nativeToolbarScrollView.backgroundColor = .clear
|
|
751
|
+
nativeToolbarScrollView.showsHorizontalScrollIndicator = false
|
|
752
|
+
nativeToolbarScrollView.showsVerticalScrollIndicator = false
|
|
753
|
+
nativeToolbarScrollView.alwaysBounceHorizontal = true
|
|
754
|
+
nativeToolbarScrollView.alwaysBounceVertical = false
|
|
755
|
+
chromeView.addSubview(nativeToolbarScrollView)
|
|
756
|
+
|
|
757
|
+
nativeToolbarView.translatesAutoresizingMaskIntoConstraints = false
|
|
758
|
+
nativeToolbarView.isHidden = true
|
|
759
|
+
nativeToolbarView.backgroundColor = .clear
|
|
760
|
+
nativeToolbarView.isTranslucent = true
|
|
761
|
+
nativeToolbarView.setContentHuggingPriority(.required, for: .vertical)
|
|
762
|
+
nativeToolbarView.setContentCompressionResistancePriority(.required, for: .vertical)
|
|
763
|
+
nativeToolbarScrollView.addSubview(nativeToolbarView)
|
|
764
|
+
|
|
547
765
|
contentStackView.translatesAutoresizingMaskIntoConstraints = false
|
|
548
766
|
contentStackView.axis = .vertical
|
|
549
767
|
contentStackView.spacing = 0
|
|
@@ -589,6 +807,8 @@ final class EditorAccessoryToolbarView: UIView {
|
|
|
589
807
|
chromeBottomConstraint = bottom
|
|
590
808
|
let mentionHeight = mentionScrollView.heightAnchor.constraint(equalToConstant: 0)
|
|
591
809
|
mentionRowHeightConstraint = mentionHeight
|
|
810
|
+
let nativeToolbarWidth = nativeToolbarView.widthAnchor.constraint(greaterThanOrEqualToConstant: Self.baseHeight)
|
|
811
|
+
nativeToolbarWidthConstraint = nativeToolbarWidth
|
|
592
812
|
|
|
593
813
|
NSLayoutConstraint.activate([
|
|
594
814
|
chromeView.topAnchor.constraint(equalTo: topAnchor),
|
|
@@ -596,6 +816,29 @@ final class EditorAccessoryToolbarView: UIView {
|
|
|
596
816
|
trailing,
|
|
597
817
|
bottom,
|
|
598
818
|
|
|
819
|
+
blurView.topAnchor.constraint(equalTo: chromeView.topAnchor),
|
|
820
|
+
blurView.leadingAnchor.constraint(equalTo: chromeView.leadingAnchor),
|
|
821
|
+
blurView.trailingAnchor.constraint(equalTo: chromeView.trailingAnchor),
|
|
822
|
+
blurView.bottomAnchor.constraint(equalTo: chromeView.bottomAnchor),
|
|
823
|
+
|
|
824
|
+
glassTintView.topAnchor.constraint(equalTo: chromeView.topAnchor),
|
|
825
|
+
glassTintView.leadingAnchor.constraint(equalTo: chromeView.leadingAnchor),
|
|
826
|
+
glassTintView.trailingAnchor.constraint(equalTo: chromeView.trailingAnchor),
|
|
827
|
+
glassTintView.bottomAnchor.constraint(equalTo: chromeView.bottomAnchor),
|
|
828
|
+
|
|
829
|
+
nativeToolbarScrollView.topAnchor.constraint(equalTo: chromeView.topAnchor),
|
|
830
|
+
nativeToolbarScrollView.leadingAnchor.constraint(equalTo: chromeView.leadingAnchor),
|
|
831
|
+
nativeToolbarScrollView.trailingAnchor.constraint(equalTo: chromeView.trailingAnchor),
|
|
832
|
+
nativeToolbarScrollView.bottomAnchor.constraint(equalTo: chromeView.bottomAnchor),
|
|
833
|
+
|
|
834
|
+
nativeToolbarView.topAnchor.constraint(equalTo: nativeToolbarScrollView.contentLayoutGuide.topAnchor),
|
|
835
|
+
nativeToolbarView.leadingAnchor.constraint(equalTo: nativeToolbarScrollView.contentLayoutGuide.leadingAnchor),
|
|
836
|
+
nativeToolbarView.trailingAnchor.constraint(equalTo: nativeToolbarScrollView.contentLayoutGuide.trailingAnchor),
|
|
837
|
+
nativeToolbarView.bottomAnchor.constraint(equalTo: nativeToolbarScrollView.contentLayoutGuide.bottomAnchor),
|
|
838
|
+
nativeToolbarView.heightAnchor.constraint(equalTo: nativeToolbarScrollView.frameLayoutGuide.heightAnchor),
|
|
839
|
+
nativeToolbarView.heightAnchor.constraint(greaterThanOrEqualToConstant: Self.baseHeight),
|
|
840
|
+
nativeToolbarWidth,
|
|
841
|
+
|
|
599
842
|
contentStackView.topAnchor.constraint(equalTo: chromeView.topAnchor, constant: 6),
|
|
600
843
|
contentStackView.leadingAnchor.constraint(equalTo: chromeView.leadingAnchor),
|
|
601
844
|
contentStackView.trailingAnchor.constraint(equalTo: chromeView.trailingAnchor),
|
|
@@ -621,7 +864,9 @@ final class EditorAccessoryToolbarView: UIView {
|
|
|
621
864
|
|
|
622
865
|
private func rebuildButtons() {
|
|
623
866
|
buttonBindings.removeAll()
|
|
867
|
+
barButtonBindings.removeAll()
|
|
624
868
|
separators.removeAll()
|
|
869
|
+
nativeToolbarDidInitializeScrollPosition = false
|
|
625
870
|
for arrangedSubview in stackView.arrangedSubviews {
|
|
626
871
|
stackView.removeArrangedSubview(arrangedSubview)
|
|
627
872
|
arrangedSubview.removeFromSuperview()
|
|
@@ -644,16 +889,137 @@ final class EditorAccessoryToolbarView: UIView {
|
|
|
644
889
|
stackView.addArrangedSubview(button)
|
|
645
890
|
}
|
|
646
891
|
|
|
892
|
+
#if compiler(>=6.2)
|
|
893
|
+
if #available(iOS 26.0, *) {
|
|
894
|
+
nativeToolbarView.setItems(makeNativeToolbarItems(from: compactItems), animated: false)
|
|
895
|
+
} else {
|
|
896
|
+
nativeToolbarView.setItems([], animated: false)
|
|
897
|
+
}
|
|
898
|
+
#else
|
|
899
|
+
nativeToolbarView.setItems([], animated: false)
|
|
900
|
+
#endif
|
|
901
|
+
|
|
902
|
+
updateNativeToolbarMetricsIfNeeded()
|
|
647
903
|
apply(theme: theme)
|
|
648
904
|
apply(state: currentState)
|
|
649
905
|
}
|
|
650
906
|
|
|
907
|
+
private func updateNativeToolbarMetricsIfNeeded() {
|
|
908
|
+
#if compiler(>=6.2)
|
|
909
|
+
guard #available(iOS 26.0, *), usesNativeBarToolbar else {
|
|
910
|
+
nativeToolbarWidthConstraint?.constant = Self.baseHeight
|
|
911
|
+
nativeToolbarDidInitializeScrollPosition = false
|
|
912
|
+
return
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
let availableWidth = max(chromeView.bounds.width, bounds.width, 1)
|
|
916
|
+
let targetHeight = max(chromeView.bounds.height, Self.baseHeight)
|
|
917
|
+
nativeToolbarView.layoutIfNeeded()
|
|
918
|
+
let fittingSize = nativeToolbarView.sizeThatFits(
|
|
919
|
+
CGSize(width: CGFloat.greatestFiniteMagnitude, height: targetHeight)
|
|
920
|
+
)
|
|
921
|
+
let contentFrames = nativeToolbarView.subviews.compactMap { subview -> CGRect? in
|
|
922
|
+
guard !subview.isHidden,
|
|
923
|
+
subview.alpha > 0.01,
|
|
924
|
+
subview.bounds.width > 0,
|
|
925
|
+
subview.bounds.height > 0
|
|
926
|
+
else {
|
|
927
|
+
return nil
|
|
928
|
+
}
|
|
929
|
+
return subview.frame
|
|
930
|
+
}
|
|
931
|
+
let measuredSubviewWidth: CGFloat
|
|
932
|
+
if let minX = contentFrames.map(\.minX).min(),
|
|
933
|
+
let maxX = contentFrames.map(\.maxX).max()
|
|
934
|
+
{
|
|
935
|
+
measuredSubviewWidth = ceil(maxX + max(0, minX))
|
|
936
|
+
} else {
|
|
937
|
+
measuredSubviewWidth = 0
|
|
938
|
+
}
|
|
939
|
+
let contentWidth = max(ceil(fittingSize.width), measuredSubviewWidth, availableWidth)
|
|
940
|
+
nativeToolbarWidthConstraint?.constant = contentWidth
|
|
941
|
+
nativeToolbarScrollView.alwaysBounceHorizontal = contentWidth > availableWidth
|
|
942
|
+
let minOffsetX = -nativeToolbarScrollView.adjustedContentInset.left
|
|
943
|
+
let maxOffsetX = max(
|
|
944
|
+
minOffsetX,
|
|
945
|
+
contentWidth - nativeToolbarScrollView.bounds.width + nativeToolbarScrollView.adjustedContentInset.right
|
|
946
|
+
)
|
|
947
|
+
let targetOffsetX: CGFloat
|
|
948
|
+
if nativeToolbarDidInitializeScrollPosition {
|
|
949
|
+
targetOffsetX = min(max(nativeToolbarScrollView.contentOffset.x, minOffsetX), maxOffsetX)
|
|
950
|
+
} else {
|
|
951
|
+
targetOffsetX = minOffsetX
|
|
952
|
+
nativeToolbarDidInitializeScrollPosition = true
|
|
953
|
+
}
|
|
954
|
+
if abs(nativeToolbarScrollView.contentOffset.x - targetOffsetX) > 0.5 {
|
|
955
|
+
nativeToolbarScrollView.setContentOffset(
|
|
956
|
+
CGPoint(x: targetOffsetX, y: nativeToolbarScrollView.contentOffset.y),
|
|
957
|
+
animated: false
|
|
958
|
+
)
|
|
959
|
+
}
|
|
960
|
+
#else
|
|
961
|
+
nativeToolbarWidthConstraint?.constant = Self.baseHeight
|
|
962
|
+
nativeToolbarDidInitializeScrollPosition = false
|
|
963
|
+
#endif
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
#if compiler(>=6.2)
|
|
967
|
+
@available(iOS 26.0, *)
|
|
968
|
+
private func makeNativeToolbarItems(from compactItems: [NativeToolbarItem]) -> [UIBarButtonItem] {
|
|
969
|
+
var toolbarItems: [UIBarButtonItem] = []
|
|
970
|
+
var previousWasSeparator = true
|
|
971
|
+
|
|
972
|
+
for item in compactItems {
|
|
973
|
+
if item.type == .separator {
|
|
974
|
+
if !previousWasSeparator, !toolbarItems.isEmpty {
|
|
975
|
+
let spacer = UIBarButtonItem(systemItem: .fixedSpace)
|
|
976
|
+
spacer.width = 0
|
|
977
|
+
toolbarItems.append(spacer)
|
|
978
|
+
}
|
|
979
|
+
previousWasSeparator = true
|
|
980
|
+
continue
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
let state = buttonState(for: item, state: currentState)
|
|
984
|
+
let barButtonItem = makeNativeBarButtonItem(item: item, enabled: state.enabled, active: state.active)
|
|
985
|
+
barButtonBindings.append(BarButtonBinding(item: item, button: barButtonItem))
|
|
986
|
+
toolbarItems.append(barButtonItem)
|
|
987
|
+
previousWasSeparator = false
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
return toolbarItems
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
@available(iOS 26.0, *)
|
|
994
|
+
private func makeNativeBarButtonItem(
|
|
995
|
+
item: NativeToolbarItem,
|
|
996
|
+
enabled: Bool,
|
|
997
|
+
active: Bool
|
|
998
|
+
) -> UIBarButtonItem {
|
|
999
|
+
let image = item.icon?.resolvedSFSymbolName().flatMap { UIImage(systemName: $0) }
|
|
1000
|
+
let title = image == nil ? item.icon?.resolvedGlyphText() : nil
|
|
1001
|
+
let action = UIAction { [weak self] _ in
|
|
1002
|
+
self?.onPressItem?(item)
|
|
1003
|
+
}
|
|
1004
|
+
let barButtonItem = UIBarButtonItem(title: title, image: image, primaryAction: action, menu: nil)
|
|
1005
|
+
|
|
1006
|
+
barButtonItem.accessibilityLabel = item.label
|
|
1007
|
+
barButtonItem.isEnabled = enabled
|
|
1008
|
+
barButtonItem.isSelected = active
|
|
1009
|
+
barButtonItem.style = active ? .prominent : .plain
|
|
1010
|
+
|
|
1011
|
+
barButtonItem.sharesBackground = true
|
|
1012
|
+
barButtonItem.hidesSharedBackground = active
|
|
1013
|
+
return barButtonItem
|
|
1014
|
+
}
|
|
1015
|
+
#endif
|
|
1016
|
+
|
|
651
1017
|
private func makeButton(item: NativeToolbarItem) -> UIButton {
|
|
652
1018
|
let button = UIButton(type: .system)
|
|
653
1019
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
654
1020
|
button.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
|
|
655
1021
|
button.accessibilityLabel = item.label
|
|
656
|
-
button.layer.cornerRadius =
|
|
1022
|
+
button.layer.cornerRadius = resolvedButtonBorderRadius
|
|
657
1023
|
button.clipsToBounds = true
|
|
658
1024
|
if #available(iOS 15.0, *) {
|
|
659
1025
|
var configuration = UIButton.Configuration.plain()
|
|
@@ -685,7 +1051,7 @@ final class EditorAccessoryToolbarView: UIView {
|
|
|
685
1051
|
button.addAction(UIAction { [weak self] _ in
|
|
686
1052
|
self?.onPressItem?(item)
|
|
687
1053
|
}, for: .touchUpInside)
|
|
688
|
-
updateButtonAppearance(button, enabled: true, active: false)
|
|
1054
|
+
updateButtonAppearance(button, item: item, enabled: true, active: false)
|
|
689
1055
|
return button
|
|
690
1056
|
}
|
|
691
1057
|
|
|
@@ -712,6 +1078,11 @@ final class EditorAccessoryToolbarView: UIView {
|
|
|
712
1078
|
enabled: state.allowedMarks.contains(mark),
|
|
713
1079
|
active: state.marks[mark] == true
|
|
714
1080
|
)
|
|
1081
|
+
case .blockquote:
|
|
1082
|
+
return (
|
|
1083
|
+
enabled: state.commands["toggleBlockquote"] == true,
|
|
1084
|
+
active: state.nodes["blockquote"] == true
|
|
1085
|
+
)
|
|
715
1086
|
case .list:
|
|
716
1087
|
switch item.listType {
|
|
717
1088
|
case .bulletList:
|
|
@@ -762,7 +1133,54 @@ final class EditorAccessoryToolbarView: UIView {
|
|
|
762
1133
|
}
|
|
763
1134
|
}
|
|
764
1135
|
|
|
765
|
-
private func updateButtonAppearance(
|
|
1136
|
+
private func updateButtonAppearance(
|
|
1137
|
+
_ button: UIButton,
|
|
1138
|
+
item: NativeToolbarItem,
|
|
1139
|
+
enabled: Bool,
|
|
1140
|
+
active: Bool
|
|
1141
|
+
) {
|
|
1142
|
+
#if compiler(>=6.2)
|
|
1143
|
+
if #available(iOS 26.0, *), usesFloatingGlassButtons {
|
|
1144
|
+
var configuration = active
|
|
1145
|
+
? UIButton.Configuration.prominentGlass()
|
|
1146
|
+
: UIButton.Configuration.glass()
|
|
1147
|
+
configuration.cornerStyle = .capsule
|
|
1148
|
+
configuration.contentInsets = NSDirectionalEdgeInsets(
|
|
1149
|
+
top: 8,
|
|
1150
|
+
leading: 10,
|
|
1151
|
+
bottom: 8,
|
|
1152
|
+
trailing: 10
|
|
1153
|
+
)
|
|
1154
|
+
configuration.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(
|
|
1155
|
+
pointSize: 16,
|
|
1156
|
+
weight: .semibold
|
|
1157
|
+
)
|
|
1158
|
+
if let symbolName = item.icon?.resolvedSFSymbolName(),
|
|
1159
|
+
let symbolImage = UIImage(systemName: symbolName)
|
|
1160
|
+
{
|
|
1161
|
+
configuration.image = symbolImage
|
|
1162
|
+
configuration.title = nil
|
|
1163
|
+
} else {
|
|
1164
|
+
configuration.image = nil
|
|
1165
|
+
configuration.title = item.icon?.resolvedGlyphText() ?? "?"
|
|
1166
|
+
}
|
|
1167
|
+
button.configuration = configuration
|
|
1168
|
+
button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .semibold)
|
|
1169
|
+
button.alpha = enabled ? 1 : 0.45
|
|
1170
|
+
return
|
|
1171
|
+
}
|
|
1172
|
+
#endif
|
|
1173
|
+
|
|
1174
|
+
if resolvedAppearance == .native {
|
|
1175
|
+
button.tintColor = enabled ? nil : .systemGray
|
|
1176
|
+
button.tintAdjustmentMode = enabled ? .automatic : .normal
|
|
1177
|
+
button.alpha = 1
|
|
1178
|
+
button.backgroundColor = active
|
|
1179
|
+
? UIColor.white.withAlphaComponent(0.18)
|
|
1180
|
+
: .clear
|
|
1181
|
+
return
|
|
1182
|
+
}
|
|
1183
|
+
|
|
766
1184
|
let tintColor: UIColor
|
|
767
1185
|
if !enabled {
|
|
768
1186
|
tintColor = theme?.buttonDisabledColor ?? .tertiaryLabel
|
|
@@ -774,11 +1192,107 @@ final class EditorAccessoryToolbarView: UIView {
|
|
|
774
1192
|
|
|
775
1193
|
button.tintColor = tintColor
|
|
776
1194
|
button.setTitleColor(tintColor, for: .normal)
|
|
1195
|
+
button.alpha = enabled ? 1 : 0.7
|
|
777
1196
|
button.backgroundColor = active
|
|
778
1197
|
? (theme?.buttonActiveBackgroundColor ?? UIColor.systemBlue.withAlphaComponent(0.12))
|
|
779
1198
|
: .clear
|
|
780
1199
|
}
|
|
781
1200
|
|
|
1201
|
+
private var resolvedAppearance: EditorToolbarAppearance {
|
|
1202
|
+
theme?.appearance ?? .custom
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
private var resolvedHorizontalInset: CGFloat {
|
|
1206
|
+
theme?.resolvedHorizontalInset ?? Self.defaultHorizontalInset
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
private var resolvedKeyboardOffset: CGFloat {
|
|
1210
|
+
theme?.resolvedKeyboardOffset ?? Self.defaultKeyboardOffset
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
private var resolvedBorderRadius: CGFloat {
|
|
1214
|
+
theme?.resolvedBorderRadius ?? 0
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
private var resolvedBorderWidth: CGFloat {
|
|
1218
|
+
theme?.resolvedBorderWidth ?? 0.5
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
private var resolvedButtonBorderRadius: CGFloat {
|
|
1222
|
+
theme?.resolvedButtonBorderRadius ?? 8
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
private var usesFloatingGlassButtons: Bool {
|
|
1226
|
+
return false
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
private var usesNativeBarToolbar: Bool {
|
|
1230
|
+
return false
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
private var activeNativeToolbarScrollViewForTesting: UIScrollView {
|
|
1234
|
+
usesNativeBarToolbar ? nativeToolbarScrollView : scrollView
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
private func resolvedBlurEffect() -> UIVisualEffect {
|
|
1238
|
+
#if compiler(>=6.2)
|
|
1239
|
+
if #available(iOS 26.0, *) {
|
|
1240
|
+
let effect = UIGlassEffect(style: .regular)
|
|
1241
|
+
effect.isInteractive = true
|
|
1242
|
+
effect.tintColor = resolvedGlassEffectTintColor
|
|
1243
|
+
return effect
|
|
1244
|
+
}
|
|
1245
|
+
#endif
|
|
1246
|
+
if #available(iOS 13.0, *) {
|
|
1247
|
+
return UIBlurEffect(style: .systemUltraThinMaterial)
|
|
1248
|
+
}
|
|
1249
|
+
return UIBlurEffect(style: .extraLight)
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
private var resolvedEffectAlpha: CGFloat {
|
|
1253
|
+
if #available(iOS 26.0, *), resolvedAppearance == .native {
|
|
1254
|
+
return 1
|
|
1255
|
+
}
|
|
1256
|
+
return resolvedAppearance == .native ? 0.72 : 1
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
private var resolvedGlassTintAlpha: CGFloat {
|
|
1260
|
+
if #available(iOS 26.0, *), resolvedAppearance == .native {
|
|
1261
|
+
return 0
|
|
1262
|
+
}
|
|
1263
|
+
return resolvedAppearance == .native ? 0.12 : 0
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
private var resolvedGlassEffectTintColor: UIColor {
|
|
1267
|
+
return .clear
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
private var resolvedBorderColor: UIColor {
|
|
1271
|
+
if resolvedAppearance != .native {
|
|
1272
|
+
return theme?.borderColor ?? UIColor.separator
|
|
1273
|
+
}
|
|
1274
|
+
if #available(iOS 26.0, *) {
|
|
1275
|
+
return .clear
|
|
1276
|
+
}
|
|
1277
|
+
return UIColor.separator.withAlphaComponent(0.22)
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
private func refreshNativeHostTransparencyIfNeeded() {
|
|
1281
|
+
guard usesFloatingGlassButtons else { return }
|
|
1282
|
+
backgroundColor = .clear
|
|
1283
|
+
isOpaque = false
|
|
1284
|
+
|
|
1285
|
+
var ancestor: UIView? = self
|
|
1286
|
+
while let view = ancestor {
|
|
1287
|
+
let className = NSStringFromClass(type(of: view))
|
|
1288
|
+
if view === self || className.contains("UIInput") || className.contains("Accessory") {
|
|
1289
|
+
view.backgroundColor = .clear
|
|
1290
|
+
view.isOpaque = false
|
|
1291
|
+
}
|
|
1292
|
+
ancestor = view.superview
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
782
1296
|
@objc private func handleSelectMentionSuggestion(_ sender: MentionSuggestionChipButton) {
|
|
783
1297
|
onSelectMentionSuggestion?(sender.suggestion)
|
|
784
1298
|
}
|
|
@@ -791,18 +1305,17 @@ final class EditorAccessoryToolbarView: UIView {
|
|
|
791
1305
|
|
|
792
1306
|
class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognizerDelegate {
|
|
793
1307
|
|
|
794
|
-
private static let updateLog = Logger(
|
|
795
|
-
subsystem: "com.apollohg.prose-editor",
|
|
796
|
-
category: "view-command"
|
|
797
|
-
)
|
|
798
|
-
|
|
799
1308
|
// MARK: - Subviews
|
|
800
1309
|
|
|
801
1310
|
let richTextView: RichTextEditorView
|
|
802
|
-
private let accessoryToolbar = EditorAccessoryToolbarView(
|
|
1311
|
+
private let accessoryToolbar = EditorAccessoryToolbarView(
|
|
1312
|
+
frame: .zero,
|
|
1313
|
+
inputViewStyle: .keyboard
|
|
1314
|
+
)
|
|
803
1315
|
private var toolbarFrameInWindow: CGRect?
|
|
804
1316
|
private var didApplyAutoFocus = false
|
|
805
1317
|
private var toolbarState = NativeToolbarState.empty
|
|
1318
|
+
private var toolbarItems: [NativeToolbarItem] = NativeToolbarItem.defaults
|
|
806
1319
|
private var showsToolbar = true
|
|
807
1320
|
private var toolbarPlacement = "keyboard"
|
|
808
1321
|
private var heightBehavior: EditorHeightBehavior = .fixed
|
|
@@ -920,6 +1433,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
920
1433
|
toolbarState = .empty
|
|
921
1434
|
accessoryToolbar.apply(state: .empty)
|
|
922
1435
|
}
|
|
1436
|
+
refreshSystemAssistantToolbarIfNeeded()
|
|
923
1437
|
refreshMentionQuery()
|
|
924
1438
|
}
|
|
925
1439
|
|
|
@@ -928,8 +1442,9 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
928
1442
|
richTextView.applyTheme(theme)
|
|
929
1443
|
accessoryToolbar.apply(theme: theme?.toolbar)
|
|
930
1444
|
accessoryToolbar.apply(mentionTheme: theme?.mentions ?? addons.mentions?.theme)
|
|
1445
|
+
refreshSystemAssistantToolbarIfNeeded()
|
|
931
1446
|
if richTextView.textView.isFirstResponder,
|
|
932
|
-
richTextView.textView.inputAccessoryView === accessoryToolbar
|
|
1447
|
+
(richTextView.textView.inputAccessoryView === accessoryToolbar || shouldUseSystemAssistantToolbar)
|
|
933
1448
|
{
|
|
934
1449
|
richTextView.textView.reloadInputViews()
|
|
935
1450
|
}
|
|
@@ -941,6 +1456,10 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
941
1456
|
refreshMentionQuery()
|
|
942
1457
|
}
|
|
943
1458
|
|
|
1459
|
+
func setRemoteSelectionsJson(_ remoteSelectionsJson: String?) {
|
|
1460
|
+
richTextView.setRemoteSelections(RemoteSelectionDecoration.from(json: remoteSelectionsJson))
|
|
1461
|
+
}
|
|
1462
|
+
|
|
944
1463
|
func setEditable(_ editable: Bool) {
|
|
945
1464
|
richTextView.textView.isEditable = editable
|
|
946
1465
|
updateAccessoryToolbarVisibility()
|
|
@@ -974,6 +1493,10 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
974
1493
|
}
|
|
975
1494
|
}
|
|
976
1495
|
|
|
1496
|
+
func setAllowImageResizing(_ allowImageResizing: Bool) {
|
|
1497
|
+
richTextView.allowImageResizing = allowImageResizing
|
|
1498
|
+
}
|
|
1499
|
+
|
|
977
1500
|
private func emitContentHeightIfNeeded(force: Bool = false) {
|
|
978
1501
|
guard heightBehavior == .autoGrow else { return }
|
|
979
1502
|
let contentHeight = ceil(richTextView.intrinsicContentSize.height)
|
|
@@ -984,7 +1507,9 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
984
1507
|
}
|
|
985
1508
|
|
|
986
1509
|
func setToolbarButtonsJson(_ toolbarButtonsJson: String?) {
|
|
987
|
-
|
|
1510
|
+
toolbarItems = NativeToolbarItem.from(json: toolbarButtonsJson)
|
|
1511
|
+
accessoryToolbar.setItems(toolbarItems)
|
|
1512
|
+
refreshSystemAssistantToolbarIfNeeded()
|
|
988
1513
|
}
|
|
989
1514
|
|
|
990
1515
|
func setToolbarFrameJson(_ toolbarFrameJson: String?) {
|
|
@@ -1024,13 +1549,9 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1024
1549
|
/// Apply an editor update from JS. Sets the echo-suppression flag so the
|
|
1025
1550
|
/// resulting delegate callback is NOT re-dispatched back to JS.
|
|
1026
1551
|
func applyEditorUpdate(_ updateJson: String) {
|
|
1027
|
-
Self.updateLog.debug("[applyEditorUpdate.begin] bytes=\(updateJson.utf8.count)")
|
|
1028
1552
|
isApplyingJSUpdate = true
|
|
1029
1553
|
richTextView.textView.applyUpdateJSON(updateJson)
|
|
1030
1554
|
isApplyingJSUpdate = false
|
|
1031
|
-
Self.updateLog.debug(
|
|
1032
|
-
"[applyEditorUpdate.end] textState=\(self.richTextView.textView.textStorage.string.count)"
|
|
1033
|
-
)
|
|
1034
1555
|
}
|
|
1035
1556
|
|
|
1036
1557
|
// MARK: - Focus Commands
|
|
@@ -1047,12 +1568,14 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1047
1568
|
|
|
1048
1569
|
@objc private func textViewDidBeginEditing(_ notification: Notification) {
|
|
1049
1570
|
installOutsideTapRecognizerIfNeeded()
|
|
1571
|
+
richTextView.textView.refreshSelectionVisualState()
|
|
1050
1572
|
refreshMentionQuery()
|
|
1051
1573
|
onFocusChange(["isFocused": true])
|
|
1052
1574
|
}
|
|
1053
1575
|
|
|
1054
1576
|
@objc private func textViewDidEndEditing(_ notification: Notification) {
|
|
1055
1577
|
uninstallOutsideTapRecognizer()
|
|
1578
|
+
richTextView.textView.refreshSelectionVisualState()
|
|
1056
1579
|
clearMentionQueryStateAndHidePopover()
|
|
1057
1580
|
onFocusChange(["isFocused": false])
|
|
1058
1581
|
}
|
|
@@ -1060,6 +1583,11 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1060
1583
|
@objc private func handleOutsideTap(_ recognizer: UITapGestureRecognizer) {
|
|
1061
1584
|
guard recognizer.state == .ended else { return }
|
|
1062
1585
|
guard richTextView.textView.isFirstResponder else { return }
|
|
1586
|
+
guard let tapWindow = gestureWindow ?? window else { return }
|
|
1587
|
+
let locationInWindow = recognizer.location(in: tapWindow)
|
|
1588
|
+
guard shouldHandleOutsideTap(locationInWindow: locationInWindow, touchedView: nil) else {
|
|
1589
|
+
return
|
|
1590
|
+
}
|
|
1063
1591
|
blur()
|
|
1064
1592
|
}
|
|
1065
1593
|
|
|
@@ -1082,16 +1610,32 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1082
1610
|
|
|
1083
1611
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
|
1084
1612
|
guard gestureRecognizer === outsideTapGestureRecognizer else { return true }
|
|
1085
|
-
|
|
1613
|
+
guard let tapWindow = gestureWindow ?? window else { return true }
|
|
1614
|
+
let locationInWindow = touch.location(in: tapWindow)
|
|
1615
|
+
let result = shouldHandleOutsideTap(
|
|
1616
|
+
locationInWindow: locationInWindow,
|
|
1617
|
+
touchedView: touch.view
|
|
1618
|
+
)
|
|
1619
|
+
return result
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
private func shouldHandleOutsideTap(
|
|
1623
|
+
locationInWindow: CGPoint,
|
|
1624
|
+
touchedView: UIView?
|
|
1625
|
+
) -> Bool {
|
|
1626
|
+
if let touchedView, touchedView.isDescendant(of: self) {
|
|
1086
1627
|
return false
|
|
1087
1628
|
}
|
|
1088
|
-
if let
|
|
1629
|
+
if let tapWindow = gestureWindow ?? window {
|
|
1630
|
+
let editorFrameInWindow = convert(bounds, to: tapWindow)
|
|
1631
|
+
if editorFrameInWindow.contains(locationInWindow) {
|
|
1632
|
+
return false
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
if let touchedView, touchedView.isDescendant(of: accessoryToolbar) {
|
|
1089
1636
|
return false
|
|
1090
1637
|
}
|
|
1091
|
-
if let toolbarFrameInWindow,
|
|
1092
|
-
let window = gestureWindow,
|
|
1093
|
-
toolbarFrameInWindow.contains(touch.location(in: window))
|
|
1094
|
-
{
|
|
1638
|
+
if let toolbarFrameInWindow, toolbarFrameInWindow.contains(locationInWindow) {
|
|
1095
1639
|
return false
|
|
1096
1640
|
}
|
|
1097
1641
|
return true
|
|
@@ -1101,7 +1645,9 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1101
1645
|
|
|
1102
1646
|
func editorTextView(_ textView: EditorTextView, selectionDidChange anchor: UInt32, head: UInt32) {
|
|
1103
1647
|
refreshToolbarStateFromEditorSelection()
|
|
1648
|
+
refreshSystemAssistantToolbarIfNeeded()
|
|
1104
1649
|
refreshMentionQuery()
|
|
1650
|
+
richTextView.refreshRemoteSelections()
|
|
1105
1651
|
onSelectionChange(["anchor": Int(anchor), "head": Int(head)])
|
|
1106
1652
|
}
|
|
1107
1653
|
|
|
@@ -1109,10 +1655,11 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1109
1655
|
if let state = NativeToolbarState(updateJSON: updateJSON) {
|
|
1110
1656
|
toolbarState = state
|
|
1111
1657
|
accessoryToolbar.apply(state: state)
|
|
1658
|
+
refreshSystemAssistantToolbarIfNeeded()
|
|
1112
1659
|
}
|
|
1113
1660
|
refreshMentionQuery()
|
|
1661
|
+
richTextView.refreshRemoteSelections()
|
|
1114
1662
|
guard !isApplyingJSUpdate else { return }
|
|
1115
|
-
Self.updateLog.debug("[didReceiveUpdate] bytes=\(updateJSON.utf8.count)")
|
|
1116
1663
|
onEditorUpdate(["updateJson": updateJSON])
|
|
1117
1664
|
}
|
|
1118
1665
|
|
|
@@ -1131,6 +1678,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1131
1678
|
accessoryToolbar.onSelectMentionSuggestion = { [weak self] suggestion in
|
|
1132
1679
|
self?.insertMentionSuggestion(suggestion)
|
|
1133
1680
|
}
|
|
1681
|
+
accessoryToolbar.setItems(toolbarItems)
|
|
1134
1682
|
accessoryToolbar.apply(state: toolbarState)
|
|
1135
1683
|
updateAccessoryToolbarVisibility()
|
|
1136
1684
|
}
|
|
@@ -1154,6 +1702,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1154
1702
|
mentionQueryState = queryState
|
|
1155
1703
|
accessoryToolbar.apply(mentionTheme: richTextView.textView.theme?.mentions ?? mentions.theme)
|
|
1156
1704
|
let didChangeToolbarHeight = accessoryToolbar.setMentionSuggestions(suggestions)
|
|
1705
|
+
refreshSystemAssistantToolbarIfNeeded()
|
|
1157
1706
|
if didChangeToolbarHeight,
|
|
1158
1707
|
richTextView.textView.isFirstResponder,
|
|
1159
1708
|
richTextView.textView.inputAccessoryView === accessoryToolbar
|
|
@@ -1172,6 +1721,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1172
1721
|
private func clearMentionQueryStateAndHidePopover() {
|
|
1173
1722
|
mentionQueryState = nil
|
|
1174
1723
|
let didChangeToolbarHeight = accessoryToolbar.setMentionSuggestions([])
|
|
1724
|
+
refreshSystemAssistantToolbarIfNeeded()
|
|
1175
1725
|
if didChangeToolbarHeight,
|
|
1176
1726
|
richTextView.textView.isFirstResponder,
|
|
1177
1727
|
richTextView.textView.inputAccessoryView === accessoryToolbar
|
|
@@ -1365,9 +1915,11 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1365
1915
|
accessoryToolbar.triggerMentionSuggestionTapForTesting(at: index)
|
|
1366
1916
|
}
|
|
1367
1917
|
private func updateAccessoryToolbarVisibility() {
|
|
1918
|
+
refreshSystemAssistantToolbarIfNeeded()
|
|
1368
1919
|
let nextAccessoryView: UIView? = showsToolbar &&
|
|
1369
1920
|
toolbarPlacement == "keyboard" &&
|
|
1370
|
-
richTextView.textView.isEditable
|
|
1921
|
+
richTextView.textView.isEditable &&
|
|
1922
|
+
!shouldUseSystemAssistantToolbar
|
|
1371
1923
|
? accessoryToolbar
|
|
1372
1924
|
: nil
|
|
1373
1925
|
if richTextView.textView.inputAccessoryView !== nextAccessoryView {
|
|
@@ -1378,6 +1930,19 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1378
1930
|
}
|
|
1379
1931
|
}
|
|
1380
1932
|
|
|
1933
|
+
private var shouldUseSystemAssistantToolbar: Bool {
|
|
1934
|
+
false
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
private func refreshSystemAssistantToolbarIfNeeded() {
|
|
1938
|
+
guard #available(iOS 26.0, *) else { return }
|
|
1939
|
+
|
|
1940
|
+
let assistantItem = richTextView.textView.inputAssistantItem
|
|
1941
|
+
assistantItem.allowsHidingShortcuts = false
|
|
1942
|
+
assistantItem.leadingBarButtonGroups = []
|
|
1943
|
+
assistantItem.trailingBarButtonGroups = []
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1381
1946
|
private func handleListToggle(_ listType: String) {
|
|
1382
1947
|
let isActive = toolbarState.nodes[listType] == true
|
|
1383
1948
|
richTextView.textView.performToolbarToggleList(listType, isActive: isActive)
|
|
@@ -1388,6 +1953,8 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1388
1953
|
case .mark:
|
|
1389
1954
|
guard let mark = item.mark else { return }
|
|
1390
1955
|
richTextView.textView.performToolbarToggleMark(mark)
|
|
1956
|
+
case .blockquote:
|
|
1957
|
+
richTextView.textView.performToolbarToggleBlockquote()
|
|
1391
1958
|
case .list:
|
|
1392
1959
|
guard let listType = item.listType?.rawValue else { return }
|
|
1393
1960
|
handleListToggle(listType)
|