Search.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. import React, { useState, useEffect } from 'react';
  2. import { setTimeout } from 'timers';
  3. import useActions from '../actions';
  4. import useStore from '../store';
  5. import SearchComponent from '../components/Search';
  6. import { getPdfPage, normalize, calculatePhraseMatch } from '../helpers/pdf';
  7. import { extractTextItems } from '../helpers/search';
  8. import { scrollIntoView } from '../helpers/utility';
  9. let timer: ReturnType<typeof setTimeout> = setTimeout(() => '', 100);
  10. let localMatchesMap: MatchType[] = [];
  11. // const localTotal = 0;
  12. const Search: React.FC = () => {
  13. // const [isProcessing, setProcessing] = useState(false);
  14. const [matchTotal, setMatchTotal] = useState(0);
  15. const [
  16. { navbarState, pdf, totalPage, queryString, currentIndex, matchesMap },
  17. dispatch,
  18. ] = useStore();
  19. const {
  20. setNavbar,
  21. setQueryString,
  22. setMatchesMap,
  23. setCurrentIndex,
  24. } = useActions(dispatch);
  25. const getMatchTextIndex = async (
  26. pageNum: number,
  27. queryStr: string
  28. ): Promise<void> => {
  29. const contentItems = await extractTextItems(
  30. (): Promise<any> => getPdfPage(pdf, pageNum)
  31. );
  32. const content = normalize(contentItems.join('').toLowerCase());
  33. const matches = calculatePhraseMatch(content, queryStr);
  34. if (matches.length) {
  35. matches.forEach(ele => {
  36. localMatchesMap.push({
  37. page: pageNum,
  38. index: ele,
  39. });
  40. setMatchesMap(localMatchesMap);
  41. });
  42. // localTotal += matches.length;
  43. setMatchTotal(cur => cur + matches.length);
  44. }
  45. // if (pageNum === totalPage) {
  46. // setMatchesMap(localMatchesMap);
  47. // setMatchTotal(localTotal);
  48. // setProcessing(false);
  49. // }
  50. if (pageNum < totalPage) {
  51. await getMatchTextIndex(pageNum + 1, queryStr);
  52. }
  53. };
  54. const startSearchPdf = async (queryStr: string): Promise<void> => {
  55. getMatchTextIndex(1, queryStr);
  56. };
  57. const reset = (): void => {
  58. localMatchesMap = [];
  59. // localTotal = 0;
  60. setMatchesMap([]);
  61. setMatchTotal(0);
  62. setCurrentIndex(-1);
  63. setQueryString('');
  64. };
  65. const handleSearch = (val: string): void => {
  66. if (!val) return;
  67. const newQueryString = normalize(val.toLowerCase());
  68. if (newQueryString !== queryString) {
  69. // setProcessing(true);
  70. reset();
  71. setQueryString(newQueryString);
  72. startSearchPdf(newQueryString);
  73. }
  74. };
  75. const scrollToPage = (pageNum: number) => {
  76. const pageDiv: HTMLDivElement = document.getElementById(
  77. `page_${pageNum}`
  78. ) as HTMLDivElement;
  79. scrollIntoView(pageDiv);
  80. };
  81. const highlightTarget = (match: MatchType, color: string) => {
  82. timer = setTimeout(() => {
  83. const id = `${match.page}_${match.index}`;
  84. const pageElement: HTMLDivElement = document.getElementById(
  85. `page_${match.page}`
  86. ) as HTMLDivElement;
  87. const textLayer: HTMLDivElement = pageElement.querySelector(
  88. '[data-id="text-layer"]'
  89. ) as HTMLDivElement;
  90. if (textLayer) {
  91. const spans = textLayer.querySelectorAll(`[class="${id}"]`) as NodeList;
  92. if (spans.length > 0) {
  93. spans.forEach((ele: Node) => {
  94. // eslint-disable-next-line no-param-reassign
  95. (ele as HTMLElement).style.backgroundColor = color;
  96. });
  97. } else {
  98. highlightTarget(match, color);
  99. }
  100. } else {
  101. highlightTarget(match, color);
  102. }
  103. }, 300);
  104. };
  105. const handleClickPrev = (): void => {
  106. if (currentIndex > 0) {
  107. const currentMatch = matchesMap[currentIndex];
  108. if (currentMatch) {
  109. highlightTarget(currentMatch, 'rgba(255, 211, 0, 0.7)');
  110. }
  111. setCurrentIndex(currentIndex - 1);
  112. const nextMatch = matchesMap[currentIndex - 1];
  113. scrollToPage(nextMatch.page);
  114. }
  115. };
  116. const handleClickNext = (): void => {
  117. if (currentIndex + 1 < matchTotal) {
  118. const currentMatch = matchesMap[currentIndex];
  119. if (currentMatch) {
  120. highlightTarget(currentMatch, 'rgba(255, 211, 0, 0.7)');
  121. }
  122. setCurrentIndex(currentIndex + 1);
  123. const indexObj = matchesMap[currentIndex + 1];
  124. scrollToPage(indexObj.page);
  125. }
  126. };
  127. const handleClose = (): void => {
  128. setNavbar('');
  129. reset();
  130. };
  131. useEffect(() => {
  132. if (matchTotal >= 1 && currentIndex === -1) {
  133. setCurrentIndex(currentIndex + 1);
  134. const indexObj = localMatchesMap[currentIndex + 1];
  135. scrollToPage(indexObj.page);
  136. }
  137. }, [matchTotal, currentIndex]);
  138. useEffect(() => {
  139. if (currentIndex >= 0) {
  140. clearTimeout(timer);
  141. const match = matchesMap[currentIndex];
  142. highlightTarget(match, 'rgba(255, 141, 0)');
  143. }
  144. }, [currentIndex, matchesMap]);
  145. return (
  146. <SearchComponent
  147. matchesTotal={matchTotal}
  148. current={currentIndex + 1}
  149. onPrev={handleClickPrev}
  150. onNext={handleClickNext}
  151. onEnter={handleSearch}
  152. isActive={navbarState === 'search'}
  153. close={handleClose}
  154. isProcessing={false}
  155. />
  156. );
  157. };
  158. export default Search;