ShapeTools.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  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,
  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, coordinate.y, coordinate.x, coordinate.y,
  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): 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. useEffect(() => {
  112. const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
  113. if (isActive && pdfViewer) {
  114. pdfViewer.addEventListener('mousedown', handleMouseDown);
  115. pdfViewer.addEventListener('mouseup', handleMouseUp);
  116. }
  117. return (): void => {
  118. if (pdfViewer) {
  119. pdfViewer.removeEventListener('mousedown', handleMouseDown);
  120. pdfViewer.removeEventListener('mouseup', handleMouseUp);
  121. }
  122. };
  123. }, [isActive, addShape]);
  124. const handleUpdate = useCallback((start: Record<string, any>, end: Record<string, any>): void => {
  125. const index = annotations.length - 1;
  126. const { x: x1, y: y1 } = start;
  127. const { x: x2, y: y2 } = end;
  128. if (annotations[index] && annotations[index].id === uuid) {
  129. const type = annotations[index].obj_type;
  130. const position = convertPosition(type, x1, y1, x2, y2);
  131. annotations[index].obj_attr.position = parsePositionForBackend(
  132. type, position, viewport.height, scale,
  133. );
  134. updateAnnots([...annotations]);
  135. }
  136. }, [annotations, viewport, scale, uuid]);
  137. useEffect(() => {
  138. if (
  139. startPosition.x && startPosition.y
  140. && cursorPosition.x && cursorPosition.y
  141. ) {
  142. handleUpdate(startPosition, cursorPosition);
  143. }
  144. }, [startPosition, cursorPosition]);
  145. return (
  146. <ExpansionPanel
  147. isActive={isActive}
  148. label={(
  149. <Button
  150. shouldFitContainer
  151. align="left"
  152. onClick={onClick}
  153. isActive={isActive}
  154. >
  155. <Icon glyph="shape" style={{ marginRight: '10px' }} />
  156. {title}
  157. </Button>
  158. )}
  159. >
  160. <ShapeOption
  161. {...data}
  162. setDataState={setDataState}
  163. />
  164. </ExpansionPanel>
  165. );
  166. };
  167. export default Shape;