Centralizes time formatting with timezone support
Addresses inconsistent time formatting and lack of timezone handling throughout the application by introducing a `TimeFormatter` utility. This class centralizes time formatting logic, providing timezone conversion (defaults to Europe/Copenhagen) and support for both 12-hour and 24-hour formats, configurable via `CalendarConfig`. It also updates event rendering to utilize the new `TimeFormatter` for consistent time displays.
This commit is contained in:
parent
c07d83d86f
commit
8b96376d1f
7 changed files with 489 additions and 199 deletions
|
|
@ -10,165 +10,6 @@ import { DateCalculator } from '../utils/DateCalculator';
|
|||
*/
|
||||
export class AllDayEventRenderer {
|
||||
|
||||
/**
|
||||
* Render all-day events in the header container
|
||||
*/
|
||||
public renderAllDayEvents(events: CalendarEvent[], container: HTMLElement): void {
|
||||
const allDayEvents = events.filter(event => event.allDay);
|
||||
|
||||
// Find the calendar header
|
||||
const calendarHeader = container.querySelector('swp-calendar-header');
|
||||
if (!calendarHeader) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find or create all-day container
|
||||
let allDayContainer = calendarHeader.querySelector('swp-allday-container') as HTMLElement;
|
||||
if (!allDayContainer) {
|
||||
allDayContainer = document.createElement('swp-allday-container');
|
||||
calendarHeader.appendChild(allDayContainer);
|
||||
}
|
||||
|
||||
// Clear existing events
|
||||
allDayContainer.innerHTML = '';
|
||||
|
||||
if (allDayEvents.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build date to column mapping
|
||||
const dayHeaders = calendarHeader.querySelectorAll('swp-day-header');
|
||||
const dateToColumnMap = new Map<string, number>();
|
||||
|
||||
dayHeaders.forEach((header, index) => {
|
||||
const dateStr = (header as HTMLElement).dataset.date;
|
||||
if (dateStr) {
|
||||
dateToColumnMap.set(dateStr, index + 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate grid positioning for events
|
||||
const eventPlacements = this.calculateEventPlacements(allDayEvents, dateToColumnMap);
|
||||
|
||||
// Render events using factory pattern
|
||||
eventPlacements.forEach(({ event, gridColumn, gridRow }) => {
|
||||
const eventDateStr = DateCalculator.formatISODate(event.start);
|
||||
const swpAllDayEvent = SwpAllDayEventElement.fromCalendarEvent(event, eventDateStr);
|
||||
const allDayElement = swpAllDayEvent.getElement();
|
||||
|
||||
// Apply grid positioning
|
||||
(allDayElement as HTMLElement).style.gridColumn = gridColumn;
|
||||
(allDayElement as HTMLElement).style.gridRow = gridRow.toString();
|
||||
|
||||
// Use event metadata for color if available
|
||||
if (event.metadata?.color) {
|
||||
(allDayElement as HTMLElement).style.backgroundColor = event.metadata.color;
|
||||
}
|
||||
|
||||
allDayContainer.appendChild(allDayElement);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate grid positioning for all-day events with overlap detection
|
||||
*/
|
||||
private calculateEventPlacements(events: CalendarEvent[], dateToColumnMap: Map<string, number>) {
|
||||
// Calculate spans for each event
|
||||
const eventItems = events.map(event => {
|
||||
const eventDateStr = DateCalculator.formatISODate(event.start);
|
||||
const endDateStr = DateCalculator.formatISODate(event.end);
|
||||
|
||||
const startColumn = dateToColumnMap.get(eventDateStr);
|
||||
const endColumn = dateToColumnMap.get(endDateStr);
|
||||
|
||||
if (startColumn === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const columnSpan = endColumn !== undefined && endColumn >= startColumn
|
||||
? endColumn - startColumn + 1
|
||||
: 1;
|
||||
|
||||
return {
|
||||
event,
|
||||
span: {
|
||||
startColumn: startColumn,
|
||||
columnSpan: columnSpan
|
||||
}
|
||||
};
|
||||
}).filter(item => item !== null) as Array<{
|
||||
event: CalendarEvent;
|
||||
span: { startColumn: number; columnSpan: number };
|
||||
}>;
|
||||
|
||||
// Calculate row placement to avoid overlaps
|
||||
interface EventPlacement {
|
||||
event: CalendarEvent;
|
||||
gridColumn: string;
|
||||
gridRow: number;
|
||||
}
|
||||
|
||||
const eventPlacements: EventPlacement[] = [];
|
||||
|
||||
eventItems.forEach(eventItem => {
|
||||
let assignedRow = 1;
|
||||
|
||||
// Find first available row
|
||||
while (true) {
|
||||
// Check if this row has any conflicts
|
||||
const rowEvents = eventPlacements.filter(p => p.gridRow === assignedRow);
|
||||
|
||||
const hasOverlap = rowEvents.some(rowEvent => {
|
||||
// Parse the existing grid column to check overlap
|
||||
const existingSpan = this.parseGridColumn(rowEvent.gridColumn);
|
||||
return this.spansOverlap(eventItem.span, existingSpan);
|
||||
});
|
||||
|
||||
if (!hasOverlap) {
|
||||
break; // Found available row
|
||||
}
|
||||
assignedRow++;
|
||||
}
|
||||
|
||||
const gridColumn = eventItem.span.columnSpan > 1
|
||||
? `${eventItem.span.startColumn} / span ${eventItem.span.columnSpan}`
|
||||
: `${eventItem.span.startColumn}`;
|
||||
|
||||
eventPlacements.push({
|
||||
event: eventItem.event,
|
||||
gridColumn,
|
||||
gridRow: assignedRow
|
||||
});
|
||||
});
|
||||
|
||||
return eventPlacements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two column spans overlap
|
||||
*/
|
||||
private spansOverlap(span1: { startColumn: number; columnSpan: number }, span2: { startColumn: number; columnSpan: number }): boolean {
|
||||
const span1End = span1.startColumn + span1.columnSpan - 1;
|
||||
const span2End = span2.startColumn + span2.columnSpan - 1;
|
||||
|
||||
return !(span1End < span2.startColumn || span2End < span1.startColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse grid column string back to span object
|
||||
*/
|
||||
private parseGridColumn(gridColumn: string): { startColumn: number; columnSpan: number } {
|
||||
if (gridColumn.includes('span')) {
|
||||
const parts = gridColumn.split(' / span ');
|
||||
return {
|
||||
startColumn: parseInt(parts[0]),
|
||||
columnSpan: parseInt(parts[1])
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
startColumn: parseInt(gridColumn),
|
||||
columnSpan: 1
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import { eventBus } from '../core/EventBus';
|
|||
import { CoreEvents } from '../constants/CoreEvents';
|
||||
import { OverlapDetector, OverlapResult, EventId } from '../utils/OverlapDetector';
|
||||
import { SwpEventElement, SwpAllDayEventElement } from '../elements/SwpEventElement';
|
||||
import { TimeFormatter } from '../utils/TimeFormatter';
|
||||
|
||||
/**
|
||||
* Interface for event rendering strategies
|
||||
|
|
@ -184,12 +185,11 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
* Create event inner structure (swp-event-time and swp-event-title)
|
||||
*/
|
||||
private createEventInnerStructure(event: CalendarEvent): string {
|
||||
const startTime = this.formatTime(event.start);
|
||||
const endTime = this.formatTime(event.end);
|
||||
const timeRange = TimeFormatter.formatTimeRange(event.start, event.end);
|
||||
const durationMinutes = (event.end.getTime() - event.start.getTime()) / (1000 * 60);
|
||||
|
||||
return `
|
||||
<swp-event-time data-duration="${durationMinutes}">${startTime} - ${endTime}</swp-event-time>
|
||||
<swp-event-time data-duration="${durationMinutes}">${timeRange}</swp-event-time>
|
||||
<swp-event-title>${event.title}</swp-event-title>
|
||||
`;
|
||||
}
|
||||
|
|
@ -269,33 +269,12 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
// Update display
|
||||
const timeElement = clone.querySelector('swp-event-time');
|
||||
if (timeElement) {
|
||||
const newTimeText = `${this.formatTime(snappedStartMinutes)} - ${this.formatTime(endTotalMinutes)}`;
|
||||
timeElement.textContent = newTimeText;
|
||||
const startTime = TimeFormatter.formatTimeFromMinutes(snappedStartMinutes);
|
||||
const endTime = TimeFormatter.formatTimeFromMinutes(endTotalMinutes);
|
||||
timeElement.textContent = `${startTime} - ${endTime}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified time formatting method - handles both total minutes and Date objects
|
||||
*/
|
||||
private formatTime(input: number | Date | string): string {
|
||||
let hours: number, minutes: number;
|
||||
|
||||
if (typeof input === 'number') {
|
||||
// Total minutes input
|
||||
hours = Math.floor(input / 60) % 24;
|
||||
minutes = input % 60;
|
||||
} else {
|
||||
// Date or ISO string input
|
||||
const date = typeof input === 'string' ? new Date(input) : input;
|
||||
hours = date.getHours();
|
||||
minutes = date.getMinutes();
|
||||
}
|
||||
|
||||
const period = hours >= 12 ? 'PM' : 'AM';
|
||||
const displayHours = hours > 12 ? hours - 12 : (hours === 0 ? 12 : hours);
|
||||
return `${displayHours}:${minutes.toString().padStart(2, '0')} ${period}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle drag start event
|
||||
*/
|
||||
|
|
@ -590,9 +569,8 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
// Update the time display
|
||||
const timeElement = element.querySelector('swp-event-time');
|
||||
if (timeElement) {
|
||||
const startTime = this.formatTime(event.start);
|
||||
const endTime = this.formatTime(event.end);
|
||||
timeElement.textContent = `${startTime} - ${endTime}`;
|
||||
const timeRange = TimeFormatter.formatTimeRange(event.start, event.end);
|
||||
timeElement.textContent = timeRange;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue