@pdfme/ui 0.0.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 (97) hide show
  1. package/README.md +9 -0
  2. package/__mocks__/assetsTransformer.js +7 -0
  3. package/__mocks__/form-render.js +7 -0
  4. package/__mocks__/lucide-react.js +19 -0
  5. package/dist/index.es.js +159393 -0
  6. package/dist/index.umd.js +1041 -0
  7. package/dist/types/__tests__/assets/helper.d.ts +3 -0
  8. package/dist/types/__tests__/components/Designer.test.d.ts +1 -0
  9. package/dist/types/__tests__/components/PluginIcon.test.d.ts +1 -0
  10. package/dist/types/__tests__/components/Preview.test.d.ts +1 -0
  11. package/dist/types/__tests__/helper.test.d.ts +1 -0
  12. package/dist/types/src/Designer.d.ts +21 -0
  13. package/dist/types/src/Form.d.ts +24 -0
  14. package/dist/types/src/Viewer.d.ts +15 -0
  15. package/dist/types/src/class.d.ts +89 -0
  16. package/dist/types/src/components/AppContextProvider.d.ts +11 -0
  17. package/dist/types/src/components/CtlBar.d.ts +14 -0
  18. package/dist/types/src/components/Designer/Canvas/Guides.d.ts +9 -0
  19. package/dist/types/src/components/Designer/Canvas/Mask.d.ts +4 -0
  20. package/dist/types/src/components/Designer/Canvas/Moveable.d.ts +37 -0
  21. package/dist/types/src/components/Designer/Canvas/Padding.d.ts +6 -0
  22. package/dist/types/src/components/Designer/Canvas/Selecto.d.ts +10 -0
  23. package/dist/types/src/components/Designer/Canvas/index.d.ts +22 -0
  24. package/dist/types/src/components/Designer/LeftSidebar.d.ts +8 -0
  25. package/dist/types/src/components/Designer/PluginIcon.d.ts +10 -0
  26. package/dist/types/src/components/Designer/RightSidebar/DetailView/AlignWidget.d.ts +4 -0
  27. package/dist/types/src/components/Designer/RightSidebar/DetailView/ButtonGroupWidget.d.ts +4 -0
  28. package/dist/types/src/components/Designer/RightSidebar/DetailView/WidgetRenderer.d.ts +7 -0
  29. package/dist/types/src/components/Designer/RightSidebar/DetailView/index.d.ts +8 -0
  30. package/dist/types/src/components/Designer/RightSidebar/ListView/Item.d.ts +45 -0
  31. package/dist/types/src/components/Designer/RightSidebar/ListView/SelectableSortableContainer.d.ts +4 -0
  32. package/dist/types/src/components/Designer/RightSidebar/ListView/SelectableSortableItem.d.ts +14 -0
  33. package/dist/types/src/components/Designer/RightSidebar/ListView/index.d.ts +4 -0
  34. package/dist/types/src/components/Designer/RightSidebar/index.d.ts +4 -0
  35. package/dist/types/src/components/Designer/RightSidebar/layout.d.ts +15 -0
  36. package/dist/types/src/components/Designer/index.d.ts +11 -0
  37. package/dist/types/src/components/ErrorScreen.d.ts +7 -0
  38. package/dist/types/src/components/Paper.d.ts +20 -0
  39. package/dist/types/src/components/Preview.d.ts +15 -0
  40. package/dist/types/src/components/Renderer.d.ts +13 -0
  41. package/dist/types/src/components/Root.d.ts +9 -0
  42. package/dist/types/src/components/Spinner.d.ts +3 -0
  43. package/dist/types/src/components/StaticSchema.d.ts +10 -0
  44. package/dist/types/src/components/UnitPager.d.ts +10 -0
  45. package/dist/types/src/constants.d.ts +11 -0
  46. package/dist/types/src/contexts.d.ts +10 -0
  47. package/dist/types/src/helper.d.ts +73 -0
  48. package/dist/types/src/hooks.d.ts +46 -0
  49. package/dist/types/src/i18n.d.ts +3 -0
  50. package/dist/types/src/index.d.ts +4 -0
  51. package/dist/types/src/theme.d.ts +2 -0
  52. package/dist/types/src/types.d.ts +19 -0
  53. package/eslint.config.mjs +41 -0
  54. package/package.json +127 -0
  55. package/src/Designer.tsx +107 -0
  56. package/src/Form.tsx +102 -0
  57. package/src/Viewer.tsx +59 -0
  58. package/src/class.ts +188 -0
  59. package/src/components/AppContextProvider.tsx +78 -0
  60. package/src/components/CtlBar.tsx +183 -0
  61. package/src/components/Designer/Canvas/Guides.tsx +49 -0
  62. package/src/components/Designer/Canvas/Mask.tsx +20 -0
  63. package/src/components/Designer/Canvas/Moveable.tsx +91 -0
  64. package/src/components/Designer/Canvas/Padding.tsx +56 -0
  65. package/src/components/Designer/Canvas/Selecto.tsx +45 -0
  66. package/src/components/Designer/Canvas/index.tsx +536 -0
  67. package/src/components/Designer/LeftSidebar.tsx +120 -0
  68. package/src/components/Designer/PluginIcon.tsx +87 -0
  69. package/src/components/Designer/RightSidebar/DetailView/AlignWidget.tsx +229 -0
  70. package/src/components/Designer/RightSidebar/DetailView/ButtonGroupWidget.tsx +78 -0
  71. package/src/components/Designer/RightSidebar/DetailView/WidgetRenderer.tsx +28 -0
  72. package/src/components/Designer/RightSidebar/DetailView/index.tsx +469 -0
  73. package/src/components/Designer/RightSidebar/ListView/Item.tsx +158 -0
  74. package/src/components/Designer/RightSidebar/ListView/SelectableSortableContainer.tsx +204 -0
  75. package/src/components/Designer/RightSidebar/ListView/SelectableSortableItem.tsx +88 -0
  76. package/src/components/Designer/RightSidebar/ListView/index.tsx +116 -0
  77. package/src/components/Designer/RightSidebar/index.tsx +72 -0
  78. package/src/components/Designer/RightSidebar/layout.tsx +75 -0
  79. package/src/components/Designer/index.tsx +389 -0
  80. package/src/components/ErrorScreen.tsx +33 -0
  81. package/src/components/Paper.tsx +117 -0
  82. package/src/components/Preview.tsx +220 -0
  83. package/src/components/Renderer.tsx +165 -0
  84. package/src/components/Root.tsx +38 -0
  85. package/src/components/Spinner.tsx +45 -0
  86. package/src/components/StaticSchema.tsx +50 -0
  87. package/src/components/UnitPager.tsx +119 -0
  88. package/src/constants.ts +21 -0
  89. package/src/contexts.ts +14 -0
  90. package/src/helper.ts +534 -0
  91. package/src/hooks.ts +308 -0
  92. package/src/i18n.ts +903 -0
  93. package/src/index.ts +5 -0
  94. package/src/theme.ts +20 -0
  95. package/src/types.ts +20 -0
  96. package/tsconfig.json +48 -0
  97. package/vite.config.mts +22 -0
@@ -0,0 +1,45 @@
1
+ import React, { useEffect } from 'react';
2
+ import SelectoComponent, {
3
+ OnSelect as SelectoOnSelect,
4
+ OnDragStart as SelectoOnDragStart,
5
+ } from 'react-selecto';
6
+ import { SELECTABLE_CLASSNAME } from '../../../constants.js';
7
+ import { theme } from 'antd';
8
+
9
+ type Props = {
10
+ container: HTMLElement | null;
11
+ continueSelect: boolean;
12
+ onDragStart: (e: SelectoOnDragStart) => void;
13
+ onSelect: (e: SelectoOnSelect) => void;
14
+ };
15
+
16
+ const className = 'pdfme-selecto';
17
+
18
+ const Selecto = (props: Props) => {
19
+ const { token } = theme.useToken();
20
+ useEffect(() => {
21
+ const containerElement = document.querySelector('.' + className);
22
+ if (containerElement instanceof HTMLElement) {
23
+ containerElement.style.backgroundColor = token.colorPrimary;
24
+ containerElement.style.opacity = '0.75';
25
+ containerElement.style.borderColor = token.colorPrimaryBorder;
26
+ }
27
+ }, [props.container, token.colorPrimary, token.colorPrimaryBorder]);
28
+
29
+ return (
30
+ <SelectoComponent
31
+ className={className}
32
+ selectFromInside={false}
33
+ selectByClick
34
+ preventDefault
35
+ hitRate={0}
36
+ selectableTargets={[`.${SELECTABLE_CLASSNAME}`]}
37
+ container={props.container}
38
+ continueSelect={props.continueSelect}
39
+ onDragStart={props.onDragStart}
40
+ onSelect={props.onSelect}
41
+ />
42
+ );
43
+ };
44
+
45
+ export default Selecto;
@@ -0,0 +1,536 @@
1
+ import React, {
2
+ Ref,
3
+ useMemo,
4
+ useContext,
5
+ MutableRefObject,
6
+ useRef,
7
+ useState,
8
+ useEffect,
9
+ forwardRef,
10
+ useCallback,
11
+ } from 'react';
12
+ import { theme, Button } from 'antd';
13
+ import MoveableComponent, { OnDrag, OnRotate, OnResize } from 'react-moveable';
14
+ import {
15
+ ZOOM,
16
+ SchemaForUI,
17
+ Size,
18
+ ChangeSchemas,
19
+ BasePdf,
20
+ isBlankPdf,
21
+ replacePlaceholders,
22
+ } from '@pdfme/common';
23
+ import { PluginsRegistry } from '../../../contexts.js';
24
+ import { X } from 'lucide-react';
25
+ import { RULER_HEIGHT, RIGHT_SIDEBAR_WIDTH, DESIGNER_CLASSNAME } from '../../../constants.js';
26
+ import { usePrevious } from '../../../hooks.js';
27
+ import { round, flatten, uuid } from '../../../helper.js';
28
+ import Paper from '../../Paper.js';
29
+ import Renderer from '../../Renderer.js';
30
+ import Selecto from './Selecto.js';
31
+ import Moveable from './Moveable.js';
32
+ import Guides from './Guides.js';
33
+ import Mask from './Mask.js';
34
+ import Padding from './Padding.js';
35
+ import StaticSchema from '../../StaticSchema.js';
36
+
37
+ const mm2px = (mm: number) => mm * 3.7795275591;
38
+
39
+ const DELETE_BTN_ID = uuid();
40
+ const fmt4Num = (prop: string) => Number(prop.replace('px', ''));
41
+ const fmt = (prop: string) => round(fmt4Num(prop) / ZOOM, 2);
42
+ const isTopLeftResize = (d: string) => d === '-1,-1' || d === '-1,0' || d === '0,-1';
43
+ const normalizeRotate = (angle: number) => ((angle % 360) + 360) % 360;
44
+
45
+ const DeleteButton = ({ activeElements: aes }: { activeElements: HTMLElement[] }) => {
46
+ const { token } = theme.useToken();
47
+
48
+ const size = 26;
49
+ const top = Math.min(...aes.map(({ style }) => fmt4Num(style.top)));
50
+ const left = Math.max(...aes.map(({ style }) => fmt4Num(style.left) + fmt4Num(style.width))) + 10;
51
+
52
+ return (
53
+ <Button
54
+ id={DELETE_BTN_ID}
55
+ className={DESIGNER_CLASSNAME + 'delete-button'}
56
+ style={{
57
+ position: 'absolute',
58
+ zIndex: 1,
59
+ top,
60
+ left,
61
+ width: size,
62
+ height: size,
63
+ padding: 2,
64
+ display: 'flex',
65
+ alignItems: 'center',
66
+ justifyContent: 'center',
67
+ borderRadius: token.borderRadius,
68
+ color: token.colorWhite,
69
+ background: token.colorPrimary,
70
+ }}
71
+ >
72
+ <X style={{ pointerEvents: 'none' }} />
73
+ </Button>
74
+ );
75
+ };
76
+
77
+ interface GuidesInterface {
78
+ getGuides(): number[];
79
+ scroll(pos: number): void;
80
+ scrollGuides(pos: number): void;
81
+ loadGuides(guides: number[]): void;
82
+ resize(): void;
83
+ }
84
+
85
+ interface Props {
86
+ basePdf: BasePdf;
87
+ height: number;
88
+ hoveringSchemaId: string | null;
89
+ onChangeHoveringSchemaId: (id: string | null) => void;
90
+ pageCursor: number;
91
+ schemasList: SchemaForUI[][];
92
+ scale: number;
93
+ backgrounds: string[];
94
+ pageSizes: Size[];
95
+ size: Size;
96
+ activeElements: HTMLElement[];
97
+ onEdit: (targets: HTMLElement[]) => void;
98
+ changeSchemas: ChangeSchemas;
99
+ removeSchemas: (ids: string[]) => void;
100
+ paperRefs: MutableRefObject<HTMLDivElement[]>;
101
+ sidebarOpen: boolean;
102
+ }
103
+
104
+ const Canvas = (props: Props, ref: Ref<HTMLDivElement>) => {
105
+ const {
106
+ basePdf,
107
+ pageCursor,
108
+ scale,
109
+ backgrounds,
110
+ pageSizes,
111
+ size,
112
+ activeElements,
113
+ schemasList,
114
+ hoveringSchemaId,
115
+ onEdit,
116
+ changeSchemas,
117
+ removeSchemas,
118
+ onChangeHoveringSchemaId,
119
+ paperRefs,
120
+ sidebarOpen,
121
+ } = props;
122
+ const { token } = theme.useToken();
123
+ const pluginsRegistry = useContext(PluginsRegistry);
124
+ const verticalGuides = useRef<GuidesInterface[]>([]);
125
+ const horizontalGuides = useRef<GuidesInterface[]>([]);
126
+ const moveable = useRef<MoveableComponent>(null);
127
+
128
+ const [isPressShiftKey, setIsPressShiftKey] = useState(false);
129
+ const [editing, setEditing] = useState(false);
130
+
131
+ const prevSchemas = usePrevious(schemasList[pageCursor]);
132
+
133
+ const onKeydown = (e: KeyboardEvent) => {
134
+ if (e.shiftKey) setIsPressShiftKey(true);
135
+ };
136
+ const onKeyup = (e: KeyboardEvent) => {
137
+ if (e.key === 'Shift' || !e.shiftKey) setIsPressShiftKey(false);
138
+ if (e.key === 'Escape' || e.key === 'Esc') setEditing(false);
139
+ };
140
+
141
+ const initEvents = useCallback(() => {
142
+ window.addEventListener('keydown', onKeydown);
143
+ window.addEventListener('keyup', onKeyup);
144
+ }, []);
145
+
146
+ const destroyEvents = useCallback(() => {
147
+ window.removeEventListener('keydown', onKeydown);
148
+ window.removeEventListener('keyup', onKeyup);
149
+ }, []);
150
+
151
+ useEffect(() => {
152
+ initEvents();
153
+
154
+ return destroyEvents;
155
+ }, [initEvents, destroyEvents]);
156
+
157
+ useEffect(() => {
158
+ moveable.current?.updateRect();
159
+ if (!prevSchemas) {
160
+ return;
161
+ }
162
+
163
+ const prevSchemaKeys = JSON.stringify(prevSchemas[pageCursor] || {});
164
+ const schemaKeys = JSON.stringify(schemasList[pageCursor] || {});
165
+
166
+ if (prevSchemaKeys === schemaKeys) {
167
+ moveable.current?.updateRect();
168
+ }
169
+ }, [pageCursor, schemasList, prevSchemas]);
170
+
171
+ const onDrag = ({ target, top, left }: OnDrag) => {
172
+ const { width: _width, height: _height } = target.style;
173
+ const targetWidth = fmt(_width);
174
+ const targetHeight = fmt(_height);
175
+ const actualTop = top / ZOOM;
176
+ const actualLeft = left / ZOOM;
177
+ const { width: pageWidth, height: pageHeight } = pageSizes[pageCursor];
178
+ let topPadding = 0;
179
+ let rightPadding = 0;
180
+ let bottomPadding = 0;
181
+ let leftPadding = 0;
182
+
183
+ if (isBlankPdf(basePdf)) {
184
+ const [t, r, b, l] = basePdf.padding;
185
+ topPadding = t * ZOOM;
186
+ rightPadding = r;
187
+ bottomPadding = b;
188
+ leftPadding = l * ZOOM;
189
+ }
190
+
191
+ if (actualTop + targetHeight > pageHeight - bottomPadding) {
192
+ target.style.top = `${(pageHeight - targetHeight - bottomPadding) * ZOOM}px`;
193
+ } else {
194
+ target.style.top = `${top < topPadding ? topPadding : top}px`;
195
+ }
196
+
197
+ if (actualLeft + targetWidth > pageWidth - rightPadding) {
198
+ target.style.left = `${(pageWidth - targetWidth - rightPadding) * ZOOM}px`;
199
+ } else {
200
+ target.style.left = `${left < leftPadding ? leftPadding : left}px`;
201
+ }
202
+ };
203
+
204
+ const onDragEnd = ({ target }: { target: HTMLElement | SVGElement }) => {
205
+ const { top, left } = target.style;
206
+ changeSchemas([
207
+ { key: 'position.y', value: fmt(top), schemaId: target.id },
208
+ { key: 'position.x', value: fmt(left), schemaId: target.id },
209
+ ]);
210
+ };
211
+
212
+ const onDragEnds = ({ targets }: { targets: (HTMLElement | SVGElement)[] }) => {
213
+ const arg = targets.map(({ style: { top, left }, id }) => [
214
+ { key: 'position.y', value: fmt(top), schemaId: id },
215
+ { key: 'position.x', value: fmt(left), schemaId: id },
216
+ ]);
217
+ changeSchemas(flatten(arg));
218
+ };
219
+
220
+ const onRotate = ({ target, rotate }: OnRotate) => {
221
+ target.style.transform = `rotate(${rotate}deg)`;
222
+ };
223
+
224
+ const onRotateEnd = ({ target }: { target: HTMLElement | SVGElement }) => {
225
+ const { transform } = target.style;
226
+ const rotate = Number(transform.replace('rotate(', '').replace('deg)', ''));
227
+ const normalizedRotate = normalizeRotate(rotate);
228
+ changeSchemas([{ key: 'rotate', value: normalizedRotate, schemaId: target.id }]);
229
+ };
230
+
231
+ const onRotateEnds = ({ targets }: { targets: (HTMLElement | SVGElement)[] }) => {
232
+ const arg = targets.map(({ style: { transform }, id }) => {
233
+ const rotate = Number(transform.replace('rotate(', '').replace('deg)', ''));
234
+ const normalizedRotate = normalizeRotate(rotate);
235
+ return [{ key: 'rotate', value: normalizedRotate, schemaId: id }];
236
+ });
237
+ changeSchemas(flatten(arg));
238
+ };
239
+
240
+ const onResizeEnd = ({ target }: { target: HTMLElement | SVGElement }) => {
241
+ const { id, style } = target;
242
+ const { width, height, top, left } = style;
243
+ changeSchemas([
244
+ { key: 'position.x', value: fmt(left), schemaId: id },
245
+ { key: 'position.y', value: fmt(top), schemaId: id },
246
+ { key: 'width', value: fmt(width), schemaId: id },
247
+ { key: 'height', value: fmt(height), schemaId: id },
248
+ ]);
249
+
250
+ const targetSchema = schemasList[pageCursor].find((schema) => schema.id === id);
251
+
252
+ if (!targetSchema) return;
253
+
254
+ targetSchema.position.x = fmt(left);
255
+ targetSchema.position.y = fmt(top);
256
+ targetSchema.width = fmt(width);
257
+ targetSchema.height = fmt(height);
258
+ };
259
+
260
+ const onResizeEnds = ({ targets }: { targets: (HTMLElement | SVGElement)[] }) => {
261
+ const arg = targets.map(({ style: { width, height, top, left }, id }) => [
262
+ { key: 'width', value: fmt(width), schemaId: id },
263
+ { key: 'height', value: fmt(height), schemaId: id },
264
+ { key: 'position.y', value: fmt(top), schemaId: id },
265
+ { key: 'position.x', value: fmt(left), schemaId: id },
266
+ ]);
267
+ changeSchemas(flatten(arg));
268
+ };
269
+
270
+ const onResize = ({ target, width, height, direction }: OnResize) => {
271
+ if (!target) return;
272
+ let topPadding = 0;
273
+ let rightPadding = 0;
274
+ let bottomPadding = 0;
275
+ let leftPadding = 0;
276
+
277
+ if (isBlankPdf(basePdf)) {
278
+ const [t, r, b, l] = basePdf.padding;
279
+ topPadding = t * ZOOM;
280
+ rightPadding = mm2px(r);
281
+ bottomPadding = mm2px(b);
282
+ leftPadding = l * ZOOM;
283
+ }
284
+
285
+ const pageWidth = mm2px(pageSizes[pageCursor].width);
286
+ const pageHeight = mm2px(pageSizes[pageCursor].height);
287
+
288
+ const obj: { top?: string; left?: string; width: string; height: string } = {
289
+ width: `${width}px`,
290
+ height: `${height}px`,
291
+ };
292
+
293
+ const s = target.style;
294
+ let newLeft = fmt4Num(s.left) + (fmt4Num(s.width) - width);
295
+ let newTop = fmt4Num(s.top) + (fmt4Num(s.height) - height);
296
+ if (newLeft < leftPadding) {
297
+ newLeft = leftPadding;
298
+ }
299
+ if (newTop < topPadding) {
300
+ newTop = topPadding;
301
+ }
302
+ if (newLeft + width > pageWidth - rightPadding) {
303
+ obj.width = `${pageWidth - rightPadding - newLeft}px`;
304
+ }
305
+ if (newTop + height > pageHeight - bottomPadding) {
306
+ obj.height = `${pageHeight - bottomPadding - newTop}px`;
307
+ }
308
+
309
+ const d = direction.toString();
310
+ if (isTopLeftResize(d)) {
311
+ obj.top = `${newTop}px`;
312
+ obj.left = `${newLeft}px`;
313
+ } else if (d === '1,-1') {
314
+ obj.top = `${newTop}px`;
315
+ } else if (d === '-1,1') {
316
+ obj.left = `${newLeft}px`;
317
+ }
318
+ Object.assign(s, obj);
319
+ };
320
+
321
+ const getGuideLines = (guides: GuidesInterface[], index: number) =>
322
+ guides[index] && guides[index].getGuides().map((g) => g * ZOOM);
323
+
324
+ const onClickMoveable = () => {
325
+ // Just set editing to true without trying to access event properties
326
+ setEditing(true);
327
+ };
328
+
329
+ const rotatable = useMemo(() => {
330
+ const selectedSchemas = (schemasList[pageCursor] || []).filter((s) =>
331
+ activeElements.map((ae) => ae.id).includes(s.id),
332
+ );
333
+ const schemaTypes = selectedSchemas.map((s) => s.type);
334
+ const uniqueSchemaTypes = [...new Set(schemaTypes)];
335
+
336
+ // Create a type-safe array of default schemas
337
+ const defaultSchemas: Record<string, unknown>[] = [];
338
+
339
+ pluginsRegistry.entries().forEach(([, plugin]) => {
340
+ if (plugin.propPanel.defaultSchema) {
341
+ defaultSchemas.push(plugin.propPanel.defaultSchema as Record<string, unknown>);
342
+ }
343
+ });
344
+
345
+ // Check if all schema types have rotate property
346
+ return uniqueSchemaTypes.every((type) => {
347
+ const matchingSchema = defaultSchemas.find((ds) => ds && 'type' in ds && ds.type === type);
348
+ return matchingSchema && 'rotate' in matchingSchema;
349
+ });
350
+ }, [activeElements, pageCursor, schemasList, pluginsRegistry]);
351
+
352
+ return (
353
+ <div
354
+ className={DESIGNER_CLASSNAME + 'canvas'}
355
+ style={{
356
+ position: 'relative',
357
+ overflow: 'auto',
358
+ marginRight: sidebarOpen ? RIGHT_SIDEBAR_WIDTH : 0,
359
+ ...size,
360
+ }}
361
+ ref={ref}
362
+ >
363
+ <Selecto
364
+ container={paperRefs.current[pageCursor]}
365
+ continueSelect={isPressShiftKey}
366
+ onDragStart={(e) => {
367
+ // Use type assertion to safely access inputEvent properties
368
+ const inputEvent = e.inputEvent as MouseEvent | TouchEvent;
369
+ const target = inputEvent.target as Element | null;
370
+ const isMoveableElement = moveable.current?.isMoveableElement(target as Element);
371
+
372
+ if ((inputEvent.type === 'touchstart' && e.isTrusted) || isMoveableElement) {
373
+ e.stop();
374
+ }
375
+
376
+ if (paperRefs.current[pageCursor] === target) {
377
+ onEdit([]);
378
+ }
379
+
380
+ // Check if the target is an HTMLElement and has an id property
381
+ const targetElement = target as HTMLElement | null;
382
+ if (targetElement && targetElement.id === DELETE_BTN_ID) {
383
+ removeSchemas(activeElements.map((ae) => ae.id));
384
+ }
385
+ }}
386
+ onSelect={(e) => {
387
+ // Use type assertions to safely access properties
388
+ const inputEvent = e.inputEvent as MouseEvent | TouchEvent;
389
+ const added = e.added as HTMLElement[];
390
+ const removed = e.removed as HTMLElement[];
391
+ const selected = e.selected as HTMLElement[];
392
+
393
+ const isClick = inputEvent.type === 'mousedown';
394
+ let newActiveElements: HTMLElement[] = isClick ? selected : [];
395
+
396
+ if (!isClick && added.length > 0) {
397
+ newActiveElements = activeElements.concat(added);
398
+ }
399
+ if (!isClick && removed.length > 0) {
400
+ newActiveElements = activeElements.filter((ae) => !removed.includes(ae));
401
+ }
402
+ onEdit(newActiveElements);
403
+
404
+ if (newActiveElements != activeElements) {
405
+ setEditing(false);
406
+ }
407
+
408
+ // For MacOS CMD+SHIFT+3/4 screenshots where the keydown event is never received, check mouse too
409
+ const mouseEvent = inputEvent as MouseEvent;
410
+ if (mouseEvent && typeof mouseEvent.shiftKey === 'boolean' && !mouseEvent.shiftKey) {
411
+ setIsPressShiftKey(false);
412
+ }
413
+ }}
414
+ />
415
+ <Paper
416
+ paperRefs={paperRefs}
417
+ scale={scale}
418
+ size={size}
419
+ schemasList={schemasList}
420
+ pageSizes={pageSizes}
421
+ backgrounds={backgrounds}
422
+ hasRulers={true}
423
+ renderPaper={({ index, paperSize }) => (
424
+ <>
425
+ {!editing && activeElements.length > 0 && pageCursor === index && (
426
+ <DeleteButton activeElements={activeElements} />
427
+ )}
428
+ <Padding basePdf={basePdf} />
429
+ <StaticSchema
430
+ template={{ schemas: schemasList, basePdf }}
431
+ input={Object.fromEntries(
432
+ schemasList.flat().map(({ name, content = '' }) => [name, content]),
433
+ )}
434
+ scale={scale}
435
+ totalPages={schemasList.length}
436
+ currentPage={index + 1}
437
+ />
438
+ <Guides
439
+ paperSize={paperSize}
440
+ horizontalRef={(e) => {
441
+ if (e) horizontalGuides.current[index] = e;
442
+ }}
443
+ verticalRef={(e) => {
444
+ if (e) verticalGuides.current[index] = e;
445
+ }}
446
+ />
447
+ {pageCursor !== index ? (
448
+ <Mask
449
+ width={paperSize.width + RULER_HEIGHT}
450
+ height={paperSize.height + RULER_HEIGHT}
451
+ />
452
+ ) : (
453
+ !editing && (
454
+ <Moveable
455
+ ref={moveable}
456
+ target={activeElements}
457
+ bounds={{ left: 0, top: 0, bottom: paperSize.height, right: paperSize.width }}
458
+ horizontalGuidelines={getGuideLines(horizontalGuides.current, index)}
459
+ verticalGuidelines={getGuideLines(verticalGuides.current, index)}
460
+ keepRatio={isPressShiftKey}
461
+ rotatable={rotatable}
462
+ onDrag={onDrag}
463
+ onDragEnd={onDragEnd}
464
+ onDragGroupEnd={onDragEnds}
465
+ onRotate={onRotate}
466
+ onRotateEnd={onRotateEnd}
467
+ onRotateGroupEnd={onRotateEnds}
468
+ onResize={onResize}
469
+ onResizeEnd={onResizeEnd}
470
+ onResizeGroupEnd={onResizeEnds}
471
+ onClick={onClickMoveable}
472
+ />
473
+ )
474
+ )}
475
+ </>
476
+ )}
477
+ renderSchema={({ schema, index }) => {
478
+ const mode =
479
+ editing && activeElements.map((ae) => ae.id).includes(schema.id)
480
+ ? 'designer'
481
+ : 'viewer';
482
+
483
+ const content = schema.content || '';
484
+ let value = content;
485
+
486
+ if (mode !== 'designer' && schema.readOnly) {
487
+ const variables = {
488
+ ...schemasList.flat().reduce(
489
+ (acc, currSchema) => {
490
+ acc[currSchema.name] = currSchema.content || '';
491
+ return acc;
492
+ },
493
+ {} as Record<string, string>,
494
+ ),
495
+ totalPages: schemasList.length,
496
+ currentPage: index + 1,
497
+ };
498
+
499
+ value = replacePlaceholders({ content, variables, schemas: schemasList });
500
+ }
501
+
502
+ return (
503
+ <Renderer
504
+ key={schema.id}
505
+ schema={schema}
506
+ basePdf={basePdf}
507
+ value={value}
508
+ onChangeHoveringSchemaId={onChangeHoveringSchemaId}
509
+ mode={mode}
510
+ onChange={
511
+ (schemasList[pageCursor] || []).some((s) => s.id === schema.id)
512
+ ? (arg) => {
513
+ // Use type assertion to safely handle the argument
514
+ type ChangeArg = { key: string; value: unknown };
515
+ const args = Array.isArray(arg) ? (arg as ChangeArg[]) : [arg as ChangeArg];
516
+ changeSchemas(
517
+ args.map(({ key, value }) => ({ key, value, schemaId: schema.id })),
518
+ );
519
+ }
520
+ : undefined
521
+ }
522
+ stopEditing={() => setEditing(false)}
523
+ outline={`1px ${hoveringSchemaId === schema.id ? 'solid' : 'dashed'} ${
524
+ schema.readOnly && hoveringSchemaId !== schema.id
525
+ ? 'transparent'
526
+ : token.colorPrimary
527
+ }`}
528
+ scale={scale}
529
+ />
530
+ );
531
+ }}
532
+ />
533
+ </div>
534
+ );
535
+ };
536
+ export default forwardRef<HTMLDivElement, Props>(Canvas);
@@ -0,0 +1,120 @@
1
+ import React, { useContext, useState, useEffect } from 'react';
2
+ import { Schema, Plugin, BasePdf, getFallbackFontName } from '@pdfme/common';
3
+ import { theme, Button } from 'antd';
4
+ import { useDraggable } from '@dnd-kit/core';
5
+ import { CSS } from '@dnd-kit/utilities';
6
+ import Renderer from '../Renderer.js';
7
+ import { LEFT_SIDEBAR_WIDTH, DESIGNER_CLASSNAME } from '../../constants.js';
8
+ import { setFontNameRecursively } from '../../helper';
9
+ import { OptionsContext, PluginsRegistry } from '../../contexts.js';
10
+ import PluginIcon from './PluginIcon.js';
11
+
12
+ const Draggable = (props: {
13
+ plugin: Plugin<Schema>;
14
+ scale: number;
15
+ basePdf: BasePdf;
16
+ children: React.ReactNode;
17
+ }) => {
18
+ const { scale, basePdf, plugin } = props;
19
+ const { token } = theme.useToken();
20
+ const options = useContext(OptionsContext);
21
+ const defaultSchema = plugin.propPanel.defaultSchema;
22
+ if (options.font) {
23
+ const fontName = getFallbackFontName(options.font);
24
+ setFontNameRecursively(defaultSchema, fontName);
25
+ }
26
+ const draggable = useDraggable({ id: defaultSchema.type, data: defaultSchema });
27
+ const { listeners, setNodeRef, attributes, transform, isDragging } = draggable;
28
+ const style = { transform: CSS.Translate.toString(transform) };
29
+
30
+ const renderedSchema = React.useMemo(
31
+ () => (
32
+ <div style={{ transform: `scale(${scale})` }}>
33
+ <Renderer
34
+ schema={{ ...defaultSchema, id: defaultSchema.type }}
35
+ basePdf={basePdf}
36
+ value={defaultSchema.content || ''}
37
+ onChangeHoveringSchemaId={() => {
38
+ void 0;
39
+ }}
40
+ mode={'viewer'}
41
+ outline={`1px solid ${token.colorPrimary}`}
42
+ scale={scale}
43
+ />
44
+ </div>
45
+ ),
46
+ [defaultSchema, basePdf, scale, token.colorPrimary],
47
+ );
48
+
49
+ return (
50
+ <div ref={setNodeRef} style={style} {...listeners} {...attributes}>
51
+ {isDragging && renderedSchema}
52
+ <div style={{ visibility: isDragging ? 'hidden' : 'visible' }}>{props.children}</div>
53
+ </div>
54
+ );
55
+ };
56
+
57
+ const LeftSidebar = ({
58
+ height,
59
+ scale,
60
+ basePdf,
61
+ }: {
62
+ height: number;
63
+ scale: number;
64
+ basePdf: BasePdf;
65
+ }) => {
66
+ const { token } = theme.useToken();
67
+ const pluginsRegistry = useContext(PluginsRegistry);
68
+ const [isDragging, setIsDragging] = useState(false);
69
+
70
+ useEffect(() => {
71
+ const handleMouseUp = () => {
72
+ if (isDragging) {
73
+ setIsDragging(false);
74
+ }
75
+ };
76
+
77
+ document.addEventListener('mouseup', handleMouseUp);
78
+
79
+ return () => {
80
+ document.removeEventListener('mouseup', handleMouseUp);
81
+ };
82
+ }, [isDragging]);
83
+
84
+ return (
85
+ <div
86
+ className={DESIGNER_CLASSNAME + 'left-sidebar'}
87
+ style={{
88
+ left: 0,
89
+ right: 0,
90
+ position: 'absolute',
91
+ zIndex: 1,
92
+ height,
93
+ width: LEFT_SIDEBAR_WIDTH,
94
+ background: token.colorBgLayout,
95
+ textAlign: 'center',
96
+ overflow: isDragging ? 'visible' : 'auto',
97
+ }}
98
+ >
99
+ {pluginsRegistry.entries().map(([label, plugin]) => {
100
+ if (!plugin?.propPanel.defaultSchema) return null;
101
+
102
+ const pluginType = plugin.propPanel.defaultSchema.type;
103
+
104
+ return (
105
+ <Draggable key={label} scale={scale} basePdf={basePdf} plugin={plugin}>
106
+ <Button
107
+ className={DESIGNER_CLASSNAME + 'plugin-' + pluginType}
108
+ onMouseDown={() => setIsDragging(true)}
109
+ style={{ width: 35, height: 35, marginTop: '0.25rem', padding: '0.25rem' }}
110
+ >
111
+ <PluginIcon plugin={plugin} label={label} />
112
+ </Button>
113
+ </Draggable>
114
+ );
115
+ })}
116
+ </div>
117
+ );
118
+ };
119
+
120
+ export default LeftSidebar;