ShapeTools.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import React, { useEffect, useState, useCallback } from 'react';
  2. import { v4 as uuidv4 } from 'uuid';
  3. import { ANNOTATION_TYPE } from '../constants';
  4. import Button from '../components/Button';
  5. import ExpansionPanel from '../components/ExpansionPanel';
  6. import Icon from '../components/Icon';
  7. import ShapeOption from '../components/ShapeOption';
  8. import { OptionPropsType } from '../constants/type';
  9. import {
  10. getAbsoluteCoordinate,
  11. parsePositionForBackend,
  12. } from '../helpers/position';
  13. import { parseAnnotationObject } from '../helpers/annotation';
  14. import useCursorPosition from '../hooks/useCursorPosition';
  15. import useActions from '../actions';
  16. import useStore from '../store';
  17. type Props = {
  18. title: string;
  19. isActive: boolean;
  20. onClick: () => void;
  21. };
  22. const Shape: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
  23. const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
  24. const [uuid, setUuid] = useState('');
  25. const [data, setData] = useState({
  26. shape: 'circle',
  27. type: 'border',
  28. color: '#FF1B89',
  29. opacity: 100,
  30. width: 6,
  31. });
  32. const [cursorPosition, setRef] = useCursorPosition();
  33. const [{ viewport, scale, annotations }, dispatch] = useStore();
  34. const { addAnnots, updateAnnots } = useActions(dispatch);
  35. const setDataState = (obj: OptionPropsType): void => {
  36. setData(prev => ({
  37. ...prev,
  38. ...obj,
  39. }));
  40. };
  41. const convertPosition = (
  42. type: string,
  43. x1: number,
  44. y1: number,
  45. x2: number,
  46. y2: number
  47. ): any => {
  48. switch (type) {
  49. case 'Line':
  50. return {
  51. start: {
  52. x: x1,
  53. y: y1,
  54. },
  55. end: {
  56. x: x2,
  57. y: y2,
  58. },
  59. };
  60. default:
  61. return {
  62. top: y2 > y1 ? y1 : y2,
  63. left: x2 > x1 ? x1 : x2,
  64. right: x2 > x1 ? x2 : x1,
  65. bottom: y2 > y1 ? y2 : y1,
  66. };
  67. }
  68. };
  69. const addShape = useCallback(
  70. (
  71. pageEle: HTMLElement,
  72. event: MouseEvent | TouchEvent,
  73. attributes: OptionPropsType
  74. ): void => {
  75. const { shape = '', type, opacity, color, width = 0 } = attributes;
  76. const pageNum = pageEle.getAttribute('data-page-num') || 0;
  77. const coordinate = getAbsoluteCoordinate(pageEle, event);
  78. const id = uuidv4();
  79. setUuid(id);
  80. setStartPosition(coordinate);
  81. const shapeType = ANNOTATION_TYPE[shape];
  82. const position = convertPosition(
  83. shapeType,
  84. coordinate.x - 8,
  85. coordinate.y - 8,
  86. coordinate.x + 8,
  87. coordinate.y + 8
  88. );
  89. const annoteData = {
  90. id,
  91. obj_type: shapeType,
  92. obj_attr: {
  93. page: pageNum as number,
  94. position,
  95. bdcolor: color,
  96. fcolor: type === 'fill' ? color : undefined,
  97. transparency: opacity,
  98. ftransparency: type === 'fill' ? opacity : undefined,
  99. bdwidth: width,
  100. is_arrow: shape === 'arrow',
  101. },
  102. };
  103. const shapeAnnotation = parseAnnotationObject(
  104. annoteData,
  105. viewport.height,
  106. scale
  107. );
  108. addAnnots([shapeAnnotation], true);
  109. },
  110. [viewport, scale, data]
  111. );
  112. const handleMouseDown = (event: MouseEvent | TouchEvent): void => {
  113. // event.preventDefault();
  114. const pageEle = (event.target as HTMLElement).parentNode as HTMLElement;
  115. if (pageEle.hasAttribute('data-page-num')) {
  116. addShape(pageEle, event, data);
  117. setRef(pageEle);
  118. }
  119. };
  120. const handleMouseUp = (): void => {
  121. setRef(null);
  122. setUuid('');
  123. setStartPosition({ x: 0, y: 0 });
  124. };
  125. const subscribeEvent = (): void => {
  126. const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
  127. pdfViewer.addEventListener('mousedown', handleMouseDown);
  128. pdfViewer.addEventListener('mouseup', handleMouseUp);
  129. pdfViewer.addEventListener('touchstart', handleMouseDown);
  130. pdfViewer.addEventListener('touchend', handleMouseUp);
  131. };
  132. const unsubscribeEvent = (): void => {
  133. const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
  134. pdfViewer.removeEventListener('mousedown', handleMouseDown);
  135. pdfViewer.removeEventListener('mouseup', handleMouseUp);
  136. pdfViewer.removeEventListener('touchstart', handleMouseDown);
  137. pdfViewer.removeEventListener('touchend', handleMouseUp);
  138. };
  139. useEffect(() => {
  140. const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
  141. if (isActive && pdfViewer) {
  142. subscribeEvent();
  143. }
  144. return (): void => {
  145. if (pdfViewer) {
  146. unsubscribeEvent();
  147. }
  148. };
  149. }, [isActive, addShape]);
  150. const handleUpdate = useCallback(
  151. (start: Record<string, any>, end: Record<string, any>): void => {
  152. const index = annotations.length - 1;
  153. const { x: x1, y: y1 } = start;
  154. const { x: x2, y: y2 } = end;
  155. if (annotations[index] && annotations[index].id === uuid) {
  156. const type = annotations[index].obj_type;
  157. const position = convertPosition(type, x1, y1, x2, y2);
  158. annotations[index].obj_attr.position = parsePositionForBackend(
  159. type,
  160. position,
  161. viewport.height,
  162. scale
  163. );
  164. updateAnnots([...annotations]);
  165. }
  166. },
  167. [annotations, viewport, scale, uuid]
  168. );
  169. useEffect(() => {
  170. if (
  171. startPosition.x &&
  172. startPosition.y &&
  173. cursorPosition.x &&
  174. cursorPosition.y
  175. ) {
  176. handleUpdate(startPosition, cursorPosition);
  177. }
  178. }, [startPosition, cursorPosition]);
  179. const Label = (
  180. <Button
  181. shouldFitContainer
  182. align="left"
  183. onClick={onClick}
  184. isActive={isActive}
  185. >
  186. <Icon glyph="shape" style={{ marginRight: '10px' }} />
  187. {title}
  188. </Button>
  189. );
  190. return (
  191. <ExpansionPanel isActive={isActive} label={Label}>
  192. <ShapeOption {...data} setDataState={setDataState} />
  193. </ExpansionPanel>
  194. );
  195. };
  196. export default Shape;