@jhits/plugin-images 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/api/resolve/route.ts +130 -11
- package/src/assets/noimagefound.jpg +0 -0
- package/src/components/BackgroundImage.tsx +25 -44
- package/src/components/GlobalImageEditor/GlobalImageEditor.tsx +374 -0
- package/src/components/GlobalImageEditor/config.ts +21 -0
- package/src/components/GlobalImageEditor/eventHandlers.ts +267 -0
- package/src/components/GlobalImageEditor/imageDetection.ts +160 -0
- package/src/components/GlobalImageEditor/imageSetup.ts +306 -0
- package/src/components/GlobalImageEditor/saveLogic.ts +133 -0
- package/src/components/GlobalImageEditor/stylingDetection.ts +122 -0
- package/src/components/GlobalImageEditor/transformParsing.ts +83 -0
- package/src/components/GlobalImageEditor/types.ts +39 -0
- package/src/components/GlobalImageEditor.tsx +185 -636
- package/src/components/Image.tsx +269 -103
- package/src/components/ImageBrowserModal.tsx +824 -0
- package/src/components/ImageEditor.tsx +323 -0
- package/src/components/ImageEffectsPanel.tsx +116 -0
- package/src/components/ImagePicker.tsx +171 -485
- package/src/components/index.ts +3 -0
- package/src/hooks/useImagePicker.ts +322 -0
- package/src/types/index.ts +24 -0
- package/src/utils/transforms.ts +54 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Image Editor Component
|
|
3
|
+
* Allows clicking on any image in the client app to edit it (admin/dev only)
|
|
4
|
+
*
|
|
5
|
+
* Reads configuration from window.__JHITS_PLUGIN_PROPS__['plugin-images']
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
|
11
|
+
import { X, Edit2 } from 'lucide-react';
|
|
12
|
+
import { ImagePicker } from '../ImagePicker';
|
|
13
|
+
import type { ImageMetadata } from '../../types';
|
|
14
|
+
import type { SelectedImage, PendingTransform } from './types';
|
|
15
|
+
import { getPluginConfig } from './config';
|
|
16
|
+
import { parseImageData } from './imageDetection';
|
|
17
|
+
import { setupImageHandlers } from './imageSetup';
|
|
18
|
+
import { saveTransformToAPI, flushPendingSave, getFilename, normalizePosition } from './saveLogic';
|
|
19
|
+
import { handleImageChange, handleBrightnessChange, handleBlurChange } from './eventHandlers';
|
|
20
|
+
|
|
21
|
+
export function GlobalImageEditor() {
|
|
22
|
+
// Configuration
|
|
23
|
+
const config = useMemo(() => getPluginConfig(), []);
|
|
24
|
+
|
|
25
|
+
// State
|
|
26
|
+
const [selectedImage, setSelectedImage] = useState<SelectedImage | null>(null);
|
|
27
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
28
|
+
const [userRole, setUserRole] = useState<string | null>(null);
|
|
29
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
30
|
+
|
|
31
|
+
// Refs for save debouncing
|
|
32
|
+
const saveTransformTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
33
|
+
const pendingTransformRef = useRef<PendingTransform | null>(null);
|
|
34
|
+
|
|
35
|
+
// Check if user is admin/dev
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const checkUser = async () => {
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch('/api/me');
|
|
40
|
+
const data = await res.json();
|
|
41
|
+
if (data.loggedIn && (data.user?.role === 'admin' || data.user?.role === 'dev')) {
|
|
42
|
+
setUserRole(data.user.role);
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('Failed to check user role:', error);
|
|
46
|
+
} finally {
|
|
47
|
+
setIsLoading(false);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
checkUser();
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
// Listen for open-image-editor custom event from Image/BackgroundImage components
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!userRole) return;
|
|
56
|
+
|
|
57
|
+
const handleOpenEditor = async (e: CustomEvent) => {
|
|
58
|
+
const { id, currentBrightness, currentBlur } = e.detail || {};
|
|
59
|
+
if (!id) return;
|
|
60
|
+
|
|
61
|
+
console.log('[GlobalImageEditor] open-image-editor event received for id:', id);
|
|
62
|
+
|
|
63
|
+
// Find the element by data-image-id or data-background-image-id
|
|
64
|
+
let element = document.querySelector(`[data-image-id="${id}"], [data-background-image-id="${id}"]`) as HTMLElement;
|
|
65
|
+
|
|
66
|
+
if (!element) {
|
|
67
|
+
element = document.querySelector(`[data-background-image-component="true"][data-background-image-id="${id}"]`) as HTMLElement;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!element) {
|
|
71
|
+
console.error('[GlobalImageEditor] Element not found for id:', id);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Extract semantic ID
|
|
76
|
+
const semanticId = element.getAttribute('data-image-id') ||
|
|
77
|
+
element.getAttribute('data-background-image-id') ||
|
|
78
|
+
id;
|
|
79
|
+
|
|
80
|
+
// Parse image data
|
|
81
|
+
const imageData = parseImageData(element, semanticId, currentBrightness, currentBlur);
|
|
82
|
+
|
|
83
|
+
if (!imageData) {
|
|
84
|
+
console.error('[GlobalImageEditor] Failed to parse image data');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Store semantic ID on element if not already set
|
|
89
|
+
if (imageData.isBackground && !element.hasAttribute('data-background-image-id')) {
|
|
90
|
+
element.setAttribute('data-background-image-id', semanticId);
|
|
91
|
+
} else if (!imageData.isBackground && !element.hasAttribute('data-image-id')) {
|
|
92
|
+
element.setAttribute('data-image-id', semanticId);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setSelectedImage(imageData);
|
|
96
|
+
setIsOpen(true);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
window.addEventListener('open-image-editor', handleOpenEditor as unknown as EventListener);
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
window.removeEventListener('open-image-editor', handleOpenEditor as unknown as EventListener);
|
|
103
|
+
};
|
|
104
|
+
}, [userRole]);
|
|
105
|
+
|
|
106
|
+
// Add click handlers to all images
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (isLoading || !userRole) {
|
|
109
|
+
console.log('[GlobalImageEditor] Skipping image handlers - isLoading:', isLoading, 'userRole:', userRole);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log('[GlobalImageEditor] Setting up image handlers for role:', userRole);
|
|
114
|
+
|
|
115
|
+
const cleanup = setupImageHandlers((imageData) => {
|
|
116
|
+
setSelectedImage(imageData);
|
|
117
|
+
setIsOpen(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Setup immediately and also after a short delay to catch dynamically loaded images
|
|
121
|
+
const timeoutId = setTimeout(() => {
|
|
122
|
+
const cleanup2 = setupImageHandlers((imageData) => {
|
|
123
|
+
setSelectedImage(imageData);
|
|
124
|
+
setIsOpen(true);
|
|
125
|
+
});
|
|
126
|
+
// Note: This creates a new cleanup, but the first one will handle the initial setup
|
|
127
|
+
}, 500);
|
|
128
|
+
|
|
129
|
+
return () => {
|
|
130
|
+
clearTimeout(timeoutId);
|
|
131
|
+
cleanup();
|
|
132
|
+
};
|
|
133
|
+
}, [isLoading, userRole]);
|
|
134
|
+
|
|
135
|
+
// Save image transform (scale/position) to API with debouncing
|
|
136
|
+
const saveImageTransformHandler = useCallback(async (
|
|
137
|
+
scale: number,
|
|
138
|
+
positionX: number,
|
|
139
|
+
positionY: number,
|
|
140
|
+
immediate: boolean = false,
|
|
141
|
+
brightnessOverride?: number,
|
|
142
|
+
blurOverride?: number
|
|
143
|
+
) => {
|
|
144
|
+
if (!selectedImage) return;
|
|
145
|
+
|
|
146
|
+
const { element } = selectedImage;
|
|
147
|
+
const semanticId = element.getAttribute('data-image-id') ||
|
|
148
|
+
element.getAttribute('data-background-image-id');
|
|
149
|
+
|
|
150
|
+
if (!semanticId) return;
|
|
151
|
+
|
|
152
|
+
const filename = await getFilename(semanticId, selectedImage);
|
|
153
|
+
if (!filename) {
|
|
154
|
+
console.error('[GlobalImageEditor] getFilename returned null for semanticId:', semanticId);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
console.log('[GlobalImageEditor] getFilename returned:', filename, 'for semanticId:', semanticId);
|
|
158
|
+
|
|
159
|
+
// Use override values if provided, otherwise use selectedImage values
|
|
160
|
+
// IMPORTANT: Overrides contain the latest values from the editor
|
|
161
|
+
const finalBrightness = brightnessOverride !== undefined ? brightnessOverride : (selectedImage?.brightness ?? 100);
|
|
162
|
+
const finalBlur = blurOverride !== undefined ? blurOverride : (selectedImage?.blur ?? 0);
|
|
163
|
+
|
|
164
|
+
// Store pending transform
|
|
165
|
+
pendingTransformRef.current = {
|
|
166
|
+
scale,
|
|
167
|
+
positionX,
|
|
168
|
+
positionY,
|
|
169
|
+
semanticId,
|
|
170
|
+
filename
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Clear existing timeout
|
|
174
|
+
if (saveTransformTimeoutRef.current) {
|
|
175
|
+
clearTimeout(saveTransformTimeoutRef.current);
|
|
176
|
+
saveTransformTimeoutRef.current = null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// If immediate save is requested (e.g., when "Done" is clicked), save right away
|
|
180
|
+
if (immediate) {
|
|
181
|
+
const normalizedPositionX = normalizePosition(positionX);
|
|
182
|
+
const normalizedPositionY = normalizePosition(positionY);
|
|
183
|
+
|
|
184
|
+
console.log('[GlobalImageEditor] Immediate save to API:', { semanticId, filename, scale, normalizedPositionX, normalizedPositionY, finalBrightness, finalBlur });
|
|
185
|
+
await saveTransformToAPI(
|
|
186
|
+
semanticId,
|
|
187
|
+
filename,
|
|
188
|
+
scale,
|
|
189
|
+
normalizedPositionX,
|
|
190
|
+
normalizedPositionY,
|
|
191
|
+
finalBrightness,
|
|
192
|
+
finalBlur
|
|
193
|
+
);
|
|
194
|
+
pendingTransformRef.current = null;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Debounce: save after 300ms of no changes
|
|
199
|
+
saveTransformTimeoutRef.current = setTimeout(async () => {
|
|
200
|
+
const pending = pendingTransformRef.current;
|
|
201
|
+
if (pending && selectedImage) {
|
|
202
|
+
await flushPendingSave(pending, selectedImage);
|
|
203
|
+
}
|
|
204
|
+
}, 300);
|
|
205
|
+
}, [selectedImage]);
|
|
206
|
+
|
|
207
|
+
// Cleanup timeout on unmount
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
return () => {
|
|
210
|
+
if (saveTransformTimeoutRef.current) {
|
|
211
|
+
clearTimeout(saveTransformTimeoutRef.current);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}, []);
|
|
215
|
+
|
|
216
|
+
// Event handlers
|
|
217
|
+
const handleImageChangeWrapper = useCallback(async (image: ImageMetadata | null) => {
|
|
218
|
+
if (!selectedImage) return;
|
|
219
|
+
await handleImageChange(image, selectedImage, handleClose);
|
|
220
|
+
}, [selectedImage]);
|
|
221
|
+
|
|
222
|
+
const handleBrightnessChangeWrapper = useCallback(async (brightness: number) => {
|
|
223
|
+
if (!selectedImage) return;
|
|
224
|
+
// Don't save immediately when in editor - will be saved when "Done" is pressed via onEditorSave
|
|
225
|
+
await handleBrightnessChange(brightness, selectedImage, setSelectedImage, false);
|
|
226
|
+
}, [selectedImage]);
|
|
227
|
+
|
|
228
|
+
const handleBlurChangeWrapper = useCallback(async (blur: number) => {
|
|
229
|
+
if (!selectedImage) return;
|
|
230
|
+
// Don't save immediately when in editor - will be saved when "Done" is pressed via onEditorSave
|
|
231
|
+
await handleBlurChange(blur, selectedImage, setSelectedImage, false);
|
|
232
|
+
}, [selectedImage]);
|
|
233
|
+
|
|
234
|
+
const handleEditorSave = useCallback(async (
|
|
235
|
+
finalScale: number,
|
|
236
|
+
finalPositionX: number,
|
|
237
|
+
finalPositionY: number,
|
|
238
|
+
finalBrightness?: number,
|
|
239
|
+
finalBlur?: number
|
|
240
|
+
) => {
|
|
241
|
+
console.log('[GlobalImageEditor] handleEditorSave callback created/updated');
|
|
242
|
+
console.log('[GlobalImageEditor] handleEditorSave called:', { finalScale, finalPositionX, finalPositionY, finalBrightness, finalBlur, hasSelectedImage: !!selectedImage });
|
|
243
|
+
if (selectedImage) {
|
|
244
|
+
// Use provided values if they're not undefined, otherwise fall back to selectedImage values
|
|
245
|
+
// Note: 0 is a valid value for blur, so we check for undefined explicitly
|
|
246
|
+
const brightness = finalBrightness !== undefined ? finalBrightness : selectedImage.brightness;
|
|
247
|
+
const blur = finalBlur !== undefined ? finalBlur : selectedImage.blur;
|
|
248
|
+
|
|
249
|
+
const updated = {
|
|
250
|
+
...selectedImage,
|
|
251
|
+
scale: finalScale,
|
|
252
|
+
positionX: finalPositionX,
|
|
253
|
+
positionY: finalPositionY,
|
|
254
|
+
brightness,
|
|
255
|
+
blur,
|
|
256
|
+
};
|
|
257
|
+
setSelectedImage(updated);
|
|
258
|
+
|
|
259
|
+
console.log('[GlobalImageEditor] Calling saveImageTransformHandler with:', { finalScale, finalPositionX, finalPositionY, brightness, blur, immediate: true });
|
|
260
|
+
// Only save once when editor closes with all final values
|
|
261
|
+
// ImagePicker prevents duplicate calls by not calling individual handlers when onEditorSave is provided
|
|
262
|
+
await saveImageTransformHandler(
|
|
263
|
+
finalScale,
|
|
264
|
+
finalPositionX,
|
|
265
|
+
finalPositionY,
|
|
266
|
+
true,
|
|
267
|
+
brightness,
|
|
268
|
+
blur
|
|
269
|
+
);
|
|
270
|
+
} else {
|
|
271
|
+
console.error('[GlobalImageEditor] handleEditorSave called but selectedImage is null!');
|
|
272
|
+
}
|
|
273
|
+
}, [selectedImage, saveImageTransformHandler]);
|
|
274
|
+
|
|
275
|
+
const handleClose = useCallback(() => {
|
|
276
|
+
setIsOpen(false);
|
|
277
|
+
setSelectedImage(null);
|
|
278
|
+
}, []);
|
|
279
|
+
|
|
280
|
+
// Don't render if disabled or user is not admin/dev
|
|
281
|
+
if (!config.enabled || isLoading || !userRole) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!isOpen || !selectedImage) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<div
|
|
291
|
+
className={`fixed inset-0 z-[9999] flex items-center justify-center ${config.overlayClassName || 'bg-black/50 backdrop-blur-sm'}`}
|
|
292
|
+
data-image-editor="true"
|
|
293
|
+
onClick={(e) => {
|
|
294
|
+
if (e.target === e.currentTarget) {
|
|
295
|
+
handleClose();
|
|
296
|
+
}
|
|
297
|
+
}}
|
|
298
|
+
>
|
|
299
|
+
<div
|
|
300
|
+
className={`relative bg-white dark:bg-neutral-900 rounded-2xl shadow-2xl max-w-7xl w-full mx-4 max-h-[90vh] flex flex-col ${config.className || ''}`}
|
|
301
|
+
onClick={(e) => e.stopPropagation()}
|
|
302
|
+
>
|
|
303
|
+
{/* Header */}
|
|
304
|
+
<div className="flex items-center justify-between p-6 border-b dark:border-neutral-800">
|
|
305
|
+
<h2 className="text-2xl font-bold dark:text-white">Edit Image</h2>
|
|
306
|
+
<button
|
|
307
|
+
onClick={handleClose}
|
|
308
|
+
className="p-2 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors"
|
|
309
|
+
aria-label="Close editor"
|
|
310
|
+
>
|
|
311
|
+
<X className="w-5 h-5" />
|
|
312
|
+
</button>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
{/* Content */}
|
|
316
|
+
<div className="p-6">
|
|
317
|
+
<ImagePicker
|
|
318
|
+
value={selectedImage.element.getAttribute('data-image-id') || selectedImage.element.getAttribute('data-background-image-id') || selectedImage.originalSrc}
|
|
319
|
+
onChange={handleImageChangeWrapper}
|
|
320
|
+
brightness={selectedImage.brightness}
|
|
321
|
+
blur={selectedImage.blur}
|
|
322
|
+
scale={selectedImage.scale}
|
|
323
|
+
positionX={selectedImage.positionX}
|
|
324
|
+
positionY={selectedImage.positionY}
|
|
325
|
+
onBrightnessChange={handleBrightnessChangeWrapper}
|
|
326
|
+
onBlurChange={handleBlurChangeWrapper}
|
|
327
|
+
onEditorSave={handleEditorSave}
|
|
328
|
+
onScaleChange={async (newScale) => {
|
|
329
|
+
console.log('[GlobalImageEditor] onScaleChange called:', newScale);
|
|
330
|
+
if (selectedImage) {
|
|
331
|
+
const updated = { ...selectedImage, scale: newScale };
|
|
332
|
+
setSelectedImage(updated);
|
|
333
|
+
await saveImageTransformHandler(newScale, updated.positionX, updated.positionY, false);
|
|
334
|
+
}
|
|
335
|
+
}}
|
|
336
|
+
onPositionXChange={async (newPositionX) => {
|
|
337
|
+
console.log('[GlobalImageEditor] onPositionXChange called:', newPositionX);
|
|
338
|
+
if (selectedImage) {
|
|
339
|
+
const updated = { ...selectedImage, positionX: newPositionX };
|
|
340
|
+
setSelectedImage(updated);
|
|
341
|
+
await saveImageTransformHandler(updated.scale, newPositionX, updated.positionY, false);
|
|
342
|
+
}
|
|
343
|
+
}}
|
|
344
|
+
onPositionYChange={async (newPositionY) => {
|
|
345
|
+
console.log('[GlobalImageEditor] onPositionYChange called:', newPositionY);
|
|
346
|
+
if (selectedImage) {
|
|
347
|
+
const updated = { ...selectedImage, positionY: newPositionY };
|
|
348
|
+
setSelectedImage(updated);
|
|
349
|
+
// If onEditorSave wasn't provided, this might be called when editor closes
|
|
350
|
+
// In that case, we should save immediately with all current values including brightness/blur
|
|
351
|
+
// We'll save immediately to ensure the final state is persisted
|
|
352
|
+
await saveImageTransformHandler(
|
|
353
|
+
updated.scale,
|
|
354
|
+
updated.positionX,
|
|
355
|
+
newPositionY,
|
|
356
|
+
true, // Save immediately - this is likely the final callback when editor closes
|
|
357
|
+
updated.brightness,
|
|
358
|
+
updated.blur
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
}}
|
|
362
|
+
onEditorSave={handleEditorSave}
|
|
363
|
+
showEffects={true}
|
|
364
|
+
darkMode={false}
|
|
365
|
+
aspectRatio={selectedImage.aspectRatio}
|
|
366
|
+
borderRadius={selectedImage.borderRadius}
|
|
367
|
+
objectFit={selectedImage.objectFit}
|
|
368
|
+
objectPosition={selectedImage.objectPosition}
|
|
369
|
+
/>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
);
|
|
374
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration utilities for GlobalImageEditor
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { PluginConfig } from './types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Read plugin configuration from window global
|
|
9
|
+
*/
|
|
10
|
+
export function getPluginConfig(): PluginConfig {
|
|
11
|
+
if (typeof window === 'undefined') {
|
|
12
|
+
return { enabled: false };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const pluginProps = (window as any).__JHITS_PLUGIN_PROPS__?.['plugin-images'];
|
|
16
|
+
return {
|
|
17
|
+
enabled: pluginProps?.enabled !== undefined ? pluginProps.enabled : true,
|
|
18
|
+
className: pluginProps?.className,
|
|
19
|
+
overlayClassName: pluginProps?.overlayClassName,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event handlers for image changes and effects
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { SelectedImage } from './types';
|
|
6
|
+
import type { ImageMetadata } from '../../types';
|
|
7
|
+
import { getFilename, saveTransformToAPI, normalizePosition } from './saveLogic';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Apply image changes to DOM and save to API
|
|
11
|
+
*/
|
|
12
|
+
export async function handleImageChange(
|
|
13
|
+
image: ImageMetadata | null,
|
|
14
|
+
selectedImage: SelectedImage,
|
|
15
|
+
onClose: () => void
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
if (!image) return;
|
|
18
|
+
|
|
19
|
+
const { element } = selectedImage;
|
|
20
|
+
const semanticId = element.getAttribute('data-image-id') ||
|
|
21
|
+
element.getAttribute('data-background-image-id');
|
|
22
|
+
|
|
23
|
+
if (!semanticId) {
|
|
24
|
+
console.error('[GlobalImageEditor] No semantic ID found for image');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Extract filename from image URL or use image.id
|
|
29
|
+
const filename = image.url.split('/').pop()?.split('?')[0] || image.id;
|
|
30
|
+
const normalizedPositionX = normalizePosition(selectedImage.positionX);
|
|
31
|
+
const normalizedPositionY = normalizePosition(selectedImage.positionY);
|
|
32
|
+
|
|
33
|
+
// Save the mapping between semantic ID and filename, including effects and transforms
|
|
34
|
+
await saveTransformToAPI(
|
|
35
|
+
semanticId,
|
|
36
|
+
filename,
|
|
37
|
+
selectedImage.scale,
|
|
38
|
+
normalizedPositionX,
|
|
39
|
+
normalizedPositionY,
|
|
40
|
+
selectedImage.brightness,
|
|
41
|
+
selectedImage.blur
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Update image src immediately - handle background images differently
|
|
45
|
+
if (selectedImage.isBackground) {
|
|
46
|
+
updateBackgroundImage(element, image.url, selectedImage, normalizedPositionX, normalizedPositionY);
|
|
47
|
+
} else {
|
|
48
|
+
updateRegularImage(element, image.url, semanticId, selectedImage, normalizedPositionX, normalizedPositionY);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Dispatch a custom event to notify Image components to re-resolve
|
|
52
|
+
window.dispatchEvent(new CustomEvent('image-mapping-updated', {
|
|
53
|
+
detail: {
|
|
54
|
+
id: semanticId,
|
|
55
|
+
filename,
|
|
56
|
+
brightness: selectedImage.brightness,
|
|
57
|
+
blur: selectedImage.blur,
|
|
58
|
+
scale: selectedImage.scale,
|
|
59
|
+
positionX: selectedImage.positionX,
|
|
60
|
+
positionY: selectedImage.positionY,
|
|
61
|
+
}
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
onClose();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Update background image element
|
|
69
|
+
*/
|
|
70
|
+
function updateBackgroundImage(
|
|
71
|
+
element: HTMLElement,
|
|
72
|
+
imageUrl: string,
|
|
73
|
+
selectedImage: SelectedImage,
|
|
74
|
+
normalizedPositionX: number,
|
|
75
|
+
normalizedPositionY: number
|
|
76
|
+
): void {
|
|
77
|
+
// For background images, update the nested Image component
|
|
78
|
+
const imgWrapper = element.querySelector('[data-image-id]');
|
|
79
|
+
if (imgWrapper) {
|
|
80
|
+
const img = imgWrapper.querySelector('img');
|
|
81
|
+
if (img) {
|
|
82
|
+
img.src = imageUrl;
|
|
83
|
+
img.setAttribute('data-edited-src', imageUrl);
|
|
84
|
+
img.style.transform = `scale(${selectedImage.scale}) translate(${normalizedPositionX}%, ${normalizedPositionY}%)`;
|
|
85
|
+
img.style.transformOrigin = 'center center';
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
// Fallback: update background-image style if no nested Image component
|
|
89
|
+
const bgImage = window.getComputedStyle(element).backgroundImage;
|
|
90
|
+
if (bgImage && bgImage !== 'none') {
|
|
91
|
+
element.style.backgroundImage = `url(${imageUrl})`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Store attributes on the background container
|
|
96
|
+
const semanticId = element.getAttribute('data-image-id') || element.getAttribute('data-background-image-id') || '';
|
|
97
|
+
element.setAttribute('data-background-image-id', semanticId);
|
|
98
|
+
element.setAttribute('data-edited-src', imageUrl);
|
|
99
|
+
element.setAttribute('data-brightness', selectedImage.brightness.toString());
|
|
100
|
+
element.setAttribute('data-blur', selectedImage.blur.toString());
|
|
101
|
+
element.setAttribute('data-scale', selectedImage.scale.toString());
|
|
102
|
+
element.setAttribute('data-position-x', normalizedPositionX.toString());
|
|
103
|
+
element.setAttribute('data-position-y', normalizedPositionY.toString());
|
|
104
|
+
|
|
105
|
+
// Apply filter to the wrapper element
|
|
106
|
+
if (selectedImage.brightness !== 100 || selectedImage.blur !== 0) {
|
|
107
|
+
element.style.filter = `brightness(${selectedImage.brightness}%) blur(${selectedImage.blur}px)`;
|
|
108
|
+
} else {
|
|
109
|
+
element.style.filter = '';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Update regular image element
|
|
115
|
+
*/
|
|
116
|
+
function updateRegularImage(
|
|
117
|
+
element: HTMLElement,
|
|
118
|
+
imageUrl: string,
|
|
119
|
+
semanticId: string,
|
|
120
|
+
selectedImage: SelectedImage,
|
|
121
|
+
normalizedPositionX: number,
|
|
122
|
+
normalizedPositionY: number
|
|
123
|
+
): void {
|
|
124
|
+
if (element.tagName === 'IMG') {
|
|
125
|
+
(element as HTMLImageElement).src = imageUrl;
|
|
126
|
+
element.setAttribute('data-edited-src', imageUrl);
|
|
127
|
+
element.setAttribute('data-image-id', semanticId);
|
|
128
|
+
element.setAttribute('data-brightness', selectedImage.brightness.toString());
|
|
129
|
+
element.setAttribute('data-blur', selectedImage.blur.toString());
|
|
130
|
+
|
|
131
|
+
// Apply filter
|
|
132
|
+
if (selectedImage.brightness !== 100 || selectedImage.blur !== 0) {
|
|
133
|
+
element.style.filter = `brightness(${selectedImage.brightness}%) blur(${selectedImage.blur}px)`;
|
|
134
|
+
} else {
|
|
135
|
+
element.style.filter = '';
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
// Next.js Image wrapper - find the actual img element
|
|
139
|
+
const img = element.querySelector('img');
|
|
140
|
+
if (img) {
|
|
141
|
+
img.src = imageUrl;
|
|
142
|
+
img.setAttribute('data-edited-src', imageUrl);
|
|
143
|
+
img.style.transform = `scale(${selectedImage.scale}) translate(${normalizedPositionX}%, ${normalizedPositionY}%)`;
|
|
144
|
+
img.style.transformOrigin = 'center center';
|
|
145
|
+
}
|
|
146
|
+
element.setAttribute('data-image-id', semanticId);
|
|
147
|
+
element.setAttribute('data-edited-src', imageUrl);
|
|
148
|
+
element.setAttribute('data-brightness', selectedImage.brightness.toString());
|
|
149
|
+
element.setAttribute('data-blur', selectedImage.blur.toString());
|
|
150
|
+
element.setAttribute('data-scale', selectedImage.scale.toString());
|
|
151
|
+
element.setAttribute('data-position-x', normalizedPositionX.toString());
|
|
152
|
+
element.setAttribute('data-position-y', normalizedPositionY.toString());
|
|
153
|
+
|
|
154
|
+
// Apply filter to wrapper
|
|
155
|
+
if (selectedImage.brightness !== 100 || selectedImage.blur !== 0) {
|
|
156
|
+
element.style.filter = `brightness(${selectedImage.brightness}%) blur(${selectedImage.blur}px)`;
|
|
157
|
+
} else {
|
|
158
|
+
element.style.filter = '';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get filename from element for saving effects
|
|
165
|
+
*/
|
|
166
|
+
async function getFilenameFromElement(
|
|
167
|
+
semanticId: string,
|
|
168
|
+
selectedImage: SelectedImage
|
|
169
|
+
): Promise<string> {
|
|
170
|
+
const filename = await getFilename(semanticId, selectedImage);
|
|
171
|
+
if (filename) return filename;
|
|
172
|
+
|
|
173
|
+
const { element } = selectedImage;
|
|
174
|
+
if (selectedImage.isBackground) {
|
|
175
|
+
const bgImage = window.getComputedStyle(element).backgroundImage;
|
|
176
|
+
const urlMatch = bgImage.match(/url\(['"]?([^'"]+)['"]?\)/);
|
|
177
|
+
if (urlMatch && urlMatch[1]) {
|
|
178
|
+
return urlMatch[1].split('/api/uploads/')[1]?.split('?')[0] || semanticId;
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
let imgSrc: string | null = null;
|
|
182
|
+
if (element instanceof HTMLImageElement && element.src) {
|
|
183
|
+
imgSrc = element.src;
|
|
184
|
+
} else {
|
|
185
|
+
const img = element.querySelector('img') as HTMLImageElement | null;
|
|
186
|
+
if (img?.src) {
|
|
187
|
+
imgSrc = img.src;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (imgSrc) {
|
|
191
|
+
return imgSrc.split('/api/uploads/')[1]?.split('?')[0] || semanticId;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return semanticId;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Handle brightness change
|
|
199
|
+
* @param saveImmediately - If true, save to API immediately. If false, only update state/DOM (for editor preview)
|
|
200
|
+
*/
|
|
201
|
+
export async function handleBrightnessChange(
|
|
202
|
+
brightness: number,
|
|
203
|
+
selectedImage: SelectedImage,
|
|
204
|
+
setSelectedImage: (image: SelectedImage) => void,
|
|
205
|
+
saveImmediately: boolean = true
|
|
206
|
+
): Promise<void> {
|
|
207
|
+
const { element } = selectedImage;
|
|
208
|
+
const semanticId = element.getAttribute('data-image-id') ||
|
|
209
|
+
element.getAttribute('data-background-image-id');
|
|
210
|
+
|
|
211
|
+
if (!semanticId) return;
|
|
212
|
+
|
|
213
|
+
const currentFilter = element.style.filter || '';
|
|
214
|
+
const blurMatch = currentFilter.match(/blur\((\d+)px\)/);
|
|
215
|
+
const blur = blurMatch ? parseInt(blurMatch[1]) : selectedImage.blur;
|
|
216
|
+
|
|
217
|
+
// Update the selectedImage state
|
|
218
|
+
setSelectedImage({
|
|
219
|
+
...selectedImage,
|
|
220
|
+
brightness,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
element.style.filter = `brightness(${brightness}%) blur(${blur}px)`;
|
|
224
|
+
element.setAttribute('data-brightness', brightness.toString());
|
|
225
|
+
|
|
226
|
+
// Save the effect immediately only if requested (not when in editor)
|
|
227
|
+
if (saveImmediately) {
|
|
228
|
+
const filename = await getFilenameFromElement(semanticId, selectedImage);
|
|
229
|
+
await saveTransformToAPI(semanticId, filename, selectedImage.scale, selectedImage.positionX, selectedImage.positionY, brightness, blur);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Handle blur change
|
|
235
|
+
* @param saveImmediately - If true, save to API immediately. If false, only update state/DOM (for editor preview)
|
|
236
|
+
*/
|
|
237
|
+
export async function handleBlurChange(
|
|
238
|
+
blur: number,
|
|
239
|
+
selectedImage: SelectedImage,
|
|
240
|
+
setSelectedImage: (image: SelectedImage) => void,
|
|
241
|
+
saveImmediately: boolean = true
|
|
242
|
+
): Promise<void> {
|
|
243
|
+
const { element } = selectedImage;
|
|
244
|
+
const semanticId = element.getAttribute('data-image-id') ||
|
|
245
|
+
element.getAttribute('data-background-image-id');
|
|
246
|
+
|
|
247
|
+
if (!semanticId) return;
|
|
248
|
+
|
|
249
|
+
const currentFilter = element.style.filter || '';
|
|
250
|
+
const brightnessMatch = currentFilter.match(/brightness\((\d+)%\)/);
|
|
251
|
+
const brightness = brightnessMatch ? parseInt(brightnessMatch[1]) : selectedImage.brightness;
|
|
252
|
+
|
|
253
|
+
// Update the selectedImage state
|
|
254
|
+
setSelectedImage({
|
|
255
|
+
...selectedImage,
|
|
256
|
+
blur,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
element.style.filter = `brightness(${brightness}%) blur(${blur}px)`;
|
|
260
|
+
element.setAttribute('data-blur', blur.toString());
|
|
261
|
+
|
|
262
|
+
// Save the effect immediately only if requested (not when in editor)
|
|
263
|
+
if (saveImmediately) {
|
|
264
|
+
const filename = await getFilenameFromElement(semanticId, selectedImage);
|
|
265
|
+
await saveTransformToAPI(semanticId, filename, selectedImage.scale, selectedImage.positionX, selectedImage.positionY, brightness, blur);
|
|
266
|
+
}
|
|
267
|
+
}
|