Merge pull request #2507 from matrix-org/bwindels/roomlistjslayout
Redesign: new layout algorithm for room sublists.
This commit is contained in:
commit
55e0838c82
7 changed files with 379 additions and 73 deletions
|
@ -32,35 +32,14 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_RoomSubList {
|
.mx_RoomSubList {
|
||||||
min-height: 31px;
|
|
||||||
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 .mx_AutoHideScrollbar_offset {
|
||||||
min-height: 74px;
|
|
||||||
|
|
||||||
.mx_AutoHideScrollbar_offset {
|
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomSubList_hidden {
|
|
||||||
flex: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomSubList_labelContainer {
|
.mx_RoomSubList_labelContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -17,13 +17,14 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_RoomList {
|
.mx_RoomList {
|
||||||
/* take up remaining space below TopLeftMenu */
|
/* take up remaining space below TopLeftMenu */
|
||||||
flex: 1 1 auto;
|
flex: 1;
|
||||||
/* use flexbox to layout sublists */
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RoomList .mx_ResizeHandle {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_SearchBox {
|
.mx_SearchBox {
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -212,6 +212,7 @@ const LeftPanel = React.createClass({
|
||||||
<CallPreview ConferenceHandler={VectorConferenceHandler} />
|
<CallPreview ConferenceHandler={VectorConferenceHandler} />
|
||||||
<RoomList
|
<RoomList
|
||||||
ref={this.collectRoomList}
|
ref={this.collectRoomList}
|
||||||
|
toolbarShown={this.props.toolbarShown}
|
||||||
collapsed={this.props.collapsed}
|
collapsed={this.props.collapsed}
|
||||||
searchFilter={this.state.searchFilter}
|
searchFilter={this.state.searchFilter}
|
||||||
ConferenceHandler={VectorConferenceHandler} />
|
ConferenceHandler={VectorConferenceHandler} />
|
||||||
|
|
|
@ -545,6 +545,7 @@ const LoggedInView = React.createClass({
|
||||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||||
<div ref={this._setResizeContainerRef} className={bodyClasses}>
|
<div ref={this._setResizeContainerRef} className={bodyClasses}>
|
||||||
<LeftPanel
|
<LeftPanel
|
||||||
|
toolbarShown={!!topBar}
|
||||||
collapsed={this.props.collapseLhs || this.state.collapseLhs || false}
|
collapsed={this.props.collapseLhs || this.state.collapseLhs || false}
|
||||||
disabled={this.props.leftDisabled}
|
disabled={this.props.leftDisabled}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -313,6 +313,12 @@ const RoomSubList = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setHeight: function(height) {
|
||||||
|
if (this.refs.subList) {
|
||||||
|
this.refs.subList.style.height = `${height}px`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const len = this.props.list.length + this.props.extraTiles.length;
|
const len = this.props.list.length + this.props.extraTiles.length;
|
||||||
if (len) {
|
if (len) {
|
||||||
|
@ -322,13 +328,13 @@ const RoomSubList = React.createClass({
|
||||||
"mx_RoomSubList_nonEmpty": len && !this.state.hidden,
|
"mx_RoomSubList_nonEmpty": len && !this.state.hidden,
|
||||||
});
|
});
|
||||||
if (this.state.hidden) {
|
if (this.state.hidden) {
|
||||||
return <div className={subListClasses}>
|
return <div ref="subList" className={subListClasses}>
|
||||||
{this._getHeaderJsx()}
|
{this._getHeaderJsx()}
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
const tiles = this.makeRoomTiles();
|
const tiles = this.makeRoomTiles();
|
||||||
tiles.push(...this.props.extraTiles);
|
tiles.push(...this.props.extraTiles);
|
||||||
return <div className={subListClasses}>
|
return <div ref="subList" className={subListClasses}>
|
||||||
{this._getHeaderJsx()}
|
{this._getHeaderJsx()}
|
||||||
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll">
|
<IndicatorScrollbar ref="scroller" className="mx_RoomSubList_scroll">
|
||||||
{ tiles }
|
{ tiles }
|
||||||
|
@ -343,7 +349,7 @@ const RoomSubList = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomSubList">
|
<div ref="subList" className="mx_RoomSubList">
|
||||||
{ this._getHeaderJsx() }
|
{ this._getHeaderJsx() }
|
||||||
{ content }
|
{ content }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -36,7 +36,8 @@ 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, RoomSubListDistributor} from '../../../resizer'
|
import {Resizer} from '../../../resizer'
|
||||||
|
import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2';
|
||||||
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))$/;
|
||||||
|
|
||||||
|
@ -79,6 +80,23 @@ module.exports = React.createClass({
|
||||||
const collapsedJson = window.localStorage.getItem("mx_roomlist_collapsed");
|
const collapsedJson = window.localStorage.getItem("mx_roomlist_collapsed");
|
||||||
this.subListSizes = sizesJson ? JSON.parse(sizesJson) : {};
|
this.subListSizes = sizesJson ? JSON.parse(sizesJson) : {};
|
||||||
this.collapsedState = collapsedJson ? JSON.parse(collapsedJson) : {};
|
this.collapsedState = collapsedJson ? JSON.parse(collapsedJson) : {};
|
||||||
|
this._layoutSections = [];
|
||||||
|
|
||||||
|
this._layout = new Layout((key, size) => {
|
||||||
|
const subList = this._subListRefs[key];
|
||||||
|
if (subList) {
|
||||||
|
subList.setHeight(size);
|
||||||
|
}
|
||||||
|
// update overflow indicators
|
||||||
|
this._checkSubListsOverflow();
|
||||||
|
// don't store height for collapsed sublists
|
||||||
|
if(!this.collapsedState[key]) {
|
||||||
|
this.subListSizes[key] = size;
|
||||||
|
window.localStorage.setItem("mx_roomlist_sizes",
|
||||||
|
JSON.stringify(this.subListSizes));
|
||||||
|
}
|
||||||
|
}, this.subListSizes, this.collapsedState);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isLoadingLeftRooms: false,
|
isLoadingLeftRooms: false,
|
||||||
totalRoomCount: null,
|
totalRoomCount: null,
|
||||||
|
@ -146,54 +164,38 @@ module.exports = React.createClass({
|
||||||
this._delayedRefreshRoomListLoopCount = 0;
|
this._delayedRefreshRoomListLoopCount = 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
_onSubListResize: function(newSize, id) {
|
|
||||||
if (!id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof newSize === "string") {
|
|
||||||
newSize = Number.MAX_SAFE_INTEGER;
|
|
||||||
}
|
|
||||||
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();
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
const cfg = {
|
const cfg = {
|
||||||
onResized: this._onSubListResize,
|
layout: this._layout,
|
||||||
};
|
};
|
||||||
this.resizer = new Resizer(this.resizeContainer, RoomSubListDistributor, cfg);
|
this.resizer = new Resizer(this.resizeContainer, Distributor, cfg);
|
||||||
this.resizer.setClassNames({
|
this.resizer.setClassNames({
|
||||||
handle: "mx_ResizeHandle",
|
handle: "mx_ResizeHandle",
|
||||||
vertical: "mx_ResizeHandle_vertical",
|
vertical: "mx_ResizeHandle_vertical",
|
||||||
reverse: "mx_ResizeHandle_reverse"
|
reverse: "mx_ResizeHandle_reverse"
|
||||||
});
|
});
|
||||||
|
this._layout.update(
|
||||||
// load stored sizes
|
this._layoutSections,
|
||||||
Object.keys(this.subListSizes).forEach((key) => {
|
this.resizeContainer && this.resizeContainer.offsetHeight,
|
||||||
this._restoreSubListSize(key);
|
);
|
||||||
});
|
|
||||||
this._checkSubListsOverflow();
|
this._checkSubListsOverflow();
|
||||||
|
|
||||||
this.resizer.attach();
|
this.resizer.attach();
|
||||||
|
window.addEventListener("resize", this.onWindowResize);
|
||||||
this.mounted = true;
|
this.mounted = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate: function(prevProps) {
|
componentDidUpdate: function(prevProps) {
|
||||||
this._repositionIncomingCallBox(undefined, false);
|
this._repositionIncomingCallBox(undefined, false);
|
||||||
if (this.props.searchFilter !== prevProps.searchFilter) {
|
// if (this.props.searchFilter !== prevProps.searchFilter) {
|
||||||
// restore sizes
|
// this._checkSubListsOverflow();
|
||||||
Object.keys(this.subListSizes).forEach((key) => {
|
// }
|
||||||
this._restoreSubListSize(key);
|
this._layout.update(
|
||||||
});
|
this._layoutSections,
|
||||||
this._checkSubListsOverflow();
|
this.resizeContainer && this.resizeContainer.clientHeight,
|
||||||
}
|
);
|
||||||
|
// TODO: call layout.setAvailableHeight, window height was changed when bannerShown prop was changed
|
||||||
},
|
},
|
||||||
|
|
||||||
onAction: function(payload) {
|
onAction: function(payload) {
|
||||||
|
@ -222,6 +224,7 @@ module.exports = React.createClass({
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
this.mounted = false;
|
this.mounted = false;
|
||||||
|
|
||||||
|
window.removeEventListener("resize", this.onWindowResize);
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
||||||
|
@ -251,6 +254,17 @@ module.exports = React.createClass({
|
||||||
this._delayedRefreshRoomList.cancelPendingCall();
|
this._delayedRefreshRoomList.cancelPendingCall();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onWindowResize: function() {
|
||||||
|
if (this.mounted && this._layout && this.resizeContainer &&
|
||||||
|
Array.isArray(this._layoutSections)
|
||||||
|
) {
|
||||||
|
this._layout.update(
|
||||||
|
this._layoutSections,
|
||||||
|
this.resizeContainer.offsetHeight
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onRoom: function(room) {
|
onRoom: function(room) {
|
||||||
this.updateVisibleRooms();
|
this.updateVisibleRooms();
|
||||||
},
|
},
|
||||||
|
@ -551,22 +565,16 @@ module.exports = React.createClass({
|
||||||
this.collapsedState[key] = collapsed;
|
this.collapsedState[key] = collapsed;
|
||||||
window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.collapsedState));
|
window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.collapsedState));
|
||||||
// load the persisted size configuration of the expanded sub list
|
// load the persisted size configuration of the expanded sub list
|
||||||
if (!collapsed) {
|
if (collapsed) {
|
||||||
this._restoreSubListSize(key);
|
this._layout.collapseSection(key);
|
||||||
|
} else {
|
||||||
|
this._layout.expandSection(key, this.subListSizes[key]);
|
||||||
}
|
}
|
||||||
// check overflow, as sub lists sizes have changed
|
// check overflow, as sub lists sizes have changed
|
||||||
// important this happens after calling resize above
|
// important this happens after calling resize above
|
||||||
this._checkSubListsOverflow();
|
this._checkSubListsOverflow();
|
||||||
},
|
},
|
||||||
|
|
||||||
_restoreSubListSize(key) {
|
|
||||||
const size = this.subListSizes[key];
|
|
||||||
const handle = this.resizer.forHandleWithId(key);
|
|
||||||
if (handle) {
|
|
||||||
handle.resize(size);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// check overflow for scroll indicator gradient
|
// check overflow for scroll indicator gradient
|
||||||
_checkSubListsOverflow() {
|
_checkSubListsOverflow() {
|
||||||
Object.values(this._subListRefs).forEach(l => l.checkOverflow());
|
Object.values(this._subListRefs).forEach(l => l.checkOverflow());
|
||||||
|
@ -581,6 +589,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_mapSubListProps: function(subListsProps) {
|
_mapSubListProps: function(subListsProps) {
|
||||||
|
this._layoutSections = [];
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
collapsed: this.props.collapsed,
|
collapsed: this.props.collapsed,
|
||||||
isFiltered: !!this.props.searchFilter,
|
isFiltered: !!this.props.searchFilter,
|
||||||
|
@ -599,6 +608,7 @@ module.exports = React.createClass({
|
||||||
return subListsProps.reduce((components, props, i) => {
|
return subListsProps.reduce((components, props, i) => {
|
||||||
props = Object.assign({}, defaultProps, props);
|
props = Object.assign({}, defaultProps, props);
|
||||||
const isLast = i === subListsProps.length - 1;
|
const isLast = i === subListsProps.length - 1;
|
||||||
|
const len = props.list.length + (props.extraTiles ? props.extraTiles.length : 0);
|
||||||
const {key, label, onHeaderClick, ... otherProps} = props;
|
const {key, label, onHeaderClick, ... otherProps} = props;
|
||||||
const chosenKey = key || label;
|
const chosenKey = key || label;
|
||||||
const onSubListHeaderClick = (collapsed) => {
|
const onSubListHeaderClick = (collapsed) => {
|
||||||
|
@ -608,7 +618,10 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const startAsHidden = props.startAsHidden || this.collapsedState[chosenKey];
|
const startAsHidden = props.startAsHidden || this.collapsedState[chosenKey];
|
||||||
|
this._layoutSections.push({
|
||||||
|
id: chosenKey,
|
||||||
|
count: len,
|
||||||
|
});
|
||||||
let subList = (<RoomSubList
|
let subList = (<RoomSubList
|
||||||
ref={this._subListRef.bind(this, chosenKey)}
|
ref={this._subListRef.bind(this, chosenKey)}
|
||||||
startAsHidden={startAsHidden}
|
startAsHidden={startAsHidden}
|
||||||
|
|
305
src/resizer/distributors/roomsublist2.js
Normal file
305
src/resizer/distributors/roomsublist2.js
Normal file
|
@ -0,0 +1,305 @@
|
||||||
|
/*
|
||||||
|
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";
|
||||||
|
|
||||||
|
// const allowWhitespace = true;
|
||||||
|
const handleHeight = 1;
|
||||||
|
|
||||||
|
function log() {
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(height, min, max) {
|
||||||
|
//log(`clamping ${height} between ${min} and ${max}`);
|
||||||
|
if (height > max) return max;
|
||||||
|
if (height < min) return min;
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Layout {
|
||||||
|
constructor(applyHeight, initialSizes, collapsedState) {
|
||||||
|
this._applyHeight = applyHeight;
|
||||||
|
this._sections = [];
|
||||||
|
this._collapsedState = Object.assign({}, collapsedState);
|
||||||
|
this._availableHeight = 0;
|
||||||
|
// heights stored by section section id
|
||||||
|
this._sectionHeights = Object.assign({}, initialSizes);
|
||||||
|
// in-progress heights, while dragging. Committed on mouse-up.
|
||||||
|
this._heights = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
setAvailableHeight(newSize) {
|
||||||
|
this._availableHeight = newSize;
|
||||||
|
// needs more work
|
||||||
|
this._applyNewSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
expandSection(id, height) {
|
||||||
|
this._collapsedState[id] = false;
|
||||||
|
this._applyNewSize();
|
||||||
|
this.openHandle(id).setHeight(height).finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
collapseSection(id) {
|
||||||
|
this._collapsedState[id] = true;
|
||||||
|
this._applyNewSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// [{id, count}]
|
||||||
|
update(sections, availableHeight) {
|
||||||
|
if (Number.isFinite(availableHeight)) {
|
||||||
|
this._availableHeight = availableHeight;
|
||||||
|
}
|
||||||
|
const totalHeight = this._getAvailableHeight();
|
||||||
|
this._sections.forEach((section, i) => {
|
||||||
|
if (!this._sectionHeights.hasOwnProperty(section.id)) {
|
||||||
|
this._sectionHeights[section.id] = clamp(
|
||||||
|
totalHeight / this._sections.length,
|
||||||
|
this._getMinHeight(i),
|
||||||
|
this._getMaxHeight(i),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this._sections = sections;
|
||||||
|
this._applyNewSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
openHandle(id) {
|
||||||
|
const index = this._getSectionIndex(id);
|
||||||
|
//log(`openHandle resolved ${id} to ${index}`);
|
||||||
|
return new Handle(this, index, this._sectionHeights[id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getAvailableHeight() {
|
||||||
|
const nonCollapsedSectionCount = this._sections.reduce((count, section) => {
|
||||||
|
const collapsed = this._collapsedState[section.id];
|
||||||
|
return count + (collapsed ? 0 : 1);
|
||||||
|
}, 0);
|
||||||
|
return this._availableHeight - ((nonCollapsedSectionCount - 1) * handleHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
_applyNewSize() {
|
||||||
|
const newHeight = this._getAvailableHeight();
|
||||||
|
const currHeight = this._sections.reduce((sum, section) => {
|
||||||
|
return sum + this._sectionHeights[section.id];
|
||||||
|
}, 0);
|
||||||
|
const offset = newHeight - currHeight;
|
||||||
|
this._heights = this._sections.map((section) => this._sectionHeights[section.id]);
|
||||||
|
const sections = this._sections.map((_, i) => i);
|
||||||
|
this._applyOverflow(-offset, sections, true);
|
||||||
|
this._applyHeights();
|
||||||
|
this._commitHeights();
|
||||||
|
}
|
||||||
|
|
||||||
|
_getSectionIndex(id) {
|
||||||
|
return this._sections.findIndex((s) => s.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getMaxHeight(i) {
|
||||||
|
const section = this._sections[i];
|
||||||
|
const collapsed = this._collapsedState[section.id];
|
||||||
|
|
||||||
|
if (collapsed) {
|
||||||
|
return this._sectionHeight(0);
|
||||||
|
} else {
|
||||||
|
return 100000;
|
||||||
|
// return this._sectionHeight(section.count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_sectionHeight(count) {
|
||||||
|
return 36 + (count === 0 ? 0 : 4 + (count * 34));
|
||||||
|
}
|
||||||
|
|
||||||
|
_getMinHeight(i) {
|
||||||
|
const section = this._sections[i];
|
||||||
|
const collapsed = this._collapsedState[section.id];
|
||||||
|
const maxItems = collapsed ? 0 : 1;
|
||||||
|
// log("_getMinHeight", i, section);
|
||||||
|
return this._sectionHeight(Math.min(section.count, maxItems));
|
||||||
|
}
|
||||||
|
|
||||||
|
_applyOverflow(overflow, sections, blend) {
|
||||||
|
//log("applyOverflow", overflow, sections);
|
||||||
|
// take the given overflow amount, and applies it to the given sections.
|
||||||
|
// calls itself recursively until it has distributed all the overflow
|
||||||
|
// or run out of unclamped sections.
|
||||||
|
|
||||||
|
const unclampedSections = [];
|
||||||
|
|
||||||
|
let overflowPerSection = blend ? (overflow / sections.length) : overflow;
|
||||||
|
for (const i of sections) {
|
||||||
|
const newHeight = clamp(
|
||||||
|
this._heights[i] - overflowPerSection,
|
||||||
|
this._getMinHeight(i),
|
||||||
|
this._getMaxHeight(i),
|
||||||
|
);
|
||||||
|
if (newHeight == this._heights[i] - overflowPerSection) {
|
||||||
|
unclampedSections.push(i);
|
||||||
|
}
|
||||||
|
// when section is growing, overflow increases?
|
||||||
|
// 100 -= 200 - 300
|
||||||
|
// 100 -= -100
|
||||||
|
// 200
|
||||||
|
overflow -= this._heights[i] - newHeight;
|
||||||
|
// console.log(`this._heights[${i}] (${this._heights[i]}) - newHeight (${newHeight}) = ${this._heights[i] - newHeight}`);
|
||||||
|
// console.log(`changing ${this._heights[i]} to ${newHeight}`);
|
||||||
|
this._heights[i] = newHeight;
|
||||||
|
// console.log(`for section ${i} overflow is ${overflow}`);
|
||||||
|
if (!blend) {
|
||||||
|
overflowPerSection = overflow;
|
||||||
|
if (Math.abs(overflow) < 1.0) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.abs(overflow) > 1.0 && unclampedSections.length > 0) {
|
||||||
|
// we weren't able to distribute all the overflow so recurse and try again
|
||||||
|
// log("recursing with", overflow, unclampedSections);
|
||||||
|
overflow = this._applyOverflow(overflow, unclampedSections, blend);
|
||||||
|
}
|
||||||
|
|
||||||
|
return overflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
_rebalanceAbove(anchor, overflowAbove) {
|
||||||
|
if (Math.abs(overflowAbove) > 1.0) {
|
||||||
|
// log(`trying to rebalance upstream with ${overflowAbove}`);
|
||||||
|
const sections = [];
|
||||||
|
for (let i = anchor - 1; i >= 0; i--) {
|
||||||
|
sections.push(i);
|
||||||
|
}
|
||||||
|
overflowAbove = this._applyOverflow(overflowAbove, sections);
|
||||||
|
}
|
||||||
|
return overflowAbove;
|
||||||
|
}
|
||||||
|
|
||||||
|
_rebalanceBelow(anchor, overflowBelow) {
|
||||||
|
if (Math.abs(overflowBelow) > 1.0) {
|
||||||
|
// log(`trying to rebalance downstream with ${overflowBelow}`);
|
||||||
|
const sections = [];
|
||||||
|
for (let i = anchor + 1; i < this._sections.length; i++) {
|
||||||
|
sections.push(i);
|
||||||
|
}
|
||||||
|
overflowBelow = this._applyOverflow(overflowBelow, sections);
|
||||||
|
//log(`rebalanced downstream with ${overflowBelow}`);
|
||||||
|
}
|
||||||
|
return overflowBelow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @param offset the amount the anchor is moved from what is stored in _sectionHeights, positive if downwards
|
||||||
|
// if we're clamped, return the offset we should be clamped at.
|
||||||
|
_relayout(anchor = 0, offset = 0, clamped = false) {
|
||||||
|
this._heights = this._sections.map((section) => this._sectionHeights[section.id]);
|
||||||
|
// are these the amounts the items above/below shrank/grew and need to be relayouted?
|
||||||
|
let overflowAbove;
|
||||||
|
let overflowBelow;
|
||||||
|
const maxHeight = this._getMaxHeight(anchor);
|
||||||
|
const minHeight = this._getMinHeight(anchor);
|
||||||
|
// new height > max ?
|
||||||
|
if (this._heights[anchor] + offset > maxHeight) {
|
||||||
|
// we're pulling downwards and clamped
|
||||||
|
// overflowAbove = minus how much are we above max height?
|
||||||
|
overflowAbove = (maxHeight - this._heights[anchor]) - offset;
|
||||||
|
overflowBelow = offset;
|
||||||
|
// log(`pulling downwards clamped at max: ${overflowAbove} ${overflowBelow}`);
|
||||||
|
} else if (this._heights[anchor] + offset < minHeight) { // new height < min?
|
||||||
|
// we're pulling upwards and clamped
|
||||||
|
// overflowAbove = ??? (offset is negative here, so - offset will add)
|
||||||
|
overflowAbove = (minHeight - this._heights[anchor]) - offset;
|
||||||
|
overflowBelow = offset;
|
||||||
|
// log(`pulling upwards clamped at min: ${overflowAbove} ${overflowBelow}`);
|
||||||
|
} else {
|
||||||
|
overflowAbove = 0;
|
||||||
|
overflowBelow = offset;
|
||||||
|
// log(`resizing the anchor: ${overflowAbove} ${overflowBelow}`);
|
||||||
|
}
|
||||||
|
this._heights[anchor] = clamp(this._heights[anchor] + offset, minHeight, maxHeight);
|
||||||
|
|
||||||
|
// these are reassigned the amount of overflow that could not be rebalanced
|
||||||
|
// meaning we dragged the handle too far and it can't follow the cursor anymore
|
||||||
|
overflowAbove = this._rebalanceAbove(anchor, overflowAbove);
|
||||||
|
overflowBelow = this._rebalanceBelow(anchor, overflowBelow);
|
||||||
|
|
||||||
|
if (!clamped) { // to avoid risk of infinite recursion
|
||||||
|
// clamp to avoid overflowing or underflowing the page
|
||||||
|
if (Math.abs(overflowAbove) > 1.0) {
|
||||||
|
// log(`clamping with overflowAbove ${overflowAbove}`);
|
||||||
|
// here we do the layout again with offset - the amount of space we took too much
|
||||||
|
this._relayout(anchor, offset + overflowAbove, true);
|
||||||
|
return offset + overflowAbove;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.abs(overflowBelow) > 1.0) {
|
||||||
|
// here we do the layout again with offset - the amount of space we took too much
|
||||||
|
// log(`clamping with overflowBelow ${overflowBelow}`);
|
||||||
|
this._relayout(anchor, offset - overflowBelow, true);
|
||||||
|
return offset - overflowBelow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._applyHeights();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
_applyHeights() {
|
||||||
|
log("updating layout, heights are now", this._heights);
|
||||||
|
// apply the heights
|
||||||
|
for (let i = 0; i < this._sections.length; i++) {
|
||||||
|
const section = this._sections[i];
|
||||||
|
this._applyHeight(section.id, this._heights[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_commitHeights() {
|
||||||
|
this._sections.forEach((section, i) => {
|
||||||
|
this._sectionHeights[section.id] = this._heights[i];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Handle {
|
||||||
|
constructor(layout, anchor, height) {
|
||||||
|
this._layout = layout;
|
||||||
|
this._anchor = anchor;
|
||||||
|
this._initialHeight = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
setHeight(height) {
|
||||||
|
this._layout._relayout(this._anchor, height - this._initialHeight);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
finish() {
|
||||||
|
this._layout._commitHeights();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Distributor extends FixedDistributor {
|
||||||
|
constructor(item, cfg) {
|
||||||
|
super(item);
|
||||||
|
const layout = cfg.layout;
|
||||||
|
this._handle = layout.openHandle(item.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
finish() {
|
||||||
|
this._handle.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
resize(height) {
|
||||||
|
this._handle.setHeight(height);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue