2025-10-07 17:16:00 +02:00
|
|
|
import { eventBus } from '../core/EventBus';
|
|
|
|
|
import { CoreEvents } from '../constants/CoreEvents';
|
2025-10-08 00:58:38 +02:00
|
|
|
import { calendarConfig } from '../core/CalendarConfig';
|
2025-10-07 17:16:00 +02:00
|
|
|
|
2025-10-06 23:38:01 +02:00
|
|
|
export class ResizeHandleManager {
|
2025-10-07 17:16:00 +02:00
|
|
|
private resizeZoneHeight = 15; // Must match CSS ::after height
|
|
|
|
|
private cachedEvents: HTMLElement[] = [];
|
2025-10-06 23:38:01 +02:00
|
|
|
|
2025-10-08 00:58:38 +02:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 23:38:01 +02:00
|
|
|
public initialize(): void {
|
2025-10-07 17:16:00 +02:00
|
|
|
this.refreshEventCache();
|
2025-10-06 23:38:01 +02:00
|
|
|
this.setupEventListeners();
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-07 17:16:00 +02:00
|
|
|
private refreshEventCache(): void {
|
|
|
|
|
this.cachedEvents = Array.from(
|
|
|
|
|
document.querySelectorAll<HTMLElement>('swp-day-columns swp-event')
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-10-06 23:38:01 +02:00
|
|
|
|
2025-10-07 17:16:00 +02:00
|
|
|
private setupEventListeners(): void {
|
2025-10-08 00:58:38 +02:00
|
|
|
// Hover detection (only when not resizing and mouse button is up)
|
2025-10-06 23:38:01 +02:00
|
|
|
document.addEventListener('mousemove', (e: MouseEvent) => {
|
2025-10-08 00:58:38 +02:00
|
|
|
if (!this.isResizing) {
|
|
|
|
|
// Only check for resize zones when mouse button is up
|
|
|
|
|
if (e.buttons === 0) {
|
|
|
|
|
this.handleGlobalMouseMove(e);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
this.handleMouseMove(e);
|
|
|
|
|
}
|
2025-10-06 23:38:01 +02:00
|
|
|
});
|
|
|
|
|
|
2025-10-08 00:58:38 +02:00
|
|
|
// Resize mouse handling
|
|
|
|
|
document.addEventListener('mousedown', (e: MouseEvent) => {
|
|
|
|
|
this.handleMouseDown(e);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.addEventListener('mouseup', (e: MouseEvent) => {
|
|
|
|
|
this.handleMouseUp(e);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Cache refresh
|
2025-10-07 17:16:00 +02:00
|
|
|
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());
|
2025-10-06 23:38:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private handleGlobalMouseMove(e: MouseEvent): void {
|
2025-10-07 17:16:00 +02:00
|
|
|
// Check all cached events to see if mouse is in their resize zone
|
|
|
|
|
const events = this.cachedEvents;
|
2025-10-06 23:38:01 +02:00
|
|
|
|
|
|
|
|
events.forEach(eventElement => {
|
2025-10-08 00:58:38 +02:00
|
|
|
// Skip the element we're currently resizing
|
|
|
|
|
if (this.resizingElement === eventElement) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-06 23:38:01 +02:00
|
|
|
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;
|
|
|
|
|
|
2025-10-07 17:16:00 +02:00
|
|
|
// Check if mouse is in bottom resize zone of the element
|
2025-10-06 23:38:01 +02:00
|
|
|
const distanceFromBottom = rect.bottom - mouseY;
|
|
|
|
|
const isInResizeZone = distanceFromBottom >= 0 && distanceFromBottom <= this.resizeZoneHeight;
|
|
|
|
|
|
|
|
|
|
if (isInHorizontalBounds && isInResizeZone) {
|
|
|
|
|
this.showResizeIndicator(eventElement);
|
2025-10-07 17:16:00 +02:00
|
|
|
console.log(`✅ In resize zone - bottom ${this.resizeZoneHeight}px`);
|
2025-10-06 23:38:01 +02:00
|
|
|
} else {
|
|
|
|
|
this.hideResizeIndicator(eventElement);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private showResizeIndicator(eventElement: HTMLElement): void {
|
|
|
|
|
// Check if indicator already exists
|
|
|
|
|
let indicator = eventElement.querySelector<HTMLElement>('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<HTMLElement>('swp-resize-indicator');
|
|
|
|
|
if (indicator) {
|
|
|
|
|
indicator.remove();
|
|
|
|
|
}
|
|
|
|
|
eventElement.removeAttribute('data-resize-hover');
|
|
|
|
|
}
|
2025-10-08 00:58:38 +02:00
|
|
|
|
|
|
|
|
private handleMouseDown(e: MouseEvent): void {
|
|
|
|
|
const target = e.target as HTMLElement;
|
|
|
|
|
const eventElement = target.closest<HTMLElement>('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<HTMLElement>('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;
|
2025-10-08 19:35:29 +02:00
|
|
|
const finalHeight = Math.max(30, snappedHeight) - 3; //a little gap, so it doesn't cover the horizontal time lines
|
2025-10-08 00:58:38 +02:00
|
|
|
|
|
|
|
|
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<HTMLElement>('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();
|
|
|
|
|
}
|
2025-10-06 23:38:01 +02:00
|
|
|
}
|