'use strict'; // --------------------------------------------------------------------------- // Reader Search // --------------------------------------------------------------------------- let _readerSearchDebounce = null; function toggleReaderSearch() { const overlay = $('reader-overlay'); const contentEl = $('reader-content'); if (!overlay || !contentEl) return; const existing = document.getElementById('reader-search-bar'); if (existing) { existing.remove(); readerSearchOpen = false; clearReaderSearch(); return; } readerSearchOpen = true; const bar = document.createElement('div'); bar.id = 'reader-search-bar'; bar.className = 'reader-search-bar'; bar.innerHTML = ` `; overlay.insertBefore(bar, $('reader-body-row')); const input = bar.querySelector('#reader-search-input'); input.focus(); input.addEventListener('input', () => { clearTimeout(_readerSearchDebounce); _readerSearchDebounce = setTimeout(() => doReaderSearch(input.value.trim()), 300); }); input.addEventListener('keydown', e => { if (e.key === 'Enter') { e.shiftKey ? readerSearchPrev() : readerSearchNext(); } if (e.key === 'Escape') { toggleReaderSearch(); } }); bar.querySelector('#rs-search-prev').addEventListener('click', readerSearchPrev); bar.querySelector('#rs-search-next').addEventListener('click', readerSearchNext); bar.querySelector('#rs-search-clear').addEventListener('click', toggleReaderSearch); } async function doReaderSearch(query) { const contentEl = $('reader-content'); if (!contentEl) return; const countEl = document.getElementById('rs-search-count'); clearReaderSearchHighlights(); searchMatches = []; searchMatchIndex = -1; if (!query) { if (countEl) countEl.textContent = ''; return; } if (!currentPdfDoc) { // EPUB: snapshot original content if (!searchOriginalContent) { searchOriginalContent = contentEl.innerHTML; } else { contentEl.innerHTML = searchOriginalContent; applyHighlightsToContent(); } const walker = document.createTreeWalker(contentEl, NodeFilter.SHOW_TEXT); const lq = query.toLowerCase(); const ranges = []; let node; while ((node = walker.nextNode())) { const text = node.textContent; const lt = text.toLowerCase(); let idx = 0; while ((idx = lt.indexOf(lq, idx)) !== -1) { const range = document.createRange(); range.setStart(node, idx); range.setEnd(node, idx + query.length); ranges.push(range); idx += query.length; } } // Insert marks in reverse to preserve range validity for (let i = ranges.length - 1; i >= 0; i--) { try { const mark = document.createElement('mark'); mark.className = 'reader-search-match'; ranges[i].surroundContents(mark); searchMatches.unshift(mark); } catch (e) {} } } else { // PDF: collect text layer spans const spans = contentEl.querySelectorAll('.pdf-text-layer > span'); const lq = query.toLowerCase(); for (const span of spans) { if (span.textContent.toLowerCase().includes(lq)) { span.classList.add('reader-search-match'); searchMatches.push(span); } } } if (countEl) countEl.textContent = searchMatches.length ? `1 / ${searchMatches.length}` : '0'; if (searchMatches.length) { searchMatchIndex = 0; scrollToSearchMatch(0); } } function clearReaderSearchHighlights() { if (!currentPdfDoc) { // EPUB: restore from snapshot if (searchOriginalContent !== null) { const contentEl = $('reader-content'); if (contentEl) { contentEl.innerHTML = searchOriginalContent; applyHighlightsToContent(); } searchOriginalContent = null; } else { // Just remove marks without full restore document.querySelectorAll('mark.reader-search-match').forEach(m => { const parent = m.parentNode; while (m.firstChild) parent.insertBefore(m.firstChild, m); parent.removeChild(m); }); } } else { // PDF: remove highlight class from spans document.querySelectorAll('.reader-search-match').forEach(el => { el.classList.remove('reader-search-match', 'active'); }); } searchMatches = []; searchMatchIndex = -1; } function clearReaderSearch() { clearTimeout(_readerSearchDebounce); clearReaderSearchHighlights(); readerSearchOpen = false; const countEl = document.getElementById('rs-search-count'); if (countEl) countEl.textContent = ''; } function scrollToSearchMatch(idx) { if (!searchMatches.length) return; _suppressScrollJumpDetect(); searchMatches.forEach((m, i) => m.classList.toggle('active', i === idx)); searchMatches[idx].scrollIntoView({behavior: 'smooth', block: 'center'}); const countEl = document.getElementById('rs-search-count'); if (countEl) countEl.textContent = `${idx + 1} / ${searchMatches.length}`; } function readerSearchNext() { if (!searchMatches.length) return; searchMatchIndex = (searchMatchIndex + 1) % searchMatches.length; scrollToSearchMatch(searchMatchIndex); } function readerSearchPrev() { if (!searchMatches.length) return; searchMatchIndex = (searchMatchIndex - 1 + searchMatches.length) % searchMatches.length; scrollToSearchMatch(searchMatchIndex); }