Greasy Fork is available in English.
use alt+up/down to scroll to top/bottom
// ==UserScript==
// @name HotKeyTopBottomScroll
// @namespace http://tampermonkey.net/
// @version 1.2.0
// @description use alt+up/down to scroll to top/bottom
// @author Kiddo
// @match *://*/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=google.com
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const EDGE_BY_KEY = {
ArrowUp: 'top',
ArrowDown: 'bottom'
};
const SCROLLABLE_OVERFLOW = new Set(['auto', 'scroll', 'overlay']);
function isHtmlElement(node) {
return node instanceof HTMLElement;
}
function getMaxScrollTop(element) {
return Math.max(0, element.scrollHeight - element.clientHeight);
}
function isScrollable(element) {
if (!isHtmlElement(element)) {
return false;
}
if (getMaxScrollTop(element) <= 1) {
return false;
}
if (
element === document.scrollingElement ||
element === document.documentElement ||
element === document.body
) {
return true;
}
const style = window.getComputedStyle(element);
const overflowY = style.overflowY === 'visible' ? style.overflow : style.overflowY;
return SCROLLABLE_OVERFLOW.has(overflowY);
}
function canScrollToEdge(element, edge) {
const maxScrollTop = getMaxScrollTop(element);
if (maxScrollTop <= 1) {
return false;
}
if (edge === 'top') {
return element.scrollTop > 0;
}
return element.scrollTop < maxScrollTop - 1;
}
function collectAncestorCandidates(startNode, pushCandidate) {
let current = startNode;
while (current) {
if (isHtmlElement(current)) {
pushCandidate(current);
}
const root = typeof current.getRootNode === 'function' ? current.getRootNode() : null;
current = current.parentElement || (root instanceof ShadowRoot ? root.host : null);
}
}
function buildCandidateList(event) {
const candidates = [];
const seen = new Set();
function pushCandidate(element) {
if (!isHtmlElement(element) || seen.has(element)) {
return;
}
seen.add(element);
candidates.push(element);
}
if (typeof event.composedPath === 'function') {
for (const item of event.composedPath()) {
pushCandidate(item);
}
}
collectAncestorCandidates(document.activeElement, pushCandidate);
const centerX = Math.max(1, Math.floor(window.innerWidth / 2));
const centerY = Math.max(1, Math.floor(window.innerHeight / 2));
for (const element of document.elementsFromPoint(centerX, centerY)) {
collectAncestorCandidates(element, pushCandidate);
}
pushCandidate(document.scrollingElement);
pushCandidate(document.documentElement);
pushCandidate(document.body);
return candidates;
}
function getRootScrollTarget() {
return document.scrollingElement || document.documentElement || document.body || null;
}
function findNestedScrollTarget(event, edge) {
const rootTarget = getRootScrollTarget();
for (const candidate of buildCandidateList(event)) {
if (
!isScrollable(candidate) ||
candidate === rootTarget ||
candidate === document.documentElement ||
candidate === document.body
) {
continue;
}
if (canScrollToEdge(candidate, edge)) {
return candidate;
}
}
return null;
}
function getRootScrollState(edge) {
const rootTarget = getRootScrollTarget();
const scrollTop = rootTarget ? rootTarget.scrollTop : window.scrollY;
const maxScrollTop = rootTarget ? getMaxScrollTop(rootTarget) : 0;
const atRequestedEdge = edge === 'top'
? scrollTop <= 1
: scrollTop >= maxScrollTop - 1;
return {
rootTarget,
scrollTop,
maxScrollTop,
atRequestedEdge,
isScrollable: isScrollable(rootTarget)
};
}
function scrollRootToEdge(edge) {
window.scrollTo({
top: edge === 'top' ? 0 : Number.MAX_SAFE_INTEGER,
behavior: 'auto'
});
}
function scrollToEdge(target, edge) {
const top = edge === 'top' ? 0 : getMaxScrollTop(target);
if (
target === document.scrollingElement ||
target === document.documentElement ||
target === document.body
) {
window.scrollTo({ top, behavior: 'auto' });
return;
}
if (typeof target.scrollTo === 'function') {
target.scrollTo({ top, behavior: 'auto' });
return;
}
target.scrollTop = top;
}
window.addEventListener('keydown', function(event) {
if (!event.altKey || event.ctrlKey || event.shiftKey || event.metaKey) {
return;
}
const edge = EDGE_BY_KEY[event.key];
if (!edge) {
return;
}
event.preventDefault();
const before = getRootScrollState(edge);
scrollRootToEdge(edge);
const after = getRootScrollState(edge);
const rootMoved = Math.abs(after.scrollTop - before.scrollTop) > 1;
const shouldFallbackToContainer =
!before.isScrollable || (!rootMoved && !before.atRequestedEdge);
if (!shouldFallbackToContainer) {
return;
}
const target = findNestedScrollTarget(event, edge);
if (!target) {
return;
}
scrollToEdge(target, edge);
event.stopImmediatePropagation();
}, { capture: true, passive: false });
})();