ShapeTools.tsx 6.0 KB

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