@merchi/product-editor 0.1.9

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 (123) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/README.md +174 -0
  3. package/dist/components/AlignMenu.d.ts +3 -0
  4. package/dist/components/AlignMenu.js +61 -0
  5. package/dist/components/FloatingToolbar.d.ts +4 -0
  6. package/dist/components/FloatingToolbar.js +97 -0
  7. package/dist/components/FormatButtons.d.ts +2 -0
  8. package/dist/components/FormatButtons.js +22 -0
  9. package/dist/components/ImageNavButton.d.ts +7 -0
  10. package/dist/components/ImageNavButton.js +14 -0
  11. package/dist/components/ImageZoomModal.d.ts +12 -0
  12. package/dist/components/ImageZoomModal.js +129 -0
  13. package/dist/components/LayerPanel.d.ts +4 -0
  14. package/dist/components/LayerPanel.js +223 -0
  15. package/dist/components/LoadingOverlay.d.ts +9 -0
  16. package/dist/components/LoadingOverlay.js +17 -0
  17. package/dist/components/ProductEditor.d.ts +4 -0
  18. package/dist/components/ProductEditor.js +67 -0
  19. package/dist/components/ProductImageGallery.d.ts +6 -0
  20. package/dist/components/ProductImageGallery.js +148 -0
  21. package/dist/components/ProductPreviews.d.ts +7 -0
  22. package/dist/components/ProductPreviews.js +281 -0
  23. package/dist/components/TextToolbar.d.ts +4 -0
  24. package/dist/components/TextToolbar.js +117 -0
  25. package/dist/components/Toolbar.d.ts +3 -0
  26. package/dist/components/Toolbar.js +47 -0
  27. package/dist/config/colorConfig.d.ts +1 -0
  28. package/dist/config/colorConfig.js +10 -0
  29. package/dist/config/fontConfig.d.ts +5 -0
  30. package/dist/config/fontConfig.js +21 -0
  31. package/dist/context/ProductEditorContext.d.ts +60 -0
  32. package/dist/context/ProductEditorContext.js +743 -0
  33. package/dist/hooks/useSwipeNavigate.d.ts +11 -0
  34. package/dist/hooks/useSwipeNavigate.js +35 -0
  35. package/dist/index.d.ts +6 -0
  36. package/dist/index.js +30 -0
  37. package/dist/styles/BottomPreviewDisplay.css +65 -0
  38. package/dist/styles/FloatingToolbar.css +41 -0
  39. package/dist/styles/ImageGallery.css +147 -0
  40. package/dist/styles/ImageNavButton.css +35 -0
  41. package/dist/styles/ImageZoomModal.css +101 -0
  42. package/dist/styles/LayerPanel.css +179 -0
  43. package/dist/styles/LoadingOverlay.css +48 -0
  44. package/dist/styles/ProductEditor.css +192 -0
  45. package/dist/styles/TextToolbar.css +327 -0
  46. package/dist/styles/Toolbar.css +125 -0
  47. package/dist/styles/index.css +5 -0
  48. package/dist/styles/textToolbarSelectStyles.d.ts +3 -0
  49. package/dist/styles/textToolbarSelectStyles.js +30 -0
  50. package/dist/types.d.ts +228 -0
  51. package/dist/types.js +17 -0
  52. package/dist/utils/canvasAddText.d.ts +11 -0
  53. package/dist/utils/canvasAddText.js +66 -0
  54. package/dist/utils/canvasUtils.d.ts +37 -0
  55. package/dist/utils/canvasUtils.js +342 -0
  56. package/dist/utils/deviceUtils.d.ts +5 -0
  57. package/dist/utils/deviceUtils.js +33 -0
  58. package/dist/utils/draftTemplateUtils.d.ts +23 -0
  59. package/dist/utils/draftTemplateUtils.js +66 -0
  60. package/dist/utils/grid.d.ts +32 -0
  61. package/dist/utils/grid.js +151 -0
  62. package/dist/utils/imageUtils.d.ts +12 -0
  63. package/dist/utils/imageUtils.js +178 -0
  64. package/dist/utils/job.d.ts +40 -0
  65. package/dist/utils/job.js +115 -0
  66. package/dist/utils/keyboard.d.ts +1 -0
  67. package/dist/utils/keyboard.js +26 -0
  68. package/dist/utils/previewUtils.d.ts +9 -0
  69. package/dist/utils/previewUtils.js +39 -0
  70. package/dist/utils/psdConverter.d.ts +40 -0
  71. package/dist/utils/psdConverter.js +516 -0
  72. package/dist/utils/psdRenderUtils.d.ts +12 -0
  73. package/dist/utils/psdRenderUtils.js +465 -0
  74. package/dist/utils/renderUtils.d.ts +2 -0
  75. package/dist/utils/renderUtils.js +85 -0
  76. package/package.json +66 -0
  77. package/src/components/AlignMenu.tsx +49 -0
  78. package/src/components/FloatingToolbar.tsx +83 -0
  79. package/src/components/FormatButtons.tsx +31 -0
  80. package/src/components/ImageNavButton.tsx +23 -0
  81. package/src/components/ImageZoomModal.tsx +163 -0
  82. package/src/components/LayerPanel.tsx +256 -0
  83. package/src/components/LoadingOverlay.tsx +27 -0
  84. package/src/components/ProductEditor.tsx +104 -0
  85. package/src/components/ProductImageGallery.tsx +181 -0
  86. package/src/components/ProductPreviews.tsx +243 -0
  87. package/src/components/TextToolbar.tsx +164 -0
  88. package/src/components/Toolbar.tsx +86 -0
  89. package/src/config/colorConfig.ts +7 -0
  90. package/src/config/fontConfig.ts +23 -0
  91. package/src/context/ProductEditorContext.tsx +829 -0
  92. package/src/hooks/useSwipeNavigate.ts +40 -0
  93. package/src/index.ts +6 -0
  94. package/src/styles/BottomPreviewDisplay.css +65 -0
  95. package/src/styles/FloatingToolbar.css +41 -0
  96. package/src/styles/ImageGallery.css +147 -0
  97. package/src/styles/ImageNavButton.css +35 -0
  98. package/src/styles/ImageZoomModal.css +101 -0
  99. package/src/styles/LayerPanel.css +179 -0
  100. package/src/styles/LoadingOverlay.css +48 -0
  101. package/src/styles/ProductEditor.css +192 -0
  102. package/src/styles/TextToolbar.css +327 -0
  103. package/src/styles/Toolbar.css +125 -0
  104. package/src/styles/index.css +5 -0
  105. package/src/styles/textToolbarSelectStyles.ts +60 -0
  106. package/src/types/ag-psd/index.d.ts +1 -0
  107. package/src/types/fabric.d.ts +14 -0
  108. package/src/types/lodash/index.d.ts +1 -0
  109. package/src/types/psd.d.ts +53 -0
  110. package/src/types.ts +247 -0
  111. package/src/utils/canvasAddText.ts +72 -0
  112. package/src/utils/canvasUtils.ts +406 -0
  113. package/src/utils/deviceUtils.ts +31 -0
  114. package/src/utils/draftTemplateUtils.ts +70 -0
  115. package/src/utils/grid.ts +154 -0
  116. package/src/utils/imageUtils.ts +218 -0
  117. package/src/utils/job.ts +128 -0
  118. package/src/utils/keyboard.ts +28 -0
  119. package/src/utils/previewUtils.ts +43 -0
  120. package/src/utils/psdConverter.ts +595 -0
  121. package/src/utils/psdRenderUtils.ts +453 -0
  122. package/src/utils/renderUtils.ts +44 -0
  123. package/tsconfig.json +17 -0
@@ -0,0 +1,829 @@
1
+ import React, { createContext, useContext, useEffect, useRef, useState, useCallback, useMemo } from 'react';
2
+ import { fabric } from 'fabric';
3
+ import { Product, Job, DraftTemplate, DraftTemplateData, DraftPreview, SavedCanvasObject, RenderedDraftPreview } from '../types';
4
+ import { drawGrid } from '../utils/grid';
5
+ import { initDraftTemplates } from '../utils/job';
6
+ import { renderEditorOrPreview } from '../utils/renderUtils';
7
+ import { setupKeyboardEvents } from '../utils/keyboard';
8
+ import { haveDraftTemplatesChanged } from '../utils/draftTemplateUtils';
9
+ import { setNewDraftPreviews } from '../utils/previewUtils';
10
+ import { debounce } from 'lodash';
11
+ import { renderClippedImage, renderCanvasWithoutGrid } from '../utils/canvasUtils';
12
+ import { FontOption, defaultFontOptions } from '../config/fontConfig';
13
+ import { defaultPalette } from '../config/colorConfig';
14
+ import { v4 as uuidv4 } from 'uuid';
15
+ import { UseFormReturn } from 'react-hook-form';
16
+ import { getDeviceAdjustedDimensions } from '../utils/deviceUtils';
17
+
18
+
19
+ interface ProductEditorContextType {
20
+ // States
21
+ canvas: fabric.Canvas | null;
22
+ draftPreviews: DraftPreview[];
23
+ draftTemplates: DraftTemplateData[]; // Updated to use the new type
24
+ isCanvasLoading: boolean;
25
+ isMobileView: boolean;
26
+ loadingPreviews: boolean;
27
+ renderedDraftPreviews: RenderedDraftPreview[];
28
+ savedObjects: SavedCanvasObject[];
29
+ selectedTemplate: number | null;
30
+ selectedTextObject: fabric.IText | null;
31
+ showGrid: boolean;
32
+ showPreview: boolean;
33
+
34
+ // State Setters
35
+ setCanvas: (canvas: fabric.Canvas) => void;
36
+ setSelectedTemplate: (templateId: number) => void;
37
+ setShowGrid: (show: boolean) => void;
38
+
39
+ // Functions
40
+ handleCancel: () => void;
41
+ handleSave: () => void;
42
+ handleTemplateChange: (draftTemplate: DraftTemplate) => void;
43
+ recordVariationFieldObjectsOnCanvas: () => void;
44
+ togglePreview: () => void;
45
+ updateSelectedText: (props: Partial<fabric.IText>) => void;
46
+ fontOptions: FontOption[];
47
+ colorPalette: (string | null)[][];
48
+ showLayerPanel: boolean;
49
+ toggleLayerPanel: () => void;
50
+ selectedObjectId: string | null;
51
+ setSelectedObjectId: (id: string | null) => void;
52
+ selectObject: (obj: fabric.Object | null) => void;
53
+ deleteObject: (obj: fabric.Object) => void;
54
+
55
+ // Props
56
+ canvasRef: React.RefObject<HTMLCanvasElement | null>;
57
+ groupIndex: number;
58
+ height: number;
59
+ inputName: string;
60
+ job: Job;
61
+ product: Product;
62
+ width: number;
63
+ hookForm: UseFormReturn<any> | null;
64
+ }
65
+
66
+ const ProductEditorContext = createContext<ProductEditorContextType | undefined>(undefined);
67
+
68
+ interface ProductEditorProviderProps {
69
+ children: React.ReactNode;
70
+ groupIndex?: number;
71
+ product: Product;
72
+ width?: number;
73
+ height?: number;
74
+ job: Job;
75
+ onSave: () => void;
76
+ onCancel: () => void;
77
+ hookForm?: UseFormReturn<any> | null; // Type the form methods prop
78
+ fontOptions?: FontOption[];
79
+ colorPalette?: (string | null)[][];
80
+ }
81
+
82
+ export const ProductEditorProvider: React.FC<ProductEditorProviderProps> = ({
83
+ children,
84
+ groupIndex = 0,
85
+ product,
86
+ width: initialWidth = 800, // Rename prop
87
+ height: initialHeight = 600, // Rename prop
88
+ job,
89
+ onSave,
90
+ onCancel,
91
+ hookForm = null, // Initialize with null
92
+ fontOptions = defaultFontOptions,
93
+ colorPalette = defaultPalette,
94
+ }) => {
95
+
96
+ const inputName = `ownDrafts[0].images[${groupIndex || 0}]`;
97
+
98
+ // Create refs to store the latest values to prevent excessive re-renders
99
+ const allVariationsRef = useRef<any[]>([]);
100
+ const productRef = useRef(product);
101
+
102
+ // Update product ref when it changes
103
+ useEffect(() => {
104
+ productRef.current = product;
105
+ }, [product]);
106
+
107
+ // Determine dimensions and mobile status once at initialization
108
+ const deviceSettings = useMemo(() =>
109
+ getDeviceAdjustedDimensions(initialWidth, initialHeight),
110
+ [initialWidth, initialHeight]
111
+ );
112
+
113
+ // Use dimensions from device settings
114
+ const canvasWidth = deviceSettings.width;
115
+ const canvasHeight = deviceSettings.height;
116
+ const isMobileView = deviceSettings.isMobile;
117
+
118
+ const [
119
+ draftTemplates,
120
+ setDraftTemplates
121
+ ] = useState<DraftTemplateData[]>([]);
122
+ const [draftPreviews, setDraftPreviews] = useState<DraftPreview[]>([]);
123
+ const canvasRef = useRef<HTMLCanvasElement>(null);
124
+ const [canvas, setCanvas] = useState<fabric.Canvas | null>(null);
125
+ const [selectedTemplate, setSelectedTemplate] = useState<number | null>(
126
+ draftTemplates?.[0]?.template?.id || null
127
+ );
128
+ const [showGrid, setShowGrid] = useState(true);
129
+ const [showPreview, setShowPreview] = useState(true);
130
+ const [isCanvasLoading, setIsCanvasLoading] = useState(true);
131
+ const [selectedTextObject, setSelectedTextObject] = useState<fabric.IText | null>(null);
132
+
133
+ const [showLayerPanel, setShowLayerPanel] = useState(false);
134
+ const [selectedObjectId, setSelectedObjectId] = useState<string | null>(null);
135
+
136
+ // Add a new state to track saved objects
137
+ const [savedObjects, setSavedObjects] = useState<SavedCanvasObject[]>([]);
138
+
139
+ // Add this ref to track initial mounting
140
+ const initialMountRef = useRef(false);
141
+
142
+ // Initialize savedObjects from localStorage on component mount
143
+ useEffect(() => {
144
+ // Only reset on initial mount
145
+ if (!initialMountRef.current) {
146
+ initialMountRef.current = true;
147
+ // Reset savedObjects on page reload instead of loading from localStorage
148
+ setSavedObjects([]);
149
+ }
150
+ }, [product]);
151
+
152
+ // Function to record the current state of variation field objects
153
+ const recordVariationFieldObjectsOnCanvas = useCallback(() => {
154
+ if (!canvas || selectedTemplate === null) return;
155
+ // Create a fresh array instead of appending to existing
156
+ const updatedSavedObjects: SavedCanvasObject[] = [];
157
+ let hasChanges = false;
158
+
159
+ // Get all objects from the canvas
160
+ const objects = canvas.getObjects();
161
+
162
+ // Filter for objects that have a fieldId property (variation field objects)
163
+ objects.forEach((obj: any) => {
164
+ // Get fieldId from either direct property or from variationField
165
+
166
+ const { fieldId } = obj;
167
+
168
+ if (fieldId) {
169
+ // Also save a serialized version for the savedObjects state
170
+ const updatedObject = {
171
+ ...obj,
172
+ };
173
+
174
+ if (obj instanceof fabric.Image) {
175
+ Object.assign(updatedObject, {
176
+ width: obj.width,
177
+ height: obj.height,
178
+ src: obj.getSrc(),
179
+ });
180
+ }
181
+
182
+ updatedSavedObjects.push(updatedObject);
183
+ hasChanges = true;
184
+ }
185
+ });
186
+
187
+ // Only update state if we found objects with fieldIds
188
+ if (hasChanges) {
189
+ // Update the savedObjects state with the new complete array
190
+ setSavedObjects(updatedSavedObjects);
191
+ }
192
+ }, [canvas, selectedTemplate]); // Remove savedObjects from the dependencies
193
+
194
+ // Function to toggle preview visibility
195
+ const togglePreview = () => {
196
+ setShowPreview(prev => !prev);
197
+ };
198
+
199
+ // Function to toggle layer panel visibility
200
+ const toggleLayerPanel = () => {
201
+ setShowLayerPanel(prev => !prev);
202
+ };
203
+
204
+ // Function to update selected text object properties
205
+ const updateSelectedText = (props: Partial<fabric.IText>) => {
206
+ if (selectedTextObject && canvas && selectedTemplate !== null) {
207
+ selectedTextObject.set(props);
208
+ canvas.requestRenderAll();
209
+
210
+ // Record the state changes after updating text properties
211
+ // This ensures color changes and other styling updates are saved
212
+ setTimeout(() => {
213
+ if (canvas && selectedTemplate !== null) {
214
+ recordVariationFieldObjectsOnCanvas();
215
+ }
216
+ }, 50);
217
+ } else {
218
+ console.warn('Cannot update text - missing canvas, text object, or template');
219
+ }
220
+ };
221
+
222
+ const handleSave = () => {
223
+ if (canvas) {
224
+ const dataUrl = canvas.toDataURL();
225
+ onSave();
226
+ }
227
+ };
228
+
229
+ // Handle template change with improved state tracking
230
+ const handleTemplateChange = async (draftTemplate: DraftTemplate) => {
231
+ if (!canvas) return;
232
+
233
+ // Record the current state of variation field objects
234
+ recordVariationFieldObjectsOnCanvas();
235
+
236
+ // Check if this is already the selected template - prevent reloading the same template
237
+ if (draftTemplate.id && selectedTemplate === draftTemplate.id) {
238
+ return;
239
+ }
240
+
241
+ // Update the selected template
242
+ if (draftTemplate.id) {
243
+ setSelectedTemplate(draftTemplate.id);
244
+ }
245
+
246
+ // Create a clean canvas reference for tracking
247
+ const currentCanvas = canvas;
248
+
249
+ // Create a new canvas before disposing the old one
250
+ if (canvasRef.current) {
251
+ // Dispose the old canvas properly
252
+ try {
253
+ currentCanvas.dispose();
254
+
255
+ // Remove any lingering canvas containers to prevent duplicates
256
+ if (canvasRef.current.parentElement) {
257
+ const containers = canvasRef.current.parentElement.querySelectorAll('.canvas-container');
258
+ if (containers.length > 1) {
259
+ // Keep only the first container
260
+ for (let i = 1; i < containers.length; i++) {
261
+ containers[i].remove();
262
+ }
263
+ }
264
+ }
265
+ } catch (e) {
266
+ console.error('Error during canvas cleanup:', e);
267
+ }
268
+
269
+ const newCanvas = new fabric.Canvas(canvasRef.current, {
270
+ width: canvasWidth,
271
+ height: canvasHeight,
272
+ backgroundColor: '#ffffff',
273
+ // not to bring the selected object to the top visually
274
+ preserveObjectStacking: true
275
+ });
276
+
277
+ // Now load the template image and add variations
278
+ const templateData = draftTemplates.find(dt => dt.template.id === draftTemplate.id);
279
+
280
+ await renderEditorOrPreview(
281
+ newCanvas,
282
+ (templateData as any).template,
283
+ (templateData as any).variationObjects,
284
+ savedObjects,
285
+ canvasHeight,
286
+ canvasWidth,
287
+ );
288
+
289
+ // Update the state with the new canvas
290
+ setCanvas(newCanvas);
291
+
292
+ // Since selectedTemplate was just updated, we need a slight delay to ensure
293
+ // grid is drawn and loading is set to false
294
+ setTimeout(() => {
295
+ // Draw grid if needed
296
+ if (showGrid) {
297
+ try {
298
+ if (newCanvas.getElement() && newCanvas.getElement().parentNode) {
299
+ drawGrid(newCanvas, canvasWidth, canvasHeight, 20, '#a0a0a0', showGrid);
300
+ }
301
+ } catch (e) {
302
+ console.error('Error drawing grid after template change:', e);
303
+ }
304
+ }
305
+ setIsCanvasLoading(false);
306
+ }, 50);
307
+
308
+ // Re-setup keyboard events for the new canvas
309
+ setupKeyboardEvents(newCanvas, (dataUrl: any) => {
310
+ if (document.activeElement === newCanvas.upperCanvasEl) {
311
+ onSave && onSave();
312
+ }
313
+ });
314
+ }
315
+ };
316
+
317
+ // Add cleanup event handlers that properly use the callback function
318
+ const createEventHandler = useCallback((callback: Function) => {
319
+ return () => {
320
+ if (callback) callback();
321
+ };
322
+ }, []);
323
+
324
+ // Replace the direct watch with a debounced version
325
+ const debouncedWatch = useMemo(() => {
326
+ return debounce(async (watchValues: { variationsGroups?: any; variations?: any }) => {
327
+ // Start loading
328
+ setIsCanvasLoading(true);
329
+
330
+ // Record the current state of variation field objects before changes
331
+ if (canvas) {
332
+ recordVariationFieldObjectsOnCanvas();
333
+ }
334
+
335
+ // Extract the variations from the watch values
336
+ const variationsGroups = watchValues.variationsGroups;
337
+ const variations = watchValues.variations;
338
+
339
+ // Process the latest variations to update allVariationsRef
340
+ const newAllVariations = variationsGroups?.[groupIndex]?.variations
341
+ ? [...variationsGroups[groupIndex].variations]
342
+ : variations || [];
343
+
344
+ // Store in ref to avoid triggering effects
345
+ allVariationsRef.current = newAllVariations;
346
+
347
+ const newDraftTemplates = initDraftTemplates(newAllVariations, productRef.current);
348
+
349
+ // Check if the currently selected template ID still exists in the new list
350
+ const currentSelectedIdStillExists = newDraftTemplates.some(
351
+ dt => dt.template.id === selectedTemplate);
352
+
353
+ // Store the canvas instance for this effect run
354
+ let fabricCanvasInstance = canvas;
355
+
356
+ // Only update if they've actually changed
357
+ if (haveDraftTemplatesChanged(draftTemplates, newDraftTemplates)) {
358
+ setDraftTemplates(newDraftTemplates);
359
+ setDraftPreviews(setNewDraftPreviews(newDraftTemplates));
360
+ }
361
+
362
+ // Determine the template to load
363
+ let templateToLoad: DraftTemplate | undefined = newDraftTemplates[0]?.template;
364
+
365
+ if (currentSelectedIdStillExists && selectedTemplate) {
366
+ const previouslySelected = newDraftTemplates.find(dt => dt.template.id === selectedTemplate);
367
+ templateToLoad = previouslySelected?.template || templateToLoad;
368
+ }
369
+
370
+ setSelectedTemplate(templateToLoad?.id || null);
371
+
372
+ // Handle canvas setup
373
+ if (fabricCanvasInstance) {
374
+ // if we have a canvas we need to clear all the old templates
375
+ fabricCanvasInstance.clear();
376
+ fabricCanvasInstance.setBackgroundColor(
377
+ '#ffffff',
378
+ () => fabricCanvasInstance?.renderAll()
379
+ );
380
+ } else if (canvasRef.current) {
381
+ fabricCanvasInstance = new fabric.Canvas(canvasRef.current, {
382
+ width: canvasWidth,
383
+ height: canvasHeight,
384
+ backgroundColor: '#ffffff',
385
+ preserveObjectStacking: true
386
+ });
387
+
388
+ // Update state immediately to ensure it's available for event handlers
389
+ setCanvas(fabricCanvasInstance);
390
+ } else {
391
+ console.error('No canvas ref available');
392
+ setIsCanvasLoading(false);
393
+ return;
394
+ }
395
+
396
+ if (templateToLoad && templateToLoad.id) {
397
+ const templateData = newDraftTemplates.find(
398
+ dt => dt.template.id === (templateToLoad as any).id);
399
+
400
+ // Render the template onto the canvas
401
+ await renderEditorOrPreview(
402
+ fabricCanvasInstance,
403
+ templateData?.template || {},
404
+ templateData?.variationObjects || [],
405
+ savedObjects,
406
+ canvasHeight,
407
+ canvasWidth,
408
+ );
409
+
410
+ if (fabricCanvasInstance.getElement() && fabricCanvasInstance.getElement().parentNode) {
411
+ drawGrid(fabricCanvasInstance, canvasWidth, canvasHeight, 20, '#a0a0a0', showGrid);
412
+ }
413
+
414
+ setIsCanvasLoading(false);
415
+ } else {
416
+ setIsCanvasLoading(false);
417
+ }
418
+ }, 500);
419
+ }, [
420
+ groupIndex,
421
+ draftTemplates,
422
+ canvasRef,
423
+ showGrid,
424
+ canvasWidth,
425
+ canvasHeight,
426
+ savedObjects,
427
+ canvas,
428
+ selectedTemplate,
429
+ recordVariationFieldObjectsOnCanvas,
430
+ setSelectedTemplate
431
+ ]);
432
+
433
+ // Set up the debounced watch subscription
434
+ useEffect(() => {
435
+ if (!hookForm) return;
436
+
437
+ // Subscribe to form changes
438
+ const subscription = hookForm.watch((value: any) => {
439
+ debouncedWatch({
440
+ variationsGroups: value.variationsGroups,
441
+ variations: value.variations,
442
+ });
443
+ });
444
+
445
+ // Clean up subscription
446
+ return () => subscription.unsubscribe();
447
+ }, [hookForm, debouncedWatch]);
448
+
449
+ // This effect is used to trigger the debounced watch when the selected template is not set
450
+ // This helps with things like add group.
451
+ useEffect(() => {
452
+ if (!selectedTemplate) {
453
+ debouncedWatch({
454
+ variationsGroups: job.variationsGroups,
455
+ variations: job.variations,
456
+ });
457
+ }
458
+ }, [groupIndex]);
459
+
460
+
461
+ const [loadingPreviews, setLoadingPreviews] = useState(false);
462
+ const [renderedDraftPreviews, setRenderedDraftPreviews] = useState<RenderedDraftPreview[]>([]);
463
+
464
+ // Extract event handler setup to a separate function to avoid recreating in debounced watch
465
+ const setupEventHandlers = (fabricCanvasInstance: fabric.Canvas, selectedTemplate: number | null) => {
466
+ if (!fabricCanvasInstance) return () => { };
467
+
468
+ // Add a flag to prevent recursive calls
469
+ let isRendering = false;
470
+
471
+ // Define the record changes function with canvas and template closure
472
+ const recordChanges = () => {
473
+ // Use the passed instance and template ID instead of state variables
474
+ if (fabricCanvasInstance) {
475
+ const objects = fabricCanvasInstance.getObjects();
476
+
477
+ const updatedSavedObjects: SavedCanvasObject[] = [];
478
+ let hasChanges = false;
479
+
480
+ objects.forEach((obj: any) => {
481
+ const { fieldId } = obj;
482
+ if (fieldId) {
483
+ const updatedObject = {
484
+ ...obj,
485
+ };
486
+
487
+ if (obj instanceof fabric.Image) {
488
+ Object.assign(updatedObject, {
489
+ width: obj.width,
490
+ height: obj.height,
491
+ src: obj.getSrc(),
492
+ });
493
+ }
494
+
495
+ updatedSavedObjects.push(updatedObject);
496
+ hasChanges = true;
497
+ }
498
+ });
499
+
500
+ if (hasChanges) {
501
+ setSavedObjects(updatedSavedObjects);
502
+ }
503
+ } else {
504
+ console.warn('Cannot record changes - missing canvas or templateId');
505
+ }
506
+ };
507
+
508
+ // Create a bound version for event handling
509
+ const boundRecordChanges = createEventHandler(recordChanges);
510
+
511
+ // Add Text Toolbar Event Listener
512
+ const handleSelection = (e: fabric.IEvent) => {
513
+ const activeObject = fabricCanvasInstance.getActiveObject();
514
+ // Check if the active object is an IText instance
515
+
516
+ // Ensure the selected object has an ID
517
+ if (activeObject && !(activeObject as any).id) {
518
+ (activeObject as any).id = uuidv4();
519
+ }
520
+ setSelectedObjectId((activeObject as any)?.id || null);
521
+
522
+ if (activeObject instanceof fabric.IText) {
523
+ setSelectedTextObject(activeObject);
524
+ } else {
525
+ setSelectedTextObject(null);
526
+ }
527
+ };
528
+
529
+ const handleSelectionCleared = (e: fabric.IEvent) => {
530
+ setSelectedObjectId(null); // Clear selection in context
531
+ setSelectedTextObject(null);
532
+ };
533
+
534
+ const updateRenderedDraftPreviews = async () => {
535
+ const templateId = selectedTemplate;
536
+ const renderedImage = await renderClippedImage(fabricCanvasInstance, { format: 'png' });
537
+ const fullCanvasImage = await renderCanvasWithoutGrid(fabricCanvasInstance, { format: 'png' });
538
+ const activeTemplateIds = draftTemplates.map(dt => dt.template.id);
539
+
540
+ if (renderedImage && templateId) {
541
+ const previews = [...renderedDraftPreviews.filter(rdp => activeTemplateIds.includes(rdp.templateId))];
542
+ const existingPreviewIndex = previews.findIndex(preview => preview.templateId === templateId);
543
+ if (existingPreviewIndex !== -1) {
544
+ // Update existing preview
545
+ previews[existingPreviewIndex] = {
546
+ templateId,
547
+ draft: renderedImage,
548
+ canvasPreview: fullCanvasImage || '',
549
+ };
550
+ } else {
551
+ // Add new preview
552
+ previews.push({
553
+ templateId,
554
+ draft: renderedImage,
555
+ canvasPreview: fullCanvasImage || '',
556
+ });
557
+ }
558
+
559
+ // Save the rendered draft previews and previews to local storage
560
+ const productDraftTemplate = localStorage.getItem(`productDraftTemplate-${product.id}`);
561
+ const storedData = productDraftTemplate ? JSON.parse(productDraftTemplate) : [];
562
+ storedData[groupIndex] = {
563
+ groupIndex,
564
+ productId: product.id,
565
+ templateData: [...previews],
566
+ previews: [...previews],
567
+ }
568
+ localStorage.setItem(`productDraftTemplate-${product.id}`, JSON.stringify(storedData));
569
+ setRenderedDraftPreviews(previews);
570
+
571
+ setLoadingPreviews(false);
572
+ }
573
+ };
574
+
575
+ // Add after:render event listener to call renderClippedImage
576
+ const handleAfterRender = async () => {
577
+ // Prevent recursive calls
578
+ if (isRendering) return;
579
+
580
+ try {
581
+ isRendering = true;
582
+ setLoadingPreviews(true);
583
+ await updateRenderedDraftPreviews();
584
+ } catch (error) {
585
+ console.error('Error rendering clipped image after render:', error);
586
+ } finally {
587
+ isRendering = false;
588
+ }
589
+ };
590
+
591
+ // Add unique ID to objects if they don't have one
592
+ const ensureObjectIds = (canvasInstance: fabric.Canvas) => {
593
+ canvasInstance.getObjects().forEach(obj => {
594
+ if (!(obj as any).id) {
595
+ (obj as any).id = uuidv4();
596
+ }
597
+ });
598
+ };
599
+ // Ensure initial objects have IDs
600
+ if (fabricCanvasInstance) {
601
+ ensureObjectIds(fabricCanvasInstance);
602
+ }
603
+
604
+ // Attach listeners
605
+ fabricCanvasInstance?.on('selection:created', handleSelection);
606
+ fabricCanvasInstance?.on('selection:updated', handleSelection);
607
+ fabricCanvasInstance?.on('selection:cleared', handleSelectionCleared);
608
+ // Also update IDs when new objects are added
609
+ fabricCanvasInstance?.on('object:added', (e) => {
610
+ if (e.target && !(e.target as any).id) {
611
+ (e.target as any).id = uuidv4();
612
+ }
613
+ });
614
+
615
+ // setup keyboard events
616
+ const cleanupKeyboardEvents = setupKeyboardEvents(fabricCanvasInstance, () => {
617
+ if (document.activeElement === fabricCanvasInstance.upperCanvasEl) {
618
+ onSave && onSave();
619
+ }
620
+ });
621
+
622
+ fabricCanvasInstance.on('selection:created', handleSelection);
623
+ fabricCanvasInstance.on('selection:updated', handleSelection);
624
+ fabricCanvasInstance.on('selection:cleared', handleSelectionCleared);
625
+ fabricCanvasInstance.on('after:render', handleAfterRender);
626
+
627
+ // Add event listeners for object modifications
628
+ fabricCanvasInstance.on('object:modified', boundRecordChanges);
629
+ fabricCanvasInstance.on('object:moved', boundRecordChanges);
630
+ fabricCanvasInstance.on('object:scaled', boundRecordChanges);
631
+ fabricCanvasInstance.on('object:rotated', boundRecordChanges);
632
+ fabricCanvasInstance.on('text:changed', boundRecordChanges);
633
+
634
+ // Add handlers for more specific events that might be triggered by color changes
635
+ fabricCanvasInstance.on('object:changed', boundRecordChanges);
636
+ fabricCanvasInstance.on('canvas:updated', boundRecordChanges);
637
+
638
+ // Return cleanup function
639
+ return () => {
640
+ fabricCanvasInstance.off('selection:created', handleSelection);
641
+ fabricCanvasInstance.off('selection:updated', handleSelection);
642
+ fabricCanvasInstance.off('selection:cleared', handleSelectionCleared);
643
+ fabricCanvasInstance.off('after:render', handleAfterRender);
644
+
645
+ fabricCanvasInstance.off('object:modified', boundRecordChanges);
646
+ fabricCanvasInstance.off('object:moved', boundRecordChanges);
647
+ fabricCanvasInstance.off('object:scaled', boundRecordChanges);
648
+ fabricCanvasInstance.off('object:rotated', boundRecordChanges);
649
+ fabricCanvasInstance.off('text:changed', boundRecordChanges);
650
+ fabricCanvasInstance.off('object:changed', boundRecordChanges);
651
+ fabricCanvasInstance.off('canvas:updated', boundRecordChanges);
652
+
653
+ cleanupKeyboardEvents();
654
+ };
655
+ };
656
+
657
+ // Move setupEventHandlers call to a useEffect hook
658
+ useEffect(() => {
659
+ if (canvas) {
660
+ try {
661
+ if (canvas.getElement() && canvas.getElement().parentNode) {
662
+ drawGrid(canvas, canvasWidth, canvasHeight, 20, '#a0a0a0', showGrid);
663
+ canvas.renderAll();
664
+ }
665
+ } catch (error) {
666
+ console.error('Error drawing/clearing grid:', error);
667
+ }
668
+
669
+ // Setup event handlers
670
+ const cleanupEventHandlers = setupEventHandlers(canvas, selectedTemplate);
671
+ return () => {
672
+ cleanupEventHandlers();
673
+ };
674
+ }
675
+ }, [canvas, selectedTemplate, showGrid, canvasWidth, canvasHeight]);
676
+
677
+ const selectObject = (obj: fabric.Object | null) => {
678
+ if (canvas) {
679
+ if (obj) {
680
+ if (!(obj as any).id) {
681
+ (obj as any).id = uuidv4();
682
+ }
683
+ canvas.setActiveObject(obj);
684
+ setSelectedObjectId((obj as any).id);
685
+ } else {
686
+ canvas.discardActiveObject();
687
+ setSelectedObjectId(null);
688
+ }
689
+ canvas.requestRenderAll();
690
+ }
691
+ };
692
+
693
+ const deleteObject = useCallback((obj: fabric.Object) => {
694
+ if (!canvas || !obj) return;
695
+
696
+ try {
697
+ setIsCanvasLoading(true);
698
+
699
+ // remove the object from the canvas
700
+ canvas.remove(obj);
701
+ canvas.renderAll();
702
+
703
+ const fieldId = (obj as any)?.fieldId;
704
+ const fileId = (obj as any)?.fileId;
705
+
706
+ if (fieldId) {
707
+ const currentTemplateId = selectedTemplate;
708
+
709
+ // update draftTemplates
710
+ const updatedDraftTemplates = draftTemplates.map(dt => {
711
+ if (dt.template.id === currentTemplateId) {
712
+ return {
713
+ ...dt,
714
+ variationObjects: dt.variationObjects.filter((vo: any) => vo.fieldId !== fieldId)
715
+ };
716
+ }
717
+ return dt;
718
+ });
719
+ setDraftTemplates(updatedDraftTemplates);
720
+
721
+ // update the form state
722
+ if (hookForm) {
723
+ const hasVariationsGroups = 'variationsGroups' in hookForm.getValues();
724
+ const variationsField = hasVariationsGroups ?
725
+ `variationsGroups.${groupIndex}.variations` : 'variations';
726
+ const values = hookForm.getValues(variationsField) || [];
727
+
728
+ const updatedValues = values.map((variation: any) => {
729
+ if (variation?.variationField?.id === fieldId) {
730
+ // file type variation and has fileId
731
+ // only delete the specific file
732
+ if (variation.variationFiles?.length > 0 && fileId) {
733
+ const updatedFiles = variation.variationFiles.filter((file: any) => file.id !== fileId);
734
+ return {
735
+ ...variation,
736
+ value: updatedFiles.length === 0 ? null : variation.value,
737
+ variationFiles: updatedFiles
738
+ };
739
+ }
740
+ // non file type or no fileId
741
+ // clear the whole field
742
+ else if (!fileId) {
743
+ return {
744
+ ...variation,
745
+ value: null,
746
+ variationFiles: variation.variationFiles ? [] : undefined
747
+ };
748
+ }
749
+ }
750
+ return variation;
751
+ });
752
+
753
+ hookForm.setValue(variationsField, updatedValues);
754
+ }
755
+ }
756
+
757
+ // clear the selection state
758
+ setSelectedObjectId(null);
759
+ setSelectedTextObject(null);
760
+ setIsCanvasLoading(false);
761
+
762
+ onSave && onSave();
763
+ } catch (error) {
764
+ console.error('Error in deleteObject:', error);
765
+ setIsCanvasLoading(false);
766
+ }
767
+ }, [canvas, draftTemplates, selectedTemplate, hookForm, groupIndex, onSave]);
768
+
769
+ return (
770
+ <ProductEditorContext.Provider
771
+ value={{
772
+ // States
773
+ canvas,
774
+ draftPreviews,
775
+ draftTemplates,
776
+ isCanvasLoading,
777
+ isMobileView,
778
+ loadingPreviews,
779
+ renderedDraftPreviews,
780
+ savedObjects,
781
+ selectedTemplate,
782
+ selectedTextObject,
783
+ showGrid,
784
+ showPreview,
785
+
786
+ // State Setters
787
+ setCanvas,
788
+ setSelectedTemplate,
789
+ setShowGrid,
790
+
791
+ // Functions
792
+ handleCancel: () => onCancel(),
793
+ handleSave,
794
+ handleTemplateChange,
795
+ recordVariationFieldObjectsOnCanvas,
796
+ togglePreview,
797
+ updateSelectedText,
798
+ fontOptions,
799
+ colorPalette,
800
+ showLayerPanel,
801
+ toggleLayerPanel,
802
+ selectedObjectId,
803
+ setSelectedObjectId,
804
+ selectObject,
805
+ deleteObject,
806
+
807
+ // Props
808
+ canvasRef,
809
+ groupIndex,
810
+ height: canvasHeight,
811
+ inputName,
812
+ job,
813
+ product,
814
+ width: canvasWidth,
815
+ hookForm,
816
+ }}
817
+ >
818
+ {children}
819
+ </ProductEditorContext.Provider>
820
+ );
821
+ };
822
+
823
+ export const useProductEditor = () => {
824
+ const context = useContext(ProductEditorContext);
825
+ if (context === undefined) {
826
+ throw new Error('useProductEditor must be used within a ProductEditorProvider');
827
+ }
828
+ return context;
829
+ };