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/_MemberEventListSummary.scss";
|
||||||
@import "./views/elements/_ProgressBar.scss";
|
@import "./views/elements/_ProgressBar.scss";
|
||||||
@import "./views/elements/_ReplyThread.scss";
|
@import "./views/elements/_ReplyThread.scss";
|
||||||
|
@import "./views/elements/_ResizeHandle.scss";
|
||||||
@import "./views/elements/_RichText.scss";
|
@import "./views/elements/_RichText.scss";
|
||||||
@import "./views/elements/_RoleButton.scss";
|
@import "./views/elements/_RoleButton.scss";
|
||||||
@import "./views/elements/_Spinner.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 TagOrderActions from '../../actions/TagOrderActions';
|
||||||
import RoomListActions from '../../actions/RoomListActions';
|
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)
|
// 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.
|
// 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.
|
// 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() {
|
componentWillMount: function() {
|
||||||
// stash the MatrixClient in case we log out before we are unmounted
|
// stash the MatrixClient in case we log out before we are unmounted
|
||||||
this._matrixClient = this.props.matrixClient;
|
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}>
|
<div className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers} onClick={this._onClick}>
|
||||||
{ topBar }
|
{ topBar }
|
||||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||||
<div className={bodyClasses}>
|
<div ref={(div) => this.resizeContainer = div} className={bodyClasses}>
|
||||||
<LeftPanel
|
<LeftPanel
|
||||||
collapsed={this.props.collapseLhs || false}
|
collapsed={this.props.collapseLhs || false}
|
||||||
disabled={this.props.leftDisabled}
|
disabled={this.props.leftDisabled}
|
||||||
/>
|
/>
|
||||||
|
<ResizeHandle/>
|
||||||
<main className='mx_MatrixChat_middlePanel'>
|
<main className='mx_MatrixChat_middlePanel'>
|
||||||
{ page_element }
|
{ page_element }
|
||||||
</main>
|
</main>
|
||||||
|
<ResizeHandle reverse={true}/>
|
||||||
{ right_panel }
|
{ right_panel }
|
||||||
</div>
|
</div>
|
||||||
</DragDropContext>
|
</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() {
|
render: function() {
|
||||||
|
this.state.badgeHover = true;
|
||||||
const isInvite = this.props.room.getMyMembership() === "invite";
|
const isInvite = this.props.room.getMyMembership() === "invite";
|
||||||
const notificationCount = this.state.notificationCount;
|
const notificationCount = this.state.notificationCount;
|
||||||
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
|
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
|
||||||
|
@ -337,10 +338,8 @@ module.exports = React.createClass({
|
||||||
{ dmIndicator }
|
{ dmIndicator }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_RoomTile_nameContainer">
|
{ label }
|
||||||
{ label }
|
{ badge }
|
||||||
{ badge }
|
|
||||||
</div>
|
|
||||||
{ /* { incomingCallBox } */ }
|
{ /* { incomingCallBox } */ }
|
||||||
{ tooltip }
|
{ tooltip }
|
||||||
</AccessibleButton>;
|
</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