Skip to content

Commit 1f4176f

Browse files
committed
refactor: image editor layer context state
1 parent 7dea296 commit 1f4176f

File tree

4 files changed

+61
-13
lines changed

4 files changed

+61
-13
lines changed

web/src/components/image-editor/imagor-editor-controls.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ interface ImageEditorControlsProps {
4949
imageEditor: ImageEditor
5050
imagePath: string
5151
params: ImageEditorState
52+
selectedLayerId: string | null
5253
openSections: EditorOpenSections
5354
onOpenSectionsChange: (sections: EditorOpenSections) => void
5455
onUpdateParams: (updates: Partial<ImageEditorState>) => void
@@ -127,6 +128,7 @@ export function ImageEditorControls({
127128
imageEditor,
128129
imagePath,
129130
params,
131+
selectedLayerId,
130132
openSections,
131133
onOpenSectionsChange,
132134
onUpdateParams,
@@ -251,6 +253,7 @@ export function ImageEditorControls({
251253
<LayerPanel
252254
imageEditor={imageEditor}
253255
imagePath={imagePath}
256+
selectedLayerId={selectedLayerId}
254257
visualCropEnabled={isVisualCropEnabled}
255258
/>
256259
),
@@ -260,6 +263,7 @@ export function ImageEditorControls({
260263
imageEditor,
261264
imagePath,
262265
params,
266+
selectedLayerId,
263267
onUpdateParams,
264268
onVisualCropToggle,
265269
isVisualCropEnabled,

web/src/components/image-editor/layer-panel.tsx

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { cn } from '@/lib/utils'
3333
interface LayerPanelProps {
3434
imageEditor: ImageEditor
3535
imagePath: string
36+
selectedLayerId: string | null
3637
visualCropEnabled?: boolean
3738
}
3839

@@ -172,10 +173,14 @@ function BaseImageItem({ imagePath, isSelected, onClick }: BaseImageItemProps) {
172173
)
173174
}
174175

175-
export function LayerPanel({ imageEditor, imagePath, visualCropEnabled = false }: LayerPanelProps) {
176+
export function LayerPanel({
177+
imageEditor,
178+
imagePath,
179+
selectedLayerId,
180+
visualCropEnabled = false,
181+
}: LayerPanelProps) {
176182
const { t } = useTranslation()
177183
const layers = imageEditor.getLayers()
178-
const [selectedLayerId, setSelectedLayerId] = useState<string | null>(null)
179184
const [editingContext, setEditingContext] = useState<string | null>(
180185
imageEditor.getEditingContext(),
181186
)
@@ -228,33 +233,29 @@ export function LayerPanel({ imageEditor, imagePath, visualCropEnabled = false }
228233

229234
const handleDelete = useCallback(
230235
(layerId: string) => {
231-
// Deselect if deleting the selected layer
232-
if (selectedLayerId === layerId) {
233-
setSelectedLayerId(null)
234-
}
235236
// Exit edit mode if deleting the editing layer
236237
if (editingContext === layerId) {
237238
imageEditor.switchContext(null)
238239
setEditingContext(null)
239240
}
241+
// removeLayer will automatically clear selection if needed
240242
imageEditor.removeLayer(layerId)
241243
},
242-
[imageEditor, selectedLayerId, editingContext],
244+
[imageEditor, editingContext],
243245
)
244246

245247
const handleSelectLayer = useCallback(
246248
(layerId: string) => {
247249
// Toggle selection without entering edit mode
248250
const newSelection = selectedLayerId === layerId ? null : layerId
249-
setSelectedLayerId(newSelection)
251+
imageEditor.setSelectedLayerId(newSelection)
250252
},
251-
[selectedLayerId],
253+
[imageEditor, selectedLayerId],
252254
)
253255

254256
const handleEditLayer = useCallback(
255257
(layerId: string) => {
256-
// Enter edit mode for this layer
257-
setSelectedLayerId(layerId)
258+
// Enter edit mode for this layer (switchContext will auto-select)
258259
imageEditor.switchContext(layerId)
259260
setEditingContext(layerId)
260261
},
@@ -269,7 +270,7 @@ export function LayerPanel({ imageEditor, imagePath, visualCropEnabled = false }
269270

270271
const handleSelectBase = useCallback(() => {
271272
// Deselect all layers and return to base
272-
setSelectedLayerId(null)
273+
imageEditor.setSelectedLayerId(null)
273274
if (editingContext !== null) {
274275
imageEditor.switchContext(null)
275276
setEditingContext(null)
@@ -355,7 +356,7 @@ export function LayerPanel({ imageEditor, imagePath, visualCropEnabled = false }
355356
imageEditor.addLayer(newLayer)
356357

357358
// Auto-select the newly added layer
358-
setSelectedLayerId(newLayer.id)
359+
imageEditor.setSelectedLayerId(newLayer.id)
359360
} catch {
360361
toast.error(t('imageEditor.layers.failedToAddLayer'))
361362
} finally {

web/src/lib/image-editor.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export interface ImageEditorCallbacks {
116116
onStateChange?: (state: ImageEditorState) => void
117117
onLoadingChange?: (isLoading: boolean) => void
118118
onHistoryChange?: () => void
119+
onSelectedLayerChange?: (layerId: string | null) => void
119120
}
120121

121122
/**
@@ -164,6 +165,8 @@ export class ImageEditor {
164165
this.undoStack = []
165166
this.redoStack = []
166167
this.pendingHistorySnapshot = null
168+
// Reset selected layer
169+
this.selectedLayerId = null
167170
// Reset state to defaults when component remounts
168171
// The page will restore from URL if there's a ?state= parameter
169172
this.state = {
@@ -1206,6 +1209,13 @@ export class ImageEditor {
12061209
*/
12071210
private editingContext: string | null = null
12081211

1212+
/**
1213+
* Currently selected layer (for UI highlighting)
1214+
* null = base image selected or no selection
1215+
* string = layer with this ID is selected
1216+
*/
1217+
private selectedLayerId: string | null = null
1218+
12091219
/**
12101220
* Saved base image configuration (when editing a layer)
12111221
* Allows restoring the original config when switching back to base
@@ -1221,6 +1231,25 @@ export class ImageEditor {
12211231
return this.editingContext
12221232
}
12231233

1234+
/**
1235+
* Get the currently selected layer ID
1236+
* @returns null for base image or no selection, or layer ID for selected layer
1237+
*/
1238+
getSelectedLayerId(): string | null {
1239+
return this.selectedLayerId
1240+
}
1241+
1242+
/**
1243+
* Set the currently selected layer ID
1244+
* Triggers onSelectedLayerChange callback if selection changes
1245+
* @param layerId - ID of the layer to select, or null for base image
1246+
*/
1247+
setSelectedLayerId(layerId: string | null): void {
1248+
if (this.selectedLayerId === layerId) return
1249+
this.selectedLayerId = layerId
1250+
this.callbacks.onSelectedLayerChange?.(layerId)
1251+
}
1252+
12241253
/**
12251254
* Switch editing context to a layer
12261255
* Loads the layer's transforms into the editor state
@@ -1251,6 +1280,11 @@ export class ImageEditor {
12511280
// Switch context
12521281
this.editingContext = layerId
12531282

1283+
// Auto-select the layer when entering edit mode
1284+
if (layerId !== null) {
1285+
this.setSelectedLayerId(layerId)
1286+
}
1287+
12541288
// Update config and load new context
12551289
if (layerId !== null) {
12561290
// Switching TO a layer
@@ -1410,6 +1444,11 @@ export class ImageEditor {
14101444
// Save current state to history BEFORE removing layer (so undo restores it)
14111445
this.saveHistorySnapshot()
14121446

1447+
// Clear selection if removing the selected layer
1448+
if (this.selectedLayerId === layerId) {
1449+
this.setSelectedLayerId(null)
1450+
}
1451+
14131452
this.state = {
14141453
...this.state,
14151454
layers: this.state.layers.filter((layer) => layer.id !== layerId),

web/src/pages/image-editor-page.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export function ImageEditorPage({ galleryKey, imageKey, loaderData }: ImageEdito
9292
const [cropAspectRatio, setCropAspectRatio] = useState<number | null>(null)
9393
const [canUndo, setCanUndo] = useState(false)
9494
const [canRedo, setCanRedo] = useState(false)
95+
const [selectedLayerId, setSelectedLayerId] = useState<string | null>(null)
9596

9697
// Unsaved changes warning hook
9798
const { showDialog, handleConfirm, handleCancel } = useUnsavedChangesWarning(canUndo)
@@ -116,6 +117,7 @@ export function ImageEditorPage({ galleryKey, imageKey, loaderData }: ImageEdito
116117
const encoded = serializeStateToUrl(state)
117118
updateLocationState(encoded)
118119
},
120+
onSelectedLayerChange: setSelectedLayerId,
119121
})
120122

121123
// restore state from URL, after callbacks are set
@@ -346,6 +348,7 @@ export function ImageEditorPage({ galleryKey, imageKey, loaderData }: ImageEdito
346348
imageEditor={imageEditor}
347349
imagePath={imagePath}
348350
params={params}
351+
selectedLayerId={selectedLayerId}
349352
openSections={editorOpenSections}
350353
onOpenSectionsChange={handleOpenSectionsChange}
351354
onUpdateParams={updateParams}
@@ -407,6 +410,7 @@ export function ImageEditorPage({ galleryKey, imageKey, loaderData }: ImageEdito
407410
imageEditor={imageEditor}
408411
imagePath={imagePath}
409412
params={params}
413+
selectedLayerId={selectedLayerId}
410414
openSections={editorOpenSections}
411415
onOpenSectionsChange={handleOpenSectionsChange}
412416
onUpdateParams={updateParams}

0 commit comments

Comments
 (0)