Dynamic Element Toggle Panel at web page

Объединённая изолированная панель управления элементами (Shadow DOM, надёжная синхронизация, минимальная нагрузка)

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Dynamic Element Toggle Panel at web page
// @namespace    gemini
// @version      3.1.0
// @description  Объединённая изолированная панель управления элементами (Shadow DOM, надёжная синхронизация, минимальная нагрузка)
// @author       Wizzergod
// @icon         https://images.icon-icons.com/2072/PNG/96/disable_eye_hidden_hide_internet_security_view_icon_127055.png
// @match        *://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-body
// @noframes     true
// @inject-into  content
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // ---- Константы и конфигурация ----
    const STORAGE_KEY = 'DynamicTogglePanelConfig';
    const HOSTNAME = location.hostname;
    const MAX_FALLBACK_ATTEMPTS = 6;
    const FALLBACK_DELAY_MS = 700;
    const APPLY_DEBOUNCE_MS = 120;

    // ---- Утилиты хранения ----
    const getFullStorage = () => GM_getValue(STORAGE_KEY, {});
    const saveFullStorage = (obj) => GM_setValue(STORAGE_KEY, obj);
    const getSiteConfig = () => (getFullStorage()[HOSTNAME] || []);
    const saveSiteConfig = (arr) => {
        const s = getFullStorage();
        if (arr.length) s[HOSTNAME] = arr; else delete s[HOSTNAME];
        saveFullStorage(s);
    };

    // ---- Изоляция UI через Shadow DOM ----
    const host = document.createElement('div');
    host.id = 'dyn-panel-shadow-host';
    host.style.position = 'fixed';
    host.style.top = '0';
    host.style.left = '0';
    host.style.zIndex = '2147483647';

    (document.body || document.documentElement).appendChild(host);
    const shadow = host.attachShadow({ mode: 'open' });

    // ---- Обновленный и приглаженный CSS ----
    const css = `
        :host { all: initial; z-index: 99989; }

        #dyn-open-btn {
            position: fixed;
            top: 40%;
            left: -30px;
            width: 40px;
            height: 80px;
            background: #1b2838;
            border: 2px solid #2a475e;
            border-radius: 0 12px 12px 0;
            color: white;
            font-size: 18px;
            cursor: pointer;
            z-index: 2147483647;
            box-shadow: 0 2px 10px rgba(0,0,0,.5);
            opacity: 0.6;
            transition: left .3s ease, opacity .2s, background .2s;
            transition-delay: .6s;
        }

        #dyn-open-btn:hover {
            left: 0px;
            background: #2a475e;
            opacity: 0.9;
            transition-delay: 0s;
        }

        #dyn-panel {
            position: fixed;
            top: 10px;
            left: 10px;
            min-width: 320px;
            max-width: 510px;
            background: #1b2838;
            border: 2px solid #2a475e;
            border-radius: 12px;
            padding: 10px;
            color: white;
            z-index: 2147483647;
            font-family: Arial, sans-serif;
            box-shadow: 0 4px 20px rgba(0,0,0,.7);
            max-height: 95vh;
            overflow-y: auto;
            scrollbar-width: thin;
            bottom: 10px;
            opacity: 0;
            transform: scale(0.95) translateY(-10px);
            pointer-events: none;
            transition: opacity 0.2s ease, transform 0.2s ease;
        }

        #dyn-panel.open {
            opacity: 1;
            transform: scale(1) translateY(0);
            pointer-events: auto;
        }

        .dyn-panel-title { margin: 0 0 15px 0; text-align: center; font-size: 16px; font-weight: bold; }
        #dyn-toggles-container { max-height: 575px; min-height: 280px; overflow-y: auto; background: #101720cc; padding: 5px; border-radius: 12px; scrollbar-width: thin; border: 1px solid #2a475e; }
        #dyn-no-items { font-size: 13px; opacity: 0.7; text-align: center; margin: 10px 0; }

        .toggle-item { display: flex; justify-content: space-between; align-items: center; margin: 0 0 4px 0; border: 1px solid transparent; transition: background .2s, border .2s; }
        .toggle-item:hover { background: rgba(185, 255, 199, 0.03); border: 1px solid #2a475e; }
        .toggle-item:nth-child(odd) { background: rgba(255,255,255,0.03); border-radius: 12px; padding: 4px 6px; }
        .toggle-item:nth-child(even) { background: #192635; border-radius: 12px; padding: 4px 6px; }

        .toggle-item-controls { display: flex; align-items: center; flex-grow: 1; margin-left: 8px; cursor: pointer; }
        .toggle-label { font-size: 14px; flex-grow: 1; margin-right: 10px; word-break: break-word; user-select: none; cursor: pointer; }

        .toggle-switch { position: relative; display: inline-block; width: 40px; height: 20px; flex-shrink: 0; cursor: pointer; }
        .toggle-switch input { opacity: 0; width: 0; height: 0; }
        .toggle-slider { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #2a475e; transition: .3s; border-radius: 12px; }
        .toggle-slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px; background: white; transition: .3s; border-radius: 12px; }

        input:checked + .toggle-slider { background-color: #5c7e10; }
        input:checked + .toggle-slider:before { transform: translateX(20px); }

        .panel-buttons { display: flex; gap: 5px; margin-top: 15px; }
        .panel-btn { flex: 1; padding: 7px; border: none; border-radius: 12px; cursor: pointer; font-size: 12px; font-weight: bold; transition: background-color 0.2s ease; }

        .show-all-btn { background: #5c7e10; color: white; }
        .show-all-btn:hover { background: #749f14; }
        .hide-all-btn { background: #7e1010; color: white; }
        .hide-all-btn:hover { background: #b21616; }

        .dyn-panel-separator { border: 0; border-top: 1px solid #2a475e; margin: 12px 0; }
        .dyn-add-form { display: flex; flex-direction: column; gap: 10px; }
        .dyn-panel-author { font-size: 13px; font-weight: bold; margin: 5px 0 0 0; text-align: center; opacity: 0.6; }

        .panel-input { width: 100%; padding: 8px; background: #101822; border: 1px solid #2a475e; color: white; border-radius: 12px; box-sizing: border-box; font-size: 13px; transition: background-color 0.2s ease; }
        .panel-input:hover, .panel-input:focus { background: #151f2d; outline: none; }

        .add-btn { background: #5c7e10; color: white; padding: 8px; }
        .add-btn:hover { background: #749f14; }

        .delete-btn { background: #2a475e; color: white; border: none; border-radius: 12px; width: 22px; height: 22px; cursor: pointer; font-size: 12px; display: flex; align-items: center; justify-content: center; padding: 0; flex-shrink: 0; transition: background-color 0.2s ease; }
        .delete-btn:hover { background: #ca2121; }

        .clear-all-btn { background: #a01a1a; color: white; width: 100%; }
        .clear-all-btn:hover { background: #ca2121; }
    `;

    const styleEl = document.createElement('style');
    styleEl.textContent = css;
    shadow.appendChild(styleEl);

    // ---- Компоненты UI ----
    const root = document.createElement('div');
    shadow.appendChild(root);

    function buildUI() {
        root.innerHTML = '';
        const openBtn = document.createElement('button');
        openBtn.id = 'dyn-open-btn';
        openBtn.innerHTML = '☰';
        openBtn.title = 'Управление элементами';

        const panel = document.createElement('div');
        panel.id = 'dyn-panel';

        const toggles = document.createElement('div');
        toggles.id = 'dyn-toggles-container';
        panel.appendChild(toggles);

        const btns = document.createElement('div');
        btns.className = 'panel-buttons';
        const showAll = document.createElement('button');
        showAll.className = 'panel-btn show-all-btn';
        showAll.textContent = 'Показать всё 🔴';
        const hideAll = document.createElement('button');
        hideAll.className = 'panel-btn hide-all-btn';
        hideAll.textContent = 'Скрыть всё 🟢';
        btns.appendChild(showAll);
        btns.appendChild(hideAll);
        panel.appendChild(btns);

        panel.appendChild(Object.assign(document.createElement('hr'), { className: 'dyn-panel-separator' }));
        const form = document.createElement('div');
        form.className = 'dyn-add-form';
        const selectorInput = document.createElement('input');
        selectorInput.className = 'panel-input';
        selectorInput.placeholder = 'CSS селектор';
        form.appendChild(selectorInput);
        const nameInput = document.createElement('input');
        nameInput.className = 'panel-input';
        nameInput.placeholder = 'Название';
        form.appendChild(nameInput);
        const addBtn = document.createElement('button');
        addBtn.className = 'panel-btn add-btn';
        addBtn.textContent = 'Добавить';
        form.appendChild(addBtn);
        panel.appendChild(form);

        panel.appendChild(Object.assign(document.createElement('hr'), { className: 'dyn-panel-separator' }));
        const clearBtn = document.createElement('button');
        clearBtn.className = 'panel-btn clear-all-btn';
        clearBtn.textContent = 'Сбросить всё для сайта';
        panel.appendChild(clearBtn);

        panel.appendChild(Object.assign(document.createElement('hr'), { className: 'dyn-panel-separator' }));
        const sub = document.createElement('h4');
        sub.className = 'dyn-panel-author';
        sub.textContent = '❮ By Wizzergod 2026 ❯';
        panel.appendChild(sub);

        root.appendChild(openBtn);
        root.appendChild(panel);

        // ---- Логика событий открытия/закрытия ----
        openBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            const isOpen = panel.classList.toggle('open');
            if (isOpen) populateToggles();
        });

        document.addEventListener('click', (e) => {
            if (panel.classList.contains('open') && !e.composedPath().includes(host)) {
                panel.classList.remove('open');
            }
        });

        addBtn.addEventListener('click', () => {
            const res = addItem(selectorInput.value.trim(), nameInput.value.trim());
            if (res) {
                selectorInput.value = '';
                nameInput.value = '';
                populateToggles();
            }
        });

        showAll.addEventListener('click', () => { updateAll(true); populateToggles(); });
        hideAll.addEventListener('click', () => { updateAll(false); populateToggles(); });
        clearBtn.addEventListener('click', () => {
            if (confirm('Сбросить все настройки для этого сайта?')) {
                const cfg = getSiteConfig();
                cfg.forEach(i => applyVisibility(i.selector, true));
                saveSiteConfig([]);
                populateToggles();
            }
        });
    }

    // ---- UI: наполнение и элементы ----
    function populateToggles() {
        const cont = root.querySelector('#dyn-toggles-container');
        if (!cont) return;
        cont.innerHTML = '';
        const cfg = getSiteConfig();
        if (cfg.length === 0) {
            cont.innerHTML = '<div id="dyn-no-items">Нет элементов</div>';
            return;
        }
        cfg.forEach(item => cont.appendChild(makeToggle(item)));
    }

    function makeToggle(item) {
        const row = document.createElement('div');
        row.className = 'toggle-item';
        row.dataset.selector = item.selector;

        const del = document.createElement('button');
        del.className = 'delete-btn';
        del.textContent = '⛔';
        del.title = 'Удалить';
        del.addEventListener('click', (e) => {
            e.stopPropagation();
            if (confirm(`Удалить "${item.name}"?`)) removeItem(item.selector);
        });

        const box = document.createElement('label');
        box.className = 'toggle-item-controls';

        const lbl = document.createElement('span');
        lbl.className = 'toggle-label';
        lbl.textContent = item.name;
        lbl.title = item.selector;

        const sw = document.createElement('span');
        sw.className = 'toggle-switch';

        const inp = document.createElement('input');
        inp.type = 'checkbox';
        inp.checked = !!item.visible;

        const sld = document.createElement('span');
        sld.className = 'toggle-slider';

        inp.addEventListener('change', () => {
            applyVisibility(item.selector, inp.checked);
            saveItemVisibility(item.selector, inp.checked);
        });

        sw.appendChild(inp);
        sw.appendChild(sld);
        box.appendChild(lbl);
        box.appendChild(sw);
        row.appendChild(del);
        row.appendChild(box);

        return row;
    }

    // ---- CRUD для настроек ----
    function addItem(selector, name) {
        if (!selector || !name) { alert('Заполните оба поля'); return false; }

        // Валидация корректности CSS селектора
        try {
            document.createDocumentFragment().querySelector(selector);
        } catch(e) {
            alert('Недопустимый CSS селектор. Пожалуйста, проверьте синтаксис.');
            return false;
        }

        const cfg = getSiteConfig();
        if (cfg.find(i => i.selector === selector)) { alert('Этот селектор уже добавлен'); return false; }

        cfg.push({ selector: selector, name: name, visible: true });
        saveSiteConfig(cfg);
        applyVisibility(selector, true);
        return true;
    }

    function removeItem(selector) {
        const cfg = getSiteConfig().filter(i => i.selector !== selector);
        applyVisibility(selector, true);
        saveSiteConfig(cfg);
        populateToggles();
    }

    function saveItemVisibility(selector, isVisible) {
        const cfg = getSiteConfig();
        const it = cfg.find(i => i.selector === selector);
        if (it) { it.visible = isVisible; saveSiteConfig(cfg); }
    }

    function updateAll(isVisible) {
        const cfg = getSiteConfig();
        cfg.forEach(i => { i.visible = isVisible; applyVisibility(i.selector, isVisible); });
        saveSiteConfig(cfg);
    }

    // ---- Надежное применение видимости ----
    function applyVisibility(selector, visible) {
        try {
            document.querySelectorAll(selector).forEach(el => {
                el.style.setProperty('display', visible ? '' : 'none', 'important');
            });
        } catch(e) {
            console.error('[TogglePanel] Ошибка применения селектора:', selector, e);
        }
    }

    let queuedSelectors = new Set();
    let applyTimer = null;

    function scheduleApply(selector) {
        queuedSelectors.add(selector);
        debounceApply();
    }

    function debounceApply() {
        if (applyTimer) clearTimeout(applyTimer);
        applyTimer = setTimeout(() => {
            const arr = Array.from(queuedSelectors);
            queuedSelectors.clear();
            if (arr.length === 0) return;

            const runLogic = () => {
                const cfg = getSiteConfig();
                arr.forEach(s => {
                    const target = cfg.find(i => i.selector === s);
                    applyVisibility(s, target ? target.visible : true);
                });
            };

            if ('requestIdleCallback' in window) {
                requestIdleCallback(runLogic, { timeout: 1000 });
            } else {
                setTimeout(runLogic, 0);
            }
        }, APPLY_DEBOUNCE_MS);
    }

    // ---- Оптимизированный MutationObserver ----
    function startObserver() {
        const observer = new MutationObserver(mutations => {
            const cfg = getSiteConfig();
            if (cfg.length === 0) return;

            let needUpdate = false;
            // Быстрый выход из цикла мутаций, чтобы убрать лишнюю нагрузку O(N)
            for (const m of mutations) {
                if ((m.addedNodes && m.addedNodes.length > 0) || m.type === 'attributes') {
                    needUpdate = true;
                    break;
                }
            }

            if (needUpdate) {
                cfg.forEach(i => queuedSelectors.add(i.selector));
                debounceApply();
            }
        });

        observer.observe(document.documentElement || document.body, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['class', 'id', 'style']
        });

        // Фоллбек-ретраи для динамических SPA-приложений
        (function fallback(attempt) {
            if (attempt > MAX_FALLBACK_ATTEMPTS) return;
            const cfg = getSiteConfig();
            if (cfg.length > 0) {
                cfg.forEach(i => applyVisibility(i.selector, i.visible));
            }
            setTimeout(() => fallback(attempt + 1), FALLBACK_DELAY_MS);
        })(0);
    }

    function initApplyAll() {
        const cfg = getSiteConfig();
        if (cfg.length === 0) return;
        cfg.forEach(i => {
            applyVisibility(i.selector, i.visible);
            scheduleApply(i.selector);
        });
    }

    // ---- Инициализация ----
    buildUI();

    try {
        initApplyAll();
        if ('requestIdleCallback' in window) {
            requestIdleCallback(startObserver, { timeout: 2000 });
        } else {
            setTimeout(startObserver, 600);
        }
    } catch(e) {
        console.error('[TogglePanel] Init error', e);
    }

    // ---- Экспорт в консоль для отладки ----
    try {
        Object.defineProperty(window, '__DynamicTogglePanel', {
            value: { getSiteConfig, saveSiteConfig, applyVisibility },
            writable: false,
            configurable: true
        });
    } catch(e) {}
})();