592 lines
24 KiB
JavaScript
592 lines
24 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
|
|
// 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);
|
|
}
|
|
});
|
|
|
|
// --- 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')) {
|
|
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; }
|
|
|
|
// Grupowanie komentarzy po selektorze
|
|
const grouped = {};
|
|
markersData.forEach((c) => {
|
|
// 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);
|
|
});
|
|
|
|
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();
|
|
}
|
|
|
|
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: 200px; 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;">
|
|
<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
|
|
pop.querySelectorAll('.btn-resolve').forEach(btn => {
|
|
btn.addEventListener('click', () => resolveComment(btn.dataset.id));
|
|
});
|
|
pop.querySelectorAll('.btn-delete').forEach(btn => {
|
|
btn.addEventListener('click', () => deleteComment(btn.dataset.id));
|
|
});
|
|
|
|
// Obsługa zamykania przy wyjechaniu z popovera
|
|
pop.addEventListener('mouseleave', () => {
|
|
if (activePopover === pop) {
|
|
activePopover.remove();
|
|
activePopover = null;
|
|
}
|
|
});
|
|
|
|
// 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 + 2) + 'px';
|
|
|
|
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() {
|
|
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 + 2);
|
|
|
|
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) {
|
|
activePopover.remove();
|
|
activePopover = null;
|
|
}
|
|
}
|
|
} catch (e) { }
|
|
});
|
|
}
|
|
|
|
// 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;
|
|
|
|
if (node.classList && (
|
|
node.classList.contains('comment-marker') ||
|
|
node.classList.contains('comment-popover') ||
|
|
node.id === 'comment-input-box' ||
|
|
node.id === 'comment-input-box-overlay'
|
|
)) return true;
|
|
|
|
if (node.closest && (
|
|
node.closest('.comment-marker') ||
|
|
node.closest('.comment-popover') ||
|
|
node.closest('#comment-input-box')
|
|
)) return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
const observer = new MutationObserver((mutations) => {
|
|
let shouldUpdate = false;
|
|
|
|
for (const mutation of mutations) {
|
|
// 1. Zmiany atrybutów na naszych elementach
|
|
if (mutation.type === 'attributes') {
|
|
if (isOurElement(mutation.target)) continue;
|
|
}
|
|
|
|
// 2. Zmiany w strukturze DOM (dodawanie/usuwanie węzłów)
|
|
if (mutation.type === 'childList') {
|
|
let externalChanges = false;
|
|
|
|
// Sprawdź dodane
|
|
if (mutation.addedNodes.length > 0) {
|
|
for (let i = 0; i < mutation.addedNodes.length; i++) {
|
|
const node = mutation.addedNodes[i];
|
|
if (node.nodeType === 1 && !isOurElement(node)) {
|
|
externalChanges = true; break;
|
|
}
|
|
// Ignorujemy same zmiany tekstowe/puste znaki w body, chyba że to istotne?
|
|
// Dla bezpieczeństwa: jeśli to tekst i nie jest pusty -> update
|
|
if (node.nodeType === 3 && node.textContent.trim() !== '') {
|
|
externalChanges = true; break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sprawdź usunięte (tylko jeśli jeszcze nie wykryto zmian)
|
|
if (!externalChanges && mutation.removedNodes.length > 0) {
|
|
for (let i = 0; i < mutation.removedNodes.length; i++) {
|
|
const node = mutation.removedNodes[i];
|
|
if (node.nodeType === 1 && !isOurElement(node)) {
|
|
externalChanges = true; break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Jeśli zmiany nie dotyczyły "zewnętrznych" elementów (czyli tylko nasze), ignoruj.
|
|
if (!externalChanges) continue;
|
|
}
|
|
|
|
shouldUpdate = true;
|
|
break;
|
|
}
|
|
|
|
if (shouldUpdate) {
|
|
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']
|
|
});
|
|
|
|
})(); |