diff --git a/src/factories/ManagerFactory.ts b/src/factories/ManagerFactory.ts index 569b0b0..c98f00e 100644 --- a/src/factories/ManagerFactory.ts +++ b/src/factories/ManagerFactory.ts @@ -8,6 +8,7 @@ import { ViewManager } from '../managers/ViewManager'; import { CalendarManager } from '../managers/CalendarManager'; import { DragDropManager } from '../managers/DragDropManager'; import { AllDayManager } from '../managers/AllDayManager'; +import { ResizeHandleManager } from '../managers/ResizeHandleManager'; import { CalendarManagers } from '../types/ManagerTypes'; /** @@ -39,6 +40,7 @@ export class ManagerFactory { const viewManager = new ViewManager(eventBus); const dragDropManager = new DragDropManager(eventBus); const allDayManager = new AllDayManager(eventManager); + const resizeHandleManager = new ResizeHandleManager(); // CalendarManager depends on all other managers const calendarManager = new CalendarManager( @@ -49,7 +51,7 @@ export class ManagerFactory { scrollManager ); - + return { eventManager, eventRenderer, @@ -59,7 +61,8 @@ export class ManagerFactory { viewManager, calendarManager, dragDropManager, - allDayManager + allDayManager, + resizeHandleManager }; } @@ -67,9 +70,10 @@ export class ManagerFactory { * Initialize all managers in the correct order */ public async initializeManagers(managers: CalendarManagers): Promise { - + try { await managers.calendarManager.initialize?.(); + managers.resizeHandleManager.initialize(); } catch (error) { throw error; } diff --git a/src/managers/ResizeHandleManager.ts b/src/managers/ResizeHandleManager.ts new file mode 100644 index 0000000..f43d5e4 --- /dev/null +++ b/src/managers/ResizeHandleManager.ts @@ -0,0 +1,89 @@ +export class ResizeHandleManager { + private resizeZoneHeight =15; // Must match CSS ::after height + private lastHoveredEvent: HTMLElement | null = null; + + public initialize(): void { + this.setupEventListeners(); + } + + private setupEventListeners(): void { + + // Listen to global mouseover from document + document.addEventListener('mouseover', (e: MouseEvent) => { + this.handleGlobalMouseOver(e); + }); + + document.addEventListener('mousemove', (e: MouseEvent) => { + this.handleGlobalMouseMove(e); + }); + + document.addEventListener('mouseout', (e: MouseEvent) => { + this.handleGlobalMouseOut(e); + }); + } + + private handleGlobalMouseOver(e: MouseEvent): void { + const target = e.target as HTMLElement; + const eventElement = target.closest('swp-day-columns swp-event'); + + if (eventElement) { + this.lastHoveredEvent = eventElement; + } + } + + private handleGlobalMouseMove(e: MouseEvent): void { + // Check all events to see if mouse is in their resize zone + const events = document.querySelectorAll('swp-day-columns swp-event'); + + events.forEach(eventElement => { + 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 25px 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 25px'); + } 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 handleGlobalMouseOut(e: MouseEvent): void { + const target = e.target as HTMLElement; + const eventElement = target.closest('swp-day-columns swp-event'); + + if (eventElement) { + // Don't remove immediately - let mousemove handle it + } + } + + +} diff --git a/src/types/ManagerTypes.ts b/src/types/ManagerTypes.ts index 8945680..ffb4958 100644 --- a/src/types/ManagerTypes.ts +++ b/src/types/ManagerTypes.ts @@ -13,6 +13,7 @@ export interface CalendarManagers { calendarManager: CalendarManager; dragDropManager: unknown; // Avoid interface conflicts allDayManager: unknown; // Avoid interface conflicts + resizeHandleManager: ResizeHandleManager; } /** @@ -70,6 +71,10 @@ export interface AllDayManager extends IManager { [key: string]: unknown; // Allow any properties from actual implementation } +export interface ResizeHandleManager extends IManager { + // ResizeHandleManager handles hover effects for resize handles +} + export interface ResourceData { resources: Resource[]; assignments?: ResourceAssignment[]; diff --git a/wwwroot/css/calendar-events-css.css b/wwwroot/css/calendar-events-css.css index 604b3f3..1607f07 100644 --- a/wwwroot/css/calendar-events-css.css +++ b/wwwroot/css/calendar-events-css.css @@ -70,6 +70,35 @@ swp-day-columns swp-event:hover { z-index: 20; } +/* Resize handle indicator - created by JavaScript */ +swp-resize-indicator { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 15px; + background-image: url('data:image/svg+xml;utf8,'); + background-position: center center; + background-repeat: no-repeat; + pointer-events: none; + z-index: 10; + opacity: 0; + animation: fadeIn 0.8s ease forwards; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +swp-day-columns swp-event[data-resize-hover="true"] { + cursor: ns-resize; +} + swp-day-columns swp-event-time { display: block; font-size: 0.875rem; @@ -83,64 +112,6 @@ swp-day-columns swp-event-title { line-height: 1.3; } -/* External resize handles */ -swp-resize-handle { - position: absolute; - left: 50%; - transform: translateX(-50%); - width: 24px; - height: 4px; - opacity: 0; - transition: opacity var(--transition-fast); - cursor: ns-resize; - z-index: 30; - background: var(--color-primary); - border-radius: 3px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); - - /* Subtle grip pattern */ - &::before { - content: ''; - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - width: 12px; - height: 1px; - background: rgba(255, 255, 255, 0.6); - border-radius: 0.5px; - box-shadow: 0 -1px 0 rgba(255, 255, 255, 0.3); - } -} - -/* Top resize handle - positioned OUTSIDE event */ -swp-resize-handle[data-position="top"] { - top: -6px; -} - -/* Bottom resize handle - positioned OUTSIDE event */ -swp-resize-handle[data-position="bottom"] { - bottom: -6px; -} - -/* Resize handles controlled by JavaScript - no general hover */ -swp-handle-hitarea { - position: absolute; - left: -8px; - right: -8px; - top: -6px; - bottom: -6px; - cursor: ns-resize; -} - -swp-handle-hitarea[data-position="top"] { - top: 4px; -} - -swp-handle-hitarea[data-position="bottom"] { - bottom: 4px; -} - /* Multi-day events */ swp-multi-day-event { position: relative;