Skrypt GTM Filter – filtrowanie i wyszukiwanie w listach GTM

Strona główna » Skrypty » Skrypt GTM Filter – filtrowanie i wyszukiwanie w listach GTM
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }

    /* ═══════════════════ 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));
})();

Dodaj komentarz

Napisz do mnie

Paweł Piekarski
Paweł Piekarski PhD, Analityk eCommerce Marketing oparty na danych

Dane kontaktowe

+48 725 473 745

poczta@pawelpiekarski.pl

linkedin.com/in/pawelpiekarskipl

    Wysyłając wiadomość, zgadzasz się na kontakt i przetwarzanie danych zgodnie z polityką prywatności.