add resize handles between 3 main app columns
This commit is contained in:
parent
313956dd99
commit
928b6d47c8
10 changed files with 372 additions and 9 deletions
|
@ -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";
|
||||
|
|
40
res/css/views/elements/_ResizeHandle.scss
Normal file
40
res/css/views/elements/_ResizeHandle.scss
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
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: blue;
|
||||
padding: 1px
|
||||
}
|
||||
|
||||
.mx_ResizeHandle.mx_ResizeHandle_horizontal {
|
||||
width: 1px;
|
||||
cursor: e-resize;
|
||||
}
|
||||
|
||||
.mx_ResizeHandle.mx_ResizeHandle_vertical {
|
||||
height: 1px;
|
||||
cursor: s-resize;
|
||||
}
|
||||
|
||||
.mx_ResizeHandle.mx_ResizeHandle_horizontal.mx_ResizeHandle_reverse {
|
||||
cursor: w-resize;
|
||||
}
|
||||
|
||||
.mx_ResizeHandle.mx_ResizeHandle_vertical.mx_ResizeHandle_reverse {
|
||||
cursor: n-resize;
|
||||
}
|
|
@ -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 {makeResizeable, FixedDistributor} 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,15 @@ const LoggedInView = React.createClass({
|
|||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
const classNames = {
|
||||
handle: "mx_ResizeHandle",
|
||||
vertical: "mx_ResizeHandle_vertical",
|
||||
reverse: "mx_ResizeHandle_reverse"
|
||||
};
|
||||
makeResizeable(this.resizeContainer, classNames, FixedDistributor);
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
// stash the MatrixClient in case we log out before we are unmounted
|
||||
this._matrixClient = this.props.matrixClient;
|
||||
|
@ -481,14 +491,16 @@ const LoggedInView = React.createClass({
|
|||
<div className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers} onClick={this._onClick}>
|
||||
{ topBar }
|
||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||
<div className={bodyClasses}>
|
||||
<div ref={(div) => this.resizeContainer = div} className={bodyClasses}>
|
||||
<LeftPanel
|
||||
collapsed={this.props.collapseLhs || false}
|
||||
disabled={this.props.leftDisabled}
|
||||
/>
|
||||
<ResizeHandle/>
|
||||
<main className='mx_MatrixChat_middlePanel'>
|
||||
{ page_element }
|
||||
</main>
|
||||
<ResizeHandle reverse={true}/>
|
||||
{ right_panel }
|
||||
</div>
|
||||
</DragDropContext>
|
||||
|
|
26
src/components/views/elements/ResizeHandle.js
Normal file
26
src/components/views/elements/ResizeHandle.js
Normal 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;
|
|
@ -243,6 +243,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
this.state.badgeHover = true;
|
||||
const isInvite = this.props.room.getMyMembership() === "invite";
|
||||
const notificationCount = this.state.notificationCount;
|
||||
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
|
||||
|
@ -337,10 +338,8 @@ module.exports = React.createClass({
|
|||
{ dmIndicator }
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_RoomTile_nameContainer">
|
||||
{ label }
|
||||
{ badge }
|
||||
</div>
|
||||
{ label }
|
||||
{ badge }
|
||||
{ /* { incomingCallBox } */ }
|
||||
{ tooltip }
|
||||
</AccessibleButton>;
|
||||
|
|
96
src/resizer/distributors.js
Normal file
96
src/resizer/distributors.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
class FixedDistributor {
|
||||
constructor(container, items, handleIndex, direction, sizer) {
|
||||
this.item = items[handleIndex + direction];
|
||||
this.beforeOffset = sizer.getItemOffset(this.item);
|
||||
this.sizer = sizer;
|
||||
}
|
||||
|
||||
resize(offset) {
|
||||
const itemSize = offset - this.beforeOffset;
|
||||
this.sizer.setItemSize(this.item, itemSize);
|
||||
return itemSize;
|
||||
}
|
||||
|
||||
finish(_offset) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class CollapseDistributor extends FixedDistributor {
|
||||
constructor(container, items, handleIndex, direction, sizer) {
|
||||
super(container, items, handleIndex, direction, sizer);
|
||||
const style = getComputedStyle(this.item);
|
||||
this.minWidth = parseInt(style.minWidth, 10); //auto becomes NaN
|
||||
}
|
||||
|
||||
resize(offset) {
|
||||
let newWidth = offset - this.sizer.getItemOffset(this.item);
|
||||
if (this.minWidth > 0) {
|
||||
if (offset < this.minWidth + 50) {
|
||||
this.item.classList.add("collapsed");
|
||||
newWidth = this.minWidth;
|
||||
}
|
||||
else {
|
||||
this.item.classList.remove("collapsed");
|
||||
}
|
||||
}
|
||||
super.resize(newWidth);
|
||||
}
|
||||
}
|
||||
|
||||
class PercentageDistributor {
|
||||
|
||||
constructor(container, items, handleIndex, direction, sizer) {
|
||||
this.container = container;
|
||||
this.totalSize = sizer.getTotalSize();
|
||||
this.sizer = sizer;
|
||||
|
||||
this.beforeItems = items.slice(0, handleIndex);
|
||||
this.afterItems = items.slice(handleIndex);
|
||||
const percentages = PercentageDistributor._getPercentages(sizer, items);
|
||||
this.beforePercentages = percentages.slice(0, handleIndex);
|
||||
this.afterPercentages = percentages.slice(handleIndex);
|
||||
}
|
||||
|
||||
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]);
|
||||
});
|
||||
}
|
||||
|
||||
finish(_offset) {
|
||||
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
70
src/resizer/event.js
Normal file
70
src/resizer/event.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
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
|
||||
*/
|
||||
|
||||
function makeResizeable(container, classNames, distributorCtor, sizerCtor = Sizer) {
|
||||
|
||||
function isResizeHandle(el) {
|
||||
return el && el.classList.contains(classNames.handle);
|
||||
}
|
||||
|
||||
function handleMouseDown(event) {
|
||||
const target = event.target;
|
||||
if (!isResizeHandle(target) || target.parentElement !== container) {
|
||||
return;
|
||||
}
|
||||
// prevent starting a drag operation
|
||||
event.preventDefault();
|
||||
// mark as currently resizing
|
||||
if (classNames.resizing) {
|
||||
container.classList.add(classNames.resizing);
|
||||
}
|
||||
|
||||
const resizeHandle = event.target;
|
||||
const vertical = resizeHandle.classList.contains(classNames.vertical);
|
||||
const reverse = resizeHandle.classList.contains(classNames.reverse);
|
||||
const direction = reverse ? 0 : -1;
|
||||
|
||||
const sizer = new sizerCtor(container, vertical, reverse);
|
||||
|
||||
const items = Array.from(container.children).filter(el => {
|
||||
return !isResizeHandle(el) && (
|
||||
isResizeHandle(el.previousElementSibling) ||
|
||||
isResizeHandle(el.nextElementSibling));
|
||||
});
|
||||
const prevItem = resizeHandle.previousElementSibling;
|
||||
const handleIndex = items.indexOf(prevItem) + 1;
|
||||
const distributor = new distributorCtor(container, items, handleIndex, direction, sizer);
|
||||
|
||||
const onMouseMove = (event) => {
|
||||
const offset = sizer.offsetFromEvent(event);
|
||||
distributor.resize(offset);
|
||||
};
|
||||
|
||||
const body = document.body;
|
||||
const onMouseUp = (event) => {
|
||||
if (classNames.resizing) {
|
||||
container.classList.remove(classNames.resizing);
|
||||
}
|
||||
const offset = sizer.offsetFromEvent(event);
|
||||
distributor.finish(offset);
|
||||
body.removeEventListener("mouseup", onMouseUp, false);
|
||||
body.removeEventListener("mousemove", onMouseMove, false);
|
||||
};
|
||||
body.addEventListener("mouseup", onMouseUp, false);
|
||||
body.addEventListener("mousemove", onMouseMove, false);
|
||||
}
|
||||
container.addEventListener("mousedown", handleMouseDown, false);
|
||||
}
|
||||
|
||||
module.exports = {makeResizeable};
|
10
src/resizer/index.js
Normal file
10
src/resizer/index.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import {Sizer} from "./sizer";
|
||||
import {FixedDistributor, PercentageDistributor} from "./distributors";
|
||||
import {makeResizeable} from "./event";
|
||||
|
||||
module.exports = {
|
||||
makeResizeable,
|
||||
Sizer,
|
||||
FixedDistributor,
|
||||
PercentageDistributor,
|
||||
};
|
39
src/resizer/room.js
Normal file
39
src/resizer/room.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
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,
|
||||
};
|
70
src/resizer/sizer.js
Normal file
70
src/resizer/sizer.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
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);
|
||||
}
|
||||
|
||||
/** returns 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;
|
||||
}
|
||||
}
|
||||
|
||||
/** returns the width/height of an item in the container */
|
||||
getItemSize(item) {
|
||||
return this.vertical ? item.offsetHeight : item.offsetWidth;
|
||||
}
|
||||
|
||||
/** returns the width/height of the container */
|
||||
getTotalSize() {
|
||||
return this.vertical ? this.container.offsetHeight : this.container.offsetWidth;
|
||||
}
|
||||
|
||||
/** 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`;
|
||||
}
|
||||
}
|
||||
|
||||
/** returns the position of cursor at event relative to the edge of the container */
|
||||
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};
|
Loading…
Reference in a new issue