@servicetitan/dte-pdf-editor 1.92.4 → 1.93.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 (209) hide show
  1. package/README.md +0 -2
  2. package/dist/components/field-config-panel/dropdown-field-items.d.ts +9 -0
  3. package/dist/components/field-config-panel/dropdown-field-items.d.ts.map +1 -0
  4. package/dist/components/field-config-panel/dropdown-field-items.js +60 -0
  5. package/dist/components/field-config-panel/dropdown-field-items.js.map +1 -0
  6. package/dist/components/field-config-panel/field-config-panel-overlay.d.ts +1 -1
  7. package/dist/components/field-config-panel/field-config-panel-overlay.d.ts.map +1 -1
  8. package/dist/components/field-config-panel/field-config-panel.d.ts +1 -1
  9. package/dist/components/field-config-panel/field-config-panel.d.ts.map +1 -1
  10. package/dist/components/field-config-panel/field-config-panel.js +5 -1
  11. package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
  12. package/dist/components/field-config-panel/fillable-default-config.d.ts +19 -0
  13. package/dist/components/field-config-panel/fillable-default-config.d.ts.map +1 -0
  14. package/dist/components/field-config-panel/fillable-default-config.js +77 -0
  15. package/dist/components/field-config-panel/fillable-default-config.js.map +1 -0
  16. package/dist/components/field-sidebar/calculated-field-type-list.d.ts.map +1 -1
  17. package/dist/components/field-sidebar/calculated-field-type-list.js +1 -1
  18. package/dist/components/field-sidebar/calculated-field-type-list.js.map +1 -1
  19. package/dist/components/field-sidebar/e-sign-field-type-list.d.ts.map +1 -1
  20. package/dist/components/field-sidebar/e-sign-field-type-list.js +1 -1
  21. package/dist/components/field-sidebar/e-sign-field-type-list.js.map +1 -1
  22. package/dist/components/field-sidebar/field-type.d.ts +2 -0
  23. package/dist/components/field-sidebar/field-type.d.ts.map +1 -1
  24. package/dist/components/field-sidebar/field-type.js +2 -2
  25. package/dist/components/field-sidebar/field-type.js.map +1 -1
  26. package/dist/components/field-sidebar/fillable-field-type-list.d.ts.map +1 -1
  27. package/dist/components/field-sidebar/fillable-field-type-list.js +1 -1
  28. package/dist/components/field-sidebar/fillable-field-type-list.js.map +1 -1
  29. package/dist/components/field-sidebar/generic-field-type-list.d.ts.map +1 -1
  30. package/dist/components/field-sidebar/generic-field-type-list.js +1 -1
  31. package/dist/components/field-sidebar/generic-field-type-list.js.map +1 -1
  32. package/dist/components/pdf-canvas/canvas-controls.d.ts +9 -15
  33. package/dist/components/pdf-canvas/canvas-controls.d.ts.map +1 -1
  34. package/dist/components/pdf-canvas/canvas-controls.js +4 -14
  35. package/dist/components/pdf-canvas/canvas-controls.js.map +1 -1
  36. package/dist/components/pdf-canvas/pdf-canvas.d.ts +6 -2
  37. package/dist/components/pdf-canvas/pdf-canvas.d.ts.map +1 -1
  38. package/dist/components/pdf-canvas/pdf-canvas.js +11 -5
  39. package/dist/components/pdf-canvas/pdf-canvas.js.map +1 -1
  40. package/dist/components/pdf-editor/pdf-editor.d.ts +2 -0
  41. package/dist/components/pdf-editor/pdf-editor.d.ts.map +1 -1
  42. package/dist/components/pdf-editor/pdf-editor.js +2 -2
  43. package/dist/components/pdf-editor/pdf-editor.js.map +1 -1
  44. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js +4 -1
  45. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js.map +1 -1
  46. package/dist/components/pdf-fields-overlay/pdf-overlay-field-e-sign.d.ts.map +1 -1
  47. package/dist/components/pdf-fields-overlay/pdf-overlay-field-e-sign.js +3 -2
  48. package/dist/components/pdf-fields-overlay/pdf-overlay-field-e-sign.js.map +1 -1
  49. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.d.ts +9 -0
  50. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.d.ts.map +1 -1
  51. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.js +39 -12
  52. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.js.map +1 -1
  53. package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts.map +1 -1
  54. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js +6 -4
  55. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js.map +1 -1
  56. package/dist/components/pdf-view/pdf-view-e-sign.d.ts.map +1 -1
  57. package/dist/components/pdf-view/pdf-view-e-sign.js +3 -2
  58. package/dist/components/pdf-view/pdf-view-e-sign.js.map +1 -1
  59. package/dist/components/pdf-view/pdf-view-field-container.d.ts.map +1 -1
  60. package/dist/components/pdf-view/pdf-view-field-container.js +9 -15
  61. package/dist/components/pdf-view/pdf-view-field-container.js.map +1 -1
  62. package/dist/components/pdf-view/pdf-view-fillable.d.ts.map +1 -1
  63. package/dist/components/pdf-view/pdf-view-fillable.js +7 -3
  64. package/dist/components/pdf-view/pdf-view-fillable.js.map +1 -1
  65. package/dist/components/pdf-view/pdf-view.d.ts +5 -0
  66. package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
  67. package/dist/components/pdf-view/pdf-view.js +58 -24
  68. package/dist/components/pdf-view/pdf-view.js.map +1 -1
  69. package/dist/constants/field.constants.d.ts +1 -0
  70. package/dist/constants/field.constants.d.ts.map +1 -1
  71. package/dist/constants/field.constants.js +36 -6
  72. package/dist/constants/field.constants.js.map +1 -1
  73. package/dist/hooks/index.d.ts +1 -0
  74. package/dist/hooks/index.d.ts.map +1 -1
  75. package/dist/hooks/index.js +1 -0
  76. package/dist/hooks/index.js.map +1 -1
  77. package/dist/hooks/usePdfFieldDnD.d.ts.map +1 -1
  78. package/dist/hooks/usePdfFieldDnD.js +6 -0
  79. package/dist/hooks/usePdfFieldDnD.js.map +1 -1
  80. package/dist/hooks/usePdfFieldSelection.d.ts +1 -1
  81. package/dist/hooks/usePdfFieldSelection.d.ts.map +1 -1
  82. package/dist/hooks/usePdfFieldSelection.js +3 -1
  83. package/dist/hooks/usePdfFieldSelection.js.map +1 -1
  84. package/dist/hooks/usePinchZoom.d.ts +14 -0
  85. package/dist/hooks/usePinchZoom.d.ts.map +1 -0
  86. package/dist/hooks/usePinchZoom.js +58 -0
  87. package/dist/hooks/usePinchZoom.js.map +1 -0
  88. package/dist/hooks/useZoom.d.ts +2 -0
  89. package/dist/hooks/useZoom.d.ts.map +1 -1
  90. package/dist/hooks/useZoom.js +18 -6
  91. package/dist/hooks/useZoom.js.map +1 -1
  92. package/dist/index.d.ts +1 -0
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +1 -0
  95. package/dist/index.js.map +1 -1
  96. package/dist/interface/types.d.ts +48 -1
  97. package/dist/interface/types.d.ts.map +1 -1
  98. package/dist/interface/types.js.map +1 -1
  99. package/dist/styles/index.css +1 -0
  100. package/dist/styles/inline-editable.css +2 -0
  101. package/dist/styles/pdf-canvas.css +92 -15
  102. package/dist/styles/pdf-editor.css +1 -0
  103. package/dist/styles/pdf-field-overlay.css +40 -118
  104. package/dist/styles/pdf-field.css +111 -0
  105. package/dist/styles/variables.css +4 -0
  106. package/dist/utils/data-model/index.d.ts +1 -0
  107. package/dist/utils/data-model/index.d.ts.map +1 -1
  108. package/dist/utils/data-model/index.js +1 -0
  109. package/dist/utils/data-model/index.js.map +1 -1
  110. package/dist/utils/data-model/resolve-fields-default-values.d.ts +3 -0
  111. package/dist/utils/data-model/resolve-fields-default-values.d.ts.map +1 -0
  112. package/dist/utils/data-model/resolve-fields-default-values.js +19 -0
  113. package/dist/utils/data-model/resolve-fields-default-values.js.map +1 -0
  114. package/dist/utils/exposes-utils.d.ts +2 -0
  115. package/dist/utils/exposes-utils.d.ts.map +1 -0
  116. package/dist/utils/exposes-utils.js +2 -0
  117. package/dist/utils/exposes-utils.js.map +1 -0
  118. package/dist/utils/field/field-background-color.utils.d.ts +1 -1
  119. package/dist/utils/field/field-background-color.utils.d.ts.map +1 -1
  120. package/dist/utils/field/field-background-color.utils.js +1 -1
  121. package/dist/utils/field/field-background-color.utils.js.map +1 -1
  122. package/dist/utils/field/get-dropdown-field-items.utils.d.ts +3 -0
  123. package/dist/utils/field/get-dropdown-field-items.utils.d.ts.map +1 -0
  124. package/dist/utils/field/get-dropdown-field-items.utils.js +7 -0
  125. package/dist/utils/field/get-dropdown-field-items.utils.js.map +1 -0
  126. package/dist/utils/field/get-field-bounds-style.utils.d.ts +1 -2
  127. package/dist/utils/field/get-field-bounds-style.utils.d.ts.map +1 -1
  128. package/dist/utils/field/get-field-bounds-style.utils.js +9 -4
  129. package/dist/utils/field/get-field-bounds-style.utils.js.map +1 -1
  130. package/dist/utils/field/get-field-type-option.utils.d.ts +3 -0
  131. package/dist/utils/field/get-field-type-option.utils.d.ts.map +1 -0
  132. package/dist/utils/field/get-field-type-option.utils.js +10 -0
  133. package/dist/utils/field/get-field-type-option.utils.js.map +1 -0
  134. package/dist/utils/field/get-fillable-default-value.utils.d.ts +29 -0
  135. package/dist/utils/field/get-fillable-default-value.utils.d.ts.map +1 -0
  136. package/dist/utils/field/get-fillable-default-value.utils.js +59 -0
  137. package/dist/utils/field/get-fillable-default-value.utils.js.map +1 -0
  138. package/dist/utils/field/get-navigable-fields.utils.js +1 -1
  139. package/dist/utils/field/get-navigable-fields.utils.js.map +1 -1
  140. package/dist/utils/field/index.d.ts +3 -0
  141. package/dist/utils/field/index.d.ts.map +1 -1
  142. package/dist/utils/field/index.js +3 -0
  143. package/dist/utils/field/index.js.map +1 -1
  144. package/dist/utils/pdf/index.d.ts +1 -0
  145. package/dist/utils/pdf/index.d.ts.map +1 -1
  146. package/dist/utils/pdf/index.js +1 -0
  147. package/dist/utils/pdf/index.js.map +1 -1
  148. package/dist/utils/pdf/pinch-zoom.utils.d.ts +6 -0
  149. package/dist/utils/pdf/pinch-zoom.utils.d.ts.map +1 -0
  150. package/dist/utils/pdf/pinch-zoom.utils.js +18 -0
  151. package/dist/utils/pdf/pinch-zoom.utils.js.map +1 -0
  152. package/dist/utils/recipients/map-colors.js +23 -23
  153. package/dist/utils/recipients/map-colors.js.map +1 -1
  154. package/package.json +1 -1
  155. package/src/__tests__/field-types/e-sign-fields.test.ts +3 -3
  156. package/src/__tests__/field-types/fillable-fields.test.ts +142 -6
  157. package/src/components/field-config-panel/dropdown-field-items.tsx +138 -0
  158. package/src/components/field-config-panel/field-config-panel-overlay.tsx +1 -1
  159. package/src/components/field-config-panel/field-config-panel.tsx +16 -1
  160. package/src/components/field-config-panel/fillable-default-config.tsx +155 -0
  161. package/src/components/field-sidebar/calculated-field-type-list.tsx +1 -0
  162. package/src/components/field-sidebar/e-sign-field-type-list.tsx +1 -0
  163. package/src/components/field-sidebar/field-type.tsx +13 -3
  164. package/src/components/field-sidebar/fillable-field-type-list.tsx +1 -0
  165. package/src/components/field-sidebar/generic-field-type-list.tsx +1 -0
  166. package/src/components/pdf-canvas/canvas-controls.tsx +30 -128
  167. package/src/components/pdf-canvas/pdf-canvas.tsx +18 -8
  168. package/src/components/pdf-editor/pdf-editor.tsx +5 -2
  169. package/src/components/pdf-fields-overlay/pdf-fields-overlay.tsx +2 -2
  170. package/src/components/pdf-fields-overlay/pdf-overlay-field-e-sign.tsx +8 -6
  171. package/src/components/pdf-fields-overlay/pdf-overlay-field-fillable.tsx +66 -3
  172. package/src/components/pdf-fields-overlay/pdf-overlay-field.tsx +22 -3
  173. package/src/components/pdf-view/pdf-view-e-sign.tsx +8 -6
  174. package/src/components/pdf-view/pdf-view-field-container.tsx +31 -31
  175. package/src/components/pdf-view/pdf-view-fillable.tsx +26 -3
  176. package/src/components/pdf-view/pdf-view.tsx +164 -95
  177. package/src/constants/field.constants.ts +38 -6
  178. package/src/hooks/index.ts +1 -0
  179. package/src/hooks/usePdfFieldDnD.ts +6 -0
  180. package/src/hooks/usePdfFieldSelection.ts +8 -2
  181. package/src/hooks/usePinchZoom.ts +86 -0
  182. package/src/hooks/useZoom.ts +20 -8
  183. package/src/index.ts +1 -0
  184. package/src/interface/types.ts +39 -1
  185. package/src/styles/index.css +1 -0
  186. package/src/styles/inline-editable.css +2 -0
  187. package/src/styles/pdf-canvas.css +92 -15
  188. package/src/styles/pdf-editor.css +1 -0
  189. package/src/styles/pdf-field-overlay.css +40 -118
  190. package/src/styles/pdf-field.css +111 -0
  191. package/src/styles/variables.css +4 -0
  192. package/src/utils/data-model/index.ts +1 -0
  193. package/src/utils/data-model/resolve-fields-default-values.ts +19 -0
  194. package/src/utils/exposes-utils.ts +1 -0
  195. package/src/utils/field/__tests__/field-background-color.utils.test.ts +3 -3
  196. package/src/utils/field/__tests__/get-field-bounds-style.utils.test.ts +0 -1
  197. package/src/utils/field/__tests__/get-field-type-option.utils.test.ts +53 -0
  198. package/src/utils/field/__tests__/get-fillable-default-value.utils.test.ts +137 -0
  199. package/src/utils/field/field-background-color.utils.ts +1 -1
  200. package/src/utils/field/get-dropdown-field-items.utils.ts +14 -0
  201. package/src/utils/field/get-field-bounds-style.utils.ts +11 -11
  202. package/src/utils/field/get-field-type-option.utils.ts +16 -0
  203. package/src/utils/field/get-fillable-default-value.utils.ts +70 -0
  204. package/src/utils/field/get-navigable-fields.utils.ts +1 -1
  205. package/src/utils/field/index.ts +3 -0
  206. package/src/utils/pdf/__tests__/pinch-zoom.utils.test.ts +58 -0
  207. package/src/utils/pdf/index.ts +1 -0
  208. package/src/utils/pdf/pinch-zoom.utils.ts +30 -0
  209. package/src/utils/recipients/map-colors.ts +23 -23
@@ -7,6 +7,7 @@ import {
7
7
  } from '../../interface/types';
8
8
  import {
9
9
  dateToUtcZero,
10
+ getDropdownFieldItems,
10
11
  getFieldPlaceholderText,
11
12
  getFieldTextClipClassName,
12
13
  getFillablePaintedLabel,
@@ -31,11 +32,12 @@ interface FillableTextInputProps {
31
32
  }
32
33
 
33
34
  const fillableTextStyles = {
34
- background: 'inherit',
35
+ background: 'transparent',
35
36
  width: '100%',
36
37
  height: '100%',
37
38
  border: 'none',
38
39
  fontSize: 'inherit',
40
+ color: 'inherit',
39
41
  boxSizing: 'border-box' as const,
40
42
  padding: 0,
41
43
  margin: 0,
@@ -96,8 +98,7 @@ export const PdfViewFillable: FC<PdfViewFillableProps> = ({
96
98
  }) => {
97
99
  const resolvedValue = data?.[field.path!];
98
100
  const isViewByCurrentRecipient = fillingBy?.includes(field.recipient!);
99
- const isFillable =
100
- viewMode === 'preview' ? true : viewMode === 'fill' ? false : isViewByCurrentRecipient;
101
+ const isFillable = viewMode === 'preview' ? true : isViewByCurrentRecipient;
101
102
 
102
103
  const isInputDisabled = !isFillable || !!readOnly;
103
104
 
@@ -208,6 +209,28 @@ export const PdfViewFillable: FC<PdfViewFillableProps> = ({
208
209
  );
209
210
  }
210
211
 
212
+ if (field.subType === 'dropdown') {
213
+ return (
214
+ <select
215
+ style={fillableTextStyles}
216
+ disabled={isInputDisabled}
217
+ id={field.path}
218
+ name={field.path}
219
+ value={resolvedValue ?? ''}
220
+ onChange={e => handleDataChange(e.target.value)}
221
+ >
222
+ <option value="" disabled hidden>
223
+ {getFieldPlaceholderText(field) || 'Select...'}
224
+ </option>
225
+ {getDropdownFieldItems(field).map(item => (
226
+ <option key={item.value} value={item.value}>
227
+ {item.label}
228
+ </option>
229
+ ))}
230
+ </select>
231
+ );
232
+ }
233
+
211
234
  const TextComponent = field.multiline ? FillableTextareaInput : FillableTextInput;
212
235
  return (
213
236
  <TextComponent
@@ -1,7 +1,7 @@
1
- import { Flex } from '@servicetitan/anvil2';
2
- import { FC, ReactNode, useMemo, useRef, useState } from 'react';
1
+ import { Flex, Text } from '@servicetitan/anvil2';
2
+ import { FC, ReactNode, useLayoutEffect, useMemo, useRef, useState } from 'react';
3
3
  import { BASE_PAGE_WIDTH } from '../../constants';
4
- import { useZoom } from '../../hooks';
4
+ import { usePinchZoom, useZoom } from '../../hooks';
5
5
  import {
6
6
  type DataChangePayload,
7
7
  type DataModelValues,
@@ -15,7 +15,11 @@ import {
15
15
  mapColorsToRecipients,
16
16
  sortFieldsByReadingOrder,
17
17
  } from '../../utils';
18
- import { CanvasControls } from '../pdf-canvas/canvas-controls';
18
+ import {
19
+ FieldNavigationControls,
20
+ ZoomControls,
21
+ ZoomControlsProps,
22
+ } from '../pdf-canvas/canvas-controls';
19
23
  import { PdfDocumentRenderer } from '../pdf-canvas/pdf-document-renderer';
20
24
  import { PdfViewCalculated } from './pdf-view-calculated';
21
25
  import { PdfViewDataModel } from './pdf-view-data-model';
@@ -30,6 +34,10 @@ export interface PdfViewProps {
30
34
  viewMode?: PdfViewMode;
31
35
  data?: DataModelValues;
32
36
  zoomable?: boolean;
37
+ zoomProps?: {
38
+ vertical: ZoomControlsProps['vertical'];
39
+ position: ZoomControlsProps['position'];
40
+ };
33
41
  enableInputNavigation?: boolean;
34
42
  pdfUrl: string;
35
43
  loading?: boolean;
@@ -74,14 +82,23 @@ export const PdfView: FC<PdfViewProps> = ({
74
82
  pdfUrl,
75
83
  recipients,
76
84
  viewMode,
85
+ zoomProps,
77
86
  zoomable = true,
78
87
  }) => {
79
88
  const [isPdfLoaded, setIsPdfLoaded] = useState<boolean>(false);
80
89
  const [previewData, setPreviewData] = useState<DataModelValues>(data ?? {});
81
- const { resetZoom, zoom, zoomIn, zoomOut } = useZoom();
90
+ const { resetZoom, setZoomValue, zoom, zoomByFactor, zoomIn, zoomOut } = useZoom();
82
91
  const activeData = viewMode === 'preview' ? previewData : data;
83
92
 
84
93
  const pdfWrapperRef = useRef<HTMLDivElement>(null);
94
+ const navControlsRef = useRef<HTMLDivElement>(null);
95
+ const contentContainerRef = useRef<HTMLDivElement>(null);
96
+ usePinchZoom(contentContainerRef, {
97
+ enabled: zoomable,
98
+ setZoomValue,
99
+ zoom,
100
+ zoomByFactor,
101
+ });
85
102
 
86
103
  const onDocumentLoadSuccess = (pageNum: number) => {
87
104
  setIsPdfLoaded(true);
@@ -111,7 +128,6 @@ export const PdfView: FC<PdfViewProps> = ({
111
128
  [navigableFields],
112
129
  );
113
130
 
114
- // -1 means no field has been navigated to yet, so the first "next" lands on index 0.
115
131
  const [currentFieldIndex, setCurrentFieldIndex] = useState(-1);
116
132
 
117
133
  const handleFieldOverlayFocus = (fieldId: string) => {
@@ -120,115 +136,168 @@ export const PdfView: FC<PdfViewProps> = ({
120
136
  }
121
137
  };
122
138
 
123
- const handleFocusField = (fieldId: string) => {
139
+ const getFieldElement = (fieldId: string) => {
124
140
  const root = pdfWrapperRef.current?.getRootNode() as Document | ShadowRoot | undefined;
125
- const fieldEl = root?.querySelector<HTMLElement>(`[id="dte-pdf-field-${fieldId}"]`);
126
- fieldEl?.scrollIntoView({ behavior: 'smooth', block: 'center' });
127
- const focusableElement = fieldEl?.querySelector<HTMLElement>('input, textarea') ?? fieldEl;
128
- focusableElement?.focus();
141
+ return root?.querySelector<HTMLElement>(`[id="dte-pdf-field-${fieldId}"]`) ?? undefined;
142
+ };
143
+
144
+ useLayoutEffect(() => {
145
+ const field = navigableFields[currentFieldIndex];
146
+ const navControlsEl = navControlsRef.current;
147
+
148
+ if (!navControlsEl) {
149
+ return;
150
+ }
151
+
152
+ if (currentFieldIndex === -1) {
153
+ navControlsEl.style.top = `20px`;
154
+ return;
155
+ }
156
+ if (!field) {
157
+ return;
158
+ }
159
+
160
+ const fieldEl = getFieldElement(field.id);
161
+ if (!fieldEl) {
162
+ return;
163
+ }
164
+ const containerTop = contentContainerRef.current?.getBoundingClientRect().top ?? 0;
165
+ navControlsEl.style.top = `${fieldEl.getBoundingClientRect().top - containerTop}px`;
166
+ }, [currentFieldIndex, navigableFields]);
167
+
168
+ const handleFocusField = (fieldId: string) => {
169
+ const fieldEl = getFieldElement(fieldId);
170
+ if (fieldEl) {
171
+ fieldEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
172
+ const focusableChild = fieldEl.querySelector<HTMLElement>('input, textarea');
173
+ (focusableChild ?? fieldEl)?.focus();
174
+ }
129
175
  };
130
176
 
131
177
  const goToField = (index: number) => {
132
- let i = index;
133
- if (i < 0) {
134
- i = 0;
135
- } else if (i >= navigableFields.length) {
136
- i = navigableFields.length - 1;
178
+ const fieldIndex = index < 0 || index >= navigableFields.length ? -1 : index;
179
+ const field = navigableFields[fieldIndex];
180
+
181
+ setCurrentFieldIndex(fieldIndex);
182
+ if (field) {
183
+ handleFocusField(field.id);
137
184
  }
138
- const field = navigableFields[i];
185
+ };
139
186
 
140
- setCurrentFieldIndex(i);
141
- handleFocusField(field.id);
187
+ const handleNextField = () => {
188
+ goToField(currentFieldIndex + 1);
142
189
  };
143
190
 
144
191
  const recipientsColors = mapColorsToRecipients(recipients);
145
- const hasNavigableFields = navigableFields.length === 0;
192
+ const hasNavigableFields = navigableFields.length !== 0;
193
+ const shouldShowFieldNavigation = enableInputNavigation && hasNavigableFields;
194
+
146
195
  return (
147
196
  <Flex
148
- flex={1}
149
- className="dte-pdf-editor-theme dte-pdf-editor-content-container --view-mode"
150
197
  direction="column"
198
+ style={{ position: 'relative', minWidth: 0, height: '100%' }}
199
+ className="dte-pdf-editor-theme"
200
+ ref={contentContainerRef}
151
201
  >
152
- {isPdfLoaded && (
153
- <CanvasControls
202
+ {isPdfLoaded && shouldShowFieldNavigation && (
203
+ <div ref={navControlsRef} className="dte-pdf-movable-next-button-wrapper">
204
+ <FieldNavigationControls
205
+ isStarted={currentFieldIndex !== -1}
206
+ onNextField={handleNextField}
207
+ />
208
+ </div>
209
+ )}
210
+ {isPdfLoaded && zoomable && (
211
+ <ZoomControls
212
+ {...zoomProps}
154
213
  zoom={zoom}
155
214
  onZoomIn={zoomIn}
156
215
  onZoomOut={zoomOut}
157
- zoomable={zoomable}
158
216
  onResetZoom={resetZoom}
159
- enableInputNavigation={enableInputNavigation}
160
- disabledPrev={hasNavigableFields}
161
- disabledNext={hasNavigableFields}
162
- disabledOnFirstField={hasNavigableFields}
163
- disabledOnLastField={hasNavigableFields}
164
- onPrevField={() => goToField(currentFieldIndex - 1)}
165
- onNextField={() => goToField(currentFieldIndex + 1)}
166
- onFirstField={() => goToField(0)}
167
- onLastField={() => goToField(navigableFields.length - 1)}
168
217
  />
169
218
  )}
170
- <div
171
- ref={pdfWrapperRef}
172
- className="dte-pdf-wrapper"
173
- style={{ width: BASE_PAGE_WIDTH * zoom }}
219
+ {shouldShowFieldNavigation && (
220
+ <Flex
221
+ justifyContent="space-between"
222
+ alignItems="center"
223
+ gap={2}
224
+ className="dte-pdf-canvas-controls"
225
+ >
226
+ <Text style={{ color: '#ffffff' }}>Review and complete</Text>
227
+ <FieldNavigationControls
228
+ isStarted={currentFieldIndex !== -1}
229
+ onNextField={handleNextField}
230
+ />
231
+ </Flex>
232
+ )}
233
+ <Flex
234
+ flex={1}
235
+ className="dte-pdf-editor-content-container --view-mode"
236
+ direction="column"
174
237
  >
175
- <PdfDocumentRenderer
176
- pdfUrl={pdfUrl}
177
- loading={loading}
178
- pageWidth={BASE_PAGE_WIDTH * zoom}
179
- errorPlaceholder={errorPlaceholder}
180
- loadingPlaceholder={loadingPlaceholder}
181
- onDocumentLoadSuccess={onDocumentLoadSuccess}
182
- />
183
- {isPdfLoaded && (
184
- <div className="dte-pdf-field-overlay --view-mode">
185
- {fields.map(field => (
186
- <PdfViewFieldContainer
187
- key={field.id}
188
- field={field}
189
- data={viewMode === 'preview' ? previewData : data}
190
- error={errors[field.path!] || ''}
191
- recipientsColors={recipientsColors}
192
- pdfWrapperRef={pdfWrapperRef}
193
- zoom={zoom}
194
- onFocus={handleFieldOverlayFocus}
195
- >
196
- {(field.type === FieldTypeEnum.dataModel ||
197
- field.type === FieldTypeEnum.forms) && (
198
- <PdfViewDataModel
199
- field={field}
200
- data={viewMode === 'preview' ? previewData : data}
201
- />
202
- )}
203
- {field.type === FieldTypeEnum.eSign && (
204
- <PdfViewESign field={field} />
205
- )}
206
- {field.type === FieldTypeEnum.calculated && (
207
- <PdfViewCalculated
208
- field={field}
209
- data={viewMode === 'preview' ? previewData : data}
210
- holidays={holidays}
211
- viewMode={viewMode}
212
- />
213
- )}
214
- {field.type === FieldTypeEnum.fillable && (
215
- <PdfViewFillable
216
- data={viewMode === 'preview' ? previewData : data}
217
- field={field}
218
- readOnly={makeFillableFieldsReadonly}
219
- fillingBy={fillingBy}
220
- viewMode={viewMode}
221
- onDataChange={handleDataChange}
222
- />
223
- )}
224
- {field.type === FieldTypeEnum.generic && (
225
- <PdfViewGeneric field={field} />
226
- )}
227
- </PdfViewFieldContainer>
228
- ))}
229
- </div>
230
- )}
231
- </div>
238
+ <div
239
+ ref={pdfWrapperRef}
240
+ className="dte-pdf-wrapper"
241
+ style={{ width: BASE_PAGE_WIDTH * zoom }}
242
+ >
243
+ <PdfDocumentRenderer
244
+ pdfUrl={pdfUrl}
245
+ loading={loading}
246
+ pageWidth={BASE_PAGE_WIDTH * zoom}
247
+ errorPlaceholder={errorPlaceholder}
248
+ loadingPlaceholder={loadingPlaceholder}
249
+ onDocumentLoadSuccess={onDocumentLoadSuccess}
250
+ />
251
+ {isPdfLoaded && (
252
+ <div className="dte-pdf-field-overlay --view-mode">
253
+ {fields.map(field => (
254
+ <PdfViewFieldContainer
255
+ key={field.id}
256
+ field={field}
257
+ data={viewMode === 'preview' ? previewData : data}
258
+ error={errors[field.path!] || errors[field.id] || ''}
259
+ recipientsColors={recipientsColors}
260
+ pdfWrapperRef={pdfWrapperRef}
261
+ zoom={zoom}
262
+ onFocus={handleFieldOverlayFocus}
263
+ >
264
+ {(field.type === FieldTypeEnum.dataModel ||
265
+ field.type === FieldTypeEnum.forms) && (
266
+ <PdfViewDataModel
267
+ field={field}
268
+ data={viewMode === 'preview' ? previewData : data}
269
+ />
270
+ )}
271
+ {field.type === FieldTypeEnum.eSign && (
272
+ <PdfViewESign field={field} />
273
+ )}
274
+ {field.type === FieldTypeEnum.calculated && (
275
+ <PdfViewCalculated
276
+ field={field}
277
+ data={viewMode === 'preview' ? previewData : data}
278
+ holidays={holidays}
279
+ viewMode={viewMode}
280
+ />
281
+ )}
282
+ {field.type === FieldTypeEnum.fillable && (
283
+ <PdfViewFillable
284
+ data={viewMode === 'preview' ? previewData : data}
285
+ field={field}
286
+ readOnly={makeFillableFieldsReadonly}
287
+ fillingBy={fillingBy}
288
+ viewMode={viewMode}
289
+ onDataChange={handleDataChange}
290
+ />
291
+ )}
292
+ {field.type === FieldTypeEnum.generic && (
293
+ <PdfViewGeneric field={field} />
294
+ )}
295
+ </PdfViewFieldContainer>
296
+ ))}
297
+ </div>
298
+ )}
299
+ </div>
300
+ </Flex>
232
301
  </Flex>
233
302
  );
234
303
  };
@@ -1,3 +1,18 @@
1
+ import NumbersIcon from '@servicetitan/anvil2/assets/icons/material/round/123.svg';
2
+ import AbcIcon from '@servicetitan/anvil2/assets/icons/material/round/abc.svg';
3
+ import ArrowDropDownCircleIcon from '@servicetitan/anvil2/assets/icons/material/round/arrow_drop_down_circle.svg';
4
+ import BadgeIcon from '@servicetitan/anvil2/assets/icons/material/round/badge.svg';
5
+ import CalculateIcon from '@servicetitan/anvil2/assets/icons/material/round/calculate.svg';
6
+ import CalendarMonthIcon from '@servicetitan/anvil2/assets/icons/material/round/calendar_month.svg';
7
+ import CheckboxIcon from '@servicetitan/anvil2/assets/icons/material/round/check_box.svg';
8
+ import EventAvailableIcon from '@servicetitan/anvil2/assets/icons/material/round/event_available.svg';
9
+ import GestureIcon from '@servicetitan/anvil2/assets/icons/material/round/gesture.svg';
10
+ import RadioIcon from '@servicetitan/anvil2/assets/icons/material/round/radio_button_checked.svg';
11
+ import ShortTextIcon from '@servicetitan/anvil2/assets/icons/material/round/short_text.svg';
12
+ import TableChartIcon from '@servicetitan/anvil2/assets/icons/material/round/table_chart.svg';
13
+ import TextIcon from '@servicetitan/anvil2/assets/icons/material/round/text_fields.svg';
14
+ import IconAppointmentsIcon from '@servicetitan/anvil2/assets/icons/st/appointments.svg';
15
+
1
16
  import {
2
17
  type CalculatedFormatForDate,
3
18
  type CalculatedFormatForNumber,
@@ -32,10 +47,13 @@ export const FIELD_FONT_SIZE_OPTIONS = Array.from({ length: 17 }, (_, index) =>
32
47
  export const FILLABLE_FIELD_DEFAULT_SIZES = {
33
48
  text: { width: 200, height: 25 },
34
49
  date: { width: 200, height: 25 },
50
+ dropdown: { width: 200, height: 25 },
35
51
  radio: { width: 20, height: 20 },
36
52
  checkbox: { width: 20, height: 20 },
37
53
  } as Record<PdfFieldSubType, { width: number; height: number }>;
38
54
 
55
+ export const DROPDOWN_FIELD_MAX_ITEMS = 50;
56
+
39
57
  export const GENERIC_FIELD_TABLE_ROW_HEIGHT = 40;
40
58
 
41
59
  export const GENERIC_FIELD_DEFAULT_SIZES = {
@@ -44,11 +62,17 @@ export const GENERIC_FIELD_DEFAULT_SIZES = {
44
62
  } as Record<PdfFieldSubType, { width: number; height: number }>;
45
63
 
46
64
  export const FILLABLE_FIELD_TYPES: FieldTypeOption[] = [
47
- { label: 'Text Field', type: FieldTypeEnum.fillable, subType: 'text' },
48
- { label: 'Number Field', type: FieldTypeEnum.fillable, subType: 'number' },
49
- { label: 'Date Field', type: FieldTypeEnum.fillable, subType: 'date' },
50
- { label: 'Checkbox', type: FieldTypeEnum.fillable, subType: 'checkbox' },
51
- { label: 'Radio', type: FieldTypeEnum.fillable, subType: 'radio' },
65
+ { label: 'Text Field', type: FieldTypeEnum.fillable, subType: 'text', icon: ShortTextIcon },
66
+ { label: 'Number Field', type: FieldTypeEnum.fillable, subType: 'number', icon: NumbersIcon },
67
+ { label: 'Date Field', type: FieldTypeEnum.fillable, subType: 'date', icon: CalendarMonthIcon },
68
+ { label: 'Checkbox', type: FieldTypeEnum.fillable, subType: 'checkbox', icon: CheckboxIcon },
69
+ { label: 'Radio', type: FieldTypeEnum.fillable, subType: 'radio', icon: RadioIcon },
70
+ {
71
+ label: 'Dropdown',
72
+ type: FieldTypeEnum.fillable,
73
+ subType: 'dropdown',
74
+ icon: ArrowDropDownCircleIcon,
75
+ },
52
76
  ];
53
77
 
54
78
  export const E_SIGN_FIELD_TYPES: FieldTypeOption[] = [
@@ -56,26 +80,31 @@ export const E_SIGN_FIELD_TYPES: FieldTypeOption[] = [
56
80
  label: 'Signature',
57
81
  type: FieldTypeEnum.eSign,
58
82
  subType: ESignFieldType.signature,
83
+ icon: GestureIcon,
59
84
  },
60
85
  {
61
86
  label: 'Initials',
62
87
  type: FieldTypeEnum.eSign,
63
88
  subType: ESignFieldType.initials,
89
+ icon: AbcIcon,
64
90
  },
65
91
  {
66
92
  label: 'Date Signed',
67
93
  type: FieldTypeEnum.eSign,
68
94
  subType: ESignFieldType.dateSigned,
95
+ icon: EventAvailableIcon,
69
96
  },
70
97
  {
71
- label: 'Date Signed Offset',
98
+ label: 'Calculated Singing Date',
72
99
  type: FieldTypeEnum.eSign,
73
100
  subType: ESignFieldType.dateSignedOffset,
101
+ icon: IconAppointmentsIcon,
74
102
  },
75
103
  {
76
104
  label: 'Full Name',
77
105
  type: FieldTypeEnum.eSign,
78
106
  subType: ESignFieldType.fullName,
107
+ icon: BadgeIcon,
79
108
  },
80
109
  ];
81
110
 
@@ -83,6 +112,7 @@ export const CALCULATED_FIELD_TYPES: FieldTypeOption[] = [
83
112
  {
84
113
  label: 'Calculated Field',
85
114
  type: FieldTypeEnum.calculated,
115
+ icon: CalculateIcon,
86
116
  },
87
117
  ];
88
118
 
@@ -91,11 +121,13 @@ export const GENERIC_FIELD_TYPES: FieldTypeOption[] = [
91
121
  label: 'Text',
92
122
  type: FieldTypeEnum.generic,
93
123
  subType: 'text',
124
+ icon: TextIcon,
94
125
  },
95
126
  {
96
127
  label: 'Table',
97
128
  type: FieldTypeEnum.generic,
98
129
  subType: 'table',
130
+ icon: TableChartIcon,
99
131
  },
100
132
  ];
101
133
 
@@ -6,5 +6,6 @@ export * from './useFormulaEditor';
6
6
  export * from './usePdfDocumentRenderer';
7
7
  export * from './usePdfFieldDnD';
8
8
  export * from './usePdfFieldSelection';
9
+ export * from './usePinchZoom';
9
10
  export * from './useToggle';
10
11
  export * from './useZoom';
@@ -61,12 +61,18 @@ const initializeDroppedFieldByType: Record<FieldTypeEnum, InitializeDroppedField
61
61
  ...base,
62
62
  recipient: recipientName,
63
63
  required: false,
64
+ hideLabel: base.subType === 'radio' || base.subType === 'checkbox',
64
65
  path: generateFillablePath(recipientName, uuidv4()),
65
66
  ...(base.subType === 'radio'
66
67
  ? {
67
68
  groupName: `Radio Group ${uuidv4()}`,
68
69
  }
69
70
  : {}),
71
+ ...(base.subType === 'dropdown'
72
+ ? {
73
+ dropdownItems: [{ value: '', label: '' }],
74
+ }
75
+ : {}),
70
76
  ...(defaultSize && {
71
77
  width: defaultSize.width,
72
78
  height: defaultSize.height,
@@ -28,9 +28,15 @@ export const usePdfFieldSelection = (
28
28
 
29
29
  const updateMatchingFields = (
30
30
  predicate: (field: PdfField) => boolean,
31
- updates: Partial<PdfField>,
31
+ updates: Partial<PdfField> | ((field: PdfField) => Partial<PdfField>),
32
32
  ) => {
33
- onFieldsChange(fields.map(f => (predicate(f) ? { ...f, ...updates } : f)));
33
+ onFieldsChange(
34
+ fields.map(f =>
35
+ predicate(f)
36
+ ? { ...f, ...(typeof updates === 'function' ? updates(f) : updates) }
37
+ : f,
38
+ ),
39
+ );
34
40
  };
35
41
 
36
42
  const deleteSelectedField = () => {
@@ -0,0 +1,86 @@
1
+ import { RefObject, useEffect, useRef } from 'react';
2
+ import {
3
+ clampZoom,
4
+ getPinchZoomValue,
5
+ getTouchDistance,
6
+ getWheelZoomFactor,
7
+ } from '../utils';
8
+
9
+ interface UsePinchZoomOptions {
10
+ enabled: boolean;
11
+ zoom: number;
12
+ setZoomValue(value: number): void;
13
+ zoomByFactor(factor: number): void;
14
+ }
15
+
16
+ /**
17
+ * Trackpad pinch (wheel + ctrlKey) and two-finger mobile pinch on `containerRef`.
18
+ * Regular two-finger trackpad scroll is left alone so the page can still pan.
19
+ */
20
+ export const usePinchZoom = (
21
+ containerRef: RefObject<HTMLElement | null>,
22
+ { enabled, setZoomValue, zoom, zoomByFactor }: UsePinchZoomOptions,
23
+ ) => {
24
+ const zoomRef = useRef(zoom);
25
+ zoomRef.current = zoom;
26
+
27
+ useEffect(() => {
28
+ const container = containerRef.current;
29
+ if (!container || !enabled) {
30
+ return;
31
+ }
32
+
33
+ let pinchStartDistance = 0;
34
+ let pinchStartZoom = zoomRef.current;
35
+
36
+ const handleWheel = (event: WheelEvent) => {
37
+ if (!event.ctrlKey) {
38
+ return;
39
+ }
40
+
41
+ event.preventDefault();
42
+ zoomByFactor(getWheelZoomFactor(event.deltaY));
43
+ };
44
+
45
+ const handleTouchStart = (event: TouchEvent) => {
46
+ if (event.touches.length !== 2) {
47
+ return;
48
+ }
49
+
50
+ pinchStartDistance = getTouchDistance(event.touches[0], event.touches[1]);
51
+ pinchStartZoom = zoomRef.current;
52
+ };
53
+
54
+ const handleTouchMove = (event: TouchEvent) => {
55
+ if (event.touches.length !== 2 || pinchStartDistance <= 0) {
56
+ return;
57
+ }
58
+
59
+ event.preventDefault();
60
+ const currentDistance = getTouchDistance(event.touches[0], event.touches[1]);
61
+ setZoomValue(
62
+ clampZoom(getPinchZoomValue(pinchStartZoom, pinchStartDistance, currentDistance)),
63
+ );
64
+ };
65
+
66
+ const handleTouchEnd = (event: TouchEvent) => {
67
+ if (event.touches.length < 2) {
68
+ pinchStartDistance = 0;
69
+ }
70
+ };
71
+
72
+ container.addEventListener('wheel', handleWheel, { passive: false });
73
+ container.addEventListener('touchstart', handleTouchStart, { passive: true });
74
+ container.addEventListener('touchmove', handleTouchMove, { passive: false });
75
+ container.addEventListener('touchend', handleTouchEnd);
76
+ container.addEventListener('touchcancel', handleTouchEnd);
77
+
78
+ return () => {
79
+ container.removeEventListener('wheel', handleWheel);
80
+ container.removeEventListener('touchstart', handleTouchStart);
81
+ container.removeEventListener('touchmove', handleTouchMove);
82
+ container.removeEventListener('touchend', handleTouchEnd);
83
+ container.removeEventListener('touchcancel', handleTouchEnd);
84
+ };
85
+ }, [containerRef, enabled, setZoomValue, zoomByFactor]);
86
+ };