5 Commits 33f5ecc93b ... 3230a372b0

Autore SHA1 Messaggio Data
  RoyLiu 3230a372b0 fix(component): cursor position not display image 4 anni fa
  RoyLiu 434324e7d1 refactor(module): optimization freehand tool and shape tool 4 anni fa
  RoyLiu 392333f259 refactor: optimization performance 4 anni fa
  RoyLiu 5c4211aaba refactor(freehand component): hide text layer when drawing 4 anni fa
  RoyLiu 4349e84f44 update test script 4 anni fa

+ 3 - 3
.eslintignore

@@ -1,8 +1,8 @@
 **/node_modules/**
-**/static/**
+**/public/**
+**/out/**
 **/__mocks__/**
 **/next-env.d.ts
-**/polyfill.js
 **/next.config.js
 **/_document.js
-**/_app.js
+**/_app.js

+ 6 - 5
__test__/button.test.tsx

@@ -1,8 +1,9 @@
 /* eslint-disable no-undef */
 import React from 'react';
-import { render, cleanup, fireEvent } from '@testing-library/react';
+import { cleanup, fireEvent } from '@testing-library/react';
 import '@testing-library/jest-dom';
 
+import testWrapper from '../helpers/testWrapper';
 import Button from '../components/Button';
 import theme from '../helpers/theme';
 
@@ -10,13 +11,13 @@ describe('Button component', () => {
   afterEach(cleanup);
 
   test('check button content', () => {
-    const { getByText } = render(<Button>btn content</Button>);
+    const { getByText } = testWrapper(<Button>btn content</Button>);
 
     expect(getByText('btn content').textContent).toBe('btn content');
   });
 
   test('check button primary theme', () => {
-    const { getByText } = render(
+    const { getByText } = testWrapper(
       <Button appearance="primary">btn content</Button>,
     );
 
@@ -32,7 +33,7 @@ describe('Button component', () => {
     const handleClick = (): void => {
       counter += 1;
     };
-    const { getByText } = render(
+    const { getByText } = testWrapper(
       <Button onClick={handleClick}>btn content</Button>,
     );
 
@@ -41,7 +42,7 @@ describe('Button component', () => {
   });
 
   test('check disable status', () => {
-    const { getByText } = render(<Button isDisabled>btn content</Button>);
+    const { getByText } = testWrapper(<Button isDisabled>btn content</Button>);
 
     expect(getByText('btn content')).toBeDisabled();
   });

+ 4 - 3
__test__/dialog.test.tsx

@@ -1,21 +1,22 @@
 /* eslint-disable no-undef */
 import React from 'react';
-import { render, cleanup } from '@testing-library/react';
+import { cleanup } from '@testing-library/react';
 import '@testing-library/jest-dom';
 
+import testWrapper from '../helpers/testWrapper';
 import Dialog from '../components/Dialog';
 
 describe('Dialog component', () => {
   afterEach(cleanup);
 
   test('check open status', () => {
-    const { getByText } = render(<Dialog open>content</Dialog>);
+    const { getByText } = testWrapper(<Dialog open>content</Dialog>);
 
     expect(getByText('content')).toBeVisible();
   });
 
   test('check close status', () => {
-    const { queryByText } = render(<Dialog open={false}>content</Dialog>);
+    const { queryByText } = testWrapper(<Dialog open={false}>content</Dialog>);
 
     expect(queryByText('content')).not.toBeInTheDocument();
   });

+ 5 - 4
__test__/drawer.test.tsx

@@ -1,19 +1,20 @@
 /* eslint-disable no-undef */
 import React from 'react';
-import { render, cleanup } from '@testing-library/react';
+import { cleanup } from '@testing-library/react';
 
+import testWrapper from '../helpers/testWrapper';
 import Drawer from '../components/Drawer';
 
 describe('Drawer component', () => {
   afterEach(cleanup);
 
   test('drawer content', () => {
-    const { getByText } = render(<Drawer>content</Drawer>);
+    const { getByText } = testWrapper(<Drawer>content</Drawer>);
     expect(getByText('content').textContent).toBe('content');
   });
 
   test('close drawer', () => {
-    const { getByTestId } = render(<Drawer>content</Drawer>);
+    const { getByTestId } = testWrapper(<Drawer>content</Drawer>);
     const ele = getByTestId('drawer');
     const style = window.getComputedStyle(ele as HTMLElement);
 
@@ -21,7 +22,7 @@ describe('Drawer component', () => {
   });
 
   test('open drawer', () => {
-    const { getByTestId } = render(<Drawer open>content</Drawer>);
+    const { getByTestId } = testWrapper(<Drawer open>content</Drawer>);
     const ele = getByTestId('drawer');
     const style = window.getComputedStyle(ele as HTMLElement);
 

+ 29 - 10
__test__/inputBox.test.tsx

@@ -1,8 +1,9 @@
 /* eslint-disable no-undef */
 import React from 'react';
-import { render, cleanup, fireEvent } from '@testing-library/react';
+import { cleanup } from '@testing-library/react';
 import '@testing-library/jest-dom';
 
+import testWrapper from '../helpers/testWrapper';
 import InputBox from '../components/InputBox';
 import TextareaBox from '../components/TextareaBox';
 
@@ -10,31 +11,49 @@ describe('Input Box component', () => {
   afterEach(cleanup);
 
   test('input element', () => {
-    const { container } = render(<InputBox />);
+    const handleChange = (value: string) => {
+      console.log(value);
+    };
+
+    const { container } = testWrapper(
+      <InputBox value="test" onChange={handleChange} />,
+    );
     const inputNode = container.querySelector('input') as HTMLElement;
     expect(inputNode.tagName).toBe('INPUT');
   });
 
   test('textarea element', () => {
-    const { container } = render(<TextareaBox />);
+    const handleChange = (value: string) => {
+      console.log(value);
+    };
+
+    const { container } = testWrapper(
+      <TextareaBox value="test" onChange={handleChange} />,
+    );
     const textAreaNode = container.querySelector('TextArea') as HTMLElement;
     expect(textAreaNode.tagName).toBe('TEXTAREA');
   });
 
-  test('onChange event', () => {
-    const { container } = render(<InputBox />);
+  test('test value', () => {
+    const handleChange = (value: string) => {
+      console.log(value);
+    };
+
+    const { container } = testWrapper(
+      <InputBox value="test default value" onChange={handleChange} />,
+    );
     const inputNode = container.querySelector('input') as HTMLInputElement;
-    fireEvent.change(inputNode, { target: { value: 'text' } });
 
-    expect(inputNode.value).toBe('text');
+    // fireEvent.change(inputNode, { target: { value: 'text' } });
+
+    expect(inputNode.value).toBe('test default value');
   });
 
   test('check disabled input', () => {
     const handleChange = jest.fn();
-    const { getByTestId } = render(
-      <InputBox disabled onChange={handleChange} />,
+    const { getByTestId } = testWrapper(
+      <InputBox disabled value="" onChange={handleChange} />,
     );
-    fireEvent.change(getByTestId('input'), { target: { value: 'text' } });
 
     expect(handleChange).toBeCalledTimes(0);
     expect(getByTestId('input')).toBeDisabled();

+ 6 - 6
__test__/selectBox.test.tsx

@@ -1,7 +1,7 @@
-/* eslint-disable no-undef */
 import React from 'react';
-import { render, cleanup, fireEvent } from '@testing-library/react';
+import { cleanup, fireEvent } from '@testing-library/react';
 
+import testWrapper from '../helpers/testWrapper';
 import SelectBox from '../components/SelectBox';
 
 describe('SelectBox component', () => {
@@ -21,7 +21,7 @@ describe('SelectBox component', () => {
   ];
 
   test('default value', () => {
-    const { getAllByTestId } = render(<SelectBox options={options} />);
+    const { getAllByTestId } = testWrapper(<SelectBox options={options} />);
     const ele = getAllByTestId('selected')[0].querySelector(
       'div',
     ) as HTMLDivElement;
@@ -30,17 +30,17 @@ describe('SelectBox component', () => {
   });
 
   test('open list', () => {
-    const { container } = render(<SelectBox options={options} />);
+    const { container } = testWrapper(<SelectBox options={options} />);
 
     fireEvent.mouseDown(
       container.querySelector('[data-testid="selected"]') as HTMLElement,
     );
 
-    expect(container.querySelectorAll('span')[1].textContent).toBe('option 2');
+    expect(container.querySelectorAll('span')[2].textContent).toBe('option 2');
   });
 
   test('use input box', () => {
-    const { container } = render(<SelectBox options={options} useInput />);
+    const { container } = testWrapper(<SelectBox options={options} useInput />);
     const inputNode = container.querySelector('input') as HTMLInputElement;
 
     expect(inputNode.value).toBe('option 1');

+ 3 - 2
__test__/slider.test.tsx

@@ -1,15 +1,16 @@
 /* eslint-disable no-undef */
 import React from 'react';
-import { render, cleanup } from '@testing-library/react';
+import { cleanup } from '@testing-library/react';
 import '@testing-library/jest-dom';
 
+import testWrapper from '../helpers/testWrapper';
 import Sliders from '../components/Sliders';
 
 describe('Slider component', () => {
   afterEach(cleanup);
 
   test('check sliders status', () => {
-    const { getByTestId } = render(<Sliders />);
+    const { getByTestId } = testWrapper(<Sliders />);
 
     expect(getByTestId('sliders')).toBeVisible();
   });

+ 10 - 5
__test__/typography.test.tsx

@@ -1,26 +1,31 @@
 /* eslint-disable no-undef */
 import React from 'react';
-import { render, cleanup } from '@testing-library/react';
+import { cleanup } from '@testing-library/react';
 import '@testing-library/jest-dom';
 
+import testWrapper from '../helpers/testWrapper';
 import Typography from '../components/Typography';
 
 describe('Typography component', () => {
   afterEach(cleanup);
 
   test('check title style', () => {
-    const { getByText } = render(
+    const { getByText } = testWrapper(
       <Typography variant="title">title</Typography>,
     );
 
-    expect(getByText('title')).toHaveStyle('font-size: 16px;color: #000000;');
+    expect(getByText('title')).toHaveStyle(
+      'font-size: 1.35rem;color: #000000;',
+    );
   });
 
   test('check subtitle style', () => {
-    const { queryByText } = render(
+    const { queryByText } = testWrapper(
       <Typography variant="subtitle">title</Typography>,
     );
 
-    expect(queryByText('title')).toHaveStyle('font-size: 14px;color: #000000;');
+    expect(queryByText('title')).toHaveStyle(
+      'font-size: 1.2rem;color: #000000;',
+    );
   });
 });

+ 5 - 4
components/AnnotationList/index.tsx

@@ -19,8 +19,9 @@ const AnnotationList: React.FC<Props> = ({
   const [list, setList] = useState<AnnotationType[]>([]);
 
   useEffect(() => {
-    if (isActive && annotations.length) {
-      annotations.sort((a, b) => {
+    if (isActive) {
+      const tmpArray = [...annotations];
+      tmpArray.sort((a, b) => {
         if (a.obj_attr.page > b.obj_attr.page) {
           return 1;
         }
@@ -29,7 +30,7 @@ const AnnotationList: React.FC<Props> = ({
         }
         return 0;
       });
-      setList(annotations);
+      setList(tmpArray);
     }
   }, [isActive, annotations]);
 
@@ -47,7 +48,7 @@ const AnnotationList: React.FC<Props> = ({
               obj_attr: { page, title, date, style },
             } = ele;
             const actualPage = page + 1;
-            const prevAnnot = annotations[index - 1];
+            const prevAnnot = list[index - 1];
             const prevPage =
               index > 0 && prevAnnot ? prevAnnot.obj_attr.page + 1 : -1;
 

+ 1 - 0
components/DeleteDialog/styled.ts

@@ -3,6 +3,7 @@ import styled from 'styled-components';
 export const TextWrapper = styled.div`
   width: 324px;
   margin-bottom: 16px;
+  font-size: 1.25rem;
 `;
 
 export const BtnWrapper = styled.div`

+ 29 - 68
components/Ink/index.tsx

@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
 
 import OuterRect from '../OuterRect';
 import {
@@ -7,13 +7,11 @@ import {
   controlPoint,
   line,
 } from '../../helpers/svgBezierCurve';
+import { pointCalc, rectCalcWithPoint, calcViewBox } from '../../helpers/brush';
 
 import { AnnotationContainer } from '../../global/otherStyled';
 import { SVG } from './styled';
 
-let points: PointType[][] = [];
-let rect: HTMLCoordinateType = { top: 0, left: 0, width: 0, height: 0 };
-
 const Ink: React.FC<AnnotationElementPropsType> = ({
   obj_attr: { position, bdcolor, bdwidth = 0, transparency },
   isCollapse,
@@ -22,53 +20,14 @@ const Ink: React.FC<AnnotationElementPropsType> = ({
   scale,
   id,
 }: AnnotationElementPropsType) => {
+  let points: PointType[][] = [];
+  let tempRect: HTMLCoordinateType = { top: 0, left: 0, width: 0, height: 0 };
   const borderWidth = bdwidth * scale;
 
-  const pointCalc = (
-    _points: PointType[][],
-    h: number,
-    s: number,
-  ): PointType[][] => {
-    const reducer = _points.reduce(
-      (acc: PointType[][], cur: PointType[]): PointType[][] => {
-        const p = cur.map((point: PointType) => ({
-          x: point.x * s,
-          y: h - point.y * s,
-        }));
-        acc.push(p);
-        return acc;
-      },
-      [],
-    );
-
-    return reducer;
-  };
-
-  const rectCalcWithPoint = (
-    pointsGroup: PointType[][],
-  ): HTMLCoordinateType => {
-    const xArray: number[] = [];
-    const yArray: number[] = [];
-    pointsGroup[0].forEach((point: PointType) => {
-      xArray.push(point.x);
-      yArray.push(point.y);
-    });
-    const top = Math.min(...yArray);
-    const left = Math.min(...xArray);
-    const bottom = Math.max(...yArray);
-    const right = Math.max(...xArray);
-    return {
-      top,
-      left,
-      width: right - left + borderWidth,
-      height: bottom - top + borderWidth,
-    };
-  };
-
   points = pointCalc(position as PointType[][], viewport.height, scale);
-  rect = rectCalcWithPoint(points);
+  tempRect = rectCalcWithPoint(points, borderWidth);
 
-  const [newRect, setRect] = useState({ top: 0, left: 0, width: 0, height: 0 });
+  const [rect, setRect] = useState({ top: 0, left: 0, width: 0, height: 0 });
 
   const handleScaleOrMove = ({
     top,
@@ -76,17 +35,16 @@ const Ink: React.FC<AnnotationElementPropsType> = ({
     width = 0,
     height = 0,
   }: CoordType): void => {
-    const xScaleRate = width / rect.width;
-    const yScaleRate = height / rect.height;
-    const xDistance = left - rect.left;
-    const yDistance = rect.top - top;
+    const xScaleRate = width / tempRect.width;
+    const yScaleRate = height / tempRect.height;
+    const xDistance = left - tempRect.left;
+    const yDistance = tempRect.top - top;
 
     const newPosition = (position as PointType[][])[0].map((ele) => ({
       x: (ele.x + xDistance) * (xScaleRate || 1),
       y: (ele.y + yDistance) * (yScaleRate || 1),
     }));
 
-    rect = { top, left, width, height };
     setRect({
       top,
       left,
@@ -99,23 +57,26 @@ const Ink: React.FC<AnnotationElementPropsType> = ({
     });
   };
 
-  const calcViewBox = (
-    { top, left, width, height }: HTMLCoordinateType,
-    _borderWidth: number,
-  ): string => `
-    ${left - _borderWidth / 2} ${top - _borderWidth / 2} ${width} ${height}
-  `;
+  useEffect(() => {
+    const newPoints = pointCalc(
+      position as PointType[][],
+      viewport.height,
+      scale,
+    );
+    const newRect = rectCalcWithPoint(newPoints, borderWidth);
+    setRect(newRect);
+  }, [viewport, scale]);
 
   return (
     <>
       <AnnotationContainer
         id={id}
-        top={`${newRect.top || rect.top}px`}
-        left={`${newRect.left || rect.left}px`}
-        width={`${newRect.width || rect.width}px`}
-        height={`${newRect.height || rect.height}px`}
+        top={`${rect.top}px`}
+        left={`${rect.left}px`}
+        width={`${rect.width}px`}
+        height={`${rect.height}px`}
       >
-        <SVG viewBox={calcViewBox(rect, bdwidth * scale)}>
+        <SVG viewBox={calcViewBox(tempRect, borderWidth)}>
           {points.map((ele: PointType[], index: number) => {
             const key = `${id}_path_${index}`;
             return (
@@ -127,7 +88,7 @@ const Ink: React.FC<AnnotationElementPropsType> = ({
                 )}
                 fill="none"
                 stroke={bdcolor}
-                strokeWidth={bdwidth * scale}
+                strokeWidth={borderWidth}
                 strokeOpacity={transparency}
               />
             );
@@ -136,10 +97,10 @@ const Ink: React.FC<AnnotationElementPropsType> = ({
       </AnnotationContainer>
       {!isCollapse ? (
         <OuterRect
-          top={newRect.top || rect.top}
-          left={newRect.left || rect.left}
-          width={newRect.width || rect.width}
-          height={newRect.height || rect.height}
+          top={rect.top}
+          left={rect.left}
+          width={rect.width}
+          height={rect.height}
           onMove={handleScaleOrMove}
           onScale={handleScaleOrMove}
         />

+ 2 - 1
components/InputBox/index.tsx

@@ -25,6 +25,7 @@ const InputBox = forwardRef<HTMLInputElement, Props>(
 
     return (
       <Input
+        type="text"
         data-testid="input"
         ref={ref}
         disabled={disabled}
@@ -44,7 +45,7 @@ InputBox.defaultProps = {
   onBlur: () => {
     // do something
   },
-  value: '',
+  value: undefined,
   defaultValue: undefined,
   placeholder: '',
   disabled: false,

+ 5 - 3
components/OuterRect/index.tsx

@@ -39,7 +39,7 @@ const initState = {
   clickY: 0,
 };
 
-const index: React.FC<Props> = ({
+const OuterRect: React.FC<Props> = ({
   left,
   top,
   width,
@@ -51,7 +51,7 @@ const index: React.FC<Props> = ({
 }: Props) => {
   const data = generateCirclesData(width, height);
   const [state, setState] = useState(initState);
-  const [cursorPosition, setRef] = useCursorPosition(25);
+  const [cursorPosition, setRef] = useCursorPosition(20);
 
   const handleMouseDown = (e: React.MouseEvent | React.TouchEvent): void => {
     e.preventDefault();
@@ -84,6 +84,8 @@ const index: React.FC<Props> = ({
   ): CoordType => ({
     left: currentPosition.x - (startPosition.x - objPosition.left),
     top: currentPosition.y - (startPosition.y - objPosition.top),
+    width: objPosition.width,
+    height: objPosition.height,
   });
 
   const calcScaleResult = (
@@ -177,4 +179,4 @@ const index: React.FC<Props> = ({
   );
 };
 
-export default index;
+export default OuterRect;

+ 3 - 3
components/OuterRectForLine/index.tsx

@@ -27,7 +27,7 @@ const initState = {
   clickY: 0,
 };
 
-const index: React.FC<Props> = ({
+const OuterRect: React.FC<Props> = ({
   top,
   left,
   width,
@@ -40,7 +40,7 @@ const index: React.FC<Props> = ({
   completeEnd,
 }: Props) => {
   const [state, setState] = useState(initState);
-  const [cursorPosition, setRef] = useCursorPosition();
+  const [cursorPosition, setRef] = useCursorPosition(20);
 
   const handleMouseDown = (e: React.MouseEvent): void => {
     const operatorId = (e.target as HTMLElement).getAttribute(
@@ -136,4 +136,4 @@ const index: React.FC<Props> = ({
   );
 };
 
-export default index;
+export default OuterRect;

+ 30 - 9
components/Page/index.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useState, useRef } from 'react';
+import React, { useEffect, useRef } from 'react';
 
 import Watermark from '../Watermark';
 import { renderPdfPage } from '../../helpers/pdf';
@@ -11,10 +11,9 @@ import {
   TextLayer,
   WatermarkLayer,
   Inner,
+  Canvas,
 } from './styled';
 
-let pdfPage: PdfPageType | null = null;
-
 type Props = {
   pageNum: number;
   renderingState: RenderingStateType;
@@ -26,6 +25,7 @@ type Props = {
   watermark: WatermarkType;
   queryString: string;
   matchesMap: MatchType[];
+  toolState: ToolType | FormType | '';
 };
 
 const PageView: React.FC<Props> = ({
@@ -39,9 +39,16 @@ const PageView: React.FC<Props> = ({
   watermark = {},
   queryString,
   matchesMap,
+  toolState,
 }: Props) => {
+  let pdfPage: any = null;
+  let renderTask: any = null;
+
   const rootEle = useRef<HTMLDivElement | null>(null);
-  const [renderTask, setRenderTask] = useState<RenderTaskType | null>(null);
+
+  const setRenderTask = (task: any) => {
+    renderTask = task;
+  };
 
   const renderPage = async (): Promise<void> => {
     if (getPage) {
@@ -73,11 +80,13 @@ const PageView: React.FC<Props> = ({
   };
 
   useEffect(() => {
-    if (renderingState === 'LOADING' && pdfPage) {
-      pdfPage.cleanup();
-    }
-    if (renderingState === 'RENDERING' && renderTask) {
+    if (renderTask) {
       renderTask.cancel();
+      renderTask = null;
+    }
+    if (pdfPage) {
+      pdfPage.cleanup();
+      pdfPage = null;
     }
     if (renderingState === 'RENDERING') {
       renderPage();
@@ -111,10 +120,22 @@ const PageView: React.FC<Props> = ({
           ) : (
             ''
           )}
-          <TextLayer data-id="text-layer" />
+          <TextLayer
+            data-id="text-layer"
+            style={{
+              display:
+                toolState === 'freehand' || toolState === 'shape'
+                  ? 'none'
+                  : 'block',
+            }}
+          />
           <AnnotationLayer data-id="annotation-layer">
             {annotations}
           </AnnotationLayer>
+          <Canvas
+            className="canvas"
+            viewBox={`0 0 ${viewport.width} ${viewport.height}`}
+          />
         </>
       )}
     </PageWrapper>

+ 7 - 0
components/Page/styled.ts

@@ -82,3 +82,10 @@ export const Inner = styled.div`
   height: 100%;
   font-size: 1.5rem;
 `;
+
+export const Canvas = styled.svg`
+  display: none;
+  position: absolute;
+  top: 0;
+  left: 0;
+`;

+ 20 - 1
components/TextField/styled.ts

@@ -4,9 +4,28 @@ const style: Record<string, string> = {
   textfield: '',
   checkbox: `
     border-radius: 8px;
+    :after {
+      content: '';
+      background-image: url('/icons/check-mark.svg');
+      background-repeat: no-repeat;
+      background-position: center;
+      background-size: contain;
+      width: 32px;
+      height: 32px;
+      opacity: 0.25;
+    }
   `,
   radio: `
     border-radius: 100%;
+
+    :after {
+      content: '';
+      background-color: black;
+      border-radius: 50%;
+      width: 50%;
+      height: 50%;
+      opacity: 0.25;
+    }
   `,
 };
 
@@ -14,7 +33,7 @@ export const TextBox = styled.div<{ type: string }>`
   width: 100%;
   height: 100%;
   background-color: rgba(51, 190, 219, 0.3);
-  font-size: 1.3rem;
+  font-size: 1.4rem;
   color: rgba(0, 0, 0, 0.5);
   display: flex;
   justify-content: center;

+ 1 - 1
components/TextareaBox/index.tsx

@@ -43,7 +43,7 @@ InputBox.defaultProps = {
   onBlur: () => {
     // do something
   },
-  value: '',
+  value: undefined,
   defaultValue: undefined,
   placeholder: '',
   disabled: false,

+ 6 - 17
components/Toolbar/styled.ts

@@ -1,14 +1,4 @@
-import styled, { css, keyframes } from 'styled-components';
-
-const closeToolbar = keyframes`
-  from {
-    pointer-events: auto;
-    opacity: 1;
-  }
-  to {
-    opacity: 0;
-  }
-`;
+import styled from 'styled-components';
 
 export const Container = styled('div')<{
   displayMode: string;
@@ -33,12 +23,11 @@ export const Container = styled('div')<{
 
   transition: all 225ms ease-in-out;
 
-  ${(props) =>
-    props.hidden
-      ? css`
-          animation: ${closeToolbar} 3s forwards;
-        `
-      : ''}
+  opacity: ${(props) => (props.hidden ? 0 : 1)};
+
+  &:hover {
+    opacity: 1;
+  }
 `;
 
 export default Container;

+ 100 - 93
containers/FreehandTool.tsx

@@ -8,9 +8,13 @@ import ExpansionPanel from '../components/ExpansionPanel';
 import InkOption from '../components/InkOption';
 
 import {
-  getAbsoluteCoordinate,
-  parsePositionForBackend,
-} from '../helpers/position';
+  svgPath,
+  bezierCommand,
+  controlPoint,
+  line,
+} from '../helpers/svgBezierCurve';
+
+import { getAbsoluteCoordinate } from '../helpers/position';
 import {
   parseAnnotationObject,
   appendUserIdAndDate,
@@ -27,18 +31,21 @@ type Props = {
   onClick: () => void;
 };
 
+let pathEle: any = null;
+
 const FreehandTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
   const [cursorPosition, setRef] = useCursorPosition(30);
 
-  const [uuid, setUuid] = useState('');
+  const [path, setPath] = useState<PointType[]>([]);
   const [data, setData] = useState({
+    page: 0,
     type: 'pen',
     opacity: 100,
     color: '#FF0000',
     width: 3,
   });
-  const [{ viewport, scale, annotations }, dispatch] = useStore();
-  const { addAnnots, updateAnnots } = useActions(dispatch);
+  const [{ viewport, scale }, dispatch] = useStore();
+  const { addAnnots } = useActions(dispatch);
 
   const setDataState = (obj: OptionPropsType): void => {
     setData((prev) => ({
@@ -47,112 +54,112 @@ const FreehandTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
     }));
   };
 
-  const handleMouseDown = useCallback(
-    (event: MouseEvent | TouchEvent): void => {
-      const pageEle = (event.target as HTMLElement).parentNode as HTMLElement;
-      switchPdfViewerScrollState('hidden');
-
-      if (pageEle.hasAttribute('data-page-num')) {
-        setRef(pageEle);
-        const pageNum = pageEle.getAttribute('data-page-num') || 0;
-        const coordinate = getAbsoluteCoordinate(pageEle, event);
-        const id = uuidv4();
-
-        setUuid(id);
-
-        const annotData = {
-          id,
-          obj_type: ANNOTATION_TYPE.ink,
-          obj_attr: {
-            page: pageNum as number,
-            bdcolor: data.color,
-            bdwidth: data.width,
-            position: [[coordinate]],
-            transparency: data.opacity,
-          },
-        };
-        const freehand = appendUserIdAndDate(
-          parseAnnotationObject(annotData, viewport.height, scale),
-        );
-
-        addAnnots([freehand]);
-      }
-    },
-    [data, viewport, scale],
-  );
+  const handleMouseDown = (event: MouseEvent): void => {
+    const pageEle = (event.target as HTMLElement).parentNode as HTMLElement;
+    switchPdfViewerScrollState('hidden');
+
+    if (pageEle.hasAttribute('data-page-num')) {
+      setRef(pageEle);
+      const pageNum = pageEle.getAttribute('data-page-num') || '0';
+      const coordinate = getAbsoluteCoordinate(pageEle, event);
+      setData((current) => ({
+        ...current,
+        page: parseInt(pageNum, 10),
+      }));
+      setPath([coordinate]);
+    }
+  };
 
   const handleMouseUp = useCallback((): void => {
     switchPdfViewerScrollState('scroll');
-
-    const index = annotations.length - 1;
-    if (annotations[index]) {
-      const position = annotations[index].obj_attr.position as PointType[][];
-
-      if (!position[0]) return;
-
-      if (position[0].length === 1 && annotations[index].id === uuid) {
-        const point = position[0][0];
-        annotations[index].obj_attr.position = [
-          [
-            { x: point.x - 5, y: point.y - 5 },
-            { x: point.x + 5, y: point.y + 5 },
-          ],
-        ];
-        annotations[index] = appendUserIdAndDate(annotations[index]);
-        updateAnnots([...annotations]);
-      }
-
+    const id = uuidv4();
+
+    if (path.length) {
+      const defaultPoints = [
+        { x: path[0].x - 3, y: path[0].y - 3 },
+        { x: path[0].x + 3, y: path[0].y + 3 },
+      ];
+
+      const annotData = {
+        id,
+        obj_type: ANNOTATION_TYPE.ink,
+        obj_attr: {
+          page: data.page as number,
+          bdcolor: data.color,
+          bdwidth: data.width,
+          position: path.length === 1 ? [defaultPoints] : [path],
+          transparency: data.opacity,
+        },
+      };
+      const freehand = appendUserIdAndDate(
+        parseAnnotationObject(annotData, viewport.height, scale),
+      );
+
+      addAnnots([freehand]);
       setRef(null);
-      setUuid('');
+      setPath([]);
     }
-  }, [annotations, uuid]);
+  }, [path, data, viewport, scale]);
 
   useEffect(() => {
-    const index = annotations.length - 1;
-
-    if (
-      annotations[index] &&
-      annotations[index].id === uuid &&
-      cursorPosition.x &&
-      cursorPosition.y
-    ) {
-      const type = annotations[index].obj_type;
-      const position = annotations[index].obj_attr.position as PointType[][];
-      const coordinates = parsePositionForBackend(
-        type,
-        { x: cursorPosition.x, y: cursorPosition.y },
-        viewport.height,
-        scale,
-      ) as PointType;
-
-      const lastPosition = position[0].slice(-1)[0];
-
-      if (
-        coordinates.x !== lastPosition.x &&
-        coordinates.y !== lastPosition.y
-      ) {
-        position[0].push(coordinates);
-        annotations[index].obj_attr.position = position;
-        annotations[index] = appendUserIdAndDate(annotations[index]);
-        updateAnnots([...annotations]);
+    if (cursorPosition.x && cursorPosition.y) {
+      const coordinates = {
+        x: cursorPosition.x,
+        y: cursorPosition.y,
+      } as PointType;
+
+      setPath((current) => {
+        return [...current, coordinates];
+      });
+    }
+  }, [cursorPosition]);
+
+  /**
+   * 1. draw to canvas when mouse move
+   * 2. trigger mouse up to remove path element
+   */
+  useEffect(() => {
+    const pageEle = document.getElementById(`page_${data.page}`);
+    const canvas = pageEle?.getElementsByClassName('canvas')[0] as HTMLElement;
+
+    if (path.length) {
+      if (pageEle && canvas) {
+        canvas.style.display = 'block';
+
+        if (pathEle) {
+          const d = svgPath(path, bezierCommand(controlPoint(line, 0.2)));
+          pathEle.setAttribute('d', d);
+        } else {
+          pathEle = document.createElementNS(
+            'http://www.w3.org/2000/svg',
+            'path',
+          );
+          pathEle.setAttribute('fill', 'none');
+          pathEle.setAttribute('stroke', data.color);
+          pathEle.setAttribute('stroke-width', data.width * scale);
+          pathEle.setAttribute('stroke-opacity', data.opacity * 0.01);
+          canvas.appendChild(pathEle);
+        }
       }
+    } else if (canvas && pathEle) {
+      canvas.style.display = 'none';
+      canvas.removeChild(pathEle);
+      pathEle = null;
     }
-  }, [annotations, cursorPosition, uuid]);
+  }, [path, data, scale]);
 
   const subscribeEvent = (): void => {
     const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
+
     pdfViewer.addEventListener('mousedown', handleMouseDown);
     pdfViewer.addEventListener('mouseup', handleMouseUp);
-    pdfViewer.addEventListener('touchstart', handleMouseDown);
-    pdfViewer.addEventListener('touchend', handleMouseUp);
   };
 
   const unsubscribeEvent = (): void => {
     const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
+
     pdfViewer.removeEventListener('mousedown', handleMouseDown);
     pdfViewer.removeEventListener('mouseup', handleMouseUp);
-    pdfViewer.removeEventListener('touchstart', handleMouseDown);
-    pdfViewer.removeEventListener('touchend', handleMouseUp);
   };
 
   useEffect(() => {
@@ -167,7 +174,7 @@ const FreehandTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
         unsubscribeEvent();
       }
     };
-  }, [isActive, handleMouseDown, handleMouseUp]);
+  }, [isActive, handleMouseUp]);
 
   const Label = (
     <Button

+ 6 - 6
containers/HighlightTools.tsx

@@ -130,13 +130,13 @@ const HighlightTools: React.FC<Props> = ({
 
     if (isActive) {
       document.addEventListener('mousedown', handleDown);
-      document.addEventListener('touchstart', handleDown);
       document.addEventListener('mousemove', handleMove);
-      document.addEventListener('touchmove', handleMove);
       document.addEventListener('mouseup', handleUp);
-      document.addEventListener('touchend', handleUp);
       document.addEventListener('selectstart', handleSelectStart);
       if (md.mobile() || md.tablet()) {
+        document.addEventListener('touchstart', handleDown);
+        document.addEventListener('touchmove', handleMove);
+        document.addEventListener('touchend', handleUp);
         document.addEventListener('selectionchange', handleSelectChange);
       }
     } else if (textLayer) {
@@ -145,13 +145,13 @@ const HighlightTools: React.FC<Props> = ({
 
     return (): void => {
       document.removeEventListener('mousedown', handleDown);
-      document.removeEventListener('touchstart', handleDown);
       document.removeEventListener('mousemove', handleMove);
-      document.removeEventListener('touchmove', handleMove);
       document.removeEventListener('mouseup', handleUp);
-      document.removeEventListener('touchend', handleUp);
       document.removeEventListener('selectstart', handleSelectStart);
       if (md.mobile() || md.tablet()) {
+        document.removeEventListener('touchstart', handleDown);
+        document.removeEventListener('touchmove', handleMove);
+        document.removeEventListener('touchend', handleUp);
         document.removeEventListener('selectionchange', handleSelectChange);
       }
     };

+ 13 - 11
containers/InsertCursor.tsx

@@ -1,32 +1,34 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, useState } from 'react';
 
-import Cursor from '../components/Cursor';
-import useCursorPosition from '../hooks/useCursorPosition';
+import CursorImage from '../components/Cursor';
 
 import useStore from '../store';
 
 const InsertCursor = () => {
   const [{ toolState }] = useStore();
-  const [cursorPosition, setRef] = useCursorPosition(25);
+  const [position, setPosition] = useState({ x: 0, y: 0 });
+
+  const getMousePosition = (event: any) => {
+    setPosition({
+      x: event.clientX,
+      y: event.clientY,
+    });
+  };
 
   useEffect(() => {
     const viewer = document.getElementById('pdf_viewer') as HTMLDivElement;
 
     if (toolState && viewer) {
-      setRef(viewer);
+      viewer.addEventListener('mousemove', getMousePosition);
       viewer.style.cursor = 'crosshair';
     } else if (viewer) {
-      setRef(null);
+      viewer.removeEventListener('mousemove', getMousePosition);
       viewer.style.cursor = 'auto';
     }
   }, [toolState]);
 
   return ['textfield', 'checkbox', 'radio'].includes(toolState) ? (
-    <Cursor
-      x={cursorPosition.clientX || -1000}
-      y={cursorPosition.clientY || -1000}
-      appearance={toolState}
-    />
+    <CursorImage x={position.x} y={position.y} appearance={toolState} />
   ) : null;
 };
 

+ 2 - 2
containers/MarkupTools.tsx

@@ -8,7 +8,7 @@ import HighlightTools from './HighlightTools';
 import FreehandTool from './FreehandTool';
 import TextTool from './FreeTextTool';
 import StickyNoteTool from './StickyNoteTool';
-import ShapeTools from './ShapeTools';
+import ShapeTool from './ShapeTool';
 
 import useActions from '../actions';
 import useStore from '../store';
@@ -86,7 +86,7 @@ const MarkupTools: React.FC<Props> = ({
           onClickTool('sticky');
         }}
       />
-      <ShapeTools
+      <ShapeTool
         title={t('shape')}
         isActive={toolState === 'shape'}
         onClick={(): void => {

+ 2 - 0
containers/PdfPage.tsx

@@ -25,6 +25,7 @@ const PdfPage: React.FC<Props> = ({ index }: Props) => {
       currentPage,
       queryString,
       matchesMap,
+      toolState,
     },
   ] = useStore();
 
@@ -69,6 +70,7 @@ const PdfPage: React.FC<Props> = ({ index }: Props) => {
       annotations={getAnnotationWithPage(annotations, index)}
       queryString={queryString}
       matchesMap={matchesMap}
+      toolState={toolState}
     />
   );
 };

+ 255 - 0
containers/ShapeTool.tsx

@@ -0,0 +1,255 @@
+import React, { useEffect, useState, useCallback } from 'react';
+import { v4 as uuidv4 } from 'uuid';
+
+import { ANNOTATION_TYPE } from '../constants';
+import Button from '../components/Button';
+import ExpansionPanel from '../components/ExpansionPanel';
+import Icon from '../components/Icon';
+import ShapeOption from '../components/ShapeOption';
+
+import { getAbsoluteCoordinate } from '../helpers/position';
+import {
+  parseAnnotationObject,
+  appendUserIdAndDate,
+} from '../helpers/annotation';
+import { switchPdfViewerScrollState } from '../helpers/pdf';
+import useCursorPosition from '../hooks/useCursorPosition';
+
+import useActions from '../actions';
+import useStore from '../store';
+
+type Props = {
+  title: string;
+  isActive: boolean;
+  onClick: () => void;
+};
+
+let shapeEle: SVGElement | null = null;
+
+const ShapeTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
+  const [startPoint, setStartPoint] = useState({ x: 0, y: 0 });
+  const [endPoint, setEndPoint] = useState({ x: 0, y: 0 });
+
+  const [data, setData] = useState({
+    page: 0,
+    shape: 'square',
+    type: 'fill',
+    color: '#FBB705',
+    opacity: 35,
+    width: 0,
+  });
+  const [cursorPosition, setRef] = useCursorPosition(30);
+
+  const [{ viewport, scale }, dispatch] = useStore();
+  const { addAnnots } = useActions(dispatch);
+
+  const setDataState = (obj: OptionPropsType): void => {
+    setData((prev) => ({
+      ...prev,
+      ...obj,
+    }));
+  };
+
+  const convertPosition = (
+    type: string,
+    x1: number,
+    y1: number,
+    x2: number,
+    y2: number,
+  ): PositionType | LinePositionType => {
+    switch (type) {
+      case 'Line':
+        return {
+          start: {
+            x: x1,
+            y: y1,
+          },
+          end: {
+            x: x2,
+            y: y2,
+          },
+        };
+      default:
+        return {
+          top: y2 > y1 ? y1 : y2,
+          left: x2 > x1 ? x1 : x2,
+          right: x2 > x1 ? x2 : x1,
+          bottom: y2 > y1 ? y2 : y1,
+        };
+    }
+  };
+
+  const handleMouseDown = (event: MouseEvent): void => {
+    switchPdfViewerScrollState('hidden');
+
+    const pageEle = (event.target as HTMLElement).parentNode as HTMLElement;
+
+    if (pageEle.hasAttribute('data-page-num')) {
+      setRef(pageEle);
+      const pageNum = pageEle.getAttribute('data-page-num') || '0';
+      const coordinate = getAbsoluteCoordinate(pageEle, event);
+      setData((current) => ({
+        ...current,
+        page: parseInt(pageNum, 10),
+      }));
+      setStartPoint(coordinate);
+    }
+  };
+
+  const handleMouseUp = useCallback((): void => {
+    switchPdfViewerScrollState('scroll');
+    const shapeType = ANNOTATION_TYPE[data.shape];
+    const id = uuidv4();
+
+    if (startPoint.x !== endPoint.x && endPoint.y) {
+      const position = convertPosition(
+        shapeType,
+        startPoint.x,
+        startPoint.y,
+        endPoint.x,
+        endPoint.y,
+      );
+
+      const annoteData = {
+        id,
+        obj_type: shapeType,
+        obj_attr: {
+          page: data.page as number,
+          position,
+          bdcolor: data.color,
+          fcolor: data.type === 'fill' ? data.color : undefined,
+          transparency: data.opacity,
+          ftransparency: data.type === 'fill' ? data.opacity : undefined,
+          bdwidth: data.width,
+          is_arrow: data.shape === 'arrow',
+        },
+      };
+      const shapeAnnotation = appendUserIdAndDate(
+        parseAnnotationObject(annoteData, viewport.height, scale),
+      );
+
+      addAnnots([shapeAnnotation]);
+
+      setRef(null);
+      setStartPoint({ x: 0, y: 0 });
+      setEndPoint({ x: 0, y: 0 });
+    }
+  }, [startPoint, endPoint, viewport, data, scale]);
+
+  const subscribeEvent = (): void => {
+    const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
+    pdfViewer.addEventListener('mousedown', handleMouseDown);
+    pdfViewer.addEventListener('mouseup', handleMouseUp);
+  };
+
+  const unsubscribeEvent = (): void => {
+    const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
+    pdfViewer.removeEventListener('mousedown', handleMouseDown);
+    pdfViewer.removeEventListener('mouseup', handleMouseUp);
+  };
+
+  useEffect(() => {
+    const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
+
+    if (isActive && pdfViewer) {
+      subscribeEvent();
+    }
+
+    return (): void => {
+      if (pdfViewer) {
+        unsubscribeEvent();
+      }
+    };
+  }, [isActive, handleMouseUp]);
+
+  useEffect(() => {
+    if (cursorPosition.x && cursorPosition.y) {
+      setEndPoint({ x: cursorPosition.x, y: cursorPosition.y });
+    }
+  }, [cursorPosition]);
+
+  const checkSvgChild = (type: string) => {
+    switch (type) {
+      case 'square':
+        return 'rect';
+      case 'circle':
+        return 'ellipse';
+      default:
+        return 'line';
+    }
+  };
+
+  useEffect(() => {
+    const pageEle = document.getElementById(`page_${data.page}`);
+    const canvas = pageEle?.getElementsByClassName('canvas')[0] as HTMLElement;
+
+    if (endPoint.x && endPoint.y) {
+      canvas.style.display = 'block';
+
+      if (shapeEle) {
+        const { top, left, right, bottom } = convertPosition(
+          ANNOTATION_TYPE[data.shape],
+          startPoint.x,
+          startPoint.y,
+          endPoint.x,
+          endPoint.y,
+        ) as PositionType;
+
+        if (data.shape === 'square') {
+          shapeEle.setAttribute('x', `${left}`);
+          shapeEle.setAttribute('y', `${top}`);
+          shapeEle.setAttribute('width', `${right - left}`);
+          shapeEle.setAttribute('height', `${bottom - top}`);
+        } else if (data.shape === 'circle') {
+          const xRadius = (right - left) / 2;
+          const yRadius = (bottom - top) / 2;
+          shapeEle.setAttribute('cx', `${left + xRadius}`);
+          shapeEle.setAttribute('cy', `${top + yRadius}`);
+          shapeEle.setAttribute('rx', `${xRadius}`);
+          shapeEle.setAttribute('ry', `${yRadius}`);
+        } else {
+          const actualWidth = data.width * scale;
+          shapeEle.setAttribute('x1', `${startPoint.x + actualWidth}`);
+          shapeEle.setAttribute('y1', `${startPoint.y + actualWidth}`);
+          shapeEle.setAttribute('x2', `${endPoint.x + actualWidth}`);
+          shapeEle.setAttribute('y2', `${endPoint.y + actualWidth}`);
+        }
+      } else {
+        shapeEle = document.createElementNS(
+          'http://www.w3.org/2000/svg',
+          checkSvgChild(data.shape),
+        );
+        shapeEle.setAttribute('fill', data.color);
+        shapeEle.setAttribute('fill-opacity', `${data.opacity * 0.01}`);
+        shapeEle.setAttribute('stroke', data.color);
+        shapeEle.setAttribute('stroke-width', `${data.width * scale}`);
+        shapeEle.setAttribute('stroke-opacity', `${data.opacity * 0.01}`);
+        canvas.appendChild(shapeEle);
+      }
+    } else if (canvas && shapeEle) {
+      canvas.style.display = 'none';
+      canvas.removeChild(shapeEle);
+      shapeEle = null;
+    }
+  }, [startPoint, endPoint, data, scale]);
+
+  const Label = (
+    <Button
+      shouldFitContainer
+      align="left"
+      onClick={onClick}
+      isActive={isActive}
+    >
+      <Icon glyph="shape" style={{ marginRight: '10px' }} />
+      {title}
+    </Button>
+  );
+
+  return (
+    <ExpansionPanel isActive={isActive} label={Label}>
+      <ShapeOption {...data} setDataState={setDataState} />
+    </ExpansionPanel>
+  );
+};
+
+export default ShapeTool;

+ 0 - 228
containers/ShapeTools.tsx

@@ -1,228 +0,0 @@
-import React, { useEffect, useState, useCallback } from 'react';
-import { v4 as uuidv4 } from 'uuid';
-
-import { ANNOTATION_TYPE } from '../constants';
-import Button from '../components/Button';
-import ExpansionPanel from '../components/ExpansionPanel';
-import Icon from '../components/Icon';
-import ShapeOption from '../components/ShapeOption';
-
-import {
-  getAbsoluteCoordinate,
-  parsePositionForBackend,
-} from '../helpers/position';
-import {
-  parseAnnotationObject,
-  appendUserIdAndDate,
-} from '../helpers/annotation';
-import { switchPdfViewerScrollState } from '../helpers/pdf';
-import useCursorPosition from '../hooks/useCursorPosition';
-
-import useActions from '../actions';
-import useStore from '../store';
-
-type Props = {
-  title: string;
-  isActive: boolean;
-  onClick: () => void;
-};
-
-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: 'square',
-    type: 'fill',
-    color: '#FBB705',
-    opacity: 35,
-    width: 0,
-  });
-  const [cursorPosition, setRef] = useCursorPosition();
-
-  const [{ viewport, scale, annotations }, dispatch] = useStore();
-  const { addAnnots, updateAnnots } = useActions(dispatch);
-
-  const setDataState = (obj: OptionPropsType): void => {
-    setData((prev) => ({
-      ...prev,
-      ...obj,
-    }));
-  };
-
-  const convertPosition = (
-    type: string,
-    x1: number,
-    y1: number,
-    x2: number,
-    y2: number,
-  ): PositionType | LinePositionType => {
-    switch (type) {
-      case 'Line':
-        return {
-          start: {
-            x: x1,
-            y: y1,
-          },
-          end: {
-            x: x2,
-            y: y2,
-          },
-        };
-      default:
-        return {
-          top: y2 > y1 ? y1 : y2,
-          left: x2 > x1 ? x1 : x2,
-          right: x2 > x1 ? x2 : x1,
-          bottom: y2 > y1 ? y2 : y1,
-        };
-    }
-  };
-
-  const addShape = useCallback(
-    (
-      pageEle: HTMLElement,
-      event: MouseEvent | TouchEvent,
-      attributes: OptionPropsType,
-    ): void => {
-      const { shape = '', type, opacity, color, width = 0 } = attributes;
-      const pageNum = pageEle.getAttribute('data-page-num') || 0;
-      const coordinate = getAbsoluteCoordinate(pageEle, event);
-      const id = uuidv4();
-
-      setUuid(id);
-      setStartPosition(coordinate);
-
-      const shapeType = ANNOTATION_TYPE[shape];
-      const position = convertPosition(
-        shapeType,
-        coordinate.x - 8,
-        coordinate.y - 8,
-        coordinate.x + 8,
-        coordinate.y + 8,
-      );
-      const annoteData = {
-        id,
-        obj_type: shapeType,
-        obj_attr: {
-          page: pageNum as number,
-          position,
-          bdcolor: color,
-          fcolor: type === 'fill' ? color : undefined,
-          transparency: opacity,
-          ftransparency: type === 'fill' ? opacity : undefined,
-          bdwidth: width,
-          is_arrow: shape === 'arrow',
-        },
-      };
-      const shapeAnnotation = appendUserIdAndDate(
-        parseAnnotationObject(annoteData, viewport.height, scale),
-      );
-
-      addAnnots([shapeAnnotation]);
-    },
-    [viewport, scale, data],
-  );
-
-  const handleMouseDown = (event: MouseEvent | TouchEvent): void => {
-    switchPdfViewerScrollState('hidden');
-
-    const pageEle = (event.target as HTMLElement).parentNode as HTMLElement;
-
-    if (pageEle.hasAttribute('data-page-num')) {
-      addShape(pageEle, event, data);
-      setRef(pageEle);
-    }
-  };
-
-  const handleMouseUp = (): void => {
-    switchPdfViewerScrollState('scroll');
-    setRef(null);
-    setUuid('');
-    setStartPosition({ x: 0, y: 0 });
-  };
-
-  const subscribeEvent = (): void => {
-    const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
-    pdfViewer.addEventListener('mousedown', handleMouseDown);
-    pdfViewer.addEventListener('mouseup', handleMouseUp);
-    pdfViewer.addEventListener('touchstart', handleMouseDown);
-    pdfViewer.addEventListener('touchend', handleMouseUp);
-  };
-
-  const unsubscribeEvent = (): void => {
-    const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
-    pdfViewer.removeEventListener('mousedown', handleMouseDown);
-    pdfViewer.removeEventListener('mouseup', handleMouseUp);
-    pdfViewer.removeEventListener('touchstart', handleMouseDown);
-    pdfViewer.removeEventListener('touchend', handleMouseUp);
-  };
-
-  useEffect(() => {
-    const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
-
-    if (isActive && pdfViewer) {
-      subscribeEvent();
-    }
-
-    return (): void => {
-      if (pdfViewer) {
-        unsubscribeEvent();
-      }
-    };
-  }, [isActive, addShape]);
-
-  const handleUpdate = useCallback(
-    (start: PointType, end: PointType): void => {
-      const index = annotations.length - 1;
-      const { x: x1, y: y1 } = start;
-      const { x: x2, y: y2 } = end;
-
-      if (annotations[index] && annotations[index].id === uuid) {
-        const type = annotations[index].obj_type;
-        const position = convertPosition(type, x1, y1, x2, y2);
-
-        annotations[index].obj_attr.position = parsePositionForBackend(
-          type,
-          position,
-          viewport.height,
-          scale,
-        );
-        annotations[index] = appendUserIdAndDate(annotations[index]);
-
-        updateAnnots([...annotations]);
-      }
-    },
-    [annotations, viewport, scale, uuid],
-  );
-
-  useEffect(() => {
-    if (
-      startPosition.x &&
-      startPosition.y &&
-      cursorPosition.x &&
-      cursorPosition.y
-    ) {
-      handleUpdate(startPosition, cursorPosition as PointType);
-    }
-  }, [startPosition, cursorPosition]);
-
-  const Label = (
-    <Button
-      shouldFitContainer
-      align="left"
-      onClick={onClick}
-      isActive={isActive}
-    >
-      <Icon glyph="shape" style={{ marginRight: '10px' }} />
-      {title}
-    </Button>
-  );
-
-  return (
-    <ExpansionPanel isActive={isActive} label={Label}>
-      <ShapeOption {...data} setDataState={setDataState} />
-    </ExpansionPanel>
-  );
-};
-
-export default Shape;

+ 18 - 5
custom.d.ts

@@ -21,13 +21,26 @@ type SelectOptionType = {
 
 type RenderingStateType = 'RENDERING' | 'LOADING' | 'FINISHED' | 'PAUSED';
 
-type LineType = 'Highlight' | 'Underline' | 'Squiggly' | 'StrikeOut';
-
-type FormType = 'textfield' | 'checkbox' | 'radio';
+enum FormType {
+  textfield = 'textfield',
+  checkbox = 'checkbox',
+  radio = 'radio',
+}
 
-type ToolType = 'highlight' | 'freehand' | 'text' | 'sticky' | 'shape';
+enum ToolType {
+  highlight = 'highlight',
+  freehand = 'freehand',
+  text = 'text',
+  sticky = 'sticky',
+  shape = 'shape',
+}
 
-type SidebarType = 'markup-tools' | 'create-form' | 'watermark' | 'image';
+enum SidebarType {
+  'markup-tools' = 'markup-tools',
+  'create-form' = 'create-form',
+  watermark = 'watermark',
+  image = 'image',
+}
 
 type ViewportType = {
   width: number;

+ 54 - 0
helpers/brush.ts

@@ -40,3 +40,57 @@ export const completePath: CompletePathType = (pathElement, penCoordinates) => {
     pathElement.setAttribute('points', points);
   }
 };
+
+export const pointCalc = (
+  _points: PointType[][],
+  h: number,
+  s: number,
+): PointType[][] => {
+  const reducer = _points.reduce(
+    (acc: PointType[][], cur: PointType[]): PointType[][] => {
+      const p = cur.map((point: PointType) => ({
+        x: point.x * s,
+        y: h - point.y * s,
+      }));
+      acc.push(p);
+      return acc;
+    },
+    [],
+  );
+
+  return reducer;
+};
+
+export const rectCalcWithPoint = (
+  pointsGroup: PointType[][],
+  borderWidth: number,
+): HTMLCoordinateType => {
+  const xArray: number[] = [];
+  const yArray: number[] = [];
+
+  pointsGroup[0].forEach((point: PointType) => {
+    xArray.push(point.x);
+    yArray.push(point.y);
+  });
+
+  const top = Math.min(...yArray);
+  const left = Math.min(...xArray);
+  const bottom = Math.max(...yArray);
+  const right = Math.max(...xArray);
+  return {
+    top,
+    left,
+    width: right - left + borderWidth,
+    height: bottom - top + borderWidth,
+  };
+};
+
+export const calcViewBox = (
+  { top, left, width, height }: HTMLCoordinateType,
+  borderWidth: number,
+): string => {
+  const distance = borderWidth / 2;
+  return `
+  ${left - distance} ${top - distance} ${width + distance} ${height + distance}
+`;
+};

+ 3 - 7
helpers/pdf.ts

@@ -1,5 +1,8 @@
+import pdfjs from 'pdfjs-dist/es5/build/pdf.js';
 import { delay } from './time';
 
+pdfjs.GlobalWorkerOptions.workerSrc = '/static/build/pdf.worker.min.js';
+
 let normalizationRegex: RegExp | null = null;
 const CHARACTERS_TO_NORMALIZE: { [index: string]: string } = {
   '\u2018': "'", // Left single quotation mark
@@ -20,9 +23,6 @@ export const fetchPdf = async (
   cb?: (progress: ProgressType) => void,
 ): Promise<PdfType> => {
   try {
-    const pdfjs = await import('pdfjs-dist/es5/build/pdf.js');
-    pdfjs.GlobalWorkerOptions.workerSrc = '/static/build/pdf.worker.min.js';
-
     const loadingTask = pdfjs.getDocument({
       url: src,
       cMapUrl: '/static/cmaps/',
@@ -56,10 +56,6 @@ export const renderTextLayer = async ({
   setTextDivs?: (elements: HTMLElement[]) => void;
 }): Promise<void> => {
   if (!pdfPage) return;
-
-  const pdfjs = await import('pdfjs-dist/es5/build/pdf.js');
-  pdfjs.GlobalWorkerOptions.workerSrc = '/static/build/pdf.worker.min.js';
-
   const textContent = await pdfPage.getTextContent({
     normalizeWhitespace: true,
   });

+ 18 - 0
helpers/testWrapper.tsx

@@ -0,0 +1,18 @@
+import React from 'react';
+import { ThemeProvider } from 'styled-components';
+import { render } from '@testing-library/react';
+
+import myTheme from './theme';
+
+const getThemeProviderWrappingComponent = (theme: Record<string, unknown>) => (
+  children: React.ReactNode,
+) => <ThemeProvider theme={theme}>{children}</ThemeProvider>;
+
+const renderwWithTheme = (
+  tree: React.ReactNode,
+  theme: Record<string, unknown> = myTheme,
+) => {
+  return render(getThemeProviderWrappingComponent(theme)(tree));
+};
+
+export default renderwWithTheme;

+ 31 - 19
hooks/useCursorPosition.ts

@@ -1,6 +1,7 @@
 import { useState, useRef, useCallback, useEffect } from 'react';
 import { fromEvent, Subscription } from 'rxjs';
 import { throttleTime } from 'rxjs/operators';
+import MobileDetect from 'mobile-detect';
 
 import { getAbsoluteCoordinate } from '../helpers/position';
 
@@ -83,6 +84,8 @@ const useCursorPosition: UseCursorPositionType = (time = defaultTime) => {
   );
 
   useEffect(() => {
+    const md = new MobileDetect(window.navigator.userAgent);
+
     let mouseSubscription: Subscription | null = null;
     let touchSubscription: Subscription | null = null;
 
@@ -116,31 +119,40 @@ const useCursorPosition: UseCursorPositionType = (time = defaultTime) => {
     };
 
     if (element) {
-      mouseSubscription = fromEvent(element, 'mousemove')
-        .pipe(throttleTime(time))
-        .subscribe(onMouseMoveEvent);
-      touchSubscription = fromEvent(element, 'touchmove')
-        .pipe(throttleTime(time))
-        .subscribe(onTouchMoveEvent);
-
       const addEvent = element.addEventListener.bind(element);
-      addEvent('mouseenter', onEnter);
-      addEvent('mouseleave', onLeave);
-      addEvent('mousedown', onDown);
-      addEvent('touchstart', onTouch);
-      addEvent('mouseup', onUp);
-      addEvent('touchend', onUp);
+
+      if (md.mobile() || md.tablet()) {
+        touchSubscription = fromEvent(element, 'touchmove')
+          .pipe(throttleTime(time))
+          .subscribe(onTouchMoveEvent);
+
+        addEvent('touchstart', onTouch);
+        addEvent('touchend', onUp);
+      } else {
+        mouseSubscription = fromEvent(element, 'mousemove')
+          .pipe(throttleTime(time))
+          .subscribe(onMouseMoveEvent);
+
+        addEvent('mouseenter', onEnter);
+        addEvent('mouseleave', onLeave);
+        addEvent('mousedown', onDown);
+        addEvent('mouseup', onUp);
+      }
     }
 
     return (): void => {
       if (element) {
         const removeEvent = element.removeEventListener.bind(element);
-        removeEvent('mouseenter', onEnter);
-        removeEvent('mouseleave', onLeave);
-        removeEvent('mousedown', onDown);
-        removeEvent('touchstart', onTouch);
-        removeEvent('mouseup', onUp);
-        removeEvent('touchend', onUp);
+
+        if (md.mobile() || md.tablet()) {
+          removeEvent('touchstart', onTouch);
+          removeEvent('touchend', onUp);
+        } else {
+          removeEvent('mouseenter', onEnter);
+          removeEvent('mouseleave', onLeave);
+          removeEvent('mousedown', onDown);
+          removeEvent('mouseup', onUp);
+        }
       }
     };
   }, [element]);

+ 5 - 3
package.json

@@ -14,7 +14,7 @@
   "husky": {
     "hooks": {
       "prepare-commit-msg": "exec < /dev/tty && git cz --hook || true",
-      "pre-commit": "lint-staged && yarn type-check"
+      "pre-commit": "lint-staged && yarn test && yarn type-check"
     }
   },
   "lint-staged": {
@@ -33,7 +33,7 @@
     "mobile-detect": "^1.4.4",
     "next": "9.5.0",
     "next-i18next": "4.3.0",
-    "pdfjs-dist": "2.5.207",
+    "pdfjs-dist": "2.4.456",
     "print-js": "^1.0.63",
     "query-string": "5",
     "react": "16.13.0",
@@ -50,7 +50,9 @@
   },
   "license": "MIT",
   "devDependencies": {
-    "@babel/node": "^7.2.2",
+    "@babel/cli": "^7.11.6",
+    "@babel/core": "^7.11.6",
+    "@babel/node": "^7.10.5",
     "@babel/preset-env": "^7.8.3",
     "@testing-library/jest-dom": "^5.8.0",
     "@testing-library/react": "^10.0.4",

+ 44 - 0
public/icons/check-mark.svg

@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="45.701px" height="45.7px" viewBox="0 0 45.701 45.7" style="enable-background:new 0 0 45.701 45.7;" xml:space="preserve"
+	>
+<g>
+	<g>
+		<path d="M20.687,38.332c-2.072,2.072-5.434,2.072-7.505,0L1.554,26.704c-2.072-2.071-2.072-5.433,0-7.504
+			c2.071-2.072,5.433-2.072,7.505,0l6.928,6.927c0.523,0.522,1.372,0.522,1.896,0L36.642,7.368c2.071-2.072,5.433-2.072,7.505,0
+			c0.995,0.995,1.554,2.345,1.554,3.752c0,1.407-0.559,2.757-1.554,3.752L20.687,38.332z"/>
+	</g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>

File diff suppressed because it is too large
+ 1 - 1
public/static/build/pdf.worker.min.js


+ 29 - 8
yarn.lock

@@ -52,6 +52,22 @@
   dependencies:
     cross-fetch "3.0.5"
 
+"@babel/cli@^7.11.6":
+  version "7.11.6"
+  resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.11.6.tgz#1fcbe61c2a6900c3539c06ee58901141f3558482"
+  integrity sha512-+w7BZCvkewSmaRM6H4L2QM3RL90teqEIHDIFXAmrW33+0jhlymnDAEdqVeCZATvxhQuio1ifoGVlJJbIiH9Ffg==
+  dependencies:
+    commander "^4.0.1"
+    convert-source-map "^1.1.0"
+    fs-readdir-recursive "^1.1.0"
+    glob "^7.0.0"
+    lodash "^4.17.19"
+    make-dir "^2.1.0"
+    slash "^2.0.0"
+    source-map "^0.5.0"
+  optionalDependencies:
+    chokidar "^2.1.8"
+
 "@babel/code-frame@7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e"
@@ -95,7 +111,7 @@
     semver "^5.4.1"
     source-map "^0.5.0"
 
-"@babel/core@^7.1.0":
+"@babel/core@^7.1.0", "@babel/core@^7.11.6":
   version "7.11.6"
   resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.6.tgz#3a9455dc7387ff1bac45770650bc13ba04a15651"
   integrity sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg==
@@ -350,7 +366,7 @@
     chalk "^2.0.0"
     js-tokens "^4.0.0"
 
-"@babel/node@^7.2.2":
+"@babel/node@^7.10.5":
   version "7.10.5"
   resolved "https://registry.yarnpkg.com/@babel/node/-/node-7.10.5.tgz#30866322aa2c0251a9bdd73d07a9167bd1f4ed64"
   integrity sha512-suosS7zZ2roj+fYVCnDuVezUbRc0sdoyF0Gj/1FzWxD4ebbGiBGtL5qyqHH4NO34B5m4vWWYWgyNhSsrqS8vwA==
@@ -3219,7 +3235,7 @@ conventional-commit-types@^3.0.0:
   resolved "https://registry.yarnpkg.com/conventional-commit-types/-/conventional-commit-types-3.0.0.tgz#7c9214e58eae93e85dd66dbfbafe7e4fffa2365b"
   integrity sha512-SmmCYnOniSsAa9GqWOeLqc179lfr5TRu5b4QFDkbsrJ5TZjPJx85wtOr3zn+1dbeNiXDKGPbZ72IKbPhLXh/Lg==
 
-convert-source-map@1.7.0, convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.7.0:
+convert-source-map@1.7.0, convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.7.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
   integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
@@ -4774,6 +4790,11 @@ fs-minipass@^2.0.0:
   dependencies:
     minipass "^3.0.0"
 
+fs-readdir-recursive@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27"
+  integrity sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==
+
 fs-write-stream-atomic@^1.0.8:
   version "1.0.10"
   resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9"
@@ -4890,7 +4911,7 @@ glob@7.1.4:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
+glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
   version "7.1.6"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
   integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
@@ -7449,10 +7470,10 @@ pbkdf2@^3.0.3:
     safe-buffer "^5.0.1"
     sha.js "^2.4.8"
 
-pdfjs-dist@2.5.207:
-  version "2.5.207"
-  resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-2.5.207.tgz#b5e8c19627be64269cd3fb6df3eaaf45ddffe7b6"
-  integrity sha512-xGDUhnCYPfHy+unMXCLCJtlpZaaZ17Ew3WIL0tnSgKFUZXHAPD49GO9xScyszSsQMoutNDgRb+rfBXIaX/lJbw==
+pdfjs-dist@2.4.456:
+  version "2.4.456"
+  resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-2.4.456.tgz#0eaad2906cda866bbb393e79a0e5b4e68bd75520"
+  integrity sha512-yckJEHq3F48hcp6wStEpbN9McOj328Ib09UrBlGAKxvN2k+qYPN5iq6TH6jD1C0pso7zTep+g/CKsYgdrQd5QA==
 
 performance-now@^2.1.0:
   version "2.1.0"