From 8b8a1e31274d71cfc0d2f5c1f4080a77566da7ee Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Wed, 8 Oct 2025 00:58:38 +0200 Subject: [PATCH] wip, resize, debugging --- src/elements/SwpEventElement.ts | 35 +++++- src/factories/ManagerFactory.ts | 5 +- src/managers/AllDayManager.ts | 1 + src/managers/DragDropManager.ts | 89 ++++++++++++++- src/managers/ResizeHandleManager.ts | 163 +++++++++++++++++++++++++++- src/renderers/EventRenderer.ts | 1 + wwwroot/css/calendar-events-css.css | 4 +- 7 files changed, 289 insertions(+), 9 deletions(-) diff --git a/src/elements/SwpEventElement.ts b/src/elements/SwpEventElement.ts index 16ff270..5dc5367 100644 --- a/src/elements/SwpEventElement.ts +++ b/src/elements/SwpEventElement.ts @@ -126,6 +126,31 @@ export class SwpEventElement extends BaseSwpEventElement { this.end = endDate; } + /** + * Update event height during resize + * @param newHeight - The new height in pixels + */ + public updateHeight(newHeight: number): void { + // 1. Update visual height + this.style.height = `${newHeight}px`; + + // 2. Calculate new end time based on height + const gridSettings = calendarConfig.getGridSettings(); + const { hourHeight } = gridSettings; + + // Get current start time + const start = this.start; + + // Calculate duration from height + const durationMinutes = (newHeight / hourHeight) * 60; + + // Calculate new end time by adding duration to start (using DateService for timezone safety) + const endDate = this.dateService.addMinutes(start, durationMinutes); + + // 3. Update end attribute (triggers attributeChangedCallback → updateDisplay) + this.end = endDate; + } + /** * Create a clone for drag operations */ @@ -135,6 +160,9 @@ export class SwpEventElement extends BaseSwpEventElement { // Apply "clone-" prefix to ID clone.dataset.eventId = `clone-${this.eventId}`; + // Disable pointer events on clone so it doesn't interfere with hover detection + clone.style.pointerEvents = 'none'; + // Cache original duration const timeEl = this.querySelector('swp-event-time'); if (timeEl) { @@ -316,10 +344,13 @@ export class SwpAllDayEventElement extends BaseSwpEventElement { */ public createClone(): SwpAllDayEventElement { const clone = this.cloneNode(true) as SwpAllDayEventElement; - + // Apply "clone-" prefix to ID clone.dataset.eventId = `clone-${this.eventId}`; - + + // Disable pointer events on clone so it doesn't interfere with hover detection + clone.style.pointerEvents = 'none'; + return clone; } diff --git a/src/factories/ManagerFactory.ts b/src/factories/ManagerFactory.ts index c98f00e..2036dd0 100644 --- a/src/factories/ManagerFactory.ts +++ b/src/factories/ManagerFactory.ts @@ -73,7 +73,10 @@ export class ManagerFactory { try { await managers.calendarManager.initialize?.(); - managers.resizeHandleManager.initialize(); + // ResizeHandleManager temporarily disabled for testing + // if (managers.resizeHandleManager && managers.resizeHandleManager.initialize) { + // managers.resizeHandleManager.initialize(); + // } } catch (error) { throw error; } diff --git a/src/managers/AllDayManager.ts b/src/managers/AllDayManager.ts index e0bde5d..97d538c 100644 --- a/src/managers/AllDayManager.ts +++ b/src/managers/AllDayManager.ts @@ -401,6 +401,7 @@ export class AllDayManager { // 2. Normalize clone ID dragEndEvent.draggedClone.dataset.eventId = dragEndEvent.draggedClone.dataset.eventId?.replace('clone-', ''); + dragEndEvent.draggedClone.style.pointerEvents = ''; // Re-enable pointer events dragEndEvent.originalElement.dataset.eventId += '_'; let eventId = dragEndEvent.draggedClone.dataset.eventId; diff --git a/src/managers/DragDropManager.ts b/src/managers/DragDropManager.ts index 1d03d42..98ff34c 100644 --- a/src/managers/DragDropManager.ts +++ b/src/managers/DragDropManager.ts @@ -42,6 +42,10 @@ export class DragDropManager { private currentColumnBounds: ColumnBounds | null = null; private isDragStarted = false; + // Hover state + private isHoverTrackingActive = false; + private currentHoveredEvent: HTMLElement | null = null; + // Movement threshold to distinguish click from drag private readonly dragThreshold = 5; // pixels @@ -106,6 +110,23 @@ export class DragDropManager { const target = e.target as HTMLElement; if (target.closest('swp-calendar-header')) { this.handleHeaderMouseEnter(e as MouseEvent); + } else if (target.closest('swp-event')) { + // Entered an event - activate hover tracking and set color + const eventElement = target.closest('swp-event'); + const mouseEvent = e as MouseEvent; + + // Only handle hover if mouse button is up + if (eventElement && !this.isDragStarted && mouseEvent.buttons === 0) { + // Clear any previous hover first + if (this.currentHoveredEvent && this.currentHoveredEvent !== eventElement) { + this.currentHoveredEvent.style.backgroundColor = ''; + } + + this.isHoverTrackingActive = true; + this.currentHoveredEvent = eventElement; + eventElement.style.backgroundColor = 'red'; + console.log('🎨 Mouse entered event:', eventElement.dataset.eventId, 'buttons:', mouseEvent.buttons, 'isDragStarted:', this.isDragStarted); + } } }, true); // Use capture phase @@ -114,6 +135,7 @@ export class DragDropManager { if (target.closest('swp-calendar-header')) { this.handleHeaderMouseLeave(e as MouseEvent); } + // Don't handle swp-event mouseleave here - let mousemove handle it }, true); // Use capture phase } @@ -154,8 +176,20 @@ export class DragDropManager { } - // Found an event - prepare for potential dragging + // Found an event - check if in resize zone first if (eventElement) { + // Check if click is in bottom resize zone + const rect = eventElement.getBoundingClientRect(); + const mouseY = event.clientY; + const distanceFromBottom = rect.bottom - mouseY; + const resizeZoneHeight = 15; // Match ResizeHandleManager + + // If in resize zone, don't handle this - let ResizeHandleManager take over + if (distanceFromBottom >= 0 && distanceFromBottom <= resizeZoneHeight) { + return; // Exit early - this is a resize operation + } + + // Normal drag - prepare for potential dragging this.draggedElement = eventElement; this.lastColumn = ColumnDetectionUtils.getColumnBounds(this.lastMousePosition) // Calculate mouse offset within event @@ -174,7 +208,19 @@ export class DragDropManager { this.currentMouseY = event.clientY; this.lastMousePosition = { x: event.clientX, y: event.clientY }; + // Log which element we're over during drag + if (this.isDragStarted) { + const elementAtPoint = document.elementFromPoint(event.clientX, event.clientY); + const eventElement = elementAtPoint?.closest('swp-event'); + if (eventElement) { + console.log('🖱️ Dragging over event:', (eventElement as HTMLElement).dataset.eventId); + } + } + // Check for event hover (coordinate-based) - only when mouse button is up + if (this.isHoverTrackingActive && event.buttons === 0) { + this.checkEventHover(event); + } if (event.buttons === 1) { const currentPosition: MousePosition = { x: event.clientX, y: event.clientY }; @@ -189,6 +235,16 @@ export class DragDropManager { // Start drag - emit drag:start event this.isDragStarted = true; + // Set high z-index on event-group if exists, otherwise on event itself + const eventGroup = this.draggedElement.closest('swp-event-group'); + if (eventGroup) { + console.log('🔝 Setting z-index 9999 on event-group', eventGroup); + eventGroup.style.zIndex = '9999'; + } else { + console.log('🔝 Setting z-index 9999 on event', this.draggedElement.dataset.eventId); + this.draggedElement.style.zIndex = '9999'; + } + // Detect current column this.currentColumnBounds = ColumnDetectionUtils.getColumnBounds(currentPosition); @@ -542,4 +598,35 @@ export class DragDropManager { }; this.eventBus.emit('drag:mouseleave-header', dragMouseLeavePayload); } + + private checkEventHover(event: MouseEvent): void { + // Use currentHoveredEvent to check if mouse is still within bounds + if (!this.currentHoveredEvent) return; + + const rect = this.currentHoveredEvent.getBoundingClientRect(); + const mouseX = event.clientX; + const mouseY = event.clientY; + + // Check if mouse is still within the current hovered event + const isStillInside = mouseX >= rect.left && mouseX <= rect.right && + mouseY >= rect.top && mouseY <= rect.bottom; + + // If mouse left the event + if (!isStillInside) { + // Only disable tracking and clear if mouse is NOT pressed (allow resize to work) + if (event.buttons === 0) { + console.log('🚪 Mouse left event:', this.currentHoveredEvent.dataset.eventId, 'clearing hover'); + this.isHoverTrackingActive = false; + this.clearEventHover(); + } + } + } + + private clearEventHover(): void { + if (this.currentHoveredEvent) { + this.currentHoveredEvent.style.backgroundColor = ''; + this.currentHoveredEvent = null; + } + } + } diff --git a/src/managers/ResizeHandleManager.ts b/src/managers/ResizeHandleManager.ts index df14582..9fd66b6 100644 --- a/src/managers/ResizeHandleManager.ts +++ b/src/managers/ResizeHandleManager.ts @@ -1,10 +1,30 @@ import { eventBus } from '../core/EventBus'; import { CoreEvents } from '../constants/CoreEvents'; +import { calendarConfig } from '../core/CalendarConfig'; export class ResizeHandleManager { private resizeZoneHeight = 15; // Must match CSS ::after height private cachedEvents: HTMLElement[] = []; + // Resize state + private isResizing = false; + private resizingElement: HTMLElement | null = null; + private initialHeight = 0; + private initialMouseY = 0; + private targetHeight = 0; + private currentHeight = 0; + private animationFrameId: number | null = null; + + // Snap configuration + private snapIntervalMinutes = 15; + private hourHeightPx: number; + + constructor() { + const gridSettings = calendarConfig.getGridSettings(); + this.hourHeightPx = gridSettings.hourHeight; + this.snapIntervalMinutes = gridSettings.snapInterval; + } + public initialize(): void { this.refreshEventCache(); this.setupEventListeners(); @@ -17,16 +37,33 @@ export class ResizeHandleManager { } private setupEventListeners(): void { + // Hover detection (only when not resizing and mouse button is up) document.addEventListener('mousemove', (e: MouseEvent) => { - this.handleGlobalMouseMove(e); + if (!this.isResizing) { + // Only check for resize zones when mouse button is up + if (e.buttons === 0) { + this.handleGlobalMouseMove(e); + } + } else { + this.handleMouseMove(e); + } }); + // Resize mouse handling + document.addEventListener('mousedown', (e: MouseEvent) => { + this.handleMouseDown(e); + }); + + document.addEventListener('mouseup', (e: MouseEvent) => { + this.handleMouseUp(e); + }); + + // Cache refresh eventBus.on(CoreEvents.GRID_RENDERED, () => this.refreshEventCache()); eventBus.on(CoreEvents.EVENTS_RENDERED, () => this.refreshEventCache()); eventBus.on(CoreEvents.EVENT_CREATED, () => this.refreshEventCache()); eventBus.on(CoreEvents.EVENT_UPDATED, () => this.refreshEventCache()); eventBus.on(CoreEvents.EVENT_DELETED, () => this.refreshEventCache()); - eventBus.on('drag:end', () => this.refreshEventCache()); } private handleGlobalMouseMove(e: MouseEvent): void { @@ -34,6 +71,11 @@ export class ResizeHandleManager { const events = this.cachedEvents; events.forEach(eventElement => { + // Skip the element we're currently resizing + if (this.resizingElement === eventElement) { + return; + } + const rect = eventElement.getBoundingClientRect(); const mouseY = e.clientY; const mouseX = e.clientX; @@ -73,4 +115,121 @@ export class ResizeHandleManager { } eventElement.removeAttribute('data-resize-hover'); } + + private handleMouseDown(e: MouseEvent): void { + const target = e.target as HTMLElement; + const eventElement = target.closest('swp-event[data-resize-hover="true"]'); + + if (!eventElement) return; + + // Check if click is in bottom resize zone + const rect = eventElement.getBoundingClientRect(); + const distanceFromBottom = rect.bottom - e.clientY; + + if (distanceFromBottom >= 0 && distanceFromBottom <= this.resizeZoneHeight) { + // START RESIZE + e.stopPropagation(); // Prevent DragDropManager from handling + e.preventDefault(); + + this.isResizing = true; + this.resizingElement = eventElement; + this.initialHeight = eventElement.offsetHeight; + this.initialMouseY = e.clientY; + + // Set high z-index on event-group if exists, otherwise on event itself + const eventGroup = eventElement.closest('swp-event-group'); + if (eventGroup) { + eventGroup.style.zIndex = '1000'; + } else { + eventElement.style.zIndex = '1000'; + } + + console.log('🔄 Resize started', this.initialHeight); + } + } + + private handleMouseMove(e: MouseEvent): void { + if (!this.isResizing || !this.resizingElement) return; + + const deltaY = e.clientY - this.initialMouseY; + const rawHeight = this.initialHeight + deltaY; + + // Apply minimum height + this.targetHeight = Math.max(30, rawHeight); + + // Start animation loop if not already running + if (this.animationFrameId === null) { + this.currentHeight = this.resizingElement.offsetHeight; + this.animate(); + } + } + + private animate(): void { + if (!this.isResizing || !this.resizingElement) { + this.animationFrameId = null; + return; + } + + // Smooth interpolation towards target + const diff = this.targetHeight - this.currentHeight; + const step = diff * 0.3; // 30% of distance per frame + + // Update if difference is significant + if (Math.abs(diff) > 0.5) { + this.currentHeight += step; + + const swpEvent = this.resizingElement as any; + if (swpEvent.updateHeight) { + swpEvent.updateHeight(this.currentHeight); + } + + this.animationFrameId = requestAnimationFrame(() => this.animate()); + } else { + // Close enough - snap to target + this.currentHeight = this.targetHeight; + const swpEvent = this.resizingElement as any; + if (swpEvent.updateHeight) { + swpEvent.updateHeight(this.currentHeight); + } + this.animationFrameId = null; + } + } + + private handleMouseUp(e: MouseEvent): void { + if (!this.isResizing || !this.resizingElement) return; + + // Cancel animation + if (this.animationFrameId !== null) { + cancelAnimationFrame(this.animationFrameId); + this.animationFrameId = null; + } + + // Snap to grid on mouse up + const snapDistancePx = (this.snapIntervalMinutes / 60) * this.hourHeightPx; + const currentHeight = this.resizingElement.offsetHeight; + const snappedHeight = Math.round(currentHeight / snapDistancePx) * snapDistancePx; + const finalHeight = Math.max(30, snappedHeight); + + const swpEvent = this.resizingElement as any; + if (swpEvent.updateHeight) { + swpEvent.updateHeight(finalHeight); + } + + console.log('✅ Resize ended', finalHeight); + + // Clear z-index on event-group if exists, otherwise on event itself + const eventGroup = this.resizingElement.closest('swp-event-group'); + if (eventGroup) { + eventGroup.style.zIndex = ''; + } else { + this.resizingElement.style.zIndex = ''; + } + + // Cleanup state + this.isResizing = false; + this.resizingElement = null; + + // Refresh cache for future operations + this.refreshEventCache(); + } } diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts index c517cc3..e7169f5 100644 --- a/src/renderers/EventRenderer.ts +++ b/src/renderers/EventRenderer.ts @@ -140,6 +140,7 @@ export class DateEventRenderer implements EventRendererStrategy { // Fully normalize the clone to be a regular event draggedClone.classList.remove('dragging'); + draggedClone.style.pointerEvents = ''; // Re-enable pointer events // Clean up instance state this.draggedClone = null; diff --git a/wwwroot/css/calendar-events-css.css b/wwwroot/css/calendar-events-css.css index 9db2ded..eab4dda 100644 --- a/wwwroot/css/calendar-events-css.css +++ b/wwwroot/css/calendar-events-css.css @@ -64,8 +64,6 @@ swp-day-columns swp-event { } swp-day-columns swp-event:hover { - box-shadow: var(--shadow-md); - transform: translateX(2px); z-index: 20; } @@ -77,7 +75,7 @@ swp-resize-indicator { transform: translateX(-50%); width: 50px; height: 8px; - background: #6b7280; + /* background set by JavaScript based on event color */ border-radius: 4px; z-index: 30; opacity: 0;