Enhance drag and drop interactions across calendar views
Adds support for dragging events between header and grid views Improves drag-and-drop state management and event persistence Enables converting all-day events to timed events when dropped in grid Refactors drag handling to support more flexible event interactions
This commit is contained in:
parent
f7f1f8afe0
commit
bc5854e09a
5 changed files with 259 additions and 67 deletions
|
|
@ -4,7 +4,7 @@ import { DateService } from '../../core/DateService';
|
||||||
import { IGridConfig } from '../../core/IGridConfig';
|
import { IGridConfig } from '../../core/IGridConfig';
|
||||||
import { calculateEventPosition, snapToGrid, pixelsToMinutes } from '../../utils/PositionUtils';
|
import { calculateEventPosition, snapToGrid, pixelsToMinutes } from '../../utils/PositionUtils';
|
||||||
import { CoreEvents } from '../../constants/CoreEvents';
|
import { CoreEvents } from '../../constants/CoreEvents';
|
||||||
import { IDragColumnChangePayload, IDragMovePayload, IDragEndPayload } from '../../types/DragTypes';
|
import { IDragColumnChangePayload, IDragMovePayload, IDragEndPayload, IDragLeaveHeaderPayload } from '../../types/DragTypes';
|
||||||
import { calculateColumnLayout } from './EventLayoutEngine';
|
import { calculateColumnLayout } from './EventLayoutEngine';
|
||||||
import { IGridGroupLayout } from './EventLayoutTypes';
|
import { IGridGroupLayout } from './EventLayoutTypes';
|
||||||
|
|
||||||
|
|
@ -51,6 +51,11 @@ export class EventRenderer {
|
||||||
const payload = (e as CustomEvent<IDragEndPayload>).detail;
|
const payload = (e as CustomEvent<IDragEndPayload>).detail;
|
||||||
this.handleDragEnd(payload);
|
this.handleDragEnd(payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.eventBus.on(CoreEvents.EVENT_DRAG_LEAVE_HEADER, (e) => {
|
||||||
|
const payload = (e as CustomEvent<IDragLeaveHeaderPayload>).detail;
|
||||||
|
this.handleDragLeaveHeader(payload);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -64,6 +69,48 @@ export class EventRenderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle header item leaving header - create swp-event in grid
|
||||||
|
*/
|
||||||
|
private handleDragLeaveHeader(payload: IDragLeaveHeaderPayload): void {
|
||||||
|
// Only handle when source is header (header item dragged to grid)
|
||||||
|
if (payload.source !== 'header') return;
|
||||||
|
if (!payload.targetColumn || !payload.start || !payload.end) return;
|
||||||
|
|
||||||
|
// Turn header item into ghost (stays visible but faded)
|
||||||
|
if (payload.element) {
|
||||||
|
payload.element.classList.add('drag-ghost');
|
||||||
|
payload.element.style.opacity = '0.3';
|
||||||
|
payload.element.style.pointerEvents = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create event object from header item data
|
||||||
|
const event: ICalendarEvent = {
|
||||||
|
id: payload.eventId,
|
||||||
|
title: payload.title || '',
|
||||||
|
description: '',
|
||||||
|
start: payload.start,
|
||||||
|
end: payload.end,
|
||||||
|
type: 'customer',
|
||||||
|
allDay: false,
|
||||||
|
syncStatus: 'pending'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create swp-event element using existing method
|
||||||
|
const element = this.createEventElement(event);
|
||||||
|
|
||||||
|
// Add to target column
|
||||||
|
let eventsLayer = payload.targetColumn.querySelector('swp-events-layer');
|
||||||
|
if (!eventsLayer) {
|
||||||
|
eventsLayer = document.createElement('swp-events-layer');
|
||||||
|
payload.targetColumn.appendChild(eventsLayer);
|
||||||
|
}
|
||||||
|
eventsLayer.appendChild(element);
|
||||||
|
|
||||||
|
// Mark as dragging so DragDropManager can continue with it
|
||||||
|
element.classList.add('dragging');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle EVENT_UPDATED - re-render affected columns
|
* Handle EVENT_UPDATED - re-render affected columns
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ import { DateService } from '../../core/DateService';
|
||||||
import {
|
import {
|
||||||
IDragEnterHeaderPayload,
|
IDragEnterHeaderPayload,
|
||||||
IDragMoveHeaderPayload,
|
IDragMoveHeaderPayload,
|
||||||
IDragLeaveHeaderPayload
|
IDragLeaveHeaderPayload,
|
||||||
|
IDragEndPayload
|
||||||
} from '../../types/DragTypes';
|
} from '../../types/DragTypes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -199,8 +200,9 @@ export class HeaderDrawerRenderer {
|
||||||
this.handleDragLeave(payload);
|
this.handleDragLeave(payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.eventBus.on(CoreEvents.EVENT_DRAG_END, () => {
|
this.eventBus.on(CoreEvents.EVENT_DRAG_END, (e) => {
|
||||||
this.handleDragEnd();
|
const payload = (e as CustomEvent<IDragEndPayload>).detail;
|
||||||
|
this.handleDragEnd(payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.eventBus.on(CoreEvents.EVENT_DRAG_CANCEL, () => {
|
this.eventBus.on(CoreEvents.EVENT_DRAG_CANCEL, () => {
|
||||||
|
|
@ -290,22 +292,23 @@ export class HeaderDrawerRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle drag end - finalize the item (it stays in header)
|
* Handle drag end - finalize based on drop target
|
||||||
* Note: EventRenderer handles removing the original element from the grid
|
|
||||||
* via EVENT_DRAG_END with target === 'header'
|
|
||||||
*/
|
*/
|
||||||
private handleDragEnd(): void {
|
private handleDragEnd(payload: IDragEndPayload): void {
|
||||||
if (!this.currentItem) return;
|
if (payload.target === 'header') {
|
||||||
|
// Grid→Header: Finalize the header item (it stays in header)
|
||||||
// Remove dragging state
|
if (this.currentItem) {
|
||||||
this.currentItem.classList.remove('dragging');
|
this.currentItem.classList.remove('dragging');
|
||||||
|
this.recalculateDrawerLayout();
|
||||||
// Recalculate layout for all items in drawer
|
this.currentItem = null;
|
||||||
this.recalculateDrawerLayout();
|
this.sourceElement = null;
|
||||||
|
}
|
||||||
// Clear references
|
} else {
|
||||||
this.currentItem = null;
|
// Header→Grid: Remove ghost header item and recalculate
|
||||||
this.sourceElement = null;
|
const ghost = document.querySelector(`swp-header-item.drag-ghost[data-event-id="${payload.swpEvent.eventId}"]`);
|
||||||
|
ghost?.remove();
|
||||||
|
this.recalculateDrawerLayout();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -18,16 +18,17 @@ import { SwpEvent } from '../types/SwpEvent';
|
||||||
interface DragState {
|
interface DragState {
|
||||||
eventId: string;
|
eventId: string;
|
||||||
element: HTMLElement;
|
element: HTMLElement;
|
||||||
ghostElement: HTMLElement;
|
ghostElement: HTMLElement | null; // Null for header items
|
||||||
startY: number;
|
startY: number;
|
||||||
mouseOffset: IMousePosition;
|
mouseOffset: IMousePosition;
|
||||||
columnElement: HTMLElement;
|
columnElement: HTMLElement | null; // Null when starting from header
|
||||||
currentColumn: HTMLElement;
|
currentColumn: HTMLElement | null; // Null when in header
|
||||||
targetY: number;
|
targetY: number;
|
||||||
currentY: number;
|
currentY: number;
|
||||||
animationId: number;
|
animationId: number;
|
||||||
sourceDateKey: string; // Source column date (where drag started)
|
sourceDateKey: string; // Source column date (where drag started)
|
||||||
sourceResourceId?: string; // Source column resource (where drag started)
|
sourceResourceId?: string; // Source column resource (where drag started)
|
||||||
|
dragSource: 'grid' | 'header'; // Where drag originated
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -86,23 +87,26 @@ export class DragDropManager {
|
||||||
// Ignore if clicking on resize handle
|
// Ignore if clicking on resize handle
|
||||||
if (target.closest('swp-resize-handle')) return;
|
if (target.closest('swp-resize-handle')) return;
|
||||||
|
|
||||||
|
// Match both swp-event and swp-header-item
|
||||||
const eventElement = target.closest('swp-event') as HTMLElement;
|
const eventElement = target.closest('swp-event') as HTMLElement;
|
||||||
|
const headerItem = target.closest('swp-header-item') as HTMLElement;
|
||||||
|
const draggable = eventElement || headerItem;
|
||||||
|
|
||||||
if (!eventElement) return;
|
if (!draggable) return;
|
||||||
|
|
||||||
// Store for potential drag
|
// Store for potential drag
|
||||||
this.mouseDownPosition = { x: e.clientX, y: e.clientY };
|
this.mouseDownPosition = { x: e.clientX, y: e.clientY };
|
||||||
this.pendingElement = eventElement;
|
this.pendingElement = draggable;
|
||||||
|
|
||||||
// Calculate mouse offset within element
|
// Calculate mouse offset within element
|
||||||
const rect = eventElement.getBoundingClientRect();
|
const rect = draggable.getBoundingClientRect();
|
||||||
this.pendingMouseOffset = {
|
this.pendingMouseOffset = {
|
||||||
x: e.clientX - rect.left,
|
x: e.clientX - rect.left,
|
||||||
y: e.clientY - rect.top
|
y: e.clientY - rect.top
|
||||||
};
|
};
|
||||||
|
|
||||||
// Capture pointer for reliable tracking
|
// Capture pointer for reliable tracking
|
||||||
eventElement.setPointerCapture(e.pointerId);
|
draggable.setPointerCapture(e.pointerId);
|
||||||
};
|
};
|
||||||
|
|
||||||
private handlePointerMove = (e: PointerEvent): void => {
|
private handlePointerMove = (e: PointerEvent): void => {
|
||||||
|
|
@ -140,12 +144,66 @@ export class DragDropManager {
|
||||||
// Stop animation
|
// Stop animation
|
||||||
cancelAnimationFrame(this.dragState.animationId);
|
cancelAnimationFrame(this.dragState.animationId);
|
||||||
|
|
||||||
|
// Handle based on drag source and target
|
||||||
|
if (this.dragState.dragSource === 'header') {
|
||||||
|
// Header item drag end
|
||||||
|
this.handleHeaderItemDragEnd();
|
||||||
|
} else {
|
||||||
|
// Grid event drag end
|
||||||
|
this.handleGridEventDragEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
this.dragState.element.classList.remove('dragging');
|
||||||
|
this.dragState = null;
|
||||||
|
this.inHeader = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle drag end for header items
|
||||||
|
*/
|
||||||
|
private handleHeaderItemDragEnd(): void {
|
||||||
|
if (!this.dragState) return;
|
||||||
|
|
||||||
|
// If dropped in grid (not in header), the swp-event was already created
|
||||||
|
// by EventRenderer listening to EVENT_DRAG_LEAVE_HEADER
|
||||||
|
// Just emit drag:end for persistence
|
||||||
|
|
||||||
|
if (!this.inHeader && this.dragState.currentColumn) {
|
||||||
|
// Dropped in grid - emit drag:end with the new swp-event element
|
||||||
|
const gridEvent = this.dragState.currentColumn.querySelector(
|
||||||
|
`swp-event[data-event-id="${this.dragState.eventId}"]`
|
||||||
|
) as HTMLElement;
|
||||||
|
|
||||||
|
if (gridEvent) {
|
||||||
|
const dateKey = this.dragState.currentColumn.dataset.date || '';
|
||||||
|
const swpEvent = SwpEvent.fromElement(gridEvent, dateKey, this.gridConfig);
|
||||||
|
|
||||||
|
const payload: IDragEndPayload = {
|
||||||
|
swpEvent,
|
||||||
|
sourceDateKey: this.dragState.sourceDateKey,
|
||||||
|
sourceResourceId: this.dragState.sourceResourceId,
|
||||||
|
target: 'grid'
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventBus.emit(CoreEvents.EVENT_DRAG_END, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If still in header, no persistence needed (stayed in header)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle drag end for grid events
|
||||||
|
*/
|
||||||
|
private handleGridEventDragEnd(): void {
|
||||||
|
if (!this.dragState || !this.dragState.columnElement) return;
|
||||||
|
|
||||||
// Snap to grid
|
// Snap to grid
|
||||||
const snappedY = snapToGrid(this.dragState.currentY, this.gridConfig);
|
const snappedY = snapToGrid(this.dragState.currentY, this.gridConfig);
|
||||||
this.dragState.element.style.top = `${snappedY}px`;
|
this.dragState.element.style.top = `${snappedY}px`;
|
||||||
|
|
||||||
// Remove ghost
|
// Remove ghost
|
||||||
this.dragState.ghostElement.remove();
|
this.dragState.ghostElement?.remove();
|
||||||
|
|
||||||
// Get dateKey from target column
|
// Get dateKey from target column
|
||||||
const dateKey = this.dragState.columnElement.dataset.date || '';
|
const dateKey = this.dragState.columnElement.dataset.date || '';
|
||||||
|
|
@ -166,19 +224,57 @@ export class DragDropManager {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.eventBus.emit(CoreEvents.EVENT_DRAG_END, payload);
|
this.eventBus.emit(CoreEvents.EVENT_DRAG_END, payload);
|
||||||
|
}
|
||||||
// Cleanup
|
|
||||||
this.dragState.element.classList.remove('dragging');
|
|
||||||
this.dragState = null;
|
|
||||||
this.inHeader = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
private initializeDrag(element: HTMLElement, mouseOffset: IMousePosition, e: PointerEvent): void {
|
private initializeDrag(element: HTMLElement, mouseOffset: IMousePosition, e: PointerEvent): void {
|
||||||
const eventId = element.dataset.eventId || '';
|
const eventId = element.dataset.eventId || '';
|
||||||
|
const isHeaderItem = element.tagName.toLowerCase() === 'swp-header-item';
|
||||||
const columnElement = element.closest('swp-day-column') as HTMLElement;
|
const columnElement = element.closest('swp-day-column') as HTMLElement;
|
||||||
|
|
||||||
if (!columnElement) return;
|
// For grid events, we need a column
|
||||||
|
if (!isHeaderItem && !columnElement) return;
|
||||||
|
|
||||||
|
if (isHeaderItem) {
|
||||||
|
// Header item drag initialization
|
||||||
|
this.initializeHeaderItemDrag(element, mouseOffset, eventId);
|
||||||
|
} else {
|
||||||
|
// Grid event drag initialization
|
||||||
|
this.initializeGridEventDrag(element, mouseOffset, e, columnElement, eventId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize drag for a header item (allDay event)
|
||||||
|
*/
|
||||||
|
private initializeHeaderItemDrag(element: HTMLElement, mouseOffset: IMousePosition, eventId: string): void {
|
||||||
|
// Mark as dragging
|
||||||
|
element.classList.add('dragging');
|
||||||
|
|
||||||
|
// Initialize drag state for header item
|
||||||
|
this.dragState = {
|
||||||
|
eventId,
|
||||||
|
element,
|
||||||
|
ghostElement: null, // No ghost for header items
|
||||||
|
startY: 0,
|
||||||
|
mouseOffset,
|
||||||
|
columnElement: null,
|
||||||
|
currentColumn: null,
|
||||||
|
targetY: 0,
|
||||||
|
currentY: 0,
|
||||||
|
animationId: 0,
|
||||||
|
sourceDateKey: '', // Will be set from header item data
|
||||||
|
sourceResourceId: undefined,
|
||||||
|
dragSource: 'header'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start in header mode
|
||||||
|
this.inHeader = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize drag for a grid event
|
||||||
|
*/
|
||||||
|
private initializeGridEventDrag(element: HTMLElement, mouseOffset: IMousePosition, e: PointerEvent, columnElement: HTMLElement, eventId: string): void {
|
||||||
// Calculate absolute Y position using getBoundingClientRect
|
// Calculate absolute Y position using getBoundingClientRect
|
||||||
const elementRect = element.getBoundingClientRect();
|
const elementRect = element.getBoundingClientRect();
|
||||||
const columnRect = columnElement.getBoundingClientRect();
|
const columnRect = columnElement.getBoundingClientRect();
|
||||||
|
|
@ -228,7 +324,8 @@ export class DragDropManager {
|
||||||
currentY: startY,
|
currentY: startY,
|
||||||
animationId: 0,
|
animationId: 0,
|
||||||
sourceDateKey: columnElement.dataset.date || '',
|
sourceDateKey: columnElement.dataset.date || '',
|
||||||
sourceResourceId: columnElement.dataset.resourceId
|
sourceResourceId: columnElement.dataset.resourceId,
|
||||||
|
dragSource: 'grid'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Emit drag:start
|
// Emit drag:start
|
||||||
|
|
@ -258,7 +355,14 @@ export class DragDropManager {
|
||||||
|
|
||||||
// Check for column change
|
// Check for column change
|
||||||
const columnAtPoint = this.getColumnAtPoint(e.clientX);
|
const columnAtPoint = this.getColumnAtPoint(e.clientX);
|
||||||
if (columnAtPoint && columnAtPoint !== this.dragState.currentColumn) {
|
|
||||||
|
// For header items entering grid, set initial column
|
||||||
|
if (this.dragState.dragSource === 'header' && columnAtPoint && !this.dragState.currentColumn) {
|
||||||
|
this.dragState.currentColumn = columnAtPoint;
|
||||||
|
this.dragState.columnElement = columnAtPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (columnAtPoint && columnAtPoint !== this.dragState.currentColumn && this.dragState.currentColumn) {
|
||||||
const payload: IDragColumnChangePayload = {
|
const payload: IDragColumnChangePayload = {
|
||||||
eventId: this.dragState.eventId,
|
eventId: this.dragState.eventId,
|
||||||
element: this.dragState.element,
|
element: this.dragState.element,
|
||||||
|
|
@ -272,6 +376,9 @@ export class DragDropManager {
|
||||||
this.dragState.columnElement = columnAtPoint;
|
this.dragState.columnElement = columnAtPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip grid position updates if no column yet
|
||||||
|
if (!this.dragState.columnElement) return;
|
||||||
|
|
||||||
const columnRect = this.dragState.columnElement.getBoundingClientRect();
|
const columnRect = this.dragState.columnElement.getBoundingClientRect();
|
||||||
const targetY = e.clientY - columnRect.top - this.dragState.mouseOffset.y;
|
const targetY = e.clientY - columnRect.top - this.dragState.mouseOffset.y;
|
||||||
|
|
||||||
|
|
@ -296,30 +403,53 @@ export class DragDropManager {
|
||||||
const isInHeader = e.clientY < rect.bottom;
|
const isInHeader = e.clientY < rect.bottom;
|
||||||
|
|
||||||
if (isInHeader && !this.inHeader) {
|
if (isInHeader && !this.inHeader) {
|
||||||
// Entered header
|
// Entered header (from grid)
|
||||||
this.inHeader = true;
|
this.inHeader = true;
|
||||||
|
|
||||||
const payload: IDragEnterHeaderPayload = {
|
if (this.dragState.dragSource === 'grid' && this.dragState.columnElement) {
|
||||||
eventId: this.dragState.eventId,
|
const payload: IDragEnterHeaderPayload = {
|
||||||
element: this.dragState.element,
|
eventId: this.dragState.eventId,
|
||||||
sourceColumnIndex: this.getColumnIndex(this.dragState.columnElement),
|
element: this.dragState.element,
|
||||||
sourceDate: this.dragState.columnElement.dataset.date || '',
|
sourceColumnIndex: this.getColumnIndex(this.dragState.columnElement),
|
||||||
title: this.dragState.element.querySelector('swp-event-title')?.textContent || '',
|
sourceDate: this.dragState.columnElement.dataset.date || '',
|
||||||
colorClass: [...this.dragState.element.classList].find(c => c.startsWith('is-')),
|
title: this.dragState.element.querySelector('swp-event-title')?.textContent || '',
|
||||||
itemType: 'event',
|
colorClass: [...this.dragState.element.classList].find(c => c.startsWith('is-')),
|
||||||
duration: 1
|
itemType: 'event',
|
||||||
};
|
duration: 1
|
||||||
|
};
|
||||||
|
|
||||||
this.eventBus.emit(CoreEvents.EVENT_DRAG_ENTER_HEADER, payload);
|
this.eventBus.emit(CoreEvents.EVENT_DRAG_ENTER_HEADER, payload);
|
||||||
|
}
|
||||||
|
// For header source re-entering header, just update inHeader flag
|
||||||
} else if (!isInHeader && this.inHeader) {
|
} else if (!isInHeader && this.inHeader) {
|
||||||
// Left header
|
// Left header (entering grid)
|
||||||
this.inHeader = false;
|
this.inHeader = false;
|
||||||
|
|
||||||
const payload: IDragLeaveHeaderPayload = {
|
const targetColumn = this.getColumnAtPoint(e.clientX);
|
||||||
eventId: this.dragState.eventId
|
|
||||||
};
|
|
||||||
|
|
||||||
this.eventBus.emit(CoreEvents.EVENT_DRAG_LEAVE_HEADER, payload);
|
if (this.dragState.dragSource === 'header') {
|
||||||
|
// Header item leaving header → create swp-event in grid
|
||||||
|
const payload: IDragLeaveHeaderPayload = {
|
||||||
|
eventId: this.dragState.eventId,
|
||||||
|
source: 'header',
|
||||||
|
element: this.dragState.element,
|
||||||
|
targetColumn: targetColumn || undefined,
|
||||||
|
start: this.dragState.element.dataset.start ? new Date(this.dragState.element.dataset.start) : undefined,
|
||||||
|
end: this.dragState.element.dataset.end ? new Date(this.dragState.element.dataset.end) : undefined,
|
||||||
|
title: this.dragState.element.textContent || '',
|
||||||
|
colorClass: [...this.dragState.element.classList].find(c => c.startsWith('is-'))
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventBus.emit(CoreEvents.EVENT_DRAG_LEAVE_HEADER, payload);
|
||||||
|
} else {
|
||||||
|
// Grid event leaving header → restore to grid
|
||||||
|
const payload: IDragLeaveHeaderPayload = {
|
||||||
|
eventId: this.dragState.eventId,
|
||||||
|
source: 'grid'
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventBus.emit(CoreEvents.EVENT_DRAG_LEAVE_HEADER, payload);
|
||||||
|
}
|
||||||
} else if (isInHeader) {
|
} else if (isInHeader) {
|
||||||
// Moving within header
|
// Moving within header
|
||||||
const column = this.getColumnAtX(e.clientX);
|
const column = this.getColumnAtX(e.clientX);
|
||||||
|
|
@ -338,8 +468,8 @@ export class DragDropManager {
|
||||||
/**
|
/**
|
||||||
* Get column index (0-based) for a column element
|
* Get column index (0-based) for a column element
|
||||||
*/
|
*/
|
||||||
private getColumnIndex(column: HTMLElement): number {
|
private getColumnIndex(column: HTMLElement | null): number {
|
||||||
if (!this.container) return 0;
|
if (!this.container || !column) return 0;
|
||||||
const columns = Array.from(this.container.querySelectorAll('swp-day-column'));
|
const columns = Array.from(this.container.querySelectorAll('swp-day-column'));
|
||||||
return columns.indexOf(column);
|
return columns.indexOf(column);
|
||||||
}
|
}
|
||||||
|
|
@ -384,15 +514,17 @@ export class DragDropManager {
|
||||||
// Update element position
|
// Update element position
|
||||||
this.dragState.element.style.top = `${this.dragState.currentY}px`;
|
this.dragState.element.style.top = `${this.dragState.currentY}px`;
|
||||||
|
|
||||||
// Emit drag:move
|
// Emit drag:move (only if we have a column)
|
||||||
const payload: IDragMovePayload = {
|
if (this.dragState.columnElement) {
|
||||||
eventId: this.dragState.eventId,
|
const payload: IDragMovePayload = {
|
||||||
element: this.dragState.element,
|
eventId: this.dragState.eventId,
|
||||||
currentY: this.dragState.currentY,
|
element: this.dragState.element,
|
||||||
columnElement: this.dragState.columnElement
|
currentY: this.dragState.currentY,
|
||||||
};
|
columnElement: this.dragState.columnElement
|
||||||
|
};
|
||||||
|
|
||||||
this.eventBus.emit(CoreEvents.EVENT_DRAG_MOVE, payload);
|
this.eventBus.emit(CoreEvents.EVENT_DRAG_MOVE, payload);
|
||||||
|
}
|
||||||
|
|
||||||
// Continue animation
|
// Continue animation
|
||||||
this.dragState.animationId = requestAnimationFrame(this.animateDrag);
|
this.dragState.animationId = requestAnimationFrame(this.animateDrag);
|
||||||
|
|
@ -413,9 +545,9 @@ export class DragDropManager {
|
||||||
element.style.transition = 'top 200ms ease-out';
|
element.style.transition = 'top 200ms ease-out';
|
||||||
element.style.top = `${startY}px`;
|
element.style.top = `${startY}px`;
|
||||||
|
|
||||||
// Remove ghost after animation
|
// Remove ghost after animation (if exists)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
ghostElement.remove();
|
ghostElement?.remove();
|
||||||
element.style.transition = '';
|
element.style.transition = '';
|
||||||
element.classList.remove('dragging');
|
element.classList.remove('dragging');
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
|
||||||
|
|
@ -41,13 +41,15 @@ export class EventPersistenceManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update and save - start/end already calculated in SwpEvent
|
// Update and save - start/end already calculated in SwpEvent
|
||||||
// If dropped in header, mark as allDay
|
// Set allDay based on drop target:
|
||||||
|
// - header: allDay = true
|
||||||
|
// - grid: allDay = false (converts allDay event to timed)
|
||||||
const updatedEvent: ICalendarEvent = {
|
const updatedEvent: ICalendarEvent = {
|
||||||
...event,
|
...event,
|
||||||
start: swpEvent.start,
|
start: swpEvent.start,
|
||||||
end: swpEvent.end,
|
end: swpEvent.end,
|
||||||
resourceId: swpEvent.resourceId ?? event.resourceId,
|
resourceId: swpEvent.resourceId ?? event.resourceId,
|
||||||
allDay: payload.target === 'header' ? true : event.allDay,
|
allDay: payload.target === 'header',
|
||||||
syncStatus: 'pending'
|
syncStatus: 'pending'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,4 +66,12 @@ export interface IDragMoveHeaderPayload {
|
||||||
|
|
||||||
export interface IDragLeaveHeaderPayload {
|
export interface IDragLeaveHeaderPayload {
|
||||||
eventId: string;
|
eventId: string;
|
||||||
|
source: 'grid' | 'header'; // Where drag originated
|
||||||
|
// Header→grid fields (when source === 'header')
|
||||||
|
element?: HTMLElement; // Header item element
|
||||||
|
targetColumn?: HTMLElement; // Target column in grid
|
||||||
|
start?: Date; // Event start from header item
|
||||||
|
end?: Date; // Event end from header item
|
||||||
|
title?: string;
|
||||||
|
colorClass?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue