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.
This commit is contained in:
Ilyas Hallak 2025-10-22 15:35:56 +02:00
parent ec12815a51
commit a041300b4f
2 changed files with 64 additions and 1 deletions

View File

@ -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)

View File

@ -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())
</script>
@ -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 ""