FreehandTool.tsx 5.6 KB

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