Search.tsx 4.5 KB

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