Rend enfin tapable le champ où l'on écrit ce qu'un numéro annonce

Le champ « À la une » se reliait à chaque frappe sur une propriété qui
découpait puis recomposait la ligne : le point-virgule tapé ouvrait une
entrée vide aussitôt élaguée, et l'espace qui le suit était rogné. Le seul
caractère qu'on ne pouvait pas y taper était son propre séparateur.

Un titre s'ajoute maintenant seul, par Entrée, et se modifie ou se retire
depuis la liste. Plus rien ne découpe, donc plus rien n'est intapable.

ArticlesTexte disparaît, et avec lui le paramètre virguleAussi de
ListeSaisie, qui n'existait que pour ce champ.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mathieu
2026-08-21 23:37:31 +02:00
co-authored by Claude Opus 5
parent abaa520f59
commit fe36091bf0
7 changed files with 298 additions and 55 deletions
@@ -26,16 +26,77 @@
</div>
@*
⚠️ Séparateur : le POINT-VIRGULE, et pas la virgule des thèmes de livres. Un titre d'article
en contient souvent (« Ukraine, deux ans après »), et la virgule le couperait en deux.
⚠️ Un titre à la fois, et surtout PAS une ligne de titres séparés par des points-virgules.
Le champ unique se reliait à chaque frappe sur une propriété qui découpait puis recomposait
la liste : la saisie était donc réécrite sous les doigts — un point-virgule tapé disparaissait
(il ouvrait une valeur vide, aussitôt élaguée), et l'espace qui le suit était rogné par le
même élagage. Autrement dit, le séparateur du champ était le seul caractère qu'on ne pouvait
pas y taper.
Ici la liste est la donnée : on écrit un titre, Entrée l'y range, et chaque titre se modifie
ou se retire ensuite. Une virgule (« Ukraine, deux ans après ») n'a plus rien à couper,
puisque plus rien ne découpe.
*@
<label class="champ">
<span class="champ-libelle">
À la une <span class="champ-aide">(titres séparés par des points-virgules)</span>
</span>
<input class="champ-saisie" type="text" @bind="Saisie.ArticlesTexte" @bind:event="oninput"
placeholder="Ukraine, deux ans après ; Le retour du train de nuit" autocomplete="off" />
</label>
<div class="champ">
<label class="champ-libelle" for="@_idChamp">
À la une <span class="champ-aide">(un titre à la fois : tapez-le, puis Entrée)</span>
</label>
<div class="ligne-creation">
<input id="@_idChamp" class="champ-saisie" type="text" @bind="_titreSaisi"
@bind:event="oninput" @onkeydown="SurToucheArticle"
placeholder="Ukraine, deux ans après" autocomplete="off" />
<button type="button" class="bouton bouton-compact"
disabled="@(string.IsNullOrWhiteSpace(_titreSaisi))" @onclick="ValiderArticle">
@(_indexModifie is null ? "Ajouter" : "Enregistrer")
</button>
@if (_indexModifie is not null)
{
<button type="button" class="bouton bouton-compact bouton-discret"
@onclick="AnnulerModification">
Annuler
</button>
}
</div>
@if (_messageArticle is not null)
{
<p class="message-discret" role="status">@_messageArticle</p>
}
@if (Saisie.Articles.Count > 0)
{
<ul class="liste-unes">
@for (var rang = 0; rang < Saisie.Articles.Count; rang++)
{
@* La copie locale est indispensable : sans elle, les fermetures des deux
boutons captureraient la variable de boucle, donc son état final. *@
var index = rang;
var titre = Saisie.Articles[rang];
<li class="ligne-une" @key="index">
<span class="ligne-une-titre">@titre</span>
<div class="actions-tome">
<button type="button" class="bouton bouton-compact"
aria-label="@($"Modifier « {titre} »")"
@onclick="() => ModifierArticle(index)">
Modifier
</button>
<button type="button" class="bouton bouton-compact bouton-danger"
aria-label="@($"Supprimer « {titre} »")"
@onclick="() => SupprimerArticle(index)">
Supprimer
</button>
</div>
</li>
}
</ul>
}
</div>
@*
⚠️ Une URL, pas une photo. Aucune source ne donne la couverture d'un NUMÉRO : l'ISSN désigne
@@ -53,6 +114,106 @@
@code {
[Parameter, EditorRequired] public AjoutNumeroRevue Saisie { get; set; } = default!;
/// <summary>Identifiant du champ « à la une », pour que son libellé le désigne vraiment.</summary>
private readonly string _idChamp = $"une-{Guid.NewGuid():N}";
/// <summary>Titre en cours de frappe. <b>Il n'est relié à rien d'autre</b> : voir plus haut.</summary>
private string? _titreSaisi;
/// <summary>Rang du titre qu'on modifie — <c>null</c> = on en ajoute un nouveau.</summary>
private int? _indexModifie;
private string? _messageArticle;
/// <summary>Saisie servie au rendu précédent, pour repérer que le parent en a changé.</summary>
private AjoutNumeroRevue? _saisiePrecedente;
/// <summary>
/// Le parent remplace la saisie entière (numéro enregistré, autre numéro ouvert en
/// modification) : ce qui restait dans le champ appartenait au numéro d'avant.
/// </summary>
protected override void OnParametersSet()
{
if (!ReferenceEquals(_saisiePrecedente, Saisie))
{
_saisiePrecedente = Saisie;
RemettreAZero();
}
}
private void RemettreAZero()
{
_titreSaisi = null;
_indexModifie = null;
_messageArticle = null;
}
/// <summary>
/// Entrée range le titre, comme le bouton.
/// </summary>
/// <remarks>
/// ⚠️ Cette touche ne remonte <b>pas</b> à <c>OnEntree</c> : celui-ci enregistre le numéro
/// entier, et l'on serait alors bien en peine de saisir deux titres à la une.
/// </remarks>
private void SurToucheArticle(KeyboardEventArgs e)
{
if (e.Key == "Enter")
{
ValiderArticle();
}
}
private void ValiderArticle()
{
var titre = _titreSaisi?.Trim();
if (string.IsNullOrEmpty(titre))
{
return;
}
// Même règle de doublon que partout ailleurs : au sens de la normalisation, et le
// libellé déjà rangé est celui qui reste.
var cle = NormalisationTexte.Normaliser(titre);
var deja = Saisie.Articles.FindIndex(a => NormalisationTexte.Normaliser(a) == cle);
if (deja >= 0 && deja != _indexModifie)
{
_messageArticle = "Ce titre est déjà à la une de ce numéro.";
return;
}
if (_indexModifie is { } index)
{
Saisie.Articles[index] = titre;
}
else
{
Saisie.Articles.Add(titre);
}
RemettreAZero();
}
/// <summary>Reprend un titre dans le champ : on le corrige là où on l'a écrit.</summary>
private void ModifierArticle(int index)
{
_messageArticle = null;
_indexModifie = index;
_titreSaisi = Saisie.Articles[index];
}
private void AnnulerModification() => RemettreAZero();
private void SupprimerArticle(int index)
{
Saisie.Articles.RemoveAt(index);
// Le titre qu'on modifiait vient peut-être de disparaître, et les rangs suivants ont
// glissé : reprendre la modification écrirait sur le mauvais titre.
RemettreAZero();
}
/// <summary>
/// Date affichée par le champ, en <b>local</b>.
/// </summary>