diff --git a/client/css/style.css b/client/css/style.css index 3ee56eeb..e37662ba 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -1921,7 +1921,8 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */ .context-menu-item:hover, .textcomplete-item:hover, -.textcomplete-menu .active { +.textcomplete-menu .active, +#chat .users .user.active { background-color: #f6f6f6; transition: none; } diff --git a/client/js/render.js b/client/js/render.js index 79809e01..1c710212 100644 --- a/client/js/render.js +++ b/client/js/render.js @@ -10,6 +10,7 @@ const constants = require("./constants"); const condensed = require("./condensed"); const JoinChannel = require("./join-channel"); const helpers_parse = require("./libs/handlebars/parse"); +const Userlist = require("./userlist"); const chat = $("#chat"); const sidebar = $("#sidebar"); @@ -117,7 +118,9 @@ function renderChannel(data) { renderChannelMessages(data); if (data.type === "channel") { - renderChannelUsers(data); + const users = renderChannelUsers(data); + + Userlist.handleKeybinds(users.find(".search")); } if (historyObserver) { @@ -173,6 +176,8 @@ function renderChannelUsers(data) { if (search.val().length) { search.trigger("input"); } + + return users; } function renderNetworks(data, singleNetwork) { diff --git a/client/js/userlist.js b/client/js/userlist.js index bbd770d3..d7bb9c33 100644 --- a/client/js/userlist.js +++ b/client/js/userlist.js @@ -2,6 +2,7 @@ const $ = require("jquery"); const fuzzy = require("fuzzy"); +const Mousetrap = require("mousetrap"); const templates = require("../views"); @@ -13,6 +14,9 @@ chat.on("input", ".users .search", function() { const names = parent.find(".names-original"); const container = parent.find(".names-filtered"); + // Input content has changed, reset the potential selection + parent.find(".user").removeClass("active"); + if (!value.length) { container.hide(); names.show(); @@ -34,3 +38,44 @@ chat.on("input", ".users .search", function() { names.hide(); container.html(templates.user_filtered({matches: result})).show(); }); + +exports.handleKeybinds = function(input) { + Mousetrap(input.get(0)).bind(["up", "down"], (_e, key) => { + const userlists = input.closest(".users"); + let users; + + // If input field has content, use the filtered list instead + if (input.val().length) { + users = userlists.find(".names-filtered .user"); + } else { + users = userlists.find(".names-original .user"); + } + + // Find which item in the array of users is currently selected, if any. + // Returns -1 if none. + const activeIndex = users.toArray() + .findIndex((user) => user.classList.contains("active")); + + // Now that we know which user is active, reset any selection + userlists.find(".user").removeClass("active"); + + // Mark next/previous user as active. + if (key === "down") { + // If no users or last user were marked as active, mark the first one. + users.eq((activeIndex + 1) % users.length).addClass("active"); + } else { + // If no users or first user was marked as active, mark the last one. + users.eq(Math.max(activeIndex, 0) - 1).addClass("active"); + } + }); + + // When pressing Enter, open the context menu (emit a click) on the active + // user + Mousetrap(input.get(0)).bind("enter", () => { + const user = input.closest(".users").find(".user.active"); + + if (user.length) { + user.click(); + } + }); +};