@servicetitan/dte-pdf-editor 1.91.0 → 1.92.1

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 (140) hide show
  1. package/dist/components/field-sidebar/calculated-field-type-list.d.ts.map +1 -1
  2. package/dist/components/field-sidebar/calculated-field-type-list.js +1 -1
  3. package/dist/components/field-sidebar/calculated-field-type-list.js.map +1 -1
  4. package/dist/components/field-sidebar/data-model-field-type-list.d.ts +1 -12
  5. package/dist/components/field-sidebar/data-model-field-type-list.d.ts.map +1 -1
  6. package/dist/components/field-sidebar/data-model-field-type-list.js +9 -29
  7. package/dist/components/field-sidebar/data-model-field-type-list.js.map +1 -1
  8. package/dist/components/field-sidebar/e-sign-field-type-list.d.ts.map +1 -1
  9. package/dist/components/field-sidebar/e-sign-field-type-list.js +1 -1
  10. package/dist/components/field-sidebar/e-sign-field-type-list.js.map +1 -1
  11. package/dist/components/field-sidebar/field-sidebar.d.ts +0 -5
  12. package/dist/components/field-sidebar/field-sidebar.d.ts.map +1 -1
  13. package/dist/components/field-sidebar/field-sidebar.js +10 -7
  14. package/dist/components/field-sidebar/field-sidebar.js.map +1 -1
  15. package/dist/components/field-sidebar/field-type.d.ts +2 -0
  16. package/dist/components/field-sidebar/field-type.d.ts.map +1 -1
  17. package/dist/components/field-sidebar/field-type.js +2 -2
  18. package/dist/components/field-sidebar/field-type.js.map +1 -1
  19. package/dist/components/field-sidebar/fillable-field-type-list.d.ts.map +1 -1
  20. package/dist/components/field-sidebar/fillable-field-type-list.js +1 -1
  21. package/dist/components/field-sidebar/fillable-field-type-list.js.map +1 -1
  22. package/dist/components/field-sidebar/form-fields-type-list.d.ts +5 -23
  23. package/dist/components/field-sidebar/form-fields-type-list.d.ts.map +1 -1
  24. package/dist/components/field-sidebar/form-fields-type-list.js +5 -20
  25. package/dist/components/field-sidebar/form-fields-type-list.js.map +1 -1
  26. package/dist/components/field-sidebar/generic-field-type-list.d.ts.map +1 -1
  27. package/dist/components/field-sidebar/generic-field-type-list.js +1 -1
  28. package/dist/components/field-sidebar/generic-field-type-list.js.map +1 -1
  29. package/dist/components/pdf-canvas/canvas-controls.d.ts +9 -15
  30. package/dist/components/pdf-canvas/canvas-controls.d.ts.map +1 -1
  31. package/dist/components/pdf-canvas/canvas-controls.js +4 -14
  32. package/dist/components/pdf-canvas/canvas-controls.js.map +1 -1
  33. package/dist/components/pdf-canvas/pdf-canvas.d.ts +6 -2
  34. package/dist/components/pdf-canvas/pdf-canvas.d.ts.map +1 -1
  35. package/dist/components/pdf-canvas/pdf-canvas.js +11 -5
  36. package/dist/components/pdf-canvas/pdf-canvas.js.map +1 -1
  37. package/dist/components/pdf-editor/pdf-editor.d.ts +2 -0
  38. package/dist/components/pdf-editor/pdf-editor.d.ts.map +1 -1
  39. package/dist/components/pdf-editor/pdf-editor.js +2 -2
  40. package/dist/components/pdf-editor/pdf-editor.js.map +1 -1
  41. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js +4 -1
  42. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js.map +1 -1
  43. package/dist/components/pdf-fields-overlay/pdf-overlay-field-e-sign.d.ts.map +1 -1
  44. package/dist/components/pdf-fields-overlay/pdf-overlay-field-e-sign.js +3 -2
  45. package/dist/components/pdf-fields-overlay/pdf-overlay-field-e-sign.js.map +1 -1
  46. package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts.map +1 -1
  47. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js +4 -2
  48. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js.map +1 -1
  49. package/dist/components/pdf-view/pdf-view-e-sign.d.ts.map +1 -1
  50. package/dist/components/pdf-view/pdf-view-e-sign.js +3 -2
  51. package/dist/components/pdf-view/pdf-view-e-sign.js.map +1 -1
  52. package/dist/components/pdf-view/pdf-view-field-container.d.ts.map +1 -1
  53. package/dist/components/pdf-view/pdf-view-field-container.js +10 -3
  54. package/dist/components/pdf-view/pdf-view-field-container.js.map +1 -1
  55. package/dist/components/pdf-view/pdf-view-fillable.js +1 -1
  56. package/dist/components/pdf-view/pdf-view-fillable.js.map +1 -1
  57. package/dist/components/pdf-view/pdf-view.d.ts +5 -0
  58. package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
  59. package/dist/components/pdf-view/pdf-view.js +51 -24
  60. package/dist/components/pdf-view/pdf-view.js.map +1 -1
  61. package/dist/constants/field.constants.d.ts.map +1 -1
  62. package/dist/constants/field.constants.js +27 -6
  63. package/dist/constants/field.constants.js.map +1 -1
  64. package/dist/constants/menu-group.d.ts +0 -5
  65. package/dist/constants/menu-group.d.ts.map +1 -1
  66. package/dist/constants/menu-group.js +2 -5
  67. package/dist/constants/menu-group.js.map +1 -1
  68. package/dist/hooks/index.d.ts +1 -0
  69. package/dist/hooks/index.d.ts.map +1 -1
  70. package/dist/hooks/index.js +1 -0
  71. package/dist/hooks/index.js.map +1 -1
  72. package/dist/hooks/usePinchZoom.d.ts +14 -0
  73. package/dist/hooks/usePinchZoom.d.ts.map +1 -0
  74. package/dist/hooks/usePinchZoom.js +58 -0
  75. package/dist/hooks/usePinchZoom.js.map +1 -0
  76. package/dist/hooks/useZoom.d.ts +2 -0
  77. package/dist/hooks/useZoom.d.ts.map +1 -1
  78. package/dist/hooks/useZoom.js +18 -6
  79. package/dist/hooks/useZoom.js.map +1 -1
  80. package/dist/interface/types.d.ts +2 -0
  81. package/dist/interface/types.d.ts.map +1 -1
  82. package/dist/interface/types.js.map +1 -1
  83. package/dist/styles/pdf-canvas.css +91 -15
  84. package/dist/styles/pdf-editor.css +1 -0
  85. package/dist/styles/pdf-field-overlay.css +109 -78
  86. package/dist/styles/variables.css +3 -0
  87. package/dist/utils/field/get-field-type-option.utils.d.ts +3 -0
  88. package/dist/utils/field/get-field-type-option.utils.d.ts.map +1 -0
  89. package/dist/utils/field/get-field-type-option.utils.js +10 -0
  90. package/dist/utils/field/get-field-type-option.utils.js.map +1 -0
  91. package/dist/utils/field/get-navigable-fields.utils.js +1 -1
  92. package/dist/utils/field/get-navigable-fields.utils.js.map +1 -1
  93. package/dist/utils/field/index.d.ts +1 -0
  94. package/dist/utils/field/index.d.ts.map +1 -1
  95. package/dist/utils/field/index.js +1 -0
  96. package/dist/utils/field/index.js.map +1 -1
  97. package/dist/utils/pdf/index.d.ts +1 -0
  98. package/dist/utils/pdf/index.d.ts.map +1 -1
  99. package/dist/utils/pdf/index.js +1 -0
  100. package/dist/utils/pdf/index.js.map +1 -1
  101. package/dist/utils/pdf/pinch-zoom.utils.d.ts +6 -0
  102. package/dist/utils/pdf/pinch-zoom.utils.d.ts.map +1 -0
  103. package/dist/utils/pdf/pinch-zoom.utils.js +18 -0
  104. package/dist/utils/pdf/pinch-zoom.utils.js.map +1 -0
  105. package/package.json +1 -1
  106. package/src/components/field-sidebar/calculated-field-type-list.tsx +1 -0
  107. package/src/components/field-sidebar/data-model-field-type-list.tsx +28 -103
  108. package/src/components/field-sidebar/e-sign-field-type-list.tsx +1 -0
  109. package/src/components/field-sidebar/field-sidebar.tsx +18 -15
  110. package/src/components/field-sidebar/field-type.tsx +13 -3
  111. package/src/components/field-sidebar/fillable-field-type-list.tsx +1 -0
  112. package/src/components/field-sidebar/form-fields-type-list.tsx +94 -109
  113. package/src/components/field-sidebar/generic-field-type-list.tsx +1 -0
  114. package/src/components/pdf-canvas/canvas-controls.tsx +30 -128
  115. package/src/components/pdf-canvas/pdf-canvas.tsx +18 -8
  116. package/src/components/pdf-editor/pdf-editor.tsx +4 -1
  117. package/src/components/pdf-fields-overlay/pdf-fields-overlay.tsx +1 -1
  118. package/src/components/pdf-fields-overlay/pdf-overlay-field-e-sign.tsx +8 -6
  119. package/src/components/pdf-fields-overlay/pdf-overlay-field.tsx +17 -1
  120. package/src/components/pdf-view/pdf-view-e-sign.tsx +8 -6
  121. package/src/components/pdf-view/pdf-view-field-container.tsx +21 -1
  122. package/src/components/pdf-view/pdf-view-fillable.tsx +1 -1
  123. package/src/components/pdf-view/pdf-view.tsx +156 -95
  124. package/src/constants/field.constants.ts +28 -6
  125. package/src/constants/menu-group.ts +2 -5
  126. package/src/hooks/index.ts +1 -0
  127. package/src/hooks/usePinchZoom.ts +86 -0
  128. package/src/hooks/useZoom.ts +20 -8
  129. package/src/interface/types.ts +3 -0
  130. package/src/styles/pdf-canvas.css +91 -15
  131. package/src/styles/pdf-editor.css +1 -0
  132. package/src/styles/pdf-field-overlay.css +109 -78
  133. package/src/styles/variables.css +3 -0
  134. package/src/utils/field/__tests__/get-field-type-option.utils.test.ts +53 -0
  135. package/src/utils/field/get-field-type-option.utils.ts +16 -0
  136. package/src/utils/field/get-navigable-fields.utils.ts +1 -1
  137. package/src/utils/field/index.ts +1 -0
  138. package/src/utils/pdf/__tests__/pinch-zoom.utils.test.ts +58 -0
  139. package/src/utils/pdf/index.ts +1 -0
  140. package/src/utils/pdf/pinch-zoom.utils.ts +30 -0
@@ -1,8 +1,14 @@
1
+ import { Icon } from '@servicetitan/anvil2';
1
2
  import { FC, MouseEvent, RefObject } from 'react';
2
3
  import { DISABLE_FIELD_RESIZE } from '../../constants';
3
4
  import { useFieldDrag, useFieldResize } from '../../hooks';
4
5
  import { FieldTypeEnum, type PdfField } from '../../interface/types';
5
- import { getFieldBackgroundColor, getFieldBoundsStyle, getPagePosition } from '../../utils';
6
+ import {
7
+ getFieldBackgroundColor,
8
+ getFieldBoundsStyle,
9
+ getFieldTypeOption,
10
+ getPagePosition,
11
+ } from '../../utils';
6
12
  import { PdfOverlayFieldCalculated } from './pdf-overlay-field-calculated';
7
13
  import { PdfOverlayFieldDataModel } from './pdf-overlay-field-data-model';
8
14
  import { PdfOverlayFieldESign } from './pdf-overlay-field-e-sign';
@@ -85,6 +91,8 @@ export const PdfOverlayField: FC<PdfOverlayFieldProps> = ({
85
91
  .filter(Boolean)
86
92
  .join(' ');
87
93
 
94
+ const fieldTypeOption = getFieldTypeOption(field);
95
+
88
96
  return (
89
97
  <div
90
98
  title={field.description}
@@ -96,6 +104,14 @@ export const PdfOverlayField: FC<PdfOverlayFieldProps> = ({
96
104
  className={className}
97
105
  style={style}
98
106
  >
107
+ {fieldTypeOption && (
108
+ <div className="dte-pdf-field-label">
109
+ {fieldTypeOption.icon && (
110
+ <Icon svg={fieldTypeOption.icon} size="small" color="inherit" />
111
+ )}
112
+ {fieldTypeOption.label}
113
+ </div>
114
+ )}
99
115
  {field.type === FieldTypeEnum.calculated && (
100
116
  <PdfOverlayFieldCalculated field={field} pathToLabel={pathToLabel} />
101
117
  )}
@@ -1,3 +1,4 @@
1
+ import { Flex } from '@servicetitan/anvil2';
1
2
  import { FC } from 'react';
2
3
  import { type PdfField } from '../../interface/types';
3
4
 
@@ -7,11 +8,12 @@ interface PdfViewESignProps {
7
8
 
8
9
  export const PdfViewESign: FC<PdfViewESignProps> = ({ field }) => {
9
10
  return (
10
- <span>
11
- {field.label} {field.required ? '*' : ''}
12
- <div className="dte-pdf-field-value" style={{ fontSize: '8px' }}>
13
- {field.path}
14
- </div>
15
- </span>
11
+ <Flex
12
+ alignItems="center"
13
+ className="dte-pdf-field-value"
14
+ style={{ fontSize: '8px', height: '100%' }}
15
+ >
16
+ {field.path} {field.required ? '*' : ''}
17
+ </Flex>
16
18
  );
17
19
  };
@@ -1,9 +1,11 @@
1
+ import { Icon } from '@servicetitan/anvil2';
1
2
  import { FC, Fragment, PropsWithChildren, RefObject } from 'react';
2
3
  import { type DataModelValues, type PdfField } from '../../interface/types';
3
4
  import {
4
5
  evaluateDisplayCondition,
5
6
  getFieldBackgroundColor,
6
7
  getFieldBoundsStyle,
8
+ getFieldTypeOption,
7
9
  getPagePosition,
8
10
  } from '../../utils';
9
11
 
@@ -36,6 +38,8 @@ export const PdfViewFieldContainer: FC<PropsWithChildren<PdfViewFieldContainer>>
36
38
 
37
39
  const pagePos = getPagePosition(field.page, pdfWrapperRef);
38
40
  const bgColor = getFieldBackgroundColor(field.recipient, recipientsColors);
41
+ const fieldTypeOption = getFieldTypeOption(field);
42
+
39
43
  return (
40
44
  <Fragment>
41
45
  <div
@@ -45,13 +49,28 @@ export const PdfViewFieldContainer: FC<PropsWithChildren<PdfViewFieldContainer>>
45
49
  style={getFieldBoundsStyle(field, {
46
50
  pageOffset: pagePos,
47
51
  backgroundColor: bgColor,
48
- borderColor: error ? 'red' : undefined,
52
+ borderColor: error ? 'red' : bgColor.replace('1f', '7d'),
49
53
  zoom,
50
54
  })}
51
55
  onFocus={() => onFocus?.(field.id)}
52
56
  >
53
57
  {children}
54
58
  </div>
59
+ {fieldTypeOption && (
60
+ <div
61
+ className="dte-pdf-field-label"
62
+ style={{
63
+ top: pagePos.top + field.y * zoom - 10,
64
+ left: pagePos.left + field.x * zoom + 3,
65
+ position: 'absolute',
66
+ }}
67
+ >
68
+ {fieldTypeOption.icon && (
69
+ <Icon svg={fieldTypeOption.icon} size="small" color="inherit" />
70
+ )}
71
+ {fieldTypeOption.label}
72
+ </div>
73
+ )}
55
74
  {error && (
56
75
  <span
57
76
  style={{
@@ -59,6 +78,7 @@ export const PdfViewFieldContainer: FC<PropsWithChildren<PdfViewFieldContainer>>
59
78
  top: pagePos.top + (field.y + field.height) * zoom + 8,
60
79
  left: pagePos.left + field.x * zoom,
61
80
  minWidth: '100px',
81
+ fontSize: '10px',
62
82
  position: 'absolute',
63
83
  }}
64
84
  >
@@ -31,7 +31,7 @@ interface FillableTextInputProps {
31
31
  }
32
32
 
33
33
  const fillableTextStyles = {
34
- background: 'inherit',
34
+ background: 'transparent',
35
35
  width: '100%',
36
36
  height: '100%',
37
37
  border: 'none',
@@ -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,160 @@ 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
+ if (!field || !navControlsEl) {
148
+ return;
149
+ }
150
+
151
+ const fieldEl = getFieldElement(field.id);
152
+ if (!fieldEl) {
153
+ return;
154
+ }
155
+
156
+ const containerTop = contentContainerRef.current?.getBoundingClientRect().top ?? 0;
157
+ navControlsEl.style.top = `${fieldEl.getBoundingClientRect().top - containerTop}px`;
158
+ }, [currentFieldIndex, navigableFields]);
159
+
160
+ const handleFocusField = (fieldId: string) => {
161
+ const fieldEl = getFieldElement(fieldId);
162
+ if (fieldEl) {
163
+ fieldEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
164
+ const focusableChild = fieldEl.querySelector<HTMLElement>('input, textarea');
165
+ (focusableChild ?? fieldEl)?.focus();
166
+ }
129
167
  };
130
168
 
131
169
  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;
170
+ const fieldIndex = index < 0 || index >= navigableFields.length ? -1 : index;
171
+ const field = navigableFields[fieldIndex];
172
+
173
+ setCurrentFieldIndex(fieldIndex);
174
+ if (field) {
175
+ handleFocusField(field.id);
137
176
  }
138
- const field = navigableFields[i];
177
+ };
139
178
 
140
- setCurrentFieldIndex(i);
141
- handleFocusField(field.id);
179
+ const handleNextField = () => {
180
+ goToField(currentFieldIndex + 1);
142
181
  };
143
182
 
144
183
  const recipientsColors = mapColorsToRecipients(recipients);
145
- const hasNavigableFields = navigableFields.length === 0;
184
+ const hasNavigableFields = navigableFields.length !== 0;
185
+ const shouldShowFieldNavigation = enableInputNavigation && hasNavigableFields;
186
+
146
187
  return (
147
188
  <Flex
148
- flex={1}
149
- className="dte-pdf-editor-theme dte-pdf-editor-content-container --view-mode"
150
189
  direction="column"
190
+ style={{ position: 'relative', minWidth: 0 }}
191
+ className="dte-pdf-editor-theme"
192
+ ref={contentContainerRef}
151
193
  >
152
- {isPdfLoaded && (
153
- <CanvasControls
194
+ {isPdfLoaded && shouldShowFieldNavigation && (
195
+ <div ref={navControlsRef} className="dte-pdf-movable-next-button-wrapper">
196
+ <FieldNavigationControls
197
+ isStarted={currentFieldIndex !== -1}
198
+ onNextField={handleNextField}
199
+ />
200
+ </div>
201
+ )}
202
+ {isPdfLoaded && zoomable && (
203
+ <ZoomControls
204
+ {...zoomProps}
154
205
  zoom={zoom}
155
206
  onZoomIn={zoomIn}
156
207
  onZoomOut={zoomOut}
157
- zoomable={zoomable}
158
208
  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
209
  />
169
210
  )}
170
- <div
171
- ref={pdfWrapperRef}
172
- className="dte-pdf-wrapper"
173
- style={{ width: BASE_PAGE_WIDTH * zoom }}
211
+ {shouldShowFieldNavigation && (
212
+ <Flex
213
+ justifyContent="space-between"
214
+ alignItems="center"
215
+ gap={2}
216
+ className="dte-pdf-canvas-controls"
217
+ >
218
+ <Text style={{ color: '#ffffff' }}>Review and complete</Text>
219
+ <FieldNavigationControls
220
+ isStarted={currentFieldIndex !== -1}
221
+ onNextField={handleNextField}
222
+ />
223
+ </Flex>
224
+ )}
225
+ <Flex
226
+ flex={1}
227
+ className="dte-pdf-editor-content-container --view-mode"
228
+ direction="column"
174
229
  >
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>
230
+ <div
231
+ ref={pdfWrapperRef}
232
+ className="dte-pdf-wrapper"
233
+ style={{ width: BASE_PAGE_WIDTH * zoom }}
234
+ >
235
+ <PdfDocumentRenderer
236
+ pdfUrl={pdfUrl}
237
+ loading={loading}
238
+ pageWidth={BASE_PAGE_WIDTH * zoom}
239
+ errorPlaceholder={errorPlaceholder}
240
+ loadingPlaceholder={loadingPlaceholder}
241
+ onDocumentLoadSuccess={onDocumentLoadSuccess}
242
+ />
243
+ {isPdfLoaded && (
244
+ <div className="dte-pdf-field-overlay --view-mode">
245
+ {fields.map(field => (
246
+ <PdfViewFieldContainer
247
+ key={field.id}
248
+ field={field}
249
+ data={viewMode === 'preview' ? previewData : data}
250
+ error={errors[field.path!] || ''}
251
+ recipientsColors={recipientsColors}
252
+ pdfWrapperRef={pdfWrapperRef}
253
+ zoom={zoom}
254
+ onFocus={handleFieldOverlayFocus}
255
+ >
256
+ {(field.type === FieldTypeEnum.dataModel ||
257
+ field.type === FieldTypeEnum.forms) && (
258
+ <PdfViewDataModel
259
+ field={field}
260
+ data={viewMode === 'preview' ? previewData : data}
261
+ />
262
+ )}
263
+ {field.type === FieldTypeEnum.eSign && (
264
+ <PdfViewESign field={field} />
265
+ )}
266
+ {field.type === FieldTypeEnum.calculated && (
267
+ <PdfViewCalculated
268
+ field={field}
269
+ data={viewMode === 'preview' ? previewData : data}
270
+ holidays={holidays}
271
+ viewMode={viewMode}
272
+ />
273
+ )}
274
+ {field.type === FieldTypeEnum.fillable && (
275
+ <PdfViewFillable
276
+ data={viewMode === 'preview' ? previewData : data}
277
+ field={field}
278
+ readOnly={makeFillableFieldsReadonly}
279
+ fillingBy={fillingBy}
280
+ viewMode={viewMode}
281
+ onDataChange={handleDataChange}
282
+ />
283
+ )}
284
+ {field.type === FieldTypeEnum.generic && (
285
+ <PdfViewGeneric field={field} />
286
+ )}
287
+ </PdfViewFieldContainer>
288
+ ))}
289
+ </div>
290
+ )}
291
+ </div>
292
+ </Flex>
232
293
  </Flex>
233
294
  );
234
295
  };
@@ -1,3 +1,17 @@
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 BadgeIcon from '@servicetitan/anvil2/assets/icons/material/round/badge.svg';
4
+ import CalculateIcon from '@servicetitan/anvil2/assets/icons/material/round/calculate.svg';
5
+ import CalendarMonthIcon from '@servicetitan/anvil2/assets/icons/material/round/calendar_month.svg';
6
+ import CheckboxIcon from '@servicetitan/anvil2/assets/icons/material/round/check_box.svg';
7
+ import EventAvailableIcon from '@servicetitan/anvil2/assets/icons/material/round/event_available.svg';
8
+ import GestureIcon from '@servicetitan/anvil2/assets/icons/material/round/gesture.svg';
9
+ import RadioIcon from '@servicetitan/anvil2/assets/icons/material/round/radio_button_checked.svg';
10
+ import ShortTextIcon from '@servicetitan/anvil2/assets/icons/material/round/short_text.svg';
11
+ import TableChartIcon from '@servicetitan/anvil2/assets/icons/material/round/table_chart.svg';
12
+ import TextIcon from '@servicetitan/anvil2/assets/icons/material/round/text_fields.svg';
13
+ import IconAppointmentsIcon from '@servicetitan/anvil2/assets/icons/st/appointments.svg';
14
+
1
15
  import {
2
16
  type CalculatedFormatForDate,
3
17
  type CalculatedFormatForNumber,
@@ -44,11 +58,11 @@ export const GENERIC_FIELD_DEFAULT_SIZES = {
44
58
  } as Record<PdfFieldSubType, { width: number; height: number }>;
45
59
 
46
60
  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' },
61
+ { label: 'Text Field', type: FieldTypeEnum.fillable, subType: 'text', icon: ShortTextIcon },
62
+ { label: 'Number Field', type: FieldTypeEnum.fillable, subType: 'number', icon: NumbersIcon },
63
+ { label: 'Date Field', type: FieldTypeEnum.fillable, subType: 'date', icon: CalendarMonthIcon },
64
+ { label: 'Checkbox', type: FieldTypeEnum.fillable, subType: 'checkbox', icon: CheckboxIcon },
65
+ { label: 'Radio', type: FieldTypeEnum.fillable, subType: 'radio', icon: RadioIcon },
52
66
  ];
53
67
 
54
68
  export const E_SIGN_FIELD_TYPES: FieldTypeOption[] = [
@@ -56,26 +70,31 @@ export const E_SIGN_FIELD_TYPES: FieldTypeOption[] = [
56
70
  label: 'Signature',
57
71
  type: FieldTypeEnum.eSign,
58
72
  subType: ESignFieldType.signature,
73
+ icon: GestureIcon,
59
74
  },
60
75
  {
61
76
  label: 'Initials',
62
77
  type: FieldTypeEnum.eSign,
63
78
  subType: ESignFieldType.initials,
79
+ icon: AbcIcon,
64
80
  },
65
81
  {
66
82
  label: 'Date Signed',
67
83
  type: FieldTypeEnum.eSign,
68
84
  subType: ESignFieldType.dateSigned,
85
+ icon: EventAvailableIcon,
69
86
  },
70
87
  {
71
- label: 'Date Signed Offset',
88
+ label: 'Calculated Singing Date',
72
89
  type: FieldTypeEnum.eSign,
73
90
  subType: ESignFieldType.dateSignedOffset,
91
+ icon: IconAppointmentsIcon,
74
92
  },
75
93
  {
76
94
  label: 'Full Name',
77
95
  type: FieldTypeEnum.eSign,
78
96
  subType: ESignFieldType.fullName,
97
+ icon: BadgeIcon,
79
98
  },
80
99
  ];
81
100
 
@@ -83,6 +102,7 @@ export const CALCULATED_FIELD_TYPES: FieldTypeOption[] = [
83
102
  {
84
103
  label: 'Calculated Field',
85
104
  type: FieldTypeEnum.calculated,
105
+ icon: CalculateIcon,
86
106
  },
87
107
  ];
88
108
 
@@ -91,11 +111,13 @@ export const GENERIC_FIELD_TYPES: FieldTypeOption[] = [
91
111
  label: 'Text',
92
112
  type: FieldTypeEnum.generic,
93
113
  subType: 'text',
114
+ icon: TextIcon,
94
115
  },
95
116
  {
96
117
  label: 'Table',
97
118
  type: FieldTypeEnum.generic,
98
119
  subType: 'table',
120
+ icon: TableChartIcon,
99
121
  },
100
122
  ];
101
123
 
@@ -1,5 +1,6 @@
1
1
  import IconBorderColor from '@servicetitan/anvil2/assets/icons/material/round/border_color.svg';
2
2
  import IconCalculate from '@servicetitan/anvil2/assets/icons/material/round/calculate.svg';
3
+ import IconFactCheck from '@servicetitan/anvil2/assets/icons/material/round/fact_check.svg';
3
4
  import IconPhotoSizeSelectSmall from '@servicetitan/anvil2/assets/icons/material/round/photo_size_select_small.svg';
4
5
  import IconCommercial from '@servicetitan/anvil2/assets/icons/st/commercial.svg';
5
6
  import IconEstimate from '@servicetitan/anvil2/assets/icons/st/estimate.svg';
@@ -11,11 +12,6 @@ export interface MenuGroupModel {
11
12
  key: FieldTypeEnum;
12
13
  }
13
14
 
14
- /**
15
- * Sidebar tabs. Forms (`FieldTypeEnum.forms`) has no tab of its own: form fields are offered as a
16
- * group inside Merge Tags, next to the data model groups. `hideFields: [FieldTypeEnum.forms]` still
17
- * hides that group.
18
- */
19
15
  export const menuGroups: MenuGroupModel[] = [
20
16
  { svgIcon: IconEstimate, label: 'Merge Tags', key: FieldTypeEnum.dataModel },
21
17
  { svgIcon: IconBorderColor, label: 'E-Sign', key: FieldTypeEnum.eSign },
@@ -34,4 +30,5 @@ export const menuGroups: MenuGroupModel[] = [
34
30
  label: 'Generic Fields',
35
31
  key: FieldTypeEnum.generic,
36
32
  },
33
+ { svgIcon: IconFactCheck, label: 'Forms', key: FieldTypeEnum.forms },
37
34
  ];
@@ -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';
@@ -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
+ };