Neon-Vision-Editor/Neon Vision Editor/UI/MarkdownPreviewWebView.swift

103 lines
3.7 KiB
Swift
Raw Normal View History

import SwiftUI
import WebKit
#if os(macOS)
/// MARK: - Types
struct MarkdownPreviewWebView: NSViewRepresentable {
let html: String
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeNSView(context: Context) -> WKWebView {
let webView = makeConfiguredWebView()
webView.loadHTMLString(html, baseURL: nil)
context.coordinator.lastHTML = html
return webView
}
func updateNSView(_ webView: WKWebView, context: Context) {
guard context.coordinator.lastHTML != html else { return }
context.coordinator.reloadPreservingScroll(webView: webView, html: html)
context.coordinator.lastHTML = html
}
final class Coordinator {
var lastHTML: String = ""
func reloadPreservingScroll(webView: WKWebView, html: String) {
let capture = "(() => { const max = Math.max(1, document.documentElement.scrollHeight - window.innerHeight); return window.scrollY / max; })();"
webView.evaluateJavaScript(capture) { value, _ in
let ratio = value as? Double ?? 0
webView.loadHTMLString(html, baseURL: nil)
let clamped = min(1.0, max(0.0, ratio))
DispatchQueue.main.asyncAfter(deadline: .now() + 0.06) {
let restore = "(() => { const max = Math.max(0, document.documentElement.scrollHeight - window.innerHeight); window.scrollTo(0, max * \(clamped)); })();"
webView.evaluateJavaScript(restore, completionHandler: nil)
}
}
}
}
}
#elseif os(iOS)
struct MarkdownPreviewWebView: UIViewRepresentable {
let html: String
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeUIView(context: Context) -> WKWebView {
let webView = makeConfiguredWebView()
webView.loadHTMLString(html, baseURL: nil)
context.coordinator.lastHTML = html
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
guard context.coordinator.lastHTML != html else { return }
context.coordinator.reloadPreservingScroll(webView: webView, html: html)
context.coordinator.lastHTML = html
}
final class Coordinator {
var lastHTML: String = ""
func reloadPreservingScroll(webView: WKWebView, html: String) {
let capture = "(() => { const max = Math.max(1, document.documentElement.scrollHeight - window.innerHeight); return window.scrollY / max; })();"
webView.evaluateJavaScript(capture) { value, _ in
let ratio = value as? Double ?? 0
webView.loadHTMLString(html, baseURL: nil)
let clamped = min(1.0, max(0.0, ratio))
DispatchQueue.main.asyncAfter(deadline: .now() + 0.06) {
let restore = "(() => { const max = Math.max(0, document.documentElement.scrollHeight - window.innerHeight); window.scrollTo(0, max * \(clamped)); })();"
webView.evaluateJavaScript(restore, completionHandler: nil)
}
}
}
}
}
#endif
private func makeConfiguredWebView() -> WKWebView {
let configuration = WKWebViewConfiguration()
configuration.defaultWebpagePreferences.allowsContentJavaScript = false
let webView = WKWebView(frame: .zero, configuration: configuration)
#if os(macOS)
webView.setValue(false, forKey: "drawsBackground")
#else
webView.isOpaque = false
webView.backgroundColor = .clear
webView.scrollView.backgroundColor = .clear
#endif
webView.allowsBackForwardNavigationGestures = false
#if os(iOS)
webView.scrollView.contentInsetAdjustmentBehavior = .never
#endif
return webView
}