Improves drag and drop functionality

Enhances drag and drop behavior by introducing free positioning during auto-scroll and snapping to grid intervals.

- Introduces a `calculateFreePosition` method to allow events to follow the mouse exactly during auto-scroll.
- Modifies the drag move event to emit the snapped position during normal drag behavior.
- Updates event rendering to use grid settings for snap intervals.
- Updates grid styles to configure CSS variables dynamically.
This commit is contained in:
Janus Knudsen 2025-09-03 20:48:23 +02:00
parent b4d758b6d9
commit 7a1c776bc1
4 changed files with 50 additions and 25 deletions

View file

@ -47,7 +47,7 @@ export class DragDropManager {
// Snap configuration
private snapIntervalMinutes = 15; // Default 15 minutes
private hourHeightPx = 60; // From CSS --hour-height
private hourHeightPx: number; // Will be set from config
// Event listener references for proper cleanup
private boundHandlers = {
@ -66,6 +66,7 @@ export class DragDropManager {
// Get config values
const gridSettings = calendarConfig.getGridSettings();
this.hourHeightPx = gridSettings.hourHeight;
this.snapIntervalMinutes = gridSettings.snapInterval;
this.init();
}
@ -164,14 +165,14 @@ export class DragDropManager {
const currentPosition: Position = { x: event.clientX, y: event.clientY };
const deltaY = Math.abs(currentPosition.y - this.lastLoggedPosition.y);
// Check for snap interval vertical movement
// Check for snap interval vertical movement (normal drag behavior)
if (deltaY >= this.snapDistancePx) {
this.lastLoggedPosition = currentPosition;
// Consolidated position calculations
// Consolidated position calculations with snapping for normal drag
const positionData = this.calculateDragPosition(currentPosition);
// Emit drag move event with consolidated data
// Emit drag move event with snapped position (normal behavior)
this.eventBus.emit('drag:move', {
eventId: this.draggedEventId,
mousePosition: currentPosition,
@ -240,7 +241,24 @@ export class DragDropManager {
}
/**
* Optimized snap position calculation with caching
* Calculate free position (follows mouse exactly)
*/
private calculateFreePosition(mouseY: number, column: string | null = null): number {
const targetColumn = column || this.currentColumn;
// Use cached column element if available
const columnElement = this.getCachedColumnElement(targetColumn);
if (!columnElement) return mouseY;
const columnRect = columnElement.getBoundingClientRect();
const relativeY = mouseY - columnRect.top - this.mouseOffset.y;
// Return free position (no snapping)
return Math.max(0, relativeY);
}
/**
* Optimized snap position calculation with caching (used only on drop)
*/
private calculateSnapPosition(mouseY: number, column: string | null = null): number {
const targetColumn = column || this.currentColumn;
@ -371,14 +389,13 @@ export class DragDropManager {
const columnElement = this.getCachedColumnElement(this.currentColumn);
if (columnElement) {
const columnRect = columnElement.getBoundingClientRect();
// Calculate position relative to column, accounting for scroll movement
// Calculate free position relative to column, accounting for scroll movement (no snapping during scroll)
const relativeY = this.currentMouseY - columnRect.top - this.mouseOffset.y;
const snappedY = Math.round(relativeY / this.snapDistancePx) * this.snapDistancePx;
const finalSnappedY = Math.max(0, snappedY);
const freeY = Math.max(0, relativeY);
this.eventBus.emit('drag:auto-scroll', {
eventId: this.draggedEventId,
snappedY: finalSnappedY,
snappedY: freeY, // Actually free position during scroll
scrollTop: this.cachedElements.scrollContainer.scrollTop
});
}

View file

@ -8,6 +8,7 @@ import { calendarConfig } from '../core/CalendarConfig';
import { CoreEvents } from '../constants/CoreEvents';
import { ResourceCalendarData, CalendarView } from '../types/CalendarTypes';
import { GridRenderer } from '../renderers/GridRenderer';
import { GridStyleManager } from '../renderers/GridStyleManager';
import { DateCalculator } from '../utils/DateCalculator';
/**
@ -19,11 +20,13 @@ export class GridManager {
private resourceData: ResourceCalendarData | null = null;
private currentView: CalendarView = 'week';
private gridRenderer: GridRenderer;
private styleManager: GridStyleManager;
private eventCleanup: (() => void)[] = [];
constructor() {
// Initialize GridRenderer with config
// Initialize GridRenderer and StyleManager with config
this.gridRenderer = new GridRenderer();
this.styleManager = new GridStyleManager();
this.init();
}
@ -85,6 +88,9 @@ export class GridManager {
return;
}
// Update CSS variables first
this.styleManager.updateGridStyles(this.resourceData);
// Delegate to GridRenderer with current view context
this.gridRenderer.renderGrid(
this.container,

View file

@ -159,25 +159,26 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
const gridSettings = calendarConfig.getGridSettings();
const hourHeight = gridSettings.hourHeight;
const dayStartHour = gridSettings.dayStartHour;
const snapInterval = 15; // TODO: Get from config
const snapInterval = gridSettings.snapInterval;
// Calculate total minutes from top
const totalMinutesFromTop = (snappedY / hourHeight) * 60;
const startTotalMinutes = Math.max(
dayStartHour * 60,
Math.round((dayStartHour * 60 + totalMinutesFromTop) / snapInterval) * snapInterval
);
// Calculate minutes from grid start (not from midnight)
const minutesFromGridStart = (snappedY / hourHeight) * 60;
// Add dayStartHour offset to get actual time
const actualStartMinutes = (dayStartHour * 60) + minutesFromGridStart;
// Snap to interval
const snappedStartMinutes = Math.round(actualStartMinutes / snapInterval) * snapInterval;
// Use cached original duration (no recalculation)
const cachedDuration = parseInt(clone.dataset.originalDuration || '60');
const endTotalMinutes = startTotalMinutes + cachedDuration;
const endTotalMinutes = snappedStartMinutes + cachedDuration;
// Update display
const timeElement = clone.querySelector('swp-event-time');
if (timeElement) {
const newTimeText = `${this.formatTime(startTotalMinutes)} - ${this.formatTime(endTotalMinutes)}`;
const newTimeText = `${this.formatTime(snappedStartMinutes)} - ${this.formatTime(endTotalMinutes)}`;
timeElement.textContent = newTimeText;
}
}
@ -619,19 +620,20 @@ export abstract class BaseEventRenderer implements EventRendererStrategy {
const dayStartHour = gridSettings.dayStartHour;
const hourHeight = gridSettings.hourHeight;
// Calculate minutes from visible day start
// Calculate minutes from midnight
const startMinutes = startDate.getHours() * 60 + startDate.getMinutes();
const endMinutes = endDate.getHours() * 60 + endDate.getMinutes();
const dayStartMinutes = dayStartHour * 60;
// Calculate top position (subtract day start to align with time axis)
// Calculate top position relative to visible grid start
// If dayStartHour=6 and event starts at 09:00 (540 min), then:
// top = ((540 - 360) / 60) * hourHeight = 3 * hourHeight (3 hours from grid start)
const top = ((startMinutes - dayStartMinutes) / 60) * hourHeight;
// Calculate height
// Calculate height based on event duration
const durationMinutes = endMinutes - startMinutes;
const height = (durationMinutes / 60) * hourHeight;
return { top, height };
}

View file

@ -110,7 +110,7 @@ swp-hour-marker {
swp-hour-marker::before {
content: '';
position: absolute;
top: 0px;
top: -1px;
left: 50px;
width: calc(100vw - 60px); /* Full viewport width minus time-axis width */
height: 1px;