Improves event resizing with smooth animation
Replaces the previous rough event resizing implementation with a smooth, animated approach. Uses pointer events for accurate tracking and adds a visual resize handle for better user interaction. Also refactors drag and drop to exclude resize handle.
This commit is contained in:
parent
ce0a9b19eb
commit
1e5b3166b2
4 changed files with 420 additions and 230 deletions
179
.workbench/anotherresize.txt
Normal file
179
.workbench/anotherresize.txt
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
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