From 060097c118129cf64b8c874e0ca8c032598bce91 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Tue, 10 Jul 2018 23:29:53 +0300 Subject: [PATCH] Implement keyboard navigation in user list. --- client/components/ChatUserList.vue | 86 +++++++++++++++++++++++++- client/components/Username.vue | 3 +- client/components/UsernameFiltered.vue | 3 +- client/js/vue.js | 5 ++ 4 files changed, 94 insertions(+), 3 deletions(-) diff --git a/client/components/ChatUserList.vue b/client/components/ChatUserList.vue index 90406384..464129dd 100644 --- a/client/components/ChatUserList.vue +++ b/client/components/ChatUserList.vue @@ -1,13 +1,22 @@ @@ -60,6 +71,7 @@ export default { data() { return { userSearchInput: "", + activeUser: null, }; }, computed: { @@ -101,6 +113,78 @@ export default { getModeClass(mode) { return modes[mode]; }, + selectUser() { + // Simulate a click on the active user to open the context menu. + // Coordinates are provided to position the menu correctly. + if (!this.activeUser) { + return; + } + + const el = this.$refs.userlist.querySelector(".active"); + const rect = el.getBoundingClientRect(); + const ev = new MouseEvent("click", { + view: window, + bubbles: true, + cancelable: true, + clientX: rect.x, + clientY: rect.y + rect.height, + }); + el.dispatchEvent(ev); + }, + navigateUserList(direction) { + let users = this.channel.users; + + // If a search is active, get the matching user objects + // TODO: this could probably be cached via `computed` + // to avoid refiltering on each keypress + if (this.userSearchInput) { + const results = fuzzy.filter( + this.userSearchInput, + this.channel.users, + { + extract: (u) => u.nick, + } + ); + users = results.map((result) => result.original); + } + + // Bail out if there's no users to select + if (!users.length) { + this.activeUser = null; + return; + } + + let currentIndex = users.indexOf(this.activeUser); + + // If there's no active user select the first or last one depending on direction + if (!this.activeUser || currentIndex === -1) { + this.activeUser = direction ? users[0] : users[users.length - 1]; + this.scrollToActiveUser(); + return; + } + + currentIndex += direction; + + // Wrap around the list if necessary. Normaly each loop iterates once at most, + // but might iterate more often if pgup or pgdown are used in a very short user list + while (currentIndex < 0) { + currentIndex += users.length; + } + + while (currentIndex > users.length - 1) { + currentIndex -= users.length; + } + + this.activeUser = users[currentIndex]; + this.scrollToActiveUser(); + }, + scrollToActiveUser() { + // Scroll the list if needed after the active class is applied + this.$nextTick(() => { + const el = this.$refs.userlist.querySelector(".active"); + el.scrollIntoView({block: "nearest", inline: "nearest"}); + }); + }, }, }; diff --git a/client/components/Username.vue b/client/components/Username.vue index b3ebeff0..6da1cf97 100644 --- a/client/components/Username.vue +++ b/client/components/Username.vue @@ -1,6 +1,6 @@ @@ -10,6 +10,7 @@ export default { name: "Username", props: { user: Object, + active: Boolean, }, }; diff --git a/client/components/UsernameFiltered.vue b/client/components/UsernameFiltered.vue index 27567332..f8b10764 100644 --- a/client/components/UsernameFiltered.vue +++ b/client/components/UsernameFiltered.vue @@ -1,6 +1,6 @@