Merge pull request #2210 from matrix-org/bwindels/resizehandles

Redesign: resizeable/collapsible sections
This commit is contained in:
Bruno Windels 2018-10-19 13:27:55 +00:00 committed by GitHub
commit 933028120b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 671 additions and 159 deletions

View file

@ -57,6 +57,7 @@
@import "./views/elements/_MemberEventListSummary.scss";
@import "./views/elements/_ProgressBar.scss";
@import "./views/elements/_ReplyThread.scss";
@import "./views/elements/_ResizeHandle.scss";
@import "./views/elements/_RichText.scss";
@import "./views/elements/_RoleButton.scss";
@import "./views/elements/_Spinner.scss";

View file

@ -15,32 +15,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_LeftPanel {
position: relative;
display: flex;
flex-direction: column;
border-right: 1px solid $panel-divider-color;
}
.mx_LeftPanel_container {
display: flex;
/* LeftPanel 260px */
flex: 0 0 260px;
min-width: 260px;
flex: 0 0 auto;
}
.mx_LeftPanel_container.mx_LeftPanel_container_hasTagPanel {
/* TagPanel 70px + LeftPanel 260px */
flex: 0 0 330px;
}
.mx_LeftPanel_container_collapsed {
.mx_LeftPanel_container.collapsed {
min-width: unset;
/* Collapsed LeftPanel 70px */
flex: 0 0 70px;
}
.mx_LeftPanel_container_collapsed.mx_LeftPanel_container_hasTagPanel {
.mx_LeftPanel_container.collapsed.mx_LeftPanel_container_hasTagPanel {
/* TagPanel 70px + Collapsed LeftPanel 70px */
flex: 0 0 140px;
}
@ -57,6 +45,15 @@ limitations under the License.
}
.mx_LeftPanel {
background-color: $secondary-accent-color;
flex: 1;
position: relative;
overflow-x: hidden;
display: flex;
flex-direction: column;
}
.mx_LeftPanel .mx_AppTile_mini {
height: 132px;
}
@ -70,7 +67,7 @@ limitations under the License.
z-index: 6;
}
.mx_LeftPanel.collapsed .mx_BottomLeftMenu {
.mx_LeftPanel_container.collapsed .mx_BottomLeftMenu {
flex: 0 0 160px;
margin-bottom: 9px;
}
@ -93,7 +90,7 @@ limitations under the License.
pointer-events: none;
}
.collapsed .mx_RoleButton {
.mx_LeftPanel_container.collapsed .mx_RoleButton {
margin-right: 0px ! important;
padding-top: 3px ! important;
padding-bottom: 3px ! important;
@ -117,7 +114,7 @@ limitations under the License.
margin-right: 0px;
}
.mx_LeftPanel.collapsed .mx_BottomLeftMenu_settings {
.mx_LeftPanel_container.collapsed .mx_BottomLeftMenu_settings {
float: none;
}
@ -126,7 +123,7 @@ limitations under the License.
flex: 0 0 50px;
}
.mx_LeftPanel.collapsed .mx_BottomLeftMenu {
.mx_LeftPanel_container.collapsed .mx_BottomLeftMenu {
flex: 0 0 160px;
}

View file

@ -68,19 +68,7 @@ limitations under the License.
transform: translateX(-50%);
}
.mx_MatrixChat .mx_LeftPanel {
order: 1;
background-color: $secondary-accent-color;
flex: 0 0 260px;
}
.mx_MatrixChat .mx_LeftPanel.collapsed {
flex: 0 0 60px;
}
.mx_MatrixChat .mx_MatrixChat_middlePanel {
order: 2;
background-color: $primary-bg-color;
flex: 1;
@ -100,13 +88,3 @@ limitations under the License.
*/
height: 100%;
}
.mx_MatrixChat .mx_RightPanel {
order: 3;
flex: 0 0 235px;
}
.mx_MatrixChat .mx_RightPanel.collapsed {
flex: 0 0 122px;
}

View file

@ -15,8 +15,10 @@ limitations under the License.
*/
.mx_RightPanel {
overflow-x: hidden;
flex: 0 0 auto;
position: relative;
min-width: 250px;
display: flex;
flex-direction: column;
}

View file

@ -29,7 +29,6 @@ limitations under the License.
.mx_RoomSubList_labelContainer {
height: 31px; /* mx_RoomSubList_label height including border */
width: 235px; /* LHS Panel width */
position: relative;
}
@ -39,7 +38,6 @@ limitations under the License.
color: $roomsublist-label-fg-color;
font-weight: 700;
font-size: 12px;
width: 203px; /* padding + width = LHS Panel width */
height: 19px; /* height + padding = 31px = mx_RoomSubList_label height */
margin-left: 16px;
padding-left: 16px; /* gutter */
@ -57,15 +55,6 @@ limitations under the License.
/* pointer-events: none; */
}
.collapsed .mx_RoomSubList_label {
height: 17px;
width: 28px; /* collapsed LHS Panel width */
}
.collapsed .mx_RoomSubList_labelContainer {
width: 28px; /* collapsed LHS Panel width */
}
.mx_RoomSubList_roomCount {
display: inline-block;
font-size: 12px;
@ -75,10 +64,6 @@ limitations under the License.
text-transform: none;
}
.collapsed .mx_RoomSubList_roomCount {
display: none;
}
.mx_RoomSubList_badge {
display: inline-block;
min-width: 15px;
@ -101,12 +86,6 @@ limitations under the License.
filter: brightness($focus-brightness);
}
/*
.collapsed .mx_RoomSubList_badge {
display: none;
}
*/
.mx_RoomSubList_badgeHighlight {
background-color: $warning-color;
}
@ -123,11 +102,6 @@ limitations under the License.
border-right: 7px solid transparent;
}
/* Hide the bottom of speech bubble */
.collapsed .mx_RoomSubList_badgeHighlight:after {
display: none;
}
.mx_RoomSubList_chevron {
left: 0px;
pointer-events: none;
@ -165,10 +139,6 @@ limitations under the License.
background-color: $secondary-accent-color;
}
.collapsed .mx_RoomSubList_ellipsis {
height: 20px;
}
.mx_RoomSubList_line {
display: inline-block;
width: 159px;
@ -176,10 +146,6 @@ limitations under the License.
vertical-align: middle;
}
.collapsed .mx_RoomSubList_line {
display: none;
}
.mx_RoomSubList_more {
display: inline-block;
text-transform: uppercase;
@ -193,10 +159,6 @@ limitations under the License.
vertical-align: middle;
}
.collapsed .mx_RoomSubList_more {
display: none;
}
.mx_RoomSubList_moreBadge {
display: inline-block;
min-width: 15px;
@ -233,12 +195,6 @@ limitations under the License.
padding-right: 4px;
}
.collapsed .mx_RoomSubList_moreBadge {
position: static;
margin-left: 16px;
margin-top: 2px;
}
.mx_RoomSubList_ellipsis .mx_RoomSubList_chevronDown {
position: relative;
top: 4px;
@ -246,3 +202,40 @@ limitations under the License.
}
.collapsed {
.mx_RoomSubList_label {
height: 17px;
width: 28px; /* collapsed LHS Panel width */
}
.mx_RoomSubList_labelContainer {
width: 28px; /* collapsed LHS Panel width */
}
.mx_RoomSubList_roomCount {
display: none;
}
/* Hide the bottom of speech bubble */
.mx_RoomSubList_badgeHighlight:after {
display: none;
}
.mx_RoomSubList_line {
display: none;
}
.mx_RoomSubList_moreBadge {
position: static;
margin-left: 16px;
margin-top: 2px;
}
.mx_RoomSubList_ellipsis {
height: 20px;
}
.mx_RoomSubList_more {
display: none;
}
}

View file

@ -0,0 +1,32 @@
/*
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.
*/
.mx_ResizeHandle {
cursor: row-resize;
flex: 0 0 auto;
background: $panel-divider-color;
padding: 1px
}
.mx_ResizeHandle.mx_ResizeHandle_horizontal {
width: 1px;
cursor: col-resize;
}
.mx_ResizeHandle.mx_ResizeHandle_vertical {
height: 1px;
cursor: row-resize;
}

View file

@ -15,12 +15,13 @@ limitations under the License.
*/
.mx_RoomTile {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
display: block;
height: 40px;
margin: 0px 9px 0px 9px;
margin: 0px 3px;
position: relative;
background-color: $secondary-accent-color;
}
@ -31,26 +32,18 @@ limitations under the License.
left: -12px;
}
.mx_RoomTile_nameContainer {
display: inline-block;
width: 180px;
height: 24px;
}
.mx_RoomTile_avatar_container {
position: relative;
}
.mx_RoomTile_avatar {
display: inline-block;
flex: 0;
padding-top: 4px;
padding-bottom: 4px;
padding-left: 14px;
padding-right: 12px;
width: 32px;
height: 32px;
vertical-align: middle;
}
.mx_RoomTile_avatar_container {
position: relative;
}
.mx_RoomTile_dm {
@ -62,19 +55,13 @@ limitations under the License.
}
.mx_RoomTile_name {
display: inline-block;
flex: 1 5 auto;
font-size: 14px;
font-weight: 600;
position: relative;
width: 165px;
vertical-align: middle;
padding-left: 6px;
padding-right: 6px;
padding-top: 2px;
padding-bottom: 3px;
padding: 6px;
color: $roomtile-name-color;
white-space: nowrap;
overflow: hidden;
overflow-x: hidden;
text-overflow: ellipsis;
}
@ -82,25 +69,30 @@ limitations under the License.
/* color: rgba(69, 69, 69, 0.5); */
}
.collapsed .mx_RoomTile_nameContainer {
width: 60px; /* colapsed panel width */
}
.collapsed {
.mx_RoomTile_name {
display: none;
}
.collapsed .mx_RoomTile_name {
display: none;
}
.mx_RoomTile_badge {
min-width: 12px;
border-radius: 16px;
padding: 0px 4px 0px 4px;
z-index: 3;
}
.collapsed .mx_RoomTile_badge {
top: 0px;
min-width: 12px;
border-radius: 16px;
padding: 0px 4px 0px 4px;
z-index: 3;
}
/* Hide the bottom of speech bubble */
.mx_RoomTile_highlight .mx_RoomTile_badge:after {
display: none;
}
/* Hide the bottom of speech bubble */
.collapsed .mx_RoomTile_highlight .mx_RoomTile_badge:after {
display: none;
.mx_RoomTile_badge {
display: block;
position: absolute;
height: 15px;
right: 5px;
top: 2px;
}
}
/* This is the bottom of the speech bubble */
@ -116,12 +108,8 @@ limitations under the License.
}
.mx_RoomTile_badge {
display: inline-block;
flex: 0 1 content;
min-width: 15px;
height: 15px;
position: absolute;
right: 8px; /*gutter */
top: 9px;
border-radius: 8px;
color: $accent-fg-color;
font-weight: 600;

View file

@ -155,7 +155,7 @@ class Tinter {
tint(primaryColor, secondaryColor, tertiaryColor) {
return;
// eslint-disable-next-line no-unreachable
this.currentTint[0] = primaryColor;
this.currentTint[1] = secondaryColor;
this.currentTint[2] = tertiaryColor;

View file

@ -192,20 +192,13 @@ var LeftPanel = React.createClass({
topBox = <SearchBox collapsed={ this.props.collapsed } onSearch={ this.onSearch } />;
}
*/
const classes = classNames(
"mx_LeftPanel",
{
"collapsed": this.props.collapsed,
},
);
const tagPanelEnabled = !SettingsStore.getValue("TagPanel.disableTagPanel");
const tagPanel = tagPanelEnabled ? <TagPanel /> : <div />;
const containerClasses = classNames(
"mx_LeftPanel_container", "mx_fadable",
{
"mx_LeftPanel_container_collapsed": this.props.collapsed,
"collapsed": this.props.collapsed,
"mx_LeftPanel_container_hasTagPanel": tagPanelEnabled,
"mx_fadable_faded": this.props.disabled,
},
@ -214,7 +207,7 @@ var LeftPanel = React.createClass({
return (
<div className={containerClasses}>
{ tagPanel }
<aside className={classes} onKeyDown={ this._onKeyDown } onFocus={ this._onFocus } onBlur={ this._onBlur }>
<aside className={"mx_LeftPanel"} onKeyDown={ this._onKeyDown } onFocus={ this._onFocus } onBlur={ this._onBlur }>
{ topBox }
<CallPreview ConferenceHandler={VectorConferenceHandler} />
<RoomList

View file

@ -34,7 +34,8 @@ import RoomListStore from "../../stores/RoomListStore";
import TagOrderActions from '../../actions/TagOrderActions';
import RoomListActions from '../../actions/RoomListActions';
import ResizeHandle from '../views/elements/ResizeHandle';
import {Resizer, CollapseDistributor} from '../../resizer'
// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.
// NB. this is just for server notices rather than pinned messages in general.
@ -91,6 +92,12 @@ const LoggedInView = React.createClass({
};
},
componentDidMount: function() {
this.resizer = this._createResizer();
this.resizer.attach();
this._loadResizerPreferences();
},
componentWillMount: function() {
// stash the MatrixClient in case we log out before we are unmounted
this._matrixClient = this.props.matrixClient;
@ -120,6 +127,7 @@ const LoggedInView = React.createClass({
if (this._sessionStoreToken) {
this._sessionStoreToken.remove();
}
this.resizer.detach();
},
// Child components assume that the client peg will not be null, so give them some
@ -145,6 +153,49 @@ const LoggedInView = React.createClass({
});
},
_createResizer() {
const classNames = {
handle: "mx_ResizeHandle",
vertical: "mx_ResizeHandle_vertical",
reverse: "mx_ResizeHandle_reverse"
};
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');
}
}
},
onResized: (size, item) => {
if (item.classList.contains("mx_LeftPanel_container")) {
window.localStorage.setItem("mx_lhs_size", '' + size);
} else if(item.classList.contains("mx_RightPanel")) {
window.localStorage.setItem("mx_rhs_size", '' + size);
}
},
};
const resizer = new Resizer(
this.resizeContainer,
CollapseDistributor,
collapseConfig);
resizer.setClassNames(classNames);
return resizer;
},
_loadResizerPreferences() {
const lhsSize = window.localStorage.getItem("mx_lhs_size");
if (lhsSize !== null) {
this.resizer.forHandleAt(0).resize(parseInt(lhsSize, 10));
}
const rhsSize = window.localStorage.getItem("mx_rhs_size");
if (rhsSize !== null) {
this.resizer.forHandleAt(1).resize(parseInt(rhsSize, 10));
}
},
onAccountData: function(event) {
if (event.getType() === "im.vector.web.settings") {
this.setState({
@ -361,6 +412,10 @@ const LoggedInView = React.createClass({
this.setState({mouseDown: null});
},
_setResizeContainerRef(div) {
this.resizeContainer = div;
},
render: function() {
const LeftPanel = sdk.getComponent('structures.LeftPanel');
const RightPanel = sdk.getComponent('structures.RightPanel');
@ -507,14 +562,16 @@ const LoggedInView = React.createClass({
<div className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers} onMouseDown={this._onMouseDown} onMouseUp={this._onMouseUp}>
{ topBar }
<DragDropContext onDragEnd={this._onDragEnd}>
<div className={bodyClasses}>
<div ref={this._setResizeContainerRef} className={bodyClasses}>
<LeftPanel
collapsed={this.props.collapseLhs || false}
collapsed={this.props.collapseLhs || this.state.collapseLhs || false}
disabled={this.props.leftDisabled}
/>
<ResizeHandle/>
<main className='mx_MatrixChat_middlePanel'>
{ page_element }
</main>
<ResizeHandle reverse={true}/>
{ right_panel }
</div>
</DragDropContext>

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../index';
import dis from '../../dispatcher';
class TopLeftMenu extends React.Component {
@ -30,7 +29,7 @@ class TopLeftMenu extends React.Component {
render() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const avatarHeight = 28;
const name = "My stuff"
const name = "My stuff";
return (
<div className="mx_TopLeftMenu">
@ -43,7 +42,7 @@ class TopLeftMenu extends React.Component {
<div className="mx_TopLeftMenu_name">
{ name }
</div>
<img className="mx_TopLeftMenu_chevron" src="img/topleft-chevron.svg" width="11" height="6"/>
<img className="mx_TopLeftMenu_chevron" src="img/topleft-chevron.svg" width="11" height="6" />
</div>
);
}

View file

@ -0,0 +1,26 @@
import React from 'react'; // eslint-disable-line no-unused-vars
import PropTypes from 'prop-types';
//see src/resizer for the actual resizing code, this is just the DOM for the resize handle
const ResizeHandle = (props) => {
const classNames = ['mx_ResizeHandle'];
if (props.vertical) {
classNames.push('mx_ResizeHandle_vertical');
} else {
classNames.push('mx_ResizeHandle_horizontal');
}
if (props.reverse) {
classNames.push('mx_ResizeHandle_reverse');
}
return (
<div className={classNames.join(' ')} />
);
};
ResizeHandle.propTypes = {
vertical: PropTypes.bool,
reverse: PropTypes.bool,
};
export default ResizeHandle;

View file

@ -337,10 +337,8 @@ module.exports = React.createClass({
{ dmIndicator }
</div>
</div>
<div className="mx_RoomTile_nameContainer">
{ label }
{ badge }
</div>
{ label }
{ badge }
{ /* { incomingCallBox } */ }
{ tooltip }
</AccessibleButton>;

128
src/resizer/distributors.js Normal file
View file

@ -0,0 +1,128 @@
/*
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 one method, `resize` that receives
the offset from the container edge of where
the mouse cursor is.
*/
class FixedDistributor {
constructor(sizer, item, config) {
this.sizer = sizer;
this.item = item;
this.beforeOffset = sizer.getItemOffset(this.item);
this.onResized = config.onResized;
}
resize(offset) {
const itemSize = offset - this.beforeOffset;
this.sizer.setItemSize(this.item, itemSize);
if (this.onResized) {
this.onResized(itemSize, this.item);
}
return itemSize;
}
sizeFromOffset(offset) {
return offset - this.beforeOffset;
}
}
class CollapseDistributor extends FixedDistributor {
constructor(sizer, item, config) {
super(sizer, item, config);
this.toggleSize = config && config.toggleSize;
this.onCollapsed = config && config.onCollapsed;
this.isCollapsed = false;
}
resize(offset) {
const newSize = this.sizeFromOffset(offset);
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(offset);
}
}
}
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,
};

27
src/resizer/index.js Normal file
View file

@ -0,0 +1,27 @@
/*
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, CollapseDistributor, PercentageDistributor} from "./distributors";
import {Resizer} from "./resizer";
module.exports = {
Resizer,
Sizer,
FixedDistributor,
CollapseDistributor,
PercentageDistributor,
};

138
src/resizer/resizer.js Normal file
View file

@ -0,0 +1,138 @@
/*
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";
/*
classNames:
// class on resize-handle
handle: string
// class on resize-handle
reverse: string
// class on resize-handle
vertical: string
// class on container
resizing: string
*/
export class Resizer {
constructor(container, distributorCtor, distributorCfg, sizerCtor = Sizer) {
this.container = container;
this.distributorCtor = distributorCtor;
this.distributorCfg = distributorCfg;
this.sizerCtor = sizerCtor;
this.classNames = {
handle: "resizer-handle",
reverse: "resizer-reverse",
vertical: "resizer-vertical",
resizing: "resizer-resizing",
};
this._onMouseDown = this._onMouseDown.bind(this);
}
setClassNames(classNames) {
this.classNames = classNames;
}
attach() {
this.container.addEventListener("mousedown", this._onMouseDown, false);
}
detach() {
this.container.removeEventListener("mousedown", this._onMouseDown, false);
}
/**
Gives the distributor for a specific resize handle, as if you would have started
to drag that handle. Can be used to manipulate the size of an item programmatically.
@param {number} handleIndex the index of the resize handle in the container
@return {Distributor} a new distributor for the given handle
*/
forHandleAt(handleIndex) {
const handles = this._getResizeHandles();
const handle = handles[handleIndex];
const {distributor} = this._createSizerAndDistributor(handle);
return distributor;
}
_isResizeHandle(el) {
return el && el.classList.contains(this.classNames.handle);
}
_onMouseDown(event) {
const target = event.target;
if (!this._isResizeHandle(target) || target.parentElement !== this.container) {
return;
}
// prevent starting a drag operation
event.preventDefault();
// mark as currently resizing
if (this.classNames.resizing) {
this.container.classList.add(this.classNames.resizing);
}
const {sizer, distributor} = this._createSizerAndDistributor(target);
const onMouseMove = (event) => {
const offset = sizer.offsetFromEvent(event);
distributor.resize(offset);
};
const body = document.body;
const onMouseUp = (event) => {
if (this.classNames.resizing) {
this.container.classList.remove(this.classNames.resizing);
}
body.removeEventListener("mouseup", onMouseUp, false);
body.removeEventListener("mousemove", onMouseMove, false);
};
body.addEventListener("mouseup", onMouseUp, 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];
// eslint-disable-next-line new-cap
const distributor = new this.distributorCtor(
sizer, item, this.distributorCfg,
items, this.container);
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);
});
}
}

55
src/resizer/room.js Normal file
View file

@ -0,0 +1,55 @@
/*
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) {
item.style.flex = null;
if (size === "show-content") {
cl.add("show-content");
cl.remove("show-available");
item.style.maxHeight = null;
}
} else {
cl.add("show-available");
//item.style.flex = `0 1 ${Math.round(size)}px`;
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");
} else {
this.sizer.setItemSize(this.item, itemSize);
}
}
}
module.exports = {
RoomSizer,
RoomDistributor,
};

100
src/resizer/sizer.js Normal file
View file

@ -0,0 +1,100 @@
/*
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.
*/
/**
implements DOM/CSS operations for resizing.
The sizer determines what CSS mechanism is used for sizing items, like flexbox, ...
*/
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
*/
getItemOffset(item) {
const offset = (this.vertical ? item.offsetTop : item.offsetLeft) - this._getOffset();
if (this.reverse) {
return this.getTotalSize() - (offset + this.getItemSize(item));
} else {
return offset;
}
}
/**
@param {Element} item the dom element being resized
@return {number} the width/height of an item in the container
*/
getItemSize(item) {
return this.vertical ? item.offsetHeight : item.offsetWidth;
}
/** @return {number} the width/height of the container */
getTotalSize() {
return this.vertical ? this.container.offsetHeight : this.container.offsetWidth;
}
/** @return {number} container offset to offsetParent */
_getOffset() {
return this.vertical ? this.container.offsetTop : this.container.offsetLeft;
}
setItemSize(item, size) {
if (this.vertical) {
item.style.height = `${Math.round(size)}px`;
} else {
item.style.width = `${Math.round(size)}px`;
}
}
/**
@param {MouseEvent} event the mouse event
@return {number} the distance between the cursor and the edge of the container,
along the applicable axis (vertical or horizontal)
*/
offsetFromEvent(event) {
const pos = this.vertical ? event.pageY : event.pageX;
if (this.reverse) {
return (this._getOffset() + this.getTotalSize()) - pos;
} else {
return pos - this._getOffset();
}
}
}
module.exports = {Sizer};