@servicetitan/dte-pdf-editor 1.90.0 → 1.92.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.
- package/dist/components/field-sidebar/calculated-field-type-list.d.ts.map +1 -1
- package/dist/components/field-sidebar/calculated-field-type-list.js +1 -1
- package/dist/components/field-sidebar/calculated-field-type-list.js.map +1 -1
- package/dist/components/field-sidebar/e-sign-field-type-list.d.ts.map +1 -1
- package/dist/components/field-sidebar/e-sign-field-type-list.js +1 -1
- package/dist/components/field-sidebar/e-sign-field-type-list.js.map +1 -1
- package/dist/components/field-sidebar/field-type.d.ts +2 -0
- package/dist/components/field-sidebar/field-type.d.ts.map +1 -1
- package/dist/components/field-sidebar/field-type.js +2 -2
- package/dist/components/field-sidebar/field-type.js.map +1 -1
- package/dist/components/field-sidebar/fillable-field-type-list.d.ts.map +1 -1
- package/dist/components/field-sidebar/fillable-field-type-list.js +1 -1
- package/dist/components/field-sidebar/fillable-field-type-list.js.map +1 -1
- package/dist/components/field-sidebar/generic-field-type-list.d.ts.map +1 -1
- package/dist/components/field-sidebar/generic-field-type-list.js +1 -1
- package/dist/components/field-sidebar/generic-field-type-list.js.map +1 -1
- package/dist/components/pdf-canvas/canvas-controls.d.ts +9 -15
- package/dist/components/pdf-canvas/canvas-controls.d.ts.map +1 -1
- package/dist/components/pdf-canvas/canvas-controls.js +4 -14
- package/dist/components/pdf-canvas/canvas-controls.js.map +1 -1
- package/dist/components/pdf-canvas/pdf-canvas.d.ts +6 -2
- package/dist/components/pdf-canvas/pdf-canvas.d.ts.map +1 -1
- package/dist/components/pdf-canvas/pdf-canvas.js +11 -5
- package/dist/components/pdf-canvas/pdf-canvas.js.map +1 -1
- package/dist/components/pdf-editor/pdf-editor.d.ts +2 -0
- package/dist/components/pdf-editor/pdf-editor.d.ts.map +1 -1
- package/dist/components/pdf-editor/pdf-editor.js +2 -2
- package/dist/components/pdf-editor/pdf-editor.js.map +1 -1
- package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js +4 -1
- package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js.map +1 -1
- package/dist/components/pdf-fields-overlay/pdf-overlay-field-e-sign.d.ts.map +1 -1
- package/dist/components/pdf-fields-overlay/pdf-overlay-field-e-sign.js +3 -2
- package/dist/components/pdf-fields-overlay/pdf-overlay-field-e-sign.js.map +1 -1
- package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts.map +1 -1
- package/dist/components/pdf-fields-overlay/pdf-overlay-field.js +4 -2
- package/dist/components/pdf-fields-overlay/pdf-overlay-field.js.map +1 -1
- package/dist/components/pdf-view/pdf-view-e-sign.d.ts.map +1 -1
- package/dist/components/pdf-view/pdf-view-e-sign.js +3 -2
- package/dist/components/pdf-view/pdf-view-e-sign.js.map +1 -1
- package/dist/components/pdf-view/pdf-view-field-container.d.ts.map +1 -1
- package/dist/components/pdf-view/pdf-view-field-container.js +10 -3
- package/dist/components/pdf-view/pdf-view-field-container.js.map +1 -1
- package/dist/components/pdf-view/pdf-view-fillable.js +1 -1
- package/dist/components/pdf-view/pdf-view-fillable.js.map +1 -1
- package/dist/components/pdf-view/pdf-view.d.ts +5 -0
- package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
- package/dist/components/pdf-view/pdf-view.js +51 -24
- package/dist/components/pdf-view/pdf-view.js.map +1 -1
- package/dist/constants/field.constants.d.ts.map +1 -1
- package/dist/constants/field.constants.js +27 -6
- package/dist/constants/field.constants.js.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/usePinchZoom.d.ts +14 -0
- package/dist/hooks/usePinchZoom.d.ts.map +1 -0
- package/dist/hooks/usePinchZoom.js +58 -0
- package/dist/hooks/usePinchZoom.js.map +1 -0
- package/dist/hooks/useZoom.d.ts +2 -0
- package/dist/hooks/useZoom.d.ts.map +1 -1
- package/dist/hooks/useZoom.js +18 -6
- package/dist/hooks/useZoom.js.map +1 -1
- package/dist/interface/types.d.ts +2 -0
- package/dist/interface/types.d.ts.map +1 -1
- package/dist/interface/types.js.map +1 -1
- package/dist/styles/pdf-canvas.css +91 -15
- package/dist/styles/pdf-editor.css +1 -0
- package/dist/styles/pdf-field-overlay.css +109 -78
- package/dist/styles/variables.css +3 -0
- package/dist/utils/field/get-field-type-option.utils.d.ts +3 -0
- package/dist/utils/field/get-field-type-option.utils.d.ts.map +1 -0
- package/dist/utils/field/get-field-type-option.utils.js +10 -0
- package/dist/utils/field/get-field-type-option.utils.js.map +1 -0
- package/dist/utils/field/get-navigable-fields.utils.js +1 -1
- package/dist/utils/field/get-navigable-fields.utils.js.map +1 -1
- package/dist/utils/field/index.d.ts +1 -0
- package/dist/utils/field/index.d.ts.map +1 -1
- package/dist/utils/field/index.js +1 -0
- package/dist/utils/field/index.js.map +1 -1
- package/dist/utils/pdf/index.d.ts +1 -0
- package/dist/utils/pdf/index.d.ts.map +1 -1
- package/dist/utils/pdf/index.js +1 -0
- package/dist/utils/pdf/index.js.map +1 -1
- package/dist/utils/pdf/pinch-zoom.utils.d.ts +6 -0
- package/dist/utils/pdf/pinch-zoom.utils.d.ts.map +1 -0
- package/dist/utils/pdf/pinch-zoom.utils.js +18 -0
- package/dist/utils/pdf/pinch-zoom.utils.js.map +1 -0
- package/package.json +1 -1
- package/src/components/field-sidebar/calculated-field-type-list.tsx +1 -0
- package/src/components/field-sidebar/e-sign-field-type-list.tsx +1 -0
- package/src/components/field-sidebar/field-type.tsx +13 -3
- package/src/components/field-sidebar/fillable-field-type-list.tsx +1 -0
- package/src/components/field-sidebar/generic-field-type-list.tsx +1 -0
- package/src/components/pdf-canvas/canvas-controls.tsx +30 -128
- package/src/components/pdf-canvas/pdf-canvas.tsx +18 -8
- package/src/components/pdf-editor/pdf-editor.tsx +4 -1
- package/src/components/pdf-fields-overlay/pdf-fields-overlay.tsx +1 -1
- package/src/components/pdf-fields-overlay/pdf-overlay-field-e-sign.tsx +8 -6
- package/src/components/pdf-fields-overlay/pdf-overlay-field.tsx +17 -1
- package/src/components/pdf-view/pdf-view-e-sign.tsx +8 -6
- package/src/components/pdf-view/pdf-view-field-container.tsx +21 -1
- package/src/components/pdf-view/pdf-view-fillable.tsx +1 -1
- package/src/components/pdf-view/pdf-view.tsx +156 -95
- package/src/constants/field.constants.ts +28 -6
- package/src/hooks/index.ts +1 -0
- package/src/hooks/usePinchZoom.ts +86 -0
- package/src/hooks/useZoom.ts +20 -8
- package/src/interface/types.ts +3 -0
- package/src/styles/pdf-canvas.css +91 -15
- package/src/styles/pdf-editor.css +1 -0
- package/src/styles/pdf-field-overlay.css +109 -78
- package/src/styles/variables.css +3 -0
- package/src/utils/field/__tests__/get-field-type-option.utils.test.ts +53 -0
- package/src/utils/field/get-field-type-option.utils.ts +16 -0
- package/src/utils/field/get-navigable-fields.utils.ts +1 -1
- package/src/utils/field/index.ts +1 -0
- package/src/utils/pdf/__tests__/pinch-zoom.utils.test.ts +58 -0
- package/src/utils/pdf/index.ts +1 -0
- package/src/utils/pdf/pinch-zoom.utils.ts +30 -0
|
@@ -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 {
|
|
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
|
|
139
|
+
const getFieldElement = (fieldId: string) => {
|
|
124
140
|
const root = pdfWrapperRef.current?.getRootNode() as Document | ShadowRoot | undefined;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
177
|
+
};
|
|
139
178
|
|
|
140
|
-
|
|
141
|
-
|
|
179
|
+
const handleNextField = () => {
|
|
180
|
+
goToField(currentFieldIndex + 1);
|
|
142
181
|
};
|
|
143
182
|
|
|
144
183
|
const recipientsColors = mapColorsToRecipients(recipients);
|
|
145
|
-
const hasNavigableFields = navigableFields.length
|
|
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
|
-
<
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
{
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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: '
|
|
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
|
|
package/src/hooks/index.ts
CHANGED
|
@@ -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
|
+
};
|
package/src/hooks/useZoom.ts
CHANGED
|
@@ -1,25 +1,37 @@
|
|
|
1
1
|
import { useCallback, useState } from 'react';
|
|
2
|
-
import { ZOOM_DEFAULT,
|
|
2
|
+
import { ZOOM_DEFAULT, ZOOM_STEP } from '../constants';
|
|
3
|
+
import { clampZoom } from '../utils';
|
|
3
4
|
|
|
4
5
|
export interface UseZoomReturn {
|
|
5
6
|
zoom: number;
|
|
6
7
|
zoomIn(): void;
|
|
7
8
|
zoomOut(): void;
|
|
8
9
|
resetZoom(): void;
|
|
10
|
+
setZoomValue(value: number): void;
|
|
11
|
+
zoomByFactor(factor: number): void;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
const clampZoom = (value: number) =>
|
|
12
|
-
Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, Math.round(value * 100) / 100));
|
|
13
|
-
|
|
14
14
|
/**
|
|
15
15
|
* Manages a page zoom factor (1 = 100%), clamped to [ZOOM_MIN, ZOOM_MAX] in ZOOM_STEP increments.
|
|
16
16
|
*/
|
|
17
17
|
export const useZoom = (initialZoom: number = ZOOM_DEFAULT): UseZoomReturn => {
|
|
18
18
|
const [zoom, setZoom] = useState<number>(initialZoom);
|
|
19
19
|
|
|
20
|
-
const zoomIn = useCallback(() =>
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const zoomIn = useCallback(() => {
|
|
21
|
+
setZoom(prev => clampZoom(prev + ZOOM_STEP));
|
|
22
|
+
}, []);
|
|
23
|
+
const zoomOut = useCallback(() => {
|
|
24
|
+
setZoom(prev => clampZoom(prev - ZOOM_STEP));
|
|
25
|
+
}, []);
|
|
26
|
+
const resetZoom = useCallback(() => {
|
|
27
|
+
setZoom(ZOOM_DEFAULT);
|
|
28
|
+
}, []);
|
|
29
|
+
const setZoomValue = useCallback((value: number) => {
|
|
30
|
+
setZoom(clampZoom(value));
|
|
31
|
+
}, []);
|
|
32
|
+
const zoomByFactor = useCallback((factor: number) => {
|
|
33
|
+
setZoom(prev => clampZoom(prev * factor));
|
|
34
|
+
}, []);
|
|
23
35
|
|
|
24
|
-
return { zoom, zoomIn, zoomOut
|
|
36
|
+
return { resetZoom, setZoomValue, zoom, zoomByFactor, zoomIn, zoomOut };
|
|
25
37
|
};
|
package/src/interface/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Svg } from '@servicetitan/anvil2';
|
|
2
|
+
|
|
1
3
|
export enum FieldTypeEnum {
|
|
2
4
|
dataModel = 'dataModel',
|
|
3
5
|
forms = 'forms',
|
|
@@ -164,6 +166,7 @@ export interface FieldTypeOption {
|
|
|
164
166
|
fieldType?: SchemaFieldType;
|
|
165
167
|
formSnapshot?: FormulaFieldFormSnapshot;
|
|
166
168
|
sampleData?: unknown;
|
|
169
|
+
icon?: Svg;
|
|
167
170
|
}
|
|
168
171
|
|
|
169
172
|
export interface DataModelFieldGroup {
|