barre-apps v0.1.6 : JS vendange (derivation -dev, menus)
This commit is contained in:
+403
-40
@@ -1,5 +1,10 @@
|
|||||||
/* Barre d'apps commune dracodev — comportement (v0.1.0, ACC-0.2 ;
|
/* Barre d'apps commune dracodev — comportement (v0.1.0, ACC-0.2 ;
|
||||||
* v0.1.3 : menu des applications ouvert par la marque ✦).
|
* v0.1.3 : menu des applications ouvert par la marque ✦ ;
|
||||||
|
* v0.1.5 : noms publics par configuration — aucun nom public d'un
|
||||||
|
* environnement particulier n'est codé en dur, voir « URL » ci-dessous ;
|
||||||
|
* v0.1.6 (prochaine publication — VERSION non bumpée dans cette PR,
|
||||||
|
* la publication est séparée) : dérivation -dev par l'hôte courant,
|
||||||
|
* menu « ••• » par défaut et menu compte fonctionnels, voir ci-dessous).
|
||||||
*
|
*
|
||||||
* Aucun gestionnaire inline : un seul écouteur délégué sur le document
|
* Aucun gestionnaire inline : un seul écouteur délégué sur le document
|
||||||
* (patron validé par le prototype — les onclick du HTML généré sont
|
* (patron validé par le prototype — les onclick du HTML généré sont
|
||||||
@@ -10,19 +15,158 @@
|
|||||||
* current: "tiktak", // app courante
|
* current: "tiktak", // app courante
|
||||||
* domain: "pro", // domaine actif
|
* domain: "pro", // domaine actif
|
||||||
* counts: { tiktak: 3, kanbante: 3, alertes: 3, spark: 0 },
|
* counts: { tiktak: 3, kanbante: 3, alertes: 3, spark: 0 },
|
||||||
|
* user: "Bruno", // nom affiché (v0.1.6) ;
|
||||||
|
* // ou { display_name: "Bruno", username: "bruno" }
|
||||||
|
* urls: { // bases des applications
|
||||||
|
* obsigate: "https://og.dracodev.net/",
|
||||||
|
* flowdeck: "https://flowdeck.dracodev.net/",
|
||||||
|
* tiktak: "https://tiktak.dracodev.net/",
|
||||||
|
* kanbante: "https://kanbante.dracodev.net/",
|
||||||
|
* agent: "https://agent.dracodev.net/",
|
||||||
|
* spark: "https://spark.dracodev.net/"
|
||||||
|
* },
|
||||||
* onAction: function (act, el) { … } // l'app relaie les actions
|
* onAction: function (act, el) { … } // l'app relaie les actions
|
||||||
* } // qu'elle ne connaît pas
|
* } // qu'elle ne connaît pas
|
||||||
|
*
|
||||||
|
* URL des applications (contrat dev/prod, v0.1.5 puis v0.1.6) —
|
||||||
|
* résolution, dans l'ordre STRICT, pour chaque app :
|
||||||
|
* 1. DRACO_BAR_CONFIG.urls.<app>, injectée par l'app hôte (rendu
|
||||||
|
* serveur ou client) depuis SA configuration d'environnement —
|
||||||
|
* explicite, donc prioritaire, même sur un hôte -dev ;
|
||||||
|
* 2. (v0.1.6) SI l'hôte courant est un hôte -dev dracodev.net
|
||||||
|
* (première étiquette se terminant par -dev : tiktak-dev,
|
||||||
|
* kanbante-dev, og-dev, flowdeck-dev, agent-dev…), l'URL sœur
|
||||||
|
* dérivée avec -dev (voir DEV_URLS). C'est le correctif du bug
|
||||||
|
* constaté sur tiktak-dev / kanbante-dev, où les liens suivaient
|
||||||
|
* les noms SANS -dev faute de configuration explicite ;
|
||||||
|
* 3. le href déjà rendu dans le fragment HTML, c.-à-d. le jeton
|
||||||
|
* {{URL_<APP>}} remplacé côté serveur par l'app hôte ;
|
||||||
|
* 4. le défaut officiel ci-dessous (production, noms SANS suffixe).
|
||||||
|
* Un jeton {{URL_…}} non remplacé n'est jamais une URL : il est ignoré
|
||||||
|
* partout (config injectée comme href rendu) et ne peut pas être exposé.
|
||||||
|
* Cas spark (v0.1.6) : AUCUNE dérivation inventée — aucun hôte
|
||||||
|
* spark-dev connu ; sur un hôte -dev, spark retombe sur l'étape 3
|
||||||
|
* (href rendu) puis 4 (défaut prod), sauf config explicite (étape 1).
|
||||||
|
* Les clés agent et spark ne correspondent à aucun onglet de la barre :
|
||||||
|
* elles sont résolues et exposées via DRACO_BAR.urls pour le panneau
|
||||||
|
* Spark (panneau-agent), qui suit le même contrat d'injection.
|
||||||
* Actions émises (data-act) : rail, palette, domaine (data-dom),
|
* Actions émises (data-act) : rail, palette, domaine (data-dom),
|
||||||
* alertes, spark, menu, rappel-entete. L'app intégratrice décide de
|
* alertes, spark, rappel-entete. L'app intégratrice décide de chacune
|
||||||
* chacune (ouvrir la palette, le panneau Spark, changer de domaine…).
|
* (ouvrir la palette, le panneau Spark, changer de domaine…).
|
||||||
* Action INTERNE (jamais relayée) : apps — la marque ✦ ouvre le menu
|
* Actions INTERNES (jamais relayées comme avant) : apps — la marque ✦
|
||||||
* des applications, construit depuis les onglets ; c'est le moyen de
|
* ouvre le menu des applications, construit depuis les onglets ;
|
||||||
* changer d'app en mobile, où les onglets sont repliés (v0.1.2).
|
* c'est le moyen de changer d'app en mobile, où les onglets sont
|
||||||
|
* repliés (v0.1.2). Menu « ••• » et compte : voir ci-dessous.
|
||||||
|
*
|
||||||
|
* Menu « ••• » (v0.1.6) — audit des hôtes : ObsiGate est le seul hôte
|
||||||
|
* connu à gérer l'action "menu" via onAction (il déclenche son propre
|
||||||
|
* bouton d'en-tête #header-menu-btn) ; les autres hôtes ne la gèrent
|
||||||
|
* pas et le bouton restait mort (relais vers un onAction absent, puis
|
||||||
|
* événement draco:bar sans écouteur). Comportement désormais :
|
||||||
|
* - sans onAction : un menu par défaut FONCTIONNEL s'ouvre — accès à
|
||||||
|
* la console agent (URL agent résolue, donc dérivée -dev sur un
|
||||||
|
* hôte -dev) et aux quatre applications (URL résolues) ; fermeture
|
||||||
|
* par clic extérieur, Échap ou choix d'un lien ;
|
||||||
|
* - avec onAction : l'hôte garde la main (comportement historique,
|
||||||
|
* zéro régression pour ObsiGate) ; il peut demander explicitement
|
||||||
|
* le menu par défaut en retournant false pour l'action "menu".
|
||||||
|
* Menu compte (v0.1.6) — l'avatar, auparavant un <span> mort affichant
|
||||||
|
* un « B » codé en dur dans le fragment, est un vrai <button>
|
||||||
|
* (data-act="compte"). Son initiale vient de cfg.user ou, à défaut, de
|
||||||
|
* la première lettre du title {{UTILISATEUR}} remplacé ; sans nom connu,
|
||||||
|
* le neutre ✦ est conservé (jamais de lettre codée en dur). Le clic
|
||||||
|
* ouvre un petit menu affichant le nom de l'utilisateur ; les items
|
||||||
|
* Profil / Déconnexion sont relayés à l'hôte via onAction ou, à défaut,
|
||||||
|
* via l'événement draco:bar (détail { act: "profil" | "deconnexion" }),
|
||||||
|
* mais le menu affiche l'identité même sans gestionnaire hôte.
|
||||||
|
* Accessibilité : vrais <button>, aria-haspopup / aria-expanded,
|
||||||
|
* role="menu" / "menuitem", fermeture par Échap et clic extérieur.
|
||||||
*/
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
var cfg = window.DRACO_BAR_CONFIG || {};
|
var cfg = window.DRACO_BAR_CONFIG || {};
|
||||||
|
|
||||||
|
/* Défauts OFFICIELS (production) — les seuls noms publics présents
|
||||||
|
* dans ce fichier, documentés comme défauts (contrat dev/prod). */
|
||||||
|
var DEFAULT_URLS = {
|
||||||
|
obsigate: "https://og.dracodev.net/",
|
||||||
|
flowdeck: "https://flowdeck.dracodev.net/",
|
||||||
|
tiktak: "https://tiktak.dracodev.net/",
|
||||||
|
kanbante: "https://kanbante.dracodev.net/",
|
||||||
|
agent: "https://agent.dracodev.net/",
|
||||||
|
spark: "https://spark.dracodev.net/"
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Dérivation -dev (v0.1.6) — appliquée UNIQUEMENT quand l'hôte courant
|
||||||
|
* est lui-même un hôte -dev dracodev.net (voir isDevHost). spark est
|
||||||
|
* volontairement absent : aucun hôte spark-dev connu, aucune URL
|
||||||
|
* inventée ; spark retombe sur href rendu puis défaut prod. */
|
||||||
|
var DEV_URLS = {
|
||||||
|
obsigate: "https://og-dev.dracodev.net/",
|
||||||
|
flowdeck: "https://flowdeck-dev.dracodev.net/",
|
||||||
|
tiktak: "https://tiktak-dev.dracodev.net/",
|
||||||
|
kanbante: "https://kanbante-dev.dracodev.net/",
|
||||||
|
agent: "https://agent-dev.dracodev.net/"
|
||||||
|
};
|
||||||
|
|
||||||
|
var APP_LABELS = {
|
||||||
|
obsigate: "ObsiGate", flowdeck: "Flowdeck",
|
||||||
|
tiktak: "TikTak", kanbante: "Kanbante"
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Vrai si hostname est un hôte -dev dracodev.net : première étiquette
|
||||||
|
* se terminant par -dev ET domaine dracodev.net (un suffixe -dev sur
|
||||||
|
* un autre domaine, ou un hôte piège du type x-dev.dracodev.net.evil,
|
||||||
|
* ne déclenche rien). Insensible à la casse. */
|
||||||
|
function isDevHost(hostname) {
|
||||||
|
if (!hostname) return false;
|
||||||
|
var h = String(hostname).toLowerCase();
|
||||||
|
if (h.slice(-13) !== ".dracodev.net") return false;
|
||||||
|
var first = h.split(".")[0];
|
||||||
|
return first.length > 4 && first.slice(-4) === "-dev";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Un jeton {{…}} non remplacé n'est pas une URL : l'ignorer. */
|
||||||
|
function cleanUrl(u) {
|
||||||
|
if (!u || typeof u !== "string") return null;
|
||||||
|
if (u.indexOf("{{") !== -1) return null;
|
||||||
|
if (!u.replace(/\s/g, "")) return null;
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
|
||||||
|
function withSlash(u) {
|
||||||
|
return u.charAt(u.length - 1) !== "/" ? u + "/" : u;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveUrls() {
|
||||||
|
var host = "";
|
||||||
|
try { host = (window.location && window.location.hostname) || ""; } catch (e) { host = ""; }
|
||||||
|
var dev = isDevHost(host);
|
||||||
|
var urls = {};
|
||||||
|
Object.keys(DEFAULT_URLS).forEach(function (app) {
|
||||||
|
/* 1. Config explicite de l'hôte — prioritaire, même sur -dev. */
|
||||||
|
var u = cleanUrl(cfg.urls && cfg.urls[app]);
|
||||||
|
/* 2. Dérivation -dev par l'hôte courant (spark : pas de dérivée). */
|
||||||
|
if (!u && dev) u = DEV_URLS[app] || null;
|
||||||
|
/* 3. href rendu dans le fragment (jeton remplacé côté serveur). */
|
||||||
|
if (!u) {
|
||||||
|
var tab = document.querySelector('.draco-apptab[data-app="' + app + '"]');
|
||||||
|
var href = tab ? tab.getAttribute("href") : null;
|
||||||
|
u = cleanUrl(href);
|
||||||
|
}
|
||||||
|
/* 4. Défaut officiel (production). */
|
||||||
|
if (!u) u = DEFAULT_URLS[app];
|
||||||
|
urls[app] = withSlash(u);
|
||||||
|
});
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
function applyUrls(urls) {
|
||||||
|
document.querySelectorAll(".draco-apptab, .draco-apps-item").forEach(function (t) {
|
||||||
|
if (t.dataset.app && urls[t.dataset.app]) t.setAttribute("href", urls[t.dataset.app]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var resolvedUrls = resolveUrls();
|
||||||
|
|
||||||
function setCounts(counts) {
|
function setCounts(counts) {
|
||||||
Object.keys(counts || {}).forEach(function (k) {
|
Object.keys(counts || {}).forEach(function (k) {
|
||||||
/* Toutes les occurrences : l'onglet ET sa ligne dans le menu. */
|
/* Toutes les occurrences : l'onglet ET sa ligne dans le menu. */
|
||||||
@@ -43,26 +187,122 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Menu des applications (v0.1.3) ──────────────────────────────
|
/* ── Identité / avatar (v0.1.6) ───────────────────────────────────
|
||||||
* Construit depuis les onglets présents dans la barre : mêmes
|
* Nom : cfg.user (chaîne, ou objet display_name / name / username),
|
||||||
* liens, mêmes icônes, mêmes badges. Ouvert par la marque ✦
|
* sinon le title de l'avatar quand le jeton {{UTILISATEUR}} a bien été
|
||||||
* (data-act="apps"), fermé par un clic extérieur, Échap, ou le
|
* remplacé côté serveur. L'initiale en découle ; sans nom, le neutre
|
||||||
* choix d'une application (la navigation suit son cours). */
|
* ✦ du fragment est conservé — jamais de lettre codée en dur. */
|
||||||
|
function userName() {
|
||||||
|
var u = cfg.user;
|
||||||
|
if (typeof u === "string" && u.replace(/\s/g, "")) return u.trim();
|
||||||
|
if (u && typeof u === "object") {
|
||||||
|
var n = u.display_name || u.name || u.username;
|
||||||
|
if (n && String(n).replace(/\s/g, "")) return String(n).trim();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function titleName() {
|
||||||
|
var av = document.querySelector(".draco-avatar");
|
||||||
|
var t = av ? av.getAttribute("title") : null;
|
||||||
|
if (t && t.indexOf("{{") === -1 && t.replace(/\s/g, "")) return t.trim();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function displayName() { return userName() || titleName(); }
|
||||||
|
function syncAvatar() {
|
||||||
|
var av = document.querySelector(".draco-avatar");
|
||||||
|
if (!av) return;
|
||||||
|
/* Compatibilité avec un fragment vendangé antérieur (avatar <span>
|
||||||
|
* sans data-act) : le rendre déclencheur du menu compte. */
|
||||||
|
if (!av.getAttribute("data-act")) av.setAttribute("data-act", "compte");
|
||||||
|
if (!av.getAttribute("aria-haspopup")) av.setAttribute("aria-haspopup", "menu");
|
||||||
|
var name = displayName();
|
||||||
|
if (name) {
|
||||||
|
av.textContent = name.charAt(0).toUpperCase();
|
||||||
|
av.setAttribute("title", name);
|
||||||
|
av.setAttribute("aria-label", "Compte : " + name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Menus déroulants (apps, •••, compte) ──────────────────────────
|
||||||
|
* Un seul menu ouvert à la fois ; fermeture par clic extérieur,
|
||||||
|
* Échap, ou choix d'un item. aria-expanded tenu à jour sur chaque
|
||||||
|
* déclencheur. Les href des menus sont (re)synchronisés avec
|
||||||
|
* resolvedUrls à chaque ouverture, pour que setUrls et la dérivation
|
||||||
|
* -dev s'y reflètent même si le menu a été construit avant. */
|
||||||
var bar = document.querySelector(".draco-bar");
|
var bar = document.querySelector(".draco-bar");
|
||||||
var brand = bar ? bar.querySelector(".draco-brand") : null;
|
var brand = bar ? bar.querySelector(".draco-brand") : null;
|
||||||
var menu = null;
|
var moreBtn = bar ? bar.querySelector('[data-act="menu"]') : null;
|
||||||
|
var avatarBtn = document.querySelector(".draco-avatar");
|
||||||
|
var appsMenu = null, moreMenu = null, compteMenu = null;
|
||||||
|
|
||||||
function buildMenu() {
|
function setExpanded(btn, open) {
|
||||||
if (!bar || !brand || menu) return;
|
if (btn) btn.setAttribute("aria-expanded", open ? "true" : "false");
|
||||||
menu = document.createElement("div");
|
}
|
||||||
menu.className = "draco-apps-menu";
|
function refreshMenuHrefs(menu) {
|
||||||
menu.hidden = true;
|
if (!menu) return;
|
||||||
menu.setAttribute("role", "menu");
|
menu.querySelectorAll(".draco-apps-item").forEach(function (a) {
|
||||||
menu.setAttribute("aria-label", "Applications");
|
if (a.dataset.app && resolvedUrls[a.dataset.app]) a.setAttribute("href", resolvedUrls[a.dataset.app]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function togglePopup(menu, btn, force) {
|
||||||
|
if (!menu) return;
|
||||||
|
var open = typeof force === "boolean" ? force : menu.hidden;
|
||||||
|
menu.hidden = !open;
|
||||||
|
setExpanded(btn, open);
|
||||||
|
}
|
||||||
|
function closeAllPopups() {
|
||||||
|
if (appsMenu) togglePopup(appsMenu, brand, false);
|
||||||
|
if (moreMenu) togglePopup(moreMenu, moreBtn, false);
|
||||||
|
if (compteMenu) togglePopup(compteMenu, avatarBtn, false);
|
||||||
|
}
|
||||||
|
function tabLabel(app) {
|
||||||
|
var tab = document.querySelector('.draco-apptab[data-app="' + app + '"]');
|
||||||
|
var fort = tab ? tab.querySelector(".draco-apptab-txt strong") : null;
|
||||||
|
return fort ? fort.textContent : (APP_LABELS[app] || app);
|
||||||
|
}
|
||||||
|
function tabRole(app) {
|
||||||
|
var tab = document.querySelector('.draco-apptab[data-app="' + app + '"]');
|
||||||
|
var petit = tab ? tab.querySelector(".draco-apptab-txt small") : null;
|
||||||
|
return petit ? petit.textContent : "";
|
||||||
|
}
|
||||||
|
function menuLink(app, nom, role) {
|
||||||
|
var a = document.createElement("a");
|
||||||
|
a.className = "draco-apps-item draco-menu-item";
|
||||||
|
a.href = resolvedUrls[app] || "#";
|
||||||
|
a.dataset.app = app;
|
||||||
|
a.setAttribute("role", "menuitem");
|
||||||
|
var txt = document.createElement("span");
|
||||||
|
txt.className = "draco-apps-item-txt";
|
||||||
|
var n = document.createElement("span");
|
||||||
|
n.className = "draco-apps-item-nom";
|
||||||
|
n.textContent = nom;
|
||||||
|
txt.appendChild(n);
|
||||||
|
if (role) {
|
||||||
|
var r = document.createElement("span");
|
||||||
|
r.className = "draco-apps-item-role";
|
||||||
|
r.textContent = role;
|
||||||
|
txt.appendChild(r);
|
||||||
|
}
|
||||||
|
a.appendChild(txt);
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Menu des applications (v0.1.3) — construit depuis les onglets
|
||||||
|
* présents dans la barre : mêmes liens, mêmes icônes, mêmes badges.
|
||||||
|
* Ouvert par la marque ✦ (data-act="apps"), fermé par un clic
|
||||||
|
* extérieur, Échap, ou le choix d'une application (la navigation
|
||||||
|
* suit son cours). */
|
||||||
|
function buildAppsMenu() {
|
||||||
|
if (!bar || !brand || appsMenu) return;
|
||||||
|
appsMenu = document.createElement("div");
|
||||||
|
appsMenu.className = "draco-apps-menu";
|
||||||
|
appsMenu.hidden = true;
|
||||||
|
appsMenu.setAttribute("role", "menu");
|
||||||
|
appsMenu.setAttribute("aria-label", "Applications");
|
||||||
document.querySelectorAll(".draco-apptab").forEach(function (t) {
|
document.querySelectorAll(".draco-apptab").forEach(function (t) {
|
||||||
var a = document.createElement("a");
|
var a = document.createElement("a");
|
||||||
a.className = "draco-apps-item";
|
a.className = "draco-apps-item";
|
||||||
a.href = t.getAttribute("href");
|
a.href = resolvedUrls[t.dataset.app] || t.getAttribute("href");
|
||||||
a.dataset.app = t.dataset.app;
|
a.dataset.app = t.dataset.app;
|
||||||
a.setAttribute("role", "menuitem");
|
a.setAttribute("role", "menuitem");
|
||||||
var ico = t.querySelector(".draco-apptab-ico");
|
var ico = t.querySelector(".draco-apptab-ico");
|
||||||
@@ -84,44 +324,167 @@
|
|||||||
a.appendChild(txt);
|
a.appendChild(txt);
|
||||||
var badge = t.querySelector(".draco-badge");
|
var badge = t.querySelector(".draco-badge");
|
||||||
if (badge) a.appendChild(badge.cloneNode(true));
|
if (badge) a.appendChild(badge.cloneNode(true));
|
||||||
menu.appendChild(a);
|
appsMenu.appendChild(a);
|
||||||
});
|
});
|
||||||
bar.appendChild(menu);
|
bar.appendChild(appsMenu);
|
||||||
|
}
|
||||||
|
function toggleAppsMenu(force) {
|
||||||
|
buildAppsMenu();
|
||||||
|
if (!appsMenu) return;
|
||||||
|
if (force !== false) refreshMenuHrefs(appsMenu);
|
||||||
|
togglePopup(appsMenu, brand, force);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMenu(force) {
|
/* Menu « ••• » par défaut (v0.1.6) — console agent + applications.
|
||||||
buildMenu();
|
* Construit depuis les URL résolues (donc dérivées -dev sur un hôte
|
||||||
if (!menu) return;
|
* -dev) ; l'hôte qui gère "menu" via onAction garde la main (voir
|
||||||
var open = typeof force === "boolean" ? force : menu.hidden;
|
* l'en-tête) et ne voit jamais ce menu, sauf s'il retourne false. */
|
||||||
menu.hidden = !open;
|
function buildMoreMenu() {
|
||||||
if (brand) brand.setAttribute("aria-expanded", open ? "true" : "false");
|
if (!bar || !moreBtn || moreMenu) return;
|
||||||
|
moreMenu = document.createElement("div");
|
||||||
|
moreMenu.className = "draco-more-menu";
|
||||||
|
moreMenu.hidden = true;
|
||||||
|
moreMenu.setAttribute("role", "menu");
|
||||||
|
moreMenu.setAttribute("aria-label", "Plus d'actions");
|
||||||
|
moreMenu.appendChild(menuLink("agent", "Console agent", "Agent commun"));
|
||||||
|
var sep = document.createElement("div");
|
||||||
|
sep.className = "draco-menu-sep";
|
||||||
|
sep.setAttribute("aria-hidden", "true");
|
||||||
|
moreMenu.appendChild(sep);
|
||||||
|
["obsigate", "flowdeck", "tiktak", "kanbante"].forEach(function (app) {
|
||||||
|
moreMenu.appendChild(menuLink(app, tabLabel(app), tabRole(app)));
|
||||||
|
});
|
||||||
|
bar.appendChild(moreMenu);
|
||||||
|
}
|
||||||
|
function toggleMoreMenu(force) {
|
||||||
|
buildMoreMenu();
|
||||||
|
if (!moreMenu) return;
|
||||||
|
if (force !== false) refreshMenuHrefs(moreMenu);
|
||||||
|
togglePopup(moreMenu, moreBtn, force);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Menu compte (v0.1.6) — en-tête d'identité + Profil / Déconnexion
|
||||||
|
* relayés à l'hôte. L'en-tête est rafraîchi à chaque ouverture, pour
|
||||||
|
* refléter un title posé après coup par l'hôte (ex. ObsiGate, qui
|
||||||
|
* remplit l'avatar depuis /api/auth/me après chargement). */
|
||||||
|
function buildCompteMenu() {
|
||||||
|
if (!bar || !avatarBtn || compteMenu) return;
|
||||||
|
compteMenu = document.createElement("div");
|
||||||
|
compteMenu.className = "draco-compte-menu";
|
||||||
|
compteMenu.hidden = true;
|
||||||
|
compteMenu.setAttribute("role", "menu");
|
||||||
|
compteMenu.setAttribute("aria-label", "Compte");
|
||||||
|
var head = document.createElement("div");
|
||||||
|
head.className = "draco-compte-nom";
|
||||||
|
compteMenu.appendChild(head);
|
||||||
|
[["profil", "Profil"], ["deconnexion", "Déconnexion"]].forEach(function (pair) {
|
||||||
|
var b = document.createElement("button");
|
||||||
|
b.type = "button";
|
||||||
|
b.className = "draco-menu-item";
|
||||||
|
b.setAttribute("data-compte-act", pair[0]);
|
||||||
|
b.setAttribute("role", "menuitem");
|
||||||
|
b.textContent = pair[1];
|
||||||
|
compteMenu.appendChild(b);
|
||||||
|
});
|
||||||
|
bar.appendChild(compteMenu);
|
||||||
|
}
|
||||||
|
function refreshCompteMenu() {
|
||||||
|
if (!compteMenu) return;
|
||||||
|
var head = compteMenu.querySelector(".draco-compte-nom");
|
||||||
|
var name = displayName();
|
||||||
|
if (head) head.textContent = name || "Compte";
|
||||||
|
}
|
||||||
|
function toggleCompteMenu(force) {
|
||||||
|
buildCompteMenu();
|
||||||
|
if (!compteMenu) return;
|
||||||
|
if (force !== false) { syncAvatar(); refreshCompteMenu(); }
|
||||||
|
togglePopup(compteMenu, avatarBtn, force);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Relais d'une action vers l'hôte : onAction si présent, sinon
|
||||||
|
* événement draco:bar (contrat historique des actions relayées). */
|
||||||
|
function relay(act, el, ev) {
|
||||||
|
if (typeof cfg.onAction === "function") cfg.onAction(act, el, ev);
|
||||||
|
else document.dispatchEvent(new CustomEvent("draco:bar", { detail: { act: act, el: el } }));
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("click", function (ev) {
|
document.addEventListener("click", function (ev) {
|
||||||
var el = ev.target.closest("[data-act]");
|
/* Items du menu compte : relais + fermeture, jamais de navigation. */
|
||||||
if (el && el.dataset.act === "apps") {
|
var compteItem = ev.target.closest ? ev.target.closest("[data-compte-act]") : null;
|
||||||
ev.preventDefault();
|
if (compteItem) {
|
||||||
toggleMenu();
|
relay(compteItem.getAttribute("data-compte-act"), compteItem, ev);
|
||||||
|
toggleCompteMenu(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (menu && !menu.hidden && !ev.target.closest(".draco-apps-menu") && !ev.target.closest(".draco-brand")) {
|
var el = ev.target.closest ? ev.target.closest("[data-act]") : null;
|
||||||
toggleMenu(false);
|
if (el && el.dataset.act === "apps") {
|
||||||
|
ev.preventDefault();
|
||||||
|
if (moreMenu) toggleMoreMenu(false);
|
||||||
|
if (compteMenu) toggleCompteMenu(false);
|
||||||
|
toggleAppsMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (el && el.dataset.act === "menu") {
|
||||||
|
if (appsMenu) toggleAppsMenu(false);
|
||||||
|
if (compteMenu) toggleCompteMenu(false);
|
||||||
|
/* Hôte prioritaire : il garde la main, sauf s'il demande
|
||||||
|
* explicitement le menu par défaut en retournant false.
|
||||||
|
* Sans onAction, le menu par défaut s'ouvre (il ne restait
|
||||||
|
* auparavant qu'un événement draco:bar sans écouteur). */
|
||||||
|
if (typeof cfg.onAction === "function") {
|
||||||
|
var handled = cfg.onAction("menu", el, ev);
|
||||||
|
if (handled === false) toggleMoreMenu();
|
||||||
|
} else {
|
||||||
|
toggleMoreMenu();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (el && el.dataset.act === "compte") {
|
||||||
|
if (appsMenu) toggleAppsMenu(false);
|
||||||
|
if (moreMenu) toggleMoreMenu(false);
|
||||||
|
toggleCompteMenu();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* Clic extérieur : ferme tout menu ouvert (clic dans un menu ou
|
||||||
|
* sur son déclencheur déjà traité ci-dessus). */
|
||||||
|
var inPopup = ev.target.closest ? (ev.target.closest(".draco-apps-menu") ||
|
||||||
|
ev.target.closest(".draco-more-menu") || ev.target.closest(".draco-compte-menu")) : null;
|
||||||
|
if (!inPopup && ((appsMenu && !appsMenu.hidden) || (moreMenu && !moreMenu.hidden) || (compteMenu && !compteMenu.hidden))) {
|
||||||
|
closeAllPopups();
|
||||||
}
|
}
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
var act = el.dataset.act;
|
var act = el.dataset.act;
|
||||||
if (act === "domaine") setDomain(el.dataset.dom);
|
if (act === "domaine") setDomain(el.dataset.dom);
|
||||||
if (typeof cfg.onAction === "function") cfg.onAction(act, el, ev);
|
relay(act, el, ev);
|
||||||
else document.dispatchEvent(new CustomEvent("draco:bar", { detail: { act: act, el: el } }));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("keydown", function (ev) {
|
document.addEventListener("keydown", function (ev) {
|
||||||
if (ev.key === "Escape") toggleMenu(false);
|
if (ev.key === "Escape") closeAllPopups();
|
||||||
});
|
});
|
||||||
|
|
||||||
/* API publique : l'app met les compteurs à jour sans recharger. */
|
/* API publique : l'app met les compteurs à jour sans recharger.
|
||||||
window.DRACO_BAR = { setCounts: setCounts, setCurrent: setCurrent, setDomain: setDomain };
|
* setUrls(urls) fusionne de nouvelles bases (même contrat que
|
||||||
|
* DRACO_BAR_CONFIG.urls — donc prioritaires sur la dérivation -dev)
|
||||||
|
* et les ré-applique aux onglets et aux menus ; DRACO_BAR.urls expose
|
||||||
|
* les bases résolues (défauts inclus), pour le panneau Spark et tout
|
||||||
|
* autre consommateur de l'hôte. */
|
||||||
|
window.DRACO_BAR = {
|
||||||
|
setCounts: setCounts, setCurrent: setCurrent, setDomain: setDomain,
|
||||||
|
urls: resolvedUrls,
|
||||||
|
setUrls: function (urls) {
|
||||||
|
cfg.urls = Object.assign({}, cfg.urls, urls || {});
|
||||||
|
resolvedUrls = resolveUrls();
|
||||||
|
window.DRACO_BAR.urls = resolvedUrls;
|
||||||
|
applyUrls(resolvedUrls);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
buildMenu();
|
applyUrls(resolvedUrls);
|
||||||
|
buildAppsMenu();
|
||||||
|
buildMoreMenu();
|
||||||
|
buildCompteMenu();
|
||||||
|
syncAvatar();
|
||||||
|
refreshCompteMenu();
|
||||||
if (cfg.current) setCurrent(cfg.current);
|
if (cfg.current) setCurrent(cfg.current);
|
||||||
if (cfg.domain) setDomain(cfg.domain);
|
if (cfg.domain) setDomain(cfg.domain);
|
||||||
if (cfg.counts) setCounts(cfg.counts);
|
if (cfg.counts) setCounts(cfg.counts);
|
||||||
|
|||||||
Reference in New Issue
Block a user