add resize handles between 3 main app columns

pull/21833/head
Bruno Windels 2018-09-24 16:07:42 +01:00
parent 313956dd99
commit 928b6d47c8
10 changed files with 372 additions and 9 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

@ -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;
}

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 {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;
@ -186,13 +196,13 @@ const LoggedInView = React.createClass({
_updateServerNoticeEvents: async function() {
const roomLists = RoomListStore.getRoomLists();
if (!roomLists['m.server_notice']) return [];
const pinnedEvents = [];
for (const room of roomLists['m.server_notice']) {
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue;
const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
for (const eventId of pinnedEventIds) {
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
@ -204,7 +214,7 @@ const LoggedInView = React.createClass({
serverNoticeEvents: pinnedEvents,
});
},
_onKeyDown: function(ev) {
/*
@ -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>

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

@ -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>;

View 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
View 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
View 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
View 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
View 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};