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
|
|
@ -6,20 +6,7 @@ import { DateCalculator } from '../utils/DateCalculator';
|
|||
import { eventBus } from '../core/EventBus';
|
||||
import { CoreEvents } from '../constants/CoreEvents';
|
||||
import { OverlapDetector, OverlapResult, EventId } from '../utils/OverlapDetector';
|
||||
|
||||
/**
|
||||
* Resize state interface
|
||||
*/
|
||||
interface ResizeState {
|
||||
element: HTMLElement;
|
||||
handle: 'top' | 'bottom';
|
||||
startY: number;
|
||||
originalTop: number;
|
||||
originalHeight: number;
|
||||
originalStartTime: Date;
|
||||
originalEndTime: Date;
|
||||
minHeightPx: number;
|
||||
}
|
||||
import { ResizeManager } from '../managers/ResizeManager';
|
||||
|
||||
/**
|
||||
* Interface for event rendering strategies
|
||||
|
|
@ -39,15 +26,15 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
private draggedClone: HTMLElement | null = null;
|
||||
private originalEvent: HTMLElement | null = null;
|
||||
|
||||
// Resize state
|
||||
private resizeState: ResizeState | null = null;
|
||||
private readonly MIN_EVENT_DURATION_MINUTES = 30;
|
||||
// Resize manager
|
||||
private resizeManager: ResizeManager;
|
||||
|
||||
constructor(dateCalculator?: DateCalculator) {
|
||||
if (!dateCalculator) {
|
||||
DateCalculator.initialize(calendarConfig);
|
||||
}
|
||||
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
|
||||
*/
|
||||
|
|
@ -378,7 +348,6 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
if (originalStackLink) {
|
||||
try {
|
||||
const stackData = JSON.parse(originalStackLink);
|
||||
const stackEventIds: string[] = [];
|
||||
|
||||
// Saml ALLE event IDs fra hele stack chain
|
||||
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
|
||||
*/
|
||||
|
|
@ -622,19 +551,6 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
// With the new system, overlap relationships are recalculated on drop
|
||||
// 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
|
||||
|
|
@ -1035,17 +951,11 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
// Setup resize handles on first mouseover only
|
||||
eventElement.addEventListener('mouseover', () => {
|
||||
if (eventElement.dataset.hasResizeHandlers !== 'true') {
|
||||
this.setupDynamicResizeHandles(eventElement);
|
||||
this.resizeManager.setupResizeHandles(eventElement);
|
||||
eventElement.dataset.hasResizeHandlers = 'true';
|
||||
}
|
||||
}, { once: true });
|
||||
|
||||
// Setup double-click for text selection
|
||||
eventElement.addEventListener('dblclick', (e) => {
|
||||
e.stopPropagation();
|
||||
this.handleEventDoubleClick(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 {
|
||||
const selector = 'swp-event, swp-event-group';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue