Convert Resizer to Typescript and create a Percentage based sizer

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2020-10-12 16:47:04 +01:00
parent c1fef5a941
commit 340e79179e
7 changed files with 205 additions and 92 deletions

View File

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

View File

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

View File

@ -0,0 +1,48 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Sizer from "../sizer";
import FixedDistributor from "./fixed";
import {IConfig} from "../resizer";
class PercentageSizer extends Sizer {
public start(item: HTMLElement) {
if (this.vertical) {
item.style.minHeight = null;
} else {
item.style.minWidth = null;
}
}
public finish(item: HTMLElement) {
const parent = item.offsetParent as HTMLElement;
if (this.vertical) {
const p = ((item.offsetHeight / parent.offsetHeight) * 100).toFixed(2) + "%";
item.style.minHeight = p;
item.style.height = p;
} else {
const p = ((item.offsetWidth / parent.offsetWidth) * 100).toFixed(2) + "%";
item.style.minWidth = p;
item.style.width = p;
}
}
}
export default class PercentageDistributor extends FixedDistributor<IConfig> {
static createSizer(containerElement: HTMLElement, vertical: boolean, reverse: boolean) {
return new PercentageSizer(containerElement, vertical, reverse);
}
}

View File

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

View File

@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,63 +15,81 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export default class ResizeItem {
constructor(handle, resizer, sizer) {
import Sizer from "./sizer";
import Resizer, {IConfig} from "./resizer";
export default class ResizeItem<C extends IConfig = IConfig> {
protected readonly domNode: HTMLElement;
protected readonly id: string;
protected reverse: boolean;
constructor(
handle: HTMLElement,
public readonly resizer: Resizer<C>,
protected readonly sizer: Sizer,
) {
const id = handle.getAttribute("data-id");
const reverse = resizer.isReverseResizeHandle(handle);
const domNode = reverse ? handle.nextElementSibling : handle.previousElementSibling;
this.domNode = domNode;
this.domNode = <HTMLElement>(reverse ? handle.nextElementSibling : handle.previousElementSibling);
this.id = id;
this.reverse = reverse;
this.resizer = resizer;
this.sizer = sizer;
}
_copyWith(handle, resizer, sizer) {
const Ctor = this.constructor;
return new Ctor(handle, resizer, sizer);
private copyWith(handle: Element, resizer: Resizer, sizer: Sizer) {
const Ctor = this.constructor as typeof ResizeItem;
return new Ctor(<HTMLElement>handle, resizer, sizer);
}
_advance(forwards) {
private advance(forwards: boolean) {
// opposite direction from fromResizeHandle to get back to handle
let handle = this.reverse ?
let handle = <HTMLElement>(this.reverse ?
this.domNode.previousElementSibling :
this.domNode.nextElementSibling;
this.domNode.nextElementSibling);
const moveNext = forwards !== this.reverse; // xor
// iterate at least once to avoid infinite loop
do {
if (moveNext) {
handle = handle.nextElementSibling;
handle = <HTMLElement>handle.nextElementSibling;
} else {
handle = handle.previousElementSibling;
handle = <HTMLElement>handle.previousElementSibling;
}
} while (handle && !this.resizer.isResizeHandle(handle));
if (handle) {
const nextHandle = this._copyWith(handle, this.resizer, this.sizer);
const nextHandle = this.copyWith(handle, this.resizer, this.sizer);
nextHandle.reverse = this.reverse;
return nextHandle;
}
}
next() {
return this._advance(true);
public next() {
return this.advance(true);
}
previous() {
return this._advance(false);
public previous() {
return this.advance(false);
}
size() {
public size() {
return this.sizer.getItemSize(this.domNode);
}
offset() {
public offset() {
return this.sizer.getItemOffset(this.domNode);
}
setSize(size) {
public start() {
this.sizer.start(this.domNode);
}
public finish() {
this.sizer.finish(this.domNode);
}
public setSize(size: number) {
this.sizer.setItemSize(this.domNode, size);
const callback = this.resizer.config.onResized;
if (callback) {
@ -78,7 +97,7 @@ export default class ResizeItem {
}
}
clearSize() {
public clearSize() {
this.sizer.clearItemSize(this.domNode);
const callback = this.resizer.config.onResized;
if (callback) {
@ -86,22 +105,21 @@ export default class ResizeItem {
}
}
first() {
public first() {
const firstHandle = Array.from(this.domNode.parentElement.children).find(el => {
return this.resizer.isResizeHandle(el);
return this.resizer.isResizeHandle(<HTMLElement>el);
});
if (firstHandle) {
return this._copyWith(firstHandle, this.resizer, this.sizer);
return this.copyWith(firstHandle, this.resizer, this.sizer);
}
}
last() {
public last() {
const lastHandle = Array.from(this.domNode.parentElement.children).reverse().find(el => {
return this.resizer.isResizeHandle(el);
return this.resizer.isResizeHandle(<HTMLElement>el);
});
if (lastHandle) {
return this._copyWith(lastHandle, this.resizer, this.sizer);
return this.copyWith(lastHandle, this.resizer, this.sizer);
}
}
}

View File

@ -1,6 +1,6 @@
/*
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");
you may not use this file except in compliance with the License.
@ -27,36 +27,59 @@ classNames:
resizing: string
*/
import FixedDistributor from "./distributors/fixed";
import Sizer from "./sizer";
import ResizeItem from "./item";
interface IClassNames {
handle?: string;
reverse?: string;
vertical?: 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
// as it doesn't make sense to mix them within one container/Resizer
constructor(container, distributorCtor, config) {
constructor(
private readonly container: HTMLElement,
private readonly distributorCtor: {
new(item: ResizeItem): FixedDistributor<C, any>;
createItem(resizeHandle: HTMLDivElement, resizer: Resizer, sizer: Sizer): ResizeItem;
createSizer(containerElement: HTMLElement, vertical: boolean, reverse: boolean): Sizer;
},
public readonly config?: C,
) {
if (!container) {
throw new Error("Resizer requires a non-null `container` arg");
}
this.container = container;
this.distributorCtor = distributorCtor;
this.config = config;
this.classNames = {
handle: "resizer-handle",
reverse: "resizer-reverse",
vertical: "resizer-vertical",
resizing: "resizer-resizing",
};
this._onMouseDown = this._onMouseDown.bind(this);
}
setClassNames(classNames) {
public setClassNames(classNames: IClassNames) {
this.classNames = classNames;
}
attach() {
this.container.addEventListener("mousedown", this._onMouseDown, false);
public attach() {
this.container.addEventListener("mousedown", this.onMouseDown, false);
}
detach() {
this.container.removeEventListener("mousedown", this._onMouseDown, false);
public detach() {
this.container.removeEventListener("mousedown", this.onMouseDown, false);
}
/**
@ -65,36 +88,36 @@ export default class Resizer {
@param {number} handleIndex the index of the resize handle in the container
@return {Distributor} a new distributor for the given handle
*/
forHandleAt(handleIndex) {
const handles = this._getResizeHandles();
public forHandleAt(handleIndex: number): FixedDistributor<C> {
const handles = this.getResizeHandles();
const handle = handles[handleIndex];
if (handle) {
const {distributor} = this._createSizerAndDistributor(handle);
const {distributor} = this.createSizerAndDistributor(<HTMLDivElement>handle);
return distributor;
}
}
forHandleWithId(id) {
const handles = this._getResizeHandles();
public forHandleWithId(id: string): FixedDistributor<C> {
const handles = this.getResizeHandles();
const handle = handles.find((h) => h.getAttribute("data-id") === id);
if (handle) {
const {distributor} = this._createSizerAndDistributor(handle);
const {distributor} = this.createSizerAndDistributor(<HTMLDivElement>handle);
return distributor;
}
}
isReverseResizeHandle(el) {
public isReverseResizeHandle(el: HTMLElement): boolean {
return el && el.classList.contains(this.classNames.reverse);
}
isResizeHandle(el) {
public isResizeHandle(el: HTMLElement): boolean {
return el && el.classList.contains(this.classNames.handle);
}
_onMouseDown(event) {
private onMouseDown = (event: MouseEvent) => {
// use closest in case the resize handle contains
// child dom nodes that can be the target
const resizeHandle = event.target && event.target.closest(`.${this.classNames.handle}`);
const resizeHandle = event.target && (<HTMLElement>event.target).closest(`.${this.classNames.handle}`);
if (!resizeHandle || resizeHandle.parentElement !== this.container) {
return;
}
@ -109,7 +132,7 @@ export default class Resizer {
this.config.onResizeStart();
}
const {sizer, distributor} = this._createSizerAndDistributor(resizeHandle);
const {sizer, distributor} = this.createSizerAndDistributor(<HTMLDivElement>resizeHandle);
distributor.start();
const onMouseMove = (event) => {
@ -133,21 +156,23 @@ export default class Resizer {
body.addEventListener("mouseup", finishResize, false);
document.addEventListener("mouseleave", finishResize, false);
body.addEventListener("mousemove", onMouseMove, false);
}
};
_createSizerAndDistributor(resizeHandle) {
private createSizerAndDistributor(
resizeHandle: HTMLDivElement,
): {sizer: Sizer, distributor: FixedDistributor<any>} {
const vertical = resizeHandle.classList.contains(this.classNames.vertical);
const reverse = this.isReverseResizeHandle(resizeHandle);
const Distributor = this.distributorCtor;
const sizer = Distributor.createSizer(this.container, vertical, reverse);
const item = Distributor.createItem(resizeHandle, this, sizer);
const distributor = new Distributor(item, this.config);
const distributor = new Distributor(item);
return {sizer, distributor};
}
_getResizeHandles() {
private getResizeHandles() {
return Array.from(this.container.children).filter(el => {
return this.isResizeHandle(el);
});
return this.isResizeHandle(<HTMLElement>el);
}) as HTMLElement[];
}
}

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, ...
*/
export default class Sizer {
constructor(container, vertical, reverse) {
this.container = container;
this.reverse = reverse;
this.vertical = vertical;
}
constructor(
protected readonly container: HTMLElement,
protected readonly vertical: boolean,
protected readonly reverse: boolean,
) {}
/**
@param {Element} item the dom element being resized
@return {number} how far the edge of the item is from the edge of the container
*/
getItemOffset(item) {
const offset = (this.vertical ? item.offsetTop : item.offsetLeft) - this._getOffset();
public getItemOffset(item: HTMLElement): number {
const offset = (this.vertical ? item.offsetTop : item.offsetLeft) - this.getOffset();
if (this.reverse) {
return this.getTotalSize() - (offset + this.getItemSize(item));
} else {
@ -42,33 +42,33 @@ export default class Sizer {
@param {Element} item the dom element being resized
@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 {number} the width/height of the container */
getTotalSize() {
public getTotalSize(): number {
return this.vertical ? this.container.offsetHeight : this.container.offsetWidth;
}
/** @return {number} container offset to offsetParent */
_getOffset() {
private getOffset(): number {
return this.vertical ? this.container.offsetTop : this.container.offsetLeft;
}
/** @return {number} container offset to document */
_getPageOffset() {
private getPageOffset() {
let element = this.container;
let offset = 0;
while (element) {
const pos = this.vertical ? element.offsetTop : element.offsetLeft;
offset = offset + pos;
element = element.offsetParent;
element = element.offsetParent as HTMLElement;
}
return offset;
}
setItemSize(item, size) {
public setItemSize(item: HTMLElement, size: number) {
if (this.vertical) {
item.style.height = `${Math.round(size)}px`;
} else {
@ -76,7 +76,7 @@ export default class Sizer {
}
}
clearItemSize(item) {
public clearItemSize(item: HTMLElement) {
if (this.vertical) {
item.style.height = null;
} else {
@ -84,17 +84,23 @@ export default class Sizer {
}
}
// TODO
public start(item: HTMLElement) {}
// TODO
public finish(item: HTMLElement) {}
/**
@param {MouseEvent} event the mouse event
@return {number} the distance between the cursor and the edge of the container,
along the applicable axis (vertical or horizontal)
*/
offsetFromEvent(event) {
public offsetFromEvent(event: MouseEvent) {
const pos = this.vertical ? event.pageY : event.pageX;
if (this.reverse) {
return (this._getPageOffset() + this.getTotalSize()) - pos;
return (this.getPageOffset() + this.getTotalSize()) - pos;
} else {
return pos - this._getPageOffset();
return pos - this.getPageOffset();
}
}
}