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 FixedDistributor from "./fixed";
import ResizeItem from "../item"; import ResizeItem from "../item";
import {IConfig} from "../resizer";
class CollapseItem extends ResizeItem { 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 +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) { static createItem(resizeHandle, resizer, sizer) {
return new CollapseItem(resizeHandle, resizer, sizer); return new CollapseItem(resizeHandle, resizer, sizer);
} }
constructor(item, config) { private readonly toggleSize: number;
private isCollapsed: boolean;
constructor(item: CollapseItem) {
super(item); super(item);
this.toggleSize = config && config.toggleSize; this.toggleSize = item.resizer?.config?.toggleSize;
this.isCollapsed = false; 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,34 @@ 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) {
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) {
return new Sizer(containerElement, vertical, reverse); return new Sizer(containerElement, vertical, reverse);
} }
constructor(item) { private readonly beforeOffset: number;
this.item = item;
constructor(protected 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() {
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. limitations under the License.
*/ */
export FixedDistributor from "./distributors/fixed"; export {default as FixedDistributor} from "./distributors/fixed";
export CollapseDistributor from "./distributors/collapse"; export {default as PercentageDistributor} from "./distributors/percentage";
export Resizer from "./resizer"; 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 2019 New Vector Ltd
Copyright 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.
@ -14,63 +15,81 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
export default class ResizeItem { import Sizer from "./sizer";
constructor(handle, resizer, 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 id = handle.getAttribute("data-id");
const reverse = resizer.isReverseResizeHandle(handle); 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.id = id;
this.reverse = reverse; this.reverse = reverse;
this.resizer = resizer; this.resizer = resizer;
this.sizer = sizer; this.sizer = sizer;
} }
_copyWith(handle, resizer, sizer) { private copyWith(handle: Element, resizer: Resizer, sizer: Sizer) {
const Ctor = this.constructor; const Ctor = this.constructor as typeof ResizeItem;
return new Ctor(handle, resizer, sizer); return new Ctor(<HTMLElement>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 = <HTMLElement>(this.reverse ?
this.domNode.previousElementSibling : this.domNode.previousElementSibling :
this.domNode.nextElementSibling; this.domNode.nextElementSibling);
const moveNext = forwards !== this.reverse; // xor const moveNext = forwards !== this.reverse; // xor
// iterate at least once to avoid infinite loop // iterate at least once to avoid infinite loop
do { do {
if (moveNext) { if (moveNext) {
handle = handle.nextElementSibling; handle = <HTMLElement>handle.nextElementSibling;
} else { } else {
handle = handle.previousElementSibling; handle = <HTMLElement>handle.previousElementSibling;
} }
} while (handle && !this.resizer.isResizeHandle(handle)); } while (handle && !this.resizer.isResizeHandle(handle));
if (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; 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 start() {
this.sizer.start(this.domNode);
}
public finish() {
this.sizer.finish(this.domNode);
}
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 +97,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 +105,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(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(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.
@ -27,36 +27,59 @@ classNames:
resizing: string 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 // 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(
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) { 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);
} }
/** /**
@ -65,36 +88,36 @@ export default class Resizer {
@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 {Distributor} 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): boolean {
return el && el.classList.contains(this.classNames.reverse); return el && el.classList.contains(this.classNames.reverse);
} }
isResizeHandle(el) { public isResizeHandle(el: HTMLElement): boolean {
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 && (<HTMLElement>event.target).closest(`.${this.classNames.handle}`);
if (!resizeHandle || resizeHandle.parentElement !== this.container) { if (!resizeHandle || resizeHandle.parentElement !== this.container) {
return; return;
} }
@ -109,7 +132,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 +156,23 @@ 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,
): {sizer: Sizer, distributor: FixedDistributor<any>} {
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() {
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 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, ... 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() {
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 = element.offsetParent as HTMLElement;
} }
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 {
@ -84,17 +84,23 @@ export default class Sizer {
} }
} }
// TODO
public start(item: HTMLElement) {}
// TODO
public finish(item: HTMLElement) {}
/** /**
@param {MouseEvent} event the mouse event @param {MouseEvent} event the mouse event
@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();
} }
} }
} }