123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- /* eslint-disable @typescript-eslint/no-use-before-define */
- import React, { useState, useEffect, useRef } from 'react';
- import useActions from '../actions';
- import useStore from '../store';
- import SearchComponent from '../components/Search';
- import { normalize, calcFindPhraseMatch, convertMatches } from '../helpers/pdf';
- import { scrollIntoView } from '../helpers/utility';
- import { delay } from '../helpers/time';
- const Search: React.FunctionComponent = () => {
- const queryString = useRef('');
- const pageIndex = useRef(0);
- const matchIndex = useRef(-1);
- const pageMatches = useRef<any[]>([]);
- const textContentItems = useRef<any[]>([]);
- const textDiv = useRef<any[]>([]);
- const pageContents: string[] = [];
- const [matchesTotal, setMatchesTotal] = useState(0);
- const [selected, setSelected] = useState({
- last: -1,
- current: -1,
- });
- const [{ navbarState, pdf, totalPage }, dispatch] = useStore();
- const { setNavbar } = useActions(dispatch);
- const extractPageText = async (pageIdx: number): Promise<any> => {
- const page = await pdf.getPage(pageIdx + 1);
- const textContent = await page.getTextContent({
- normalizeWhitespace: true,
- });
- const textItems = textContent.items;
- const strBuf = [];
- for (let j = 0, jj = textItems.length; j < jj; j += 1) {
- if (textItems[j].str.match(/[^\s]/)) {
- strBuf.push(textItems[j].str);
- }
- }
- pageContents[pageIdx] = normalize(strBuf.join('').toLowerCase());
- textContentItems.current[pageIdx] = strBuf;
- };
- const extractPdfText = async (): Promise<any> => {
- const extractTextPromises = [];
- for (let i = 0; i < totalPage; i += 1) {
- extractTextPromises.push(extractPageText(i));
- }
- await Promise.all(extractTextPromises);
- };
- const appendTextToDiv = (
- divIdx: number,
- fromOffset: number,
- toOffset: number | null,
- highlight: boolean,
- ): void => {
- const textContentItem = textContentItems.current[pageIndex.current];
- const div = textDiv.current[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.5)';
- span.appendChild(node);
- div.appendChild(span);
- } else {
- div.appendChild(node);
- }
- };
- const cleanMatch = (): void => {
- if (queryString.current) {
- const pageMatch = pageMatches.current[pageIndex.current][matchIndex.current];
- const textContentItem = textContentItems.current[pageIndex.current];
- const { begin, end } = convertMatches(queryString.current, pageMatch, textContentItem);
- const len = begin.divIdx + (end.divIdx - begin.divIdx);
- for (let i = begin.divIdx; i <= len; i += 1) {
- const offset = {
- divIdx: i,
- offset: null,
- };
- startText(offset);
- }
- }
- };
- const startText = (begin: Record<string, any>): void => {
- textDiv.current[begin.divIdx].textContent = '';
- appendTextToDiv(begin.divIdx, 0, begin.offset, false);
- };
- const renderMatches = async (): Promise<any> => {
- const pageDiv: HTMLDivElement = document.getElementById(`page_${pageIndex.current + 1}`) as HTMLDivElement;
- scrollIntoView(pageDiv);
- await delay(500);
- const textLayer: HTMLDivElement = pageDiv.querySelector('[data-id="text-layer"]') as HTMLDivElement;
- textDiv.current = Array.from(textLayer.children);
- await delay(200);
- const pageMatch = pageMatches.current[pageIndex.current][matchIndex.current];
- const textContentItem = textContentItems.current[pageIndex.current];
- const { begin, end } = convertMatches(queryString.current, pageMatch, textContentItem);
- startText(begin);
- if (begin.divIdx === end.divIdx) {
- appendTextToDiv(begin.divIdx, begin.offset, end.offset, true);
- } else {
- const len = begin.divIdx + (end.divIdx - begin.divIdx);
- for (let i = begin.divIdx; i <= len; i += 1) {
- switch (i) {
- case begin.divIdx:
- appendTextToDiv(i, begin.offset, null, true);
- break;
- case end.divIdx:
- appendTextToDiv(end.divIdx, 0, end.offset, true);
- break;
- default: {
- startText(begin);
- appendTextToDiv(begin.divIdx, 0, null, true);
- break;
- }
- }
- }
- // append end text
- appendTextToDiv(end.divIdx, end.offset, null, true);
- }
- };
- const advanceOffsetPage = (previous: boolean): void => {
- if (previous && pageIndex.current > 0) {
- pageIndex.current -= 1;
- const numPageMatches = pageMatches.current[pageIndex.current].length;
- matchIndex.current = numPageMatches;
- prevMatch();
- } else if (!previous && pageIndex.current < totalPage) {
- pageIndex.current += 1;
- matchIndex.current = -1;
- nextMatch();
- }
- };
- const prevMatch = (): void => {
- const numPageMatches = pageMatches.current[pageIndex.current].length;
- if (numPageMatches && matchIndex.current - 1 >= 0) {
- matchIndex.current -= 1;
- renderMatches();
- } else {
- advanceOffsetPage(true);
- }
- };
- const nextMatch = (): void => {
- const numPageMatches = pageMatches.current[pageIndex.current].length;
- if (numPageMatches && matchIndex.current + 1 < numPageMatches) {
- matchIndex.current += 1;
- renderMatches();
- } else {
- advanceOffsetPage(false);
- }
- };
- const clickPrev = (): void => {
- cleanMatch();
- setSelected((prev) => {
- if (prev.current - 1 >= 0) {
- return {
- last: prev.current,
- current: prev.current - 1,
- };
- }
- return prev;
- });
- };
- const clickNext = (): void => {
- cleanMatch();
- setSelected((prev) => {
- if (prev.current + 1 < matchesTotal) {
- return {
- last: prev.current,
- current: prev.current + 1,
- };
- }
- return prev;
- });
- };
- const handleSearch = async (val: string): Promise<any> => {
- if (!pageContents.length) {
- await extractPdfText();
- }
- const query = val.toLowerCase();
- queryString.current = query;
- for (let i = 0; i < totalPage; i += 1) {
- const matches = calcFindPhraseMatch(pageContents[i], query);
- pageMatches.current[i] = matches;
- }
- if (pageMatches.current.length) {
- const total = pageMatches.current.reduce((prev, curr) => prev + curr.length, 0);
- setMatchesTotal(total);
- setSelected({
- last: -1,
- current: 0,
- });
- }
- };
- const handleClose = (): void => {
- cleanMatch();
- setNavbar('');
- };
- useEffect(() => {
- if (selected.current > selected.last) {
- nextMatch();
- } else if (selected.current < selected.last) {
- prevMatch();
- }
- }, [selected]);
- return (
- <SearchComponent
- matchesTotal={matchesTotal}
- matchIndex={selected.current}
- onPrev={clickPrev}
- onNext={clickNext}
- onEnter={handleSearch}
- isActive={navbarState === 'search'}
- close={handleClose}
- />
- );
- };
- export default Search;
|