Add search term highlighting in PDF viewer

Search Results Enhancement:
- Pass search query to document viewer via URL parameter
- Search results already show highlights via Meilisearch <mark> 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 <noreply@anthropic.com>
This commit is contained in:
ggq-admin 2025-10-20 09:33:55 +02:00
parent e0ae22cf63
commit a2c0eee572
2 changed files with 65 additions and 1 deletions

View file

@ -157,6 +157,7 @@ const route = useRoute()
const documentId = ref(route.params.id) const documentId = ref(route.params.id)
const currentPage = ref(parseInt(route.query.page, 10) || 1) const currentPage = ref(parseInt(route.query.page, 10) || 1)
const pageInput = ref(currentPage.value) const pageInput = ref(currentPage.value)
const searchQuery = ref(route.query.q || '')
const totalPages = ref(0) const totalPages = ref(0)
const documentTitle = ref('Loading...') const documentTitle = ref('Loading...')
const boatInfo = ref('') 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, '<mark class="search-highlight">$1</mark>')
// 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) { async function renderPage(pageNum) {
if (!pdfDoc || componentIsUnmounting) return if (!pdfDoc || componentIsUnmounting) return
@ -280,6 +316,12 @@ async function renderPage(pageNum) {
viewport: viewport, viewport: viewport,
textDivs: [] textDivs: []
}) })
// Highlight search terms if query exists
if (searchQuery.value) {
await nextTick()
highlightSearchTerms()
}
} catch (textErr) { } catch (textErr) {
console.warn('Failed to render text layer:', textErr) console.warn('Failed to render text layer:', textErr)
} }
@ -477,4 +519,23 @@ onBeforeUnmount(() => {
.textLayer ::-moz-selection { .textLayer ::-moz-selection {
background: rgba(255, 92, 178, 0.3); 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);
}
}
</style> </style>

View file

@ -187,7 +187,10 @@ function viewDocument(result) {
router.push({ router.push({
name: 'document', name: 'document',
params: { id: result.docId }, params: { id: result.docId },
query: { page: result.pageNumber } query: {
page: result.pageNumber,
q: searchQuery.value // Pass search query for highlighting
}
}) })
} }