XPath工具

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

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