From 928b6d47c8449b1b784e4ea04606085ab8dc446c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 24 Sep 2018 16:07:42 +0100 Subject: [PATCH] add resize handles between 3 main app columns --- res/css/_components.scss | 1 + res/css/views/elements/_ResizeHandle.scss | 40 ++++++++ src/components/structures/LoggedInView.js | 22 ++++- src/components/views/elements/ResizeHandle.js | 26 +++++ src/components/views/rooms/RoomTile.js | 7 +- src/resizer/distributors.js | 96 +++++++++++++++++++ src/resizer/event.js | 70 ++++++++++++++ src/resizer/index.js | 10 ++ src/resizer/room.js | 39 ++++++++ src/resizer/sizer.js | 70 ++++++++++++++ 10 files changed, 372 insertions(+), 9 deletions(-) create mode 100644 res/css/views/elements/_ResizeHandle.scss create mode 100644 src/components/views/elements/ResizeHandle.js create mode 100644 src/resizer/distributors.js create mode 100644 src/resizer/event.js create mode 100644 src/resizer/index.js create mode 100644 src/resizer/room.js create mode 100644 src/resizer/sizer.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 1e1d3e6596..c43d9edc16 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -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"; diff --git a/res/css/views/elements/_ResizeHandle.scss b/res/css/views/elements/_ResizeHandle.scss new file mode 100644 index 0000000000..550dcff911 --- /dev/null +++ b/res/css/views/elements/_ResizeHandle.scss @@ -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; +} diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 0c4688a411..174a742c44 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -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({
{ topBar } -
+
this.resizeContainer = div} className={bodyClasses}> +
{ page_element }
+ { right_panel }
diff --git a/src/components/views/elements/ResizeHandle.js b/src/components/views/elements/ResizeHandle.js new file mode 100644 index 0000000000..ae324a752b --- /dev/null +++ b/src/components/views/elements/ResizeHandle.js @@ -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 ( +
+ ); +}; + +ResizeHandle.propTypes = { + vertical: PropTypes.bool, + reverse: PropTypes.bool, +}; + +export default ResizeHandle; diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index a8552aa142..e70ea210f4 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -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 }
-
- { label } - { badge } -
+ { label } + { badge } { /* { incomingCallBox } */ } { tooltip } ; diff --git a/src/resizer/distributors.js b/src/resizer/distributors.js new file mode 100644 index 0000000000..bef3377df2 --- /dev/null +++ b/src/resizer/distributors.js @@ -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, +}; diff --git a/src/resizer/event.js b/src/resizer/event.js new file mode 100644 index 0000000000..3baa67e097 --- /dev/null +++ b/src/resizer/event.js @@ -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}; diff --git a/src/resizer/index.js b/src/resizer/index.js new file mode 100644 index 0000000000..923471b1f2 --- /dev/null +++ b/src/resizer/index.js @@ -0,0 +1,10 @@ +import {Sizer} from "./sizer"; +import {FixedDistributor, PercentageDistributor} from "./distributors"; +import {makeResizeable} from "./event"; + +module.exports = { + makeResizeable, + Sizer, + FixedDistributor, + PercentageDistributor, +}; diff --git a/src/resizer/room.js b/src/resizer/room.js new file mode 100644 index 0000000000..0080cca3eb --- /dev/null +++ b/src/resizer/room.js @@ -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, +}; diff --git a/src/resizer/sizer.js b/src/resizer/sizer.js new file mode 100644 index 0000000000..2dc116714f --- /dev/null +++ b/src/resizer/sizer.js @@ -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};