Greasy Fork is available in English.
Объединённая изолированная панель управления элементами (Shadow DOM, надёжная синхронизация, минимальная нагрузка)
// ==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) {}
})();