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(); } private refreshEventCache(): void { this.cachedEvents = Array.from( document.querySelectorAll('swp-day-columns swp-event') ); } private setupEventListeners(): void { // Hover detection (only when not resizing and mouse button is up) document.addEventListener('mousemove', (e: MouseEvent) => { 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()); } private handleGlobalMouseMove(e: MouseEvent): void { // Check all cached events to see if mouse is in their resize zone 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; // Check if mouse is within element bounds horizontally const isInHorizontalBounds = mouseX >= rect.left && mouseX <= rect.right; // Check if mouse is in bottom resize zone of the element const distanceFromBottom = rect.bottom - mouseY; const isInResizeZone = distanceFromBottom >= 0 && distanceFromBottom <= this.resizeZoneHeight; if (isInHorizontalBounds && isInResizeZone) { this.showResizeIndicator(eventElement); console.log(`✅ In resize zone - bottom ${this.resizeZoneHeight}px`); } else { this.hideResizeIndicator(eventElement); } }); } private showResizeIndicator(eventElement: HTMLElement): void { // Check if indicator already exists let indicator = eventElement.querySelector('swp-resize-indicator'); if (!indicator) { indicator = document.createElement('swp-resize-indicator'); eventElement.appendChild(indicator); } eventElement.setAttribute('data-resize-hover', 'true'); } private hideResizeIndicator(eventElement: HTMLElement): void { const indicator = eventElement.querySelector('swp-resize-indicator'); if (indicator) { indicator.remove(); } 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(); } }