XPath工具

按自定义快捷键显示输入框,提供XPath操作和功能

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         XPath工具
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  按自定义快捷键显示输入框,提供XPath操作和功能
// @author       Ace
// @match        https://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_download
// ==/UserScript==

(function() {
    'use strict';

    let toolbar = null;
    let settingsPanel = null;
    let currentElement = null;
    let isModifierPressed = false;
    let originalBackgroundColor = '';
    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: "#FFF59D",
        selectModifier: "Shift"
    };

    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;
        }
        originalBackgroundColor = getComputedStyle(element).backgroundColor;
        element.style.backgroundColor = config.highlightColor;
        currentElement = element;
    }

    function clearHighlight() {
        if (currentElement) {
            currentElement.style.backgroundColor = originalBackgroundColor;
            currentElement = null;
        }
    }

    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 sibling of siblings) {
            if (sibling === element) {
                return `${getXPath(element.parentNode)}/${element.tagName.toLowerCase()}[${ix + 1}]`;
            }
            if (sibling.nodeType === 1 && sibling.tagName === element.tagName) {
                ix++;
            }
        }
    }

    function exportConfig() {
        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
        });
    }

    function importConfig() {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = 'application/json';
        input.onchange = e => {
            const file = e.target.files[0];
            const reader = new FileReader();
            reader.onload = event => {
                try {
                    const imported = JSON.parse(event.target.result);
                    config = {...config, ...imported};
                    GM_setValue("xpath_config", config);
                    updateSettingsDisplay();
                } catch (error) {
                    alert("配置文件解析失败");
                }
            };
            reader.readAsText(file);
        };
        input.click();
    }

    function createSettingsPanel() {
        settingsPanel = document.createElement('div');
        settingsPanel.id = 'xpath-settings';
        settingsPanel.style.cssText = `
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: #2c3e50;
            padding: 20px;
            border-radius: 8px;
            color: white;
            z-index: 10000;
            display: none;
            box-shadow: 0 0 15px rgba(0,0,0,0.3);
            min-width: 300px;
            user-select: none;
        `;

        settingsPanel.innerHTML = `
                <h3 style="margin:0 0 15px 0; border-bottom:1px solid #34495e; padding-bottom:10px; cursor: move;">设置</h3>
            <div class="setting-item">
                <label>
                    <input type="checkbox" id="clearOnClose" ${config.clearOnClose ? 'checked' : ''}>
                    关闭时清空输入框
                </label>
            </div>
            <div class="setting-item">
                <label>工具栏快捷键:
                    <input type="text" 
                        id="hotkey" 
                        value="${config.hotkey}" 
                        placeholder="例如:Shift+X"
                        style="color: #333; background: #fff">
                </label>
            </div>
            <div class="setting-item">
                <label>元素选择键:
                    <select id="selectModifier" 
                            style="color: #333; 
                                background: #fff;
                                padding: 4px;
                                border: 1px solid #34495e;
                                border-radius: 4px;">
                        <option value="Shift" ${config.selectModifier === 'Shift' ? 'selected' : ''}>Shift</option>
                        <option value="Ctrl" ${config.selectModifier === 'Ctrl' ? 'selected' : ''}>Ctrl</option>
                        <option value="Alt" ${config.selectModifier === 'Alt' ? 'selected' : ''}>Alt</option>
                    </select>
                </label>
            </div>
            <div class="setting-item">
                <label>高亮颜色:
                    <input type="color" 
                        id="highlightColor" 
                        value="${config.highlightColor}"
                        style="height: 30px;
                                padding: 2px;">
                </label>
            </div>
            <div style="margin-top:15px;">
                <button id="exportConfig" style="padding:6px 12px; background:#3498db; border:none; color:white; border-radius:4px;">导出配置</button>
                <button id="importConfig" style="margin-left:10px; padding:6px 12px; background:#3498db; border:none; color:white; border-radius:4px;">导入配置</button>
            </div>
            <div style="margin-top:20px; text-align:right;">
                <button id="saveSettings" style="padding:8px 16px; background:#27ae60; border:none; color:white; border-radius:4px;">保存</button>
                <button id="closeSettings" style="margin-left:10px; padding:8px 16px; background:#e74c3c; border:none; color:white; border-radius:4px;">取消</button>
            </div>
        `;

        const header = settingsPanel.querySelector('h3');
        header.addEventListener('mousedown', startDragSettings);
        document.addEventListener('mousemove', handleDragSettings);
        document.addEventListener('mouseup', stopDragSettings);

        settingsPanel.querySelector('#saveSettings').addEventListener('click', function(e) {
            e.stopPropagation();
            saveSettings();
        });

        settingsPanel.querySelector('#closeSettings').addEventListener('click', function(e) {
            e.stopPropagation();
            settingsPanel.style.display = 'none';
        });

        settingsPanel.querySelector('#exportConfig').addEventListener('click', exportConfig);
        settingsPanel.querySelector('#importConfig').addEventListener('click', importConfig);

        document.body.appendChild(settingsPanel);
    }

    function updateSettingsDisplay() {
        document.getElementById('clearOnClose').checked = config.clearOnClose;
        document.getElementById('hotkey').value = config.hotkey;
        document.getElementById('selectModifier').value = config.selectModifier;
        document.getElementById('highlightColor').value = config.highlightColor;
    }

    function saveSettings() {
        config.clearOnClose = document.getElementById('clearOnClose').checked;
        config.hotkey = document.getElementById('hotkey').value.trim();
        config.selectModifier = document.getElementById('selectModifier').value;
        config.highlightColor = document.getElementById('highlightColor').value;
        GM_setValue("xpath_config", config);
        setupHotkeyListener();
        settingsPanel.style.display = 'none';
    }

    function startDragSettings(e) {
        isDraggingSettings = true;
        dragStartX = e.clientX;
        dragStartY = e.clientY;
        const rect = settingsPanel.getBoundingClientRect();
        settingsX = rect.left;
        settingsY = rect.top;
        settingsPanel.style.transform = 'none';
    }

    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;
    }

    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': modifiers.ctrl = true; break;
                case 'alt': modifiers.alt = true; break;
                case 'meta': 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();
            showToolbar();
        }
    }

    function createToolbar() {
        if (document.getElementById('xpath-toolbar')) return;

        toolbar = document.createElement('div');
        toolbar.id = 'xpath-toolbar';
        toolbar.style.cssText = `
            position: fixed;
            z-index: 9999;
            background: #2c3e50;
            color: white;
            padding: 15px;
            border-radius: 8px;
            box-shadow: 0 4px 15px rgba(0,0,0,0.3);
            display: none;
            min-width: 300px;
            user-select: none;
        `;

        toolbar.innerHTML = `
            <div style="position:relative;">
                <div id="toolbarHeader" style="cursor: move; margin-bottom: 10px; padding: 5px; border-radius: 4px; background: #34495e;">
                    XPath工具
                    <button id="settingsBtn" style="float:right; background:none; border:none; color:white; cursor:pointer; padding:0 5px;">⚙</button>
                </div>
                <div style="position:relative; width: 250px;">
                    <input type="text" id="xpath-input" placeholder="输入或生成的XPath" 
                        style="width: 100%;
                               padding: 8px 25px 8px 8px;
                               border: 1px solid #34495e;
                               border-radius: 4px;
                               background: #34495e;
                               color: white;
                               box-sizing: border-box;">
                    <span id="clearInput" 
                        style="position: absolute;
                               right: 8px;
                               top: 50%;
                               transform: translateY(-50%);
                               cursor: pointer;
                               color: #888;
                               padding: 0 5px;
                               background: #34495e;
                               border-radius: 50%;">×</span>
                </div>
                <div style="margin-top:10px;">
                    <button id="deleteBtn" style="padding:6px 12px; background:#e74c3c; border:none; border-radius:4px; color:white; cursor:pointer;">删除元素</button>
                    <label style="margin-left:10px; font-size:0.9em;">
                        <input type="checkbox" id="hideMode"> 隐藏模式
                    </label>
                </div>
            </div>
        `;

        const header = toolbar.querySelector('#toolbarHeader');
        const input = toolbar.querySelector('#xpath-input');
        const deleteBtn = toolbar.querySelector('#deleteBtn');
        const settingsBtn = toolbar.querySelector('#settingsBtn');
        const clearBtn = toolbar.querySelector('#clearInput');

        toolbar.addEventListener('mousedown', e => e.stopPropagation());
        toolbar.addEventListener('click', e => e.stopPropagation());

        header.addEventListener('mousedown', startDragToolbar);
        document.addEventListener('mousemove', handleDragToolbar);
        document.addEventListener('mouseup', stopDragToolbar);

        function startDragToolbar(e) {
            isDraggingToolbar = true;
            dragStartX = e.clientX;
            dragStartY = e.clientY;
            const rect = toolbar.getBoundingClientRect();
            toolbarX = rect.left;
            toolbarY = rect.top;
        }

        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;
        }

        settingsBtn.addEventListener('click', () => {
            settingsPanel.style.display = 'block';
            updateSettingsDisplay();
        });

        clearBtn.addEventListener('click', () => {
            input.value = '';
            input.focus();
        });

        deleteBtn.addEventListener('click', () => {
            const xpath = input.value;
            if (!xpath) return;

            try {
                const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
                if (result.singleNodeValue) {
                    if (toolbar.querySelector('#hideMode').checked) {
                        result.singleNodeValue.style.display = 'none';
                    } else {
                        result.singleNodeValue.remove();
                    }
                    input.value = '';
                }
            } catch (error) {
                alert('无效的XPath表达式');
            }
        });

        document.body.appendChild(toolbar);
    }

    (function init() {
        createToolbar();
        createSettingsPanel();
        setupHotkeyListener();

        document.addEventListener('click', (e) => {
            if (settingsPanel.style.display !== 'block' && 
                !toolbar.contains(e.target) &&
                !isModifierPressed) {
                hideToolbar();
            }

            if (settingsPanel.style.display === 'block' && 
                !settingsPanel.contains(e.target) && 
                !toolbar.contains(e.target)) {
                settingsPanel.style.display = 'none';
            }
        });

        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape') hideToolbar();
        });

        document.addEventListener('mouseover', (e) => {
            if (isModifierPressed && !toolbar.contains(e.target)) {
                highlightElement(e.target);
            }
        });

        document.addEventListener('click', (e) => {
            if (isModifierPressed && !toolbar.contains(e.target)) {
                e.preventDefault();
                e.stopPropagation();
                document.getElementById('xpath-input').value = getXPath(e.target);
            }
        });

        document.addEventListener('keydown', (e) => {
            if (e.key === config.selectModifier) isModifierPressed = true;
        });

        document.addEventListener('keyup', (e) => {
            if (e.key === config.selectModifier) {
                isModifierPressed = false;
                clearHighlight();
            }
        });

        GM_registerMenuCommand("XPath工具设置", () => {
            settingsPanel.style.display = 'block';
            updateSettingsDisplay();
        });
    })();

    function setupHotkeyListener() {
        document.removeEventListener('keydown', handleHotkey);
        document.addEventListener('keydown', handleHotkey);
    }

    function showToolbar() {
        toolbar.style.display = 'block';
        toolbar.style.left = `${mouseX}px`;
        toolbar.style.top = `${mouseY}px`;
        document.getElementById('xpath-input').focus();
    }

    function hideToolbar() {
        toolbar.style.display = 'none';
        if (config.clearOnClose) {
            document.getElementById('xpath-input').value = '';
        }
    }
})();