// assets/js/comments.js (function () { // Konfiguracja API const baseUrl = (typeof MAGICO_BASE_URL !== 'undefined') ? MAGICO_BASE_URL : ''; const API_URL = baseUrl + '/api-comments.php'; const CURRENT_PATH = window.location.pathname.replace(/^\/|\/$/g, ''); let isCommentMode = false; let pendingElement = null; // Element, który chcemy skomentować let markersData = []; // Przechowujemy dane o markerach lokalnie do szybkiego odświeżania // --- 1. GENEROWANIE UI --- function initUI() { document.body.style.paddingTop = '50px'; const bar = document.createElement('div'); bar.id = 'prototype-topbar'; bar.innerHTML = `
Magico Feedback
Komentarzy: 0
Tryb Komentowania
`; document.body.appendChild(bar); // Kontener na input (domyślnie w body) const modalHtml = `
Dodaj notatkę
Wszystkie komentarze
`; document.body.insertAdjacentHTML('beforeend', modalHtml); // Listenery UI const toggle = document.getElementById('comment-toggle'); toggle.addEventListener('change', function (e) { if (e.target.checked) { // Próba włączenia - autoryzacja checkAuth().then(ok => { if (ok) { isCommentMode = true; localStorage.setItem('magico_comment_mode', 'true'); toggleCommentMode(true); } else { e.target.checked = false; // Cofnij switch } }); } else { isCommentMode = false; localStorage.removeItem('magico_comment_mode'); toggleCommentMode(false); } }); // Toggle visibility const visToggle = document.getElementById('comments-visibility-toggle'); const savedVis = localStorage.getItem('magico_markers_visible'); if (savedVis === 'false') { visToggle.checked = false; document.body.classList.add('comments-hidden'); } visToggle.addEventListener('change', (e) => { if (e.target.checked) { document.body.classList.remove('comments-hidden'); localStorage.setItem('magico_markers_visible', 'true'); } else { document.body.classList.add('comments-hidden'); localStorage.setItem('magico_markers_visible', 'false'); } }); // Open list dialog document.getElementById('comments-count-badge').addEventListener('click', showAllCommentsDialog); document.getElementById('close-all-comments').addEventListener('click', () => { document.getElementById('all-comments-dialog').style.display = 'none'; document.getElementById('all-comments-dialog-overlay').style.display = 'none'; }); // --- RESTORE SESSION --- const savedMode = localStorage.getItem('magico_comment_mode'); if (savedMode === 'true') { // Sprawdź cicho czy sesja PHP jest aktywna checkAuth(true).then(ok => { if (ok) { toggle.checked = true; isCommentMode = true; toggleCommentMode(true); } else { localStorage.removeItem('magico_comment_mode'); // Sesja wygasła } }); } document.getElementById('cancel-comment').addEventListener('click', closeInputBox); document.getElementById('save-comment').addEventListener('click', saveComment); // Scroll listener (capture) - żeby aktualizować pineski przy każdym przewinięciu (okna lub modala) window.addEventListener('scroll', updateMarkerPositions, true); } // --- 1.5 AUTORYZACJA --- // Prosta weryfikacja - czy mamy flagę w sesji JS? // Lepiej: przy włączeniu zapytać o hasło jeśli nie mamy, i wysłać do API. // API ustawi sesję PHP. let isAuthorized = false; // Lokalna flaga, aby nie pytać co chwilę async function checkAuth(silent = false) { if (isAuthorized) return true; // Najpierw zapytaj API czy już jesteśmy zalogowani (sesja PHP) try { const check = await fetch(API_URL + '?action=check_auth'); const checkData = await check.json(); if (checkData.status === 'success') { isAuthorized = true; return true; } } catch (e) { } if (silent) return false; const pass = prompt("Podaj hasło do trybu komentowania:"); if (!pass) return false; try { const res = await fetch(API_URL + '?action=auth', { method: 'POST', body: JSON.stringify({ password: pass }) }); const data = await res.json(); if (data.status === 'success') { isAuthorized = true; return true; } else { alert("Błąd: " + (data.message || "Nieprawidłowe hasło")); return false; } } catch (e) { alert("Błąd połączenia z API"); return false; } } // --- 2. LOGIKA TRYBU (AGRESYWNE BLOKOWANIE) --- function toggleCommentMode(active) { if (active) { document.body.classList.add('comment-mode-active'); // Blokujemy wszystko: click, mousedown, mouseup, submit ['click', 'mousedown', 'mouseup', 'submit'].forEach(evt => window.addEventListener(evt, handleInteraction, true) ); } else { document.body.classList.remove('comment-mode-active'); ['click', 'mousedown', 'mouseup', 'submit'].forEach(evt => window.removeEventListener(evt, handleInteraction, true) ); } } // --- 3. OBSŁUGA INTERAKCJI --- function handleInteraction(e) { // ZAWSZE pozwalamy na interakcję z naszym UI if (e.target.closest('#prototype-topbar') || e.target.closest('#comment-input-box') || e.target.closest('.comment-marker') || e.target.closest('.comment-popover')) { // Dodano popover do wykluczeń return; } // Blokujemy natywną akcję e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); // Reagujemy tylko na 'click' (żeby nie odpalać input boxa 3 razy na mousedown/up/click) if (e.type === 'click') { pendingElement = e.target; openInputBox(); } } // --- 4. SELEKTOR CSS --- function getCssSelector(el) { if (!(el instanceof Element)) return; const path = []; while (el.nodeType === Node.ELEMENT_NODE && el.tagName !== 'BODY') { let selector = el.tagName.toLowerCase(); if (el.id) { selector += '#' + el.id; path.unshift(selector); break; } else { let sib = el, nth = 1; while (sib = sib.previousElementSibling) { if (sib.tagName.toLowerCase() == selector) nth++; } if (nth > 1) selector += ":nth-of-type(" + nth + ")"; if (el.classList.length > 0) { for (let cls of el.classList) { if (cls !== 'active' && cls !== 'show' && cls !== 'collapsed' && cls !== 'fade') { selector += "." + cls; break; } } } } path.unshift(selector); el = el.parentNode; } return path.join(" > "); } // --- 5. OBSŁUGA INPUTA (PRZENOSZENIE DO MODALA) --- function openInputBox() { const box = document.getElementById('comment-input-box'); const overlay = document.getElementById('comment-input-box-overlay'); // FIX: Sprawdź czy jest otwarty modal Bootstrapa const activeModal = document.querySelector('.modal.show .modal-content'); if (activeModal) { // Jeśli tak, przenieś nasz box do modala (żeby działało pisanie i focus) activeModal.appendChild(box); // Overlay też, żeby przykrył modal activeModal.appendChild(overlay); box.style.position = 'absolute'; // W modalu pozycjonujemy absolutnie względem modala } else { // Jeśli nie, wracamy do body document.body.appendChild(overlay); document.body.appendChild(box); box.style.position = 'fixed'; } // Tymczasowe wyłączenie trybu wybierania, żeby można było klikać w Boxie // Ale NIE zdejmujemy listenerów blokujących tło (bo tło nadal ma być nieklikalne) // Po prostu nasza funkcja handleInteraction przepuszcza kliknięcia w #comment-input-box overlay.style.display = 'block'; box.style.display = 'block'; // Focus po małym timeout, żeby przeglądarka zdążyła przenieść element setTimeout(() => { const authorInput = document.getElementById('new-comment-author'); const savedAuthor = localStorage.getItem('magico_comment_author'); if (savedAuthor) authorInput.value = savedAuthor; // Focus na tekst, chyba że brak autora if (!savedAuthor) authorInput.focus(); else document.getElementById('new-comment-text').focus(); }, 50); } function closeInputBox() { const box = document.getElementById('comment-input-box'); const overlay = document.getElementById('comment-input-box-overlay'); overlay.style.display = 'none'; box.style.display = 'none'; document.getElementById('new-comment-text').value = ''; pendingElement = null; // Wracamy z boxem do body na wszelki wypadek document.body.appendChild(overlay); document.body.appendChild(box); } function saveComment() { const text = document.getElementById('new-comment-text').value; const author = document.getElementById('new-comment-author').value || 'Anonim'; if (!text || !pendingElement) { closeInputBox(); return; } // Zapisz autora na przyszłość localStorage.setItem('magico_comment_author', author); const selector = getCssSelector(pendingElement); fetch(API_URL + '?action=add', { method: 'POST', body: JSON.stringify({ page_path: CURRENT_PATH, selector: selector, comment: text, author: author }) }) .then(res => res.json()) .then(data => { if (data.status === 'success') { closeInputBox(); loadComments(); } else { alert('Błąd: ' + data.message); } }); } // --- 6. PINESKI I AKTUALIZACJA POZYCJI --- function loadComments() { fetch(API_URL + '?action=list&page_path=' + encodeURIComponent(CURRENT_PATH)) .then(res => res.json()) .then(resp => { if (resp.status === 'success') { markersData = resp.data; // Zapisz dane renderMarkers(); // Narysuj } }); } let activePopover = null; function renderMarkers() { window.magicoIsUpdating = true; try { // Usuń stare document.querySelectorAll('.comment-marker').forEach(el => el.remove()); if (activePopover) { activePopover.remove(); activePopover = null; } // Grupowanie komentarzy po selektorze const grouped = {}; let activeCount = 0; markersData.forEach((c) => { if (c.is_resolved == 0) activeCount++; // Pomiń rozwiązane, jeśli chcemy je ukrywać (na razie pokazujemy wszystkie, albo tylko nierozwiązane?) // User: "archiwum zmienia flagę is_resolved". Zakładamy że archiwum = ukryte? // "Archiwum" zwykle oznacza ukryte. Ukryjmy rozwiązane. if (c.is_resolved == 1) return; if (!grouped[c.dom_selector]) grouped[c.dom_selector] = []; grouped[c.dom_selector].push(c); }); // Aktualizacja licznika na pasku const cntEl = document.getElementById('comments-count-val'); if (cntEl) cntEl.textContent = activeCount; Object.keys(grouped).forEach((selector, index) => { const commentsList = grouped[selector]; if (!commentsList.length) return; try { const el = document.querySelector(selector); if (el && el.offsetParent !== null) { // Tylko widoczne elementy const marker = document.createElement('div'); marker.className = 'comment-marker'; marker.dataset.selector = selector; // Jeśli więcej niż 1 komentarz, pokaż licznik if (commentsList.length > 1) { marker.textContent = commentsList.length; marker.style.display = 'flex'; marker.style.alignItems = 'center'; marker.style.justifyContent = 'center'; marker.style.color = 'white'; marker.style.fontSize = '12px'; marker.style.fontWeight = 'bold'; // Resetuejmu transform dla tekstu żeby był czytelny? Nie, rotacja jest na pinezce. // Tekst też się obróci. Trzeba by go odkręcić. // Prościej: dodajmy span w środku który odkręcimy. marker.innerHTML = `${commentsList.length}`; } // Zdarzenie HOVER (najazd myszką) marker.addEventListener('mouseenter', (e) => { showPopover(marker, commentsList); }); marker.addEventListener('mouseleave', (e) => { marker._leaveTimeout = setTimeout(() => { if (activePopover && activePopover._associatedMarker === marker) { if (!activePopover.matches(':hover')) { activePopover.remove(); activePopover = null; } } }, 300); }); document.body.appendChild(marker); } } catch (err) { } }); // Po stworzeniu od razu ustaw pozycje updateMarkerPositions(); } finally { setTimeout(() => { window.magicoIsUpdating = false; }, 0); } } function showPopover(marker, commentsList) { if (activePopover) activePopover.remove(); const pop = document.createElement('div'); pop.className = 'comment-popover'; // Budowanie listy komentarzy let html = '
'; commentsList.forEach(data => { // Escape (chociaż API to robi, warto mieć warstwę w JS w razie czego, ale API już robi htmlspecialchars) // Skoro API robi htmlspecialchars, to tutaj możemy bezpiecznie wstawić // Ale uwaga: JS textContent jest bezpieczniejszy. // Hack na szybkie budowanie bezpiecznego HTML w pętli stringów jest trudny. // Zbudujmy to jako elementy DOM potem? Albo zaufajmy API + textContent buildera. // Zrobimy placeholder i podstawimy wartości. html += `
${data.author || 'Anonim'}
${data.created_at}

${data.comment}

`; }); html += '
'; pop.innerHTML = html; document.body.appendChild(pop); activePopover = pop; pop._associatedMarker = marker; // Bindowanie akcji z stopPropagation pop.querySelectorAll('.btn-resolve').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); // Ważne! resolveComment(btn.dataset.id); }); }); pop.querySelectorAll('.btn-delete').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); // Ważne! deleteComment(btn.dataset.id); }); }); // Obsługa interakcji (np. scrollbar) pop.addEventListener('mousedown', () => { pop._isInteracting = true; }); // Obsługa zamykania przy wyjechaniu z popovera pop.addEventListener('mouseleave', () => { pop._leaveTimeout = setTimeout(() => { if (activePopover === pop) { // Jeśli user trzyma przycisk myszy (np. na scrollbarze), nie zamykaj if (pop._isInteracting) return; if (!activePopover.matches(':hover') && !marker.matches(':hover')) { activePopover.remove(); activePopover = null; } } }, 500); // Zwiększono do 500ms }); pop.addEventListener('mouseenter', () => { if (pop._leaveTimeout) clearTimeout(pop._leaveTimeout); if (marker._leaveTimeout) clearTimeout(marker._leaveTimeout); }); // Pozycjonowanie const rect = marker.getBoundingClientRect(); const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; pop.style.top = (rect.top + scrollTop - 5) + 'px'; pop.style.left = (rect.right + scrollLeft) + 'px'; // Usunięto przerwę +2px if (rect.right + 260 > window.innerWidth) { pop.style.left = (rect.left + scrollLeft - 260) + 'px'; } } function resolveComment(id) { if (!confirm('Czy na pewno chcesz rozwiązać/zarchiwizować ten komentarz?')) return; fetch(API_URL + '?action=resolve', { method: 'POST', body: JSON.stringify({ id: id }) }).then(res => res.json()).then(res => { if (res.status === 'success') loadComments(); else alert('Błąd: ' + res.message); }); } function deleteComment(id) { if (!confirm('Czy na pewno chcesz trwale usunąć ten komentarz?')) return; fetch(API_URL + '?action=delete', { method: 'POST', body: JSON.stringify({ id: id }) }).then(res => res.json()).then(res => { if (res.status === 'success') loadComments(); else alert('Błąd: ' + res.message); }); } // (Usunięto click-outside listener, bo teraz działamy na hover) // Aktualizuj też pozycję otwartego popovera przy scrollu const originalUpdatePositions = updateMarkerPositions; // ... w sumie updateMarkerPositions jest niżej zdefiniowana, // lepiej wrzucimy to w updateMarkerPositions bezpośrednio. // Funkcja wywoływana przy scrollowaniu - musi być szybka function updateMarkerPositions() { window.magicoIsUpdating = true; // Zaczynamy aktualizację try { const markers = document.querySelectorAll('.comment-marker'); markers.forEach(marker => { const selector = marker.dataset.selector; if (!selector) return; try { const el = document.querySelector(selector); if (el && el.offsetParent !== null) { const rect = el.getBoundingClientRect(); // Jeśli element wyjechał poza ekran (jest w scrollowanym divie ale schowany) marker.style.top = (rect.top + window.scrollY) + 'px'; marker.style.left = (rect.left + window.scrollX + (rect.width / 2)) + 'px'; marker.style.display = (marker.textContent.length > 0) ? 'flex' : 'block'; // Flex dla licznika, block dla zwykłego? A w stylach mamy display? // W renderMarkers ustawilismy display:flex ręcznie dla licznika. // Tu musimy uwazac zeby tego nie zepsuc. Pominmy style.display='block' jesli ma flex. if (marker.style.display !== 'flex') marker.style.display = 'block'; if (activePopover && activePopover._associatedMarker === marker) { const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; // Recalculate position let top = (rect.top + scrollTop - 5); let left = (rect.right + scrollLeft); if (rect.right + 260 > window.innerWidth) { left = (rect.left + scrollLeft - 260); } activePopover.style.top = top + 'px'; activePopover.style.left = left + 'px'; } } else { marker.style.display = 'none'; if (activePopover && activePopover._associatedMarker === marker) { // Jeśli element zniknął (np. scroll), ale user trzyma myszkę na popoverze - nie usuwaj if (!activePopover.matches(':hover') && !activePopover._isInteracting) { activePopover.remove(); activePopover = null; } } } } catch (e) { } }); } finally { // Mały timeout, bo MutationObserver jest asynchroniczny (microtask) // Ale my chcemy zablokować detekcję zmian, które WŁAŚNIE zaszły. // Ponieważ observer odpala się "później", flaga false może zostać ustawiona ZA WCZEŚNIE? // Nie, observer zbiera zmiany i odpala callback. // Jeśli callback odpali się kiedy flaga jest true, to return. // Ale callback odpali się w następnym ticku. // Więc musimy przetrzymać flagę true do następnego ticku? // Tak, bezpieczniej setTimeout(..., 0). setTimeout(() => { window.magicoIsUpdating = false; }, 0); } } // Global mouseup do resetowania flagi interakcji document.addEventListener('mouseup', () => { if (activePopover) { activePopover._isInteracting = false; // Opcjonalnie: sprawdź czy zamknąć, jeśli myszka jest poza if (activePopover && !activePopover.matches(':hover') && (!activePopover._associatedMarker || !activePopover._associatedMarker.matches(':hover'))) { // Możemy wywołać logikę zamykania, ale ona jest w mouseleave. // Jeśli user puścił myszkę POZA popoverem, mouseleave już dawno poszło (i timeout mógł zostać zablokowany przez flagę). // Więc tutaj warto sprawdzić. setTimeout(() => { // Sprawdź ponownie (bezpiecznik) if (activePopover && !activePopover._isInteracting && !activePopover.matches(':hover')) { activePopover.remove(); activePopover = null; } }, 200); } } }); // START initUI(); loadComments(); // Odświeżanie przy zmianie rozmiaru okna window.addEventListener('resize', updateMarkerPositions); // MutationObserver - odświeżaj pozycje jak coś się zmieni w wygenerowanym DOM (ale ignoruj nasze markery) // Helper sprawdzający czy element jest częścią naszego UI function isOurElement(node) { if (!node || node.nodeType !== 1) return false; // Lista ID elementów systemu const ourIds = [ 'prototype-topbar', 'comment-input-box', 'comment-input-box-overlay', 'all-comments-dialog', 'all-comments-dialog-overlay' ]; if (ourIds.includes(node.id)) return true; if (node.classList && ( node.classList.contains('comment-marker') || node.classList.contains('comment-popover') )) return true; // Sprawdź rodziców if (node.closest) { for (const id of ourIds) { if (node.closest('#' + id)) return true; } if (node.closest('.comment-marker') || node.closest('.comment-popover')) return true; } return false; } // --- 8. OBSŁUGA OBSERVERA I PĘTLI --- // Zmienna na observer zdefiniowana niżej, ale potrzebujemy jej tu. // Przenieśmy definicję observera wyżej albo użyjmy funkcji. let observer; // deklaracja wstępna function ignoreObserver(action) { if (observer) observer.disconnect(); try { action(); } finally { if (observer) startObserver(); } } function startObserver() { observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'style', 'hidden'] }); } // --- MODYFIKACJA renderMarkers i updateMarkerPositions --- // Przechwytujemy stare funkcje i opakowujemy je // Ale lepiej po prostu zmienić ich ciała w kodzie (co robię tym replace'm) // Nadpiszmy renderMarkers, żeby używało ignoreObserver // Uwaga: Funkcja renderMarkers i updateMarkerPositions są wyżej. // Ten tool replace musi być sprytny. // Zamiast nadpisywać, zmienię logikę observera na dole pliku i podmienię wywołania. // W tym bloku (EndLine: 734) jest końcówka pliku i definicja observera. // Zdefiniujmy observera tutaj poprawnie. observer = new MutationObserver((mutations) => { // Jeśli my sami aktualizujemy, ignoruj wszystko if (window.magicoIsUpdating) return; let shouldUpdate = false; // Optymalizacja: Sprawdzamy czy zmiany są ISTOTNE for (const mutation of mutations) { // Ignorujemy zmiany na naszych elementach (nawet jeśli disconnect nie zadziałał idealnie) if (isOurElement(mutation.target)) continue; // Zmiany childList if (mutation.type === 'childList') { let externalChanges = false; // Sprawdź dodane for (let i = 0; i < mutation.addedNodes.length; i++) { const node = mutation.addedNodes[i]; if (node.nodeType === 1 && !isOurElement(node)) { externalChanges = true; break; } if (node.nodeType === 3 && node.textContent.trim() !== '') { externalChanges = true; break; } } // Sprawdź usunięte if (!externalChanges) { for (let i = 0; i < mutation.removedNodes.length; i++) { const node = mutation.removedNodes[i]; if (node.nodeType === 1 && !isOurElement(node)) { externalChanges = true; break; } } } if (!externalChanges) continue; } shouldUpdate = true; break; } if (shouldUpdate) { if (window.commentUpdateTimeout) clearTimeout(window.commentUpdateTimeout); window.commentUpdateTimeout = setTimeout(() => { // Renderowanie wyzwolone przez zmiany zewnętrzne // Nie musimy tu robić ignoreObserver bo renderMarkers samo to zrobi wewnątrz? // Nie, bo renderMarkers to funkcja. // Wywołajmy renderMarkers. renderMarkers(); }, 200); } }); startObserver(); // EXPORT funkcji do użycia wewnątrz renderMarkers/update // Ponieważ nie edytuję całego pliku, musimy jakoś wstrzyknąć ignoreObserver do wywołań wyżej? // Nie da się bez edycji tamtych funkcji. // WIĘC: Zostawiam observera jak jest (z ulepszonym isOurElement), // ALE w renderMarkers i updateMarkerPositions dodam wywołania disconnect/connect. // Czekaj, nie mam dostępu do zmiennej `observer` wewnątrz funkcji zadeklarowanych wyżej, jeśli zadeklaruję ją na dole. // Ale w JS var/let w tym samym scope (IIFE) są widoczne. // Muszę przenieść deklarację `let observer` na górę IIFE (innym replacem) lub // Zmienić observer na `window.magicoObserver`? Nieładnie. // Zrobię tak: Zdefiniuję `ignoreObserver` tutaj, i użyję `observer.disconnect()` wewnątrz. // Ale muszę mieć pewność że `observer` jest zdefiniowany. // W obecnym kodzie jest zdefiniowany jako `const observer = ...` na dole. // const nie jest hoisted. // PLAN B: // Zmienię definicję `const observer` na `let observer` i przesunę definicję funkcji `isOurElement` i observera na dół, // a potem w replace'ach wyżej dodam wywołania wrapperów. // LUB PROŚCIEJ: // Zmodyfikujmy `renderMarkers` i `updateMarkerPositions` żeby sprawdzały flagę globalną `window.magicoIsUpdating`. // A observer będzie sprawdzał tę flagę. // IMPLEMENTACJA FLAGI: window.magicoIsUpdating = false; // --- 7. DIALOG Z LISTĄ WSZYSTKICH --- function showAllCommentsDialog() { const dialog = document.getElementById('all-comments-dialog'); const overlay = document.getElementById('all-comments-dialog-overlay'); const list = document.getElementById('all-comments-list'); // Budowanie listy // Filtrujemy tylko nierozwiązane??? W sumie user chciał "wszystkimi zebranymi z tego ekranu". // Pokażmy wszystkie uporządkowane od najnowszych, z oznaczeniem rozwiązanych. // Kopia i sortowanie const sorted = [...markersData].sort((a, b) => b.id - a.id); if (sorted.length === 0) { list.innerHTML = '

Brak komentarzy na tym ekranie.

'; } else { let html = ''; sorted.forEach(c => { const bg = c.is_resolved == 1 ? '#f0fff0' : '#fff'; const status = c.is_resolved == 1 ? 'Rozwiązany' : ''; html += `
${c.author || 'Anonim'} ${status}
${c.created_at}

${c.comment}

Element: ${c.dom_selector.substring(0, 30)}...
${c.is_resolved == 0 ? `` : ''}
`; }); list.innerHTML = html; // Listenery w liście list.querySelectorAll('.btn-resolve-list').forEach(btn => { btn.addEventListener('click', () => resolveComment(btn.dataset.id)); }); list.querySelectorAll('.btn-delete-list').forEach(btn => { btn.addEventListener('click', () => deleteComment(btn.dataset.id)); }); } dialog.style.display = 'block'; overlay.style.display = 'block'; } // Dodajmy do renderMarkers aktualizację licznika // Musimy przechwycić oryginalny renderMarkers wyżej // Ale jesteśmy w module, więc po prostu dopiszmy to do renderMarkers // W renderMarkers (linia ~300) dodać: // document.getElementById('comments-count-val').textContent = markersData.filter(c => c.is_resolved == 0).length; // Albo wszystkich? "Liczba komentarzy". Raczej wszystkich aktywnych. })();