Éditeur HTML WYSIWYG pour Blazor — barre d'outils complète (police, taille, gras/italique/souligné, couleurs, alignement, listes), zéro dépendance JS tierce pour le mode visuel, et basculement vers l'édition HTML source via Monaco Editor chargé à la volée depuis le CDN.
- Vue d'ensemble
- Démarrage rapide
- Architecture
- Référence API
- Barre d'outils — détail des actions
- Mode édition HTML (Monaco)
- Exemples d'utilisation
- Format de la valeur
- Sauvegarde et restauration de la sélection
- Personnalisation CSS
- Bonnes pratiques
- Limitations connues
SuperHtmlEditor est un éditeur de texte enrichi WYSIWYG (What You See Is What You Get) basé sur un div[contenteditable] natif du navigateur.
Fonctionnalités clés
- 🖊️ Édition visuelle — zone
contenteditablenative, performances maximales - 🔤 Police & taille — sélecteur de fonte (6 familles) + sélecteur de taille (7 niveaux)
- B I S Gras, Italique, Souligné — avec état actif reflété en temps réel
- 🎨 Couleur du texte & couleur de fond — color pickers natifs
<input type="color"> ↔️ Alignement — gauche, centre, droite- 📋 Listes — numérotée et à puces
- 🧹 Effacer la mise en forme — retire tous les styles de la sélection
</>Mode HTML source — bascule vers Monaco Editor chargé à la volée (lazy-load CDN)- 🌗 Thème Monaco adaptatif — détecte automatiquement
data-bs-theme="dark" - ♿ Accessible — labels,
titlesur chaque bouton,disabledpropagé - 🎨 CSS scoped — aucune pollution de styles globaux
- 📐 Hauteur configurable —
MinHeight,MaxHeight,MonacoHeight
@using SuperBlazorComponents.Components.SuperHtmlEditorAucun enregistrement spécifique au-delà du service de base :
// Program.cs
builder.Services.AddSuperComponents();@using SuperBlazorComponents.Components.SuperHtmlEditor
<SuperHtmlEditor @bind-Value="_html" />
@code {
private string? _html;
}┌─────────────────────────────────────────────────────────────────────┐
│ she-toolbar │
│ ┌────────┐ ┌──────┐ │ B I S │ │ 🎨 🖌️ │ │ ← → ↔ │ │ 1. • │ 🧹 </>│
│ │ Fonte │ │Taille│ │ │ │ │ │ │ │ │ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ she-content (div[contenteditable]) ← mode WYSIWYG │
│ │
│ Texte <strong>riche</strong> éditable ici… │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ she-monaco-container (Monaco Editor) ← mode HTML source │
│ │
│ <p>Texte <strong>riche</strong> éditable ici…</p> │
└─────────────────────────────────────────────────────────────────────┘
Flux de données
Utilisateur frappe / formate
│
▼
contenteditable (DOM)
│ event: input
▼
SuperHtmlEditor.razor.js → invokeMethodAsync('OnContentChanged', html)
│
▼
SuperHtmlEditor.razor.cs → ValueChanged.InvokeAsync(html)
│
▼
Composant parent (@bind-Value)
Interop JS (module ES)
Le fichier .razor.js est chargé comme module ES via import() au premier rendu. Il expose les fonctions publiques suivantes :
| Fonction JS | Rôle |
|---|---|
initialize(el, toolbar, dotnetRef, html) |
Initialise le contenteditable, attache les listeners |
execCommand(el, command, value) |
Restaure la sélection puis exécute document.execCommand |
getHtml(el) |
Retourne el.innerHTML |
setHtml(el, html) |
Met à jour el.innerHTML et notifie Blazor |
loadMonaco(container, html) |
Lazy-charge le loader AMD de Monaco depuis le CDN puis crée l'éditeur |
setMonacoValue(html) |
Met à jour le contenu de l'instance Monaco existante |
getMonacoValue() |
Lit le contenu de Monaco |
dispose(el) |
Nettoie tous les listeners et dispose Monaco |
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
Value |
string? |
null |
Contenu HTML courant |
ValueChanged |
EventCallback<string?> |
— | Déclenché à chaque modification du contenu |
Label |
string? |
null |
Label affiché au-dessus de l'éditeur |
Disabled |
bool |
false |
Désactive toutes les interactions (toolbar + zone éditable) |
MinHeight |
int |
150 |
Hauteur minimale de la zone WYSIWYG en pixels |
MaxHeight |
int |
0 |
Hauteur maximale de la zone WYSIWYG en pixels (0 = illimitée) |
MonacoHeight |
int |
300 |
Hauteur du panneau Monaco Editor en pixels |
| Méthode | Signature | Déclencheur |
|---|---|---|
OnContentChanged |
Task OnContentChanged(string html) |
Événement input sur le contenteditable |
OnFocusChanged |
void OnFocusChanged(bool focused) |
focus / blur sur le contenteditable |
OnSelectionStateChanged |
void OnSelectionStateChanged(bool bold, bool italic, bool underline) |
keyup / mouseup sur le contenteditable |
| Bouton | Commande execCommand |
Notes |
|---|---|---|
| Fonte (select) | fontName |
6 familles : Arial, Georgia, Courier New, Times New Roman, Verdana, Trebuchet MS |
| Taille (select) | fontSize |
Valeurs 1–7 (équivalent HTML font size : ~8 à 36 pt) |
| B Gras | bold |
Toggle — état actif reflété par .she-active |
| I Italique | italic |
Toggle — état actif reflété |
| S Souligné | underline |
Toggle — état actif reflété |
| 🎨 Couleur texte | foreColor |
Color picker natif <input type="color"> |
| 🖌️ Couleur fond | hiliteColor |
Color picker natif — surlignage de la sélection |
| Aligner gauche | justifyLeft |
— |
| Centrer | justifyCenter |
— |
| Aligner droite | justifyRight |
— |
| Liste numérotée | insertOrderedList |
Toggle <ol> |
| Liste à puces | insertUnorderedList |
Toggle <ul> |
| Effacer format | removeFormat |
Retire tous les styles inline de la sélection |
</> HTML |
— | Bascule vers Monaco (voir ci-dessous) |
Note :
document.execCommandest marqué deprecated dans la spec W3C mais reste supporté et fonctionnel dans tous les navigateurs actuels. Il n'existe pas d'API standardisée de remplacement à ce jour.
- L'utilisateur clique sur le bouton
</> HTML - Le composant lit l'HTML courant depuis le
contenteditable - Si Monaco n'est pas encore chargé, un spinner s'affiche et le loader AMD est injecté dynamiquement depuis le CDN jsDelivr
- Monaco Editor s'initialise en mode
htmlavec coloration syntaxique, numéros de ligne, retour à la ligne automatique etautomaticLayout - Quand l'utilisateur reclique sur
</> HTML, la valeur Monaco est réinjectée dans lecontenteditableetValueChangedest déclenché
Premier clic sur </> HTML
│
▼
<script src="https://cdn.jsdelivr.net/.../loader.js"> injecté dynamiquement
│
▼
require(['vs/editor/editor.main'], callback)
│
▼
monaco.editor.create(container, { language: 'html', ... })
│
▼
_monacoReady = true → spinner masqué, éditeur visible
Les clics suivants réutilisent l'instance existante (singleton par composant).
Le thème est sélectionné automatiquement à la création de l'éditeur :
theme: document.documentElement.getAttribute('data-bs-theme') === 'dark'
|| document.documentElement.classList.contains('dark')
? 'vs-dark' : 'vs'La version utilisée est 0.52.0 (jsDelivr). Pour pointer une autre version, modifier l'URL dans SuperHtmlEditor.razor.js :
// Remplacer 0.52.0 par la version souhaitée
loaderScript.src = 'https://cdn.jsdelivr.net/npm/monaco-editor@0.52.0/min/vs/loader.js';<SuperHtmlEditor @bind-Value="_html" />
@code {
private string? _html;
}<SuperHtmlEditor Label="Corps du message" @bind-Value="_body" /><SuperHtmlEditor Label="Description"
@bind-Value="_description" />
@code {
private string? _description =
"<p>Bienvenue dans <strong>SuperHtmlEditor</strong> !</p>" +
"<p>Modifiez ce texte librement.</p>";
}<!-- Zone compacte avec défilement au-delà de 200 px -->
<SuperHtmlEditor Label="Note"
@bind-Value="_note"
MinHeight="80"
MaxHeight="200"
MonacoHeight="250" />
<!-- Grande zone pour un éditeur de contenu long -->
<SuperHtmlEditor Label="Article"
@bind-Value="_article"
MinHeight="400"
MonacoHeight="500" /><SuperHtmlEditor Label="Contenu (lecture seule)"
Value="@_readonlyHtml"
Disabled="true" />Utilisez (MarkupString) pour afficher le HTML produit dans la page :
<div class="row">
<div class="col-md-6">
<SuperHtmlEditor Label="Éditeur" @bind-Value="_html" MinHeight="250" />
</div>
<div class="col-md-6">
<label class="form-label">Aperçu</label>
<div class="border rounded p-3">
@((MarkupString)(_html ?? ""))
</div>
</div>
</div>
@code {
private string? _html = "<h3>Titre</h3><p>Contenu…</p>";
}<EditForm Model="_model" OnValidSubmit="Save">
<DataAnnotationsValidator />
<div class="mb-3">
<SuperHtmlEditor Label="Description *"
@bind-Value="_model.Description"
MinHeight="200" />
<ValidationMessage For="@(() => _model.Description)" />
</div>
<button type="submit" class="btn btn-primary">Enregistrer</button>
</EditForm>
@code {
private ArticleModel _model = new();
private async Task Save()
{
// _model.Description contient le HTML
}
public class ArticleModel
{
[Required(ErrorMessage = "La description est obligatoire.")]
public string? Description { get; set; }
}
}Note :
SuperHtmlEditorne déclenche pas encoreEditContextautomatiquement (pas deValueExpression). L'appel àValueChangedmet cependant à jour le modèle lié, ce qui suffit pour la validationDataAnnotationsValidator.
Value / ValueChanged transportent du HTML brut tel que produit par le navigateur depuis le contenteditable. Exemples :
<!-- Gras -->
<b>Texte</b>
<!-- Couleur de texte -->
<font color="#e03333">Rouge</font>
<!-- Police -->
<font face="Georgia">Georgia</font>
<!-- Taille -->
<font size="5">Grand</font>
<!-- Alignement -->
<div style="text-align: center;">Centré</div>
<!-- Liste -->
<ul><li>Élément 1</li><li>Élément 2</li></ul>Le HTML est généré par
document.execCommandet peut varier légèrement selon le navigateur. Pour un affichage fidèle, utilisez toujours(MarkupString)et non un binding texte.
Quand l'utilisateur interagit avec la barre d'outils (clic sur un bouton, ouverture d'un <select>), le contenteditable peut perdre le focus et donc la sélection de texte.
Le module JS résout ce problème en deux temps :
- Sauvegarde — à chaque
keyup,mouseupetblursur l'éditeur, un clone duRangecourant est mémorisé dansstate.savedRange - Restauration — avant chaque
execCommand, le focus est remis sur l'éditeur et la sélection sauvegardée est restaurée viasel.addRange(state.savedRange)
// Sauvegarde
const saveRange = () => {
const sel = window.getSelection();
if (sel && sel.rangeCount > 0) {
state.savedRange = sel.getRangeAt(0).cloneRange();
}
};
// Restauration avant commande
el.focus();
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(state.savedRange);
document.execCommand(command, false, value);Toutes les classes CSS sont préfixées she- et définies dans SuperHtmlEditor.razor.css (scoped). Les variables Bootstrap 5 sont utilisées systématiquement pour la compatibilité dark/light.
| Variable CSS | Usage |
|---|---|
--bs-border-color |
Bordures de la boîte et de la toolbar |
--bs-body-bg |
Fond de la zone éditable et des selects |
--bs-tertiary-bg |
Fond de la toolbar |
--bs-body-color |
Couleur du texte |
--bs-primary |
Fond des boutons actifs (she-active) |
--bs-secondary-bg |
Fond au survol des boutons |
/* Arrondir davantage le composant */
.she-editor-box {
border-radius: 12px;
}
/* Fond légèrement coloré pour la toolbar */
.she-toolbar {
background: #f0f4ff;
}
/* Focus plus visible */
.she-editor-box.she-focused {
border-color: #6610f2;
box-shadow: 0 0 0 .25rem rgba(102, 16, 242, .25);
}- Sanitiser le HTML côté serveur avant de le stocker en base ou de le ré-afficher. Utilisez une librairie comme HtmlSanitizer (NuGet) pour prévenir les attaques XSS.
- Ne pas utiliser
@Html.Raw()/(MarkupString)sur du contenu non fiable sans sanitisation préalable. - Pour les formulaires avec validation, gérez la propriété liée directement (
@bind-Value) —EditContextn'est pas nécessaire pour la validation côté modèle. - Pour les contenus très longs (articles, pages), préférez
MaxHeight+MonacoHeightélevé afin de donner de l'espace à l'éditeur source. - Le lazy-load Monaco nécessite une connexion internet au premier clic. Dans un contexte offline ou intranet strict, hébergez Monaco en local et ajustez l'URL dans
SuperHtmlEditor.razor.js.
| Limitation | Détail |
|---|---|
| Pas d'insertion d'images | Choix délibéré — utiliser un composant dédié si nécessaire |
execCommand déprécié |
Toujours fonctionnel dans tous les navigateurs actuels ; aucun remplacement standard n'existe encore |
| Monaco CDN requis | La première utilisation du mode HTML source nécessite internet (ou hébergement local) |
| Thème Monaco fixé à la création | Le thème Monaco ne se met pas à jour dynamiquement si l'utilisateur change de thème pendant qu'il est ouvert |
Pas d'EditContext automatique |
Pas de ValueExpression → pas de marquage d'invalidité natif dans un EditForm |