diff --git a/src/factories/ManagerFactory.ts b/src/factories/ManagerFactory.ts
index 9a370b2..c3a9c29 100644
--- a/src/factories/ManagerFactory.ts
+++ b/src/factories/ManagerFactory.ts
@@ -7,7 +7,7 @@ import { ScrollManager } from '../managers/ScrollManager';
import { NavigationManager } from '../managers/NavigationManager';
import { ViewManager } from '../managers/ViewManager';
import { CalendarManager } from '../managers/CalendarManager';
-import { ColumnDetector } from '../managers/ColumnDetector';
+import { DragDropManager } from '../managers/DragDropManager';
/**
* Factory for creating and managing calendar managers with proper dependency injection
@@ -35,6 +35,7 @@ export class ManagerFactory {
navigationManager: NavigationManager;
viewManager: ViewManager;
calendarManager: CalendarManager;
+ dragDropManager: DragDropManager;
} {
console.log('🏠ManagerFactory: Creating managers with proper DI...');
@@ -45,7 +46,7 @@ export class ManagerFactory {
const scrollManager = new ScrollManager();
const navigationManager = new NavigationManager(eventBus, eventRenderer);
const viewManager = new ViewManager(eventBus);
- const columnDetector = new ColumnDetector();
+ const dragDropManager = new DragDropManager(eventBus, config);
// CalendarManager depends on all other managers
const calendarManager = new CalendarManager(
@@ -66,7 +67,8 @@ export class ManagerFactory {
scrollManager,
navigationManager,
viewManager,
- calendarManager
+ calendarManager,
+ dragDropManager
};
}
diff --git a/src/managers/ColumnDetector.ts b/src/managers/ColumnDetector.ts
deleted file mode 100644
index fec7dff..0000000
--- a/src/managers/ColumnDetector.ts
+++ /dev/null
@@ -1,669 +0,0 @@
-/**
- * ColumnDetector - Bare detect hvilken kolonne musen er over
- */
-
-import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig';
-import { eventBus } from '../core/EventBus';
-
-export class ColumnDetector {
- private currentColumn: string | null = null;
- private isMouseDown = false;
- private lastMousePosition = { x: 0, y: 0 };
- private lastLoggedPosition = { x: 0, y: 0 };
- private draggedClone: HTMLElement | null = null;
- private originalEvent: HTMLElement | null = null;
- private mouseOffset = { x: 0, y: 0 };
-
- // Auto-scroll properties
- private scrollContainer: HTMLElement | null = null;
- private autoScrollAnimationId: number | null = null;
- private scrollSpeed = 10; // pixels per frame
- private scrollThreshold = 30; // pixels from edge
- private currentMouseY = 0; // Track current mouse Y for scroll updates
-
- // Konfiguration for snap interval
- private snapIntervalMinutes = 15; // 15 minutter
- private hourHeightPx = 60; // Fra CSS --hour-height
- private get snapDistancePx(): number {
- return (this.snapIntervalMinutes / 60) * this.hourHeightPx; // 15/60 * 60 = 15px
- }
-
- constructor() {
- this.init();
- }
-
- /**
- * Konfigurer snap interval
- */
- public setSnapInterval(minutes: number): void {
- this.snapIntervalMinutes = minutes;
- console.log(`Snap interval set to ${minutes} minutes (${this.snapDistancePx}px)`);
- }
-
- /**
- * Fade out og fjern element fra DOM
- */
- private fadeOutAndRemove(element: HTMLElement): void {
- element.style.transition = 'opacity 0.3s ease-out';
- element.style.opacity = '0';
-
- setTimeout(() => {
- element.remove();
- }, 300);
- }
-
- /**
- * Fjern "clone-" prefix fra event ID og gendan pointer events
- */
- private removeClonePrefix(clone: HTMLElement): void {
- const cloneId = clone.dataset.eventId;
- if (cloneId && cloneId.startsWith('clone-')) {
- const originalId = cloneId.replace('clone-', '');
- clone.dataset.eventId = originalId;
- console.log(`Removed clone prefix: ${cloneId} -> ${originalId}`);
- }
-
- // Gendan pointer events sĂĄ klonen kan dragges igen
- clone.style.pointerEvents = '';
- }
-
- private init(): void {
- // Lyt til mouse move pĂĄ hele body
- document.body.addEventListener('mousemove', this.handleMouseMove.bind(this));
-
- // Lyt til click pĂĄ hele body
- document.body.addEventListener('click', this.handleClick.bind(this));
-
- // Lyt til mouse down og up
- document.body.addEventListener('mousedown', this.handleMouseDown.bind(this));
- document.body.addEventListener('mouseup', this.handleMouseUp.bind(this));
-
- // Listen for header mouseover events (both day-headers and all-day-containers)
- eventBus.on('header:mouseover', (event) => {
- const { element, targetDate, headerRenderer } = (event as CustomEvent).detail;
-
- if (this.isMouseDown && this.draggedClone && targetDate) {
- // Scenario 1: Timed event being dragged to header - convert to all-day
- if (this.draggedClone.tagName === 'SWP-EVENT') {
- console.log('Converting timed event to all-day for date:', targetDate);
- headerRenderer.addToAllDay(element);
- this.convertToAllDayPreview(targetDate);
- }
-
- // Scenario 2: All-day event being moved to different day
- else if (this.draggedClone.tagName === 'SWP-ALLDAY-EVENT') {
- const currentDate = this.draggedClone.parentElement?.getAttribute('data-date');
- if (currentDate !== targetDate) {
- console.log('Moving all-day event from', currentDate, 'to', targetDate);
- this.moveAllDayToNewDate(targetDate);
- }
- }
- }
- });
- }
-
-
- private handleMouseMove(event: MouseEvent): void {
- // Track current mouse position for auto-scroll updates
- this.currentMouseY = event.clientY;
-
- // Hvis musen er holdt nede, tjek for snap interval vertikal bevægelse
- if (this.isMouseDown) {
- const deltaY = Math.abs(event.clientY - this.lastLoggedPosition.y);
-
- if (deltaY >= this.snapDistancePx) {
- console.log(`Mouse dragged ${this.snapIntervalMinutes} minutes (${this.snapDistancePx}px) vertically:`, {
- from: this.lastLoggedPosition,
- to: { x: event.clientX, y: event.clientY },
- verticalDistance: Math.round(deltaY),
- snapInterval: `${this.snapIntervalMinutes} minutes`
- });
- this.lastLoggedPosition = { x: event.clientX, y: event.clientY };
-
- // Snap klonens position til nærmeste 15-min interval (only for timed events)
- if (this.draggedClone && this.draggedClone.parentElement && this.draggedClone.tagName !== 'SWP-ALLDAY-EVENT') {
- const columnRect = this.draggedClone.parentElement.getBoundingClientRect();
- const rawRelativeY = event.clientY - columnRect.top - this.mouseOffset.y;
-
- // Snap til nærmeste 15-min grid
- const snappedY = Math.round(rawRelativeY / this.snapDistancePx) * this.snapDistancePx;
- this.draggedClone.style.top = snappedY + 'px';
- }
- }
-
- // Kontinuerlig opdatering under auto-scroll for at sikre klonen følger musen (only for timed events)
- if (this.draggedClone && this.draggedClone.parentElement && this.autoScrollAnimationId !== null && this.draggedClone.tagName !== 'SWP-ALLDAY-EVENT') {
- const columnRect = this.draggedClone.parentElement.getBoundingClientRect();
- const relativeY = event.clientY - columnRect.top - this.mouseOffset.y;
- this.draggedClone.style.top = relativeY + 'px';
- }
-
- // Auto-scroll detection nĂĄr der er en aktiv clone
- if (this.draggedClone) {
- this.checkAutoScroll(event);
- }
- }
-
- // Find hvilket element musen er over (altid)
- {
- const elementUnder = document.elementFromPoint(event.clientX, event.clientY);
- if (!elementUnder) {
- // Ingen element under musen
- if (this.currentColumn !== null) {
- console.log('Left all columns');
- this.currentColumn = null;
- }
- return;
- }
-
- // Gå op gennem DOM træet for at finde swp-day-column
- let element = elementUnder as HTMLElement;
- while (element && element.tagName !== 'SWP-DAY-COLUMN') {
- element = element.parentElement as HTMLElement;
- if (!element) {
- // Ikke i en kolonne
- if (this.currentColumn !== null) {
- console.log('Left all columns');
- this.currentColumn = null;
- }
- return;
- }
- }
-
- // Vi fandt en kolonne
- const date = element.dataset.date;
- if (date && date !== this.currentColumn) {
- console.log('Entered column:', date);
- this.currentColumn = date;
-
- // Flyt klonen til ny kolonne ved kolonneskift (only for timed events)
- if (this.draggedClone && this.isMouseDown && this.draggedClone.tagName !== 'SWP-ALLDAY-EVENT') {
- // Flyt klonen til den nye kolonne
- const newColumnElement = document.querySelector(`swp-day-column[data-date="${date}"]`);
- if (newColumnElement) {
- newColumnElement.appendChild(this.draggedClone);
- // Opdater Y-position relativt til den nye kolonne
- const columnRect = newColumnElement.getBoundingClientRect();
- const relativeY = event.clientY - columnRect.top - this.mouseOffset.y;
- this.draggedClone.style.top = relativeY + 'px';
- }
- }
- }
- }
- }
-
- private handleClick(event: MouseEvent): void {
- const target = event.target as HTMLElement;
-
- // Find event element
- let eventElement = target;
- while (eventElement && eventElement.tagName !== 'SWP-EVENTS-LAYER') {
- if (eventElement.tagName === 'SWP-EVENT' || eventElement.tagName === 'SWP-ALLDAY-EVENT') {
- break;
- }
- eventElement = eventElement.parentElement as HTMLElement;
- if (!eventElement) return;
- }
-
- // Hvis vi nĂĄede til SWP-EVENTS-LAYER uden at finde et event, sĂĄ return
- if (!eventElement || eventElement.tagName === 'SWP-EVENTS-LAYER') {
- return;
- }
-
- // Log event info
- const eventId = eventElement.dataset.eventId;
- const eventType = eventElement.dataset.type;
- console.log('Clicked event:', {
- id: eventId,
- type: eventType,
- element: eventElement,
- title: eventElement.textContent
- });
- }
-
- private handleMouseDown(event: MouseEvent): void {
- this.isMouseDown = true;
- this.lastMousePosition = { x: event.clientX, y: event.clientY };
- this.lastLoggedPosition = { x: event.clientX, y: event.clientY };
- console.log('Mouse down at:', this.lastMousePosition);
-
- // Tjek om mousedown er pĂĄ et event
- const target = event.target as HTMLElement;
- let eventElement = target;
- while (eventElement && eventElement.tagName !== 'SWP-EVENTS-LAYER') {
- if (eventElement.tagName === 'SWP-EVENT' || eventElement.tagName === 'SWP-ALLDAY-EVENT') {
- break;
- }
- eventElement = eventElement.parentElement as HTMLElement;
- if (!eventElement) return;
- }
-
- // Hvis vi nĂĄede til SWP-EVENTS-LAYER uden at finde et event, sĂĄ return
- if (!eventElement || eventElement.tagName === 'SWP-EVENTS-LAYER') {
- return;
- }
-
- // Hvis vi fandt et event, lav en clone
- if (eventElement) {
- // Gem reference til original event
- this.originalEvent = eventElement;
- this.cloneEvent(eventElement, event);
- // Sæt originalen til gennemsigtig og forhindre text selection mens der trækkes
- eventElement.style.opacity = '0.6';
- eventElement.style.userSelect = 'none';
- }
- }
-
- private cloneEvent(originalEvent: HTMLElement, mouseEvent: MouseEvent): void {
- // Lav en clone
- const clone = originalEvent.cloneNode(true) as HTMLElement;
-
- // Præfiks ID med "clone-"
- const originalId = originalEvent.dataset.eventId;
- if (originalId) {
- clone.dataset.eventId = `clone-${originalId}`;
- }
-
- // Beregn hvor pĂĄ event'et musen klikkede
- const eventRect = originalEvent.getBoundingClientRect();
- this.mouseOffset = {
- x: mouseEvent.clientX - eventRect.left, // Stadig nødvendig for cursor placering
- y: mouseEvent.clientY - eventRect.top
- };
-
- // Gør klonen ready til at blive trukket
- clone.style.position = 'absolute';
- clone.style.zIndex = '999999';
- clone.style.pointerEvents = 'none';
-
- // Sæt størrelse fra det originale event
- clone.style.width = eventRect.width + 'px';
- clone.style.height = eventRect.height + 'px';
-
- // Find den aktuelle kolonne og placer klonen der
- const currentColumnElement = document.querySelector(`swp-day-column[data-date="${this.currentColumn}"]`);
- if (currentColumnElement) {
- // Sæt initial position relativt til kolonnen
- const columnRect = currentColumnElement.getBoundingClientRect();
- const relativeY = mouseEvent.clientY - columnRect.top - this.mouseOffset.y;
- clone.style.top = relativeY + 'px';
-
- currentColumnElement.appendChild(clone);
- } else {
- console.error('Could not find current column element:', this.currentColumn);
- // Fallback til original placering
- originalEvent.parentNode?.insertBefore(clone, originalEvent.nextSibling);
- }
-
- // Gem reference til klonen
- this.draggedClone = clone;
-
- console.log('Cloned event:', {
- original: originalId,
- clone: clone.dataset.eventId,
- offset: this.mouseOffset
- });
- }
-
- private handleMouseUp(event: MouseEvent): void {
- this.isMouseDown = false;
- console.log('Mouse up at:', { x: event.clientX, y: event.clientY });
-
- // Stop auto-scroll
- this.stopAutoScroll();
-
- // Drop operationen: fade out original og remove clone suffix
- if (this.originalEvent && this.draggedClone) {
- // Check if clone was converted to all-day (is now in header)
- const cloneInHeader = this.draggedClone.closest('swp-calendar-header');
-
- if (cloneInHeader) {
- console.log('Drop completed: all-day event created');
- // Clone is now an all-day event, just fade out original
- this.fadeOutAndRemove(this.originalEvent);
- } else {
- console.log('Drop in regular area - keeping as timed event');
- // Normal drop: fade out original and keep clone as timed
- this.fadeOutAndRemove(this.originalEvent);
- this.removeClonePrefix(this.draggedClone);
- }
-
- // Ryd op
- this.originalEvent = null;
- this.draggedClone = null;
- this.scrollContainer = null;
- }
-
- // Cleanup hvis ingen drop (ingen clone var aktiv)
- if (this.originalEvent && !this.draggedClone) {
- this.originalEvent.style.opacity = '';
- this.originalEvent.style.userSelect = '';
- this.originalEvent = null;
- }
- }
-
- /**
- * Expand header to show all-day row when clone is dragged into header
- */
- private expandHeaderForAllDay(): void {
- const root = document.documentElement;
- const currentHeight = parseInt(getComputedStyle(root).getPropertyValue('--all-day-row-height') || '0');
-
- if (currentHeight === 0) {
- root.style.setProperty('--all-day-row-height', `${ALL_DAY_CONSTANTS.SINGLE_ROW_HEIGHT}px`);
- console.log('Header expanded for all-day row');
- }
- }
-
- /**
- * Check if auto-scroll should be triggered based on mouse position
- */
- private checkAutoScroll(event: MouseEvent): void {
- // Find scrollable content if not cached
- if (!this.scrollContainer) {
- this.scrollContainer = document.querySelector('swp-scrollable-content') as HTMLElement;
- if (!this.scrollContainer) {
- console.warn('ColumnDetector: Could not find swp-scrollable-content for auto-scroll');
- return;
- }
- console.log('ColumnDetector: Found scroll container:', this.scrollContainer);
- }
-
- const containerRect = this.scrollContainer.getBoundingClientRect();
- const mouseY = event.clientY;
-
- // Calculate distances from edges
- const distanceFromTop = mouseY - containerRect.top;
- const distanceFromBottom = containerRect.bottom - mouseY;
-
-
- // Check if we need to scroll up
- if (distanceFromTop <= this.scrollThreshold && distanceFromTop > 0) {
- this.startAutoScroll('up');
- console.log(`Auto-scroll up triggered: ${Math.round(distanceFromTop)}px from top edge`);
- }
- // Check if we need to scroll down
- else if (distanceFromBottom <= this.scrollThreshold && distanceFromBottom > 0) {
- this.startAutoScroll('down');
- console.log(`Auto-scroll down triggered: ${Math.round(distanceFromBottom)}px from bottom edge`);
- }
- // Stop scrolling if not in threshold zone
- else {
- this.stopAutoScroll();
- }
- }
-
- /**
- * Start auto-scroll animation in specified direction
- */
- private startAutoScroll(direction: 'up' | 'down'): void {
- // Don't start if already scrolling in same direction
- if (this.autoScrollAnimationId !== null) {
- return;
- }
-
- const scroll = () => {
- if (!this.scrollContainer || !this.isMouseDown || !this.draggedClone) {
- this.stopAutoScroll();
- return;
- }
-
- const scrollAmount = direction === 'up' ? -this.scrollSpeed : this.scrollSpeed;
- this.scrollContainer.scrollTop += scrollAmount;
-
- // Update clone position based on current mouse position after scroll (only for timed events)
- if (this.draggedClone && this.draggedClone.parentElement && this.draggedClone.tagName !== 'SWP-ALLDAY-EVENT') {
- const columnRect = this.draggedClone.parentElement.getBoundingClientRect();
- const relativeY = this.currentMouseY - columnRect.top - this.mouseOffset.y;
- this.draggedClone.style.top = relativeY + 'px';
- }
-
- // Continue animation
- this.autoScrollAnimationId = requestAnimationFrame(scroll);
- };
-
- this.autoScrollAnimationId = requestAnimationFrame(scroll);
- }
-
- /**
- * Stop auto-scroll animation
- */
- private stopAutoScroll(): void {
- if (this.autoScrollAnimationId !== null) {
- cancelAnimationFrame(this.autoScrollAnimationId);
- this.autoScrollAnimationId = null;
- }
- }
-
- /**
- * Convert dragged clone to all-day event preview
- */
- private convertToAllDayPreview(targetDate: string): 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);
-
- // No need to recalculate height - addToAllDay already handles this
- }
-
- /**
- * Transform clone from timed event to all-day event format
- */
- private transformCloneToAllDay(clone: HTMLElement, targetDate: string): void {
- const calendarHeader = document.querySelector('swp-calendar-header');
- if (!calendarHeader) return;
-
- // Find or create all-day container for target date
- const container = this.findOrCreateAllDayContainer(calendarHeader as HTMLElement, targetDate);
- if (!container) return;
-
- // Extract title from original clone (remove time info)
- const titleElement = clone.querySelector('swp-event-title');
- const eventTitle = titleElement ? titleElement.textContent || 'Untitled Event' : 'Untitled Event';
-
- // Calculate which column this date corresponds to
- const dayHeaders = document.querySelectorAll('swp-day-header');
- let columnIndex = 1; // Default to first column
-
- dayHeaders.forEach((header, index) => {
- if ((header as HTMLElement).dataset.date === targetDate) {
- columnIndex = index + 1; // 1-based grid index
- }
- });
-
- // Create new all-day event element
- const allDayEvent = document.createElement('swp-allday-event');
- allDayEvent.setAttribute('data-event-id', clone.dataset.eventId || '');
- allDayEvent.setAttribute('data-type', clone.dataset.type || 'work');
- allDayEvent.textContent = eventTitle;
-
- // Position event in correct column and find available row
- (allDayEvent as HTMLElement).style.gridColumn = columnIndex.toString();
-
- // Find first available row (simple assignment to row 1 for dropped events)
- (allDayEvent as HTMLElement).style.gridRow = '1';
-
- // Clear any positioning styles from the original timed event (top, left, position, etc.)
- (allDayEvent as HTMLElement).style.top = '';
- (allDayEvent as HTMLElement).style.left = '';
- (allDayEvent as HTMLElement).style.position = '';
-
- // Remove the original clone from its current parent
- if (clone.parentElement) {
- clone.parentElement.removeChild(clone);
- }
-
- // Add new all-day event to container
- container.appendChild(allDayEvent);
-
- // Update reference to point to new element
- this.draggedClone = allDayEvent;
-
- // Recalculate height after adding new all-day event
- this.recalculateAllDayHeight();
- }
-
- /**
- * Move all-day event to a new date container
- */
- private moveAllDayToNewDate(targetDate: string): void {
- if (!this.draggedClone) return;
-
- const calendarHeader = document.querySelector('swp-calendar-header');
- if (!calendarHeader) return;
-
- // Find or create container for new date
- const newContainer = this.findOrCreateAllDayContainer(calendarHeader as HTMLElement, targetDate);
-
- // Move the dragged clone to new container
- if (newContainer && this.draggedClone.parentElement !== newContainer) {
- newContainer.appendChild(this.draggedClone);
-
- // Recalculate height after moving
- this.recalculateAllDayHeight();
- }
- }
-
- /**
- * Find existing or create new all-day container for specific date
- */
- private findOrCreateAllDayContainer(calendarHeader: HTMLElement, targetDate: string): HTMLElement | null {
- // Find day headers to determine column index
- const dayHeaders = calendarHeader.querySelectorAll('swp-day-header');
- let columnIndex = -1;
-
- for (let i = 0; i < dayHeaders.length; i++) {
- const dayHeader = dayHeaders[i] as HTMLElement;
- if (dayHeader.dataset.date === targetDate) {
- columnIndex = i + 1; // 1-based grid index
- break;
- }
- }
-
- if (columnIndex === -1) {
- console.error(`Could not find column for date: ${targetDate}`);
- return null;
- }
-
- // Find the all-day container
- const container = calendarHeader.querySelector('swp-allday-container');
- if (!container) {
- console.warn('ColumnDetector: No swp-allday-container found - HeaderRenderer should create this');
- return null;
- }
-
- return container as HTMLElement;
- }
-
-
- /**
- * Recalculate all-day row height based on number of rows in use
- */
- private recalculateAllDayHeight(): void {
- const calendarHeader = document.querySelector('swp-calendar-header') as HTMLElement;
- if (!calendarHeader) return;
-
- // Find all-day container
- const allDayContainer = calendarHeader.querySelector('swp-allday-container');
- if (!allDayContainer) {
- console.warn('ColumnDetector: No swp-allday-container found for height recalculation');
- return;
- }
-
- // Count highest row used by any event
- const events = allDayContainer.querySelectorAll('swp-allday-event');
- let maxRow = 1;
-
- events.forEach(event => {
- const gridRow = (event as HTMLElement).style.gridRow;
- if (gridRow) {
- const rowNum = parseInt(gridRow);
- if (rowNum > maxRow) {
- maxRow = rowNum;
- }
- }
- });
-
- // Calculate new height
- const root = document.documentElement;
- const eventHeight = parseInt(getComputedStyle(root).getPropertyValue('--allday-event-height') || '26');
- const calculatedHeight = maxRow * eventHeight;
-
- // Get current heights for animation
- const headerHeight = parseInt(getComputedStyle(root).getPropertyValue('--header-height'));
- const currentAllDayHeight = parseInt(getComputedStyle(root).getPropertyValue('--all-day-row-height') || '0');
- const currentTotalHeight = headerHeight + currentAllDayHeight;
- const targetTotalHeight = headerHeight + calculatedHeight;
-
- // Only animate if height actually changes
- if (currentAllDayHeight !== calculatedHeight) {
- // Find header spacer
- const headerSpacer = document.querySelector('swp-header-spacer') as HTMLElement;
-
- // Animate both header and spacer simultaneously
- const animations = [
- calendarHeader.animate([
- { height: `${currentTotalHeight}px` },
- { height: `${targetTotalHeight}px` }
- ], {
- duration: 150,
- easing: 'ease-out',
- fill: 'forwards'
- })
- ];
-
- if (headerSpacer) {
- animations.push(
- headerSpacer.animate([
- { height: `${currentTotalHeight}px` },
- { height: `${targetTotalHeight}px` }
- ], {
- duration: 150,
- easing: 'ease-out',
- fill: 'forwards'
- })
- );
- }
-
- // Wait for all animations to finish before setting CSS variable
- Promise.all(animations.map(anim => anim.finished)).then(() => {
- root.style.setProperty('--all-day-row-height', `${calculatedHeight}px`);
-
- // Update grid-template-rows for all swp-allday-containers
- const allDayContainers = document.querySelectorAll('swp-allday-container');
- allDayContainers.forEach(container => {
- const gridRows = `repeat(${maxRow}, var(--allday-event-height, 26px))`;
- (container as HTMLElement).style.gridTemplateRows = gridRows;
- });
-
- // Notify ScrollManager about header height change
- eventBus.emit('header:height-changed');
- });
-
- console.log(`Animated all-day height: ${currentAllDayHeight}px → ${calculatedHeight}px (max stack: ${maxRow})`);
- } else {
- // Height hasn't changed but we still need to update grid-template-rows in case of different row arrangements
- const allDayContainers = document.querySelectorAll('swp-allday-container');
- allDayContainers.forEach(container => {
- const gridRows = `repeat(${maxRow}, var(--allday-event-height, 26px))`;
- (container as HTMLElement).style.gridTemplateRows = gridRows;
- });
-
- console.log(`All-day height unchanged (${currentAllDayHeight}px) but updated grid-template-rows to ${maxRow} rows`);
- }
- }
-
- public destroy(): void {
- this.stopAutoScroll();
- document.body.removeEventListener('mousemove', this.handleMouseMove.bind(this));
- document.body.removeEventListener('click', this.handleClick.bind(this));
- document.body.removeEventListener('mousedown', this.handleMouseDown.bind(this));
- document.body.removeEventListener('mouseup', this.handleMouseUp.bind(this));
- }
-}
\ No newline at end of file
diff --git a/src/managers/DragDropManager.ts b/src/managers/DragDropManager.ts
new file mode 100644
index 0000000..5859da3
--- /dev/null
+++ b/src/managers/DragDropManager.ts
@@ -0,0 +1,338 @@
+/**
+ * DragDropManager - Handles drag and drop interaction logic
+ * Emits events for visual updates handled by EventRenderer
+ */
+
+import { IEventBus } from '../types/CalendarTypes';
+import { CalendarConfig } from '../core/CalendarConfig';
+
+export class DragDropManager {
+ private eventBus: IEventBus;
+ private config: CalendarConfig;
+
+ // Mouse tracking
+ private isMouseDown = false;
+ private lastMousePosition = { x: 0, y: 0 };
+ private lastLoggedPosition = { x: 0, y: 0 };
+ private currentMouseY = 0;
+ private mouseOffset = { x: 0, y: 0 };
+
+ // Drag state
+ private draggedEventId: string | null = null;
+ private originalElement: HTMLElement | null = null;
+ private currentColumn: string | null = null;
+
+ // Auto-scroll properties
+ private scrollContainer: HTMLElement | null = null;
+ private autoScrollAnimationId: number | null = null;
+ private scrollSpeed = 10; // pixels per frame
+ private scrollThreshold = 30; // pixels from edge
+
+ // Snap configuration
+ private snapIntervalMinutes = 15; // Default 15 minutes
+ private hourHeightPx = 60; // From CSS --hour-height
+
+ private get snapDistancePx(): number {
+ return (this.snapIntervalMinutes / 60) * this.hourHeightPx;
+ }
+
+ constructor(eventBus: IEventBus, config: CalendarConfig) {
+ this.eventBus = eventBus;
+ this.config = config;
+
+ // Get config values
+ const gridSettings = config.getGridSettings();
+ this.hourHeightPx = gridSettings.hourHeight;
+
+ this.init();
+ }
+
+ /**
+ * Configure snap interval
+ */
+ public setSnapInterval(minutes: number): void {
+ this.snapIntervalMinutes = minutes;
+ console.log(`DragDropManager: Snap interval set to ${minutes} minutes (${this.snapDistancePx}px)`);
+ }
+
+ private init(): void {
+ // Listen to mouse events on body
+ document.body.addEventListener('mousemove', this.handleMouseMove.bind(this));
+ document.body.addEventListener('mousedown', this.handleMouseDown.bind(this));
+ document.body.addEventListener('mouseup', this.handleMouseUp.bind(this));
+
+ // Listen for header mouseover events
+ this.eventBus.on('header:mouseover', (event) => {
+ const { element, targetDate, headerRenderer } = (event as CustomEvent).detail;
+
+ if (this.isMouseDown && this.draggedEventId && targetDate) {
+ // Emit event to convert to all-day
+ this.eventBus.emit('drag:convert-to-allday', {
+ eventId: this.draggedEventId,
+ targetDate,
+ element,
+ headerRenderer
+ });
+ }
+ });
+ }
+
+ private handleMouseDown(event: MouseEvent): void {
+ this.isMouseDown = true;
+ this.lastMousePosition = { x: event.clientX, y: event.clientY };
+ this.lastLoggedPosition = { x: event.clientX, y: event.clientY };
+
+ // Check if mousedown is on an event
+ const target = event.target as HTMLElement;
+ let eventElement = target;
+
+ while (eventElement && eventElement.tagName !== 'SWP-EVENTS-LAYER') {
+ if (eventElement.tagName === 'SWP-EVENT' || eventElement.tagName === 'SWP-ALLDAY-EVENT') {
+ break;
+ }
+ eventElement = eventElement.parentElement as HTMLElement;
+ if (!eventElement) return;
+ }
+
+ // If we reached SWP-EVENTS-LAYER without finding an event, return
+ if (!eventElement || eventElement.tagName === 'SWP-EVENTS-LAYER') {
+ return;
+ }
+
+ // Found an event - start dragging
+ if (eventElement) {
+ this.originalElement = eventElement;
+ this.draggedEventId = eventElement.dataset.eventId || null;
+
+ // Calculate mouse offset within event
+ const eventRect = eventElement.getBoundingClientRect();
+ this.mouseOffset = {
+ x: event.clientX - eventRect.left,
+ y: event.clientY - eventRect.top
+ };
+
+ // Detect current column
+ const column = this.detectColumn(event.clientX, event.clientY);
+ if (column) {
+ this.currentColumn = column;
+ }
+
+ // Emit drag start event
+ this.eventBus.emit('drag:start', {
+ originalElement: eventElement,
+ eventId: this.draggedEventId,
+ mousePosition: { x: event.clientX, y: event.clientY },
+ mouseOffset: this.mouseOffset,
+ column: this.currentColumn
+ });
+
+ console.log('DragDropManager: Drag started', {
+ eventId: this.draggedEventId,
+ column: this.currentColumn
+ });
+ }
+ }
+
+ private handleMouseMove(event: MouseEvent): void {
+ this.currentMouseY = event.clientY;
+
+ if (this.isMouseDown && this.draggedEventId) {
+ const deltaY = Math.abs(event.clientY - this.lastLoggedPosition.y);
+
+ // Check for snap interval vertical movement
+ if (deltaY >= this.snapDistancePx) {
+ this.lastLoggedPosition = { x: event.clientX, y: event.clientY };
+
+ // Calculate snapped position
+ const column = this.detectColumn(event.clientX, event.clientY);
+ const snappedY = this.calculateSnapPosition(event.clientY);
+
+ // Emit drag move event with snapped position
+ this.eventBus.emit('drag:move', {
+ eventId: this.draggedEventId,
+ mousePosition: { x: event.clientX, y: event.clientY },
+ snappedY,
+ column,
+ mouseOffset: this.mouseOffset
+ });
+
+ console.log(`DragDropManager: Drag moved ${this.snapIntervalMinutes} minutes`, {
+ snappedY,
+ column
+ });
+ }
+
+ // Check for auto-scroll
+ this.checkAutoScroll(event);
+
+ // Check for column change
+ const newColumn = this.detectColumn(event.clientX, event.clientY);
+ if (newColumn && newColumn !== this.currentColumn) {
+ console.log(`DragDropManager: Column changed from ${this.currentColumn} to ${newColumn}`);
+ this.currentColumn = newColumn;
+
+ this.eventBus.emit('drag:column-change', {
+ eventId: this.draggedEventId,
+ previousColumn: this.currentColumn,
+ newColumn,
+ mousePosition: { x: event.clientX, y: event.clientY }
+ });
+ }
+ }
+ }
+
+ private handleMouseUp(event: MouseEvent): void {
+ if (!this.isMouseDown) return;
+
+ this.isMouseDown = false;
+
+ // Stop auto-scroll
+ this.stopAutoScroll();
+
+ if (this.draggedEventId && this.originalElement) {
+ // Calculate final position
+ const finalColumn = this.detectColumn(event.clientX, event.clientY);
+ const finalY = this.calculateSnapPosition(event.clientY);
+
+ // Emit drag end event
+ this.eventBus.emit('drag:end', {
+ eventId: this.draggedEventId,
+ originalElement: this.originalElement,
+ finalPosition: { x: event.clientX, y: event.clientY },
+ finalColumn,
+ finalY
+ });
+
+ console.log('DragDropManager: Drag ended', {
+ eventId: this.draggedEventId,
+ finalColumn,
+ finalY
+ });
+
+ // Clean up
+ this.draggedEventId = null;
+ this.originalElement = null;
+ this.currentColumn = null;
+ this.scrollContainer = null;
+ }
+ }
+
+ /**
+ * Calculate snapped Y position based on mouse Y
+ */
+ private calculateSnapPosition(mouseY: number): number {
+ // Find the column element to get relative position
+ const columnElement = this.currentColumn
+ ? document.querySelector(`swp-day-column[data-date="${this.currentColumn}"]`)
+ : null;
+
+ if (!columnElement) return mouseY;
+
+ const columnRect = columnElement.getBoundingClientRect();
+ const relativeY = mouseY - columnRect.top - this.mouseOffset.y;
+
+ // Snap to nearest interval
+ const snappedY = Math.round(relativeY / this.snapDistancePx) * this.snapDistancePx;
+
+ // Ensure non-negative
+ return Math.max(0, snappedY);
+ }
+
+ /**
+ * Detect which column the mouse is over
+ */
+ private detectColumn(mouseX: number, mouseY: number): string | null {
+ const element = document.elementFromPoint(mouseX, mouseY);
+ if (!element) return null;
+
+ // Walk up DOM tree to find swp-day-column
+ let current = element as HTMLElement;
+ while (current && current.tagName !== 'SWP-DAY-COLUMN') {
+ current = current.parentElement as HTMLElement;
+ if (!current) return null;
+ }
+
+ return current.dataset.date || null;
+ }
+
+ /**
+ * Check if auto-scroll should be triggered
+ */
+ private checkAutoScroll(event: MouseEvent): void {
+ // Find scrollable content if not cached
+ if (!this.scrollContainer) {
+ this.scrollContainer = document.querySelector('swp-scrollable-content') as HTMLElement;
+ if (!this.scrollContainer) {
+ console.warn('DragDropManager: Could not find swp-scrollable-content for auto-scroll');
+ return;
+ }
+ }
+
+ const containerRect = this.scrollContainer.getBoundingClientRect();
+ const mouseY = event.clientY;
+
+ // Calculate distances from edges
+ const distanceFromTop = mouseY - containerRect.top;
+ const distanceFromBottom = containerRect.bottom - mouseY;
+
+ // Check if we need to scroll
+ if (distanceFromTop <= this.scrollThreshold && distanceFromTop > 0) {
+ this.startAutoScroll('up');
+ } else if (distanceFromBottom <= this.scrollThreshold && distanceFromBottom > 0) {
+ this.startAutoScroll('down');
+ } else {
+ this.stopAutoScroll();
+ }
+ }
+
+ /**
+ * Start auto-scroll animation
+ */
+ private startAutoScroll(direction: 'up' | 'down'): void {
+ if (this.autoScrollAnimationId !== null) return;
+
+ const scroll = () => {
+ if (!this.scrollContainer || !this.isMouseDown) {
+ this.stopAutoScroll();
+ return;
+ }
+
+ const scrollAmount = direction === 'up' ? -this.scrollSpeed : this.scrollSpeed;
+ this.scrollContainer.scrollTop += scrollAmount;
+
+ // Emit updated position during scroll
+ if (this.draggedEventId) {
+ const snappedY = this.calculateSnapPosition(this.currentMouseY);
+ this.eventBus.emit('drag:auto-scroll', {
+ eventId: this.draggedEventId,
+ snappedY,
+ scrollTop: this.scrollContainer.scrollTop
+ });
+ }
+
+ this.autoScrollAnimationId = requestAnimationFrame(scroll);
+ };
+
+ this.autoScrollAnimationId = requestAnimationFrame(scroll);
+ }
+
+ /**
+ * Stop auto-scroll animation
+ */
+ private stopAutoScroll(): void {
+ if (this.autoScrollAnimationId !== null) {
+ cancelAnimationFrame(this.autoScrollAnimationId);
+ this.autoScrollAnimationId = null;
+ }
+ }
+
+ /**
+ * Clean up event listeners
+ */
+ public destroy(): void {
+ this.stopAutoScroll();
+ document.body.removeEventListener('mousemove', this.handleMouseMove.bind(this));
+ document.body.removeEventListener('mousedown', this.handleMouseDown.bind(this));
+ document.body.removeEventListener('mouseup', this.handleMouseUp.bind(this));
+ }
+}
\ No newline at end of file
diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts
index de38ab0..fdf9b7b 100644
--- a/src/renderers/EventRenderer.ts
+++ b/src/renderers/EventRenderer.ts
@@ -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 = `
- ${startTime} - ${endTime}
+ ${startTime} - ${endTime}
${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();