在 Twitter / X 页面添加回到顶部按钮,支持快捷键 T
// ==UserScript==
// @name Twitter / X 回到顶部按钮
// @namespace twitter-top-button
// @version 1.0.1
// @description 在 Twitter / X 页面添加回到顶部按钮,支持快捷键 T
// @author ryanuo
// @match https://twitter.com/*
// @match https://x.com/*
// @grant none
// @run-at document-idle
// @license apache-2.0
// ==/UserScript==
(() => {
'use strict'
const BUTTON_ID = 'tm-back-to-top-btn-ryanuo'
const SCROLL_THRESHOLD = 300
const style = `
#${BUTTON_ID} {
position: fixed;
right: 20px;
bottom: 28px;
z-index: 999999;
width: 46px;
height: 46px;
border-radius: 999px;
box-shadow: 0 8px 20px rgba(0,0,0,0.2);
background: linear-gradient(135deg,#1d9bf0 0%,#0b7cd6 100%);
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
transform: translateY(10px) scale(0.98);
transition: opacity 200ms ease, transform 200ms ease;
font-size: 20px;
user-select: none;
}
#${BUTTON_ID}.visible {
opacity: 1;
transform: translateY(0) scale(1);
}
#${BUTTON_ID}:hover {
box-shadow: 0 12px 28px rgba(0,0,0,0.3);
transform: translateY(-2px) scale(1.02);
}
#${BUTTON_ID}.hidden-for-input {
opacity: 0.4;
}
/* small screens */
@media (max-width:480px){
#${BUTTON_ID} { right: 12px; bottom: 20px; width:42px; height:42px; font-size:18px; }
}
`
function injectStyle(cssText) {
const s = document.createElement('style')
s.setAttribute('type', 'text/css')
s.textContent = cssText
document.head.appendChild(s)
}
function createButton() {
const existBtn = document.getElementById(BUTTON_ID)
if (existBtn)
return existBtn
const btn = document.createElement('div')
btn.id = BUTTON_ID
btn.setAttribute('role', 'button')
btn.setAttribute('aria-label', '回到顶部 (按 T)')
btn.title = '回到顶部 (按 T)'
btn.innerHTML = '↑'
btn.addEventListener('click', scrollToTop)
document.body.appendChild(btn)
return btn
}
function scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
function shouldShowButton() {
return window.scrollY > SCROLL_THRESHOLD
}
function updateButtonVisibility(btn) {
if (shouldShowButton())
btn.classList.add('visible')
else btn.classList.remove('visible')
}
function isTypingInInput() {
const active = document.activeElement
if (!active)
return false
const tag = active.tagName.toLowerCase()
return tag === 'input' || tag === 'textarea' || active.isContentEditable
}
function setupKeyboardShortcuts(_btn) {
window.addEventListener('keydown', (e) => {
if ((e.key === 't' || e.key === 'T') && !isTypingInInput()) {
e.preventDefault()
scrollToTop()
}
})
}
function init() {
injectStyle(style)
let btn = createButton()
updateButtonVisibility(btn)
let ticking = false
window.addEventListener('scroll', () => {
if (!ticking) {
window.requestAnimationFrame(() => {
updateButtonVisibility(btn)
ticking = false
})
ticking = true
}
}, { passive: true })
document.addEventListener('focusin', () => {
btn.classList.toggle('hidden-for-input', isTypingInInput())
})
document.addEventListener('focusout', () => {
btn.classList.toggle('hidden-for-input', isTypingInInput())
})
setupKeyboardShortcuts()
const mo = new MutationObserver(() => {
if (!document.getElementById(BUTTON_ID)) {
btn = createButton()
setupKeyboardShortcuts()
}
})
mo.observe(document.documentElement || document.body, { childList: true, subtree: true })
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init)
}
else {
init()
}
})()