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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,20 +6,7 @@ import { DateCalculator } from '../utils/DateCalculator';
|
||||||
import { eventBus } from '../core/EventBus';
|
import { eventBus } from '../core/EventBus';
|
||||||
import { CoreEvents } from '../constants/CoreEvents';
|
import { CoreEvents } from '../constants/CoreEvents';
|
||||||
import { OverlapDetector, OverlapResult, EventId } from '../utils/OverlapDetector';
|
import { OverlapDetector, OverlapResult, EventId } from '../utils/OverlapDetector';
|
||||||
|
import { ResizeManager } from '../managers/ResizeManager';
|
||||||
/**
|
|
||||||
* Resize state interface
|
|
||||||
*/
|
|
||||||
interface ResizeState {
|
|
||||||
element: HTMLElement;
|
|
||||||
handle: 'top' | 'bottom';
|
|
||||||
startY: number;
|
|
||||||
originalTop: number;
|
|
||||||
originalHeight: number;
|
|
||||||
originalStartTime: Date;
|
|
||||||
originalEndTime: Date;
|
|
||||||
minHeightPx: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for event rendering strategies
|
* Interface for event rendering strategies
|
||||||
|
|
@ -39,15 +26,15 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
private draggedClone: HTMLElement | null = null;
|
private draggedClone: HTMLElement | null = null;
|
||||||
private originalEvent: HTMLElement | null = null;
|
private originalEvent: HTMLElement | null = null;
|
||||||
|
|
||||||
// Resize state
|
// Resize manager
|
||||||
private resizeState: ResizeState | null = null;
|
private resizeManager: ResizeManager;
|
||||||
private readonly MIN_EVENT_DURATION_MINUTES = 30;
|
|
||||||
|
|
||||||
constructor(dateCalculator?: DateCalculator) {
|
constructor(dateCalculator?: DateCalculator) {
|
||||||
if (!dateCalculator) {
|
if (!dateCalculator) {
|
||||||
DateCalculator.initialize(calendarConfig);
|
DateCalculator.initialize(calendarConfig);
|
||||||
}
|
}
|
||||||
this.dateCalculator = dateCalculator || new DateCalculator();
|
this.dateCalculator = dateCalculator || new DateCalculator();
|
||||||
|
this.resizeManager = new ResizeManager(eventBus);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
@ -259,23 +246,6 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate event duration in minutes from element height
|
|
||||||
*/
|
|
||||||
private getEventDuration(element: HTMLElement): number {
|
|
||||||
const gridSettings = calendarConfig.getGridSettings();
|
|
||||||
const hourHeight = gridSettings.hourHeight;
|
|
||||||
|
|
||||||
// Get height from style or computed
|
|
||||||
let heightPx = parseInt(element.style.height) || 0;
|
|
||||||
if (!heightPx) {
|
|
||||||
const rect = element.getBoundingClientRect();
|
|
||||||
heightPx = rect.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.round((heightPx / hourHeight) * 60);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unified time formatting method - handles both total minutes and Date objects
|
* Unified time formatting method - handles both total minutes and Date objects
|
||||||
*/
|
*/
|
||||||
|
|
@ -378,7 +348,6 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
if (originalStackLink) {
|
if (originalStackLink) {
|
||||||
try {
|
try {
|
||||||
const stackData = JSON.parse(originalStackLink);
|
const stackData = JSON.parse(originalStackLink);
|
||||||
const stackEventIds: string[] = [];
|
|
||||||
|
|
||||||
// Saml ALLE event IDs fra hele stack chain
|
// Saml ALLE event IDs fra hele stack chain
|
||||||
const allStackEventIds: Set<string> = new Set();
|
const allStackEventIds: Set<string> = new Set();
|
||||||
|
|
@ -510,46 +479,6 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle event double-click for text selection
|
|
||||||
*/
|
|
||||||
private handleEventDoubleClick(eventElement: HTMLElement): void {
|
|
||||||
console.log('handleEventDoubleClick:', eventElement.dataset.eventId);
|
|
||||||
|
|
||||||
// Enable text selection temporarily
|
|
||||||
eventElement.classList.add('text-selectable');
|
|
||||||
|
|
||||||
// Auto-select the event text
|
|
||||||
const selection = window.getSelection();
|
|
||||||
if (selection) {
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNodeContents(eventElement);
|
|
||||||
selection.removeAllRanges();
|
|
||||||
selection.addRange(range);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove text selection mode when clicking outside
|
|
||||||
const removeSelectable = (e: Event) => {
|
|
||||||
// Don't remove if clicking within the same event
|
|
||||||
if (e.target && eventElement.contains(e.target as Node)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
eventElement.classList.remove('text-selectable');
|
|
||||||
document.removeEventListener('click', removeSelectable);
|
|
||||||
|
|
||||||
// Clear selection
|
|
||||||
if (selection) {
|
|
||||||
selection.removeAllRanges();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add click outside listener after a short delay
|
|
||||||
setTimeout(() => {
|
|
||||||
document.addEventListener('click', removeSelectable);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle overlap detection and re-rendering after drag-drop
|
* Handle overlap detection and re-rendering after drag-drop
|
||||||
*/
|
*/
|
||||||
|
|
@ -623,19 +552,6 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
// No need to manually track and remove from groups
|
// No need to manually track and remove from groups
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore normal event styling (full column width)
|
|
||||||
*/
|
|
||||||
private restoreNormalEventStyling(eventElement: HTMLElement): void {
|
|
||||||
eventElement.style.position = 'absolute';
|
|
||||||
eventElement.style.left = '2px';
|
|
||||||
eventElement.style.right = '2px';
|
|
||||||
eventElement.style.width = '';
|
|
||||||
// Behold z-index for stacked events
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update element's dataset with new times after successful drop
|
* Update element's dataset with new times after successful drop
|
||||||
*/
|
*/
|
||||||
|
|
@ -1035,17 +951,11 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
// Setup resize handles on first mouseover only
|
// Setup resize handles on first mouseover only
|
||||||
eventElement.addEventListener('mouseover', () => {
|
eventElement.addEventListener('mouseover', () => {
|
||||||
if (eventElement.dataset.hasResizeHandlers !== 'true') {
|
if (eventElement.dataset.hasResizeHandlers !== 'true') {
|
||||||
this.setupDynamicResizeHandles(eventElement);
|
this.resizeManager.setupResizeHandles(eventElement);
|
||||||
eventElement.dataset.hasResizeHandlers = 'true';
|
eventElement.dataset.hasResizeHandlers = 'true';
|
||||||
}
|
}
|
||||||
}, { once: true });
|
}, { once: true });
|
||||||
|
|
||||||
// Setup double-click for text selection
|
|
||||||
eventElement.addEventListener('dblclick', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
this.handleEventDoubleClick(eventElement);
|
|
||||||
});
|
|
||||||
|
|
||||||
return eventElement;
|
return eventElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1115,220 +1025,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup dynamic resize handles that are only created when needed
|
|
||||||
*/
|
|
||||||
private setupDynamicResizeHandles(eventElement: HTMLElement): void {
|
|
||||||
let topHandle: HTMLElement | null = null;
|
|
||||||
let bottomHandle: HTMLElement | null = null;
|
|
||||||
|
|
||||||
console.log('Setting up dynamic resize handles for event:', eventElement.dataset.eventId);
|
|
||||||
|
|
||||||
// Create handles on mouse enter
|
|
||||||
eventElement.addEventListener('mouseenter', () => {
|
|
||||||
console.log('Mouse ENTER event:', eventElement.dataset.eventId);
|
|
||||||
// Only create if they don't already exist
|
|
||||||
if (!topHandle || !bottomHandle) {
|
|
||||||
topHandle = document.createElement('swp-resize-handle');
|
|
||||||
topHandle.setAttribute('data-position', 'top');
|
|
||||||
topHandle.style.opacity = '0';
|
|
||||||
|
|
||||||
bottomHandle = document.createElement('swp-resize-handle');
|
|
||||||
bottomHandle.setAttribute('data-position', 'bottom');
|
|
||||||
bottomHandle.style.opacity = '0';
|
|
||||||
|
|
||||||
// Add mousedown listeners for resize functionality
|
|
||||||
topHandle.addEventListener('mousedown', (e: MouseEvent) => {
|
|
||||||
e.stopPropagation(); // Forhindre normal drag
|
|
||||||
e.preventDefault();
|
|
||||||
this.startResize(eventElement, 'top', e);
|
|
||||||
});
|
|
||||||
|
|
||||||
bottomHandle.addEventListener('mousedown', (e: MouseEvent) => {
|
|
||||||
e.stopPropagation(); // Forhindre normal drag
|
|
||||||
e.preventDefault();
|
|
||||||
this.startResize(eventElement, 'bottom', e);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Insert handles at beginning and end
|
|
||||||
eventElement.insertBefore(topHandle, eventElement.firstChild);
|
|
||||||
eventElement.appendChild(bottomHandle);
|
|
||||||
console.log('Created resize handles for event:', eventElement.dataset.eventId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mouse move handler for smart visibility
|
|
||||||
eventElement.addEventListener('mousemove', (e: MouseEvent) => {
|
|
||||||
if (!topHandle || !bottomHandle) return;
|
|
||||||
|
|
||||||
const rect = eventElement.getBoundingClientRect();
|
|
||||||
const y = e.clientY - rect.top;
|
|
||||||
const height = rect.height;
|
|
||||||
|
|
||||||
// Show top handle if mouse is in top 12px
|
|
||||||
if (y <= 12) {
|
|
||||||
topHandle.style.opacity = '1';
|
|
||||||
bottomHandle.style.opacity = '0';
|
|
||||||
}
|
|
||||||
// Show bottom handle if mouse is in bottom 12px
|
|
||||||
else if (y >= height - 12) {
|
|
||||||
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 (men kun hvis ikke i 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 fra toppen
|
|
||||||
const newTop = this.resizeState.originalTop + snappedDelta;
|
|
||||||
const newHeight = this.resizeState.originalHeight - snappedDelta;
|
|
||||||
|
|
||||||
// Check minimum højde
|
|
||||||
if (newHeight >= this.resizeState.minHeightPx && newTop >= 0) {
|
|
||||||
this.resizeState.element.style.top = newTop + 'px';
|
|
||||||
this.resizeState.element.style.height = newHeight + 'px';
|
|
||||||
|
|
||||||
// Opdater tidspunkter
|
|
||||||
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 fra bunden
|
|
||||||
const newHeight = this.resizeState.originalHeight + snappedDelta;
|
|
||||||
|
|
||||||
// Check minimum højde
|
|
||||||
if (newHeight >= this.resizeState.minHeightPx) {
|
|
||||||
this.resizeState.element.style.height = newHeight + 'px';
|
|
||||||
|
|
||||||
// Opdater tidspunkter
|
|
||||||
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;
|
|
||||||
|
|
||||||
// Få finale tider fra 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 med nye tider
|
|
||||||
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 {
|
|
||||||
// Beregn ny duration i minutter
|
|
||||||
const durationMinutes = (endTime.getTime() - startTime.getTime()) / (1000 * 60);
|
|
||||||
|
|
||||||
// Opdater dataset
|
|
||||||
element.dataset.start = startTime.toISOString();
|
|
||||||
element.dataset.end = endTime.toISOString();
|
|
||||||
element.dataset.duration = durationMinutes.toString();
|
|
||||||
|
|
||||||
// Opdater visual tid
|
|
||||||
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}`;
|
|
||||||
|
|
||||||
// Opdater også data-duration attribut på time elementet
|
|
||||||
timeElement.setAttribute('data-duration', durationMinutes.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add minutes to a date
|
|
||||||
*/
|
|
||||||
private addMinutes(date: Date, minutes: number): Date {
|
|
||||||
return new Date(date.getTime() + minutes * 60000);
|
|
||||||
}
|
|
||||||
|
|
||||||
clearEvents(container?: HTMLElement): void {
|
clearEvents(container?: HTMLElement): void {
|
||||||
const selector = 'swp-event, swp-event-group';
|
const selector = 'swp-event, swp-event-group';
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue