|
@@ -1,80 +1,48 @@
|
|
import React, { useState, useEffect } from 'react';
|
|
import React, { useState, useEffect } from 'react';
|
|
|
|
|
|
|
|
+import { setTimeout } from 'timers';
|
|
import useActions from '../actions';
|
|
import useActions from '../actions';
|
|
import useStore from '../store';
|
|
import useStore from '../store';
|
|
import SearchComponent from '../components/Search';
|
|
import SearchComponent from '../components/Search';
|
|
-import {
|
|
|
|
- normalize,
|
|
|
|
- calculatePhraseMatch,
|
|
|
|
- convertMatches,
|
|
|
|
-} from '../helpers/pdf';
|
|
|
|
|
|
+import { getPdfPage, normalize, calculatePhraseMatch } from '../helpers/pdf';
|
|
|
|
+import { extractTextItems } from '../helpers/search';
|
|
import { scrollIntoView } from '../helpers/utility';
|
|
import { scrollIntoView } from '../helpers/utility';
|
|
|
|
|
|
-type MatchType = {
|
|
|
|
- page: number;
|
|
|
|
- index: number;
|
|
|
|
-};
|
|
|
|
|
|
+let timer: ReturnType<typeof setTimeout> = setTimeout(() => '', 100);
|
|
|
|
|
|
-let readyScroll = false;
|
|
|
|
|
|
+let localMatchesMap: MatchType[] = [];
|
|
|
|
|
|
const Search: React.FC = () => {
|
|
const Search: React.FC = () => {
|
|
- const [queryString, setQueryString] = useState('');
|
|
|
|
- const [matchesMap, setMatchesMap] = useState<MatchType[]>([]);
|
|
|
|
const [matchTotal, setMatchTotal] = useState(0);
|
|
const [matchTotal, setMatchTotal] = useState(0);
|
|
- const [prevIndex, setPrevIndex] = useState(-1);
|
|
|
|
- const [currentIndex, setCurrentIndex] = useState(-1);
|
|
|
|
|
|
+
|
|
const [
|
|
const [
|
|
- { navbarState, pdf, totalPage, textDivs, currentPage },
|
|
|
|
|
|
+ { navbarState, pdf, totalPage, queryString, currentIndex, matchesMap },
|
|
dispatch,
|
|
dispatch,
|
|
] = useStore();
|
|
] = useStore();
|
|
- const { setNavbar } = useActions(dispatch);
|
|
|
|
-
|
|
|
|
- const getTextContent = async (pageNum: number): Promise<any[]> => {
|
|
|
|
- const page = await pdf.getPage(pageNum);
|
|
|
|
- const textContent = await page.getTextContent({
|
|
|
|
- normalizeWhitespace: true,
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
- return textContent.items;
|
|
|
|
- };
|
|
|
|
-
|
|
|
|
- const extractTextItems = async (pageNum: number): Promise<string[]> => {
|
|
|
|
- const textContent = await getTextContent(pageNum);
|
|
|
|
- const strBuf = [];
|
|
|
|
-
|
|
|
|
- for (let j = 0, len = textContent.length; j < len; j += 1) {
|
|
|
|
- // add whitespace in front if start character is Uppercase
|
|
|
|
- if (
|
|
|
|
- textContent[j].str.match(/^[A-Z]/) &&
|
|
|
|
- j > 0 &&
|
|
|
|
- textContent[j - 1].str !== ' '
|
|
|
|
- ) {
|
|
|
|
- strBuf.push(` ${textContent[j].str}`);
|
|
|
|
- } else {
|
|
|
|
- strBuf.push(textContent[j].str);
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- return strBuf;
|
|
|
|
- };
|
|
|
|
|
|
+ const {
|
|
|
|
+ setNavbar,
|
|
|
|
+ setQueryString,
|
|
|
|
+ setMatchesMap,
|
|
|
|
+ setCurrentIndex,
|
|
|
|
+ } = useActions(dispatch);
|
|
|
|
|
|
const getMatchTextIndex = async (
|
|
const getMatchTextIndex = async (
|
|
pageNum: number,
|
|
pageNum: number,
|
|
queryStr: string
|
|
queryStr: string
|
|
): Promise<void> => {
|
|
): Promise<void> => {
|
|
- const contentItems = await extractTextItems(pageNum);
|
|
|
|
|
|
+ const contentItems = await extractTextItems(
|
|
|
|
+ (): Promise<any> => getPdfPage(pdf, pageNum)
|
|
|
|
+ );
|
|
const content = normalize(contentItems.join('').toLowerCase());
|
|
const content = normalize(contentItems.join('').toLowerCase());
|
|
const matches = calculatePhraseMatch(content, queryStr);
|
|
const matches = calculatePhraseMatch(content, queryStr);
|
|
|
|
|
|
if (matches.length) {
|
|
if (matches.length) {
|
|
matches.forEach(ele => {
|
|
matches.forEach(ele => {
|
|
- setMatchesMap((prevState: MatchType[]) => {
|
|
|
|
- prevState.push({
|
|
|
|
- page: pageNum,
|
|
|
|
- index: ele,
|
|
|
|
- });
|
|
|
|
- return prevState;
|
|
|
|
|
|
+ localMatchesMap.push({
|
|
|
|
+ page: pageNum,
|
|
|
|
+ index: ele,
|
|
});
|
|
});
|
|
|
|
+ setMatchesMap(localMatchesMap);
|
|
});
|
|
});
|
|
|
|
|
|
setMatchTotal(cur => cur + matches.length);
|
|
setMatchTotal(cur => cur + matches.length);
|
|
@@ -89,109 +57,12 @@ const Search: React.FC = () => {
|
|
getMatchTextIndex(1, queryStr);
|
|
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 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 reset = (): void => {
|
|
const reset = (): void => {
|
|
|
|
+ localMatchesMap = [];
|
|
|
|
+ setMatchesMap([]);
|
|
setMatchTotal(0);
|
|
setMatchTotal(0);
|
|
setCurrentIndex(-1);
|
|
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;
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
|
|
+ setQueryString('');
|
|
};
|
|
};
|
|
|
|
|
|
const handleSearch = (val: string): void => {
|
|
const handleSearch = (val: string): void => {
|
|
@@ -199,91 +70,98 @@ const Search: React.FC = () => {
|
|
|
|
|
|
const queryStr = normalize(val.toLowerCase());
|
|
const queryStr = normalize(val.toLowerCase());
|
|
if (queryStr !== queryString) {
|
|
if (queryStr !== queryString) {
|
|
- readyScroll = true;
|
|
|
|
reset();
|
|
reset();
|
|
setQueryString(queryStr);
|
|
setQueryString(queryStr);
|
|
startSearchPdf(queryStr);
|
|
startSearchPdf(queryStr);
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
- const clickPrev = (): void => {
|
|
|
|
- readyScroll = true;
|
|
|
|
|
|
+ const scrollToPage = (pageNum: number) => {
|
|
|
|
+ const pageDiv: HTMLDivElement = document.getElementById(
|
|
|
|
+ `page_${pageNum}`
|
|
|
|
+ ) as HTMLDivElement;
|
|
|
|
+
|
|
|
|
+ scrollIntoView(pageDiv);
|
|
|
|
+ };
|
|
|
|
|
|
- setCurrentIndex(cur => {
|
|
|
|
- setPrevIndex(cur);
|
|
|
|
- if (cur > 0) {
|
|
|
|
- return cur - 1;
|
|
|
|
|
|
+ const highlightTarget = (match: MatchType, color: string) => {
|
|
|
|
+ timer = setTimeout(() => {
|
|
|
|
+ const id = `${match.page}_${match.index}`;
|
|
|
|
+ const pageElement: HTMLDivElement = document.getElementById(
|
|
|
|
+ `page_${match.page}`
|
|
|
|
+ ) as HTMLDivElement;
|
|
|
|
+ const textLayer: HTMLDivElement = pageElement.querySelector(
|
|
|
|
+ '[data-id="text-layer"]'
|
|
|
|
+ ) as HTMLDivElement;
|
|
|
|
+
|
|
|
|
+ if (textLayer) {
|
|
|
|
+ const spans = textLayer.querySelectorAll(`[class="${id}"]`) as NodeList;
|
|
|
|
+
|
|
|
|
+ if (spans.length > 0) {
|
|
|
|
+ spans.forEach((ele: Node) => {
|
|
|
|
+ // eslint-disable-next-line no-param-reassign
|
|
|
|
+ (ele as HTMLElement).style.backgroundColor = color;
|
|
|
|
+ });
|
|
|
|
+ } else {
|
|
|
|
+ highlightTarget(match, color);
|
|
|
|
+ }
|
|
}
|
|
}
|
|
- setPrevIndex(cur - 1);
|
|
|
|
- return cur;
|
|
|
|
- });
|
|
|
|
|
|
+ }, 200);
|
|
};
|
|
};
|
|
|
|
|
|
- const clickNext = (): void => {
|
|
|
|
- readyScroll = true;
|
|
|
|
|
|
+ const handleClickPrev = (): void => {
|
|
|
|
+ if (currentIndex > 0) {
|
|
|
|
+ const currentMatch = matchesMap[currentIndex];
|
|
|
|
+ if (currentMatch) {
|
|
|
|
+ highlightTarget(currentMatch, 'rgba(255, 211, 0, 0.7)');
|
|
|
|
+ }
|
|
|
|
|
|
- setCurrentIndex(cur => {
|
|
|
|
- setPrevIndex(cur);
|
|
|
|
- if (cur + 1 < matchTotal) {
|
|
|
|
- return cur + 1;
|
|
|
|
|
|
+ setCurrentIndex(currentIndex - 1);
|
|
|
|
+ const nextMatch = matchesMap[currentIndex - 1];
|
|
|
|
+ scrollToPage(nextMatch.page);
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const handleClickNext = (): void => {
|
|
|
|
+ if (currentIndex + 1 < matchTotal) {
|
|
|
|
+ const currentMatch = matchesMap[currentIndex];
|
|
|
|
+ if (currentMatch) {
|
|
|
|
+ highlightTarget(currentMatch, 'rgba(255, 211, 0, 0.7)');
|
|
}
|
|
}
|
|
- return cur;
|
|
|
|
- });
|
|
|
|
|
|
+
|
|
|
|
+ setCurrentIndex(currentIndex + 1);
|
|
|
|
+ const indexObj = matchesMap[currentIndex + 1];
|
|
|
|
+ scrollToPage(indexObj.page);
|
|
|
|
+ }
|
|
};
|
|
};
|
|
|
|
|
|
const handleClose = (): void => {
|
|
const handleClose = (): void => {
|
|
setNavbar('');
|
|
setNavbar('');
|
|
reset();
|
|
reset();
|
|
-
|
|
|
|
- const currentMatches = matchesMap[currentIndex];
|
|
|
|
- if (currentMatches) {
|
|
|
|
- cleanMatches(currentMatches.page, currentMatches.index, queryString);
|
|
|
|
- }
|
|
|
|
};
|
|
};
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
if (matchTotal >= 1 && currentIndex === -1) {
|
|
if (matchTotal >= 1 && currentIndex === -1) {
|
|
- clickNext();
|
|
|
|
|
|
+ setCurrentIndex(currentIndex + 1);
|
|
|
|
+ const indexObj = localMatchesMap[currentIndex + 1];
|
|
|
|
+ scrollToPage(indexObj.page);
|
|
}
|
|
}
|
|
}, [matchTotal, currentIndex]);
|
|
}, [matchTotal, currentIndex]);
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
if (currentIndex >= 0) {
|
|
if (currentIndex >= 0) {
|
|
- const indexObj = matchesMap[currentIndex];
|
|
|
|
- if (indexObj.page !== currentPage && readyScroll) {
|
|
|
|
- const pageDiv: HTMLDivElement = document.getElementById(
|
|
|
|
- `page_${indexObj.page}`
|
|
|
|
- ) as HTMLDivElement;
|
|
|
|
-
|
|
|
|
- scrollIntoView(pageDiv);
|
|
|
|
- readyScroll = false;
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- }, [currentIndex, matchesMap, currentPage]);
|
|
|
|
-
|
|
|
|
- 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);
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
|
|
+ clearTimeout(timer);
|
|
|
|
+ const match = matchesMap[currentIndex];
|
|
|
|
+ highlightTarget(match, 'rgba(255, 141, 0)');
|
|
}
|
|
}
|
|
- }, [currentIndex, prevIndex, matchesMap, queryString, textDivs]);
|
|
|
|
|
|
+ }, [currentIndex, matchesMap]);
|
|
|
|
|
|
return (
|
|
return (
|
|
<SearchComponent
|
|
<SearchComponent
|
|
matchesTotal={matchTotal}
|
|
matchesTotal={matchTotal}
|
|
current={currentIndex + 1}
|
|
current={currentIndex + 1}
|
|
- onPrev={clickPrev}
|
|
|
|
- onNext={clickNext}
|
|
|
|
|
|
+ onPrev={handleClickPrev}
|
|
|
|
+ onNext={handleClickNext}
|
|
onEnter={handleSearch}
|
|
onEnter={handleSearch}
|
|
isActive={navbarState === 'search'}
|
|
isActive={navbarState === 'search'}
|
|
close={handleClose}
|
|
close={handleClose}
|