Calendar/src/utils/PositionUtils.ts

291 lines
8.8 KiB
TypeScript
Raw Normal View History

import { CalendarConfig } from '../core/CalendarConfig.js';
/**
* PositionUtils - Utility funktioner til pixel/minut konvertering
* Håndterer positionering og størrelse beregninger for calendar events
*/
export class PositionUtils {
private config: CalendarConfig;
constructor(config: CalendarConfig) {
this.config = config;
}
/**
* Konverter minutter til pixels
*/
public minutesToPixels(minutes: number): number {
const pixelsPerHour = this.config.get('hourHeight');
return (minutes / 60) * pixelsPerHour;
}
/**
* Konverter pixels til minutter
*/
public pixelsToMinutes(pixels: number): number {
const pixelsPerHour = this.config.get('hourHeight');
return (pixels / pixelsPerHour) * 60;
}
/**
* Konverter tid (HH:MM) til pixels fra dag start
*/
public timeToPixels(timeString: string): number {
const [hours, minutes] = timeString.split(':').map(Number);
const totalMinutes = (hours * 60) + minutes;
const dayStartMinutes = this.config.get('dayStartHour') * 60;
const minutesFromDayStart = totalMinutes - dayStartMinutes;
return this.minutesToPixels(minutesFromDayStart);
}
/**
* Konverter Date object til pixels fra dag start
*/
public dateToPixels(date: Date): number {
const hours = date.getHours();
const minutes = date.getMinutes();
const totalMinutes = (hours * 60) + minutes;
const dayStartMinutes = this.config.get('dayStartHour') * 60;
const minutesFromDayStart = totalMinutes - dayStartMinutes;
return this.minutesToPixels(minutesFromDayStart);
}
/**
* Konverter pixels til tid (HH:MM format)
*/
public pixelsToTime(pixels: number): string {
const minutes = this.pixelsToMinutes(pixels);
const dayStartMinutes = this.config.get('dayStartHour') * 60;
const totalMinutes = dayStartMinutes + minutes;
const hours = Math.floor(totalMinutes / 60);
const mins = Math.round(totalMinutes % 60);
return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`;
}
/**
* Beregn event position og størrelse
*/
public calculateEventPosition(startTime: string | Date, endTime: string | Date): {
top: number;
height: number;
duration: number;
} {
let startPixels: number;
let endPixels: number;
if (typeof startTime === 'string') {
startPixels = this.timeToPixels(startTime);
} else {
startPixels = this.dateToPixels(startTime);
}
if (typeof endTime === 'string') {
endPixels = this.timeToPixels(endTime);
} else {
endPixels = this.dateToPixels(endTime);
}
const height = Math.max(endPixels - startPixels, this.getMinimumEventHeight());
const duration = this.pixelsToMinutes(height);
return {
top: startPixels,
height,
duration
};
}
/**
* Snap position til grid interval
*/
public snapToGrid(pixels: number): number {
const snapInterval = this.config.get('snapInterval');
const snapPixels = this.minutesToPixels(snapInterval);
return Math.round(pixels / snapPixels) * snapPixels;
}
/**
* Snap tid til interval
*/
public snapTimeToInterval(timeString: string): string {
const [hours, minutes] = timeString.split(':').map(Number);
const totalMinutes = (hours * 60) + minutes;
const snapInterval = this.config.get('snapInterval');
const snappedMinutes = Math.round(totalMinutes / snapInterval) * snapInterval;
const snappedHours = Math.floor(snappedMinutes / 60);
const remainingMinutes = snappedMinutes % 60;
return `${snappedHours.toString().padStart(2, '0')}:${remainingMinutes.toString().padStart(2, '0')}`;
}
/**
* Beregn kolonne position for overlappende events
*/
public calculateColumnPosition(eventIndex: number, totalColumns: number, containerWidth: number): {
left: number;
width: number;
} {
const columnWidth = containerWidth / totalColumns;
const left = eventIndex * columnWidth;
// Lav lidt margin mellem kolonnerne
const margin = 2;
const adjustedWidth = columnWidth - margin;
return {
left: left + (margin / 2),
width: Math.max(adjustedWidth, 50) // Minimum width
};
}
/**
* Check om to events overlapper i tid
*/
public eventsOverlap(
start1: string | Date,
end1: string | Date,
start2: string | Date,
end2: string | Date
): boolean {
const pos1 = this.calculateEventPosition(start1, end1);
const pos2 = this.calculateEventPosition(start2, end2);
const event1End = pos1.top + pos1.height;
const event2End = pos2.top + pos2.height;
return !(event1End <= pos2.top || event2End <= pos1.top);
}
/**
* Beregn Y position fra mouse/touch koordinat
*/
public getPositionFromCoordinate(clientY: number, containerElement: HTMLElement): number {
const rect = containerElement.getBoundingClientRect();
const relativeY = clientY - rect.top;
// Snap til grid
return this.snapToGrid(relativeY);
}
/**
* Beregn tid fra mouse/touch koordinat
*/
public getTimeFromCoordinate(clientY: number, containerElement: HTMLElement): string {
const position = this.getPositionFromCoordinate(clientY, containerElement);
return this.pixelsToTime(position);
}
/**
* Valider at tid er inden for arbejdstimer
*/
public isWithinWorkHours(timeString: string): boolean {
const [hours] = timeString.split(':').map(Number);
return hours >= this.config.get('workStartHour') && hours < this.config.get('workEndHour');
}
/**
* Valider at tid er inden for dag grænser
*/
public isWithinDayBounds(timeString: string): boolean {
const [hours] = timeString.split(':').map(Number);
return hours >= this.config.get('dayStartHour') && hours < this.config.get('dayEndHour');
}
/**
* Hent minimum event højde i pixels
*/
public getMinimumEventHeight(): number {
// Minimum 15 minutter
return this.minutesToPixels(15);
}
/**
* Hent maksimum event højde i pixels (hele dagen)
*/
public getMaximumEventHeight(): number {
const dayDurationHours = this.config.get('dayEndHour') - this.config.get('dayStartHour');
return dayDurationHours * this.config.get('hourHeight');
}
/**
* Beregn total kalender højde
*/
public getTotalCalendarHeight(): number {
return this.getMaximumEventHeight();
}
/**
* Konverter ISO datetime til lokal tid string
*/
public isoToTimeString(isoString: string): string {
const date = new Date(isoString);
const hours = date.getHours();
const minutes = date.getMinutes();
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
}
/**
* Konverter lokal tid string til ISO datetime for i dag
*/
public timeStringToIso(timeString: string, date: Date = new Date()): string {
const [hours, minutes] = timeString.split(':').map(Number);
const newDate = new Date(date);
newDate.setHours(hours, minutes, 0, 0);
return newDate.toISOString();
}
/**
* Beregn event varighed i minutter
*/
public calculateDuration(startTime: string | Date, endTime: string | Date): number {
let startMs: number;
let endMs: number;
if (typeof startTime === 'string') {
startMs = new Date(startTime).getTime();
} else {
startMs = startTime.getTime();
}
if (typeof endTime === 'string') {
endMs = new Date(endTime).getTime();
} else {
endMs = endTime.getTime();
}
return Math.round((endMs - startMs) / (1000 * 60)); // Minutter
}
/**
* Format varighed til læsbar tekst
*/
public formatDuration(minutes: number): string {
if (minutes < 60) {
return `${minutes} min`;
}
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
if (remainingMinutes === 0) {
return `${hours} time${hours !== 1 ? 'r' : ''}`;
}
return `${hours}t ${remainingMinutes}m`;
}
/**
* Opdater konfiguration
*/
public updateConfig(newConfig: CalendarConfig): void {
this.config = newConfig;
}
}