import { Component, ElementRef, HostListener, computed, inject, input, output, viewChild } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ThemeGridEditorItem, ThemeGridRect, ThemeLayoutContainerDefinition } from '../../domain/theme.models'; type DragMode = 'move' | 'resize'; interface DragState { key: string; mode: DragMode; startClientX: number; startClientY: number; startGrid: ThemeGridRect; } @Component({ selector: 'app-theme-grid-editor', standalone: true, imports: [CommonModule], templateUrl: './theme-grid-editor.component.html', styleUrl: './theme-grid-editor.component.scss' }) export class ThemeGridEditorComponent { readonly container = input.required(); readonly items = input.required(); readonly selectedKey = input(null); readonly disabled = input(false); readonly itemChanged = output<{ key: string; grid: ThemeGridRect }>(); readonly itemSelected = output(); private readonly host = inject>(ElementRef); private dragState: DragState | null = null; readonly canvasRef = viewChild.required>('canvasRef'); readonly frameStyle = computed(() => ({ '--theme-grid-columns': `${this.container().columns}`, '--theme-grid-rows': `${this.container().rows}` })); itemStyle(item: ThemeGridEditorItem): Record { const { columns, rows } = this.container(); return { left: `${(item.grid.x / columns) * 100}%`, top: `${(item.grid.y / rows) * 100}%`, width: `${(item.grid.w / columns) * 100}%`, height: `${(item.grid.h / rows) * 100}%` }; } selectItem(key: string): void { this.itemSelected.emit(key); } startMove(event: PointerEvent, item: ThemeGridEditorItem): void { this.startDrag(event, item, 'move'); } startResize(event: PointerEvent, item: ThemeGridEditorItem): void { this.startDrag(event, item, 'resize'); } @HostListener('document:pointermove', ['$event']) onPointerMove(event: PointerEvent): void { if (!this.dragState || this.disabled()) { return; } const canvasRect = this.canvasRef().nativeElement.getBoundingClientRect(); const columnWidth = canvasRect.width / this.container().columns; const rowHeight = canvasRect.height / this.container().rows; const deltaColumns = Math.round((event.clientX - this.dragState.startClientX) / columnWidth); const deltaRows = Math.round((event.clientY - this.dragState.startClientY) / rowHeight); const nextGrid = { ...this.dragState.startGrid }; if (this.dragState.mode === 'move') { nextGrid.x = this.clamp(deltaColumns + this.dragState.startGrid.x, 0, this.container().columns - nextGrid.w); nextGrid.y = this.clamp(deltaRows + this.dragState.startGrid.y, 0, this.container().rows - nextGrid.h); } else { nextGrid.w = this.clamp(deltaColumns + this.dragState.startGrid.w, 1, this.container().columns - nextGrid.x); nextGrid.h = this.clamp(deltaRows + this.dragState.startGrid.h, 1, this.container().rows - nextGrid.y); } this.itemChanged.emit({ key: this.dragState.key, grid: nextGrid }); } @HostListener('document:pointerup') @HostListener('document:pointercancel') onPointerUp(): void { this.dragState = null; } @HostListener('document:keydown.escape') onEscape(): void { this.dragState = null; } private startDrag(event: PointerEvent, item: ThemeGridEditorItem, mode: DragMode): void { if (this.disabled()) { return; } event.preventDefault(); event.stopPropagation(); this.itemSelected.emit(item.key); this.dragState = { key: item.key, mode, startClientX: event.clientX, startClientY: event.clientY, startGrid: { ...item.grid } }; } private clamp(value: number, minimum: number, maximum: number): number { return Math.min(Math.max(value, minimum), maximum); } }