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
/*
==UserScript==
@name lemmy navigation
@match https://lemmy.world/*
@match https://lemm.ee/*
@version 1.1
@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]);
}
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;
}
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")
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(
imgContainer,
currentEntry.offsetHeight - imgContainer.offsetHeight + 10
);
}, true);
}
}
if (textExpandButton) {
textExpandButton.click();
const textContainers = [currentEntry.querySelector("#postContent"), currentEntry.querySelector(".card-body")];
textContainers.forEach(container => {
if (container) {
scrollIntoViewWithOffset(
container,
currentEntry.offsetHeight - container.offsetHeight + 10
);
}
});
}
}
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
});
}
}
I can’t thank you enough for this boobslider100 and afoutopatisa!
Is this GPL? I took the liberty to fork it, to change into arrow navigation and change the styling. I also added upvote/downvote keys, before seeing shadshack did the same 🤣. Hope it’s ok!
Oh yea go for it. I should’ve put it on github in the first place but I’m lazy.
Love it, I’d like to get also
c
to open comments orl
/Return
to open the selected one (maybe in a new tab).Updated 👍. I just did the c and enter for now.
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!
This is great! I’ve been working with the code and added keys for upvote/downvote as well (it’s basically the same as the Expand code, but targeting the Upvote/Downvote buttons. I also have it set so that if you vote, it automatically scrolls to the next post and maintains “expand” status.
Now I can scroll lemmy and upvote/downvote to mark posts as read with just a/z, exactly how I used to use RES keyboard shortcuts for Reddit.
Here’s the code I’m using (pastebin because posting it in the comment keeps timing out…): https://pastebin.com/BTYyU17L
I always find myself tapping J and K on lemmy and expecting it to work so thank you for making my muscle memory not go to waste! :D
Ooooh very nice! downloaded and installed