|
@@ -1,78 +1,149 @@
|
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
|
+import { v4 as uuidv4 } from 'uuid';
|
|
|
|
|
|
+import { OptionPropsType, PointType } from '../constants/type';
|
|
|
+import { ANNOTATION_TYPE } from '../constants';
|
|
|
import Icon from '../components/Icon';
|
|
|
import Button from '../components/Button';
|
|
|
import ExpansionPanel from '../components/ExpansionPanel';
|
|
|
-import FreehandOption from '../components/FreehandOption';
|
|
|
+import InkOption from '../components/InkOption';
|
|
|
|
|
|
-import { createPolyline, completePath } from '../helpers/brush';
|
|
|
-import { getAbsoluteCoordinate } from '../helpers/utility';
|
|
|
+import { getAbsoluteCoordinate, parsePositionForBackend } from '../helpers/position';
|
|
|
+import { parseAnnotationObject } from '../helpers/annotation';
|
|
|
import useCursorPosition from '../hooks/useCursorPosition';
|
|
|
|
|
|
+import useActions from '../actions';
|
|
|
+import useStore from '../store';
|
|
|
+
|
|
|
type Props = {
|
|
|
+ title: string;
|
|
|
isActive: boolean;
|
|
|
onClick: () => void;
|
|
|
};
|
|
|
|
|
|
-const FreehandTools: React.FunctionComponent<Props> = ({
|
|
|
+const FreehandTools: React.FC<Props> = ({
|
|
|
+ title,
|
|
|
isActive,
|
|
|
onClick,
|
|
|
}: Props) => {
|
|
|
- const [position, setRef] = useCursorPosition();
|
|
|
- const [type, setType] = useState('pen');
|
|
|
- const [color, setColor] = useState('#FCFF36');
|
|
|
- const [opacity, setOpacity] = useState(100);
|
|
|
- const [width, setWidth] = useState(12);
|
|
|
- const [polylineElement, setElement] = useState<SVGPolylineElement | null>(null);
|
|
|
-
|
|
|
- const handleOpacity = (value: number): void => {
|
|
|
- setOpacity(value);
|
|
|
- };
|
|
|
-
|
|
|
- const handleWidth = (value: number): void => {
|
|
|
- setWidth(value);
|
|
|
+ const [cursorPosition, setRef] = useCursorPosition(40);
|
|
|
+
|
|
|
+ const [uuid, setUuid] = useState('');
|
|
|
+ const [data, setData] = useState({
|
|
|
+ type: 'pen',
|
|
|
+ opacity: 100,
|
|
|
+ color: '#FCFF36',
|
|
|
+ width: 12,
|
|
|
+ });
|
|
|
+ const [{ viewport, scale, annotations }, dispatch] = useStore();
|
|
|
+ const { addAnnots, updateAnnots } = useActions(dispatch);
|
|
|
+
|
|
|
+ const setDataState = (obj: OptionPropsType): void => {
|
|
|
+ setData(prev => ({
|
|
|
+ ...prev,
|
|
|
+ ...obj,
|
|
|
+ }));
|
|
|
};
|
|
|
|
|
|
- const handleMouseDown = useCallback((e: MouseEvent): void => {
|
|
|
- const page = (e.target as HTMLElement).parentNode as HTMLElement;
|
|
|
- if (page.hasAttribute('data-page-num')) {
|
|
|
- setRef(page);
|
|
|
-
|
|
|
- const coordinate = getAbsoluteCoordinate(page, e);
|
|
|
- const element = createPolyline(coordinate, color, width);
|
|
|
+ const handleMouseDown = useCallback((e: MouseEvent | TouchEvent): void => {
|
|
|
+ const pageEle = (e.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, e);
|
|
|
+ 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 = parseAnnotationObject(annotData, viewport.height, scale);
|
|
|
+
|
|
|
+ addAnnots([freehand], true);
|
|
|
+ }
|
|
|
+ }, [data, viewport, scale]);
|
|
|
+
|
|
|
+ const handleMouseUp = useCallback((): void => {
|
|
|
+ const index = annotations.length - 1;
|
|
|
+ const position = annotations[index].obj_attr.position as PointType[][];
|
|
|
+
|
|
|
+ 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 },
|
|
|
+ ]];
|
|
|
+ updateAnnots([...annotations]);
|
|
|
+ }
|
|
|
|
|
|
- setElement(element);
|
|
|
+ setRef(null);
|
|
|
+ setUuid('');
|
|
|
+ }, [annotations, uuid]);
|
|
|
|
|
|
- const svg = page.querySelector('svg');
|
|
|
- if (svg && element) {
|
|
|
- svg.appendChild(element);
|
|
|
+ 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;
|
|
|
+ updateAnnots([...annotations]);
|
|
|
}
|
|
|
}
|
|
|
- }, [color, width]);
|
|
|
+ }, [annotations, cursorPosition, uuid]);
|
|
|
+
|
|
|
+ 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 handleMouseUp = (): void => {
|
|
|
- setRef(null);
|
|
|
- setElement(null);
|
|
|
+ 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(() => {
|
|
|
- if (polylineElement) {
|
|
|
- const coordinate = { x: position.x, y: position.y };
|
|
|
- completePath(polylineElement, coordinate);
|
|
|
- }
|
|
|
- }, [polylineElement, position]);
|
|
|
+ const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
|
|
|
|
|
|
- useEffect(() => {
|
|
|
- if (isActive) {
|
|
|
- window.addEventListener('mousedown', handleMouseDown);
|
|
|
- window.addEventListener('mouseup', handleMouseUp);
|
|
|
+ if (isActive && pdfViewer) {
|
|
|
+ subscribeEvent();
|
|
|
}
|
|
|
|
|
|
return (): void => {
|
|
|
- window.removeEventListener('mousedown', handleMouseDown);
|
|
|
- window.removeEventListener('mouseup', handleMouseUp);
|
|
|
+ if (pdfViewer) {
|
|
|
+ unsubscribeEvent();
|
|
|
+ }
|
|
|
};
|
|
|
- }, [isActive]);
|
|
|
+ }, [isActive, handleMouseDown, handleMouseUp]);
|
|
|
|
|
|
return (
|
|
|
<ExpansionPanel
|
|
@@ -84,21 +155,18 @@ const FreehandTools: React.FunctionComponent<Props> = ({
|
|
|
isActive={isActive}
|
|
|
>
|
|
|
<Icon glyph="freehand" style={{ marginRight: '10px' }} />
|
|
|
- Freehand
|
|
|
+ {title}
|
|
|
</Button>
|
|
|
)}
|
|
|
isActive={isActive}
|
|
|
showBottomBorder
|
|
|
>
|
|
|
- <FreehandOption
|
|
|
- type={type}
|
|
|
- setType={setType}
|
|
|
- color={color}
|
|
|
- setColor={setColor}
|
|
|
- opacity={opacity}
|
|
|
- handleOpacity={handleOpacity}
|
|
|
- width={width}
|
|
|
- handleWidth={handleWidth}
|
|
|
+ <InkOption
|
|
|
+ type={data.type}
|
|
|
+ color={data.color}
|
|
|
+ opacity={data.opacity}
|
|
|
+ width={data.width}
|
|
|
+ setDataState={setDataState}
|
|
|
/>
|
|
|
</ExpansionPanel>
|
|
|
);
|