// ==UserScript== // @name XPath工具 // @namespace http://tampermonkey.net/ // @version 2.2 // @description 按自定义快捷键显示输入框,提供XPath操作和功能 // @author Ace // @match *://*/* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_download // @grant GM_notification // @resource iconFont https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css // @require https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js // ==/UserScript== (function() { 'use strict'; // 配置和状态变量 let toolbar = null; let settingsPanel = null; let currentElement = null; let isModifierPressed = false; let originalBackgroundColor = ''; let originalOutline = ''; let isDraggingToolbar = false; let isDraggingSettings = false; let dragStartX = 0; let dragStartY = 0; let toolbarX = 0; let toolbarY = 0; let settingsX = 0; let settingsY = 0; let mouseX = 0; let mouseY = 0; // 默认配置 const defaultConfig = { clearOnClose: true, hotkey: "Shift+X", highlightColor: "#FFA500", selectModifier: "Shift", enableOutline: true, enableSound: false, theme: "dark" }; // 从存储中获取配置或使用默认值 let config = { ...defaultConfig, ...GM_getValue("xpath_config", {}) }; // 监听鼠标移动以获取当前位置 document.addEventListener('mousemove', (e) => { mouseX = e.clientX; mouseY = e.clientY; }); // 高亮显示元素 function highlightElement(element) { if (currentElement) { currentElement.style.backgroundColor = originalBackgroundColor; currentElement.style.outline = originalOutline; } originalBackgroundColor = getComputedStyle(element).backgroundColor; originalOutline = getComputedStyle(element).outline; element.style.backgroundColor = config.highlightColor; if (config.enableOutline) { element.style.outline = "2px solid #3498db"; } currentElement = element; } // 清除高亮 function clearHighlight() { if (currentElement) { currentElement.style.backgroundColor = originalBackgroundColor; currentElement.style.outline = originalOutline; currentElement = null; } } // 获取元素的XPath function getXPath(element) { if (element.id) { return `//*[@id="${element.id}"]`; } if (element === document.body) { return '/html/body'; } let ix = 0; const siblings = element.parentNode.childNodes; for (let i = 0; i < siblings.length; i++) { const sibling = siblings[i]; if (sibling === element) { return `${getXPath(element.parentNode)}/${element.tagName.toLowerCase()}[${ix + 1}]`; } if (sibling.nodeType === 1 && sibling.tagName === element.tagName) { ix++; } } } // 导出配置 function exportConfig() { try { const data = JSON.stringify(config, null, 2); const blob = new Blob([data], {type: "application/json"}); const url = URL.createObjectURL(blob); GM_download({ url: url, name: "xpath_config.json", saveAs: true }); } catch (error) { showNotification("导出配置失败: " + error.message, "error"); } } // 导入配置 function importConfig() { const input = document.createElement('input'); input.type = 'file'; input.accept = 'application/json'; input.onchange = e => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = event => { try { const imported = JSON.parse(event.target.result); // 验证导入的配置 if (typeof imported !== 'object') { throw new Error("无效的配置文件"); } config = {...config, ...imported}; GM_setValue("xpath_config", config); updateSettingsDisplay(); showNotification("配置导入成功", "success"); } catch (error) { showNotification("配置文件解析失败: " + error.message, "error"); } }; reader.readAsText(file); }; input.click(); } // 显示通知 function showNotification(message, type = "info") { if (typeof GM_notification === "function") { GM_notification({ text: message, title: "XPath工具", image: type === "error" ? "https://icons.iconarchive.com/icons/paomedia/small-n-flat/1024/sign-error-icon.png" : type === "success" ? "https://icons.iconarchive.com/icons/paomedia/small-n-flat/1024/sign-success-icon.png" : "https://icons.iconarchive.com/icons/paomedia/small-n-flat/1024/sign-info-icon.png" }); } else { alert(`XPath工具: ${message}`); } } // 创建设置面板 function createSettingsPanel() { if (document.getElementById('xpath-settings')) return; settingsPanel = document.createElement('div'); settingsPanel.id = 'xpath-settings'; settingsPanel.className = `xpath-panel ${config.theme}-theme`; settingsPanel.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: ${config.theme === 'dark' ? '#2c3e50' : '#f5f5f5'}; padding: 20px; border-radius: 8px; color: ${config.theme === 'dark' ? 'white' : '#333'}; z-index: 10000; display: none; box-shadow: 0 0 20px rgba(0,0,0,0.3); min-width: 320px; user-select: none; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; transition: opacity 0.3s ease; `; settingsPanel.innerHTML = `

设置

×
${config.highlightColor}
`; const header = settingsPanel.querySelector('.panel-header'); header.addEventListener('mousedown', startDragSettings); document.addEventListener('mousemove', handleDragSettings); document.addEventListener('mouseup', stopDragSettings); settingsPanel.querySelector('#saveSettings').addEventListener('click', function(e) { e.stopPropagation(); saveSettings(); }); settingsPanel.querySelector('#cancelSettings').addEventListener('click', function(e) { e.stopPropagation(); hideSettings(); }); settingsPanel.querySelector('#closeSettingsBtn').addEventListener('click', function(e) { e.stopPropagation(); hideSettings(); }); settingsPanel.querySelector('#exportConfig').addEventListener('click', exportConfig); settingsPanel.querySelector('#importConfig').addEventListener('click', importConfig); // 颜色选择器实时更新显示 const colorInput = settingsPanel.querySelector('#highlightColor'); const colorText = colorInput.nextElementSibling; colorInput.addEventListener('input', function() { colorText.textContent = this.value; }); document.body.appendChild(settingsPanel); } // 隐藏设置面板 function hideSettings() { if (settingsPanel) { settingsPanel.style.display = 'none'; } } // 更新设置显示 function updateSettingsDisplay() { if (!document.getElementById('clearOnClose')) return; document.getElementById('clearOnClose').checked = config.clearOnClose; document.getElementById('hotkey').value = config.hotkey; document.getElementById('selectModifier').value = config.selectModifier; document.getElementById('highlightColor').value = config.highlightColor; document.getElementById('highlightColor').nextElementSibling.textContent = config.highlightColor; document.getElementById('enableOutline').checked = config.enableOutline; document.getElementById('theme').value = config.theme; // 更新面板主题 if (settingsPanel) { settingsPanel.className = `xpath-panel ${config.theme}-theme`; const isDark = config.theme === 'dark'; settingsPanel.style.background = isDark ? '#2c3e50' : '#f5f5f5'; settingsPanel.style.color = isDark ? 'white' : '#333'; } } // 保存设置 function saveSettings() { try { config.clearOnClose = document.getElementById('clearOnClose').checked; config.hotkey = document.getElementById('hotkey').value.trim() || defaultConfig.hotkey; config.selectModifier = document.getElementById('selectModifier').value; config.highlightColor = document.getElementById('highlightColor').value; config.enableOutline = document.getElementById('enableOutline').checked; config.theme = document.getElementById('theme').value; GM_setValue("xpath_config", config); setupHotkeyListener(); hideSettings(); showNotification("设置已保存", "success"); } catch (error) { showNotification("保存设置失败: " + error.message, "error"); } } // 开始拖拽设置面板 function startDragSettings(e) { if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT' || e.target.tagName === 'BUTTON') return; isDraggingSettings = true; dragStartX = e.clientX; dragStartY = e.clientY; const rect = settingsPanel.getBoundingClientRect(); settingsX = rect.left; settingsY = rect.top; settingsPanel.style.transform = 'none'; settingsPanel.style.cursor = 'grabbing'; } // 处理设置面板拖拽 function handleDragSettings(e) { if (!isDraggingSettings) return; const dx = e.clientX - dragStartX; const dy = e.clientY - dragStartY; settingsPanel.style.left = `${settingsX + dx}px`; settingsPanel.style.top = `${settingsY + dy}px`; } // 停止拖拽设置面板 function stopDragSettings() { isDraggingSettings = false; if (settingsPanel) { settingsPanel.style.cursor = ''; } } // 解析快捷键 function parseHotkey(hotkey) { const parts = hotkey.split('+').map(p => p.trim().toLowerCase()); const modifiers = { shift: false, ctrl: false, alt: false, meta: false }; let mainKey = ''; for (const part of parts) { switch (part.toLowerCase()) { case 'shift': modifiers.shift = true; break; case 'ctrl': case 'control': modifiers.ctrl = true; break; case 'alt': modifiers.alt = true; break; case 'meta': case 'cmd': case 'command': modifiers.meta = true; break; default: mainKey = part; } } return { modifiers, mainKey }; } // 处理快捷键 function handleHotkey(event) { const { modifiers, mainKey } = parseHotkey(config.hotkey); const matchModifiers = event.shiftKey === modifiers.shift && event.ctrlKey === modifiers.ctrl && event.altKey === modifiers.alt && event.metaKey === modifiers.meta; const matchKey = mainKey ? event.key.toLowerCase() === mainKey.toLowerCase() : false; if (matchModifiers && matchKey) { event.preventDefault(); event.stopPropagation(); toggleToolbar(); } } // 创建工具栏 function createToolbar() { if (document.getElementById('xpath-toolbar')) return; toolbar = document.createElement('div'); toolbar.id = 'xpath-toolbar'; toolbar.className = `xpath-panel ${config.theme}-theme`; toolbar.style.cssText = ` position: fixed; z-index: 9999; background: ${config.theme === 'dark' ? '#2c3e50' : '#f5f5f5'}; color: ${config.theme === 'dark' ? 'white' : '#333'}; padding: 15px; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); display: none; min-width: 300px; user-select: none; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; transition: opacity 0.3s ease; `; toolbar.innerHTML = `
XPath工具
×
`; const header = toolbar.querySelector('#toolbarHeader'); const input = toolbar.querySelector('#xpath-input'); const deleteBtn = toolbar.querySelector('#deleteBtn'); const evaluateBtn = toolbar.querySelector('#evaluateBtn'); const copyBtn = toolbar.querySelector('#copyBtn'); const settingsBtn = toolbar.querySelector('#settingsBtn'); const closeBtn = toolbar.querySelector('#closeToolbar'); const clearBtn = toolbar.querySelector('#clearInput'); const hideMode = toolbar.querySelector('#hideMode'); toolbar.addEventListener('mousedown', e => e.stopPropagation()); toolbar.addEventListener('click', e => e.stopPropagation()); header.addEventListener('mousedown', startDragToolbar); document.addEventListener('mousemove', handleDragToolbar); document.addEventListener('mouseup', stopDragToolbar); hideMode.addEventListener('change', function() { deleteBtn.innerHTML = this.checked ? ' 隐藏' : ' 删除'; }); function startDragToolbar(e) { if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return; isDraggingToolbar = true; dragStartX = e.clientX; dragStartY = e.clientY; const rect = toolbar.getBoundingClientRect(); toolbarX = rect.left; toolbarY = rect.top; toolbar.style.cursor = 'grabbing'; } function handleDragToolbar(e) { if (!isDraggingToolbar) return; const dx = e.clientX - dragStartX; const dy = e.clientY - dragStartY; toolbar.style.left = `${toolbarX + dx}px`; toolbar.style.top = `${toolbarY + dy}px`; } function stopDragToolbar() { isDraggingToolbar = false; toolbar.style.cursor = ''; } settingsBtn.addEventListener('click', () => { showSettings(); }); closeBtn.addEventListener('click', () => { hideToolbar(); }); clearBtn.addEventListener('click', () => { input.value = ''; input.focus(); }); evaluateBtn.addEventListener('click', () => { evaluateXPath(input.value); }); copyBtn.addEventListener('click', () => { if (input.value) { navigator.clipboard.writeText(input.value).then(() => { showNotification("XPath已复制到剪贴板", "success"); }).catch(err => { showNotification("复制失败: " + err.message, "error"); }); } }); deleteBtn.addEventListener('click', () => { const xpath = input.value; if (!xpath) { showNotification("请输入XPath表达式", "error"); return; } try { const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); if (result.singleNodeValue) { if (hideMode.checked) { result.singleNodeValue.style.display = 'none'; showNotification("元素已隐藏", "success"); } else { result.singleNodeValue.remove(); showNotification("元素已删除", "success"); } input.value = ''; } else { showNotification("未找到匹配的元素", "error"); } } catch (error) { showNotification('无效的XPath表达式: ' + error.message, "error"); } }); input.addEventListener('keypress', (e) => { if (e.key === 'Enter') { evaluateXPath(input.value); } }); document.body.appendChild(toolbar); } // 评估XPath表达式 function evaluateXPath(xpath) { if (!xpath) { showNotification("请输入XPath表达式", "error"); return; } try { const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); if (result.singleNodeValue) { result.singleNodeValue.scrollIntoView({behavior: 'smooth', block: 'center'}); highlightElement(result.singleNodeValue); showNotification("找到匹配的元素", "success"); } else { showNotification("未找到匹配的元素", "error"); } } catch (error) { showNotification('无效的XPath表达式: ' + error.message, "error"); } } // 显示设置面板 function showSettings() { if (!settingsPanel) { createSettingsPanel(); } settingsPanel.style.display = 'block'; updateSettingsDisplay(); } // 初始化 (function init() { // 添加样式 const style = document.createElement('style'); style.textContent = ` .xpath-btn { border: none; border-radius: 4px; padding: 6px 12px; cursor: pointer; font-size: 14px; transition: all 0.2s ease; } .xpath-btn.primary { background: #3498db; color: white; } .xpath-btn.secondary { background: #95a5a6; color: white; } .xpath-btn.danger { background: #e74c3c; color: white; } .xpath-btn:hover { opacity: 0.9; transform: translateY(-1px); } .xpath-panel { animation: fadeIn 0.3s ease; } @keyframes fadeIn { from { opacity: 0; transform: translate(-50%, -50%) scale(0.9); } to { opacity: 1; transform: translate(-50%, -50%) scale(1); } } .dark-theme { background: #2c3e50; color: white; } .light-theme { background: #f5f5f5; color: #333; } `; document.head.appendChild(style); createToolbar(); createSettingsPanel(); setupHotkeyListener(); document.addEventListener('click', (e) => { if (settingsPanel && settingsPanel.style.display === 'block' && !settingsPanel.contains(e.target) && !toolbar.contains(e.target)) { hideSettings(); } if (toolbar.style.display === 'block' && !toolbar.contains(e.target) && !isModifierPressed) { hideToolbar(); } }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { if (settingsPanel && settingsPanel.style.display === 'block') { hideSettings(); } else { hideToolbar(); } } }); document.addEventListener('mouseover', (e) => { if (isModifierPressed && toolbar && !toolbar.contains(e.target)) { highlightElement(e.target); } }); document.addEventListener('click', (e) => { if (isModifierPressed && toolbar && !toolbar.contains(e.target)) { e.preventDefault(); e.stopPropagation(); const xpathInput = document.getElementById('xpath-input'); if (xpathInput) { xpathInput.value = getXPath(e.target); xpathInput.focus(); } } }); document.addEventListener('keydown', (e) => { if (e.key === config.selectModifier) { isModifierPressed = true; document.body.style.cursor = 'crosshair'; } }); document.addEventListener('keyup', (e) => { if (e.key === config.selectModifier) { isModifierPressed = false; document.body.style.cursor = ''; clearHighlight(); } }); if (typeof GM_registerMenuCommand === 'function') { GM_registerMenuCommand("XPath工具设置", showSettings); } })(); // 设置快捷键监听 function setupHotkeyListener() { document.removeEventListener('keydown', handleHotkey); document.addEventListener('keydown', handleHotkey); } // 切换工具栏显示/隐藏 function toggleToolbar() { if (toolbar.style.display === 'block') { hideToolbar(); } else { showToolbar(); } } // 显示工具栏 function showToolbar() { toolbar.style.display = 'block'; toolbar.style.left = `${Math.min(mouseX, window.innerWidth - 320)}px`; toolbar.style.top = `${Math.min(mouseY, window.innerHeight - 200)}px`; document.getElementById('xpath-input').focus(); } // 隐藏工具栏 function hideToolbar() { toolbar.style.display = 'none'; if (config.clearOnClose) { document.getElementById('xpath-input').value = ''; } clearHighlight(); } })();