@servicetitan/dte-pdf-editor 1.77.0 → 1.78.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/pdf-canvas/canvas-controls.d.ts +24 -0
- package/dist/components/pdf-canvas/canvas-controls.d.ts.map +1 -0
- package/dist/components/pdf-canvas/canvas-controls.js +22 -0
- package/dist/components/pdf-canvas/canvas-controls.js.map +1 -0
- package/dist/components/pdf-canvas/pdf-canvas.d.ts +1 -0
- package/dist/components/pdf-canvas/pdf-canvas.d.ts.map +1 -1
- package/dist/components/pdf-canvas/pdf-canvas.js +6 -3
- package/dist/components/pdf-canvas/pdf-canvas.js.map +1 -1
- package/dist/components/pdf-editor/pdf-editor.d.ts +1 -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.d.ts +1 -0
- package/dist/components/pdf-fields-overlay/pdf-fields-overlay.d.ts.map +1 -1
- package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js +2 -2
- package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js.map +1 -1
- package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts +1 -0
- 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 -1
- package/dist/components/pdf-fields-overlay/pdf-overlay-field.js.map +1 -1
- package/dist/components/pdf-view/pdf-view-field-container.d.ts +1 -0
- package/dist/components/pdf-view/pdf-view-field-container.d.ts.map +1 -1
- package/dist/components/pdf-view/pdf-view-field-container.js +5 -4
- package/dist/components/pdf-view/pdf-view-field-container.js.map +1 -1
- package/dist/components/pdf-view/pdf-view.d.ts +2 -0
- package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
- package/dist/components/pdf-view/pdf-view.js +29 -5
- package/dist/components/pdf-view/pdf-view.js.map +1 -1
- package/dist/constants/pdf-editor.constants.d.ts +4 -0
- package/dist/constants/pdf-editor.constants.d.ts.map +1 -1
- package/dist/constants/pdf-editor.constants.js +4 -0
- package/dist/constants/pdf-editor.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/useFieldDrag.d.ts +2 -1
- package/dist/hooks/useFieldDrag.d.ts.map +1 -1
- package/dist/hooks/useFieldDrag.js +7 -6
- package/dist/hooks/useFieldDrag.js.map +1 -1
- package/dist/hooks/useFieldResize.d.ts +2 -1
- package/dist/hooks/useFieldResize.d.ts.map +1 -1
- package/dist/hooks/useFieldResize.js +7 -3
- package/dist/hooks/useFieldResize.js.map +1 -1
- package/dist/hooks/useZoom.d.ts +11 -0
- package/dist/hooks/useZoom.d.ts.map +1 -0
- package/dist/hooks/useZoom.js +14 -0
- package/dist/hooks/useZoom.js.map +1 -0
- package/dist/styles/pdf-canvas.css +24 -5
- package/dist/styles/pdf-editor.css +1 -0
- package/dist/styles/pdf-field-overlay.css +14 -0
- package/dist/styles/variables.css +10 -1
- package/dist/utils/field/field-drag.utils.d.ts +4 -2
- package/dist/utils/field/field-drag.utils.d.ts.map +1 -1
- package/dist/utils/field/field-drag.utils.js +14 -10
- package/dist/utils/field/field-drag.utils.js.map +1 -1
- package/dist/utils/field/field-reading-order.utils.d.ts +15 -0
- package/dist/utils/field/field-reading-order.utils.d.ts.map +1 -0
- package/dist/utils/field/field-reading-order.utils.js +33 -0
- package/dist/utils/field/field-reading-order.utils.js.map +1 -0
- package/dist/utils/field/field-resize.utils.d.ts +4 -2
- package/dist/utils/field/field-resize.utils.d.ts.map +1 -1
- package/dist/utils/field/field-resize.utils.js +12 -10
- package/dist/utils/field/field-resize.utils.js.map +1 -1
- package/dist/utils/field/get-field-bounds-style.utils.d.ts +7 -0
- package/dist/utils/field/get-field-bounds-style.utils.d.ts.map +1 -1
- package/dist/utils/field/get-field-bounds-style.utils.js +5 -5
- package/dist/utils/field/get-field-bounds-style.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/package.json +1 -1
- package/src/components/pdf-canvas/canvas-controls.tsx +183 -0
- package/src/components/pdf-canvas/pdf-canvas.tsx +64 -33
- package/src/components/pdf-editor/pdf-editor.tsx +12 -2
- package/src/components/pdf-fields-overlay/pdf-fields-overlay.tsx +3 -0
- package/src/components/pdf-fields-overlay/pdf-overlay-field.tsx +5 -0
- package/src/components/pdf-view/pdf-view-field-container.tsx +6 -2
- package/src/components/pdf-view/pdf-view.tsx +78 -5
- package/src/constants/pdf-editor.constants.ts +5 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useFieldDrag.ts +10 -5
- package/src/hooks/useFieldResize.ts +16 -2
- package/src/hooks/useZoom.ts +25 -0
- package/src/styles/pdf-canvas.css +24 -5
- package/src/styles/pdf-editor.css +1 -0
- package/src/styles/pdf-field-overlay.css +14 -0
- package/src/styles/variables.css +10 -1
- package/src/utils/field/__tests__/field-reading-order.utils.test.ts +62 -0
- package/src/utils/field/field-drag.utils.ts +14 -8
- package/src/utils/field/field-reading-order.utils.ts +43 -0
- package/src/utils/field/field-resize.utils.ts +12 -8
- package/src/utils/field/get-field-bounds-style.utils.ts +17 -5
- package/src/utils/field/index.ts +1 -0
|
@@ -10,6 +10,7 @@ interface UseFieldResizeOptions {
|
|
|
10
10
|
newHeight: number,
|
|
11
11
|
pageNumber: number,
|
|
12
12
|
) => void;
|
|
13
|
+
zoom?: number;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export interface UseFieldResizeReturn {
|
|
@@ -24,6 +25,7 @@ export interface UseFieldResizeReturn {
|
|
|
24
25
|
export const useFieldResize = ({
|
|
25
26
|
onFieldResize,
|
|
26
27
|
pdfWrapperRef,
|
|
28
|
+
zoom = 1,
|
|
27
29
|
}: UseFieldResizeOptions): UseFieldResizeReturn => {
|
|
28
30
|
const resizeStartDataRef = useRef<{
|
|
29
31
|
field: PdfField;
|
|
@@ -34,12 +36,17 @@ export const useFieldResize = ({
|
|
|
34
36
|
page: number;
|
|
35
37
|
} | null>(null);
|
|
36
38
|
const onFieldResizeRef = useRef(onFieldResize);
|
|
39
|
+
const zoomRef = useRef(zoom);
|
|
37
40
|
const [isResizing, setIsResizing] = useState(false);
|
|
38
41
|
|
|
39
42
|
useEffect(() => {
|
|
40
43
|
onFieldResizeRef.current = onFieldResize;
|
|
41
44
|
}, [onFieldResize]);
|
|
42
45
|
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
zoomRef.current = zoom;
|
|
48
|
+
}, [zoom]);
|
|
49
|
+
|
|
43
50
|
useEffect(() => {
|
|
44
51
|
if (!isResizing) {
|
|
45
52
|
return;
|
|
@@ -54,7 +61,14 @@ export const useFieldResize = ({
|
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
const { field, ...resizeStartData } = resizeStartDataRef.current;
|
|
57
|
-
handleFieldResize(
|
|
64
|
+
handleFieldResize(
|
|
65
|
+
e,
|
|
66
|
+
field,
|
|
67
|
+
resizeStartData,
|
|
68
|
+
pdfWrapperRef,
|
|
69
|
+
onFieldResizeRef.current,
|
|
70
|
+
zoomRef.current,
|
|
71
|
+
);
|
|
58
72
|
};
|
|
59
73
|
|
|
60
74
|
const handleMouseUp = () => {
|
|
@@ -78,7 +92,7 @@ export const useFieldResize = ({
|
|
|
78
92
|
e.stopPropagation();
|
|
79
93
|
e.preventDefault();
|
|
80
94
|
|
|
81
|
-
const resizeStartData = handleFieldResizeStart(e, field, pdfWrapperRef);
|
|
95
|
+
const resizeStartData = handleFieldResizeStart(e, field, pdfWrapperRef, zoom);
|
|
82
96
|
if (resizeStartData) {
|
|
83
97
|
resizeStartDataRef.current = {
|
|
84
98
|
field,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import { ZOOM_DEFAULT, ZOOM_MAX, ZOOM_MIN, ZOOM_STEP } from '../constants';
|
|
3
|
+
|
|
4
|
+
export interface UseZoomReturn {
|
|
5
|
+
zoom: number;
|
|
6
|
+
zoomIn(): void;
|
|
7
|
+
zoomOut(): void;
|
|
8
|
+
resetZoom(): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const clampZoom = (value: number) =>
|
|
12
|
+
Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, Math.round(value * 100) / 100));
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Manages a page zoom factor (1 = 100%), clamped to [ZOOM_MIN, ZOOM_MAX] in ZOOM_STEP increments.
|
|
16
|
+
*/
|
|
17
|
+
export const useZoom = (initialZoom: number = ZOOM_DEFAULT): UseZoomReturn => {
|
|
18
|
+
const [zoom, setZoom] = useState<number>(initialZoom);
|
|
19
|
+
|
|
20
|
+
const zoomIn = useCallback(() => setZoom(prev => clampZoom(prev + ZOOM_STEP)), []);
|
|
21
|
+
const zoomOut = useCallback(() => setZoom(prev => clampZoom(prev - ZOOM_STEP)), []);
|
|
22
|
+
const resetZoom = useCallback(() => setZoom(ZOOM_DEFAULT), []);
|
|
23
|
+
|
|
24
|
+
return { zoom, zoomIn, zoomOut, resetZoom };
|
|
25
|
+
};
|
|
@@ -1,11 +1,30 @@
|
|
|
1
|
-
.dte-pdf-canvas-container {
|
|
2
|
-
overflow: auto;
|
|
3
|
-
position: relative;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
1
|
.dte-pdf-wrapper {
|
|
7
2
|
position: relative;
|
|
8
3
|
width: 100%;
|
|
9
4
|
margin-inline-start: auto;
|
|
10
5
|
margin-inline-end: auto;
|
|
11
6
|
}
|
|
7
|
+
|
|
8
|
+
.dte-pdf-canvas-controls {
|
|
9
|
+
z-index: 1;
|
|
10
|
+
position: sticky;
|
|
11
|
+
top: 0;
|
|
12
|
+
left: 0;
|
|
13
|
+
background: var(--bg-color-main, #fafafa);
|
|
14
|
+
box-shadow: 0 2px 4px rgb(0 0 0 / 15%);
|
|
15
|
+
padding: var(--spacing-half) var(--spacing-1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.dte-pdf-zoom-controls-value {
|
|
19
|
+
min-width: 44px;
|
|
20
|
+
text-align: center;
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
user-select: none;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.dte-pdf-zoom-controls,
|
|
26
|
+
.dte-pdf-field-navigation-controls {
|
|
27
|
+
border-radius: var(--radius-1, 4px);
|
|
28
|
+
padding: var(--spacing-half);
|
|
29
|
+
background-color: var(--bg-color-secondary, #eeeeee);
|
|
30
|
+
}
|
|
@@ -101,6 +101,15 @@
|
|
|
101
101
|
line-height: 1;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
.dte-pdf-field-content input[type='checkbox']:focus,
|
|
105
|
+
.dte-pdf-field-content input[type='checkbox']:active,
|
|
106
|
+
.dte-pdf-field-content input[type='radio']:active,
|
|
107
|
+
.dte-pdf-field-content input[type='radio']:focus {
|
|
108
|
+
box-shadow:
|
|
109
|
+
0 0 0 2px var(--border-color-active, #2270ee) inset,
|
|
110
|
+
0 0 0 3px var(--color-neutral-0, #ffffff) inset;
|
|
111
|
+
}
|
|
112
|
+
|
|
104
113
|
/*
|
|
105
114
|
* Printed PDF text modes (no ellipsis — hard clip like PdfSharp).
|
|
106
115
|
*/
|
|
@@ -169,6 +178,11 @@
|
|
|
169
178
|
resize: none;
|
|
170
179
|
}
|
|
171
180
|
|
|
181
|
+
.dte-pdf-field-fillable:focus {
|
|
182
|
+
outline: 2px solid var(--border-color-active, #2270ee);
|
|
183
|
+
outline-offset: 2px;
|
|
184
|
+
}
|
|
185
|
+
|
|
172
186
|
.dte-pdf-field-generic-table {
|
|
173
187
|
padding: var(--spacing-0, 0);
|
|
174
188
|
margin: var(--spacing-0, 0);
|
package/src/styles/variables.css
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
@import '@servicetitan/tokens/dist/tokens.css';
|
|
2
2
|
|
|
3
|
-
.dte-pdf-editor {
|
|
3
|
+
.dte-pdf-editor-theme {
|
|
4
4
|
--menu-active-color: var(--color-blue-400, #2270ee);
|
|
5
5
|
--menu-active-bg-light-color: var(--color-blue-100, #e0f2ff);
|
|
6
6
|
--menu-hover-color: var(--color-neutral-100, #606162);
|
|
@@ -16,14 +16,23 @@
|
|
|
16
16
|
--spacing-2: 16px;
|
|
17
17
|
--spacing-3: 24px;
|
|
18
18
|
|
|
19
|
+
/* Radius */
|
|
20
|
+
--radius-0: 0;
|
|
21
|
+
--radius-1: 4px;
|
|
22
|
+
--radius-2: 8px;
|
|
23
|
+
|
|
19
24
|
/* Colors - Neutral */
|
|
20
25
|
--color-neutral-0: var(--color-neutral-0, #ffffff);
|
|
26
|
+
--color-neutral-30: var(--color-neutral-30, #f7f7f7);
|
|
27
|
+
--color-neutral-50: var(--color-neutral-50, #eeeeee);
|
|
21
28
|
--color-neutral-70: var(--color-neutral-70, #737475);
|
|
22
29
|
--color-neutral-200: var(--color-neutral-200, #6c6d70);
|
|
23
30
|
--color-neutral-300: var(--color-neutral-300, #2d2e31);
|
|
24
31
|
|
|
25
32
|
/* Background colors */
|
|
26
33
|
--bg-color-light: var(--color-neutral-0, #ffffff);
|
|
34
|
+
--bg-color-main: var(--color-neutral-20, #fafafa);
|
|
35
|
+
--bg-color-secondary: var(--color-neutral-50, #eeeeee);
|
|
27
36
|
--bg-color: var(--color-neutral-200, #f5f5f5);
|
|
28
37
|
--bg-color-hover: #eaeaea;
|
|
29
38
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { sortFieldsByReadingOrder } from '../field-reading-order.utils';
|
|
2
|
+
|
|
3
|
+
interface TestField {
|
|
4
|
+
id: string;
|
|
5
|
+
page: number;
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const field = (id: string, page: number, x: number, y: number, height = 20): TestField => ({
|
|
12
|
+
id,
|
|
13
|
+
page,
|
|
14
|
+
x,
|
|
15
|
+
y,
|
|
16
|
+
height,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('sortFieldsByReadingOrder', () => {
|
|
20
|
+
const ids = (fields: TestField[]) => fields.map(f => f.id);
|
|
21
|
+
|
|
22
|
+
test('orders fields left-to-right within the same row', () => {
|
|
23
|
+
const fields = [field('right', 1, 300, 100), field('left', 1, 50, 100)];
|
|
24
|
+
|
|
25
|
+
expect(ids(sortFieldsByReadingOrder(fields))).toEqual(['left', 'right']);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('orders fields top-to-bottom across rows', () => {
|
|
29
|
+
const fields = [field('bottom', 1, 50, 300), field('top', 1, 50, 50)];
|
|
30
|
+
|
|
31
|
+
expect(ids(sortFieldsByReadingOrder(fields))).toEqual(['top', 'bottom']);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('keeps a field left-of-and-below another on the same row grouped correctly', () => {
|
|
35
|
+
// "left" starts slightly lower than "right" but they visually sit on the same row.
|
|
36
|
+
const fields = [field('right', 1, 500, 100), field('left', 1, 50, 105)];
|
|
37
|
+
|
|
38
|
+
expect(ids(sortFieldsByReadingOrder(fields))).toEqual(['left', 'right']);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('does not merge a lower row into a taller row above it', () => {
|
|
42
|
+
const tall = field('tall', 1, 50, 50, 200);
|
|
43
|
+
const nextRow = field('next-row', 1, 500, 260);
|
|
44
|
+
|
|
45
|
+
expect(ids(sortFieldsByReadingOrder([nextRow, tall]))).toEqual(['tall', 'next-row']);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('orders by page before position', () => {
|
|
49
|
+
const fields = [field('page2', 2, 0, 0), field('page1', 1, 500, 500)];
|
|
50
|
+
|
|
51
|
+
expect(ids(sortFieldsByReadingOrder(fields))).toEqual(['page1', 'page2']);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('does not mutate the input array', () => {
|
|
55
|
+
const fields = [field('b', 1, 300, 100), field('a', 1, 50, 100)];
|
|
56
|
+
const copy = [...fields];
|
|
57
|
+
|
|
58
|
+
sortFieldsByReadingOrder(fields);
|
|
59
|
+
|
|
60
|
+
expect(fields).toEqual(copy);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -14,24 +14,28 @@ interface DragOffset {
|
|
|
14
14
|
* @param e - The drag event
|
|
15
15
|
* @param field - The field being dragged
|
|
16
16
|
* @param pdfWrapperRef - Reference to the PDF wrapper element
|
|
17
|
+
* @param zoom - The current zoom level of the PDF
|
|
17
18
|
* @returns The drag offset object or null if wrapper is not available
|
|
18
19
|
*/
|
|
19
20
|
export const handleFieldDragStart = (
|
|
20
21
|
e: DragEvent<HTMLDivElement>,
|
|
21
22
|
field: PdfField,
|
|
22
23
|
pdfWrapperRef: RefObject<HTMLDivElement>,
|
|
24
|
+
zoom = 1,
|
|
23
25
|
): DragOffset | null => {
|
|
24
26
|
if (!pdfWrapperRef.current) {
|
|
25
27
|
return null;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
/*
|
|
29
|
-
* Calculate drag offset in page-relative coordinates
|
|
31
|
+
* Calculate drag offset in page-relative coordinates.
|
|
32
|
+
* DOM measurements reflect the current zoom, so divide by zoom to bring
|
|
33
|
+
* them into the same unzoomed reference space as the stored field.x/y.
|
|
30
34
|
*/
|
|
31
35
|
const startPagePos = getPagePosition(field.page, pdfWrapperRef);
|
|
32
36
|
const wrapperRect = pdfWrapperRef.current.getBoundingClientRect();
|
|
33
|
-
const cursorXInPage = e.clientX - wrapperRect.left - startPagePos.left;
|
|
34
|
-
const cursorYInPage = e.clientY - wrapperRect.top - startPagePos.top;
|
|
37
|
+
const cursorXInPage = (e.clientX - wrapperRect.left - startPagePos.left) / zoom;
|
|
38
|
+
const cursorYInPage = (e.clientY - wrapperRect.top - startPagePos.top) / zoom;
|
|
35
39
|
|
|
36
40
|
const dragOffset: DragOffset = {
|
|
37
41
|
x: cursorXInPage - field.x,
|
|
@@ -65,6 +69,7 @@ export const handleFieldDragStart = (
|
|
|
65
69
|
* @param dragOffset - The drag offset from drag start
|
|
66
70
|
* @param pdfWrapperRef - Reference to the PDF wrapper element
|
|
67
71
|
* @param onFieldMove - Callback to update field position
|
|
72
|
+
* @param zoom - The current zoom level of the PDF
|
|
68
73
|
*/
|
|
69
74
|
export const handleFieldDrag = (
|
|
70
75
|
e: DragEvent<HTMLDivElement>,
|
|
@@ -72,6 +77,7 @@ export const handleFieldDrag = (
|
|
|
72
77
|
dragOffset: DragOffset,
|
|
73
78
|
pdfWrapperRef: RefObject<HTMLDivElement>,
|
|
74
79
|
onFieldMove: (fieldId: string, newX: number, newY: number, pageNumber: number) => void,
|
|
80
|
+
zoom = 1,
|
|
75
81
|
): void => {
|
|
76
82
|
if (!pdfWrapperRef.current || e.clientX === 0 || e.clientY === 0) {
|
|
77
83
|
return;
|
|
@@ -82,9 +88,9 @@ export const handleFieldDrag = (
|
|
|
82
88
|
const wrapperRect = pdfWrapperRef.current.getBoundingClientRect();
|
|
83
89
|
const pageDimensions = getPageDimensions(currentPage, pdfWrapperRef);
|
|
84
90
|
|
|
85
|
-
// Calculate position in page coordinates
|
|
86
|
-
const x = e.clientX - wrapperRect.left - currentPagePos.left - dragOffset.x;
|
|
87
|
-
const y = e.clientY - wrapperRect.top - currentPagePos.top - dragOffset.y;
|
|
91
|
+
// Calculate position in page (unzoomed reference) coordinates
|
|
92
|
+
const x = (e.clientX - wrapperRect.left - currentPagePos.left) / zoom - dragOffset.x;
|
|
93
|
+
const y = (e.clientY - wrapperRect.top - currentPagePos.top) / zoom - dragOffset.y;
|
|
88
94
|
|
|
89
95
|
/*
|
|
90
96
|
* Clamp x and y to stay within page boundaries.
|
|
@@ -92,8 +98,8 @@ export const handleFieldDrag = (
|
|
|
92
98
|
*/
|
|
93
99
|
const minX = 0;
|
|
94
100
|
const minY = 0;
|
|
95
|
-
const maxX = Math.max(0, pageDimensions.width - field.width);
|
|
96
|
-
const maxY = Math.max(0, pageDimensions.height - field.height);
|
|
101
|
+
const maxX = Math.max(0, pageDimensions.width / zoom - field.width);
|
|
102
|
+
const maxY = Math.max(0, pageDimensions.height / zoom - field.height);
|
|
97
103
|
|
|
98
104
|
const clampedX = Math.max(minX, Math.min(maxX, x));
|
|
99
105
|
const clampedY = Math.max(minY, Math.min(maxY, y));
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
interface ReadingOrderField {
|
|
2
|
+
page: number;
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
height: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Orders fields the way a person visually scans a page: top row first,
|
|
10
|
+
* left-to-right within a row, then down to the next row - rather than a
|
|
11
|
+
* strict y-then-x sort, which misorders fields that sit on the same visual
|
|
12
|
+
* row but don't share an exact y coordinate.
|
|
13
|
+
*/
|
|
14
|
+
export const sortFieldsByReadingOrder = <T extends ReadingOrderField>(fields: T[]): T[] => {
|
|
15
|
+
const byPosition = [...fields].sort((a, b) => a.page - b.page || a.y - b.y || a.x - b.x);
|
|
16
|
+
|
|
17
|
+
const rows: T[][] = [];
|
|
18
|
+
let currentRow: T[] = [];
|
|
19
|
+
let currentPage = NaN;
|
|
20
|
+
let rowBottom = -Infinity;
|
|
21
|
+
|
|
22
|
+
for (const field of byPosition) {
|
|
23
|
+
const startsNewRow = field.page !== currentPage || field.y >= rowBottom;
|
|
24
|
+
|
|
25
|
+
if (startsNewRow) {
|
|
26
|
+
if (currentRow.length) {
|
|
27
|
+
rows.push(currentRow);
|
|
28
|
+
}
|
|
29
|
+
currentRow = [field];
|
|
30
|
+
currentPage = field.page;
|
|
31
|
+
rowBottom = field.y + field.height;
|
|
32
|
+
} else {
|
|
33
|
+
currentRow.push(field);
|
|
34
|
+
rowBottom = Math.max(rowBottom, field.y + field.height);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (currentRow.length) {
|
|
39
|
+
rows.push(currentRow);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return rows.flatMap(row => row.sort((a, b) => a.x - b.x));
|
|
43
|
+
};
|
|
@@ -16,12 +16,14 @@ interface ResizeStartData {
|
|
|
16
16
|
* @param e - Mouse event
|
|
17
17
|
* @param field - The field being resized
|
|
18
18
|
* @param pdfWrapperRef - Reference to the PDF wrapper element
|
|
19
|
+
* @param zoom - The current zoom level of the PDF
|
|
19
20
|
* @returns Resize start data or null if calculation fails
|
|
20
21
|
*/
|
|
21
22
|
export const handleFieldResizeStart = (
|
|
22
23
|
e: { clientX: number; clientY: number },
|
|
23
24
|
field: PdfField,
|
|
24
25
|
pdfWrapperRef: RefObject<HTMLDivElement>,
|
|
26
|
+
zoom = 1,
|
|
25
27
|
): ResizeStartData | null => {
|
|
26
28
|
if (!pdfWrapperRef.current) {
|
|
27
29
|
return null;
|
|
@@ -30,9 +32,9 @@ export const handleFieldResizeStart = (
|
|
|
30
32
|
const pagePos = getPagePosition(field.page, pdfWrapperRef);
|
|
31
33
|
const wrapperRect = pdfWrapperRef.current.getBoundingClientRect();
|
|
32
34
|
|
|
33
|
-
// Calculate mouse position relative to the page
|
|
34
|
-
const startX = e.clientX - wrapperRect.left - pagePos.left;
|
|
35
|
-
const startY = e.clientY - wrapperRect.top - pagePos.top;
|
|
35
|
+
// Calculate mouse position relative to the page, in unzoomed reference coordinates
|
|
36
|
+
const startX = (e.clientX - wrapperRect.left - pagePos.left) / zoom;
|
|
37
|
+
const startY = (e.clientY - wrapperRect.top - pagePos.top) / zoom;
|
|
36
38
|
|
|
37
39
|
return {
|
|
38
40
|
startX,
|
|
@@ -51,6 +53,7 @@ export const handleFieldResizeStart = (
|
|
|
51
53
|
* @param resizeStartData - Data from resize start
|
|
52
54
|
* @param pdfWrapperRef - Reference to the PDF wrapper element
|
|
53
55
|
* @param onFieldResize - Callback to update field dimensions
|
|
56
|
+
* @param zoom - The current zoom level of the PDF
|
|
54
57
|
*/
|
|
55
58
|
export const handleFieldResize = (
|
|
56
59
|
e: { clientX: number; clientY: number },
|
|
@@ -63,6 +66,7 @@ export const handleFieldResize = (
|
|
|
63
66
|
newHeight: number,
|
|
64
67
|
pageNumber: number,
|
|
65
68
|
) => void,
|
|
69
|
+
zoom = 1,
|
|
66
70
|
): void => {
|
|
67
71
|
if (!pdfWrapperRef.current) {
|
|
68
72
|
return;
|
|
@@ -72,9 +76,9 @@ export const handleFieldResize = (
|
|
|
72
76
|
const pagePos = getPagePosition(resizeStartData.page, pdfWrapperRef);
|
|
73
77
|
const wrapperRect = pdfWrapperRef.current.getBoundingClientRect();
|
|
74
78
|
|
|
75
|
-
// Calculate mouse position relative to the page
|
|
76
|
-
const mouseX = e.clientX - wrapperRect.left - pagePos.left;
|
|
77
|
-
const mouseY = e.clientY - wrapperRect.top - pagePos.top;
|
|
79
|
+
// Calculate mouse position relative to the page, in unzoomed reference coordinates
|
|
80
|
+
const mouseX = (e.clientX - wrapperRect.left - pagePos.left) / zoom;
|
|
81
|
+
const mouseY = (e.clientY - wrapperRect.top - pagePos.top) / zoom;
|
|
78
82
|
|
|
79
83
|
// Calculate delta from the start position
|
|
80
84
|
const deltaX = mouseX - resizeStartData.startX;
|
|
@@ -103,8 +107,8 @@ export const handleFieldResize = (
|
|
|
103
107
|
newHeight = Math.max(FIELD_CONSTANTS.minHeight, newHeight);
|
|
104
108
|
|
|
105
109
|
// Clamp to page boundaries (ensure the field doesn't go outside the page)
|
|
106
|
-
const maxWidth = pageDimensions.width - field.x;
|
|
107
|
-
const maxHeight = pageDimensions.height - field.y;
|
|
110
|
+
const maxWidth = pageDimensions.width / zoom - field.x;
|
|
111
|
+
const maxHeight = pageDimensions.height / zoom - field.y;
|
|
108
112
|
newWidth = Math.min(newWidth, maxWidth);
|
|
109
113
|
newHeight = Math.min(newHeight, maxHeight);
|
|
110
114
|
|
|
@@ -6,6 +6,13 @@ interface FieldBoundsStyleOptions {
|
|
|
6
6
|
pageOffset?: { left: number; top: number };
|
|
7
7
|
backgroundColor?: string;
|
|
8
8
|
borderColor?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Current page zoom factor (1 = 100%). Field `x/y/width/height` are stored
|
|
11
|
+
* relative to the unzoomed reference page width, so they must be scaled by
|
|
12
|
+
* zoom for display while `pageOffset` (read live from the DOM) already
|
|
13
|
+
* reflects the zoomed page.
|
|
14
|
+
*/
|
|
15
|
+
zoom?: number;
|
|
9
16
|
}
|
|
10
17
|
|
|
11
18
|
const getFieldTypeSpecificStyle = (field: PdfField): CSSProperties => {
|
|
@@ -39,14 +46,19 @@ export const getFieldBoundsStyle = (
|
|
|
39
46
|
field: PdfField,
|
|
40
47
|
options?: FieldBoundsStyleOptions,
|
|
41
48
|
): CSSProperties => {
|
|
42
|
-
const {
|
|
49
|
+
const {
|
|
50
|
+
backgroundColor,
|
|
51
|
+
borderColor,
|
|
52
|
+
pageOffset = { left: 0, top: 0 },
|
|
53
|
+
zoom = 1,
|
|
54
|
+
} = options ?? {};
|
|
43
55
|
|
|
44
56
|
return {
|
|
45
57
|
position: 'absolute',
|
|
46
|
-
left: pageOffset.left + field.x,
|
|
47
|
-
top: pageOffset.top + field.y,
|
|
48
|
-
width: field.width,
|
|
49
|
-
height: field.height,
|
|
58
|
+
left: pageOffset.left + field.x * zoom,
|
|
59
|
+
top: pageOffset.top + field.y * zoom,
|
|
60
|
+
width: field.width * zoom,
|
|
61
|
+
height: field.height * zoom,
|
|
50
62
|
display: 'flex',
|
|
51
63
|
alignItems: 'flex-start',
|
|
52
64
|
justifyContent: 'flex-start',
|
package/src/utils/field/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from './field-background-color.utils';
|
|
|
3
3
|
export * from './field-coordinates.utils';
|
|
4
4
|
export * from './field-drag.utils';
|
|
5
5
|
export * from './field-placeholder-text.utils';
|
|
6
|
+
export * from './field-reading-order.utils';
|
|
6
7
|
export * from './field-resize.utils';
|
|
7
8
|
export * from './get-field-bounds-style.utils';
|
|
8
9
|
export * from './get-field-font-size.utils';
|