ShapeTools.tsx 5.6 KB

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