Refactor event handling with SwpEvent wrapper
Introduces SwpEvent class to centralize event element data and calculations Improves drag and resize event handling by extracting common logic Simplifies calculation of event start/end times from element positions Enhances type safety and reduces code complexity in event managers Removes direct pixel/minute calculations from multiple managers
This commit is contained in:
parent
9e568fbd8e
commit
f670598e7a
7 changed files with 132 additions and 62 deletions
|
|
@ -59,7 +59,7 @@ export class EventRenderer {
|
||||||
private handleDragEnd(payload: IDragEndPayload): void {
|
private handleDragEnd(payload: IDragEndPayload): void {
|
||||||
if (payload.target === 'header') {
|
if (payload.target === 'header') {
|
||||||
// Event was dropped in header drawer - remove from grid
|
// Event was dropped in header drawer - remove from grid
|
||||||
const element = this.container?.querySelector(`swp-content-viewport swp-event[data-event-id="${payload.eventId}"]`);
|
const element = this.container?.querySelector(`swp-content-viewport swp-event[data-event-id="${payload.swpEvent.eventId}"]`);
|
||||||
element?.remove();
|
element?.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -284,8 +284,11 @@ export class EventRenderer {
|
||||||
private createEventElement(event: ICalendarEvent): HTMLElement {
|
private createEventElement(event: ICalendarEvent): HTMLElement {
|
||||||
const element = document.createElement('swp-event');
|
const element = document.createElement('swp-event');
|
||||||
|
|
||||||
// Only essential data attribute (eventId for DragDropManager compatibility)
|
// Data attributes for SwpEvent compatibility
|
||||||
element.dataset.eventId = event.id;
|
element.dataset.eventId = event.id;
|
||||||
|
if (event.resourceId) {
|
||||||
|
element.dataset.resourceId = event.resourceId;
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate position
|
// Calculate position
|
||||||
const position = calculateEventPosition(event.start, event.end, this.gridConfig);
|
const position = calculateEventPosition(event.start, event.end, this.gridConfig);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
IDragMoveHeaderPayload,
|
IDragMoveHeaderPayload,
|
||||||
IDragLeaveHeaderPayload
|
IDragLeaveHeaderPayload
|
||||||
} from '../types/DragTypes';
|
} from '../types/DragTypes';
|
||||||
|
import { SwpEvent } from '../types/SwpEvent';
|
||||||
|
|
||||||
interface DragState {
|
interface DragState {
|
||||||
eventId: string;
|
eventId: string;
|
||||||
|
|
@ -146,18 +147,19 @@ export class DragDropManager {
|
||||||
// Remove ghost
|
// Remove ghost
|
||||||
this.dragState.ghostElement.remove();
|
this.dragState.ghostElement.remove();
|
||||||
|
|
||||||
// Get column data (target = current column, source = where drag started)
|
// Get dateKey from target column
|
||||||
const dateKey = this.dragState.columnElement.dataset.date || '';
|
const dateKey = this.dragState.columnElement.dataset.date || '';
|
||||||
const resourceId = this.dragState.columnElement.dataset.resourceId;
|
|
||||||
|
// Create SwpEvent from element (reads top/height/eventId/resourceId from element)
|
||||||
|
const swpEvent = SwpEvent.fromElement(
|
||||||
|
this.dragState.element,
|
||||||
|
dateKey,
|
||||||
|
this.gridConfig
|
||||||
|
);
|
||||||
|
|
||||||
// Emit drag:end
|
// Emit drag:end
|
||||||
const payload: IDragEndPayload = {
|
const payload: IDragEndPayload = {
|
||||||
eventId: this.dragState.eventId,
|
swpEvent,
|
||||||
element: this.dragState.element,
|
|
||||||
snappedY,
|
|
||||||
columnElement: this.dragState.columnElement,
|
|
||||||
dateKey,
|
|
||||||
resourceId,
|
|
||||||
sourceDateKey: this.dragState.sourceDateKey,
|
sourceDateKey: this.dragState.sourceDateKey,
|
||||||
sourceResourceId: this.dragState.sourceResourceId,
|
sourceResourceId: this.dragState.sourceResourceId,
|
||||||
target: this.inHeader ? 'header' : 'grid'
|
target: this.inHeader ? 'header' : 'grid'
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,15 @@
|
||||||
|
|
||||||
import { ICalendarEvent, IEventBus, IEventUpdatedPayload } from '../types/CalendarTypes';
|
import { ICalendarEvent, IEventBus, IEventUpdatedPayload } from '../types/CalendarTypes';
|
||||||
import { EventService } from '../storage/events/EventService';
|
import { EventService } from '../storage/events/EventService';
|
||||||
import { IGridConfig } from '../core/IGridConfig';
|
|
||||||
import { DateService } from '../core/DateService';
|
import { DateService } from '../core/DateService';
|
||||||
import { CoreEvents } from '../constants/CoreEvents';
|
import { CoreEvents } from '../constants/CoreEvents';
|
||||||
import { IDragEndPayload } from '../types/DragTypes';
|
import { IDragEndPayload } from '../types/DragTypes';
|
||||||
import { IResizeEndPayload } from '../types/ResizeTypes';
|
import { IResizeEndPayload } from '../types/ResizeTypes';
|
||||||
import { pixelsToMinutes } from '../utils/PositionUtils';
|
|
||||||
|
|
||||||
export class EventPersistenceManager {
|
export class EventPersistenceManager {
|
||||||
constructor(
|
constructor(
|
||||||
private eventService: EventService,
|
private eventService: EventService,
|
||||||
private eventBus: IEventBus,
|
private eventBus: IEventBus,
|
||||||
private gridConfig: IGridConfig,
|
|
||||||
private dateService: DateService
|
private dateService: DateService
|
||||||
) {
|
) {
|
||||||
this.setupListeners();
|
this.setupListeners();
|
||||||
|
|
@ -34,44 +31,34 @@ export class EventPersistenceManager {
|
||||||
*/
|
*/
|
||||||
private handleDragEnd = async (e: Event): Promise<void> => {
|
private handleDragEnd = async (e: Event): Promise<void> => {
|
||||||
const payload = (e as CustomEvent<IDragEndPayload>).detail;
|
const payload = (e as CustomEvent<IDragEndPayload>).detail;
|
||||||
|
const { swpEvent } = payload;
|
||||||
|
|
||||||
// Get existing event
|
// Get existing event to merge with
|
||||||
const event = await this.eventService.get(payload.eventId);
|
const event = await this.eventService.get(swpEvent.eventId);
|
||||||
if (!event) {
|
if (!event) {
|
||||||
console.warn(`EventPersistenceManager: Event ${payload.eventId} not found`);
|
console.warn(`EventPersistenceManager: Event ${swpEvent.eventId} not found`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate new start time from snappedY
|
// Update and save - start/end already calculated in SwpEvent
|
||||||
const minutesFromDayStart = pixelsToMinutes(payload.snappedY, this.gridConfig);
|
|
||||||
const totalMinutes = (this.gridConfig.dayStartHour * 60) + minutesFromDayStart;
|
|
||||||
|
|
||||||
// Preserve duration
|
|
||||||
const durationMs = event.end.getTime() - event.start.getTime();
|
|
||||||
|
|
||||||
// Create new dates with correct day from dateKey
|
|
||||||
const newStart = new Date(payload.dateKey);
|
|
||||||
newStart.setHours(Math.floor(totalMinutes / 60), totalMinutes % 60, 0, 0);
|
|
||||||
const newEnd = new Date(newStart.getTime() + durationMs);
|
|
||||||
|
|
||||||
// Update and save
|
|
||||||
const updatedEvent: ICalendarEvent = {
|
const updatedEvent: ICalendarEvent = {
|
||||||
...event,
|
...event,
|
||||||
start: newStart,
|
start: swpEvent.start,
|
||||||
end: newEnd,
|
end: swpEvent.end,
|
||||||
resourceId: payload.resourceId ?? event.resourceId,
|
resourceId: swpEvent.resourceId ?? event.resourceId,
|
||||||
syncStatus: 'pending'
|
syncStatus: 'pending'
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.eventService.save(updatedEvent);
|
await this.eventService.save(updatedEvent);
|
||||||
|
|
||||||
// Emit EVENT_UPDATED for EventRenderer to re-render affected columns
|
// Emit EVENT_UPDATED for EventRenderer to re-render affected columns
|
||||||
|
const targetDateKey = this.dateService.getDateKey(swpEvent.start);
|
||||||
const updatePayload: IEventUpdatedPayload = {
|
const updatePayload: IEventUpdatedPayload = {
|
||||||
eventId: updatedEvent.id,
|
eventId: updatedEvent.id,
|
||||||
sourceDateKey: payload.sourceDateKey,
|
sourceDateKey: payload.sourceDateKey,
|
||||||
sourceResourceId: payload.sourceResourceId,
|
sourceResourceId: payload.sourceResourceId,
|
||||||
targetDateKey: payload.dateKey,
|
targetDateKey,
|
||||||
targetResourceId: payload.resourceId
|
targetResourceId: swpEvent.resourceId
|
||||||
};
|
};
|
||||||
this.eventBus.emit(CoreEvents.EVENT_UPDATED, updatePayload);
|
this.eventBus.emit(CoreEvents.EVENT_UPDATED, updatePayload);
|
||||||
};
|
};
|
||||||
|
|
@ -81,21 +68,19 @@ export class EventPersistenceManager {
|
||||||
*/
|
*/
|
||||||
private handleResizeEnd = async (e: Event): Promise<void> => {
|
private handleResizeEnd = async (e: Event): Promise<void> => {
|
||||||
const payload = (e as CustomEvent<IResizeEndPayload>).detail;
|
const payload = (e as CustomEvent<IResizeEndPayload>).detail;
|
||||||
|
const { swpEvent } = payload;
|
||||||
|
|
||||||
// Get existing event
|
// Get existing event to merge with
|
||||||
const event = await this.eventService.get(payload.eventId);
|
const event = await this.eventService.get(swpEvent.eventId);
|
||||||
if (!event) {
|
if (!event) {
|
||||||
console.warn(`EventPersistenceManager: Event ${payload.eventId} not found`);
|
console.warn(`EventPersistenceManager: Event ${swpEvent.eventId} not found`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate new end time
|
// Update and save - end already calculated in SwpEvent
|
||||||
const newEnd = new Date(event.start.getTime() + payload.newDurationMinutes * 60 * 1000);
|
|
||||||
|
|
||||||
// Update and save
|
|
||||||
const updatedEvent: ICalendarEvent = {
|
const updatedEvent: ICalendarEvent = {
|
||||||
...event,
|
...event,
|
||||||
end: newEnd,
|
end: swpEvent.end,
|
||||||
syncStatus: 'pending'
|
syncStatus: 'pending'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -103,13 +88,13 @@ export class EventPersistenceManager {
|
||||||
|
|
||||||
// Emit EVENT_UPDATED for EventRenderer to re-render the column
|
// Emit EVENT_UPDATED for EventRenderer to re-render the column
|
||||||
// Resize stays in same column, so source and target are the same
|
// Resize stays in same column, so source and target are the same
|
||||||
const dateKey = this.dateService.getDateKey(event.start);
|
const dateKey = this.dateService.getDateKey(swpEvent.start);
|
||||||
const updatePayload: IEventUpdatedPayload = {
|
const updatePayload: IEventUpdatedPayload = {
|
||||||
eventId: updatedEvent.id,
|
eventId: updatedEvent.id,
|
||||||
sourceDateKey: dateKey,
|
sourceDateKey: dateKey,
|
||||||
sourceResourceId: event.resourceId,
|
sourceResourceId: swpEvent.resourceId,
|
||||||
targetDateKey: dateKey,
|
targetDateKey: dateKey,
|
||||||
targetResourceId: event.resourceId
|
targetResourceId: swpEvent.resourceId
|
||||||
};
|
};
|
||||||
this.eventBus.emit(CoreEvents.EVENT_UPDATED, updatePayload);
|
this.eventBus.emit(CoreEvents.EVENT_UPDATED, updatePayload);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { pixelsToMinutes, minutesToPixels, snapToGrid } from '../utils/PositionU
|
||||||
import { DateService } from '../core/DateService';
|
import { DateService } from '../core/DateService';
|
||||||
import { CoreEvents } from '../constants/CoreEvents';
|
import { CoreEvents } from '../constants/CoreEvents';
|
||||||
import { IResizeStartPayload, IResizeEndPayload } from '../types/ResizeTypes';
|
import { IResizeStartPayload, IResizeEndPayload } from '../types/ResizeTypes';
|
||||||
|
import { SwpEvent } from '../types/SwpEvent';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ResizeManager - Handles resize of calendar events
|
* ResizeManager - Handles resize of calendar events
|
||||||
|
|
@ -250,15 +251,20 @@ export class ResizeManager {
|
||||||
// Remove global resizing class
|
// Remove global resizing class
|
||||||
document.documentElement.classList.remove('swp--resizing');
|
document.documentElement.classList.remove('swp--resizing');
|
||||||
|
|
||||||
// Emit resize end event
|
// Get dateKey from parent column
|
||||||
const newHeight = this.resizeState.currentHeight;
|
const column = this.resizeState.element.closest('swp-day-column') as HTMLElement;
|
||||||
const newDurationMinutes = pixelsToMinutes(newHeight, this.gridConfig);
|
const dateKey = column?.dataset.date || '';
|
||||||
|
|
||||||
|
// Create SwpEvent from element (reads top/height/eventId/resourceId from element)
|
||||||
|
const swpEvent = SwpEvent.fromElement(
|
||||||
|
this.resizeState.element,
|
||||||
|
dateKey,
|
||||||
|
this.gridConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
// Emit resize end event
|
||||||
this.eventBus.emit(CoreEvents.EVENT_RESIZE_END, {
|
this.eventBus.emit(CoreEvents.EVENT_RESIZE_END, {
|
||||||
eventId: this.resizeState.eventId,
|
swpEvent
|
||||||
element: this.resizeState.element,
|
|
||||||
newHeight,
|
|
||||||
newDurationMinutes
|
|
||||||
} as IResizeEndPayload);
|
} as IResizeEndPayload);
|
||||||
|
|
||||||
// Reset state
|
// Reset state
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
* DragTypes - Event payloads for drag-drop operations
|
* DragTypes - Event payloads for drag-drop operations
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { SwpEvent } from './SwpEvent';
|
||||||
|
|
||||||
export interface IMousePosition {
|
export interface IMousePosition {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
|
@ -24,12 +26,7 @@ export interface IDragMovePayload {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDragEndPayload {
|
export interface IDragEndPayload {
|
||||||
eventId: string;
|
swpEvent: SwpEvent; // Wrapper with element, start, end, eventId, resourceId
|
||||||
element: HTMLElement;
|
|
||||||
snappedY: number; // Final snapped position
|
|
||||||
columnElement: HTMLElement;
|
|
||||||
dateKey: string; // Target column date (from dataset)
|
|
||||||
resourceId?: string; // Target column resource (resource mode)
|
|
||||||
sourceDateKey: string; // Source column date (where drag started)
|
sourceDateKey: string; // Source column date (where drag started)
|
||||||
sourceResourceId?: string; // Source column resource (where drag started)
|
sourceResourceId?: string; // Source column resource (where drag started)
|
||||||
target: 'grid' | 'header'; // Where the event was dropped
|
target: 'grid' | 'header'; // Where the event was dropped
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
* ResizeTypes - Event payloads for resize operations
|
* ResizeTypes - Event payloads for resize operations
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { SwpEvent } from './SwpEvent';
|
||||||
|
|
||||||
export interface IResizeStartPayload {
|
export interface IResizeStartPayload {
|
||||||
eventId: string;
|
eventId: string;
|
||||||
element: HTMLElement;
|
element: HTMLElement;
|
||||||
|
|
@ -9,8 +11,5 @@ export interface IResizeStartPayload {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IResizeEndPayload {
|
export interface IResizeEndPayload {
|
||||||
eventId: string;
|
swpEvent: SwpEvent; // Wrapper with element, start, end, eventId, resourceId
|
||||||
element: HTMLElement;
|
|
||||||
newHeight: number;
|
|
||||||
newDurationMinutes: number;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
78
src/v2/types/SwpEvent.ts
Normal file
78
src/v2/types/SwpEvent.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { IGridConfig } from '../core/IGridConfig';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SwpEvent - Wrapper class for calendar event elements
|
||||||
|
*
|
||||||
|
* Encapsulates an HTMLElement and provides computed properties
|
||||||
|
* for start/end times based on element position and grid config.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* - All data (eventId, resourceId) is read from element.dataset
|
||||||
|
* - Position (top, height) is read from element.style
|
||||||
|
* - Factory method `fromElement()` calculates Date objects
|
||||||
|
*/
|
||||||
|
export class SwpEvent {
|
||||||
|
readonly element: HTMLElement;
|
||||||
|
private _start: Date;
|
||||||
|
private _end: Date;
|
||||||
|
|
||||||
|
constructor(element: HTMLElement, start: Date, end: Date) {
|
||||||
|
this.element = element;
|
||||||
|
this._start = start;
|
||||||
|
this._end = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Event ID from element.dataset.eventId */
|
||||||
|
get eventId(): string {
|
||||||
|
return this.element.dataset.eventId || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resource ID from element.dataset.resourceId */
|
||||||
|
get resourceId(): string | undefined {
|
||||||
|
return this.element.dataset.resourceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
get start(): Date {
|
||||||
|
return this._start;
|
||||||
|
}
|
||||||
|
|
||||||
|
get end(): Date {
|
||||||
|
return this._end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Duration in minutes */
|
||||||
|
get durationMinutes(): number {
|
||||||
|
return (this._end.getTime() - this._start.getTime()) / (1000 * 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Duration in milliseconds */
|
||||||
|
get durationMs(): number {
|
||||||
|
return this._end.getTime() - this._start.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory: Create SwpEvent from element + dateKey
|
||||||
|
* Reads top/height from element.style to calculate start/end
|
||||||
|
*/
|
||||||
|
static fromElement(
|
||||||
|
element: HTMLElement,
|
||||||
|
dateKey: string,
|
||||||
|
gridConfig: IGridConfig
|
||||||
|
): SwpEvent {
|
||||||
|
const topPixels = parseFloat(element.style.top) || 0;
|
||||||
|
const heightPixels = parseFloat(element.style.height) || 0;
|
||||||
|
|
||||||
|
// Calculate start from top position
|
||||||
|
const startMinutesFromGrid = (topPixels / gridConfig.hourHeight) * 60;
|
||||||
|
const totalMinutes = (gridConfig.dayStartHour * 60) + startMinutesFromGrid;
|
||||||
|
|
||||||
|
const start = new Date(dateKey);
|
||||||
|
start.setHours(Math.floor(totalMinutes / 60), totalMinutes % 60, 0, 0);
|
||||||
|
|
||||||
|
// Calculate end from height
|
||||||
|
const durationMinutes = (heightPixels / gridConfig.hourHeight) * 60;
|
||||||
|
const end = new Date(start.getTime() + durationMinutes * 60 * 1000);
|
||||||
|
|
||||||
|
return new SwpEvent(element, start, end);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue