Merge pull request #1842 from thelounge/astorije/userlist-up-down
Move the user list client code to its own file and make it possible to navigate with keyboard
This commit is contained in:
commit
8deb056ecf
@ -1208,7 +1208,7 @@ kbd {
|
|||||||
color: #50a656;
|
color: #50a656;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat .user:hover {
|
.chat .user:hover {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1925,7 +1925,8 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */
|
|||||||
|
|
||||||
.context-menu-item:hover,
|
.context-menu-item:hover,
|
||||||
.textcomplete-item:hover,
|
.textcomplete-item:hover,
|
||||||
.textcomplete-menu .active {
|
.textcomplete-menu .active,
|
||||||
|
#chat .users .user.active {
|
||||||
background-color: #f6f6f6;
|
background-color: #f6f6f6;
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ require("jquery-ui/ui/widgets/sortable");
|
|||||||
const $ = require("jquery");
|
const $ = require("jquery");
|
||||||
const moment = require("moment");
|
const moment = require("moment");
|
||||||
const URI = require("urijs");
|
const URI = require("urijs");
|
||||||
const fuzzy = require("fuzzy");
|
|
||||||
|
|
||||||
// our libraries
|
// our libraries
|
||||||
require("./libs/jquery/inputhistory");
|
require("./libs/jquery/inputhistory");
|
||||||
@ -585,34 +584,6 @@ $(function() {
|
|||||||
contextMenuActions.execute(contextAction, itemData);
|
contextMenuActions.execute(contextAction, itemData);
|
||||||
});
|
});
|
||||||
|
|
||||||
chat.on("input", ".search", function() {
|
|
||||||
const value = $(this).val();
|
|
||||||
const parent = $(this).closest(".users");
|
|
||||||
const names = parent.find(".names-original");
|
|
||||||
const container = parent.find(".names-filtered");
|
|
||||||
|
|
||||||
if (!value.length) {
|
|
||||||
container.hide();
|
|
||||||
names.show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fuzzyOptions = {
|
|
||||||
pre: "<b>",
|
|
||||||
post: "</b>",
|
|
||||||
extract: (el) => $(el).text(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = fuzzy.filter(
|
|
||||||
value,
|
|
||||||
names.find(".user").toArray(),
|
|
||||||
fuzzyOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
names.hide();
|
|
||||||
container.html(templates.user_filtered({matches: result})).show();
|
|
||||||
});
|
|
||||||
|
|
||||||
if ($(document.body).hasClass("public") && (window.location.hash === "#connect" || window.location.hash === "")) {
|
if ($(document.body).hasClass("public") && (window.location.hash === "#connect" || window.location.hash === "")) {
|
||||||
$("#connect").one("show", function() {
|
$("#connect").one("show", function() {
|
||||||
const params = URI(document.location.search).search(true);
|
const params = URI(document.location.search).search(true);
|
||||||
|
@ -10,6 +10,7 @@ const constants = require("./constants");
|
|||||||
const condensed = require("./condensed");
|
const condensed = require("./condensed");
|
||||||
const JoinChannel = require("./join-channel");
|
const JoinChannel = require("./join-channel");
|
||||||
const helpers_parse = require("./libs/handlebars/parse");
|
const helpers_parse = require("./libs/handlebars/parse");
|
||||||
|
const Userlist = require("./userlist");
|
||||||
|
|
||||||
const chat = $("#chat");
|
const chat = $("#chat");
|
||||||
const sidebar = $("#sidebar");
|
const sidebar = $("#sidebar");
|
||||||
@ -117,7 +118,9 @@ function renderChannel(data) {
|
|||||||
renderChannelMessages(data);
|
renderChannelMessages(data);
|
||||||
|
|
||||||
if (data.type === "channel") {
|
if (data.type === "channel") {
|
||||||
renderChannelUsers(data);
|
const users = renderChannelUsers(data);
|
||||||
|
|
||||||
|
Userlist.handleKeybinds(users.find(".search"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (historyObserver) {
|
if (historyObserver) {
|
||||||
@ -160,6 +163,11 @@ function renderChannelUsers(data) {
|
|||||||
.sort((a, b) => b.lastMessage - a.lastMessage)
|
.sort((a, b) => b.lastMessage - a.lastMessage)
|
||||||
.map((a) => a.nick);
|
.map((a) => a.nick);
|
||||||
|
|
||||||
|
// Before re-rendering the list of names, there might have been an entry
|
||||||
|
// marked as active (i.e. that was highlighted by keyboard navigation).
|
||||||
|
// It is `undefined` if there was none.
|
||||||
|
const previouslyActive = users.find(".active").data("name");
|
||||||
|
|
||||||
const search = users
|
const search = users
|
||||||
.find(".search")
|
.find(".search")
|
||||||
.prop("placeholder", nicks.length + " " + (nicks.length === 1 ? "user" : "users"));
|
.prop("placeholder", nicks.length + " " + (nicks.length === 1 ? "user" : "users"));
|
||||||
@ -173,6 +181,17 @@ function renderChannelUsers(data) {
|
|||||||
if (search.val().length) {
|
if (search.val().length) {
|
||||||
search.trigger("input");
|
search.trigger("input");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a nick was highlighted before re-rendering the lists, re-highlight it in
|
||||||
|
// the newly-rendered list.
|
||||||
|
if (previouslyActive) {
|
||||||
|
// We need to un-highlight everything first because triggering `input` with
|
||||||
|
// a value highlights the first entry.
|
||||||
|
users.find(".user").removeClass("active");
|
||||||
|
users.find(`.user[data-name="${previouslyActive}"]`).addClass("active");
|
||||||
|
}
|
||||||
|
|
||||||
|
return users;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderNetworks(data, singleNetwork) {
|
function renderNetworks(data, singleNetwork) {
|
||||||
|
116
client/js/userlist.js
Normal file
116
client/js/userlist.js
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const $ = require("jquery");
|
||||||
|
const fuzzy = require("fuzzy");
|
||||||
|
const Mousetrap = require("mousetrap");
|
||||||
|
|
||||||
|
const templates = require("../views");
|
||||||
|
|
||||||
|
const chat = $("#chat");
|
||||||
|
|
||||||
|
chat.on("input", ".users .search", function() {
|
||||||
|
const value = $(this).val();
|
||||||
|
const parent = $(this).closest(".users");
|
||||||
|
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();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fuzzyOptions = {
|
||||||
|
pre: "<b>",
|
||||||
|
post: "</b>",
|
||||||
|
extract: (el) => $(el).text(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = fuzzy.filter(
|
||||||
|
value,
|
||||||
|
names.find(".user").toArray(),
|
||||||
|
fuzzyOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
names.hide();
|
||||||
|
container.html(templates.user_filtered({matches: result})).show();
|
||||||
|
|
||||||
|
// Mark the first result as active for convenience
|
||||||
|
container.find(".user").first().addClass("active");
|
||||||
|
});
|
||||||
|
|
||||||
|
chat.on("mouseenter", ".users .user", function() {
|
||||||
|
// Reset any potential selection, this is required in cas there is already a
|
||||||
|
// nick previously selected by keyboard
|
||||||
|
$(".users .user").removeClass("active");
|
||||||
|
|
||||||
|
$(this).addClass("active");
|
||||||
|
});
|
||||||
|
|
||||||
|
chat.on("mouseleave", ".users .user", function() {
|
||||||
|
// Reset any potential selection
|
||||||
|
$(".users .user").removeClass("active");
|
||||||
|
});
|
||||||
|
|
||||||
|
exports.handleKeybinds = function(input) {
|
||||||
|
Mousetrap(input.get(0)).bind(["up", "down"], (_e, key) => {
|
||||||
|
const userlists = input.closest(".users");
|
||||||
|
let userlist;
|
||||||
|
|
||||||
|
// If input field has content, use the filtered list instead
|
||||||
|
if (input.val().length) {
|
||||||
|
userlist = userlists.find(".names-filtered");
|
||||||
|
} else {
|
||||||
|
userlist = userlists.find(".names-original");
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = userlist.find(".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");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust scroll when active item is outside of the visible area
|
||||||
|
const userlistHeight = userlist.height();
|
||||||
|
const userlistScroll = userlist.scrollTop();
|
||||||
|
const active = $(".user.active");
|
||||||
|
const activeTop = active.position().top;
|
||||||
|
const activeHeight = active.height();
|
||||||
|
|
||||||
|
if (activeTop > userlistHeight - activeHeight) {
|
||||||
|
userlist.scrollTop(userlistScroll + activeTop - userlistHeight + activeHeight);
|
||||||
|
} else if (activeTop < 0) {
|
||||||
|
userlist.scrollTop(userlistScroll + activeTop - activeHeight);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
const clickEvent = new $.Event("click");
|
||||||
|
const userOffset = user.offset();
|
||||||
|
clickEvent.pageX = userOffset.left;
|
||||||
|
clickEvent.pageY = userOffset.top + user.height();
|
||||||
|
user.trigger(clickEvent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user