Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 52 additions & 22 deletions mac/Config/Config/PackageContentWebView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import WebKit
import KeymanSettings

public struct PackageContentWebView: NSViewRepresentable {

let packageFileUrl: URL

// create the AppKit view instance
Expand All @@ -21,12 +22,18 @@ public struct PackageContentWebView: NSViewRepresentable {

// Connect the delegate to catch link clicks
webView.navigationDelegate = context.coordinator

return webView
}

// update the view when SwiftUI state changes
/**
* update the view when SwiftUI state changes
*/
public func updateNSView(_ nsView: WKWebView, context: Context) {
// first update the parent in the coordinator so we know which
// package we are displaying content for
context.coordinator.parent = self

let request = URLRequest(url: packageFileUrl)

// only load the request if it's not already loading/loaded to prevent infinite loops
Expand All @@ -41,50 +48,73 @@ public struct PackageContentWebView: NSViewRepresentable {
* Coordinator acts as the WKNavigationDelegate
*/
public func makeCoordinator() -> Coordinator {
Coordinator()
Coordinator(self)
}

/**
* If a url links to the web rather than locally, open it in the default browser
*/
@MainActor
public class Coordinator: NSObject, WKNavigationDelegate {
var parent: PackageContentWebView

init(_ parent: PackageContentWebView) {
self.parent = parent
}

/**
* If a url links to the web rather than locally, open it in the default browser
* Must force load links in the package content that change the main frame
* because WKWebView's security sandbox will block loading of new pages
*/
public func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping @MainActor @Sendable (WKNavigationActionPolicy) -> Void) {

// if not url, cancel
// if no url, cancel
guard let url = navigationAction.request.url else {
decisionHandler(.cancel)
return
}

// if not user-activated, pass through, e.g. for redirects
guard navigationAction.navigationType == .linkActivated else {
decisionHandler(.allow)
return
print("url not user activated \(url.path())")
decisionHandler(.allow)
return
}

// local files load in webview
// load local files in webview, force-loading files that are in the same directory
if url.isFileURL {
decisionHandler(.allow)
let standardizedIncomingUrl = url.standardizedFileURL
let packageDirectoryUrl = parent.packageFileUrl.deletingLastPathComponent()
let standardizedPackageDirectoryUrl = packageDirectoryUrl.standardizedFileURL

// check whether the file is from the current package
if standardizedIncomingUrl.path.hasPrefix(standardizedPackageDirectoryUrl.path) {
decisionHandler(.cancel) // Cancel regular navigation

// Force load with read permissions of package directory
webView.loadFileURL(standardizedIncomingUrl, allowingReadAccessTo: standardizedPackageDirectoryUrl)
return
} else {
// block local files outside of your target directory, not sure how we would receive one
decisionHandler(.cancel)
return
}
}

// handle external links by opening in web browser
// not a file URL: handle external links by opening in web browser
decisionHandler(.cancel)

var externalUrl = url

// strip "link:" prefix if present
let urlString = url.absoluteString
if urlString.hasPrefix("link:https://") || urlString.hasPrefix("link:http://") {
let cleanString = urlString.replacingOccurrences(of: "link:", with: "")
if let cleanUrl = URL(string: cleanString) {
externalUrl = cleanUrl
}
let cleanString = urlString.replacingOccurrences(of: "link:", with: "")
if let cleanUrl = URL(string: cleanString) {
externalUrl = cleanUrl
}
}

// Open the external link in the default browser
NSWorkspace.shared.open(externalUrl)
}
Expand Down