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