|
|
@@ -1,8 +1,6 @@
|
|
|
import Capacitor
|
|
|
import UIKit
|
|
|
|
|
|
-// MARK: - Model
|
|
|
-
|
|
|
private struct NativeEditorAction {
|
|
|
let id: String
|
|
|
let title: String
|
|
|
@@ -16,8 +14,6 @@ private struct NativeEditorAction {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// MARK: - Toolbar View
|
|
|
-
|
|
|
private class NativeEditorToolbarView: UIView {
|
|
|
/// Callback when any action is tapped.
|
|
|
var onActionTapped: ((String) -> Void)?
|
|
|
@@ -29,9 +25,6 @@ private class NativeEditorToolbarView: UIView {
|
|
|
private var storedActions: [NativeEditorAction] = []
|
|
|
private var storedTrailingAction: NativeEditorAction?
|
|
|
|
|
|
- /// Bottom constraint we adjust when the keyboard moves.
|
|
|
- private var bottomConstraint: NSLayoutConstraint?
|
|
|
-
|
|
|
private let blurView: UIVisualEffectView = {
|
|
|
let effect = UIBlurEffect(style: .systemChromeMaterial)
|
|
|
let view = UIVisualEffectView(effect: effect)
|
|
|
@@ -103,8 +96,6 @@ private class NativeEditorToolbarView: UIView {
|
|
|
|
|
|
private var trailingActionId: String?
|
|
|
|
|
|
- // MARK: - Init
|
|
|
-
|
|
|
override init(frame: CGRect) {
|
|
|
super.init(frame: frame)
|
|
|
setupView()
|
|
|
@@ -121,16 +112,11 @@ private class NativeEditorToolbarView: UIView {
|
|
|
actions: [NativeEditorAction],
|
|
|
trailingAction: NativeEditorAction?,
|
|
|
tintColor: UIColor?,
|
|
|
- backgroundColor: UIColor?,
|
|
|
- initialKeyboardOverlap: CGFloat) {
|
|
|
+ backgroundColor: UIColor?) {
|
|
|
// Bump generation to invalidate any previous dismiss completion
|
|
|
dismissGeneration += 1
|
|
|
layer.removeAllAnimations()
|
|
|
|
|
|
- // reset visual state in case a previous animation left us mid-way
|
|
|
- alpha = 1
|
|
|
- transform = .identity
|
|
|
-
|
|
|
// Store actions so we can re-apply them when theme changes
|
|
|
storedActions = actions
|
|
|
storedTrailingAction = trailingAction
|
|
|
@@ -139,10 +125,6 @@ private class NativeEditorToolbarView: UIView {
|
|
|
configure(actions: actions,
|
|
|
trailingAction: trailingAction)
|
|
|
attachIfNeeded(to: host)
|
|
|
-
|
|
|
- // Apply current keyboard overlap immediately, so we are in the right spot
|
|
|
- updateBottomInset(extra: initialKeyboardOverlap)
|
|
|
-
|
|
|
animateIn()
|
|
|
}
|
|
|
|
|
|
@@ -153,7 +135,6 @@ private class NativeEditorToolbarView: UIView {
|
|
|
layer.removeAllAnimations()
|
|
|
|
|
|
guard animated else {
|
|
|
- guard currentGen == self.dismissGeneration else { return }
|
|
|
removeFromSuperview()
|
|
|
transform = .identity
|
|
|
alpha = 1
|
|
|
@@ -163,25 +144,19 @@ private class NativeEditorToolbarView: UIView {
|
|
|
UIView.animate(withDuration: 0.16,
|
|
|
delay: 0,
|
|
|
options: [.curveEaseIn, .allowUserInteraction],
|
|
|
- animations: { [weak self] in
|
|
|
- guard let self = self, currentGen == self.dismissGeneration else { return }
|
|
|
+ animations: {
|
|
|
self.alpha = 0
|
|
|
self.transform = CGAffineTransform(translationX: 0, y: 8)
|
|
|
- }, completion: { [weak self] _ in
|
|
|
- guard let self = self, currentGen == self.dismissGeneration else { return }
|
|
|
- self.removeFromSuperview()
|
|
|
- self.transform = .identity
|
|
|
- self.alpha = 1
|
|
|
+ }, completion: { _ in
|
|
|
+ // Only remove if no newer present/dismiss has happened.
|
|
|
+ if currentGen == self.dismissGeneration {
|
|
|
+ self.removeFromSuperview()
|
|
|
+ self.transform = .identity
|
|
|
+ self.alpha = 1
|
|
|
+ }
|
|
|
})
|
|
|
}
|
|
|
|
|
|
- /// Called by the plugin when the keyboard frame changes.
|
|
|
- func updateBottomInset(extra: CGFloat) {
|
|
|
- let bottomGap: CGFloat = 8 // same as above
|
|
|
- bottomConstraint?.constant = -bottomGap - extra
|
|
|
- superview?.layoutIfNeeded()
|
|
|
- }
|
|
|
-
|
|
|
// MARK: - Theme / trait changes
|
|
|
|
|
|
/// Returns the theme-appropriate tint (light: black, dark: white).
|
|
|
@@ -262,8 +237,6 @@ private class NativeEditorToolbarView: UIView {
|
|
|
// Always use Logseq background (theme-aware)
|
|
|
let bgBase = UIColor.logseqBackground
|
|
|
|
|
|
- // For debugging, you can temporarily color this aggressively:
|
|
|
- // blurView.backgroundColor = UIColor.systemPink.withAlphaComponent(0.9)
|
|
|
blurView.backgroundColor = bgBase.withAlphaComponent(0.9)
|
|
|
blurView.contentView.backgroundColor = .clear
|
|
|
|
|
|
@@ -321,18 +294,14 @@ private class NativeEditorToolbarView: UIView {
|
|
|
|
|
|
let leading = leadingAnchor.constraint(equalTo: host.leadingAnchor, constant: 16)
|
|
|
let trailing = trailingAnchor.constraint(equalTo: host.trailingAnchor, constant: -16)
|
|
|
- let bottomGap: CGFloat = 8 // tweak to taste
|
|
|
+ let bottom: NSLayoutConstraint
|
|
|
+ if #available(iOS 15.0, *) {
|
|
|
+ bottom = bottomAnchor.constraint(equalTo: host.keyboardLayoutGuide.topAnchor, constant: -10)
|
|
|
+ } else {
|
|
|
+ bottom = bottomAnchor.constraint(equalTo: host.safeAreaLayoutGuide.bottomAnchor, constant: -16)
|
|
|
+ }
|
|
|
|
|
|
- let bottom = bottomAnchor.constraint(
|
|
|
- equalTo: host.bottomAnchor,
|
|
|
- constant: -bottomGap
|
|
|
- )
|
|
|
- bottomConstraint = bottom
|
|
|
NSLayoutConstraint.activate([leading, trailing, bottom])
|
|
|
-
|
|
|
- #if DEBUG
|
|
|
- print("[NativeEditorToolbar] attachIfNeeded host=\(type(of: host)) frame=\(host.bounds)")
|
|
|
- #endif
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -401,8 +370,6 @@ private class NativeEditorToolbarView: UIView {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// MARK: - Plugin
|
|
|
-
|
|
|
@objc(NativeEditorToolbarPlugin)
|
|
|
public class NativeEditorToolbarPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
public let identifier = "NativeEditorToolbarPlugin"
|
|
|
@@ -413,41 +380,6 @@ public class NativeEditorToolbarPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
]
|
|
|
|
|
|
private var toolbar: NativeEditorToolbarView?
|
|
|
- private var keyboardObservers: [NSObjectProtocol] = []
|
|
|
- private var lastKeyboardOverlap: CGFloat = 0
|
|
|
-
|
|
|
- // MARK: - Lifecycle
|
|
|
-
|
|
|
- public override func load() {
|
|
|
- super.load()
|
|
|
-
|
|
|
- let center = NotificationCenter.default
|
|
|
-
|
|
|
- let willChange = center.addObserver(
|
|
|
- forName: UIResponder.keyboardWillChangeFrameNotification,
|
|
|
- object: nil,
|
|
|
- queue: .main
|
|
|
- ) { [weak self] note in
|
|
|
- self?.handleKeyboard(notification: note)
|
|
|
- }
|
|
|
-
|
|
|
- let willHide = center.addObserver(
|
|
|
- forName: UIResponder.keyboardWillHideNotification,
|
|
|
- object: nil,
|
|
|
- queue: .main
|
|
|
- ) { [weak self] note in
|
|
|
- self?.handleKeyboard(notification: note)
|
|
|
- }
|
|
|
-
|
|
|
- keyboardObservers = [willChange, willHide]
|
|
|
- }
|
|
|
-
|
|
|
- deinit {
|
|
|
- let center = NotificationCenter.default
|
|
|
- keyboardObservers.forEach { center.removeObserver($0) }
|
|
|
- }
|
|
|
-
|
|
|
- // MARK: - Public API
|
|
|
|
|
|
@objc func present(_ call: CAPPluginCall) {
|
|
|
let rawActions = call.getArray("actions", JSObject.self) ?? []
|
|
|
@@ -467,7 +399,7 @@ public class NativeEditorToolbarPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
|
|
|
// If there are no actions and no trailing action, dismiss and clear toolbar
|
|
|
guard !actions.isEmpty || trailingAction != nil else {
|
|
|
- self.toolbar?.dismiss(animated: false)
|
|
|
+ self.toolbar?.dismiss(animated: true)
|
|
|
self.toolbar = nil
|
|
|
call.resolve()
|
|
|
return
|
|
|
@@ -483,8 +415,7 @@ public class NativeEditorToolbarPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
actions: actions,
|
|
|
trailingAction: trailingAction,
|
|
|
tintColor: nil,
|
|
|
- backgroundColor: nil,
|
|
|
- initialKeyboardOverlap: self.lastKeyboardOverlap)
|
|
|
+ backgroundColor: nil)
|
|
|
|
|
|
self.toolbar = bar
|
|
|
call.resolve()
|
|
|
@@ -493,55 +424,17 @@ public class NativeEditorToolbarPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
|
|
|
@objc func dismiss(_ call: CAPPluginCall) {
|
|
|
DispatchQueue.main.async {
|
|
|
- // JS-driven dismisses can be non-animated to avoid timing glitches
|
|
|
- self.toolbar?.dismiss(animated: false)
|
|
|
+ self.toolbar?.dismiss(animated: true)
|
|
|
self.toolbar = nil
|
|
|
call.resolve()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // MARK: - Keyboard handling
|
|
|
-
|
|
|
- private func handleKeyboard(notification: Notification) {
|
|
|
- guard let host = hostView() else { return }
|
|
|
-
|
|
|
- guard
|
|
|
- let userInfo = notification.userInfo,
|
|
|
- let frameValue = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
|
|
|
- else { return }
|
|
|
-
|
|
|
- let keyboardFrameInScreen = frameValue.cgRectValue
|
|
|
- let keyboardFrameInHost = host.convert(keyboardFrameInScreen, from: nil)
|
|
|
-
|
|
|
- let overlap = max(0, host.bounds.maxY - keyboardFrameInHost.minY)
|
|
|
- lastKeyboardOverlap = overlap
|
|
|
-
|
|
|
- guard let toolbar = self.toolbar else { return }
|
|
|
-
|
|
|
- let duration = (userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0.25
|
|
|
- let curveRaw = (userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber)?.intValue ?? UIView.AnimationCurve.easeInOut.rawValue
|
|
|
- let curve = UIView.AnimationCurve(rawValue: curveRaw) ?? .easeInOut
|
|
|
- let options = UIView.AnimationOptions(rawValue: UInt(curve.rawValue << 16))
|
|
|
-
|
|
|
- UIView.animate(withDuration: duration,
|
|
|
- delay: 0,
|
|
|
- options: [options, .allowUserInteraction],
|
|
|
- animations: {
|
|
|
- toolbar.updateBottomInset(extra: overlap)
|
|
|
- }, completion: nil)
|
|
|
- }
|
|
|
-
|
|
|
- // MARK: - Host view resolution
|
|
|
-
|
|
|
private func hostView() -> UIView? {
|
|
|
- // Root view that owns the webview for both Home & Capture
|
|
|
- if let vcView = bridge?.viewController?.view {
|
|
|
- return vcView
|
|
|
- }
|
|
|
- if let parentView = bridge?.viewController?.parent?.view {
|
|
|
- return parentView
|
|
|
+ if let parent = bridge?.viewController?.parent?.view {
|
|
|
+ return parent
|
|
|
}
|
|
|
- return nil
|
|
|
+ return bridge?.viewController?.view
|
|
|
}
|
|
|
}
|
|
|
|