Introduces DateService for time zone handling
Adds DateService using date-fns-tz for robust time zone conversions and date manipulations. Refactors DateCalculator and TimeFormatter to utilize the DateService, centralizing date logic and ensuring consistent time zone handling throughout the application. Improves event dragging by updating time displays and data attributes, handling cross-midnight events correctly.
This commit is contained in:
parent
1821d805d1
commit
53cf097a47
8 changed files with 764 additions and 136 deletions
|
|
@ -11,6 +11,8 @@ import { PositionUtils } from '../utils/PositionUtils';
|
|||
import { DragOffset, StackLinkData } from '../types/DragDropTypes';
|
||||
import { ColumnBounds } from '../utils/ColumnDetectionUtils';
|
||||
import { DragColumnChangeEventPayload, DragMoveEventPayload, DragStartEventPayload } from '../types/EventTypes';
|
||||
import { DateService } from '../utils/DateService';
|
||||
import { format, setHours, setMinutes, setSeconds, addDays } from 'date-fns';
|
||||
|
||||
/**
|
||||
* Interface for event rendering strategies
|
||||
|
|
@ -113,37 +115,95 @@ export class DateEventRenderer implements EventRendererStrategy {
|
|||
* Update clone timestamp based on new position
|
||||
*/
|
||||
private updateCloneTimestamp(payload: DragMoveEventPayload): void {
|
||||
|
||||
//important as events can pile up, so they will still fire after event has been converted to another rendered type
|
||||
if (payload.draggedClone.dataset.allDay == "true") return;
|
||||
if (payload.draggedClone.dataset.allDay === "true" || !payload.columnBounds) return;
|
||||
|
||||
const gridSettings = calendarConfig.getGridSettings();
|
||||
const hourHeight = gridSettings.hourHeight;
|
||||
const dayStartHour = gridSettings.dayStartHour;
|
||||
const snapInterval = gridSettings.snapInterval;
|
||||
|
||||
// Calculate minutes from grid start (not from midnight)
|
||||
const minutesFromGridStart = (payload.snappedY / hourHeight) * 60;
|
||||
|
||||
// Add dayStartHour offset to get actual time
|
||||
const actualStartMinutes = (dayStartHour * 60) + minutesFromGridStart;
|
||||
|
||||
// Snap to interval
|
||||
const snappedStartMinutes = Math.round(actualStartMinutes / snapInterval) * snapInterval;
|
||||
|
||||
|
||||
if (!payload.draggedClone.dataset.originalDuration)
|
||||
throw new DOMException("missing clone.dataset.originalDuration")
|
||||
|
||||
const endTotalMinutes = snappedStartMinutes + parseInt(payload.draggedClone.dataset.originalDuration);
|
||||
|
||||
// Update visual time display only
|
||||
const timeElement = payload.draggedClone.querySelector('swp-event-time');
|
||||
if (timeElement) {
|
||||
let startTime = TimeFormatter.formatTimeFromMinutes(snappedStartMinutes);
|
||||
let endTime = TimeFormatter.formatTimeFromMinutes(endTotalMinutes);
|
||||
timeElement.textContent = `${startTime} - ${endTime}`;
|
||||
const { hourHeight, dayStartHour, snapInterval } = gridSettings;
|
||||
|
||||
if (!payload.draggedClone.dataset.originalDuration) {
|
||||
throw new DOMException("missing clone.dataset.originalDuration");
|
||||
}
|
||||
|
||||
// Calculate snapped start minutes
|
||||
const minutesFromGridStart = (payload.snappedY / hourHeight) * 60;
|
||||
const snappedStartMinutes = this.calculateSnappedMinutes(
|
||||
minutesFromGridStart, dayStartHour, snapInterval
|
||||
);
|
||||
|
||||
// Calculate end minutes
|
||||
const originalDuration = parseInt(payload.draggedClone.dataset.originalDuration);
|
||||
const endTotalMinutes = snappedStartMinutes + originalDuration;
|
||||
|
||||
// Update UI
|
||||
this.updateTimeDisplay(payload.draggedClone, snappedStartMinutes, endTotalMinutes);
|
||||
|
||||
// Update data attributes
|
||||
this.updateDateTimeAttributes(
|
||||
payload.draggedClone,
|
||||
new Date(payload.columnBounds.date),
|
||||
snappedStartMinutes,
|
||||
endTotalMinutes
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate snapped minutes from grid start
|
||||
*/
|
||||
private calculateSnappedMinutes(minutesFromGridStart: number, dayStartHour: number, snapInterval: number): number {
|
||||
const actualStartMinutes = (dayStartHour * 60) + minutesFromGridStart;
|
||||
return Math.round(actualStartMinutes / snapInterval) * snapInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update time display in the UI
|
||||
*/
|
||||
private updateTimeDisplay(element: HTMLElement, startMinutes: number, endMinutes: number): void {
|
||||
const timeElement = element.querySelector('swp-event-time');
|
||||
if (!timeElement) return;
|
||||
|
||||
const startTime = this.formatTimeFromMinutes(startMinutes);
|
||||
const endTime = this.formatTimeFromMinutes(endMinutes);
|
||||
timeElement.textContent = `${startTime} - ${endTime}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update data-start and data-end attributes with ISO timestamps
|
||||
*/
|
||||
private updateDateTimeAttributes(element: HTMLElement, columnDate: Date, startMinutes: number, endMinutes: number): void {
|
||||
const startDate = this.createDateWithMinutes(columnDate, startMinutes);
|
||||
|
||||
let endDate = this.createDateWithMinutes(columnDate, endMinutes);
|
||||
|
||||
// Handle cross-midnight events
|
||||
if (endMinutes >= 1440) {
|
||||
const extraDays = Math.floor(endMinutes / 1440);
|
||||
endDate = addDays(endDate, extraDays);
|
||||
}
|
||||
|
||||
element.dataset.start = startDate.toISOString();
|
||||
element.dataset.end = endDate.toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a date with specific minutes since midnight
|
||||
*/
|
||||
private createDateWithMinutes(baseDate: Date, totalMinutes: number): Date {
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
|
||||
return setSeconds(setMinutes(setHours(baseDate, hours), minutes), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format minutes since midnight to time string
|
||||
*/
|
||||
private formatTimeFromMinutes(totalMinutes: number): string {
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
const date = new Date();
|
||||
date.setHours(hours, minutes, 0, 0);
|
||||
|
||||
return format(date, 'HH:mm');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -209,7 +269,19 @@ export class DateEventRenderer implements EventRendererStrategy {
|
|||
const eventsLayer = dragColumnChangeEvent.newColumn.element.querySelector('swp-events-layer');
|
||||
if (eventsLayer && this.draggedClone.parentElement !== eventsLayer) {
|
||||
eventsLayer.appendChild(this.draggedClone);
|
||||
|
||||
|
||||
// Recalculate timestamps with new column date
|
||||
const currentTop = parseFloat(this.draggedClone.style.top) || 0;
|
||||
const mockPayload: DragMoveEventPayload = {
|
||||
draggedElement: dragColumnChangeEvent.originalElement,
|
||||
draggedClone: this.draggedClone,
|
||||
mousePosition: dragColumnChangeEvent.mousePosition,
|
||||
mouseOffset: { x: 0, y: 0 },
|
||||
columnBounds: dragColumnChangeEvent.newColumn,
|
||||
snappedY: currentTop
|
||||
};
|
||||
|
||||
this.updateCloneTimestamp(mockPayload);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -312,14 +384,8 @@ export class DateEventRenderer implements EventRendererStrategy {
|
|||
draggedClone.classList.remove('dragging');
|
||||
// Behold z-index hvis det er et stacked event
|
||||
|
||||
// Update dataset with new times after successful drop (only for timed events)
|
||||
if (draggedClone.dataset.displayType !== 'allday') {
|
||||
const newEvent = SwpEventElement.extractCalendarEventFromElement(draggedClone);
|
||||
if (newEvent) {
|
||||
draggedClone.dataset.start = newEvent.start.toISOString();
|
||||
draggedClone.dataset.end = newEvent.end.toISOString();
|
||||
}
|
||||
}
|
||||
// Data attributes are already updated during drag:move, so no need to update again
|
||||
// The updateCloneTimestamp method keeps them synchronized throughout the drag operation
|
||||
|
||||
// Detect overlaps with other events in the target column and reposition if needed
|
||||
this.handleDragDropOverlaps(draggedClone, finalColumn);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue