// 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
Tryb Komentowania
`; document.body.appendChild(bar); // Kontener na input (domyślnie w body) const modalHtml = `
Dodaj notatkę
`; document.body.insertAdjacentHTML('beforeend', modalHtml); // Listenery UI document.getElementById('comment-toggle').addEventListener('change', function (e) { isCommentMode = e.target.checked; toggleCommentMode(isCommentMode); }); 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); } // --- 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')) { 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(() => 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; if (!text || !pendingElement) { closeInputBox(); return; } const selector = getCssSelector(pendingElement); fetch(API_URL + '?action=add', { method: 'POST', body: JSON.stringify({ page_path: CURRENT_PATH, selector: selector, comment: text, author: 'User' }) }) .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() { // Usuń stare document.querySelectorAll('.comment-marker').forEach(el => el.remove()); if (activePopover) { activePopover.remove(); activePopover = null; } markersData.forEach((c, index) => { try { const el = document.querySelector(c.dom_selector); if (el && el.offsetParent !== null) { // Tylko widoczne elementy const marker = document.createElement('div'); marker.className = 'comment-marker'; marker.dataset.index = index; // Żeby łatwo znaleźć dane // Zdarzenie kliknięcia w marker marker.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); showPopover(marker, c); }); document.body.appendChild(marker); } } catch (err) { } }); // Po stworzeniu od razu ustaw pozycje updateMarkerPositions(); } function showPopover(marker, data) { if (activePopover) activePopover.remove(); const pop = document.createElement('div'); pop.className = 'comment-popover'; // Escape HTML - XSS Protection const safeAuthor = document.createElement('div'); safeAuthor.textContent = data.author || 'Anonim'; const safeComment = document.createElement('div'); safeComment.textContent = data.comment || ''; pop.innerHTML = `
${safeAuthor.innerHTML}

${safeComment.innerHTML}

`; document.body.appendChild(pop); activePopover = pop; pop._associatedMarker = marker; // 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 - 10) + 'px'; // Trochę wyżej pop.style.left = (rect.right + scrollLeft + 10) + 'px'; // Obok markera // Jeśli wychodzi poza ekran z prawej, daj na lewo if (rect.right + 260 > window.innerWidth) { pop.style.left = (rect.left + scrollLeft - 260) + 'px'; } } // Zamknij popover przy kliknięciu w tło window.addEventListener('click', (e) => { if (activePopover && !e.target.closest('.comment-popover') && !e.target.closest('.comment-marker')) { activePopover.remove(); activePopover = null; } }); // 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() { const markers = document.querySelectorAll('.comment-marker'); markers.forEach(marker => { const index = marker.dataset.index; const data = markersData[index]; if (!data) return; try { const el = document.querySelector(data.dom_selector); if (el && el.offsetParent !== null) { const rect = el.getBoundingClientRect(); // Jeśli element wyjechał poza ekran (jest w scrollowanym divie ale schowany) // Można go ukryć, ale na razie po prostu przesuwamy marker.style.top = (rect.top + window.scrollY) + 'px'; marker.style.left = (rect.left + window.scrollX + (rect.width / 2)) + 'px'; 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 - 10); let left = (rect.right + scrollLeft + 10); if (rect.right + 260 > window.innerWidth) { left = (rect.left + scrollLeft - 260); // Flip to left } activePopover.style.top = top + 'px'; activePopover.style.left = left + 'px'; } } else { marker.style.display = 'none'; // Ukryj jeśli element zniknął } } catch (e) { } }); // Jeśli jest aktywny popover, zamknij go przy szybkim scrollu żeby nie pływał dziwnie? // Albo po prostu zostaw. } // START initUI(); loadComments(); // Odświeżanie przy zmianie rozmiaru okna window.addEventListener('resize', updateMarkerPositions); // MutationObserver - odświeżaj pozycje jak coś się zmieni w DOM (np otwarcie taba) const observer = new MutationObserver(() => { // Debounce dla wydajności if (window.commentUpdateTimeout) clearTimeout(window.commentUpdateTimeout); window.commentUpdateTimeout = setTimeout(renderMarkers, 200); }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'style', 'hidden'] }); })();