Explorar o código

Merge branch '9-mark' into 'master'

Resolve "搜尋到的文字全部mark,被選中當前結果特別加深顏色。"

Closes #9

See merge request cloud/external/kangaroo_client!4
劉倚成 %!s(int64=4) %!d(string=hai) anos
pai
achega
eef4be0ab4

+ 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;

+ 1 - 1
components/FreeTextOption/index.tsx

@@ -19,7 +19,7 @@ const TextOption: React.SFC<OptionPropsType> = ({
   color,
   opacity,
   setDataState = (): void => {
-    // do nothing
+    // do something
   },
 }: OptionPropsType) => {
   const { t } = useTranslation('sidebar');

+ 37 - 0
components/FullScreenButton/index.tsx

@@ -0,0 +1,37 @@
+import React from 'react';
+
+import Icon from '../Icon';
+
+import { ToggleButton } from './styled';
+
+type Props = {
+  displayMode: string;
+  toggleDisplayMode: (state: string) => void;
+};
+
+const FullScreenButton: React.FC<Props> = ({
+  displayMode,
+  toggleDisplayMode,
+}: Props) => {
+  return (
+    <ToggleButton>
+      {displayMode === 'normal' ? (
+        <Icon
+          glyph="tool-close"
+          onClick={(): void => {
+            toggleDisplayMode('full');
+          }}
+        />
+      ) : (
+        <Icon
+          glyph="tool-open"
+          onClick={(): void => {
+            toggleDisplayMode('normal');
+          }}
+        />
+      )}
+    </ToggleButton>
+  );
+};
+
+export default FullScreenButton;

+ 14 - 0
components/FullScreenButton/styled.ts

@@ -0,0 +1,14 @@
+import styled from 'styled-components';
+
+export const ToggleButton = styled.div`
+  position: fixed;
+  right: 20px;
+  bottom: 15px;
+  z-index: 2;
+  box-shadow: 1px 1px 4px 2px rgba(0, 0, 0, 0.32);
+  border-radius: 40px;
+  width: 80px;
+  height: 80px;
+`;
+
+export default ToggleButton;

+ 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;
+`;

+ 8 - 8
components/ShapeOption/data.tsx

@@ -3,13 +3,13 @@ import Icon from '../Icon';
 
 export const shapeOptions = [
   {
-    key: 'circle',
-    content: <Icon glyph="circle" />,
+    key: 'square',
+    content: <Icon glyph="square" />,
     child: '',
   },
   {
-    key: 'square',
-    content: <Icon glyph="square" />,
+    key: 'circle',
+    content: <Icon glyph="circle" />,
     child: '',
   },
   {
@@ -26,13 +26,13 @@ export const shapeOptions = [
 
 export const typeOptions = [
   {
-    key: 'border',
-    content: <Icon glyph="border" />,
+    key: 'fill',
+    content: <Icon glyph="fill" />,
     child: '',
   },
   {
-    key: 'fill',
-    content: <Icon glyph="fill" />,
+    key: 'border',
+    content: <Icon glyph="border" />,
     child: '',
   },
 ];

+ 42 - 67
components/Toolbar/index.tsx

@@ -7,7 +7,7 @@ import SelectBox from '../SelectBox';
 import Divider from '../Divider';
 import { scaleCheck } from '../../helpers/utility';
 
-import { Container, ToggleButton } from './styled';
+import { Container } from './styled';
 import dataset from './data';
 
 type Props = {
@@ -19,10 +19,10 @@ type Props = {
   handleCounterclockwiseRotation: () => void;
   handleZoomIn: () => void;
   handleZoomOut: () => void;
+  handleMouseOver: () => void;
   scale: number;
   viewport: ViewportType;
   displayMode: string;
-  toggleDisplayMode: (state: string) => void;
   handleHandClick: () => void;
   hidden: boolean;
 };
@@ -36,19 +36,17 @@ const Toolbar: React.FC<Props> = ({
   handleCounterclockwiseRotation,
   handleZoomIn,
   handleZoomOut,
+  handleMouseOver,
   scale,
   viewport,
   displayMode,
-  toggleDisplayMode,
   handleHandClick,
   hidden,
 }: Props) => {
   const { t } = useTranslation('toolbar');
   const data = dataset(t);
 
-  const handleScaleSelect = async (
-    selected: SelectOptionType
-  ): Promise<any> => {
+  const handleScaleSelect = (selected: SelectOptionType) => {
     if (selected.child === 'fit') {
       const screenWidth = window.document.body.offsetWidth - 288;
       const originPdfWidth = viewport.width / scale;
@@ -60,67 +58,44 @@ const Toolbar: React.FC<Props> = ({
   };
 
   return (
-    <>
-      <Container hidden={hidden} displayMode={displayMode}>
-        <Pagination
-          totalPage={totalPage}
-          currentPage={currentPage}
-          onChange={setCurrentPage}
-        />
-        <Divider />
-        <Icon
-          glyph="hand"
-          style={{ width: '30px' }}
-          onClick={handleHandClick}
-        />
-        <Icon
-          glyph="zoom-in"
-          style={{ width: '30px' }}
-          onClick={handleZoomIn}
-        />
-        <Icon
-          glyph="zoom-out"
-          style={{ width: '30px' }}
-          onClick={handleZoomOut}
-        />
-        <SelectBox
-          options={data.scaleOptions}
-          style={{ width: '94px' }}
-          useInput
-          isDivide
-          onChange={handleScaleSelect}
-          defaultValue={`${Math.round(scale * 100)} %`}
-        />
-        <Divider />
-        <Icon
-          glyph="rotate-left"
-          style={{ width: '30px' }}
-          onClick={handleCounterclockwiseRotation}
-        />
-        <Icon
-          glyph="rotate-right"
-          style={{ width: '30px' }}
-          onClick={handleClockwiseRotation}
-        />
-      </Container>
-      <ToggleButton>
-        {displayMode === 'normal' ? (
-          <Icon
-            glyph="tool-close"
-            onClick={(): void => {
-              toggleDisplayMode('full');
-            }}
-          />
-        ) : (
-          <Icon
-            glyph="tool-open"
-            onClick={(): void => {
-              toggleDisplayMode('normal');
-            }}
-          />
-        )}
-      </ToggleButton>
-    </>
+    <Container
+      onMouseOver={handleMouseOver}
+      hidden={hidden}
+      displayMode={displayMode}
+    >
+      <Pagination
+        totalPage={totalPage}
+        currentPage={currentPage}
+        onChange={setCurrentPage}
+      />
+      <Divider />
+      <Icon glyph="hand" style={{ width: '30px' }} onClick={handleHandClick} />
+      <Icon glyph="zoom-in" style={{ width: '30px' }} onClick={handleZoomIn} />
+      <Icon
+        glyph="zoom-out"
+        style={{ width: '30px' }}
+        onClick={handleZoomOut}
+      />
+      <SelectBox
+        options={data.scaleOptions}
+        style={{ width: '94px' }}
+        useInput
+        isDivide
+        onChange={handleScaleSelect}
+        defaultValue={`${Math.round(scale * 100)} %`}
+      />
+      <Divider />
+      <Icon
+        glyph="rotate-left"
+        style={{ width: '30px' }}
+        onClick={handleCounterclockwiseRotation}
+      />
+      <Icon
+        glyph="rotate-right"
+        style={{ width: '30px' }}
+        onClick={handleClockwiseRotation}
+      />
+    </Container>
   );
 };
 

+ 1 - 11
components/Toolbar/styled.ts

@@ -6,7 +6,6 @@ const closeToolbar = keyframes`
     opacity: 1;
   }
   to {
-    pointer-events: none;
     opacity: 0;
   }
 `;
@@ -42,13 +41,4 @@ export const Container = styled('div')<{
       : ''}
 `;
 
-export const ToggleButton = styled.div`
-  position: fixed;
-  right: 20px;
-  bottom: 15px;
-  z-index: 2;
-  box-shadow: 1px 1px 4px 2px rgba(0, 0, 0, 0.32);
-  border-radius: 40px;
-  width: 80px;
-  height: 80px;
-`;
+export default Container;

+ 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';

+ 0 - 1
containers/FreehandTools.tsx

@@ -53,7 +53,6 @@ const FreehandTools: React.FC<Props> = ({
 
   const handleMouseDown = useCallback(
     (event: MouseEvent | TouchEvent): void => {
-      event.preventDefault();
       const pageEle = (event.target as HTMLElement).parentNode as HTMLElement;
       switchPdfViewerScrollState('hidden');
 

+ 21 - 0
containers/FullScreenButton.tsx

@@ -0,0 +1,21 @@
+import React from 'react';
+
+import FullScreenButton from '../components/FullScreenButton';
+
+import useActions from '../actions';
+import useStore from '../store';
+
+const FullScreenButtonContainer = () => {
+  const [{ displayMode }, dispatch] = useStore();
+
+  const { toggleDisplayMode } = useActions(dispatch);
+
+  return (
+    <FullScreenButton
+      displayMode={displayMode}
+      toggleDisplayMode={toggleDisplayMode}
+    />
+  );
+};
+
+export default FullScreenButtonContainer;

+ 12 - 1
containers/MarkupTools.tsx

@@ -1,4 +1,5 @@
-import React from 'react';
+import React, { useEffect } from 'react';
+import { useRouter } from 'next/router';
 import { useTranslation } from 'react-i18next';
 
 // import Button from '../components/Button';
@@ -19,6 +20,8 @@ const MarkupTools: React.FC = () => {
   const [{ sidebarState }, dispatch] = useStore();
   const { setSidebar } = useActions(dispatch);
 
+  const router = useRouter();
+
   // const onClickSidebar = (state: string): void => {
   //   if (state === sidebarState) {
   //     setSidebar('');
@@ -54,6 +57,14 @@ const MarkupTools: React.FC = () => {
   //   </Button>
   // );
 
+  useEffect(() => {
+    if (router.query.tool === 'freehand') {
+      setSidebar('freehand');
+    } else {
+      setSidebar('highlight');
+    }
+  }, [router]);
+
   return (
     <>
       <HighlightTools

+ 13 - 5
containers/PdfPage.tsx

@@ -4,7 +4,6 @@ import Page from '../components/Page';
 import Annotation from './Annotation';
 import { getPdfPage } from '../helpers/pdf';
 
-import useActions from '../actions';
 import useStore from '../store';
 
 type Props = {
@@ -14,10 +13,18 @@ type Props = {
 
 const PdfPage: React.FC<Props> = ({ index, renderingState }: Props) => {
   const [
-    { viewport, pdf, rotation, annotations, scale, watermark, currentPage },
-    dispatch,
+    {
+      viewport,
+      pdf,
+      rotation,
+      annotations,
+      scale,
+      watermark,
+      currentPage,
+      queryString,
+      matchesMap,
+    },
   ] = useStore();
-  const { setTextDivs } = useActions(dispatch);
 
   const getAnnotationWithPage = (
     arr: AnnotationType[],
@@ -48,8 +55,9 @@ const PdfPage: React.FC<Props> = ({ index, renderingState }: Props) => {
       rotation={rotation}
       watermark={watermark}
       annotations={getAnnotationWithPage(annotations, index)}
-      setTextDivs={setTextDivs}
       currentPage={currentPage}
+      queryString={queryString}
+      matchesMap={matchesMap}
     />
   );
 };

+ 2 - 2
containers/PdfPages.tsx

@@ -28,7 +28,7 @@ const PdfPages: React.FC<Props> = ({ scrollToUpdate }: Props) => {
         <PdfPage
           key={key}
           index={i}
-          renderingState={_.range(1, 4).includes(i) ? 'RENDERING' : 'LOADING'}
+          renderingState={_.range(1, 3).includes(i) ? 'RENDERING' : 'LOADING'}
         />
       );
       pagesContent.push(component);
@@ -39,7 +39,7 @@ const PdfPages: React.FC<Props> = ({ scrollToUpdate }: Props) => {
 
   const updatePages = (): void => {
     const renderingIndexQueue = _.range(currentPage - 1, currentPage + 2);
-    let index = currentPage - 4;
+    let index = currentPage - 3;
     const end = currentPage + 3;
 
     while (currentPage) {

+ 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}

+ 3 - 4
containers/ShapeTools.tsx

@@ -31,10 +31,10 @@ const Shape: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
   const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
   const [uuid, setUuid] = useState('');
   const [data, setData] = useState({
-    shape: 'circle',
-    type: 'border',
+    shape: 'square',
+    type: 'fill',
     color: '#FBB705',
-    opacity: 100,
+    opacity: 35,
     width: 3,
   });
   const [cursorPosition, setRef] = useCursorPosition();
@@ -124,7 +124,6 @@ const Shape: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
   );
 
   const handleMouseDown = (event: MouseEvent | TouchEvent): void => {
-    event.preventDefault();
     switchPdfViewerScrollState('hidden');
 
     const pageEle = (event.target as HTMLElement).parentNode as HTMLElement;

+ 6 - 3
containers/Toolbar.tsx

@@ -14,13 +14,12 @@ const Toolbar: React.FC = () => {
     dispatch,
   ] = useStore();
   const [hideToolbar, setHideToolbar] = useState(false);
-  let scrollTimeout: any = null;
+  let scrollTimeout = 0;
 
   const {
     setCurrentPage: setCurrentPageAction,
     changeScale,
     changeRotate,
-    toggleDisplayMode,
     setSidebar,
   } = useActions(dispatch);
 
@@ -76,6 +75,10 @@ const Toolbar: React.FC = () => {
     setHideToolbar(false);
   };
 
+  const handleMouseOver = (): void => {
+    setHideToolbar(false);
+  };
+
   useEffect(() => {
     const viewer = document.getElementById('pdf_viewer');
 
@@ -113,10 +116,10 @@ const Toolbar: React.FC = () => {
       handleZoomIn={handleZoomIn}
       handleZoomOut={handleZoomOut}
       handleHandClick={handleHandClick}
+      handleMouseOver={handleMouseOver}
       scale={scale}
       viewport={viewport}
       displayMode={displayMode}
-      toggleDisplayMode={toggleDisplayMode}
       hidden={hideToolbar}
     />
   );

+ 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;
+        }
+      }
+    }
+  }
+};

+ 4 - 2
pages/index.tsx

@@ -4,6 +4,7 @@ import { NextPage } from 'next';
 import Navbar from '../containers/Navbar';
 import Sidebar from '../containers/Sidebar';
 import Toolbar from '../containers/Toolbar';
+import FullScreenButton from '../containers/FullScreenButton';
 import Placeholder from '../containers/Placeholder';
 import PdfViewer from '../containers/PdfViewer';
 import AutoSave from '../containers/AutoSave';
@@ -11,16 +12,17 @@ import Loading from '../containers/Loading';
 import Embed from '../components/Embed';
 
 const index: NextPage = () => (
-  <div>
+  <>
     <Navbar />
     <Sidebar />
     <Toolbar />
     <Placeholder />
     <PdfViewer />
+    <FullScreenButton />
     <AutoSave />
     <Loading />
     <Embed />
-  </div>
+  </>
 );
 
 export default index;

+ 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: [],
+};