ShapeTools.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  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: 'circle',
  30. type: 'border',
  31. color: '#FBB705',
  32. opacity: 100,
  33. width: 3,
  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. ): any => {
  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. event.preventDefault();
  115. switchPdfViewerScrollState('hidden');
  116. const pageEle = (event.target as HTMLElement).parentNode as HTMLElement;
  117. if (pageEle.hasAttribute('data-page-num')) {
  118. addShape(pageEle, event, data);
  119. setRef(pageEle);
  120. }
  121. };
  122. const handleMouseUp = (): void => {
  123. switchPdfViewerScrollState('scroll');
  124. setRef(null);
  125. setUuid('');
  126. setStartPosition({ x: 0, y: 0 });
  127. };
  128. const subscribeEvent = (): void => {
  129. const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
  130. pdfViewer.addEventListener('mousedown', handleMouseDown);
  131. pdfViewer.addEventListener('mouseup', handleMouseUp);
  132. pdfViewer.addEventListener('touchstart', handleMouseDown);
  133. pdfViewer.addEventListener('touchend', handleMouseUp);
  134. };
  135. const unsubscribeEvent = (): void => {
  136. const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
  137. pdfViewer.removeEventListener('mousedown', handleMouseDown);
  138. pdfViewer.removeEventListener('mouseup', handleMouseUp);
  139. pdfViewer.removeEventListener('touchstart', handleMouseDown);
  140. pdfViewer.removeEventListener('touchend', handleMouseUp);
  141. };
  142. useEffect(() => {
  143. const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
  144. if (isActive && pdfViewer) {
  145. subscribeEvent();
  146. }
  147. return (): void => {
  148. if (pdfViewer) {
  149. unsubscribeEvent();
  150. }
  151. };
  152. }, [isActive, addShape]);
  153. const handleUpdate = useCallback(
  154. (start: Record<string, any>, end: Record<string, any>): void => {
  155. const index = annotations.length - 1;
  156. const { x: x1, y: y1 } = start;
  157. const { x: x2, y: y2 } = end;
  158. if (annotations[index] && annotations[index].id === uuid) {
  159. const type = annotations[index].obj_type;
  160. const position = convertPosition(type, x1, y1, x2, y2);
  161. annotations[index].obj_attr.position = parsePositionForBackend(
  162. type,
  163. position,
  164. viewport.height,
  165. scale
  166. );
  167. annotations[index] = appendUserIdAndDate(annotations[index]);
  168. updateAnnots([...annotations]);
  169. }
  170. },
  171. [annotations, viewport, scale, uuid]
  172. );
  173. useEffect(() => {
  174. if (
  175. startPosition.x &&
  176. startPosition.y &&
  177. cursorPosition.x &&
  178. cursorPosition.y
  179. ) {
  180. handleUpdate(startPosition, cursorPosition);
  181. }
  182. }, [startPosition, cursorPosition]);
  183. const Label = (
  184. <Button
  185. shouldFitContainer
  186. align="left"
  187. onClick={onClick}
  188. isActive={isActive}
  189. >
  190. <Icon glyph="shape" style={{ marginRight: '10px' }} />
  191. {title}
  192. </Button>
  193. );
  194. return (
  195. <ExpansionPanel isActive={isActive} label={Label}>
  196. <ShapeOption {...data} setDataState={setDataState} />
  197. </ExpansionPanel>
  198. );
  199. };
  200. export default Shape;