Enhances drag and drop functionality
Improves drag and drop event handling, including conversion between all-day and timed events. Introduces HeaderManager to handle header-related event logic and centralizes header event handling for better code organization and separation of concerns. Optimizes event listeners and throttles events for improved performance. Removes redundant code and improves the overall drag and drop experience.
This commit is contained in:
parent
d087e333fe
commit
3bd74d6f4e
6 changed files with 418 additions and 170 deletions
|
|
@ -140,6 +140,12 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
this.handleConvertToAllDay(eventId, targetDate, headerRenderer);
|
||||
});
|
||||
|
||||
// Handle convert to timed event
|
||||
eventBus.on('drag:convert-to-timed', (event) => {
|
||||
const { eventId, targetColumn, targetY } = (event as CustomEvent).detail;
|
||||
this.handleConvertToTimed(eventId, targetColumn, targetY);
|
||||
});
|
||||
|
||||
// Handle navigation period change (when slide animation completes)
|
||||
eventBus.on(CoreEvents.NAVIGATION_COMPLETED, () => {
|
||||
// Animate all-day height after navigation completes
|
||||
|
|
@ -183,6 +189,45 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
return 60;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply common drag styling to an element
|
||||
*/
|
||||
private applyDragStyling(element: HTMLElement): void {
|
||||
element.style.position = 'absolute';
|
||||
element.style.zIndex = '999999';
|
||||
element.style.pointerEvents = 'none';
|
||||
element.style.opacity = '0.8';
|
||||
element.style.left = '2px';
|
||||
element.style.right = '2px';
|
||||
element.style.marginLeft = '0px';
|
||||
element.style.width = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create event inner structure (swp-event-time and swp-event-title)
|
||||
*/
|
||||
private createEventInnerStructure(event: CalendarEvent): string {
|
||||
const startTime = this.formatTime(event.start);
|
||||
const endTime = this.formatTime(event.end);
|
||||
const durationMinutes = (event.end.getTime() - event.start.getTime()) / (1000 * 60);
|
||||
|
||||
return `
|
||||
<swp-event-time data-duration="${durationMinutes}">${startTime} - ${endTime}</swp-event-time>
|
||||
<swp-event-title>${event.title}</swp-event-title>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply standard event positioning
|
||||
*/
|
||||
private applyEventPositioning(element: HTMLElement, top: number, height: number): void {
|
||||
element.style.position = 'absolute';
|
||||
element.style.top = `${top}px`;
|
||||
element.style.height = `${height}px`;
|
||||
element.style.left = '2px';
|
||||
element.style.right = '2px';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a clone of an event for dragging
|
||||
*/
|
||||
|
|
@ -199,18 +244,10 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
const originalDurationMinutes = this.getOriginalEventDuration(originalEvent);
|
||||
clone.dataset.originalDuration = originalDurationMinutes.toString();
|
||||
|
||||
// Apply common drag styling
|
||||
this.applyDragStyling(clone);
|
||||
|
||||
// Style for dragging
|
||||
clone.style.position = 'absolute';
|
||||
clone.style.zIndex = '999999';
|
||||
clone.style.pointerEvents = 'none';
|
||||
clone.style.opacity = '0.8';
|
||||
|
||||
// Dragged event skal have fuld kolonne bredde
|
||||
clone.style.left = '2px';
|
||||
clone.style.right = '2px';
|
||||
clone.style.marginLeft = '0px';
|
||||
clone.style.width = '';
|
||||
// Set height from original event
|
||||
clone.style.height = originalEvent.style.height || `${originalEvent.getBoundingClientRect().height}px`;
|
||||
|
||||
return clone;
|
||||
|
|
@ -220,6 +257,10 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
* Update clone timestamp based on new position
|
||||
*/
|
||||
private updateCloneTimestamp(clone: HTMLElement, snappedY: number): void {
|
||||
|
||||
//important as events can pile up, so they will still fire after event has been converted to another rendered type
|
||||
if(clone.dataset.allDay == "true") return;
|
||||
|
||||
const gridSettings = calendarConfig.getGridSettings();
|
||||
const hourHeight = gridSettings.hourHeight;
|
||||
const dayStartHour = gridSettings.dayStartHour;
|
||||
|
|
@ -248,7 +289,6 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
|
||||
clone.dataset.start = startDate.toISOString();
|
||||
clone.dataset.end = endDate.toISOString();
|
||||
|
||||
// Update display
|
||||
const timeElement = clone.querySelector('swp-event-time');
|
||||
if (timeElement) {
|
||||
|
|
@ -283,7 +323,6 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
* Handle drag start event
|
||||
*/
|
||||
private handleDragStart(originalElement: HTMLElement, eventId: string, mouseOffset: any, column: string): void {
|
||||
console.log('handleDragStart:', eventId);
|
||||
this.originalEvent = originalElement;
|
||||
|
||||
// Remove stacking styling during drag will be handled by new system
|
||||
|
|
@ -346,10 +385,9 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
* Handle drag end event
|
||||
*/
|
||||
private handleDragEnd(eventId: string, originalElement: HTMLElement, finalColumn: string, finalY: number): void {
|
||||
console.log('handleDragEnd:', eventId);
|
||||
|
||||
if (!this.draggedClone || !this.originalEvent) {
|
||||
console.log('Missing draggedClone or originalEvent');
|
||||
console.warn('Missing draggedClone or originalEvent');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -443,11 +481,13 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
this.draggedClone.style.userSelect = '';
|
||||
// Behold z-index hvis det er et stacked event
|
||||
|
||||
// Update dataset with new times after successful drop
|
||||
const newEvent = this.elementToCalendarEvent(this.draggedClone);
|
||||
if (newEvent) {
|
||||
this.draggedClone.dataset.start = newEvent.start.toISOString();
|
||||
this.draggedClone.dataset.end = newEvent.end.toISOString();
|
||||
// Update dataset with new times after successful drop (only for timed events)
|
||||
if (this.draggedClone.tagName !== 'SWP-ALLDAY-EVENT') {
|
||||
const newEvent = this.elementToCalendarEvent(this.draggedClone);
|
||||
if (newEvent) {
|
||||
this.draggedClone.dataset.start = newEvent.start.toISOString();
|
||||
this.draggedClone.dataset.end = newEvent.end.toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
// Detect overlaps with other events in the target column and reposition if needed
|
||||
|
|
@ -687,12 +727,15 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
const allDayEvent = document.createElement('swp-allday-event');
|
||||
allDayEvent.dataset.eventId = clone.dataset.eventId || '';
|
||||
allDayEvent.dataset.title = eventTitle;
|
||||
allDayEvent.dataset.start = `${targetDate}T${eventTime.split(' - ')[0]}:00`;
|
||||
allDayEvent.dataset.end = `${targetDate}T${eventTime.split(' - ')[1]}:00`;
|
||||
allDayEvent.dataset.start = `${targetDate}T00:00:00`;
|
||||
allDayEvent.dataset.end = `${targetDate}T23:59:59`;
|
||||
allDayEvent.dataset.type = clone.dataset.type || 'work';
|
||||
allDayEvent.dataset.duration = eventDuration;
|
||||
allDayEvent.dataset.allDay = "true";
|
||||
|
||||
allDayEvent.textContent = eventTitle;
|
||||
|
||||
|
||||
console.log("allDayEvent", allDayEvent.dataset);
|
||||
// Position in grid
|
||||
(allDayEvent as HTMLElement).style.gridColumn = columnIndex.toString();
|
||||
// grid-row will be set by checkAndAnimateAllDayHeight() based on actual position
|
||||
|
|
@ -711,8 +754,101 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
// Check if height animation is needed
|
||||
this.triggerAllDayHeightAnimation();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Handle conversion from all-day to timed event
|
||||
*/
|
||||
private handleConvertToTimed(eventId: string, targetColumn: string, targetY: number): void {
|
||||
if (!this.draggedClone) return;
|
||||
|
||||
// Only convert if it's an all-day event
|
||||
if (this.draggedClone.tagName !== 'SWP-ALLDAY-EVENT') return;
|
||||
|
||||
// Transform clone to timed format
|
||||
this.transformAllDayToTimed(this.draggedClone, targetColumn, targetY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform clone from all-day to timed event
|
||||
*/
|
||||
private transformAllDayToTimed(allDayClone: HTMLElement, targetColumn: string, targetY: number): void {
|
||||
// Find target column element
|
||||
const columnElement = document.querySelector(`swp-day-column[data-date="${targetColumn}"]`);
|
||||
if (!columnElement) return;
|
||||
|
||||
const eventsLayer = columnElement.querySelector('swp-events-layer');
|
||||
if (!eventsLayer) return;
|
||||
|
||||
// Extract event data from all-day element
|
||||
const eventId = allDayClone.dataset.eventId || '';
|
||||
const eventTitle = allDayClone.dataset.title || allDayClone.textContent || 'Untitled';
|
||||
const eventType = allDayClone.dataset.type || 'work';
|
||||
|
||||
// Calculate time from Y position
|
||||
const gridSettings = calendarConfig.getGridSettings();
|
||||
const hourHeight = gridSettings.hourHeight;
|
||||
const dayStartHour = gridSettings.dayStartHour;
|
||||
const snapInterval = gridSettings.snapInterval;
|
||||
|
||||
// Calculate start time from position
|
||||
const minutesFromGridStart = (targetY / hourHeight) * 60;
|
||||
const actualStartMinutes = (dayStartHour * 60) + minutesFromGridStart;
|
||||
const snappedStartMinutes = Math.round(actualStartMinutes / snapInterval) * snapInterval;
|
||||
|
||||
// Use default duration or extract from dataset
|
||||
const duration = parseInt(allDayClone.dataset.duration || '60');
|
||||
const endMinutes = snappedStartMinutes + duration;
|
||||
|
||||
// Create dates with target column date
|
||||
const columnDate = new Date(targetColumn + 'T00:00:00');
|
||||
const startDate = new Date(columnDate);
|
||||
startDate.setMinutes(snappedStartMinutes);
|
||||
|
||||
const endDate = new Date(columnDate);
|
||||
endDate.setMinutes(endMinutes);
|
||||
|
||||
// Create CalendarEvent object for helper methods
|
||||
const tempEvent: CalendarEvent = {
|
||||
id: eventId,
|
||||
title: eventTitle,
|
||||
start: startDate,
|
||||
end: endDate,
|
||||
type: eventType,
|
||||
allDay: false,
|
||||
syncStatus: 'synced',
|
||||
metadata: {
|
||||
duration: duration
|
||||
}
|
||||
};
|
||||
|
||||
// Create timed event element
|
||||
const timedEvent = document.createElement('swp-event');
|
||||
timedEvent.dataset.eventId = eventId;
|
||||
timedEvent.dataset.title = eventTitle;
|
||||
timedEvent.dataset.type = eventType;
|
||||
timedEvent.dataset.start = startDate.toISOString();
|
||||
timedEvent.dataset.end = endDate.toISOString();
|
||||
timedEvent.dataset.duration = duration.toString();
|
||||
timedEvent.dataset.originalDuration = duration.toString();
|
||||
|
||||
// Create inner structure using helper method
|
||||
timedEvent.innerHTML = this.createEventInnerStructure(tempEvent);
|
||||
|
||||
// Apply drag styling and positioning
|
||||
this.applyDragStyling(timedEvent);
|
||||
const eventHeight = (duration / 60) * hourHeight - 3;
|
||||
timedEvent.style.height = `${eventHeight}px`;
|
||||
timedEvent.style.top = `${targetY}px`;
|
||||
|
||||
// Remove all-day element
|
||||
allDayClone.remove();
|
||||
|
||||
// Add timed event to events layer
|
||||
eventsLayer.appendChild(timedEvent);
|
||||
|
||||
// Update reference
|
||||
this.draggedClone = timedEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fade out and remove element
|
||||
|
|
@ -872,26 +1008,14 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
eventElement.dataset.type = event.type;
|
||||
eventElement.dataset.duration = event.metadata?.duration?.toString() || '60';
|
||||
|
||||
// Calculate position based on time
|
||||
// Calculate and apply position based on time
|
||||
const position = this.calculateEventPosition(event);
|
||||
eventElement.style.position = 'absolute';
|
||||
eventElement.style.top = `${position.top + 1}px`;
|
||||
eventElement.style.height = `${position.height - 3}px`; //adjusted so bottom does not cover horizontal time lines.
|
||||
this.applyEventPositioning(eventElement, position.top + 1, position.height - 3);
|
||||
|
||||
// Color is now handled by CSS classes based on data-type attribute
|
||||
|
||||
// Format time for display using unified method
|
||||
const startTime = this.formatTime(event.start);
|
||||
const endTime = this.formatTime(event.end);
|
||||
|
||||
// Calculate duration in minutes
|
||||
const durationMinutes = (event.end.getTime() - event.start.getTime()) / (1000 * 60);
|
||||
|
||||
// Create event content
|
||||
eventElement.innerHTML = `
|
||||
<swp-event-time data-duration="${durationMinutes}">${startTime} - ${endTime}</swp-event-time>
|
||||
<swp-event-title>${event.title}</swp-event-title>
|
||||
`;
|
||||
// Create event content using helper method
|
||||
eventElement.innerHTML = this.createEventInnerStructure(event);
|
||||
|
||||
// Setup resize handles on first mouseover only
|
||||
eventElement.addEventListener('mouseover', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue