Add buttons to scroll to the bottom and top of the website. Two-finger tap or Ctrl+Z to hide/show buttons.
Version au
// ==UserScript==
// @name Scroll Button
// @name:zh 滚动按钮
// @namespace https://greasyfork.org/
// @version 2.3
// @description Add buttons to scroll to the bottom and top of the website. Two-finger tap or Ctrl+Z to hide/show buttons.
// @description:zh 添加按钮以滚动到网页的底部和顶部,支持双指单击或 Ctrl+Z 隐藏/显示按钮
// @author chowxi
// @match *://*/*
// @license MIT
// @grant none
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
if (window !== window.top) {
return;
}
function throttle(func, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall < delay) {
return;
}
lastCall = now;
return func.apply(this, args);
};
}
function createButton(iconBase64, altText, buttonStyles) {
const btn = document.createElement('button');
const img = document.createElement('img');
img.src = `data:image/svg+xml;base64,${iconBase64}`;
img.alt = altText;
img.style.width = '16px';
img.style.height = '16px';
img.style.display = 'block';
btn.appendChild(img);
Object.assign(btn.style, buttonStyles);
return btn;
}
function setHoverShadow(button) {
button.addEventListener('mouseenter', function() {
button.style.boxShadow = '0 4px 8px rgba(0,0,0,0.25)';
});
button.addEventListener('mouseleave', function() {
button.style.boxShadow = '0 1px 3px rgba(0,0,0,0.15)';
});
}
function init() {
if (!document.body) {
// 如果 document.body 不存在,等待 DOMContentLoaded 事件
window.addEventListener('DOMContentLoaded', init);
return;
}
// Base64-encoded SVG data for bottom icon
const base64BottomIcon =
'PHN2ZyBzdHJva2U9ImN1cnJlbnRDb2xvciIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIyIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgY2xhc3M9Imljb24tc20gbS0xIiBoZWlnaHQ9IjFlbSIgd2lkdGg9IjFlbSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48bGluZSB4MT0iMTIiIHkxPSI1IiB4Mj0iMTIiIHkyPSIxOSI+PC9saW5lPjxwb2x5bGluZSBwb2ludHM9IjE5IDEyIDEyIDE5IDUgMTIiPjwvcG9seWxpbmU+PC9zdmc+';
// Base64-encoded SVG data for top icon
const base64TopIcon =
'PHN2ZyBzdHJva2U9ImN1cnJlbnRDb2xvciIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIyIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgY2xhc3M9Imljb24tc20gbS0xIiBoZWlnaHQ9IjFlbSIgd2lkdGg9IjFlbSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48bGluZSB4MT0iMTIiIHkxPSIxOSIgeDI9IjEyIiB5Mj0iNSI+PC9saW5lPjxwb2x5bGluZSBwb2ludHM9IjUgMTIgMTIgNSAxOSAxMiI+PC9wb2x5bGluZT48L3N2Zz4=';
// Common styles for both buttons
const buttonStyles = {
position: 'fixed',
zIndex: '99',
width: '25px',
height: '25px',
minWidth: '25px',
minHeight: '25px',
maxWidth: '25px',
maxHeight: '25px',
flexShrink: '0',
boxSizing: 'border-box',
backgroundColor: 'transparent',
backdropFilter: 'blur(8px)',
WebkitBackdropFilter: 'blur(8px)',
boxShadow: '0 1px 3px rgba(0,0,0,0.35)',
border: '0.5px solid transparent',
borderRadius: '50%',
padding: '4px',
margin: '0',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'opacity 0.2s, box-shadow 0.2s'
};
// 按钮显示/隐藏状态
let buttonsHidden = false;
function applyButtonsVisibility() {
if (buttonsHidden) {
bottomButton.style.setProperty('display', 'none', 'important');
topButton.style.setProperty('display', 'none', 'important');
} else {
bottomButton.style.setProperty('display', 'flex', 'important');
toggleTopButton();
}
}
function toggleButtonsVisibility() {
buttonsHidden = !buttonsHidden;
applyButtonsVisibility();
}
const bottomButton = createButton(base64BottomIcon, 'Scroll to Bottom', buttonStyles);
bottomButton.style.bottom = '14px';
bottomButton.style.right = '14px';
bottomButton.addEventListener('click', function() {
window.scrollTo({
top: document.documentElement.scrollHeight || document.body.scrollHeight,
behavior: 'smooth'
});
});
const topButton = createButton(base64TopIcon, 'Scroll to Top', buttonStyles);
topButton.style.bottom = '50px';
topButton.style.right = '14px';
topButton.addEventListener('click', function() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
document.body.append(bottomButton, topButton);
setHoverShadow(bottomButton);
setHoverShadow(topButton);
// 智能避让:检测右下角是否有其他浮动元素,自动上移按钮
function avoidOverlap() {
const margin = 14; // 按钮离右边的基础距离
const baseBottom = 14; // 最底部按钮的基础距离
const gap = 36; // 按钮之间的固定间距
const buttonSize = 32; // 按钮估算尺寸(含 padding)
let extraOffset = 0;
const allElements = document.querySelectorAll('body *');
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
allElements.forEach(el => {
if (el === bottomButton || el === topButton || bottomButton.contains(el) || topButton.contains(el)) {
return;
}
const style = window.getComputedStyle(el);
if (style.position !== 'fixed' && style.position !== 'sticky') {
return;
}
if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) === 0) {
return;
}
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
return;
}
const isInBottomRightZone =
rect.right > viewportWidth - 120 &&
rect.bottom > viewportHeight - 150;
if (isInBottomRightZone) {
const occupiedFromBottom = viewportHeight - rect.top;
if (occupiedFromBottom > extraOffset) {
extraOffset = occupiedFromBottom;
}
}
});
const offset = extraOffset > 0 ? extraOffset + 8 : 0;
bottomButton.style.setProperty('bottom', `${baseBottom + offset}px`, 'important');
topButton.style.setProperty('bottom', `${baseBottom + gap + offset}px`, 'important');
}
avoidOverlap();
window.addEventListener('scroll', throttle(avoidOverlap, 300));
window.addEventListener('resize', throttle(avoidOverlap, 300));
const observer = new MutationObserver(throttle(avoidOverlap, 500));
observer.observe(document.body, { childList: true, subtree: true });
function toggleTopButton() {
if (buttonsHidden) {
topButton.style.display = 'none';
return;
}
topButton.style.display = window.scrollY === 0 ? 'none' : 'flex';
}
toggleTopButton();
window.addEventListener('scroll', throttle(toggleTopButton, 100));
// 快捷键 Ctrl+Z:切换按钮显示/隐藏
window.addEventListener('keydown', function(e) {
if (e.ctrlKey && !e.metaKey && !e.altKey && (e.key === 'z' || e.key === 'Z')) {
e.preventDefault();
toggleButtonsVisibility();
}
});
let touchPointerCount = 0;
window.addEventListener('pointerdown', function(e) {
if (e.pointerType !== 'touch') {
return;
}
touchPointerCount++;
if (touchPointerCount === 2) {
toggleButtonsVisibility();
}
}, { passive: true, capture: true });
window.addEventListener('pointerup', function(e) {
if (e.pointerType === 'touch' && touchPointerCount > 0) {
touchPointerCount--;
}
}, { passive: true, capture: true });
window.addEventListener('pointercancel', function(e) {
if (e.pointerType === 'touch' && touchPointerCount > 0) {
touchPointerCount--;
}
}, { passive: true, capture: true });
}
init();
})();