Procházet zdrojové kódy

optimization search feature

RoyLiu před 4 roky
rodič
revize
68138f7de3

+ 2 - 0
actions/index.ts

@@ -1,10 +1,12 @@
 import { Dispatch } from 'react';
 import pdfActions from './pdf';
 import mainActions from './main';
+import searchActions from './search';
 
 const useActions = (dispatch: Dispatch<any>): Record<string, any> => ({
   ...pdfActions(dispatch),
   ...mainActions(dispatch),
+  ...searchActions(dispatch),
 });
 
 export default useActions;

+ 12 - 0
actions/search.ts

@@ -0,0 +1,12 @@
+import * as types from '../constants/actionTypes';
+
+const actions: ActionType = dispatch => ({
+  setQueryString: (state: string): void =>
+    dispatch({ type: types.SET_QUERY_STRING, payload: state }),
+  setCurrentIndex: (state: number): void =>
+    dispatch({ type: types.SET_CURRENT_INDEX, payload: state }),
+  setMatchesMap: (state: MatchType[]): void =>
+    dispatch({ type: types.SET_MATCHES_MAP, payload: state }),
+});
+
+export default actions;

+ 22 - 10
components/Page/index.tsx

@@ -2,6 +2,7 @@ import React, { useEffect, useState, useRef } from 'react';
 
 import Watermark from '../Watermark';
 import { renderPdfPage } from '../../helpers/pdf';
+import { renderMatches } from '../../helpers/search';
 
 import {
   PageWrapper,
@@ -9,6 +10,7 @@ import {
   AnnotationLayer,
   TextLayer,
   WatermarkLayer,
+  Inner,
 } from './styled';
 
 type Props = {
@@ -21,8 +23,9 @@ type Props = {
   annotations?: React.ReactNode[];
   drawing?: React.ReactNode[];
   watermark?: WatermarkType;
-  setTextDivs: (pageNum: number, elements: HTMLElement[]) => void;
   currentPage: number;
+  queryString: string;
+  matchesMap: MatchType[];
 };
 
 const PageView: React.FC<Props> = ({
@@ -35,7 +38,8 @@ const PageView: React.FC<Props> = ({
   annotations = [],
   watermark = {},
   currentPage,
-  setTextDivs,
+  queryString,
+  matchesMap,
 }: Props) => {
   const rootEle = useRef<HTMLDivElement | null>(null);
   const [pdfPage, setPdfPage] = useState<any>(null);
@@ -47,8 +51,13 @@ const PageView: React.FC<Props> = ({
         setPdfPage(obj);
 
         const setTextDivsWithPage = (elements: HTMLElement[]) => {
-          if (currentPage === pageNum) {
-            setTextDivs(pageNum, elements);
+          if (matchesMap.length) {
+            matchesMap.forEach(item => {
+              if (item.page === currentPage) {
+                const id = `${item.page}_${item.index}`;
+                renderMatches(elements, getPage, item.index, queryString, id);
+              }
+            });
           }
         };
 
@@ -66,18 +75,23 @@ const PageView: React.FC<Props> = ({
   };
 
   useEffect(() => {
-    if (pdfPage) {
+    if (renderingState === 'LOADING' && pdfPage) {
       pdfPage.cleanup();
     }
-    if (renderTask) {
+    if (renderingState === 'RENDERING' && renderTask) {
       renderTask.cancel();
     }
-
     if (renderingState === 'RENDERING') {
       renderPage();
     }
   }, [currentPage, renderingState, viewport]);
 
+  useEffect(() => {
+    if (queryString === '' && !matchesMap.length) {
+      renderPage();
+    }
+  }, [queryString, matchesMap]);
+
   return (
     <PageWrapper
       ref={rootEle}
@@ -88,9 +102,7 @@ const PageView: React.FC<Props> = ({
       rotation={rotation}
     >
       {renderingState === 'LOADING' ? (
-        <>
-          <TextLayer data-id="text-layer" />
-        </>
+        <Inner>載入中...</Inner>
       ) : (
         <>
           <PdfCanvas />

+ 8 - 0
components/Page/styled.ts

@@ -74,3 +74,11 @@ export const WatermarkLayer = styled.div`
   justify-content: center;
   align-items: center;
 `;
+
+export const Inner = styled.div`
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100%;
+  font-size: 18px;
+`;

+ 4 - 0
constants/actionTypes.ts

@@ -18,3 +18,7 @@ export const UPDATE_ANNOTS = 'UPDATE_ANNOTS';
 export const UPDATE_WATERMARK = 'UPDATE_WATERMARK';
 
 export const SET_TEXT_DIV = 'SET_TEXT_DIV';
+
+export const SET_QUERY_STRING = 'SET_QUERY_STRING';
+export const SET_MATCHES_MAP = 'SET_MATCHES_MAP';
+export const SET_CURRENT_INDEX = 'SET_CURRENT_INDEX';

+ 83 - 205
containers/Search.tsx

@@ -1,80 +1,48 @@
 import React, { useState, useEffect } from 'react';
 
+import { setTimeout } from 'timers';
 import useActions from '../actions';
 import useStore from '../store';
 import SearchComponent from '../components/Search';
-import {
-  normalize,
-  calculatePhraseMatch,
-  convertMatches,
-} from '../helpers/pdf';
+import { getPdfPage, normalize, calculatePhraseMatch } from '../helpers/pdf';
+import { extractTextItems } from '../helpers/search';
 import { scrollIntoView } from '../helpers/utility';
 
-type MatchType = {
-  page: number;
-  index: number;
-};
+let timer: ReturnType<typeof setTimeout> = setTimeout(() => '', 100);
 
-let readyScroll = false;
+let localMatchesMap: MatchType[] = [];
 
 const Search: React.FC = () => {
-  const [queryString, setQueryString] = useState('');
-  const [matchesMap, setMatchesMap] = useState<MatchType[]>([]);
   const [matchTotal, setMatchTotal] = useState(0);
-  const [prevIndex, setPrevIndex] = useState(-1);
-  const [currentIndex, setCurrentIndex] = useState(-1);
+
   const [
-    { navbarState, pdf, totalPage, textDivs, currentPage },
+    { navbarState, pdf, totalPage, queryString, currentIndex, matchesMap },
     dispatch,
   ] = useStore();
-  const { setNavbar } = useActions(dispatch);
-
-  const getTextContent = async (pageNum: number): Promise<any[]> => {
-    const page = await pdf.getPage(pageNum);
-    const textContent = await page.getTextContent({
-      normalizeWhitespace: true,
-    });
-
-    return textContent.items;
-  };
-
-  const extractTextItems = async (pageNum: number): Promise<string[]> => {
-    const textContent = await getTextContent(pageNum);
-    const strBuf = [];
-
-    for (let j = 0, len = textContent.length; j < len; j += 1) {
-      // add whitespace in front if start character is Uppercase
-      if (
-        textContent[j].str.match(/^[A-Z]/) &&
-        j > 0 &&
-        textContent[j - 1].str !== ' '
-      ) {
-        strBuf.push(` ${textContent[j].str}`);
-      } else {
-        strBuf.push(textContent[j].str);
-      }
-    }
-
-    return strBuf;
-  };
+  const {
+    setNavbar,
+    setQueryString,
+    setMatchesMap,
+    setCurrentIndex,
+  } = useActions(dispatch);
 
   const getMatchTextIndex = async (
     pageNum: number,
     queryStr: string
   ): Promise<void> => {
-    const contentItems = await extractTextItems(pageNum);
+    const contentItems = await extractTextItems(
+      (): Promise<any> => getPdfPage(pdf, pageNum)
+    );
     const content = normalize(contentItems.join('').toLowerCase());
     const matches = calculatePhraseMatch(content, queryStr);
 
     if (matches.length) {
       matches.forEach(ele => {
-        setMatchesMap((prevState: MatchType[]) => {
-          prevState.push({
-            page: pageNum,
-            index: ele,
-          });
-          return prevState;
+        localMatchesMap.push({
+          page: pageNum,
+          index: ele,
         });
+        setMatchesMap(localMatchesMap);
       });
 
       setMatchTotal(cur => cur + matches.length);
@@ -89,109 +57,12 @@ const Search: React.FC = () => {
     getMatchTextIndex(1, queryStr);
   };
 
-  const appendTextToDiv = async (
-    pageNum: number,
-    divIdx: number,
-    fromOffset: number,
-    toOffset: number | undefined,
-    highlight: boolean
-  ): Promise<any> => {
-    const textContentItem = await extractTextItems(pageNum);
-
-    if (textDivs[pageNum]) {
-      const domElements = textDivs[pageNum][divIdx];
-
-      const content = textContentItem[divIdx].substring(fromOffset, toOffset);
-      const node = document.createTextNode(content);
-      const span = document.createElement('span');
-      if (highlight) {
-        span.style.backgroundColor = 'rgba(255, 211, 0, 0.7)';
-        span.appendChild(node);
-        domElements.appendChild(span);
-        scrollIntoView(domElements, { top: -120 });
-      } else {
-        domElements.textContent = '';
-        domElements.appendChild(node);
-      }
-    }
-  };
-
-  const beginText = async (
-    pageNum: number,
-    begin: Record<string, any>
-  ): Promise<any> => {
-    const { divIdx } = begin;
-    const domElements = textDivs[pageNum][divIdx];
-    if (domElements) {
-      domElements.textContent = '';
-      appendTextToDiv(pageNum, divIdx, 0, begin.offset, false);
-    }
-  };
-
-  const cleanMatches = async (
-    pageNum: number,
-    matchIndex: number,
-    queryStr: string
-  ): Promise<any> => {
-    const textContentItem = await extractTextItems(pageNum);
-    const { begin, end } = convertMatches(
-      queryStr,
-      matchIndex,
-      textContentItem
-    );
-
-    for (let i = begin.divIdx; i <= end.divIdx; i += 1) {
-      appendTextToDiv(pageNum, i, 0, undefined, false);
-    }
-  };
-
   const reset = (): void => {
+    localMatchesMap = [];
+    setMatchesMap([]);
     setMatchTotal(0);
     setCurrentIndex(-1);
-    setPrevIndex(-1);
-    setMatchesMap([]);
-  };
-
-  const renderMatches = async (
-    pageNum: number,
-    matchIndex: number,
-    queryStr: string
-  ): Promise<any> => {
-    const textContentItem = await extractTextItems(pageNum);
-    const { begin, end } = convertMatches(
-      queryStr,
-      matchIndex,
-      textContentItem
-    );
-
-    beginText(pageNum, begin);
-
-    if (begin.divIdx === end.divIdx) {
-      appendTextToDiv(pageNum, begin.divIdx, begin.offset, end.offset, true);
-    } else {
-      for (let i = begin.divIdx; i <= end.divIdx; i += 1) {
-        switch (i) {
-          case begin.divIdx:
-            appendTextToDiv(
-              pageNum,
-              begin.divIdx,
-              begin.offset,
-              undefined,
-              true
-            );
-            break;
-          case end.divIdx:
-            beginText(pageNum, { divIdx: end.divIdx, offset: 0 });
-            appendTextToDiv(pageNum, end.divIdx, 0, end.offset, true);
-            break;
-          default: {
-            beginText(pageNum, { divIdx: i, offset: 0 });
-            appendTextToDiv(pageNum, i, 0, undefined, true);
-            break;
-          }
-        }
-      }
-    }
+    setQueryString('');
   };
 
   const handleSearch = (val: string): void => {
@@ -199,91 +70,98 @@ const Search: React.FC = () => {
 
     const queryStr = normalize(val.toLowerCase());
     if (queryStr !== queryString) {
-      readyScroll = true;
       reset();
       setQueryString(queryStr);
       startSearchPdf(queryStr);
     }
   };
 
-  const clickPrev = (): void => {
-    readyScroll = true;
+  const scrollToPage = (pageNum: number) => {
+    const pageDiv: HTMLDivElement = document.getElementById(
+      `page_${pageNum}`
+    ) as HTMLDivElement;
+
+    scrollIntoView(pageDiv);
+  };
 
-    setCurrentIndex(cur => {
-      setPrevIndex(cur);
-      if (cur > 0) {
-        return cur - 1;
+  const highlightTarget = (match: MatchType, color: string) => {
+    timer = setTimeout(() => {
+      const id = `${match.page}_${match.index}`;
+      const pageElement: HTMLDivElement = document.getElementById(
+        `page_${match.page}`
+      ) as HTMLDivElement;
+      const textLayer: HTMLDivElement = pageElement.querySelector(
+        '[data-id="text-layer"]'
+      ) as HTMLDivElement;
+
+      if (textLayer) {
+        const spans = textLayer.querySelectorAll(`[class="${id}"]`) as NodeList;
+
+        if (spans.length > 0) {
+          spans.forEach((ele: Node) => {
+            // eslint-disable-next-line no-param-reassign
+            (ele as HTMLElement).style.backgroundColor = color;
+          });
+        } else {
+          highlightTarget(match, color);
+        }
       }
-      setPrevIndex(cur - 1);
-      return cur;
-    });
+    }, 200);
   };
 
-  const clickNext = (): void => {
-    readyScroll = true;
+  const handleClickPrev = (): void => {
+    if (currentIndex > 0) {
+      const currentMatch = matchesMap[currentIndex];
+      if (currentMatch) {
+        highlightTarget(currentMatch, 'rgba(255, 211, 0, 0.7)');
+      }
 
-    setCurrentIndex(cur => {
-      setPrevIndex(cur);
-      if (cur + 1 < matchTotal) {
-        return cur + 1;
+      setCurrentIndex(currentIndex - 1);
+      const nextMatch = matchesMap[currentIndex - 1];
+      scrollToPage(nextMatch.page);
+    }
+  };
+
+  const handleClickNext = (): void => {
+    if (currentIndex + 1 < matchTotal) {
+      const currentMatch = matchesMap[currentIndex];
+      if (currentMatch) {
+        highlightTarget(currentMatch, 'rgba(255, 211, 0, 0.7)');
       }
-      return cur;
-    });
+
+      setCurrentIndex(currentIndex + 1);
+      const indexObj = matchesMap[currentIndex + 1];
+      scrollToPage(indexObj.page);
+    }
   };
 
   const handleClose = (): void => {
     setNavbar('');
     reset();
-
-    const currentMatches = matchesMap[currentIndex];
-    if (currentMatches) {
-      cleanMatches(currentMatches.page, currentMatches.index, queryString);
-    }
   };
 
   useEffect(() => {
     if (matchTotal >= 1 && currentIndex === -1) {
-      clickNext();
+      setCurrentIndex(currentIndex + 1);
+      const indexObj = localMatchesMap[currentIndex + 1];
+      scrollToPage(indexObj.page);
     }
   }, [matchTotal, currentIndex]);
 
   useEffect(() => {
     if (currentIndex >= 0) {
-      const indexObj = matchesMap[currentIndex];
-      if (indexObj.page !== currentPage && readyScroll) {
-        const pageDiv: HTMLDivElement = document.getElementById(
-          `page_${indexObj.page}`
-        ) as HTMLDivElement;
-
-        scrollIntoView(pageDiv);
-        readyScroll = false;
-      }
-    }
-  }, [currentIndex, matchesMap, currentPage]);
-
-  useEffect(() => {
-    if (currentIndex >= 0) {
-      const currentMatches = matchesMap[currentIndex];
-
-      if (textDivs[currentMatches.page]) {
-        renderMatches(currentMatches.page, currentMatches.index, queryString);
-      }
-
-      if (currentIndex !== prevIndex && prevIndex >= 0) {
-        const prevMatches = matchesMap[prevIndex];
-        if (prevMatches) {
-          cleanMatches(prevMatches.page, prevMatches.index, queryString);
-        }
-      }
+      clearTimeout(timer);
+      const match = matchesMap[currentIndex];
+      highlightTarget(match, 'rgba(255, 141, 0)');
     }
-  }, [currentIndex, prevIndex, matchesMap, queryString, textDivs]);
+  }, [currentIndex, matchesMap]);
 
   return (
     <SearchComponent
       matchesTotal={matchTotal}
       current={currentIndex + 1}
-      onPrev={clickPrev}
-      onNext={clickNext}
+      onPrev={handleClickPrev}
+      onNext={handleClickNext}
       onEnter={handleSearch}
       isActive={navbarState === 'search'}
       close={handleClose}

+ 5 - 0
custom.d.ts

@@ -175,3 +175,8 @@ type WatermarkType = {
   textcolor?: string;
   isfront?: 'yes' | 'no';
 };
+
+type MatchType = {
+  page: number;
+  index: number;
+};

+ 0 - 46
helpers/pdf.ts

@@ -153,52 +153,6 @@ export const calculatePhraseMatch = (
   return matches;
 };
 
-export const convertMatches = (
-  queryString: string,
-  matchIndex: number,
-  textContentItem: any[]
-): Record<string, any> => {
-  let i = 0;
-  let iIndex = 0;
-  const end = textContentItem.length - 1;
-  const queryLen = queryString.length;
-
-  // Loop over the divIdxs.
-  while (i !== end && matchIndex >= iIndex + textContentItem[i].length) {
-    iIndex += textContentItem[i].length;
-    i += 1;
-  }
-
-  if (i === textContentItem.length) {
-    console.error('Could not find a matching mapping');
-  }
-
-  const match: Record<string, any> = {
-    begin: {
-      divIdx: i,
-      offset: matchIndex - iIndex,
-    },
-  };
-
-  // Calculate the end position.
-  // eslint-disable-next-line no-param-reassign
-  matchIndex += queryLen;
-
-  // Somewhat the same array as above, but use > instead of >= to get
-  // the end position right.
-  while (i !== end && matchIndex > iIndex + textContentItem[i].length) {
-    iIndex += textContentItem[i].length;
-    i += 1;
-  }
-
-  match.end = {
-    divIdx: i,
-    offset: matchIndex - iIndex,
-  };
-
-  return match;
-};
-
 export const getPdfPage = async (pdf: any, pageNum: number): Promise<any> => {
   const page = await pdf.getPage(pageNum);
   return page;

+ 174 - 0
helpers/search.ts

@@ -0,0 +1,174 @@
+export const extractTextItems = async (
+  getPage: () => Promise<any>
+): Promise<string[]> => {
+  const page = await getPage();
+  let textContent = await page.getTextContent({
+    normalizeWhitespace: true,
+  });
+
+  textContent = textContent.items;
+
+  const strBuf = [];
+
+  for (let j = 0, len = textContent.length; j < len; j += 1) {
+    // add whitespace in front if start character is Uppercase
+    if (
+      textContent[j].str.match(/^[A-Z]/) &&
+      j > 0 &&
+      textContent[j - 1].str !== ' '
+    ) {
+      strBuf.push(` ${textContent[j].str}`);
+    } else {
+      strBuf.push(textContent[j].str);
+    }
+  }
+
+  return strBuf;
+};
+
+export const convertMatches = (
+  queryString: string,
+  matchIndex: number,
+  textContentItem: any[]
+): Record<string, any> => {
+  let i = 0;
+  let iIndex = 0;
+  const end = textContentItem.length - 1;
+  const queryLen = queryString.length;
+
+  // Loop over the divIdxs.
+  while (i !== end && matchIndex >= iIndex + textContentItem[i].length) {
+    iIndex += textContentItem[i].length;
+    i += 1;
+  }
+
+  if (i === textContentItem.length) {
+    console.error('Could not find a matching mapping');
+  }
+
+  const match: Record<string, any> = {
+    begin: {
+      divIdx: i,
+      offset: matchIndex - iIndex,
+    },
+  };
+
+  // Calculate the end position.
+  // eslint-disable-next-line no-param-reassign
+  matchIndex += queryLen;
+
+  // Somewhat the same array as above, but use > instead of >= to get
+  // the end position right.
+  while (i !== end && matchIndex > iIndex + textContentItem[i].length) {
+    iIndex += textContentItem[i].length;
+    i += 1;
+  }
+
+  match.end = {
+    divIdx: i,
+    offset: matchIndex - iIndex,
+  };
+
+  return match;
+};
+
+const appendTextToDiv = async (
+  domElements: HTMLElement[],
+  getPage: () => Promise<any>,
+  divIdx: number,
+  fromOffset: number,
+  toOffset: number | undefined,
+  highlight: boolean,
+  id: string
+): Promise<any> => {
+  const textContentItem = await extractTextItems(getPage);
+
+  const domElement = domElements[divIdx];
+  const content = textContentItem[divIdx].substring(fromOffset, toOffset);
+  const node = document.createTextNode(content);
+  const span = document.createElement('span');
+
+  if (highlight) {
+    span.setAttribute('class', id);
+    span.style.backgroundColor = 'rgba(255, 211, 0, 0.7)';
+    span.appendChild(node);
+    domElement.appendChild(span);
+  } else {
+    // eslint-disable-next-line no-param-reassign
+    domElement.textContent = '';
+    domElement.appendChild(node);
+  }
+};
+
+const beginText = async (
+  domElements: HTMLElement[],
+  getPage: () => Promise<any>,
+  begin: Record<string, any>
+): Promise<any> => {
+  const { divIdx } = begin;
+  const domElement = domElements[divIdx];
+
+  if (domElement) {
+    // eslint-disable-next-line no-param-reassign
+    domElement.textContent = '';
+    appendTextToDiv(domElements, getPage, divIdx, 0, begin.offset, false, '');
+  }
+};
+
+export const renderMatches = async (
+  domElements: HTMLElement[],
+  getPage: () => Promise<any>,
+  matchIndex: number,
+  queryStr: string,
+  id: string
+): Promise<any> => {
+  const textContentItem = await extractTextItems(getPage);
+  const { begin, end } = convertMatches(queryStr, matchIndex, textContentItem);
+
+  beginText(domElements, getPage, begin);
+
+  if (begin.divIdx === end.divIdx) {
+    appendTextToDiv(
+      domElements,
+      getPage,
+      begin.divIdx,
+      begin.offset,
+      end.offset,
+      true,
+      id
+    );
+  } else {
+    for (let i = begin.divIdx; i <= end.divIdx; i += 1) {
+      switch (i) {
+        case begin.divIdx:
+          appendTextToDiv(
+            domElements,
+            getPage,
+            begin.divIdx,
+            begin.offset,
+            undefined,
+            true,
+            id
+          );
+          break;
+        case end.divIdx:
+          beginText(domElements, getPage, { divIdx: end.divIdx, offset: 0 });
+          appendTextToDiv(
+            domElements,
+            getPage,
+            end.divIdx,
+            0,
+            end.offset,
+            true,
+            id
+          );
+          break;
+        default: {
+          beginText(domElements, getPage, { divIdx: i, offset: 0 });
+          appendTextToDiv(domElements, getPage, i, 0, undefined, true, id);
+          break;
+        }
+      }
+    }
+  }
+};

+ 5 - 0
reducers/index.ts

@@ -1,5 +1,6 @@
 import * as mainActions from './main';
 import * as pdfActions from './pdf';
+import * as searchActions from './search';
 import * as types from '../constants/actionTypes';
 
 const createReducer = (handlers: { [key: string]: any }) => (
@@ -32,4 +33,8 @@ export default createReducer({
   [types.UPDATE_ANNOTS]: pdfActions.updateAnnotation,
   [types.UPDATE_WATERMARK]: pdfActions.updateWatermark,
   [types.SET_TEXT_DIV]: pdfActions.setTextDivs,
+
+  [types.SET_QUERY_STRING]: searchActions.setQueryString,
+  [types.SET_CURRENT_INDEX]: searchActions.setCurrentIndex,
+  [types.SET_MATCHES_MAP]: searchActions.setMatchesMap,
 });

+ 14 - 0
reducers/search.ts

@@ -0,0 +1,14 @@
+export const setQueryString: ReducerFuncType = (state, { payload }) => ({
+  ...state,
+  queryString: payload,
+});
+
+export const setCurrentIndex: ReducerFuncType = (state, { payload }) => ({
+  ...state,
+  currentIndex: payload,
+});
+
+export const setMatchesMap: ReducerFuncType = (state, { payload }) => ({
+  ...state,
+  matchesMap: payload,
+});

+ 5 - 1
store/index.tsx

@@ -4,17 +4,21 @@ import initialMainState, {
   StateType as MainStateType,
 } from './initialMainState';
 import initialPdfState, { StateType as PdfStateType } from './initialPdfState';
+import initialSearchState, {
+  StateType as SearchStateType,
+} from './initialSearchState';
 
 import reducers from '../reducers';
 import applyMiddleware from '../reducers/middleware';
 
-type StateType = MainStateType & PdfStateType;
+type StateType = MainStateType & PdfStateType & SearchStateType;
 
 type IContextProps = [StateType, ({ type }: { type: string }) => void];
 
 export const initialState = {
   ...initialMainState,
   ...initialPdfState,
+  ...initialSearchState,
 };
 
 export const StateContext = createContext({} as IContextProps);

+ 11 - 0
store/initialSearchState.ts

@@ -0,0 +1,11 @@
+export type StateType = {
+  queryString: string;
+  currentIndex: number;
+  matchesMap: MatchType[];
+};
+
+export default {
+  queryString: '',
+  currentIndex: -1,
+  matchesMap: [],
+};