import { IEventBus, ICalendarEvent } from '../../types/CalendarTypes'; import { IGridConfig } from '../../core/IGridConfig'; import { CoreEvents } from '../../constants/CoreEvents'; import { HeaderDrawerManager } from '../../core/HeaderDrawerManager'; import { EventService } from '../../storage/events/EventService'; import { DateService } from '../../core/DateService'; import { IDragEnterHeaderPayload, IDragMoveHeaderPayload, IDragLeaveHeaderPayload } from '../../types/DragTypes'; /** * HeaderDrawerRenderer - Handles rendering of items in the header drawer * * Listens to drag events from DragDropManager and creates/manages * swp-header-item elements in the header drawer. * * Uses subgrid for column alignment with parent swp-calendar-header. * Position items via gridArea for explicit row/column placement. */ export class HeaderDrawerRenderer { private currentItem: HTMLElement | null = null; private container: HTMLElement | null = null; private sourceElement: HTMLElement | null = null; private wasExpandedBeforeDrag = false; constructor( private eventBus: IEventBus, private gridConfig: IGridConfig, private headerDrawerManager: HeaderDrawerManager, private eventService: EventService, private dateService: DateService ) { this.setupListeners(); } /** * Render allDay events into the header drawer */ async render(container: HTMLElement, filter: Record): Promise { const drawer = container.querySelector('swp-header-drawer'); if (!drawer) return; const visibleDates = filter['date'] || []; if (visibleDates.length === 0) return; // Fetch events for date range const startDate = new Date(visibleDates[0]); const endDate = new Date(visibleDates[visibleDates.length - 1]); endDate.setHours(23, 59, 59, 999); const events = await this.eventService.getByDateRange(startDate, endDate); // Filter to allDay events only (allDay !== false) const allDayEvents = events.filter(event => event.allDay !== false); // Clear existing items drawer.innerHTML = ''; // Render each allDay event allDayEvents.forEach(event => { const item = this.createHeaderItem(event, visibleDates); if (item) drawer.appendChild(item); }); // Expand drawer if there are items if (allDayEvents.length > 0) { this.headerDrawerManager.expand(); } } /** * Create a header item element for an allDay event * Supports multi-day events with column span */ private createHeaderItem(event: ICalendarEvent, visibleDates: string[]): HTMLElement | null { const startDateKey = this.dateService.getDateKey(event.start); const endDateKey = this.dateService.getDateKey(event.end); const startColIndex = visibleDates.indexOf(startDateKey); const endColIndex = visibleDates.indexOf(endDateKey); // Event skal mindst starte eller slutte inden for synlige datoer if (startColIndex === -1 && endColIndex === -1) return null; const item = document.createElement('swp-header-item'); item.dataset.eventId = event.id; item.dataset.itemType = 'event'; item.dataset.start = event.start.toISOString(); item.dataset.end = event.end.toISOString(); item.textContent = event.title; // Color class const colorClass = this.getColorClass(event); if (colorClass) item.classList.add(colorClass); // Grid position (1-indexed, clamp til synlige kolonner) const col = Math.max(0, startColIndex) + 1; const endCol = (endColIndex !== -1 ? endColIndex : visibleDates.length - 1) + 2; item.style.gridArea = `1 / ${col} / 2 / ${endCol}`; return item; } /** * Get color class based on event metadata or type */ private getColorClass(event: ICalendarEvent): string { if (event.metadata?.color) { return `is-${event.metadata.color}`; } const typeColors: Record = { 'customer': 'is-blue', 'vacation': 'is-green', 'break': 'is-amber', 'meeting': 'is-purple', 'blocked': 'is-red' }; return typeColors[event.type] || 'is-blue'; } /** * Setup event listeners for drag events */ private setupListeners(): void { this.eventBus.on(CoreEvents.EVENT_DRAG_ENTER_HEADER, (e) => { const payload = (e as CustomEvent).detail; this.handleDragEnter(payload); }); this.eventBus.on(CoreEvents.EVENT_DRAG_MOVE_HEADER, (e) => { const payload = (e as CustomEvent).detail; this.handleDragMove(payload); }); this.eventBus.on(CoreEvents.EVENT_DRAG_LEAVE_HEADER, (e) => { const payload = (e as CustomEvent).detail; this.handleDragLeave(payload); }); this.eventBus.on(CoreEvents.EVENT_DRAG_END, () => { this.handleDragEnd(); }); this.eventBus.on(CoreEvents.EVENT_DRAG_CANCEL, () => { this.cleanup(); }); } /** * Handle drag entering header zone - create preview item */ private handleDragEnter(payload: IDragEnterHeaderPayload): void { this.container = document.querySelector('swp-header-drawer'); if (!this.container) return; // Remember if drawer was already expanded this.wasExpandedBeforeDrag = this.headerDrawerManager.isExpanded(); // Expand drawer with animation this.headerDrawerManager.expand(); // Store reference to source element this.sourceElement = payload.element; // Create header item const item = document.createElement('swp-header-item'); item.dataset.eventId = payload.eventId; item.dataset.itemType = payload.itemType; item.dataset.date = payload.sourceDate; item.dataset.duration = String(payload.duration); item.textContent = payload.title; // Apply color class if present if (payload.colorClass) { item.classList.add(payload.colorClass); } // Add dragging state item.classList.add('dragging'); // Initial placement (duration determines column span) // gridArea format: "row / col-start / row+1 / col-end" const col = payload.sourceColumnIndex + 1; const endCol = col + payload.duration; item.style.gridArea = `1 / ${col} / 2 / ${endCol}`; this.container.appendChild(item); this.currentItem = item; // Hide original element while in header payload.element.style.visibility = 'hidden'; } /** * Handle drag moving within header - update column position */ private handleDragMove(payload: IDragMoveHeaderPayload): void { if (!this.currentItem) return; // Update column position (duration=1 for now) const col = payload.columnIndex + 1; const duration = parseInt(this.currentItem.dataset.duration || '1', 10); const endCol = col + duration; this.currentItem.style.gridArea = `1 / ${col} / 2 / ${endCol}`; this.currentItem.dataset.date = payload.dateKey; } /** * Handle drag leaving header - remove preview and restore source */ private handleDragLeave(_payload: IDragLeaveHeaderPayload): void { this.cleanup(); } /** * Handle drag end - finalize the item (it stays in header) * Note: EventRenderer handles removing the original element from the grid * via EVENT_DRAG_END with target === 'header' */ private handleDragEnd(): void { if (!this.currentItem) return; // Remove dragging state this.currentItem.classList.remove('dragging'); // TODO: Emit event to persist allDay=true change // Clear references this.currentItem = null; this.sourceElement = null; } /** * Cleanup preview item and restore source visibility */ private cleanup(): void { // Remove preview item this.currentItem?.remove(); this.currentItem = null; // Restore source element visibility if (this.sourceElement) { this.sourceElement.style.visibility = ''; this.sourceElement = null; } // Collapse drawer if it wasn't expanded before drag if (!this.wasExpandedBeforeDrag) { this.headerDrawerManager.collapse(); } } }