diff --git a/src/factories/CalendarTypeFactory.ts b/src/factories/CalendarTypeFactory.ts index 952be43..96df1af 100644 --- a/src/factories/CalendarTypeFactory.ts +++ b/src/factories/CalendarTypeFactory.ts @@ -3,7 +3,7 @@ import { CalendarMode } from '../types/CalendarTypes'; import { HeaderRenderer, DateHeaderRenderer, ResourceHeaderRenderer } from '../renderers/HeaderRenderer'; import { ColumnRenderer, DateColumnRenderer, ResourceColumnRenderer } from '../renderers/ColumnRenderer'; -import { EventRendererStrategy, DateEventRenderer, ResourceEventRenderer } from '../renderers/EventRenderer'; +import { EventRendererStrategy, DateEventRenderer } from '../renderers/EventRenderer'; import { calendarConfig } from '../core/CalendarConfig'; /** @@ -37,11 +37,11 @@ export class CalendarTypeFactory { eventRenderer: new DateEventRenderer() }); - this.registerRenderers('resource', { - headerRenderer: new ResourceHeaderRenderer(), - columnRenderer: new ResourceColumnRenderer(), - eventRenderer: new ResourceEventRenderer() - }); + //this.registerRenderers('resource', { + // headerRenderer: new ResourceHeaderRenderer(), + // columnRenderer: new ResourceColumnRenderer(), + // eventRenderer: new ResourceEventRenderer() + //}); this.isInitialized = true; } diff --git a/src/managers/AllDayManager.ts b/src/managers/AllDayManager.ts index 6fb10a7..143b8b1 100644 --- a/src/managers/AllDayManager.ts +++ b/src/managers/AllDayManager.ts @@ -44,11 +44,7 @@ export class AllDayManager { this.allDayEventRenderer = new AllDayEventRenderer(); // Sync CSS variable with TypeScript constant to ensure consistency - document.documentElement.style.setProperty( - '--single-row-height', - `${ALL_DAY_CONSTANTS.EVENT_HEIGHT}px` - ); - + document.documentElement.style.setProperty('--single-row-height', `${ALL_DAY_CONSTANTS.EVENT_HEIGHT}px`); this.setupEventListeners(); } @@ -59,6 +55,9 @@ export class AllDayManager { eventBus.on('drag:mouseenter-header', (event) => { const payload = (event as CustomEvent).detail; + if (payload.draggedClone.hasAttribute('data-allday')) + return; + console.log('🔄 AllDayManager: Received drag:mouseenter-header', { targetDate: payload.targetColumn, originalElementId: payload.originalElement?.dataset?.eventId, @@ -75,33 +74,27 @@ export class AllDayManager { originalElementId: originalElement?.dataset?.eventId }); - //this.checkAndAnimateAllDayHeight(); }); // Listen for drag operations on all-day events eventBus.on('drag:start', (event) => { - const { draggedElement, draggedClone, mouseOffset } = (event as CustomEvent).detail; + let payload: DragStartEventPayload = (event as CustomEvent).detail; - // Check if this is an all-day event by checking if it's in all-day container - const isAllDayEvent = draggedElement.closest('swp-allday-container'); - if (!isAllDayEvent) return; // Not an all-day event + if (!payload.draggedClone?.hasAttribute('data-allday')) { + return; + } - const eventId = draggedElement.dataset.eventId; - console.log('🎯 AllDayManager: Starting drag for all-day event', { eventId }); - this.handleDragStart(draggedElement, eventId || '', mouseOffset); + this.allDayEventRenderer.handleDragStart(payload); }); eventBus.on('drag:column-change', (event) => { - const { originalElement: draggedElement, draggedClone, mousePosition } = (event as CustomEvent).detail; + let payload: DragColumnChangeEventPayload = (event as CustomEvent).detail; - if (draggedClone == null) + if (!payload.draggedClone?.hasAttribute('data-allday')) { return; - - if (!draggedClone.hasAttribute('data-allday')) { - return; // This is not an all-day event } - this.handleColumnChange(draggedClone, mousePosition); + this.handleColumnChange(payload); }); eventBus.on('drag:end', (event) => { @@ -320,25 +313,13 @@ export class AllDayManager { */ private handleConvertToAllDay(payload: DragMouseEnterHeaderEventPayload): void { - if (payload.draggedClone?.dataset == null) - console.error("payload.cloneElement.dataset.eventId is null"); - - - console.log('🔄 AllDayManager: Converting to all-day (row 1 only during drag)', { - eventId: payload.draggedClone.dataset.eventId, - targetDate: payload.targetColumn - }); - - // Get all-day container, request creation if needed let allDayContainer = this.getAllDayContainer(); - payload.draggedClone.removeAttribute('style'); payload.draggedClone.style.gridRow = '1'; payload.draggedClone.style.gridColumn = payload.targetColumn.index.toString(); - payload.draggedClone.dataset.allday = 'true'; // Set the all-day attribute for filtering + payload.draggedClone.dataset.allday = 'true'; - // Add to container allDayContainer?.appendChild(payload.draggedClone); ColumnDetectionUtils.updateColumnBoundsCache(); @@ -346,63 +327,36 @@ export class AllDayManager { } - /** - * Handle drag start for all-day events - */ - private handleDragStart(originalElement: HTMLElement, eventId: string, mouseOffset: DragOffset): void { - // Create clone - const clone = originalElement.cloneNode(true) as HTMLElement; - clone.dataset.eventId = `clone-${eventId}`; - - // Get container - const container = this.getAllDayContainer(); - if (!container) return; - - // Add clone to container - container.appendChild(clone); - - // Copy positioning from original - clone.style.gridColumn = originalElement.style.gridColumn; - clone.style.gridRow = originalElement.style.gridRow; - - // Add dragging style - clone.classList.add('dragging'); - clone.style.zIndex = '1000'; - clone.style.cursor = 'grabbing'; - - // Make original semi-transparent - originalElement.style.opacity = '0.3'; - - console.log('✅ AllDayManager: Created drag clone for all-day event', { - eventId, - cloneId: clone.dataset.eventId, - gridColumn: clone.style.gridColumn, - gridRow: clone.style.gridRow - }); - } - /** * Handle drag move for all-day events - SPECIALIZED FOR ALL-DAY CONTAINER */ - private handleColumnChange(dragClone: HTMLElement, mousePosition: MousePosition): void { - // Get the all-day container to understand its grid structure - const allDayContainer = this.getAllDayContainer(); + private handleColumnChange(dragColumnChangeEventPayload: DragColumnChangeEventPayload): void { + + let allDayContainer = this.getAllDayContainer(); if (!allDayContainer) return; - // Calculate target column using ColumnDetectionUtils - const targetColumn = ColumnDetectionUtils.getColumnBounds(mousePosition); + let targetColumn = ColumnDetectionUtils.getColumnBounds(dragColumnChangeEventPayload.mousePosition); if (targetColumn == null) return; + if (!dragColumnChangeEventPayload.draggedClone) + return; + // Update clone position - ALWAYS keep in row 1 during drag // Use simple grid positioning that matches all-day container structure - dragClone.style.gridColumn = targetColumn.index.toString(); - //dragClone.style.gridRow = '1'; // Force row 1 during drag - dragClone.style.gridArea = `1 / ${targetColumn.index} / 2 / ${targetColumn.index + 1}`; + dragColumnChangeEventPayload.draggedClone.style.gridColumn = targetColumn.index.toString(); + //dragColumnChangeEventPayload.draggedClone.style.gridRow = dragColumnChangeEventPayload.draggedClone.style.gridRow; // Bevar nuværende row } + private fadeOutAndRemove(element: HTMLElement): void { + element.style.transition = 'opacity 0.3s ease-out'; + element.style.opacity = '0'; + setTimeout(() => { + element.remove(); + }, 300); + } /** * Handle drag end for all-day events - WITH DIFFERENTIAL UPDATES */ @@ -414,6 +368,7 @@ export class AllDayManager { // 2. Normalize clone ID dragEndEvent.draggedClone.dataset.eventId = dragEndEvent.draggedClone.dataset.eventId?.replace('clone-', ''); + // 3. Create temporary array with existing events + the dropped event let eventId = dragEndEvent.draggedClone.dataset.eventId; let eventDate = dragEndEvent.finalPosition.column?.date; @@ -423,6 +378,7 @@ export class AllDayManager { return; + const droppedEvent: CalendarEvent = { id: eventId, title: dragEndEvent.draggedClone.dataset.title || '', @@ -434,27 +390,29 @@ export class AllDayManager { }; // Use current events + dropped event for calculation - const tempEvents = [...this.currentAllDayEvents, droppedEvent]; + const tempEvents = [...this.currentAllDayEvents, droppedEvent].except(dragEndEvent.originalElement); // 4. Calculate new layouts for ALL events this.newLayouts = this.calculateAllDayEventsLayout(tempEvents, this.currentWeekDates); // 5. Apply differential updates - only update events that changed let changedCount = 0; + let container = this.getAllDayContainer(); this.newLayouts.forEach((layout) => { // Find current layout for this event let currentLayout = this.currentLayouts.find(old => old.calenderEvent.id === layout.calenderEvent.id); if (currentLayout?.gridArea !== layout.gridArea) { changedCount++; - const element = dragEndEvent.draggedClone; + let element = container?.querySelector(`[data-event-id="${layout.calenderEvent.id}"]`) as HTMLElement; if (element) { - // Add transition class for smooth animation + element.classList.add('transitioning'); element.style.gridArea = layout.gridArea; element.style.gridRow = layout.row.toString(); element.style.gridColumn = `${layout.startColumn} / ${layout.endColumn + 1}`; + if (layout.row > ALL_DAY_CONSTANTS.MAX_COLLAPSED_ROWS) if (!this.isExpanded) element.classList.add('max-event-overflow-hide'); @@ -477,7 +435,8 @@ export class AllDayManager { dragEndEvent.draggedClone.style.opacity = ''; // 7. Restore original element opacity - dragEndEvent.originalElement.remove(); //TODO: this should be an event that only fade and remove if confirmed dragdrop + //dragEndEvent.originalElement.remove(); //TODO: this should be an event that only fade and remove if confirmed dragdrop + this.fadeOutAndRemove(dragEndEvent.originalElement); // 8. Check if height adjustment is needed this.checkAndAnimateAllDayHeight(); diff --git a/src/managers/DragDropManager.ts b/src/managers/DragDropManager.ts index d1679af..f074504 100644 --- a/src/managers/DragDropManager.ts +++ b/src/managers/DragDropManager.ts @@ -211,31 +211,32 @@ export class DragDropManager { // Continue with normal drag behavior only if drag has started if (this.isDragStarted && this.draggedElement && this.draggedClone) { - const deltaY = Math.abs(currentPosition.y - this.lastLoggedPosition.y); + if (!this.draggedElement.hasAttribute("data-allday")) { + const deltaY = Math.abs(currentPosition.y - this.lastLoggedPosition.y); - // Check for snap interval vertical movement (normal drag behavior) - if (deltaY >= this.snapDistancePx) { - this.lastLoggedPosition = currentPosition; + // Check for snap interval vertical movement (normal drag behavior) + if (deltaY >= this.snapDistancePx) { + this.lastLoggedPosition = currentPosition; - // Consolidated position calculations with snapping for normal drag - const positionData = this.calculateDragPosition(currentPosition); + // Consolidated position calculations with snapping for normal drag + const positionData = this.calculateDragPosition(currentPosition); - // Emit drag move event with snapped position (normal behavior) - const dragMovePayload: DragMoveEventPayload = { - draggedElement: this.draggedElement, - draggedClone: this.draggedClone, - mousePosition: currentPosition, - snappedY: positionData.snappedY, - columnBounds: positionData.column, - mouseOffset: this.mouseOffset - }; - this.eventBus.emit('drag:move', dragMovePayload); + // Emit drag move event with snapped position (normal behavior) + const dragMovePayload: DragMoveEventPayload = { + draggedElement: this.draggedElement, + draggedClone: this.draggedClone, + mousePosition: currentPosition, + snappedY: positionData.snappedY, + columnBounds: positionData.column, + mouseOffset: this.mouseOffset + }; + this.eventBus.emit('drag:move', dragMovePayload); + } + + // Check for auto-scroll + this.checkAutoScroll(currentPosition); } - // Check for auto-scroll - this.checkAutoScroll(currentPosition); - - // Check for column change using cached data const newColumn = ColumnDetectionUtils.getColumnBounds(currentPosition); if (newColumn == null) return; @@ -294,8 +295,8 @@ export class DragDropManager { target: dropTarget }; this.eventBus.emit('drag:end', dragEndPayload); - - + + this.draggedElement = null; } else { diff --git a/src/renderers/AllDayEventRenderer.ts b/src/renderers/AllDayEventRenderer.ts index 63e977a..c3b9fe8 100644 --- a/src/renderers/AllDayEventRenderer.ts +++ b/src/renderers/AllDayEventRenderer.ts @@ -3,21 +3,20 @@ import { SwpAllDayEventElement } from '../elements/SwpEventElement'; import { EventLayout } from '../utils/AllDayLayoutEngine'; import { ColumnBounds } from '../utils/ColumnDetectionUtils'; import { EventManager } from '../managers/EventManager'; -/** - * 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 - */ +import { DragStartEventPayload } from '../types/EventTypes'; +import { EventRendererStrategy } from './EventRenderer'; + export class AllDayEventRenderer { + private container: HTMLElement | null = null; + private originalEvent: HTMLElement | null = null; + private draggedClone: HTMLElement | null = null; constructor() { this.getContainer(); } - /** - * Get or cache all-day container, create if it doesn't exist - SIMPLIFIED (no ghost columns) - */ + private getContainer(): HTMLElement | null { const header = document.querySelector('swp-calendar-header'); @@ -27,14 +26,45 @@ export class AllDayEventRenderer { if (!this.container) { this.container = document.createElement('swp-allday-container'); header.appendChild(this.container); - } } return this.container; - } - // REMOVED: createGhostColumns() method - no longer needed! + + private getAllDayContainer(): HTMLElement | null { + return document.querySelector('swp-calendar-header swp-allday-container'); + } + /** + * Handle drag start for all-day events + */ + public handleDragStart(payload: DragStartEventPayload): void { + + this.originalEvent = payload.draggedElement;; + this.draggedClone = payload.draggedClone; + + if (this.draggedClone) { + + const container = this.getAllDayContainer(); + if (!container) return; + + this.draggedClone.style.gridColumn = this.originalEvent.style.gridColumn; + this.draggedClone.style.gridRow = this.originalEvent.style.gridRow; + console.log('handleDragStart:this.draggedClone', this.draggedClone); + container.appendChild(this.draggedClone); + + // Add dragging style + this.draggedClone.classList.add('dragging'); + this.draggedClone.style.zIndex = '1000'; + this.draggedClone.style.cursor = 'grabbing'; + + // Make original semi-transparent + this.originalEvent.style.opacity = '0.3'; + this.originalEvent.style.userSelect = 'none'; + } + } + + /** * Render an all-day event with pre-calculated layout @@ -77,28 +107,14 @@ export class AllDayEventRenderer { * Render all-day events for specific period using AllDayEventRenderer */ public renderAllDayEventsForPeriod(eventLayouts: EventLayout[]): void { - // Get events from EventManager for the period - // const events = this.eventManager.getEventsForPeriod(startDate, endDate); - - - - // Clear existing all-day events first this.clearAllDayEvents(); - // Get actual visible dates from DOM headers instead of generating them - - // const layouts = this.allDayManager.initAllDayEventsLayout(allDayEvents, weekDates); - - // Render each all-day event with pre-calculated layout eventLayouts.forEach(layout => { this.renderAllDayEventWithLayout(layout.calenderEvent, layout); }); - } - /** - * Clear only all-day events - */ + private clearAllDayEvents(): void { const allDayContainer = document.querySelector('swp-allday-container'); if (allDayContainer) { diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts index fd2e5a1..75a2068 100644 --- a/src/renderers/EventRenderer.ts +++ b/src/renderers/EventRenderer.ts @@ -26,25 +26,32 @@ export interface EventRendererStrategy { handleColumnChange?(payload: DragColumnChangeEventPayload): void; handleNavigationCompleted?(): void; } +// Abstract methods that subclasses must implement +// private getColumns(container: HTMLElement): HTMLElement[]; +// private getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[]; + /** - * Base class for event renderers with common functionality + * Date-based event renderer */ -export abstract class BaseEventRenderer implements EventRendererStrategy { - protected dateCalculator: DateCalculator; +export class DateEventRenderer implements EventRendererStrategy { - // Drag and drop state - private draggedClone: HTMLElement | null = null; - private originalEvent: HTMLElement | null = null; - - // Resize manager constructor(dateCalculator?: DateCalculator) { + if (!dateCalculator) { DateCalculator.initialize(calendarConfig); } this.dateCalculator = dateCalculator || new DateCalculator(); + + + this.setupDragEventListeners(); } + private dateCalculator: DateCalculator; + + private draggedClone: HTMLElement | null = null; + private originalEvent: HTMLElement | null = null; + // ============================================ // NEW OVERLAP DETECTION SYSTEM @@ -96,10 +103,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { }); } - - /** - * Apply common drag styling to an element - */ + private applyDragStyling(element: HTMLElement): void { element.classList.add('dragging'); } @@ -127,8 +131,8 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { // Snap to interval const snappedStartMinutes = Math.round(actualStartMinutes / snapInterval) * snapInterval; - - if(!clone.dataset.originalDuration) + + if (!clone.dataset.originalDuration) throw new DOMException("missing clone.dataset.originalDuration") const endTotalMinutes = snappedStartMinutes + parseInt(clone.dataset.originalDuration); @@ -153,7 +157,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { this.draggedClone = payload.draggedClone; if (this.draggedClone) { - // Apply drag styling + // Apply drag styling this.applyDragStyling(this.draggedClone); // Add to current column's events layer (not directly to column) @@ -296,7 +300,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { // Fade out original // 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. - this.fadeOutAndRemove(originalElement); + this.fadeOutAndRemove(originalElement); // Remove clone prefix and normalize clone to be a regular event const cloneId = draggedClone.dataset.eventId; @@ -452,17 +456,14 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { }); } - // Abstract methods that subclasses must implement - protected abstract getColumns(container: HTMLElement): HTMLElement[]; - protected abstract getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[]; - protected renderEvent(event: CalendarEvent): HTMLElement { + private renderEvent(event: CalendarEvent): HTMLElement { const swpEvent = SwpEventElement.fromCalendarEvent(event); const eventElement = swpEvent.getElement(); // Setup resize handles on first mouseover only - eventElement.addEventListener('mouseover', () => { + eventElement.addEventListener('mouseover', () => { // TODO: This is not the correct way... we should not add eventlistener on every event if (eventElement.dataset.hasResizeHandlers !== 'true') { eventElement.dataset.hasResizeHandlers = 'true'; } @@ -544,16 +545,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { element.style.minWidth = '50px'; }); } -} -/** - * Date-based event renderer - */ -export class DateEventRenderer extends BaseEventRenderer { - constructor(dateCalculator?: DateCalculator) { - super(dateCalculator); - this.setupDragEventListeners(); - } /** * Setup drag event listeners - placeholder method @@ -585,32 +577,3 @@ export class DateEventRenderer extends BaseEventRenderer { return columnEvents; } } - -/** - * Resource-based event renderer - */ -export class ResourceEventRenderer extends BaseEventRenderer { - protected getColumns(container: HTMLElement): HTMLElement[] { - const columns = container.querySelectorAll('swp-resource-column'); - return Array.from(columns) as HTMLElement[]; - } - - protected getEventsForColumn(column: HTMLElement, events: CalendarEvent[]): CalendarEvent[] { - const resourceName = column.dataset.resource; - if (!resourceName) return []; - - const columnEvents = events.filter(event => { - return event.resource?.name === resourceName; - }); - - return columnEvents; - } - - // ============================================ - // NEW OVERLAP DETECTION SYSTEM - // All new functions prefixed with new_ - // ============================================ - - protected overlapDetector = new OverlapDetector(); - -} \ No newline at end of file diff --git a/src/renderers/EventRendererManager.ts b/src/renderers/EventRendererManager.ts index 074bdd7..ad35e84 100644 --- a/src/renderers/EventRendererManager.ts +++ b/src/renderers/EventRendererManager.ts @@ -142,29 +142,29 @@ export class EventRenderingService { * Setup all drag event listeners - moved from EventRenderer for better separation of concerns */ private setupDragEventListeners(): void { - // Handle drag start this.eventBus.on('drag:start', (event: Event) => { const dragStartPayload = (event as CustomEvent).detail; - // Use the draggedElement directly - no need for DOM query + + if (dragStartPayload.draggedElement.hasAttribute('data-allday')) { + return; + } + if (dragStartPayload.draggedElement && this.strategy.handleDragStart && dragStartPayload.columnBounds) { this.strategy.handleDragStart(dragStartPayload); } }); - // Handle drag move this.eventBus.on('drag:move', (event: Event) => { let dragEvent = (event as CustomEvent).detail; - // Filter: Only handle events WITHOUT data-allday attribute (normal timed events) if (dragEvent.draggedElement.hasAttribute('data-allday')) { - return; // This is an all-day event, let AllDayManager handle it + return; } if (this.strategy.handleDragMove) { this.strategy.handleDragMove(dragEvent); } }); - // Handle drag auto-scroll this.eventBus.on('drag:auto-scroll', (event: Event) => { const { draggedElement, snappedY } = (event as CustomEvent).detail; if (this.strategy.handleDragAutoScroll) {