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.
This commit is contained in:
parent
1e5b3166b2
commit
e83753a7d2
3 changed files with 12 additions and 183 deletions
|
|
@ -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<SwpEventEl>('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<HTMLElement>('swp-event-group') ?? el).style.zIndex;
|
||||
(el.closest<HTMLElement>('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<HTMLElement>('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();
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue