import React, { useState, useEffect } from 'react'; import useActions from '../actions'; import useStore from '../store'; import SearchComponent from '../components/Search'; import { normalize, calculatePhraseMatch, convertMatches, } from '../helpers/pdf'; import { scrollIntoView } from '../helpers/utility'; type MatchType = { page: number; index: number; }; const Search: React.FC = () => { const [queryString, setQueryString] = useState(''); const [matchesMap, setMatchesMap] = useState([]); const [matchTotal, setMatchTotal] = useState(0); const [prevIndex, setPrevIndex] = useState(-1); const [currentIndex, setCurrentIndex] = useState(-1); const [{ navbarState, pdf, totalPage, textDivs }, dispatch] = useStore(); const { setNavbar } = useActions(dispatch); const getTextContent = async (pageNum: number): Promise => { const page = await pdf.getPage(pageNum); const textContent = await page.getTextContent({ normalizeWhitespace: true, }); return textContent.items; }; const extractTextItems = async (pageNum: number): Promise => { const textContent = await getTextContent(pageNum); const strBuf = []; for (let j = 0, len = textContent.length; j < len; j += 1) { if (textContent[j].str) { strBuf.push(textContent[j].str); } } return strBuf; }; const getMatchTextIndex = async ( pageNum: number, queryStr: string ): Promise => { const contentItems = await extractTextItems(pageNum); const content = normalize(contentItems.join('').toLowerCase()); const matches = calculatePhraseMatch(content, queryStr); if (matches.length) { matches.forEach(ele => { matchesMap.push({ page: pageNum, index: ele, }); }); setMatchesMap(matchesMap); setMatchTotal(cur => cur + matches.length); } if (pageNum < totalPage) { await getMatchTextIndex(pageNum + 1, queryStr); } }; const startSearchPdf = async (queryStr: string): Promise => { getMatchTextIndex(1, queryStr); }; const appendTextToDiv = async ( pageNum: number, divIdx: number, fromOffset: number, toOffset: number | undefined, highlight: boolean ): Promise => { const textContentItem = await extractTextItems(pageNum); if (textDivs[pageNum]) { const domElements = textDivs[pageNum][divIdx]; const content = textContentItem[divIdx].substring(fromOffset, toOffset); const node = document.createTextNode(content); const span = document.createElement('span'); if (highlight) { span.style.backgroundColor = 'rgba(255, 211, 0, 0.7)'; span.appendChild(node); domElements.appendChild(span); scrollIntoView(domElements, { top: -120 }); } else { domElements.textContent = ''; domElements.appendChild(node); } } }; const beginText = async ( pageNum: number, begin: Record ): Promise => { const { divIdx } = begin; const domElements = textDivs[pageNum][divIdx]; if (domElements) { domElements.textContent = ''; appendTextToDiv(pageNum, divIdx, 0, begin.offset, false); } }; const cleanMatches = async ( pageNum: number, matchIndex: number, queryStr: string ): Promise => { const textContentItem = await extractTextItems(pageNum); const { begin, end } = convertMatches( queryStr, matchIndex, textContentItem ); for (let i = begin.divIdx; i <= end.divIdx; i += 1) { appendTextToDiv(pageNum, i, 0, undefined, false); } }; const reset = (): void => { setMatchTotal(0); setCurrentIndex(-1); setPrevIndex(-1); setMatchesMap([]); }; const renderMatches = async ( pageNum: number, matchIndex: number, queryStr: string ): Promise => { const textContentItem = await extractTextItems(pageNum); const { begin, end } = convertMatches( queryStr, matchIndex, textContentItem ); beginText(pageNum, begin); if (begin.divIdx === end.divIdx) { appendTextToDiv(pageNum, begin.divIdx, begin.offset, end.offset, true); } else { for (let i = begin.divIdx; i <= end.divIdx; i += 1) { switch (i) { case begin.divIdx: appendTextToDiv( pageNum, begin.divIdx, begin.offset, undefined, true ); break; case end.divIdx: beginText(pageNum, { divIdx: end.divIdx, offset: 0 }); appendTextToDiv(pageNum, end.divIdx, 0, end.offset, true); break; default: { beginText(pageNum, { divIdx: i, offset: 0 }); appendTextToDiv(pageNum, i, 0, undefined, true); break; } } } } }; const handleSearch = (val: string): void => { if (!val) return; const queryStr = normalize(val.toLowerCase()); if (queryStr !== queryString) { reset(); setQueryString(queryStr); startSearchPdf(queryStr); } }; const clickPrev = (): void => { setCurrentIndex(cur => { setPrevIndex(cur); if (cur > 0) { return cur - 1; } setPrevIndex(cur - 1); return cur; }); }; const clickNext = (): void => { setCurrentIndex(cur => { setPrevIndex(cur); if (cur + 1 < matchTotal) { return cur + 1; } return cur; }); }; const handleClose = (): void => { setNavbar(''); reset(); const currentMatches = matchesMap[currentIndex]; if (currentMatches) { cleanMatches(currentMatches.page, currentMatches.index, queryString); } }; useEffect(() => { if (matchTotal >= 1 && currentIndex === -1) { clickNext(); } }, [matchTotal, currentIndex]); useEffect(() => { if (currentIndex >= 0) { const indexObj = matchesMap[currentIndex]; const pageDiv: HTMLDivElement = document.getElementById( `page_${indexObj.page}` ) as HTMLDivElement; scrollIntoView(pageDiv); } }, [currentIndex, matchesMap]); useEffect(() => { if (currentIndex >= 0) { const currentMatches = matchesMap[currentIndex]; if (textDivs[currentMatches.page]) { renderMatches(currentMatches.page, currentMatches.index, queryString); } if (currentIndex !== prevIndex && prevIndex >= 0) { const prevMatches = matchesMap[prevIndex]; if (prevMatches) { cleanMatches(prevMatches.page, prevMatches.index, queryString); } } } }, [currentIndex, prevIndex, matchesMap, queryString, textDivs]); return ( ); }; export default Search;