5 Commits 3230a372b0 ... 4394d9f9d1

Auteur SHA1 Message Date
  RoyLiu 4394d9f9d1 fix(annotation tools): optimization event. brush position. add svg attributs il y a 4 ans
  RoyLiu c4e082cdd2 fix(line module): drag and drop performance il y a 4 ans
  RoyLiu 3f29879b58 update(brush): add offset value il y a 4 ans
  RoyLiu 0172dd3755 optimization(shape and brush): drag and drop event il y a 4 ans
  RoyLiu 1bed240222 feature(touch event): support touch for tablet il y a 4 ans

+ 15 - 4
.eslintrc.json

@@ -15,10 +15,16 @@
     "prettier/@typescript-eslint"
   ],
   "parser": "@typescript-eslint/parser",
-  "plugins": ["jsx-a11y", "@typescript-eslint", "prettier"],
+  "plugins": [
+    "jsx-a11y",
+    "@typescript-eslint",
+    "prettier"
+  ],
   "rules": {
     "react/display-name": "off",
-    // "@typescript-eslint/no-explicit-any": "off",
+    "@typescript-eslint/no-explicit-any": "off",
+    "@typescript-eslint/explicit-function-return-type": "off",
+    "@typescript-eslint/camelcase": "off",
     "@typescript-eslint/explicit-module-boundary-types": "off"
   },
   "settings": {
@@ -27,8 +33,13 @@
     },
     "import/resolver": {
       "node": {
-        "extensions": [".js", ".jsx", ".ts", ".tsx"]
+        "extensions": [
+          ".js",
+          ".jsx",
+          ".ts",
+          ".tsx"
+        ]
       }
     }
   }
-}
+}

+ 2 - 7
components/AnnotationList/index.tsx

@@ -45,24 +45,19 @@ const AnnotationList: React.FC<Props> = ({
             const key = `annotation_item_${index}`;
             const {
               obj_type,
-              obj_attr: { page, title, date, style },
+              obj_attr: { page, title, date, is_arrow },
             } = ele;
             const actualPage = page + 1;
             const prevAnnot = list[index - 1];
             const prevPage =
               index > 0 && prevAnnot ? prevAnnot.obj_attr.page + 1 : -1;
 
-            let annotType = obj_type;
-            if (obj_type === 'checkbox' && style === '1') {
-              annotType = 'radio';
-            }
-
             return (
               <Item
                 key={key}
                 title={title}
                 date={date}
-                type={annotType}
+                type={is_arrow ? 'Arrow' : obj_type}
                 page={actualPage}
                 showPageNum={actualPage !== prevPage}
               />

+ 62 - 26
components/Ink/index.tsx

@@ -20,14 +20,11 @@ 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;
-
-  points = pointCalc(position as PointType[][], viewport.height, scale);
-  tempRect = rectCalcWithPoint(points, borderWidth);
-
+  const [points, setPoints] = useState<PointType[][]>([]);
   const [rect, setRect] = useState({ top: 0, left: 0, width: 0, height: 0 });
+  const [moving, setMoving] = useState(false);
+
+  const borderWidth = bdwidth * scale;
 
   const handleScaleOrMove = ({
     top,
@@ -35,28 +32,48 @@ const Ink: React.FC<AnnotationElementPropsType> = ({
     width = 0,
     height = 0,
   }: CoordType): void => {
-    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),
-    }));
-
-    setRect({
-      top,
-      left,
-      width,
-      height,
-    });
+    setMoving(true);
+
+    setRect((currentRect) => {
+      const rx = width / currentRect.width;
+      const ry = height / currentRect.height;
+      const dx = left - currentRect.left;
+      const dy = currentRect.top - top;
+      let disX = 0;
+      let disY = 0;
+
+      setPoints((currentPoint) => {
+        const newRect = rectCalcWithPoint(currentPoint, borderWidth);
+        disX = newRect.left - left;
+        disY = newRect.top - top;
 
-    onUpdate({
-      position: [newPosition],
+        const newPoints = (currentPoint as PointType[][])[0].map((ele) => {
+          let x = (ele.x + dx) * rx;
+          let y = (ele.y - dy) * ry;
+
+          if (rx !== 1) {
+            x = x - disX;
+          }
+          if (ry !== 1) {
+            y = y - disY + 5;
+          }
+
+          return {
+            x,
+            y,
+          };
+        });
+        return [newPoints];
+      });
+
+      return { top, left, width, height };
     });
   };
 
+  const handleMouseUp = () => {
+    setMoving(false);
+  };
+
   useEffect(() => {
     const newPoints = pointCalc(
       position as PointType[][],
@@ -64,9 +81,23 @@ const Ink: React.FC<AnnotationElementPropsType> = ({
       scale,
     );
     const newRect = rectCalcWithPoint(newPoints, borderWidth);
+    setPoints(newPoints);
     setRect(newRect);
   }, [viewport, scale]);
 
+  useEffect(() => {
+    if (!moving && points[0]) {
+      const newPoints = (points as PointType[][])[0].map((ele) => ({
+        x: ele.x / scale,
+        y: (viewport.height - ele.y) / scale,
+      }));
+
+      onUpdate({
+        position: [newPoints],
+      });
+    }
+  }, [moving, points]);
+
   return (
     <>
       <AnnotationContainer
@@ -76,7 +107,11 @@ const Ink: React.FC<AnnotationElementPropsType> = ({
         width={`${rect.width}px`}
         height={`${rect.height}px`}
       >
-        <SVG viewBox={calcViewBox(tempRect, borderWidth)}>
+        <SVG
+          width={rect.width + borderWidth}
+          height={rect.height + borderWidth}
+          viewBox={calcViewBox(rect, borderWidth)}
+        >
           {points.map((ele: PointType[], index: number) => {
             const key = `${id}_path_${index}`;
             return (
@@ -103,6 +138,7 @@ const Ink: React.FC<AnnotationElementPropsType> = ({
           height={rect.height}
           onMove={handleScaleOrMove}
           onScale={handleScaleOrMove}
+          onMouseUp={handleMouseUp}
         />
       ) : (
         ''

+ 3 - 0
components/Ink/styled.ts

@@ -4,6 +4,9 @@ export const SVG = styled.svg`
   pointer-events: none;
   user-select: none;
   touch-action: none;
+  position: absolute;
+  left: 0;
+  top: 0;
 `;
 
 export const PolyLine = styled('polyline')<{ isCovered: boolean }>`

+ 139 - 119
components/Line/index.tsx

@@ -1,136 +1,157 @@
-import React from 'react';
+import React, { useState, useEffect } from 'react';
 import { v4 as uuidv4 } from 'uuid';
 
 import Outer from '../OuterRectForLine';
 import { parsePositionForBackend } from '../../helpers/position';
+
 import { AnnotationContainer } from '../../global/otherStyled';
 
-const Note: React.SFC<AnnotationElementPropsType> = ({
+import { SVG } from './styled';
+
+const getActualPoint = (
+  { start, end }: LinePositionType,
+  h: number,
+  s: number,
+): LinePositionType => {
+  const x1 = start.x * s;
+  const y1 = h - start.y * s;
+  const x2 = end.x * s;
+  const y2 = h - end.y * s;
+  return {
+    start: {
+      x: x1,
+      y: y1,
+    },
+    end: {
+      x: x2,
+      y: y2,
+    },
+  };
+};
+
+const rectCalc = ({ start, end }: LinePositionType): HTMLCoordinateType => {
+  return {
+    top: start.y < end.y ? start.y : end.y,
+    left: start.x < end.x ? start.x : end.x,
+    width: Math.abs(end.x - start.x),
+    height: Math.abs(end.y - start.y),
+  };
+};
+
+const Line: React.FC<AnnotationElementPropsType> = ({
   id,
   obj_type,
-  obj_attr,
+  obj_attr: { bdcolor = '', bdwidth = 0, transparency = 0, position, is_arrow },
   scale,
   viewport,
   isCollapse,
   onUpdate,
 }: AnnotationElementPropsType) => {
-  const {
-    bdcolor = '',
-    bdwidth = 0,
-    transparency = 0,
-    position,
-    is_arrow,
-  } = obj_attr;
+  const [startPoint, setStartPoint] = useState({ x: 0, y: 0 });
+  const [endPoint, setEndPoint] = useState({ x: 0, y: 0 });
+  const [rect, setRect] = useState({ top: 0, left: 0, width: 0, height: 0 });
+  const [moving, setMoving] = useState(false);
   const uuid = uuidv4();
 
-  const getActualPoint = (
-    { start, end }: LinePositionType,
-    h: number,
-    s: number,
-  ): LinePositionType => {
-    const x1 = start.x * scale;
-    const y1 = h - start.y * s;
-    const x2 = end.x * scale;
-    const y2 = h - end.y * s;
-    return {
-      start: {
-        x: x1,
-        y: y1,
-      },
-      end: {
-        x: x2,
-        y: y2,
-      },
-    };
-  };
-
-  const pointCalc = (
-    { start, end }: LinePositionType,
-    borderWidth: number,
-  ): LinePositionType => ({
-    start: {
-      x: start.x + borderWidth,
-      y: start.y + borderWidth,
-    },
-    end: {
-      x: end.x + borderWidth,
-      y: end.y + borderWidth,
-    },
-  });
-
-  const rectCalc = (
-    { start, end }: LinePositionType,
-    h: number,
-    s: number,
-  ): HTMLCoordinateType => {
-    const startY = h - start.y * s;
-    const endY = h - end.y * s;
-
-    return {
-      top: startY > endY ? endY : startY,
-      left: start.x > end.x ? end.x * s : start.x * s,
-      width: Math.abs((end.x - start.x) * s),
-      height: Math.abs(endY - startY),
-    };
-  };
-
   const actualbdwidth = bdwidth * scale;
-  const annotRect = rectCalc(
-    position as LinePositionType,
-    viewport.height,
-    scale,
-  );
-  const { start, end } = getActualPoint(
-    position as LinePositionType,
-    viewport.height,
-    scale,
-  );
-  const { start: completeStart, end: completeEnd } = pointCalc(
-    { start, end } as LinePositionType,
-    actualbdwidth,
-  );
-
-  const actualWidth = annotRect.width + actualbdwidth + 15;
-  const actualHeight = annotRect.height + actualbdwidth + 15;
 
   const handleMove = ({
     start: moveStart,
     end: moveEnd,
   }: LinePositionType): void => {
-    const newPosition = {
-      start: {
-        x: moveStart.x,
-        y: moveStart.y,
-      },
-      end: {
-        x: moveEnd.x,
-        y: moveEnd.y,
-      },
-    };
-
-    onUpdate({
-      position: parsePositionForBackend(
-        obj_type,
-        newPosition,
-        viewport.height,
-        scale,
-      ),
-    });
+    setMoving(true);
+    setStartPoint(moveStart);
+    setEndPoint(moveEnd);
+
+    const newRect = rectCalc({ start: moveStart, end: moveEnd });
+
+    if (is_arrow) {
+      setRect({
+        top: newRect.top - actualbdwidth * 2,
+        left: newRect.left - actualbdwidth * 2,
+        width: newRect.width + actualbdwidth * 4,
+        height: newRect.height + actualbdwidth * 4,
+      });
+    } else {
+      setRect({
+        top: newRect.top - actualbdwidth / 2,
+        left: newRect.left - actualbdwidth / 2,
+        width: newRect.width + actualbdwidth,
+        height: newRect.height + actualbdwidth,
+      });
+    }
   };
 
+  const handleMouseUp = () => {
+    setMoving(false);
+  };
+
+  useEffect(() => {
+    if (!moving && startPoint.x) {
+      const newPosition = {
+        start: {
+          x: startPoint.x,
+          y: startPoint.y,
+        },
+        end: {
+          x: endPoint.x,
+          y: endPoint.y,
+        },
+      };
+
+      onUpdate({
+        position: parsePositionForBackend(
+          obj_type,
+          newPosition,
+          viewport.height,
+          scale,
+        ),
+      });
+    }
+  }, [moving, startPoint, endPoint]);
+
+  useEffect(() => {
+    const { start, end } = getActualPoint(
+      position as LinePositionType,
+      viewport.height,
+      scale,
+    );
+
+    setStartPoint(start);
+    setEndPoint(end);
+
+    const initRect = rectCalc({ start, end });
+
+    if (is_arrow) {
+      setRect({
+        top: initRect.top - actualbdwidth * 2,
+        left: initRect.left - actualbdwidth * 2,
+        width: initRect.width + actualbdwidth * 4,
+        height: initRect.height + actualbdwidth * 4,
+      });
+    } else {
+      setRect({
+        top: initRect.top - actualbdwidth / 2,
+        left: initRect.left - actualbdwidth / 2,
+        width: initRect.width + actualbdwidth,
+        height: initRect.height + actualbdwidth,
+      });
+    }
+  }, [viewport, scale]);
+
   return (
     <>
       <AnnotationContainer
         id={id}
-        top={`${annotRect.top}px`}
-        left={`${annotRect.left}px`}
-        width={`${actualWidth}px`}
-        height={`${actualHeight}px`}
+        top={`${rect.top}px`}
+        left={`${rect.left}px`}
+        width={`${rect.width}px`}
+        height={`${rect.height}px`}
       >
-        <svg
-          width={`${actualWidth}px`}
-          height={`${actualHeight}px`}
-          viewBox={`${annotRect.left} ${annotRect.top} ${actualWidth} ${actualHeight}`}
+        <SVG
+          width={rect.width}
+          height={rect.height}
+          viewBox={`${rect.left} ${rect.top} ${rect.width} ${rect.height}`}
         >
           {is_arrow ? (
             <defs>
@@ -154,28 +175,27 @@ const Note: React.SFC<AnnotationElementPropsType> = ({
             </defs>
           ) : null}
           <line
-            x1={completeStart.x}
-            y1={completeStart.y}
-            x2={completeEnd.x}
-            y2={completeEnd.y}
+            x1={startPoint.x}
+            y1={startPoint.y}
+            x2={endPoint.x}
+            y2={endPoint.y}
             stroke={bdcolor}
             strokeWidth={actualbdwidth}
             strokeOpacity={transparency}
             markerEnd={is_arrow ? `url(#${uuid})` : ''}
           />
-        </svg>
+        </SVG>
       </AnnotationContainer>
       {!isCollapse ? (
         <Outer
-          top={annotRect.top}
-          left={annotRect.left}
-          width={actualWidth}
-          height={actualHeight}
-          start={start}
-          end={end}
-          completeStart={completeStart}
-          completeEnd={completeEnd}
+          top={rect.top}
+          left={rect.left}
+          width={rect.width}
+          height={rect.height}
+          start={startPoint}
+          end={endPoint}
           onMove={handleMove}
+          onMouseUp={handleMouseUp}
         />
       ) : (
         ''
@@ -184,4 +204,4 @@ const Note: React.SFC<AnnotationElementPropsType> = ({
   );
 };
 
-export default Note;
+export default Line;

+ 15 - 0
components/Line/styled.ts

@@ -0,0 +1,15 @@
+import styled from 'styled-components';
+
+export const SVG = styled.svg`
+  pointer-events: none;
+  user-select: none;
+  touch-action: none;
+  position: absolute;
+  left: 0;
+  top: 0;
+`;
+
+export const PolyLine = styled('polyline')<{ isCovered: boolean }>`
+  pointer-events: visiblepainted;
+  cursor: ${(props) => (props.isCovered ? 'pointer' : 'default')};
+`;

+ 6 - 1
components/OuterRect/index.tsx

@@ -17,6 +17,7 @@ type Props = {
   onScale?: (scaleCoord: CoordType) => void;
   onClick?: (event: React.MouseEvent) => void;
   onDoubleClick?: () => void;
+  onMouseUp?: () => void;
 };
 
 type ObjPositionType = {
@@ -48,10 +49,11 @@ const OuterRect: React.FC<Props> = ({
   onScale,
   onClick,
   onDoubleClick,
+  onMouseUp,
 }: Props) => {
   const data = generateCirclesData(width, height);
   const [state, setState] = useState(initState);
-  const [cursorPosition, setRef] = useCursorPosition(20);
+  const [cursorPosition, setRef] = useCursorPosition(25);
 
   const handleMouseDown = (e: React.MouseEvent | React.TouchEvent): void => {
     e.preventDefault();
@@ -75,6 +77,9 @@ const OuterRect: React.FC<Props> = ({
   const handleMouseUp = (): void => {
     setRef(null);
     setState(initState);
+    if (onMouseUp) {
+      onMouseUp();
+    }
   };
 
   const calcMoveResult = (

+ 16 - 17
components/OuterRectForLine/index.tsx

@@ -1,8 +1,8 @@
 import React, { useEffect, useState } from 'react';
 
-import theme from '../../helpers/theme';
 import useCursorPosition from '../../hooks/useCursorPosition';
 import { getAbsoluteCoordinate } from '../../helpers/position';
+import theme from '../../helpers/theme';
 
 import { SVG, Circle } from './styled';
 
@@ -13,11 +13,10 @@ type Props = LinePositionType & {
   height: number;
   onMove: (position: LinePositionType) => void;
   onClick?: (event: React.MouseEvent) => void;
-  completeStart: PointType;
-  completeEnd: PointType;
+  onMouseUp: () => void;
 };
 
-const MARGIN_DISTANCE = 18;
+const MARGIN_DISTANCE = 10;
 
 const initState = {
   start: { x: 0, y: 0 },
@@ -27,7 +26,7 @@ const initState = {
   clickY: 0,
 };
 
-const OuterRect: React.FC<Props> = ({
+const index: React.FC<Props> = ({
   top,
   left,
   width,
@@ -36,11 +35,10 @@ const OuterRect: React.FC<Props> = ({
   end,
   onMove,
   onClick,
-  completeStart,
-  completeEnd,
+  onMouseUp,
 }: Props) => {
   const [state, setState] = useState(initState);
-  const [cursorPosition, setRef] = useCursorPosition(20);
+  const [cursorPosition, setRef] = useCursorPosition(25);
 
   const handleMouseDown = (e: React.MouseEvent): void => {
     const operatorId = (e.target as HTMLElement).getAttribute(
@@ -61,6 +59,7 @@ const OuterRect: React.FC<Props> = ({
   const handleMouseUp = (): void => {
     setRef(null);
     setState(initState);
+    onMouseUp();
   };
 
   useEffect(() => {
@@ -68,7 +67,6 @@ const OuterRect: React.FC<Props> = ({
       if (state.operator === 'start') {
         const x1 = cursorPosition.x - (state.clickX - state.start.x);
         const y1 = cursorPosition.y - (state.clickY - state.start.y);
-
         onMove({
           start: { x: x1, y: y1 },
           end,
@@ -106,12 +104,13 @@ const OuterRect: React.FC<Props> = ({
   return (
     <SVG
       data-id="move"
-      viewBox={`${left} ${top} ${width + 10} ${height + 10}`}
+      viewBox={`${left} ${top} ${width + MARGIN_DISTANCE} 
+      ${height + MARGIN_DISTANCE}`}
       style={{
         left: `${left - MARGIN_DISTANCE}px`,
         top: `${top - MARGIN_DISTANCE}px`,
-        width: `${width + 10}px`,
-        height: `${height + 10}px`,
+        width: `${width + MARGIN_DISTANCE}px`,
+        height: `${height + MARGIN_DISTANCE}px`,
       }}
       onClick={onClick}
       onMouseDown={handleMouseDown}
@@ -120,20 +119,20 @@ const OuterRect: React.FC<Props> = ({
         data-id="start"
         onMouseDown={handleMouseDown}
         fill={theme.colors.primary}
-        cx={completeStart.x + MARGIN_DISTANCE}
-        cy={completeStart.y + MARGIN_DISTANCE}
+        cx={start.x + MARGIN_DISTANCE}
+        cy={start.y + MARGIN_DISTANCE}
         r={6}
       />
       <Circle
         data-id="end"
         onMouseDown={handleMouseDown}
         fill={theme.colors.primary}
-        cx={completeEnd.x + MARGIN_DISTANCE}
-        cy={completeEnd.y + MARGIN_DISTANCE}
+        cx={end.x + MARGIN_DISTANCE}
+        cy={end.y + MARGIN_DISTANCE}
         r={6}
       />
     </SVG>
   );
 };
 
-export default OuterRect;
+export default index;

+ 5 - 10
components/Page/index.tsx

@@ -120,22 +120,17 @@ const PageView: React.FC<Props> = ({
           ) : (
             ''
           )}
+          <Canvas
+            className="canvas"
+            viewBox={`0 0 ${viewport.width} ${viewport.height}`}
+          />
           <TextLayer
             data-id="text-layer"
-            style={{
-              display:
-                toolState === 'freehand' || toolState === 'shape'
-                  ? 'none'
-                  : 'block',
-            }}
+            disabledSelect={toolState === 'freehand' || toolState === 'shape'}
           />
           <AnnotationLayer data-id="annotation-layer">
             {annotations}
           </AnnotationLayer>
-          <Canvas
-            className="canvas"
-            viewBox={`0 0 ${viewport.width} ${viewport.height}`}
-          />
         </>
       )}
     </PageWrapper>

+ 5 - 2
components/Page/styled.ts

@@ -27,9 +27,10 @@ export const PdfCanvas = styled.canvas`
   display: block;
   width: 100%;
   height: 100%;
+  pointer-events: none;
 `;
 
-export const TextLayer = styled.div`
+export const TextLayer = styled.div<{ disabledSelect: boolean }>`
   position: absolute;
   left: 0;
   top: 0;
@@ -39,6 +40,7 @@ export const TextLayer = styled.div`
   overflow: hidden;
   opacity: 0.3;
   line-height: 1;
+  pointer-events: ${(props) => (props.disabledSelect ? 'none' : 'auto')};
 
   & > span {
     color: transparent;
@@ -73,6 +75,7 @@ export const WatermarkLayer = styled.div`
   display: flex;
   justify-content: center;
   align-items: center;
+  pointer-events: none;
 `;
 
 export const Inner = styled.div`
@@ -84,7 +87,7 @@ export const Inner = styled.div`
 `;
 
 export const Canvas = styled.svg`
-  display: none;
+  display: block;
   position: absolute;
   top: 0;
   left: 0;

+ 1 - 0
components/SelectBox/index.tsx

@@ -41,6 +41,7 @@ const SelectBox: React.FC<Props> = ({
 
   const handleSelect = (index: number): void => {
     setValue(options[index].content);
+    handleBlur();
 
     if (onChange) {
       onChange(options[index]);

+ 59 - 23
components/Shape/index.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
 
 import OuterRect from '../OuterRect';
 import SvgShapeElement from '../SvgShapeElement';
@@ -23,7 +23,14 @@ const Shape: React.FC<AnnotationElementPropsType> = ({
   isCollapse,
   onUpdate,
 }: AnnotationElementPropsType) => {
-  const annotRect = rectCalc(position as PositionType, viewport.height, scale);
+  const [moving, setMoving] = useState(false);
+  const [localePos, setLocalePos] = useState({
+    top: 0,
+    left: 0,
+    width: 0,
+    height: 0,
+  });
+  const actualbdwidth = bdwidth * scale;
 
   const handleScaleOrMove = ({
     top,
@@ -31,51 +38,80 @@ const Shape: React.FC<AnnotationElementPropsType> = ({
     width = 0,
     height = 0,
   }: CoordType): void => {
+    setMoving(true);
     const newPosition = {
       top,
       left,
-      bottom: top + (height || annotRect.height),
-      right: left + (width || annotRect.width),
+      height,
+      width,
     };
 
-    onUpdate({
-      position: parsePositionForBackend(
-        obj_type,
-        newPosition,
-        viewport.height,
-        scale,
-      ),
-    });
+    setLocalePos(newPosition);
+  };
+
+  const handleMouseUp = () => {
+    setMoving(false);
   };
 
+  useEffect(() => {
+    const rect = rectCalc(position as PositionType, viewport.height, scale);
+    setLocalePos({
+      top: rect.top - actualbdwidth / 2,
+      left: rect.left - actualbdwidth / 2,
+      width: rect.width + actualbdwidth,
+      height: rect.height + actualbdwidth,
+    });
+  }, [viewport, scale]);
+
+  useEffect(() => {
+    if (!moving && localePos.width) {
+      const newPosition = {
+        top: localePos.top,
+        left: localePos.left,
+        right: localePos.left + localePos.width,
+        bottom: localePos.top + localePos.height,
+      };
+
+      onUpdate({
+        position: parsePositionForBackend(
+          obj_type,
+          newPosition as PositionType,
+          viewport.height,
+          scale,
+        ),
+      });
+    }
+  }, [moving, localePos]);
+
   return (
     <>
       <AnnotationContainer
         id={id}
-        top={`${annotRect.top}px`}
-        left={`${annotRect.left}px`}
-        width={`${annotRect.width}px`}
-        height={`${annotRect.height}px`}
+        top={`${localePos.top}px`}
+        left={`${localePos.left}px`}
+        width={`${localePos.width}px`}
+        height={`${localePos.height}px`}
       >
         <SvgShapeElement
           shape={obj_type}
-          width={annotRect.width}
-          height={annotRect.height}
+          width={localePos.width}
+          height={localePos.height}
           bdcolor={bdcolor}
           transparency={transparency}
-          bdwidth={bdwidth * scale}
+          bdwidth={actualbdwidth}
           fcolor={fcolor}
           ftransparency={ftransparency}
         />
       </AnnotationContainer>
       {!isCollapse ? (
         <OuterRect
-          top={annotRect.top}
-          left={annotRect.left}
-          width={annotRect.width}
-          height={annotRect.height}
+          top={localePos.top}
+          left={localePos.left}
+          width={localePos.width}
+          height={localePos.height}
           onMove={handleScaleOrMove}
           onScale={handleScaleOrMove}
+          onMouseUp={handleMouseUp}
         />
       ) : (
         ''

+ 3 - 3
components/SvgShapeElement/index.tsx

@@ -21,11 +21,11 @@ const SvgShapeElement: React.FC<Props> = ({
   fcolor,
   ftransparency,
 }: Props) => (
-  <svg viewBox={`0 0 ${width} ${height}`}>
+  <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}>
     {shape === 'Circle' ? (
       <ellipse
-        cx="50%"
-        cy="50%"
+        cx={width / 2}
+        cy={height / 2}
         rx={(width - bdwidth) / 2}
         ry={(height - bdwidth) / 2}
         stroke={bdcolor}

+ 9 - 3
containers/FreeTextTool.tsx

@@ -99,9 +99,15 @@ const HighlightTool: React.FC<Props> = ({
 
   useEffect(() => {
     const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
-    if (isActive && pdfViewer) {
-      pdfViewer.addEventListener('mousedown', handleMouseDown);
-      pdfViewer.addEventListener('touchstart', handleMouseDown);
+
+    if (pdfViewer) {
+      if (isActive) {
+        pdfViewer.addEventListener('mousedown', handleMouseDown);
+        pdfViewer.addEventListener('touchstart', handleMouseDown);
+      } else {
+        pdfViewer.removeEventListener('mousedown', handleMouseDown);
+        pdfViewer.removeEventListener('touchstart', handleMouseDown);
+      }
     }
 
     return (): void => {

+ 12 - 9
containers/FreehandTool.tsx

@@ -14,7 +14,6 @@ import {
   line,
 } from '../helpers/svgBezierCurve';
 
-import { getAbsoluteCoordinate } from '../helpers/position';
 import {
   parseAnnotationObject,
   appendUserIdAndDate,
@@ -54,19 +53,18 @@ const FreehandTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
     }));
   };
 
-  const handleMouseDown = (event: MouseEvent): void => {
+  const handleMouseDown = (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);
+
       setData((current) => ({
         ...current,
         page: parseInt(pageNum, 10),
       }));
-      setPath([coordinate]);
     }
   };
 
@@ -124,8 +122,6 @@ const FreehandTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
 
     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);
@@ -142,7 +138,6 @@ const FreehandTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
         }
       }
     } else if (canvas && pathEle) {
-      canvas.style.display = 'none';
       canvas.removeChild(pathEle);
       pathEle = null;
     }
@@ -153,6 +148,8 @@ const FreehandTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
 
     pdfViewer.addEventListener('mousedown', handleMouseDown);
     pdfViewer.addEventListener('mouseup', handleMouseUp);
+    pdfViewer.addEventListener('touchstart', handleMouseDown);
+    pdfViewer.addEventListener('touchend', handleMouseUp);
   };
 
   const unsubscribeEvent = (): void => {
@@ -160,13 +157,19 @@ const FreehandTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
 
     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();
+    if (pdfViewer) {
+      if (isActive) {
+        subscribeEvent();
+      } else {
+        unsubscribeEvent();
+      }
     }
 
     return (): void => {

+ 26 - 31
containers/HighlightTools.tsx

@@ -1,5 +1,4 @@
 import React, { useEffect, useState, useCallback } from 'react';
-import MobileDetect from 'mobile-detect';
 
 import Icon from '../components/Icon';
 import Button from '../components/Button';
@@ -68,8 +67,6 @@ const HighlightTools: React.FC<Props> = ({
             ) as HTMLElement;
           }
           if (textLayer) {
-            textLayer.style.zIndex = '10';
-
             const newMarkup = getMarkupWithSelection({
               ...data,
               scale,
@@ -84,10 +81,6 @@ const HighlightTools: React.FC<Props> = ({
         }
       }
       if (selection?.isCollapsed) {
-        if (textLayer) {
-          textLayer.style.zIndex = '0';
-        }
-
         setCurrentId('');
       }
     },
@@ -125,35 +118,37 @@ const HighlightTools: React.FC<Props> = ({
     }
   }, [annotations, data, scale, currentId]);
 
-  useEffect(() => {
-    const md = new MobileDetect(window.navigator.userAgent);
+  const subscribeEvent = () => {
+    document.addEventListener('mousedown', handleDown);
+    document.addEventListener('mousemove', handleMove);
+    document.addEventListener('mouseup', handleUp);
+    document.addEventListener('selectstart', handleSelectStart);
+    document.addEventListener('touchstart', handleDown);
+    document.addEventListener('touchmove', handleMove);
+    document.addEventListener('touchend', handleUp);
+    document.addEventListener('selectionchange', handleSelectChange);
+  };
 
+  const unsubscribeEvent = () => {
+    document.removeEventListener('mousedown', handleDown);
+    document.removeEventListener('mousemove', handleMove);
+    document.removeEventListener('mouseup', handleUp);
+    document.removeEventListener('selectstart', handleSelectStart);
+    document.removeEventListener('touchstart', handleDown);
+    document.removeEventListener('touchmove', handleMove);
+    document.removeEventListener('touchend', handleUp);
+    document.removeEventListener('selectionchange', handleSelectChange);
+  };
+
+  useEffect(() => {
     if (isActive) {
-      document.addEventListener('mousedown', handleDown);
-      document.addEventListener('mousemove', handleMove);
-      document.addEventListener('mouseup', 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) {
-      textLayer.style.zIndex = '0';
+      subscribeEvent();
+    } else {
+      unsubscribeEvent();
     }
 
     return (): void => {
-      document.removeEventListener('mousedown', handleDown);
-      document.removeEventListener('mousemove', handleMove);
-      document.removeEventListener('mouseup', 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);
-      }
+      unsubscribeEvent();
     };
   }, [isActive, handleUp, handleSelectChange]);
 

+ 35 - 21
containers/ShapeTool.tsx

@@ -26,10 +26,9 @@ type Props = {
 
 let shapeEle: SVGElement | null = null;
 
-const ShapeTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
+const Shape: 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',
@@ -116,7 +115,12 @@ const ShapeTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
         obj_attr: {
           page: data.page as number,
           position,
-          bdcolor: data.color,
+          bdcolor:
+            data.shape === 'line' ||
+            data.shape === 'arrow' ||
+            data.type === 'border'
+              ? data.color
+              : undefined,
           fcolor: data.type === 'fill' ? data.color : undefined,
           transparency: data.opacity,
           ftransparency: data.type === 'fill' ? data.opacity : undefined,
@@ -151,8 +155,12 @@ const ShapeTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
   useEffect(() => {
     const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
 
-    if (isActive && pdfViewer) {
-      subscribeEvent();
+    if (pdfViewer) {
+      if (isActive) {
+        subscribeEvent();
+      } else {
+        unsubscribeEvent();
+      }
     }
 
     return (): void => {
@@ -160,7 +168,7 @@ const ShapeTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
         unsubscribeEvent();
       }
     };
-  }, [isActive, handleMouseUp]);
+  }, [isActive, handleMouseDown, handleMouseUp]);
 
   useEffect(() => {
     if (cursorPosition.x && cursorPosition.y) {
@@ -168,7 +176,7 @@ const ShapeTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
     }
   }, [cursorPosition]);
 
-  const checkSvgChild = (type: string) => {
+  const checkSvgChild = (type: string): string => {
     switch (type) {
       case 'square':
         return 'rect';
@@ -184,8 +192,6 @@ const ShapeTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
     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],
@@ -208,26 +214,34 @@ const ShapeTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
           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}`);
+          shapeEle.setAttribute('x1', `${startPoint.x}`);
+          shapeEle.setAttribute('y1', `${startPoint.y}`);
+          shapeEle.setAttribute('x2', `${endPoint.x}`);
+          shapeEle.setAttribute('y2', `${endPoint.y}`);
         }
       } 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}`);
+
+        if (
+          data.type !== 'fill' ||
+          data.shape === 'line' ||
+          data.shape === 'arrow'
+        ) {
+          shapeEle.setAttribute('fill', 'none');
+          shapeEle.setAttribute('stroke', data.color);
+          shapeEle.setAttribute('stroke-width', `${data.width * scale}`);
+          shapeEle.setAttribute('stroke-opacity', `${data.opacity * 0.01}`);
+        } else {
+          shapeEle.setAttribute('fill', data.color);
+          shapeEle.setAttribute('fill-opacity', `${data.opacity * 0.01}`);
+        }
+
         canvas.appendChild(shapeEle);
       }
     } else if (canvas && shapeEle) {
-      canvas.style.display = 'none';
       canvas.removeChild(shapeEle);
       shapeEle = null;
     }
@@ -252,4 +266,4 @@ const ShapeTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
   );
 };
 
-export default ShapeTool;
+export default Shape;

+ 2 - 2
containers/Sidebar.tsx

@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
 import Typography from '../components/Typography';
 import FormTools from './FormTools';
 import MarkupTools from './MarkupTools';
-import ImageTool from './ImageTool';
+// import ImageTool from './ImageTool';
 import WatermarkTool from './WatermarkTool';
 import InsertCursor from './InsertCursor';
 
@@ -44,7 +44,7 @@ const Sidebar: React.FC = () => {
         sidebarState={sidebarState}
         onClickSidebar={handleClickSidebar}
       />
-      <ImageTool onClickSidebar={handleClickSidebar} />
+      {/* <ImageTool onClickSidebar={handleClickSidebar} /> */}
       <WatermarkTool onClickSidebar={handleClickSidebar} />
       <InsertCursor />
     </SidebarWrapper>

+ 8 - 3
containers/StickyNoteTool.tsx

@@ -68,9 +68,14 @@ const StickyNoteTool: React.FC<Props> = ({
   useEffect(() => {
     const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
 
-    if (isActive && pdfViewer) {
-      window.addEventListener('touchstart', handleMouseDown);
-      window.addEventListener('mousedown', handleMouseDown);
+    if (pdfViewer) {
+      if (isActive) {
+        window.addEventListener('mousedown', handleMouseDown);
+        window.addEventListener('touchstart', handleMouseDown);
+      } else {
+        window.removeEventListener('mousedown', handleMouseDown);
+        window.removeEventListener('touchstart', handleMouseDown);
+      }
     }
 
     return (): void => {

+ 5 - 6
helpers/brush.ts

@@ -78,10 +78,10 @@ export const rectCalcWithPoint = (
   const bottom = Math.max(...yArray);
   const right = Math.max(...xArray);
   return {
-    top,
-    left,
-    width: right - left + borderWidth,
-    height: bottom - top + borderWidth,
+    top: top - borderWidth,
+    left: left - borderWidth,
+    width: right - left + borderWidth * 2,
+    height: bottom - top + borderWidth * 2,
   };
 };
 
@@ -89,8 +89,7 @@ export const calcViewBox = (
   { top, left, width, height }: HTMLCoordinateType,
   borderWidth: number,
 ): string => {
-  const distance = borderWidth / 2;
   return `
-  ${left - distance} ${top - distance} ${width + distance} ${height + distance}
+  ${left} ${top} ${width + borderWidth} ${height + borderWidth}
 `;
 };

+ 21 - 29
hooks/useCursorPosition.ts

@@ -1,7 +1,6 @@
 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';
 
@@ -84,8 +83,6 @@ const useCursorPosition: UseCursorPositionType = (time = defaultTime) => {
   );
 
   useEffect(() => {
-    const md = new MobileDetect(window.navigator.userAgent);
-
     let mouseSubscription: Subscription | null = null;
     let touchSubscription: Subscription | null = null;
 
@@ -121,38 +118,33 @@ const useCursorPosition: UseCursorPositionType = (time = defaultTime) => {
     if (element) {
       const addEvent = element.addEventListener.bind(element);
 
-      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);
-      }
+      touchSubscription = fromEvent(element, 'touchmove')
+        .pipe(throttleTime(time))
+        .subscribe(onTouchMoveEvent);
+
+      addEvent('touchstart', onTouch);
+      addEvent('touchend', onUp);
+
+      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);
 
-        if (md.mobile() || md.tablet()) {
-          removeEvent('touchstart', onTouch);
-          removeEvent('touchend', onUp);
-        } else {
-          removeEvent('mouseenter', onEnter);
-          removeEvent('mouseleave', onLeave);
-          removeEvent('mousedown', onDown);
-          removeEvent('mouseup', onUp);
-        }
+        removeEvent('touchstart', onTouch);
+        removeEvent('touchend', onUp);
+        removeEvent('mouseenter', onEnter);
+        removeEvent('mouseleave', onLeave);
+        removeEvent('mousedown', onDown);
+        removeEvent('mouseup', onUp);
       }
     };
   }, [element]);

+ 3 - 0
i18n.js

@@ -42,6 +42,9 @@ i18n
     interpolation: {
       escapeValue: false,
     },
+    react: {
+      useSuspense: false,
+    },
   });
 
 export default i18n;

+ 0 - 1
package.json

@@ -30,7 +30,6 @@
     "i18next-browser-languagedetector": "^4.0.2",
     "i18next-http-backend": "^1.0.21",
     "lodash": "^4.17.15",
-    "mobile-detect": "^1.4.4",
     "next": "9.5.0",
     "next-i18next": "4.3.0",
     "pdfjs-dist": "2.4.456",

+ 1 - 1
public/static/locales/en/sidebar.json

@@ -18,7 +18,7 @@
   "size": "Size",
   "align": "Align",
   "image": "Image",
-  "text": "Image",
+  "text": "Text",
   "type": "Type",
   "scale": "Scale",
   "rotate": "Rotate",

+ 0 - 5
yarn.lock

@@ -6772,11 +6772,6 @@ mkdirp@0.x, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1:
   dependencies:
     minimist "^1.2.5"
 
-mobile-detect@^1.4.4:
-  version "1.4.4"
-  resolved "https://registry.yarnpkg.com/mobile-detect/-/mobile-detect-1.4.4.tgz#686c74e92d3cc06b09a9b3594b7b981494b137f6"
-  integrity sha512-vTgEjKjS89C5yHL5qWPpT6BzKuOVqABp+A3Szpbx34pIy3sngxlGaFpgHhfj6fKze1w0QKeOSDbU7SKu7wDvRQ==
-
 move-concurrently@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"