@servicetitan/dte-pdf-editor 1.16.0 → 1.18.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 (190) hide show
  1. package/README.md +35 -7
  2. package/dist/components/field-config-panel/advanced-settings.d.ts +9 -0
  3. package/dist/components/field-config-panel/advanced-settings.d.ts.map +1 -0
  4. package/dist/components/field-config-panel/advanced-settings.js +17 -0
  5. package/dist/components/field-config-panel/advanced-settings.js.map +1 -0
  6. package/dist/components/field-config-panel/field-config-panel-overlay.d.ts +4 -1
  7. package/dist/components/field-config-panel/field-config-panel-overlay.d.ts.map +1 -1
  8. package/dist/components/field-config-panel/field-config-panel-overlay.js +2 -2
  9. package/dist/components/field-config-panel/field-config-panel-overlay.js.map +1 -1
  10. package/dist/components/field-config-panel/field-config-panel.d.ts +4 -1
  11. package/dist/components/field-config-panel/field-config-panel.d.ts.map +1 -1
  12. package/dist/components/field-config-panel/field-config-panel.js +11 -5
  13. package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
  14. package/dist/components/field-config-panel/field-sidebar.d.ts +13 -0
  15. package/dist/components/field-config-panel/field-sidebar.d.ts.map +1 -0
  16. package/dist/components/field-config-panel/field-sidebar.js +20 -0
  17. package/dist/components/field-config-panel/field-sidebar.js.map +1 -0
  18. package/dist/components/field-config-panel/formula-generator.d.ts +11 -0
  19. package/dist/components/field-config-panel/formula-generator.d.ts.map +1 -0
  20. package/dist/components/field-config-panel/formula-generator.js +51 -0
  21. package/dist/components/field-config-panel/formula-generator.js.map +1 -0
  22. package/dist/components/field-config-panel/formula-modal.d.ts +12 -0
  23. package/dist/components/field-config-panel/formula-modal.d.ts.map +1 -0
  24. package/dist/components/field-config-panel/formula-modal.js +99 -0
  25. package/dist/components/field-config-panel/formula-modal.js.map +1 -0
  26. package/dist/components/field-config-panel/formula-workspace.d.ts +23 -0
  27. package/dist/components/field-config-panel/formula-workspace.d.ts.map +1 -0
  28. package/dist/components/field-config-panel/formula-workspace.js +28 -0
  29. package/dist/components/field-config-panel/formula-workspace.js.map +1 -0
  30. package/dist/components/field-config-panel/result-type-selector.d.ts +9 -0
  31. package/dist/components/field-config-panel/result-type-selector.d.ts.map +1 -0
  32. package/dist/components/field-config-panel/result-type-selector.js +10 -0
  33. package/dist/components/field-config-panel/result-type-selector.js.map +1 -0
  34. package/dist/components/field-sidebar/calculated-field-type-list.d.ts +9 -0
  35. package/dist/components/field-sidebar/calculated-field-type-list.d.ts.map +1 -0
  36. package/dist/components/field-sidebar/calculated-field-type-list.js +12 -0
  37. package/dist/components/field-sidebar/calculated-field-type-list.js.map +1 -0
  38. package/dist/components/field-sidebar/data-model-field-type-list.d.ts +0 -1
  39. package/dist/components/field-sidebar/data-model-field-type-list.d.ts.map +1 -1
  40. package/dist/components/field-sidebar/data-model-field-type-list.js +8 -7
  41. package/dist/components/field-sidebar/data-model-field-type-list.js.map +1 -1
  42. package/dist/components/field-sidebar/field-menu-group.d.ts +11 -0
  43. package/dist/components/field-sidebar/field-menu-group.d.ts.map +1 -0
  44. package/dist/components/field-sidebar/field-menu-group.js +6 -0
  45. package/dist/components/field-sidebar/field-menu-group.js.map +1 -0
  46. package/dist/components/field-sidebar/field-sidebar.d.ts.map +1 -1
  47. package/dist/components/field-sidebar/field-sidebar.js +6 -15
  48. package/dist/components/field-sidebar/field-sidebar.js.map +1 -1
  49. package/dist/components/field-sidebar/fillable-field-type-list.d.ts +0 -1
  50. package/dist/components/field-sidebar/fillable-field-type-list.d.ts.map +1 -1
  51. package/dist/components/field-sidebar/fillable-field-type-list.js +8 -9
  52. package/dist/components/field-sidebar/fillable-field-type-list.js.map +1 -1
  53. package/dist/components/pdf-editor/pdf-editor.d.ts.map +1 -1
  54. package/dist/components/pdf-editor/pdf-editor.js +1 -1
  55. package/dist/components/pdf-editor/pdf-editor.js.map +1 -1
  56. package/dist/components/pdf-fields-overlay/pdf-overlay-field-calculated.d.ts +8 -0
  57. package/dist/components/pdf-fields-overlay/pdf-overlay-field-calculated.d.ts.map +1 -0
  58. package/dist/components/pdf-fields-overlay/pdf-overlay-field-calculated.js +5 -0
  59. package/dist/components/pdf-fields-overlay/pdf-overlay-field-calculated.js.map +1 -0
  60. package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts.map +1 -1
  61. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js +11 -6
  62. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js.map +1 -1
  63. package/dist/components/pdf-view/pdf-view-calculated.d.ts +9 -0
  64. package/dist/components/pdf-view/pdf-view-calculated.d.ts.map +1 -0
  65. package/dist/components/pdf-view/pdf-view-calculated.js +18 -0
  66. package/dist/components/pdf-view/pdf-view-calculated.js.map +1 -0
  67. package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
  68. package/dist/components/pdf-view/pdf-view.js +2 -1
  69. package/dist/components/pdf-view/pdf-view.js.map +1 -1
  70. package/dist/constants/field.constants.d.ts +3 -2
  71. package/dist/constants/field.constants.d.ts.map +1 -1
  72. package/dist/constants/field.constants.js +6 -0
  73. package/dist/constants/field.constants.js.map +1 -1
  74. package/dist/constants/menu-group.d.ts +8 -0
  75. package/dist/constants/menu-group.d.ts.map +1 -0
  76. package/dist/constants/menu-group.js +20 -0
  77. package/dist/constants/menu-group.js.map +1 -0
  78. package/dist/hooks/index.d.ts +1 -0
  79. package/dist/hooks/index.d.ts.map +1 -1
  80. package/dist/hooks/index.js +1 -0
  81. package/dist/hooks/index.js.map +1 -1
  82. package/dist/hooks/useFieldDrag.d.ts +9 -2
  83. package/dist/hooks/useFieldDrag.d.ts.map +1 -1
  84. package/dist/hooks/useFieldDrag.js +60 -8
  85. package/dist/hooks/useFieldDrag.js.map +1 -1
  86. package/dist/hooks/useFormulaEditor.d.ts +22 -0
  87. package/dist/hooks/useFormulaEditor.d.ts.map +1 -0
  88. package/dist/hooks/useFormulaEditor.js +290 -0
  89. package/dist/hooks/useFormulaEditor.js.map +1 -0
  90. package/dist/hooks/usePdfFieldDnD.d.ts.map +1 -1
  91. package/dist/hooks/usePdfFieldDnD.js +19 -3
  92. package/dist/hooks/usePdfFieldDnD.js.map +1 -1
  93. package/dist/interface/types.d.ts +45 -3
  94. package/dist/interface/types.d.ts.map +1 -1
  95. package/dist/interface/types.js +3 -0
  96. package/dist/interface/types.js.map +1 -1
  97. package/dist/utils/data-model/extract-fields.utils.d.ts +5 -5
  98. package/dist/utils/data-model/extract-fields.utils.d.ts.map +1 -1
  99. package/dist/utils/data-model/extract-fields.utils.js +42 -8
  100. package/dist/utils/data-model/extract-fields.utils.js.map +1 -1
  101. package/dist/utils/formula/caret.utils.d.ts +3 -0
  102. package/dist/utils/formula/caret.utils.d.ts.map +1 -0
  103. package/dist/utils/formula/caret.utils.js +123 -0
  104. package/dist/utils/formula/caret.utils.js.map +1 -0
  105. package/dist/utils/formula/dom.utils.d.ts +4 -0
  106. package/dist/utils/formula/dom.utils.d.ts.map +1 -0
  107. package/dist/utils/formula/dom.utils.js +34 -0
  108. package/dist/utils/formula/dom.utils.js.map +1 -0
  109. package/dist/utils/formula/evaluate-formula.utils.d.ts +13 -0
  110. package/dist/utils/formula/evaluate-formula.utils.d.ts.map +1 -0
  111. package/dist/utils/formula/evaluate-formula.utils.js +134 -0
  112. package/dist/utils/formula/evaluate-formula.utils.js.map +1 -0
  113. package/dist/utils/formula/expression.utils.d.ts +18 -0
  114. package/dist/utils/formula/expression.utils.d.ts.map +1 -0
  115. package/dist/utils/formula/expression.utils.js +84 -0
  116. package/dist/utils/formula/expression.utils.js.map +1 -0
  117. package/dist/utils/formula/format-calculated-result.utils.d.ts +7 -0
  118. package/dist/utils/formula/format-calculated-result.utils.d.ts.map +1 -0
  119. package/dist/utils/formula/format-calculated-result.utils.js +50 -0
  120. package/dist/utils/formula/format-calculated-result.utils.js.map +1 -0
  121. package/dist/utils/formula/formula-types.d.ts +3 -0
  122. package/dist/utils/formula/formula-types.d.ts.map +1 -0
  123. package/dist/utils/formula/formula-types.js +2 -0
  124. package/dist/utils/formula/formula-types.js.map +1 -0
  125. package/dist/utils/formula/index.d.ts +11 -0
  126. package/dist/utils/formula/index.d.ts.map +1 -0
  127. package/dist/utils/formula/index.js +11 -0
  128. package/dist/utils/formula/index.js.map +1 -0
  129. package/dist/utils/formula/referenced-paths.utils.d.ts +7 -0
  130. package/dist/utils/formula/referenced-paths.utils.d.ts.map +1 -0
  131. package/dist/utils/formula/referenced-paths.utils.js +18 -0
  132. package/dist/utils/formula/referenced-paths.utils.js.map +1 -0
  133. package/dist/utils/formula/render-formula.utils.d.ts +8 -0
  134. package/dist/utils/formula/render-formula.utils.d.ts.map +1 -0
  135. package/dist/utils/formula/render-formula.utils.js +39 -0
  136. package/dist/utils/formula/render-formula.utils.js.map +1 -0
  137. package/dist/utils/formula/serialize-formula.utils.d.ts +14 -0
  138. package/dist/utils/formula/serialize-formula.utils.d.ts.map +1 -0
  139. package/dist/utils/formula/serialize-formula.utils.js +33 -0
  140. package/dist/utils/formula/serialize-formula.utils.js.map +1 -0
  141. package/dist/utils/formula/validate-formula.utils.d.ts +11 -0
  142. package/dist/utils/formula/validate-formula.utils.d.ts.map +1 -0
  143. package/dist/utils/formula/validate-formula.utils.js +79 -0
  144. package/dist/utils/formula/validate-formula.utils.js.map +1 -0
  145. package/dist/utils/index.d.ts +1 -0
  146. package/dist/utils/index.d.ts.map +1 -1
  147. package/dist/utils/index.js +1 -0
  148. package/dist/utils/index.js.map +1 -1
  149. package/package.json +2 -2
  150. package/src/components/field-config-panel/advanced-settings.tsx +113 -0
  151. package/src/components/field-config-panel/field-config-panel-overlay.tsx +8 -1
  152. package/src/components/field-config-panel/field-config-panel.tsx +43 -15
  153. package/src/components/field-config-panel/field-sidebar.tsx +91 -0
  154. package/src/components/field-config-panel/formula-generator.tsx +122 -0
  155. package/src/components/field-config-panel/formula-modal.tsx +229 -0
  156. package/src/components/field-config-panel/formula-workspace.tsx +116 -0
  157. package/src/components/field-config-panel/result-type-selector.tsx +34 -0
  158. package/src/components/field-sidebar/calculated-field-type-list.tsx +29 -0
  159. package/src/components/field-sidebar/data-model-field-type-list.tsx +10 -4
  160. package/src/components/field-sidebar/field-menu-group.tsx +36 -0
  161. package/src/components/field-sidebar/field-sidebar.tsx +14 -55
  162. package/src/components/field-sidebar/fillable-field-type-list.tsx +11 -9
  163. package/src/components/pdf-editor/pdf-editor.tsx +2 -0
  164. package/src/components/pdf-fields-overlay/pdf-overlay-field-calculated.tsx +15 -0
  165. package/src/components/pdf-fields-overlay/pdf-overlay-field.tsx +10 -5
  166. package/src/components/pdf-view/pdf-view-calculated.tsx +23 -0
  167. package/src/components/pdf-view/pdf-view.tsx +4 -0
  168. package/src/constants/field.constants.ts +9 -2
  169. package/src/constants/menu-group.ts +26 -0
  170. package/src/hooks/index.ts +1 -0
  171. package/src/hooks/useFieldDrag.ts +84 -8
  172. package/src/hooks/useFormulaEditor.ts +336 -0
  173. package/src/hooks/usePdfFieldDnD.ts +36 -14
  174. package/src/interface/types.ts +38 -2
  175. package/src/styles/formula-modal.css +307 -0
  176. package/src/styles/index.css +1 -0
  177. package/src/styles/pdf-field-overlay.css +1 -0
  178. package/src/utils/data-model/extract-fields.utils.ts +65 -7
  179. package/src/utils/formula/caret.utils.ts +125 -0
  180. package/src/utils/formula/dom.utils.ts +35 -0
  181. package/src/utils/formula/evaluate-formula.utils.ts +159 -0
  182. package/src/utils/formula/expression.utils.ts +99 -0
  183. package/src/utils/formula/format-calculated-result.utils.ts +79 -0
  184. package/src/utils/formula/formula-types.ts +2 -0
  185. package/src/utils/formula/index.ts +10 -0
  186. package/src/utils/formula/referenced-paths.utils.ts +18 -0
  187. package/src/utils/formula/render-formula.utils.ts +40 -0
  188. package/src/utils/formula/serialize-formula.utils.ts +40 -0
  189. package/src/utils/formula/validate-formula.utils.ts +94 -0
  190. package/src/utils/index.ts +1 -0
@@ -0,0 +1,336 @@
1
+ import {
2
+ KeyboardEvent as ReactKeyboardEvent,
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
9
+ import {
10
+ getCaretExpressionIndex,
11
+ placeCaretAtEnd,
12
+ readExpressionFromEditor,
13
+ renderFormulaHtml,
14
+ setCaretByExpressionIndex,
15
+ tokenizeExpression,
16
+ } from '../utils';
17
+
18
+ export const useFormulaEditor = (params: {
19
+ currentExpression: string;
20
+ opened: boolean;
21
+ pathToLabel: Map<string, string>;
22
+ }) => {
23
+ const { currentExpression, opened, pathToLabel } = params;
24
+ const [draftExpression, setDraftExpression] = useState(currentExpression);
25
+ const [history, setHistory] = useState<string[]>([currentExpression]);
26
+ const [historyIndex, setHistoryIndex] = useState(0);
27
+ const [isDirty, setIsDirty] = useState(false);
28
+ const editorRef = useRef<HTMLDivElement | null>(null);
29
+ const updateSourceRef = useRef<'editor' | 'state' | null>(null);
30
+ const pendingCaretRef = useRef<number | null>(null);
31
+ const lastCaretRef = useRef(0);
32
+ const historyIndexRef = useRef(0);
33
+ const historyRef = useRef<string[]>([currentExpression]);
34
+
35
+ useEffect(() => {
36
+ if (opened) {
37
+ setDraftExpression(currentExpression);
38
+ setIsDirty(false);
39
+ setHistory([currentExpression]);
40
+ setHistoryIndex(0);
41
+ historyIndexRef.current = 0;
42
+ historyRef.current = [currentExpression];
43
+ const editor = editorRef.current;
44
+ if (editor) {
45
+ editor.innerHTML = renderFormulaHtml(currentExpression, pathToLabel);
46
+ placeCaretAtEnd(editor);
47
+ }
48
+ }
49
+ }, [opened, currentExpression, pathToLabel]);
50
+
51
+ useEffect(() => {
52
+ const editor = editorRef.current;
53
+ if (!editor) {
54
+ return;
55
+ }
56
+ if (updateSourceRef.current === 'editor') {
57
+ updateSourceRef.current = null;
58
+ return;
59
+ }
60
+ editor.innerHTML = renderFormulaHtml(draftExpression, pathToLabel);
61
+ if (pendingCaretRef.current !== null) {
62
+ setCaretByExpressionIndex(editor, pendingCaretRef.current);
63
+ pendingCaretRef.current = null;
64
+ } else {
65
+ placeCaretAtEnd(editor);
66
+ }
67
+ }, [draftExpression, pathToLabel]);
68
+
69
+ useEffect(() => {
70
+ historyIndexRef.current = historyIndex;
71
+ historyRef.current = history;
72
+ }, [historyIndex, history]);
73
+
74
+ const handleEditorInput = useCallback(() => {
75
+ const editor = editorRef.current;
76
+ if (!editor) {
77
+ return;
78
+ }
79
+ const nextValue = readExpressionFromEditor(editor);
80
+ const caretIndex = getCaretExpressionIndex(editor);
81
+ pendingCaretRef.current = caretIndex;
82
+ lastCaretRef.current = caretIndex;
83
+ updateSourceRef.current = 'editor';
84
+ setDraftExpression(nextValue);
85
+ setIsDirty(true);
86
+ const cur = historyIndexRef.current;
87
+ const hist = historyRef.current;
88
+ const base = hist.slice(0, cur + 1);
89
+ if (base[base.length - 1] !== nextValue) {
90
+ setHistory([...base, nextValue]);
91
+ setHistoryIndex(base.length);
92
+ }
93
+ }, []);
94
+
95
+ const getCaretIndexForInsert = useCallback((): number => {
96
+ const editor = editorRef.current;
97
+ if (!editor) {
98
+ return draftExpression.length;
99
+ }
100
+ const sel = window.getSelection();
101
+ if (sel?.rangeCount && editor.contains(sel.getRangeAt(0).startContainer)) {
102
+ const idx = getCaretExpressionIndex(editor);
103
+ lastCaretRef.current = idx;
104
+ return idx;
105
+ }
106
+ return lastCaretRef.current;
107
+ }, [draftExpression.length]);
108
+
109
+ const pushHistory = useCallback((nextValue: string, nextCaret: number) => {
110
+ pendingCaretRef.current = nextCaret;
111
+ lastCaretRef.current = nextCaret;
112
+ setDraftExpression(nextValue);
113
+ setIsDirty(true);
114
+ const cur = historyIndexRef.current;
115
+ const hist = historyRef.current;
116
+ const base = hist.slice(0, cur + 1);
117
+ if (base[base.length - 1] !== nextValue) {
118
+ setHistory([...base, nextValue]);
119
+ setHistoryIndex(base.length);
120
+ }
121
+ requestAnimationFrame(() => editorRef.current?.focus());
122
+ }, []);
123
+
124
+ const insertField = useCallback(
125
+ (path: string) => {
126
+ const editor = editorRef.current;
127
+ if (!editor) {
128
+ return;
129
+ }
130
+ const inserted = `${path} `;
131
+ const caretIndex = getCaretIndexForInsert();
132
+ const before = draftExpression.slice(0, caretIndex);
133
+ const after = draftExpression.slice(caretIndex);
134
+ const nextValue = `${before}${inserted}${after}`;
135
+ updateSourceRef.current = 'state';
136
+ pushHistory(nextValue, caretIndex + inserted.length);
137
+ },
138
+ [draftExpression, getCaretIndexForInsert, pushHistory],
139
+ );
140
+
141
+ const insertOperator = useCallback(
142
+ (op: string) => {
143
+ const editor = editorRef.current;
144
+ if (!editor) {
145
+ return;
146
+ }
147
+ const caretIndex = getCaretIndexForInsert();
148
+ const before = draftExpression.slice(0, caretIndex);
149
+ const after = draftExpression.slice(caretIndex);
150
+ const inserted = ` ${op} `;
151
+ const nextValue = `${before}${inserted}${after}`;
152
+ updateSourceRef.current = 'state';
153
+ pushHistory(nextValue, caretIndex + inserted.length);
154
+ },
155
+ [draftExpression, getCaretIndexForInsert, pushHistory],
156
+ );
157
+
158
+ const removeFieldAtIndex = useCallback(
159
+ (fieldIndex: number) => {
160
+ const parts = tokenizeExpression(draftExpression);
161
+ let currentFieldIndex = -1;
162
+ let index = 0;
163
+ let removed = false;
164
+ const nextValue = parts
165
+ .map(part => {
166
+ if (part.type === 'field') {
167
+ currentFieldIndex += 1;
168
+ if (currentFieldIndex === fieldIndex) {
169
+ removed = true;
170
+ index += part.value.length;
171
+ return '';
172
+ }
173
+ }
174
+ index += part.value.length;
175
+ return part.value;
176
+ })
177
+ .join('');
178
+ if (!removed) {
179
+ return;
180
+ }
181
+ updateSourceRef.current = 'state';
182
+ const nextCaret = Math.min(index, nextValue.length);
183
+ pendingCaretRef.current = nextCaret;
184
+ lastCaretRef.current = nextCaret;
185
+ setDraftExpression(nextValue);
186
+ setIsDirty(true);
187
+ const cur = historyIndexRef.current;
188
+ const hist = historyRef.current;
189
+ const base = hist.slice(0, cur + 1);
190
+ if (base[base.length - 1] !== nextValue) {
191
+ setHistory([...base, nextValue]);
192
+ setHistoryIndex(base.length);
193
+ }
194
+ requestAnimationFrame(() => editorRef.current?.focus());
195
+ },
196
+ [draftExpression],
197
+ );
198
+
199
+ const handleKeyDown = useCallback((e: ReactKeyboardEvent<HTMLDivElement>) => {
200
+ if (e.ctrlKey || e.metaKey) {
201
+ const key = e.key.toLowerCase();
202
+ if (key === 'z') {
203
+ e.preventDefault();
204
+ if (historyIndexRef.current > 0) {
205
+ const next = historyRef.current[historyIndexRef.current - 1];
206
+ updateSourceRef.current = 'state';
207
+ pendingCaretRef.current = next.length;
208
+ setDraftExpression(next);
209
+ setHistoryIndex(historyIndexRef.current - 1);
210
+ setIsDirty(true);
211
+ }
212
+ return;
213
+ }
214
+ if (key === 'y' || (e.shiftKey && key === 'z')) {
215
+ e.preventDefault();
216
+ const cur = historyIndexRef.current;
217
+ const hist = historyRef.current;
218
+ if (cur < hist.length - 1) {
219
+ const next = hist[cur + 1];
220
+ updateSourceRef.current = 'state';
221
+ pendingCaretRef.current = next.length;
222
+ setDraftExpression(next);
223
+ setHistoryIndex(cur + 1);
224
+ setIsDirty(true);
225
+ }
226
+ return;
227
+ }
228
+ }
229
+ const allowed = new Set([
230
+ '+',
231
+ '-',
232
+ '*',
233
+ '/',
234
+ '(',
235
+ ')',
236
+ '0',
237
+ '1',
238
+ '2',
239
+ '3',
240
+ '4',
241
+ '5',
242
+ '6',
243
+ '7',
244
+ '8',
245
+ '9',
246
+ '.',
247
+ 'Backspace',
248
+ 'Delete',
249
+ 'ArrowLeft',
250
+ 'ArrowRight',
251
+ 'ArrowUp',
252
+ 'ArrowDown',
253
+ 'Home',
254
+ 'End',
255
+ 'Tab',
256
+ ' ',
257
+ ]);
258
+ if (!allowed.has(e.key)) {
259
+ e.preventDefault();
260
+ }
261
+ }, []);
262
+
263
+ const handlePaste = useCallback(
264
+ (text: string) => {
265
+ if (!text?.trim()) {
266
+ return;
267
+ }
268
+ const parts = tokenizeExpression(text).filter(
269
+ p => p.type !== 'text' || p.value.trim() !== '',
270
+ );
271
+ const sanitized = parts.map(p => p.value).join('');
272
+ if (!sanitized) {
273
+ return;
274
+ }
275
+ const editor = editorRef.current;
276
+ if (!editor) {
277
+ return;
278
+ }
279
+ const caretIndex = getCaretIndexForInsert();
280
+ const before = draftExpression.slice(0, caretIndex);
281
+ const after = draftExpression.slice(caretIndex);
282
+ const nextValue = `${before}${sanitized}${after}`;
283
+ updateSourceRef.current = 'state';
284
+ pushHistory(nextValue, caretIndex + sanitized.length);
285
+ },
286
+ [draftExpression, getCaretIndexForInsert, pushHistory],
287
+ );
288
+
289
+ const undo = useCallback(() => {
290
+ if (historyIndexRef.current > 0) {
291
+ const next = historyRef.current[historyIndexRef.current - 1];
292
+ updateSourceRef.current = 'state';
293
+ pendingCaretRef.current = next.length;
294
+ setDraftExpression(next);
295
+ setHistoryIndex(historyIndexRef.current - 1);
296
+ setIsDirty(true);
297
+ requestAnimationFrame(() => editorRef.current?.focus());
298
+ }
299
+ }, []);
300
+
301
+ const redo = useCallback(() => {
302
+ const cur = historyIndexRef.current;
303
+ const hist = historyRef.current;
304
+ if (cur < hist.length - 1) {
305
+ const next = hist[cur + 1];
306
+ updateSourceRef.current = 'state';
307
+ pendingCaretRef.current = next.length;
308
+ setDraftExpression(next);
309
+ setHistoryIndex(cur + 1);
310
+ setIsDirty(true);
311
+ requestAnimationFrame(() => editorRef.current?.focus());
312
+ }
313
+ }, []);
314
+
315
+ const selectedFieldPaths = useMemo(() => {
316
+ const parts = tokenizeExpression(draftExpression);
317
+ return new Set(parts.filter(p => p.type === 'field').map(p => p.value));
318
+ }, [draftExpression]);
319
+
320
+ return {
321
+ draftExpression,
322
+ editorRef,
323
+ isDirty,
324
+ canUndo: historyIndex > 0,
325
+ canRedo: historyIndex < history.length - 1,
326
+ undo,
327
+ redo,
328
+ handleEditorInput,
329
+ handleKeyDown,
330
+ handlePaste,
331
+ insertField,
332
+ insertOperator,
333
+ removeFieldAtIndex,
334
+ selectedFieldPaths,
335
+ };
336
+ };
@@ -1,4 +1,4 @@
1
- import { DragEvent, RefObject, useState } from 'react';
1
+ import { DragEvent, RefObject, useCallback, useRef, useState } from 'react';
2
2
  import { v4 as uuidv4 } from 'uuid';
3
3
  import { FIELD_CONSTANTS, FILLABLE_FIELD_DEFAULT_SIZES } from '../constants';
4
4
  import {
@@ -15,6 +15,8 @@ import {
15
15
  isDragOverCanvas,
16
16
  } from '../utils';
17
17
 
18
+ const DRAG_OVER_THROTTLE_MS = 32; // ~30fps for drop effect updates
19
+
18
20
  interface UsePdfFieldDnDProps {
19
21
  fields: PdfField[];
20
22
  recipients: RecipientInfo[];
@@ -31,29 +33,45 @@ export const usePdfFieldDnD = ({
31
33
  recipients,
32
34
  }: UsePdfFieldDnDProps) => {
33
35
  const [draggedFieldOption, setDraggedFieldOption] = useState<FieldTypeOption | null>(null);
36
+ const lastDragOverTimeRef = useRef(0);
37
+ const lastDropEffectRef = useRef<'copy' | 'none'>('none');
34
38
 
35
39
  const handleDragStart = (fieldOption: FieldTypeOption) => {
36
40
  setDraggedFieldOption(fieldOption);
41
+ lastDragOverTimeRef.current = 0;
37
42
  };
38
43
 
39
44
  const handleDragEnd = () => {
40
45
  setDraggedFieldOption(null);
41
46
  };
42
47
 
43
- const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
44
- if (!draggedFieldOption) {
45
- e.dataTransfer.dropEffect = 'none';
46
- return;
47
- }
48
+ const handleDragOver = useCallback(
49
+ (e: DragEvent<HTMLDivElement>) => {
50
+ if (!draggedFieldOption) {
51
+ e.dataTransfer.dropEffect = 'none';
52
+ return;
53
+ }
48
54
 
49
- if (isDragOverCanvas(e, pdfWrapperRef)) {
50
- e.preventDefault();
51
- e.stopPropagation();
52
- e.dataTransfer.dropEffect = 'copy';
53
- } else {
54
- e.dataTransfer.dropEffect = 'none';
55
- }
56
- };
55
+ const now = Date.now();
56
+ if (now - lastDragOverTimeRef.current < DRAG_OVER_THROTTLE_MS) {
57
+ e.preventDefault();
58
+ e.dataTransfer.dropEffect = lastDropEffectRef.current;
59
+ return;
60
+ }
61
+ lastDragOverTimeRef.current = now;
62
+
63
+ if (isDragOverCanvas(e, pdfWrapperRef)) {
64
+ e.preventDefault();
65
+ e.stopPropagation();
66
+ e.dataTransfer.dropEffect = 'copy';
67
+ lastDropEffectRef.current = 'copy';
68
+ } else {
69
+ e.dataTransfer.dropEffect = 'none';
70
+ lastDropEffectRef.current = 'none';
71
+ }
72
+ },
73
+ [draggedFieldOption, pdfWrapperRef],
74
+ );
57
75
 
58
76
  const handleDrop = (e: DragEvent<HTMLDivElement>, pageNumber: number) => {
59
77
  e.preventDefault();
@@ -104,6 +122,10 @@ export const usePdfFieldDnD = ({
104
122
  }
105
123
  }
106
124
 
125
+ if (newField.type === FieldTypeEnum.calculated) {
126
+ newField.path = newField.id;
127
+ }
128
+
107
129
  onFieldsChange([...fields, newField]);
108
130
  onSelectField(newField.id);
109
131
  setDraggedFieldOption(null);
@@ -2,6 +2,7 @@ export enum FieldTypeEnum {
2
2
  dataModel = 'dataModel',
3
3
  eSign = 'eSign',
4
4
  fillable = 'fillable',
5
+ calculated = 'calculated',
5
6
  }
6
7
 
7
8
  export enum ESignFieldType {
@@ -13,10 +14,12 @@ export enum ESignFieldType {
13
14
 
14
15
  export type FillableFieldType = 'text' | 'date' | 'checkbox' | 'radio' | 'number';
15
16
 
17
+ export type PdfFieldSubType = FillableFieldType | ESignFieldType;
18
+
16
19
  export interface PdfField {
17
20
  id: string;
18
21
  type: FieldTypeEnum;
19
- subType?: FillableFieldType | ESignFieldType;
22
+ subType?: PdfFieldSubType;
20
23
  x: number;
21
24
  y: number;
22
25
  page: number;
@@ -27,12 +30,14 @@ export interface PdfField {
27
30
  path?: string;
28
31
  recipient?: string;
29
32
  description?: string;
33
+ formula?: StructuredFormula;
34
+ formulaFormat?: CalculatedFieldFormat;
30
35
  }
31
36
 
32
37
  export interface FieldTypeOption {
33
38
  label: string;
34
39
  type: FieldTypeEnum;
35
- subType?: FillableFieldType | ESignFieldType;
40
+ subType?: PdfFieldSubType;
36
41
  path?: string;
37
42
  }
38
43
 
@@ -41,11 +46,42 @@ export interface DataModelFieldGroup {
41
46
  fields: FieldTypeOption[];
42
47
  }
43
48
 
49
+ /** Supported formula operators */
50
+ export const FORMULA_OPERATORS = ['+', '-', '*', '/'];
51
+ export type FormulaOperator = (typeof FORMULA_OPERATORS)[number];
52
+
53
+ /** Single token in a structured formula (number, operator, paren, or field reference) */
54
+ export type FormulaToken =
55
+ | { type: 'number'; value: string }
56
+ | { type: 'operator'; value: FormulaOperator }
57
+ | { type: 'lparen'; value: '(' }
58
+ | { type: 'rparen'; value: ')' }
59
+ | { type: 'field'; path: string; label: string };
60
+
61
+ /** Structured formula representation (AST-like token list) for validation and safe editing */
62
+ export interface StructuredFormula {
63
+ tokens: FormulaToken[];
64
+ }
65
+
66
+ /** Format options for a calculated field result (display and rounding) */
67
+ export interface CalculatedFieldFormat {
68
+ resultType: 'number' | 'currency' | 'percent';
69
+ thousandsSeparator: boolean;
70
+ decimals: number;
71
+ roundingMode: 'round' | 'floor' | 'ceil';
72
+ decimalSeparatorEnabled: boolean;
73
+ decimalSeparator: '.' | ',';
74
+ prefixText: string;
75
+ postfixText: string;
76
+ }
77
+
44
78
  export interface SchemaFieldBaseOptions {
45
79
  placeholder?: any;
46
80
  description?: any;
47
81
  sampleData?: any;
48
82
  showInEditor?: boolean;
83
+ useInCalculatedFields?: boolean;
84
+ useInConditionals?: boolean;
49
85
  isHighlighted?: boolean;
50
86
  }
51
87