Files
magico.prototype/assets/js/comments.js

870 lines
37 KiB
JavaScript

// 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 = `
<div style="display:flex; align-items:center;">
<div style="font-weight: bold; font-size: 14px; margin-right: 20px;">
<span style="color: #696cff;">Magico</span> Feedback
</div>
</div>
<div style="display:flex; align-items:center; gap: 20px;">
<!-- Licznik -->
<div id="comments-count-badge" style="cursor:pointer; font-size:13px; display:flex; align-items:center; gap:5px; padding: 5px 10px; background: rgba(255,255,255,0.1); border-radius: 4px;">
<span style="opacity:0.8;">Komentarzy:</span>
<span id="comments-count-val" style="background:#696cff; padding:2px 8px; border-radius:10px; font-weight:bold; font-size:11px;">0</span>
</div>
<!-- Pokaż/Ukryj -->
<div style="display:flex; align-items:center; font-size:13px; gap: 5px;">
<input type="checkbox" id="comments-visibility-toggle" checked style="cursor:pointer;">
<label for="comments-visibility-toggle" style="cursor:pointer; margin-bottom:0;">Pokaż pineski</label>
</div>
<div style="width: 1px; height: 20px; background: rgba(255,255,255,0.2);"></div>
<!-- Tryb -->
<div class="mode-switch">
<span style="font-size: 13px; margin-right: 8px;">Tryb Komentowania</span>
<label class="switch-label">
<input type="checkbox" id="comment-toggle">
<span class="slider"></span>
</label>
</div>
</div>
`;
document.body.appendChild(bar);
// Kontener na input (domyślnie w body)
const modalHtml = `
<div id="comment-input-box-overlay"></div>
<div id="comment-input-box">
<h6 style="margin-top:0;">Dodaj notatkę</h6>
<input type="text" id="new-comment-author" class="form-control mb-2" placeholder="Twój podpis (np. Jan)" style="font-size: 0.9rem;">
<textarea id="new-comment-text" class="form-control mb-2" rows="3" placeholder="Wpisz uwagę..."></textarea>
<div style="text-align: right; gap: 5px; display: flex; justify-content: flex-end;">
<button id="cancel-comment" class="btn btn-sm btn-outline-secondary">Anuluj</button>
<button id="save-comment" class="btn btn-sm btn-primary">Zapisz</button>
</div>
</div>
<!-- Modal Lista Wszystkich -->
<div id="all-comments-dialog-overlay"></div>
<div id="all-comments-dialog">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px; border-bottom:1px solid #eee; padding-bottom:10px;">
<h5 style="margin:0;">Wszystkie komentarze</h5>
<button id="close-all-comments" class="btn btn-sm btn-outline-secondary">X</button>
</div>
<div id="all-comments-list" style="max-height:60vh; overflow-y:auto; padding-right:5px;"></div>
</div>
`;
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 = `<span style="transform: rotate(45deg); display:block;">${commentsList.length}</span>`;
}
// 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 = '<div style="max-height: 600px; overflow-y: auto;">';
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 += `
<div class="comment-item" style="border-bottom:1px solid #eee; padding-bottom:8px; margin-bottom:8px;">
<div style="display:flex; justify-content:space-between; margin-bottom:4px;">
<h6 style="margin:0;">${data.author || 'Anonim'}</h6>
<span style="font-size:10px; color:#999;">${data.created_at}</span>
</div>
<p style="margin-bottom:5px;">${data.comment}</p>
<div style="text-align:right; gap:5px; display:flex; justify-content:flex-end;">
${isAuthorized ? `<button class="btn btn-xs btn-outline-success btn-resolve" data-id="${data.id}" style="font-size:10px; padding: 2px 5px;">Rozwiąż</button>
<button class="btn btn-xs btn-outline-danger btn-delete" data-id="${data.id}" style="font-size:10px; padding: 2px 5px;">Usuń</button>` : ''}
</div>
</div>
`;
});
html += '</div>';
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();
// Sprawdź autoryzację w tle (nawet wyłączony tryb komentowania), w celu wyświetlenia przycisków
checkAuth(true).then(() => {
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 = '<p style="text-align:center; color:#999; padding:20px;">Brak komentarzy na tym ekranie.</p>';
} else {
let html = '';
sorted.forEach(c => {
const bg = c.is_resolved == 1 ? '#f0fff0' : '#fff';
const status = c.is_resolved == 1 ? '<span class="badge bg-success" style="font-size:10px;">Rozwiązany</span>' : '';
html += `
<div class="comment-list-item" style="background:${bg};">
<div style="display:flex; justify-content:space-between; margin-bottom:5px;">
<div>
<strong>${c.author || 'Anonim'}</strong>
${status}
</div>
<small style="color:#999;">${c.created_at}</small>
</div>
<p style="margin-bottom:8px;">${c.comment}</p>
<div style="font-size:11px; color:#888; margin-bottom:5px;">
Element: <code>${c.dom_selector.substring(0, 30)}...</code>
</div>
<div style="text-align:right;">
${isAuthorized && c.is_resolved == 0 ? `<button class="btn btn-sm btn-outline-success btn-resolve-list" data-id="${c.id}">Rozwiąż</button>` : ''}
${isAuthorized ? `<button class="btn btn-sm btn-outline-danger btn-delete-list" data-id="${c.id}">Usuń</button>` : ''}
</div>
</div>
`;
});
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.
})();