Reddit: Keyboard comment navigation

Adds keyboard shortcuts for moving through the comments on a post — next and previous comment, parent, next thread — taking over reddit's own j/k.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Reddit: Keyboard comment navigation
// @namespace    https://github.com/jshute96/userscripts
// @version      1.1.1
// @description  Adds keyboard shortcuts for moving through the comments on a post — next and previous comment, parent, next thread — taking over reddit's own j/k.
// @author       Jeff Shute <[email protected]>
// @license      MIT
// @match        https://www.reddit.com/*
// @require      https://update.greasyfork.org/scripts/592123/1907419/keyboard-shortcuts.js
// @require      https://update.greasyfork.org/scripts/592124/1907421/keyboard-comment-nav.js
// @grant        none
// @run-at       document-idle
// @noframes
// ==/UserScript==

(function () {
  'use strict';

  const TAG = '[reddit nav]';

  if (window.__redditNavLoaded) {
    console.log(TAG, 'already loaded; skipping duplicate run');
    return;
  }
  window.__redditNavLoaded = true;

  console.log(TAG, 'initializing');

  function thingId(c) {
    return c.getAttribute('thingid') || '?';
  }

  CommentNav.create({
    tag: TAG,

    // Reddit binds its own j/k. Capture phase plus
    // stopImmediatePropagation on handled keys is what beats it.
    capture: true,

    // @match is the whole site so the script survives SPA navigation
    // into a thread; this gate decides whether to act.
    enabled: () => /\/comments\//.test(location.pathname),

    comments: () => [...document.querySelectorAll('shreddit-comment')],

    // Reddit ids each comment's body div `<thingid>-comment-rtjson-content`.
    // The shreddit-comment element itself wraps the entire subtree
    // (body + nested children + reply box) and stays intersecting the
    // viewport long after the text has scrolled past.
    body: c => document.getElementById(`${thingId(c)}-comment-rtjson-content`) || c,

    id: thingId,

    // Nested shreddit-comments live in their parent's light DOM (they
    // get slotted into the shadow DOM for rendering), so the light-DOM
    // ancestor chain is the comment tree.
    parentOf: c => c.parentElement?.closest('shreddit-comment') || null,

    // Reddit's top banner is sticky. It sets scroll-margin-top on
    // depth-0 comments itself, but not on nested replies or the
    // comments header, so we compute the offset for everything.
    //
    // Reddit declares the height as --shreddit-header-height, but on
    // <shreddit-app> rather than the document root — reading it off
    // :root silently yields "" and falls through to the default. Both
    // are checked so a future move back to :root also works; measured
    // against the live <reddit-header-large> the declared 56px is
    // accurate to a pixel.
    headerOffset: () => {
      for (const el of [document.querySelector('shreddit-app'),
                        document.documentElement]) {
        if (!el) continue;
        const n = parseFloat(getComputedStyle(el)
          .getPropertyValue('--shreddit-header-height').trim());
        if (Number.isFinite(n)) return n + 8;
      }
      return 56 + 8;
    },

    commentsTop: () => document.querySelector('shreddit-comments-sort-dropdown')
      || document.querySelector('shreddit-comment-tree-stats')
      || document.querySelector('shreddit-comment-tree'),
  });
})();