Files
Toju/toju-app/src/app/domains/theme/feature/settings/theme-grid-editor.component.ts
2026-04-02 00:08:38 +02:00

135 lines
4.0 KiB
TypeScript

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<ThemeLayoutContainerDefinition>();
readonly items = input.required<ThemeGridEditorItem[]>();
readonly selectedKey = input<string | null>(null);
readonly disabled = input(false);
readonly itemChanged = output<{ key: string; grid: ThemeGridRect }>();
readonly itemSelected = output<string>();
private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);
private dragState: DragState | null = null;
readonly canvasRef = viewChild.required<ElementRef<HTMLElement>>('canvasRef');
readonly frameStyle = computed(() => ({
'--theme-grid-columns': `${this.container().columns}`,
'--theme-grid-rows': `${this.container().rows}`
}));
itemStyle(item: ThemeGridEditorItem): Record<string, string> {
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);
}
}