Search.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import React, { useState, useEffect } from 'react';
  2. import useActions from '../actions';
  3. import useStore from '../store';
  4. import SearchComponent from '../components/Search';
  5. import {
  6. normalize,
  7. calculatePhraseMatch,
  8. convertMatches,
  9. } from '../helpers/pdf';
  10. import { scrollIntoView } from '../helpers/utility';
  11. type MatchType = {
  12. page: number;
  13. index: number;
  14. };
  15. const Search: React.FC = () => {
  16. const [queryString, setQueryString] = useState('');
  17. const [matchesMap, setMatchesMap] = useState<MatchType[]>([]);
  18. const [matchTotal, setMatchTotal] = useState(0);
  19. const [prevIndex, setPrevIndex] = useState(-1);
  20. const [currentIndex, setCurrentIndex] = useState(-1);
  21. const [{ navbarState, pdf, totalPage, textDivs }, dispatch] = useStore();
  22. const { setNavbar } = useActions(dispatch);
  23. const getTextContent = async (pageNum: number): Promise<any[]> => {
  24. const page = await pdf.getPage(pageNum);
  25. const textContent = await page.getTextContent({
  26. normalizeWhitespace: true,
  27. });
  28. return textContent.items;
  29. };
  30. const extractTextItems = async (pageNum: number): Promise<string[]> => {
  31. const textContent = await getTextContent(pageNum);
  32. const strBuf = [];
  33. for (let j = 0, len = textContent.length; j < len; j += 1) {
  34. if (textContent[j].str) {
  35. strBuf.push(textContent[j].str);
  36. }
  37. }
  38. return strBuf;
  39. };
  40. const getMatchTextIndex = async (
  41. pageNum: number,
  42. queryStr: string
  43. ): Promise<void> => {
  44. const contentItems = await extractTextItems(pageNum);
  45. const content = normalize(contentItems.join('').toLowerCase());
  46. const matches = calculatePhraseMatch(content, queryStr);
  47. if (matches.length) {
  48. matches.forEach(ele => {
  49. matchesMap.push({
  50. page: pageNum,
  51. index: ele,
  52. });
  53. });
  54. setMatchesMap(matchesMap);
  55. setMatchTotal(cur => cur + matches.length);
  56. }
  57. if (pageNum < totalPage) {
  58. await getMatchTextIndex(pageNum + 1, queryStr);
  59. }
  60. };
  61. const startSearchPdf = async (queryStr: string): Promise<void> => {
  62. getMatchTextIndex(1, queryStr);
  63. };
  64. const appendTextToDiv = async (
  65. pageNum: number,
  66. divIdx: number,
  67. fromOffset: number,
  68. toOffset: number | undefined,
  69. highlight: boolean
  70. ): Promise<any> => {
  71. const textContentItem = await extractTextItems(pageNum);
  72. if (textDivs[pageNum]) {
  73. const domElements = textDivs[pageNum][divIdx];
  74. const content = textContentItem[divIdx].substring(fromOffset, toOffset);
  75. const node = document.createTextNode(content);
  76. const span = document.createElement('span');
  77. if (highlight) {
  78. span.style.backgroundColor = 'rgba(255, 211, 0, 0.7)';
  79. span.appendChild(node);
  80. domElements.appendChild(span);
  81. scrollIntoView(domElements, { top: -120 });
  82. } else {
  83. domElements.textContent = '';
  84. domElements.appendChild(node);
  85. }
  86. }
  87. };
  88. const beginText = async (
  89. pageNum: number,
  90. begin: Record<string, any>
  91. ): Promise<any> => {
  92. const { divIdx } = begin;
  93. const domElements = textDivs[pageNum][divIdx];
  94. if (domElements) {
  95. domElements.textContent = '';
  96. appendTextToDiv(pageNum, divIdx, 0, begin.offset, false);
  97. }
  98. };
  99. const cleanMatches = async (
  100. pageNum: number,
  101. matchIndex: number,
  102. queryStr: string
  103. ): Promise<any> => {
  104. const textContentItem = await extractTextItems(pageNum);
  105. const { begin, end } = convertMatches(
  106. queryStr,
  107. matchIndex,
  108. textContentItem
  109. );
  110. for (let i = begin.divIdx; i <= end.divIdx; i += 1) {
  111. appendTextToDiv(pageNum, i, 0, undefined, false);
  112. }
  113. };
  114. const reset = (): void => {
  115. setMatchTotal(0);
  116. setCurrentIndex(-1);
  117. setPrevIndex(-1);
  118. setMatchesMap([]);
  119. };
  120. const renderMatches = async (
  121. pageNum: number,
  122. matchIndex: number,
  123. queryStr: string
  124. ): Promise<any> => {
  125. const textContentItem = await extractTextItems(pageNum);
  126. const { begin, end } = convertMatches(
  127. queryStr,
  128. matchIndex,
  129. textContentItem
  130. );
  131. beginText(pageNum, begin);
  132. if (begin.divIdx === end.divIdx) {
  133. appendTextToDiv(pageNum, begin.divIdx, begin.offset, end.offset, true);
  134. } else {
  135. for (let i = begin.divIdx; i <= end.divIdx; i += 1) {
  136. switch (i) {
  137. case begin.divIdx:
  138. appendTextToDiv(
  139. pageNum,
  140. begin.divIdx,
  141. begin.offset,
  142. undefined,
  143. true
  144. );
  145. break;
  146. case end.divIdx:
  147. beginText(pageNum, { divIdx: end.divIdx, offset: 0 });
  148. appendTextToDiv(pageNum, end.divIdx, 0, end.offset, true);
  149. break;
  150. default: {
  151. beginText(pageNum, { divIdx: i, offset: 0 });
  152. appendTextToDiv(pageNum, i, 0, undefined, true);
  153. break;
  154. }
  155. }
  156. }
  157. }
  158. };
  159. const handleSearch = (val: string): void => {
  160. if (!val) return;
  161. const queryStr = normalize(val.toLowerCase());
  162. if (queryStr !== queryString) {
  163. reset();
  164. setQueryString(queryStr);
  165. startSearchPdf(queryStr);
  166. }
  167. };
  168. const clickPrev = (): void => {
  169. setCurrentIndex(cur => {
  170. setPrevIndex(cur);
  171. if (cur > 0) {
  172. return cur - 1;
  173. }
  174. setPrevIndex(cur - 1);
  175. return cur;
  176. });
  177. };
  178. const clickNext = (): void => {
  179. setCurrentIndex(cur => {
  180. setPrevIndex(cur);
  181. if (cur + 1 < matchTotal) {
  182. return cur + 1;
  183. }
  184. return cur;
  185. });
  186. };
  187. const handleClose = (): void => {
  188. setNavbar('');
  189. reset();
  190. const currentMatches = matchesMap[currentIndex];
  191. if (currentMatches) {
  192. cleanMatches(currentMatches.page, currentMatches.index, queryString);
  193. }
  194. };
  195. useEffect(() => {
  196. if (matchTotal >= 1 && currentIndex === -1) {
  197. clickNext();
  198. }
  199. }, [matchTotal, currentIndex]);
  200. useEffect(() => {
  201. if (currentIndex >= 0) {
  202. const indexObj = matchesMap[currentIndex];
  203. const pageDiv: HTMLDivElement = document.getElementById(
  204. `page_${indexObj.page}`
  205. ) as HTMLDivElement;
  206. scrollIntoView(pageDiv);
  207. }
  208. }, [currentIndex, matchesMap]);
  209. useEffect(() => {
  210. if (currentIndex >= 0) {
  211. const currentMatches = matchesMap[currentIndex];
  212. if (textDivs[currentMatches.page]) {
  213. renderMatches(currentMatches.page, currentMatches.index, queryString);
  214. }
  215. if (currentIndex !== prevIndex && prevIndex >= 0) {
  216. const prevMatches = matchesMap[prevIndex];
  217. if (prevMatches) {
  218. cleanMatches(prevMatches.page, prevMatches.index, queryString);
  219. }
  220. }
  221. }
  222. }, [currentIndex, prevIndex, matchesMap, queryString, textDivs]);
  223. return (
  224. <SearchComponent
  225. matchesTotal={matchTotal}
  226. current={currentIndex + 1}
  227. onPrev={clickPrev}
  228. onNext={clickNext}
  229. onEnter={handleSearch}
  230. isActive={navbarState === 'search'}
  231. close={handleClose}
  232. />
  233. );
  234. };
  235. export default Search;