Element Selector Inspector - Advanced (Multiple Selections & Strict Relationships)

Click: Main, Alt+Click: Addition, Ctrl+Click: Excluded. Supports multiple selections per type & strict relationship rules.

// ==UserScript==
// @name         Element Selector Inspector - Advanced (Multiple Selections & Strict Relationships)
// @namespace    http://tampermonkey.net/
// @version      3.8
// @description  Click: Main, Alt+Click: Addition, Ctrl+Click: Excluded. Supports multiple selections per type & strict relationship rules.
// @author       lmdw
// @match        *://*/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // --- Tùy chỉnh ---
    let activated = false;
    const toggleKey = { ctrl: true, alt: true, key: '1' }; // Phím tắt để bật/tắt (Ctrl+Alt+1)
    const overlayPadding = 4; // Khoảng cách giữa overlay và phần tử
    const hoverColor = 'rgba(255,223,0,0.8)'; // Màu khi di chuột
    const mainColor = 'rgba(255,165,0,1)'; // Màu phần tử chính (vàng cam)
    const additionColor = 'rgba(0,255,0,1)'; // Màu phần tử bổ sung (xanh lá)
    const excludeColor = 'rgba(255,0,0,1)'; // Màu phần tử loại trừ (đỏ)
    const infoLabelBackground = 'rgba(255,223,0,0.95)'; // Màu nền của info label

    // --- Biến toàn cục (Lưu trữ các Set phần tử đã chọn) ---
    let hoverOverlay, infoLabel;
    let mainElements = new Set(); // Các phần tử chính
    let additionElements = new Set(); // Các phần tử bổ sung
    let excludedElements = new Set(); // Các phần tử loại trừ

    // --- Ánh xạ phần tử sang overlay ---
    // Sử dụng WeakMap để tránh rò rỉ bộ nhớ nếu phần tử bị xóa khỏi DOM
    const elementOverlayMap = new WeakMap();
    const elementInfoMap = new WeakMap(); // Có thể không cần thiết nếu infoLabel chỉ hiển thị cho hover

    // --- Hàm tạo overlay chung ---
    function createOverlays() {
        if (!hoverOverlay) {
            hoverOverlay = document.createElement('div');
             // Style chung cho hover overlay
            Object.assign(hoverOverlay.style, {
                position: 'absolute',
                borderRadius: '4px',
                pointerEvents: 'none',
                zIndex: 2147483646,
                display: 'none',
                boxSizing: 'border-box',
                border: `2px solid ${hoverColor}`,
                background: hoverColor.replace(/[^,]+(?=\))/, '0.1')
            });
            document.body.appendChild(hoverOverlay);
        }
        if (!infoLabel) {
            infoLabel = document.createElement('div');
             // Style cho info label
            Object.assign(infoLabel.style, {
                position: 'absolute',
                padding: '2px 6px',
                background: infoLabelBackground,
                color: '#000',
                fontSize: '12px',
                fontFamily: 'Arial',
                borderRadius: '4px',
                pointerEvents: 'none',
                zIndex: 2147483647,
                whiteSpace: 'nowrap',
                display: 'none'
            });
            document.body.appendChild(infoLabel);
        }
    }

    // --- Hàm cập nhật vị trí và kích thước overlay ---
    function updateOverlay(overlay, el) {
        try {
            const rect = el.getBoundingClientRect();
             // Kiểm tra nếu phần tử không còn trong viewport hoặc có kích thước không hợp lệ
            if (rect.width === 0 || rect.height === 0 || rect.right < 0 || rect.bottom < 0 || rect.left > window.innerWidth || rect.top > window.innerHeight) {
                 overlay.style.display = 'none'; // Ẩn overlay nếu phần tử không hiển thị
                 return false; // Báo hiệu không cập nhật thành công
            }

            overlay.style.top = `${rect.top + window.scrollY - overlayPadding}px`;
            overlay.style.left = `${rect.left + window.scrollX - overlayPadding}px`;
            overlay.style.width = `${rect.width + overlayPadding * 2}px`;
            overlay.style.height = `${rect.height + overlayPadding * 2}px`;
            overlay.style.display = 'block';
            return true; // Báo hiệu cập nhật thành công
        } catch (e) {
            // Xử lý lỗi nếu getBoundingClientRect thất bại (ví dụ: phần tử bị display: none đột ngột)
            overlay.style.display = 'none';
            console.warn("Error updating overlay for element:", el, e);
            return false;
        }
    }


    // --- Hàm cập nhật thông tin trên label ---
    function updateInfoLabel(el, x, y) {
        const tag = el.tagName.toLowerCase();
        const id = el.id ? `#${el.id}` : '';
        // Lấy các class và lọc bỏ khoảng trắng thừa, sau đó nối lại
        const classes = el.className ? `.${el.className.trim().split(/\s+/).filter(cls => cls.length > 0).join('.')}` : '';

        infoLabel.textContent = `${tag}${id}${classes}`;
        infoLabel.style.top = `${y + 15 + window.scrollY}px`;
        infoLabel.style.left = `${x + 15 + window.scrollX}px`;
        infoLabel.style.display = 'block';
    }

    // --- Hàm xử lý mouseover ---
    function mouseOver(e) {
        // Bỏ qua nếu không activated, hoặc di chuột vào chính overlay/label, hoặc root document
        if (!activated || e.target === hoverOverlay || e.target === infoLabel || e.target === document.documentElement || e.target === document.body) return;

        // Tránh cập nhật nếu di chuột qua một phần tử đã có overlay (tránh nhấp nháy)
        // if (elementOverlayMap.has(e.target)) return; // Commented out - want hover overlay even on selected

        updateOverlay(hoverOverlay, e.target);
        updateInfoLabel(e.target, e.clientX, e.clientY);
    }

    // --- Hàm xử lý mouseout/mouseleave ---
    function mouseOut(e) {
        if (!activated) return;
        // Ẩn label và hover overlay khi chuột rời khỏi bất kỳ phần tử nào (hoặc trang)
        hoverOverlay.style.display = 'none';
        infoLabel.style.display = 'none';
    }

    // --- Hàm tạo và trả về overlay với style và zIndex ---
    function createAndStoreOverlay(el, color, zIndex, hatched = false) {
        // Kiểm tra nếu overlay đã tồn tại cho phần tử này, xóa nó trước
        if (elementOverlayMap.has(el)) {
             removeOverlay(el);
        }

        const overlay = document.createElement('div');
        Object.assign(overlay.style, {
            position: 'absolute',
            borderRadius: '4px',
            pointerEvents: 'none',
            boxSizing: 'border-box',
            border: `2px solid ${color}`,
            background: color.replace(/[^,]+(?=\))/, '0.2'),
            display: 'block',
            zIndex: zIndex,
            opacity: 1, // Mặc định là 1
            transition: 'opacity 0.2s ease' // Thêm transition cho hiệu ứng mờ dần khi xóa
        });
        if (hatched) {
            overlay.style.backgroundImage = 'linear-gradient(45deg, rgba(0,0,0,0.1) 25%, transparent 25%, transparent 50%, rgba(0,0,0,0.1) 50%, rgba(0,0,0,0.1) 75%, transparent 75%, transparent)';
            overlay.style.backgroundSize = '20px 20px';
        }

        // Thử cập nhật vị trí, nếu thất bại (phần tử không hiển thị), không thêm overlay
        if (!updateOverlay(overlay, el)) {
             return null; // Không thêm overlay nếu phần tử không hiển thị
        }

        document.body.appendChild(overlay);
        elementOverlayMap.set(el, overlay); // Lưu trữ tham chiếu overlay
        return overlay;
    }

    // --- Hàm xóa overlay của một phần tử ---
    function removeOverlay(el) {
        const overlay = elementOverlayMap.get(el);
        if (overlay) {
            // Sử dụng transition để mờ dần trước khi xóa
            overlay.style.opacity = 0;
            overlay.addEventListener('transitionend', function() {
                 if (overlay.parentNode) {
                    overlay.parentNode.removeChild(overlay);
                 }
            });
            elementOverlayMap.delete(el);
        }
    }

    // --- Helper: Kiểm tra mối quan hệ (chứa, bị chứa, hoặc giống nhau) ---
    function isRelated(el1, el2) {
        if (!el1 || !el2) return false;
         // Kiểm tra nếu el1 hoặc el2 không còn trong DOM
        if (!el1.isConnected || !el2.isConnected) return false;
        return el1 === el2 || el1.contains(el2) || el2.contains(el1);
    }

    // --- Helper: Kiểm tra nếu element là tổ tiên của bất kỳ phần tử nào trong set ---
    function isAncestorOfAnyInSet(element, set) {
        if (!element || !set || set.size === 0) return false;
        for (const el of set) {
            if (element !== el && element.contains(el)) {
                return true;
            }
        }
        return false;
    }

     // --- Helper: Kiểm tra nếu element là con cháu của bất kỳ phần tử nào trong set ---
    function isDescendantOfAnyInSet(element, set) {
         if (!element || !set || set.size === 0) return false;
         for (const el of set) {
             if (element !== el && el.contains(element)) {
                 return true;
             }
         }
         return false;
    }

    // --- Helper: Kiểm tra nếu element là con cháu của bất kỳ phần tử nào trong Main hoặc Addition ---
    function isDescendantOfAnyMainOrAddition(element) {
        return isDescendantOfAnyInSet(element, mainElements) || isDescendantOfAnyInSet(element, additionElements);
    }

    // --- Hàm xóa phần tử và overlay của nó theo loại ---
    function removeElement(el, type) {
        let targetSet;
        let overlayProp;
        let zIndex;

        switch (type) {
            case 'main':
                targetSet = mainElements;
                overlayProp = 'mainOverlay';
                zIndex = 2147483645;
                break;
            case 'addition':
                targetSet = additionElements;
                overlayProp = 'additionOverlay';
                zIndex = 2147483646;
                break;
            case 'exclude':
                targetSet = excludedElements;
                overlayProp = 'excludeOverlay';
                zIndex = 2147483647;
                break;
            default:
                console.error("Invalid removal type:", type);
                return;
        }

        if (targetSet.has(el)) {
            targetSet.delete(el);
            removeOverlay(el); // Xóa overlay qua WeakMap

            // Xử lý dọn dẹp các phần tử Excluded là con cháu của phần tử vừa xóa
            if (type === 'main' || type === 'addition') {
                const excludedToRemove = [];
                excludedElements.forEach(ex => {
                    // Nếu Excluded này là con cháu của `el` VÀ không phải con cháu của bất kỳ Main/Addition còn lại nào
                    if (el.contains(ex) && !isDescendantOfAnyMainOrAddition(ex)) {
                        excludedToRemove.push(ex);
                    }
                });
                excludedToRemove.forEach(ex => removeElement(ex, 'exclude'));
                if (excludedToRemove.length > 0) {
                    console.log(`Removed ${excludedToRemove.length} Excluded elements dependent on the removed ${type} element.`);
                }
            }
             console.log(`${type.charAt(0).toUpperCase() + type.slice(1)} Element Removed:`, el);
        } else {
             // console.warn(`Attempted to remove a ${type} element that was not in the set:`, el);
        }
    }

    // --- Hàm xử lý sự kiện click ---
    function clickHandler(e) {
        // Bỏ qua nếu không activated, hoặc click vào chính overlay/label
        if (!activated || e.target === hoverOverlay || e.target === infoLabel) return;

        e.preventDefault();
        e.stopPropagation();

        const target = e.target;
        let mode = null;
        let targetSet = null;
        let overlayColor = null;
        let overlayZIndex = null;
        let isHatched = false;

        // Xác định chế độ click
        if (!e.ctrlKey && !e.altKey) {
            mode = 'main';
            targetSet = mainElements;
            overlayColor = mainColor;
            overlayZIndex = 2147483645;
        } else if (e.altKey && !e.ctrlKey) {
            mode = 'addition';
            targetSet = additionElements;
            overlayColor = additionColor;
            overlayZIndex = 2147483646;
        } else if (e.ctrlKey && !e.altKey) {
            mode = 'exclude';
            targetSet = excludedElements;
            overlayColor = excludeColor;
            overlayZIndex = 2147483647;
            isHatched = true;
        } else {
            // Tổ hợp phím không xác định, bỏ qua
            console.log("Unknown key combination clicked.");
            return;
        }

         // --- Bước 1: Xử lý Bỏ chọn (nếu click vào phần tử đã chọn cùng loại) ---
        if (targetSet.has(target)) {
            removeElement(target, mode);
            return; // Đã xử lý, thoát
        }

        // --- Bước 2: Xử lý Logic Tổ tiên/Con cháu trong CÙNG loại ---
        // Nếu target là con cháu của phần tử đã chọn cùng loại, bỏ qua click này cho loại đó
        if (isDescendantOfAnyInSet(target, targetSet)) {
             console.log(`Clicked element is a descendant of an already selected ${mode} element. Ignoring.`);
             return; // Đã xử lý, thoát
        }

        // Nếu target là tổ tiên của phần tử đã chọn cùng loại, xóa các con cháu đó
        const descendantsToRemoveInSameMode = [];
        targetSet.forEach(el => {
            if (target.contains(el)) {
                descendantsToRemoveInSameMode.push(el);
            }
        });
        descendantsToRemoveInSameMode.forEach(el => removeElement(el, mode));
        if (descendantsToRemoveInSameMode.length > 0) {
             console.log(`Clicked element is an ancestor of selected ${mode} elements. Replaced descendants with ancestor.`);
        }

        // --- Bước 3: Xử lý Logic Quan hệ giữa CÁC loại ---

        // Chụp lại trạng thái hiện tại trước khi xử lý xung đột
        const initiallySelectedAsMain = mainElements.has(target);
        const initiallySelectedAsAddition = additionElements.has(target);
        const initiallySelectedAsExcluded = excludedElements.has(target);

        let allowAdd = true; // Cờ để quyết định có thêm target vào targetSet cuối cùng không

        if (mode === 'main') {
            // Nếu click Main, xóa mọi phần tử Addition có quan hệ với target
            const additionsToRemove = [];
            additionElements.forEach(addEl => {
                 if (isRelated(target, addEl)) {
                     additionsToRemove.push(addEl);
                 }
            });
            additionsToRemove.forEach(addEl => removeElement(addEl, 'addition'));
            if (additionsToRemove.length > 0) {
                console.log(`Conflict with Addition elements on Main click. Removed ${additionsToRemove.length} conflicting Additions.`);
            }

            // Nếu target là Excluded, xóa nó khỏi Excluded
            if (initiallySelectedAsExcluded) {
                 removeElement(target, 'exclude');
                 console.log(`Element was Excluded. Removed exclusion on Main click.`);
            }

        } else if (mode === 'addition') {
             // Nếu click Addition, kiểm tra xem có quan hệ với Main không
            const mainsRelated = Array.from(mainElements).filter(mainEl => isRelated(target, mainEl));
            if (mainsRelated.length > 0) {
                 // Yêu cầu: ưu tiên Main, vô hiệu hóa click Addition nếu xung đột
                 console.log('Invalid Addition element: Related to a Main element. Ignoring Addition click.');
                 allowAdd = false; // Không cho phép thêm vào Addition
            } else {
                // Nếu không xung đột với Main, kiểm tra và xóa khỏi Excluded nếu cần
                 if (initiallySelectedAsExcluded) {
                     removeElement(target, 'exclude');
                     console.log(`Element was Excluded. Removed exclusion on Addition click.`);
                 }
                 // Không cần kiểm tra xung đột giữa Addition với Addition khác theo yêu cầu
            }

        } else if (mode === 'exclude') {
             // Nếu click Excluded, kiểm tra xem có phải là con cháu của Main hoặc Addition không
            const isValidExclude = isDescendantOfAnyMainOrAddition(target);

            if (!isValidExclude) {
                 console.log('Invalid Excluded element: Not a child/descendant of any Main or Addition element. Ignoring click.');
                 allowAdd = false; // Không cho phép thêm vào Excluded
            } else {
                 // Nếu hợp lệ làm Excluded, kiểm tra và xóa khỏi Main hoặc Addition nếu cần
                 if (initiallySelectedAsMain) {
                     removeElement(target, 'main');
                     console.log(`Element was Main. Removed Main selection on Excluded click.`);
                 }
                 if (initiallySelectedAsAddition) {
                     removeElement(target, 'addition');
                     console.log(`Element was Addition. Removed Addition selection on Excluded click.`);
                 }
            }
        }

        // --- Bước 4: Thêm phần tử (nếu được phép) và tạo Overlay ---
        if (allowAdd) {
            // Kiểm tra lại lần cuối xem phần tử có còn hợp lệ để thêm vào set hiện tại sau khi xử lý xung đột không
            // (Ví dụ: bấm Ctrl+Click vào 1 phần tử, nó xóa Main/Addition chứa nó, nhưng nó lại không phải con của bất kỳ Main/Addition còn lại nào)
             if (mode === 'exclude') {
                 // Riêng với Exclude, cần kiểm tra lại điều kiện con cháu sau khi potentially xóa Main/Addition cha
                 if (!isDescendantOfAnyMainOrAddition(target)) {
                      console.log('Element became invalid for Excluded after removing its parent Main/Addition. Ignoring add.');
                      allowAdd = false; // Không thêm nếu không còn cha hợp lệ
                 }
             }
        }


        if (allowAdd) {
            targetSet.add(target);
            // Tạo/cập nhật overlay
            const overlay = createAndStoreOverlay(target, overlayColor, overlayZIndex, isHatched);
            if (overlay) {
                 console.log(`${mode.charAt(0).toUpperCase() + mode.slice(1)} Element Added:`, target);
            } else {
                 // Xóa khỏi set nếu không tạo được overlay (ví dụ: display: none)
                 targetSet.delete(target);
                 console.log(`Could not add ${mode} element (not visible):`, target);
            }
        }

        // --- Bước 5: Dọn dẹp cuối cùng cho Excluded sau mọi thay đổi ---
        // Đảm bảo tất cả Excluded còn lại đều là con cháu của ít nhất 1 Main hoặc 1 Addition
        const excludedStillValid = new Set();
        excludedElements.forEach(ex => {
            if (isDescendantOfAnyMainOrAddition(ex)) {
                excludedStillValid.add(ex);
            } else {
                removeElement(ex, 'exclude'); // Xóa các Excluded không còn hợp lệ
                console.log('Removed Excluded element that lost its Main/Addition parent:', ex);
            }
        });
         // Cập nhật lại set excludedElements
        excludedElements = excludedStillValid;

         // Note: Có thể cần một logic để cập nhật overlay nếu phần tử thay đổi kích thước/vị trí (cuộn trang, resize)
         // Điều này phức tạp hơn và không nằm trong yêu cầu hiện tại. UpdateOverlay chỉ chạy 1 lần khi tạo.
         // Cần thêm listeners cho scroll/resize để update các overlays đang hiển thị.
    }

    // --- Hàm kích hoạt inspector ---
    function activateInspector() {
        if (!hoverOverlay || !infoLabel) createOverlays();
        document.addEventListener('mouseover', mouseOver, true);
        document.addEventListener('mouseout', mouseOut, true);
        document.addEventListener('mouseleave', mouseOut, true);
        document.addEventListener('click', clickHandler, true);
        // Thêm listener cho scroll/resize để cập nhật vị trí overlays
        window.addEventListener('scroll', updateAllOverlays, true);
        window.addEventListener('resize', updateAllOverlays, true);

        activated = true;
        console.log('Inspector Activated. Use Ctrl+Alt+1 to toggle. Click modes: Main, Alt+Click: Addition, Ctrl+Click: Excluded.');
    }

    // --- Hàm hủy kích hoạt inspector ---
    function deactivateInspector() {
        document.removeEventListener('mouseover', mouseOver, true);
        document.removeEventListener('mouseout', mouseOut, true);
        document.removeEventListener('mouseleave', mouseOut, true);
        document.removeEventListener('click', clickHandler, true);
        window.removeEventListener('scroll', updateAllOverlays, true);
        window.removeEventListener('resize', updateAllOverlays, true);

        // Dọn dẹp tất cả phần tử và overlays
        removeAllElementsAndOverlays();

        hoverOverlay.style.display = 'none';
        infoLabel.style.display = 'none';
        activated = false;
        console.log('Inspector Deactivated.');
    }

    // --- Hàm xóa tất cả phần tử đã chọn và overlays ---
    function removeAllElementsAndOverlays() {
        [...mainElements].forEach(el => removeElement(el, 'main'));
        [...additionElements].forEach(el => removeElement(el, 'addition'));
        [...excludedElements].forEach(el => removeElement(el, 'exclude')); // Excluded nên được xóa cuối cùng hoặc độc lập
         // Đảm bảo tất cả overlays từ WeakMap bị xóa
        for(let [el, overlay] of elementOverlayMap) {
             if (overlay && overlay.parentNode) {
                  overlay.parentNode.removeChild(overlay);
             }
        }
        elementOverlayMap = new WeakMap(); // Reset WeakMap
         mainElements.clear();
         additionElements.clear();
         excludedElements.clear();
    }

     // --- Hàm cập nhật vị trí của tất cả overlays đang hiển thị ---
     function updateAllOverlays() {
         if (!activated) return; // Chỉ cập nhật khi inspector đang hoạt động

         // Tạo danh sách các phần tử cần cập nhật overlay
         const elementsToUpdate = new Set([...mainElements, ...additionElements, ...excludedElements]);

         elementsToUpdate.forEach(el => {
             const overlay = elementOverlayMap.get(el);
             if (overlay) {
                  // Nếu không thể cập nhật overlay (phần tử không hiển thị), xóa phần tử khỏi set tương ứng
                 if (!updateOverlay(overlay, el)) {
                     if (mainElements.has(el)) removeElement(el, 'main');
                     else if (additionElements.has(el)) removeElement(el, 'addition');
                     else if (excludedElements.has(el)) removeElement(el, 'exclude');
                 }
             } else {
                  // Nếu phần tử có trong set nhưng không có overlay (lỗi?), thử tạo lại hoặc xóa khỏi set
                  console.warn("Element in set but no overlay found. Removing:", el);
                  if (mainElements.has(el)) removeElement(el, 'main');
                  else if (additionElements.has(el)) removeElement(el, 'addition');
                  else if (excludedElements.has(el)) removeElement(el, 'addition');
             }
         });

         // Sau khi cập nhật/xóa các overlay, cần chạy lại bước dọn dẹp Excluded cuối cùng
         // để loại bỏ những Excluded mà cha/mẹ của nó vừa bị xóa do không hiển thị
         const excludedStillValid = new Set();
         excludedElements.forEach(ex => {
             if (isDescendantOfAnyMainOrAddition(ex)) {
                 excludedStillValid.add(ex);
             } else {
                 removeElement(ex, 'exclude');
                 console.log('Removed Excluded element that lost its Main/Addition parent (due to visibility change):', ex);
             }
         });
         excludedElements = excludedStillValid; // Cập nhật lại set
     }


    // --- Hàm bật/tắt inspector ---
    function toggleInspector(e) {
        // Kiểm tra nếu đang gõ vào input/textarea, bỏ qua
        if (e.target.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)) {
             return;
        }

        if (e.ctrlKey === toggleKey.ctrl && e.altKey === toggleKey.alt && e.key === toggleKey.key) {
            if (activated) {
                 deactivateInspector();
            } else {
                 // Tạo overlays ban đầu khi bật
                 createOverlays();
                 activateInspector();
            }
            e.preventDefault(); // Ngăn chặn hành vi mặc định của phím (ví dụ: Alt+1 có thể làm gì đó trên trình duyệt)
        }
    }

    // --- Lắng nghe sự kiện phím tắt ---
    document.addEventListener('keydown', toggleInspector, false);

    // --- Khởi tạo overlays ẩn khi script chạy lần đầu ---
    // Điều này giúp các biến hoverOverlay và infoLabel có giá trị ngay cả trước khi inspector được kích hoạt lần đầu
    createOverlays();
    hoverOverlay.style.display = 'none';
    infoLabel.style.display = 'none';


})();

QingJ © 2025

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