247 lines
7.4 KiB
Swift
247 lines
7.4 KiB
Swift
import WebKit
|
|
|
|
/// Describes actions that can be sent from the WebView back to native views.
|
|
/// The names on the javascript side must match for an action to be handled.
|
|
public enum WebViewAction: String, CaseIterable {
|
|
case highlightAction
|
|
case readingProgressUpdate
|
|
}
|
|
|
|
public final class WebView: WKWebView {
|
|
#if os(iOS)
|
|
private var panGestureRecognizer: UIPanGestureRecognizer?
|
|
private var tapGestureRecognizer: UITapGestureRecognizer?
|
|
#endif
|
|
|
|
override init(frame: CGRect, configuration: WKWebViewConfiguration) {
|
|
super.init(frame: frame, configuration: configuration)
|
|
|
|
#if os(iOS)
|
|
initNativeIOSMenus()
|
|
#endif
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder _: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
public func increaseFontSize() {
|
|
dispatchEvent("increaseFontSize")
|
|
}
|
|
|
|
public func decreaseFontSize() {
|
|
dispatchEvent("decreaseFontSize")
|
|
}
|
|
|
|
public func increaseMargin() {
|
|
print("increase margin")
|
|
dispatchEvent("increaseMargin")
|
|
}
|
|
|
|
public func decreaseMargin() {
|
|
print("decrease margin")
|
|
dispatchEvent("decreaseMargin")
|
|
}
|
|
|
|
public func increaseLineHeight() {
|
|
print("increase line height")
|
|
dispatchEvent("increaseLineHeight")
|
|
}
|
|
|
|
public func decreaseLineHeight() {
|
|
print("decrease line height")
|
|
dispatchEvent("decreaseLineHeight")
|
|
}
|
|
|
|
public func shareOriginalItem() {
|
|
dispatchEvent("share")
|
|
}
|
|
|
|
func dispatchEvent(_ name: String) {
|
|
let dispatch = "document.dispatchEvent(new Event('\(name)'));"
|
|
evaluateJavaScript(dispatch) { obj, err in
|
|
if let err = err { print(err) }
|
|
if let obj = obj { print(obj) }
|
|
}
|
|
}
|
|
|
|
#if os(iOS)
|
|
override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
super.traitCollectionDidChange(previousTraitCollection)
|
|
guard previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle else { return }
|
|
|
|
if traitCollection.userInterfaceStyle == .dark {
|
|
dispatchEvent("switchToDarkMode")
|
|
} else {
|
|
dispatchEvent("switchToLightMode")
|
|
}
|
|
}
|
|
|
|
#elseif os(macOS)
|
|
override public func viewDidChangeEffectiveAppearance() {
|
|
super.viewDidChangeEffectiveAppearance()
|
|
switch effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) {
|
|
case .some(.darkAqua):
|
|
dispatchEvent("switchToDarkMode")
|
|
default:
|
|
dispatchEvent("switchToLightMode")
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#if os(iOS)
|
|
extension WebView: UIGestureRecognizerDelegate, WKScriptMessageHandler {
|
|
func initNativeIOSMenus() {
|
|
isUserInteractionEnabled = true
|
|
|
|
NotificationCenter.default
|
|
.addObserver(self,
|
|
selector: #selector(menuDidHide),
|
|
name: UIMenuController.didHideMenuNotification,
|
|
object: nil)
|
|
|
|
setDefaultMenu()
|
|
}
|
|
|
|
public func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) {
|
|
guard let messageBody = message.body as? [String: Any] else { return }
|
|
guard let actionID = messageBody["actionID"] as? String else { return }
|
|
|
|
switch actionID {
|
|
case "showMenu":
|
|
if let rectX = messageBody["rectX"] as? Double,
|
|
let rectY = messageBody["rectY"] as? Double,
|
|
let rectWidth = messageBody["rectWidth"] as? Double,
|
|
let rectHeight = messageBody["rectHeight"] as? Double
|
|
{ // swiftlint:disable:this opening_brace
|
|
showHighlightMenu(CGRect(x: rectX, y: rectY, width: rectWidth, height: rectHeight))
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
private func setDefaultMenu() {
|
|
let annotate = UIMenuItem(title: "Annotate", action: #selector(annotateSelection))
|
|
let highlight = UIMenuItem(title: "Highlight", action: #selector(highlightSelection))
|
|
// let share = UIMenuItem(title: "Share", action: #selector(shareSelection))
|
|
|
|
UIMenuController.shared.menuItems = [highlight, /* share, */ annotate]
|
|
}
|
|
|
|
private func setHighlightMenu() {
|
|
let annotate = UIMenuItem(title: "Annotate", action: #selector(annotateSelection))
|
|
let remove = UIMenuItem(title: "Remove", action: #selector(removeSelection))
|
|
// let share = UIMenuItem(title: "Share", action: #selector(shareSelection))
|
|
|
|
UIMenuController.shared.menuItems = [remove, /* share, */ annotate]
|
|
}
|
|
|
|
override public var canBecomeFirstResponder: Bool {
|
|
true
|
|
}
|
|
|
|
@objc func menuDidHide() {
|
|
setDefaultMenu()
|
|
}
|
|
|
|
// swiftlint:disable:next line_length
|
|
public func gestureRecognizer(_: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith _: UIGestureRecognizer) -> Bool {
|
|
true
|
|
}
|
|
|
|
override public func canPerformAction(_ action: Selector, withSender _: Any?) -> Bool {
|
|
switch action {
|
|
case #selector(annotateSelection): return true
|
|
case #selector(highlightSelection): return true
|
|
case #selector(shareSelection): return true
|
|
case #selector(removeSelection): return true
|
|
case #selector(copy(_:)): return true
|
|
case Selector(("_lookup:")): return true
|
|
default: return false
|
|
}
|
|
}
|
|
|
|
@objc private func gestureHandled() {
|
|
hideMenuAndDismissHighlight()
|
|
}
|
|
|
|
@objc private func annotateSelection() {
|
|
dispatchEvent("annotate")
|
|
hideMenu()
|
|
}
|
|
|
|
@objc private func highlightSelection() {
|
|
dispatchEvent("highlight")
|
|
hideMenu()
|
|
}
|
|
|
|
@objc private func shareSelection() {
|
|
dispatchEvent("share")
|
|
hideMenu()
|
|
}
|
|
|
|
@objc private func removeSelection() {
|
|
dispatchEvent("remove")
|
|
hideMenu()
|
|
}
|
|
|
|
@objc override public func copy(_ sender: Any?) {
|
|
super.copy(sender)
|
|
dispatchEvent("copyHighlight")
|
|
hideMenu()
|
|
}
|
|
|
|
private func hideMenu() {
|
|
UIMenuController.shared.hideMenu()
|
|
if let tapGestureRecognizer = tapGestureRecognizer {
|
|
removeGestureRecognizer(tapGestureRecognizer)
|
|
self.tapGestureRecognizer = nil
|
|
}
|
|
if let panGestureRecognizer = panGestureRecognizer {
|
|
removeGestureRecognizer(panGestureRecognizer)
|
|
self.panGestureRecognizer = nil
|
|
}
|
|
setDefaultMenu()
|
|
}
|
|
|
|
private func hideMenuAndDismissHighlight() {
|
|
hideMenu()
|
|
dispatchEvent("dismissHighlight")
|
|
}
|
|
|
|
private func showHighlightMenu(_ rect: CGRect) {
|
|
setHighlightMenu()
|
|
|
|
// When the highlight menu is displayed we set up gesture recognizers so it
|
|
// can be dismissed if the user interacts with another part of the view.
|
|
// This isn't needed for the default menu as the system will handle that.
|
|
if tapGestureRecognizer == nil {
|
|
let tap = UITapGestureRecognizer(target: self, action: #selector(gestureHandled))
|
|
tap.delegate = self
|
|
addGestureRecognizer(tap)
|
|
tapGestureRecognizer = tap
|
|
}
|
|
if panGestureRecognizer == nil {
|
|
let pan = UIPanGestureRecognizer(target: self, action: #selector(gestureHandled))
|
|
pan.delegate = self
|
|
addGestureRecognizer(pan)
|
|
panGestureRecognizer = pan
|
|
}
|
|
|
|
UIMenuController.shared.showMenu(from: self, rect: rect)
|
|
}
|
|
|
|
public func saveAnnotation(annotation: String) {
|
|
// swiftlint:disable:next line_length
|
|
let dispatch = "var event = new Event('saveAnnotation');event.annotation = '\(annotation)';document.dispatchEvent(event);"
|
|
evaluateJavaScript(dispatch) { obj, err in
|
|
if let err = err { print(err) }
|
|
if let obj = obj { print(obj) }
|
|
}
|
|
}
|
|
}
|
|
#endif
|