Enhances the drag and drop experience for all-day events by expanding the header to display the all-day row when dragging an event over it. Introduces constants for all-day event layout.
351 lines
No EOL
14 KiB
TypeScript
351 lines
No EOL
14 KiB
TypeScript
// Event rendering strategy interface and implementations
|
|
|
|
import { CalendarEvent } from '../types/CalendarTypes';
|
|
import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig';
|
|
import { CalendarConfig } from '../core/CalendarConfig';
|
|
import { DateCalculator } from '../utils/DateCalculator';
|
|
|
|
/**
|
|
* Interface for event rendering strategies
|
|
*/
|
|
export interface EventRendererStrategy {
|
|
renderEvents(events: CalendarEvent[], container: HTMLElement, config: CalendarConfig): void;
|
|
clearEvents(container?: HTMLElement): void;
|
|
}
|
|
|
|
/**
|
|
* Base class for event renderers with common functionality
|
|
*/
|
|
export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|
protected dateCalculator: DateCalculator;
|
|
|
|
constructor(config: CalendarConfig) {
|
|
this.dateCalculator = new DateCalculator(config);
|
|
}
|
|
renderEvents(events: CalendarEvent[], container: HTMLElement, config: CalendarConfig): void {
|
|
console.log('BaseEventRenderer: renderEvents called with', events.length, 'events');
|
|
|
|
// NOTE: Removed clearEvents() to support sliding animation
|
|
// With sliding animation, multiple grid containers exist simultaneously
|
|
// clearEvents() would remove events from all containers, breaking the animation
|
|
// Events are now rendered directly into the new container without clearing
|
|
|
|
// Separate all-day events from regular events
|
|
const allDayEvents = events.filter(event => event.allDay);
|
|
const regularEvents = events.filter(event => !event.allDay);
|
|
|
|
console.log(`BaseEventRenderer: Rendering ${allDayEvents.length} all-day events and ${regularEvents.length} regular events`);
|
|
|
|
// Always call renderAllDayEvents to ensure height is set correctly (even to 0)
|
|
this.renderAllDayEvents(allDayEvents, container, config);
|
|
|
|
// Find columns in the specific container for regular events
|
|
const columns = this.getColumns(container);
|
|
console.log(`BaseEventRenderer: Found ${columns.length} columns in container`);
|
|
|
|
columns.forEach(column => {
|
|
const columnEvents = this.getEventsForColumn(column, regularEvents);
|
|
console.log(`BaseEventRenderer: Rendering ${columnEvents.length} regular events in column`);
|
|
|
|
const eventsLayer = column.querySelector('swp-events-layer');
|
|
if (eventsLayer) {
|
|
columnEvents.forEach(event => {
|
|
console.log(`BaseEventRenderer: Rendering event "${event.title}" in events layer`);
|
|
this.renderEvent(event, eventsLayer, config);
|
|
});
|
|
|
|
// Debug: Verify events were actually added
|
|
const renderedEvents = eventsLayer.querySelectorAll('swp-event');
|
|
console.log(`BaseEventRenderer: Events layer now has ${renderedEvents.length} events`);
|
|
} else {
|
|
console.warn('BaseEventRenderer: No events layer found in column');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Abstract methods that subclasses must implement
|
|
protected abstract getColumns(container: HTMLElement): HTMLElement[];
|
|
protected abstract getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[];
|
|
|
|
/**
|
|
* Render all-day events in the header row 2
|
|
*/
|
|
protected renderAllDayEvents(allDayEvents: CalendarEvent[], container: HTMLElement, config: CalendarConfig): void {
|
|
console.log(`BaseEventRenderer: Rendering ${allDayEvents.length} all-day events`);
|
|
|
|
// Find the calendar header
|
|
const calendarHeader = container.querySelector('swp-calendar-header');
|
|
if (!calendarHeader) {
|
|
console.warn('BaseEventRenderer: No calendar header found for all-day events');
|
|
return;
|
|
}
|
|
|
|
// Clear any existing all-day containers first
|
|
const existingContainers = calendarHeader.querySelectorAll('swp-allday-container');
|
|
existingContainers.forEach(container => container.remove());
|
|
|
|
// Track maximum number of stacked events to calculate row height
|
|
let maxStackHeight = 0;
|
|
|
|
// Get day headers to build date map
|
|
const dayHeaders = calendarHeader.querySelectorAll('swp-day-header');
|
|
const dateToColumnMap = new Map<string, number>();
|
|
const visibleDates: string[] = [];
|
|
|
|
dayHeaders.forEach((header, index) => {
|
|
const dateStr = (header as any).dataset.date;
|
|
if (dateStr) {
|
|
dateToColumnMap.set(dateStr, index + 1); // 1-based column index
|
|
visibleDates.push(dateStr);
|
|
}
|
|
});
|
|
|
|
// Group events by their start column for container creation
|
|
const eventsByStartColumn = new Map<number, CalendarEvent[]>();
|
|
|
|
allDayEvents.forEach(event => {
|
|
const startDate = new Date(event.start);
|
|
const startDateKey = this.dateCalculator.formatISODate(startDate);
|
|
const startColumn = dateToColumnMap.get(startDateKey);
|
|
|
|
if (!startColumn) {
|
|
console.log(`BaseEventRenderer: Event "${event.title}" starts outside visible week`);
|
|
return;
|
|
}
|
|
|
|
// Store event with its start column
|
|
if (!eventsByStartColumn.has(startColumn)) {
|
|
eventsByStartColumn.set(startColumn, []);
|
|
}
|
|
eventsByStartColumn.get(startColumn)!.push(event);
|
|
});
|
|
|
|
// Create containers and render events
|
|
eventsByStartColumn.forEach((events, startColumn) => {
|
|
events.forEach(event => {
|
|
const startDate = new Date(event.start);
|
|
const endDate = new Date(event.end);
|
|
|
|
// Calculate span
|
|
let endColumn = startColumn;
|
|
const currentDate = new Date(startDate);
|
|
|
|
while (currentDate <= endDate) {
|
|
currentDate.setDate(currentDate.getDate() + 1);
|
|
const dateKey = this.dateCalculator.formatISODate(currentDate);
|
|
const col = dateToColumnMap.get(dateKey);
|
|
if (col) {
|
|
endColumn = col;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
const columnSpan = endColumn - startColumn + 1;
|
|
|
|
// Create or find container for this column span
|
|
const containerKey = `${startColumn}-${columnSpan}`;
|
|
let allDayContainer = calendarHeader.querySelector(`swp-allday-container[data-container-key="${containerKey}"]`);
|
|
|
|
if (!allDayContainer) {
|
|
// Create container that spans the appropriate columns
|
|
allDayContainer = document.createElement('swp-allday-container');
|
|
allDayContainer.setAttribute('data-container-key', containerKey);
|
|
(allDayContainer as HTMLElement).style.gridColumn = columnSpan > 1
|
|
? `${startColumn} / span ${columnSpan}`
|
|
: `${startColumn}`;
|
|
(allDayContainer as HTMLElement).style.gridRow = '2';
|
|
calendarHeader.appendChild(allDayContainer);
|
|
}
|
|
|
|
// Create the all-day event element inside container
|
|
const allDayEvent = document.createElement('swp-allday-event');
|
|
allDayEvent.textContent = event.title;
|
|
allDayEvent.setAttribute('data-event-id', event.id);
|
|
allDayEvent.setAttribute('data-type', event.type || 'work');
|
|
|
|
// Use event metadata for color if available
|
|
if (event.metadata?.color) {
|
|
(allDayEvent as HTMLElement).style.backgroundColor = event.metadata.color;
|
|
}
|
|
|
|
console.log(`BaseEventRenderer: All-day event "${event.title}" in container spanning columns ${startColumn} to ${endColumn}`);
|
|
|
|
allDayContainer.appendChild(allDayEvent);
|
|
|
|
// Track max stack height
|
|
const containerEventCount = allDayContainer.querySelectorAll('swp-allday-event').length;
|
|
if (containerEventCount > maxStackHeight) {
|
|
maxStackHeight = containerEventCount;
|
|
}
|
|
});
|
|
});
|
|
|
|
// Calculate and set the all-day row height based on max stack
|
|
const calculatedHeight = maxStackHeight > 0
|
|
? (maxStackHeight * ALL_DAY_CONSTANTS.EVENT_HEIGHT) + ((maxStackHeight - 1) * ALL_DAY_CONSTANTS.EVENT_GAP) + ALL_DAY_CONSTANTS.CONTAINER_PADDING
|
|
: 0; // No height if no events
|
|
|
|
// Set CSS variable for row height
|
|
const root = document.documentElement;
|
|
root.style.setProperty('--all-day-row-height', `${calculatedHeight}px`);
|
|
|
|
// Also update header-spacer height
|
|
const headerSpacer = container.querySelector('swp-header-spacer');
|
|
if (headerSpacer) {
|
|
const headerHeight = parseInt(getComputedStyle(root).getPropertyValue('--header-height') || '80');
|
|
(headerSpacer as HTMLElement).style.height = `${headerHeight + calculatedHeight}px`;
|
|
}
|
|
|
|
console.log(`BaseEventRenderer: Set all-day row height to ${calculatedHeight}px (max stack: ${maxStackHeight})`);
|
|
}
|
|
|
|
protected renderEvent(event: CalendarEvent, container: Element, config: CalendarConfig): void {
|
|
const eventElement = document.createElement('swp-event');
|
|
eventElement.dataset.eventId = event.id;
|
|
eventElement.dataset.type = event.type;
|
|
|
|
// Calculate position based on time
|
|
const position = this.calculateEventPosition(event, config);
|
|
eventElement.style.position = 'absolute';
|
|
eventElement.style.top = `${position.top + 1}px`;
|
|
eventElement.style.height = `${position.height - 3}px`; //adjusted so bottom does not cover horizontal time lines.
|
|
|
|
// Color is now handled by CSS classes based on data-type attribute
|
|
|
|
// Format time for display
|
|
const startTime = this.dateCalculator.formatTime(new Date(event.start));
|
|
const endTime = this.dateCalculator.formatTime(new Date(event.end));
|
|
|
|
// Create event content
|
|
eventElement.innerHTML = `
|
|
<swp-event-time>${startTime} - ${endTime}</swp-event-time>
|
|
<swp-event-title>${event.title}</swp-event-title>
|
|
`;
|
|
|
|
container.appendChild(eventElement);
|
|
|
|
console.log(`BaseEventRenderer: Created event element for "${event.title}":`, {
|
|
top: eventElement.style.top,
|
|
height: eventElement.style.height,
|
|
dataType: eventElement.dataset.type,
|
|
position: eventElement.style.position,
|
|
innerHTML: eventElement.innerHTML
|
|
});
|
|
}
|
|
|
|
protected calculateEventPosition(event: CalendarEvent, config: CalendarConfig): { top: number; height: number } {
|
|
const startDate = new Date(event.start);
|
|
const endDate = new Date(event.end);
|
|
|
|
const gridSettings = config.getGridSettings();
|
|
const dayStartHour = gridSettings.dayStartHour;
|
|
const hourHeight = gridSettings.hourHeight;
|
|
|
|
// Calculate minutes from visible day start
|
|
const startMinutes = startDate.getHours() * 60 + startDate.getMinutes();
|
|
const endMinutes = endDate.getHours() * 60 + endDate.getMinutes();
|
|
const dayStartMinutes = dayStartHour * 60;
|
|
|
|
// Calculate top position (subtract day start to align with time axis)
|
|
const top = ((startMinutes - dayStartMinutes) / 60) * hourHeight;
|
|
|
|
// Calculate height
|
|
const durationMinutes = endMinutes - startMinutes;
|
|
const height = (durationMinutes / 60) * hourHeight;
|
|
|
|
console.log('Event positioning calculation:', {
|
|
eventTime: `${startDate.getHours()}:${startDate.getMinutes()}`,
|
|
startMinutes,
|
|
endMinutes,
|
|
dayStartMinutes,
|
|
dayStartHour,
|
|
hourHeight,
|
|
top,
|
|
height
|
|
});
|
|
|
|
return { top, height };
|
|
}
|
|
|
|
protected formatTime(isoString: string): string {
|
|
const date = new Date(isoString);
|
|
const hours = date.getHours();
|
|
const minutes = date.getMinutes();
|
|
const period = hours >= 12 ? 'PM' : 'AM';
|
|
const displayHour = hours > 12 ? hours - 12 : (hours === 0 ? 12 : hours);
|
|
|
|
return `${displayHour}:${minutes.toString().padStart(2, '0')} ${period}`;
|
|
}
|
|
|
|
clearEvents(container?: HTMLElement): void {
|
|
const selector = 'swp-event';
|
|
const existingEvents = container
|
|
? container.querySelectorAll(selector)
|
|
: document.querySelectorAll(selector);
|
|
|
|
if (existingEvents.length > 0) {
|
|
console.log(`BaseEventRenderer: Clearing ${existingEvents.length} events`,
|
|
container ? 'from container' : 'globally');
|
|
}
|
|
existingEvents.forEach(event => event.remove());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Date-based event renderer
|
|
*/
|
|
export class DateEventRenderer extends BaseEventRenderer {
|
|
protected getColumns(container: HTMLElement): HTMLElement[] {
|
|
const columns = container.querySelectorAll('swp-day-column');
|
|
console.log('DateEventRenderer: Found', columns.length, 'day columns in container');
|
|
return Array.from(columns) as HTMLElement[];
|
|
}
|
|
|
|
protected getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[] {
|
|
const columnDate = column.dataset.date;
|
|
if (!columnDate) {
|
|
console.log(`DateEventRenderer: Column has no dataset.date`);
|
|
return [];
|
|
}
|
|
|
|
const columnEvents = events.filter(event => {
|
|
const eventDate = new Date(event.start);
|
|
const eventDateStr = this.dateCalculator.formatISODate(eventDate);
|
|
const matches = eventDateStr === columnDate;
|
|
|
|
if (!matches) {
|
|
if(event.title == 'Architecture Planning')
|
|
console.log(`DateEventRenderer: Event ${event.title} (${eventDateStr}) does not match column (${columnDate})`);
|
|
}
|
|
|
|
return matches;
|
|
});
|
|
|
|
console.log(`DateEventRenderer: Column ${columnDate} has ${columnEvents.length} events from ${events.length} total`);
|
|
return columnEvents;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resource-based event renderer
|
|
*/
|
|
export class ResourceEventRenderer extends BaseEventRenderer {
|
|
protected getColumns(container: HTMLElement): HTMLElement[] {
|
|
const columns = container.querySelectorAll('swp-resource-column');
|
|
console.log('ResourceEventRenderer: Found', columns.length, 'resource columns in container');
|
|
return Array.from(columns) as HTMLElement[];
|
|
}
|
|
|
|
protected getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[] {
|
|
const resourceName = column.dataset.resource;
|
|
if (!resourceName) return [];
|
|
|
|
const columnEvents = events.filter(event => {
|
|
return event.resource?.name === resourceName;
|
|
});
|
|
|
|
console.log(`ResourceEventRenderer: Resource ${resourceName} has ${columnEvents.length} events`);
|
|
return columnEvents;
|
|
}
|
|
} |