2022-03-17 23:29:55 +08:00

217 lines
5.9 KiB
TypeScript

import { Keys } from '@editor/type';
interface ScrollViewerOptions {
container: HTMLDivElement;
target: HTMLDivElement;
zoom: number;
}
export class ScrollViewer {
private enter = false;
private targetEnter = false;
private keydown = false;
private container: HTMLDivElement;
private target: HTMLDivElement;
private zoom = 1;
private scrollLeft = 0;
private scrollTop = 0;
private x = 0;
private y = 0;
private resizeObserver = new ResizeObserver((entries) => {
for (const { contentRect } of entries) {
const { width, height } = contentRect;
const targetRect = this.target.getBoundingClientRect();
const targetWidth = targetRect.width * this.zoom;
const targetMarginTop = Number(this.target.style.marginTop) || 0;
const targetHeight = (targetRect.height + targetMarginTop) * this.zoom;
if (targetWidth < width) {
(this.target as any)._left = 0;
}
if (targetHeight < height) {
(this.target as any)._top = 0;
}
this.scroll();
}
});
constructor(options: ScrollViewerOptions) {
this.container = options.container;
this.target = options.target;
this.zoom = options.zoom;
globalThis.addEventListener('keydown', this.keydownHandler);
globalThis.addEventListener('keyup', this.keyupHandler);
this.container.addEventListener('mouseenter', this.mouseEnterHandler);
this.container.addEventListener('mouseleave', this.mouseLeaveHandler);
this.target.addEventListener('mouseenter', this.targetMouseEnterHandler);
this.target.addEventListener('mouseleave', this.targetMouseLeaveHandler);
this.container.addEventListener('wheel', this.wheelHandler);
this.resizeObserver.observe(this.container);
}
public destroy() {
this.resizeObserver.disconnect();
this.container.removeEventListener('mouseenter', this.mouseEnterHandler);
this.container.removeEventListener('mouseleave', this.mouseLeaveHandler);
this.target.removeEventListener('mouseenter', this.targetMouseEnterHandler);
this.target.removeEventListener('mouseleave', this.targetMouseLeaveHandler);
globalThis.removeEventListener('keydown', this.keydownHandler);
globalThis.removeEventListener('keyup', this.keyupHandler);
}
public setZoom(zoom: number) {
this.zoom = zoom;
}
private scroll() {
const scrollLeft = (this.target as any)._left;
const scrollTop = (this.target as any)._top;
this.target.style.transform = `translate(${scrollLeft}px, ${scrollTop}px)`;
}
private removeHandler() {
this.target.style.cursor = '';
this.target.removeEventListener('mousedown', this.mousedownHandler);
document.removeEventListener('mousemove', this.mousemoveHandler);
document.removeEventListener('mouseup', this.mouseupHandler);
}
private wheelHandler = (event: WheelEvent) => {
if (this.targetEnter) return;
const { deltaX, deltaY, currentTarget } = event;
if (currentTarget !== this.container) return;
this.setScrollOffset(deltaX, deltaY);
this.scroll();
this.scrollLeft = (this.target as any)._left;
this.scrollTop = (this.target as any)._top;
};
private mouseEnterHandler = () => {
this.enter = true;
};
private mouseLeaveHandler = () => {
this.enter = false;
};
private targetMouseEnterHandler = () => {
this.targetEnter = true;
};
private targetMouseLeaveHandler = () => {
this.targetEnter = false;
};
private mousedownHandler = (event: MouseEvent) => {
if (!this.keydown) return;
event.stopImmediatePropagation();
event.stopPropagation();
this.target.style.cursor = 'grabbing';
this.x = event.clientX;
this.y = event.clientY;
document.addEventListener('mousemove', this.mousemoveHandler);
document.addEventListener('mouseup', this.mouseupHandler);
};
private mouseupHandler = () => {
this.x = 0;
this.y = 0;
this.scrollLeft = (this.target as any)._left;
this.scrollTop = (this.target as any)._top;
this.removeHandler();
};
private mousemoveHandler = (event: MouseEvent) => {
event.stopImmediatePropagation();
event.stopPropagation();
const deltaX = event.clientX - this.x;
const deltaY = event.clientY - this.y;
this.setScrollOffset(deltaX, deltaY);
this.scroll();
};
private keydownHandler = (event: KeyboardEvent) => {
if (event.code === Keys.ESCAPE && this.enter) {
event.preventDefault();
event.stopImmediatePropagation();
event.stopPropagation();
}
if (event.code !== Keys.ESCAPE || !this.enter || this.keydown) {
return;
}
this.keydown = true;
this.target.style.cursor = 'grab';
this.container.addEventListener('mousedown', this.mousedownHandler);
};
private keyupHandler = (event: KeyboardEvent) => {
if (event.code !== Keys.ESCAPE || !this.keydown) {
return;
}
event.preventDefault();
event.stopImmediatePropagation();
event.stopPropagation();
this.keydown = false;
event.preventDefault();
this.removeHandler();
};
private setScrollOffset(deltaX: number, deltaY: number) {
const { width, height } = this.container.getBoundingClientRect();
const targetRect = this.target.getBoundingClientRect();
const targetWidth = targetRect.width * this.zoom;
const targetHeight = targetRect.height * this.zoom;
let y = 0;
if (targetHeight > height) {
if (deltaY > 0) {
y = this.scrollTop + Math.min(targetHeight - height - this.scrollTop, deltaY);
} else {
y = this.scrollTop + Math.max(-(targetHeight - height + this.scrollTop), deltaY);
}
}
let x = 0;
if (targetWidth > width) {
if (deltaX > 0) {
x = this.scrollLeft + Math.min(targetWidth - width - this.scrollLeft, deltaX);
} else {
x = this.scrollLeft + Math.max(-(targetWidth - width + this.scrollLeft), deltaX);
}
}
(this.target as any)._left = x;
(this.target as any)._top = y;
}
}