8

This is a convenience user script. It prevents page reloading when following links to answers that happen to be present on the same page, and to their comments; instead, it simply scrolls to the link target on the current page.

A tiny warning (perhaps not significant to most people): when following links to comments in the /posts/comments/CommentId form, the script may incur additional HTTP(S) traffic; this happens when following a link to a comment that has not (yet) been loaded on the current page, to check if it’s attached to a post that is present on the current page. Since this is a HEAD request, which consists only of HTTP headers, I estimate it to be no more than 2 kB per click (excluding TLS overhead). In the worst case, when linking to a comment for a post not present on the page, this may trigger the HEAD request, and then perform a full page reload anyway; in the middling case, when the post is present, full comments for that post will be loaded. To avoid this additional traffic, prefer linking to comments in a form that includes the #commentCommentId_PostId anchor, which includes both the comment ID and the post ID to which the comment is attached.

// ==UserScript==
// @name     Stack Exchange: Faster Answer Links
// @grant    none
// @run-at   document-start
// @match    https://*.stackexchange.com/*
// @match    https://*.superuser.com/*
// @match    https://*.stackoverflow.com/*
// @match    https://*.mathoverflow.net/*
// @match    https://*.serverfault.com/*
// @match    https://*.askubuntu.com/*
// @match    https://stackapps.com/*
// @exclude  https://chat.stackexchange.com/*
// @exclude  https://api.stackexchange.com/*
// @exclude  https://data.stackexchange.com/*
// @exclude  https://openid.stackexchange.com/*
// @exclude  https://contests.stackoverflow.com/*
// @exclude  /^https?:\/\/winterbash\d{4,}\.stackexchange\.com\//
// ==/UserScript==

// attaching to window because jQuery attaches *its* delegated
// event handlers to document, and we need to run *after* those

window.addEventListener('click', ev => {
  if (ev.defaultPrevented)
    return;

  const target = ev.target.closest(':any-link');
  
  // ignore non-link clicks
  if (!target)
    return;
  
  // ignore target="_blank" links (especially on review pages)
  if (target.target)
    return;
  
  // ignore clicks with modifier keys; we presume
  // those would not result in normal navigation
  if (ev.ctrlKey || ev.altKey || ev.shiftKey || ev.metaKey)
    return;

  // ignore href="#" pseudo-links
  // (.getAttribute because .href returns the resolved absolute URL)
  if (target.getAttribute('href') === '#')
    return;

  const parseURL = u => {
    let m;
    
    // catch both HTTPS and older plaintext-HTTP links
    if (u.host !== location.host || !/^https?:$/.test(u.protocol))
      return null;
    
    // /posts/comments/<comment_id>
    if (m = /^\/posts\/comments\/(\d+)(?=\/|$)/u.exec(u.pathname)) {
      return { commentId: m[1], commentPostId: null };
    }
    
    // #comment<comment_id>_<post_id>
    if (/^\/(q|a|questions)\//u.test(u.pathname)
        && (m = /^#comment(\d+)_(\d+)$/u.exec(u.hash))) {
      return { commentId: m[1], commentPostId: m[2] };
    }

    // /a/<answer_id>
    // /questions/<question_id>/<question_title>/<answer_id>
    if (m = /^\/(?:a|questions\/(\d+)\/[^\/]+)\/(\d+)(?=\/|$)/u.exec(u.pathname)) {
      return { questionId: m[1] ?? null, answerId: m[2] };
    }
    
    // /q/<question_id>
    // /questions/<question_id>/<question_title>
    if (m = /^\/(?:q|questions)\/(\d+)(?=\/|$)/u.exec(u.pathname)) {
      const questionId = m[1];
      if (m = /^#(?:answer-)?(\d+)$/u.exec(u.hash))
        return { questionId, answerId: m[1] };
      if (!u.hash || u.hash === '#question')
        return { questionId, answerId: null };
    }
    
    return null;
  };
  
  const insteadGoTo = target => {
    ev.preventDefault();
    location.href = target;
  };
  
  const isThereQuestion = postId =>
    !!document.querySelector(`#question[data-questionid="${postId}"]`);
  const isThereAnswer = postId =>
    !!document.getElementById(`answer-${postId}`);
  const isTherePost = postId =>
    !!document.querySelector(`#answer-${postId}, #question[data-questionid="${postId}"]`)
  
  const u = new URL(target.href);  
  const linkTarget = parseURL(u);
  
  if (!linkTarget)
    return;
  
  if (linkTarget.commentId != null) {
    if (linkTarget.commentPostId != null) {
      const { commentPostId, commentId } = linkTarget;     
      if (isTherePost(commentPostId))
        return insteadGoTo(`#comment${commentId}_${commentPostId}`);
    } else {
      let node;

      // when rerouting comment links, use the magic fragment id
      // (processed by Stack Exchange JS)  instead of the proper
      // element id (natively supported in the browser)
      // because the latter does not work all that well
      // (probably something to do with 'display: contents;')
      // additionally, a magic fragment id will work for comments
      // that have not been loaded yet

      if (node = document.querySelector(`#comment-${linkTarget.commentId}`)) {
        // happy path: comment already loaded, just navigate to it

        const commentId = node.dataset.commentId;
        const commentPostId = node.closest('.comments[data-post-id]').dataset.postId;

        return insteadGoTo(`#comment${commentId}_${commentPostId}`);
      }
      
      // if there are no hidden comments on the current page, don't bother,
      // just do a full page reload
      if (!document.querySelector('.js-post-comments-component .js-show-link:not(.dno)')) {
        return;
      }
      
      // prevent default now; if fetching the target fails,
      // we will just redirect manually later
      ev.preventDefault();
      
      (async () => {
        target.style.cursor = 'progress';

        try {
          const response = await fetch(
            new URL(`/posts/comments/${linkTarget.commentId}`, location),
            { method: 'HEAD' }
          );
          const redirectTarget = response.redirected && parseURL(new URL(response.url));

          if (redirectTarget) {
            // the fallbacks are here because response.url of fetch requests
            // made from Greasemonkey for some reason omit the fragment
            // identifier

            const commentId =
                  redirectTarget.commentId ??
                  linkTarget.commentId;
            const commentPostId =
                  redirectTarget.commentPostId ??
                  redirectTarget.answerId ??
                  redirectTarget.questionId;

            if (isTherePost(commentPostId)) {
              // mediocre path: post is present on the page, and navigating
              // to the magic link should trigger loading full comments

              location.href = `#comment${commentId}_${commentPostId}`;
              return;
            }

            // sad path: post not present on the page at all, we have to follow the link

            console.warn(
              `[FAL] Failed to follow ${u.href}: post #${commentPostId} not found on the page`);
          } else {
            console.warn(
              `[FAL] Failed to resolve ${u.href}: response is `, response);
          }

          location.href = u.href;
        } finally {
          target.style.cursor = '';
        }
      })().catch(e => {
        location.href = u.href;
        console.warn(`[FAL] Failed to resolve ${u.href}: error `, e);
      });
    }
  } else if (linkTarget.answerId != null) {
    // checking question id is probably not necessary, and may
    // even be harmful if an answer is moved between questions

    if (isThereAnswer(linkTarget.answerId))
      return insteadGoTo(`#answer-${linkTarget.answerId}`);
  } else if (linkTarget.questionId != null) {
    // a link to the current question is unusual, but possible; we handle this too
    // however, we make an exception for the link in the header,
    // sometimes clicked in lieu of refreshing the page

    if (target.matches('#question-header :any-link'))
      return;

    if (isThereQuestion(linkTarget.questionId))
      return insteadGoTo('#question');
  } else {
    console.warn(`[FAL] URL ${u} parsed, but not handled:`, linkTarget);
  }
}, false);
0

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.