Sets up calendar package with core infrastructure
Adds core calendar package components including: - Base services for events, resources, and settings - Calendar app and orchestrator - Build and bundling configuration - IndexedDB storage setup Prepares foundational architecture for calendar functionality
This commit is contained in:
parent
12e7594f30
commit
ceb44446f0
97 changed files with 13858 additions and 1 deletions
140
packages/calendar/src/managers/EdgeScrollManager.ts
Normal file
140
packages/calendar/src/managers/EdgeScrollManager.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
/**
|
||||
* EdgeScrollManager - Auto-scroll when dragging near viewport edges
|
||||
*
|
||||
* 2-zone system:
|
||||
* - Inner zone (0-50px): Fast scroll (640 px/sec)
|
||||
* - Outer zone (50-100px): Slow scroll (140 px/sec)
|
||||
*/
|
||||
|
||||
import { IEventBus } from '../types/CalendarTypes';
|
||||
import { CoreEvents } from '../constants/CoreEvents';
|
||||
|
||||
export class EdgeScrollManager {
|
||||
private scrollableContent: HTMLElement | null = null;
|
||||
private timeGrid: HTMLElement | null = null;
|
||||
private draggedElement: HTMLElement | null = null;
|
||||
private scrollRAF: number | null = null;
|
||||
private mouseY = 0;
|
||||
private isDragging = false;
|
||||
private isScrolling = false;
|
||||
private lastTs = 0;
|
||||
private rect: DOMRect | null = null;
|
||||
private initialScrollTop = 0;
|
||||
|
||||
private readonly OUTER_ZONE = 100;
|
||||
private readonly INNER_ZONE = 50;
|
||||
private readonly SLOW_SPEED = 140;
|
||||
private readonly FAST_SPEED = 640;
|
||||
|
||||
constructor(private eventBus: IEventBus) {
|
||||
this.subscribeToEvents();
|
||||
document.addEventListener('pointermove', this.trackMouse);
|
||||
}
|
||||
|
||||
init(scrollableContent: HTMLElement): void {
|
||||
this.scrollableContent = scrollableContent;
|
||||
this.timeGrid = scrollableContent.querySelector('swp-time-grid');
|
||||
this.scrollableContent.style.scrollBehavior = 'auto';
|
||||
}
|
||||
|
||||
private trackMouse = (e: PointerEvent): void => {
|
||||
if (this.isDragging) {
|
||||
this.mouseY = e.clientY;
|
||||
}
|
||||
};
|
||||
|
||||
private subscribeToEvents(): void {
|
||||
this.eventBus.on(CoreEvents.EVENT_DRAG_START, (event: Event) => {
|
||||
const payload = (event as CustomEvent).detail;
|
||||
this.draggedElement = payload.element;
|
||||
this.startDrag();
|
||||
});
|
||||
|
||||
this.eventBus.on(CoreEvents.EVENT_DRAG_END, () => this.stopDrag());
|
||||
this.eventBus.on(CoreEvents.EVENT_DRAG_CANCEL, () => this.stopDrag());
|
||||
}
|
||||
|
||||
private startDrag(): void {
|
||||
this.isDragging = true;
|
||||
this.isScrolling = false;
|
||||
this.lastTs = 0;
|
||||
this.initialScrollTop = this.scrollableContent?.scrollTop ?? 0;
|
||||
|
||||
if (this.scrollRAF === null) {
|
||||
this.scrollRAF = requestAnimationFrame(this.scrollTick);
|
||||
}
|
||||
}
|
||||
|
||||
private stopDrag(): void {
|
||||
this.isDragging = false;
|
||||
this.setScrollingState(false);
|
||||
|
||||
if (this.scrollRAF !== null) {
|
||||
cancelAnimationFrame(this.scrollRAF);
|
||||
this.scrollRAF = null;
|
||||
}
|
||||
|
||||
this.rect = null;
|
||||
this.lastTs = 0;
|
||||
this.initialScrollTop = 0;
|
||||
}
|
||||
|
||||
private calculateVelocity(): number {
|
||||
if (!this.rect) return 0;
|
||||
|
||||
const distTop = this.mouseY - this.rect.top;
|
||||
const distBot = this.rect.bottom - this.mouseY;
|
||||
|
||||
if (distTop < this.INNER_ZONE) return -this.FAST_SPEED;
|
||||
if (distTop < this.OUTER_ZONE) return -this.SLOW_SPEED;
|
||||
if (distBot < this.INNER_ZONE) return this.FAST_SPEED;
|
||||
if (distBot < this.OUTER_ZONE) return this.SLOW_SPEED;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private isAtBoundary(velocity: number): boolean {
|
||||
if (!this.scrollableContent || !this.timeGrid || !this.draggedElement) return false;
|
||||
|
||||
const atTop = this.scrollableContent.scrollTop <= 0 && velocity < 0;
|
||||
const atBottom = velocity > 0 &&
|
||||
this.draggedElement.getBoundingClientRect().bottom >=
|
||||
this.timeGrid.getBoundingClientRect().bottom;
|
||||
|
||||
return atTop || atBottom;
|
||||
}
|
||||
|
||||
private setScrollingState(scrolling: boolean): void {
|
||||
if (this.isScrolling === scrolling) return;
|
||||
|
||||
this.isScrolling = scrolling;
|
||||
if (scrolling) {
|
||||
this.eventBus.emit(CoreEvents.EDGE_SCROLL_STARTED, {});
|
||||
} else {
|
||||
this.initialScrollTop = this.scrollableContent?.scrollTop ?? 0;
|
||||
this.eventBus.emit(CoreEvents.EDGE_SCROLL_STOPPED, {});
|
||||
}
|
||||
}
|
||||
|
||||
private scrollTick = (ts: number): void => {
|
||||
if (!this.isDragging || !this.scrollableContent) return;
|
||||
|
||||
const dt = this.lastTs ? (ts - this.lastTs) / 1000 : 0;
|
||||
this.lastTs = ts;
|
||||
this.rect ??= this.scrollableContent.getBoundingClientRect();
|
||||
|
||||
const velocity = this.calculateVelocity();
|
||||
|
||||
if (velocity !== 0 && !this.isAtBoundary(velocity)) {
|
||||
const scrollDelta = velocity * dt;
|
||||
this.scrollableContent.scrollTop += scrollDelta;
|
||||
this.rect = null;
|
||||
this.eventBus.emit(CoreEvents.EDGE_SCROLL_TICK, { scrollDelta });
|
||||
this.setScrollingState(true);
|
||||
} else {
|
||||
this.setScrollingState(false);
|
||||
}
|
||||
|
||||
this.scrollRAF = requestAnimationFrame(this.scrollTick);
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue