Files
ThreadNet-Web/src/resizer/resizer.ts
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

197 lines
7.5 KiB
TypeScript
Raw Normal View History

2018-10-17 11:38:25 +02:00
/*
2024-09-09 14:57:16 +01:00
Copyright 2024 New Vector Ltd.
Copyright 2018-2020 The Matrix.org Foundation C.I.C.
2018-10-17 11:38:25 +02:00
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
2024-09-09 14:57:16 +01:00
Please see LICENSE files in the repository root for full details.
2018-10-17 11:38:25 +02:00
*/
2021-06-29 13:11:58 +01:00
import { throttle } from "lodash";
2020-10-13 16:11:58 +01:00
2025-02-05 13:25:06 +00:00
import type FixedDistributor from "./distributors/fixed";
import type ResizeItem from "./item";
import type Sizer from "./sizer";
interface IClassNames {
// class on resize-handle
handle?: string;
// class on resize-handle
reverse?: string;
// class on resize-handle
vertical?: string;
// class on container
resizing?: string;
}
export interface IConfig {
onResizeStart?(): void;
onResizeStop?(): void;
onResized?(size: number | null, id: string | null, element: HTMLElement): void;
2021-08-24 13:05:46 +02:00
handler?: HTMLDivElement;
}
2023-07-07 13:37:26 +01:00
export default class Resizer<C extends IConfig, I extends ResizeItem<C> = ResizeItem<C>> {
private classNames: IClassNames;
2018-12-19 23:51:19 +01:00
// TODO move vertical/horizontal to config option/container class
// as it doesn't make sense to mix them within one container/Resizer
public constructor(
public container: HTMLElement | null,
private readonly distributorCtor: {
2023-07-07 13:37:26 +01:00
new (item: I): FixedDistributor<C, I>;
createItem(resizeHandle: HTMLDivElement, resizer: Resizer<C, I>, sizer: Sizer, container?: HTMLElement): I;
createSizer(containerElement: HTMLElement | null, vertical: boolean, reverse: boolean): Sizer;
},
public readonly config?: C,
) {
this.classNames = {
handle: "resizer-handle",
reverse: "resizer-reverse",
vertical: "resizer-vertical",
resizing: "resizer-resizing",
};
}
public setClassNames(classNames: IClassNames): void {
this.classNames = classNames;
}
public attach(): void {
2021-08-24 16:06:10 +02:00
const attachment = this?.config?.handler?.parentElement ?? this.container;
attachment?.addEventListener("mousedown", this.onMouseDown, false);
2020-10-13 16:11:58 +01:00
window.addEventListener("resize", this.onResize);
}
public detach(): void {
2021-08-24 18:22:06 +02:00
const attachment = this?.config?.handler?.parentElement ?? this.container;
attachment?.removeEventListener("mousedown", this.onMouseDown, false);
2020-10-13 16:11:58 +01:00
window.removeEventListener("resize", this.onResize);
}
2018-10-17 13:45:56 +02:00
/**
Gives the distributor for a specific resize handle, as if you would have started
to drag that handle. Can be used to manipulate the size of an item programmatically.
@param {number} handleIndex the index of the resize handle in the container
2020-10-20 11:03:03 +01:00
@return {FixedDistributor} a new distributor for the given handle
2018-10-17 13:45:56 +02:00
*/
2023-07-07 13:37:26 +01:00
public forHandleAt(handleIndex: number): FixedDistributor<C, I> | undefined {
const handles = this.getResizeHandles();
const handle = handles[handleIndex];
if (handle) {
2021-06-29 13:11:58 +01:00
const { distributor } = this.createSizerAndDistributor(<HTMLDivElement>handle);
return distributor;
}
}
2023-07-07 13:37:26 +01:00
public forHandleWithId(id: string): FixedDistributor<C, I> | undefined {
const handles = this.getResizeHandles();
const handle = handles.find((h) => h.getAttribute("data-id") === id);
if (handle) {
2021-06-29 13:11:58 +01:00
const { distributor } = this.createSizerAndDistributor(<HTMLDivElement>handle);
return distributor;
}
}
public isReverseResizeHandle(el: HTMLElement): boolean {
return el.classList.contains(this.classNames.reverse!);
2018-12-19 23:51:19 +01:00
}
public isResizeHandle(el: HTMLElement): boolean {
return el.classList.contains(this.classNames.handle!);
}
private onMouseDown = (event: MouseEvent): void => {
const LEFT_MOUSE_BUTTON = 0;
if (event.button !== LEFT_MOUSE_BUTTON) {
return;
}
// use closest in case the resize handle contains
// child dom nodes that can be the target
2020-10-20 11:03:03 +01:00
const resizeHandle = event.target && (<HTMLDivElement>event.target).closest(`.${this.classNames.handle}`);
2021-08-24 13:05:46 +02:00
const hasHandler = this?.config?.handler;
// prevent that stacked resizer's are both activated with one mouse event
// (this is possible because the mouse events are connected to the containers not the handles)
if (
!resizeHandle || // if no resizeHandle exist / mouse event hit the container not the handle
(!hasHandler && resizeHandle.parentElement !== this.container) || // no handler from config -> check if the containers match
(hasHandler && resizeHandle !== hasHandler)
) {
// handler from config -> check if the handlers match
return;
}
// prevent starting a drag operation
event.preventDefault();
// mark as currently resizing
if (this.classNames.resizing) {
2021-09-22 08:29:04 +02:00
this.container?.classList?.add(this.classNames.resizing);
}
this.config?.onResizeStart?.();
2021-06-29 13:11:58 +01:00
const { sizer, distributor } = this.createSizerAndDistributor(<HTMLDivElement>resizeHandle);
distributor.start();
const onMouseMove = (event: MouseEvent): void => {
const offset = sizer.offsetFromEvent(event);
distributor.resizeFromContainerOffset(offset);
};
const body = document.body;
const finishResize = (): void => {
if (this.classNames.resizing) {
2021-09-22 08:29:04 +02:00
this.container?.classList?.remove(this.classNames.resizing);
}
2020-10-13 12:28:23 +01:00
distributor.finish();
this.config?.onResizeStop?.();
body.removeEventListener("mouseup", finishResize, false);
document.removeEventListener("mouseleave", finishResize, false);
body.removeEventListener("mousemove", onMouseMove, false);
};
body.addEventListener("mouseup", finishResize, false);
document.addEventListener("mouseleave", finishResize, false);
body.addEventListener("mousemove", onMouseMove, false);
};
2020-10-13 16:11:58 +01:00
private onResize = throttle(
() => {
2020-10-15 11:14:48 +01:00
const distributors = this.getDistributors();
2020-10-13 16:11:58 +01:00
// relax all items if they had any overconstrained flexboxes
distributors.forEach((d) => d.start());
distributors.forEach((d) => d.finish());
2021-06-29 13:11:58 +01:00
},
100,
{ trailing: true, leading: true },
);
2020-10-13 16:11:58 +01:00
2023-07-07 13:37:26 +01:00
public getDistributors = (): FixedDistributor<C, I>[] => {
2020-10-15 11:14:48 +01:00
return this.getResizeHandles().map((handle) => {
2021-06-29 13:11:58 +01:00
const { distributor } = this.createSizerAndDistributor(<HTMLDivElement>handle);
2020-10-15 11:14:48 +01:00
return distributor;
});
};
private createSizerAndDistributor(resizeHandle: HTMLDivElement): {
sizer: Sizer;
2023-07-07 13:37:26 +01:00
distributor: FixedDistributor<C, I>;
} {
const vertical = resizeHandle.classList.contains(this.classNames.vertical!);
2018-12-19 23:51:19 +01:00
const reverse = this.isReverseResizeHandle(resizeHandle);
2019-01-14 20:24:54 +01:00
const Distributor = this.distributorCtor;
const useItemContainer = this.config?.handler ? this.container : undefined;
2019-01-14 20:24:54 +01:00
const sizer = Distributor.createSizer(this.container, vertical, reverse);
const item = Distributor.createItem(resizeHandle, this, sizer, useItemContainer ?? undefined);
const distributor = new Distributor(item);
2021-06-29 13:11:58 +01:00
return { sizer, distributor };
}
private getResizeHandles(): HTMLElement[] {
2021-08-24 13:05:46 +02:00
if (this?.config?.handler) {
return [this.config.handler];
}
2021-09-22 08:29:04 +02:00
if (!this.container?.children) return [];
return Array.from(this.container.querySelectorAll(`.${this.classNames.handle}`));
}
}