Improves all-day event layout calculation
Refactors all-day event rendering to use a layout engine for overlap detection and positioning, ensuring events are placed in available rows and columns. Removes deprecated method and adds unit tests.
This commit is contained in:
parent
274753936e
commit
a624394ffb
11 changed files with 2898 additions and 145 deletions
|
|
@ -3,6 +3,7 @@
|
|||
import { eventBus } from '../core/EventBus';
|
||||
import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig';
|
||||
import { AllDayEventRenderer } from '../renderers/AllDayEventRenderer';
|
||||
import { AllDayLayoutEngine } from '../utils/AllDayLayoutEngine';
|
||||
import { CalendarEvent } from '../types/CalendarTypes';
|
||||
import {
|
||||
DragMouseEnterHeaderEventPayload,
|
||||
|
|
@ -14,10 +15,11 @@ import { DragOffset, MousePosition } from '../types/DragDropTypes';
|
|||
|
||||
/**
|
||||
* AllDayManager - Handles all-day row height animations and management
|
||||
* Separated from HeaderManager for clean responsibility separation
|
||||
* Uses AllDayLayoutEngine for all overlap detection and layout calculation
|
||||
*/
|
||||
export class AllDayManager {
|
||||
private allDayEventRenderer: AllDayEventRenderer;
|
||||
private layoutEngine: AllDayLayoutEngine | null = null;
|
||||
|
||||
constructor() {
|
||||
this.allDayEventRenderer = new AllDayEventRenderer();
|
||||
|
|
@ -28,8 +30,6 @@ export class AllDayManager {
|
|||
* Setup event listeners for drag conversions
|
||||
*/
|
||||
private setupEventListeners(): void {
|
||||
|
||||
|
||||
eventBus.on('drag:mouseenter-header', (event) => {
|
||||
const { targetDate, mousePosition, originalElement, cloneElement } = (event as CustomEvent<DragMouseEnterHeaderEventPayload>).detail;
|
||||
|
||||
|
|
@ -58,7 +58,6 @@ export class AllDayManager {
|
|||
}
|
||||
|
||||
this.checkAndAnimateAllDayHeight();
|
||||
|
||||
});
|
||||
|
||||
// Listen for drag operations on all-day events
|
||||
|
|
@ -101,7 +100,6 @@ export class AllDayManager {
|
|||
});
|
||||
const dragClone = document.querySelector(`swp-allday-container swp-event[data-event-id="clone-${eventId}"]`);
|
||||
|
||||
|
||||
console.log('🎯 AllDayManager: Ending drag for all-day event', { eventId });
|
||||
this.handleDragEnd(draggedElement, dragClone as HTMLElement, { column: finalPosition.column || '', y: 0 });
|
||||
});
|
||||
|
|
@ -126,7 +124,6 @@ export class AllDayManager {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
private getAllDayContainer(): HTMLElement | null {
|
||||
return document.querySelector('swp-calendar-header swp-allday-container');
|
||||
}
|
||||
|
|
@ -149,7 +146,9 @@ export class AllDayManager {
|
|||
} {
|
||||
const root = document.documentElement;
|
||||
const targetHeight = targetRows * ALL_DAY_CONSTANTS.SINGLE_ROW_HEIGHT;
|
||||
const currentHeight = parseInt(getComputedStyle(root).getPropertyValue('--all-day-row-height') || '0');
|
||||
// Read CSS variable directly from style property or default to 0
|
||||
const currentHeightStr = root.style.getPropertyValue('--all-day-row-height') || '0px';
|
||||
const currentHeight = parseInt(currentHeightStr) || 0;
|
||||
const heightDifference = targetHeight - currentHeight;
|
||||
|
||||
return { targetHeight, currentHeight, heightDifference };
|
||||
|
|
@ -182,7 +181,7 @@ export class AllDayManager {
|
|||
let highestRow = 0;
|
||||
|
||||
(Array.from(allDayEvents) as HTMLElement[]).forEach((event: HTMLElement) => {
|
||||
const gridRow = parseInt(getComputedStyle(event).gridRowStart) || 1;
|
||||
const gridRow = parseInt(event.style.gridRow) || 1;
|
||||
highestRow = Math.max(highestRow, gridRow);
|
||||
});
|
||||
|
||||
|
|
@ -235,8 +234,10 @@ export class AllDayManager {
|
|||
// Add spacer animation if spacer exists, but don't use fill: 'forwards'
|
||||
if (headerSpacer) {
|
||||
const root = document.documentElement;
|
||||
const currentSpacerHeight = parseInt(getComputedStyle(root).getPropertyValue('--header-height')) + currentHeight;
|
||||
const targetSpacerHeight = parseInt(getComputedStyle(root).getPropertyValue('--header-height')) + targetHeight;
|
||||
const headerHeightStr = root.style.getPropertyValue('--header-height');
|
||||
const headerHeight = parseInt(headerHeightStr);
|
||||
const currentSpacerHeight = headerHeight + currentHeight;
|
||||
const targetSpacerHeight = headerHeight + targetHeight;
|
||||
|
||||
animations.push(
|
||||
headerSpacer.animate([
|
||||
|
|
@ -258,11 +259,64 @@ export class AllDayManager {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate layout for ALL all-day events using AllDayLayoutEngine
|
||||
* This is the correct method that processes all events together for proper overlap detection
|
||||
*/
|
||||
public calculateAllDayEventsLayout(events: CalendarEvent[], weekDates: string[]): Map<string, {
|
||||
startColumn: number;
|
||||
endColumn: number;
|
||||
row: number;
|
||||
columnSpan: number;
|
||||
gridArea: string;
|
||||
}> {
|
||||
console.log('🔍 AllDayManager: calculateAllDayEventsLayout - Processing all events together', {
|
||||
eventCount: events.length,
|
||||
events: events.map(e => ({ id: e.id, title: e.title, start: e.start.toISOString().split('T')[0], end: e.end.toISOString().split('T')[0] })),
|
||||
weekDates
|
||||
});
|
||||
|
||||
// Initialize layout engine with provided week dates
|
||||
this.layoutEngine = new AllDayLayoutEngine(weekDates);
|
||||
|
||||
// Calculate layout for all events together - AllDayLayoutEngine handles CalendarEvents directly
|
||||
const layouts = this.layoutEngine.calculateLayout(events);
|
||||
|
||||
// Convert to expected return format
|
||||
const result = new Map<string, {
|
||||
startColumn: number;
|
||||
endColumn: number;
|
||||
row: number;
|
||||
columnSpan: number;
|
||||
gridArea: string;
|
||||
}>();
|
||||
|
||||
layouts.forEach((layout, eventId) => {
|
||||
result.set(eventId, {
|
||||
startColumn: layout.startColumn,
|
||||
endColumn: layout.endColumn,
|
||||
row: layout.row,
|
||||
columnSpan: layout.columnSpan,
|
||||
gridArea: layout.gridArea
|
||||
});
|
||||
|
||||
console.log('✅ AllDayManager: Calculated layout for event', {
|
||||
eventId,
|
||||
title: events.find(e => e.id === eventId)?.title,
|
||||
gridArea: layout.gridArea,
|
||||
layout: layout
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle conversion of timed event to all-day event using CSS styling
|
||||
*/
|
||||
private handleConvertToAllDay(targetDate: string, cloneElement: HTMLElement): void {
|
||||
console.log('🔄 AllDayManager: Converting to all-day using CSS approach', {
|
||||
console.log('🔄 AllDayManager: Converting to all-day using AllDayLayoutEngine', {
|
||||
eventId: cloneElement.dataset.eventId,
|
||||
targetDate
|
||||
});
|
||||
|
|
@ -282,72 +336,52 @@ export class AllDayManager {
|
|||
}
|
||||
}
|
||||
|
||||
// Calculate position BEFORE adding to container (to avoid counting clone as existing event)
|
||||
const columnIndex = this.getColumnIndexForDate(targetDate);
|
||||
const availableRow = this.findAvailableRow(targetDate);
|
||||
// Create mock event for layout calculation
|
||||
const mockEvent: CalendarEvent = {
|
||||
id: cloneElement.dataset.eventId || '',
|
||||
title: cloneElement.dataset.title || '',
|
||||
start: new Date(targetDate),
|
||||
end: new Date(targetDate),
|
||||
type: 'work',
|
||||
allDay: true,
|
||||
syncStatus: 'synced'
|
||||
};
|
||||
|
||||
// Get existing all-day events from EventManager
|
||||
const existingEvents = this.getExistingAllDayEvents();
|
||||
|
||||
// Add the new drag event to the array
|
||||
const allEvents = [...existingEvents, mockEvent];
|
||||
|
||||
// Get actual visible dates from DOM headers (same as EventRendererManager does)
|
||||
const weekDates = this.getVisibleDatesFromDOM();
|
||||
|
||||
// Calculate layout for all events including the new one
|
||||
const layouts = this.calculateAllDayEventsLayout(allEvents, weekDates);
|
||||
const layout = layouts.get(mockEvent.id);
|
||||
|
||||
if (!layout) {
|
||||
console.error('AllDayManager: No layout found for drag event', mockEvent.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set all properties BEFORE adding to DOM
|
||||
cloneElement.classList.add('all-day-style');
|
||||
cloneElement.style.gridColumn = columnIndex.toString();
|
||||
cloneElement.style.gridRow = availableRow.toString();
|
||||
cloneElement.style.gridColumn = layout.startColumn.toString();
|
||||
cloneElement.style.gridRow = layout.row.toString();
|
||||
cloneElement.dataset.allDayDate = targetDate;
|
||||
cloneElement.style.display = '';
|
||||
|
||||
// NOW add to container (after all positioning is calculated)
|
||||
allDayContainer.appendChild(cloneElement);
|
||||
|
||||
console.log('✅ AllDayManager: Converted to all-day style', {
|
||||
console.log('✅ AllDayManager: Converted to all-day style using AllDayLayoutEngine', {
|
||||
eventId: cloneElement.dataset.eventId,
|
||||
gridColumn: columnIndex,
|
||||
gridRow: availableRow
|
||||
gridColumn: layout.startColumn,
|
||||
gridRow: layout.row
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get column index for a specific date
|
||||
*/
|
||||
private getColumnIndexForDate(targetDate: string): number {
|
||||
const dayHeaders = document.querySelectorAll('swp-day-header');
|
||||
let columnIndex = 1;
|
||||
dayHeaders.forEach((header, index) => {
|
||||
if ((header as HTMLElement).dataset.date === targetDate) {
|
||||
columnIndex = index + 1;
|
||||
}
|
||||
});
|
||||
return columnIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find available row for all-day event in specific date column
|
||||
*/
|
||||
private findAvailableRow(targetDate: string): number {
|
||||
const container = this.getAllDayContainer();
|
||||
if (!container) return 1;
|
||||
|
||||
const columnIndex = this.getColumnIndexForDate(targetDate);
|
||||
const existingEvents = container.querySelectorAll('swp-event');
|
||||
const occupiedRows = new Set<number>();
|
||||
|
||||
existingEvents.forEach(event => {
|
||||
const style = getComputedStyle(event);
|
||||
const eventStartCol = parseInt(style.gridColumnStart);
|
||||
const eventRow = parseInt(style.gridRowStart) || 1;
|
||||
|
||||
// Only check events in the same column
|
||||
if (eventStartCol === columnIndex) {
|
||||
occupiedRows.add(eventRow);
|
||||
}
|
||||
});
|
||||
|
||||
// Find first available row
|
||||
let targetRow = 1;
|
||||
while (occupiedRows.has(targetRow)) {
|
||||
targetRow++;
|
||||
}
|
||||
|
||||
return targetRow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle conversion from all-day back to timed event
|
||||
*/
|
||||
|
|
@ -366,9 +400,6 @@ export class AllDayManager {
|
|||
// Remove all-day date attribute
|
||||
delete cloneElement.dataset.allDayDate;
|
||||
|
||||
// Move back to appropriate day column (will be handled by drag logic)
|
||||
// The drag system will position it correctly
|
||||
|
||||
console.log('✅ AllDayManager: Converted from all-day back to timed');
|
||||
}
|
||||
|
||||
|
|
@ -436,7 +467,6 @@ export class AllDayManager {
|
|||
* Handle drag end for all-day events
|
||||
*/
|
||||
private handleDragEnd(originalElement: HTMLElement, dragClone: HTMLElement, finalPosition: { column: string; y: number }): void {
|
||||
|
||||
// Normalize clone
|
||||
const cloneId = dragClone.dataset.eventId;
|
||||
if (cloneId?.startsWith('clone-')) {
|
||||
|
|
@ -449,11 +479,59 @@ export class AllDayManager {
|
|||
dragClone.style.cursor = '';
|
||||
dragClone.style.opacity = '';
|
||||
|
||||
|
||||
|
||||
console.log('✅ AllDayManager: Completed drag operation for all-day event', {
|
||||
eventId: dragClone.dataset.eventId,
|
||||
finalColumn: dragClone.style.gridColumn
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get existing all-day events from DOM
|
||||
* Since we don't have direct access to EventManager, we'll get events from the current DOM
|
||||
*/
|
||||
private getExistingAllDayEvents(): CalendarEvent[] {
|
||||
const allDayContainer = this.getAllDayContainer();
|
||||
if (!allDayContainer) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const existingElements = allDayContainer.querySelectorAll('swp-event');
|
||||
const events: CalendarEvent[] = [];
|
||||
|
||||
existingElements.forEach(element => {
|
||||
const htmlElement = element as HTMLElement;
|
||||
const eventId = htmlElement.dataset.eventId;
|
||||
const title = htmlElement.dataset.title || htmlElement.textContent || '';
|
||||
const allDayDate = htmlElement.dataset.allDayDate;
|
||||
|
||||
if (eventId && allDayDate) {
|
||||
events.push({
|
||||
id: eventId,
|
||||
title: title,
|
||||
start: new Date(allDayDate),
|
||||
end: new Date(allDayDate),
|
||||
type: 'work',
|
||||
allDay: true,
|
||||
syncStatus: 'synced'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
private getVisibleDatesFromDOM(): string[] {
|
||||
const dayHeaders = document.querySelectorAll('swp-calendar-header swp-day-header');
|
||||
const weekDates: string[] = [];
|
||||
|
||||
dayHeaders.forEach(header => {
|
||||
const dateAttr = header.getAttribute('data-date');
|
||||
if (dateAttr) {
|
||||
weekDates.push(dateAttr);
|
||||
}
|
||||
});
|
||||
|
||||
return weekDates;
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue