获取CSS选择器

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

// ==UserScript==
// @name          获取CSS选择器
// @description   JS实现获取CSS选择器,方便开发者使用
// @version      1.2
// @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 escapeClassName = (cls) => {
                return cls.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1');
            };

            const classes = className.split(/\s+/).filter(c => c && !c.includes(':'));
            if (classes.length > 0) {
                // 初始包含所有类名(已转义)
                str += classes.map(c => `.${escapeClassName(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 => `.${escapeClassName(c)}`)
                        .join('')}`;

                        // 检查唯一性
                        let matchCount = 0;
                        try {
                            for (const child of parentNode.children) {
                                if (child.matches(testSelector)) matchCount++;
                            }
                        } catch (e) {
                            continue; // 如果选择器无效,跳过这个类
                        }

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

        // 检查处理后的选择器在父元素下的匹配数量
        let matchCount = 0;
        try {
            for (const child of parentNode.children) {
                if (child.matches(str)) {
                    matchCount++;
                }
            }
        } catch (e) {
            // 如果选择器无效,回退到简单标签选择器
            str = tagName.toLowerCase();
            matchCount = Array.from(parentNode.children).filter(el => el.tagName.toLowerCase() === str).length;
        }

        // 如果仍然多个匹配,添加:nth-child
        if (matchCount > 1) {
            // 去除多余class
            const classes = str.match(/\.[^\\]*(?:\\.[^\\]*)*/g); // 匹配转义后的类名
            if (classes) {
                let canRemove = true;
                while (canRemove) {
                    canRemove = false;
                    for (let i = 0; i < classes.length; i++) {
                        const testSelector = str.replace(classes[i], '');
                        let newMatchCount = 0;
                        try {
                            for (const child of parentNode.children) {
                                if (child.matches(testSelector)) {
                                    newMatchCount++;
                                }
                            }
                        } catch (e) {
                            continue; // 如果选择器无效,跳过这个类
                        }
                        if (newMatchCount <= matchCount) {
                            str = testSelector;
                            matchCount = newMatchCount;
                            classes.splice(i, 1);
                            canRemove = true;
                            break;
                        }
                    }
                }
            }
            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 now = new Date();
        const timeString = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;

        console.group(`🔍 选择器获取结果 ${timeString}`);
        log(
            '元素:\nA: %o\nA选择器: %c%s%c\nB: %o\nB选择器: %c%s%c',
            state.elementA || '<未定义>',
            'color: orange', selectorA || '<无差异>', 'color: inherit',
            state.elementB || '<未定义>',
            'color: orange', selectorB || '<无差异>', 'color: inherit'
        );
        log(
            '公共祖先: %o\n祖: %c%s%c\nA: %c%s%c\nB: %c%s%c',
            ancestor || '<未定义>',
            'color: orange', selector || '<无相同>', 'color: inherit',
            'color: orange', diffA || '<无差异>', 'color: inherit',
            'color: orange', diffB || '<无差异>', 'color: inherit'
        );
        const diffLines = [];
        if (selector.includes('\\')) {
            diffLines.push(`祖: %c${selector.replace(/\\/g, '\\\\')}%c`);
        }
        if (selectorA.includes('\\')) {
            diffLines.push(`A: %c${selectorA.replace(/\\/g, '\\\\')}%c`);
        }
        if (selectorB.includes('\\')) {
            diffLines.push(`B: %c${selectorB.replace(/\\/g, '\\\\')}%c`);
        }
        if (diffLines.length > 0) {
            // 合并 diffLines 到一个 console.log
            log(
                '选择器 (转义后):\n' + diffLines.map(line =>
                                               line.split('%c')[0] + '%c' + line.split('%c')[1] + '%c'
                                              ).join('\n'),
                ...diffLines.flatMap(() => ['color: orange', 'color: inherit'])
            );
        }
        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();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址