diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss index 2d471ee198..b7fe19ca89 100644 --- a/res/css/structures/_RoomSubList.scss +++ b/res/css/structures/_RoomSubList.scss @@ -33,13 +33,25 @@ limitations under the License. .mx_RoomSubList { min-height: 31px; - flex: 0 100000000 auto; + flex: 0 10000 auto; display: flex; flex-direction: column; } +.mx_RoomSubList.resized-sized { + /* + flex-basis to 0 so sublists + are not shrinking/growing relative + to their content (as would be the case with auto), + as this intervenes with sizing an item exactly + when not available space is available + in the flex container + */ + flex: 1 1 0; +} + .mx_RoomSubList_nonEmpty { - min-height: 70px; + min-height: 74px; .mx_AutoHideScrollbar_offset { padding-bottom: 4px; @@ -50,17 +62,6 @@ limitations under the License. flex: none !important; } -.mx_RoomSubList.resized-all { - flex: 0 1 auto; -} - -.mx_RoomSubList.resized-sized { - /* resizer set max-height on resized-sized, - so that limits the height and hence - needs a very small flex-shrink */ - flex: 0 10000 auto; -} - .mx_RoomSubList_labelContainer { display: flex; flex-direction: row; diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index ed8142cd53..1a714e3a84 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -36,7 +36,7 @@ import GroupStore from '../../../stores/GroupStore'; import RoomSubList from '../../structures/RoomSubList'; import ResizeHandle from '../elements/ResizeHandle'; -import {Resizer, RoomDistributor, RoomSizer} from '../../../resizer' +import {Resizer, RoomSubListDistributor} from '../../../resizer' const HIDE_CONFERENCE_CHANS = true; const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; @@ -153,7 +153,11 @@ module.exports = React.createClass({ if (typeof newSize === "string") { newSize = Number.MAX_SAFE_INTEGER; } - this.subListSizes[id] = newSize; + if (newSize === null) { + delete this.subListSizes[id]; + } else { + this.subListSizes[id] = newSize; + } window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.subListSizes)); // update overflow indicators this._checkSubListsOverflow(); @@ -164,7 +168,7 @@ module.exports = React.createClass({ const cfg = { onResized: this._onSubListResize, }; - this.resizer = new Resizer(this.resizeContainer, RoomDistributor, cfg, RoomSizer); + this.resizer = new Resizer(this.resizeContainer, RoomSubListDistributor, cfg); this.resizer.setClassNames({ handle: "mx_ResizeHandle", vertical: "mx_ResizeHandle_vertical", @@ -724,4 +728,4 @@ module.exports = React.createClass({ ); }, -}); \ No newline at end of file +}); diff --git a/src/resizer/distributors.js b/src/resizer/distributors.js deleted file mode 100644 index caf677a18f..0000000000 --- a/src/resizer/distributors.js +++ /dev/null @@ -1,82 +0,0 @@ -/* -Copyright 2018 New Vector Ltd - -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. -*/ - -/** -distributors translate a moving cursor into -CSS/DOM changes by calling the sizer - -they have two methods: - `resize` receives then new item size - `resizeFromContainerOffset` receives resize handle location - within the container bounding box. For internal use. - This method usually ends up calling `resize` once the start offset is subtracted. -the offset from the container edge of where -the mouse cursor is. -*/ -class FixedDistributor { - constructor(sizer, item, id, config) { - this.sizer = sizer; - this.item = item; - this.id = id; - this.beforeOffset = sizer.getItemOffset(this.item); - this.onResized = config && config.onResized; - } - - resize(itemSize) { - this.sizer.setItemSize(this.item, itemSize); - if (this.onResized) { - this.onResized(itemSize, this.id, this.item); - } - return itemSize; - } - - resizeFromContainerOffset(offset) { - this.resize(offset - this.beforeOffset); - } -} - - -class CollapseDistributor extends FixedDistributor { - constructor(sizer, item, id, config) { - super(sizer, item, id, config); - this.toggleSize = config && config.toggleSize; - this.onCollapsed = config && config.onCollapsed; - this.isCollapsed = false; - } - - resize(newSize) { - const isCollapsedSize = newSize < this.toggleSize; - if (isCollapsedSize && !this.isCollapsed) { - this.isCollapsed = true; - if (this.onCollapsed) { - this.onCollapsed(true, this.item); - } - } else if (!isCollapsedSize && this.isCollapsed) { - if (this.onCollapsed) { - this.onCollapsed(false, this.item); - } - this.isCollapsed = false; - } - if (!isCollapsedSize) { - super.resize(newSize); - } - } -} - -module.exports = { - FixedDistributor, - CollapseDistributor, -}; diff --git a/src/resizer/distributors/collapse.js b/src/resizer/distributors/collapse.js new file mode 100644 index 0000000000..784532a0eb --- /dev/null +++ b/src/resizer/distributors/collapse.js @@ -0,0 +1,53 @@ +/* +Copyright 2019 New Vector Ltd + +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 FixedDistributor from "./fixed"; +import ResizeItem from "../item"; + +class CollapseItem extends ResizeItem { + notifyCollapsed(collapsed) { + const callback = this.resizer.config.onCollapsed; + if (callback) { + callback(collapsed, this.id, this.domNode); + } + } +} + +export default class CollapseDistributor extends FixedDistributor { + static createItem(resizeHandle, resizer, sizer) { + return new CollapseItem(resizeHandle, resizer, sizer); + } + + constructor(item, config) { + super(item); + this.toggleSize = config && config.toggleSize; + this.isCollapsed = false; + } + + resize(newSize) { + const isCollapsedSize = newSize < this.toggleSize; + if (isCollapsedSize && !this.isCollapsed) { + this.isCollapsed = true; + this.item.notifyCollapsed(true); + } else if (!isCollapsedSize && this.isCollapsed) { + this.item.notifyCollapsed(false); + this.isCollapsed = false; + } + if (!isCollapsedSize) { + super.resize(newSize); + } + } +} diff --git a/src/resizer/distributors/fixed.js b/src/resizer/distributors/fixed.js new file mode 100644 index 0000000000..e93c6fbcee --- /dev/null +++ b/src/resizer/distributors/fixed.js @@ -0,0 +1,55 @@ +/* +Copyright 2019 New Vector Ltd + +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 ResizeItem from "../item"; +import Sizer from "../sizer"; + +/** +distributors translate a moving cursor into +CSS/DOM changes by calling the sizer + +they have two methods: + `resize` receives then new item size + `resizeFromContainerOffset` receives resize handle location + 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) { + return new ResizeItem(resizeHandle, resizer, sizer); + } + + static createSizer(containerElement, vertical, reverse) { + return new Sizer(containerElement, vertical, reverse); + } + + constructor(item) { + this.item = item; + this.beforeOffset = item.offset(); + } + + resize(size) { + this.item.setSize(size); + } + + resizeFromContainerOffset(offset) { + this.resize(offset - this.beforeOffset); + } + + start() {} + + finish() {} +} diff --git a/src/resizer/distributors/roomsublist.js b/src/resizer/distributors/roomsublist.js new file mode 100644 index 0000000000..cc7875bfb0 --- /dev/null +++ b/src/resizer/distributors/roomsublist.js @@ -0,0 +1,132 @@ +/* +Copyright 2019 New Vector Ltd + +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 ResizeItem from "../item"; + +class RoomSizer extends Sizer { + setItemSize(item, size) { + item.style.maxHeight = `${Math.round(size)}px`; + item.classList.add("resized-sized"); + } + + clearItemSize(item) { + item.style.maxHeight = null; + item.classList.remove("resized-sized"); + } +} + +class RoomSubListItem extends ResizeItem { + isCollapsed() { + return this.domNode.classList.contains("mx_RoomSubList_hidden"); + } + + maxSize() { + const header = this.domNode.querySelector(".mx_RoomSubList_labelContainer"); + const scrollItem = this.domNode.querySelector(".mx_RoomSubList_scroll"); + const headerHeight = this.sizer.getItemSize(header); + return headerHeight + (scrollItem ? scrollItem.scrollHeight : 0); + } + + minSize() { + const isNotEmpty = this.domNode.classList.contains("mx_RoomSubList_nonEmpty"); + return isNotEmpty ? 74 : 31; //size of header + 1? room tile (see room sub list css) + } + + isSized() { + return this.domNode.classList.contains("resized-sized"); + } +} + +export default class RoomSubListDistributor { + static createItem(resizeHandle, resizer, sizer) { + return new RoomSubListItem(resizeHandle, resizer, sizer); + } + + static createSizer(containerElement, vertical, reverse) { + return new RoomSizer(containerElement, vertical, reverse); + } + + constructor(item) { + this.item = item; + } + + _handleSize() { + return 1; + } + + resize(size) { + //console.log("*** starting resize session with size", size); + let item = this.item; + while (item) { + const minSize = item.minSize(); + if (item.isCollapsed()) { + item = item.previous(); + } else if (size <= minSize) { + //console.log(" - resizing", item.id, "to min size", minSize); + item.setSize(minSize); + const remainder = minSize - size; + item = item.previous(); + if (item) { + size = item.size() - remainder - this._handleSize(); + } + } else { + const maxSize = item.maxSize(); + if (size > maxSize) { + // console.log(" - resizing", item.id, "to maxSize", maxSize); + item.setSize(maxSize); + const remainder = size - maxSize; + item = item.previous(); + if (item) { + size = item.size() + remainder; // todo: handle size here? + } + } else { + //console.log(" - resizing", item.id, "to size", size); + item.setSize(size); + item = null; + size = 0; + } + } + } + //console.log("*** ending resize session"); + } + + resizeFromContainerOffset(containerOffset) { + this.resize(containerOffset - this.item.offset()); + } + + start() { + // set all max-height props to the actual height. + let item = this.item.first(); + const sizes = []; + while (item) { + if (!item.isCollapsed()) { + sizes.push(item.size()); + } else { + sizes.push(100); + } + item = item.next(); + } + item = this.item.first(); + sizes.forEach((size) => { + item.setSize(size); + item = item.next(); + }); + } + + finish() { + } +} diff --git a/src/resizer/index.js b/src/resizer/index.js index 0720fa36ce..bc4c8f388c 100644 --- a/src/resizer/index.js +++ b/src/resizer/index.js @@ -14,17 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {Sizer, FlexSizer} from "./sizer"; -import {FixedDistributor, CollapseDistributor} from "./distributors"; -import {Resizer} from "./resizer"; -import {RoomSizer, RoomDistributor} from "./room"; +import FixedDistributor from "./distributors/fixed"; +import CollapseDistributor from "./distributors/collapse"; +import RoomSubListDistributor from "./distributors/roomsublist"; +import Resizer from "./resizer"; module.exports = { Resizer, - Sizer, - FlexSizer, FixedDistributor, CollapseDistributor, - RoomSizer, - RoomDistributor, + RoomSubListDistributor, }; diff --git a/src/resizer/item.js b/src/resizer/item.js new file mode 100644 index 0000000000..2e06ad217c --- /dev/null +++ b/src/resizer/item.js @@ -0,0 +1,107 @@ +/* +Copyright 2019 New Vector Ltd + +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. +*/ + +export default class ResizeItem { + constructor(handle, resizer, sizer) { + const id = handle.getAttribute("data-id"); + const reverse = resizer.isReverseResizeHandle(handle); + const domNode = reverse ? handle.nextElementSibling : handle.previousElementSibling; + + this.domNode = domNode; + 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); + } + + _advance(forwards) { + // opposite direction from fromResizeHandle to get back to handle + let handle = this.reverse ? + this.domNode.previousElementSibling : + this.domNode.nextElementSibling; + const moveNext = forwards !== this.reverse; // xor + // iterate at least once to avoid infinite loop + do { + if (moveNext) { + handle = handle.nextElementSibling; + } else { + handle = handle.previousElementSibling; + } + } while (handle && !this.resizer.isResizeHandle(handle)); + + if (handle) { + const nextHandle = this._copyWith(handle, this.resizer, this.sizer); + nextHandle.reverse = this.reverse; + return nextHandle; + } + } + + next() { + return this._advance(true); + } + + previous() { + return this._advance(false); + } + + size() { + return this.sizer.getItemSize(this.domNode); + } + + offset() { + return this.sizer.getItemOffset(this.domNode); + } + + setSize(size) { + this.sizer.setItemSize(this.domNode, size); + const callback = this.resizer.config.onResized; + if (callback) { + callback(size, this.id, this.domNode); + } + } + + clearSize() { + this.sizer.clearItemSize(this.domNode); + const callback = this.resizer.config.onResized; + if (callback) { + callback(null, this.id, this.domNode); + } + } + + + first() { + const firstHandle = Array.from(this.domNode.parentElement.children).find(el => { + return this.resizer.isResizeHandle(el); + }); + if (firstHandle) { + return this._copyWith(firstHandle, this.resizer, this.sizer); + } + } + + last() { + const lastHandle = Array.from(this.domNode.parentElement.children).reverse().find(el => { + return this.resizer.isResizeHandle(el); + }); + if (lastHandle) { + return this._copyWith(lastHandle, this.resizer, this.sizer); + } + } +} diff --git a/src/resizer/resizer.js b/src/resizer/resizer.js index 0e113b3664..4d999652a6 100644 --- a/src/resizer/resizer.js +++ b/src/resizer/resizer.js @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {Sizer} from "./sizer"; - /* classNames: // class on resize-handle @@ -28,12 +26,14 @@ classNames: resizing: string */ -export class Resizer { - constructor(container, distributorCtor, distributorCfg, sizerCtor = Sizer) { + +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) { this.container = container; this.distributorCtor = distributorCtor; - this.distributorCfg = distributorCfg; - this.sizerCtor = sizerCtor; + this.config = config; this.classNames = { handle: "resizer-handle", reverse: "resizer-reverse", @@ -79,7 +79,11 @@ export class Resizer { } } - _isResizeHandle(el) { + isReverseResizeHandle(el) { + return el && el.classList.contains(this.classNames.reverse); + } + + isResizeHandle(el) { return el && el.classList.contains(this.classNames.handle); } @@ -99,6 +103,7 @@ export class Resizer { } const {sizer, distributor} = this._createSizerAndDistributor(resizeHandle); + distributor.start(); const onMouseMove = (event) => { const offset = sizer.offsetFromEvent(event); @@ -106,48 +111,33 @@ export class Resizer { }; const body = document.body; - const onMouseUp = (event) => { + const finishResize = () => { if (this.classNames.resizing) { this.container.classList.remove(this.classNames.resizing); } - body.removeEventListener("mouseup", onMouseUp, false); + distributor.finish(); + body.removeEventListener("mouseup", finishResize, false); + document.removeEventListener("mouseleave", finishResize, false); body.removeEventListener("mousemove", onMouseMove, false); }; - body.addEventListener("mouseup", onMouseUp, false); + body.addEventListener("mouseup", finishResize, false); + document.addEventListener("mouseleave", finishResize, false); body.addEventListener("mousemove", onMouseMove, false); } _createSizerAndDistributor(resizeHandle) { const vertical = resizeHandle.classList.contains(this.classNames.vertical); - const reverse = resizeHandle.classList.contains(this.classNames.reverse); - - // eslint-disable-next-line new-cap - const sizer = new this.sizerCtor(this.container, vertical, reverse); - - const items = this._getResizableItems(); - const prevItem = resizeHandle.previousElementSibling; - // if reverse, resize the item after the handle instead of before, so + 1 - const itemIndex = items.indexOf(prevItem) + (reverse ? 1 : 0); - const item = items[itemIndex]; - const id = resizeHandle.getAttribute("data-id"); - // eslint-disable-next-line new-cap - const distributor = new this.distributorCtor( - sizer, item, id, this.distributorCfg, - items, this.container); + 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); return {sizer, distributor}; } - _getResizableItems() { - return Array.from(this.container.children).filter(el => { - return !this._isResizeHandle(el) && ( - this._isResizeHandle(el.previousElementSibling) || - this._isResizeHandle(el.nextElementSibling)); - }); - } - _getResizeHandles() { return Array.from(this.container.children).filter(el => { - return this._isResizeHandle(el); + return this.isResizeHandle(el); }); } } diff --git a/src/resizer/room.js b/src/resizer/room.js deleted file mode 100644 index def12d49eb..0000000000 --- a/src/resizer/room.js +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright 2018 New Vector Ltd - -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 "./distributors"; - -class RoomSizer extends Sizer { - setItemSize(item, size) { - const isString = typeof size === "string"; - const cl = item.classList; - if (isString) { - if (size === "resized-all") { - cl.add("resized-all"); - cl.remove("resized-sized"); - item.style.maxHeight = null; - } - } else { - cl.add("resized-sized"); - cl.remove("resized-all"); - item.style.maxHeight = `${Math.round(size)}px`; - } - } -} - -class RoomDistributor extends FixedDistributor { - resize(itemSize) { - const scrollItem = this.item.querySelector(".mx_RoomSubList_scroll"); - if (!scrollItem) { - return; //FIXME: happens when starting the page on a community url, taking the safe way out for now - } - const fixedHeight = this.item.offsetHeight - scrollItem.offsetHeight; - if (itemSize > (fixedHeight + scrollItem.scrollHeight)) { - super.resize("resized-all"); - } else { - super.resize(itemSize); - } - } - - resizeFromContainerOffset(offset) { - return this.resize(offset - this.sizer.getItemOffset(this.item)); - } -} - -module.exports = { - RoomSizer, - RoomDistributor, -}; diff --git a/src/resizer/sizer.js b/src/resizer/sizer.js index 303214854b..50861d34d5 100644 --- a/src/resizer/sizer.js +++ b/src/resizer/sizer.js @@ -18,31 +18,13 @@ limitations under the License. implements DOM/CSS operations for resizing. The sizer determines what CSS mechanism is used for sizing items, like flexbox, ... */ -class Sizer { +export default class Sizer { constructor(container, vertical, reverse) { this.container = container; this.reverse = reverse; this.vertical = vertical; } - getItemPercentage(item) { - /* - const flexGrow = window.getComputedStyle(item).flexGrow; - if (flexGrow === "") { - return null; - } - return parseInt(flexGrow) / 1000; - */ - const style = window.getComputedStyle(item); - const sizeStr = this.vertical ? style.height : style.width; - const size = parseInt(sizeStr, 10); - return size / this.getTotalSize(); - } - - setItemPercentage(item, percent) { - item.style.flexGrow = Math.round(percent * 1000); - } - /** @param {Element} item the dom element being resized @return {number} how far the edge of the item is from the edge of the container @@ -82,6 +64,14 @@ class Sizer { } } + clearItemSize(item) { + if (this.vertical) { + item.style.height = null; + } else { + item.style.width = null; + } + } + /** @param {MouseEvent} event the mouse event @return {number} the distance between the cursor and the edge of the container, @@ -96,12 +86,3 @@ class Sizer { } } } - -class FlexSizer extends Sizer { - setItemSize(item, size) { - item.style.flexGrow = `0`; - item.style.flexBasis = `${Math.round(size)}px`; - } -} - -module.exports = {Sizer, FlexSizer};