From 8b8ee21765722ebebf9558bf293e70881000c7a1 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 5 Apr 2016 13:14:11 +0100 Subject: [PATCH] Catch pageup/down and ctrl-home/end at the top level Make the scroll keys work when the focus is outside the message panel. --- src/KeyCode.js | 32 +++++++++++ src/components/structures/MatrixChat.js | 55 ++++++++++++++----- src/components/structures/MessagePanel.js | 28 ++++++++++ src/components/structures/RoomView.js | 18 ++++++ src/components/structures/ScrollPanel.js | 51 +++++++++++++++++ src/components/structures/TimelinePanel.js | 18 ++++++ .../views/rooms/MessageComposerInput.js | 10 +--- 7 files changed, 189 insertions(+), 23 deletions(-) create mode 100644 src/KeyCode.js diff --git a/src/KeyCode.js b/src/KeyCode.js new file mode 100644 index 0000000000..b80703d39e --- /dev/null +++ b/src/KeyCode.js @@ -0,0 +1,32 @@ +/* +Copyright 2016 OpenMarket 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. +*/ + +/* a selection of key codes, as used in KeyboardEvent.keyCode */ +module.exports = { + BACKSPACE: 8, + TAB: 9, + ENTER: 13, + SHIFT: 16, + PAGE_UP: 33, + PAGE_DOWN: 34, + END: 35, + HOME: 36, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + DELETE: 46, +}; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 4ee45c2034..497251e5aa 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -35,6 +35,7 @@ var Tinter = require("../../Tinter"); var sdk = require('../../index'); var MatrixTools = require('../../MatrixTools'); var linkifyMatrix = require("../../linkify-matrix"); +var KeyCode = require('../../KeyCode'); module.exports = React.createClass({ displayName: 'MatrixChat', @@ -722,11 +723,10 @@ module.exports = React.createClass({ }, onKeyDown: function(ev) { - if (ev.altKey) { /* // Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers // Will need to find a better meta key if anyone actually cares about using this. - if (ev.ctrlKey && ev.keyCode > 48 && ev.keyCode < 58) { + if (ev.altKey && ev.ctrlKey && ev.keyCode > 48 && ev.keyCode < 58) { dis.dispatch({ action: 'view_indexed_room', roomIndex: ev.keyCode - 49, @@ -736,18 +736,45 @@ module.exports = React.createClass({ return; } */ - switch (ev.keyCode) { - case 38: - dis.dispatch({action: 'view_prev_room'}); - ev.stopPropagation(); - ev.preventDefault(); - break; - case 40: - dis.dispatch({action: 'view_next_room'}); - ev.stopPropagation(); - ev.preventDefault(); - break; - } + + var handled = false; + + switch (ev.keyCode) { + case KeyCode.UP: + case KeyCode.DOWN: + if (ev.altKey) { + var action = ev.keyCode == KeyCode.UP ? + 'view_prev_room' : 'view_next_room'; + dis.dispatch({action: action}); + handled = true; + } + break; + + case KeyCode.PAGE_UP: + case KeyCode.PAGE_DOWN: + this._onScrollKeyPressed(ev); + handled = true; + break; + + case KeyCode.HOME: + case KeyCode.END: + if (ev.ctrlKey) { + this._onScrollKeyPressed(ev); + handled = true; + } + break; + } + + if (handled) { + ev.stopPropagation(); + ev.preventDefault(); + } + }, + + /** dispatch a page-up/page-down/etc to the appropriate component */ + _onScrollKeyPressed(ev) { + if (this.refs.roomView) { + this.refs.roomView.handleScrollKey(ev); } }, diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index d74fe202be..6c4c2a0acb 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -132,6 +132,14 @@ module.exports = React.createClass({ } }, + /* jump to the top of the content. + */ + scrollToTop: function() { + if (this.refs.scrollPanel) { + this.refs.scrollPanel.scrollToTop(); + } + }, + /* jump to the bottom of the content. */ scrollToBottom: function() { @@ -139,6 +147,26 @@ module.exports = React.createClass({ this.refs.scrollPanel.scrollToBottom(); } }, + + /** + * Page up/down. + * + * mult: -1 to page up, +1 to page down + */ + scrollRelative: function(mult) { + if (this.refs.scrollPanel) { + this.refs.scrollPanel.scrollRelative(mult); + } + }, + + /** + * Scroll up/down in response to a scroll key + */ + handleScrollKey: function(ev) { + if (this.refs.scrollPanel) { + this.refs.handleScrollKey(ev); + } + }, /* jump to the given event id. * diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index b5c34de20d..7128faf3d7 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1115,6 +1115,24 @@ module.exports = React.createClass({ } }, + /** + * called by the parent component when PageUp/Down/etc is pressed. + * + * We pass it down to the scroll panel. + */ + handleScrollKey: function(ev) { + var panel; + if(this.refs.searchResultsPanel) { + panel = this.refs.searchResultsPanel; + } else if(this.refs.messagePanel) { + panel = this.refs.messagePanel; + } + + if(panel) { + panel.handleScrollKey(ev); + } + }, + // this has to be a proper method rather than an unnamed function, // otherwise react calls it with null on each update. _gatherTimelinePanelRef: function(r) { diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 6825e41cdb..e62b67e314 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -18,6 +18,7 @@ var React = require("react"); var ReactDOM = require("react-dom"); var GeminiScrollbar = require('react-gemini-scrollbar'); var q = require("q"); +var KeyCode = require('../../KeyCode'); var DEBUG_SCROLL = false; // var DEBUG_SCROLL = true; @@ -326,6 +327,17 @@ module.exports = React.createClass({ this.scrollState = {stuckAtBottom: true}; }, + /** + * jump to the top of the content. + */ + scrollToTop: function() { + this._setScrollTop(0); + this._saveScrollState(); + }, + + /** + * jump to the bottom of the content. + */ scrollToBottom: function() { // the easiest way to make sure that the scroll state is correctly // saved is to do the scroll, then save the updated state. (Calculating @@ -335,6 +347,45 @@ module.exports = React.createClass({ this._saveScrollState(); }, + /** + * Page up/down. + * + * mult: -1 to page up, +1 to page down + */ + scrollRelative: function(mult) { + var scrollNode = this._getScrollNode(); + var delta = mult * scrollNode.clientHeight * 0.5; + this._setScrollTop(scrollNode.scrollTop + delta); + this._saveScrollState(); + }, + + /** + * Scroll up/down in response to a scroll key + */ + handleScrollKey: function(ev) { + switch (ev.keyCode) { + case KeyCode.PAGE_UP: + this.scrollRelative(-1); + break; + + case KeyCode.PAGE_DOWN: + this.scrollRelative(1); + break; + + case KeyCode.HOME: + if (ev.ctrlKey) { + this.scrollToTop(); + } + break; + + case KeyCode.END: + if (ev.ctrlKey) { + this.scrollToBottom(); + } + break; + } + }, + /* Scroll the panel to bring the DOM node with the scroll token * `scrollToken` into view. * diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index be2667201e..12931fed37 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -27,6 +27,7 @@ var dis = require("../../dispatcher"); var ObjectUtils = require('../../ObjectUtils'); var Modal = require("../../Modal"); var UserActivity = require("../../UserActivity"); +var KeyCode = require('../../KeyCode'); var PAGINATE_SIZE = 20; var INITIAL_SIZE = 20; @@ -520,6 +521,23 @@ var TimelinePanel = React.createClass({ return null; }, + /** + * called by the parent component when PageUp/Down/etc is pressed. + * + * We pass it down to the scroll panel. + */ + handleScrollKey: function(ev) { + if (!this.refs.messagePanel) { return; } + + // jump to the live timeline on ctrl-end, rather than the end of the + // timeline window. + if (ev.ctrlKey && ev.keyCode == KeyCode.END) { + this.jumpToLiveTimeline(); + } else { + this.refs.messagePanel.handleScrollKey(ev); + } + }, + _initTimeline: function(props) { var initialEvent = props.eventId; var pixelOffset = props.eventPixelOffset; diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 1aa2beb41c..733d9e6056 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -34,15 +34,7 @@ var MemberEntry = require("../../../TabCompleteEntries").MemberEntry; var sdk = require('../../../index'); var dis = require("../../../dispatcher"); -var KeyCode = { - ENTER: 13, - BACKSPACE: 8, - DELETE: 46, - TAB: 9, - SHIFT: 16, - UP: 38, - DOWN: 40 -}; +var KeyCode = require("../../../KeyCode"); var TYPING_USER_TIMEOUT = 10000; var TYPING_SERVER_TIMEOUT = 30000;