// ==UserScript== // @name 任意网页多快捷键绑定器 // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description 2秒内按4次ESC唤出可拖动圆形按钮,支持多个按钮,双击按钮删除,每个可绑定不同按键,独立触发点击 // @author Lin // @match *://*/* // @grant none // ==/UserScript== (function() { 'use strict'; // --- 存储所有按钮的数组 --- let buttons = []; // 每个元素: { dom, key, selector, waiting } // --- ESC组合计数 --- let escPressCount = 0; let firstEscTime = 0; // --- 工具函数:获取元素简洁且尽量唯一的选择器 --- function getSelector(el) { if (!el || el === document.documentElement) return 'html'; if (el.id) return `#${el.id}`; const path = []; while (el && el.nodeType === Node.ELEMENT_NODE && el !== document.body) { let selector = el.tagName.toLowerCase(); const parent = el.parentNode; if (parent) { const siblings = Array.from(parent.children).filter(c => c.tagName === el.tagName); if (siblings.length > 1) { const sameTagIndex = siblings.indexOf(el) + 1; selector += `:nth-of-type(${sameTagIndex})`; } else { const allChildren = parent.children; const index = Array.from(allChildren).indexOf(el) + 1; selector = `${el.tagName.toLowerCase()}:nth-child(${index})`; } } path.unshift(selector); el = parent; } if (el === document.body) path.unshift('body'); return path.join(' > '); } // --- 移除所有未绑定的按钮 (key === null) --- function removeUnboundButtons() { for (let i = buttons.length - 1; i >= 0; i--) { const btn = buttons[i]; if (btn.key === null) { btn.dom.remove(); buttons.splice(i, 1); } } } // --- 移除指定按钮 (从数组和DOM) --- function removeButton(btnObj) { const index = buttons.indexOf(btnObj); if (index !== -1) { btnObj.dom.remove(); buttons.splice(index, 1); } } // --- 创建新按钮 (屏幕中央) --- function createFloatingButton(x, y) { // 先清理所有未绑定的按钮 removeUnboundButtons(); // 创建DOM const btn = document.createElement('div'); btn.id = 'floating-shortcut-btn-' + Date.now(); Object.assign(btn.style, { position: 'fixed', width: '50px', height: '50px', borderRadius: '50%', backgroundColor: '#ff5722', color: 'white', textAlign: 'center', lineHeight: '50px', fontSize: '16px', fontWeight: 'bold', cursor: 'move', userSelect: 'none', boxShadow: '0 4px 12px rgba(0,0,0,0.3)', zIndex: '999999', left: (x - 25) + 'px', top: (y - 25) + 'px', transition: 'box-shadow 0.1s ease', fontFamily: 'Arial, sans-serif' }); btn.textContent = '⚡'; // 等待绑定 btn.title = '双击删除'; // 删除方式提示 document.documentElement.appendChild(btn); // 创建按钮对象 const btnObj = { dom: btn, key: null, // 未绑定 selector: null, waiting: true // 新按钮进入等待绑定状态 }; buttons.push(btnObj); // 确保其他按钮退出等待状态 (只能有一个等待) buttons.forEach(b => { if (b !== btnObj && b.waiting) b.waiting = false; }); // --- 双击删除 --- btn.addEventListener('dblclick', (e) => { e.stopPropagation(); removeButton(btnObj); }); // --- 右键菜单:仅阻止默认菜单,不删除 --- btn.addEventListener('contextmenu', (e) => { e.preventDefault(); e.stopPropagation(); }); // --- 拖拽功能 (左键) --- let isDragging = false; let offsetX, offsetY; const onMouseDown = (e) => { if (e.button !== 0) return; // 仅左键 e.preventDefault(); isDragging = false; const rect = btn.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; const onMouseMove = (moveEvent) => { moveEvent.preventDefault(); isDragging = true; let newLeft = moveEvent.clientX - offsetX; let newTop = moveEvent.clientY - offsetY; const maxLeft = window.innerWidth - btn.offsetWidth; const maxTop = window.innerHeight - btn.offsetHeight; newLeft = Math.max(0, Math.min(newLeft, maxLeft)); newTop = Math.max(0, Math.min(newTop, maxTop)); btn.style.left = newLeft + 'px'; btn.style.top = newTop + 'px'; }; const onMouseUp = () => { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); isDragging = false; }; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }; btn.addEventListener('mousedown', onMouseDown); btn.addEventListener('mouseup', (e) => e.preventDefault()); return btnObj; } // --- 辅助:按钮闪烁 --- function flashButton(btnObj, text, duration = 300) { if (!btnObj || !btnObj.dom) return; const dom = btnObj.dom; const originalText = dom.textContent; const originalBg = dom.style.backgroundColor; dom.textContent = text; dom.style.backgroundColor = '#4caf50'; setTimeout(() => { if (dom && dom.parentNode) { dom.textContent = originalText; dom.style.backgroundColor = originalBg; } }, duration); } // --- 处理等待状态下的按键绑定 --- function handleBindingKey(e) { const waitingBtn = buttons.find(b => b.waiting === true); if (!waitingBtn) return false; const key = e.key; // 允许按ESC取消等待并移除按钮 if (key === 'Escape') { e.preventDefault(); e.stopPropagation(); removeButton(waitingBtn); return true; } // 忽略修饰键 const modifierKeys = ['Shift', 'Control', 'Alt', 'Meta', 'OS', 'CapsLock', 'Tab']; if (modifierKeys.includes(key)) { flashButton(waitingBtn, '🚫'); return false; } if (e.ctrlKey || e.altKey || e.metaKey) { flashButton(waitingBtn, '组合键'); return false; } // 检查按键是否已被其他已绑定的按钮占用 const existing = buttons.find(b => b.key === key && b !== waitingBtn); if (existing) { flashButton(waitingBtn, '❌占用'); return false; } // 合法按键,进行绑定 e.preventDefault(); e.stopPropagation(); // 获取按钮下方元素 const rect = waitingBtn.dom.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; waitingBtn.dom.style.visibility = 'hidden'; let elem = document.elementFromPoint(centerX, centerY); waitingBtn.dom.style.visibility = 'visible'; if (!elem || elem === document.documentElement) { elem = document.body; } waitingBtn.key = key; waitingBtn.selector = getSelector(elem); waitingBtn.waiting = false; waitingBtn.dom.textContent = key; console.log(`[快捷键] 绑定按键 "${key}" 到元素:`, elem, '选择器:', waitingBtn.selector); flashButton(waitingBtn, '✓', 200); return true; } // --- 触发已绑定的快捷键 --- function triggerShortcut(e) { const key = e.key; if (e.ctrlKey || e.altKey || e.metaKey) return false; const target = e.target; const isInput = target.matches('input, textarea, select, [contenteditable="true"], [contenteditable=""]'); if (isInput) return false; const matchedBtns = buttons.filter(b => b.key === key && !b.waiting); if (matchedBtns.length === 0) return false; const btnObj = matchedBtns[0]; if (!btnObj.selector) return false; e.preventDefault(); e.stopPropagation(); try { const element = document.querySelector(btnObj.selector); if (element) { element.click(); flashButton(btnObj, '💥', 150); console.log(`[快捷键] 触发按键 "${key}",点击元素:`, element); } else { console.warn('[快捷键] 绑定的元素未找到,请重新绑定'); flashButton(btnObj, '❌'); } } catch (err) { console.error('[快捷键] 触发错误:', err); } return true; } // --- 处理ESC组合唤出 --- function handleEscCombo(e) { if (e.key !== 'Escape') return; const now = Date.now(); if (escPressCount === 0) { firstEscTime = now; escPressCount = 1; } else { if (now - firstEscTime <= 2000) { escPressCount++; } else { firstEscTime = now; escPressCount = 1; } } if (escPressCount === 4) { e.preventDefault(); e.stopPropagation(); createFloatingButton(window.innerWidth / 2, window.innerHeight / 2); escPressCount = 0; firstEscTime = 0; } } // --- 全局键盘监听 (捕获阶段) --- window.addEventListener('keydown', (e) => { // 1. 优先处理等待绑定 if (buttons.some(b => b.waiting)) { if (handleBindingKey(e)) return; } // 2. 触发已绑定的快捷键 if (triggerShortcut(e)) return; // 3. 处理ESC组合唤出 handleEscCombo(e); }, true); // --- 清理 (页面卸载时) --- window.addEventListener('unload', () => { buttons.forEach(b => b.dom.remove()); buttons = []; }); })();