Search.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  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. let 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(() => getPdfPage(pdf, pageNum));
  30. const content = normalize(contentItems.join('').toLowerCase());
  31. const matches = calculatePhraseMatch(content, queryStr);
  32. if (matches.length) {
  33. matches.forEach((ele) => {
  34. localMatchesMap.push({
  35. page: pageNum,
  36. index: ele,
  37. });
  38. });
  39. localTotal += matches.length;
  40. }
  41. if (pageNum === totalPage) {
  42. setMatchesMap(localMatchesMap);
  43. setMatchTotal(localTotal);
  44. setProcessing(false);
  45. }
  46. if (pageNum < totalPage) {
  47. await getMatchTextIndex(pageNum + 1, queryStr);
  48. }
  49. };
  50. const startSearchPdf = async (queryStr: string): Promise<void> => {
  51. getMatchTextIndex(1, queryStr);
  52. };
  53. const reset = (): void => {
  54. localMatchesMap = [];
  55. localTotal = 0;
  56. setMatchesMap([]);
  57. setMatchTotal(0);
  58. setCurrentIndex(-1);
  59. setQueryString('');
  60. };
  61. const handleSearch = (val: string): void => {
  62. if (!val) return;
  63. const newQueryString = normalize(val.toLowerCase());
  64. if (newQueryString !== queryString) {
  65. setProcessing(true);
  66. reset();
  67. setQueryString(newQueryString);
  68. startSearchPdf(newQueryString);
  69. }
  70. };
  71. const scrollToPage = (pageNum: number) => {
  72. const pageDiv: HTMLDivElement = document.getElementById(
  73. `page_${pageNum}`,
  74. ) as HTMLDivElement;
  75. scrollIntoView(pageDiv);
  76. };
  77. const highlightTarget = (match: MatchType, color: string) => {
  78. timer = setTimeout(() => {
  79. const id = `${match.page}_${match.index}`;
  80. const pageElement: HTMLDivElement = document.getElementById(
  81. `page_${match.page}`,
  82. ) as HTMLDivElement;
  83. const textLayer: HTMLDivElement = pageElement.querySelector(
  84. '[data-id="text-layer"]',
  85. ) as HTMLDivElement;
  86. if (textLayer) {
  87. const span = textLayer.querySelector(`[class="${id}"]`);
  88. if (span) {
  89. (span as HTMLElement).style.backgroundColor = color;
  90. } else {
  91. highlightTarget(match, color);
  92. }
  93. } else {
  94. highlightTarget(match, color);
  95. }
  96. }, 200);
  97. };
  98. const handleClickPrev = (): void => {
  99. if (currentIndex > 0) {
  100. const currentMatch = matchesMap[currentIndex];
  101. if (currentMatch) {
  102. highlightTarget(currentMatch, 'rgba(255, 211, 0, 0.7)');
  103. }
  104. setCurrentIndex(currentIndex - 1);
  105. const nextMatch = matchesMap[currentIndex - 1];
  106. scrollToPage(nextMatch.page);
  107. }
  108. };
  109. const handleClickNext = (): void => {
  110. if (currentIndex + 1 < matchTotal) {
  111. const currentMatch = matchesMap[currentIndex];
  112. if (currentMatch) {
  113. highlightTarget(currentMatch, 'rgba(255, 211, 0, 0.7)');
  114. }
  115. setCurrentIndex(currentIndex + 1);
  116. const indexObj = matchesMap[currentIndex + 1];
  117. scrollToPage(indexObj.page);
  118. }
  119. };
  120. const handleClose = (): void => {
  121. setNavbar('');
  122. reset();
  123. };
  124. useEffect(() => {
  125. if (matchTotal >= 1 && currentIndex === -1) {
  126. setCurrentIndex(currentIndex + 1);
  127. const indexObj = localMatchesMap[currentIndex + 1];
  128. scrollToPage(indexObj.page);
  129. }
  130. }, [matchTotal, currentIndex]);
  131. useEffect(() => {
  132. if (currentIndex >= 0) {
  133. clearTimeout(timer);
  134. const match = matchesMap[currentIndex];
  135. highlightTarget(match, 'rgb(255, 141, 0)');
  136. }
  137. }, [currentIndex, matchesMap]);
  138. return (
  139. <SearchComponent
  140. matchesTotal={matchTotal}
  141. current={currentIndex + 1}
  142. onPrev={handleClickPrev}
  143. onNext={handleClickNext}
  144. onEnter={handleSearch}
  145. isActive={navbarState === 'search'}
  146. close={handleClose}
  147. isProcessing={isProcessing}
  148. />
  149. );
  150. };
  151. export default Search;