@servicetitan/dte-pdf-editor 1.76.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.
Files changed (105) hide show
  1. package/dist/components/display-conditions/condition-group.d.ts +1 -1
  2. package/dist/components/display-conditions/condition-group.d.ts.map +1 -1
  3. package/dist/components/display-conditions/condition-groups-section.d.ts +1 -1
  4. package/dist/components/display-conditions/condition-groups-section.d.ts.map +1 -1
  5. package/dist/components/display-conditions/condition-row.d.ts +1 -1
  6. package/dist/components/display-conditions/condition-row.d.ts.map +1 -1
  7. package/dist/components/display-conditions/condition-value-control.d.ts +1 -1
  8. package/dist/components/display-conditions/condition-value-control.d.ts.map +1 -1
  9. package/dist/components/display-conditions/display-condition-modal.d.ts +1 -1
  10. package/dist/components/display-conditions/display-condition-modal.d.ts.map +1 -1
  11. package/dist/components/pdf-canvas/canvas-controls.d.ts +24 -0
  12. package/dist/components/pdf-canvas/canvas-controls.d.ts.map +1 -0
  13. package/dist/components/pdf-canvas/canvas-controls.js +22 -0
  14. package/dist/components/pdf-canvas/canvas-controls.js.map +1 -0
  15. package/dist/components/pdf-canvas/pdf-canvas.d.ts +1 -0
  16. package/dist/components/pdf-canvas/pdf-canvas.d.ts.map +1 -1
  17. package/dist/components/pdf-canvas/pdf-canvas.js +6 -3
  18. package/dist/components/pdf-canvas/pdf-canvas.js.map +1 -1
  19. package/dist/components/pdf-editor/pdf-editor.d.ts +1 -0
  20. package/dist/components/pdf-editor/pdf-editor.d.ts.map +1 -1
  21. package/dist/components/pdf-editor/pdf-editor.js +2 -2
  22. package/dist/components/pdf-editor/pdf-editor.js.map +1 -1
  23. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.d.ts +1 -0
  24. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.d.ts.map +1 -1
  25. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js +2 -2
  26. package/dist/components/pdf-fields-overlay/pdf-fields-overlay.js.map +1 -1
  27. package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts +1 -0
  28. package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts.map +1 -1
  29. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js +4 -1
  30. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js.map +1 -1
  31. package/dist/components/pdf-view/pdf-view-field-container.d.ts +1 -0
  32. package/dist/components/pdf-view/pdf-view-field-container.d.ts.map +1 -1
  33. package/dist/components/pdf-view/pdf-view-field-container.js +5 -4
  34. package/dist/components/pdf-view/pdf-view-field-container.js.map +1 -1
  35. package/dist/components/pdf-view/pdf-view.d.ts +2 -0
  36. package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
  37. package/dist/components/pdf-view/pdf-view.js +29 -5
  38. package/dist/components/pdf-view/pdf-view.js.map +1 -1
  39. package/dist/constants/pdf-editor.constants.d.ts +4 -0
  40. package/dist/constants/pdf-editor.constants.d.ts.map +1 -1
  41. package/dist/constants/pdf-editor.constants.js +4 -0
  42. package/dist/constants/pdf-editor.constants.js.map +1 -1
  43. package/dist/hooks/index.d.ts +1 -0
  44. package/dist/hooks/index.d.ts.map +1 -1
  45. package/dist/hooks/index.js +1 -0
  46. package/dist/hooks/index.js.map +1 -1
  47. package/dist/hooks/useFieldDrag.d.ts +2 -1
  48. package/dist/hooks/useFieldDrag.d.ts.map +1 -1
  49. package/dist/hooks/useFieldDrag.js +7 -6
  50. package/dist/hooks/useFieldDrag.js.map +1 -1
  51. package/dist/hooks/useFieldResize.d.ts +2 -1
  52. package/dist/hooks/useFieldResize.d.ts.map +1 -1
  53. package/dist/hooks/useFieldResize.js +7 -3
  54. package/dist/hooks/useFieldResize.js.map +1 -1
  55. package/dist/hooks/useZoom.d.ts +11 -0
  56. package/dist/hooks/useZoom.d.ts.map +1 -0
  57. package/dist/hooks/useZoom.js +14 -0
  58. package/dist/hooks/useZoom.js.map +1 -0
  59. package/dist/styles/pdf-canvas.css +24 -5
  60. package/dist/styles/pdf-editor.css +1 -0
  61. package/dist/styles/pdf-field-overlay.css +14 -0
  62. package/dist/styles/variables.css +10 -1
  63. package/dist/utils/field/field-drag.utils.d.ts +4 -2
  64. package/dist/utils/field/field-drag.utils.d.ts.map +1 -1
  65. package/dist/utils/field/field-drag.utils.js +14 -10
  66. package/dist/utils/field/field-drag.utils.js.map +1 -1
  67. package/dist/utils/field/field-reading-order.utils.d.ts +15 -0
  68. package/dist/utils/field/field-reading-order.utils.d.ts.map +1 -0
  69. package/dist/utils/field/field-reading-order.utils.js +33 -0
  70. package/dist/utils/field/field-reading-order.utils.js.map +1 -0
  71. package/dist/utils/field/field-resize.utils.d.ts +4 -2
  72. package/dist/utils/field/field-resize.utils.d.ts.map +1 -1
  73. package/dist/utils/field/field-resize.utils.js +12 -10
  74. package/dist/utils/field/field-resize.utils.js.map +1 -1
  75. package/dist/utils/field/get-field-bounds-style.utils.d.ts +7 -0
  76. package/dist/utils/field/get-field-bounds-style.utils.d.ts.map +1 -1
  77. package/dist/utils/field/get-field-bounds-style.utils.js +5 -5
  78. package/dist/utils/field/get-field-bounds-style.utils.js.map +1 -1
  79. package/dist/utils/field/index.d.ts +1 -0
  80. package/dist/utils/field/index.d.ts.map +1 -1
  81. package/dist/utils/field/index.js +1 -0
  82. package/dist/utils/field/index.js.map +1 -1
  83. package/package.json +1 -1
  84. package/src/components/pdf-canvas/canvas-controls.tsx +183 -0
  85. package/src/components/pdf-canvas/pdf-canvas.tsx +64 -33
  86. package/src/components/pdf-editor/pdf-editor.tsx +12 -2
  87. package/src/components/pdf-fields-overlay/pdf-fields-overlay.tsx +3 -0
  88. package/src/components/pdf-fields-overlay/pdf-overlay-field.tsx +5 -0
  89. package/src/components/pdf-view/pdf-view-field-container.tsx +6 -2
  90. package/src/components/pdf-view/pdf-view.tsx +78 -5
  91. package/src/constants/pdf-editor.constants.ts +5 -0
  92. package/src/hooks/index.ts +1 -0
  93. package/src/hooks/useFieldDrag.ts +10 -5
  94. package/src/hooks/useFieldResize.ts +16 -2
  95. package/src/hooks/useZoom.ts +25 -0
  96. package/src/styles/pdf-canvas.css +24 -5
  97. package/src/styles/pdf-editor.css +1 -0
  98. package/src/styles/pdf-field-overlay.css +14 -0
  99. package/src/styles/variables.css +10 -1
  100. package/src/utils/field/__tests__/field-reading-order.utils.test.ts +62 -0
  101. package/src/utils/field/field-drag.utils.ts +14 -8
  102. package/src/utils/field/field-reading-order.utils.ts +43 -0
  103. package/src/utils/field/field-resize.utils.ts +12 -8
  104. package/src/utils/field/get-field-bounds-style.utils.ts +17 -5
  105. package/src/utils/field/index.ts +1 -0
@@ -1 +1 @@
1
- {"version":3,"file":"get-field-bounds-style.utils.d.ts","sourceRoot":"","sources":["../../../src/utils/field/get-field-bounds-style.utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AACtC,OAAO,EAAiB,KAAK,QAAQ,EAAwB,MAAM,uBAAuB,CAAC;AAG3F,UAAU,uBAAuB;IAC7B,UAAU,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAsBD;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAC5B,OAAO,QAAQ,EACf,UAAU,uBAAuB,KAClC,aAwBF,CAAC"}
1
+ {"version":3,"file":"get-field-bounds-style.utils.d.ts","sourceRoot":"","sources":["../../../src/utils/field/get-field-bounds-style.utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AACtC,OAAO,EAAiB,KAAK,QAAQ,EAAwB,MAAM,uBAAuB,CAAC;AAG3F,UAAU,uBAAuB;IAC7B,UAAU,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AAsBD;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAC5B,OAAO,QAAQ,EACf,UAAU,uBAAuB,KAClC,aA6BF,CAAC"}
@@ -27,13 +27,13 @@ const getFieldTypeSpecificStyle = (field) => {
27
27
  * and shared typography (font size / family / weight).
28
28
  */
29
29
  export const getFieldBoundsStyle = (field, options) => {
30
- const { backgroundColor, borderColor, pageOffset = { left: 0, top: 0 } } = options !== null && options !== void 0 ? options : {};
30
+ const { backgroundColor, borderColor, pageOffset = { left: 0, top: 0 }, zoom = 1, } = options !== null && options !== void 0 ? options : {};
31
31
  return {
32
32
  position: 'absolute',
33
- left: pageOffset.left + field.x,
34
- top: pageOffset.top + field.y,
35
- width: field.width,
36
- height: field.height,
33
+ left: pageOffset.left + field.x * zoom,
34
+ top: pageOffset.top + field.y * zoom,
35
+ width: field.width * zoom,
36
+ height: field.height * zoom,
37
37
  display: 'flex',
38
38
  alignItems: 'flex-start',
39
39
  justifyContent: 'flex-start',
@@ -1 +1 @@
1
- {"version":3,"file":"get-field-bounds-style.utils.js","sourceRoot":"","sources":["../../../src/utils/field/get-field-bounds-style.utils.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAuC,MAAM,uBAAuB,CAAC;AAC3F,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAQvE,MAAM,yBAAyB,GAAG,CAAC,KAAe,EAAiB,EAAE;;IACjE,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;QACjB,OAAO,EAAE,CAAC;IACd,CAAC;IACD,MAAM,MAAM,GAAG;QACX,CAAC,aAAa,CAAC,QAAQ,CAAC,EAAE;YACtB,QAAQ,EAAE;gBACN,KAAK,EAAE,OAAO;gBACd,MAAM,EAAE,OAAO;aAClB;YACD,KAAK,EAAE;gBACH,KAAK,EAAE,OAAO;gBACd,MAAM,EAAE,OAAO;aAClB;SACJ;KAC6D,CAAC;IAEnE,OAAO,MAAA,MAAA,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,0CAAG,KAAK,CAAC,OAAO,CAAC,mCAAI,EAAE,CAAC;AACrD,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAC/B,KAAe,EACf,OAAiC,EACpB,EAAE;IACf,MAAM,EAAE,eAAe,EAAE,WAAW,EAAE,UAAU,GAAG,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,GAAG,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,EAAE,CAAC;IAEzF,OAAO;QACH,QAAQ,EAAE,UAAU;QACpB,IAAI,EAAE,UAAU,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC;QAC/B,GAAG,EAAE,UAAU,CAAC,GAAG,GAAG,KAAK,CAAC,CAAC;QAC7B,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,OAAO,EAAE,MAAM;QACf,UAAU,EAAE,YAAY;QACxB,cAAc,EAAE,YAAY;QAC5B,SAAS,EAAE,aAAa;QACxB,OAAO,EAAE,CAAC;QACV,MAAM,EAAE,CAAC;QACT,QAAQ,EAAE,CAAC;QACX,SAAS,EAAE,CAAC;QACZ,QAAQ,EAAE,QAAQ;QAClB,UAAU,EAAE,CAAC;QACb,GAAG,yBAAyB,CAAC,KAAK,CAAC;QACnC,GAAG,uBAAuB,CAAC,KAAK,CAAC;QACjC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC1C,CAAC;AACN,CAAC,CAAC"}
1
+ {"version":3,"file":"get-field-bounds-style.utils.js","sourceRoot":"","sources":["../../../src/utils/field/get-field-bounds-style.utils.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAuC,MAAM,uBAAuB,CAAC;AAC3F,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAevE,MAAM,yBAAyB,GAAG,CAAC,KAAe,EAAiB,EAAE;;IACjE,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;QACjB,OAAO,EAAE,CAAC;IACd,CAAC;IACD,MAAM,MAAM,GAAG;QACX,CAAC,aAAa,CAAC,QAAQ,CAAC,EAAE;YACtB,QAAQ,EAAE;gBACN,KAAK,EAAE,OAAO;gBACd,MAAM,EAAE,OAAO;aAClB;YACD,KAAK,EAAE;gBACH,KAAK,EAAE,OAAO;gBACd,MAAM,EAAE,OAAO;aAClB;SACJ;KAC6D,CAAC;IAEnE,OAAO,MAAA,MAAA,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,0CAAG,KAAK,CAAC,OAAO,CAAC,mCAAI,EAAE,CAAC;AACrD,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAC/B,KAAe,EACf,OAAiC,EACpB,EAAE;IACf,MAAM,EACF,eAAe,EACf,WAAW,EACX,UAAU,GAAG,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,EAChC,IAAI,GAAG,CAAC,GACX,GAAG,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,EAAE,CAAC;IAElB,OAAO;QACH,QAAQ,EAAE,UAAU;QACpB,IAAI,EAAE,UAAU,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,GAAG,IAAI;QACtC,GAAG,EAAE,UAAU,CAAC,GAAG,GAAG,KAAK,CAAC,CAAC,GAAG,IAAI;QACpC,KAAK,EAAE,KAAK,CAAC,KAAK,GAAG,IAAI;QACzB,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI;QAC3B,OAAO,EAAE,MAAM;QACf,UAAU,EAAE,YAAY;QACxB,cAAc,EAAE,YAAY;QAC5B,SAAS,EAAE,aAAa;QACxB,OAAO,EAAE,CAAC;QACV,MAAM,EAAE,CAAC;QACT,QAAQ,EAAE,CAAC;QACX,SAAS,EAAE,CAAC;QACZ,QAAQ,EAAE,QAAQ;QAClB,UAAU,EAAE,CAAC;QACb,GAAG,yBAAyB,CAAC,KAAK,CAAC;QACnC,GAAG,uBAAuB,CAAC,KAAK,CAAC;QACjC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC1C,CAAC;AACN,CAAC,CAAC"}
@@ -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';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/field/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,oBAAoB,CAAC;AACnC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,sBAAsB,CAAC;AACrC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/field/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,oBAAoB,CAAC;AACnC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,sBAAsB,CAAC;AACrC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC"}
@@ -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';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/utils/field/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,oBAAoB,CAAC;AACnC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,sBAAsB,CAAC;AACrC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/utils/field/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,oBAAoB,CAAC;AACnC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,sBAAsB,CAAC;AACrC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@servicetitan/dte-pdf-editor",
3
- "version": "1.76.0",
3
+ "version": "1.78.0",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "typings": "./dist/index.d.ts",
@@ -0,0 +1,183 @@
1
+ import { Button, Divider, Flex, Icon, Text } from '@servicetitan/anvil2';
2
+ import IconNavigateDown from '@servicetitan/anvil2/assets/icons/material/round/arrow_downward.svg';
3
+ import IconNavigateUp from '@servicetitan/anvil2/assets/icons/material/round/arrow_upward.svg';
4
+ import IconNavigateBefore from '@servicetitan/anvil2/assets/icons/material/round/navigate_before.svg';
5
+ import IconNavigateNext from '@servicetitan/anvil2/assets/icons/material/round/navigate_next.svg';
6
+ import IconZoomIn from '@servicetitan/anvil2/assets/icons/material/round/zoom_in.svg';
7
+ import IconZoomOut from '@servicetitan/anvil2/assets/icons/material/round/zoom_out.svg';
8
+ import { FC } from 'react';
9
+ import { ZOOM_MAX, ZOOM_MIN } from '../../constants';
10
+
11
+ interface ZoomControlsProps {
12
+ zoom: number;
13
+ onZoomIn(): void;
14
+ onZoomOut(): void;
15
+ onResetZoom(): void;
16
+ }
17
+
18
+ const ZoomControls: FC<ZoomControlsProps> = ({ onResetZoom, onZoomIn, onZoomOut, zoom }) => {
19
+ return (
20
+ <Flex alignItems="center" gap={1} className="dte-pdf-zoom-controls">
21
+ <Button
22
+ size="xsmall"
23
+ appearance="ghost"
24
+ onClick={onZoomOut}
25
+ disabled={zoom <= ZOOM_MIN}
26
+ aria-label="Zoom out"
27
+ title="Zoom out"
28
+ >
29
+ <Icon svg={IconZoomOut} size="medium" />
30
+ </Button>
31
+
32
+ <Text
33
+ variant="body"
34
+ size="small"
35
+ className="dte-pdf-zoom-controls-value"
36
+ onClick={onResetZoom}
37
+ title="Reset zoom"
38
+ >
39
+ {Math.round(zoom * 100)}%
40
+ </Text>
41
+
42
+ <Button
43
+ size="xsmall"
44
+ appearance="ghost"
45
+ onClick={onZoomIn}
46
+ disabled={zoom >= ZOOM_MAX}
47
+ aria-label="Zoom in"
48
+ title="Zoom in"
49
+ >
50
+ <Icon svg={IconZoomIn} size="medium" />
51
+ </Button>
52
+ </Flex>
53
+ );
54
+ };
55
+
56
+ interface FieldNavigationControlsProps {
57
+ disabledNext: boolean;
58
+ disabledOnFirstField: boolean;
59
+ disabledOnLastField: boolean;
60
+ disabledPrev: boolean;
61
+ onNextField(): void;
62
+ onPrevField(): void;
63
+ onFirstField(): void;
64
+ onLastField(): void;
65
+ }
66
+
67
+ const FieldNavigationControls: FC<FieldNavigationControlsProps> = ({
68
+ disabledNext,
69
+ disabledOnFirstField,
70
+ disabledOnLastField,
71
+ disabledPrev,
72
+ onFirstField,
73
+ onLastField,
74
+ onNextField,
75
+ onPrevField,
76
+ }) => {
77
+ return (
78
+ <Flex alignItems="center" gap="2" className="dte-pdf-field-navigation-controls">
79
+ <Button
80
+ size="xsmall"
81
+ appearance="ghost"
82
+ onClick={onPrevField}
83
+ disabled={disabledPrev}
84
+ aria-label="Go to previous field"
85
+ title="Go to previous field"
86
+ >
87
+ <Icon svg={IconNavigateBefore} size="medium" />
88
+ </Button>
89
+ <Button
90
+ size="xsmall"
91
+ appearance="ghost"
92
+ onClick={onNextField}
93
+ disabled={disabledNext}
94
+ aria-label="Go to next field"
95
+ title="Go to next field"
96
+ >
97
+ <Icon svg={IconNavigateNext} size="medium" />
98
+ </Button>
99
+ <Divider vertical style={{ height: '100%' }} />
100
+ <Button
101
+ size="xsmall"
102
+ appearance="ghost"
103
+ onClick={onFirstField}
104
+ disabled={disabledOnFirstField}
105
+ aria-label="Go to first field"
106
+ title="Go to first field"
107
+ >
108
+ <Icon svg={IconNavigateUp} size="medium" />
109
+ </Button>
110
+ <Button
111
+ size="xsmall"
112
+ appearance="ghost"
113
+ onClick={onLastField}
114
+ disabled={disabledOnLastField}
115
+ aria-label="Go to last field"
116
+ title="Go to last field"
117
+ >
118
+ <Icon svg={IconNavigateDown} size="medium" />
119
+ </Button>
120
+ </Flex>
121
+ );
122
+ };
123
+
124
+ export interface CanvasControlsProps extends ZoomControlsProps {
125
+ /** Show the zoom in/out/reset controls. */
126
+ zoomable?: boolean;
127
+ /** Show the previous/next fillable-field navigation controls. */
128
+ enableInputNavigation?: boolean;
129
+ disabledNext?: boolean;
130
+ disabledPrev?: boolean;
131
+ disabledOnFirstField?: boolean;
132
+ disabledOnLastField?: boolean;
133
+ onNextField?(): void;
134
+ onPrevField?(): void;
135
+ onFirstField?(): void;
136
+ onLastField?(): void;
137
+ }
138
+
139
+ export const CanvasControls: FC<CanvasControlsProps> = ({
140
+ disabledNext = false,
141
+ disabledOnFirstField = false,
142
+ disabledOnLastField = false,
143
+ disabledPrev = false,
144
+ enableInputNavigation = false,
145
+ onFirstField = () => {},
146
+ onLastField = () => {},
147
+ onNextField = () => {},
148
+ onPrevField = () => {},
149
+ onResetZoom,
150
+ onZoomIn,
151
+ onZoomOut,
152
+ zoom,
153
+ zoomable = false,
154
+ }) => {
155
+ if (!zoomable && !enableInputNavigation) {
156
+ return null;
157
+ }
158
+
159
+ return (
160
+ <Flex justifyContent="center" gap={2} className="dte-pdf-canvas-controls">
161
+ {zoomable && (
162
+ <ZoomControls
163
+ zoom={zoom}
164
+ onZoomIn={onZoomIn}
165
+ onZoomOut={onZoomOut}
166
+ onResetZoom={onResetZoom}
167
+ />
168
+ )}
169
+ {enableInputNavigation && (
170
+ <FieldNavigationControls
171
+ disabledOnFirstField={disabledOnFirstField}
172
+ disabledOnLastField={disabledOnLastField}
173
+ onFirstField={onFirstField}
174
+ onLastField={onLastField}
175
+ disabledNext={disabledNext}
176
+ disabledPrev={disabledPrev}
177
+ onNextField={onNextField}
178
+ onPrevField={onPrevField}
179
+ />
180
+ )}
181
+ </Flex>
182
+ );
183
+ };
@@ -1,12 +1,24 @@
1
1
  import { Flex } from '@servicetitan/anvil2';
2
- import { DragEvent, FC, MouseEvent, ReactNode, RefObject, useEffect, useState } from 'react';
2
+ import {
3
+ DragEvent,
4
+ FC,
5
+ Fragment,
6
+ MouseEvent,
7
+ ReactNode,
8
+ RefObject,
9
+ useEffect,
10
+ useState,
11
+ } from 'react';
3
12
  import { BASE_PAGE_WIDTH } from '../../constants';
13
+ import { useZoom } from '../../hooks';
4
14
  import { type PdfField } from '../../interface/types';
5
15
  import { PdfFieldsOverlay } from '../pdf-fields-overlay/pdf-fields-overlay';
16
+ import { CanvasControls } from './canvas-controls';
6
17
  import { PdfDocumentRenderer } from './pdf-document-renderer';
7
18
 
8
19
  interface PdfCanvasProps {
9
20
  pdfUrl: string;
21
+ zoomable: boolean;
10
22
  errorPlaceholder?: ReactNode;
11
23
  loadingPlaceholder?: ReactNode;
12
24
  fields: PdfField[];
@@ -49,8 +61,10 @@ export const PdfCanvas: FC<PdfCanvasProps> = ({
49
61
  pdfWrapperRef,
50
62
  recipientsColors,
51
63
  selectedField,
64
+ zoomable,
52
65
  }) => {
53
66
  const [isPdfLoaded, setIsPdfLoaded] = useState<boolean>(false);
67
+ const { resetZoom, zoom, zoomIn, zoomOut } = useZoom();
54
68
 
55
69
  const onDocumentLoadSuccess = (pageNum: number) => {
56
70
  setIsPdfLoaded(true);
@@ -62,39 +76,56 @@ export const PdfCanvas: FC<PdfCanvasProps> = ({
62
76
  }, [pdfUrl]);
63
77
 
64
78
  return (
65
- <Flex
66
- flex={1}
67
- ref={pdfContainerRef}
68
- className="dte-pdf-canvas-container"
69
- onClick={onDeselectField}
70
- >
71
- <div ref={pdfWrapperRef} className="dte-pdf-wrapper" style={{ width: BASE_PAGE_WIDTH }}>
72
- <PdfDocumentRenderer
73
- pdfUrl={pdfUrl}
74
- onDrop={onDrop}
75
- loading={loading}
76
- onDragOver={onDragOver}
77
- pageWidth={BASE_PAGE_WIDTH}
78
- errorPlaceholder={errorPlaceholder}
79
- loadingPlaceholder={loadingPlaceholder}
80
- onDocumentLoadSuccess={onDocumentLoadSuccess}
79
+ <Fragment>
80
+ {isPdfLoaded && (
81
+ <CanvasControls
82
+ enableInputNavigation={false}
83
+ zoomable={zoomable}
84
+ zoom={zoom}
85
+ onZoomIn={zoomIn}
86
+ onZoomOut={zoomOut}
87
+ onResetZoom={resetZoom}
81
88
  />
82
- {isPdfLoaded && (
83
- <PdfFieldsOverlay
84
- fields={fields}
85
- errors={errors}
86
- onFieldConfigChange={onFieldConfigChange}
87
- handleAddNewField={handleAddNewField}
88
- pdfWrapperRef={pdfWrapperRef}
89
- recipientsColors={recipientsColors}
90
- selectedField={selectedField ?? null}
91
- onFieldClick={onFieldClick}
92
- onFieldMove={onFieldMove}
93
- onFieldResize={onFieldResize}
94
- fieldWrapperClassNames={fieldWrapperClassNames}
89
+ )}
90
+ <Flex
91
+ flex={1}
92
+ ref={pdfContainerRef}
93
+ className="dte-pdf-canvas-container"
94
+ onClick={onDeselectField}
95
+ >
96
+ <div
97
+ ref={pdfWrapperRef}
98
+ className="dte-pdf-wrapper"
99
+ style={{ width: BASE_PAGE_WIDTH * zoom }}
100
+ >
101
+ <PdfDocumentRenderer
102
+ pdfUrl={pdfUrl}
103
+ onDrop={onDrop}
104
+ loading={loading}
105
+ onDragOver={onDragOver}
106
+ pageWidth={BASE_PAGE_WIDTH * zoom}
107
+ errorPlaceholder={errorPlaceholder}
108
+ loadingPlaceholder={loadingPlaceholder}
109
+ onDocumentLoadSuccess={onDocumentLoadSuccess}
95
110
  />
96
- )}
97
- </div>
98
- </Flex>
111
+ {isPdfLoaded && (
112
+ <PdfFieldsOverlay
113
+ fields={fields}
114
+ errors={errors}
115
+ onFieldConfigChange={onFieldConfigChange}
116
+ handleAddNewField={handleAddNewField}
117
+ pdfWrapperRef={pdfWrapperRef}
118
+ recipientsColors={recipientsColors}
119
+ selectedField={selectedField ?? null}
120
+ onFieldClick={onFieldClick}
121
+ onFieldMove={onFieldMove}
122
+ onFieldResize={onFieldResize}
123
+ fieldWrapperClassNames={fieldWrapperClassNames}
124
+ zoom={zoom}
125
+ />
126
+ )}
127
+ </div>
128
+ </Flex>
129
+ </Fragment>
99
130
  );
100
131
  };
@@ -19,6 +19,7 @@ import '../../styles/index.css';
19
19
  export interface PdfEditorProps {
20
20
  pdfUrl: string;
21
21
  loading?: boolean;
22
+ zoomable?: boolean;
22
23
  errors?: Record<string, string>;
23
24
  loadingPlaceholder?: ReactNode;
24
25
  errorPlaceholder?: ReactNode;
@@ -51,6 +52,7 @@ export const PdfEditor: FC<PdfEditorProps> = ({
51
52
  onLoadSuccess,
52
53
  pdfUrl,
53
54
  recipients = [],
55
+ zoomable = true,
54
56
  }) => {
55
57
  const pdfContainerRef = useRef<HTMLDivElement>(null);
56
58
  const pdfWrapperRef = useRef<HTMLDivElement>(null);
@@ -77,7 +79,10 @@ export const PdfEditor: FC<PdfEditorProps> = ({
77
79
  const recipientsColors = mapColorsToRecipients(recipients);
78
80
 
79
81
  return (
80
- <Flex flex={1} className={`dte-pdf-editor ${loading ? 'skeleton' : ''}`}>
82
+ <Flex
83
+ flex={1}
84
+ className={`dte-pdf-editor-theme dte-pdf-editor ${loading ? 'skeleton' : ''}`}
85
+ >
81
86
  {pdfFieldSelection.selectedField && (
82
87
  <FieldConfigPanelOverlay
83
88
  dataModel={dataModel}
@@ -110,8 +115,13 @@ export const PdfEditor: FC<PdfEditorProps> = ({
110
115
  />
111
116
  </Flex>
112
117
 
113
- <Flex gap={12} flex={1} className="dte-pdf-editor-content-container skeleton-item">
118
+ <Flex
119
+ direction="column"
120
+ flex={1}
121
+ className="dte-pdf-editor-content-container skeleton-item"
122
+ >
114
123
  <PdfCanvas
124
+ zoomable={zoomable}
115
125
  pdfUrl={pdfUrl}
116
126
  fields={fields}
117
127
  loading={loading}
@@ -9,6 +9,7 @@ interface PdfFieldsOverlayProps {
9
9
  pdfWrapperRef: RefObject<HTMLDivElement>;
10
10
  recipientsColors: Record<string, string>;
11
11
  fieldWrapperClassNames?: string;
12
+ zoom?: number;
12
13
  onFieldConfigChange(updates: Partial<PdfField>): void;
13
14
  handleAddNewField(field: PdfField): void;
14
15
  onFieldClick(fieldId: string, e: MouseEvent): void;
@@ -28,6 +29,7 @@ export const PdfFieldsOverlay: FC<PdfFieldsOverlayProps> = ({
28
29
  pdfWrapperRef,
29
30
  recipientsColors,
30
31
  selectedField,
32
+ zoom = 1,
31
33
  }) => {
32
34
  const pathToLabel = useMemo(() => {
33
35
  const map = new Map<string, string>();
@@ -57,6 +59,7 @@ export const PdfFieldsOverlay: FC<PdfFieldsOverlayProps> = ({
57
59
  recipientsColors={recipientsColors}
58
60
  isSameGroup={selectedField?.path === field.path}
59
61
  pathToLabel={pathToLabel}
62
+ zoom={zoom}
60
63
  />
61
64
  ))}
62
65
  </div>
@@ -19,6 +19,7 @@ interface PdfOverlayFieldProps {
19
19
  pdfWrapperRef: RefObject<HTMLDivElement>;
20
20
  pathToLabel?: Map<string, string>;
21
21
  fieldWrapperClassNames?: string;
22
+ zoom?: number;
22
23
  onFieldConfigChange(updates: Partial<PdfField>): void;
23
24
  handleAddNewField(field: PdfField): void;
24
25
  onFieldClick(fieldId: string, e: MouseEvent): void;
@@ -40,16 +41,19 @@ export const PdfOverlayField: FC<PdfOverlayFieldProps> = ({
40
41
  pathToLabel,
41
42
  pdfWrapperRef,
42
43
  recipientsColors,
44
+ zoom = 1,
43
45
  }) => {
44
46
  const { dragVisualPosition, handleDrag, handleDragEnd, handleDragStart } = useFieldDrag({
45
47
  pdfWrapperRef,
46
48
  onFieldMove,
49
+ zoom,
47
50
  });
48
51
  const isDragging = dragVisualPosition !== null;
49
52
 
50
53
  const { handleResizeStart } = useFieldResize({
51
54
  pdfWrapperRef,
52
55
  onFieldResize,
56
+ zoom,
53
57
  });
54
58
 
55
59
  const page = dragVisualPosition?.page ?? field.page;
@@ -65,6 +69,7 @@ export const PdfOverlayField: FC<PdfOverlayFieldProps> = ({
65
69
  {
66
70
  pageOffset: pagePos,
67
71
  backgroundColor: getFieldBackgroundColor(field.recipient, recipientsColors),
72
+ zoom,
68
73
  },
69
74
  );
70
75
 
@@ -13,6 +13,7 @@ interface PdfViewFieldContainer {
13
13
  error?: string;
14
14
  field: PdfField;
15
15
  recipientsColors: Record<string, string>;
16
+ zoom?: number;
16
17
  }
17
18
 
18
19
  export const PdfViewFieldContainer: FC<PropsWithChildren<PdfViewFieldContainer>> = ({
@@ -22,6 +23,7 @@ export const PdfViewFieldContainer: FC<PropsWithChildren<PdfViewFieldContainer>>
22
23
  field,
23
24
  pdfWrapperRef,
24
25
  recipientsColors,
26
+ zoom = 1,
25
27
  }) => {
26
28
  const visible =
27
29
  !field.displayCondition || evaluateDisplayCondition(field.displayCondition, data);
@@ -35,11 +37,13 @@ export const PdfViewFieldContainer: FC<PropsWithChildren<PdfViewFieldContainer>>
35
37
  return (
36
38
  <Fragment>
37
39
  <div
40
+ id={`dte-pdf-field-${field.id}`}
38
41
  className="dte-pdf-field-view"
39
42
  style={getFieldBoundsStyle(field, {
40
43
  pageOffset: pagePos,
41
44
  backgroundColor: bgColor,
42
45
  borderColor: error ? 'red' : undefined,
46
+ zoom,
43
47
  })}
44
48
  >
45
49
  {children}
@@ -48,8 +52,8 @@ export const PdfViewFieldContainer: FC<PropsWithChildren<PdfViewFieldContainer>>
48
52
  <span
49
53
  style={{
50
54
  color: 'red',
51
- top: pagePos.top + field.y + field.height + 8,
52
- left: pagePos.left + field.x,
55
+ top: pagePos.top + (field.y + field.height) * zoom + 8,
56
+ left: pagePos.left + field.x * zoom,
53
57
  minWidth: '100px',
54
58
  position: 'absolute',
55
59
  }}
@@ -1,6 +1,7 @@
1
1
  import { Flex } from '@servicetitan/anvil2';
2
- import { FC, ReactNode, useRef, useState } from 'react';
2
+ import { FC, ReactNode, useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { BASE_PAGE_WIDTH } from '../../constants';
4
+ import { useZoom } from '../../hooks';
4
5
  import {
5
6
  type DataChangePayload,
6
7
  type DataModelValues,
@@ -9,7 +10,12 @@ import {
9
10
  type PdfViewMode,
10
11
  type RecipientInfo,
11
12
  } from '../../interface/types';
12
- import { mapColorsToRecipients } from '../../utils';
13
+ import {
14
+ evaluateDisplayCondition,
15
+ mapColorsToRecipients,
16
+ sortFieldsByReadingOrder,
17
+ } from '../../utils';
18
+ import { CanvasControls } from '../pdf-canvas/canvas-controls';
13
19
  import { PdfDocumentRenderer } from '../pdf-canvas/pdf-document-renderer';
14
20
  import { PdfViewCalculated } from './pdf-view-calculated';
15
21
  import { PdfViewDataModel } from './pdf-view-data-model';
@@ -23,6 +29,8 @@ export interface PdfViewProps {
23
29
  errors?: Record<string, string>;
24
30
  viewMode?: PdfViewMode;
25
31
  data?: DataModelValues;
32
+ zoomable?: boolean;
33
+ enableInputNavigation?: boolean;
26
34
  pdfUrl: string;
27
35
  loading?: boolean;
28
36
  /**
@@ -52,6 +60,7 @@ export interface PdfViewProps {
52
60
 
53
61
  export const PdfView: FC<PdfViewProps> = ({
54
62
  data,
63
+ enableInputNavigation = true,
55
64
  errorPlaceholder,
56
65
  errors = {},
57
66
  fields,
@@ -65,9 +74,11 @@ export const PdfView: FC<PdfViewProps> = ({
65
74
  pdfUrl,
66
75
  recipients,
67
76
  viewMode,
77
+ zoomable = true,
68
78
  }) => {
69
79
  const [isPdfLoaded, setIsPdfLoaded] = useState<boolean>(false);
70
80
  const [previewData, setPreviewData] = useState<DataModelValues>(data ?? {});
81
+ const { resetZoom, zoom, zoomIn, zoomOut } = useZoom();
71
82
 
72
83
  const pdfWrapperRef = useRef<HTMLDivElement>(null);
73
84
 
@@ -86,14 +97,75 @@ export const PdfView: FC<PdfViewProps> = ({
86
97
  onDataChange?.(data, field);
87
98
  };
88
99
 
100
+ const activeData = viewMode === 'preview' ? previewData : data;
101
+
102
+ const navigableFields = useMemo(
103
+ () =>
104
+ sortFieldsByReadingOrder(
105
+ fields
106
+ .filter(field => field.type === FieldTypeEnum.fillable)
107
+ .filter(
108
+ field =>
109
+ !field.displayCondition ||
110
+ evaluateDisplayCondition(field.displayCondition, activeData),
111
+ ),
112
+ ),
113
+ [fields, activeData],
114
+ );
115
+
116
+ // -1 means no field has been navigated to yet, so the first "next" lands on index 0.
117
+ const [currentFieldIndex, setCurrentFieldIndex] = useState(-1);
118
+
119
+ useEffect(() => {
120
+ setCurrentFieldIndex(index => Math.min(index, navigableFields.length - 1));
121
+ }, [navigableFields.length]);
122
+
123
+ const goToField = (index: number) => {
124
+ const field = navigableFields[index];
125
+ if (!field) {
126
+ return;
127
+ }
128
+
129
+ setCurrentFieldIndex(index);
130
+ const fieldEl = document.getElementById(`dte-pdf-field-${field.id}`);
131
+ fieldEl?.scrollIntoView({ behavior: 'smooth', block: 'center' });
132
+ fieldEl?.querySelector<HTMLElement>('input, textarea')?.focus();
133
+ };
134
+
89
135
  const recipientsColors = mapColorsToRecipients(recipients);
90
136
  return (
91
- <Flex flex={1} className="dte-pdf-view-container" style={{ maxWidth: BASE_PAGE_WIDTH }}>
92
- <div ref={pdfWrapperRef} className="dte-pdf-wrapper">
137
+ <Flex
138
+ flex={1}
139
+ className="dte-pdf-editor-theme dte-pdf-editor-content-container"
140
+ direction="column"
141
+ >
142
+ {isPdfLoaded && (
143
+ <CanvasControls
144
+ zoom={zoom}
145
+ onZoomIn={zoomIn}
146
+ onZoomOut={zoomOut}
147
+ zoomable={zoomable}
148
+ onResetZoom={resetZoom}
149
+ enableInputNavigation={enableInputNavigation}
150
+ disabledPrev={currentFieldIndex <= 0}
151
+ disabledOnFirstField={currentFieldIndex === 0}
152
+ disabledNext={currentFieldIndex >= navigableFields.length - 1}
153
+ disabledOnLastField={currentFieldIndex === navigableFields.length - 1}
154
+ onPrevField={() => goToField(currentFieldIndex - 1)}
155
+ onNextField={() => goToField(currentFieldIndex + 1)}
156
+ onFirstField={() => goToField(0)}
157
+ onLastField={() => goToField(navigableFields.length - 1)}
158
+ />
159
+ )}
160
+ <div
161
+ ref={pdfWrapperRef}
162
+ className="dte-pdf-wrapper"
163
+ style={{ width: BASE_PAGE_WIDTH * zoom }}
164
+ >
93
165
  <PdfDocumentRenderer
94
166
  pdfUrl={pdfUrl}
95
167
  loading={loading}
96
- pageWidth={BASE_PAGE_WIDTH}
168
+ pageWidth={BASE_PAGE_WIDTH * zoom}
97
169
  errorPlaceholder={errorPlaceholder}
98
170
  loadingPlaceholder={loadingPlaceholder}
99
171
  onDocumentLoadSuccess={onDocumentLoadSuccess}
@@ -108,6 +180,7 @@ export const PdfView: FC<PdfViewProps> = ({
108
180
  error={errors[field.path!] || ''}
109
181
  recipientsColors={recipientsColors}
110
182
  pdfWrapperRef={pdfWrapperRef}
183
+ zoom={zoom}
111
184
  >
112
185
  {(field.type === FieldTypeEnum.dataModel ||
113
186
  field.type === FieldTypeEnum.forms) && (
@@ -1 +1,6 @@
1
1
  export const BASE_PAGE_WIDTH = 980;
2
+
3
+ export const ZOOM_MIN = 0.4;
4
+ export const ZOOM_MAX = 2;
5
+ export const ZOOM_STEP = 0.1;
6
+ export const ZOOM_DEFAULT = 1;