Implements drag and drop functionality
Introduces a DragDropManager to handle event dragging and dropping, replacing the ColumnDetector. This change centralizes drag and drop logic, improving code organization and maintainability. The EventRenderer now uses the DragDropManager's events to visually update the calendar during drag operations. Removes ColumnDetector which is now replaced by the drag and drop manager.
This commit is contained in:
parent
be4a8af7c4
commit
f697944d75
4 changed files with 658 additions and 676 deletions
|
|
@ -4,6 +4,7 @@ import { CalendarEvent } from '../types/CalendarTypes';
|
|||
import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig';
|
||||
import { CalendarConfig } from '../core/CalendarConfig';
|
||||
import { DateCalculator } from '../utils/DateCalculator';
|
||||
import { eventBus } from '../core/EventBus';
|
||||
|
||||
/**
|
||||
* Interface for event rendering strategies
|
||||
|
|
@ -18,9 +19,312 @@ export interface EventRendererStrategy {
|
|||
*/
|
||||
export abstract class BaseEventRenderer implements EventRendererStrategy {
|
||||
protected dateCalculator: DateCalculator;
|
||||
protected config: CalendarConfig;
|
||||
|
||||
// Drag and drop state
|
||||
private draggedClone: HTMLElement | null = null;
|
||||
private originalEvent: HTMLElement | null = null;
|
||||
|
||||
constructor(config: CalendarConfig) {
|
||||
this.config = config;
|
||||
this.dateCalculator = new DateCalculator(config);
|
||||
this.setupDragEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup listeners for drag events from DragDropManager
|
||||
*/
|
||||
private setupDragEventListeners(): void {
|
||||
// Handle drag start
|
||||
eventBus.on('drag:start', (event) => {
|
||||
const { originalElement, eventId, mouseOffset, column } = (event as CustomEvent).detail;
|
||||
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 end
|
||||
eventBus.on('drag:end', (event) => {
|
||||
const { eventId, originalElement, finalColumn, finalY } = (event as CustomEvent).detail;
|
||||
this.handleDragEnd(eventId, originalElement, finalColumn, finalY);
|
||||
});
|
||||
|
||||
// Handle column change
|
||||
eventBus.on('drag:column-change', (event) => {
|
||||
const { eventId, newColumn } = (event as CustomEvent).detail;
|
||||
this.handleColumnChange(eventId, newColumn);
|
||||
});
|
||||
|
||||
// Handle convert to all-day
|
||||
eventBus.on('drag:convert-to-allday', (event) => {
|
||||
const { eventId, targetDate, headerRenderer } = (event as CustomEvent).detail;
|
||||
this.handleConvertToAllDay(eventId, targetDate, headerRenderer);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
console.log(`EventRenderer: Read duration ${durationMinutes} minutes from data-duration attribute`);
|
||||
return durationMinutes;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to 60 minutes if attribute not found
|
||||
console.warn('EventRenderer: No data-duration found, using fallback 60 minutes');
|
||||
return 60;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
console.log(`EventRenderer: Clone created with ${originalDurationMinutes} minutes duration from data-duration`);
|
||||
|
||||
// Style for dragging
|
||||
clone.style.position = 'absolute';
|
||||
clone.style.zIndex = '999999';
|
||||
clone.style.pointerEvents = 'none';
|
||||
clone.style.opacity = '0.8';
|
||||
|
||||
// Keep original dimensions (height stays the same)
|
||||
const rect = originalEvent.getBoundingClientRect();
|
||||
clone.style.width = rect.width + 'px';
|
||||
clone.style.height = rect.height + 'px';
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update clone timestamp based on new position
|
||||
*/
|
||||
private updateCloneTimestamp(clone: HTMLElement, snappedY: number): void {
|
||||
const gridSettings = this.config.getGridSettings();
|
||||
const hourHeight = gridSettings.hourHeight;
|
||||
const dayStartHour = gridSettings.dayStartHour;
|
||||
const snapInterval = 15; // TODO: Get from config
|
||||
|
||||
// Calculate total minutes from top
|
||||
const totalMinutesFromTop = (snappedY / hourHeight) * 60;
|
||||
const startTotalMinutes = Math.max(
|
||||
dayStartHour * 60,
|
||||
Math.round((dayStartHour * 60 + totalMinutesFromTop) / snapInterval) * snapInterval
|
||||
);
|
||||
|
||||
// Use cached original duration (no recalculation)
|
||||
const cachedDuration = parseInt(clone.dataset.originalDuration || '60');
|
||||
const endTotalMinutes = startTotalMinutes + cachedDuration;
|
||||
|
||||
// Update display
|
||||
const timeElement = clone.querySelector('swp-event-time');
|
||||
if (timeElement) {
|
||||
const newTimeText = `${this.formatTime(startTotalMinutes)} - ${this.formatTime(endTotalMinutes)}`;
|
||||
timeElement.textContent = newTimeText;
|
||||
|
||||
console.log(`EventRenderer: Updated timestamp to ${newTimeText} (${cachedDuration} min duration)`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate event duration in minutes from element height
|
||||
*/
|
||||
private getEventDuration(element: HTMLElement): number {
|
||||
const gridSettings = this.config.getGridSettings();
|
||||
const hourHeight = gridSettings.hourHeight;
|
||||
|
||||
// Get height from style or computed
|
||||
let heightPx = parseFloat(element.style.height) || 0;
|
||||
if (!heightPx) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
heightPx = rect.height;
|
||||
}
|
||||
|
||||
return Math.round((heightPx / hourHeight) * 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time from total minutes
|
||||
*/
|
||||
private formatTime(totalMinutes: number): string {
|
||||
const hours = Math.floor(totalMinutes / 60) % 24;
|
||||
const minutes = totalMinutes % 60;
|
||||
const period = hours >= 12 ? 'PM' : 'AM';
|
||||
const displayHours = hours % 12 || 12;
|
||||
return `${displayHours}:${minutes.toString().padStart(2, '0')} ${period}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle drag start event
|
||||
*/
|
||||
private handleDragStart(originalElement: HTMLElement, eventId: string, mouseOffset: any, column: string): void {
|
||||
this.originalEvent = originalElement;
|
||||
|
||||
// Create clone
|
||||
this.draggedClone = this.createEventClone(originalElement);
|
||||
|
||||
// Add to current column
|
||||
const columnElement = document.querySelector(`swp-day-column[data-date="${column}"]`);
|
||||
if (columnElement) {
|
||||
columnElement.appendChild(this.draggedClone);
|
||||
}
|
||||
|
||||
// Make original semi-transparent
|
||||
originalElement.style.opacity = '0.3';
|
||||
originalElement.style.userSelect = 'none';
|
||||
|
||||
console.log('EventRenderer: Drag started, clone created');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle drag move event
|
||||
*/
|
||||
private handleDragMove(eventId: string, snappedY: number, column: string, mouseOffset: any): void {
|
||||
if (!this.draggedClone) return;
|
||||
|
||||
// Update position
|
||||
this.draggedClone.style.top = snappedY + 'px';
|
||||
|
||||
// Update timestamp display
|
||||
this.updateCloneTimestamp(this.draggedClone, snappedY);
|
||||
|
||||
console.log('EventRenderer: Clone position and timestamp updated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle column change during drag
|
||||
*/
|
||||
private handleColumnChange(eventId: string, newColumn: string): void {
|
||||
if (!this.draggedClone) return;
|
||||
|
||||
// Move clone to new column
|
||||
const newColumnElement = document.querySelector(`swp-day-column[data-date="${newColumn}"]`);
|
||||
if (newColumnElement && this.draggedClone.parentElement !== newColumnElement) {
|
||||
newColumnElement.appendChild(this.draggedClone);
|
||||
console.log(`EventRenderer: Clone moved to column ${newColumn}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle drag end event
|
||||
*/
|
||||
private handleDragEnd(eventId: string, originalElement: HTMLElement, finalColumn: string, finalY: number): void {
|
||||
if (!this.draggedClone || !this.originalEvent) return;
|
||||
|
||||
// Fade out original
|
||||
this.fadeOutAndRemove(this.originalEvent);
|
||||
|
||||
// Remove clone prefix and enable pointer events
|
||||
const cloneId = this.draggedClone.dataset.eventId;
|
||||
if (cloneId && cloneId.startsWith('clone-')) {
|
||||
this.draggedClone.dataset.eventId = cloneId.replace('clone-', '');
|
||||
}
|
||||
this.draggedClone.style.pointerEvents = '';
|
||||
this.draggedClone.style.opacity = '';
|
||||
|
||||
// Clean up
|
||||
this.draggedClone = null;
|
||||
this.originalEvent = null;
|
||||
|
||||
console.log('EventRenderer: Drag completed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle conversion to all-day event
|
||||
*/
|
||||
private handleConvertToAllDay(eventId: string, targetDate: string, headerRenderer: any): void {
|
||||
if (!this.draggedClone) return;
|
||||
|
||||
// Only convert once
|
||||
if (this.draggedClone.tagName === 'SWP-ALLDAY-EVENT') return;
|
||||
|
||||
// Transform clone to all-day format
|
||||
this.transformCloneToAllDay(this.draggedClone, targetDate);
|
||||
|
||||
// Expand header if needed
|
||||
headerRenderer.addToAllDay(this.draggedClone.parentElement);
|
||||
|
||||
console.log(`EventRenderer: Converted to all-day event for date ${targetDate}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform clone from timed to all-day event
|
||||
*/
|
||||
private transformCloneToAllDay(clone: HTMLElement, targetDate: string): void {
|
||||
const calendarHeader = document.querySelector('swp-calendar-header');
|
||||
if (!calendarHeader) return;
|
||||
|
||||
// Find all-day container
|
||||
const allDayContainer = calendarHeader.querySelector('swp-allday-container');
|
||||
if (!allDayContainer) return;
|
||||
|
||||
// Extract title
|
||||
const titleElement = clone.querySelector('swp-event-title');
|
||||
const eventTitle = titleElement ? titleElement.textContent || 'Untitled' : 'Untitled';
|
||||
|
||||
// Calculate column index
|
||||
const dayHeaders = document.querySelectorAll('swp-day-header');
|
||||
let columnIndex = 1;
|
||||
dayHeaders.forEach((header, index) => {
|
||||
if ((header as HTMLElement).dataset.date === targetDate) {
|
||||
columnIndex = index + 1;
|
||||
}
|
||||
});
|
||||
|
||||
// Create all-day event
|
||||
const allDayEvent = document.createElement('swp-allday-event');
|
||||
allDayEvent.dataset.eventId = clone.dataset.eventId || '';
|
||||
allDayEvent.dataset.type = clone.dataset.type || 'work';
|
||||
allDayEvent.textContent = eventTitle;
|
||||
|
||||
// Position in grid
|
||||
(allDayEvent as HTMLElement).style.gridColumn = columnIndex.toString();
|
||||
(allDayEvent as HTMLElement).style.gridRow = '1';
|
||||
|
||||
// Remove original clone
|
||||
if (clone.parentElement) {
|
||||
clone.parentElement.removeChild(clone);
|
||||
}
|
||||
|
||||
// Add to all-day container
|
||||
allDayContainer.appendChild(allDayEvent);
|
||||
|
||||
// Update reference
|
||||
this.draggedClone = allDayEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fade out and remove element
|
||||
*/
|
||||
private fadeOutAndRemove(element: HTMLElement): void {
|
||||
element.style.transition = 'opacity 0.3s ease-out';
|
||||
element.style.opacity = '0';
|
||||
|
||||
setTimeout(() => {
|
||||
element.remove();
|
||||
}, 300);
|
||||
}
|
||||
renderEvents(events: CalendarEvent[], container: HTMLElement, config: CalendarConfig): void {
|
||||
console.log('BaseEventRenderer: renderEvents called with', events.length, 'events');
|
||||
|
|
@ -186,14 +490,21 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
// Color is now handled by CSS classes based on data-type attribute
|
||||
|
||||
// Format time for display
|
||||
const startTime = this.dateCalculator.formatTime(new Date(event.start));
|
||||
const endTime = this.dateCalculator.formatTime(new Date(event.end));
|
||||
const startTime = this.formatTimeFromISOString(event.start);
|
||||
const endTime = this.formatTimeFromISOString(event.end);
|
||||
|
||||
// Calculate duration in minutes
|
||||
const startDate = new Date(event.start);
|
||||
const endDate = new Date(event.end);
|
||||
const durationMinutes = (endDate.getTime() - startDate.getTime()) / (1000 * 60);
|
||||
|
||||
// Create event content
|
||||
eventElement.innerHTML = `
|
||||
<swp-event-time>${startTime} - ${endTime}</swp-event-time>
|
||||
<swp-event-time data-duration="${durationMinutes}">${startTime} - ${endTime}</swp-event-time>
|
||||
<swp-event-title>${event.title}</swp-event-title>
|
||||
`;
|
||||
|
||||
console.log(`BaseEventRenderer: Rendered "${event.title}" with ${durationMinutes} minutes duration`);
|
||||
|
||||
container.appendChild(eventElement);
|
||||
|
||||
|
|
@ -240,7 +551,7 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
|
|||
return { top, height };
|
||||
}
|
||||
|
||||
protected formatTime(isoString: string): string {
|
||||
protected formatTimeFromISOString(isoString: string): string {
|
||||
const date = new Date(isoString);
|
||||
const hours = date.getHours();
|
||||
const minutes = date.getMinutes();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue