135 lines
4.0 KiB
TypeScript
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);
|
|
}
|
|
} |