From eb08a284953ff7a5e4e5aa71ed6c04f95d912179 Mon Sep 17 00:00:00 2001 From: Janus Knudsen Date: Sun, 24 Aug 2025 23:31:11 +0200 Subject: [PATCH] Improves all-day event drag and drop Enhances the drag and drop experience for all-day events by expanding the header to display the all-day row when dragging an event over it. Introduces constants for all-day event layout. --- src/core/CalendarConfig.ts | 12 +++++ src/managers/ColumnDetector.ts | 26 ++++++++++ src/renderers/EventRenderer.ts | 7 +-- src/renderers/GridRenderer.ts | 9 ++++ src/renderers/HeaderRenderer.ts | 73 +++++++++++++++++++++++++++-- wwwroot/css/calendar-layout-css.css | 1 + 6 files changed, 118 insertions(+), 10 deletions(-) diff --git a/src/core/CalendarConfig.ts b/src/core/CalendarConfig.ts index d1765bd..f7938e6 100644 --- a/src/core/CalendarConfig.ts +++ b/src/core/CalendarConfig.ts @@ -4,6 +4,18 @@ import { eventBus } from './EventBus'; import { CoreEvents } from '../constants/CoreEvents'; import { CalendarConfig as ICalendarConfig, ViewPeriod, CalendarMode } from '../types/CalendarTypes'; +/** + * All-day event layout constants + */ +export const ALL_DAY_CONSTANTS = { + EVENT_HEIGHT: 22, // Height of single all-day event + EVENT_GAP: 2, // Gap between stacked events + CONTAINER_PADDING: 4, // Container padding (top + bottom) + get SINGLE_ROW_HEIGHT() { + return this.EVENT_HEIGHT + this.EVENT_GAP + this.CONTAINER_PADDING; // 28px + } +} as const; + /** * Layout and timing settings for the calendar grid */ diff --git a/src/managers/ColumnDetector.ts b/src/managers/ColumnDetector.ts index 1354fc0..29758c3 100644 --- a/src/managers/ColumnDetector.ts +++ b/src/managers/ColumnDetector.ts @@ -2,6 +2,9 @@ * 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; @@ -67,8 +70,18 @@ export class ColumnDetector { // 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 + eventBus.on('header:mouseover', (event) => { + const { dayHeader, headerRenderer } = (event as CustomEvent).detail; + if (this.isMouseDown && this.draggedClone) { + console.log('Dragging clone over header - calling addToAllDay'); + headerRenderer.addToAllDay(dayHeader); + } + }); } + private handleMouseMove(event: MouseEvent): void { // Hvis musen er holdt nede, tjek for snap interval vertikal bevægelse if (this.isMouseDown) { @@ -280,6 +293,19 @@ export class ColumnDetector { } } + /** + * 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'); + } + } + public destroy(): void { document.body.removeEventListener('mousemove', this.handleMouseMove.bind(this)); document.body.removeEventListener('click', this.handleClick.bind(this)); diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts index 1fc2b6a..21185cc 100644 --- a/src/renderers/EventRenderer.ts +++ b/src/renderers/EventRenderer.ts @@ -1,6 +1,7 @@ // Event rendering strategy interface and implementations import { CalendarEvent } from '../types/CalendarTypes'; +import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig'; import { CalendarConfig } from '../core/CalendarConfig'; import { DateCalculator } from '../utils/DateCalculator'; @@ -181,12 +182,8 @@ export abstract class BaseEventRenderer implements EventRendererStrategy { }); // Calculate and set the all-day row height based on max stack - // Each event is 22px height + 2px gap - const eventHeight = 22; - const gap = 2; - const padding = 4; // Container padding (2px top + 2px bottom) const calculatedHeight = maxStackHeight > 0 - ? (maxStackHeight * eventHeight) + ((maxStackHeight - 1) * gap) + padding + ? (maxStackHeight * ALL_DAY_CONSTANTS.EVENT_HEIGHT) + ((maxStackHeight - 1) * ALL_DAY_CONSTANTS.EVENT_GAP) + ALL_DAY_CONSTANTS.CONTAINER_PADDING : 0; // No height if no events // Set CSS variable for row height diff --git a/src/renderers/GridRenderer.ts b/src/renderers/GridRenderer.ts index 656bc79..aaeeca5 100644 --- a/src/renderers/GridRenderer.ts +++ b/src/renderers/GridRenderer.ts @@ -3,6 +3,7 @@ import { ResourceCalendarData } from '../types/CalendarTypes'; import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; import { HeaderRenderContext } from './HeaderRenderer'; import { ColumnRenderContext } from './ColumnRenderer'; +import { eventBus } from '../core/EventBus'; /** * GridRenderer - Handles DOM rendering for the calendar grid * Separated from GridManager to follow Single Responsibility Principle @@ -133,6 +134,14 @@ export class GridRenderer { }; headerRenderer.render(calendarHeader, context); + + // Add mouseover listeners on day headers for drag detection + const dayHeaders = calendarHeader.querySelectorAll('swp-day-header'); + dayHeaders.forEach(dayHeader => { + dayHeader.addEventListener('mouseover', () => { + eventBus.emit('header:mouseover', { dayHeader, headerRenderer }); + }); + }); } /** diff --git a/src/renderers/HeaderRenderer.ts b/src/renderers/HeaderRenderer.ts index d6bc4f3..0225bfe 100644 --- a/src/renderers/HeaderRenderer.ts +++ b/src/renderers/HeaderRenderer.ts @@ -1,6 +1,6 @@ // Header rendering strategy interface and implementations -import { CalendarConfig } from '../core/CalendarConfig'; +import { CalendarConfig, ALL_DAY_CONSTANTS } from '../core/CalendarConfig'; import { ResourceCalendarData } from '../types/CalendarTypes'; import { DateCalculator } from '../utils/DateCalculator'; @@ -9,6 +9,71 @@ import { DateCalculator } from '../utils/DateCalculator'; */ export interface HeaderRenderer { render(calendarHeader: HTMLElement, context: HeaderRenderContext): void; + addToAllDay(dayHeader: HTMLElement): void; +} + +/** + * Base class with shared addToAllDay implementation + */ +export abstract class BaseHeaderRenderer implements HeaderRenderer { + abstract render(calendarHeader: HTMLElement, context: HeaderRenderContext): void; + + /** + * Expand header to show all-day row + */ + addToAllDay(dayHeader: HTMLElement): void { + const root = document.documentElement; + const currentHeight = parseInt(getComputedStyle(root).getPropertyValue('--all-day-row-height') || '0'); + + if (currentHeight === 0) { + // Find the calendar header element to animate + const calendarHeader = dayHeader.closest('swp-calendar-header') as HTMLElement; + if (calendarHeader) { + this.animateHeaderExpansion(calendarHeader); + } + console.log('BaseHeaderRenderer: Header expanded for all-day row'); + } + } + + private animateHeaderExpansion(calendarHeader: HTMLElement): void { + const root = document.documentElement; + const currentHeaderHeight = parseInt(getComputedStyle(root).getPropertyValue('--header-height') || '80'); + const targetHeight = currentHeaderHeight + ALL_DAY_CONSTANTS.SINGLE_ROW_HEIGHT; + + // Find header spacer + const headerSpacer = document.querySelector('swp-header-spacer') as HTMLElement; + + // Animate both header and spacer simultaneously + const animations = [ + calendarHeader.animate([ + { height: `${currentHeaderHeight}px` }, + { height: `${targetHeight}px` } + ], { + duration: 300, + easing: 'ease-out', + fill: 'forwards' + }) + ]; + + if (headerSpacer) { + animations.push( + headerSpacer.animate([ + { height: `${currentHeaderHeight}px` }, + { height: `${targetHeight}px` } + ], { + duration: 300, + easing: 'ease-out', + fill: 'forwards' + }) + ); + } + + // Wait for all animations to finish + Promise.all(animations.map(anim => anim.finished)).then(() => { + // Set the CSS variable after animation + root.style.setProperty('--all-day-row-height', `${ALL_DAY_CONSTANTS.SINGLE_ROW_HEIGHT}px`); + }); + } } /** @@ -23,7 +88,7 @@ export interface HeaderRenderContext { /** * Date-based header renderer (original functionality) */ -export class DateHeaderRenderer implements HeaderRenderer { +export class DateHeaderRenderer extends BaseHeaderRenderer { private dateCalculator!: DateCalculator; render(calendarHeader: HTMLElement, context: HeaderRenderContext): void { @@ -53,14 +118,12 @@ export class DateHeaderRenderer implements HeaderRenderer { calendarHeader.appendChild(header); }); } - - } /** * Resource-based header renderer */ -export class ResourceHeaderRenderer implements HeaderRenderer { +export class ResourceHeaderRenderer extends BaseHeaderRenderer { render(calendarHeader: HTMLElement, context: HeaderRenderContext): void { const { resourceData } = context; diff --git a/wwwroot/css/calendar-layout-css.css b/wwwroot/css/calendar-layout-css.css index 8eadd27..27bd8f5 100644 --- a/wwwroot/css/calendar-layout-css.css +++ b/wwwroot/css/calendar-layout-css.css @@ -151,6 +151,7 @@ swp-calendar-header { top: 0; z-index: 3; /* Lower than header-spacer so it slides under during horizontal scroll */ height: calc(var(--header-height) + var(--all-day-row-height)); /* Same calculation as spacers */ + transition: height 0.3s ease; /* Force scrollbar to appear for alignment */ overflow-y: scroll;