diff --git a/src/resizer/distributors/collapse.js b/src/resizer/distributors/collapse.ts similarity index 73% rename from src/resizer/distributors/collapse.js rename to src/resizer/distributors/collapse.ts index 784532a0eb..c4b53d5892 100644 --- a/src/resizer/distributors/collapse.js +++ b/src/resizer/distributors/collapse.ts @@ -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 { + 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 { 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; diff --git a/src/resizer/distributors/fixed.js b/src/resizer/distributors/fixed.ts similarity index 67% rename from src/resizer/distributors/fixed.js rename to src/resizer/distributors/fixed.ts index e93c6fbcee..10539a412a 100644 --- a/src/resizer/distributors/fixed.js +++ b/src/resizer/distributors/fixed.ts @@ -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 = ResizeItem> { + 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(); + } } diff --git a/src/resizer/distributors/percentage.ts b/src/resizer/distributors/percentage.ts new file mode 100644 index 0000000000..5e216216e5 --- /dev/null +++ b/src/resizer/distributors/percentage.ts @@ -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 { + static createSizer(containerElement: HTMLElement, vertical: boolean, reverse: boolean) { + return new PercentageSizer(containerElement, vertical, reverse); + } +} diff --git a/src/resizer/index.js b/src/resizer/index.ts similarity index 70% rename from src/resizer/index.js rename to src/resizer/index.ts index 1fd8f4da46..e3624f76ac 100644 --- a/src/resizer/index.js +++ b/src/resizer/index.ts @@ -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"; diff --git a/src/resizer/item.js b/src/resizer/item.ts similarity index 55% rename from src/resizer/item.js rename to src/resizer/item.ts index 2e06ad217c..101cf5dbf2 100644 --- a/src/resizer/item.js +++ b/src/resizer/item.ts @@ -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 { + protected readonly domNode: HTMLElement; + protected readonly id: string; + protected reverse: boolean; + + constructor( + handle: HTMLElement, + public readonly resizer: Resizer, + 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 = (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(handle, resizer, sizer); } - _advance(forwards) { + private advance(forwards: boolean) { // opposite direction from fromResizeHandle to get back to handle - let handle = this.reverse ? + let handle = (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 = handle.nextElementSibling; } else { - handle = handle.previousElementSibling; + handle = 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(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(el); }); if (lastHandle) { - return this._copyWith(lastHandle, this.resizer, this.sizer); + return this.copyWith(lastHandle, this.resizer, this.sizer); } } } diff --git a/src/resizer/resizer.js b/src/resizer/resizer.ts similarity index 63% rename from src/resizer/resizer.js rename to src/resizer/resizer.ts index 1e75bf3bdf..692ce5e22f 100644 --- a/src/resizer/resizer.js +++ b/src/resizer/resizer.ts @@ -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 { + 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; + 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 { + const handles = this.getResizeHandles(); const handle = handles[handleIndex]; if (handle) { - const {distributor} = this._createSizerAndDistributor(handle); + const {distributor} = this.createSizerAndDistributor(handle); return distributor; } } - forHandleWithId(id) { - const handles = this._getResizeHandles(); + public forHandleWithId(id: string): FixedDistributor { + 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(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 && (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(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} { 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(el); + }) as HTMLElement[]; } } diff --git a/src/resizer/sizer.js b/src/resizer/sizer.ts similarity index 75% rename from src/resizer/sizer.js rename to src/resizer/sizer.ts index 4ce9232457..16000806a2 100644 --- a/src/resizer/sizer.js +++ b/src/resizer/sizer.ts @@ -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(); } } }