v2026.5.19 — Drag-and-drop des popups épinglés

This commit is contained in:
Quentin Rouiller
2026-04-23 14:34:08 +02:00
parent 1a7393c297
commit ad952ebc55
3 changed files with 196 additions and 50 deletions
+123 -21
View File
@@ -6094,6 +6094,11 @@ let bulleState = {
};
function showTooltip(e, iv, rowEl) {
// v2026.5.19 : pendant qu'un popup épinglé est en cours de drag, on ignore
// les mouseenter sur les cartes — sinon en survolant une carte on déclenche
// l'ouverture d'un nouveau tooltip par-dessus ce qu'on est en train de bouger.
if (state._popupDragging) return;
// v4.1.15 : si la bulle est épinglée sur une autre iv, on NE REMPLACE PAS
// son contenu (l'user veut garder la fiche épinglée même en survolant
// d'autres cartes).
@@ -6485,6 +6490,9 @@ function pinTooltip() {
popup.dataset.ref = iv.ref || "";
popup.dataset.colorKey = (typeof deriveColorKey === "function" ? deriveColorKey(iv) : "autre") || "autre";
// v2026.5.19 : mémoriser aussi la date pour l'afficher sur la pastille dock
popup.dataset.originDate = state.currentDate || "";
// v2026.5.17 : masquer l'icône 📌 du contenu cloné (redondante car le
// popup a sa propre topbar avec le bouton "désépingler" 📍 explicite)
const oldPin = popup.querySelector('.tooltip-pinbtn[data-action="pin"]');
@@ -6522,6 +6530,26 @@ function pinTooltip() {
});
topbar.appendChild(minBtn);
// v2026.5.19 : Bouton Actualiser (icône ↻)
// Re-fetch la fiche de l'intervention pour mettre à jour les infos (statut,
// commentaires, action text) sans recharger le planning entier.
const refreshBtn = document.createElement("button");
refreshBtn.type = "button";
refreshBtn.className = "pinned-popup-btn pinned-popup-refresh";
refreshBtn.innerHTML = "↻";
refreshBtn.title = "Actualiser les informations de cette intervention";
refreshBtn.addEventListener("click", async (e) => {
e.stopPropagation();
if (refreshBtn.classList.contains("spinning")) return;
refreshBtn.classList.add("spinning");
try {
await _refreshPinnedPopupIv(popup, iv);
} finally {
setTimeout(() => refreshBtn.classList.remove("spinning"), 300);
}
});
topbar.appendChild(refreshBtn);
// Bouton Désépingler (icône épingle plantée)
const unpinBtn = document.createElement("button");
unpinBtn.type = "button";
@@ -6614,6 +6642,44 @@ function _closePinnedPopup(el) {
el.remove();
}
/**
* v2026.5.19 : re-fetch les infos d'une intervention et met à jour le contenu
* du popup épinglé correspondant. Utilise fetchAndUpdateIntervention qui fait
* xhr2 + fiche, puis régénère le HTML du tooltip avec buildTooltipHTML.
*/
async function _refreshPinnedPopupIv(popup, iv) {
if (!popup || !iv) return;
try {
// Forcer le refetch : on invalide les flags qui disent "déjà fetché"
iv.xhr2Fetched = false;
iv.xhr2Fetching = false;
iv.ficheFetched = false;
iv.ficheFetching = false;
// Token de refresh actuel (pour que fetchAndUpdateIntervention ne soit
// pas abortée par les checks isRefreshAborted)
const token = (typeof currentRefreshToken !== "undefined") ? currentRefreshToken : 0;
await fetchAndUpdateIntervention(iv, token);
// Régénérer le HTML du tooltip avec les nouvelles infos.
// On doit réinjecter juste le contenu, en gardant la topbar et la dragbar
// (qui ne sont PAS dans le tooltip source, elles sont propres au popup).
const topbar = popup.querySelector(".pinned-popup-topbar");
const dragbar = popup.querySelector(".pinned-popup-dragbar");
const newHtml = buildTooltipHTML(iv);
popup.innerHTML = newHtml;
// Virer aussi la vieille icône 📌 si elle revient dans le rebuild
const oldPin = popup.querySelector('.tooltip-pinbtn[data-action="pin"]');
if (oldPin) oldPin.remove();
// Remettre topbar et dragbar
if (topbar) popup.appendChild(topbar);
if (dragbar) popup.appendChild(dragbar);
} catch (err) {
console.warn("[refresh-popup]", err);
}
}
/**
* Désépinglage "mou" : la popup n'est plus considérée épinglée (elle n'est
* plus dans pinnedPopups, donc le comptage pour Ctrl×2 etc. ignore) mais on
@@ -6686,12 +6752,16 @@ function _softUnpinPopup(el) {
/**
* Passe un popup épinglé en mode Minimisé : on ne montre plus que la ref,
* dans un petit cadre flottant toujours drag-able.
*
* v2026.5.19 : au lieu de masquer tout le contenu via CSS et tenter de
* réafficher la ref (fragile), on crée un élément dédié `.pinned-popup-minref`
* qui contient juste la ref + la date. Cet élément est ajouté/retiré au besoin.
*/
function _minimizePinnedPopup(popup) {
if (!popup) return;
popup.classList.add("pinned-popup-minimized");
// Adapter les boutons topbar : [_] devient [⬆] (agrandir)
// Adapter les boutons topbar : [] devient [⬆] (agrandir)
const minBtn = popup.querySelector(".pinned-popup-minimize");
if (minBtn) {
minBtn.innerHTML = "⬆";
@@ -6705,20 +6775,22 @@ function _minimizePinnedPopup(popup) {
});
}
// Clic sur la ref (dans iv-ref-header) = agrandir aussi
const refEl = popup.querySelector(".iv-ref-header");
if (refEl) {
refEl.style.cursor = "pointer";
refEl.title = "Cliquer pour agrandir";
refEl.addEventListener("click", _onMinimizedRefClick);
// Créer un élément dédié pour afficher la ref en mode minimisé
let minRef = popup.querySelector(".pinned-popup-minref");
if (!minRef) {
minRef = document.createElement("div");
minRef.className = "pinned-popup-minref";
const refText = popup.dataset.ref || "(sans ref)";
minRef.textContent = refText;
minRef.title = "Cliquer pour agrandir";
minRef.addEventListener("click", (e) => {
e.stopPropagation();
_expandPinnedPopup(popup);
});
popup.appendChild(minRef);
}
}
function _onMinimizedRefClick(e) {
const popup = e.currentTarget.closest(".pinned-popup");
if (popup) _expandPinnedPopup(popup);
}
/**
* Repasse un popup minimisé en mode Normal (complet).
*/
@@ -6739,13 +6811,9 @@ function _expandPinnedPopup(popup) {
});
}
// Retirer listener du clic-agrandir sur la ref
const refEl = popup.querySelector(".iv-ref-header");
if (refEl) {
refEl.style.cursor = "";
refEl.title = "";
refEl.removeEventListener("click", _onMinimizedRefClick);
}
// Retirer l'élément ref dédié (s'il existe)
const minRef = popup.querySelector(".pinned-popup-minref");
if (minRef) minRef.remove();
}
/**
@@ -6778,12 +6846,26 @@ function _reducePinnedPopup(popup) {
// Créer la pastille dock
// v2026.5.18 : le fond de la pastille prend la couleur de catégorie
// (via la classe color-XXX déjà utilisée ailleurs dans le CSS)
// v2026.5.19 : pastille à 2 lignes — ref (gras) + date origine (petit)
const pill = document.createElement("button");
pill.type = "button";
pill.className = "pinned-popup-dock-pill color-" + colorKey;
pill.textContent = label;
pill.title = "Cliquer pour agrandir";
const pillRef = document.createElement("span");
pillRef.className = "pinned-popup-dock-pill-ref";
pillRef.textContent = label;
pill.appendChild(pillRef);
// Date d'origine (ex: "21.04")
const originDate = popup.dataset.originDate || "";
if (originDate) {
const pillDate = document.createElement("span");
pillDate.className = "pinned-popup-dock-pill-date";
pillDate.textContent = _formatDateShort(originDate);
pill.appendChild(pillDate);
}
// Mémoriser la position/taille du popup avant de le masquer
const rect = popup.getBoundingClientRect();
popup.dataset.prevLeft = popup.style.left || (rect.left + "px");
@@ -6811,7 +6893,7 @@ function _reducePinnedPopup(popup) {
}
/**
* v2026.5.18 : réduit TOUS les popups épinglés actuellement ouverts (en mode
* v2026.5.19 : réduit TOUS les popups épinglés actuellement ouverts (en mode
* normal ou minimisé) dans la taskbar du bas. Appelé au changement de date.
*/
function _reduceAllPinnedPopups() {
@@ -6821,6 +6903,16 @@ function _reduceAllPinnedPopups() {
});
}
/**
* v2026.5.19 : ISO date (YYYY-MM-DD) format court "DD.MM" pour le dock.
*/
function _formatDateShort(iso) {
if (!iso) return "";
const m = String(iso).match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!m) return iso;
return `${m[3]}.${m[2]}`;
}
/**
* v2026.5.18 : ajoute (ou met à jour) le bouton "Fermer tous" dans le dock
* quand au moins 2 popups épinglés existent (réduits OU affichés).
@@ -6997,6 +7089,11 @@ function _attachPopupDragHandler(popup, dragbar) {
popup.classList.remove("dragging");
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
// v2026.5.19 : retirer la classe body et le flag global après un petit
// délai pour laisser le temps au mouseleave de la carte de se propager
// sans déclencher de tooltip parasite.
document.body.classList.remove("popup-dragging");
setTimeout(() => { state._popupDragging = false; }, 50);
// Mettre à jour le rect mémorisé pour la détection de chevauchement
const entry = pinnedPopups.find(p => p.el === popup);
@@ -7019,6 +7116,11 @@ function _attachPopupDragHandler(popup, dragbar) {
startLeft = parseFloat(popup.style.left) || 0;
startTop = parseFloat(popup.style.top) || 0;
popup.classList.add("dragging");
// v2026.5.19 : flag global pour que showTooltip ignore les mouseenter
// pendant le drag. Ajout d'une classe sur <body> qui désactive les
// pointer-events sur les cartes.
state._popupDragging = true;
document.body.classList.add("popup-dragging");
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
});