diff --git a/Examples/Example macOS/Example macOS/UI/ViewController.swift b/Examples/Example macOS/Example macOS/UI/ViewController.swift index a6c0daf..2b4535c 100644 --- a/Examples/Example macOS/Example macOS/UI/ViewController.swift +++ b/Examples/Example macOS/Example macOS/UI/ViewController.swift @@ -21,6 +21,7 @@ final class ViewController: NSViewController { super.viewDidLoad() editor = RichHTMLEditorView() + editor.delegate = self if let cssURL = Bundle.main.url(forResource: "style", withExtension: "css"), let styleCSS = try? String(contentsOf: cssURL) { editor.injectAdditionalCSS(styleCSS) @@ -50,8 +51,39 @@ final class ViewController: NSViewController { editor.italic() case .underline: editor.underline() + case .addLink: + presentLinkAlert() default: print("Action not handled.") } } + + private func presentLinkAlert() { + let alert = NSAlert() + alert.alertStyle = .informational + alert.messageText = "Insert a new link" + let validateButton = alert.addButton(withTitle: "Validate") + alert.addButton(withTitle: "Cancel") + + let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 200, height: 24)) + alert.accessoryView = textField + + let response = alert.runModal() + + guard response.rawValue == validateButton.tag, + !textField.stringValue.isEmpty, + let url = URL(string: textField.stringValue) + else { return } + + editor.addLink(url: url, text: url.absoluteString) + } +} + +// MARK: - RichHTMLEditorViewDelegate + +extension ViewController: RichHTMLEditorViewDelegate { + func richHTMLEditorView(_ richHTMLEditorView: RichHTMLEditorView, shouldHandleLink link: URL) -> Bool { + NSWorkspace.shared.open(link) + return true + } } diff --git a/Examples/Example macOS/Example macOS/UI/WindowController.swift b/Examples/Example macOS/Example macOS/UI/WindowController.swift index df30d1b..8ffaab2 100644 --- a/Examples/Example macOS/Example macOS/UI/WindowController.swift +++ b/Examples/Example macOS/Example macOS/UI/WindowController.swift @@ -17,6 +17,7 @@ extension NSToolbarItem.Identifier { static let bold = NSToolbarItem.Identifier(rawValue: "Bold") static let italic = NSToolbarItem.Identifier(rawValue: "Italic") static let underline = NSToolbarItem.Identifier(rawValue: "Underline") + static let addLink = NSToolbarItem.Identifier(rawValue: "AddLink") } extension Notification.Name { @@ -40,7 +41,8 @@ extension WindowController: NSToolbarDelegate { return [ NSToolbarItem.Identifier.bold, NSToolbarItem.Identifier.italic, - NSToolbarItem.Identifier.underline + NSToolbarItem.Identifier.underline, + NSToolbarItem.Identifier.addLink ] } @@ -49,7 +51,8 @@ extension WindowController: NSToolbarDelegate { NSToolbarItem.Identifier.flexibleSpace, NSToolbarItem.Identifier.bold, NSToolbarItem.Identifier.italic, - NSToolbarItem.Identifier.underline + NSToolbarItem.Identifier.underline, + NSToolbarItem.Identifier.addLink ] } @@ -65,6 +68,8 @@ extension WindowController: NSToolbarDelegate { return createToolbarItem(itemIdentifier: itemIdentifier, image: "italic", label: "Italic") case .underline: return createToolbarItem(itemIdentifier: itemIdentifier, image: "underline", label: "Underline") + case .addLink: + return createToolbarItem(itemIdentifier: itemIdentifier, image: "link", label: "Link") default: return nil } diff --git a/Sources/InfomaniakRichHTMLEditor/RichHTMLEditorView.swift b/Sources/InfomaniakRichHTMLEditor/RichHTMLEditorView.swift index d721a46..44f61c8 100644 --- a/Sources/InfomaniakRichHTMLEditor/RichHTMLEditorView.swift +++ b/Sources/InfomaniakRichHTMLEditor/RichHTMLEditorView.swift @@ -200,6 +200,7 @@ public extension RichHTMLEditorView { #if canImport(UIKit) webView.scrollView.delegate = self #endif + webView.navigationDelegate = self addSubview(webView) NSLayoutConstraint.activate([ @@ -276,6 +277,27 @@ public extension RichHTMLEditorView { #endif } +// MARK: - WKNavigationDelegate + +extension RichHTMLEditorView: WKNavigationDelegate { + public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping @MainActor (WKNavigationActionPolicy) -> Void) { + switch navigationAction.navigationType { + case .linkActivated: + if let url = navigationAction.request.url, delegate?.richHTMLEditorView(self, shouldHandleLink: url) == true { + decisionHandler(.cancel) + } else { + decisionHandler(.allow) + } + case .backForward, .formSubmitted, .reload, .formResubmitted: + decisionHandler(.cancel) + case .other: + decisionHandler(.allow) + @unknown default: + decisionHandler(.allow) + } + } +} + // MARK: - UIScrollViewDelegate #if canImport(UIKit) diff --git a/Sources/InfomaniakRichHTMLEditor/RichHTMLEditorViewDelegate.swift b/Sources/InfomaniakRichHTMLEditor/RichHTMLEditorViewDelegate.swift index 3645097..308b7d9 100644 --- a/Sources/InfomaniakRichHTMLEditor/RichHTMLEditorViewDelegate.swift +++ b/Sources/InfomaniakRichHTMLEditor/RichHTMLEditorViewDelegate.swift @@ -81,6 +81,22 @@ public protocol RichHTMLEditorViewDelegate: AnyObject { javascriptFunctionDidFail javascriptError: any Error, whileExecuting function: String ) + + /// Asks the delegate if the editor should handle opening the link itself. + /// + /// The user can open the link thanks to the contextual menu. If the editor handles this task itself, + /// the link will open in the default browser on iOS but in the editor on macOS. + /// You may need to customize this behaviour. If the method returns `true`, the editor won't + /// open the link and you will be responsible for doing so. + /// + /// Implementation of this method is optional. Default return value is `false`. + /// + /// - Parameters: + /// - richHTMLEditorView: The editor which is loaded. + /// - shouldHandleLink: The URL the user clicked on. + /// + /// - Returns: `false` if the editor should handle the link opening itself. + func richHTMLEditorView(_ richHTMLEditorView: RichHTMLEditorView, shouldHandleLink link: URL) -> Bool } // Default implementation for optional functions @@ -97,4 +113,7 @@ public extension RichHTMLEditorViewDelegate { javascriptFunctionDidFail javascriptError: any Error, whileExecuting function: String ) {} + func richHTMLEditorView(_ richHTMLEditorView: RichHTMLEditorView, shouldHandleLink link: URL) -> Bool { + return false + } } diff --git a/Sources/InfomaniakRichHTMLEditor/SwiftUI/Views/RichEditor+Modifier.swift b/Sources/InfomaniakRichHTMLEditor/SwiftUI/Views/RichEditor+Modifier.swift index 1fe60e4..7b14e1d 100644 --- a/Sources/InfomaniakRichHTMLEditor/SwiftUI/Views/RichEditor+Modifier.swift +++ b/Sources/InfomaniakRichHTMLEditor/SwiftUI/Views/RichEditor+Modifier.swift @@ -79,4 +79,17 @@ public extension View { func introspectEditor(perform action: @escaping (RichHTMLEditorView) -> Void) -> some View { environment(\.introspectEditor, action) } + + /// Tells the editor whether to handle the opening a link or perform an action to open it. + /// + /// The default behavior is to let the editor handle opening links. + /// + /// - Parameter action: A closure to run when the editor tries to open a link. The closure + /// should return `false` if the editor should handle the opening, `true` if you intend to manage + /// this task yourself. + /// + /// - Returns: A view with the customizations applied to editor. + func handleLinkOpening(perform action: @escaping (URL) -> Bool) -> some View { + environment(\.handleLinkOpening, action) + } } diff --git a/Sources/InfomaniakRichHTMLEditor/SwiftUI/Views/RichHTMLEditor+Environment.swift b/Sources/InfomaniakRichHTMLEditor/SwiftUI/Views/RichHTMLEditor+Environment.swift index c27d50b..d16dbfd 100644 --- a/Sources/InfomaniakRichHTMLEditor/SwiftUI/Views/RichHTMLEditor+Environment.swift +++ b/Sources/InfomaniakRichHTMLEditor/SwiftUI/Views/RichHTMLEditor+Environment.swift @@ -47,6 +47,10 @@ public struct IntrospectEditorKey: EnvironmentKey { public static let defaultValue: ((RichHTMLEditorView) -> Void)? = nil } +public struct HandleLinkOpeningKey: EnvironmentKey { + public static let defaultValue: ((URL) -> Bool)? = nil +} + // MARK: - Environment Values public extension EnvironmentValues { @@ -88,4 +92,9 @@ public extension EnvironmentValues { get { self[IntrospectEditorKey.self] } set { self[IntrospectEditorKey.self] = newValue } } + + var handleLinkOpening: ((URL) -> Bool)? { + get { self[HandleLinkOpeningKey.self] } + set { self[HandleLinkOpeningKey.self] = newValue } + } } diff --git a/Sources/InfomaniakRichHTMLEditor/SwiftUI/Views/RichHTMLEditor.swift b/Sources/InfomaniakRichHTMLEditor/SwiftUI/Views/RichHTMLEditor.swift index 4b983b8..b9e4690 100644 --- a/Sources/InfomaniakRichHTMLEditor/SwiftUI/Views/RichHTMLEditor.swift +++ b/Sources/InfomaniakRichHTMLEditor/SwiftUI/Views/RichHTMLEditor.swift @@ -32,6 +32,7 @@ public struct RichHTMLEditor: PlateformViewRepresentable { @Environment(\.onCaretPositionChange) var onCaretPositionChange @Environment(\.onJavaScriptFunctionFail) var onJavaScriptFunctionFail @Environment(\.introspectEditor) var introspectEditor + @Environment(\.handleLinkOpening) var handleLinkOpening @Binding public var html: String @ObservedObject public var textAttributes: TextAttributes diff --git a/Sources/InfomaniakRichHTMLEditor/SwiftUI/Views/RichHTMLEditorCoordinator.swift b/Sources/InfomaniakRichHTMLEditor/SwiftUI/Views/RichHTMLEditorCoordinator.swift index 6728bc7..f2446cb 100644 --- a/Sources/InfomaniakRichHTMLEditor/SwiftUI/Views/RichHTMLEditorCoordinator.swift +++ b/Sources/InfomaniakRichHTMLEditor/SwiftUI/Views/RichHTMLEditorCoordinator.swift @@ -48,4 +48,8 @@ public final class RichHTMLEditorCoordinator: RichHTMLEditorViewDelegate { ) { parent.onJavaScriptFunctionFail?(javascriptError, function) } + + public func richHTMLEditorView(_ richHTMLEditorView: RichHTMLEditorView, shouldHandleLink link: URL) -> Bool { + return parent.handleLinkOpening?(link) ?? false + } }