Improves drag and drop for timed and all-day events

Refactors drag and drop handling to use a cloned event element,
ensuring correct positioning and styling during drag operations
for both regular timed events and all-day events.

This change streamlines the drag and drop process by:
- Creating a clone of the dragged event at the start of the drag.
- Passing the clone through the drag events.
- Handling all-day events with the AllDayManager
- Handling regular timed events with the EventRendererManager

This resolves issues with event positioning and styling during
drag, especially when moving events across columns or between
all-day and timed sections.
This commit is contained in:
Janus C. H. Knudsen 2025-09-26 22:53:49 +02:00
parent 0553089085
commit 9dfd4574d8
5 changed files with 62 additions and 45 deletions

View file

@ -65,7 +65,7 @@ export class AllDayManager {
// Listen for drag operations on all-day events // Listen for drag operations on all-day events
eventBus.on('drag:start', (event) => { eventBus.on('drag:start', (event) => {
const { draggedElement, mouseOffset } = (event as CustomEvent<DragStartEventPayload>).detail; const { draggedElement, draggedClone, mouseOffset } = (event as CustomEvent<DragStartEventPayload>).detail;
// Check if this is an all-day event by checking if it's in all-day container // Check if this is an all-day event by checking if it's in all-day container
const isAllDayEvent = draggedElement.closest('swp-allday-container'); const isAllDayEvent = draggedElement.closest('swp-allday-container');
@ -77,26 +77,22 @@ export class AllDayManager {
}); });
eventBus.on('drag:column-change', (event) => { eventBus.on('drag:column-change', (event) => {
const { draggedElement, mousePosition } = (event as CustomEvent<DragColumnChangeEventPayload>).detail; const { draggedElement, draggedClone, mousePosition } = (event as CustomEvent<DragColumnChangeEventPayload>).detail;
if(draggedClone == null)
return;
// Check if there's an all-day clone for this event // Filter: Only handle events where clone IS an all-day event
const eventId = draggedElement.dataset.eventId; if (!draggedClone.hasAttribute('data-allday')) {
const dragClone = document.querySelector(`swp-allday-container swp-event[data-event-id="clone-${eventId}"]`) as HTMLElement; return; // This is not an all-day event, let EventRendererManager handle it
}
console.log('🔄 AllDayManager: Handling drag:column-change for all-day event', {
eventId : draggedElement.dataset.eventId,
cloneId: draggedClone.dataset.eventId
});
if (!dragClone.hasAttribute('data-allday')) { this.handleColumnChange(draggedClone, mousePosition);
return;
}
// If we find an all-day clone, handle the drag move
if (dragClone) {
console.log('🔄 AllDayManager: Found all-day clone, handling drag:column-change', {
eventId,
cloneId: dragClone.dataset.eventId
});
this.handleColumnChange(dragClone, mousePosition);
}
}); });
eventBus.on('drag:end', (event) => { eventBus.on('drag:end', (event) => {

View file

@ -7,6 +7,7 @@ import { IEventBus } from '../types/CalendarTypes';
import { calendarConfig } from '../core/CalendarConfig'; import { calendarConfig } from '../core/CalendarConfig';
import { PositionUtils } from '../utils/PositionUtils'; import { PositionUtils } from '../utils/PositionUtils';
import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils'; import { ColumnDetectionUtils } from '../utils/ColumnDetectionUtils';
import { SwpEventElement } from '../elements/SwpEventElement';
import { import {
DragStartEventPayload, DragStartEventPayload,
DragMoveEventPayload, DragMoveEventPayload,
@ -40,6 +41,7 @@ export class DragDropManager {
// Drag state // Drag state
private draggedElement!: HTMLElement | null; private draggedElement!: HTMLElement | null;
private draggedClone!: HTMLElement | null;
private currentColumn: string | null = null; private currentColumn: string | null = null;
private isDragStarted = false; private isDragStarted = false;
@ -198,8 +200,17 @@ export class DragDropManager {
// Start drag - emit drag:start event // Start drag - emit drag:start event
this.isDragStarted = true; this.isDragStarted = true;
// Create SwpEventElement from existing DOM element and clone it
const originalSwpEvent = SwpEventElement.fromExistingElement(this.draggedElement);
const clonedSwpEvent = originalSwpEvent.createClone();
// Get the cloned DOM element
this.draggedClone = clonedSwpEvent.getElement();
const dragStartPayload: DragStartEventPayload = { const dragStartPayload: DragStartEventPayload = {
draggedElement: this.draggedElement, draggedElement: this.draggedElement,
draggedClone: this.draggedClone,
mousePosition: this.initialMousePosition, mousePosition: this.initialMousePosition,
mouseOffset: this.mouseOffset, mouseOffset: this.mouseOffset,
column: this.currentColumn column: this.currentColumn
@ -244,6 +255,7 @@ export class DragDropManager {
const dragColumnChangePayload: DragColumnChangeEventPayload = { const dragColumnChangePayload: DragColumnChangeEventPayload = {
draggedElement: this.draggedElement, draggedElement: this.draggedElement,
draggedClone: this.draggedClone,
previousColumn, previousColumn,
newColumn, newColumn,
mousePosition: currentPosition mousePosition: currentPosition
@ -514,6 +526,7 @@ export class DragDropManager {
*/ */
private cleanupDragState(): void { private cleanupDragState(): void {
this.draggedElement = null; this.draggedElement = null;
this.draggedClone = null;
this.currentColumn = null; this.currentColumn = null;
this.isDragStarted = false; this.isDragStarted = false;
this.isInHeader = false; this.isInHeader = false;

View file

@ -16,7 +16,7 @@ import { DragOffset, StackLinkData } from '../types/DragDropTypes';
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: DragOffset, column: string): void; handleDragStart?(payload: import('../types/EventTypes').DragStartEventPayload): void;
handleDragMove?(eventId: string, snappedY: number, column: string, mouseOffset: DragOffset): void; handleDragMove?(eventId: string, snappedY: number, column: string, mouseOffset: DragOffset): void;
handleDragAutoScroll?(eventId: string, snappedY: number): void; handleDragAutoScroll?(eventId: string, snappedY: number): void;
handleDragEnd?(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: string, finalY: number): void; handleDragEnd?(eventId: string, originalElement: HTMLElement, draggedClone: HTMLElement, finalColumn: string, finalY: number): void;
@ -160,30 +160,31 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
/** /**
* Handle drag start event * Handle drag start event
*/ */
public handleDragStart(originalElement: HTMLElement, eventId: string, mouseOffset: DragOffset, column: string): void { public handleDragStart(payload: import('../types/EventTypes').DragStartEventPayload): void {
const originalElement = payload.draggedElement;
const eventId = originalElement.dataset.eventId || '';
const mouseOffset = payload.mouseOffset;
const column = payload.column || '';
this.originalEvent = originalElement; this.originalEvent = originalElement;
// Remove stacking styling during drag will be handled by new system // Use the clone from the payload instead of creating a new one
this.draggedClone = payload.draggedClone;
// Create SwpEventElement from existing DOM element and clone it if (this.draggedClone) {
const originalSwpEvent = SwpEventElement.fromExistingElement(originalElement); // Apply drag styling
const clonedSwpEvent = originalSwpEvent.createClone(); this.applyDragStyling(this.draggedClone);
// Get the cloned DOM element // Add to current column's events layer (not directly to column)
this.draggedClone = clonedSwpEvent.getElement(); const columnElement = document.querySelector(`swp-day-column[data-date="${column}"]`);
if (columnElement) {
// Apply drag styling const eventsLayer = columnElement.querySelector('swp-events-layer');
this.applyDragStyling(this.draggedClone); if (eventsLayer) {
eventsLayer.appendChild(this.draggedClone);
// Add to current column's events layer (not directly to column) } else {
const columnElement = document.querySelector(`swp-day-column[data-date="${column}"]`); // Fallback to column if events layer not found
if (columnElement) { columnElement.appendChild(this.draggedClone);
const eventsLayer = columnElement.querySelector('swp-events-layer'); }
if (eventsLayer) {
eventsLayer.appendChild(this.draggedClone);
} else {
// Fallback to column if events layer not found
columnElement.appendChild(this.draggedClone);
} }
} }

View file

@ -179,11 +179,10 @@ export class EventRenderingService {
private setupDragEventListeners(): void { private setupDragEventListeners(): void {
// Handle drag start // Handle drag start
this.eventBus.on('drag:start', (event: Event) => { this.eventBus.on('drag:start', (event: Event) => {
const { draggedElement, mouseOffset, column } = (event as CustomEvent<DragStartEventPayload>).detail; const dragStartPayload = (event as CustomEvent<DragStartEventPayload>).detail;
// Use the draggedElement directly - no need for DOM query // Use the draggedElement directly - no need for DOM query
if (draggedElement && this.strategy.handleDragStart && column) { if (dragStartPayload.draggedElement && this.strategy.handleDragStart && dragStartPayload.column) {
const eventId = draggedElement.dataset.eventId || ''; this.strategy.handleDragStart(dragStartPayload);
this.strategy.handleDragStart(draggedElement, eventId, mouseOffset, column);
} }
}); });
@ -246,10 +245,16 @@ export class EventRenderingService {
// Handle column change // Handle column change
this.eventBus.on('drag:column-change', (event: Event) => { this.eventBus.on('drag:column-change', (event: Event) => {
const { draggedElement, newColumn } = (event as CustomEvent<DragColumnChangeEventPayload>).detail; const { draggedElement, draggedClone, newColumn } = (event as CustomEvent<DragColumnChangeEventPayload>).detail;
// Filter: Only handle events where clone is NOT an all-day event (normal timed events)
if (draggedClone && draggedClone.hasAttribute('data-allday')) {
return; // This is an all-day event, let AllDayManager handle it
}
if (this.strategy.handleColumnChange) { if (this.strategy.handleColumnChange) {
const eventId = draggedElement.dataset.eventId || ''; const eventId = draggedElement.dataset.eventId || '';
this.strategy.handleColumnChange(eventId, newColumn); this.strategy.handleColumnChange(eventId, newColumn); //TODO: Should be refactored to use payload, no need to lookup clone again inside
} }
}); });

View file

@ -46,6 +46,7 @@ export interface MousePosition {
// Drag start event payload // Drag start event payload
export interface DragStartEventPayload { export interface DragStartEventPayload {
draggedElement: HTMLElement; draggedElement: HTMLElement;
draggedClone: HTMLElement | null;
mousePosition: MousePosition; mousePosition: MousePosition;
mouseOffset: MousePosition; mouseOffset: MousePosition;
column: string | null; column: string | null;
@ -90,6 +91,7 @@ export interface DragMouseLeaveHeaderEventPayload {
// Drag column change event payload // Drag column change event payload
export interface DragColumnChangeEventPayload { export interface DragColumnChangeEventPayload {
draggedElement: HTMLElement; draggedElement: HTMLElement;
draggedClone: HTMLElement | null;
previousColumn: string | null; previousColumn: string | null;
newColumn: string; newColumn: string;
mousePosition: MousePosition; mousePosition: MousePosition;