// Mes issues — filtrage instantané côté client (0.33.0). // Aucune requête réseau : les lignes portent leurs facettes en // attributs data-*, le moteur ne fait que comparer des chaînes // normalisées. Les filtres structurels (projet, colonnes, // étiquette, tri) persistent en localStorage ; la recherche // texte, elle, est éphémère. (() => { const search = document.getElementById('my-search'); if (!search) return; // page sans résultats : rien à filtrer const repoSel = document.getElementById('my-repo'); const labelSel = document.getElementById('my-label'); const sortSel = document.getElementById('my-sort'); const chipsBox = document.getElementById('my-col-chips'); const countEl = document.getElementById('my-count'); const resetBtn = document.getElementById('my-reset'); const emptyBox = document.getElementById('my-empty'); const emptyReset = document.getElementById('my-empty-reset'); const STORE_KEY = 'kanbante-my-filters'; const norm = (s) => (s || '') .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, ''); const state = { q: '', repo: '', label: '', cols: new Set(), sort: 'recent', }; // ── Persistance des filtres structurels ──────────────── try { const saved = JSON.parse(localStorage.getItem(STORE_KEY) || '{}'); if (saved.repo) state.repo = saved.repo; if (saved.label) state.label = saved.label; if (Array.isArray(saved.cols)) state.cols = new Set(saved.cols); if (saved.sort) state.sort = saved.sort; } catch { /* stockage indisponible : état par défaut */ } function persist() { try { localStorage.setItem(STORE_KEY, JSON.stringify({ repo: state.repo, label: state.label, cols: [...state.cols], sort: state.sort, })); } catch { /* sans gravité */ } } // Refléter l'état restauré dans les contrôles. if (state.repo) repoSel.value = state.repo; if (state.label) labelSel.value = state.label; sortSel.value = state.sort; // Un filtre restauré peut ne plus exister (projet disparu) : // dans ce cas le select retombe à « tous » tout seul. if (repoSel.value !== state.repo) state.repo = repoSel.value; if (labelSel.value !== state.label) state.label = labelSel.value; const groups = [...document.querySelectorAll('.focus-group')]; const allRows = [...document.querySelectorAll('.issue-row')]; allRows.forEach((row) => { row._search = norm(row.dataset.search); row._labels = norm(row.dataset.labels).split(' ').filter(Boolean); row._title = norm(row.dataset.title); }); function syncChips() { chipsBox.querySelectorAll('.filter-chip').forEach((chip) => { const col = chip.dataset.col; const on = col === '' ? state.cols.size === 0 : state.cols.has(col); chip.classList.toggle('on', on); if (col !== '') chip.setAttribute('aria-pressed', on ? 'true' : 'false'); }); } function sortRows() { const mode = state.sort; if (mode === 'repo') { const main = document.querySelector('.focus-main'); [...groups].sort((a, b) => a.dataset.repo.localeCompare(b.dataset.repo) ).forEach((g) => main.appendChild(g)); } groups.forEach((group) => { const box = group.querySelector('.issue-results'); const rows = [...box.querySelectorAll('.issue-row')]; const by = { recent: (a, b) => (b.dataset.updated || '').localeCompare(a.dataset.updated || ''), oldest: (a, b) => (a.dataset.updated || '').localeCompare(b.dataset.updated || ''), title: (a, b) => a._title.localeCompare(b._title), repo: (a, b) => a._title.localeCompare(b._title), }[mode]; rows.sort(by).forEach((r) => box.appendChild(r)); }); } function apply() { const q = norm(state.q).trim(); let visible = 0; groups.forEach((group) => { let groupVisible = 0; group.querySelectorAll('.issue-row').forEach((row) => { const okText = !q || row._search.includes(q); const okRepo = !state.repo || group.dataset.repo === state.repo; const okCol = state.cols.size === 0 || state.cols.has(row.dataset.column); const okLabel = !state.label || row._labels.includes(norm(state.label)); const ok = okText && okRepo && okCol && okLabel; row.hidden = !ok; if (ok) groupVisible++; }); group.hidden = groupVisible === 0; const badge = group.querySelector('[data-group-count]'); if (badge) badge.textContent = groupVisible; visible += groupVisible; }); countEl.textContent = visible === allRows.length ? `${visible} issue${visible > 1 ? 's' : ''}` : `${visible} sur ${allRows.length} issues`; const filtering = q || state.repo || state.label || state.cols.size > 0; resetBtn.hidden = !filtering; emptyBox.hidden = visible !== 0; syncChips(); persist(); } function resetAll() { state.q = ''; search.value = ''; state.repo = ''; repoSel.value = ''; state.label = ''; labelSel.value = ''; state.cols.clear(); sortSel.value = 'recent'; state.sort = 'recent'; sortRows(); apply(); search.focus(); } // ── Événements ───────────────────────────────────────── let raf = 0; search.addEventListener('input', () => { cancelAnimationFrame(raf); raf = requestAnimationFrame(() => { state.q = search.value; apply(); }); }); search.addEventListener('keydown', (e) => { if (e.key === 'Escape') { search.value = ''; state.q = ''; apply(); } }); repoSel.addEventListener('change', () => { state.repo = repoSel.value; apply(); }); labelSel.addEventListener('change', () => { state.label = labelSel.value; apply(); }); sortSel.addEventListener('change', () => { state.sort = sortSel.value; sortRows(); apply(); }); chipsBox.addEventListener('click', (e) => { const chip = e.target.closest('.filter-chip'); if (!chip) return; const col = chip.dataset.col; if (col === '') state.cols.clear(); else if (state.cols.has(col)) state.cols.delete(col); else state.cols.add(col); apply(); }); resetBtn.addEventListener('click', resetAll); emptyReset?.addEventListener('click', resetAll); document.addEventListener('keydown', (e) => { if (e.key !== '/') return; const tag = document.activeElement?.tagName; if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return; e.preventDefault(); search.focus(); }); sortRows(); apply(); })();