获取CSS选择器

JS实现获取CSS选择器,方便开发者使用

目前为 2025-04-05 提交的版本。查看 最新版本

// ==UserScript==
// @name          获取CSS选择器
// @description   JS实现获取CSS选择器,方便开发者使用
// @version      1.0
// @namespace   https://space.bilibili.com/482343
// @author      古海沉舟
// @license     古海沉舟
// @include        **
// @noframes
// @grant          GM_setClipboard
// ==/UserScript==

(function () {
    'use strict';
    var ancestor;
    const state = {
        active: false,
        elementA: null,
        elementB: null,
        masks: [],
        mousePos: { x: -1, y: -1 }
    };

    function init() {
        injectStyles();
        createMasks();
        setupEventListeners();
    }

    function setupEventListeners() {
        document.addEventListener('mousemove', handleMouseMove);
        document.addEventListener('keydown', handleKeyPress);
        document.addEventListener('click', handleActivationClick, true);
    }

    function handleMouseMove(e) {
        state.mousePos = { x: e.clientX, y: e.clientY };
        if (state.active) updateMasks();
    }

    function handleKeyPress(e) {
        if (parseKeyCombo(e) === 'C-A-s') {
            if (!state.active) {
                startSelection();
            } else {
                completeSelectionWithCurrent();
            }
            e.preventDefault();
        }
    }

    function handleActivationClick(e) {
        if (state.active) {
            e.preventDefault();
            e.stopImmediatePropagation();
            completeSelectionWithCurrent();
        }
    }

    function generateSelector(elem, iss = 0) {
        const { tagName, id, className, parentNode } = elem;

        if (tagName === 'HTML') return 'html';

        let str = tagName.toLowerCase();

        const isDescendant = (!ancestor) || (!ancestor.contains(elem)) || ancestor==elem;
        if (id) {
            if (iss == 0) {
                str += `#${id}`;
                return str;
            } else if (iss == 1 && isDescendant) {
                str += `#${id}`;
                return str;
            }
        }

        if (className) {
            const classes = className.split(/\s+/).filter(c => c);
            if (classes.length > 0) {
                // 初始包含所有类名
                str += classes.map(c => `.${c}`).join('');

                // 尝试精简类名
                let canOptimize = true;
                while (canOptimize) {
                    canOptimize = false;
                    // 遍历每个现有类名
                    for (let i = 0; i < classes.length; i++) {
                        if (!classes[i]) continue; // 跳过已删除项

                        // 构建测试选择器(移除当前类)
                        const testSelector = `${tagName.toLowerCase()}${classes
                        .filter((_, idx) => idx != i)
                        .map(c => `.${c}`)
                        .join('')}`;

                        // 检查唯一性
                        let matchCount = 0;
                        for (const child of parentNode.children) {
                            if (child.matches(testSelector)) matchCount++;
                        }

                        // 如果移除后仍唯一
                        if (matchCount == 1) {
                            classes.splice(i, 1);   // 永久删除该类
                            str = testSelector;     // 更新当前选择器
                            canOptimize = true;     // 允许继续优化
                            break;                  // 重新遍历修改后的列表
                        }
                    }
                }
            }
        }

        // 检查处理后的选择器在父元素下的匹配数量
        let matchCount = 0;
        for (const child of parentNode.children) {
            if (child.matches(str)) {
                matchCount++;
            }
        }

        // 如果仍然多个匹配,添加:nth-child
        if (matchCount > 1) {
            let childIndex = 1;
            for (let e = elem; e.previousElementSibling; e = e.previousElementSibling) {
                childIndex++;
            }
            str += `:nth-child(${childIndex})`;
        }

        return `${generateSelector(parentNode,iss)} > ${str}`;
    }

    function startSelection() {
        const initialElement = getCurrentElement();
        if (initialElement) {
            state.active = true;
            state.elementA = initialElement;
            showMasks();
        }
    }

    function completeSelectionWithCurrent() {
        const currentElement = getCurrentElement();
        if (currentElement) {
            if (!state.elementA) {
                state.elementA = currentElement;
                log('设置元素A:', currentElement);
            } else {
                state.elementB = currentElement;
                processResult();
                cleanup();
            }
        }
    }

    function createMasks() {
        state.masks = Array(2).fill().map((_, i) => {
            const mask = document.createElement('div');
            mask.className = `ancestor-mask ${i ? 'active' : ''}`;
            document.body.appendChild(mask);
            return mask;
        });
    }

    function updateMasks() {
        state.masks.forEach((mask, i) => {
            const target = i === 0 ? state.elementA : getCurrentElement();
            updateMask(mask, target);
        });
    }

    function updateMask(mask, element) {
        if (!element || element === document.body) {
            mask.style.display = 'none';
            return;
        }

        const rect = getVisibleRect(element);
        Object.assign(mask.style, {
            top: `${rect.top}px`,
            left: `${rect.left}px`,
            width: `${rect.width}px`,
            height: `${rect.height}px`,
            display: 'block'
        });
    }

    function getCurrentElement() {
        let element;
        try {
            element = document.elementFromPoint(
                state.mousePos.x,
                state.mousePos.y
            );
            while (element && element.classList.contains('ancestor-mask')) {
                element = document.elementFromPoint(
                    state.mousePos.x,
                    state.mousePos.y
                );
            }
        } catch (e) {
            return null;
        }
        return element && element !== document.body ? element : null;
    }

    function processResult() {
        ancestor = findCommonAncestor(state.elementA, state.elementB);
        if (!ancestor || ancestor === document.body) {
            log('错误:未找到有效公共祖先');
            return;
        }

        //const selector = generateSelector(ancestor);
        const selectorA = generateSelector(state.elementA, 1);
        const selectorB = generateSelector(state.elementB, 1);

        const [diffA, diffB,selector] = compareSelectors(selectorA, selectorB);
        const validation = validateSelector(selector, ancestor);
        const diffAnalysis = [
            "选择器:",
            `祖: ${selector || '<无相同>'}`,
            `A: ${diffA || '<无差异>'}`,
            `B: ${diffB || '<无差异>'}`
        ].join('\n');

        console.group('🔍 分析结果');
        log('元素A:', state.elementA);
        log('A 选择器:', selectorA);
        log('元素B:', state.elementB);
        log('B 选择器:', selectorB);
        log('公共祖先:', ancestor);
        log(diffAnalysis);
        log('验证结果:', validation.message);
        if (validation.success) {
            GM_setClipboard(selector, { type: 'text', mimetype: 'text/plain' });
        }
        console.groupEnd();
    }

    function findCommonAncestor(a, b) {
        const getPath = el => {
            const path = [];
            while (el && el !== document.body) {
                path.push(el);
                el = el.parentElement;
            }
            return path;
        };

        const pathA = getPath(a);
        return pathA.find(node => node.contains(b)) || document.body;
    }
    function compareSelectors(selectorA, selectorB) {
        const partsA = selectorA.split('>').map(p => p.trim());
        const partsB = selectorB.split('>').map(p => p.trim());

        let maxCommonLength = 0;
        const minLength = Math.min(partsA.length, partsB.length);

        // 计算最大公共前缀长度
        while (maxCommonLength < minLength && partsA[maxCommonLength] === partsB[maxCommonLength]) {
            maxCommonLength++;
        }

        // 特殊处理完全匹配的多级选择器
        if (maxCommonLength === partsA.length && maxCommonLength === partsB.length && maxCommonLength > 0) {
            maxCommonLength--;
        }

        // 提取公共部分和差异部分
        const commonPart = partsA.slice(0, maxCommonLength).join(' > ');
        const splitIndex = maxCommonLength;

        const getDiff = (arr) => {
            return splitIndex < arr.length ? arr.slice(splitIndex).join(' > ') : '';
        };

        const diffA = getDiff(partsA);
        const diffB = getDiff(partsB);

        // 特殊处理单级完全匹配
        if (partsA.length === 1 && partsB.length === 1 && diffA === diffB) {
            return [diffA, diffB, ''];
        }

        return [diffA, diffB, commonPart];
    }

    function validateSelector(selector, expected) {
        try {
            const found = document.querySelector(selector);
            return {
                success: found === expected,
                element: found,
                message: found === expected ?
                '✅ 选择器验证通过' :
                `❌ 匹配到其他元素: ${found?.outerHTML?.slice(0, 100)}...`
            };
        } catch (e) {
            return {
                success: false,
                message: `❌ 无效选择器: ${e.message}`
            };
        }
    }

    function getVisibleRect(el) {
        const rect = el.getBoundingClientRect();
        return {
            top: rect.top,
            left: rect.left,
            width: rect.width,
            height: rect.height
        };
    }

    function injectStyles() {
        const style = document.createElement('style');
        style.textContent = `
            .ancestor-mask {
                position: fixed;
                pointer-events: none;
                background: rgba(110, 180, 255, 0.2);
                border: 2px solid #1a73e8;
                z-index: 2147483647;
                transition: all 0.15s ease-out;
                display: none;
                box-shadow: 0 0 8px rgba(0,0,0,0.1);
            }
            .ancestor-mask.active {
                background: rgba(255, 80, 80, 0.2);
                border-color: #e53935;
            }
            .ancestor-mask.visible {
                display: block !important;
            }
        `;
        document.head.appendChild(style);
    }

    function parseKeyCombo(e) {
        return [
            e.ctrlKey ? 'C-' : '',
            e.altKey ? 'A-' : '',
            e.shiftKey ? 'S-' : '',
            e.key.toLowerCase()
        ].join('');
    }

    function showMasks() {
        state.masks.forEach(mask => mask.classList.add('visible'));
        updateMasks();
    }

    function cleanup() {
        state.active = false;
        state.elementA = null;
        state.elementB = null;
        hideMasks();
    }

    function hideMasks() {
        state.masks.forEach(mask => {
            mask.style.display = 'none';
            mask.classList.remove('visible');
        });
    }

    function log(...args) {
        if (true) console.log(...args);
    }

    init();
})();