// ==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 = `
`;
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 = `
`;
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();
}
})();