Refactors event drag-drop and cloning logic
Centralizes drag event listener setup in `EventRendererManager` for better separation of concerns. Introduces factory and cloning methods in `SwpEventElement` to simplify event cloning and data extraction from DOM elements during drag operations. Enhances `DragDropManager` to pass the actual dragged element for conversion and accurately detect the drop target (day column or header). Updates `EventRenderer` to expose drag-handling methods publicly, allowing the `EventRendererManager` to delegate event-specific drag operations based on drop target.
This commit is contained in:
parent
0b7499521e
commit
b4f5b29da3
6 changed files with 357 additions and 304 deletions
|
|
@ -101,6 +101,83 @@ export class SwpEventElement extends BaseEventElement {
|
||||||
return new SwpEventElement(event);
|
return new SwpEventElement(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a clone of this SwpEventElement with "clone-" prefix
|
||||||
|
*/
|
||||||
|
public createClone(): SwpEventElement {
|
||||||
|
// Clone the underlying DOM element
|
||||||
|
const clonedElement = this.element.cloneNode(true) as HTMLElement;
|
||||||
|
|
||||||
|
// Create new SwpEventElement instance from the cloned DOM
|
||||||
|
const clonedSwpEvent = SwpEventElement.fromExistingElement(clonedElement);
|
||||||
|
|
||||||
|
// Apply "clone-" prefix to ID
|
||||||
|
clonedSwpEvent.updateEventId(`clone-${this.event.id}`);
|
||||||
|
|
||||||
|
// Cache original duration for drag operations
|
||||||
|
const originalDuration = this.getOriginalEventDuration();
|
||||||
|
clonedSwpEvent.element.dataset.originalDuration = originalDuration.toString();
|
||||||
|
|
||||||
|
// Set height from original element
|
||||||
|
clonedSwpEvent.element.style.height = this.element.style.height || `${this.element.getBoundingClientRect().height}px`;
|
||||||
|
|
||||||
|
return clonedSwpEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory method to create SwpEventElement from existing DOM element
|
||||||
|
*/
|
||||||
|
public static fromExistingElement(element: HTMLElement): SwpEventElement {
|
||||||
|
// Extract CalendarEvent data from DOM element
|
||||||
|
const event = this.extractCalendarEventFromElement(element);
|
||||||
|
|
||||||
|
// Create new instance but replace the created element with the existing one
|
||||||
|
const swpEvent = new SwpEventElement(event);
|
||||||
|
swpEvent.element = element;
|
||||||
|
|
||||||
|
return swpEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the event ID in both the CalendarEvent and DOM element
|
||||||
|
*/
|
||||||
|
private updateEventId(newId: string): void {
|
||||||
|
this.event.id = newId;
|
||||||
|
this.element.dataset.eventId = newId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract original event duration from DOM element
|
||||||
|
*/
|
||||||
|
private getOriginalEventDuration(): number {
|
||||||
|
const timeElement = this.element.querySelector('swp-event-time');
|
||||||
|
if (timeElement) {
|
||||||
|
const duration = timeElement.getAttribute('data-duration');
|
||||||
|
if (duration) {
|
||||||
|
return parseInt(duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 60; // Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract CalendarEvent from DOM element
|
||||||
|
*/
|
||||||
|
private static extractCalendarEventFromElement(element: HTMLElement): CalendarEvent {
|
||||||
|
return {
|
||||||
|
id: element.dataset.eventId || '',
|
||||||
|
title: element.dataset.title || '',
|
||||||
|
start: new Date(element.dataset.start || ''),
|
||||||
|
end: new Date(element.dataset.end || ''),
|
||||||
|
type: element.dataset.type || 'work',
|
||||||
|
allDay: false,
|
||||||
|
syncStatus: 'synced',
|
||||||
|
metadata: {
|
||||||
|
duration: element.dataset.duration
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory method to convert an all-day HTML element to a timed SwpEventElement
|
* Factory method to convert an all-day HTML element to a timed SwpEventElement
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -74,14 +74,26 @@ export class AllDayManager {
|
||||||
});
|
});
|
||||||
|
|
||||||
eventBus.on('drag:end', (event) => {
|
eventBus.on('drag:end', (event) => {
|
||||||
const { eventId, finalPosition } = (event as CustomEvent).detail;
|
|
||||||
|
const { eventId, finalColumn, finalY, dropTarget } = (event as CustomEvent).detail;
|
||||||
|
|
||||||
|
if (dropTarget != 'SWP-DAY-HEADER')//we are not inside the swp-day-header, so just ignore.
|
||||||
|
return;
|
||||||
|
|
||||||
|
console.log('🎬 AllDayManager: Received drag:end', {
|
||||||
|
eventId: eventId,
|
||||||
|
finalColumn: finalColumn,
|
||||||
|
finalY: finalY
|
||||||
|
});
|
||||||
|
|
||||||
// Check if this was an all-day event
|
// Check if this was an all-day event
|
||||||
const originalElement = document.querySelector(`swp-allday-container swp-allday-event[data-event-id="${eventId}"]`);
|
const originalElement = document.querySelector(`swp-allday-container swp-allday-event[data-event-id="${eventId}"]`);
|
||||||
const dragClone = document.querySelector(`swp-allday-container swp-allday-event[data-event-id="clone-${eventId}"]`);
|
const dragClone = document.querySelector(`swp-allday-container swp-allday-event[data-event-id="clone-${eventId}"]`);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
console.log('🎯 AllDayManager: Ending drag for all-day event', { eventId });
|
console.log('🎯 AllDayManager: Ending drag for all-day event', { eventId });
|
||||||
this.handleDragEnd(originalElement as HTMLElement, dragClone as HTMLElement, finalPosition);
|
this.handleDragEnd(originalElement as HTMLElement, dragClone as HTMLElement, finalColumn);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -172,8 +172,11 @@ export class DragDropManager {
|
||||||
const mousePosition = { x: this.lastMousePosition.x, y: this.lastMousePosition.y };
|
const mousePosition = { x: this.lastMousePosition.x, y: this.lastMousePosition.y };
|
||||||
const column = this.getColumnDateFromX(mousePosition.x);
|
const column = this.getColumnDateFromX(mousePosition.x);
|
||||||
|
|
||||||
|
// Find the actual dragged element
|
||||||
|
const draggedElement = document.querySelector(`[data-event-id="${this.draggedEventId}"]`) as HTMLElement;
|
||||||
|
|
||||||
this.eventBus.emit('drag:convert-to-time_event', {
|
this.eventBus.emit('drag:convert-to-time_event', {
|
||||||
draggedEventId: this.draggedEventId,
|
draggedElement: draggedElement,
|
||||||
mousePosition: mousePosition,
|
mousePosition: mousePosition,
|
||||||
column: column
|
column: column
|
||||||
});
|
});
|
||||||
|
|
@ -319,10 +322,14 @@ export class DragDropManager {
|
||||||
// Use consolidated position calculation
|
// Use consolidated position calculation
|
||||||
const positionData = this.calculateDragPosition(finalPosition);
|
const positionData = this.calculateDragPosition(finalPosition);
|
||||||
|
|
||||||
|
// Detect drop target (swp-day-column or swp-day-header)
|
||||||
|
const dropTarget = this.detectDropTarget(finalPosition);
|
||||||
|
|
||||||
console.log('🎯 DragDropManager: Emitting drag:end', {
|
console.log('🎯 DragDropManager: Emitting drag:end', {
|
||||||
eventId: eventId,
|
eventId: eventId,
|
||||||
finalColumn: positionData.column,
|
finalColumn: positionData.column,
|
||||||
finalY: positionData.snappedY,
|
finalY: positionData.snappedY,
|
||||||
|
dropTarget: dropTarget,
|
||||||
isDragStarted: isDragStarted
|
isDragStarted: isDragStarted
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -330,7 +337,8 @@ export class DragDropManager {
|
||||||
eventId: eventId,
|
eventId: eventId,
|
||||||
finalPosition,
|
finalPosition,
|
||||||
finalColumn: positionData.column,
|
finalColumn: positionData.column,
|
||||||
finalY: positionData.snappedY
|
finalY: positionData.snappedY,
|
||||||
|
target: dropTarget
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// This was just a click - emit click event instead
|
// This was just a click - emit click event instead
|
||||||
|
|
@ -411,9 +419,6 @@ export class DragDropManager {
|
||||||
// Sorter efter x-position (fra venstre til højre)
|
// Sorter efter x-position (fra venstre til højre)
|
||||||
this.columnBoundsCache.sort((a, b) => a.left - b.left);
|
this.columnBoundsCache.sort((a, b) => a.left - b.left);
|
||||||
|
|
||||||
console.log('📏 DragDropManager: Updated column bounds cache', {
|
|
||||||
columns: this.columnBoundsCache.length
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -592,6 +597,28 @@ export class DragDropManager {
|
||||||
return allDayElement !== null;
|
return allDayElement !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect drop target - whether dropped in swp-day-column or swp-day-header
|
||||||
|
*/
|
||||||
|
private detectDropTarget(position: Position): 'swp-day-column' | 'swp-day-header' | null {
|
||||||
|
const elementAtPosition = document.elementFromPoint(position.x, position.y);
|
||||||
|
if (!elementAtPosition) return null;
|
||||||
|
|
||||||
|
// Traverse up the DOM tree to find the target container
|
||||||
|
let currentElement = elementAtPosition as HTMLElement;
|
||||||
|
while (currentElement && currentElement !== document.body) {
|
||||||
|
if (currentElement.tagName === 'SWP-DAY-HEADER') {
|
||||||
|
return 'swp-day-header';
|
||||||
|
}
|
||||||
|
if (currentElement.tagName === 'SWP-DAY-COLUMN') {
|
||||||
|
return 'swp-day-column';
|
||||||
|
}
|
||||||
|
currentElement = currentElement.parentElement as HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up all resources and event listeners
|
* Clean up all resources and event listeners
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -44,22 +44,15 @@ export class HeaderManager {
|
||||||
* Setup header drag event listeners - REFACTORED to use mouseenter
|
* Setup header drag event listeners - REFACTORED to use mouseenter
|
||||||
*/
|
*/
|
||||||
public setupHeaderDragListeners(): void {
|
public setupHeaderDragListeners(): void {
|
||||||
const calendarHeader = this.getCalendarHeader();
|
if (!this.getCalendarHeader()) return;
|
||||||
if (!calendarHeader) return;
|
|
||||||
|
|
||||||
console.log('🎯 HeaderManager: Setting up drag listeners with mouseenter');
|
console.log('🎯 HeaderManager: Setting up drag listeners with mouseenter');
|
||||||
|
|
||||||
// Track last processed date to avoid duplicates
|
|
||||||
let lastProcessedDate: string | null = null;
|
|
||||||
let lastProcessedTime = 0;
|
|
||||||
|
|
||||||
// Use mouseenter instead of mouseover to avoid continuous firing
|
// Use mouseenter instead of mouseover to avoid continuous firing
|
||||||
this.headerEventListener = (event: Event) => {
|
this.headerEventListener = (event: Event) => {
|
||||||
// OPTIMIZED: Check for active drag operation FIRST before doing any other work
|
|
||||||
const isDragActive = document.querySelector('.dragging') !== null;
|
|
||||||
|
|
||||||
if (!isDragActive) {
|
if (!document.querySelector('.dragging') !== null) {
|
||||||
// Ingen drag operation, spring resten af funktionen over
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,9 +107,8 @@ export class HeaderManager {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use mouseenter with capture to catch events early
|
this.getCalendarHeader()?.addEventListener('mouseenter', this.headerEventListener, true);
|
||||||
calendarHeader.addEventListener('mouseenter', this.headerEventListener, true);
|
this.getCalendarHeader()?.addEventListener('mouseleave', this.headerMouseLeaveListener);
|
||||||
calendarHeader.addEventListener('mouseleave', this.headerMouseLeaveListener);
|
|
||||||
|
|
||||||
console.log('✅ HeaderManager: Event listeners attached (mouseenter + mouseleave)');
|
console.log('✅ HeaderManager: Event listeners attached (mouseenter + mouseleave)');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,13 @@ import { PositionUtils } from '../utils/PositionUtils';
|
||||||
export interface EventRendererStrategy {
|
export interface EventRendererStrategy {
|
||||||
renderEvents(events: CalendarEvent[], container: HTMLElement): void;
|
renderEvents(events: CalendarEvent[], container: HTMLElement): void;
|
||||||
clearEvents(container?: HTMLElement): void;
|
clearEvents(container?: HTMLElement): void;
|
||||||
|
handleDragStart?(originalElement: HTMLElement, eventId: string, mouseOffset: any, column: string): void;
|
||||||
|
handleDragMove?(eventId: string, snappedY: number, column: string, mouseOffset: any): void;
|
||||||
|
handleDragAutoScroll?(eventId: string, snappedY: number): void;
|
||||||
|
handleDragEnd?(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: string, finalY: number): void;
|
||||||
|
handleEventClick?(eventId: string, originalElement: HTMLElement): void;
|
||||||
|
handleColumnChange?(eventId: string, newColumn: string): void;
|
||||||
|
handleNavigationCompleted?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -23,11 +30,11 @@ export interface EventRendererStrategy {
|
||||||
*/
|
*/
|
||||||
export abstract class BaseEventRenderer implements EventRendererStrategy {
|
export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
protected dateCalculator: DateCalculator;
|
protected dateCalculator: DateCalculator;
|
||||||
|
|
||||||
// Drag and drop state
|
// Drag and drop state
|
||||||
private draggedClone: HTMLElement | null = null;
|
private draggedClone: HTMLElement | null = null;
|
||||||
private originalEvent: HTMLElement | null = null;
|
private originalEvent: HTMLElement | null = null;
|
||||||
|
|
||||||
// Resize manager
|
// Resize manager
|
||||||
|
|
||||||
constructor(dateCalculator?: DateCalculator) {
|
constructor(dateCalculator?: DateCalculator) {
|
||||||
|
|
@ -70,12 +77,12 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
|
|
||||||
const remainingEvents = events.slice(index + 1);
|
const remainingEvents = events.slice(index + 1);
|
||||||
const overlappingEvents = this.overlapDetector.resolveOverlap(currentEvent, remainingEvents);
|
const overlappingEvents = this.overlapDetector.resolveOverlap(currentEvent, remainingEvents);
|
||||||
|
|
||||||
if (overlappingEvents.length > 0) {
|
if (overlappingEvents.length > 0) {
|
||||||
// Der er overlaps - opret stack links
|
// Der er overlaps - opret stack links
|
||||||
const result = this.overlapDetector.decorateWithStackLinks(currentEvent, overlappingEvents);
|
const result = this.overlapDetector.decorateWithStackLinks(currentEvent, overlappingEvents);
|
||||||
this.renderOverlappingEvents(result, container);
|
this.renderOverlappingEvents(result, container);
|
||||||
|
|
||||||
// Marker alle events i overlap gruppen som processeret
|
// Marker alle events i overlap gruppen som processeret
|
||||||
overlappingEvents.forEach(event => processedEvents.add(event.id));
|
overlappingEvents.forEach(event => processedEvents.add(event.id));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -90,90 +97,14 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup listeners for drag events from DragDropManager
|
* Setup listeners for drag events from DragDropManager
|
||||||
|
* NOTE: Event listeners moved to EventRendererManager for better separation of concerns
|
||||||
*/
|
*/
|
||||||
protected setupDragEventListeners(): void {
|
protected setupDragEventListeners(): void {
|
||||||
// Handle drag start
|
// All event listeners now handled by EventRendererManager
|
||||||
eventBus.on('drag:start', (event) => {
|
// This method kept for backward compatibility but does nothing
|
||||||
const { eventId, mouseOffset, column } = (event as CustomEvent).detail;
|
|
||||||
// Find element dynamically
|
|
||||||
const originalElement = document.querySelector(`swp-event[data-event-id="${eventId}"]`) as HTMLElement;
|
|
||||||
if (originalElement) {
|
|
||||||
this.handleDragStart(originalElement, eventId, mouseOffset, column);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle drag move
|
|
||||||
eventBus.on('drag:move', (event) => {
|
|
||||||
const { eventId, snappedY, column, mouseOffset } = (event as CustomEvent).detail;
|
|
||||||
this.handleDragMove(eventId, snappedY, column, mouseOffset);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle drag auto-scroll (when dragging near edges triggers scroll)
|
|
||||||
eventBus.on('drag:auto-scroll', (event) => {
|
|
||||||
const { eventId, snappedY } = (event as CustomEvent).detail;
|
|
||||||
if (!this.draggedClone) return;
|
|
||||||
|
|
||||||
// Update position directly using the calculated snapped position
|
|
||||||
this.draggedClone.style.top = snappedY + 'px';
|
|
||||||
|
|
||||||
// Update timestamp display
|
|
||||||
this.updateCloneTimestamp(this.draggedClone, snappedY);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle drag end
|
|
||||||
eventBus.on('drag:end', (event) => {
|
|
||||||
const { eventId, finalColumn, finalY } = (event as CustomEvent).detail;
|
|
||||||
|
|
||||||
console.log('🎬 EventRenderer: Received drag:end', {
|
|
||||||
eventId: eventId,
|
|
||||||
finalColumn: finalColumn,
|
|
||||||
finalY: finalY
|
|
||||||
});
|
|
||||||
|
|
||||||
// Find element dynamically - could be swp-event or swp-allday-event
|
|
||||||
let originalElement = document.querySelector(`swp-event[data-event-id="${eventId}"]`) as HTMLElement;
|
|
||||||
let elementType = 'day-event';
|
|
||||||
if (!originalElement) {
|
|
||||||
originalElement = document.querySelector(`swp-allday-event[data-event-id="${eventId}"]`) as HTMLElement;
|
|
||||||
elementType = 'all-day-event';
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('🔍 EventRenderer: Found element', {
|
|
||||||
elementType: elementType,
|
|
||||||
found: !!originalElement,
|
|
||||||
tagName: originalElement?.tagName
|
|
||||||
});
|
|
||||||
|
|
||||||
if (originalElement) {
|
|
||||||
this.handleDragEnd(eventId, originalElement, finalColumn, finalY);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle click (when drag threshold not reached)
|
|
||||||
eventBus.on('event:click', (event) => {
|
|
||||||
const { eventId } = (event as CustomEvent).detail;
|
|
||||||
// Find element dynamically
|
|
||||||
let originalElement = document.querySelector(`swp-event[data-event-id="${eventId}"]`) as HTMLElement;
|
|
||||||
if (!originalElement) {
|
|
||||||
originalElement = document.querySelector(`swp-allday-event[data-event-id="${eventId}"]`) as HTMLElement;
|
|
||||||
}
|
|
||||||
this.handleEventClick(eventId, originalElement);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle column change
|
|
||||||
eventBus.on('drag:column-change', (event) => {
|
|
||||||
const { eventId, newColumn } = (event as CustomEvent).detail;
|
|
||||||
this.handleColumnChange(eventId, newColumn);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Handle navigation period change (when slide animation completes)
|
|
||||||
eventBus.on(CoreEvents.NAVIGATION_COMPLETED, () => {
|
|
||||||
// Animate all-day height after navigation completes
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleanup method for proper resource management
|
* Cleanup method for proper resource management
|
||||||
*/
|
*/
|
||||||
|
|
@ -182,23 +113,6 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
this.originalEvent = null;
|
this.originalEvent = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get original event duration from data-duration attribute
|
|
||||||
*/
|
|
||||||
private getOriginalEventDuration(originalEvent: HTMLElement): number {
|
|
||||||
// Find the swp-event-time element with data-duration attribute
|
|
||||||
const timeElement = originalEvent.querySelector('swp-event-time');
|
|
||||||
if (timeElement) {
|
|
||||||
const duration = timeElement.getAttribute('data-duration');
|
|
||||||
if (duration) {
|
|
||||||
const durationMinutes = parseInt(duration);
|
|
||||||
return durationMinutes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to 60 minutes if attribute not found
|
|
||||||
return 60;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply common drag styling to an element
|
* Apply common drag styling to an element
|
||||||
|
|
@ -207,89 +121,41 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
element.classList.add('dragging');
|
element.classList.add('dragging');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create event inner structure (swp-event-time and swp-event-title)
|
|
||||||
*/
|
|
||||||
private createEventInnerStructure(event: CalendarEvent): string {
|
|
||||||
const timeRange = TimeFormatter.formatTimeRange(event.start, event.end);
|
|
||||||
const durationMinutes = (event.end.getTime() - event.start.getTime()) / (1000 * 60);
|
|
||||||
|
|
||||||
return `
|
|
||||||
<swp-event-time data-duration="${durationMinutes}">${timeRange}</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
|
|
||||||
*/
|
|
||||||
private createEventClone(originalEvent: HTMLElement): HTMLElement {
|
|
||||||
const clone = originalEvent.cloneNode(true) as HTMLElement;
|
|
||||||
|
|
||||||
// Prefix ID with "clone-"
|
|
||||||
const originalId = originalEvent.dataset.eventId;
|
|
||||||
if (originalId) {
|
|
||||||
clone.dataset.eventId = `clone-${originalId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get and cache original duration from data-duration attribute
|
|
||||||
const originalDurationMinutes = this.getOriginalEventDuration(originalEvent);
|
|
||||||
clone.dataset.originalDuration = originalDurationMinutes.toString();
|
|
||||||
|
|
||||||
// Apply common drag styling
|
|
||||||
this.applyDragStyling(clone);
|
|
||||||
|
|
||||||
// Set height from original event
|
|
||||||
clone.style.height = originalEvent.style.height || `${originalEvent.getBoundingClientRect().height}px`;
|
|
||||||
|
|
||||||
return clone;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update clone timestamp based on new position
|
* Update clone timestamp based on new position
|
||||||
*/
|
*/
|
||||||
private updateCloneTimestamp(clone: HTMLElement, snappedY: number): void {
|
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
|
//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;
|
if (clone.dataset.allDay == "true") return;
|
||||||
|
|
||||||
const gridSettings = calendarConfig.getGridSettings();
|
const gridSettings = calendarConfig.getGridSettings();
|
||||||
const hourHeight = gridSettings.hourHeight;
|
const hourHeight = gridSettings.hourHeight;
|
||||||
const dayStartHour = gridSettings.dayStartHour;
|
const dayStartHour = gridSettings.dayStartHour;
|
||||||
const snapInterval = gridSettings.snapInterval;
|
const snapInterval = gridSettings.snapInterval;
|
||||||
|
|
||||||
// Calculate minutes from grid start (not from midnight)
|
// Calculate minutes from grid start (not from midnight)
|
||||||
const minutesFromGridStart = (snappedY / hourHeight) * 60;
|
const minutesFromGridStart = (snappedY / hourHeight) * 60;
|
||||||
|
|
||||||
// Add dayStartHour offset to get actual time
|
// Add dayStartHour offset to get actual time
|
||||||
const actualStartMinutes = (dayStartHour * 60) + minutesFromGridStart;
|
const actualStartMinutes = (dayStartHour * 60) + minutesFromGridStart;
|
||||||
|
|
||||||
// Snap to interval
|
// Snap to interval
|
||||||
const snappedStartMinutes = Math.round(actualStartMinutes / snapInterval) * snapInterval;
|
const snappedStartMinutes = Math.round(actualStartMinutes / snapInterval) * snapInterval;
|
||||||
|
|
||||||
// Use cached original duration (no recalculation)
|
// Use cached original duration (no recalculation)
|
||||||
const cachedDuration = parseInt(clone.dataset.originalDuration || '60');
|
const cachedDuration = parseInt(clone.dataset.originalDuration || '60');
|
||||||
const endTotalMinutes = snappedStartMinutes + cachedDuration;
|
const endTotalMinutes = snappedStartMinutes + cachedDuration;
|
||||||
|
|
||||||
// Update dataset with reference date for performance
|
// Update dataset with reference date for performance
|
||||||
const referenceDate = new Date('1970-01-01T00:00:00');
|
const referenceDate = new Date('1970-01-01T00:00:00');
|
||||||
const startDate = new Date(referenceDate);
|
const startDate = new Date(referenceDate);
|
||||||
startDate.setMinutes(startDate.getMinutes() + snappedStartMinutes);
|
startDate.setMinutes(startDate.getMinutes() + snappedStartMinutes);
|
||||||
|
|
||||||
const endDate = new Date(referenceDate);
|
const endDate = new Date(referenceDate);
|
||||||
endDate.setMinutes(endDate.getMinutes() + endTotalMinutes);
|
endDate.setMinutes(endDate.getMinutes() + endTotalMinutes);
|
||||||
|
|
||||||
clone.dataset.start = startDate.toISOString();
|
clone.dataset.start = startDate.toISOString();
|
||||||
clone.dataset.end = endDate.toISOString();
|
clone.dataset.end = endDate.toISOString();
|
||||||
// Update display
|
// Update display
|
||||||
|
|
@ -300,18 +166,25 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
timeElement.textContent = `${startTime} - ${endTime}`;
|
timeElement.textContent = `${startTime} - ${endTime}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle drag start event
|
* Handle drag start event
|
||||||
*/
|
*/
|
||||||
private handleDragStart(originalElement: HTMLElement, eventId: string, mouseOffset: any, column: string): void {
|
public handleDragStart(originalElement: HTMLElement, eventId: string, mouseOffset: any, column: string): void {
|
||||||
this.originalEvent = originalElement;
|
this.originalEvent = originalElement;
|
||||||
|
|
||||||
// Remove stacking styling during drag will be handled by new system
|
// Remove stacking styling during drag will be handled by new system
|
||||||
|
|
||||||
|
// Create SwpEventElement from existing DOM element and clone it
|
||||||
|
const originalSwpEvent = SwpEventElement.fromExistingElement(originalElement);
|
||||||
|
const clonedSwpEvent = originalSwpEvent.createClone();
|
||||||
|
|
||||||
// Create clone
|
// Get the cloned DOM element
|
||||||
this.draggedClone = this.createEventClone(originalElement);
|
this.draggedClone = clonedSwpEvent.getElement();
|
||||||
|
|
||||||
|
// Apply drag styling
|
||||||
|
this.applyDragStyling(this.draggedClone);
|
||||||
|
|
||||||
// Add to current column's events layer (not directly to column)
|
// Add to current column's events layer (not directly to column)
|
||||||
const columnElement = document.querySelector(`swp-day-column[data-date="${column}"]`);
|
const columnElement = document.querySelector(`swp-day-column[data-date="${column}"]`);
|
||||||
if (columnElement) {
|
if (columnElement) {
|
||||||
|
|
@ -323,33 +196,46 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
columnElement.appendChild(this.draggedClone);
|
columnElement.appendChild(this.draggedClone);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make original semi-transparent
|
// Make original semi-transparent
|
||||||
originalElement.style.opacity = '0.3';
|
originalElement.style.opacity = '0.3';
|
||||||
originalElement.style.userSelect = 'none';
|
originalElement.style.userSelect = 'none';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle drag move event
|
* Handle drag move event
|
||||||
*/
|
*/
|
||||||
private handleDragMove(eventId: string, snappedY: number, column: string, mouseOffset: any): void {
|
public handleDragMove(eventId: string, snappedY: number, column: string, mouseOffset: any): void {
|
||||||
if (!this.draggedClone) return;
|
if (!this.draggedClone) return;
|
||||||
|
|
||||||
// Update position
|
// Update position
|
||||||
this.draggedClone.style.top = snappedY + 'px';
|
this.draggedClone.style.top = snappedY + 'px';
|
||||||
|
|
||||||
// Update timestamp display
|
// Update timestamp display
|
||||||
this.updateCloneTimestamp(this.draggedClone, snappedY);
|
this.updateCloneTimestamp(this.draggedClone, snappedY);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle drag auto-scroll event
|
||||||
|
*/
|
||||||
|
public handleDragAutoScroll(eventId: string, snappedY: number): void {
|
||||||
|
if (!this.draggedClone) return;
|
||||||
|
|
||||||
|
// Update position directly using the calculated snapped position
|
||||||
|
this.draggedClone.style.top = snappedY + 'px';
|
||||||
|
|
||||||
|
// Update timestamp display
|
||||||
|
this.updateCloneTimestamp(this.draggedClone, snappedY);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle column change during drag
|
* Handle column change during drag
|
||||||
*/
|
*/
|
||||||
private handleColumnChange(eventId: string, newColumn: string): void {
|
public handleColumnChange(eventId: string, newColumn: string): void {
|
||||||
if (!this.draggedClone) return;
|
if (!this.draggedClone) return;
|
||||||
|
|
||||||
// Move clone to new column's events layer
|
// Move clone to new column's events layer
|
||||||
const newColumnElement = document.querySelector(`swp-day-column[data-date="${newColumn}"]`);
|
const newColumnElement = document.querySelector(`swp-day-column[data-date="${newColumn}"]`);
|
||||||
if (newColumnElement) {
|
if (newColumnElement) {
|
||||||
|
|
@ -362,24 +248,24 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle drag end event
|
* Handle drag end event
|
||||||
*/
|
*/
|
||||||
private handleDragEnd(eventId: string, originalElement: HTMLElement, finalColumn: string, finalY: number): void {
|
public handleDragEnd(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: string, finalY: number): void {
|
||||||
|
|
||||||
if (!this.draggedClone || !this.originalEvent) {
|
if (!draggedClone || !originalElement) {
|
||||||
console.warn('Missing draggedClone or originalEvent');
|
console.warn('Missing draggedClone or originalElement');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check om original event var del af en stack
|
// Check om original event var del af en stack
|
||||||
const originalStackLink = this.originalEvent.dataset.stackLink;
|
const originalStackLink = originalElement.dataset.stackLink;
|
||||||
|
|
||||||
if (originalStackLink) {
|
if (originalStackLink) {
|
||||||
try {
|
try {
|
||||||
const stackData = JSON.parse(originalStackLink);
|
const stackData = JSON.parse(originalStackLink);
|
||||||
|
|
||||||
// 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();
|
||||||
|
|
||||||
|
|
@ -392,10 +278,10 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
try {
|
try {
|
||||||
const prevLinkData = JSON.parse(prevElement.dataset.stackLink);
|
const prevLinkData = JSON.parse(prevElement.dataset.stackLink);
|
||||||
traverseStack(prevLinkData, visitedIds);
|
traverseStack(prevLinkData, visitedIds);
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (linkData.next && !visitedIds.has(linkData.next)) {
|
if (linkData.next && !visitedIds.has(linkData.next)) {
|
||||||
visitedIds.add(linkData.next);
|
visitedIds.add(linkData.next);
|
||||||
const nextElement = document.querySelector(`swp-time-grid [data-event-id="${linkData.next}"]`) as HTMLElement;
|
const nextElement = document.querySelector(`swp-time-grid [data-event-id="${linkData.next}"]`) as HTMLElement;
|
||||||
|
|
@ -403,7 +289,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
try {
|
try {
|
||||||
const nextLinkData = JSON.parse(nextElement.dataset.stackLink);
|
const nextLinkData = JSON.parse(nextElement.dataset.stackLink);
|
||||||
traverseStack(nextLinkData, visitedIds);
|
traverseStack(nextLinkData, visitedIds);
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -425,17 +311,17 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
if (!container) {
|
if (!container) {
|
||||||
container = element.closest('swp-events-layer') as HTMLElement;
|
container = element.closest('swp-events-layer') as HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = this.elementToCalendarEvent(element);
|
const event = this.elementToCalendarEvent(element);
|
||||||
if (event) {
|
if (event) {
|
||||||
stackEvents.push(event);
|
stackEvents.push(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fjern elementet
|
// Fjern elementet
|
||||||
element.remove();
|
element.remove();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Re-render stack events hvis vi fandt nogle
|
// Re-render stack events hvis vi fandt nogle
|
||||||
if (stackEvents.length > 0 && container) {
|
if (stackEvents.length > 0 && container) {
|
||||||
this.handleEventOverlaps(stackEvents, container);
|
this.handleEventOverlaps(stackEvents, container);
|
||||||
|
|
@ -444,93 +330,100 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
console.warn('Failed to parse stackLink data:', e);
|
console.warn('Failed to parse stackLink data:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove original event from any existing groups first
|
// Remove original event from any existing groups first
|
||||||
this.removeEventFromExistingGroups(this.originalEvent);
|
this.removeEventFromExistingGroups(originalElement);
|
||||||
|
|
||||||
// Fade out original
|
// Fade out original
|
||||||
this.fadeOutAndRemove(this.originalEvent);
|
this.fadeOutAndRemove(originalElement);
|
||||||
|
|
||||||
// Remove clone prefix and normalize clone to be a regular event
|
// Remove clone prefix and normalize clone to be a regular event
|
||||||
const cloneId = this.draggedClone.dataset.eventId;
|
const cloneId = draggedClone.dataset.eventId;
|
||||||
if (cloneId && cloneId.startsWith('clone-')) {
|
if (cloneId && cloneId.startsWith('clone-')) {
|
||||||
this.draggedClone.dataset.eventId = cloneId.replace('clone-', '');
|
draggedClone.dataset.eventId = cloneId.replace('clone-', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fully normalize the clone to be a regular event
|
// Fully normalize the clone to be a regular event
|
||||||
this.draggedClone.classList.remove('dragging');
|
draggedClone.classList.remove('dragging');
|
||||||
// Behold z-index hvis det er et stacked event
|
// Behold z-index hvis det er et stacked event
|
||||||
|
|
||||||
// Update dataset with new times after successful drop (only for timed events)
|
// Update dataset with new times after successful drop (only for timed events)
|
||||||
if (this.draggedClone.dataset.displayType !== 'allday') {
|
if (draggedClone.dataset.displayType !== 'allday') {
|
||||||
const newEvent = this.elementToCalendarEvent(this.draggedClone);
|
const newEvent = this.elementToCalendarEvent(draggedClone);
|
||||||
if (newEvent) {
|
if (newEvent) {
|
||||||
this.draggedClone.dataset.start = newEvent.start.toISOString();
|
draggedClone.dataset.start = newEvent.start.toISOString();
|
||||||
this.draggedClone.dataset.end = newEvent.end.toISOString();
|
draggedClone.dataset.end = newEvent.end.toISOString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect overlaps with other events in the target column and reposition if needed
|
// Detect overlaps with other events in the target column and reposition if needed
|
||||||
this.handleDragDropOverlaps(this.draggedClone, finalColumn);
|
this.handleDragDropOverlaps(draggedClone, finalColumn);
|
||||||
|
|
||||||
// Fjern stackLink data fra dropped element
|
// Fjern stackLink data fra dropped element
|
||||||
if (this.draggedClone.dataset.stackLink) {
|
if (draggedClone.dataset.stackLink) {
|
||||||
delete this.draggedClone.dataset.stackLink;
|
delete draggedClone.dataset.stackLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up
|
// Clean up instance state (no longer needed since we get elements as parameters)
|
||||||
this.draggedClone = null;
|
this.draggedClone = null;
|
||||||
this.originalEvent = null;
|
this.originalEvent = null;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle event click (when drag threshold not reached)
|
* Handle event click (when drag threshold not reached)
|
||||||
*/
|
*/
|
||||||
private handleEventClick(eventId: string, originalElement: HTMLElement): void {
|
public handleEventClick(eventId: string, originalElement: HTMLElement): void {
|
||||||
console.log('handleEventClick:', eventId);
|
console.log('handleEventClick:', eventId);
|
||||||
|
|
||||||
// Clean up any drag artifacts from failed drag attempt
|
// Clean up any drag artifacts from failed drag attempt
|
||||||
if (this.draggedClone) {
|
if (this.draggedClone) {
|
||||||
this.draggedClone.classList.remove('dragging');
|
this.draggedClone.classList.remove('dragging');
|
||||||
this.draggedClone.remove();
|
this.draggedClone.remove();
|
||||||
this.draggedClone = null;
|
this.draggedClone = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore original element styling if it was modified
|
// Restore original element styling if it was modified
|
||||||
if (this.originalEvent) {
|
if (this.originalEvent) {
|
||||||
this.originalEvent.style.opacity = '';
|
this.originalEvent.style.opacity = '';
|
||||||
this.originalEvent.style.userSelect = '';
|
this.originalEvent.style.userSelect = '';
|
||||||
this.originalEvent = null;
|
this.originalEvent = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit a clean click event for other components to handle
|
// Emit a clean click event for other components to handle
|
||||||
eventBus.emit('event:clicked', {
|
eventBus.emit('event:clicked', {
|
||||||
eventId: eventId,
|
eventId: eventId,
|
||||||
element: originalElement
|
element: originalElement
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle navigation completed event
|
||||||
|
*/
|
||||||
|
public handleNavigationCompleted(): void {
|
||||||
|
// Default implementation - can be overridden by subclasses
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle overlap detection and re-rendering after drag-drop
|
* Handle overlap detection and re-rendering after drag-drop
|
||||||
*/
|
*/
|
||||||
private handleDragDropOverlaps(droppedElement: HTMLElement, targetColumn: string): void {
|
private handleDragDropOverlaps(droppedElement: HTMLElement, targetColumn: string): void {
|
||||||
const targetColumnElement = document.querySelector(`swp-day-column[data-date="${targetColumn}"]`);
|
const targetColumnElement = document.querySelector(`swp-day-column[data-date="${targetColumn}"]`);
|
||||||
if (!targetColumnElement) return;
|
if (!targetColumnElement) return;
|
||||||
|
|
||||||
const eventsLayer = targetColumnElement.querySelector('swp-events-layer') as HTMLElement;
|
const eventsLayer = targetColumnElement.querySelector('swp-events-layer') as HTMLElement;
|
||||||
if (!eventsLayer) return;
|
if (!eventsLayer) return;
|
||||||
|
|
||||||
// Convert dropped element to CalendarEvent with new position
|
// Convert dropped element to CalendarEvent with new position
|
||||||
const droppedEvent = this.elementToCalendarEvent(droppedElement);
|
const droppedEvent = this.elementToCalendarEvent(droppedElement);
|
||||||
if (!droppedEvent) return;
|
if (!droppedEvent) return;
|
||||||
|
|
||||||
// Get existing events in the column (excluding the dropped element)
|
// Get existing events in the column (excluding the dropped element)
|
||||||
const existingEvents = this.getEventsInColumn(eventsLayer, droppedElement.dataset.eventId);
|
const existingEvents = this.getEventsInColumn(eventsLayer, droppedElement.dataset.eventId);
|
||||||
|
|
||||||
// Find overlaps with the dropped event
|
// Find overlaps with the dropped event
|
||||||
const overlappingEvents = this.overlapDetector.resolveOverlap(droppedEvent, existingEvents);
|
const overlappingEvents = this.overlapDetector.resolveOverlap(droppedEvent, existingEvents);
|
||||||
|
|
||||||
if (overlappingEvents.length > 0) {
|
if (overlappingEvents.length > 0) {
|
||||||
// Remove only affected events from DOM
|
// Remove only affected events from DOM
|
||||||
const affectedEventIds = [droppedEvent.id, ...overlappingEvents.map(e => e.id)];
|
const affectedEventIds = [droppedEvent.id, ...overlappingEvents.map(e => e.id)];
|
||||||
|
|
@ -540,7 +433,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
el.remove();
|
el.remove();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Re-render affected events with overlap handling
|
// Re-render affected events with overlap handling
|
||||||
const affectedEvents = [droppedEvent, ...overlappingEvents];
|
const affectedEvents = [droppedEvent, ...overlappingEvents];
|
||||||
this.handleEventOverlaps(affectedEvents, eventsLayer);
|
this.handleEventOverlaps(affectedEvents, eventsLayer);
|
||||||
|
|
@ -556,22 +449,22 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
private getEventsInColumn(eventsLayer: HTMLElement, excludeEventId?: string): CalendarEvent[] {
|
private getEventsInColumn(eventsLayer: HTMLElement, excludeEventId?: string): CalendarEvent[] {
|
||||||
const eventElements = eventsLayer.querySelectorAll('swp-event');
|
const eventElements = eventsLayer.querySelectorAll('swp-event');
|
||||||
const events: CalendarEvent[] = [];
|
const events: CalendarEvent[] = [];
|
||||||
|
|
||||||
eventElements.forEach(el => {
|
eventElements.forEach(el => {
|
||||||
const element = el as HTMLElement;
|
const element = el as HTMLElement;
|
||||||
const eventId = element.dataset.eventId;
|
const eventId = element.dataset.eventId;
|
||||||
|
|
||||||
// Skip the excluded event (e.g., the dropped event)
|
// Skip the excluded event (e.g., the dropped event)
|
||||||
if (excludeEventId && eventId === excludeEventId) {
|
if (excludeEventId && eventId === excludeEventId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const event = this.elementToCalendarEvent(element);
|
const event = this.elementToCalendarEvent(element);
|
||||||
if (event) {
|
if (event) {
|
||||||
events.push(event);
|
events.push(event);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -584,23 +477,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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update element's dataset with new times after successful drop
|
|
||||||
*/
|
|
||||||
private updateElementDataset(element: HTMLElement, event: CalendarEvent): void {
|
|
||||||
element.dataset.start = event.start.toISOString();
|
|
||||||
element.dataset.end = event.end.toISOString();
|
|
||||||
|
|
||||||
// Update the time display
|
|
||||||
const timeElement = element.querySelector('swp-event-time');
|
|
||||||
if (timeElement) {
|
|
||||||
const timeRange = TimeFormatter.formatTimeRange(event.start, event.end);
|
|
||||||
timeElement.textContent = timeRange;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert DOM element to CalendarEvent - handles both normal and 1970 reference dates
|
* Convert DOM element to CalendarEvent - handles both normal and 1970 reference dates
|
||||||
*/
|
*/
|
||||||
|
|
@ -610,21 +486,21 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
const type = element.dataset.type;
|
const type = element.dataset.type;
|
||||||
const start = element.dataset.start;
|
const start = element.dataset.start;
|
||||||
const end = element.dataset.end;
|
const end = element.dataset.end;
|
||||||
|
|
||||||
if (!eventId || !title || !type || !start || !end) {
|
if (!eventId || !title || !type || !start || !end) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let startDate = new Date(start);
|
let startDate = new Date(start);
|
||||||
let endDate = new Date(end);
|
let endDate = new Date(end);
|
||||||
|
|
||||||
// Check if we have 1970 reference date (from drag operations)
|
// Check if we have 1970 reference date (from drag operations)
|
||||||
if (startDate.getFullYear() === 1970) {
|
if (startDate.getFullYear() === 1970) {
|
||||||
// Find the parent column to get the actual date
|
// Find the parent column to get the actual date
|
||||||
const columnElement = element.closest('swp-day-column') as HTMLElement;
|
const columnElement = element.closest('swp-day-column') as HTMLElement;
|
||||||
if (columnElement && columnElement.dataset.date) {
|
if (columnElement && columnElement.dataset.date) {
|
||||||
const columnDate = new Date(columnElement.dataset.date);
|
const columnDate = new Date(columnElement.dataset.date);
|
||||||
|
|
||||||
// Keep the time portion from the 1970 dates, but use the column's date
|
// Keep the time portion from the 1970 dates, but use the column's date
|
||||||
startDate = new Date(
|
startDate = new Date(
|
||||||
columnDate.getFullYear(),
|
columnDate.getFullYear(),
|
||||||
|
|
@ -633,7 +509,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
startDate.getHours(),
|
startDate.getHours(),
|
||||||
startDate.getMinutes()
|
startDate.getMinutes()
|
||||||
);
|
);
|
||||||
|
|
||||||
endDate = new Date(
|
endDate = new Date(
|
||||||
columnDate.getFullYear(),
|
columnDate.getFullYear(),
|
||||||
columnDate.getMonth(),
|
columnDate.getMonth(),
|
||||||
|
|
@ -643,7 +519,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: eventId,
|
id: eventId,
|
||||||
title: title,
|
title: title,
|
||||||
|
|
@ -657,33 +533,33 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle conversion to all-day event
|
* Handle conversion to all-day event
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fade out and remove element
|
* Fade out and remove element
|
||||||
*/
|
*/
|
||||||
private fadeOutAndRemove(element: HTMLElement): void {
|
private fadeOutAndRemove(element: HTMLElement): void {
|
||||||
element.style.transition = 'opacity 0.3s ease-out';
|
element.style.transition = 'opacity 0.3s ease-out';
|
||||||
element.style.opacity = '0';
|
element.style.opacity = '0';
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
element.remove();
|
element.remove();
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
renderEvents(events: CalendarEvent[], container: HTMLElement): void {
|
renderEvents(events: CalendarEvent[], container: HTMLElement): void {
|
||||||
|
|
||||||
// NOTE: Removed clearEvents() to support sliding animation
|
// NOTE: Removed clearEvents() to support sliding animation
|
||||||
// With sliding animation, multiple grid containers exist simultaneously
|
// With sliding animation, multiple grid containers exist simultaneously
|
||||||
// clearEvents() would remove events from all containers, breaking the animation
|
// clearEvents() would remove events from all containers, breaking the animation
|
||||||
// Events are now rendered directly into the new container without clearing
|
// Events are now rendered directly into the new container without clearing
|
||||||
|
|
||||||
// Only handle regular (non-all-day) events
|
// Only handle regular (non-all-day) events
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Find columns in the specific container for regular events
|
// Find columns in the specific container for regular events
|
||||||
|
|
@ -691,7 +567,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
|
|
||||||
columns.forEach(column => {
|
columns.forEach(column => {
|
||||||
const columnEvents = this.getEventsForColumn(column, events);
|
const columnEvents = this.getEventsForColumn(column, events);
|
||||||
|
|
||||||
const eventsLayer = column.querySelector('swp-events-layer');
|
const eventsLayer = column.querySelector('swp-events-layer');
|
||||||
if (eventsLayer) {
|
if (eventsLayer) {
|
||||||
// NY TILGANG: Kald vores nye overlap handling
|
// NY TILGANG: Kald vores nye overlap handling
|
||||||
|
|
@ -708,14 +584,14 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
protected renderEvent(event: CalendarEvent): HTMLElement {
|
protected renderEvent(event: CalendarEvent): HTMLElement {
|
||||||
const swpEvent = SwpEventElement.fromCalendarEvent(event);
|
const swpEvent = SwpEventElement.fromCalendarEvent(event);
|
||||||
const eventElement = swpEvent.getElement();
|
const eventElement = swpEvent.getElement();
|
||||||
|
|
||||||
// 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') {
|
||||||
eventElement.dataset.hasResizeHandlers = 'true';
|
eventElement.dataset.hasResizeHandlers = 'true';
|
||||||
}
|
}
|
||||||
}, { once: true });
|
}, { once: true });
|
||||||
|
|
||||||
return eventElement;
|
return eventElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -729,7 +605,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
const existingEvents = container
|
const existingEvents = container
|
||||||
? container.querySelectorAll(selector)
|
? container.querySelectorAll(selector)
|
||||||
: document.querySelectorAll(selector);
|
: document.querySelectorAll(selector);
|
||||||
|
|
||||||
existingEvents.forEach(event => event.remove());
|
existingEvents.forEach(event => event.remove());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -743,16 +619,16 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
for (const [eventId, stackLink] of result.stackLinks.entries()) {
|
for (const [eventId, stackLink] of result.stackLinks.entries()) {
|
||||||
const event = result.overlappingEvents.find(e => e.id === eventId);
|
const event = result.overlappingEvents.find(e => e.id === eventId);
|
||||||
if (!event) continue;
|
if (!event) continue;
|
||||||
|
|
||||||
const element = this.renderEvent(event);
|
const element = this.renderEvent(event);
|
||||||
|
|
||||||
// Gem stack link information på DOM elementet
|
// Gem stack link information på DOM elementet
|
||||||
element.dataset.stackLink = JSON.stringify({
|
element.dataset.stackLink = JSON.stringify({
|
||||||
prev: stackLink.prev,
|
prev: stackLink.prev,
|
||||||
next: stackLink.next,
|
next: stackLink.next,
|
||||||
stackLevel: stackLink.stackLevel
|
stackLevel: stackLink.stackLevel
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check om dette event deler kolonne med foregående (samme start tid)
|
// Check om dette event deler kolonne med foregående (samme start tid)
|
||||||
if (stackLink.prev) {
|
if (stackLink.prev) {
|
||||||
const prevEvent = result.overlappingEvents.find(e => e.id === stackLink.prev);
|
const prevEvent = result.overlappingEvents.find(e => e.id === stackLink.prev);
|
||||||
|
|
@ -767,7 +643,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||||
// Første event i stack
|
// Første event i stack
|
||||||
this.new_applyStackStyling(element, stackLink.stackLevel);
|
this.new_applyStackStyling(element, stackLink.stackLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
container.appendChild(element);
|
container.appendChild(element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -817,8 +693,8 @@ export class DateEventRenderer extends BaseEventRenderer {
|
||||||
const columnEvents = events.filter(event => {
|
const columnEvents = events.filter(event => {
|
||||||
const eventDateStr = DateCalculator.formatISODate(event.start);
|
const eventDateStr = DateCalculator.formatISODate(event.start);
|
||||||
const matches = eventDateStr === columnDate;
|
const matches = eventDateStr === columnDate;
|
||||||
|
|
||||||
|
|
||||||
return matches;
|
return matches;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,25 +71,18 @@ export class EventRenderingService {
|
||||||
this.handleViewChanged(event as CustomEvent);
|
this.handleViewChanged(event as CustomEvent);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simple drag:end listener to clean up day event clones
|
// Handle all drag events and delegate to appropriate renderer
|
||||||
this.eventBus.on('drag:end', (event: Event) => {
|
this.setupDragEventListeners();
|
||||||
const { eventId } = (event as CustomEvent).detail;
|
|
||||||
const dayEventClone = document.querySelector(`swp-event[data-event-id="clone-${eventId}"]`);
|
|
||||||
|
|
||||||
if (dayEventClone) {
|
|
||||||
dayEventClone.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for conversion from all-day event to time event
|
// Listen for conversion from all-day event to time event
|
||||||
this.eventBus.on('drag:convert-to-time_event', (event: Event) => {
|
this.eventBus.on('drag:convert-to-time_event', (event: Event) => {
|
||||||
const { draggedEventId, mousePosition, column } = (event as CustomEvent).detail;
|
const { draggedElement, mousePosition, column } = (event as CustomEvent).detail;
|
||||||
console.log('🔄 EventRendererManager: Received drag:convert-to-time_event', {
|
console.log('🔄 EventRendererManager: Received drag:convert-to-time_event', {
|
||||||
draggedEventId,
|
draggedElement: draggedElement?.dataset.eventId,
|
||||||
mousePosition,
|
mousePosition,
|
||||||
column
|
column
|
||||||
});
|
});
|
||||||
this.handleConvertToTimeEvent(draggedEventId, mousePosition, column);
|
this.handleConvertToTimeEvent(draggedElement, mousePosition, column);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -153,18 +146,94 @@ export class EventRenderingService {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup all drag event listeners - moved from EventRenderer for better separation of concerns
|
||||||
|
*/
|
||||||
|
private setupDragEventListeners(): void {
|
||||||
|
// Handle drag start
|
||||||
|
this.eventBus.on('drag:start', (event: Event) => {
|
||||||
|
const { eventId, mouseOffset, column } = (event as CustomEvent).detail;
|
||||||
|
// Find element dynamically
|
||||||
|
const originalElement = document.querySelector(`swp-event[data-event-id="${eventId}"]`) as HTMLElement;
|
||||||
|
if (originalElement && this.strategy.handleDragStart) {
|
||||||
|
this.strategy.handleDragStart(originalElement, eventId, mouseOffset, column);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle drag move
|
||||||
|
this.eventBus.on('drag:move', (event: Event) => {
|
||||||
|
const { eventId, snappedY, column, mouseOffset } = (event as CustomEvent).detail;
|
||||||
|
if (this.strategy.handleDragMove) {
|
||||||
|
this.strategy.handleDragMove(eventId, snappedY, column, mouseOffset);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle drag auto-scroll
|
||||||
|
this.eventBus.on('drag:auto-scroll', (event: Event) => {
|
||||||
|
const { eventId, snappedY } = (event as CustomEvent).detail;
|
||||||
|
if (this.strategy.handleDragAutoScroll) {
|
||||||
|
this.strategy.handleDragAutoScroll(eventId, snappedY);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle drag end events and delegate to appropriate renderer
|
||||||
|
this.eventBus.on('drag:end', (event: Event) => {
|
||||||
|
const { eventId, finalColumn, finalY, target } = (event as CustomEvent).detail;
|
||||||
|
|
||||||
|
// Only handle day column drops for EventRenderer
|
||||||
|
if (target === 'swp-day-column') {
|
||||||
|
// Find both original element and dragged clone
|
||||||
|
const originalElement = document.querySelector(`swp-day-column swp-event[data-event-id="${eventId}"]`) as HTMLElement;
|
||||||
|
const draggedClone = document.querySelector(`swp-day-column swp-event[data-event-id="clone-${eventId}"]`) as HTMLElement;
|
||||||
|
|
||||||
|
if (originalElement && draggedClone && this.strategy.handleDragEnd) {
|
||||||
|
this.strategy.handleDragEnd(eventId, originalElement, draggedClone, finalColumn, finalY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up any remaining day event clones
|
||||||
|
const dayEventClone = document.querySelector(`swp-event[data-event-id="clone-${eventId}"]`);
|
||||||
|
if (dayEventClone) {
|
||||||
|
dayEventClone.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle click (when drag threshold not reached)
|
||||||
|
this.eventBus.on('event:click', (event: Event) => {
|
||||||
|
const { eventId } = (event as CustomEvent).detail;
|
||||||
|
// Find element dynamically
|
||||||
|
const originalElement = document.querySelector(`swp-event[data-event-id="${eventId}"]`) as HTMLElement;
|
||||||
|
if (originalElement && this.strategy.handleEventClick) {
|
||||||
|
this.strategy.handleEventClick(eventId, originalElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle column change
|
||||||
|
this.eventBus.on('drag:column-change', (event: Event) => {
|
||||||
|
const { eventId, newColumn } = (event as CustomEvent).detail;
|
||||||
|
if (this.strategy.handleColumnChange) {
|
||||||
|
this.strategy.handleColumnChange(eventId, newColumn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle navigation period change
|
||||||
|
this.eventBus.on(CoreEvents.NAVIGATION_COMPLETED, () => {
|
||||||
|
// Delegate to strategy if it handles navigation
|
||||||
|
if (this.strategy.handleNavigationCompleted) {
|
||||||
|
this.strategy.handleNavigationCompleted();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle conversion from all-day event to time event
|
* Handle conversion from all-day event to time event
|
||||||
*/
|
*/
|
||||||
private handleConvertToTimeEvent(draggedEventId: string, mousePosition: any, column: string): void {
|
private handleConvertToTimeEvent(draggedElement: HTMLElement, mousePosition: any, column: string): void {
|
||||||
// Find all-day event clone
|
// Use the provided draggedElement directly
|
||||||
const allDayClone = document.querySelector(`swp-allday-container swp-allday-event[data-event-id="clone-${draggedEventId}"]`);
|
const allDayClone = draggedElement;
|
||||||
|
const draggedEventId = draggedElement?.dataset.eventId?.replace('clone-', '') || '';
|
||||||
if (!allDayClone) {
|
|
||||||
console.warn('EventRendererManager: All-day clone not found - drag may not have started properly', { draggedEventId });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Use SwpEventElement factory to create day event from all-day event
|
// Use SwpEventElement factory to create day event from all-day event
|
||||||
const dayEventElement = SwpEventElement.fromAllDayElement(allDayClone as HTMLElement);
|
const dayEventElement = SwpEventElement.fromAllDayElement(allDayClone as HTMLElement);
|
||||||
const dayElement = dayEventElement.getElement();
|
const dayElement = dayEventElement.getElement();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue