From a041300b4f20fedaa28ed8470ea55b9e8d253473 Mon Sep 17 00:00:00 2001 From: Ilyas Hallak Date: Wed, 22 Oct 2025 15:35:56 +0200 Subject: [PATCH] feat: Add text selection support for iOS 26+ NativeWebView Implement text selection detection in NativeWebView: - Add onTextSelected callback parameter to NativeWebView - Use JavaScript polling to detect text selections - Calculate text offsets for precise annotation positioning - Integrate color picker in BookmarkDetailView2 for iOS 26+ - Match feature parity with legacy WebView implementation Text selection now works on both WebView implementations. --- .../BookmarkDetail/BookmarkDetailView2.swift | 8 ++- readeck/UI/Components/NativeWebView.swift | 57 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/readeck/UI/BookmarkDetail/BookmarkDetailView2.swift b/readeck/UI/BookmarkDetail/BookmarkDetailView2.swift index 043ba27..a5b355c 100644 --- a/readeck/UI/BookmarkDetail/BookmarkDetailView2.swift +++ b/readeck/UI/BookmarkDetail/BookmarkDetailView2.swift @@ -471,7 +471,13 @@ struct BookmarkDetailView2: View { webViewHeight = height } }, - selectedAnnotationId: viewModel.selectedAnnotationId + selectedAnnotationId: viewModel.selectedAnnotationId, + onTextSelected: { text, startOffset, endOffset in + selectedText = text + selectedStartOffset = startOffset + selectedEndOffset = endOffset + showingColorPicker = true + } ) .frame(height: webViewHeight) .cornerRadius(14) diff --git a/readeck/UI/Components/NativeWebView.swift b/readeck/UI/Components/NativeWebView.swift index 6ba8c48..f0ebd0b 100644 --- a/readeck/UI/Components/NativeWebView.swift +++ b/readeck/UI/Components/NativeWebView.swift @@ -12,6 +12,7 @@ struct NativeWebView: View { let onHeightChange: (CGFloat) -> Void var onScroll: ((Double) -> Void)? = nil var selectedAnnotationId: String? + var onTextSelected: ((String, Int, Int) -> Void)? = nil @State private var webPage = WebPage() @Environment(\.colorScheme) private var colorScheme @@ -21,6 +22,7 @@ struct NativeWebView: View { .scrollDisabled(true) // Disable internal scrolling .onAppear { loadStyledContent() + setupTextSelectionCallback() } .onChange(of: htmlContent) { _, _ in loadStyledContent() @@ -42,6 +44,53 @@ struct NativeWebView: View { } } } + + private func setupTextSelectionCallback() { + guard let onTextSelected = onTextSelected else { return } + + // Poll for text selection using JavaScript + Task { + while true { + try? await Task.sleep(nanoseconds: 300_000_000) // Check every 0.3s + + let script = """ + (function() { + const selection = window.getSelection(); + if (selection && selection.toString().length > 0) { + const range = selection.getRangeAt(0); + const selectedText = selection.toString(); + + const preRange = document.createRange(); + preRange.selectNodeContents(document.body); + preRange.setEnd(range.startContainer, range.startOffset); + const startOffset = preRange.toString().length; + const endOffset = startOffset + selectedText.length; + + return { + text: selectedText, + startOffset: startOffset, + endOffset: endOffset + }; + } + return null; + })(); + """ + + do { + if let result = try await webPage.evaluateJavaScript(script) as? [String: Any], + let text = result["text"] as? String, + let startOffset = result["startOffset"] as? Int, + let endOffset = result["endOffset"] as? Int { + await MainActor.run { + onTextSelected(text, startOffset, endOffset) + } + } + } catch { + // Silently continue polling + } + } + } + } private func updateContentHeightWithJS() async { var lastHeight: CGFloat = 0 @@ -290,6 +339,9 @@ struct NativeWebView: View { scheduleHeightCheck(); + // Text selection detection + \(generateTextSelectionJS()) + // Scroll to selected annotation \(generateScrollToAnnotationJS()) @@ -324,6 +376,11 @@ struct NativeWebView: View { } } + private func generateTextSelectionJS() -> String { + // Not needed for iOS 26 - we use polling instead + return "" + } + private func generateScrollToAnnotationJS() -> String { guard let selectedId = selectedAnnotationId else { return ""