Convert resizer to Typescript

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2020-10-20 11:03:03 +01:00
parent 59871263ab
commit 329ded92c1
7 changed files with 141 additions and 111 deletions

View file

@ -52,6 +52,7 @@ import RoomListStore from "../../stores/room-list/RoomListStore";
import NonUrgentToastContainer from "./NonUrgentToastContainer"; import NonUrgentToastContainer from "./NonUrgentToastContainer";
import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload"; import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload";
import { IThreepidInvite } from "../../stores/ThreepidInviteStore"; import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
import { ICollapseConfig } from "../../resizer/distributors/collapse";
// We need to fetch each pinned message individually (if we don't already have it) // We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity. // so each pinned message may trigger a request. Limit the number per room for sanity.
@ -205,13 +206,8 @@ class LoggedInView extends React.Component<IProps, IState> {
}; };
_createResizer() { _createResizer() {
const classNames = {
handle: "mx_ResizeHandle",
vertical: "mx_ResizeHandle_vertical",
reverse: "mx_ResizeHandle_reverse",
};
let size; let size;
const collapseConfig = { const collapseConfig: ICollapseConfig = {
toggleSize: 260 - 50, toggleSize: 260 - 50,
onCollapsed: (collapsed) => { onCollapsed: (collapsed) => {
if (collapsed) { if (collapsed) {
@ -234,7 +230,11 @@ class LoggedInView extends React.Component<IProps, IState> {
}, },
}; };
const resizer = new Resizer(this._resizeContainer.current, CollapseDistributor, collapseConfig); const resizer = new Resizer(this._resizeContainer.current, CollapseDistributor, collapseConfig);
resizer.setClassNames(classNames); resizer.setClassNames({
handle: "mx_ResizeHandle",
vertical: "mx_ResizeHandle_vertical",
reverse: "mx_ResizeHandle_reverse",
});
return resizer; return resizer;
} }

View file

@ -16,9 +16,16 @@ limitations under the License.
import FixedDistributor from "./fixed"; import FixedDistributor from "./fixed";
import ResizeItem from "../item"; import ResizeItem from "../item";
import Resizer, {IConfig} from "../resizer";
import Sizer from "../sizer";
class CollapseItem extends ResizeItem { export interface ICollapseConfig extends IConfig {
notifyCollapsed(collapsed) { toggleSize: number;
onCollapsed?(collapsed: boolean, id: string, element: HTMLElement): void;
}
class CollapseItem extends ResizeItem<ICollapseConfig> {
notifyCollapsed(collapsed: boolean) {
const callback = this.resizer.config.onCollapsed; const callback = this.resizer.config.onCollapsed;
if (callback) { if (callback) {
callback(collapsed, this.id, this.domNode); callback(collapsed, this.id, this.domNode);
@ -26,18 +33,20 @@ class CollapseItem extends ResizeItem {
} }
} }
export default class CollapseDistributor extends FixedDistributor { export default class CollapseDistributor extends FixedDistributor<ICollapseConfig, CollapseItem> {
static createItem(resizeHandle, resizer, sizer) { static createItem(resizeHandle: HTMLDivElement, resizer: Resizer<ICollapseConfig>, sizer: Sizer) {
return new CollapseItem(resizeHandle, resizer, sizer); return new CollapseItem(resizeHandle, resizer, sizer);
} }
constructor(item, config) { private readonly toggleSize: number;
private isCollapsed = false;
constructor(item: CollapseItem) {
super(item); super(item);
this.toggleSize = config && config.toggleSize; this.toggleSize = item.resizer?.config?.toggleSize;
this.isCollapsed = false;
} }
resize(newSize) { public resize(newSize: number) {
const isCollapsedSize = newSize < this.toggleSize; const isCollapsedSize = newSize < this.toggleSize;
if (isCollapsedSize && !this.isCollapsed) { if (isCollapsedSize && !this.isCollapsed) {
this.isCollapsed = true; this.isCollapsed = true;

View file

@ -16,6 +16,7 @@ limitations under the License.
import ResizeItem from "../item"; import ResizeItem from "../item";
import Sizer from "../sizer"; import Sizer from "../sizer";
import Resizer, {IConfig} from "../resizer";
/** /**
distributors translate a moving cursor into distributors translate a moving cursor into
@ -27,29 +28,30 @@ they have two methods:
within the container bounding box. For internal use. within the container bounding box. For internal use.
This method usually ends up calling `resize` once the start offset is subtracted. This method usually ends up calling `resize` once the start offset is subtracted.
*/ */
export default class FixedDistributor { export default class FixedDistributor<C extends IConfig, I extends ResizeItem<any> = ResizeItem<C>> {
static createItem(resizeHandle, resizer, sizer) { static createItem(resizeHandle: HTMLDivElement, resizer: Resizer, sizer: Sizer): ResizeItem {
return new ResizeItem(resizeHandle, resizer, sizer); return new ResizeItem(resizeHandle, resizer, sizer);
} }
static createSizer(containerElement, vertical, reverse) { static createSizer(containerElement: HTMLElement, vertical: boolean, reverse: boolean): Sizer {
return new Sizer(containerElement, vertical, reverse); return new Sizer(containerElement, vertical, reverse);
} }
constructor(item) { private readonly beforeOffset: number;
this.item = item;
constructor(public readonly item: I) {
this.beforeOffset = item.offset(); this.beforeOffset = item.offset();
} }
resize(size) { public resize(size: number) {
this.item.setSize(size); this.item.setSize(size);
} }
resizeFromContainerOffset(offset) { public resizeFromContainerOffset(offset: number) {
this.resize(offset - this.beforeOffset); this.resize(offset - this.beforeOffset);
} }
start() {} public start() {}
finish() {} public finish() {}
} }

View file

@ -15,6 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
export FixedDistributor from "./distributors/fixed"; export {default as FixedDistributor} from "./distributors/fixed";
export CollapseDistributor from "./distributors/collapse"; export {default as CollapseDistributor} from "./distributors/collapse";
export Resizer from "./resizer"; export {default as Resizer} from "./resizer";

View file

@ -14,25 +14,30 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
export default class ResizeItem { import Resizer, {IConfig} from "./resizer";
constructor(handle, resizer, sizer) { import Sizer from "./sizer";
const id = handle.getAttribute("data-id");
const reverse = resizer.isReverseResizeHandle(handle);
const domNode = reverse ? handle.nextElementSibling : handle.previousElementSibling;
this.domNode = domNode; export default class ResizeItem<C extends IConfig = IConfig> {
this.id = id; public readonly domNode: HTMLElement;
this.reverse = reverse; protected readonly id: string;
this.resizer = resizer; protected reverse: boolean;
this.sizer = sizer;
constructor(
handle: HTMLElement,
public readonly resizer: Resizer<C>,
public readonly sizer: Sizer,
) {
this.reverse = resizer.isReverseResizeHandle(handle);
this.domNode = <HTMLElement>(this.reverse ? handle.nextElementSibling : handle.previousElementSibling);
this.id = handle.getAttribute("data-id");
} }
_copyWith(handle, resizer, sizer) { private copyWith(handle: HTMLElement, resizer: Resizer, sizer: Sizer) {
const Ctor = this.constructor; const Ctor = this.constructor as typeof ResizeItem;
return new Ctor(handle, resizer, sizer); return new Ctor(handle, resizer, sizer);
} }
_advance(forwards) { private advance(forwards: boolean) {
// opposite direction from fromResizeHandle to get back to handle // opposite direction from fromResizeHandle to get back to handle
let handle = this.reverse ? let handle = this.reverse ?
this.domNode.previousElementSibling : this.domNode.previousElementSibling :
@ -45,32 +50,32 @@ export default class ResizeItem {
} else { } else {
handle = handle.previousElementSibling; handle = handle.previousElementSibling;
} }
} while (handle && !this.resizer.isResizeHandle(handle)); } while (handle && !this.resizer.isResizeHandle(<HTMLElement>handle));
if (handle) { if (handle) {
const nextHandle = this._copyWith(handle, this.resizer, this.sizer); const nextHandle = this.copyWith(<HTMLElement>handle, this.resizer, this.sizer);
nextHandle.reverse = this.reverse; nextHandle.reverse = this.reverse;
return nextHandle; return nextHandle;
} }
} }
next() { public next() {
return this._advance(true); return this.advance(true);
} }
previous() { public previous() {
return this._advance(false); return this.advance(false);
} }
size() { public size() {
return this.sizer.getItemSize(this.domNode); return this.sizer.getItemSize(this.domNode);
} }
offset() { public offset() {
return this.sizer.getItemOffset(this.domNode); return this.sizer.getItemOffset(this.domNode);
} }
setSize(size) { public setSize(size: number) {
this.sizer.setItemSize(this.domNode, size); this.sizer.setItemSize(this.domNode, size);
const callback = this.resizer.config.onResized; const callback = this.resizer.config.onResized;
if (callback) { if (callback) {
@ -78,7 +83,7 @@ export default class ResizeItem {
} }
} }
clearSize() { public clearSize() {
this.sizer.clearItemSize(this.domNode); this.sizer.clearItemSize(this.domNode);
const callback = this.resizer.config.onResized; const callback = this.resizer.config.onResized;
if (callback) { if (callback) {
@ -86,22 +91,21 @@ export default class ResizeItem {
} }
} }
public first() {
first() {
const firstHandle = Array.from(this.domNode.parentElement.children).find(el => { const firstHandle = Array.from(this.domNode.parentElement.children).find(el => {
return this.resizer.isResizeHandle(el); return this.resizer.isResizeHandle(<HTMLElement>el);
}); });
if (firstHandle) { if (firstHandle) {
return this._copyWith(firstHandle, this.resizer, this.sizer); return this.copyWith(<HTMLElement>firstHandle, this.resizer, this.sizer);
} }
} }
last() { public last() {
const lastHandle = Array.from(this.domNode.parentElement.children).reverse().find(el => { const lastHandle = Array.from(this.domNode.parentElement.children).reverse().find(el => {
return this.resizer.isResizeHandle(el); return this.resizer.isResizeHandle(<HTMLElement>el);
}); });
if (lastHandle) { if (lastHandle) {
return this._copyWith(lastHandle, this.resizer, this.sizer); return this.copyWith(<HTMLElement>lastHandle, this.resizer, this.sizer);
} }
} }
} }

View file

@ -1,6 +1,6 @@
/* /*
Copyright 2018 New Vector Ltd Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,86 +15,101 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
/* import FixedDistributor from "./distributors/fixed";
classNames: import ResizeItem from "./item";
import Sizer from "./sizer";
interface IClassNames {
// class on resize-handle // class on resize-handle
handle: string handle?: string;
// class on resize-handle // class on resize-handle
reverse: string reverse?: string;
// class on resize-handle // class on resize-handle
vertical: string vertical?: string;
// class on container // class on container
resizing: string resizing?: string;
*/ }
export interface IConfig {
onResizeStart?(): void;
onResizeStop?(): void;
onResized?(size: number, id: string, element: HTMLElement): void;
}
export default class Resizer<C extends IConfig = IConfig> {
private classNames: IClassNames;
export default class Resizer {
// TODO move vertical/horizontal to config option/container class // TODO move vertical/horizontal to config option/container class
// as it doesn't make sense to mix them within one container/Resizer // as it doesn't make sense to mix them within one container/Resizer
constructor(container, distributorCtor, config) { constructor(
public container: HTMLElement,
private readonly distributorCtor: {
new(item: ResizeItem): FixedDistributor<C>;
createItem(resizeHandle: HTMLDivElement, resizer: Resizer, sizer: Sizer): ResizeItem;
createSizer(containerElement: HTMLElement, vertical: boolean, reverse: boolean): Sizer;
},
public readonly config?: C,
) {
if (!container) { if (!container) {
throw new Error("Resizer requires a non-null `container` arg"); throw new Error("Resizer requires a non-null `container` arg");
} }
this.container = container;
this.distributorCtor = distributorCtor;
this.config = config;
this.classNames = { this.classNames = {
handle: "resizer-handle", handle: "resizer-handle",
reverse: "resizer-reverse", reverse: "resizer-reverse",
vertical: "resizer-vertical", vertical: "resizer-vertical",
resizing: "resizer-resizing", resizing: "resizer-resizing",
}; };
this._onMouseDown = this._onMouseDown.bind(this);
} }
setClassNames(classNames) { public setClassNames(classNames: IClassNames) {
this.classNames = classNames; this.classNames = classNames;
} }
attach() { public attach() {
this.container.addEventListener("mousedown", this._onMouseDown, false); this.container.addEventListener("mousedown", this.onMouseDown, false);
} }
detach() { public detach() {
this.container.removeEventListener("mousedown", this._onMouseDown, false); this.container.removeEventListener("mousedown", this.onMouseDown, false);
} }
/** /**
Gives the distributor for a specific resize handle, as if you would have started 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. 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 @param {number} handleIndex the index of the resize handle in the container
@return {Distributor} a new distributor for the given handle @return {FixedDistributor} a new distributor for the given handle
*/ */
forHandleAt(handleIndex) { public forHandleAt(handleIndex: number): FixedDistributor<C> {
const handles = this._getResizeHandles(); const handles = this.getResizeHandles();
const handle = handles[handleIndex]; const handle = handles[handleIndex];
if (handle) { if (handle) {
const {distributor} = this._createSizerAndDistributor(handle); const {distributor} = this.createSizerAndDistributor(<HTMLDivElement>handle);
return distributor; return distributor;
} }
} }
forHandleWithId(id) { public forHandleWithId(id: string): FixedDistributor<C> {
const handles = this._getResizeHandles(); const handles = this.getResizeHandles();
const handle = handles.find((h) => h.getAttribute("data-id") === id); const handle = handles.find((h) => h.getAttribute("data-id") === id);
if (handle) { if (handle) {
const {distributor} = this._createSizerAndDistributor(handle); const {distributor} = this.createSizerAndDistributor(<HTMLDivElement>handle);
return distributor; return distributor;
} }
} }
isReverseResizeHandle(el) { public isReverseResizeHandle(el: HTMLElement) {
return el && el.classList.contains(this.classNames.reverse); return el && el.classList.contains(this.classNames.reverse);
} }
isResizeHandle(el) { public isResizeHandle(el: HTMLElement) {
return el && el.classList.contains(this.classNames.handle); return el && el.classList.contains(this.classNames.handle);
} }
_onMouseDown(event) { private onMouseDown = (event: MouseEvent) => {
// use closest in case the resize handle contains // use closest in case the resize handle contains
// child dom nodes that can be the target // child dom nodes that can be the target
const resizeHandle = event.target && event.target.closest(`.${this.classNames.handle}`); const resizeHandle = event.target && (<HTMLDivElement>event.target).closest(`.${this.classNames.handle}`);
if (!resizeHandle || resizeHandle.parentElement !== this.container) { if (!resizeHandle || resizeHandle.parentElement !== this.container) {
return; return;
} }
@ -109,7 +124,7 @@ export default class Resizer {
this.config.onResizeStart(); this.config.onResizeStart();
} }
const {sizer, distributor} = this._createSizerAndDistributor(resizeHandle); const {sizer, distributor} = this.createSizerAndDistributor(<HTMLDivElement>resizeHandle);
distributor.start(); distributor.start();
const onMouseMove = (event) => { const onMouseMove = (event) => {
@ -133,21 +148,21 @@ export default class Resizer {
body.addEventListener("mouseup", finishResize, false); body.addEventListener("mouseup", finishResize, false);
document.addEventListener("mouseleave", finishResize, false); document.addEventListener("mouseleave", finishResize, false);
body.addEventListener("mousemove", onMouseMove, false); body.addEventListener("mousemove", onMouseMove, false);
} };
_createSizerAndDistributor(resizeHandle) { private createSizerAndDistributor(resizeHandle: HTMLDivElement) {
const vertical = resizeHandle.classList.contains(this.classNames.vertical); const vertical = resizeHandle.classList.contains(this.classNames.vertical);
const reverse = this.isReverseResizeHandle(resizeHandle); const reverse = this.isReverseResizeHandle(resizeHandle);
const Distributor = this.distributorCtor; const Distributor = this.distributorCtor;
const sizer = Distributor.createSizer(this.container, vertical, reverse); const sizer = Distributor.createSizer(this.container, vertical, reverse);
const item = Distributor.createItem(resizeHandle, this, sizer); const item = Distributor.createItem(resizeHandle, this, sizer);
const distributor = new Distributor(item, this.config); const distributor = new Distributor(item);
return {sizer, distributor}; return {sizer, distributor};
} }
_getResizeHandles() { private getResizeHandles(): HTMLDivElement[] {
return Array.from(this.container.children).filter(el => { return Array.from(this.container.children).filter(el => {
return this.isResizeHandle(el); return this.isResizeHandle(<HTMLElement>el);
}); }) as HTMLDivElement[];
} }
} }

View file

@ -19,18 +19,18 @@ implements DOM/CSS operations for resizing.
The sizer determines what CSS mechanism is used for sizing items, like flexbox, ... The sizer determines what CSS mechanism is used for sizing items, like flexbox, ...
*/ */
export default class Sizer { export default class Sizer {
constructor(container, vertical, reverse) { constructor(
this.container = container; protected readonly container: HTMLElement,
this.reverse = reverse; protected readonly vertical: boolean,
this.vertical = vertical; protected readonly reverse: boolean,
} ) {}
/** /**
@param {Element} item the dom element being resized @param {Element} item the dom element being resized
@return {number} how far the edge of the item is from the edge of the container @return {number} how far the edge of the item is from the edge of the container
*/ */
getItemOffset(item) { public getItemOffset(item: HTMLElement): number {
const offset = (this.vertical ? item.offsetTop : item.offsetLeft) - this._getOffset(); const offset = (this.vertical ? item.offsetTop : item.offsetLeft) - this.getOffset();
if (this.reverse) { if (this.reverse) {
return this.getTotalSize() - (offset + this.getItemSize(item)); return this.getTotalSize() - (offset + this.getItemSize(item));
} else { } else {
@ -42,33 +42,33 @@ export default class Sizer {
@param {Element} item the dom element being resized @param {Element} item the dom element being resized
@return {number} the width/height of an item in the container @return {number} the width/height of an item in the container
*/ */
getItemSize(item) { public getItemSize(item: HTMLElement): number {
return this.vertical ? item.offsetHeight : item.offsetWidth; return this.vertical ? item.offsetHeight : item.offsetWidth;
} }
/** @return {number} the width/height of the container */ /** @return {number} the width/height of the container */
getTotalSize() { public getTotalSize(): number {
return this.vertical ? this.container.offsetHeight : this.container.offsetWidth; return this.vertical ? this.container.offsetHeight : this.container.offsetWidth;
} }
/** @return {number} container offset to offsetParent */ /** @return {number} container offset to offsetParent */
_getOffset() { private getOffset(): number {
return this.vertical ? this.container.offsetTop : this.container.offsetLeft; return this.vertical ? this.container.offsetTop : this.container.offsetLeft;
} }
/** @return {number} container offset to document */ /** @return {number} container offset to document */
_getPageOffset() { private getPageOffset(): number {
let element = this.container; let element = this.container;
let offset = 0; let offset = 0;
while (element) { while (element) {
const pos = this.vertical ? element.offsetTop : element.offsetLeft; const pos = this.vertical ? element.offsetTop : element.offsetLeft;
offset = offset + pos; offset = offset + pos;
element = element.offsetParent; element = <HTMLElement>element.offsetParent;
} }
return offset; return offset;
} }
setItemSize(item, size) { public setItemSize(item: HTMLElement, size: number) {
if (this.vertical) { if (this.vertical) {
item.style.height = `${Math.round(size)}px`; item.style.height = `${Math.round(size)}px`;
} else { } else {
@ -76,7 +76,7 @@ export default class Sizer {
} }
} }
clearItemSize(item) { public clearItemSize(item: HTMLElement) {
if (this.vertical) { if (this.vertical) {
item.style.height = null; item.style.height = null;
} else { } else {
@ -89,12 +89,12 @@ export default class Sizer {
@return {number} the distance between the cursor and the edge of the container, @return {number} the distance between the cursor and the edge of the container,
along the applicable axis (vertical or horizontal) along the applicable axis (vertical or horizontal)
*/ */
offsetFromEvent(event) { public offsetFromEvent(event: MouseEvent) {
const pos = this.vertical ? event.pageY : event.pageX; const pos = this.vertical ? event.pageY : event.pageX;
if (this.reverse) { if (this.reverse) {
return (this._getPageOffset() + this.getTotalSize()) - pos; return (this.getPageOffset() + this.getTotalSize()) - pos;
} else { } else {
return pos - this._getPageOffset(); return pos - this.getPageOffset();
} }
} }
} }