Adds edge scroll functionality for drag interactions
Implements EdgeScrollManager to enable automatic scrolling during drag operations Introduces new scroll management system that: - Detects mouse proximity to container edges - Provides variable scroll speed based on mouse position - Compensates dragged elements during scrolling Enhances drag-and-drop user experience with smooth scrolling
This commit is contained in:
parent
8b95f2735f
commit
10d8a444d8
5 changed files with 219 additions and 2 deletions
|
|
@ -52,6 +52,7 @@ import { ResourceScheduleService } from './storage/schedules/ResourceScheduleSer
|
||||||
|
|
||||||
// Managers
|
// Managers
|
||||||
import { DragDropManager } from './managers/DragDropManager';
|
import { DragDropManager } from './managers/DragDropManager';
|
||||||
|
import { EdgeScrollManager } from './managers/EdgeScrollManager';
|
||||||
|
|
||||||
const defaultTimeFormatConfig: ITimeFormatConfig = {
|
const defaultTimeFormatConfig: ITimeFormatConfig = {
|
||||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
|
|
@ -149,6 +150,7 @@ export function createV2Container(): Container {
|
||||||
builder.registerType(ScrollManager).as<ScrollManager>();
|
builder.registerType(ScrollManager).as<ScrollManager>();
|
||||||
builder.registerType(HeaderDrawerManager).as<HeaderDrawerManager>();
|
builder.registerType(HeaderDrawerManager).as<HeaderDrawerManager>();
|
||||||
builder.registerType(DragDropManager).as<DragDropManager>();
|
builder.registerType(DragDropManager).as<DragDropManager>();
|
||||||
|
builder.registerType(EdgeScrollManager).as<EdgeScrollManager>();
|
||||||
|
|
||||||
// Demo app
|
// Demo app
|
||||||
builder.registerType(DemoApp).as<DemoApp>();
|
builder.registerType(DemoApp).as<DemoApp>();
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,11 @@ export const CoreEvents = {
|
||||||
EVENT_DRAG_CANCEL: 'event:drag-cancel',
|
EVENT_DRAG_CANCEL: 'event:drag-cancel',
|
||||||
EVENT_DRAG_COLUMN_CHANGE: 'event:drag-column-change',
|
EVENT_DRAG_COLUMN_CHANGE: 'event:drag-column-change',
|
||||||
|
|
||||||
|
// Edge scroll
|
||||||
|
EDGE_SCROLL_TICK: 'edge-scroll:tick',
|
||||||
|
EDGE_SCROLL_STARTED: 'edge-scroll:started',
|
||||||
|
EDGE_SCROLL_STOPPED: 'edge-scroll:stopped',
|
||||||
|
|
||||||
// System events
|
// System events
|
||||||
ERROR: 'system:error',
|
ERROR: 'system:error',
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { IndexedDBContext } from '../storage/IndexedDBContext';
|
||||||
import { DataSeeder } from '../workers/DataSeeder';
|
import { DataSeeder } from '../workers/DataSeeder';
|
||||||
import { ViewConfig } from '../core/ViewConfig';
|
import { ViewConfig } from '../core/ViewConfig';
|
||||||
import { DragDropManager } from '../managers/DragDropManager';
|
import { DragDropManager } from '../managers/DragDropManager';
|
||||||
|
import { EdgeScrollManager } from '../managers/EdgeScrollManager';
|
||||||
|
|
||||||
export class DemoApp {
|
export class DemoApp {
|
||||||
private animator!: NavigationAnimator;
|
private animator!: NavigationAnimator;
|
||||||
|
|
@ -23,7 +24,8 @@ export class DemoApp {
|
||||||
private headerDrawerManager: HeaderDrawerManager,
|
private headerDrawerManager: HeaderDrawerManager,
|
||||||
private indexedDBContext: IndexedDBContext,
|
private indexedDBContext: IndexedDBContext,
|
||||||
private dataSeeder: DataSeeder,
|
private dataSeeder: DataSeeder,
|
||||||
private dragDropManager: DragDropManager
|
private dragDropManager: DragDropManager,
|
||||||
|
private edgeScrollManager: EdgeScrollManager
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
|
|
@ -55,6 +57,10 @@ export class DemoApp {
|
||||||
// Init drag-drop
|
// Init drag-drop
|
||||||
this.dragDropManager.init(this.container);
|
this.dragDropManager.init(this.container);
|
||||||
|
|
||||||
|
// Init edge scroll
|
||||||
|
const scrollableContent = this.container.querySelector('swp-scrollable-content') as HTMLElement;
|
||||||
|
this.edgeScrollManager.init(scrollableContent);
|
||||||
|
|
||||||
// Setup event handlers
|
// Setup event handlers
|
||||||
this.setupNavigation();
|
this.setupNavigation();
|
||||||
this.setupDrawerToggle();
|
this.setupDrawerToggle();
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,22 @@ export class DragDropManager {
|
||||||
constructor(
|
constructor(
|
||||||
private eventBus: IEventBus,
|
private eventBus: IEventBus,
|
||||||
private gridConfig: IGridConfig
|
private gridConfig: IGridConfig
|
||||||
) {}
|
) {
|
||||||
|
this.setupScrollListener();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupScrollListener(): void {
|
||||||
|
this.eventBus.on(CoreEvents.EDGE_SCROLL_TICK, (e) => {
|
||||||
|
if (!this.dragState) return;
|
||||||
|
const { scrollDelta } = (e as CustomEvent<{ scrollDelta: number }>).detail;
|
||||||
|
|
||||||
|
// Element skal flytte med scroll for at forblive under musen
|
||||||
|
// (elementets top er relativ til kolonnen, som scroller med viewport)
|
||||||
|
this.dragState.targetY += scrollDelta;
|
||||||
|
this.dragState.currentY += scrollDelta;
|
||||||
|
this.dragState.element.style.top = `${this.dragState.currentY}px`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize drag-drop on a container element
|
* Initialize drag-drop on a container element
|
||||||
|
|
@ -165,6 +180,9 @@ export class DragDropManager {
|
||||||
const columnRect = columnElement.getBoundingClientRect();
|
const columnRect = columnElement.getBoundingClientRect();
|
||||||
const targetY = e.clientY - columnRect.top - mouseOffset.y;
|
const targetY = e.clientY - columnRect.top - mouseOffset.y;
|
||||||
|
|
||||||
|
// Reset scroll compensation
|
||||||
|
this.scrollDeltaY = 0;
|
||||||
|
|
||||||
// Initialize drag state
|
// Initialize drag state
|
||||||
this.dragState = {
|
this.dragState = {
|
||||||
eventId,
|
eventId,
|
||||||
|
|
|
||||||
186
src/v2/managers/EdgeScrollManager.ts
Normal file
186
src/v2/managers/EdgeScrollManager.ts
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
/**
|
||||||
|
* EdgeScrollManager - Auto-scroll when dragging near edges
|
||||||
|
* Uses time-based scrolling with 2-zone system for variable speed
|
||||||
|
*
|
||||||
|
* Copied from V1 with minor adaptations for V2 event names.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 scrollListener: ((e: Event) => void) | null = null;
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
private readonly OUTER_ZONE = 100;
|
||||||
|
private readonly INNER_ZONE = 50;
|
||||||
|
private readonly SLOW_SPEED_PXS = 140;
|
||||||
|
private readonly FAST_SPEED_PXS = 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');
|
||||||
|
|
||||||
|
// Disable smooth scroll for instant auto-scroll
|
||||||
|
this.scrollableContent.style.scrollBehavior = 'auto';
|
||||||
|
|
||||||
|
// Add scroll listener to detect actual scrolling
|
||||||
|
this.scrollListener = this.handleScroll.bind(this);
|
||||||
|
this.scrollableContent.addEventListener('scroll', this.scrollListener, { passive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = performance.now();
|
||||||
|
|
||||||
|
if (this.scrollableContent) {
|
||||||
|
this.initialScrollTop = this.scrollableContent.scrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.scrollRAF === null) {
|
||||||
|
this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopDrag(): void {
|
||||||
|
this.isDragging = false;
|
||||||
|
|
||||||
|
if (this.isScrolling) {
|
||||||
|
this.isScrolling = false;
|
||||||
|
this.eventBus.emit(CoreEvents.EDGE_SCROLL_STOPPED, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.scrollRAF !== null) {
|
||||||
|
cancelAnimationFrame(this.scrollRAF);
|
||||||
|
this.scrollRAF = null;
|
||||||
|
}
|
||||||
|
this.rect = null;
|
||||||
|
this.lastTs = 0;
|
||||||
|
this.initialScrollTop = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleScroll(): void {
|
||||||
|
if (!this.isDragging || !this.scrollableContent) return;
|
||||||
|
|
||||||
|
const currentScrollTop = this.scrollableContent.scrollTop;
|
||||||
|
const scrollDelta = Math.abs(currentScrollTop - this.initialScrollTop);
|
||||||
|
|
||||||
|
if (scrollDelta > 1 && !this.isScrolling) {
|
||||||
|
this.isScrolling = true;
|
||||||
|
this.eventBus.emit(CoreEvents.EDGE_SCROLL_STARTED, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private scrollTick(ts: number): void {
|
||||||
|
const dt = this.lastTs ? (ts - this.lastTs) / 1000 : 0;
|
||||||
|
this.lastTs = ts;
|
||||||
|
|
||||||
|
if (!this.scrollableContent) {
|
||||||
|
this.stopDrag();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache rect for performance
|
||||||
|
if (!this.rect) {
|
||||||
|
this.rect = this.scrollableContent.getBoundingClientRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
let vy = 0;
|
||||||
|
if (this.isDragging) {
|
||||||
|
const distTop = this.mouseY - this.rect.top;
|
||||||
|
const distBot = this.rect.bottom - this.mouseY;
|
||||||
|
|
||||||
|
// Check top edge
|
||||||
|
if (distTop < this.INNER_ZONE) {
|
||||||
|
vy = -this.FAST_SPEED_PXS;
|
||||||
|
} else if (distTop < this.OUTER_ZONE) {
|
||||||
|
vy = -this.SLOW_SPEED_PXS;
|
||||||
|
}
|
||||||
|
// Check bottom edge
|
||||||
|
else if (distBot < this.INNER_ZONE) {
|
||||||
|
vy = this.FAST_SPEED_PXS;
|
||||||
|
} else if (distBot < this.OUTER_ZONE) {
|
||||||
|
vy = this.SLOW_SPEED_PXS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vy !== 0 && this.isDragging && this.timeGrid && this.draggedElement) {
|
||||||
|
const currentScrollTop = this.scrollableContent.scrollTop;
|
||||||
|
const cloneRect = this.draggedElement.getBoundingClientRect();
|
||||||
|
const cloneBottom = cloneRect.bottom;
|
||||||
|
const timeGridRect = this.timeGrid.getBoundingClientRect();
|
||||||
|
const timeGridBottom = timeGridRect.bottom;
|
||||||
|
|
||||||
|
// Check boundaries
|
||||||
|
const atTop = currentScrollTop <= 0 && vy < 0;
|
||||||
|
const atBottom = (cloneBottom >= timeGridBottom) && vy > 0;
|
||||||
|
|
||||||
|
if (atTop || atBottom) {
|
||||||
|
if (this.isScrolling) {
|
||||||
|
this.isScrolling = false;
|
||||||
|
this.initialScrollTop = this.scrollableContent.scrollTop;
|
||||||
|
this.eventBus.emit(CoreEvents.EDGE_SCROLL_STOPPED, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isDragging) {
|
||||||
|
this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Apply scroll
|
||||||
|
const scrollDelta = vy * dt;
|
||||||
|
this.scrollableContent.scrollTop += scrollDelta;
|
||||||
|
this.rect = null;
|
||||||
|
|
||||||
|
// Emit tick for DragDropManager compensation
|
||||||
|
this.eventBus.emit(CoreEvents.EDGE_SCROLL_TICK, { scrollDelta });
|
||||||
|
|
||||||
|
this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.isScrolling) {
|
||||||
|
this.isScrolling = false;
|
||||||
|
this.initialScrollTop = this.scrollableContent.scrollTop;
|
||||||
|
this.eventBus.emit(CoreEvents.EDGE_SCROLL_STOPPED, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isDragging) {
|
||||||
|
this.scrollRAF = requestAnimationFrame((ts) => this.scrollTick(ts));
|
||||||
|
} else {
|
||||||
|
this.stopDrag();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue