Tryb komentowania ekranów
This commit is contained in:
225
assets/css/comments.css
Normal file
225
assets/css/comments.css
Normal file
@@ -0,0 +1,225 @@
|
||||
/* assets/css/comments.css */
|
||||
|
||||
/* Pasek narzędzi na górze */
|
||||
#prototype-topbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background: #232f3e;
|
||||
color: white;
|
||||
z-index: 100000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
#prototype-topbar .mode-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Przełącznik (Toggle) */
|
||||
.switch-label {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.switch-label input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: .4s;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked+.slider {
|
||||
background-color: #2196F3;
|
||||
}
|
||||
|
||||
input:checked+.slider:before {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
/* Pinezki (Markery) */
|
||||
.comment-marker {
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #ff3e1d;
|
||||
border: 2px solid white;
|
||||
border-radius: 50% 50% 50% 0;
|
||||
transform: rotate(-45deg) translate(-50%, -100%);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
cursor: pointer;
|
||||
z-index: 10000;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.comment-marker:hover {
|
||||
z-index: 10001;
|
||||
transform: rotate(-45deg) scale(1.2);
|
||||
}
|
||||
|
||||
.comment-marker.resolved {
|
||||
background: #71dd37;
|
||||
/* Zielony dla rozwiązanych */
|
||||
}
|
||||
|
||||
/* Dymek z komentarzem (Tooltip) */
|
||||
.comment-popover {
|
||||
position: absolute;
|
||||
background: white;
|
||||
border: 1px solid #d9dee3;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
width: 250px;
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
|
||||
z-index: 10002;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.comment-popover h6 {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.comment-popover p {
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Tryb wyboru - podświetlanie elementów */
|
||||
.comment-mode-active *:hover {
|
||||
outline: 2px dashed #2196F3 !important;
|
||||
cursor: comment !important;
|
||||
}
|
||||
|
||||
/* Wykluczenia, żeby nie podświetlać samego UI komentarzy */
|
||||
.comment-mode-active #prototype-topbar *:hover,
|
||||
.comment-mode-active .comment-marker:hover,
|
||||
.comment-mode-active .comment-popover *:hover {
|
||||
outline: none !important;
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
/* Modal do dodawania (prosty) */
|
||||
#comment-input-box {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
z-index: 100002;
|
||||
width: 300px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#comment-input-box textarea {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
margin-bottom: 10px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#comment-input-box-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 100001;
|
||||
display: none;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
/* --- POPRAWKA: Wykluczenie okienka edycji z efektów wizualnych --- */
|
||||
|
||||
/* Nawet w trybie comment-mode-active, nasze okno dialogowe ma wyglądać normalnie */
|
||||
.comment-mode-active #comment-input-box,
|
||||
.comment-mode-active #comment-input-box * {
|
||||
outline: none !important;
|
||||
cursor: auto !important;
|
||||
/* Przywraca normalny kursor (strzałkę/łapkę) */
|
||||
}
|
||||
|
||||
/* --- POPRAWKA: Wykluczenie okienka edycji z efektów wizualnych --- */
|
||||
|
||||
/* assets/css/comments.css */
|
||||
|
||||
/* ... poprzednie style bez zmian ... */
|
||||
|
||||
/* POPRAWKI DLA MODALA I FOCUSU */
|
||||
#comment-input-box {
|
||||
/* Ustawiamy fixed/absolute dynamicznie w JS, ale bazowo: */
|
||||
z-index: 100000;
|
||||
/* Musi być wyżej niż Bootstrap Modal (zwykle 1055) */
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
width: 320px;
|
||||
display: none;
|
||||
/* Centrowanie - teraz zrobimy to sprytniej w CSS */
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
#comment-input-box-overlay {
|
||||
position: fixed;
|
||||
/* JS zmieni na absolute jeśli w modalu */
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(2px);
|
||||
z-index: 99999;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Ważne: Pineski muszą być nad wszystkim */
|
||||
.comment-marker {
|
||||
z-index: 100001;
|
||||
pointer-events: auto !important;
|
||||
/* Żeby dało się w nie klikać nawet w trybie blokady */
|
||||
}
|
||||
358
assets/js/comments.js
Normal file
358
assets/js/comments.js
Normal file
@@ -0,0 +1,358 @@
|
||||
// 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']
|
||||
});
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user