Files
magico.prototype/assets/js/comments.js
2026-02-18 21:17:24 +01:00

358 lines
14 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="font-weight: bold; font-size: 14px;">
<span style="color: #696cff;">Magico</span> Feedback
</div>
<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>
`;
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>
<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>
`;
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 = `
<h6>${safeAuthor.innerHTML}</h6>
<p>${safeComment.innerHTML}</p>
`;
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']
});
})();