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
2192
package-lock.json
generated
2192
package-lock.json
generated
File diff suppressed because it is too large
Load diff
11
package.json
11
package.json
|
|
@ -7,13 +7,20 @@
|
|||
"build": "node build.js",
|
||||
"build-simple": "esbuild src/**/*.ts --outdir=js --format=esm --sourcemap=inline --target=es2020",
|
||||
"watch": "esbuild src/**/*.ts --outdir=js --format=esm --sourcemap=inline --target=es2020 --watch",
|
||||
"clean": "powershell -Command \"if (Test-Path js) { Remove-Item -Recurse -Force js }\""
|
||||
"clean": "powershell -Command \"if (Test-Path js) { Remove-Item -Recurse -Force js }\"",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run",
|
||||
"test:ui": "vitest --ui"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"esbuild": "^0.19.0",
|
||||
"typescript": "^5.0.0"
|
||||
"jsdom": "^27.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rollup/rollup-win32-x64-msvc": "^4.52.2",
|
||||
"fuse.js": "^7.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,80 +283,49 @@ export class SwpAllDayEventElement extends BaseEventElement {
|
|||
}
|
||||
|
||||
/**
|
||||
* Factory method to create from CalendarEvent and target date
|
||||
* Factory method to create from CalendarEvent and layout (provided by AllDayManager)
|
||||
*/
|
||||
public static fromCalendarEvent(event: CalendarEvent, targetDate?: string): SwpAllDayEventElement {
|
||||
// Calculate column span based on event start and end dates
|
||||
const { startColumn, endColumn, columnSpan } = this.calculateColumnSpan(event);
|
||||
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);
|
||||
|
||||
// For backwards compatibility, use targetDate if provided, otherwise use calculated start column
|
||||
const finalStartColumn = targetDate ? this.getColumnIndexForDate(targetDate) : startColumn;
|
||||
const finalEndColumn = targetDate ? finalStartColumn : endColumn;
|
||||
const finalColumnSpan = targetDate ? 1 : columnSpan;
|
||||
|
||||
// Find occupied rows in the spanned columns using computedStyle
|
||||
const existingEvents = document.querySelectorAll('swp-allday-container swp-event');
|
||||
const occupiedRows = new Set<number>();
|
||||
// Set complete grid-area instead of individual properties
|
||||
const gridArea = `${layout.row} / ${layout.startColumn} / ${layout.row + 1} / ${layout.endColumn + 1}`;
|
||||
element.element.style.gridArea = gridArea;
|
||||
|
||||
console.log('🔍 SwpAllDayEventElement: Checking grid row for new event', {
|
||||
targetDate,
|
||||
finalStartColumn,
|
||||
finalEndColumn,
|
||||
existingEventsCount: existingEvents.length
|
||||
});
|
||||
|
||||
existingEvents.forEach(existingEvent => {
|
||||
const style = getComputedStyle(existingEvent);
|
||||
const eventStartCol = parseInt(style.gridColumnStart);
|
||||
const eventEndCol = parseInt(style.gridColumnEnd);
|
||||
const eventRow = parseInt(style.gridRowStart) || 1;
|
||||
const eventId = (existingEvent as HTMLElement).dataset.eventId;
|
||||
|
||||
console.log('📊 SwpAllDayEventElement: Checking existing event', {
|
||||
eventId,
|
||||
eventStartCol,
|
||||
eventEndCol,
|
||||
eventRow,
|
||||
newEventColumn: finalStartColumn
|
||||
});
|
||||
|
||||
// FIXED: Only check events in the same column (not overlap detection)
|
||||
if (eventStartCol === finalStartColumn) {
|
||||
console.log('✅ SwpAllDayEventElement: Same column - adding occupied row', eventRow);
|
||||
occupiedRows.add(eventRow);
|
||||
} else {
|
||||
console.log('⏭️ SwpAllDayEventElement: Different column - skipping');
|
||||
}
|
||||
});
|
||||
|
||||
// Find first available row
|
||||
let targetRow = 1;
|
||||
while (occupiedRows.has(targetRow)) {
|
||||
targetRow++;
|
||||
}
|
||||
|
||||
console.log('🎯 SwpAllDayEventElement: Final row assignment', {
|
||||
targetDate,
|
||||
finalStartColumn,
|
||||
occupiedRows: Array.from(occupiedRows).sort(),
|
||||
assignedRow: targetRow
|
||||
});
|
||||
|
||||
// Create element with calculated column span
|
||||
const element = new SwpAllDayEventElement(event, finalStartColumn);
|
||||
element.setGridRow(targetRow);
|
||||
element.setColumnSpan(finalStartColumn, finalEndColumn);
|
||||
|
||||
console.log('✅ SwpAllDayEventElement: Created all-day event', {
|
||||
console.log('✅ SwpAllDayEventElement: Created all-day event with AllDayLayoutEngine', {
|
||||
eventId: event.id,
|
||||
title: event.title,
|
||||
column: finalStartColumn,
|
||||
row: targetRow
|
||||
gridArea: gridArea,
|
||||
layout: layout
|
||||
});
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import { SwpAllDayEventElement } from '../elements/SwpEventElement';
|
|||
/**
|
||||
* AllDayEventRenderer - Simple rendering of all-day events
|
||||
* Handles adding and removing all-day events from the header container
|
||||
* NOTE: Layout calculation is now handled by AllDayManager
|
||||
*/
|
||||
export class AllDayEventRenderer {
|
||||
private container: HTMLElement | null = null;
|
||||
|
|
@ -34,19 +35,23 @@ export class AllDayEventRenderer {
|
|||
// REMOVED: createGhostColumns() method - no longer needed!
|
||||
|
||||
/**
|
||||
* Render an all-day event using factory pattern
|
||||
* Render an all-day event with pre-calculated layout
|
||||
*/
|
||||
public renderAllDayEvent(event: CalendarEvent, targetDate?: string): HTMLElement | null {
|
||||
public renderAllDayEventWithLayout(
|
||||
event: CalendarEvent,
|
||||
layout: { startColumn: number; endColumn: number; row: number; columnSpan: number }
|
||||
): HTMLElement | null {
|
||||
const container = this.getContainer();
|
||||
if (!container) return null;
|
||||
|
||||
const allDayElement = SwpAllDayEventElement.fromCalendarEvent(event, targetDate);
|
||||
const allDayElement = SwpAllDayEventElement.fromCalendarEventWithLayout(event, layout);
|
||||
const element = allDayElement.getElement();
|
||||
|
||||
container.appendChild(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove an all-day event by ID
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { CoreEvents } from '../constants/CoreEvents';
|
|||
import { calendarConfig } from '../core/CalendarConfig';
|
||||
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
|
||||
import { EventManager } from '../managers/EventManager';
|
||||
import { AllDayManager } from '../managers/AllDayManager';
|
||||
import { EventRendererStrategy } from './EventRenderer';
|
||||
import { SwpEventElement } from '../elements/SwpEventElement';
|
||||
import { AllDayEventRenderer } from './AllDayEventRenderer';
|
||||
|
|
@ -17,7 +18,7 @@ export class EventRenderingService {
|
|||
private eventManager: EventManager;
|
||||
private strategy: EventRendererStrategy;
|
||||
private allDayEventRenderer: AllDayEventRenderer;
|
||||
|
||||
private allDayManager: AllDayManager;
|
||||
|
||||
private dragMouseLeaveHeaderListener: ((event: Event) => void) | null = null;
|
||||
|
||||
|
|
@ -29,8 +30,9 @@ export class EventRenderingService {
|
|||
const calendarType = calendarConfig.getCalendarMode();
|
||||
this.strategy = CalendarTypeFactory.getEventRenderer(calendarType);
|
||||
|
||||
// Initialize all-day event renderer
|
||||
// Initialize all-day event renderer and manager
|
||||
this.allDayEventRenderer = new AllDayEventRenderer();
|
||||
this.allDayManager = new AllDayManager();
|
||||
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
|
@ -349,13 +351,35 @@ export class EventRenderingService {
|
|||
// Clear existing all-day events first
|
||||
this.clearAllDayEvents();
|
||||
|
||||
// Render each all-day event
|
||||
// Get actual visible dates from DOM headers instead of generating them
|
||||
const weekDates = this.getVisibleDatesFromDOM();
|
||||
|
||||
console.log('🔍 EventRenderingService: Using visible dates from DOM', {
|
||||
weekDates,
|
||||
count: weekDates.length
|
||||
});
|
||||
|
||||
// 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 renderedElement = this.allDayEventRenderer.renderAllDayEvent(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', {
|
||||
console.log('✅ EventRenderingService: Rendered all-day event with AllDayLayoutEngine', {
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
gridArea: layout.gridArea,
|
||||
element: renderedElement.tagName
|
||||
});
|
||||
} else {
|
||||
|
|
@ -392,6 +416,26 @@ export class EventRenderingService {
|
|||
this.clearEvents(container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get visible dates from DOM headers - only the dates that are actually displayed
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
public destroy(): void {
|
||||
this.clearEvents();
|
||||
}
|
||||
|
|
|
|||
166
src/utils/AllDayLayoutEngine.ts
Normal file
166
src/utils/AllDayLayoutEngine.ts
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
/**
|
||||
* AllDayLayoutEngine - Pure data-driven layout calculation for all-day events
|
||||
*/
|
||||
|
||||
import { CalendarEvent } from '../types/CalendarTypes';
|
||||
|
||||
export interface EventLayout {
|
||||
id: string;
|
||||
gridArea: string; // "row-start / col-start / row-end / col-end"
|
||||
startColumn: number;
|
||||
endColumn: number;
|
||||
row: number;
|
||||
columnSpan: number;
|
||||
}
|
||||
|
||||
export class AllDayLayoutEngine {
|
||||
private weekDates: string[];
|
||||
|
||||
constructor(weekDates: string[]) {
|
||||
this.weekDates = weekDates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate layout for all events with proper overlap detection
|
||||
*/
|
||||
public calculateLayout(events: CalendarEvent[]): Map<string, EventLayout> {
|
||||
const layouts = new Map<string, EventLayout>();
|
||||
|
||||
// Sort by event duration (longest first), then by start date
|
||||
const sortedEvents = [...events].sort((a, b) => {
|
||||
const durationA = this.calculateEventDuration(a);
|
||||
const durationB = this.calculateEventDuration(b);
|
||||
|
||||
// Primary sort: longest duration first
|
||||
if (durationA !== durationB) {
|
||||
return durationB - durationA;
|
||||
}
|
||||
|
||||
// Secondary sort: earliest start date first
|
||||
const startA = a.start.toISOString().split('T')[0];
|
||||
const startB = b.start.toISOString().split('T')[0];
|
||||
return startA.localeCompare(startB);
|
||||
});
|
||||
|
||||
sortedEvents.forEach(event => {
|
||||
const layout = this.calculateEventLayout(event, layouts);
|
||||
layouts.set(event.id, layout);
|
||||
});
|
||||
|
||||
return layouts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate event duration in days
|
||||
*/
|
||||
private calculateEventDuration(event: CalendarEvent): number {
|
||||
const startDate = event.start;
|
||||
const endDate = event.end;
|
||||
const diffTime = endDate.getTime() - startDate.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1; // +1 because same day = 1 day
|
||||
return diffDays;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate layout for single event considering existing events
|
||||
*/
|
||||
private calculateEventLayout(event: CalendarEvent, existingLayouts: Map<string, EventLayout>): EventLayout {
|
||||
// Calculate column span
|
||||
const { startColumn, endColumn, columnSpan } = this.calculateColumnSpan(event);
|
||||
|
||||
// Find available row using overlap detection
|
||||
const availableRow = this.findAvailableRow(startColumn, endColumn, existingLayouts);
|
||||
|
||||
// Generate grid-area string: "row-start / col-start / row-end / col-end"
|
||||
const gridArea = `${availableRow} / ${startColumn} / ${availableRow + 1} / ${endColumn + 1}`;
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
gridArea,
|
||||
startColumn,
|
||||
endColumn,
|
||||
row: availableRow,
|
||||
columnSpan
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate column span based on event start and end dates
|
||||
*/
|
||||
private calculateColumnSpan(event: CalendarEvent): { startColumn: number; endColumn: number; columnSpan: number } {
|
||||
// Convert CalendarEvent dates to YYYY-MM-DD format
|
||||
const startDate = event.start.toISOString().split('T')[0];
|
||||
const endDate = event.end.toISOString().split('T')[0];
|
||||
// Find start and end column indices (1-based)
|
||||
let startColumn = -1;
|
||||
let endColumn = -1;
|
||||
|
||||
this.weekDates.forEach((dateStr, index) => {
|
||||
if (dateStr === startDate) {
|
||||
startColumn = index + 1;
|
||||
}
|
||||
if (dateStr === endDate) {
|
||||
endColumn = index + 1;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle events that start before or end after the week
|
||||
if (startColumn === -1) {
|
||||
startColumn = 1; // Event starts before this week
|
||||
}
|
||||
if (endColumn === -1) {
|
||||
endColumn = this.weekDates.length; // Event ends after this week
|
||||
}
|
||||
|
||||
// Ensure end column is at least start column
|
||||
if (endColumn < startColumn) {
|
||||
endColumn = startColumn;
|
||||
}
|
||||
|
||||
const columnSpan = endColumn - startColumn + 1;
|
||||
|
||||
return { startColumn, endColumn, columnSpan };
|
||||
}
|
||||
|
||||
/**
|
||||
* Find available row using overlap detection
|
||||
*/
|
||||
private findAvailableRow(
|
||||
newStartColumn: number,
|
||||
newEndColumn: number,
|
||||
existingLayouts: Map<string, EventLayout>
|
||||
): number {
|
||||
const occupiedRows = new Set<number>();
|
||||
|
||||
// Check all existing events for overlaps
|
||||
existingLayouts.forEach(layout => {
|
||||
const overlaps = this.columnsOverlap(
|
||||
newStartColumn, newEndColumn,
|
||||
layout.startColumn, layout.endColumn
|
||||
);
|
||||
|
||||
if (overlaps) {
|
||||
occupiedRows.add(layout.row);
|
||||
}
|
||||
});
|
||||
|
||||
// Find first available row
|
||||
let targetRow = 1;
|
||||
while (occupiedRows.has(targetRow)) {
|
||||
targetRow++;
|
||||
}
|
||||
|
||||
return targetRow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two column ranges overlap
|
||||
*/
|
||||
private columnsOverlap(
|
||||
startA: number, endA: number,
|
||||
startB: number, endB: number
|
||||
): boolean {
|
||||
// Two ranges overlap if one doesn't end before the other starts
|
||||
return !(endA < startB || endB < startA);
|
||||
}
|
||||
}
|
||||
138
test/managers/AllDayLayoutEngine.test.ts
Normal file
138
test/managers/AllDayLayoutEngine.test.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { AllDayLayoutEngine } from '../../src/utils/AllDayLayoutEngine';
|
||||
import { CalendarEvent } from '../../src/types/CalendarTypes';
|
||||
|
||||
describe('AllDay Layout Engine - Pure Data Tests', () => {
|
||||
const weekDates = [
|
||||
'2025-09-22', '2025-09-23', '2025-09-24', '2025-09-25',
|
||||
'2025-09-26', '2025-09-27', '2025-09-28'
|
||||
];
|
||||
const layoutEngine = new AllDayLayoutEngine(weekDates);
|
||||
|
||||
// Test data: events med start/end datoer og forventet grid-area
|
||||
const testCases = [
|
||||
{
|
||||
name: 'Single day events - no overlap',
|
||||
events: [
|
||||
{ id: '1', title: 'Event 1', start: new Date('2025-09-22'), end: new Date('2025-09-22'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
||||
{ id: '2', title: 'Event 2', start: new Date('2025-09-24'), end: new Date('2025-09-24'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent
|
||||
],
|
||||
expected: [
|
||||
{ id: '1', gridArea: '1 / 1 / 2 / 2' }, // row 1, column 1 (Sept 22)
|
||||
{ id: '2', gridArea: '1 / 3 / 2 / 4' } // row 1, column 3 (Sept 24)
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Overlapping multi-day events - Autumn Equinox vs Teknisk Workshop',
|
||||
events: [
|
||||
{ id: 'autumn', title: 'Autumn Equinox', start: new Date('2025-09-22'), end: new Date('2025-09-23'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
||||
{ id: 'workshop', title: 'Teknisk Workshop', start: new Date('2025-09-23'), end: new Date('2025-09-26'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent
|
||||
],
|
||||
expected: [
|
||||
{ id: 'autumn', gridArea: '2 / 1 / 3 / 3' }, // row 2, columns 1-2 (2 dage, processed second)
|
||||
{ id: 'workshop', gridArea: '1 / 2 / 2 / 6' } // row 1, columns 2-5 (4 dage, processed first)
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Multiple events same column',
|
||||
events: [
|
||||
{ id: '1', title: 'Event 1', start: new Date('2025-09-23'), end: new Date('2025-09-23'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
||||
{ id: '2', title: 'Event 2', start: new Date('2025-09-23'), end: new Date('2025-09-23'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
||||
{ id: '3', title: 'Event 3', start: new Date('2025-09-23'), end: new Date('2025-09-23'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent
|
||||
],
|
||||
expected: [
|
||||
{ id: '1', gridArea: '1 / 2 / 2 / 3' }, // row 1, column 2 (Sept 23)
|
||||
{ id: '2', gridArea: '2 / 2 / 3 / 3' }, // row 2, column 2 (Sept 23)
|
||||
{ id: '3', gridArea: '3 / 2 / 4 / 3' } // row 3, column 2 (Sept 23)
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Partial overlaps',
|
||||
events: [
|
||||
{ id: '1', title: 'Event 1', start: new Date('2025-09-22'), end: new Date('2025-09-23'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
||||
{ id: '2', title: 'Event 2', start: new Date('2025-09-23'), end: new Date('2025-09-24'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
||||
{ id: '3', title: 'Event 3', start: new Date('2025-09-25'), end: new Date('2025-09-26'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent
|
||||
],
|
||||
expected: [
|
||||
{ id: '1', gridArea: '1 / 1 / 2 / 3' }, // row 1, columns 1-2 (Sept 22-23)
|
||||
{ id: '2', gridArea: '2 / 2 / 3 / 4' }, // row 2, columns 2-3 (Sept 23-24, overlap på column 2)
|
||||
{ id: '3', gridArea: '1 / 4 / 2 / 6' } // row 1, columns 4-5 (Sept 25-26, no overlap)
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Complex overlapping pattern',
|
||||
events: [
|
||||
{ id: '1', title: 'Long Event', start: new Date('2025-09-22'), end: new Date('2025-09-25'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
||||
{ id: '2', title: 'Short Event', start: new Date('2025-09-23'), end: new Date('2025-09-24'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
||||
{ id: '3', title: 'Another Event', start: new Date('2025-09-24'), end: new Date('2025-09-26'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent
|
||||
],
|
||||
expected: [
|
||||
{ id: '1', gridArea: '1 / 1 / 2 / 5' }, // row 1, columns 1-4 (4 dage, processed first)
|
||||
{ id: '2', gridArea: '3 / 2 / 4 / 4' }, // row 3, columns 2-3 (2 dage, processed last)
|
||||
{ id: '3', gridArea: '2 / 3 / 3 / 6' } // row 2, columns 3-5 (3 dage, processed second)
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Real-world bug scenario - Multiple overlapping events (Sept 21-28)',
|
||||
events: [
|
||||
{ id: '112', title: 'Autumn Equinox', start: new Date('2025-09-22'), end: new Date('2025-09-23'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
||||
{ id: '122', title: 'Multi-Day Conference', start: new Date('2025-09-21'), end: new Date('2025-09-24'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
||||
{ id: '123', title: 'Project Sprint', start: new Date('2025-09-22'), end: new Date('2025-09-25'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
||||
{ id: '143', title: 'Weekend Hackathon', start: new Date('2025-09-26'), end: new Date('2025-09-28'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent,
|
||||
{ id: '161', title: 'Teknisk Workshop', start: new Date('2025-09-23'), end: new Date('2025-09-26'), type: 'work', allDay: true, syncStatus: 'synced' } as CalendarEvent
|
||||
],
|
||||
expected: [
|
||||
{ id: '112', gridArea: '4 / 1 / 5 / 3' }, // Autumn Equinox: row 4, columns 1-2 (2 dage, processed last)
|
||||
{ id: '122', gridArea: '1 / 1 / 2 / 4' }, // Multi-Day Conference: row 1, columns 1-3 (4 dage, starts 21/9, processed first)
|
||||
{ id: '123', gridArea: '2 / 1 / 3 / 5' }, // Project Sprint: row 2, columns 1-4 (4 dage, starts 22/9, processed second)
|
||||
{ id: '143', gridArea: '1 / 5 / 2 / 8' }, // Weekend Hackathon: row 1, columns 5-7 (3 dage, no overlap, reuse row 1)
|
||||
{ id: '161', gridArea: '3 / 2 / 4 / 6' } // Teknisk Workshop: row 3, columns 2-5 (4 dage, starts 23/9, processed third)
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(testCase => {
|
||||
it(testCase.name, () => {
|
||||
// Calculate actual layouts using AllDayLayoutEngine
|
||||
const layouts = layoutEngine.calculateLayout(testCase.events);
|
||||
|
||||
// Verify we got layouts for all events
|
||||
expect(layouts.size).toBe(testCase.events.length);
|
||||
|
||||
// Check each expected result
|
||||
testCase.expected.forEach(expected => {
|
||||
const actualLayout = layouts.get(expected.id);
|
||||
expect(actualLayout).toBeDefined();
|
||||
expect(actualLayout!.gridArea).toBe(expected.gridArea);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Grid-area format validation', () => {
|
||||
// Test at grid-area format er korrekt
|
||||
const gridArea = '2 / 3 / 3 / 5'; // row 2, columns 3-4
|
||||
const parts = gridArea.split(' / ');
|
||||
|
||||
const rowStart = parseInt(parts[0]); // 2
|
||||
const colStart = parseInt(parts[1]); // 3
|
||||
const rowEnd = parseInt(parts[2]); // 3
|
||||
const colEnd = parseInt(parts[3]); // 5
|
||||
|
||||
expect(rowStart).toBe(2);
|
||||
expect(colStart).toBe(3);
|
||||
expect(rowEnd).toBe(3);
|
||||
expect(colEnd).toBe(5);
|
||||
|
||||
// Verify spans
|
||||
const rowSpan = rowEnd - rowStart; // 1 row
|
||||
const colSpan = colEnd - colStart; // 2 columns
|
||||
|
||||
expect(rowSpan).toBe(1);
|
||||
expect(colSpan).toBe(2);
|
||||
});
|
||||
});
|
||||
134
test/managers/AllDayManager.test.ts
Normal file
134
test/managers/AllDayManager.test.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { AllDayManager } from '../../src/managers/AllDayManager';
|
||||
import { setupMockDOM, createMockEvent } from '../helpers/dom-helpers';
|
||||
|
||||
describe('AllDayManager - Layout Calculation', () => {
|
||||
let allDayManager: AllDayManager;
|
||||
|
||||
beforeEach(() => {
|
||||
setupMockDOM();
|
||||
allDayManager = new AllDayManager();
|
||||
});
|
||||
|
||||
describe('Overlap Detection Scenarios', () => {
|
||||
it('Scenario 1: Non-overlapping single-day events', () => {
|
||||
// Event 1: Sept 22 (column 1)
|
||||
const event1 = createMockEvent('1', 'Event 1', '2024-09-22', '2024-09-22');
|
||||
const layout1 = allDayManager.calculateAllDayEventLayout(event1);
|
||||
|
||||
expect(layout1.startColumn).toBe(1);
|
||||
expect(layout1.row).toBe(1);
|
||||
|
||||
// Event 2: Sept 24 (column 3) - different column, should be row 1
|
||||
const event2 = createMockEvent('2', 'Event 2', '2024-09-24', '2024-09-24');
|
||||
const layout2 = allDayManager.calculateAllDayEventLayout(event2);
|
||||
|
||||
expect(layout2.startColumn).toBe(3);
|
||||
expect(layout2.row).toBe(1);
|
||||
});
|
||||
|
||||
it('Scenario 2: Overlapping multi-day events - Autumn Equinox vs Teknisk Workshop', () => {
|
||||
// Autumn Equinox: Sept 22-23 (columns 1-2)
|
||||
const autumnEvent = createMockEvent('autumn', 'Autumn Equinox', '2024-09-22', '2024-09-23');
|
||||
const autumnLayout = allDayManager.calculateAllDayEventLayout(autumnEvent);
|
||||
|
||||
expect(autumnLayout.startColumn).toBe(1);
|
||||
expect(autumnLayout.endColumn).toBe(2);
|
||||
expect(autumnLayout.row).toBe(1);
|
||||
|
||||
// Teknisk Workshop: Sept 23-26 (columns 2-5) - overlaps on Sept 23
|
||||
const workshopEvent = createMockEvent('workshop', 'Teknisk Workshop', '2024-09-23', '2024-09-26');
|
||||
const workshopLayout = allDayManager.calculateAllDayEventLayout(workshopEvent);
|
||||
|
||||
expect(workshopLayout.startColumn).toBe(2);
|
||||
expect(workshopLayout.endColumn).toBe(5);
|
||||
expect(workshopLayout.row).toBe(2); // Should be row 2 due to overlap
|
||||
});
|
||||
|
||||
it('Scenario 3: Multiple events in same column', () => {
|
||||
// Event 1: Sept 23 only
|
||||
const event1 = createMockEvent('1', 'Event 1', '2024-09-23', '2024-09-23');
|
||||
const layout1 = allDayManager.calculateAllDayEventLayout(event1);
|
||||
|
||||
expect(layout1.startColumn).toBe(2);
|
||||
expect(layout1.row).toBe(1);
|
||||
|
||||
// Event 2: Sept 23 only - same column, should be row 2
|
||||
const event2 = createMockEvent('2', 'Event 2', '2024-09-23', '2024-09-23');
|
||||
const layout2 = allDayManager.calculateAllDayEventLayout(event2);
|
||||
|
||||
expect(layout2.startColumn).toBe(2);
|
||||
expect(layout2.row).toBe(2);
|
||||
|
||||
// Event 3: Sept 23 only - same column, should be row 3
|
||||
const event3 = createMockEvent('3', 'Event 3', '2024-09-23', '2024-09-23');
|
||||
const layout3 = allDayManager.calculateAllDayEventLayout(event3);
|
||||
|
||||
expect(layout3.startColumn).toBe(2);
|
||||
expect(layout3.row).toBe(3);
|
||||
});
|
||||
|
||||
it('Scenario 4: Partial overlaps', () => {
|
||||
// Event 1: Sept 22-23 (columns 1-2)
|
||||
const event1 = createMockEvent('1', 'Event 1', '2024-09-22', '2024-09-23');
|
||||
const layout1 = allDayManager.calculateAllDayEventLayout(event1);
|
||||
|
||||
expect(layout1.startColumn).toBe(1);
|
||||
expect(layout1.endColumn).toBe(2);
|
||||
expect(layout1.row).toBe(1);
|
||||
|
||||
// Event 2: Sept 23-24 (columns 2-3) - overlaps on Sept 23
|
||||
const event2 = createMockEvent('2', 'Event 2', '2024-09-23', '2024-09-24');
|
||||
const layout2 = allDayManager.calculateAllDayEventLayout(event2);
|
||||
|
||||
expect(layout2.startColumn).toBe(2);
|
||||
expect(layout2.endColumn).toBe(3);
|
||||
expect(layout2.row).toBe(2); // Should be row 2 due to overlap
|
||||
|
||||
// Event 3: Sept 25-26 (columns 4-5) - no overlap, should be row 1
|
||||
const event3 = createMockEvent('3', 'Event 3', '2024-09-25', '2024-09-26');
|
||||
const layout3 = allDayManager.calculateAllDayEventLayout(event3);
|
||||
|
||||
expect(layout3.startColumn).toBe(4);
|
||||
expect(layout3.endColumn).toBe(5);
|
||||
expect(layout3.row).toBe(1); // No overlap, back to row 1
|
||||
});
|
||||
|
||||
it('Scenario 5: Complex overlapping pattern', () => {
|
||||
// Event 1: Sept 22-25 (columns 1-4) - spans most of week
|
||||
const event1 = createMockEvent('1', 'Long Event', '2024-09-22', '2024-09-25');
|
||||
const layout1 = allDayManager.calculateAllDayEventLayout(event1);
|
||||
|
||||
expect(layout1.startColumn).toBe(1);
|
||||
expect(layout1.endColumn).toBe(4);
|
||||
expect(layout1.row).toBe(1);
|
||||
|
||||
// Event 2: Sept 23-24 (columns 2-3) - overlaps with Event 1
|
||||
const event2 = createMockEvent('2', 'Short Event', '2024-09-23', '2024-09-24');
|
||||
const layout2 = allDayManager.calculateAllDayEventLayout(event2);
|
||||
|
||||
expect(layout2.startColumn).toBe(2);
|
||||
expect(layout2.endColumn).toBe(3);
|
||||
expect(layout2.row).toBe(2);
|
||||
|
||||
// Event 3: Sept 24-26 (columns 3-5) - overlaps with both
|
||||
const event3 = createMockEvent('3', 'Another Event', '2024-09-24', '2024-09-26');
|
||||
const layout3 = allDayManager.calculateAllDayEventLayout(event3);
|
||||
|
||||
expect(layout3.startColumn).toBe(3);
|
||||
expect(layout3.endColumn).toBe(5);
|
||||
expect(layout3.row).toBe(3); // Should be row 3 due to overlaps with both
|
||||
});
|
||||
|
||||
it('Scenario 6: Drag-drop target date override', () => {
|
||||
// Multi-day event dragged to specific date should use single column
|
||||
const event = createMockEvent('drag', 'Dragged Event', '2024-09-22', '2024-09-25');
|
||||
const layout = allDayManager.calculateAllDayEventLayout(event, '2024-09-24');
|
||||
|
||||
expect(layout.startColumn).toBe(3); // Sept 24 is column 3
|
||||
expect(layout.endColumn).toBe(3); // Single column when targetDate specified
|
||||
expect(layout.columnSpan).toBe(1);
|
||||
expect(layout.row).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
13
test/setup.ts
Normal file
13
test/setup.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { beforeEach } from 'vitest';
|
||||
|
||||
// Global test setup
|
||||
beforeEach(() => {
|
||||
// Clear DOM before each test
|
||||
document.body.innerHTML = '';
|
||||
document.head.innerHTML = '';
|
||||
|
||||
// Reset any global state
|
||||
if (typeof window !== 'undefined') {
|
||||
// Clear any event listeners or global variables if needed
|
||||
}
|
||||
});
|
||||
9
vitest.config.ts
Normal file
9
vitest.config.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./test/setup.ts'],
|
||||
globals: true,
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue