diff --git a/client/src/views/DocumentView.vue b/client/src/views/DocumentView.vue index b52c76f..a7c190a 100644 --- a/client/src/views/DocumentView.vue +++ b/client/src/views/DocumentView.vue @@ -157,6 +157,7 @@ const route = useRoute() const documentId = ref(route.params.id) const currentPage = ref(parseInt(route.query.page, 10) || 1) const pageInput = ref(currentPage.value) +const searchQuery = ref(route.query.q || '') const totalPages = ref(0) const documentTitle = ref('Loading...') const boatInfo = ref('') @@ -216,6 +217,41 @@ async function loadDocument() { } } +function highlightSearchTerms() { + if (!textLayer.value || !searchQuery.value) return + + const spans = textLayer.value.querySelectorAll('span') + const query = searchQuery.value.toLowerCase().trim() + let firstMatch = null + + spans.forEach(span => { + const text = span.textContent + if (!text) return + + const lowerText = text.toLowerCase() + if (lowerText.includes(query)) { + // Create a highlighted version + const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi') + const highlightedText = text.replace(regex, '$1') + + // Wrap in a container to preserve PDF.js positioning + span.innerHTML = highlightedText + + // Track first match for scrolling + if (!firstMatch) { + firstMatch = span + } + } + }) + + // Scroll to first match + if (firstMatch) { + setTimeout(() => { + firstMatch.scrollIntoView({ behavior: 'smooth', block: 'center' }) + }, 100) + } +} + async function renderPage(pageNum) { if (!pdfDoc || componentIsUnmounting) return @@ -280,6 +316,12 @@ async function renderPage(pageNum) { viewport: viewport, textDivs: [] }) + + // Highlight search terms if query exists + if (searchQuery.value) { + await nextTick() + highlightSearchTerms() + } } catch (textErr) { console.warn('Failed to render text layer:', textErr) } @@ -477,4 +519,23 @@ onBeforeUnmount(() => { .textLayer ::-moz-selection { background: rgba(255, 92, 178, 0.3); } + +/* Search highlighting */ +.search-highlight { + background-color: rgba(255, 215, 0, 0.6); + color: #000; + padding: 2px 0; + border-radius: 2px; + font-weight: 600; + animation: highlight-pulse 1.5s ease-in-out; +} + +@keyframes highlight-pulse { + 0%, 100% { + background-color: rgba(255, 215, 0, 0.6); + } + 50% { + background-color: rgba(255, 215, 0, 0.9); + } +} diff --git a/client/src/views/SearchView.vue b/client/src/views/SearchView.vue index 0bdd931..5308f95 100644 --- a/client/src/views/SearchView.vue +++ b/client/src/views/SearchView.vue @@ -187,7 +187,10 @@ function viewDocument(result) { router.push({ name: 'document', params: { id: result.docId }, - query: { page: result.pageNumber } + query: { + page: result.pageNumber, + q: searchQuery.value // Pass search query for highlighting + } }) }