# html-builder — ajouter / modifier / supprimer un bloc

Guide pratique, centré sur le système de blocs. Pour la référence complète de
l'API (options du constructeur, thème, snippets, colonnes...), voir
[`html-builder/docs/API.md`](html-builder/docs/API.md) — ce document-ci est un
complément orienté "je veux faire X", pas un remplacement.

Source : `/var/www/js/html-builder/`. Servi tel quel par `js.letareau.fr`
(pas de build tool, pas de copie à faire — `js.letareau.fr` sert directement
`/var/www/js/`).

## Où vivent les blocs

```
html-builder/src/
├── core/
│   ├── registry.js        # BlockRegistry — la Map qui stocke tous les types
│   └── blocks-builtin.js  # Les 12 blocs du "cœur" (text, heading, button,
│                           # image, divider, spacer, banner, callout, code,
│                           # footer, columns, html)
└── blocks/
    └── social.js           # Bloc "Réseaux" — exemple de bloc OPTIONNEL,
                             # buildé à part (dist/html-builder-block-social.js)
```

`banner`/`callout`/`code`/`footer` (ajoutés le 27/08/2026) reprennent le
vocabulaire visuel des mails transactionnels codés à la main dans le Desk
(`models/Membership.php`, `models/User.php`, `core/EmailTemplate.php` côté
West Up — bannière dégradée, encart, affichage de code, pied de page),
couleurs de marque en défaut, pour pouvoir recomposer ces mails-là via
l'éditeur de blocs plutôt qu'en PHP brut. Les 4 sont éditables directement
dans le canvas (comme `text`/`heading`), pas seulement via le panneau.

Le format d'ensemble du mail se choisit indépendamment des blocs, via
l'option `card` de `renderEmail()` (voir [Modifier un bloc existant](#modifier-un-bloc-existant)
n'est pas le bon endroit pour ça — c'est une option de rendu, pas un bloc) :
- **Classique** (défaut) : blocs à la suite, sans cadre.
- **Carte** (`{ card: true }`) : le tout encadré (fond blanc, bordure,
  coins arrondis) sur un fond gris clair — le style "Bienvenue chez West Up".
  Le composer mailing du Desk expose ce choix comme un toggle deux boutons
  (`views/mailing/create.php`), persisté en base (`mailing_campaigns.email_format`).

Deux familles :
- **Blocs du cœur** — toujours disponibles, définis dans `blocks-builtin.js`, inclus dans `dist/html-builder.js`.
- **Blocs optionnels** — fichier séparé (comme `social.js`), buildé à part, à charger explicitement par la page hôte avec un `<script>` en plus. Utile pour un bloc spécifique à un seul projet, sans l'imposer à tous les usages du module.

## Anatomie d'un bloc

Un bloc s'enregistre via `BlockRegistry.register({...})` (blocs du cœur) ou
`HtmlBuilder.registerBlockType({...})` (blocs optionnels — même forme,
juste un point d'entrée public différent). Exemple réel, le bloc `text` :

```js
BlockRegistry.register({
    type: 'text',                      // requis, unique — identifiant stocké dans blocks_json
    icon: '¶',                         // affiché dans la palette de l'éditeur
    label: 'Texte',                    // libellé affiché
    defaultContent: () => ({ content: '<p>Votre texte ici…</p>', align: 'left' }),
    fields: null,                      // panneau de propriétés générique, ou null si le bloc
                                        // fournit sa propre UI dédiée (voir editor.template.js)
    renderEmail: (c) => `<tr><td style="padding:12px 24px;text-align:${c.align || 'left'};">${c.content || ''}</td></tr>`,
    renderWeb:   (c) => `<div class="hbw-text" style="text-align:${c.align || 'left'}">${c.content || ''}</div>`,
    renderText:  (c) => { /* extraction texte brut, pour l'AltBody des emails */ },
});
```

| Champ | Obligatoire | Rôle |
|---|---|---|
| `type` | oui | Identifiant unique, stocké dans `blocks_json` |
| `icon`, `label` | non | Affichage dans la palette (défauts : `▢`, le `type` lui-même) |
| `defaultContent` | non | Contenu initial quand on ajoute le bloc (défaut : `{}`) |
| `fields` | non | Panneau de propriétés généré automatiquement — voir plus bas. `null` si le bloc a son propre panneau dédié (codé dans `src/ui/editor.template.js`) |
| `allowedInColumn` | non | `false` pour l'exclure de la palette imbriquée des colonnes (défaut : `true`) |
| `renderEmail(content, ctx)` | non | HTML compatible clients mail (table + styles inline) |
| `renderWeb(content, ctx)` | non | HTML pour affichage web (classes `hbw-*`) |
| `renderText(content)` | non | Texte brut (AltBody email, aperçu "Texte") |

`fields[].type` disponibles pour le panneau générique : `text`, `textarea`,
`number`, `color`, `checkbox`, `select` (+ `options: [[value, label], ...]`),
`align`, `variant` (thème, voir API.md).

`ctx` passé aux fonctions de rendu : `{ theme, options }`.

---

## Ajouter un bloc

### Cas 1 — bloc du cœur (disponible partout)

1. Ouvrir `html-builder/src/core/blocks-builtin.js`.
2. Ajouter un nouvel appel `BlockRegistry.register({ ... })`, sur le modèle
   des blocs existants dans ce même fichier.
3. Si le bloc a besoin d'une UI d'édition riche (pas juste des champs
   génériques `fields`), l'ajouter dans `src/ui/editor.template.js` (panneau
   dédié, voir comment `text`/`social` le font).
4. Reconstruire (voir [Déploiement](#déploiement)).
5. L'ajouter à `allowedBlocks` sur les pages hôtes qui doivent le proposer
   (voir [Où c'est utilisé](#où-cest-utilisé--west-up)) — sans ça, le bloc
   existe mais n'apparaît dans la palette d'aucune page.

### Cas 2 — bloc optionnel (un seul projet, ou pas systématique)

Créer un nouveau fichier dans `src/blocks/`, sur le modèle de
`src/blocks/social.js` :

```js
// src/blocks/video.js
(function () {
    if (typeof HtmlBuilder === 'undefined' || typeof HtmlBuilder.registerBlockType !== 'function') {
        console.error('html-builder-block-video.js: HtmlBuilder doit être chargé avant ce fichier.');
        return;
    }

    // Les blocs externes tournent dans leur propre IIFE, sans accès aux
    // fonctions internes du bundle — HtmlBuilder.utils est l'API stable
    // pour ça (escHtml, escAttr, capitalizeFirst).
    const escAttr = HtmlBuilder.utils.escAttr;

    HtmlBuilder.registerBlockType({
        type: 'video',
        icon: '▶',
        label: 'Vidéo',
        defaultContent: () => ({ url: '' }),
        fields: [{ key: 'url', label: 'URL YouTube/Vimeo', type: 'text' }],
        renderEmail: (c) => `<tr><td>[Vidéo non supportée en email : ${escAttr(c.url)}]</td></tr>`,
        renderWeb:   (c) => `<div class="hbw-video"><iframe src="${escAttr(c.url)}"></iframe></div>`,
        renderText:  (c) => `[Vidéo : ${c.url}]`,
    });
})();
```

Puis ajouter le nouveau build dans `build.sh` (copier le bloc `SOCIAL_OUT`
existant, l'adapter au nouveau fichier), reconstruire, et charger le script
généré **après** `html-builder.js` sur les pages qui en ont besoin :

```html
<script src="https://js.letareau.fr/html-builder/dist/html-builder.js"></script>
<script src="https://js.letareau.fr/html-builder/dist/html-builder-block-video.js"></script>
```

---

## Modifier un bloc existant

Éditer directement sa définition dans `blocks-builtin.js` (cœur) ou son
fichier dédié (`src/blocks/*.js`, optionnel), puis reconstruire. Aucune
migration de données nécessaire tant que la **forme** de `content` ne
change pas (mêmes clés) — seul le rendu (`renderEmail`/`renderWeb`) change,
`blocks_json` reste valide tel quel.

**Exemple réel** (27/08/2026) — le bloc `text` produisait plusieurs `<p>` à
la suite sans marge (`<p>...</p><p>...</p>`), rendant l'espacement entre
paragraphes dépendant du navigateur/client mail (incohérent, invisible en
aperçu). Correctif : une fonction utilitaire dans `src/core/escape.js`,
appliquée dans `renderEmail`/`renderWeb` du bloc `text` :

```js
// src/core/escape.js
function normalizeParagraphSpacing(html) {
    if (!html) return html;
    const tmp = document.createElement('div');
    tmp.innerHTML = html;
    tmp.querySelectorAll('p, ul, ol').forEach((el) => {
        if (!el.getAttribute('style')) el.setAttribute('style', 'margin:0 0 16px 0;');
    });
    return tmp.innerHTML;
}

// src/core/blocks-builtin.js, bloc "text"
renderEmail: (c) => `<tr><td style="...">${normalizeParagraphSpacing(c.content) || ''}</td></tr>`,
renderWeb:   (c) => `<div class="hbw-text" ...>${normalizeParagraphSpacing(c.content) || ''}</div>`,
```

`content` (le HTML riche saisi par l'utilisateur) n'a pas changé de forme —
seule la fonction de rendu transforme le HTML au moment de l'export. Aucune
donnée existante à migrer.

Si en revanche la **forme** de `content` change (renommer une clé, changer
un format), prévoir une migration au chargement — voir comment
`setBlocksJSON()` migre automatiquement l'ancien format de colonnes
(`src/core/state.js`) pour le modèle à suivre.

---

## Supprimer un bloc

Deux niveaux, selon l'intention :

### Masquer un bloc pour un projet donné (le garder disponible ailleurs)

Ne rien toucher dans `html-builder/`. Restreindre la palette côté page
hôte avec l'option `allowedBlocks` du constructeur :

```js
new HtmlBuilder(el, {
    allowedBlocks: ['text', 'heading', 'button', 'image'], // pas de divider/spacer/social/columns/html
});
```

### Retirer un bloc définitivement du module

1. Supprimer (ou commenter) son `BlockRegistry.register({...})` dans
   `blocks-builtin.js`, ou supprimer le fichier `src/blocks/*.js` optionnel
   correspondant et sa ligne dans `build.sh`.
2. Reconstruire.

⚠️ **Effet sur les données existantes** : un `blocks_json` déjà enregistré
en base qui référence encore ce type ne plante pas (`console.error` +
rendu vide pour ce bloc précis, le reste de l'email s'affiche normalement —
voir `renderEmail(blocks, options)` dans `src/renderers/renderer-email.js`),
mais le bloc devient invisible/vide partout où il était déjà utilisé. Avant
de supprimer un bloc du cœur, vérifier s'il est utilisé dans des campagnes
existantes (`blocks_json` en base côté chaque projet hôte) ou dans des
modèles/templates sauvegardés.

---

## Déploiement

```sh
cd /var/www/js/html-builder
./build.sh
```

Régénère `dist/html-builder.js` (+ `dist/html-builder-block-social.js` s'il
y a un bloc optionnel). `js.letareau.fr` sert directement `/var/www/js/` —
rien à copier ailleurs pour la version canonique.

La copie de démo sur `test.letareau.fr` doit être resynchronisée
manuellement (voir la section Déploiement de `docs/API.md`) — ne pas
oublier si un test doit refléter le dernier état.

## Où c'est utilisé (West Up)

Le module est chargé sur `/mailing/create` et `/mailing/{id}/edit` du Desk
(`desk/views/mailing/create.php`, dupliqué sur `dev.desk`) :

```html
<script src="https://js.letareau.fr/html-builder/dist/html-builder.js"></script>
<script src="https://js.letareau.fr/html-builder/dist/html-builder-block-social.js"></script>
```

```js
this.builder = new HtmlBuilder(this.$refs.hbMount, {
    mode: 'email',
    allowedBlocks: ['text', 'heading', 'button', 'image', 'divider', 'spacer', 'social', 'columns', 'html'],
    theme: { /* charte West Up */ },
});
```

C'est cette ligne `allowedBlocks` qu'il faut modifier pour ajouter/retirer
un bloc de la palette du mailing, une fois le bloc enregistré côté
`html-builder`. Penser à répercuter le changement dans les deux copies
(`desk/` et `dev.desk/`).
