Search.tsx 4.7 KB

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