Improves all-day event drag and drop

Refactors all-day event drag and drop handling for improved accuracy and performance.

Introduces a shared `ColumnDetectionUtils` for consistent column detection.

Simplifies all-day conversion during drag, placing events in row 1 and calculating the column from the target date.

Implements differential updates during drag end, updating only changed events for smoother transitions.
This commit is contained in:
Janus C. H. Knudsen 2025-09-26 22:11:57 +02:00
parent 41d078e2e8
commit 0553089085
6 changed files with 307 additions and 185 deletions

View file

@ -4,12 +4,14 @@ import { eventBus } from '../core/EventBus';
import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig'; import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig';
import { AllDayEventRenderer } from '../renderers/AllDayEventRenderer'; import { AllDayEventRenderer } from '../renderers/AllDayEventRenderer';
import { AllDayLayoutEngine } from '../utils/AllDayLayoutEngine'; import { AllDayLayoutEngine } from '../utils/AllDayLayoutEngine';
import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
import { CalendarEvent } from '../types/CalendarTypes'; import { CalendarEvent } from '../types/CalendarTypes';
import { import {
DragMouseEnterHeaderEventPayload, DragMouseEnterHeaderEventPayload,
DragStartEventPayload, DragStartEventPayload,
DragMoveEventPayload, DragMoveEventPayload,
DragEndEventPayload DragEndEventPayload,
DragColumnChangeEventPayload
} from '../types/EventTypes'; } from '../types/EventTypes';
import { DragOffset, MousePosition } from '../types/DragDropTypes'; import { DragOffset, MousePosition } from '../types/DragDropTypes';
@ -20,6 +22,11 @@ import { DragOffset, MousePosition } from '../types/DragDropTypes';
export class AllDayManager { export class AllDayManager {
private allDayEventRenderer: AllDayEventRenderer; private allDayEventRenderer: AllDayEventRenderer;
private layoutEngine: AllDayLayoutEngine | null = null; private layoutEngine: AllDayLayoutEngine | null = null;
// State tracking for differential updates
private currentLayouts: Map<string, string> = new Map();
private currentAllDayEvents: CalendarEvent[] = [];
private currentWeekDates: string[] = [];
constructor() { constructor() {
this.allDayEventRenderer = new AllDayEventRenderer(); this.allDayEventRenderer = new AllDayEventRenderer();
@ -53,10 +60,6 @@ export class AllDayManager {
originalElementId: originalElement?.dataset?.eventId originalElementId: originalElement?.dataset?.eventId
}); });
if (cloneElement && cloneElement.classList.contains('all-day-style')) {
this.handleConvertFromAllDay(cloneElement);
}
this.checkAndAnimateAllDayHeight(); this.checkAndAnimateAllDayHeight();
}); });
@ -73,17 +76,26 @@ export class AllDayManager {
this.handleDragStart(draggedElement, eventId || '', mouseOffset); this.handleDragStart(draggedElement, eventId || '', mouseOffset);
}); });
eventBus.on('drag:move', (event) => { eventBus.on('drag:column-change', (event) => {
const { draggedElement, mousePosition } = (event as CustomEvent<DragMoveEventPayload>).detail; const { draggedElement, mousePosition } = (event as CustomEvent<DragColumnChangeEventPayload>).detail;
// Only handle for all-day events - check if original element is all-day
const isAllDayEvent = draggedElement.closest('swp-allday-container');
if (!isAllDayEvent) return;
// Check if there's an all-day clone for this event
const eventId = draggedElement.dataset.eventId; const eventId = draggedElement.dataset.eventId;
const dragClone = document.querySelector(`swp-allday-container swp-event[data-event-id="clone-${eventId}"]`); const dragClone = document.querySelector(`swp-allday-container swp-event[data-event-id="clone-${eventId}"]`) as HTMLElement;
if (!dragClone.hasAttribute('data-allday')) {
return;
}
// If we find an all-day clone, handle the drag move
if (dragClone) { if (dragClone) {
this.handleDragMove(dragClone as HTMLElement, mousePosition); console.log('🔄 AllDayManager: Found all-day clone, handling drag:column-change', {
eventId,
cloneId: dragClone.dataset.eventId
});
this.handleColumnChange(dragClone, mousePosition);
} }
}); });
@ -259,6 +271,42 @@ export class AllDayManager {
}); });
} }
/**
* Store current layouts from DOM for comparison
*/
private storeCurrentLayouts(): void {
this.currentLayouts.clear();
const container = this.getAllDayContainer();
if (!container) return;
container.querySelectorAll('swp-event').forEach(element => {
const htmlElement = element as HTMLElement;
const eventId = htmlElement.dataset.eventId;
const gridArea = htmlElement.style.gridArea;
if (eventId && gridArea) {
this.currentLayouts.set(eventId, gridArea);
}
});
console.log('📋 AllDayManager: Stored current layouts', {
count: this.currentLayouts.size,
layouts: Array.from(this.currentLayouts.entries())
});
}
/**
* Set current events and week dates (called by EventRendererManager)
*/
public setCurrentEvents(events: CalendarEvent[], weekDates: string[]): void {
this.currentAllDayEvents = events;
this.currentWeekDates = weekDates;
console.log('📝 AllDayManager: Set current events', {
eventCount: events.length,
weekDatesCount: weekDates.length
});
}
/** /**
* Calculate layout for ALL all-day events using AllDayLayoutEngine * Calculate layout for ALL all-day events using AllDayLayoutEngine
* This is the correct method that processes all events together for proper overlap detection * This is the correct method that processes all events together for proper overlap detection
@ -276,6 +324,10 @@ export class AllDayManager {
weekDates weekDates
}); });
// Store current state
this.currentAllDayEvents = events;
this.currentWeekDates = weekDates;
// Initialize layout engine with provided week dates // Initialize layout engine with provided week dates
this.layoutEngine = new AllDayLayoutEngine(weekDates); this.layoutEngine = new AllDayLayoutEngine(weekDates);
@ -313,95 +365,37 @@ export class AllDayManager {
/** /**
* Handle conversion of timed event to all-day event using CSS styling * Handle conversion of timed event to all-day event - SIMPLIFIED
* During drag: Place in row 1 only, calculate column from targetDate
*/ */
private handleConvertToAllDay(targetDate: string, cloneElement: HTMLElement): void { private handleConvertToAllDay(targetDate: string, cloneElement: HTMLElement): void {
console.log('🔄 AllDayManager: Converting to all-day using AllDayLayoutEngine', { console.log('🔄 AllDayManager: Converting to all-day (row 1 only during drag)', {
eventId: cloneElement.dataset.eventId, eventId: cloneElement.dataset.eventId,
targetDate targetDate
}); });
// Get all-day container, request creation if needed // Get all-day container, request creation if needed
let allDayContainer = this.getAllDayContainer(); let allDayContainer = this.getAllDayContainer();
if (!allDayContainer) {
console.log('🔄 AllDayManager: All-day container not found, requesting creation...');
// Request HeaderManager to create container
eventBus.emit('header:ensure-allday-container');
// Try again after request // Calculate target column from targetDate using ColumnDetectionUtils
allDayContainer = this.getAllDayContainer(); const targetColumn = ColumnDetectionUtils.getColumnIndexFromDate(targetDate);
if (!allDayContainer) {
console.error('All-day container still not found after creation request');
return;
}
}
// Create mock event for layout calculation cloneElement.removeAttribute('style');
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.classList.add('all-day-style');
cloneElement.style.gridColumn = layout.startColumn.toString(); cloneElement.style.gridRow = '1';
cloneElement.style.gridRow = layout.row.toString(); cloneElement.style.gridColumn = targetColumn.toString();
cloneElement.dataset.allDayDate = targetDate; cloneElement.dataset.allday = 'true'; // Set the all-day attribute for filtering
cloneElement.style.display = '';
// NOW add to container (after all positioning is calculated) // Add to container
allDayContainer.appendChild(cloneElement); allDayContainer?.appendChild(cloneElement);
console.log('✅ AllDayManager: Converted to all-day style using AllDayLayoutEngine', { console.log('✅ AllDayManager: Converted to all-day style (simple row 1)', {
eventId: cloneElement.dataset.eventId, eventId: cloneElement.dataset.eventId,
gridColumn: layout.startColumn, gridColumn: targetColumn,
gridRow: layout.row gridRow: 1
}); });
} }
/**
* Handle conversion from all-day back to timed event
*/
private handleConvertFromAllDay(cloneElement: HTMLElement): void {
console.log('🔄 AllDayManager: Converting from all-day back to timed', {
eventId: cloneElement.dataset.eventId
});
// Remove all-day CSS class
cloneElement.classList.remove('all-day-style');
// Reset grid positioning
cloneElement.style.gridColumn = '';
cloneElement.style.gridRow = '';
// Remove all-day date attribute
delete cloneElement.dataset.allDayDate;
console.log('✅ AllDayManager: Converted from all-day back to timed');
}
/** /**
* Handle drag start for all-day events * Handle drag start for all-day events
@ -439,49 +433,114 @@ export class AllDayManager {
} }
/** /**
* Handle drag move for all-day events * Handle drag move for all-day events - SPECIALIZED FOR ALL-DAY CONTAINER
*/ */
private handleDragMove(dragClone: HTMLElement, mousePosition: MousePosition): void { private handleColumnChange(dragClone: HTMLElement, mousePosition: MousePosition): void {
// Calculate grid column based on mouse position // Get the all-day container to understand its grid structure
const dayHeaders = document.querySelectorAll('swp-day-header'); const allDayContainer = this.getAllDayContainer();
let targetColumn = 1; if (!allDayContainer) return;
dayHeaders.forEach((header, index) => { // Calculate target column using ColumnDetectionUtils
const rect = header.getBoundingClientRect(); const targetColumn = ColumnDetectionUtils.getColumnIndexFromX(mousePosition.x);
if (mousePosition.x >= rect.left && mousePosition.x <= rect.right) {
targetColumn = index + 1;
}
});
// Update clone position // Update clone position - ALWAYS keep in row 1 during drag
// Use simple grid positioning that matches all-day container structure
dragClone.style.gridColumn = targetColumn.toString(); dragClone.style.gridColumn = targetColumn.toString();
dragClone.style.gridRow = '1'; // Force row 1 during drag
dragClone.style.gridArea = `1 / ${targetColumn} / 2 / ${targetColumn + 1}`;
console.log('🔄 AllDayManager: Updated drag clone position', { console.log('🔄 AllDayManager: Updated all-day drag clone position', {
eventId: dragClone.dataset.eventId, eventId: dragClone.dataset.eventId,
targetColumn, targetColumn,
gridRow: 1,
gridArea: dragClone.style.gridArea,
mouseX: mousePosition.x mouseX: mousePosition.x
}); });
} }
/** /**
* Handle drag end for all-day events * Handle drag end for all-day events - WITH DIFFERENTIAL UPDATES
*/ */
private handleDragEnd(originalElement: HTMLElement, dragClone: HTMLElement, finalPosition: { column: string; y: number }): void { private handleDragEnd(originalElement: HTMLElement, dragClone: HTMLElement, finalPosition: { column: string; y: number }): void {
// Normalize clone console.log('🎯 AllDayManager: Starting drag end with differential updates', {
eventId: dragClone.dataset.eventId,
finalColumn: finalPosition.column
});
// 1. Store current layouts BEFORE any changes
this.storeCurrentLayouts();
// 2. Normalize clone ID
const cloneId = dragClone.dataset.eventId; const cloneId = dragClone.dataset.eventId;
if (cloneId?.startsWith('clone-')) { if (cloneId?.startsWith('clone-')) {
dragClone.dataset.eventId = cloneId.replace('clone-', ''); dragClone.dataset.eventId = cloneId.replace('clone-', '');
} }
// Remove dragging styles // 3. Create temporary array with existing events + the dropped event
const droppedEventId = dragClone.dataset.eventId || '';
const droppedEventDate = dragClone.dataset.allDayDate || finalPosition.column;
const droppedEvent: CalendarEvent = {
id: droppedEventId,
title: dragClone.dataset.title || dragClone.textContent || '',
start: new Date(droppedEventDate),
end: new Date(droppedEventDate),
type: 'work',
allDay: true,
syncStatus: 'synced'
};
// Use current events + dropped event for calculation
const tempEvents = [...this.currentAllDayEvents, droppedEvent];
// 4. Calculate new layouts for ALL events
const newLayouts = this.calculateAllDayEventsLayout(tempEvents, this.currentWeekDates);
// 5. Apply differential updates - only update events that changed
let changedCount = 0;
newLayouts.forEach((layout, eventId) => {
const oldGridArea = this.currentLayouts.get(eventId);
const newGridArea = layout.gridArea;
if (oldGridArea !== newGridArea) {
changedCount++;
const element = document.querySelector(`[data-event-id="${eventId}"]`) as HTMLElement;
if (element) {
console.log('🔄 AllDayManager: Updating event position', {
eventId,
oldGridArea,
newGridArea
});
// Add transition class for smooth animation
element.classList.add('transitioning');
element.style.gridArea = newGridArea;
element.style.gridRow = layout.row.toString();
element.style.gridColumn = `${layout.startColumn} / ${layout.endColumn + 1}`;
// Remove transition class after animation
setTimeout(() => element.classList.remove('transitioning'), 200);
}
}
});
// 6. Clean up drag styles from the dropped clone
dragClone.classList.remove('dragging'); dragClone.classList.remove('dragging');
dragClone.style.zIndex = ''; dragClone.style.zIndex = '';
dragClone.style.cursor = ''; dragClone.style.cursor = '';
dragClone.style.opacity = ''; dragClone.style.opacity = '';
console.log('✅ AllDayManager: Completed drag operation for all-day event', { // 7. Restore original element opacity
eventId: dragClone.dataset.eventId, originalElement.style.opacity = '';
finalColumn: dragClone.style.gridColumn
// 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
}); });
} }

View file

@ -6,12 +6,14 @@
import { IEventBus } from '../types/CalendarTypes'; import { IEventBus } from '../types/CalendarTypes';
import { calendarConfig } from '../core/CalendarConfig'; import { calendarConfig } from '../core/CalendarConfig';
import { PositionUtils } from '../utils/PositionUtils'; import { PositionUtils } from '../utils/PositionUtils';
import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
import { import {
DragStartEventPayload, DragStartEventPayload,
DragMoveEventPayload, DragMoveEventPayload,
DragEndEventPayload, DragEndEventPayload,
DragMouseEnterHeaderEventPayload, DragMouseEnterHeaderEventPayload,
DragMouseLeaveHeaderEventPayload DragMouseLeaveHeaderEventPayload,
DragColumnChangeEventPayload
} from '../types/EventTypes'; } from '../types/EventTypes';
interface CachedElements { interface CachedElements {
@ -25,11 +27,6 @@ interface Position {
y: number; y: number;
} }
interface ColumnBounds {
date: string;
left: number;
right: number;
}
export class DragDropManager { export class DragDropManager {
private eventBus: IEventBus; private eventBus: IEventBus;
@ -59,8 +56,6 @@ export class DragDropManager {
lastColumnDate: null lastColumnDate: null
}; };
// Column bounds cache for coordinate-based column detection
private columnBoundsCache: ColumnBounds[] = [];
// Auto-scroll properties // Auto-scroll properties
@ -120,16 +115,16 @@ export class DragDropManager {
} }
// Initialize column bounds cache // Initialize column bounds cache
this.updateColumnBoundsCache(); ColumnDetectionUtils.updateColumnBoundsCache();
// Listen to resize events to update cache // Listen to resize events to update cache
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
this.updateColumnBoundsCache(); ColumnDetectionUtils.updateColumnBoundsCache();
}); });
// Listen to navigation events to update cache // Listen to navigation events to update cache
this.eventBus.on('navigation:completed', () => { this.eventBus.on('navigation:completed', () => {
this.updateColumnBoundsCache(); ColumnDetectionUtils.updateColumnBoundsCache();
}); });
} }
@ -247,12 +242,13 @@ export class DragDropManager {
const previousColumn = this.currentColumn; const previousColumn = this.currentColumn;
this.currentColumn = newColumn; this.currentColumn = newColumn;
this.eventBus.emit('drag:column-change', { const dragColumnChangePayload: DragColumnChangeEventPayload = {
draggedElement: this.draggedElement, draggedElement: this.draggedElement,
previousColumn, previousColumn,
newColumn, newColumn,
mousePosition: currentPosition mousePosition: currentPosition
}); };
this.eventBus.emit('drag:column-change', dragColumnChangePayload);
} }
} }
} }
@ -377,58 +373,13 @@ export class DragDropManager {
return Math.max(0, snappedY); return Math.max(0, snappedY);
} }
/**
* Update column bounds cache for coordinate-based column detection
*/
private updateColumnBoundsCache(): void {
// Reset cache
this.columnBoundsCache = [];
// Find alle kolonner
const columns = document.querySelectorAll('swp-day-column');
// Cache hver kolonnes x-grænser
columns.forEach(column => {
const rect = column.getBoundingClientRect();
const date = (column as HTMLElement).dataset.date;
if (date) {
this.columnBoundsCache.push({
date,
left: rect.left,
right: rect.right
});
}
});
// Sorter efter x-position (fra venstre til højre)
this.columnBoundsCache.sort((a, b) => a.left - b.left);
}
/**
* Get column date from X coordinate using cached bounds
*/
private getColumnDateFromX(x: number): string | null {
// Opdater cache hvis tom
if (this.columnBoundsCache.length === 0) {
this.updateColumnBoundsCache();
}
// Find den kolonne hvor x-koordinaten er indenfor grænserne
const column = this.columnBoundsCache.find(col =>
x >= col.left && x <= col.right
);
return column ? column.date : null;
}
/** /**
* Coordinate-based column detection (replaces DOM traversal) * Coordinate-based column detection (replaces DOM traversal)
*/ */
private detectColumn(mouseX: number, mouseY: number): string | null { private detectColumn(mouseX: number, mouseY: number): string | null {
// Brug den koordinatbaserede metode direkte // Brug den koordinatbaserede metode direkte
const columnDate = this.getColumnDateFromX(mouseX); const columnDate = ColumnDetectionUtils.getColumnDateFromX(mouseX);
// Opdater stadig den eksisterende cache hvis vi finder en kolonne // Opdater stadig den eksisterende cache hvis vi finder en kolonne
if (columnDate && columnDate !== this.cachedElements.lastColumnDate) { if (columnDate && columnDate !== this.cachedElements.lastColumnDate) {
@ -610,7 +561,7 @@ export class DragDropManager {
this.isInHeader = true; this.isInHeader = true;
// Calculate target date using existing method // Calculate target date using existing method
const targetDate = this.getColumnDateFromX(event.clientX); const targetDate = ColumnDetectionUtils.getColumnDateFromX(event.clientX);
if (targetDate) { if (targetDate) {
console.log('🎯 DragDropManager: Emitting drag:mouseenter-header', { targetDate }); console.log('🎯 DragDropManager: Emitting drag:mouseenter-header', { targetDate });
@ -636,7 +587,7 @@ export class DragDropManager {
console.log('🚪 DragDropManager: Emitting drag:mouseleave-header'); console.log('🚪 DragDropManager: Emitting drag:mouseleave-header');
// Calculate target date using existing method // Calculate target date using existing method
const targetDate = this.getColumnDateFromX(event.clientX); const targetDate = ColumnDetectionUtils.getColumnDateFromX(event.clientX);
// Find clone element (if it exists) // Find clone element (if it exists)
const eventId = this.draggedElement?.dataset.eventId; const eventId = this.draggedElement?.dataset.eventId;

View file

@ -8,7 +8,7 @@ import { AllDayManager } from '../managers/AllDayManager';
import { EventRendererStrategy } from './EventRenderer'; import { EventRendererStrategy } from './EventRenderer';
import { SwpEventElement } from '../elements/SwpEventElement'; import { SwpEventElement } from '../elements/SwpEventElement';
import { AllDayEventRenderer } from './AllDayEventRenderer'; import { AllDayEventRenderer } from './AllDayEventRenderer';
import { DragStartEventPayload, DragMoveEventPayload, DragEndEventPayload, DragMouseEnterHeaderEventPayload, DragMouseLeaveHeaderEventPayload, HeaderReadyEventPayload } from '../types/EventTypes'; import { DragStartEventPayload, DragMoveEventPayload, DragEndEventPayload, DragMouseEnterHeaderEventPayload, DragMouseLeaveHeaderEventPayload, DragColumnChangeEventPayload, HeaderReadyEventPayload } from '../types/EventTypes';
/** /**
* EventRenderingService - Render events i DOM med positionering using Strategy Pattern * EventRenderingService - Render events i DOM med positionering using Strategy Pattern
* Håndterer event positioning og overlap detection * Håndterer event positioning og overlap detection
@ -190,6 +190,11 @@ export class EventRenderingService {
// Handle drag move // Handle drag move
this.eventBus.on('drag:move', (event: Event) => { this.eventBus.on('drag:move', (event: Event) => {
const { draggedElement, snappedY, column, mouseOffset } = (event as CustomEvent<DragMoveEventPayload>).detail; const { draggedElement, snappedY, column, mouseOffset } = (event as CustomEvent<DragMoveEventPayload>).detail;
// Filter: Only handle events WITHOUT data-allday attribute (normal timed events)
if (draggedElement.hasAttribute('data-allday')) {
return; // This is an all-day event, let AllDayManager handle it
}
if (this.strategy.handleDragMove && column) { if (this.strategy.handleDragMove && column) {
const eventId = draggedElement.dataset.eventId || ''; const eventId = draggedElement.dataset.eventId || '';
this.strategy.handleDragMove(eventId, snappedY, column, mouseOffset); this.strategy.handleDragMove(eventId, snappedY, column, mouseOffset);
@ -241,7 +246,7 @@ export class EventRenderingService {
// Handle column change // Handle column change
this.eventBus.on('drag:column-change', (event: Event) => { this.eventBus.on('drag:column-change', (event: Event) => {
const { draggedElement, newColumn } = (event as CustomEvent).detail; const { draggedElement, newColumn } = (event as CustomEvent<DragColumnChangeEventPayload>).detail;
if (this.strategy.handleColumnChange) { if (this.strategy.handleColumnChange) {
const eventId = draggedElement.dataset.eventId || ''; const eventId = draggedElement.dataset.eventId || '';
this.strategy.handleColumnChange(eventId, newColumn); this.strategy.handleColumnChange(eventId, newColumn);
@ -359,6 +364,9 @@ export class EventRenderingService {
count: weekDates.length count: weekDates.length
}); });
// Pass current events to AllDayManager for state tracking
this.allDayManager.setCurrentEvents(allDayEvents, weekDates);
// Calculate layout for ALL all-day events together using AllDayLayoutEngine // Calculate layout for ALL all-day events together using AllDayLayoutEngine
const layouts = this.allDayManager.calculateAllDayEventsLayout(allDayEvents, weekDates); const layouts = this.allDayManager.calculateAllDayEventsLayout(allDayEvents, weekDates);

View file

@ -87,6 +87,14 @@ export interface DragMouseLeaveHeaderEventPayload {
cloneElement: HTMLElement| null; cloneElement: HTMLElement| null;
} }
// Drag column change event payload
export interface DragColumnChangeEventPayload {
draggedElement: HTMLElement;
previousColumn: string | null;
newColumn: string;
mousePosition: MousePosition;
}
// Header ready event payload // Header ready event payload
export interface HeaderReadyEventPayload { export interface HeaderReadyEventPayload {
headerElement: HTMLElement; headerElement: HTMLElement;

View file

@ -0,0 +1,94 @@
/**
* ColumnDetectionUtils - Shared utility for column detection and caching
* Used by both DragDropManager and AllDayManager for consistent column detection
*/
export interface ColumnBounds {
date: string;
left: number;
right: number;
}
export class ColumnDetectionUtils {
private static columnBoundsCache: ColumnBounds[] = [];
/**
* Update column bounds cache for coordinate-based column detection
*/
public static updateColumnBoundsCache(): void {
// Reset cache
this.columnBoundsCache = [];
// Find alle kolonner
const columns = document.querySelectorAll('swp-day-column');
// Cache hver kolonnes x-grænser
columns.forEach(column => {
const rect = column.getBoundingClientRect();
const date = (column as HTMLElement).dataset.date;
if (date) {
this.columnBoundsCache.push({
date,
left: rect.left,
right: rect.right
});
}
});
// Sorter efter x-position (fra venstre til højre)
this.columnBoundsCache.sort((a, b) => a.left - b.left);
}
/**
* Get column date from X coordinate using cached bounds
*/
public static getColumnDateFromX(x: number): string | null {
// Opdater cache hvis tom
if (this.columnBoundsCache.length === 0) {
this.updateColumnBoundsCache();
}
// Find den kolonne hvor x-koordinaten er indenfor grænserne
const column = this.columnBoundsCache.find(col =>
x >= col.left && x <= col.right
);
return column ? column.date : null;
}
/**
* Get column index (1-based) from date
*/
public static getColumnIndexFromDate(date: string): number {
// Opdater cache hvis tom
if (this.columnBoundsCache.length === 0) {
this.updateColumnBoundsCache();
}
const columnIndex = this.columnBoundsCache.findIndex(col => col.date === date);
return columnIndex >= 0 ? columnIndex + 1 : 1; // 1-based index
}
/**
* Get column index from X coordinate
*/
public static getColumnIndexFromX(x: number): number {
const date = this.getColumnDateFromX(x);
return date ? this.getColumnIndexFromDate(date) : 1;
}
/**
* Clear cache (useful for testing or when DOM structure changes)
*/
public static clearCache(): void {
this.columnBoundsCache = [];
}
/**
* Get current cache for debugging
*/
public static getCache(): ColumnBounds[] {
return [...this.columnBoundsCache];
}
}

View file

@ -125,24 +125,21 @@ swp-resize-handle[data-position="bottom"] {
} }
/* Resize handles controlled by JavaScript - no general hover */ /* Resize handles controlled by JavaScript - no general hover */
swp-handle-hitarea {
/* Hit area */ position: absolute;
swp-handle-hitarea { left: -8px;
position: absolute; right: -8px;
left: -8px; top: -6px;
right: -8px; bottom: -6px;
top: -6px; cursor: ns-resize;
bottom: -6px; }
cursor: ns-resize;
} swp-handle-hitarea[data-position="top"] {
top: 4px;
&[data-position="top"] { }
top: 4px;
} swp-handle-hitarea[data-position="bottom"] {
bottom: 4px;
&[data-position="bottom"] {
bottom: 4px;
}
} }
/* Multi-day events */ /* Multi-day events */
@ -250,3 +247,8 @@ swp-event-group swp-event {
right: 0; right: 0;
margin: 0; margin: 0;
} }
/* All-day event transition for smooth repositioning */
swp-allday-container swp-event.transitioning {
transition: grid-area 200ms ease-out, grid-row 200ms ease-out, grid-column 200ms ease-out;
}