forked from FroSteel/Planification
v2026.5.20 — Safe area : popups jamais cachés sous topbar/dock
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Planification",
|
"name": "Planification",
|
||||||
"version": "2026.5.19",
|
"version": "2026.5.20",
|
||||||
"description": "Vue claire et rapide du planning des techniciens EasyVista. Regroupe interventions et réservations par tech, affiche horaires, contact, lieu, catégorie et statut en un coup d'œil.",
|
"description": "Vue claire et rapide du planning des techniciens EasyVista. Regroupe interventions et réservations par tech, affiche horaires, contact, lieu, catégorie et statut en un coup d'œil.",
|
||||||
"permissions": ["activeTab", "scripting", "storage", "tabs", "alarms"],
|
"permissions": ["activeTab", "scripting", "storage", "tabs", "alarms"],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
|
|||||||
+58
-1
@@ -2793,8 +2793,15 @@ header.topbar::before {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
.pinned-popup-refresh.spinning {
|
.pinned-popup-refresh svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
.pinned-popup-refresh.spinning svg {
|
||||||
animation: pinned-popup-refresh-spin 0.6s linear infinite;
|
animation: pinned-popup-refresh-spin 0.6s linear infinite;
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
}
|
||||||
|
.pinned-popup-refresh.spinning {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@keyframes pinned-popup-refresh-spin {
|
@keyframes pinned-popup-refresh-spin {
|
||||||
@@ -2834,3 +2841,53 @@ body.popup-dragging .pinned-popup {
|
|||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
font-family: var(--mono, monospace);
|
font-family: var(--mono, monospace);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
v2026.5.20 : mini-menu au survol d'une pastille dock
|
||||||
|
========================================================================== */
|
||||||
|
.pill-hover-menu {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 60;
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||||
|
padding: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 130px;
|
||||||
|
animation: pill-hover-menu-appear 0.12s ease-out;
|
||||||
|
}
|
||||||
|
@keyframes pill-hover-menu-appear {
|
||||||
|
from { opacity: 0; transform: translateY(4px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
.pill-hover-menu-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
transition: background 0.12s;
|
||||||
|
}
|
||||||
|
.pill-hover-menu-btn:hover {
|
||||||
|
background: var(--bg-muted);
|
||||||
|
}
|
||||||
|
.pill-hover-menu-btn.pill-hover-menu-close:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.15);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
.pill-menu-ico {
|
||||||
|
font-size: 14px;
|
||||||
|
width: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|||||||
@@ -565,18 +565,65 @@ function bindTopbar() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// v2026.5.20 : nouveau comportement de la touche Échap
|
||||||
|
// - Appui court : ferme uniquement le popup SOUS la souris (normal ou
|
||||||
|
// minimisé). Si la souris n'est sur aucun popup, ne fait rien.
|
||||||
|
// Ferme aussi le popup user-badge et la grande bulle anchored.
|
||||||
|
// - Maintenu ≥ 3 secondes : ferme TOUS les popups flottants, mais garde
|
||||||
|
// les pastilles dock (popups "réduits" en bas).
|
||||||
|
let _escHoldTimer = null;
|
||||||
|
let _escHoldTriggered = false;
|
||||||
|
const ESC_HOLD_MS = 3000;
|
||||||
|
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key !== "Escape") return;
|
||||||
hideUserNamePopup();
|
// keydown peut se répéter si la touche est maintenue ; on ignore les répétitions.
|
||||||
// v4.2.4 : Échap ferme aussi la grande bulle anchored
|
if (e.repeat) return;
|
||||||
const tip = tooltipEl();
|
// Armer le timer "maintenu 3s"
|
||||||
if (tip && tip.dataset.mode === "anchored" && tip.classList.contains("visible")) {
|
_escHoldTriggered = false;
|
||||||
hideTooltip({ force: true });
|
if (_escHoldTimer) clearTimeout(_escHoldTimer);
|
||||||
}
|
_escHoldTimer = setTimeout(() => {
|
||||||
// v4.3.0 : Échap ferme TOUS les popups épinglés (le user veut tout fermer)
|
_escHoldTriggered = true;
|
||||||
if (typeof closeAllPinnedPopups === "function") {
|
_escHoldTimer = null;
|
||||||
closeAllPinnedPopups();
|
// Fermer TOUS les popups flottants (normaux + minimisés) mais pas les dockés
|
||||||
|
document.querySelectorAll(".pinned-popup:not(.pinned-popup-reduced)").forEach(p => {
|
||||||
|
try { p.remove(); } catch (err) {}
|
||||||
|
});
|
||||||
|
// Nettoyer la liste
|
||||||
|
for (let i = pinnedPopups.length - 1; i >= 0; i--) {
|
||||||
|
if (!document.body.contains(pinnedPopups[i].el)) {
|
||||||
|
pinnedPopups.splice(i, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
_ensureDockCloseAllBtn();
|
||||||
|
}, ESC_HOLD_MS);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keyup", (e) => {
|
||||||
|
if (e.key !== "Escape") return;
|
||||||
|
if (_escHoldTimer) {
|
||||||
|
clearTimeout(_escHoldTimer);
|
||||||
|
_escHoldTimer = null;
|
||||||
|
}
|
||||||
|
if (_escHoldTriggered) {
|
||||||
|
// On a déjà fait l'action "maintenu", ne rien faire de plus
|
||||||
|
_escHoldTriggered = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Appui court : fermer le popup sous la souris si applicable
|
||||||
|
hideUserNamePopup();
|
||||||
|
const tip = tooltipEl();
|
||||||
|
if (tip && tip.dataset.mode === "anchored" && tip.classList.contains("visible")) {
|
||||||
|
hideTooltip({ force: true });
|
||||||
|
}
|
||||||
|
// Quel popup est sous la souris ? Utiliser :hover pour détecter
|
||||||
|
const hovered = document.querySelector(".pinned-popup:hover");
|
||||||
|
if (hovered && !hovered.classList.contains("pinned-popup-reduced")) {
|
||||||
|
// Retirer aussi de pinnedPopups
|
||||||
|
const idx = pinnedPopups.findIndex(p => p.el === hovered);
|
||||||
|
if (idx >= 0) pinnedPopups.splice(idx, 1);
|
||||||
|
hovered.remove();
|
||||||
|
_ensureDockCloseAllBtn();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -6399,31 +6446,41 @@ function _rectsOverlap(a, b) {
|
|||||||
function _findFreePopupPosition(rowEl, w, h) {
|
function _findFreePopupPosition(rowEl, w, h) {
|
||||||
const pad = 14;
|
const pad = 14;
|
||||||
const rowRect = rowEl.getBoundingClientRect();
|
const rowRect = rowEl.getBoundingClientRect();
|
||||||
const viewportW = window.innerWidth;
|
// v2026.5.20 : utiliser la safe area (en dessous topbar, au-dessus dock)
|
||||||
const viewportH = window.innerHeight;
|
const safe = _getPopupSafeArea();
|
||||||
|
|
||||||
// 4 candidats, en coords viewport
|
// 4 candidats d'abord, autour de la row source (en coords viewport)
|
||||||
const candidates = [
|
const candidates = [
|
||||||
// Droite
|
{ x: rowRect.right + pad, y: rowRect.top, name: "droite" },
|
||||||
{ x: rowRect.right + pad, y: rowRect.top, name: "droite" },
|
{ x: rowRect.left - w - pad, y: rowRect.top, name: "gauche" },
|
||||||
// Gauche
|
{ x: rowRect.left, y: rowRect.bottom + pad, name: "dessous" },
|
||||||
{ x: rowRect.left - w - pad, y: rowRect.top, name: "gauche" },
|
{ x: rowRect.left, y: rowRect.top - h - pad, name: "dessus" }
|
||||||
// Dessous
|
|
||||||
{ x: rowRect.left, y: rowRect.bottom + pad, name: "dessous" },
|
|
||||||
// Dessus
|
|
||||||
{ x: rowRect.left, y: rowRect.top - h - pad, name: "dessus" }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Pour chaque candidat, clamper dans le viewport (marge 8px) et convertir
|
// v2026.5.20 : ajouter une grille de positions de fallback couvrant toute
|
||||||
// en coord document, puis tester le chevauchement
|
// la safe area (pas de 60px × 60px) — garantit qu'on trouve ~toujours une
|
||||||
|
// place, sauf si vraiment trop de popups actifs.
|
||||||
|
const availW = safe.right - safe.left;
|
||||||
|
const availH = safe.bottom - safe.top;
|
||||||
|
if (availW > w + 20 && availH > h + 20) {
|
||||||
|
for (let y = safe.top; y + h <= safe.bottom; y += 60) {
|
||||||
|
for (let x = safe.left; x + w <= safe.right; x += 60) {
|
||||||
|
candidates.push({ x, y, name: "grid" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tester chaque candidat dans l'ordre
|
||||||
for (const c of candidates) {
|
for (const c of candidates) {
|
||||||
let x = c.x, y = c.y;
|
let x = c.x, y = c.y;
|
||||||
// Clamp horizontal dans le viewport
|
// Clamp dans la safe area
|
||||||
if (x < 4) x = 4;
|
if (x < safe.left) x = safe.left;
|
||||||
if (x + w > viewportW - 8) x = viewportW - 8 - w;
|
if (x + w > safe.right) x = safe.right - w;
|
||||||
// Clamp vertical dans le viewport
|
if (x < safe.left) continue; // popup plus large que safe area
|
||||||
if (y < 4) y = 4;
|
if (y < safe.top) y = safe.top;
|
||||||
if (y + h > viewportH - 8) y = viewportH - 8 - h;
|
if (y + h > safe.bottom) y = safe.bottom - h;
|
||||||
|
if (y < safe.top) continue;
|
||||||
|
|
||||||
// Si, après clamp, la popup chevaucherait la ligne source elle-même,
|
// Si, après clamp, la popup chevaucherait la ligne source elle-même,
|
||||||
// on ignore ce candidat (on préfère une direction qui la laisse visible).
|
// on ignore ce candidat (on préfère une direction qui la laisse visible).
|
||||||
const rowRectClamped = {
|
const rowRectClamped = {
|
||||||
@@ -6431,9 +6488,12 @@ function _findFreePopupPosition(rowEl, w, h) {
|
|||||||
right: rowRect.right, bottom: rowRect.bottom
|
right: rowRect.right, bottom: rowRect.bottom
|
||||||
};
|
};
|
||||||
const candRect = { left: x, top: y, right: x + w, bottom: y + h };
|
const candRect = { left: x, top: y, right: x + w, bottom: y + h };
|
||||||
if (_rectsOverlap(candRect, rowRectClamped)) continue;
|
// v2026.5.20 : pour les 4 candidats principaux, on refuse de chevaucher
|
||||||
|
// la row ; pour les candidats "grid" de fallback, on l'accepte
|
||||||
|
// (on veut une place à tout prix).
|
||||||
|
if (c.name !== "grid" && _rectsOverlap(candRect, rowRectClamped)) continue;
|
||||||
|
|
||||||
// Test chevauchement avec les popups déjà épinglés
|
// Test chevauchement avec les popups déjà épinglés (coords document)
|
||||||
const docRect = {
|
const docRect = {
|
||||||
left: _viewportToDocumentX(x),
|
left: _viewportToDocumentX(x),
|
||||||
top: _viewportToDocumentY(y),
|
top: _viewportToDocumentY(y),
|
||||||
@@ -6442,13 +6502,14 @@ function _findFreePopupPosition(rowEl, w, h) {
|
|||||||
};
|
};
|
||||||
let overlapsOther = false;
|
let overlapsOther = false;
|
||||||
for (const p of pinnedPopups) {
|
for (const p of pinnedPopups) {
|
||||||
|
// Ne pas comparer avec un popup qui est dans le dock (réduit)
|
||||||
|
if (p.el && p.el.classList && p.el.classList.contains("pinned-popup-reduced")) continue;
|
||||||
if (_rectsOverlap(docRect, p.rect)) {
|
if (_rectsOverlap(docRect, p.rect)) {
|
||||||
overlapsOther = true;
|
overlapsOther = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!overlapsOther) {
|
if (!overlapsOther) {
|
||||||
// Position libre trouvée
|
|
||||||
return {
|
return {
|
||||||
viewportX: x, viewportY: y,
|
viewportX: x, viewportY: y,
|
||||||
docX: docRect.left, docY: docRect.top,
|
docX: docRect.left, docY: docRect.top,
|
||||||
@@ -6456,6 +6517,30 @@ function _findFreePopupPosition(rowEl, w, h) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v2026.5.20 : ultime fallback — accepter de chevaucher mais décaler
|
||||||
|
// un peu par rapport au 1er popup épinglé existant. Évite complètement
|
||||||
|
// le "Pas de place" injuste.
|
||||||
|
if (pinnedPopups.length > 0) {
|
||||||
|
const last = pinnedPopups[pinnedPopups.length - 1];
|
||||||
|
let x = (last.rect.left - (window.scrollX || 0)) + 30;
|
||||||
|
let y = (last.rect.top - (window.scrollY || 0)) + 30;
|
||||||
|
if (x + w > safe.right) x = safe.right - w;
|
||||||
|
if (y + h > safe.bottom) y = safe.bottom - h;
|
||||||
|
if (x < safe.left) x = safe.left;
|
||||||
|
if (y < safe.top) y = safe.top;
|
||||||
|
const docRect = {
|
||||||
|
left: _viewportToDocumentX(x),
|
||||||
|
top: _viewportToDocumentY(y),
|
||||||
|
right: _viewportToDocumentX(x + w),
|
||||||
|
bottom: _viewportToDocumentY(y + h)
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
viewportX: x, viewportY: y,
|
||||||
|
docX: docRect.left, docY: docRect.top,
|
||||||
|
rect: docRect
|
||||||
|
};
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6530,13 +6615,13 @@ function pinTooltip() {
|
|||||||
});
|
});
|
||||||
topbar.appendChild(minBtn);
|
topbar.appendChild(minBtn);
|
||||||
|
|
||||||
// v2026.5.19 : Bouton Actualiser (icône ↻)
|
// v2026.5.19 : Bouton Actualiser (même icône SVG que le tooltip standard)
|
||||||
// Re-fetch la fiche de l'intervention pour mettre à jour les infos (statut,
|
// Re-fetch la fiche de l'intervention pour mettre à jour les infos (statut,
|
||||||
// commentaires, action text) sans recharger le planning entier.
|
// commentaires, action text) sans recharger le planning entier.
|
||||||
const refreshBtn = document.createElement("button");
|
const refreshBtn = document.createElement("button");
|
||||||
refreshBtn.type = "button";
|
refreshBtn.type = "button";
|
||||||
refreshBtn.className = "pinned-popup-btn pinned-popup-refresh";
|
refreshBtn.className = "pinned-popup-btn pinned-popup-refresh";
|
||||||
refreshBtn.innerHTML = "↻";
|
refreshBtn.innerHTML = '<svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M2 8a6 6 0 1 0 1.76-4.24M2 3v3h3" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
||||||
refreshBtn.title = "Actualiser les informations de cette intervention";
|
refreshBtn.title = "Actualiser les informations de cette intervention";
|
||||||
refreshBtn.addEventListener("click", async (e) => {
|
refreshBtn.addEventListener("click", async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -6616,6 +6701,9 @@ function pinTooltip() {
|
|||||||
popup.style.top = pos.docY + "px";
|
popup.style.top = pos.docY + "px";
|
||||||
popup.style.visibility = "visible";
|
popup.style.visibility = "visible";
|
||||||
|
|
||||||
|
// v2026.5.20 : clamper dans la safe area (topbar + dock)
|
||||||
|
_clampPopupInSafeArea(popup);
|
||||||
|
|
||||||
// Enregistrer dans la liste
|
// Enregistrer dans la liste
|
||||||
pinnedPopups.push({ el: popup, iv: iv, rect: pos.rect });
|
pinnedPopups.push({ el: popup, iv: iv, rect: pos.rect });
|
||||||
|
|
||||||
@@ -6885,11 +6973,189 @@ function _reducePinnedPopup(popup) {
|
|||||||
_restorePinnedPopupFromDock(popup);
|
_restorePinnedPopupFromDock(popup);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// v2026.5.20 : mini-menu au survol (Agrandir / Fermer)
|
||||||
|
pill.addEventListener("mouseenter", () => {
|
||||||
|
_showPillHoverMenu(pill, popup);
|
||||||
|
});
|
||||||
|
pill.addEventListener("mouseleave", (e) => {
|
||||||
|
// Le menu peut être sous la souris — on ne ferme pas si on entre dans le menu
|
||||||
|
_schedulePillMenuClose();
|
||||||
|
});
|
||||||
|
|
||||||
dock.appendChild(pill);
|
dock.appendChild(pill);
|
||||||
dock.classList.add("visible");
|
dock.classList.add("visible");
|
||||||
|
|
||||||
// v2026.5.18 : s'assurer qu'il y a un bouton "Fermer tous" si 2+ popups
|
// v2026.5.18 : s'assurer qu'il y a un bouton "Fermer tous" si 2+ popups
|
||||||
_ensureDockCloseAllBtn();
|
_ensureDockCloseAllBtn();
|
||||||
|
|
||||||
|
// v2026.5.20 : le dock qui apparaît peut chevaucher des popups flottants —
|
||||||
|
// les reclamper pour qu'ils restent dans la safe area.
|
||||||
|
_reclampAllFloatingPopups();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* v2026.5.20 : affiche un mini-menu au-dessus d'une pastille dock au survol.
|
||||||
|
* Contient 2 actions : Agrandir, Fermer.
|
||||||
|
*/
|
||||||
|
let _pillMenuCloseTimer = null;
|
||||||
|
function _showPillHoverMenu(pill, popup) {
|
||||||
|
// Annuler une fermeture en cours
|
||||||
|
if (_pillMenuCloseTimer) {
|
||||||
|
clearTimeout(_pillMenuCloseTimer);
|
||||||
|
_pillMenuCloseTimer = null;
|
||||||
|
}
|
||||||
|
// S'il existe déjà un menu pour un autre pill, le fermer
|
||||||
|
const existing = document.getElementById("pill-hover-menu");
|
||||||
|
if (existing) {
|
||||||
|
if (existing._linkedPill === pill) return; // déjà pour ce pill
|
||||||
|
existing.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu = document.createElement("div");
|
||||||
|
menu.id = "pill-hover-menu";
|
||||||
|
menu.className = "pill-hover-menu";
|
||||||
|
menu._linkedPill = pill;
|
||||||
|
menu._linkedPopup = popup;
|
||||||
|
|
||||||
|
const restoreBtn = document.createElement("button");
|
||||||
|
restoreBtn.type = "button";
|
||||||
|
restoreBtn.className = "pill-hover-menu-btn";
|
||||||
|
restoreBtn.innerHTML = '<span class="pill-menu-ico">⬆</span> Agrandir';
|
||||||
|
restoreBtn.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
_hidePillHoverMenu();
|
||||||
|
_restorePinnedPopupFromDock(popup);
|
||||||
|
});
|
||||||
|
menu.appendChild(restoreBtn);
|
||||||
|
|
||||||
|
const closeBtn = document.createElement("button");
|
||||||
|
closeBtn.type = "button";
|
||||||
|
closeBtn.className = "pill-hover-menu-btn pill-hover-menu-close";
|
||||||
|
closeBtn.innerHTML = '<span class="pill-menu-ico">✕</span> Fermer';
|
||||||
|
closeBtn.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
_hidePillHoverMenu();
|
||||||
|
// Retirer le popup de la liste et supprimer le DOM
|
||||||
|
const idx = pinnedPopups.findIndex(p => p.el === popup);
|
||||||
|
if (idx >= 0) pinnedPopups.splice(idx, 1);
|
||||||
|
try { popup.remove(); } catch (err) {}
|
||||||
|
try { pill.remove(); } catch (err) {}
|
||||||
|
const dock = document.getElementById("pinned-popups-dock");
|
||||||
|
if (dock && dock.querySelectorAll(".pinned-popup-dock-pill").length === 0) {
|
||||||
|
dock.classList.remove("visible");
|
||||||
|
const closeAllBtn = document.getElementById("pinned-popups-close-all");
|
||||||
|
if (closeAllBtn) closeAllBtn.remove();
|
||||||
|
_reclampAllFloatingPopups();
|
||||||
|
} else {
|
||||||
|
_ensureDockCloseAllBtn();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
menu.appendChild(closeBtn);
|
||||||
|
|
||||||
|
document.body.appendChild(menu);
|
||||||
|
|
||||||
|
// Positionner au-dessus de la pastille
|
||||||
|
const r = pill.getBoundingClientRect();
|
||||||
|
const menuR = menu.getBoundingClientRect();
|
||||||
|
let left = r.left + (r.width / 2) - (menuR.width / 2);
|
||||||
|
if (left < 4) left = 4;
|
||||||
|
if (left + menuR.width > window.innerWidth - 4) left = window.innerWidth - menuR.width - 4;
|
||||||
|
menu.style.left = left + "px";
|
||||||
|
menu.style.top = (r.top - menuR.height - 8) + "px";
|
||||||
|
|
||||||
|
// Garder ouvert si la souris entre dans le menu
|
||||||
|
menu.addEventListener("mouseenter", () => {
|
||||||
|
if (_pillMenuCloseTimer) {
|
||||||
|
clearTimeout(_pillMenuCloseTimer);
|
||||||
|
_pillMenuCloseTimer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
menu.addEventListener("mouseleave", () => {
|
||||||
|
_schedulePillMenuClose();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _schedulePillMenuClose() {
|
||||||
|
if (_pillMenuCloseTimer) clearTimeout(_pillMenuCloseTimer);
|
||||||
|
_pillMenuCloseTimer = setTimeout(() => {
|
||||||
|
_hidePillHoverMenu();
|
||||||
|
_pillMenuCloseTimer = null;
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _hidePillHoverMenu() {
|
||||||
|
const existing = document.getElementById("pill-hover-menu");
|
||||||
|
if (existing) existing.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* v2026.5.20 : calcule la safe area pour les popups épinglés.
|
||||||
|
* Retourne {top, bottom, left, right} en coords viewport.
|
||||||
|
* - top : hauteur de la topbar (les popups ne doivent pas passer dessous)
|
||||||
|
* - bottom : top du dock si visible, sinon hauteur viewport
|
||||||
|
*/
|
||||||
|
function _getPopupSafeArea() {
|
||||||
|
let topLimit = 4;
|
||||||
|
const topbar = document.querySelector("header.topbar");
|
||||||
|
if (topbar) {
|
||||||
|
const r = topbar.getBoundingClientRect();
|
||||||
|
if (r.bottom > topLimit) topLimit = r.bottom + 4;
|
||||||
|
}
|
||||||
|
let bottomLimit = window.innerHeight - 4;
|
||||||
|
const dock = document.getElementById("pinned-popups-dock");
|
||||||
|
if (dock && dock.classList.contains("visible")) {
|
||||||
|
const r = dock.getBoundingClientRect();
|
||||||
|
if (r.top < bottomLimit) bottomLimit = r.top - 4;
|
||||||
|
}
|
||||||
|
return { top: topLimit, bottom: bottomLimit, left: 4, right: window.innerWidth - 4 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* v2026.5.20 : contraint un popup flottant (en coords document via style.left/top)
|
||||||
|
* dans la safe area. Appelé à l'épinglage, pendant le drag, et quand le dock
|
||||||
|
* apparaît/disparaît.
|
||||||
|
*/
|
||||||
|
function _clampPopupInSafeArea(popup) {
|
||||||
|
if (!popup) return;
|
||||||
|
if (popup.classList.contains("pinned-popup-reduced")) return; // pas clamp si docké
|
||||||
|
const safe = _getPopupSafeArea();
|
||||||
|
const rect = popup.getBoundingClientRect();
|
||||||
|
const w = rect.width || popup.offsetWidth || 280;
|
||||||
|
const h = rect.height || popup.offsetHeight || 200;
|
||||||
|
|
||||||
|
// Les coords viewport actuelles
|
||||||
|
const vLeft = rect.left;
|
||||||
|
const vTop = rect.top;
|
||||||
|
|
||||||
|
// Calcul des coords viewport cibles après clamp
|
||||||
|
let newVLeft = vLeft;
|
||||||
|
let newVTop = vTop;
|
||||||
|
if (newVLeft < safe.left) newVLeft = safe.left;
|
||||||
|
if (newVLeft + w > safe.right) newVLeft = safe.right - w;
|
||||||
|
if (newVLeft < safe.left) newVLeft = safe.left; // si popup plus large que viewport
|
||||||
|
|
||||||
|
if (newVTop < safe.top) newVTop = safe.top;
|
||||||
|
if (newVTop + h > safe.bottom) newVTop = safe.bottom - h;
|
||||||
|
if (newVTop < safe.top) newVTop = safe.top;
|
||||||
|
|
||||||
|
if (newVLeft === vLeft && newVTop === vTop) return; // rien à faire
|
||||||
|
|
||||||
|
// Différence = appliquer au style.left / style.top (qui sont en document coords)
|
||||||
|
const dx = newVLeft - vLeft;
|
||||||
|
const dy = newVTop - vTop;
|
||||||
|
const curLeft = parseFloat(popup.style.left) || 0;
|
||||||
|
const curTop = parseFloat(popup.style.top) || 0;
|
||||||
|
popup.style.left = (curLeft + dx) + "px";
|
||||||
|
popup.style.top = (curTop + dy) + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Réclampe tous les popups flottants (utile après apparition/disparition du dock).
|
||||||
|
*/
|
||||||
|
function _reclampAllFloatingPopups() {
|
||||||
|
document.querySelectorAll(".pinned-popup:not(.pinned-popup-reduced)").forEach(p => {
|
||||||
|
_clampPopupInSafeArea(p);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -7075,12 +7341,12 @@ function _attachPopupDragHandler(popup, dragbar) {
|
|||||||
let newLeft = startLeft + dx;
|
let newLeft = startLeft + dx;
|
||||||
let newTop = startTop + dy;
|
let newTop = startTop + dy;
|
||||||
|
|
||||||
// Clamper dans le document (pas sortir trop à gauche/haut)
|
// v2026.5.20 : clamper dans la safe area (topbar en haut, dock en bas,
|
||||||
if (newLeft < 4) newLeft = 4;
|
// bordures viewport gauche/droite). On calcule en coords viewport puis
|
||||||
if (newTop < 4) newTop = 4;
|
// on applique en coords document.
|
||||||
|
|
||||||
popup.style.left = newLeft + "px";
|
popup.style.left = newLeft + "px";
|
||||||
popup.style.top = newTop + "px";
|
popup.style.top = newTop + "px";
|
||||||
|
_clampPopupInSafeArea(popup);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMouseUp = () => {
|
const onMouseUp = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user