Calendar/src/v2/features/headerdrawer/HeaderDrawerRenderer.ts

248 lines
7.7 KiB
TypeScript
Raw Normal View History

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<string, string[]>): Promise<void> {
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
*/
private createHeaderItem(event: ICalendarEvent, visibleDates: string[]): HTMLElement | null {
const dateKey = this.dateService.getDateKey(event.start);
const colIndex = visibleDates.indexOf(dateKey);
if (colIndex === -1) return null; // Event not in visible range
const item = document.createElement('swp-header-item');
item.dataset.eventId = event.id;
item.dataset.itemType = 'event';
item.dataset.date = dateKey;
item.textContent = event.title;
// Color class
const colorClass = this.getColorClass(event);
if (colorClass) item.classList.add(colorClass);
// Grid position (1-indexed)
const col = colIndex + 1;
item.style.gridArea = `1 / ${col} / 2 / ${col + 1}`;
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<string, string> = {
'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<IDragEnterHeaderPayload>).detail;
this.handleDragEnter(payload);
});
this.eventBus.on(CoreEvents.EVENT_DRAG_MOVE_HEADER, (e) => {
const payload = (e as CustomEvent<IDragMoveHeaderPayload>).detail;
this.handleDragMove(payload);
});
this.eventBus.on(CoreEvents.EVENT_DRAG_LEAVE_HEADER, (e) => {
const payload = (e as CustomEvent<IDragLeaveHeaderPayload>).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)
2025-12-11 21:16:40 +01:00
* 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
2025-12-11 21:16:40 +01:00
// 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();
}
}
}