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:
parent
ec12815a51
commit
a041300b4f
@ -471,7 +471,13 @@ struct BookmarkDetailView2: View {
|
|||||||
webViewHeight = height
|
webViewHeight = height
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
selectedAnnotationId: viewModel.selectedAnnotationId
|
selectedAnnotationId: viewModel.selectedAnnotationId,
|
||||||
|
onTextSelected: { text, startOffset, endOffset in
|
||||||
|
selectedText = text
|
||||||
|
selectedStartOffset = startOffset
|
||||||
|
selectedEndOffset = endOffset
|
||||||
|
showingColorPicker = true
|
||||||
|
}
|
||||||
)
|
)
|
||||||
.frame(height: webViewHeight)
|
.frame(height: webViewHeight)
|
||||||
.cornerRadius(14)
|
.cornerRadius(14)
|
||||||
|
|||||||
@ -12,6 +12,7 @@ struct NativeWebView: View {
|
|||||||
let onHeightChange: (CGFloat) -> Void
|
let onHeightChange: (CGFloat) -> Void
|
||||||
var onScroll: ((Double) -> Void)? = nil
|
var onScroll: ((Double) -> Void)? = nil
|
||||||
var selectedAnnotationId: String?
|
var selectedAnnotationId: String?
|
||||||
|
var onTextSelected: ((String, Int, Int) -> Void)? = nil
|
||||||
|
|
||||||
@State private var webPage = WebPage()
|
@State private var webPage = WebPage()
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
@ -21,6 +22,7 @@ struct NativeWebView: View {
|
|||||||
.scrollDisabled(true) // Disable internal scrolling
|
.scrollDisabled(true) // Disable internal scrolling
|
||||||
.onAppear {
|
.onAppear {
|
||||||
loadStyledContent()
|
loadStyledContent()
|
||||||
|
setupTextSelectionCallback()
|
||||||
}
|
}
|
||||||
.onChange(of: htmlContent) { _, _ in
|
.onChange(of: htmlContent) { _, _ in
|
||||||
loadStyledContent()
|
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 {
|
private func updateContentHeightWithJS() async {
|
||||||
var lastHeight: CGFloat = 0
|
var lastHeight: CGFloat = 0
|
||||||
@ -290,6 +339,9 @@ struct NativeWebView: View {
|
|||||||
|
|
||||||
scheduleHeightCheck();
|
scheduleHeightCheck();
|
||||||
|
|
||||||
|
// Text selection detection
|
||||||
|
\(generateTextSelectionJS())
|
||||||
|
|
||||||
// Scroll to selected annotation
|
// Scroll to selected annotation
|
||||||
\(generateScrollToAnnotationJS())
|
\(generateScrollToAnnotationJS())
|
||||||
</script>
|
</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 {
|
private func generateScrollToAnnotationJS() -> String {
|
||||||
guard let selectedId = selectedAnnotationId else {
|
guard let selectedId = selectedAnnotationId else {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user