Merge pull request #2297 from matrix-org/bwindels/roomlistsizingimprovements

Redesign: improve room sub list sizing & persist sizes
This commit is contained in:
Bruno Windels 2018-11-27 13:40:48 +00:00 committed by GitHub
commit 8f4292399b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 163 additions and 124 deletions

View file

@ -14,15 +14,51 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/* a word of explanation about the flex-shrink values employed here:
there are 3 priotized categories of screen real-estate grabbing,
each with a flex-shrink difference of 4 order of magnitude,
so they ideally wouldn't affect each other.
lowest category: .mx_RoomSubList
flex:-shrink: 10000000
distribute size of items within the same categery by their size
middle category: .mx_RoomSubList.resized-sized
flex:-shrink: 1000
applied when using the resizer, will have a max-height set to it,
to limit the size
highest category: .mx_RoomSubList.resized-all
flex:-shrink: 1
small flex-shrink value (1), is only added if you can drag the resizer so far
so in practice you can only assign this category if there is enough space.
*/
.mx_RoomSubList {
min-height: 31px;
flex: 0 1 auto;
flex: 0 100000000 auto;
display: flex;
flex-direction: column;
}
.mx_RoomSubList_nonEmpty {
margin-bottom: 4px;
min-height: 76px;
.mx_AutoHideScrollbar_offset {
padding-bottom: 4px;
}
}
.mx_RoomSubList_hidden {
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 {
@ -105,39 +141,42 @@ limitations under the License.
}
.mx_RoomSubList_scroll {
/* let rooms list grab all available space */
/* let rooms list grab as much space as it needs (auto),
potentially overflowing and showing a scrollbar */
flex: 0 1 auto;
padding: 0 8px;
}
.mx_RoomSubList_scroll.mx_IndicatorScrollbar_topOverflow::before,
.mx_RoomSubList_scroll.mx_IndicatorScrollbar_bottomOverflow::after {
position: sticky;
left: 0;
right: 0;
height: 40px;
content: "";
display: block;
z-index: 100;
pointer-events: none;
}
// overflow indicators
.mx_RoomSubList:not(.resized-all) > .mx_RoomSubList_scroll {
&.mx_IndicatorScrollbar_topOverflow::before,
&.mx_IndicatorScrollbar_bottomOverflow::after {
position: sticky;
left: 0;
right: 0;
height: 40px;
content: "";
display: block;
z-index: 100;
pointer-events: none;
}
&.mx_IndicatorScrollbar_topOverflow > .mx_AutoHideScrollbar_offset {
margin-top: -40px;
}
&.mx_IndicatorScrollbar_bottomOverflow > .mx_AutoHideScrollbar_offset {
margin-bottom: -40px;
}
.mx_RoomSubList_scroll.mx_IndicatorScrollbar_topOverflow > .mx_AutoHideScrollbar_offset {
margin-top: -40px;
}
.mx_RoomSubList_scroll.mx_IndicatorScrollbar_bottomOverflow > .mx_AutoHideScrollbar_offset {
margin-bottom: -40px;
}
&.mx_IndicatorScrollbar_topOverflow::before {
top: 0;
background: linear-gradient($secondary-accent-color, transparent);
}
.mx_RoomSubList_scroll.mx_IndicatorScrollbar_topOverflow::before {
top: 0;
background: linear-gradient($secondary-accent-color, transparent);
}
.mx_RoomSubList_scroll.mx_IndicatorScrollbar_bottomOverflow::after {
bottom: 0;
background: linear-gradient(transparent, $secondary-accent-color);
&.mx_IndicatorScrollbar_bottomOverflow::after {
bottom: 0;
background: linear-gradient(transparent, $secondary-accent-color);
}
}
.collapsed {

View file

@ -24,6 +24,10 @@ limitations under the License.
min-height: 0;
}
.mx_SearchBox {
flex: none;
}
/* hide resize handles next to collapsed / empty sublists */
.mx_RoomList .mx_RoomSubList:not(.mx_RoomSubList_nonEmpty) + .mx_ResizeHandle {
display: none;

View file

@ -164,15 +164,13 @@ const LoggedInView = React.createClass({
};
const collapseConfig = {
toggleSize: 260 - 50,
onCollapsed: (collapsed, item) => {
if (item.classList.contains("mx_LeftPanel_container")) {
this.setState({collapseLhs: collapsed});
if (collapsed) {
window.localStorage.setItem("mx_lhs_size", '0');
}
onCollapsed: (collapsed) => {
this.setState({collapseLhs: collapsed});
if (collapsed) {
window.localStorage.setItem("mx_lhs_size", '0');
}
},
onResized: (size, item) => {
onResized: (size) => {
window.localStorage.setItem("mx_lhs_size", '' + size);
},
};

View file

@ -318,20 +318,17 @@ const RoomSubList = React.createClass({
if (len) {
const subListClasses = classNames({
"mx_RoomSubList": true,
"mx_RoomSubList_hidden": this.state.hidden,
"mx_RoomSubList_nonEmpty": len && !this.state.hidden,
});
if (this.state.hidden) {
return <div className={subListClasses} style={{flexBasis: "unset", flexGrow: "unset"}}>
return <div className={subListClasses}>
{this._getHeaderJsx()}
</div>;
} else {
const heightEstimation = (len * 44) + 31 + (8 + 8);
const style = {
maxHeight: `${heightEstimation}px`,
};
const tiles = this.makeRoomTiles();
tiles.push(...this.props.extraTiles);
return <div style={style} className={subListClasses}>
return <div className={subListClasses}>
{this._getHeaderJsx()}
<IndicatorScrollbar className="mx_RoomSubList_scroll">
{ tiles }

View file

@ -14,7 +14,7 @@ const ResizeHandle = (props) => {
classNames.push('mx_ResizeHandle_reverse');
}
return (
<div className={classNames.join(' ')} />
<div className={classNames.join(' ')} data-id={props.id} />
);
};

View file

@ -36,7 +36,7 @@ import GroupStore from '../../../stores/GroupStore';
import RoomSubList from '../../structures/RoomSubList';
import ResizeHandle from '../elements/ResizeHandle';
import {Resizer, FixedDistributor, FlexSizer} from '../../../resizer'
import {Resizer, RoomDistributor, RoomSizer} from '../../../resizer'
const HIDE_CONFERENCE_CHANS = true;
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
@ -70,6 +70,10 @@ module.exports = React.createClass({
},
getInitialState: function() {
const sizesJson = window.localStorage.getItem("mx_roomlist_sizes");
this.subListSizes = sizesJson ? JSON.parse(sizesJson) : {};
return {
isLoadingLeftRooms: false,
totalRoomCount: null,
@ -134,14 +138,34 @@ module.exports = React.createClass({
this._delayedRefreshRoomListLoopCount = 0;
},
_onSubListResize: function(newSize, id) {
if (!id) {
return;
}
if (typeof newSize === "string") {
newSize = Number.MAX_SAFE_INTEGER;
}
this.subListSizes[id] = newSize;
window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.subListSizes));
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
this.resizer = new Resizer(this.resizeContainer, FixedDistributor, null, FlexSizer);
const cfg = {
onResized: this._onSubListResize,
};
this.resizer = new Resizer(this.resizeContainer, RoomDistributor, cfg, RoomSizer);
this.resizer.setClassNames({
handle: "mx_ResizeHandle",
vertical: "mx_ResizeHandle_vertical",
reverse: "mx_ResizeHandle_reverse"
});
// load stored sizes
Object.entries(this.subListSizes).forEach(([id, size]) => {
this.resizer.forHandleWithId(id).resize(size);
});
this.resizer.attach();
this.mounted = true;
},
@ -476,7 +500,7 @@ module.exports = React.createClass({
if (!isLast) {
return components.concat(
subList,
<ResizeHandle key={chosenKey+"-resizer"} vertical={true} />
<ResizeHandle key={chosenKey+"-resizer"} vertical={true} id={chosenKey} />
);
} else {
return components.concat(subList);
@ -484,6 +508,10 @@ module.exports = React.createClass({
}, []);
},
_collectResizeContainer: function(el) {
this.resizeContainer = el;
},
render: function() {
let subLists = [
{
@ -560,7 +588,7 @@ module.exports = React.createClass({
const subListComponents = this._mapSubListProps(subLists);
return (
<div ref={(d) => this.resizeContainer = d} className="mx_RoomList">
<div ref={this._collectResizeContainer} className="mx_RoomList">
{ subListComponents }
</div>
);

View file

@ -18,43 +18,46 @@ limitations under the License.
distributors translate a moving cursor into
CSS/DOM changes by calling the sizer
they have one method, `resize` that receives
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, config) {
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(offset) {
const itemSize = offset - this.beforeOffset;
resize(itemSize) {
this.sizer.setItemSize(this.item, itemSize);
if (this.onResized) {
this.onResized(itemSize, this.item);
this.onResized(itemSize, this.id, this.item);
}
return itemSize;
}
sizeFromOffset(offset) {
return offset - this.beforeOffset;
resizeFromContainerOffset(offset) {
this.resize(offset - this.beforeOffset);
}
}
class CollapseDistributor extends FixedDistributor {
constructor(sizer, item, config) {
super(sizer, item, config);
constructor(sizer, item, id, config) {
super(sizer, item, id, config);
this.toggleSize = config && config.toggleSize;
this.onCollapsed = config && config.onCollapsed;
this.isCollapsed = false;
}
resize(offset) {
const newSize = this.sizeFromOffset(offset);
resize(newSize) {
const isCollapsedSize = newSize < this.toggleSize;
if (isCollapsedSize && !this.isCollapsed) {
this.isCollapsed = true;
@ -68,60 +71,12 @@ class CollapseDistributor extends FixedDistributor {
this.isCollapsed = false;
}
if (!isCollapsedSize) {
super.resize(offset);
super.resize(newSize);
}
}
}
class PercentageDistributor {
constructor(sizer, item, _config, items, container) {
this.container = container;
this.totalSize = sizer.getTotalSize();
this.sizer = sizer;
const itemIndex = items.indexOf(item);
this.beforeItems = items.slice(0, itemIndex);
this.afterItems = items.slice(itemIndex);
const percentages = PercentageDistributor._getPercentages(sizer, items);
this.beforePercentages = percentages.slice(0, itemIndex);
this.afterPercentages = percentages.slice(itemIndex);
}
resize(offset) {
const percent = offset / this.totalSize;
const beforeSum =
this.beforePercentages.reduce((total, p) => total + p, 0);
const beforePercentages =
this.beforePercentages.map(p => (p / beforeSum) * percent);
const afterSum =
this.afterPercentages.reduce((total, p) => total + p, 0);
const afterPercentages =
this.afterPercentages.map(p => (p / afterSum) * (1 - percent));
this.beforeItems.forEach((item, index) => {
this.sizer.setItemPercentage(item, beforePercentages[index]);
});
this.afterItems.forEach((item, index) => {
this.sizer.setItemPercentage(item, afterPercentages[index]);
});
}
static _getPercentages(sizer, items) {
const percentages = items.map(i => sizer.getItemPercentage(i));
const setPercentages = percentages.filter(p => p !== null);
const unsetCount = percentages.length - setPercentages.length;
const setTotal = setPercentages.reduce((total, p) => total + p, 0);
const implicitPercentage = (1 - setTotal) / unsetCount;
return percentages.map(p => p === null ? implicitPercentage : p);
}
static setPercentage(el, percent) {
el.style.flexGrow = Math.round(percent * 1000);
}
}
module.exports = {
FixedDistributor,
CollapseDistributor,
PercentageDistributor,
};

View file

@ -15,8 +15,9 @@ limitations under the License.
*/
import {Sizer, FlexSizer} from "./sizer";
import {FixedDistributor, CollapseDistributor, PercentageDistributor} from "./distributors";
import {FixedDistributor, CollapseDistributor} from "./distributors";
import {Resizer} from "./resizer";
import {RoomSizer, RoomDistributor} from "./room";
module.exports = {
Resizer,
@ -24,5 +25,6 @@ module.exports = {
FlexSizer,
FixedDistributor,
CollapseDistributor,
PercentageDistributor,
RoomSizer,
RoomDistributor,
};

View file

@ -64,8 +64,19 @@ export class Resizer {
forHandleAt(handleIndex) {
const handles = this._getResizeHandles();
const handle = handles[handleIndex];
const {distributor} = this._createSizerAndDistributor(handle);
return distributor;
if (handle) {
const {distributor} = this._createSizerAndDistributor(handle);
return distributor;
}
}
forHandleWithId(id) {
const handles = this._getResizeHandles();
const handle = handles.find((h) => h.getAttribute("data-id") === id);
if (handle) {
const {distributor} = this._createSizerAndDistributor(handle);
return distributor;
}
}
_isResizeHandle(el) {
@ -79,6 +90,7 @@ export class Resizer {
}
// prevent starting a drag operation
event.preventDefault();
// mark as currently resizing
if (this.classNames.resizing) {
this.container.classList.add(this.classNames.resizing);
@ -88,7 +100,7 @@ export class Resizer {
const onMouseMove = (event) => {
const offset = sizer.offsetFromEvent(event);
distributor.resize(offset);
distributor.resizeFromContainerOffset(offset);
};
const body = document.body;
@ -115,9 +127,10 @@ export class Resizer {
// 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, this.distributorCfg,
sizer, item, id, this.distributorCfg,
items, this.container);
return {sizer, distributor};
}

View file

@ -22,30 +22,33 @@ class RoomSizer extends Sizer {
const isString = typeof size === "string";
const cl = item.classList;
if (isString) {
item.style.flex = null;
if (size === "show-content") {
cl.add("show-content");
cl.remove("show-available");
if (size === "resized-all") {
cl.add("resized-all");
cl.remove("resized-sized");
item.style.maxHeight = null;
}
} else {
cl.add("show-available");
//item.style.flex = `0 1 ${Math.round(size)}px`;
cl.add("resized-sized");
cl.remove("resized-all");
item.style.maxHeight = `${Math.round(size)}px`;
}
}
}
class RoomDistributor extends FixedDistributor {
resize(offset) {
const itemSize = offset - this.sizer.getItemOffset(this.item);
if (itemSize > this.item.scrollHeight) {
this.sizer.setItemSize(this.item, "show-content");
resize(itemSize) {
const scrollItem = this.item.querySelector(".mx_RoomSubList_scroll");
const fixedHeight = this.item.offsetHeight - scrollItem.offsetHeight;
if (itemSize > (fixedHeight + scrollItem.scrollHeight)) {
super.resize("resized-all");
} else {
this.sizer.setItemSize(this.item, itemSize);
super.resize(itemSize);
}
}
resizeFromContainerOffset(offset) {
return this.resize(offset - this.sizer.getItemOffset(this.item));
}
}
module.exports = {