3 familles, 13 fichiers (+153/−167) :
1. no-empty ×70 = TOUS des `catch (x) {}` vides → `catch { /* volontaire */ }`
(binding optionnel ES2019 + commentaire : passe no-empty ET
no-unused-vars, zéro changement de comportement).
2. no-unused-vars ×171 :
- bindings de catch inutilisés retirés (e/err/ex/e2/e3)
- 24 lignes mortes déterministes, chaque suppression validée par assert
sur le texte exact (`var self = this` ×8, `var lang`, `var acc`,
`var today`, `var path/restored/files/resolved/items/clickEl`,
`uid()`/`propName()` sans un seul appel, `.then` + `resolved++`
compteurs jamais lus)
- `/* exported */` sur les 10 fonctions appelées depuis les attributs
HTML des templates (vérifiées par grep : 1 template chacune) :
setActiveTab/kanbanBoard/filterSystem/sortSystem/newIssueForm/
showNewIssue, importWizard, libraryPage, workspacesPage, settingsInit
3. no-undef ×44 = vrais globaux déclarés dans eslint.config.mjs
(getSvgIcon = script inline de base.html, TextDecoder = API navigateur,
Prism = CDN) + 2 vrais correctifs :
- settings.js : `typeof toast === 'function'` = guard TOUJOURS faux
(pas de toast global) → les toasts timezone/SAML ne s'affichaient
jamais → `window.showToast` (2 sites)
- local_workspace.js : `_wsInitData = window._wsInitData`
(auto-affectation sans effet, global implicite) supprimé
eslint static/js : **0 erreur / 0 warning** (285 → 0) · node --check vert
sur tous les fichiers · suite **1089/1089** · ruff OK · docs à jour
166 lines
6.9 KiB
JavaScript
166 lines
6.9 KiB
JavaScript
|
|
/* ═════════════════════════════════════════════════════════════════════
|
|
fdCtx — store Alpine UNIQUE du menu contextuel partagé.
|
|
Enregistré dans 'alpine:init' avec garde d'exécution : que la page
|
|
inclue ce partial dans base.html ou directement, le js ne s'exécute
|
|
qu'une seule fois. Chaque page, à l'ouverture du menu, positionne :
|
|
fdCtx.openMenu(evt, node, pageHash, handlers)
|
|
─────────────────────────────────────────────────────────────────── */
|
|
(function () {
|
|
if (window.__fdCtxMenuRegistered) return;
|
|
window.__fdCtxMenuRegistered = true;
|
|
|
|
// Sur un chargement complet de page, ce script inline s'exécute AVANT
|
|
// alpine.min.js (defer) → Alpine n'existe pas encore : on attend 'alpine:init'.
|
|
// Sur une navigation partielle (fdNavigate → htmx), Alpine est déjà démarré et
|
|
// 'alpine:init' ne sera plus jamais émis → on enregistre le store tout de suite,
|
|
// sinon le menu contextuel reste mort jusqu'au prochain chargement complet.
|
|
function registerFdCtx() {
|
|
if (!window.Alpine) return;
|
|
if (window.Alpine.__fdCtxStore) return;
|
|
window.Alpine.__fdCtxStore = true;
|
|
|
|
Alpine.store('fdCtx', {
|
|
open: false, x: 0, y: 0, node: null, page: null,
|
|
maxH: 0, _cx: 0, _cy: 0, _ro: null,
|
|
handlers: {},
|
|
/* Tags (workspace) */
|
|
availTags: [], tagAdding: false, tagExistingOpen: false,
|
|
newTagColor: '#787774',
|
|
tagColors: ['#787774','#E03E3E','#D9730D','#DFAB01','#0F7B6C','#0B6E99','#6940A5','#AD1A72','#E16259','#D4A72C','#448361','#337EA9','#9065B0','#C94D8B'],
|
|
/* Icon picker */
|
|
iconOpen: false,
|
|
iconChoices: ['📄','📝','📕','📁','⭐','🔖','📌','✅','💡','🔥','🚀','🎯','📊','🗓️','🔗','🧩','📎','🎨','🐛','⚙️','❤️','👍','✨','🏷️','🗒️','📚'],
|
|
|
|
/* Positionne le menu à l'écran et expose les handlers de la page.
|
|
⚠️ Nom : openMenu (et PAS open) — « open » est réservé au booléen
|
|
d'état x-show. Une collision ici rendrait le menu PERMANENT. */
|
|
openMenu(ev, node, pageHash, handlers) {
|
|
handlers = handlers || {};
|
|
this.node = node;
|
|
this.page = pageHash;
|
|
this.handlers = handlers;
|
|
this.tagAdding = false;
|
|
this.tagExistingOpen = false;
|
|
this.iconOpen = false;
|
|
this.newTagColor = (node && node.tags && node.tags[0] && node.tags[0].color) || '#787774';
|
|
var cx = (ev && ev.clientX) || 0;
|
|
var cy = (ev && ev.clientY) || 0;
|
|
this._cx = cx;
|
|
this._cy = cy;
|
|
this.x = Math.max(8, Math.min(cx, window.innerWidth - 240));
|
|
this.y = Math.max(8, Math.min(cy, window.innerHeight - 48));
|
|
this.open = true;
|
|
var self = this;
|
|
/* Mesure la taille réelle du menu (une fois rendu) puis le replace
|
|
pour qu'il reste TOUJOURS entièrement visible dans le viewport.
|
|
Un ResizeObserver relance le placement à chaque fois que le menu
|
|
change de taille (ouverture d'un sous-menu « tag », icônes, …),
|
|
sinon il grandissait vers le bas et sortait de l'écran. */
|
|
var after = function () {
|
|
self._place();
|
|
var el = document.querySelector('.fd-ctx-menu');
|
|
if (el && window.ResizeObserver) {
|
|
if (self._ro) { try { self._ro.disconnect(); } catch { /* volontaire */ } }
|
|
self._ro = new ResizeObserver(function () { self._place(); });
|
|
self._ro.observe(el);
|
|
}
|
|
};
|
|
if (window.Alpine && window.Alpine.nextTick) window.Alpine.nextTick(after);
|
|
else setTimeout(after, 0);
|
|
},
|
|
/* Reclasse le menu dans le viewport et limite sa hauteur à l'espace
|
|
disponible sous son ancre (le contenu déborde en scroll interne). */
|
|
_place() {
|
|
var el = document.querySelector('.fd-ctx-menu');
|
|
if (!el) return;
|
|
var prev = el.style.maxHeight;
|
|
el.style.maxHeight = 'none';
|
|
var w = el.offsetWidth || 240;
|
|
var h = el.offsetHeight || 320;
|
|
el.style.maxHeight = prev || '';
|
|
var vw = window.innerWidth, vh = window.innerHeight;
|
|
var cx = (this._cx == null ? this.x : this._cx);
|
|
var cy = (this._cy == null ? this.y : this._cy);
|
|
var nx = cx;
|
|
if (nx + w > vw - 8) nx = vw - w - 8;
|
|
nx = Math.max(8, nx);
|
|
var ny = cy;
|
|
if (ny + h > vh - 8) {
|
|
if (cy - h >= 8) ny = cy - h;
|
|
else ny = Math.max(8, vh - h - 8);
|
|
}
|
|
this.x = nx;
|
|
this.y = ny;
|
|
this.maxH = Math.max(120, vh - ny - 8);
|
|
},
|
|
close() {
|
|
if (this._ro) { try { this._ro.disconnect(); } catch { /* volontaire */ } this._ro = null; }
|
|
this.open = false;
|
|
this.node = null;
|
|
this.handlers = {};
|
|
this.tagAdding = false;
|
|
this.tagExistingOpen = false;
|
|
this.iconOpen = false;
|
|
},
|
|
|
|
/* Un item est-il disponible pour CETTE page ? (union vs capacités) */
|
|
has(key) { return typeof this.handlers[key] === 'function'; },
|
|
|
|
/* Exécute l'action → handler fourni par la page courante. */
|
|
run(key) {
|
|
if (!this.node) { this.close(); return; }
|
|
var fn = this.handlers[key];
|
|
if (fn) try { fn(this.node); } catch (e) { console.error('fdCtx.run', e); }
|
|
this.close();
|
|
},
|
|
|
|
/* ── Tags ── */
|
|
avail() { return this.availTags || []; },
|
|
setAvail(a) { this.availTags = a || []; },
|
|
removeTag(id) {
|
|
var fn = this.handlers.tagRemove;
|
|
if (fn) try { fn(id); } catch (e) { console.error('fdCtx.removeTag', e); }
|
|
},
|
|
addExistingTag(t) {
|
|
var fn = this.handlers.tagExisting;
|
|
if (fn) try { fn(t); } catch (e) { console.error('fdCtx.addExistingTag', e); }
|
|
this.tagExistingOpen = false;
|
|
},
|
|
addNewTag(name, color) {
|
|
name = (name || '').trim();
|
|
if (!name) return;
|
|
var fn = this.handlers.tagAdd;
|
|
if (fn) try { fn(name, color || this.newTagColor); } catch (e) { console.error('fdCtx.addNewTag', e); }
|
|
this.tagAdding = false;
|
|
},
|
|
|
|
/* ── Icon picker ── */
|
|
setIcon(icon) {
|
|
icon = (icon || '').trim();
|
|
var fn = this.handlers.setIcon;
|
|
if (fn) try { fn(icon); } catch (e) { console.error('fdCtx.setIcon', e); }
|
|
this.close();
|
|
},
|
|
openIconPicker() {
|
|
var fn = this.handlers.setIcon;
|
|
if (!fn) return;
|
|
if (!window.FDIconPicker) return;
|
|
window.FDIconPicker.openFor({
|
|
x: this.x,
|
|
y: this.y,
|
|
onPick: fn,
|
|
onRemove: function () { fn(''); }
|
|
});
|
|
this.close();
|
|
},
|
|
});
|
|
}
|
|
|
|
if (window.Alpine) {
|
|
registerFdCtx();
|
|
} else {
|
|
document.addEventListener('alpine:init', registerFdCtx);
|
|
}
|
|
})();
|