FreehandTool.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import React, { useState, useEffect, useCallback } from 'react';
  2. import { v4 as uuidv4 } from 'uuid';
  3. import { ANNOTATION_TYPE } from '../constants';
  4. import Icon from '../components/Icon';
  5. import Button from '../components/Button';
  6. import ExpansionPanel from '../components/ExpansionPanel';
  7. import InkOption from '../components/InkOption';
  8. import {
  9. svgPath,
  10. bezierCommand,
  11. controlPoint,
  12. line,
  13. } from '../helpers/svgBezierCurve';
  14. import {
  15. parseAnnotationObject,
  16. appendUserIdAndDate,
  17. } from '../helpers/annotation';
  18. import { switchPdfViewerScrollState } from '../helpers/pdf';
  19. import useCursorPosition from '../hooks/useCursorPosition';
  20. import useActions from '../actions';
  21. import useStore from '../store';
  22. type Props = {
  23. title: string;
  24. isActive: boolean;
  25. onClick: () => void;
  26. };
  27. let pathEle: any = null;
  28. const FreehandTool: React.FC<Props> = ({ title, isActive, onClick }: Props) => {
  29. const [cursorPosition, setRef] = useCursorPosition(30);
  30. const [path, setPath] = useState<PointType[]>([]);
  31. const [data, setData] = useState({
  32. page: 0,
  33. type: 'pen',
  34. opacity: 100,
  35. color: '#FF0000',
  36. width: 3,
  37. });
  38. const [{ viewport, scale }, dispatch] = useStore();
  39. const { addAnnots } = useActions(dispatch);
  40. const setDataState = (obj: OptionPropsType): void => {
  41. setData((prev) => ({
  42. ...prev,
  43. ...obj,
  44. }));
  45. };
  46. const handleMouseDown = (event: MouseEvent | TouchEvent): void => {
  47. const pageEle = (event.target as HTMLElement).parentNode as HTMLElement;
  48. switchPdfViewerScrollState('hidden');
  49. if (pageEle.hasAttribute('data-page-num')) {
  50. setRef(pageEle);
  51. const pageNum = pageEle.getAttribute('data-page-num') || '0';
  52. setData((current) => ({
  53. ...current,
  54. page: parseInt(pageNum, 10),
  55. }));
  56. }
  57. };
  58. const handleMouseUp = useCallback((): void => {
  59. switchPdfViewerScrollState('scroll');
  60. const id = uuidv4();
  61. if (path.length) {
  62. const defaultPoints = [
  63. { x: path[0].x - 3, y: path[0].y - 3 },
  64. { x: path[0].x + 3, y: path[0].y + 3 },
  65. ];
  66. const annotData = {
  67. id,
  68. obj_type: ANNOTATION_TYPE.ink,
  69. obj_attr: {
  70. page: data.page as number,
  71. bdcolor: data.color,
  72. bdwidth: data.width,
  73. position: path.length === 1 ? [defaultPoints] : [path],
  74. transparency: data.opacity,
  75. },
  76. };
  77. const freehand = appendUserIdAndDate(
  78. parseAnnotationObject(annotData, viewport.height, scale),
  79. );
  80. addAnnots([freehand]);
  81. setRef(null);
  82. setPath([]);
  83. }
  84. }, [path, data, viewport, scale]);
  85. useEffect(() => {
  86. if (cursorPosition.x && cursorPosition.y) {
  87. const coordinates = {
  88. x: cursorPosition.x,
  89. y: cursorPosition.y,
  90. } as PointType;
  91. setPath((current) => {
  92. return [...current, coordinates];
  93. });
  94. }
  95. }, [cursorPosition]);
  96. /**
  97. * 1. draw to canvas when mouse move
  98. * 2. trigger mouse up to remove path element
  99. */
  100. useEffect(() => {
  101. const pageEle = document.getElementById(`page_${data.page}`);
  102. const canvas = pageEle?.getElementsByClassName('canvas')[0] as HTMLElement;
  103. if (path.length) {
  104. if (pageEle && canvas) {
  105. if (pathEle) {
  106. const d = svgPath(path, bezierCommand(controlPoint(line, 0.2)));
  107. pathEle.setAttribute('d', d);
  108. } else {
  109. pathEle = document.createElementNS(
  110. 'http://www.w3.org/2000/svg',
  111. 'path',
  112. );
  113. pathEle.setAttribute('fill', 'none');
  114. pathEle.setAttribute('stroke', data.color);
  115. pathEle.setAttribute('stroke-width', data.width * scale);
  116. pathEle.setAttribute('stroke-opacity', data.opacity * 0.01);
  117. canvas.appendChild(pathEle);
  118. }
  119. }
  120. } else if (canvas && pathEle) {
  121. canvas.removeChild(pathEle);
  122. pathEle = null;
  123. }
  124. }, [path, data, scale]);
  125. const subscribeEvent = (): void => {
  126. const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
  127. pdfViewer.addEventListener('mousedown', handleMouseDown);
  128. pdfViewer.addEventListener('mouseup', handleMouseUp);
  129. pdfViewer.addEventListener('touchstart', handleMouseDown);
  130. pdfViewer.addEventListener('touchend', handleMouseUp);
  131. };
  132. const unsubscribeEvent = (): void => {
  133. const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
  134. pdfViewer.removeEventListener('mousedown', handleMouseDown);
  135. pdfViewer.removeEventListener('mouseup', handleMouseUp);
  136. pdfViewer.removeEventListener('touchstart', handleMouseDown);
  137. pdfViewer.removeEventListener('touchend', handleMouseUp);
  138. };
  139. useEffect(() => {
  140. const pdfViewer = document.getElementById('pdf_viewer') as HTMLDivElement;
  141. if (pdfViewer) {
  142. if (isActive) {
  143. subscribeEvent();
  144. } else {
  145. unsubscribeEvent();
  146. }
  147. }
  148. return (): void => {
  149. if (pdfViewer) {
  150. unsubscribeEvent();
  151. }
  152. };
  153. }, [isActive, handleMouseUp]);
  154. const Label = (
  155. <Button
  156. shouldFitContainer
  157. align="left"
  158. onClick={onClick}
  159. isActive={isActive}
  160. >
  161. <Icon glyph="freehand" style={{ marginRight: '10px' }} />
  162. {title}
  163. </Button>
  164. );
  165. return (
  166. <ExpansionPanel label={Label} isActive={isActive} showBottomBorder>
  167. <InkOption
  168. type={data.type}
  169. color={data.color}
  170. opacity={data.opacity}
  171. width={data.width}
  172. setDataState={setDataState}
  173. />
  174. </ExpansionPanel>
  175. );
  176. };
  177. export default FreehandTool;