kopia lustrzana https://github.com/shoelace-style/shoelace
177 wiersze
5.2 KiB
TypeScript
177 wiersze
5.2 KiB
TypeScript
import { prefersReducedMotion } from 'src/internal/animate';
|
|
import { debounce } from 'src/internal/debounce';
|
|
import { waitForEvent } from 'src/internal/event';
|
|
import type { ReactiveController, ReactiveElement } from 'lit';
|
|
|
|
interface ScrollHost extends ReactiveElement {
|
|
scrollContainer: HTMLElement;
|
|
}
|
|
|
|
/**
|
|
* A controller for handling scrolling and mouse dragging.
|
|
*/
|
|
export class ScrollController<T extends ScrollHost> implements ReactiveController {
|
|
private host: T;
|
|
private pointers = new Set();
|
|
|
|
dragging = false;
|
|
scrolling = false;
|
|
mouseDragging = false;
|
|
|
|
constructor(host: T) {
|
|
this.host = host;
|
|
|
|
host.addController(this);
|
|
|
|
this.handleScroll = this.handleScroll.bind(this);
|
|
this.handlePointerDown = this.handlePointerDown.bind(this);
|
|
this.handlePointerMove = this.handlePointerMove.bind(this);
|
|
this.handlePointerUp = this.handlePointerUp.bind(this);
|
|
this.handlePointerUp = this.handlePointerUp.bind(this);
|
|
this.handleTouchStart = this.handleTouchStart.bind(this);
|
|
this.handleTouchEnd = this.handleTouchEnd.bind(this);
|
|
}
|
|
|
|
async hostConnected() {
|
|
const host = this.host;
|
|
await host.updateComplete;
|
|
|
|
const scrollContainer = host.scrollContainer;
|
|
|
|
scrollContainer.addEventListener('scroll', this.handleScroll, { passive: true });
|
|
scrollContainer.addEventListener('pointerdown', this.handlePointerDown);
|
|
scrollContainer.addEventListener('pointerup', this.handlePointerUp);
|
|
scrollContainer.addEventListener('pointercancel', this.handlePointerUp);
|
|
scrollContainer.addEventListener('touchstart', this.handleTouchStart, { passive: true });
|
|
scrollContainer.addEventListener('touchend', this.handleTouchEnd);
|
|
}
|
|
|
|
hostDisconnected(): void {
|
|
const host = this.host;
|
|
const scrollContainer = host.scrollContainer;
|
|
|
|
scrollContainer.removeEventListener('scroll', this.handleScroll);
|
|
scrollContainer.removeEventListener('pointerdown', this.handlePointerDown);
|
|
scrollContainer.removeEventListener('pointerup', this.handlePointerUp);
|
|
scrollContainer.removeEventListener('pointercancel', this.handlePointerUp);
|
|
scrollContainer.removeEventListener('touchstart', this.handleTouchStart);
|
|
scrollContainer.removeEventListener('touchend', this.handleTouchEnd);
|
|
}
|
|
|
|
handleScroll() {
|
|
if (!this.scrolling) {
|
|
this.scrolling = true;
|
|
this.host.requestUpdate();
|
|
}
|
|
this.handleScrollEnd();
|
|
}
|
|
|
|
@debounce(100)
|
|
handleScrollEnd() {
|
|
if (!this.pointers.size) {
|
|
this.scrolling = false;
|
|
this.host.scrollContainer.dispatchEvent(
|
|
new CustomEvent('scrollend', {
|
|
bubbles: false,
|
|
cancelable: false
|
|
})
|
|
);
|
|
this.host.requestUpdate();
|
|
}
|
|
}
|
|
|
|
handlePointerDown(event: PointerEvent) {
|
|
if (event.pointerType === 'touch') {
|
|
return;
|
|
}
|
|
|
|
const scrollContainer = this.host.scrollContainer;
|
|
this.pointers.add(event.pointerId);
|
|
scrollContainer.setPointerCapture(event.pointerId);
|
|
|
|
if (this.mouseDragging && this.pointers.size === 1) {
|
|
event.preventDefault();
|
|
scrollContainer.addEventListener('pointermove', this.handlePointerMove);
|
|
}
|
|
}
|
|
|
|
handlePointerMove(event: PointerEvent) {
|
|
const host = this.host;
|
|
const scrollContainer = host.scrollContainer;
|
|
|
|
if (scrollContainer.hasPointerCapture(event.pointerId)) {
|
|
if (!this.dragging) {
|
|
this.handleDragStart();
|
|
}
|
|
|
|
this.handleDrag(event);
|
|
}
|
|
}
|
|
|
|
handlePointerUp(event: PointerEvent) {
|
|
const host = this.host;
|
|
const scrollContainer = host.scrollContainer;
|
|
|
|
this.pointers.delete(event.pointerId);
|
|
scrollContainer.releasePointerCapture(event.pointerId);
|
|
|
|
if (this.pointers.size === 0) {
|
|
this.handleDragEnd();
|
|
}
|
|
}
|
|
|
|
handleTouchEnd(event: TouchEvent) {
|
|
for (const touch of event.changedTouches) {
|
|
this.pointers.delete(touch.identifier);
|
|
}
|
|
}
|
|
|
|
handleTouchStart(event: TouchEvent) {
|
|
for (const touch of event.touches) {
|
|
this.pointers.add(touch.identifier);
|
|
}
|
|
}
|
|
|
|
handleDragStart() {
|
|
const host = this.host;
|
|
|
|
this.dragging = true;
|
|
host.scrollContainer.style.setProperty('scroll-snap-type', 'unset');
|
|
host.requestUpdate();
|
|
}
|
|
|
|
handleDrag(event: PointerEvent) {
|
|
this.host.scrollContainer.scrollBy({
|
|
left: -event.movementX,
|
|
top: -event.movementY
|
|
});
|
|
}
|
|
|
|
async handleDragEnd() {
|
|
const host = this.host;
|
|
const scrollContainer = host.scrollContainer;
|
|
|
|
scrollContainer.removeEventListener('pointermove', this.handlePointerMove);
|
|
this.dragging = false;
|
|
|
|
const startLeft = scrollContainer.scrollLeft;
|
|
const startTop = scrollContainer.scrollTop;
|
|
|
|
scrollContainer.style.removeProperty('scroll-snap-type');
|
|
const finalLeft = scrollContainer.scrollLeft;
|
|
const finalTop = scrollContainer.scrollTop;
|
|
|
|
scrollContainer.style.setProperty('scroll-snap-type', 'unset');
|
|
scrollContainer.scrollTo({ left: startLeft, top: startTop, behavior: 'auto' });
|
|
scrollContainer.scrollTo({ left: finalLeft, top: finalTop, behavior: prefersReducedMotion() ? 'auto' : 'smooth' });
|
|
|
|
if (this.scrolling) {
|
|
await waitForEvent(scrollContainer, 'scrollend');
|
|
}
|
|
|
|
scrollContainer.style.removeProperty('scroll-snap-type');
|
|
|
|
host.requestUpdate();
|
|
}
|
|
}
|