Implements event resizing functionality
Introduces event resizing feature, allowing users to dynamically adjust event durations by dragging handles on the top or bottom of events. Moves resize logic into a dedicated ResizeManager class for better code organization and separation of concerns.
This commit is contained in:
parent
86fa7d5bab
commit
d205ccb0b6
2 changed files with 270 additions and 309 deletions
264
src/managers/ResizeManager.ts
Normal file
264
src/managers/ResizeManager.ts
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
import { calendarConfig } from '../core/CalendarConfig';
|
||||
import { eventBus } from '../core/EventBus';
|
||||
import { IEventBus } from '../types/CalendarTypes';
|
||||
|
||||
/**
|
||||
* Resize state interface
|
||||
*/
|
||||
interface ResizeState {
|
||||
element: HTMLElement;
|
||||
handle: 'top' | 'bottom';
|
||||
startY: number;
|
||||
originalTop: number;
|
||||
originalHeight: number;
|
||||
originalStartTime: Date;
|
||||
originalEndTime: Date;
|
||||
minHeightPx: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* ResizeManager - Handles event resizing functionality
|
||||
*/
|
||||
export class ResizeManager {
|
||||
private resizeState: ResizeState | null = null;
|
||||
private readonly MIN_EVENT_DURATION_MINUTES = 15;
|
||||
|
||||
constructor(private eventBus: IEventBus) {
|
||||
// Bind methods for event listeners
|
||||
this.handleResize = this.handleResize.bind(this);
|
||||
this.endResize = this.endResize.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup dynamic resize handles that are only created when needed
|
||||
* @param eventElement - Event element to add resize handles to
|
||||
*/
|
||||
public setupResizeHandles(eventElement: HTMLElement): void {
|
||||
// Variables to track resize handles
|
||||
let topHandle: HTMLElement | null = null;
|
||||
let bottomHandle: HTMLElement | null = null;
|
||||
|
||||
console.log('Setting up dynamic resize handles for event:', eventElement.dataset.eventId);
|
||||
|
||||
// Create resize handles on first mouseover
|
||||
eventElement.addEventListener('mouseenter', () => {
|
||||
if (!topHandle && !bottomHandle) {
|
||||
topHandle = document.createElement('swp-resize-handle');
|
||||
topHandle.className = 'swp-resize-handle swp-resize-top';
|
||||
|
||||
bottomHandle = document.createElement('swp-resize-handle');
|
||||
bottomHandle.className = 'swp-resize-handle swp-resize-bottom';
|
||||
|
||||
// Add mousedown listeners for resize functionality
|
||||
topHandle.addEventListener('mousedown', (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.startResize(eventElement, 'top', e);
|
||||
});
|
||||
|
||||
bottomHandle.addEventListener('mousedown', (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.startResize(eventElement, 'bottom', e);
|
||||
});
|
||||
|
||||
eventElement.appendChild(topHandle);
|
||||
eventElement.appendChild(bottomHandle);
|
||||
console.log('Created resize handles for event:', eventElement.dataset.eventId);
|
||||
}
|
||||
});
|
||||
|
||||
// Show/hide handles based on mouse position
|
||||
eventElement.addEventListener('mousemove', (e: MouseEvent) => {
|
||||
if (!topHandle || !bottomHandle) return;
|
||||
|
||||
const rect = eventElement.getBoundingClientRect();
|
||||
const mouseY = e.clientY - rect.top;
|
||||
const eventHeight = rect.height;
|
||||
const topZone = eventHeight * 0.2;
|
||||
const bottomZone = eventHeight * 0.8;
|
||||
|
||||
// Show top handle in upper 20%
|
||||
if (mouseY < topZone) {
|
||||
topHandle.style.opacity = '1';
|
||||
bottomHandle.style.opacity = '0';
|
||||
}
|
||||
// Show bottom handle in lower 20%
|
||||
else if (mouseY > bottomZone) {
|
||||
topHandle.style.opacity = '0';
|
||||
bottomHandle.style.opacity = '1';
|
||||
}
|
||||
// Hide both if mouse is in middle
|
||||
else {
|
||||
topHandle.style.opacity = '0';
|
||||
bottomHandle.style.opacity = '0';
|
||||
}
|
||||
});
|
||||
|
||||
// Hide handles when mouse leaves event (but only if not in resize mode)
|
||||
eventElement.addEventListener('mouseleave', () => {
|
||||
console.log('Mouse LEAVE event:', eventElement.dataset.eventId);
|
||||
if (!this.resizeState && topHandle && bottomHandle) {
|
||||
topHandle.style.opacity = '0';
|
||||
bottomHandle.style.opacity = '0';
|
||||
console.log('Hidden resize handles for event:', eventElement.dataset.eventId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start resize operation
|
||||
*/
|
||||
private startResize(eventElement: HTMLElement, handle: 'top' | 'bottom', e: MouseEvent): void {
|
||||
const gridSettings = calendarConfig.getGridSettings();
|
||||
const minHeightPx = (this.MIN_EVENT_DURATION_MINUTES / 60) * gridSettings.hourHeight;
|
||||
|
||||
this.resizeState = {
|
||||
element: eventElement,
|
||||
handle: handle,
|
||||
startY: e.clientY,
|
||||
originalTop: parseFloat(eventElement.style.top),
|
||||
originalHeight: parseFloat(eventElement.style.height),
|
||||
originalStartTime: new Date(eventElement.dataset.start || ''),
|
||||
originalEndTime: new Date(eventElement.dataset.end || ''),
|
||||
minHeightPx: minHeightPx
|
||||
};
|
||||
|
||||
// Global listeners for resize
|
||||
document.addEventListener('mousemove', this.handleResize);
|
||||
document.addEventListener('mouseup', this.endResize);
|
||||
|
||||
// Add resize cursor to body
|
||||
document.body.style.cursor = handle === 'top' ? 'n-resize' : 's-resize';
|
||||
|
||||
console.log('Starting resize:', handle, 'element:', eventElement.dataset.eventId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle resize drag
|
||||
*/
|
||||
private handleResize(e: MouseEvent): void {
|
||||
if (!this.resizeState) return;
|
||||
|
||||
const deltaY = e.clientY - this.resizeState.startY;
|
||||
const snappedDelta = this.snapToGrid(deltaY);
|
||||
const gridSettings = calendarConfig.getGridSettings();
|
||||
|
||||
if (this.resizeState.handle === 'top') {
|
||||
// Resize from top
|
||||
const newTop = this.resizeState.originalTop + snappedDelta;
|
||||
const newHeight = this.resizeState.originalHeight - snappedDelta;
|
||||
|
||||
// Check minimum height
|
||||
if (newHeight >= this.resizeState.minHeightPx && newTop >= 0) {
|
||||
this.resizeState.element.style.top = newTop + 'px';
|
||||
this.resizeState.element.style.height = newHeight + 'px';
|
||||
|
||||
// Update times
|
||||
const minutesDelta = (snappedDelta / gridSettings.hourHeight) * 60;
|
||||
const newStartTime = this.addMinutes(this.resizeState.originalStartTime, minutesDelta);
|
||||
this.updateEventDisplay(this.resizeState.element, newStartTime, this.resizeState.originalEndTime);
|
||||
}
|
||||
} else {
|
||||
// Resize from bottom
|
||||
const newHeight = this.resizeState.originalHeight + snappedDelta;
|
||||
|
||||
// Check minimum height
|
||||
if (newHeight >= this.resizeState.minHeightPx) {
|
||||
this.resizeState.element.style.height = newHeight + 'px';
|
||||
|
||||
// Update times
|
||||
const minutesDelta = (snappedDelta / gridSettings.hourHeight) * 60;
|
||||
const newEndTime = this.addMinutes(this.resizeState.originalEndTime, minutesDelta);
|
||||
this.updateEventDisplay(this.resizeState.element, this.resizeState.originalStartTime, newEndTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* End resize operation
|
||||
*/
|
||||
private endResize(): void {
|
||||
if (!this.resizeState) return;
|
||||
|
||||
// Get final times from element
|
||||
const finalStart = this.resizeState.element.dataset.start;
|
||||
const finalEnd = this.resizeState.element.dataset.end;
|
||||
|
||||
console.log('Ending resize:', this.resizeState.element.dataset.eventId, 'New times:', finalStart, finalEnd);
|
||||
|
||||
// Emit event with new times
|
||||
this.eventBus.emit('event:resized', {
|
||||
eventId: this.resizeState.element.dataset.eventId,
|
||||
newStart: finalStart,
|
||||
newEnd: finalEnd
|
||||
});
|
||||
|
||||
// Cleanup
|
||||
document.removeEventListener('mousemove', this.handleResize);
|
||||
document.removeEventListener('mouseup', this.endResize);
|
||||
document.body.style.cursor = '';
|
||||
this.resizeState = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Snap delta to grid intervals
|
||||
*/
|
||||
private snapToGrid(deltaY: number): number {
|
||||
const gridSettings = calendarConfig.getGridSettings();
|
||||
const snapInterval = gridSettings.snapInterval;
|
||||
const hourHeight = gridSettings.hourHeight;
|
||||
const snapDistancePx = (snapInterval / 60) * hourHeight;
|
||||
return Math.round(deltaY / snapDistancePx) * snapDistancePx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update event display during resize
|
||||
*/
|
||||
private updateEventDisplay(element: HTMLElement, startTime: Date, endTime: Date): void {
|
||||
// Calculate new duration in minutes
|
||||
const durationMinutes = (endTime.getTime() - startTime.getTime()) / (1000 * 60);
|
||||
|
||||
// Update dataset
|
||||
element.dataset.start = startTime.toISOString();
|
||||
element.dataset.end = endTime.toISOString();
|
||||
element.dataset.duration = durationMinutes.toString();
|
||||
|
||||
// Update visual time
|
||||
const timeElement = element.querySelector('swp-event-time');
|
||||
if (timeElement) {
|
||||
const startStr = this.formatTime(startTime.toISOString());
|
||||
const endStr = this.formatTime(endTime.toISOString());
|
||||
timeElement.textContent = `${startStr} - ${endStr}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add minutes to a date
|
||||
*/
|
||||
private addMinutes(date: Date, minutes: number): Date {
|
||||
return new Date(date.getTime() + minutes * 60000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time for display
|
||||
*/
|
||||
private formatTime(input: Date | string): string {
|
||||
let hours: number;
|
||||
let minutes: number;
|
||||
|
||||
if (input instanceof Date) {
|
||||
hours = input.getHours();
|
||||
minutes = input.getMinutes();
|
||||
} else {
|
||||
// Date or ISO string input
|
||||
const date = typeof input === 'string' ? new Date(input) : input;
|
||||
hours = date.getHours();
|
||||
minutes = date.getMinutes();
|
||||
}
|
||||
|
||||
const period = hours >= 12 ? 'PM' : 'AM';
|
||||
const displayHours = hours > 12 ? hours - 12 : (hours === 0 ? 12 : hours);
|
||||
return `${displayHours}:${minutes.toString().padStart(2, '0')} ${period}`;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue