mirror of
https://github.com/element-hq/element-web
synced 2024-11-26 19:26:04 +03:00
Merge pull request #2297 from matrix-org/bwindels/roomlistsizingimprovements
Redesign: improve room sub list sizing & persist sizes
This commit is contained in:
commit
8f4292399b
10 changed files with 163 additions and 124 deletions
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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} />
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
Loading…
Reference in a new issue