Skip to content

Instantly share code, notes, and snippets.

@yorickvP
Last active November 3, 2024 13:49
Show Gist options
  • Save yorickvP/c5f38322867440ea1774594c59ece3a2 to your computer and use it in GitHub Desktop.
Save yorickvP/c5f38322867440ea1774594c59ece3a2 to your computer and use it in GitHub Desktop.
HN Keyboard Navigation

HN Keyboard Navigation

A userscript that adds keyboard navigation to Hacker News comment threads.

Features

  • Navigate between comments using keyboard shortcuts
  • Highlight currently selected comment
  • Collapse/expand comment threads
  • Jump between comments at the same level

Installation

  1. Install a userscript manager like Tampermonkey or Greasemonkey
  2. Click the raw link for this gist
  3. Your userscript manager should prompt you to install

Usage

  • j/k: Move to next/previous comment
  • n/p: Move to next/previous comment at same level
  • space: Toggle collapse current comment thread
// ==UserScript==
// @name HN Keyboard Navigation
// @namespace http://tampermonkey.net/
// @version 0.1
// @description Reddit-style keyboard navigation for Hacker News comments
// @author You
// @match https://news.ycombinator.com/item?id=*
// @grant GM_openInTab
// ==/UserScript==
(function() {
'use strict';
let currentComment = null;
let comments = [];
function initializeComments() {
// Get all comment rows (excluding deleted comments)
comments = Array.from(document.querySelectorAll('tr.comtr')).filter(
comment => !comment.querySelector('.comment').textContent.includes('[deleted]')
);
}
function getCommentLevel(comment) {
const indent = comment.querySelector('td.ind img');
return indent ? parseInt(indent.width) / 40 : 0;
}
function highlightComment(comment) {
// Remove previous highlight
if (currentComment) {
currentComment.style.backgroundColor = '';
currentComment.style.border = '';
}
// Add new highlight
if (comment) {
comment.style.backgroundColor = '#fff6d5';
comment.style.border = '1px solid #ff6600';
// Scroll into view if any part is outside viewport
const rect = comment.getBoundingClientRect();
if (rect.top < 0 || rect.bottom > window.innerHeight) {
comment.scrollIntoView({ block: 'center' });
}
}
currentComment = comment;
}
function findNextSameLevel() {
if (!currentComment) return null;
const currentIndex = comments.indexOf(currentComment);
const currentLevel = getCommentLevel(currentComment);
// Find next comment at same or higher level
for (let i = currentIndex + 1; i < comments.length; i++) {
const level = getCommentLevel(comments[i]);
if (level <= currentLevel) return comments[i];
}
return null;
}
function findPrevSameLevel() {
if (!currentComment) return null;
const currentIndex = comments.indexOf(currentComment);
const currentLevel = getCommentLevel(currentComment);
// Find previous comment at same or higher level
for (let i = currentIndex - 1; i >= 0; i--) {
const level = getCommentLevel(comments[i]);
if (level <= currentLevel) return comments[i];
}
return null;
}
function toggleComment() {
if (!currentComment) return;
const toggleLink = currentComment.querySelector('.togg');
if (toggleLink) {
toggleLink.click();
}
}
function openUrlInNewTab() {
// Find the main story link
const mainLink = document.querySelector('.title > .titleline > a');
if (mainLink) {
GM_openInTab(mainLink.href, { active: true, setParent: true });
}
}
// Initialize on page load
initializeComments();
// Add click listeners to comments
comments.forEach(comment => {
comment.addEventListener('click', (e) => {
// Only trigger if clicking the comment itself, not links within it
if (e.target.closest('.comment')) {
highlightComment(comment);
e.stopPropagation(); // Prevent bubbling
}
});
});
// Add keyboard listeners
document.addEventListener('keydown', function(e) {
// Ignore if user is typing in an input
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
return;
}
switch(e.key) {
case 'j':
case 'k': {
if (!currentComment) {
highlightComment(comments[0]);
break;
}
const currentIndex = comments.indexOf(currentComment);
const nextIndex = e.key === 'j' ?
Math.min(currentIndex + 1, comments.length - 1) :
Math.max(currentIndex - 1, 0);
highlightComment(comments[nextIndex]);
break;
}
case 'J': {
// Move to next comment at same level
const nextSameLevel = findNextSameLevel();
if (nextSameLevel) {
highlightComment(nextSameLevel);
}
break;
}
case 'K': {
// Move to previous comment at same level
const prevSameLevel = findPrevSameLevel();
if (prevSameLevel) {
highlightComment(prevSameLevel);
}
break;
}
case 'Enter':
// Toggle comment collapse
toggleComment();
break;
case 'Escape':
// Remove focus
highlightComment(null);
break;
case 'v':
// Open URL in new tab
openUrlInNewTab();
break;
}
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment