@servicetitan/dte-pdf-editor 1.77.0 → 1.79.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 (95) hide show
  1. package/dist/components/pdf-canvas/canvas-controls.d.ts +24 -0
  2. package/dist/components/pdf-canvas/canvas-controls.d.ts.map +1 -0
  3. package/dist/components/pdf-canvas/canvas-controls.js +22 -0
  4. package/dist/components/pdf-canvas/canvas-controls.js.map +1 -0
  5. package/dist/components/pdf-canvas/pdf-canvas.d.ts +1 -0
  6. package/dist/components/pdf-canvas/pdf-canvas.d.ts.map +1 -1
  7. package/dist/components/pdf-canvas/pdf-canvas.js +6 -3
  8. package/dist/components/pdf-canvas/pdf-canvas.js.map +1 -1
  9. package/dist/components/pdf-editor/pdf-editor.d.ts +1 -0
  10. package/dist/components/pdf-editor/pdf-editor.d.ts.map +1 -1
  11. package/dist/components/pdf-editor/pdf-editor.js +2 -2
  12. package/dist/components/pdf-editor/pdf-editor.js.map +1 -1
  13. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.d.ts +1 -0
  14. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.d.ts.map +1 -1
  15. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js +2 -2
  16. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js.map +1 -1
  17. package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts +1 -0
  18. package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts.map +1 -1
  19. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js +4 -1
  20. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js.map +1 -1
  21. package/dist/components/pdf-view/pdf-view-field-container.d.ts +1 -0
  22. package/dist/components/pdf-view/pdf-view-field-container.d.ts.map +1 -1
  23. package/dist/components/pdf-view/pdf-view-field-container.js +5 -4
  24. package/dist/components/pdf-view/pdf-view-field-container.js.map +1 -1
  25. package/dist/components/pdf-view/pdf-view.d.ts +2 -0
  26. package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
  27. package/dist/components/pdf-view/pdf-view.js +32 -5
  28. package/dist/components/pdf-view/pdf-view.js.map +1 -1
  29. package/dist/constants/pdf-editor.constants.d.ts +4 -0
  30. package/dist/constants/pdf-editor.constants.d.ts.map +1 -1
  31. package/dist/constants/pdf-editor.constants.js +4 -0
  32. package/dist/constants/pdf-editor.constants.js.map +1 -1
  33. package/dist/hooks/index.d.ts +1 -0
  34. package/dist/hooks/index.d.ts.map +1 -1
  35. package/dist/hooks/index.js +1 -0
  36. package/dist/hooks/index.js.map +1 -1
  37. package/dist/hooks/useFieldDrag.d.ts +2 -1
  38. package/dist/hooks/useFieldDrag.d.ts.map +1 -1
  39. package/dist/hooks/useFieldDrag.js +7 -6
  40. package/dist/hooks/useFieldDrag.js.map +1 -1
  41. package/dist/hooks/useFieldResize.d.ts +2 -1
  42. package/dist/hooks/useFieldResize.d.ts.map +1 -1
  43. package/dist/hooks/useFieldResize.js +7 -3
  44. package/dist/hooks/useFieldResize.js.map +1 -1
  45. package/dist/hooks/useZoom.d.ts +11 -0
  46. package/dist/hooks/useZoom.d.ts.map +1 -0
  47. package/dist/hooks/useZoom.js +14 -0
  48. package/dist/hooks/useZoom.js.map +1 -0
  49. package/dist/styles/pdf-canvas.css +24 -5
  50. package/dist/styles/pdf-editor.css +1 -0
  51. package/dist/styles/pdf-field-overlay.css +14 -0
  52. package/dist/styles/variables.css +10 -1
  53. package/dist/utils/field/field-drag.utils.d.ts +4 -2
  54. package/dist/utils/field/field-drag.utils.d.ts.map +1 -1
  55. package/dist/utils/field/field-drag.utils.js +14 -10
  56. package/dist/utils/field/field-drag.utils.js.map +1 -1
  57. package/dist/utils/field/field-reading-order.utils.d.ts +15 -0
  58. package/dist/utils/field/field-reading-order.utils.d.ts.map +1 -0
  59. package/dist/utils/field/field-reading-order.utils.js +33 -0
  60. package/dist/utils/field/field-reading-order.utils.js.map +1 -0
  61. package/dist/utils/field/field-resize.utils.d.ts +4 -2
  62. package/dist/utils/field/field-resize.utils.d.ts.map +1 -1
  63. package/dist/utils/field/field-resize.utils.js +12 -10
  64. package/dist/utils/field/field-resize.utils.js.map +1 -1
  65. package/dist/utils/field/get-field-bounds-style.utils.d.ts +7 -0
  66. package/dist/utils/field/get-field-bounds-style.utils.d.ts.map +1 -1
  67. package/dist/utils/field/get-field-bounds-style.utils.js +5 -5
  68. package/dist/utils/field/get-field-bounds-style.utils.js.map +1 -1
  69. package/dist/utils/field/index.d.ts +1 -0
  70. package/dist/utils/field/index.d.ts.map +1 -1
  71. package/dist/utils/field/index.js +1 -0
  72. package/dist/utils/field/index.js.map +1 -1
  73. package/package.json +1 -1
  74. package/src/components/pdf-canvas/canvas-controls.tsx +183 -0
  75. package/src/components/pdf-canvas/pdf-canvas.tsx +64 -33
  76. package/src/components/pdf-editor/pdf-editor.tsx +12 -2
  77. package/src/components/pdf-fields-overlay/pdf-fields-overlay.tsx +3 -0
  78. package/src/components/pdf-fields-overlay/pdf-overlay-field.tsx +5 -0
  79. package/src/components/pdf-view/pdf-view-field-container.tsx +6 -2
  80. package/src/components/pdf-view/pdf-view.tsx +85 -5
  81. package/src/constants/pdf-editor.constants.ts +5 -0
  82. package/src/hooks/index.ts +1 -0
  83. package/src/hooks/useFieldDrag.ts +10 -5
  84. package/src/hooks/useFieldResize.ts +16 -2
  85. package/src/hooks/useZoom.ts +25 -0
  86. package/src/styles/pdf-canvas.css +24 -5
  87. package/src/styles/pdf-editor.css +1 -0
  88. package/src/styles/pdf-field-overlay.css +14 -0
  89. package/src/styles/variables.css +10 -1
  90. package/src/utils/field/__tests__/field-reading-order.utils.test.ts +62 -0
  91. package/src/utils/field/field-drag.utils.ts +14 -8
  92. package/src/utils/field/field-reading-order.utils.ts +43 -0
  93. package/src/utils/field/field-resize.utils.ts +12 -8
  94. package/src/utils/field/get-field-bounds-style.utils.ts +17 -5
  95. package/src/utils/field/index.ts +1 -0
@@ -0,0 +1,183 @@
1
+ import { Button, Divider, Flex, Icon, Text } from '@servicetitan/anvil2';
2
+ import IconNavigateDown from '@servicetitan/anvil2/assets/icons/material/round/arrow_downward.svg';
3
+ import IconNavigateUp from '@servicetitan/anvil2/assets/icons/material/round/arrow_upward.svg';
4
+ import IconNavigateBefore from '@servicetitan/anvil2/assets/icons/material/round/navigate_before.svg';
5
+ import IconNavigateNext from '@servicetitan/anvil2/assets/icons/material/round/navigate_next.svg';
6
+ import IconZoomIn from '@servicetitan/anvil2/assets/icons/material/round/zoom_in.svg';
7
+ import IconZoomOut from '@servicetitan/anvil2/assets/icons/material/round/zoom_out.svg';
8
+ import { FC } from 'react';
9
+ import { ZOOM_MAX, ZOOM_MIN } from '../../constants';
10
+
11
+ interface ZoomControlsProps {
12
+ zoom: number;
13
+ onZoomIn(): void;
14
+ onZoomOut(): void;
15
+ onResetZoom(): void;
16
+ }
17
+
18
+ const ZoomControls: FC<ZoomControlsProps> = ({ onResetZoom, onZoomIn, onZoomOut, zoom }) => {
19
+ return (
20
+ <Flex alignItems="center" gap={1} className="dte-pdf-zoom-controls">
21
+ <Button
22
+ size="xsmall"
23
+ appearance="ghost"
24
+ onClick={onZoomOut}
25
+ disabled={zoom <= ZOOM_MIN}
26
+ aria-label="Zoom out"
27
+ title="Zoom out"
28
+ >
29
+ <Icon svg={IconZoomOut} size="medium" />
30
+ </Button>
31
+
32
+ <Text
33
+ variant="body"
34
+ size="small"
35
+ className="dte-pdf-zoom-controls-value"
36
+ onClick={onResetZoom}
37
+ title="Reset zoom"
38
+ >
39
+ {Math.round(zoom * 100)}%
40
+ </Text>
41
+
42
+ <Button
43
+ size="xsmall"
44
+ appearance="ghost"
45
+ onClick={onZoomIn}
46
+ disabled={zoom >= ZOOM_MAX}
47
+ aria-label="Zoom in"
48
+ title="Zoom in"
49
+ >
50
+ <Icon svg={IconZoomIn} size="medium" />
51
+ </Button>
52
+ </Flex>
53
+ );
54
+ };
55
+
56
+ interface FieldNavigationControlsProps {
57
+ disabledNext: boolean;
58
+ disabledOnFirstField: boolean;
59
+ disabledOnLastField: boolean;
60
+ disabledPrev: boolean;
61
+ onNextField(): void;
62
+ onPrevField(): void;
63
+ onFirstField(): void;
64
+ onLastField(): void;
65
+ }
66
+
67
+ const FieldNavigationControls: FC<FieldNavigationControlsProps> = ({
68
+ disabledNext,
69
+ disabledOnFirstField,
70
+ disabledOnLastField,
71
+ disabledPrev,
72
+ onFirstField,
73
+ onLastField,
74
+ onNextField,
75
+ onPrevField,
76
+ }) => {
77
+ return (
78
+ <Flex alignItems="center" gap="2" className="dte-pdf-field-navigation-controls">
79
+ <Button
80
+ size="xsmall"
81
+ appearance="ghost"
82
+ onClick={onPrevField}
83
+ disabled={disabledPrev}
84
+ aria-label="Go to previous field"
85
+ title="Go to previous field"
86
+ >
87
+ <Icon svg={IconNavigateBefore} size="medium" />
88
+ </Button>
89
+ <Button
90
+ size="xsmall"
91
+ appearance="ghost"
92
+ onClick={onNextField}
93
+ disabled={disabledNext}
94
+ aria-label="Go to next field"
95
+ title="Go to next field"
96
+ >
97
+ <Icon svg={IconNavigateNext} size="medium" />
98
+ </Button>
99
+ <Divider vertical style={{ height: '100%' }} />
100
+ <Button
101
+ size="xsmall"
102
+ appearance="ghost"
103
+ onClick={onFirstField}
104
+ disabled={disabledOnFirstField}
105
+ aria-label="Go to first field"
106
+ title="Go to first field"
107
+ >
108
+ <Icon svg={IconNavigateUp} size="medium" />
109
+ </Button>
110
+ <Button
111
+ size="xsmall"
112
+ appearance="ghost"
113
+ onClick={onLastField}
114
+ disabled={disabledOnLastField}
115
+ aria-label="Go to last field"
116
+ title="Go to last field"
117
+ >
118
+ <Icon svg={IconNavigateDown} size="medium" />
119
+ </Button>
120
+ </Flex>
121
+ );
122
+ };
123
+
124
+ export interface CanvasControlsProps extends ZoomControlsProps {
125
+ /** Show the zoom in/out/reset controls. */
126
+ zoomable?: boolean;
127
+ /** Show the previous/next fillable-field navigation controls. */
128
+ enableInputNavigation?: boolean;
129
+ disabledNext?: boolean;
130
+ disabledPrev?: boolean;
131
+ disabledOnFirstField?: boolean;
132
+ disabledOnLastField?: boolean;
133
+ onNextField?(): void;
134
+ onPrevField?(): void;
135
+ onFirstField?(): void;
136
+ onLastField?(): void;
137
+ }
138
+
139
+ export const CanvasControls: FC<CanvasControlsProps> = ({
140
+ disabledNext = false,
141
+ disabledOnFirstField = false,
142
+ disabledOnLastField = false,
143
+ disabledPrev = false,
144
+ enableInputNavigation = false,
145
+ onFirstField = () => {},
146
+ onLastField = () => {},
147
+ onNextField = () => {},
148
+ onPrevField = () => {},
149
+ onResetZoom,
150
+ onZoomIn,
151
+ onZoomOut,
152
+ zoom,
153
+ zoomable = false,
154
+ }) => {
155
+ if (!zoomable && !enableInputNavigation) {
156
+ return null;
157
+ }
158
+
159
+ return (
160
+ <Flex justifyContent="center" gap={2} className="dte-pdf-canvas-controls">
161
+ {zoomable && (
162
+ <ZoomControls
163
+ zoom={zoom}
164
+ onZoomIn={onZoomIn}
165
+ onZoomOut={onZoomOut}
166
+ onResetZoom={onResetZoom}
167
+ />
168
+ )}
169
+ {enableInputNavigation && (
170
+ <FieldNavigationControls
171
+ disabledOnFirstField={disabledOnFirstField}
172
+ disabledOnLastField={disabledOnLastField}
173
+ onFirstField={onFirstField}
174
+ onLastField={onLastField}
175
+ disabledNext={disabledNext}
176
+ disabledPrev={disabledPrev}
177
+ onNextField={onNextField}
178
+ onPrevField={onPrevField}
179
+ />
180
+ )}
181
+ </Flex>
182
+ );
183
+ };
@@ -1,12 +1,24 @@
1
1
  import { Flex } from '@servicetitan/anvil2';
2
- import { DragEvent, FC, MouseEvent, ReactNode, RefObject, useEffect, useState } from 'react';
2
+ import {
3
+ DragEvent,
4
+ FC,
5
+ Fragment,
6
+ MouseEvent,
7
+ ReactNode,
8
+ RefObject,
9
+ useEffect,
10
+ useState,
11
+ } from 'react';
3
12
  import { BASE_PAGE_WIDTH } from '../../constants';
13
+ import { useZoom } from '../../hooks';
4
14
  import { type PdfField } from '../../interface/types';
5
15
  import { PdfFieldsOverlay } from '../pdf-fields-overlay/pdf-fields-overlay';
16
+ import { CanvasControls } from './canvas-controls';
6
17
  import { PdfDocumentRenderer } from './pdf-document-renderer';
7
18
 
8
19
  interface PdfCanvasProps {
9
20
  pdfUrl: string;
21
+ zoomable: boolean;
10
22
  errorPlaceholder?: ReactNode;
11
23
  loadingPlaceholder?: ReactNode;
12
24
  fields: PdfField[];
@@ -49,8 +61,10 @@ export const PdfCanvas: FC<PdfCanvasProps> = ({
49
61
  pdfWrapperRef,
50
62
  recipientsColors,
51
63
  selectedField,
64
+ zoomable,
52
65
  }) => {
53
66
  const [isPdfLoaded, setIsPdfLoaded] = useState<boolean>(false);
67
+ const { resetZoom, zoom, zoomIn, zoomOut } = useZoom();
54
68
 
55
69
  const onDocumentLoadSuccess = (pageNum: number) => {
56
70
  setIsPdfLoaded(true);
@@ -62,39 +76,56 @@ export const PdfCanvas: FC<PdfCanvasProps> = ({
62
76
  }, [pdfUrl]);
63
77
 
64
78
  return (
65
- <Flex
66
- flex={1}
67
- ref={pdfContainerRef}
68
- className="dte-pdf-canvas-container"
69
- onClick={onDeselectField}
70
- >
71
- <div ref={pdfWrapperRef} className="dte-pdf-wrapper" style={{ width: BASE_PAGE_WIDTH }}>
72
- <PdfDocumentRenderer
73
- pdfUrl={pdfUrl}
74
- onDrop={onDrop}
75
- loading={loading}
76
- onDragOver={onDragOver}
77
- pageWidth={BASE_PAGE_WIDTH}
78
- errorPlaceholder={errorPlaceholder}
79
- loadingPlaceholder={loadingPlaceholder}
80
- onDocumentLoadSuccess={onDocumentLoadSuccess}
79
+ <Fragment>
80
+ {isPdfLoaded && (
81
+ <CanvasControls
82
+ enableInputNavigation={false}
83
+ zoomable={zoomable}
84
+ zoom={zoom}
85
+ onZoomIn={zoomIn}
86
+ onZoomOut={zoomOut}
87
+ onResetZoom={resetZoom}
81
88
  />
82
- {isPdfLoaded && (
83
- <PdfFieldsOverlay
84
- fields={fields}
85
- errors={errors}
86
- onFieldConfigChange={onFieldConfigChange}
87
- handleAddNewField={handleAddNewField}
88
- pdfWrapperRef={pdfWrapperRef}
89
- recipientsColors={recipientsColors}
90
- selectedField={selectedField ?? null}
91
- onFieldClick={onFieldClick}
92
- onFieldMove={onFieldMove}
93
- onFieldResize={onFieldResize}
94
- fieldWrapperClassNames={fieldWrapperClassNames}
89
+ )}
90
+ <Flex
91
+ flex={1}
92
+ ref={pdfContainerRef}
93
+ className="dte-pdf-canvas-container"
94
+ onClick={onDeselectField}
95
+ >
96
+ <div
97
+ ref={pdfWrapperRef}
98
+ className="dte-pdf-wrapper"
99
+ style={{ width: BASE_PAGE_WIDTH * zoom }}
100
+ >
101
+ <PdfDocumentRenderer
102
+ pdfUrl={pdfUrl}
103
+ onDrop={onDrop}
104
+ loading={loading}
105
+ onDragOver={onDragOver}
106
+ pageWidth={BASE_PAGE_WIDTH * zoom}
107
+ errorPlaceholder={errorPlaceholder}
108
+ loadingPlaceholder={loadingPlaceholder}
109
+ onDocumentLoadSuccess={onDocumentLoadSuccess}
95
110
  />
96
- )}
97
- </div>
98
- </Flex>
111
+ {isPdfLoaded && (
112
+ <PdfFieldsOverlay
113
+ fields={fields}
114
+ errors={errors}
115
+ onFieldConfigChange={onFieldConfigChange}
116
+ handleAddNewField={handleAddNewField}
117
+ pdfWrapperRef={pdfWrapperRef}
118
+ recipientsColors={recipientsColors}
119
+ selectedField={selectedField ?? null}
120
+ onFieldClick={onFieldClick}
121
+ onFieldMove={onFieldMove}
122
+ onFieldResize={onFieldResize}
123
+ fieldWrapperClassNames={fieldWrapperClassNames}
124
+ zoom={zoom}
125
+ />
126
+ )}
127
+ </div>
128
+ </Flex>
129
+ </Fragment>
99
130
  );
100
131
  };
@@ -19,6 +19,7 @@ import '../../styles/index.css';
19
19
  export interface PdfEditorProps {
20
20
  pdfUrl: string;
21
21
  loading?: boolean;
22
+ zoomable?: boolean;
22
23
  errors?: Record<string, string>;
23
24
  loadingPlaceholder?: ReactNode;
24
25
  errorPlaceholder?: ReactNode;
@@ -51,6 +52,7 @@ export const PdfEditor: FC<PdfEditorProps> = ({
51
52
  onLoadSuccess,
52
53
  pdfUrl,
53
54
  recipients = [],
55
+ zoomable = true,
54
56
  }) => {
55
57
  const pdfContainerRef = useRef<HTMLDivElement>(null);
56
58
  const pdfWrapperRef = useRef<HTMLDivElement>(null);
@@ -77,7 +79,10 @@ export const PdfEditor: FC<PdfEditorProps> = ({
77
79
  const recipientsColors = mapColorsToRecipients(recipients);
78
80
 
79
81
  return (
80
- <Flex flex={1} className={`dte-pdf-editor ${loading ? 'skeleton' : ''}`}>
82
+ <Flex
83
+ flex={1}
84
+ className={`dte-pdf-editor-theme dte-pdf-editor ${loading ? 'skeleton' : ''}`}
85
+ >
81
86
  {pdfFieldSelection.selectedField && (
82
87
  <FieldConfigPanelOverlay
83
88
  dataModel={dataModel}
@@ -110,8 +115,13 @@ export const PdfEditor: FC<PdfEditorProps> = ({
110
115
  />
111
116
  </Flex>
112
117
 
113
- <Flex gap={12} flex={1} className="dte-pdf-editor-content-container skeleton-item">
118
+ <Flex
119
+ direction="column"
120
+ flex={1}
121
+ className="dte-pdf-editor-content-container skeleton-item"
122
+ >
114
123
  <PdfCanvas
124
+ zoomable={zoomable}
115
125
  pdfUrl={pdfUrl}
116
126
  fields={fields}
117
127
  loading={loading}
@@ -9,6 +9,7 @@ interface PdfFieldsOverlayProps {
9
9
  pdfWrapperRef: RefObject<HTMLDivElement>;
10
10
  recipientsColors: Record<string, string>;
11
11
  fieldWrapperClassNames?: string;
12
+ zoom?: number;
12
13
  onFieldConfigChange(updates: Partial<PdfField>): void;
13
14
  handleAddNewField(field: PdfField): void;
14
15
  onFieldClick(fieldId: string, e: MouseEvent): void;
@@ -28,6 +29,7 @@ export const PdfFieldsOverlay: FC<PdfFieldsOverlayProps> = ({
28
29
  pdfWrapperRef,
29
30
  recipientsColors,
30
31
  selectedField,
32
+ zoom = 1,
31
33
  }) => {
32
34
  const pathToLabel = useMemo(() => {
33
35
  const map = new Map<string, string>();
@@ -57,6 +59,7 @@ export const PdfFieldsOverlay: FC<PdfFieldsOverlayProps> = ({
57
59
  recipientsColors={recipientsColors}
58
60
  isSameGroup={selectedField?.path === field.path}
59
61
  pathToLabel={pathToLabel}
62
+ zoom={zoom}
60
63
  />
61
64
  ))}
62
65
  </div>
@@ -19,6 +19,7 @@ interface PdfOverlayFieldProps {
19
19
  pdfWrapperRef: RefObject<HTMLDivElement>;
20
20
  pathToLabel?: Map<string, string>;
21
21
  fieldWrapperClassNames?: string;
22
+ zoom?: number;
22
23
  onFieldConfigChange(updates: Partial<PdfField>): void;
23
24
  handleAddNewField(field: PdfField): void;
24
25
  onFieldClick(fieldId: string, e: MouseEvent): void;
@@ -40,16 +41,19 @@ export const PdfOverlayField: FC<PdfOverlayFieldProps> = ({
40
41
  pathToLabel,
41
42
  pdfWrapperRef,
42
43
  recipientsColors,
44
+ zoom = 1,
43
45
  }) => {
44
46
  const { dragVisualPosition, handleDrag, handleDragEnd, handleDragStart } = useFieldDrag({
45
47
  pdfWrapperRef,
46
48
  onFieldMove,
49
+ zoom,
47
50
  });
48
51
  const isDragging = dragVisualPosition !== null;
49
52
 
50
53
  const { handleResizeStart } = useFieldResize({
51
54
  pdfWrapperRef,
52
55
  onFieldResize,
56
+ zoom,
53
57
  });
54
58
 
55
59
  const page = dragVisualPosition?.page ?? field.page;
@@ -65,6 +69,7 @@ export const PdfOverlayField: FC<PdfOverlayFieldProps> = ({
65
69
  {
66
70
  pageOffset: pagePos,
67
71
  backgroundColor: getFieldBackgroundColor(field.recipient, recipientsColors),
72
+ zoom,
68
73
  },
69
74
  );
70
75
 
@@ -13,6 +13,7 @@ interface PdfViewFieldContainer {
13
13
  error?: string;
14
14
  field: PdfField;
15
15
  recipientsColors: Record<string, string>;
16
+ zoom?: number;
16
17
  }
17
18
 
18
19
  export const PdfViewFieldContainer: FC<PropsWithChildren<PdfViewFieldContainer>> = ({
@@ -22,6 +23,7 @@ export const PdfViewFieldContainer: FC<PropsWithChildren<PdfViewFieldContainer>>
22
23
  field,
23
24
  pdfWrapperRef,
24
25
  recipientsColors,
26
+ zoom = 1,
25
27
  }) => {
26
28
  const visible =
27
29
  !field.displayCondition || evaluateDisplayCondition(field.displayCondition, data);
@@ -35,11 +37,13 @@ export const PdfViewFieldContainer: FC<PropsWithChildren<PdfViewFieldContainer>>
35
37
  return (
36
38
  <Fragment>
37
39
  <div
40
+ id={`dte-pdf-field-${field.id}`}
38
41
  className="dte-pdf-field-view"
39
42
  style={getFieldBoundsStyle(field, {
40
43
  pageOffset: pagePos,
41
44
  backgroundColor: bgColor,
42
45
  borderColor: error ? 'red' : undefined,
46
+ zoom,
43
47
  })}
44
48
  >
45
49
  {children}
@@ -48,8 +52,8 @@ export const PdfViewFieldContainer: FC<PropsWithChildren<PdfViewFieldContainer>>
48
52
  <span
49
53
  style={{
50
54
  color: 'red',
51
- top: pagePos.top + field.y + field.height + 8,
52
- left: pagePos.left + field.x,
55
+ top: pagePos.top + (field.y + field.height) * zoom + 8,
56
+ left: pagePos.left + field.x * zoom,
53
57
  minWidth: '100px',
54
58
  position: 'absolute',
55
59
  }}
@@ -1,6 +1,7 @@
1
1
  import { Flex } from '@servicetitan/anvil2';
2
- import { FC, ReactNode, useRef, useState } from 'react';
2
+ import { FC, ReactNode, useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { BASE_PAGE_WIDTH } from '../../constants';
4
+ import { useZoom } from '../../hooks';
4
5
  import {
5
6
  type DataChangePayload,
6
7
  type DataModelValues,
@@ -9,7 +10,12 @@ import {
9
10
  type PdfViewMode,
10
11
  type RecipientInfo,
11
12
  } from '../../interface/types';
12
- import { mapColorsToRecipients } from '../../utils';
13
+ import {
14
+ evaluateDisplayCondition,
15
+ mapColorsToRecipients,
16
+ sortFieldsByReadingOrder,
17
+ } from '../../utils';
18
+ import { CanvasControls } from '../pdf-canvas/canvas-controls';
13
19
  import { PdfDocumentRenderer } from '../pdf-canvas/pdf-document-renderer';
14
20
  import { PdfViewCalculated } from './pdf-view-calculated';
15
21
  import { PdfViewDataModel } from './pdf-view-data-model';
@@ -23,6 +29,8 @@ export interface PdfViewProps {
23
29
  errors?: Record<string, string>;
24
30
  viewMode?: PdfViewMode;
25
31
  data?: DataModelValues;
32
+ zoomable?: boolean;
33
+ enableInputNavigation?: boolean;
26
34
  pdfUrl: string;
27
35
  loading?: boolean;
28
36
  /**
@@ -52,6 +60,7 @@ export interface PdfViewProps {
52
60
 
53
61
  export const PdfView: FC<PdfViewProps> = ({
54
62
  data,
63
+ enableInputNavigation = true,
55
64
  errorPlaceholder,
56
65
  errors = {},
57
66
  fields,
@@ -65,9 +74,11 @@ export const PdfView: FC<PdfViewProps> = ({
65
74
  pdfUrl,
66
75
  recipients,
67
76
  viewMode,
77
+ zoomable = true,
68
78
  }) => {
69
79
  const [isPdfLoaded, setIsPdfLoaded] = useState<boolean>(false);
70
80
  const [previewData, setPreviewData] = useState<DataModelValues>(data ?? {});
81
+ const { resetZoom, zoom, zoomIn, zoomOut } = useZoom();
71
82
 
72
83
  const pdfWrapperRef = useRef<HTMLDivElement>(null);
73
84
 
@@ -86,14 +97,82 @@ export const PdfView: FC<PdfViewProps> = ({
86
97
  onDataChange?.(data, field);
87
98
  };
88
99
 
100
+ const activeData = viewMode === 'preview' ? previewData : data;
101
+
102
+ const navigableFields = useMemo(
103
+ () =>
104
+ sortFieldsByReadingOrder(
105
+ fields
106
+ .filter(
107
+ field =>
108
+ field.type === FieldTypeEnum.fillable &&
109
+ field.recipient &&
110
+ fillingBy?.includes(field.recipient),
111
+ )
112
+ .filter(
113
+ field =>
114
+ !field.displayCondition ||
115
+ evaluateDisplayCondition(field.displayCondition, activeData),
116
+ ),
117
+ ),
118
+ [fields, activeData, fillingBy],
119
+ );
120
+
121
+ // -1 means no field has been navigated to yet, so the first "next" lands on index 0.
122
+ const [currentFieldIndex, setCurrentFieldIndex] = useState(-1);
123
+
124
+ useEffect(() => {
125
+ setCurrentFieldIndex(index => Math.min(index, navigableFields.length - 1));
126
+ }, [navigableFields.length]);
127
+
128
+ const goToField = (index: number) => {
129
+ const field = navigableFields[index];
130
+ if (!field) {
131
+ return;
132
+ }
133
+
134
+ setCurrentFieldIndex(index);
135
+
136
+ const root = pdfWrapperRef.current?.getRootNode() as Document | ShadowRoot | undefined;
137
+ const fieldEl = root?.querySelector<HTMLElement>(`[id="dte-pdf-field-${field.id}"]`);
138
+ fieldEl?.scrollIntoView({ behavior: 'smooth', block: 'center' });
139
+ fieldEl?.querySelector<HTMLElement>('input, textarea')?.focus();
140
+ };
141
+
89
142
  const recipientsColors = mapColorsToRecipients(recipients);
90
143
  return (
91
- <Flex flex={1} className="dte-pdf-view-container" style={{ maxWidth: BASE_PAGE_WIDTH }}>
92
- <div ref={pdfWrapperRef} className="dte-pdf-wrapper">
144
+ <Flex
145
+ flex={1}
146
+ className="dte-pdf-editor-theme dte-pdf-editor-content-container"
147
+ direction="column"
148
+ >
149
+ {isPdfLoaded && (
150
+ <CanvasControls
151
+ zoom={zoom}
152
+ onZoomIn={zoomIn}
153
+ onZoomOut={zoomOut}
154
+ zoomable={zoomable}
155
+ onResetZoom={resetZoom}
156
+ enableInputNavigation={enableInputNavigation}
157
+ disabledPrev={currentFieldIndex <= 0}
158
+ disabledOnFirstField={currentFieldIndex === 0}
159
+ disabledNext={currentFieldIndex >= navigableFields.length - 1}
160
+ disabledOnLastField={currentFieldIndex === navigableFields.length - 1}
161
+ onPrevField={() => goToField(currentFieldIndex - 1)}
162
+ onNextField={() => goToField(currentFieldIndex + 1)}
163
+ onFirstField={() => goToField(0)}
164
+ onLastField={() => goToField(navigableFields.length - 1)}
165
+ />
166
+ )}
167
+ <div
168
+ ref={pdfWrapperRef}
169
+ className="dte-pdf-wrapper"
170
+ style={{ width: BASE_PAGE_WIDTH * zoom }}
171
+ >
93
172
  <PdfDocumentRenderer
94
173
  pdfUrl={pdfUrl}
95
174
  loading={loading}
96
- pageWidth={BASE_PAGE_WIDTH}
175
+ pageWidth={BASE_PAGE_WIDTH * zoom}
97
176
  errorPlaceholder={errorPlaceholder}
98
177
  loadingPlaceholder={loadingPlaceholder}
99
178
  onDocumentLoadSuccess={onDocumentLoadSuccess}
@@ -108,6 +187,7 @@ export const PdfView: FC<PdfViewProps> = ({
108
187
  error={errors[field.path!] || ''}
109
188
  recipientsColors={recipientsColors}
110
189
  pdfWrapperRef={pdfWrapperRef}
190
+ zoom={zoom}
111
191
  >
112
192
  {(field.type === FieldTypeEnum.dataModel ||
113
193
  field.type === FieldTypeEnum.forms) && (
@@ -1 +1,6 @@
1
1
  export const BASE_PAGE_WIDTH = 980;
2
+
3
+ export const ZOOM_MIN = 0.4;
4
+ export const ZOOM_MAX = 2;
5
+ export const ZOOM_STEP = 0.1;
6
+ export const ZOOM_DEFAULT = 1;
@@ -7,3 +7,4 @@ export * from './usePdfDocumentRenderer';
7
7
  export * from './usePdfFieldDnD';
8
8
  export * from './usePdfFieldSelection';
9
9
  export * from './useToggle';
10
+ export * from './useZoom';
@@ -16,6 +16,7 @@ export interface DragVisualPosition {
16
16
  interface UseFieldDragOptions {
17
17
  pdfWrapperRef: RefObject<HTMLDivElement>;
18
18
  onFieldMove: (fieldId: string, newX: number, newY: number, pageNumber: number) => void;
19
+ zoom?: number;
19
20
  }
20
21
 
21
22
  interface UseFieldDragReturn {
@@ -41,6 +42,7 @@ interface PendingMove {
41
42
  export const useFieldDrag = ({
42
43
  onFieldMove,
43
44
  pdfWrapperRef,
45
+ zoom = 1,
44
46
  }: UseFieldDragOptions): UseFieldDragReturn => {
45
47
  const dragOffsetRef = useRef<{ x: number; y: number; page: number } | null>(null);
46
48
  const draggingFieldIdRef = useRef<string | null>(null);
@@ -68,7 +70,7 @@ export const useFieldDrag = ({
68
70
  }
69
71
  setDragVisualPosition({ x: field.x, y: field.y, page: field.page });
70
72
 
71
- const dragOffset = handleFieldDragStart(e, field, pdfWrapperRef);
73
+ const dragOffset = handleFieldDragStart(e, field, pdfWrapperRef, zoom);
72
74
  if (dragOffset) {
73
75
  dragOffsetRef.current = dragOffset;
74
76
  }
@@ -87,13 +89,16 @@ export const useFieldDrag = ({
87
89
  const wrapperRect = pdfWrapperRef.current.getBoundingClientRect();
88
90
  const pageDimensions = getPageDimensions(currentPage, pdfWrapperRef);
89
91
 
90
- const x = e.clientX - wrapperRect.left - currentPagePos.left - dragOffsetRef.current.x;
91
- const y = e.clientY - wrapperRect.top - currentPagePos.top - dragOffsetRef.current.y;
92
+ // Positions in unzoomed reference coordinates, matching stored field.x/y
93
+ const x =
94
+ (e.clientX - wrapperRect.left - currentPagePos.left) / zoom - dragOffsetRef.current.x;
95
+ const y =
96
+ (e.clientY - wrapperRect.top - currentPagePos.top) / zoom - dragOffsetRef.current.y;
92
97
 
93
98
  const minX = 0;
94
99
  const minY = 0;
95
- const maxX = Math.max(0, pageDimensions.width - field.width);
96
- const maxY = Math.max(0, pageDimensions.height - field.height);
100
+ const maxX = Math.max(0, pageDimensions.width / zoom - field.width);
101
+ const maxY = Math.max(0, pageDimensions.height / zoom - field.height);
97
102
 
98
103
  const clampedX = Math.max(minX, Math.min(maxX, x));
99
104
  const clampedY = Math.max(minY, Math.min(maxY, y));