Improves drag and drop with edge scrolling

Enhances the drag and drop experience by integrating edge scrolling,
allowing users to scroll the calendar view while dragging events.

Fixes issues with event positioning during scrolling by compensating
for scroll changes during drag operations. Also, adds mock events
to data.
This commit is contained in:
Janus C. H. Knudsen 2025-10-13 17:20:17 +02:00
parent a0344c6143
commit faf8b50593
3 changed files with 244 additions and 60 deletions

View file

@ -2675,5 +2675,135 @@
"duration": 60,
"color": "#dda15e"
}
},
{
"id": "169",
"title": "Morgen Standup",
"start": "2025-10-13T05:00:00Z",
"end": "2025-10-13T05:30:00Z",
"type": "meeting",
"allDay": false,
"syncStatus": "synced",
"metadata": {
"duration": 30,
"color": "#ff5722"
}
},
{
"id": "170",
"title": "Produktvejledning",
"start": "2025-10-13T07:00:00Z",
"end": "2025-10-13T08:30:00Z",
"type": "meeting",
"allDay": false,
"syncStatus": "synced",
"metadata": {
"duration": 90,
"color": "#9c27b0"
}
},
{
"id": "171",
"title": "Team Standup",
"start": "2025-10-14T05:00:00Z",
"end": "2025-10-14T05:30:00Z",
"type": "meeting",
"allDay": false,
"syncStatus": "synced",
"metadata": {
"duration": 30,
"color": "#ff5722"
}
},
{
"id": "172",
"title": "Udviklingssession",
"start": "2025-10-14T06:00:00Z",
"end": "2025-10-14T09:00:00Z",
"type": "work",
"allDay": false,
"syncStatus": "synced",
"metadata": {
"duration": 180,
"color": "#2196f3"
}
},
{
"id": "173",
"title": "Klient Gennemgang",
"start": "2025-10-15T11:00:00Z",
"end": "2025-10-15T12:00:00Z",
"type": "meeting",
"allDay": false,
"syncStatus": "synced",
"metadata": {
"duration": 60,
"color": "#795548"
}
},
{
"id": "174",
"title": "Team Standup",
"start": "2025-10-16T05:00:00Z",
"end": "2025-10-16T05:30:00Z",
"type": "meeting",
"allDay": false,
"syncStatus": "synced",
"metadata": {
"duration": 30,
"color": "#ff5722"
}
},
{
"id": "175",
"title": "Arkitektur Workshop",
"start": "2025-10-16T10:00:00Z",
"end": "2025-10-16T13:00:00Z",
"type": "meeting",
"allDay": false,
"syncStatus": "synced",
"metadata": {
"duration": 180,
"color": "#009688"
}
},
{
"id": "176",
"title": "Team Standup",
"start": "2025-10-17T05:00:00Z",
"end": "2025-10-17T05:30:00Z",
"type": "meeting",
"allDay": false,
"syncStatus": "synced",
"metadata": {
"duration": 30,
"color": "#ff5722"
}
},
{
"id": "177",
"title": "Sprint Review",
"start": "2025-10-17T10:00:00Z",
"end": "2025-10-17T11:00:00Z",
"type": "meeting",
"allDay": false,
"syncStatus": "synced",
"metadata": {
"duration": 60,
"color": "#607d8b"
}
},
{
"id": "178",
"title": "Weekend Kodning",
"start": "2025-10-18T06:00:00Z",
"end": "2025-10-18T10:00:00Z",
"type": "work",
"allDay": false,
"syncStatus": "synced",
"metadata": {
"duration": 240,
"color": "#3f51b5"
}
}
]

View file

@ -18,6 +18,7 @@ import {
DragColumnChangeEventPayload
} from '../types/EventTypes';
import { MousePosition } from '../types/DragDropTypes';
import { CoreEvents } from '../constants/CoreEvents';
export class DragDropManager {
private eventBus: IEventBus;
@ -41,6 +42,12 @@ export class DragDropManager {
// Movement threshold to distinguish click from drag
private readonly dragThreshold = 5; // pixels
// Scroll compensation
private scrollableContent: HTMLElement | null = null;
private initialScrollTop = 0;
private initialCloneTop = 0;
private isScrollCompensating = false; // Track if scroll compensation is active
private scrollListener: ((e: Event) => void) | null = null;
// Smooth drag animation
private dragAnimationId: number | null = null;
@ -53,6 +60,8 @@ export class DragDropManager {
// Get config values
const gridSettings = calendarConfig.getGridSettings();
this.init();
}
@ -98,6 +107,9 @@ export class DragDropManager {
// Initialize column bounds cache
ColumnDetectionUtils.updateColumnBoundsCache();
// Listen to resize events to update cache
window.addEventListener('resize', () => {
ColumnDetectionUtils.updateColumnBoundsCache();
@ -108,6 +120,25 @@ export class DragDropManager {
ColumnDetectionUtils.updateColumnBoundsCache();
});
this.eventBus.on(CoreEvents.GRID_RENDERED, (event: Event) => {
this.handleGridRendered(event as CustomEvent);
});
// Listen to edge-scroll events to control scroll compensation
this.eventBus.on('edgescroll:started', () => {
this.isScrollCompensating = true;
console.log('🎬 DragDropManager: Edge-scroll started - disabling continueDrag()');
});
this.eventBus.on('edgescroll:stopped', () => {
this.isScrollCompensating = false;
console.log('🛑 DragDropManager: Edge-scroll stopped - enabling continueDrag()');
});
}
private handleGridRendered(event: CustomEvent) {
this.scrollableContent = document.querySelector('swp-scrollable-content');
this.scrollableContent!.addEventListener('scroll', this.handleScroll.bind(this), { passive: true });
}
private handleMouseDown(event: MouseEvent): void {
@ -151,6 +182,8 @@ export class DragDropManager {
* Optimized mouse move handler with consolidated position calculations
*/
private handleMouseMove(event: MouseEvent): void {
if (this.isScrollCompensating) return;
//this.currentMouseY = event.clientY;
// this.lastMousePosition = { x: event.clientX, y: event.clientY };
@ -195,6 +228,8 @@ export class DragDropManager {
// Start drag
this.isDragStarted = true;
// Set high z-index on event-group if exists, otherwise on event itself
const eventGroup = this.originalElement!.closest<HTMLElement>('swp-event-group');
if (eventGroup) {
@ -209,7 +244,7 @@ export class DragDropManager {
const dragStartPayload: DragStartEventPayload = {
originalElement: this.originalElement!,
draggedClone: this.draggedClone,
draggedClone: this.draggedClone,
mousePosition: this.mouseDownPosition,
mouseOffset: this.mouseOffset,
columnBounds: this.currentColumn
@ -221,6 +256,7 @@ export class DragDropManager {
private continueDrag(currentPosition: MousePosition): void {
if (!this.draggedClone!.hasAttribute("data-allday")) {
// Calculate raw position from mouse (no snapping)
const column = ColumnDetectionUtils.getColumnBounds(currentPosition);
@ -269,6 +305,7 @@ export class DragDropManager {
*/
private handleMouseUp(event: MouseEvent): void {
this.stopDragAnimation();
this.removeScrollListener();
if (this.originalElement) {
@ -339,6 +376,7 @@ export class DragDropManager {
console.log('🚫 DragDropManager: Cancelling drag - mouse left grid container');
this.cleanupAllClones();
this.removeScrollListener();
this.originalElement.style.opacity = '';
this.originalElement.style.cursor = '';
@ -415,6 +453,51 @@ export class DragDropManager {
}
}
/**
* Handle scroll during drag - compensate clone position
*/
private handleScroll(): void {
if (!this.isDragStarted || !this.draggedClone || !this.scrollableContent || !this.isScrollCompensating) return;
// First time scrolling - save initial positions NOW!
this.initialScrollTop = this.scrollableContent.scrollTop;
this.initialCloneTop = parseFloat(this.draggedClone.style.top || '0');
console.log('💾 DragDropManager: Scroll compensation started', {
initialScrollTop: this.initialScrollTop,
initialCloneTop: this.initialCloneTop
});
const currentScrollTop = this.scrollableContent.scrollTop;
const totalScrollDelta = currentScrollTop - this.initialScrollTop;
// Beregn ny position baseret på initial position + total scroll delta
const newTop = this.initialCloneTop + totalScrollDelta;
this.draggedClone.style.top = `${newTop}px`;
console.log('📜 DragDropManager: Scroll compensation', {
initialScrollTop: this.initialScrollTop,
currentScrollTop,
totalScrollDelta,
initialCloneTop: this.initialCloneTop,
newTop
});
}
/**
* Remove scroll listener
*/
private removeScrollListener(): void {
if (this.scrollListener && this.scrollableContent) {
this.scrollableContent.removeEventListener('scroll', this.scrollListener);
this.scrollListener = null;
}
this.isScrollCompensating = false;
this.initialScrollTop = 0;
this.initialCloneTop = 0;
}
/**
* Stop drag animation

View file

@ -11,18 +11,15 @@ export class EdgeScrollManager {
private scrollRAF: number | null = null;
private mouseY = 0;
private isDragging = false;
private isScrolling = false; // Track if edge-scroll is active
private lastTs = 0;
private rect: DOMRect | null = null;
private draggedClone: HTMLElement | null = null;
private initialScrollTop = 0;
private initialCloneTop = 0;
private scrollListener: ((e: Event) => void) | null = null;
// Constants - fixed values as per requirements
private readonly OUTER_ZONE = 100; // px from edge (slow zone)
private readonly INNER_ZONE = 50; // px from edge (fast zone)
private readonly SLOW_SPEED_PXS = 800; // px/sec in outer zone
private readonly FAST_SPEED_PXS = 2400; // px/sec in inner zone
private readonly SLOW_SPEED_PXS = 80; // px/sec in outer zone
private readonly FAST_SPEED_PXS = 240; // px/sec in inner zone
constructor(private eventBus: IEventBus) {
this.init();
@ -35,31 +32,26 @@ export class EdgeScrollManager {
if (this.scrollableContent) {
// Disable smooth scroll for instant auto-scroll
this.scrollableContent.style.scrollBehavior = 'auto';
// Add scroll listener
this.scrollListener = this.handleScroll.bind(this);
this.scrollableContent.addEventListener('scroll', this.scrollListener, { passive: true });
}
}, 100);
// Listen to mousemove directly from document to always get mouse coords
document.body.addEventListener('mousemove', (e: MouseEvent) => {
if (this.isDragging) {
this.mouseY = e.clientY;
}
});
this.subscribeToEvents();
}
private subscribeToEvents(): void {
// Listen to drag events from DragDropManager
this.eventBus.on('drag:start', (event: Event) => {
let customEvent = event as CustomEvent<DragStartEventPayload>;
this.draggedClone = customEvent.detail.draggedClone;
this.eventBus.on('drag:start', () => {
this.startDrag();
});
this.eventBus.on('drag:move', (event: Event) => {
let customEvent = event as CustomEvent<DragMoveEventPayload>;
this.draggedClone = customEvent.detail.draggedClone;
this.updateMouseY(customEvent.detail.mousePosition.y);
});
this.eventBus.on('drag:end', () => this.stopDrag());
this.eventBus.on('drag:cancelled', () => this.stopDrag());
}
@ -67,62 +59,25 @@ export class EdgeScrollManager {
private startDrag(): void {
console.log('🎬 EdgeScrollManager: Starting drag');
this.isDragging = true;
this.isScrolling = false; // Reset scroll state
this.lastTs = performance.now();
// Gem initial scroll position OG clone position
this.initialScrollTop = this.scrollableContent?.scrollTop || 0;
this.initialCloneTop = parseFloat(this.draggedClone?.style.top || '0');
console.log('💾 EdgeScrollManager: Saved initial state', {
initialScrollTop: this.initialScrollTop,
initialCloneTop: this.initialCloneTop
});
// Don't save initial positions here - wait until scrolling actually starts!
if (this.scrollRAF === null) {
this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts));
}
}
private updateMouseY(y: number): void {
// console.log('🖱️ EdgeScrollManager: updateMouseY called', { oldMouseY: this.mouseY, newMouseY: y });
this.mouseY = y;
// Ensure RAF loop is running during drag
if (this.isDragging && this.scrollRAF === null) {
this.lastTs = performance.now();
this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts));
}
}
private stopDrag(): void {
this.isDragging = false;
this.isScrolling = false;
if (this.scrollRAF !== null) {
cancelAnimationFrame(this.scrollRAF);
this.scrollRAF = null;
}
this.rect = null;
this.lastTs = 0;
this.draggedClone = null;
this.initialScrollTop = 0;
this.initialCloneTop = 0;
}
private handleScroll(): void {
if (!this.isDragging || !this.draggedClone || !this.scrollableContent) return;
const currentScrollTop = this.scrollableContent.scrollTop;
const totalScrollDelta = currentScrollTop - this.initialScrollTop;
// Beregn ny position baseret på initial position + total scroll delta
const newTop = this.initialCloneTop + totalScrollDelta;
//this.draggedClone.style.top = `${newTop}px`;
console.log('📜 EdgeScrollManager: Scroll event - updated clone', {
initialScrollTop: this.initialScrollTop,
currentScrollTop,
totalScrollDelta,
initialCloneTop: this.initialCloneTop,
newTop
});
}
private scrollTick(ts: number): void {
@ -159,11 +114,27 @@ export class EdgeScrollManager {
}
if (vy !== 0 && this.isDragging) {
// Mark that scrolling is active
if (!this.isScrolling) {
this.isScrolling = true;
console.log('💾 EdgeScrollManager: Edge-scroll started');
// Notify DragDropManager that scroll compensation should start
this.eventBus.emit('edgescroll:started', {});
}
// Time-based scrolling for frame-rate independence
this.scrollableContent.scrollTop += vy * dt;
this.rect = null; // Invalidate cache for next frame
this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts));
} else {
// Mouse moved away from edge - stop scrolling
if (this.isScrolling) {
this.isScrolling = false;
console.log('🛑 EdgeScrollManager: Edge-scroll stopped');
// Notify DragDropManager that scroll compensation should stop
this.eventBus.emit('edgescroll:stopped', {});
}
// Continue RAF loop even if not scrolling, to detect edge entry
if (this.isDragging) {
this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts));