add resize handles between 3 main app columns

This commit is contained in:
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

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