|
@@ -1,6 +1,5 @@
|
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
|
-import MobileDetect from 'mobile-detect';
|
|
|
|
|
|
import { ANNOTATION_TYPE } from '../constants';
|
|
|
import Icon from '../components/Icon';
|
|
@@ -9,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,
|
|
@@ -28,18 +31,21 @@ type Props = {
|
|
|
onClick: () => void;
|
|
|
};
|
|
|
|
|
|
+let pathEle: any = null;
|
|
|
+
|
|
|
const FreehandTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
|
|
|
- const [cursorPosition, setRef] = useCursorPosition(20);
|
|
|
+ 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) => ({
|
|
@@ -48,122 +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 - 3, y: point.y - 3 },
|
|
|
- { x: point.x + 3, y: point.y + 3 },
|
|
|
- ],
|
|
|
- ];
|
|
|
- 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 md = new MobileDetect(window.navigator.userAgent);
|
|
|
const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
|
|
|
|
|
|
- if (md.mobile() || md.tablet()) {
|
|
|
- pdfViewer.addEventListener('touchstart', handleMouseDown);
|
|
|
- pdfViewer.addEventListener('touchend', handleMouseUp);
|
|
|
- } else {
|
|
|
- pdfViewer.addEventListener('mousedown', handleMouseDown);
|
|
|
- pdfViewer.addEventListener('mouseup', handleMouseUp);
|
|
|
- }
|
|
|
+ pdfViewer.addEventListener('mousedown', handleMouseDown);
|
|
|
+ pdfViewer.addEventListener('mouseup', handleMouseUp);
|
|
|
};
|
|
|
|
|
|
const unsubscribeEvent = (): void => {
|
|
|
- const md = new MobileDetect(window.navigator.userAgent);
|
|
|
const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
|
|
|
|
|
|
- if (md.mobile() || md.tablet()) {
|
|
|
- pdfViewer.removeEventListener('touchstart', handleMouseDown);
|
|
|
- pdfViewer.removeEventListener('touchend', handleMouseUp);
|
|
|
- } else {
|
|
|
- pdfViewer.removeEventListener('mousedown', handleMouseDown);
|
|
|
- pdfViewer.removeEventListener('mouseup', handleMouseUp);
|
|
|
- }
|
|
|
+ pdfViewer.removeEventListener('mousedown', handleMouseDown);
|
|
|
+ pdfViewer.removeEventListener('mouseup', handleMouseUp);
|
|
|
};
|
|
|
|
|
|
useEffect(() => {
|
|
@@ -178,7 +174,7 @@ const FreehandTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
|
|
|
unsubscribeEvent();
|
|
|
}
|
|
|
};
|
|
|
- }, [isActive, handleMouseDown, handleMouseUp]);
|
|
|
+ }, [isActive, handleMouseUp]);
|
|
|
|
|
|
const Label = (
|
|
|
<Button
|