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.
This commit is contained in:
Janus Knudsen 2025-08-24 23:31:11 +02:00
parent 457e222262
commit eb08a28495
6 changed files with 118 additions and 10 deletions

View file

@ -4,6 +4,18 @@ import { eventBus } from './EventBus';
import { CoreEvents } from '../constants/CoreEvents'; import { CoreEvents } from '../constants/CoreEvents';
import { CalendarConfig as ICalendarConfig, ViewPeriod, CalendarMode } from '../types/CalendarTypes'; 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 * Layout and timing settings for the calendar grid
*/ */

View file

@ -2,6 +2,9 @@
* ColumnDetector - Bare detect hvilken kolonne musen er over * ColumnDetector - Bare detect hvilken kolonne musen er over
*/ */
import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig';
import { eventBus } from '../core/EventBus';
export class ColumnDetector { export class ColumnDetector {
private currentColumn: string | null = null; private currentColumn: string | null = null;
private isMouseDown = false; private isMouseDown = false;
@ -67,7 +70,17 @@ export class ColumnDetector {
// Lyt til mouse down og up // Lyt til mouse down og up
document.body.addEventListener('mousedown', this.handleMouseDown.bind(this)); document.body.addEventListener('mousedown', this.handleMouseDown.bind(this));
document.body.addEventListener('mouseup', this.handleMouseUp.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 { private handleMouseMove(event: MouseEvent): void {
// Hvis musen er holdt nede, tjek for snap interval vertikal bevægelse // Hvis musen er holdt nede, tjek for snap interval vertikal bevægelse
@ -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 { public destroy(): void {
document.body.removeEventListener('mousemove', this.handleMouseMove.bind(this)); document.body.removeEventListener('mousemove', this.handleMouseMove.bind(this));
document.body.removeEventListener('click', this.handleClick.bind(this)); document.body.removeEventListener('click', this.handleClick.bind(this));

View file

@ -1,6 +1,7 @@
// Event rendering strategy interface and implementations // Event rendering strategy interface and implementations
import { CalendarEvent } from '../types/CalendarTypes'; import { CalendarEvent } from '../types/CalendarTypes';
import { ALL_DAY_CONSTANTS } from '../core/CalendarConfig';
import { CalendarConfig } from '../core/CalendarConfig'; import { CalendarConfig } from '../core/CalendarConfig';
import { DateCalculator } from '../utils/DateCalculator'; 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 // 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 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 : 0; // No height if no events
// Set CSS variable for row height // Set CSS variable for row height

View file

@ -3,6 +3,7 @@ import { ResourceCalendarData } from '../types/CalendarTypes';
import { CalendarTypeFactory } from '../factories/CalendarTypeFactory'; import { CalendarTypeFactory } from '../factories/CalendarTypeFactory';
import { HeaderRenderContext } from './HeaderRenderer'; import { HeaderRenderContext } from './HeaderRenderer';
import { ColumnRenderContext } from './ColumnRenderer'; import { ColumnRenderContext } from './ColumnRenderer';
import { eventBus } from '../core/EventBus';
/** /**
* GridRenderer - Handles DOM rendering for the calendar grid * GridRenderer - Handles DOM rendering for the calendar grid
* Separated from GridManager to follow Single Responsibility Principle * Separated from GridManager to follow Single Responsibility Principle
@ -133,6 +134,14 @@ export class GridRenderer {
}; };
headerRenderer.render(calendarHeader, context); 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 });
});
});
} }
/** /**

View file

@ -1,6 +1,6 @@
// Header rendering strategy interface and implementations // 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 { ResourceCalendarData } from '../types/CalendarTypes';
import { DateCalculator } from '../utils/DateCalculator'; import { DateCalculator } from '../utils/DateCalculator';
@ -9,6 +9,71 @@ import { DateCalculator } from '../utils/DateCalculator';
*/ */
export interface HeaderRenderer { export interface HeaderRenderer {
render(calendarHeader: HTMLElement, context: HeaderRenderContext): void; 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) * Date-based header renderer (original functionality)
*/ */
export class DateHeaderRenderer implements HeaderRenderer { export class DateHeaderRenderer extends BaseHeaderRenderer {
private dateCalculator!: DateCalculator; private dateCalculator!: DateCalculator;
render(calendarHeader: HTMLElement, context: HeaderRenderContext): void { render(calendarHeader: HTMLElement, context: HeaderRenderContext): void {
@ -53,14 +118,12 @@ export class DateHeaderRenderer implements HeaderRenderer {
calendarHeader.appendChild(header); calendarHeader.appendChild(header);
}); });
} }
} }
/** /**
* Resource-based header renderer * Resource-based header renderer
*/ */
export class ResourceHeaderRenderer implements HeaderRenderer { export class ResourceHeaderRenderer extends BaseHeaderRenderer {
render(calendarHeader: HTMLElement, context: HeaderRenderContext): void { render(calendarHeader: HTMLElement, context: HeaderRenderContext): void {
const { resourceData } = context; const { resourceData } = context;

View file

@ -151,6 +151,7 @@ swp-calendar-header {
top: 0; top: 0;
z-index: 3; /* Lower than header-spacer so it slides under during horizontal scroll */ 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 */ 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 */ /* Force scrollbar to appear for alignment */
overflow-y: scroll; overflow-y: scroll;