Keyboard navigation shortcuts for lemmy
Keyboard navigation shortcuts for lemmy
Rewrote something I made for kbin to work with lemmy. Mimics some of RES' keyboard navigation functionality.
Edit: updated so that expanded images scroll into view.
Edit 2: 2023/07/04
- added ability to open links/comments (hold shift to open in new tab, might have to disable popup blocker)
- traversing through entries while expand was toggled on will collapse previous entry and expand current entry preview
- handle expanding of text posts
Edit 3: 2023/07/04
- add ability to change to next/previous page
Edit 4: 2023/07/06
- updated scroll into view logic
- prevent shortcut actions when modifier keys are held (ctrl+c won't load comment page anymore)
- updated open link button to also consider images with external links
- updated user script metadata section for compatibility per @God@sh.itjust.works
- navigating to next/previous page while in "expand mode" will auto-expand the first post of the new page
// ==UserScript==
// @name lemmy navigation
// @description Lemmy hotkeys for navigating.
// @match https://sh.itjust.works/*
// @match https://burggit.moe/*
// @match https://vlemmy.net/*
// @match https://lemmy.world/*
// @match https://lemm.ee/*
// @version 1.2
// @run-at document-start
// ==/UserScript==
// Set selected entry colors
const backgroundColor = 'darkslategray';
const textColor = 'white';
// Set navigation keys with keycodes here: https://www.toptal.com/developers/keycode
const nextKey = 'KeyJ';
const prevKey = 'KeyK';
const expandKey = 'KeyX';
const openCommentsKey = 'KeyC';
const openLinkKey = 'Enter';
const nextPageKey = 'KeyN';
const prevPageKey = 'KeyP';
const css = [
".selected {",
" background-color: " + backgroundColor + " !important;",
" color: " + textColor + ";",
"}"
].join("\n");
if (typeof GM_addStyle !== "undefined") {
GM_addStyle(css);
} else if (typeof PRO_addStyle !== "undefined") {
PRO_addStyle(css);
} else if (typeof addStyle !== "undefined") {
addStyle(css);
} else {
let node = document.createElement("style");
node.type = "text/css";
node.appendChild(document.createTextNode(css));
let heads = document.getElementsByTagName("head");
if (heads.length > 0) {
heads[0].appendChild(node);
} else {
// no head yet, stick it whereever
document.documentElement.appendChild(node);
}
}
const selectedClass = "selected";
let currentEntry;
let entries = [];
let previousUrl = "";
let expand = false;
const targetNode = document.documentElement;
const config = { childList: true, subtree: true };
const observer = new MutationObserver(() => {
entries = document.querySelectorAll(".post-listing, .comment-node");
if (entries.length > 0) {
if (location.href !== previousUrl) {
previousUrl = location.href;
currentEntry = null;
}
init();
}
});
observer.observe(targetNode, config);
function init() {
// If jumping to comments
if (window.location.search.includes("scrollToComments=true") &&
entries.length > 1 &&
(!currentEntry || Array.from(entries).indexOf(currentEntry) < 0)
) {
selectEntry(entries[1], true);
}
// If jumping to comment from anchor link
else if (window.location.pathname.includes("/comment/") &&
(!currentEntry || Array.from(entries).indexOf(currentEntry) < 0)
) {
const commentId = window.location.pathname.replace("/comment/", "");
const anchoredEntry = document.getElementById("comment-" + commentId);
if (anchoredEntry) {
selectEntry(anchoredEntry, true);
}
}
// If no entries yet selected, default to first
else if (!currentEntry || Array.from(entries).indexOf(currentEntry) < 0) {
selectEntry(entries[0]);
if (expand) expandEntry();
}
Array.from(entries).forEach(entry => {
entry.removeEventListener("click", clickEntry, true);
entry.addEventListener('click', clickEntry, true);
});
document.removeEventListener("keydown", handleKeyPress, true);
document.addEventListener("keydown", handleKeyPress, true);
}
function handleKeyPress(event) {
if (["TEXTAREA", "INPUT"].indexOf(event.target.tagName) > -1) {
return;
}
// Ignore when modifier keys held
if (event.altKey || event.ctrlKey || event.metaKey) {
return;
}
switch (event.code) {
case nextKey:
case prevKey:
let selectedEntry;
// Next button
if (event.code === nextKey) {
// if shift key also pressed
if (event.shiftKey) {
selectedEntry = getNextEntrySameLevel(currentEntry);
} else {
selectedEntry = getNextEntry(currentEntry);
}
}
// Previous button
if (event.code === prevKey) {
// if shift key also pressed
if (event.shiftKey) {
selectedEntry = getPrevEntrySameLevel(currentEntry);
} else {
selectedEntry = getPrevEntry(currentEntry);
}
}
if (selectedEntry) {
if (expand) collapseEntry();
selectEntry(selectedEntry, true);
if (expand) expandEntry();
}
break;
case expandKey:
toggleExpand();
expand = isExpanded() ? true : false;
break;
case openCommentsKey:
if (event.shiftKey) {
window.open(
currentEntry.querySelector("a.btn[title$='Comments']").href,
);
} else {
currentEntry.querySelector("a.btn[title$='Comments']").click();
}
break;
case openLinkKey:
const linkElement = currentEntry.querySelector(".col.flex-grow-0.px-0>div>a") || currentEntry.querySelector(".col.flex-grow-1>p>a");
if (linkElement) {
if (event.shiftKey) {
window.open(linkElement.href);
} else {
linkElement.click();
}
}
break;
case nextPageKey:
case prevPageKey:
const pageButtons = Array.from(document.querySelectorAll(".paginator>button"));
if (pageButtons) {
const buttonText = event.code === nextPageKey ? "Next" : "Prev";
pageButtons.find(btn => btn.innerHTML === buttonText).click();
}
}
}
function getNextEntry(e) {
const currentEntryIndex = Array.from(entries).indexOf(e);
if (currentEntryIndex + 1 >= entries.length) {
return e;
}
return entries[currentEntryIndex + 1];
}
function getPrevEntry(e) {
const currentEntryIndex = Array.from(entries).indexOf(e);
if (currentEntryIndex - 1 < 0) {
return e;
}
return entries[currentEntryIndex - 1];
}
function getNextEntrySameLevel(e) {
const nextSibling = e.parentElement.nextElementSibling;
if (!nextSibling || nextSibling.getElementsByTagName("article").length < 1) {
return getNextEntry(e);
}
return nextSibling.getElementsByTagName("article")[0];
}
function getPrevEntrySameLevel(e) {
const prevSibling = e.parentElement.previousElementSibling;
if (!prevSibling || prevSibling.getElementsByTagName("article").length < 1) {
return getPrevEntry(e);
}
return prevSibling.getElementsByTagName("article")[0];
}
function clickEntry(event) {
const e = event.currentTarget;
const target = event.target;
// Deselect if already selected, also ignore if clicking on any link/button
if (e === currentEntry && e.classList.contains(selectedClass) &&
!(
target.tagName.toLowerCase() === "button" || target.tagName.toLowerCase() === "a" ||
target.parentElement.tagName.toLowerCase() === "button" ||
target.parentElement.tagName.toLowerCase() === "a" ||
target.parentElement.parentElement.tagName.toLowerCase() === "button" ||
target.parentElement.parentElement.tagName.toLowerCase() === "a"
)
) {
e.classList.remove(selectedClass);
} else {
selectEntry(e);
}
}
function selectEntry(e, scrollIntoView=false) {
if (currentEntry) {
currentEntry.classList.remove(selectedClass);
}
currentEntry = e;
currentEntry.classList.add(selectedClass);
if (scrollIntoView) {
scrollIntoViewWithOffset(e, 15)
}
}
function isExpanded() {
if (
currentEntry.querySelector("a.d-inline-block:not(.thumbnail)") ||
currentEntry.querySelector("#postContent") ||
currentEntry.querySelector(".card-body")
) {
return true;
}
return false;
}
function toggleExpand() {
const expandButton = currentEntry.querySelector("button[aria-label='Expand here']");
const textExpandButton = currentEntry.querySelector(".post-title>button");
if (expandButton) {
expandButton.click();
// Scroll into view if picture/text preview cut off
const imgContainer = currentEntry.querySelector("a.d-inline-block");
if (imgContainer) {
// Check container positions once image is loaded
imgContainer.querySelector("img").addEventListener("load", function() {
scrollIntoViewWithOffset(currentEntry, 0);
}, true);
}
}
if (textExpandButton) {
textExpandButton.click();
}
scrollIntoViewWithOffset(currentEntry, 0);
}
function expandEntry() {
if (!isExpanded()) toggleExpand();
}
function collapseEntry() {
if (isExpanded()) toggleExpand();
}
function scrollIntoViewWithOffset(e, offset) {
if (e.getBoundingClientRect().top < 0 ||
e.getBoundingClientRect().bottom > window.innerHeight
) {
const y = e.getBoundingClientRect().top + window.pageYOffset - offset;
window.scrollTo({
top: y
});
}
}
You're viewing a single thread.
View all comments
18
comments
Would you be able to add upvote and downvote buttons? Also could you make collapse work on comments? Thanks for the script! It's great!
2 0 Reply
You've viewed 18 comments.
Scroll to top