|
// ==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; |
|
} |
|
}); |
|
})(); |