Refactors all-day event layout calculation

Simplifies all-day event rendering by streamlining the layout
calculation and event placement process, using the AllDayLayoutEngine
to determine the grid positions. This removes deprecated methods
and improves overall code clarity.
This commit is contained in:
Janus C. H. Knudsen 2025-09-27 15:01:22 +02:00
parent 9dfd4574d8
commit 4141bffca4
7 changed files with 76 additions and 321 deletions

View file

@ -2,6 +2,7 @@ import { CalendarEvent } from '../types/CalendarTypes';
import { calendarConfig } from '../core/CalendarConfig';
import { TimeFormatter } from '../utils/TimeFormatter';
import { PositionUtils } from '../utils/PositionUtils';
import { EventLayout } from '../utils/AllDayLayoutEngine';
/**
* Abstract base class for event DOM elements
@ -228,14 +229,12 @@ export class SwpEventElement extends BaseEventElement {
* All-day event element (now using unified swp-event tag)
*/
export class SwpAllDayEventElement extends BaseEventElement {
private columnIndex: number;
private constructor(event: CalendarEvent, columnIndex: number) {
constructor(event: CalendarEvent) {
super(event);
this.columnIndex = columnIndex;
this.setAllDayAttributes();
this.createInnerStructure();
this.applyGridPositioning();
// this.applyGridPositioning();
}
protected createElement(): HTMLElement {
@ -264,128 +263,9 @@ export class SwpAllDayEventElement extends BaseEventElement {
/**
* Apply CSS grid positioning
*/
private applyGridPositioning(): void {
this.element.style.gridColumn = this.columnIndex.toString();
}
/**
* Set grid row for this all-day event
*/
public setGridRow(row: number): void {
this.element.style.gridRow = row.toString();
}
/**
* Set grid column span for this all-day event
*/
public setColumnSpan(startColumn: number, endColumn: number): void {
this.element.style.gridColumn = `${startColumn} / ${endColumn + 1}`;
}
/**
* Factory method to create from CalendarEvent and layout (provided by AllDayManager)
*/
public static fromCalendarEventWithLayout(
event: CalendarEvent,
layout: { startColumn: number; endColumn: number; row: number; columnSpan: number }
): SwpAllDayEventElement {
// Create element with provided layout
const element = new SwpAllDayEventElement(event, layout.startColumn);
// Set complete grid-area instead of individual properties
public applyGridPositioning(layout: EventLayout): void {
const gridArea = `${layout.row} / ${layout.startColumn} / ${layout.row + 1} / ${layout.endColumn + 1}`;
element.element.style.gridArea = gridArea;
console.log('✅ SwpAllDayEventElement: Created all-day event with AllDayLayoutEngine', {
eventId: event.id,
title: event.title,
gridArea: gridArea,
layout: layout
});
return element;
this.element.style.gridArea = gridArea;
}
/**
* Factory method to create from CalendarEvent and target date (DEPRECATED - use AllDayManager.calculateAllDayEventLayout)
* @deprecated Use AllDayManager.calculateAllDayEventLayout() and fromCalendarEventWithLayout() instead
*/
public static fromCalendarEvent(event: CalendarEvent, targetDate?: string): SwpAllDayEventElement {
console.warn('⚠️ SwpAllDayEventElement.fromCalendarEvent is deprecated. Use AllDayManager.calculateAllDayEventLayout() instead.');
// Fallback to simple column calculation without overlap detection
const { startColumn, endColumn } = this.calculateColumnSpan(event);
const finalStartColumn = targetDate ? this.getColumnIndexForDate(targetDate) : startColumn;
const finalEndColumn = targetDate ? finalStartColumn : endColumn;
// Create element with row 1 (no overlap detection)
const element = new SwpAllDayEventElement(event, finalStartColumn);
element.setGridRow(1);
element.setColumnSpan(finalStartColumn, finalEndColumn);
return element;
}
/**
* Calculate column span based on event start and end dates
*/
private static calculateColumnSpan(event: CalendarEvent): { startColumn: number; endColumn: number; columnSpan: number } {
const dayHeaders = document.querySelectorAll('swp-day-header');
// Extract dates from headers
const headerDates: string[] = [];
dayHeaders.forEach(header => {
const date = (header as HTMLElement).dataset.date;
if (date) {
headerDates.push(date);
}
});
// Format event dates for comparison (YYYY-MM-DD format)
const eventStartDate = event.start.toISOString().split('T')[0];
const eventEndDate = event.end.toISOString().split('T')[0];
// Find start and end column indices
let startColumn = 1;
let endColumn = headerDates.length;
headerDates.forEach((dateStr, index) => {
if (dateStr === eventStartDate) {
startColumn = index + 1;
}
if (dateStr === eventEndDate) {
endColumn = index + 1;
}
});
// Ensure end column is at least start column
if (endColumn < startColumn) {
endColumn = startColumn;
}
const columnSpan = endColumn - startColumn + 1;
return { startColumn, endColumn, columnSpan };
}
/**
* Get column index for a specific date
*/
private static 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;
}
/**
* Check if two column ranges overlap
*/
private static columnsOverlap(startA: number, endA: number, startB: number, endB: number): boolean {
return !(endA < startB || endB < startA);
}
}

View file

@ -3,7 +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 { AllDayLayoutEngine, EventLayout } from '../utils/AllDayLayoutEngine';
import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
import { CalendarEvent } from '../types/CalendarTypes';
import {
@ -96,7 +96,7 @@ export class AllDayManager {
});
eventBus.on('drag:end', (event) => {
const { draggedElement, mousePosition, finalPosition, target } = (event as CustomEvent<DragEndEventPayload>).detail;
const { draggedElement, mousePosition, finalPosition, target, draggedClone } = (event as CustomEvent<DragEndEventPayload>).detail;
if (target != 'swp-day-header') // we are not inside the swp-day-header, so just ignore.
return;
@ -106,10 +106,9 @@ export class AllDayManager {
eventId: eventId,
finalPosition
});
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 });
this.handleDragEnd(draggedElement, draggedClone as HTMLElement, { column: finalPosition.column || '', y: 0 });
});
// Listen for drag cancellation to recalculate height
@ -307,18 +306,7 @@ 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
});
public calculateAllDayEventsLayout(events: CalendarEvent[], weekDates: string[]): EventLayout[] {
// Store current state
this.currentAllDayEvents = events;
@ -328,35 +316,8 @@ export class AllDayManager {
this.layoutEngine = new AllDayLayoutEngine(weekDates);
// Calculate layout for all events together - AllDayLayoutEngine handles CalendarEvents directly
const layouts = this.layoutEngine.calculateLayout(events);
return 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;
}
@ -494,19 +455,14 @@ export class AllDayManager {
// 5. Apply differential updates - only update events that changed
let changedCount = 0;
newLayouts.forEach((layout, eventId) => {
const oldGridArea = this.currentLayouts.get(eventId);
newLayouts.forEach((layout) => {
const oldGridArea = this.currentLayouts.get(layout.calenderEvent.id);
const newGridArea = layout.gridArea;
if (oldGridArea !== newGridArea) {
changedCount++;
const element = document.querySelector(`[data-event-id="${eventId}"]`) as HTMLElement;
const element = document.querySelector(`[data-event-id="${layout.calenderEvent.id}"]`) as HTMLElement;
if (element) {
console.log('🔄 AllDayManager: Updating event position', {
eventId,
oldGridArea,
newGridArea
});
// Add transition class for smooth animation
element.classList.add('transitioning');
@ -532,61 +488,6 @@ export class AllDayManager {
// 8. Check if height adjustment is needed
this.checkAndAnimateAllDayHeight();
console.log('✅ AllDayManager: Completed differential drag end', {
eventId: droppedEventId,
totalEvents: newLayouts.size,
changedEvents: changedCount,
finalGridArea: newLayouts.get(droppedEventId)?.gridArea
});
}
/**
* 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;
}
}

View file

@ -132,7 +132,10 @@ export class DragDropManager {
}
private handleMouseDown(event: MouseEvent): void {
this.isDragStarted = false;
// Clean up drag state first
this.cleanupDragState();
this.lastMousePosition = { x: event.clientX, y: event.clientY };
this.lastLoggedPosition = { x: event.clientX, y: event.clientY };
this.initialMousePosition = { x: event.clientX, y: event.clientY };
@ -274,11 +277,10 @@ export class DragDropManager {
if (this.draggedElement) {
// Store variables locally before cleanup
const draggedElement = this.draggedElement;
//const draggedElement = this.draggedElement;
const isDragStarted = this.isDragStarted;
// Clean up drag state first
this.cleanupDragState();
// Only emit drag:end if drag was actually started
@ -292,7 +294,7 @@ export class DragDropManager {
const dropTarget = this.detectDropTarget(mousePosition);
console.log('🎯 DragDropManager: Emitting drag:end', {
draggedElement: draggedElement.dataset.eventId,
draggedElement: this.draggedElement.dataset.eventId,
finalColumn: positionData.column,
finalY: positionData.snappedY,
dropTarget: dropTarget,
@ -300,19 +302,20 @@ export class DragDropManager {
});
const dragEndPayload: DragEndEventPayload = {
draggedElement: draggedElement,
draggedElement: this.draggedElement,
draggedClone : this.draggedClone,
mousePosition,
finalPosition: positionData,
target: dropTarget
};
this.eventBus.emit('drag:end', dragEndPayload);
draggedElement.remove();
this.draggedElement.remove(); // TODO: this should be changed into a subscriber which only after a succesful placement is fired, not just mouseup as this can remove elements that are not placed.
} else {
// This was just a click - emit click event instead
this.eventBus.emit('event:click', {
draggedElement: draggedElement,
draggedElement: this.draggedElement,
mousePosition: { x: event.clientX, y: event.clientY }
});
}
@ -540,13 +543,11 @@ export class DragDropManager {
* Detect drop target - whether dropped in swp-day-column or swp-day-header
*/
private detectDropTarget(position: Position): 'swp-day-column' | 'swp-day-header' | null {
const elementAtPosition = document.elementFromPoint(position.x, position.y);
if (!elementAtPosition) return null;
// Traverse up the DOM tree to find the target container
let currentElement = elementAtPosition as HTMLElement;
let currentElement = this.draggedClone;
while (currentElement && currentElement !== document.body) {
if (currentElement.tagName === 'SWP-DAY-HEADER') {
if (currentElement.tagName === 'SWP-ALLDAY-CONTAINER') {
return 'swp-day-header';
}
if (currentElement.tagName === 'SWP-DAY-COLUMN') {

View file

@ -1,5 +1,6 @@
import { CalendarEvent } from '../types/CalendarTypes';
import { SwpAllDayEventElement } from '../elements/SwpEventElement';
import { EventLayout } from '../utils/AllDayLayoutEngine';
/**
* AllDayEventRenderer - Simple rendering of all-day events
@ -39,16 +40,15 @@ export class AllDayEventRenderer {
*/
public renderAllDayEventWithLayout(
event: CalendarEvent,
layout: { startColumn: number; endColumn: number; row: number; columnSpan: number }
): HTMLElement | null {
layout: EventLayout
) {
const container = this.getContainer();
if (!container) return null;
const allDayElement = SwpAllDayEventElement.fromCalendarEventWithLayout(event, layout);
const element = allDayElement.getElement();
let dayEvent = new SwpAllDayEventElement(event);
dayEvent.applyGridPositioning(layout);
container.appendChild(element);
return element;
container.appendChild(dayEvent.getElement());
}

View file

@ -372,35 +372,11 @@ export class EventRenderingService {
// Pass current events to AllDayManager for state tracking
this.allDayManager.setCurrentEvents(allDayEvents, weekDates);
// Calculate layout for ALL all-day events together using AllDayLayoutEngine
const layouts = this.allDayManager.calculateAllDayEventsLayout(allDayEvents, weekDates);
// Render each all-day event with pre-calculated layout
allDayEvents.forEach(event => {
const layout = layouts.get(event.id);
if (!layout) {
console.warn('❌ EventRenderingService: No layout found for all-day event', {
id: event.id,
title: event.title
});
return;
}
// Render with pre-calculated layout
const renderedElement = this.allDayEventRenderer.renderAllDayEventWithLayout(event, layout);
if (renderedElement) {
console.log('✅ EventRenderingService: Rendered all-day event with AllDayLayoutEngine', {
id: event.id,
title: event.title,
gridArea: layout.gridArea,
element: renderedElement.tagName
});
} else {
console.warn('❌ EventRenderingService: Failed to render all-day event', {
id: event.id,
title: event.title
});
}
layouts.forEach(layout => {
this.allDayEventRenderer.renderAllDayEventWithLayout(layout.calenderEvent, layout);
});
// Check and adjust all-day container height after rendering

View file

@ -64,6 +64,7 @@ export interface DragMoveEventPayload {
// Drag end event payload
export interface DragEndEventPayload {
draggedElement: HTMLElement;
draggedClone: HTMLElement | null;
mousePosition: MousePosition;
finalPosition: {
column: string | null;

View file

@ -1,7 +1,7 @@
import { CalendarEvent } from '../types/CalendarTypes';
export interface EventLayout {
id: string;
calenderEvent: CalendarEvent;
gridArea: string; // "row-start / col-start / row-end / col-end"
startColumn: number;
endColumn: number;
@ -21,13 +21,9 @@ export class AllDayLayoutEngine {
/**
* Calculate layout for all events using clean day-based logic
*/
public calculateLayout(events: CalendarEvent[]): Map<string, EventLayout> {
const layouts = new Map<string, EventLayout>();
if (this.weekDates.length === 0) {
return layouts;
}
public calculateLayout(events: CalendarEvent[]): EventLayout[] {
let layouts: EventLayout[] = [];
// Reset tracks for new calculation
this.tracks = [new Array(this.weekDates.length).fill(false)];
@ -48,15 +44,15 @@ export class AllDayLayoutEngine {
}
const layout: EventLayout = {
id: event.id,
calenderEvent: event,
gridArea: `${track + 1} / ${startDay} / ${track + 2} / ${endDay + 1}`,
startColumn: startDay,
endColumn: endDay,
row: track + 1,
columnSpan: endDay - startDay + 1
};
layouts.push(layout);
layouts.set(event.id, layout);
}
}