Merge pull request #2440 from matrix-org/bwindels/smarterresizer

Improve room sublist resizing
This commit is contained in:
Bruno Windels 2019-01-16 10:55:53 +00:00 committed by GitHub
commit 0229453482
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 407 additions and 229 deletions

View file

@ -33,13 +33,25 @@ limitations under the License.
.mx_RoomSubList { .mx_RoomSubList {
min-height: 31px; min-height: 31px;
flex: 0 100000000 auto; flex: 0 10000 auto;
display: flex; display: flex;
flex-direction: column; 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 { .mx_RoomSubList_nonEmpty {
min-height: 70px; min-height: 74px;
.mx_AutoHideScrollbar_offset { .mx_AutoHideScrollbar_offset {
padding-bottom: 4px; padding-bottom: 4px;
@ -50,17 +62,6 @@ limitations under the License.
flex: none !important; 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 { .mx_RoomSubList_labelContainer {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View file

@ -36,7 +36,7 @@ import GroupStore from '../../../stores/GroupStore';
import RoomSubList from '../../structures/RoomSubList'; import RoomSubList from '../../structures/RoomSubList';
import ResizeHandle from '../elements/ResizeHandle'; import ResizeHandle from '../elements/ResizeHandle';
import {Resizer, RoomDistributor, RoomSizer} from '../../../resizer' import {Resizer, RoomSubListDistributor} from '../../../resizer'
const HIDE_CONFERENCE_CHANS = true; const HIDE_CONFERENCE_CHANS = true;
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; 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") { if (typeof newSize === "string") {
newSize = Number.MAX_SAFE_INTEGER; 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)); window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.subListSizes));
// update overflow indicators // update overflow indicators
this._checkSubListsOverflow(); this._checkSubListsOverflow();
@ -164,7 +168,7 @@ module.exports = React.createClass({
const cfg = { const cfg = {
onResized: this._onSubListResize, onResized: this._onSubListResize,
}; };
this.resizer = new Resizer(this.resizeContainer, RoomDistributor, cfg, RoomSizer); this.resizer = new Resizer(this.resizeContainer, RoomSubListDistributor, cfg);
this.resizer.setClassNames({ this.resizer.setClassNames({
handle: "mx_ResizeHandle", handle: "mx_ResizeHandle",
vertical: "mx_ResizeHandle_vertical", vertical: "mx_ResizeHandle_vertical",

View file

@ -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,
};

View file

@ -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);
}
}
}

View file

@ -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() {}
}

View file

@ -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() {
}
}

View file

@ -14,17 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {Sizer, FlexSizer} from "./sizer"; import FixedDistributor from "./distributors/fixed";
import {FixedDistributor, CollapseDistributor} from "./distributors"; import CollapseDistributor from "./distributors/collapse";
import {Resizer} from "./resizer"; import RoomSubListDistributor from "./distributors/roomsublist";
import {RoomSizer, RoomDistributor} from "./room"; import Resizer from "./resizer";
module.exports = { module.exports = {
Resizer, Resizer,
Sizer,
FlexSizer,
FixedDistributor, FixedDistributor,
CollapseDistributor, CollapseDistributor,
RoomSizer, RoomSubListDistributor,
RoomDistributor,
}; };

107
src/resizer/item.js Normal file
View file

@ -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);
}
}
}

View file

@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {Sizer} from "./sizer";
/* /*
classNames: classNames:
// class on resize-handle // class on resize-handle
@ -28,12 +26,14 @@ classNames:
resizing: string 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.container = container;
this.distributorCtor = distributorCtor; this.distributorCtor = distributorCtor;
this.distributorCfg = distributorCfg; this.config = config;
this.sizerCtor = sizerCtor;
this.classNames = { this.classNames = {
handle: "resizer-handle", handle: "resizer-handle",
reverse: "resizer-reverse", 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); return el && el.classList.contains(this.classNames.handle);
} }
@ -99,6 +103,7 @@ export class Resizer {
} }
const {sizer, distributor} = this._createSizerAndDistributor(resizeHandle); const {sizer, distributor} = this._createSizerAndDistributor(resizeHandle);
distributor.start();
const onMouseMove = (event) => { const onMouseMove = (event) => {
const offset = sizer.offsetFromEvent(event); const offset = sizer.offsetFromEvent(event);
@ -106,48 +111,33 @@ export class Resizer {
}; };
const body = document.body; const body = document.body;
const onMouseUp = (event) => { const finishResize = () => {
if (this.classNames.resizing) { if (this.classNames.resizing) {
this.container.classList.remove(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.removeEventListener("mousemove", onMouseMove, false);
}; };
body.addEventListener("mouseup", onMouseUp, false); body.addEventListener("mouseup", finishResize, false);
document.addEventListener("mouseleave", finishResize, false);
body.addEventListener("mousemove", onMouseMove, false); body.addEventListener("mousemove", onMouseMove, false);
} }
_createSizerAndDistributor(resizeHandle) { _createSizerAndDistributor(resizeHandle) {
const vertical = resizeHandle.classList.contains(this.classNames.vertical); const vertical = resizeHandle.classList.contains(this.classNames.vertical);
const reverse = resizeHandle.classList.contains(this.classNames.reverse); const reverse = this.isReverseResizeHandle(resizeHandle);
const Distributor = this.distributorCtor;
// eslint-disable-next-line new-cap const sizer = Distributor.createSizer(this.container, vertical, reverse);
const sizer = new this.sizerCtor(this.container, vertical, reverse); const item = Distributor.createItem(resizeHandle, this, sizer);
const distributor = new Distributor(item, this.config);
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);
return {sizer, distributor}; 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() { _getResizeHandles() {
return Array.from(this.container.children).filter(el => { return Array.from(this.container.children).filter(el => {
return this._isResizeHandle(el); return this.isResizeHandle(el);
}); });
} }
} }

View file

@ -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,
};

View file

@ -18,31 +18,13 @@ limitations under the License.
implements DOM/CSS operations for resizing. 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, ...
*/ */
class Sizer { export default class Sizer {
constructor(container, vertical, reverse) { constructor(container, vertical, reverse) {
this.container = container; this.container = container;
this.reverse = reverse; this.reverse = reverse;
this.vertical = vertical; 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 @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
@ -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 @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,
@ -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};