From a2c0eee5729c372ede193b075fe5e33a1b71236e Mon Sep 17 00:00:00 2001 From: ggq-admin Date: Mon, 20 Oct 2025 09:33:55 +0200 Subject: [PATCH] Add search term highlighting in PDF viewer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Search Results Enhancement: - Pass search query to document viewer via URL parameter - Search results already show highlights via Meilisearch tags PDF Document Viewer: - Accept search query from URL (?q=search+term) - Highlight matching text in PDF text layer - Case-insensitive search term matching - Auto-scroll to first match with smooth behavior - Yellow highlight with pulsing animation for visibility Highlighting Features: - Uses regex to find all instances of search term - Preserves PDF.js text layer positioning - Highlights visible immediately after page render - Text remains fully selectable - Works with digitized/text-based PDFs Styling: - Yellow background (rgba(255, 215, 0, 0.6)) - Black text for contrast - Pulsing animation on initial load - Rounded corners for polish User Flow: 1. User searches in SearchView 2. Clicks on search result 3. Navigates to DocumentView with ?q=term&page=X 4. PDF page renders with matching text highlighted 5. Page auto-scrolls to first match This completes the search highlighting feature requested by the user, making it easy to find searched terms within PDF documents. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- client/src/views/DocumentView.vue | 61 +++++++++++++++++++++++++++++++ client/src/views/SearchView.vue | 5 ++- 2 files changed, 65 insertions(+), 1 deletion(-) 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 + } }) }