|
@@ -3,7 +3,11 @@ import React, { useState, useEffect } from 'react';
|
|
|
import useActions from '../actions';
|
|
|
import useStore from '../store';
|
|
|
import SearchComponent from '../components/Search';
|
|
|
-import { normalize, calcFindPhraseMatch } from '../helpers/pdf';
|
|
|
+import {
|
|
|
+ normalize,
|
|
|
+ calculatePhraseMatch,
|
|
|
+ convertMatches,
|
|
|
+} from '../helpers/pdf';
|
|
|
import { scrollIntoView } from '../helpers/utility';
|
|
|
|
|
|
type MatchType = {
|
|
@@ -12,34 +16,44 @@ type MatchType = {
|
|
|
};
|
|
|
|
|
|
const Search: React.FC = () => {
|
|
|
- let queryString = '';
|
|
|
+ const [queryString, setQueryString] = useState('');
|
|
|
const [matchesMap, setMatchesMap] = useState<MatchType[]>([]);
|
|
|
const [matchTotal, setMatchTotal] = useState(0);
|
|
|
+ const [prevIndex, setPrevIndex] = useState(-1);
|
|
|
const [currentIndex, setCurrentIndex] = useState(-1);
|
|
|
- const [{ navbarState, pdf, totalPage }, dispatch] = useStore();
|
|
|
+ const [{ navbarState, pdf, totalPage, textDivs }, dispatch] = useStore();
|
|
|
const { setNavbar } = useActions(dispatch);
|
|
|
|
|
|
- const extractTextItems = async (pageNum: number): Promise<string[]> => {
|
|
|
+ const getTextContent = async (pageNum: number): Promise<any[]> => {
|
|
|
const page = await pdf.getPage(pageNum);
|
|
|
const textContent = await page.getTextContent({
|
|
|
normalizeWhitespace: true,
|
|
|
});
|
|
|
- const { items } = textContent;
|
|
|
+
|
|
|
+ return textContent.items;
|
|
|
+ };
|
|
|
+
|
|
|
+ const extractTextItems = async (pageNum: number): Promise<string[]> => {
|
|
|
+ const textContent = await getTextContent(pageNum);
|
|
|
const strBuf = [];
|
|
|
|
|
|
- for (let j = 0, len = items.length; j < len; j += 1) {
|
|
|
- if (items[j].str.match(/[^\s]/)) {
|
|
|
- strBuf.push(items[j].str);
|
|
|
+ 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): Promise<void> => {
|
|
|
+ const getMatchTextIndex = async (
|
|
|
+ pageNum: number,
|
|
|
+ queryStr: string
|
|
|
+ ): Promise<void> => {
|
|
|
const contentItems = await extractTextItems(pageNum);
|
|
|
const content = normalize(contentItems.join('').toLowerCase());
|
|
|
- const matches = calcFindPhraseMatch(content, queryString);
|
|
|
+ const matches = calculatePhraseMatch(content, queryStr);
|
|
|
+
|
|
|
if (matches.length) {
|
|
|
matches.forEach(ele => {
|
|
|
matchesMap.push({
|
|
@@ -53,40 +67,144 @@ const Search: React.FC = () => {
|
|
|
}
|
|
|
|
|
|
if (pageNum < totalPage) {
|
|
|
- getMatchTextIndex(pageNum + 1);
|
|
|
+ await getMatchTextIndex(pageNum + 1, queryStr);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const startSearchPdf = async (queryStr: string): Promise<void> => {
|
|
|
+ getMatchTextIndex(1, queryStr);
|
|
|
+ };
|
|
|
+
|
|
|
+ const appendTextToDiv = async (
|
|
|
+ pageNum: number,
|
|
|
+ divIdx: number,
|
|
|
+ fromOffset: number,
|
|
|
+ toOffset: number | undefined,
|
|
|
+ highlight: boolean
|
|
|
+ ): Promise<any> => {
|
|
|
+ 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 searchPdfPages = async (): Promise<void> => {
|
|
|
- getMatchTextIndex(1);
|
|
|
+ const beginText = async (
|
|
|
+ pageNum: number,
|
|
|
+ begin: Record<string, any>
|
|
|
+ ): Promise<any> => {
|
|
|
+ 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<any> => {
|
|
|
+ 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 cleanMatch = (): void => {
|
|
|
+ const reset = (): void => {
|
|
|
setMatchTotal(0);
|
|
|
setCurrentIndex(-1);
|
|
|
+ setPrevIndex(-1);
|
|
|
setMatchesMap([]);
|
|
|
};
|
|
|
|
|
|
+ const renderMatches = async (
|
|
|
+ pageNum: number,
|
|
|
+ matchIndex: number,
|
|
|
+ queryStr: string
|
|
|
+ ): Promise<any> => {
|
|
|
+ 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 => {
|
|
|
- cleanMatch();
|
|
|
+ if (!val) return;
|
|
|
|
|
|
- if (val) {
|
|
|
- queryString = val.toLowerCase();
|
|
|
- searchPdfPages();
|
|
|
+ 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;
|
|
|
}
|
|
@@ -96,14 +214,19 @@ const Search: React.FC = () => {
|
|
|
|
|
|
const handleClose = (): void => {
|
|
|
setNavbar('');
|
|
|
- cleanMatch();
|
|
|
+ reset();
|
|
|
+
|
|
|
+ const currentMatches = matchesMap[currentIndex];
|
|
|
+ if (currentMatches) {
|
|
|
+ cleanMatches(currentMatches.page, currentMatches.index, queryString);
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
useEffect(() => {
|
|
|
- if (matchTotal === 1) {
|
|
|
+ if (matchTotal >= 1 && currentIndex === -1) {
|
|
|
clickNext();
|
|
|
}
|
|
|
- }, [matchTotal]);
|
|
|
+ }, [matchTotal, currentIndex]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
if (currentIndex >= 0) {
|
|
@@ -111,10 +234,28 @@ const Search: React.FC = () => {
|
|
|
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 (
|
|
|
<SearchComponent
|
|
|
matchesTotal={matchTotal}
|