/** * ───────────────────────────────────────────────────────────────────────────── * Composants UI réutilisables (equivalent des composants Compose Android) : * fabrique DOM `el()`, dialogs modaux, dropdown, champs date/heure, toast. * * Aucun framework : tout est construit via `el(tag, attrs, children)` — un * helper minimal et lisible, commenté, qui remplace JSX/hyperscript. * ───────────────────────────────────────────────────────────────────────────── */ import { t as tr } from '../util/i18n.js'; /** * Fabrique DOM : crée un élément, applique les attributs/écouteurs, attache * les enfants (string → nœud texte ; Element → append ; Array → aplatit ; * null/undefined/false → ignoré). * * attrs spéciaux : * - `class` : liste de classes (string) ; * - `on` : addEventListener (ex. `onclick`, `oninput`) ; * - tout autre attribut passe par setAttribute (sauf `value`/`checked`/ * `disabled`/`selected` → propriétés DOM directes). * * @param {string} tag * @param {object} [attrs={}] * @param {Array|Node|string|null} [children] * @returns {HTMLElement} */ export function el(tag, attrs = {}, children = null) { const node = document.createElement(tag); for (const [key, value] of Object.entries(attrs || {})) { if (value === null || value === undefined || value === false) continue; if (key.startsWith('on') && typeof value === 'function') { node.addEventListener(key.slice(2), value); } else if (key === 'class') { node.className = value; } else if (key === 'value' || key === 'checked' || key === 'disabled' || key === 'selected') { node[key] = value; } else { node.setAttribute(key, value); } } appendChildren(node, children); return node; } /** @private */ function appendChildren(node, children) { if (children === null || children === undefined || children === false) return; if (Array.isArray(children)) { for (const c of children) appendChildren(node, c); } else if (children instanceof Node) { node.appendChild(children); } else { node.appendChild(document.createTextNode(String(children))); } } /** * Dialog modal générique (miroir AlertDialog M3) : * - fond cliquable = dismiss (comme onDismissRequest) ; * - body : nœud ou string ; actions : [{label, className, onClick}]. * * @param {object} opts { title, body, actions, dismissible=true } * @returns {{close:Function, root:HTMLElement}} */ export function showDialog({ title, body, actions = [], dismissible = true }) { const root = el('div', { class: 'dialog-backdrop' }); const dialog = el('div', { class: 'dialog' }); if (title) dialog.appendChild(el('h2', {}, title)); if (body) appendChildren(dialog, body); if (actions.length) { const row = el('div', { class: 'actions' }); for (const a of actions) { row.appendChild(el('button', { class: a.className || 'btn text', onclick: () => a.onClick(), }, a.label)); } dialog.appendChild(row); } root.appendChild(dialog); const close = () => root.remove(); if (dismissible) { root.addEventListener('click', (e) => { if (e.target === root) close(); // clic sur le fond seulement }); } document.body.appendChild(root); return { close, root }; } /** * Toast de feedback (message temporaire en bas d'écran — feedback des * actions Paramètres, miroir du `message` SettingsScreen Android). * * @param {string} text */ export function toast(text) { const node = el('div', { class: 'toast' }, text); document.body.appendChild(node); setTimeout(() => node.remove(), 3500); } /** * Champ de saisie (label + input/select/textarea) — miroir OutlinedTextField. * * @param {object} opts { label, type, value, onInput, options (select), * placeholder, attrs, inputEl (retourne l'élément via * result.input) } * @returns {HTMLElement} le conteneur (.input = l'élément de saisie) */ export function field({ label, type = 'text', value = '', onInput = null, options = null, placeholder = null, attrs = {} }) { let input; if (options) { input = el('select', { onchange: (e) => onInput && onInput(e.target.value), ...attrs, }, options.map((o) => el('option', { value: o.value, selected: o.value === value }, o.label))); } else if (type === 'textarea') { input = el('textarea', { oninput: (e) => onInput && onInput(e.target.value), placeholder, ...attrs }, value || ''); } else { input = el('input', { type, value, oninput: (e) => onInput && onInput(e.target.value), placeholder, ...attrs, }); } const wrap = el('div', { class: 'field' }, [el('label', {}, label), input]); wrap.input = input; return wrap; } /** * Dropdown (miroir ExposedDropdownMenuBox) : un