@servicetitan/dte-pdf-editor 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/README.md +356 -0
  2. package/dist/components/data-model-field-type-list.d.ts +11 -0
  3. package/dist/components/data-model-field-type-list.d.ts.map +1 -0
  4. package/dist/components/data-model-field-type-list.js +23 -0
  5. package/dist/components/data-model-field-type-list.js.map +1 -0
  6. package/dist/components/e-sign-field-type-list.d.ts +9 -0
  7. package/dist/components/e-sign-field-type-list.d.ts.map +1 -0
  8. package/dist/components/e-sign-field-type-list.js +12 -0
  9. package/dist/components/e-sign-field-type-list.js.map +1 -0
  10. package/dist/components/field-config-panel-overlay.d.ts +13 -0
  11. package/dist/components/field-config-panel-overlay.d.ts.map +1 -0
  12. package/dist/components/field-config-panel-overlay.js +8 -0
  13. package/dist/components/field-config-panel-overlay.js.map +1 -0
  14. package/dist/components/field-config-panel.d.ts +12 -0
  15. package/dist/components/field-config-panel.d.ts.map +1 -0
  16. package/dist/components/field-config-panel.js +38 -0
  17. package/dist/components/field-config-panel.js.map +1 -0
  18. package/dist/components/field-sidebar.d.ts +10 -0
  19. package/dist/components/field-sidebar.d.ts.map +1 -0
  20. package/dist/components/field-sidebar.js +25 -0
  21. package/dist/components/field-sidebar.js.map +1 -0
  22. package/dist/components/field-type.d.ts +9 -0
  23. package/dist/components/field-type.d.ts.map +1 -0
  24. package/dist/components/field-type.js +12 -0
  25. package/dist/components/field-type.js.map +1 -0
  26. package/dist/components/fillable-field-type-list.d.ts +10 -0
  27. package/dist/components/fillable-field-type-list.d.ts.map +1 -0
  28. package/dist/components/fillable-field-type-list.js +17 -0
  29. package/dist/components/fillable-field-type-list.js.map +1 -0
  30. package/dist/components/pdf-canvas.d.ts +22 -0
  31. package/dist/components/pdf-canvas.d.ts.map +1 -0
  32. package/dist/components/pdf-canvas.js +14 -0
  33. package/dist/components/pdf-canvas.js.map +1 -0
  34. package/dist/components/pdf-document-renderer.d.ts +16 -0
  35. package/dist/components/pdf-document-renderer.d.ts.map +1 -0
  36. package/dist/components/pdf-document-renderer.js +28 -0
  37. package/dist/components/pdf-document-renderer.js.map +1 -0
  38. package/dist/components/pdf-editor.d.ts +21 -0
  39. package/dist/components/pdf-editor.d.ts.map +1 -0
  40. package/dist/components/pdf-editor.js +108 -0
  41. package/dist/components/pdf-editor.js.map +1 -0
  42. package/dist/components/pdf-field-overlay.d.ts +14 -0
  43. package/dist/components/pdf-field-overlay.d.ts.map +1 -0
  44. package/dist/components/pdf-field-overlay.js +32 -0
  45. package/dist/components/pdf-field-overlay.js.map +1 -0
  46. package/dist/constants/field.constants.d.ts +16 -0
  47. package/dist/constants/field.constants.d.ts.map +1 -0
  48. package/dist/constants/field.constants.js +28 -0
  49. package/dist/constants/field.constants.js.map +1 -0
  50. package/dist/constants/index.d.ts +3 -0
  51. package/dist/constants/index.d.ts.map +1 -0
  52. package/dist/constants/index.js +3 -0
  53. package/dist/constants/index.js.map +1 -0
  54. package/dist/constants/pdf-editor.constants.d.ts +2 -0
  55. package/dist/constants/pdf-editor.constants.d.ts.map +1 -0
  56. package/dist/constants/pdf-editor.constants.js +2 -0
  57. package/dist/constants/pdf-editor.constants.js.map +1 -0
  58. package/dist/hooks/index.d.ts +4 -0
  59. package/dist/hooks/index.d.ts.map +1 -0
  60. package/dist/hooks/index.js +4 -0
  61. package/dist/hooks/index.js.map +1 -0
  62. package/dist/hooks/useFieldDrag.d.ts +24 -0
  63. package/dist/hooks/useFieldDrag.d.ts.map +1 -0
  64. package/dist/hooks/useFieldDrag.js +34 -0
  65. package/dist/hooks/useFieldDrag.js.map +1 -0
  66. package/dist/hooks/useFieldResize.d.ts +17 -0
  67. package/dist/hooks/useFieldResize.d.ts.map +1 -0
  68. package/dist/hooks/useFieldResize.js +58 -0
  69. package/dist/hooks/useFieldResize.js.map +1 -0
  70. package/dist/hooks/usePdfDocumentRenderer.d.ts +9 -0
  71. package/dist/hooks/usePdfDocumentRenderer.d.ts.map +1 -0
  72. package/dist/hooks/usePdfDocumentRenderer.js +13 -0
  73. package/dist/hooks/usePdfDocumentRenderer.js.map +1 -0
  74. package/dist/index.d.ts +3 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/dist/index.js +2 -0
  77. package/dist/index.js.map +1 -0
  78. package/dist/interface/pdf-editor.d.ts +64 -0
  79. package/dist/interface/pdf-editor.d.ts.map +1 -0
  80. package/dist/interface/pdf-editor.js +14 -0
  81. package/dist/interface/pdf-editor.js.map +1 -0
  82. package/dist/utils/calculate-drop-coordinates.utils.d.ts +15 -0
  83. package/dist/utils/calculate-drop-coordinates.utils.d.ts.map +1 -0
  84. package/dist/utils/calculate-drop-coordinates.utils.js +48 -0
  85. package/dist/utils/calculate-drop-coordinates.utils.js.map +1 -0
  86. package/dist/utils/extract-grouped-fields-from-data-model.utils.d.ts +7 -0
  87. package/dist/utils/extract-grouped-fields-from-data-model.utils.d.ts.map +1 -0
  88. package/dist/utils/extract-grouped-fields-from-data-model.utils.js +57 -0
  89. package/dist/utils/extract-grouped-fields-from-data-model.utils.js.map +1 -0
  90. package/dist/utils/generate-e-sign-path.d.ts +3 -0
  91. package/dist/utils/generate-e-sign-path.d.ts.map +1 -0
  92. package/dist/utils/generate-e-sign-path.js +4 -0
  93. package/dist/utils/generate-e-sign-path.js.map +1 -0
  94. package/dist/utils/get-page-dimensions.utils.d.ts +12 -0
  95. package/dist/utils/get-page-dimensions.utils.d.ts.map +1 -0
  96. package/dist/utils/get-page-dimensions.utils.js +31 -0
  97. package/dist/utils/get-page-dimensions.utils.js.map +1 -0
  98. package/dist/utils/get-page-number-from-client-y.utils.d.ts +9 -0
  99. package/dist/utils/get-page-number-from-client-y.utils.d.ts.map +1 -0
  100. package/dist/utils/get-page-number-from-client-y.utils.js +24 -0
  101. package/dist/utils/get-page-number-from-client-y.utils.js.map +1 -0
  102. package/dist/utils/get-page-position.utils.d.ts +12 -0
  103. package/dist/utils/get-page-position.utils.d.ts.map +1 -0
  104. package/dist/utils/get-page-position.utils.js +22 -0
  105. package/dist/utils/get-page-position.utils.js.map +1 -0
  106. package/dist/utils/handle-field-drag-start.utils.d.ts +16 -0
  107. package/dist/utils/handle-field-drag-start.utils.d.ts.map +1 -0
  108. package/dist/utils/handle-field-drag-start.utils.js +41 -0
  109. package/dist/utils/handle-field-drag-start.utils.js.map +1 -0
  110. package/dist/utils/handle-field-drag.utils.d.ts +19 -0
  111. package/dist/utils/handle-field-drag.utils.d.ts.map +1 -0
  112. package/dist/utils/handle-field-drag.utils.js +36 -0
  113. package/dist/utils/handle-field-drag.utils.js.map +1 -0
  114. package/dist/utils/handle-field-resize.utils.d.ts +35 -0
  115. package/dist/utils/handle-field-resize.utils.d.ts.map +1 -0
  116. package/dist/utils/handle-field-resize.utils.js +66 -0
  117. package/dist/utils/handle-field-resize.utils.js.map +1 -0
  118. package/dist/utils/index.d.ts +13 -0
  119. package/dist/utils/index.d.ts.map +1 -0
  120. package/dist/utils/index.js +13 -0
  121. package/dist/utils/index.js.map +1 -0
  122. package/dist/utils/is-drag-over-canvas.utils.d.ts +9 -0
  123. package/dist/utils/is-drag-over-canvas.utils.d.ts.map +1 -0
  124. package/dist/utils/is-drag-over-canvas.utils.js +26 -0
  125. package/dist/utils/is-drag-over-canvas.utils.js.map +1 -0
  126. package/dist/utils/map-colors-to-recipients.d.ts +3 -0
  127. package/dist/utils/map-colors-to-recipients.d.ts.map +1 -0
  128. package/dist/utils/map-colors-to-recipients.js +35 -0
  129. package/dist/utils/map-colors-to-recipients.js.map +1 -0
  130. package/dist/utils/pdfjs-init.d.ts +6 -0
  131. package/dist/utils/pdfjs-init.d.ts.map +1 -0
  132. package/dist/utils/pdfjs-init.js +25 -0
  133. package/dist/utils/pdfjs-init.js.map +1 -0
  134. package/package.json +28 -0
  135. package/src/components/data-model-field-type-list.tsx +58 -0
  136. package/src/components/e-sign-field-type-list.tsx +27 -0
  137. package/src/components/field-config-panel-overlay.tsx +51 -0
  138. package/src/components/field-config-panel.tsx +142 -0
  139. package/src/components/field-sidebar.tsx +93 -0
  140. package/src/components/field-type.tsx +28 -0
  141. package/src/components/fillable-field-type-list.tsx +42 -0
  142. package/src/components/pdf-canvas.tsx +81 -0
  143. package/src/components/pdf-document-renderer.tsx +78 -0
  144. package/src/components/pdf-editor.tsx +216 -0
  145. package/src/components/pdf-field-overlay.tsx +83 -0
  146. package/src/constants/field.constants.ts +31 -0
  147. package/src/constants/index.ts +2 -0
  148. package/src/constants/pdf-editor.constants.ts +1 -0
  149. package/src/hooks/index.ts +3 -0
  150. package/src/hooks/useFieldDrag.ts +56 -0
  151. package/src/hooks/useFieldResize.ts +95 -0
  152. package/src/hooks/usePdfDocumentRenderer.ts +21 -0
  153. package/src/index.ts +2 -0
  154. package/src/interface/pdf-editor.ts +74 -0
  155. package/src/styles/field-config-panel-overlay.css +33 -0
  156. package/src/styles/field-sidebar.css +31 -0
  157. package/src/styles/field-type-list.css +8 -0
  158. package/src/styles/field-type.css +10 -0
  159. package/src/styles/generic.css +3 -0
  160. package/src/styles/index.css +10 -0
  161. package/src/styles/pdf-canvas.css +9 -0
  162. package/src/styles/pdf-document-renderer.css +4 -0
  163. package/src/styles/pdf-editor.css +14 -0
  164. package/src/styles/pdf-field-overlay.css +54 -0
  165. package/src/styles/variables.css +26 -0
  166. package/src/utils/calculate-drop-coordinates.utils.ts +68 -0
  167. package/src/utils/extract-grouped-fields-from-data-model.utils.ts +73 -0
  168. package/src/utils/generate-e-sign-path.ts +5 -0
  169. package/src/utils/get-page-dimensions.utils.ts +39 -0
  170. package/src/utils/get-page-number-from-client-y.utils.ts +31 -0
  171. package/src/utils/get-page-position.utils.ts +30 -0
  172. package/src/utils/handle-field-drag-start.utils.ts +52 -0
  173. package/src/utils/handle-field-drag.utils.ts +55 -0
  174. package/src/utils/handle-field-resize.utils.ts +102 -0
  175. package/src/utils/index.ts +12 -0
  176. package/src/utils/is-drag-over-canvas.utils.ts +35 -0
  177. package/src/utils/map-colors-to-recipients.ts +37 -0
  178. package/src/utils/pdfjs-init.ts +27 -0
  179. package/src/vite-env.d.ts +16 -0
@@ -0,0 +1,28 @@
1
+ import { Icon, Text } from '@servicetitan/anvil2';
2
+ import IconDragIndicator from '@servicetitan/anvil2/assets/icons/material/round/drag_indicator.svg';
3
+ import { FC } from 'react';
4
+
5
+ interface FieldTypeProps {
6
+ label: string;
7
+ onDragEnd(): void;
8
+ onDragStart(): void;
9
+ }
10
+
11
+ export const FieldType: FC<FieldTypeProps> = ({ label, onDragEnd, onDragStart }) => {
12
+ return (
13
+ <div
14
+ draggable
15
+ onDragStart={e => {
16
+ onDragStart();
17
+ // Mark that we're dragging a new field type, not an existing field
18
+ e.dataTransfer.effectAllowed = 'copy';
19
+ e.dataTransfer.setData('text/plain', 'new-field-type');
20
+ }}
21
+ onDragEnd={onDragEnd}
22
+ className="dte-field-type-item border m-t-1"
23
+ >
24
+ <Icon svg={IconDragIndicator} size="medium" />
25
+ <Text size="small">{label}</Text>
26
+ </div>
27
+ );
28
+ };
@@ -0,0 +1,42 @@
1
+ import { FC, Fragment, useMemo } from 'react';
2
+ import { FILLABLE_FIELD_TYPES } from '../constants';
3
+ import { FieldTypeOption } from '../interface/pdf-editor';
4
+ import { FieldType } from './field-type';
5
+
6
+ interface FillableFieldTypeListProps {
7
+ searchText?: string;
8
+ onDragStart(fieldOption: FieldTypeOption): void;
9
+ onDragEnd(): void;
10
+ }
11
+
12
+ export const FillableFieldTypeList: FC<FillableFieldTypeListProps> = ({
13
+ onDragEnd,
14
+ onDragStart,
15
+ searchText,
16
+ }) => {
17
+ const searchedFieldTypes = useMemo(() => {
18
+ if (!searchText) {
19
+ return FILLABLE_FIELD_TYPES;
20
+ }
21
+
22
+ const searchLower = searchText.toLowerCase();
23
+ return FILLABLE_FIELD_TYPES.filter(({ label }) =>
24
+ label.toLowerCase().includes(searchLower),
25
+ );
26
+ }, [searchText]);
27
+
28
+ return (
29
+ <Fragment>
30
+ {searchedFieldTypes.map(fieldOption => {
31
+ return (
32
+ <FieldType
33
+ key={fieldOption.subType}
34
+ label={fieldOption.label}
35
+ onDragEnd={onDragEnd}
36
+ onDragStart={() => onDragStart(fieldOption)}
37
+ />
38
+ );
39
+ })}
40
+ </Fragment>
41
+ );
42
+ };
@@ -0,0 +1,81 @@
1
+ import { Flex } from '@servicetitan/anvil2';
2
+ import { DragEvent, FC, MouseEvent, ReactNode, RefObject, useState } from 'react';
3
+ import { BASE_PAGE_WIDTH } from '../constants';
4
+ import { PdfField } from '../interface/pdf-editor';
5
+ import { PdfDocumentRenderer } from './pdf-document-renderer';
6
+ import { PdfFieldOverlay } from './pdf-field-overlay';
7
+
8
+ interface PdfCanvasProps {
9
+ pdfUrl: string;
10
+ errorPlaceholder?: ReactNode;
11
+ loadingPlaceholder?: ReactNode;
12
+ fields: PdfField[];
13
+ loading: boolean;
14
+ selectedField: string | null;
15
+ pdfContainerRef: RefObject<HTMLDivElement>;
16
+ pdfWrapperRef: RefObject<HTMLDivElement>;
17
+ recipientsColors: Record<string, string>;
18
+ onDragOver(e: DragEvent<HTMLDivElement>): void;
19
+ onDrop(e: DragEvent<HTMLDivElement>, pageNumber: number): void;
20
+ onFieldClick(fieldId: string, e: MouseEvent): void;
21
+ onFieldMove(fieldId: string, newX: number, newY: number, pageNumber: number): void;
22
+ onFieldResize(fieldId: string, newWidth: number, newHeight: number, pageNumber: number): void;
23
+ onDeselectField(): void;
24
+ }
25
+
26
+ export const PdfCanvas: FC<PdfCanvasProps> = ({
27
+ errorPlaceholder,
28
+ fields,
29
+ loading,
30
+ loadingPlaceholder,
31
+ onDeselectField,
32
+ onDragOver,
33
+ onDrop,
34
+ onFieldClick,
35
+ onFieldMove,
36
+ onFieldResize,
37
+ pdfContainerRef,
38
+ pdfUrl,
39
+ pdfWrapperRef,
40
+ recipientsColors,
41
+ selectedField,
42
+ }) => {
43
+ const [isPdfLoaded, setIsPdfLoaded] = useState<boolean>(false);
44
+
45
+ const onDocumentLoadSuccess = (_: number) => {
46
+ setIsPdfLoaded(true);
47
+ };
48
+
49
+ return (
50
+ <Flex
51
+ flex={1}
52
+ ref={pdfContainerRef}
53
+ className="dte-pdf-canvas-container"
54
+ onClick={onDeselectField}
55
+ >
56
+ <div ref={pdfWrapperRef} className="dte-pdf-wrapper">
57
+ <PdfDocumentRenderer
58
+ pdfUrl={pdfUrl}
59
+ onDrop={onDrop}
60
+ loading={loading}
61
+ onDragOver={onDragOver}
62
+ pageWidth={BASE_PAGE_WIDTH}
63
+ errorPlaceholder={errorPlaceholder}
64
+ loadingPlaceholder={loadingPlaceholder}
65
+ onDocumentLoadSuccess={onDocumentLoadSuccess}
66
+ />
67
+ {isPdfLoaded && fields && (
68
+ <PdfFieldOverlay
69
+ fields={fields}
70
+ pdfWrapperRef={pdfWrapperRef}
71
+ recipientsColors={recipientsColors}
72
+ selectedField={selectedField ?? null}
73
+ onFieldClick={onFieldClick ?? (() => {})}
74
+ onFieldMove={onFieldMove ?? (() => {})}
75
+ onFieldResize={onFieldResize ?? (() => {})}
76
+ />
77
+ )}
78
+ </div>
79
+ </Flex>
80
+ );
81
+ };
@@ -0,0 +1,78 @@
1
+ import { DragEvent, FC, ReactNode } from 'react';
2
+ import { Document, Page } from 'react-pdf';
3
+ import 'react-pdf/dist/Page/AnnotationLayer.css';
4
+ import 'react-pdf/dist/Page/TextLayer.css';
5
+ import { usePdfDocumentRenderer } from '../hooks';
6
+ import { initializePdfJsWorker } from '../utils';
7
+
8
+ initializePdfJsWorker();
9
+
10
+ interface PdfDocumentRendererProps {
11
+ pdfUrl: string;
12
+ pageWidth?: number;
13
+ loading: boolean;
14
+ errorPlaceholder?: ReactNode;
15
+ loadingPlaceholder?: ReactNode;
16
+ onDocumentLoadSuccess?: (numPages: number) => void;
17
+ onDragOver?(e: DragEvent<HTMLDivElement>): void;
18
+ onDrop?(e: DragEvent<HTMLDivElement>, pageNumber: number): void;
19
+ }
20
+
21
+ export const PdfDocumentRenderer: FC<PdfDocumentRendererProps> = ({
22
+ errorPlaceholder,
23
+ loading,
24
+ loadingPlaceholder,
25
+ onDocumentLoadSuccess,
26
+ onDragOver,
27
+ onDrop,
28
+ pageWidth,
29
+ pdfUrl,
30
+ }) => {
31
+ const { handleDocumentLoadSuccess: handleLoadSuccess, pages } = usePdfDocumentRenderer();
32
+
33
+ const handleDocumentLoadSuccess = ({ numPages }: { numPages: number }) => {
34
+ handleLoadSuccess({ numPages });
35
+ onDocumentLoadSuccess?.(numPages);
36
+ };
37
+
38
+ const handlePageDrop = (e: DragEvent<HTMLDivElement>, pageNumber: number) => {
39
+ e.preventDefault();
40
+ e.stopPropagation();
41
+ onDrop?.(e, pageNumber);
42
+ };
43
+
44
+ const handlePageDragOver = (e: DragEvent<HTMLDivElement>) => {
45
+ e.preventDefault();
46
+ e.stopPropagation();
47
+ onDragOver?.(e);
48
+ };
49
+
50
+ return (
51
+ <Document
52
+ file={pdfUrl}
53
+ error={errorPlaceholder}
54
+ loading={loadingPlaceholder}
55
+ onLoadSuccess={handleDocumentLoadSuccess}
56
+ noData={loading ? loadingPlaceholder : undefined}
57
+ >
58
+ {pages.map(pageNumber => {
59
+ return (
60
+ <div
61
+ key={`page_${pageNumber}`}
62
+ data-page-number={pageNumber}
63
+ className="dte-pdf-document-renderer-page"
64
+ onDragOver={handlePageDragOver}
65
+ onDrop={e => handlePageDrop(e, pageNumber)}
66
+ >
67
+ <Page
68
+ pageNumber={pageNumber}
69
+ width={pageWidth}
70
+ renderTextLayer={false}
71
+ renderAnnotationLayer={false}
72
+ />
73
+ </div>
74
+ );
75
+ })}
76
+ </Document>
77
+ );
78
+ };
@@ -0,0 +1,216 @@
1
+ import { Flex } from '@servicetitan/anvil2';
2
+ import { DragEvent, FC, MouseEvent, ReactNode, useMemo, useRef, useState } from 'react';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+ import { FIELD_CONSTANTS } from '../constants';
5
+
6
+ import {
7
+ ESignFieldType,
8
+ FieldTypeEnum,
9
+ FieldTypeOption,
10
+ PdfField,
11
+ SchemaObject,
12
+ } from '../interface/pdf-editor';
13
+ import '../styles/index.css';
14
+ import {
15
+ calculateDropCoordinates,
16
+ extractGroupedFieldsFromDataModel,
17
+ generateESignPath,
18
+ initializePdfJsWorker,
19
+ isDragOverCanvas,
20
+ mapColorsToRecipients,
21
+ } from '../utils';
22
+ import { FieldConfigPanelOverlay } from './field-config-panel-overlay';
23
+ import { FieldSidebar } from './field-sidebar';
24
+ import { PdfCanvas } from './pdf-canvas';
25
+
26
+ initializePdfJsWorker();
27
+
28
+ export interface RecipientInfo {
29
+ id: number;
30
+ name: string;
31
+ displayName: string;
32
+ }
33
+
34
+ interface PdfEditorProps {
35
+ pdfUrl: string;
36
+ loading?: boolean;
37
+ loadingPlaceholder?: ReactNode;
38
+ errorPlaceholder?: ReactNode;
39
+ dataModel?: SchemaObject;
40
+ recipients?: RecipientInfo[];
41
+ fields?: PdfField[];
42
+ onFieldsChange(fields: PdfField[]): void;
43
+ }
44
+
45
+ export const PdfEditor: FC<PdfEditorProps> = ({
46
+ dataModel,
47
+ errorPlaceholder,
48
+ fields = [],
49
+ loading = false,
50
+ loadingPlaceholder,
51
+ onFieldsChange,
52
+ pdfUrl,
53
+ recipients = [],
54
+ }) => {
55
+ const dataModelGroups = useMemo(() => {
56
+ return dataModel ? extractGroupedFieldsFromDataModel(dataModel) : [];
57
+ }, [dataModel]);
58
+ const [draggedFieldOption, setDraggedFieldOption] = useState<FieldTypeOption | null>(null);
59
+ const [selectedField, setSelectedField] = useState<string | null>(null);
60
+ const pdfContainerRef = useRef<HTMLDivElement>(null);
61
+ const pdfWrapperRef = useRef<HTMLDivElement>(null);
62
+
63
+ const handleDragStart = (fieldOption: FieldTypeOption) => {
64
+ setDraggedFieldOption(fieldOption);
65
+ };
66
+
67
+ const handleDragEnd = () => {
68
+ setDraggedFieldOption(null);
69
+ };
70
+
71
+ const handleDrop = (e: DragEvent<HTMLDivElement>, pageNumber: number) => {
72
+ e.preventDefault();
73
+ e.stopPropagation();
74
+
75
+ const isExistingField = e.dataTransfer.getData('text/plain') === 'existing-field';
76
+ if (isExistingField || !draggedFieldOption) {
77
+ return;
78
+ }
79
+
80
+ const coordinates = calculateDropCoordinates(e, pageNumber, pdfWrapperRef);
81
+ if (!coordinates) {
82
+ setDraggedFieldOption(null);
83
+ return;
84
+ }
85
+
86
+ const newField: PdfField = {
87
+ id: uuidv4(),
88
+ type: draggedFieldOption.type,
89
+ subType: draggedFieldOption.subType,
90
+ x: coordinates.x,
91
+ y: coordinates.y,
92
+ page: pageNumber,
93
+ label: draggedFieldOption.label,
94
+ width: FIELD_CONSTANTS.defaultWidth,
95
+ height: FIELD_CONSTANTS.defaultHeight,
96
+ path: draggedFieldOption.path,
97
+ };
98
+
99
+ if (draggedFieldOption.type === FieldTypeEnum.eSign) {
100
+ newField.recipient = recipients[0]?.name ?? '';
101
+ newField.path = generateESignPath(
102
+ recipients[0].name!,
103
+ draggedFieldOption.subType as ESignFieldType,
104
+ );
105
+ }
106
+ if (draggedFieldOption.type === FieldTypeEnum.fillable) {
107
+ newField.recipient = recipients[0]?.name ?? '';
108
+ newField.required = false;
109
+ }
110
+
111
+ onFieldsChange([...fields, newField]);
112
+ setSelectedField(newField.id);
113
+ setDraggedFieldOption(null);
114
+ };
115
+
116
+ const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
117
+ if (!draggedFieldOption) {
118
+ e.dataTransfer.dropEffect = 'none';
119
+ return;
120
+ }
121
+
122
+ const isOverCanvas = isDragOverCanvas(e, pdfWrapperRef);
123
+
124
+ // Only allow a drop if we're over the actual PDF canvas
125
+ if (isOverCanvas) {
126
+ e.preventDefault();
127
+ e.stopPropagation();
128
+ e.dataTransfer.dropEffect = 'copy';
129
+ } else {
130
+ e.dataTransfer.dropEffect = 'none';
131
+ }
132
+ };
133
+
134
+ const handleFieldClick = (fieldId: string, e: MouseEvent) => {
135
+ e.stopPropagation();
136
+ setSelectedField(fieldId);
137
+ };
138
+
139
+ const handleFieldMove = (fieldId: string, newX: number, newY: number, pageNumber: number) => {
140
+ onFieldsChange(
141
+ fields.map(f => (f.id === fieldId ? { ...f, x: newX, y: newY, page: pageNumber } : f)),
142
+ );
143
+ };
144
+
145
+ const handleFieldResize = (
146
+ fieldId: string,
147
+ newWidth: number,
148
+ newHeight: number,
149
+ pageNumber: number,
150
+ ) => {
151
+ onFieldsChange(
152
+ fields.map(f =>
153
+ f.id === fieldId
154
+ ? { ...f, width: newWidth, height: newHeight, page: pageNumber }
155
+ : f,
156
+ ),
157
+ );
158
+ };
159
+
160
+ const handleDeleteField = () => {
161
+ if (selectedField) {
162
+ onFieldsChange(fields.filter(f => f.id !== selectedField));
163
+ setSelectedField(null);
164
+ }
165
+ };
166
+
167
+ const handleFieldConfigChange = (updates: Partial<PdfField>) => {
168
+ if (selectedField) {
169
+ onFieldsChange(fields.map(f => (f.id === selectedField ? { ...f, ...updates } : f)));
170
+ }
171
+ };
172
+
173
+ const selectedFieldData = fields.find(f => f.id === selectedField) ?? null;
174
+
175
+ const recipientsColors = mapColorsToRecipients(recipients);
176
+
177
+ return (
178
+ <Flex flex={1} className={`dte-pdf-editor ${loading ? 'skeleton' : ''}`}>
179
+ <Flex className="dte-pdf-editor-sidebar-container ">
180
+ <FieldSidebar
181
+ dataModelGroups={dataModelGroups}
182
+ onDragEnd={handleDragEnd}
183
+ onDragStart={handleDragStart}
184
+ />
185
+ {selectedFieldData && (
186
+ <FieldConfigPanelOverlay
187
+ selectedField={selectedFieldData}
188
+ onFieldConfigChange={handleFieldConfigChange}
189
+ onDeleteField={handleDeleteField}
190
+ recipients={recipients}
191
+ onDeselectField={() => setSelectedField(null)}
192
+ />
193
+ )}
194
+ </Flex>
195
+ <Flex gap={12} flex={1} className="dte-pdf-editor-content-container skeleton-item">
196
+ <PdfCanvas
197
+ pdfUrl={pdfUrl}
198
+ fields={fields}
199
+ loading={loading}
200
+ recipientsColors={recipientsColors}
201
+ selectedField={selectedField}
202
+ pdfContainerRef={pdfContainerRef}
203
+ pdfWrapperRef={pdfWrapperRef}
204
+ errorPlaceholder={errorPlaceholder}
205
+ loadingPlaceholder={loadingPlaceholder}
206
+ onDrop={handleDrop}
207
+ onDragOver={handleDragOver}
208
+ onFieldClick={handleFieldClick}
209
+ onFieldMove={handleFieldMove}
210
+ onFieldResize={handleFieldResize}
211
+ onDeselectField={() => setSelectedField(null)}
212
+ />
213
+ </Flex>
214
+ </Flex>
215
+ );
216
+ };
@@ -0,0 +1,83 @@
1
+ import { FC, MouseEvent, RefObject } from 'react';
2
+ import { useFieldDrag, useFieldResize } from '../hooks';
3
+ import { FieldTypeEnum, PdfField } from '../interface/pdf-editor';
4
+ import { getPagePosition } from '../utils';
5
+
6
+ interface PdfFieldOverlayProps {
7
+ fields: PdfField[];
8
+ selectedField: string | null;
9
+ pdfWrapperRef: RefObject<HTMLDivElement>;
10
+ recipientsColors: Record<string, string>;
11
+ onFieldClick(fieldId: string, e: MouseEvent): void;
12
+ onFieldMove(fieldId: string, newX: number, newY: number, pageNumber: number): void;
13
+ onFieldResize(fieldId: string, newWidth: number, newHeight: number, pageNumber: number): void;
14
+ }
15
+
16
+ export const PdfFieldOverlay: FC<PdfFieldOverlayProps> = ({
17
+ fields,
18
+ onFieldClick,
19
+ onFieldMove,
20
+ onFieldResize,
21
+ pdfWrapperRef,
22
+ recipientsColors,
23
+ selectedField,
24
+ }) => {
25
+ const { draggingFieldIdRef, handleDrag, handleDragEnd, handleDragStart } = useFieldDrag({
26
+ pdfWrapperRef,
27
+ onFieldMove,
28
+ });
29
+
30
+ const { handleResizeStart } = useFieldResize({
31
+ pdfWrapperRef,
32
+ onFieldResize,
33
+ });
34
+
35
+ return (
36
+ <div className="dte-pdf-field-overlay skeleton-item">
37
+ {fields.map(field => {
38
+ const isSelected = selectedField === field.id;
39
+ const isDragging = draggingFieldIdRef.current === field.id;
40
+ const pagePos = getPagePosition(field.page, pdfWrapperRef);
41
+ const bgColor =
42
+ field.recipient && recipientsColors[field.recipient]
43
+ ? recipientsColors[field.recipient]
44
+ : 'rgba(255, 255, 255, 0.8)';
45
+
46
+ return (
47
+ <div
48
+ key={field.id}
49
+ onClick={e => onFieldClick(field.id, e)}
50
+ draggable
51
+ onDragStart={e => handleDragStart(e, field)}
52
+ onDrag={e => handleDrag(e, field)}
53
+ onDragEnd={handleDragEnd}
54
+ className={`dte-pdf-field ${isSelected ? 'dte-pdf-field-selected' : 'dte-pdf-field-unselected'} ${isDragging ? 'dte-pdf-field-dragging' : ''}`}
55
+ style={{
56
+ left: `${pagePos.left + field.x}px`,
57
+ top: `${pagePos.top + field.y}px`,
58
+ width: `${field.width}px`,
59
+ height: `${field.height}px`,
60
+ backgroundColor: bgColor,
61
+ }}
62
+ >
63
+ <span>
64
+ {(field.type === FieldTypeEnum.eSign ||
65
+ field.type === FieldTypeEnum.fillable) &&
66
+ `${field.label} ${field.required ? '*' : ''}`}
67
+
68
+ {field.path && (
69
+ <div className="dte-pdf-field-path">{`{{${field.path}}}`}</div>
70
+ )}
71
+ </span>
72
+ {isSelected && (
73
+ <div
74
+ className="dte-pdf-field-resize-handle"
75
+ onMouseDown={e => handleResizeStart(e, field)}
76
+ />
77
+ )}
78
+ </div>
79
+ );
80
+ })}
81
+ </div>
82
+ );
83
+ };
@@ -0,0 +1,31 @@
1
+ import { ESignFieldType, FieldTypeEnum, FieldTypeOption } from '../interface/pdf-editor';
2
+
3
+ export const FIELD_CONSTANTS = {
4
+ defaultWidth: 200,
5
+ defaultHeight: 30,
6
+ minWidth: 50,
7
+ minHeight: 20,
8
+ dropOffsetX: 75,
9
+ dropOffsetY: 15,
10
+ } as const;
11
+
12
+ export const E_SIGN_FIELD_TYPE_OPTIONS: { name: string; id: ESignFieldType }[] = [
13
+ { name: 'Signature', id: ESignFieldType.signature },
14
+ { name: 'Initials', id: ESignFieldType.initials },
15
+ { name: 'Date Signed', id: ESignFieldType.dateSigned },
16
+ { name: 'Full Name', id: ESignFieldType.fullName },
17
+ ];
18
+
19
+ export const FILLABLE_FIELD_TYPES: FieldTypeOption[] = [
20
+ { label: 'Text Field', type: FieldTypeEnum.fillable, subType: 'text' },
21
+ { label: 'Date Field', type: FieldTypeEnum.fillable, subType: 'date' },
22
+ { label: 'Checkbox', type: FieldTypeEnum.fillable, subType: 'checkbox' },
23
+ ];
24
+
25
+ export const E_SIGN_FIELD_TYPES: FieldTypeOption[] = [
26
+ {
27
+ label: 'E-Sign',
28
+ type: FieldTypeEnum.eSign,
29
+ subType: ESignFieldType.signature,
30
+ },
31
+ ];
@@ -0,0 +1,2 @@
1
+ export * from './field.constants';
2
+ export * from './pdf-editor.constants';
@@ -0,0 +1 @@
1
+ export const BASE_PAGE_WIDTH = 980;
@@ -0,0 +1,3 @@
1
+ export * from './useFieldDrag';
2
+ export * from './useFieldResize';
3
+ export * from './usePdfDocumentRenderer';
@@ -0,0 +1,56 @@
1
+ import { DragEvent, MutableRefObject, RefObject, useRef } from 'react';
2
+ import { PdfField } from '../interface/pdf-editor';
3
+ import { handleFieldDrag, handleFieldDragStart } from '../utils';
4
+
5
+ interface UseFieldDragOptions {
6
+ pdfWrapperRef: RefObject<HTMLDivElement>;
7
+ onFieldMove: (fieldId: string, newX: number, newY: number, pageNumber: number) => void;
8
+ }
9
+
10
+ interface UseFieldDragReturn {
11
+ dragOffsetRef: MutableRefObject<{ x: number; y: number; page: number } | null>;
12
+ draggingFieldIdRef: MutableRefObject<string | null>;
13
+ handleDragStart: (e: DragEvent<HTMLDivElement>, field: PdfField) => void;
14
+ handleDrag: (e: DragEvent<HTMLDivElement>, field: PdfField) => void;
15
+ handleDragEnd: () => void;
16
+ }
17
+
18
+ /**
19
+ * Custom hook for handling field drag operations
20
+ * Manages drag state and coordinates field movement
21
+ */
22
+ export const useFieldDrag = ({
23
+ onFieldMove,
24
+ pdfWrapperRef,
25
+ }: UseFieldDragOptions): UseFieldDragReturn => {
26
+ const dragOffsetRef = useRef<{ x: number; y: number; page: number } | null>(null);
27
+ const draggingFieldIdRef = useRef<string | null>(null);
28
+
29
+ const handleDragStart = (e: DragEvent<HTMLDivElement>, field: PdfField) => {
30
+ draggingFieldIdRef.current = field.id;
31
+
32
+ const dragOffset = handleFieldDragStart(e, field, pdfWrapperRef);
33
+ if (dragOffset) {
34
+ dragOffsetRef.current = dragOffset;
35
+ }
36
+ };
37
+
38
+ const handleDrag = (e: DragEvent<HTMLDivElement>, field: PdfField) => {
39
+ if (dragOffsetRef.current) {
40
+ handleFieldDrag(e, field, dragOffsetRef.current, pdfWrapperRef, onFieldMove);
41
+ }
42
+ };
43
+
44
+ const handleDragEnd = () => {
45
+ dragOffsetRef.current = null;
46
+ draggingFieldIdRef.current = null;
47
+ };
48
+
49
+ return {
50
+ dragOffsetRef,
51
+ draggingFieldIdRef,
52
+ handleDragStart,
53
+ handleDrag,
54
+ handleDragEnd,
55
+ };
56
+ };