From e83753a7d2eac933f2174d6f6a7296b890831cc6 Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Wed, 8 Oct 2025 21:50:41 +0200 Subject: [PATCH] Improves event drag and drop Enhances the event drag and drop functionality by setting the initial position of the dragged event to prevent it from jumping to the top of the column. Also adjust event transition for a smoother user experience. Removes unused resize logic. --- .workbench/anotherresize.txt | 179 ---------------------------- src/renderers/EventRenderer.ts | 14 ++- wwwroot/css/calendar-events-css.css | 2 +- 3 files changed, 12 insertions(+), 183 deletions(-) delete mode 100644 .workbench/anotherresize.txt diff --git a/.workbench/anotherresize.txt b/.workbench/anotherresize.txt deleted file mode 100644 index 01b7b0f..0000000 --- a/.workbench/anotherresize.txt +++ /dev/null @@ -1,179 +0,0 @@ -import { eventBus } from '../core/EventBus'; -import { CoreEvents } from '../constants/CoreEvents'; -import { calendarConfig } from '../core/CalendarConfig'; - -type SwpEventEl = HTMLElement & { updateHeight?: (h: number) => void }; - -export class ResizeHandleManager { - private cachedEvents: SwpEventEl[] = []; - private isResizing = false; - private targetEl: SwpEventEl | null = null; - - private startY = 0; - private startDurationMin = 0; - private direction: 'grow' | 'shrink' = 'grow'; - - private hourHeightPx: number; - private snapMin: number; - private minDurationMin: number; - private animationId: number | null = null; - private currentHeight = 0; - private targetHeight = 0; - - // cleanup - private unsubscribers: Array<() => void> = []; - private pointerCaptured = false; - private prevZ?: string; - - constructor() { - const grid = calendarConfig.getGridSettings(); - this.hourHeightPx = grid.hourHeight; - this.snapMin = grid.snapInterval; - this.minDurationMin = grid.minEventDuration ?? this.snapMin; - } - - public initialize(): void { - this.refreshEventCache(); - this.attachGlobalListeners(); - this.attachHandles(); - this.subToBus(); - } - - public destroy(): void { - document.removeEventListener('pointerdown', this.onPointerDown, true); - document.removeEventListener('pointermove', this.onPointerMove, true); - document.removeEventListener('pointerup', this.onPointerUp, true); - this.unsubscribers.forEach(u => u()); - } - - private minutesPerPx(): number { - return 60 / this.hourHeightPx; - } - - private pxFromMinutes(min: number): number { - return (min / 60) * this.hourHeightPx; - } - - private roundSnap(min: number, dir: 'grow' | 'shrink'): number { - const q = min / this.snapMin; - return (dir === 'grow' ? Math.ceil(q) : Math.floor(q)) * this.snapMin; - } - - private refreshEventCache(): void { - this.cachedEvents = Array.from( - document.querySelectorAll('swp-day-columns swp-event') - ); - } - - private attachHandles(): void { - // ensure a single handle per event - this.cachedEvents.forEach(el => { - if (!el.querySelector(':scope > swp-resize-handle')) { - const handle = document.createElement('swp-resize-handle'); - handle.setAttribute('aria-label', 'Resize event'); - handle.setAttribute('role', 'separator'); - el.appendChild(handle); - } - }); - } - - private attachGlobalListeners(): void { - document.addEventListener('pointerdown', this.onPointerDown, true); - document.addEventListener('pointermove', this.onPointerMove, true); - document.addEventListener('pointerup', this.onPointerUp, true); - } - - private subToBus(): void { - const sub = (ev: string, fn: () => void) => { - eventBus.on(ev, fn); - this.unsubscribers.push(() => eventBus.off(ev, fn)); - }; - const refresh = () => { this.refreshEventCache(); this.attachHandles(); }; - [CoreEvents.GRID_RENDERED, CoreEvents.EVENTS_RENDERED, - CoreEvents.EVENT_CREATED, CoreEvents.EVENT_UPDATED, - CoreEvents.EVENT_DELETED].forEach(ev => sub(ev, refresh)); - } - - private onPointerDown = (e: PointerEvent) => { - const handle = (e.target as HTMLElement).closest('swp-resize-handle'); - if (!handle) return; - - const el = handle.parentElement as SwpEventEl; - this.targetEl = el; - this.isResizing = true; - this.startY = e.clientY; - - // udled start-varighed fra højde - const startHeight = el.offsetHeight; - this.startDurationMin = Math.max( - this.minDurationMin, - Math.round(startHeight * this.minutesPerPx()) - ); - - this.prevZ = (el.closest('swp-event-group') ?? el).style.zIndex; - (el.closest('swp-event-group') ?? el).style.zIndex = '1000'; - - (e.target as Element).setPointerCapture?.(e.pointerId); - this.pointerCaptured = true; - document.documentElement.classList.add('swp--resizing'); // fx user-select: none; cursor: ns-resize - e.preventDefault(); - }; - - private onPointerMove = (e: PointerEvent) => { - if (!this.isResizing || !this.targetEl) return; - - const dy = e.clientY - this.startY; - this.direction = dy >= 0 ? 'grow' : 'shrink'; - - const deltaMin = dy * this.minutesPerPx(); - const rawMin = this.startDurationMin + deltaMin; - const clamped = Math.max(this.minDurationMin, rawMin); - const snappedMin = this.roundSnap(clamped, this.direction); - - this.targetHeight = this.pxFromMinutes(snappedMin); - - if (this.animationId == null) { - this.currentHeight = this.targetEl.offsetHeight; - this.animate(); - } - }; - - private animate = () => { - if (!this.isResizing || !this.targetEl) { this.animationId = null; return; } - - const diff = this.targetHeight - this.currentHeight; - if (Math.abs(diff) > 0.5) { - this.currentHeight += diff * 0.35; - this.targetEl.updateHeight?.(this.currentHeight); - this.animationId = requestAnimationFrame(this.animate); - } else { - this.currentHeight = this.targetHeight; - this.targetEl.updateHeight?.(this.currentHeight); - this.animationId = null; - } - }; - - private onPointerUp = (e: PointerEvent) => { - if (!this.isResizing || !this.targetEl) return; - - if (this.animationId != null) cancelAnimationFrame(this.animationId); - this.animationId = null; - - // sikker slut-snap - this.targetEl.updateHeight?.(this.targetHeight - 3); // lille gap til grid-linjer - - const group = this.targetEl.closest('swp-event-group') ?? this.targetEl; - group.style.zIndex = this.prevZ ?? ''; - this.prevZ = undefined; - - this.isResizing = false; - this.targetEl = null; - - if (this.pointerCaptured) { - try { (e.target as Element).releasePointerCapture?.(e.pointerId); } catch {} - this.pointerCaptured = false; - } - document.documentElement.classList.remove('swp--resizing'); - this.refreshEventCache(); - }; -} diff --git a/src/renderers/EventRenderer.ts b/src/renderers/EventRenderer.ts index e7169f5..9dd9ea0 100644 --- a/src/renderers/EventRenderer.ts +++ b/src/renderers/EventRenderer.ts @@ -60,14 +60,22 @@ export class DateEventRenderer implements EventRendererStrategy { // Use the clone from the payload instead of creating a new one this.draggedClone = payload.draggedClone; - if (this.draggedClone) { - // Apply drag styling + if (this.draggedClone && payload.columnBounds) { + // Apply drag styling this.applyDragStyling(this.draggedClone); // Add to current column's events layer (not directly to column) - const eventsLayer = payload.columnBounds?.element.querySelector('swp-events-layer'); + const eventsLayer = payload.columnBounds.element.querySelector('swp-events-layer'); if (eventsLayer) { eventsLayer.appendChild(this.draggedClone); + + // Set initial position to prevent "jump to top" effect + // Calculate absolute Y position from original element + const originalRect = this.originalEvent.getBoundingClientRect(); + const columnRect = payload.columnBounds.boundingClientRect; + const initialTop = originalRect.top - columnRect.top; + + this.draggedClone.style.top = `${initialTop}px`; } } diff --git a/wwwroot/css/calendar-events-css.css b/wwwroot/css/calendar-events-css.css index 0d11d43..c70f962 100644 --- a/wwwroot/css/calendar-events-css.css +++ b/wwwroot/css/calendar-events-css.css @@ -6,7 +6,7 @@ swp-day-columns swp-event { border-radius: 3px; overflow: hidden; cursor: pointer; - transition: box-shadow 150ms ease, transform 150ms ease; + transition: background-color 200ms ease, box-shadow 150ms ease, transform 150ms ease; z-index: 10; left: 2px; right: 2px;