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.
// ==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>_<owner_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) {
// for detouring comment links, we use the magic fragment id handled by Stack
// Exchange's JavaScript, instead of the actual comment element id, because
// the latter does not work all that well (probably something to do with
// 'display: contents;')
if (linkTarget.commentPostId != null) {
const { commentPostId, commentId } = linkTarget;
if (isTherePost(commentPostId))
return insteadGoTo(`#comment${commentId}_${commentPostId}`);
} else {
let node;
if (node = document.querySelector(`#comment-${linkTarget.commentId}`)) {
const commentId = node.dataset.commentId;
const commentPostId = node.closest('.comments[data-post-id]').dataset.postId;
return insteadGoTo(`#comment${commentId}_${commentPostId}`);
}
// prevent default for 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)) {
location.href = `#comment${commentId}_${commentPostId}`;
return;
}
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, for which I have
// something of a habit of clicking 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);