/** * Modul: User Multi-Select * Zweck: Wiederverwendbare Mehrfachauswahl-Komponente für Benutzer (Tasks & Kalender) * Abhängigkeiten: public/utils/html.js, public/i18n.js */ import { esc } from '/utils/html.js'; import { t } from '/i18n.js'; /** * Rendert einen Avatar-Stack für mehrere zugewiesene Benutzer. * @param {Array<{id, display_name, color}>} users * @param {object} opts * @param {number} [opts.size=28] Avatar-Größe in px * @param {number} [opts.maxVisible=3] Maximale Avatare vor "+N"-Anzeige * @returns {string} HTML-String */ export function renderAvatarStack(users, { size = 28, maxVisible = 3 } = {}) { if (!users?.length) return ''; const visible = users.slice(0, maxVisible); const overflow = users.length - visible.length; const fs = Math.round(size * 0.4); const avatars = visible.map((u) => { const initials = (u.display_name ?? '') .split(' ') .map((w) => w[0] ?? '') .join('') .toUpperCase() .slice(0, 2); return ` ${esc(initials)} `; }); if (overflow > 0) { avatars.push(`+${overflow}`); } return `${avatars.join('')}`; } /** * Rendert das Multi-Select-Widget als Dropdown-Checkbox-Liste. * @param {Array<{id, display_name, avatar_color}>} allUsers Alle verfügbaren Benutzer * @param {number[]} selectedIds Bereits ausgewählte IDs * @param {string} inputName Name-Attribut des Widgets * @param {string} labelKey i18n-Schlüssel für das Label * @returns {string} HTML-String */ export function renderUserMultiSelect(allUsers, selectedIds, inputName, labelKey) { const selectedSet = new Set(selectedIds ?? []); const items = allUsers.map((u) => { const checked = selectedSet.has(u.id) ? 'checked' : ''; const initials = (u.display_name ?? '') .split(' ') .map((w) => w[0] ?? '') .join('') .toUpperCase() .slice(0, 2); return ` `; }); const noneLabel = t('userMultiSelect.nobody'); return `
${items.join('')}
`; } /** * Liest die ausgewählten User-IDs aus einem gerenderten Multi-Select-Widget. * @param {Element} container DOM-Element, das das Widget enthält * @param {string} inputName Name-Attribut des Widgets * @returns {number[]} */ export function getSelectedUserIds(container, inputName) { const checkboxes = container.querySelectorAll( `[data-ms-input="${CSS.escape(inputName)}"]:not(.user-ms__none):checked` ); return Array.from(checkboxes).map((cb) => Number(cb.value)).filter(Boolean); } /** * Bindet die Checkbox-Logik: * - "Niemand" deselektiert alle anderen * - Andere Auswahl deselektiert "Niemand" * @param {Element} container * @param {string} inputName */ export function bindUserMultiSelect(container, inputName) { const widget = container.querySelector(`.user-ms[data-ms-name="${CSS.escape(inputName)}"]`); if (!widget) return; widget.addEventListener('change', (e) => { const cb = e.target; if (!cb.matches('.user-ms__checkbox')) return; if (cb.classList.contains('user-ms__none') && cb.checked) { widget.querySelectorAll('.user-ms__checkbox:not(.user-ms__none)').forEach((c) => { c.checked = false; }); } else if (!cb.classList.contains('user-ms__none') && cb.checked) { const none = widget.querySelector('.user-ms__none'); if (none) none.checked = false; } }); }