mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
1418957b6f
- resolve all 69 client type-checking errors for stricter TS compliance - remove obsolete type-check baseline scripts and files - overhaul project root with updated README.md and new DEVELOPMENT.md - migrate to unified baseline migration strategy and cleanup docker configs - remove redundant production and dev-specific shell scripts
109 lines
2.8 KiB
TypeScript
109 lines
2.8 KiB
TypeScript
import { ref } from 'vue'
|
|
import type { Ref } from 'vue'
|
|
|
|
export interface PdfFindState {
|
|
query: string
|
|
matchCase: boolean
|
|
wholeWord: boolean
|
|
highlightAll: boolean
|
|
matchCount: number
|
|
currentIndex: number
|
|
}
|
|
|
|
const MATCH_CLASS = 'pdf-find-match'
|
|
const CURRENT_CLASS = 'pdf-find-match-current'
|
|
|
|
export function usePdfFind(textLayerMap: Ref<Map<number, HTMLElement>>) {
|
|
const query = ref('')
|
|
const matchCase = ref(false)
|
|
const wholeWord = ref(false)
|
|
const highlightAll = ref(true)
|
|
const matchCount = ref(0)
|
|
const currentIndex = ref(0)
|
|
|
|
let matches: HTMLElement[] = []
|
|
|
|
function normalize(s: string) {
|
|
return matchCase.value ? s : s.toLowerCase()
|
|
}
|
|
|
|
function isWordBoundary(text: string, start: number, len: number): boolean {
|
|
const before = start === 0 ? ' ' : (text[start - 1] ?? ' ')
|
|
const after = start + len >= text.length ? ' ' : (text[start + len] ?? ' ')
|
|
return /\W/.test(before) && /\W/.test(after)
|
|
}
|
|
|
|
function clearHighlights() {
|
|
for (const el of matches) {
|
|
el.classList.remove(MATCH_CLASS, CURRENT_CLASS)
|
|
}
|
|
matches = []
|
|
}
|
|
|
|
function search() {
|
|
clearHighlights()
|
|
|
|
const q = normalize(query.value.trim())
|
|
if (!q) {
|
|
matchCount.value = 0
|
|
currentIndex.value = 0
|
|
return
|
|
}
|
|
|
|
const found: HTMLElement[] = []
|
|
for (const [, layer] of textLayerMap.value) {
|
|
const spans = Array.from(layer.querySelectorAll('span')) as HTMLElement[]
|
|
for (const span of spans) {
|
|
const text = normalize(span.textContent ?? '')
|
|
if (!text.includes(q)) continue
|
|
if (wholeWord.value && !isWordBoundary(text, text.indexOf(q), q.length)) continue
|
|
found.push(span)
|
|
}
|
|
}
|
|
|
|
matches = found
|
|
matchCount.value = found.length
|
|
|
|
if (found.length === 0) {
|
|
currentIndex.value = 0
|
|
return
|
|
}
|
|
|
|
currentIndex.value = Math.min(currentIndex.value, found.length - 1)
|
|
|
|
for (const el of found) {
|
|
el.classList.add(MATCH_CLASS)
|
|
}
|
|
scrollToMatch(currentIndex.value)
|
|
}
|
|
|
|
function scrollToMatch(idx: number) {
|
|
for (const el of matches) el.classList.remove(CURRENT_CLASS)
|
|
const el = matches[idx]
|
|
if (!el) return
|
|
el.classList.add(CURRENT_CLASS)
|
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
}
|
|
|
|
function next() {
|
|
if (matchCount.value === 0) return
|
|
currentIndex.value = (currentIndex.value + 1) % matchCount.value
|
|
scrollToMatch(currentIndex.value)
|
|
}
|
|
|
|
function prev() {
|
|
if (matchCount.value === 0) return
|
|
currentIndex.value = (currentIndex.value - 1 + matchCount.value) % matchCount.value
|
|
scrollToMatch(currentIndex.value)
|
|
}
|
|
|
|
function clear() {
|
|
clearHighlights()
|
|
query.value = ''
|
|
matchCount.value = 0
|
|
currentIndex.value = 0
|
|
}
|
|
|
|
return { query, matchCase, wholeWord, highlightAll, matchCount, currentIndex, search, next, prev, clear }
|
|
}
|