Instrukcja użycia→ Ściągnij wtyczkę Tampermonkey do przeglądarki
→ W opcjach wtyczki włącz opcję „Allow User Scripts” (dzięki temu będzie można korzystać z własnych skryptów)
→ Wklej w Tampermonkey poniższy skrypt GTM Toolbox
→ Wejdź do Google Tag Manager i korzystaj z filtrowania i wyszukiwania w listach tagów, reguł i zmiennych
Skrypt GTM Toolbox
// ==UserScript==
// @name GTM Filter
// @namespace https://pawelpiekarski.pl/
// @version 7.0
// @description Filtrowanie po typie elementu, typie reguły uruchamiającej i wyszukiwanie po nazwie (z regex) w listach GTM
// @author Paweł Piekarski
// @match https://tagmanager.google.com/*
// @icon https://www.gstatic.com/analytics-suite/header/suite/v2/ic_tag_manager.svg
// @grant none
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const WID = 'gtm-tb';
const LS_COL = 'gtmTbCol';
const LS_TRIG = 'gtmTbTrigTypes'; // + ':' + klucz kontenera
const POLL_MS = 1200;
let collapsed = localStorage.getItem(LS_COL) === '1';
let currentSection = null;
let widget = null;
let lastScanKey = '';
let typeMap = {};
let trigTypeMap = {}; // { typReguły: liczbaTagów } — tylko sekcja tagów
let activeTypeFilters = new Set();
let activeTrigTypeFilters = new Set(); // wybrane TYPY reguł
let trigNameSearch = ''; // szukajka po NAZWIE reguły (filtruje tagi)
let trigNameRegex = null;
let trigNameError = '';
let nameSearch = '';
let nameSearchRegex = null;
let nameSearchError = '';
// mapa nazwa reguły → typ reguły (budowana przy wizycie na stronie Reguł,
// trzymana w localStorage per kontener)
let trigTypeStore = {};
let lastContainerKey = '';
// cache układu kolumn per tabela
const colCache = new WeakMap();
const C = {
bg: '#111827', hover: '#1e293b',
accent: '#3b82f6', accentDk: '#1e3a5f',
green: '#22c55e', red: '#ef4444', yellow: '#eab308',
text: '#e5e7eb', dim: '#6b7280', border: '#374151',
};
const COLORS = ['#3b82f6','#ef4444','#22c55e','#eab308','#a855f7','#f97316','#06b6d4','#ec4899','#14b8a6','#f43f5e','#8b5cf6','#64748b','#84cc16','#0ea5e9','#d946ef'];
const tcMap = {}; let ci = 0;
function tc(t) { if (!tcMap[t]) tcMap[t] = COLORS[ci++ % COLORS.length]; return tcMap[t]; }
function escHtml(s) { return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
/* ═══════════════════ REGEX HELPERS ═══════════════════ */
const REGEX_SIGNAL = /[|\\^$*+?.()\[\]{}]/;
function compileSearch(raw) {
nameSearch = raw;
nameSearchError = '';
nameSearchRegex = null;
if (!raw) return;
if (REGEX_SIGNAL.test(raw)) {
try {
nameSearchRegex = new RegExp(raw, 'i');
} catch (e) {
nameSearchError = e.message.replace(/^Invalid regular expression: /, '');
nameSearchRegex = null;
}
}
}
function nameMatchesSearch(name) {
if (!nameSearch) return true;
if (nameSearchRegex) return nameSearchRegex.test(name);
return name.toLowerCase().includes(nameSearch.toLowerCase());
}
function compileTrigSearch(raw) {
trigNameSearch = raw;
trigNameError = '';
trigNameRegex = null;
if (!raw) return;
if (REGEX_SIGNAL.test(raw)) {
try {
trigNameRegex = new RegExp(raw, 'i');
} catch (e) {
trigNameError = e.message.replace(/^Invalid regular expression: /, '');
trigNameRegex = null;
}
}
}
function trigNameMatches(name) {
if (!trigNameSearch) return true;
if (trigNameRegex) return trigNameRegex.test(name);
return name.toLowerCase().includes(trigNameSearch.toLowerCase());
}
/* ═══════════════════ MAPA TYPÓW REGUŁ ═══════════════════ */
function containerKey() {
const m = location.href.match(/accounts\/(\d+)\/containers\/(\d+)/);
return m ? m[1] + ':' + m[2] : 'default';
}
function loadTrigTypeStore() {
try {
trigTypeStore = JSON.parse(localStorage.getItem(LS_TRIG + ':' + containerKey()) || '{}');
} catch (e) { trigTypeStore = {}; }
lastContainerKey = containerKey();
}
function saveTrigTypeStore() {
try {
localStorage.setItem(LS_TRIG + ':' + containerKey(), JSON.stringify(trigTypeStore));
} catch (e) { /* quota — trudno */ }
}
// Reguły wbudowane nie pojawiają się na liście Reguł — mapujemy ręcznie
const BUILTIN_TRIG_TYPES = {
'All Pages': 'Page View',
'Wszystkie strony': 'Page View',
'Initialization - All Pages': 'Initialization',
'Inicjowanie – wszystkie strony': 'Initialization',
'Inicjowanie - wszystkie strony': 'Initialization',
'Consent Initialization - All Pages': 'Consent Initialization',
'Inicjowanie zgody – wszystkie strony': 'Consent Initialization',
'Inicjowanie zgody - wszystkie strony': 'Consent Initialization',
};
const UNKNOWN_TYPE = '❓ Nieznany typ';
// Typ reguły odczytywany WPROST z ikony chipa w tabeli tagów:
// <i class="gtm-trigger-custom-event-icon-small ..."> → 'custom-event'
const ICON_TYPE_MAP = {
'pageview': 'Page View',
'dom-ready': 'DOM Ready',
'window-loaded': 'Window Loaded',
'custom-event': 'Custom Event',
'click': 'Click',
'link-click': 'Just Links',
'element-visibility': 'Element Visibility',
'form-submit': 'Form Submission',
'form-submission': 'Form Submission',
'history-change': 'History Change',
'js-error': 'JavaScript Error',
'scroll-depth': 'Scroll Depth',
'timer': 'Timer',
'youtube-video': 'YouTube Video',
'trigger-group': 'Trigger Group',
'init': 'Initialization',
'consent-init': 'Consent Initialization',
};
function iconTypeFromChip(chipEl) {
const icon = chipEl.querySelector('i[class*="gtm-trigger-"]');
if (!icon) return null;
const m = icon.className.match(/gtm-trigger-([a-z-]+?)-icon/);
if (!m) return null;
const slug = m[1];
if (ICON_TYPE_MAP[slug]) return ICON_TYPE_MAP[slug];
// nieznany slug → ładnie sformatuj (np. 'ampl-timer' → 'Ampl Timer')
return slug.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
}
function typeForTrigger(name) {
return trigTypeStore[name] || BUILTIN_TRIG_TYPES[name] || UNKNOWN_TYPE;
}
// Na stronie Reguł: zbierz pary nazwa→typ i zapisz
function harvestTriggerTypes() {
if (currentSection !== 'triggers') return;
let changed = false;
getDataRows().forEach(row => {
const n = nameFromRow(row), t = typeFromRow(row);
if (n && t && trigTypeStore[n] !== t) { trigTypeStore[n] = t; changed = true; }
});
if (changed) saveTrigTypeStore();
}
/* ═══════════════════ GTM DOM (odporne wykrywanie) ═══════════════════ */
function detectSection() {
const u = location.href;
if (/\/tags\b/.test(u)) return 'tags';
if (/\/triggers\b/.test(u)) return 'triggers';
if (/\/variables\b/.test(u)) return 'variables';
return null;
}
function sectionLabel(s) { return { tags: 'Tagi', triggers: 'Reguły', variables: 'Zmienne' }[s] || '—'; }
// Znajdź WSZYSTKIE tabele z danymi (strona zmiennych ma dwie:
// zmienne wbudowane + zdefiniowane przez użytkownika)
function findDataTables() {
const legacy = [...document.querySelectorAll('table.gtm-table--hide-empty')]
.filter(t => t.offsetParent !== null && t.querySelector('tbody tr'));
if (legacy.length) return legacy;
const out = [];
document.querySelectorAll('table').forEach(tab => {
if (tab.offsetParent === null) return;
if (tab.closest('#' + WID)) return;
const rows = tab.querySelectorAll('tbody tr');
if (!rows.length) return;
const hasLink = [...rows].some(r => r.querySelector('a'));
const headTxt = [...tab.querySelectorAll('thead th, thead td')]
.map(th => th.textContent.trim().toLowerCase());
const hasNameHeader = headTxt.some(t => /^(nazwa|name)\b/.test(t));
if (hasLink || hasNameHeader) out.push(tab);
});
return out;
}
// Ustal indeksy kolumn: Nazwa, Typ, Reguły uruchamiające (cache per tabela)
function resolveColumns(table) {
const cached = colCache.get(table);
if (cached) return cached;
let nameIdx = -1, typeIdx = -1, trigIdx = -1;
const headCells = table.querySelectorAll('thead th, thead td');
[...headCells].forEach((th, i) => {
const txt = th.textContent.trim().toLowerCase();
if (nameIdx < 0 && /^(nazwa|name)\b/.test(txt)) nameIdx = i;
if (typeIdx < 0 && /^(typ|type)\b/.test(txt)) typeIdx = i;
if (trigIdx < 0 && /(regu|trigger)/.test(txt)) trigIdx = i;
});
if (nameIdx < 0) {
const firstRow = table.querySelector('tbody tr');
if (firstRow) {
const cells = firstRow.querySelectorAll('td');
[...cells].forEach((c, i) => {
if (nameIdx < 0 && c.querySelector('a')) nameIdx = i;
});
}
}
if (nameIdx < 0) nameIdx = 1;
if (typeIdx < 0) typeIdx = nameIdx + 1;
if (trigIdx < 0) trigIdx = typeIdx + 1; // w tagach: Nazwa | Typ | Reguły
const cols = { nameIdx, typeIdx, trigIdx };
colCache.set(table, cols);
return cols;
}
function getDataRows() {
const tables = findDataTables();
const out = [];
tables.forEach(table => {
const { nameIdx } = resolveColumns(table);
[...table.querySelectorAll('tbody tr')].forEach(r => {
const cells = r.querySelectorAll('td');
if (cells.length > nameIdx && cells[nameIdx].textContent.trim().length > 0) out.push(r);
});
});
return out;
}
function colsForRow(row) {
const table = row.closest('table');
return table ? resolveColumns(table) : { nameIdx: 1, typeIdx: 2, trigIdx: 3 };
}
function nameFromRow(row) {
const { nameIdx } = colsForRow(row);
const cells = row.querySelectorAll('td');
if (cells.length > nameIdx) {
const a = cells[nameIdx].querySelector('a');
return (a ? a.textContent : cells[nameIdx].textContent).trim();
}
return '';
}
function typeFromRow(row) {
const { typeIdx } = colsForRow(row);
const cells = row.querySelectorAll('td');
if (cells.length > typeIdx) return cells[typeIdx].textContent.trim();
return null;
}
// Reguły uruchamiające z wiersza tagu: [{ name, type }]
// Typ czytamy z ikony chipa (gtm-trigger-*-icon), z fallbackiem na mapę
// nazwa→typ z localStorage i mapę reguł wbudowanych.
function triggersFromRow(row) {
const { trigIdx } = colsForRow(row);
const cells = row.querySelectorAll('td');
if (cells.length <= trigIdx) return [];
const cell = cells[trigIdx];
// 1) chipy reguł (nowy i stary UI)
const chips = [...cell.querySelectorAll('gtm-trigger-chip a, a.small-trigger-chip')];
if (chips.length) {
const seen = new Set(), out = [];
chips.forEach(a => {
const name = a.textContent.trim();
if (!name || seen.has(name)) return;
seen.add(name);
const type = iconTypeFromChip(a) || typeForTrigger(name);
out.push({ name, type });
});
return out;
}
// 2) fallback: dowolne linki / dzieci / cały tekst — typ z mapy nazw
let items = [...cell.querySelectorAll('a')].map(a => a.textContent.trim());
if (!items.length) items = [...cell.children].map(el => el.textContent.trim());
if (!items.length) {
const whole = cell.textContent.trim();
items = whole ? [whole] : [];
}
return [...new Set(items.filter(t => t && t.length > 1))]
.map(name => ({ name, type: typeForTrigger(name) }));
}
// TYPY reguł uruchamiających dla wiersza tagu
function triggerTypesFromRow(row) {
return [...new Set(triggersFromRow(row).map(t => t.type))];
}
function scanTypes() {
const map = {};
getDataRows().forEach(row => { const t = typeFromRow(row); if (t) map[t] = (map[t] || 0) + 1; });
return map;
}
function scanTriggerTypes() {
const map = {};
if (currentSection !== 'tags') return map;
getDataRows().forEach(row => {
triggerTypesFromRow(row).forEach(t => { map[t] = (map[t] || 0) + 1; });
});
return map;
}
/* ═══════════════════ PAGINACJA: wymuś "ALL" ═══════════════════ */
// GTM domyślnie pokazuje 50 wierszy na stronę — filtr działałby tylko na
// widocznej stronie. Przy aktywnym filtrze przełączamy "Show rows" na ALL.
function ensureShowAll() {
document.querySelectorAll('gtm-pagination select, .gtm-pagination select').forEach(sel => {
if (sel.offsetParent === null) return;
const allOpt = [...sel.options].find(o =>
/all/i.test(o.value) || /all/i.test(o.label || '') || /all/i.test(o.textContent || ''));
if (!allOpt || sel.value === allOpt.value) return;
sel.value = allOpt.value;
sel.dispatchEvent(new Event('change', { bubbles: true }));
});
}
/* ═══════════════════ FILTER ═══════════════════ */
let filterStyleEl = null;
function getFilterStyleEl() {
if (!filterStyleEl || !filterStyleEl.isConnected) {
filterStyleEl = document.createElement('style');
filterStyleEl.id = 'gtm-tb-filter-css';
document.head.appendChild(filterStyleEl);
}
return filterStyleEl;
}
function hasActiveFilters() {
return activeTypeFilters.size > 0 || activeTrigTypeFilters.size > 0 || nameSearch || trigNameSearch;
}
function applyAllFilters() {
if (hasActiveFilters()) ensureShowAll();
const rows = getDataRows();
if (!hasActiveFilters()) {
rows.forEach(r => r.removeAttribute('data-tb-hidden'));
getFilterStyleEl().textContent = '';
return;
}
rows.forEach(row => {
let show = true;
if (activeTypeFilters.size > 0) {
const t = typeFromRow(row);
if (!t || !activeTypeFilters.has(t)) show = false;
}
if (show && activeTrigTypeFilters.size > 0) {
const types = triggerTypesFromRow(row);
if (!types.some(t => activeTrigTypeFilters.has(t))) show = false;
}
if (show && trigNameSearch && !trigNameError) {
const trigs = triggersFromRow(row);
if (!trigs.some(t => trigNameMatches(t.name))) show = false;
}
if (show && nameSearch) {
if (nameSearchError) { /* nie filtruj zepsutym regexem */ }
else if (!nameMatchesSearch(nameFromRow(row))) show = false;
}
if (show) row.removeAttribute('data-tb-hidden');
else row.setAttribute('data-tb-hidden', '1');
});
getFilterStyleEl().textContent = 'tr[data-tb-hidden="1"] { display: none !important; }';
}
function clearAllFilters() {
activeTypeFilters.clear();
activeTrigTypeFilters.clear();
trigNameSearch = '';
trigNameRegex = null;
trigNameError = '';
nameSearch = '';
nameSearchRegex = null;
nameSearchError = '';
getDataRows().forEach(r => r.removeAttribute('data-tb-hidden'));
getFilterStyleEl().textContent = '';
}
/* ═══════════════════ WIDGET ═══════════════════ */
function buildWidget() {
if (widget) widget.remove();
widget = document.createElement('div');
widget.id = WID;
widget.innerHTML = `
<style>
#${WID}{position:fixed;bottom:16px;right:16px;z-index:2147483647;font-family:'Google Sans',system-ui,sans-serif;width:310px;border-radius:14px;overflow:hidden;box-shadow:0 10px 36px rgba(0,0,0,.55);border:1px solid ${C.border};background:${C.bg};user-select:none}
#${WID} *{box-sizing:border-box}
.tb-hdr{background:linear-gradient(135deg,${C.accentDk},${C.bg});padding:10px 14px;display:flex;align-items:center;gap:8px;cursor:move;border-bottom:1px solid ${C.border}}
.tb-logo{width:22px;height:22px;flex-shrink:0}
.tb-logo svg{display:block}
.tb-title{flex:1;font-size:13px;font-weight:600;color:#fff;letter-spacing:.3px}
.tb-badge{font-size:10px;background:${C.border};color:${C.dim};padding:2px 8px;border-radius:10px}
.tb-hdr-btn{background:none;border:none;color:#fff;font-size:18px;cursor:pointer;padding:0 4px;line-height:1;opacity:.6;transition:opacity .15s}
.tb-hdr-btn:hover{opacity:1}
.tb-wrap{overflow:hidden;transition:max-height .25s ease}
.tb-wrap.open{max-height:85vh}.tb-wrap.closed{max-height:0!important}
.tb-panel{padding:10px 14px;max-height:65vh;overflow-y:auto}
.tb-panel::-webkit-scrollbar{width:5px}.tb-panel::-webkit-scrollbar-thumb{background:${C.border};border-radius:3px}
.tb-label{font-size:10px;color:${C.dim};font-weight:700;text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px}
.tb-input-wrap{position:relative;margin-bottom:8px}
.tb-input{width:100%;background:${C.border};border:1px solid #4b5563;color:${C.text};padding:7px 10px;border-radius:8px;font-size:12px;outline:none;font-family:inherit}
.tb-input:focus{border-color:${C.accent}}
.tb-input::placeholder{color:#6b7280}
.tb-input.tb-input--regex{font-family:'Consolas','Monaco','Courier New',monospace;border-color:#7c3aed}
.tb-input.tb-input--regex:focus{border-color:#a78bfa}
.tb-input.tb-input--error{border-color:${C.red}!important}
.tb-search-mode{position:absolute;right:6px;top:50%;transform:translateY(-50%);font-size:9px;padding:2px 6px;border-radius:4px;font-weight:700;letter-spacing:.3px;border:none;background:none;pointer-events:none}
.tb-search-mode--text{background:${C.border};color:${C.dim}}
.tb-search-mode--clear{background:${C.border};color:${C.text};pointer-events:auto;cursor:pointer}
.tb-search-mode--clear:hover{background:#4b5563}
.tb-search-mode--regex{background:#7c3aed;color:#fff;pointer-events:auto;cursor:pointer}
.tb-search-mode--regex:hover{background:#6d28d9}
.tb-search-mode--error{background:${C.red};color:#fff;pointer-events:auto;cursor:pointer}
.tb-search-mode--error:hover{background:#dc2626}
.tb-input-sm{width:100%;background:${C.border};border:1px solid #4b5563;color:${C.text};padding:5px 8px;border-radius:6px;font-size:11px;outline:none;font-family:inherit;margin-bottom:6px}
.tb-input-sm:focus{border-color:${C.accent}}
.tb-input-sm::placeholder{color:#6b7280}
.tb-search-error{font-size:10px;color:${C.red};margin:-4px 0 6px 2px;line-height:1.3}
.tb-search-hint{font-size:10px;color:${C.dim};margin:-4px 0 6px 2px;line-height:1.3}
.tb-frow{display:flex;align-items:center;gap:9px;padding:6px 4px;cursor:pointer;transition:background .08s;border-radius:6px}
.tb-frow:hover{background:${C.hover}}
.tb-chk{width:16px;height:16px;border-radius:4px;border:2px solid ${C.border};display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all .12s;font-size:10px;color:transparent}
.tb-frow.on .tb-chk{background:${C.accent};border-color:${C.accent};color:#fff}
.tb-dot{width:7px;height:7px;border-radius:2px;flex-shrink:0}
.tb-fname{flex:1;font-size:12px;color:${C.text};white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tb-fcnt{font-size:10px;color:${C.dim};background:${C.border};padding:1px 7px;border-radius:7px;min-width:20px;text-align:center}
.tb-note{font-size:10px;color:${C.yellow};margin:4px 0 0 2px;line-height:1.4}
.tb-foot{border-top:1px solid ${C.border};padding:7px 14px;display:flex;justify-content:space-between;align-items:center}
.tb-foot-txt{font-size:10px;color:${C.dim}}
.tb-foot-btn{font-size:10px;color:${C.accent};background:none;border:none;cursor:pointer;font-weight:600;padding:2px 4px;border-radius:4px}
.tb-foot-btn:hover{background:rgba(59,130,246,.12)}
</style>
<div class="tb-hdr" id="tb-hdr">
<div class="tb-logo"><svg width="22" height="22" viewBox="0 0 192 192" xmlns="http://www.w3.org/2000/svg"><rect width="192" height="192" rx="24" fill="#4285F4"/><path d="M96 36L140 96l-44 60-44-60L96 36z" fill="#fff" opacity=".9"/><circle cx="96" cy="96" r="20" fill="#fff"/></svg></div>
<span class="tb-title">GTM Filter</span>
<span class="tb-badge" id="tb-badge">—</span>
<button class="tb-hdr-btn" id="tb-col">▾</button>
</div>
<div class="tb-wrap ${collapsed ? 'closed' : 'open'}" id="tb-wrap">
<div class="tb-panel">
<div class="tb-label">Szukaj po nazwie</div>
<div style="font-size:10px;color:#9ca3af;margin:-2px 0 5px 2px">obsługuje regex, np. GA4|FB|cHTML</div>
<div class="tb-input-wrap">
<input class="tb-input" id="tb-name-search" style="padding-right:38px">
<span class="tb-search-mode tb-search-mode--text" id="tb-search-mode">TEXT</span>
</div>
<div id="tb-search-hint" class="tb-search-hint" style="display:none"></div>
<div class="tb-label" style="margin-top:6px">Filtruj po typie</div>
<div id="tb-type-list"></div>
<div id="tb-trig-section" style="display:none">
<div class="tb-label" style="margin-top:10px">Szukaj po nazwie reguły uruchamiającej</div>
<div style="font-size:10px;color:#9ca3af;margin:-2px 0 5px 2px">obsługuje regex, np. CE - |Click</div>
<div class="tb-input-wrap">
<input class="tb-input" id="tb-trig-search" style="padding-right:38px">
<span class="tb-search-mode tb-search-mode--text" id="tb-trig-mode">TEXT</span>
</div>
<div id="tb-trig-hint" class="tb-search-hint" style="display:none"></div>
<div class="tb-label" style="margin-top:6px">Filtruj po typie reguły uruchamiającej</div>
<div id="tb-trig-list"></div>
<div class="tb-note" id="tb-trig-note" style="display:none"></div>
</div>
</div>
<div class="tb-foot">
<span class="tb-foot-txt" id="tb-foot-txt"></span>
<button class="tb-foot-btn" id="tb-clear">Wyczyść filtr</button>
</div>
</div>
`;
document.body.appendChild(widget);
const colBtn = document.getElementById('tb-col');
colBtn.textContent = collapsed ? '▸' : '▾';
colBtn.addEventListener('click', () => {
collapsed = !collapsed; localStorage.setItem(LS_COL, collapsed ? '1' : '0');
updateExpandDirection();
document.getElementById('tb-wrap').classList.toggle('open', !collapsed);
document.getElementById('tb-wrap').classList.toggle('closed', collapsed);
colBtn.textContent = collapsed ? '▸' : '▾';
});
document.getElementById('tb-name-search').addEventListener('input', (e) => {
compileSearch(e.target.value);
updateSearchIndicator();
applyAllFilters();
updateFooter();
});
document.getElementById('tb-search-mode').addEventListener('click', () => {
if (!nameSearch) return;
const input = document.getElementById('tb-name-search');
input.value = '';
compileSearch('');
updateSearchIndicator();
applyAllFilters();
updateFooter();
input.focus();
});
document.getElementById('tb-trig-search').addEventListener('input', (e) => {
compileTrigSearch(e.target.value);
updateTrigSearchIndicator();
applyAllFilters();
updateFooter();
});
document.getElementById('tb-trig-mode').addEventListener('click', () => {
if (!trigNameSearch) return;
const input = document.getElementById('tb-trig-search');
input.value = '';
compileTrigSearch('');
updateTrigSearchIndicator();
applyAllFilters();
updateFooter();
input.focus();
});
document.getElementById('tb-clear').addEventListener('click', () => {
clearAllFilters();
document.getElementById('tb-name-search').value = '';
document.getElementById('tb-trig-search').value = '';
updateSearchIndicator();
updateTrigSearchIndicator();
renderFilterTab();
renderTrigTypeList();
updateFooter();
});
makeDraggable(widget, document.getElementById('tb-hdr'));
}
function updateSearchIndicator() {
const input = document.getElementById('tb-name-search');
const badge = document.getElementById('tb-search-mode');
const hint = document.getElementById('tb-search-hint');
input.classList.remove('tb-input--regex', 'tb-input--error');
badge.className = 'tb-search-mode';
hint.style.display = 'none';
if (!nameSearch) {
badge.className = 'tb-search-mode tb-search-mode--text';
badge.textContent = 'TEXT';
return;
}
if (nameSearchError) {
input.classList.add('tb-input--regex', 'tb-input--error');
badge.className = 'tb-search-mode tb-search-mode--error';
badge.textContent = '✕';
hint.style.display = 'block';
hint.className = 'tb-search-error';
hint.textContent = nameSearchError;
} else if (nameSearchRegex) {
input.classList.add('tb-input--regex');
badge.className = 'tb-search-mode tb-search-mode--regex';
badge.textContent = '✕';
hint.style.display = 'block';
hint.className = 'tb-search-hint';
hint.textContent = 'Tryb regex · użyj | jako OR, np. GA4|FB|cHTML';
} else {
badge.className = 'tb-search-mode tb-search-mode--clear';
badge.textContent = '✕';
}
}
function updateTrigSearchIndicator() {
const input = document.getElementById('tb-trig-search');
const badge = document.getElementById('tb-trig-mode');
const hint = document.getElementById('tb-trig-hint');
if (!input || !badge) return;
input.classList.remove('tb-input--regex', 'tb-input--error');
badge.className = 'tb-search-mode';
hint.style.display = 'none';
if (!trigNameSearch) {
badge.className = 'tb-search-mode tb-search-mode--text';
badge.textContent = 'TEXT';
return;
}
if (trigNameError) {
input.classList.add('tb-input--regex', 'tb-input--error');
badge.className = 'tb-search-mode tb-search-mode--error';
badge.textContent = '✕';
hint.style.display = 'block';
hint.className = 'tb-search-error';
hint.textContent = trigNameError;
} else if (trigNameRegex) {
input.classList.add('tb-input--regex');
badge.className = 'tb-search-mode tb-search-mode--regex';
badge.textContent = '✕';
hint.style.display = 'block';
hint.className = 'tb-search-hint';
hint.textContent = 'Tryb regex · użyj | jako OR, np. CE - |Click';
} else {
badge.className = 'tb-search-mode tb-search-mode--clear';
badge.textContent = '✕';
}
}
function renderFilterTab() {
const list = document.getElementById('tb-type-list'); list.innerHTML = '';
Object.entries(typeMap).sort((a, b) => b[1] - a[1]).forEach(([name, count]) => {
const row = document.createElement('div');
row.className = 'tb-frow' + (activeTypeFilters.has(name) ? ' on' : '');
row.innerHTML = `<div class="tb-chk">✓</div><div class="tb-dot" style="background:${tc(name)}"></div><span class="tb-fname" title="${escHtml(name)}">${escHtml(name)}</span><span class="tb-fcnt">${count}</span>`;
row.addEventListener('click', () => {
if (activeTypeFilters.has(name)) { activeTypeFilters.delete(name); row.classList.remove('on'); }
else { activeTypeFilters.add(name); row.classList.add('on'); }
applyAllFilters(); updateFooter();
});
list.appendChild(row);
});
updateFooter();
}
function renderTrigTypeList() {
const section = document.getElementById('tb-trig-section');
const list = document.getElementById('tb-trig-list');
const note = document.getElementById('tb-trig-note');
if (!section || !list) return;
if (currentSection !== 'tags' || Object.keys(trigTypeMap).length === 0) {
section.style.display = 'none';
return;
}
section.style.display = '';
list.innerHTML = '';
Object.entries(trigTypeMap)
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
.forEach(([name, count]) => {
const row = document.createElement('div');
row.className = 'tb-frow' + (activeTrigTypeFilters.has(name) ? ' on' : '');
row.innerHTML = `<div class="tb-chk">✓</div><div class="tb-dot" style="background:${tc('trig:' + name)}"></div><span class="tb-fname" title="${escHtml(name)}">${escHtml(name)}</span><span class="tb-fcnt">${count}</span>`;
row.addEventListener('click', () => {
if (activeTrigTypeFilters.has(name)) { activeTrigTypeFilters.delete(name); row.classList.remove('on'); }
else { activeTrigTypeFilters.add(name); row.classList.add('on'); }
applyAllFilters(); updateFooter();
});
list.appendChild(row);
});
// podpowiedź, gdy część reguł nie ma jeszcze znanego typu
if (trigTypeMap[UNKNOWN_TYPE]) {
note.style.display = '';
note.textContent = 'Część reguł ma nieznany typ — wejdź raz na kartę Reguły, żeby je zeskanować.';
} else {
note.style.display = 'none';
}
}
function updateFooter() {
const rows = getDataRows(), total = rows.length, visible = rows.filter(r => !r.hasAttribute('data-tb-hidden')).length;
const ft = document.getElementById('tb-foot-txt');
ft.textContent = !hasActiveFilters()
? `${Object.keys(typeMap).length} typów · ${total} elementów`
: `${visible} z ${total} widocznych`;
}
/* ═══════════════════ EXPAND DIRECTION ═══════════════════ */
function updateExpandDirection() {
if (!widget) return;
const rect = widget.getBoundingClientRect();
const hdrH = document.getElementById('tb-hdr').offsetHeight;
const hdrMid = rect.top + hdrH / 2;
const viewMid = window.innerHeight / 2;
if (hdrMid > viewMid) {
widget.style.bottom = (window.innerHeight - rect.bottom) + 'px';
widget.style.top = 'auto';
} else {
widget.style.top = rect.top + 'px';
widget.style.bottom = 'auto';
}
}
/* ═══════════════════ DRAG ═══════════════════ */
function makeDraggable(el, handle) {
let sx, sy, sl, st;
handle.addEventListener('mousedown', e => {
if (e.target.closest('button')) return; e.preventDefault();
sx = e.clientX; sy = e.clientY; const r = el.getBoundingClientRect(); sl = r.left; st = r.top;
const mv = ev => { el.style.left = (sl+ev.clientX-sx)+'px'; el.style.top = (st+ev.clientY-sy)+'px'; el.style.right = 'auto'; el.style.bottom = 'auto'; };
const up = () => { document.removeEventListener('mousemove', mv); document.removeEventListener('mouseup', up); updateExpandDirection(); };
document.addEventListener('mousemove', mv); document.addEventListener('mouseup', up);
});
}
/* ═══════════════════ POLL ═══════════════════ */
function poll() {
// zmiana kontenera → przeładuj mapę typów reguł
if (containerKey() !== lastContainerKey) loadTrigTypeStore();
const section = detectSection();
if (section !== currentSection) {
currentSection = section;
clearAllFilters();
const si = document.getElementById('tb-name-search');
if (si) si.value = '';
const ts = document.getElementById('tb-trig-search');
if (ts) ts.value = '';
updateSearchIndicator();
updateTrigSearchIndicator();
}
document.getElementById('tb-badge').textContent = sectionLabel(currentSection);
if (!currentSection) {
typeMap = {}; trigTypeMap = {}; lastScanKey = '';
renderFilterTab(); renderTrigTypeList();
return;
}
// na stronie Reguł: buduj mapę nazwa→typ do wykorzystania na Tagach
harvestTriggerTypes();
const newTypes = scanTypes();
const newTrigTypes = scanTriggerTypes();
const key = JSON.stringify([newTypes, newTrigTypes]);
if (key !== lastScanKey) {
lastScanKey = key;
typeMap = newTypes;
trigTypeMap = newTrigTypes;
renderFilterTab();
renderTrigTypeList();
}
if (hasActiveFilters()) applyAllFilters();
updateFooter();
}
/* ═══════════════════ TABLE OBSERVER ═══════════════════ */
let tableObserver = null;
const observedTables = new WeakSet();
function watchTable() {
if (!tableObserver) {
tableObserver = new MutationObserver(() => {
if (hasActiveFilters()) applyAllFilters();
});
}
findDataTables().forEach(table => {
if (observedTables.has(table)) return;
observedTables.add(table);
tableObserver.observe(table, { childList: true, subtree: true });
});
}
/* ═══════════════════ DEBUG ═══════════════════ */
// W konsoli: gtmTbDebug() — pokaże co skrypt widzi
window.gtmTbDebug = function () {
const tables = findDataTables();
console.log('[GTM Filter] sekcja:', currentSection, '· kontener:', containerKey());
console.log('[GTM Filter] tabel:', tables.length, tables);
tables.forEach((table, ti) => {
const cols = resolveColumns(table);
console.log(`[GTM Filter] tabela #${ti} — name:`, cols.nameIdx, 'type:', cols.typeIdx, 'trig:', cols.trigIdx);
});
const rows = getDataRows();
console.log('[GTM Filter] wierszy łącznie:', rows.length);
rows.slice(0, 8).forEach(r => console.log(' →', nameFromRow(r), '|', typeFromRow(r), '|', triggersFromRow(r).map(t => `${t.name} [${t.type}]`).join(', ')));
console.log('[GTM Filter] typeMap:', typeMap);
console.log('[GTM Filter] trigTypeMap:', trigTypeMap);
console.log('[GTM Filter] trigTypeStore (nazwa→typ):', trigTypeStore);
};
/* ═══════════════════ INIT ═══════════════════ */
function init() {
loadTrigTypeStore();
buildWidget(); updateExpandDirection(); poll(); watchTable();
setInterval(() => { poll(); watchTable(); }, POLL_MS);
let lastUrl = location.href;
new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
setTimeout(() => { poll(); watchTable(); }, 600);
}
}).observe(document.body, { childList: true, subtree: true });
}
if (document.readyState === 'complete') setTimeout(init, 1500);
else window.addEventListener('load', () => setTimeout(init, 1500));
})();