fix(android): editing a note was about to strip its line breaks

Making every field editable put a note's body behind an <input>, and HTML's
value sanitization removes CR and LF without a word. The formatting would
have been lost on the phone and published to the repository at the next
save - silent, and irreversible once the desktop pulled it.

Multi-line is marked on the row in Delphi, like the field name, rather than
guessed from the caption or from whether the value happens to contain a
newline today: a single-line note one wants to make multi-line deserves the
same control.

The row wraps through a class set when the editor opens, not through :has() -
that selector does not exist before Chrome 105, and an older engine would
ignore the rule without saying so.
This commit is contained in:
r-zakarya
2026-08-22 11:33:10 +01:00
parent 0fa521b709
commit 8315ce9230
2 changed files with 27 additions and 2 deletions
+10 -2
View File
@@ -166,6 +166,11 @@ type
/// Rang de la piece jointe dans l'entree, ou -1. Il ne se deduit pas du /// Rang de la piece jointe dans l'entree, ou -1. Il ne se deduit pas du
/// rang de la ligne : les champs personnalises s'intercalent avant. /// rang de la ligne : les champs personnalises s'intercalent avant.
AttachIndex: Integer; AttachIndex: Integer;
/// La valeur tient sur plusieurs lignes. Marque ICI, pas devine du libelle
/// ni du contenu : un <input> APPLIQUE la sanitisation de HTML et retire
/// les retours a la ligne en silence. Une note modifiee depuis le telephone
/// y perdait sa mise en forme, et la perte partait au depot.
Multiline: Boolean;
end; end;
function BuildRows(const E: TVaultEntry): TArray<TDetailRow>; function BuildRows(const E: TVaultEntry): TArray<TDetailRow>;
@@ -174,7 +179,8 @@ var
I: Integer; I: Integer;
procedure Add(const ACaption, AValue: string; ASecret: Boolean; procedure Add(const ACaption, AValue: string; ASecret: Boolean;
const AField: string = ''; AAttach: Integer = -1); const AField: string = ''; AAttach: Integer = -1;
AMultiline: Boolean = False);
begin begin
// Une ligne vide n'apprend rien - SAUF si elle se remplit. Un identifiant // Une ligne vide n'apprend rien - SAUF si elle se remplit. Un identifiant
// absent qu'on ne peut pas ajouter serait une impasse. // absent qu'on ne peut pas ajouter serait une impasse.
@@ -185,6 +191,7 @@ var
LRows[High(LRows)].Secret := ASecret; LRows[High(LRows)].Secret := ASecret;
LRows[High(LRows)].Field := AField; LRows[High(LRows)].Field := AField;
LRows[High(LRows)].AttachIndex := AAttach; LRows[High(LRows)].AttachIndex := AAttach;
LRows[High(LRows)].Multiline := AMultiline;
end; end;
begin begin
@@ -195,7 +202,7 @@ begin
// Un site et un identifiant n'ont aucun sens pour une note. Les proposer // Un site et un identifiant n'ont aucun sens pour une note. Les proposer
// vides reviendrait a inviter a remplir ce que rien n'affichera jamais. // vides reviendrait a inviter a remplir ce que rien n'affichera jamais.
if E.IsNote then if E.IsNote then
Add('Content', E.Password, False, 'password') Add('Content', E.Password, False, 'password', -1, True)
else else
begin begin
Add('Site', E.Site, False, 'site'); Add('Site', E.Site, False, 'site');
@@ -263,6 +270,7 @@ begin
// "enregistrable", et la page y met le bouton de telechargement. // "enregistrable", et la page y met le bouton de telechargement.
if LRows[I].AttachIndex >= 0 then if LRows[I].AttachIndex >= 0 then
LObj.AddPair('a', TJSONNumber.Create(LRows[I].AttachIndex)); LObj.AddPair('a', TJSONNumber.Create(LRows[I].AttachIndex));
if LRows[I].Multiline then LObj.AddPair('m', TJSONBool.Create(True));
LArr.AddElement(LObj); LArr.AddElement(LObj);
end; end;
Result := LRoot.ToJSON; Result := LRoot.ToJSON;
+17
View File
@@ -85,6 +85,13 @@
dependre d'une mesure qui n'arrive jamais. Elle disparait au premier dependre d'une mesure qui n'arrive jamais. Elle disparait au premier
champ quitte, donc rien ne flotte quand on ne tape pas. */ champ quitte, donc rien ne flotte quand on ne tape pas. */
body.typing .scroll { padding-bottom: 60vh; } body.typing .scroll { padding-bottom: 60vh; }
/* Une zone de texte occupe plusieurs lignes : la rangee cesse d'etre une
rangee. Sans cela les boutons la compriment a la hauteur d'un champ.
UNE CLASSE POSEE A L'OUVERTURE, pas un selecteur :has() - celui-ci
n'existe pas avant Chrome 105, et un moteur plus ancien ignorerait la
regle sans rien dire. */
.line.wrap { flex-wrap: wrap; }
.line.wrap textarea { flex-basis: 100%; }
/* --- barre de recherche ------------------------------------------------ */ /* --- barre de recherche ------------------------------------------------ */
.top { padding: 10px 12px 6px; display: flex; gap: 8px; } .top { padding: 10px 12px 6px; display: flex; gap: 8px; }
@@ -830,6 +837,15 @@ document.getElementById('fields').addEventListener('click', function (ev) {
function editorControl(k) { function editorControl(k) {
var val = detailRows[k].v; var val = detailRows[k].v;
var style = ' style="margin:0;flex:1;min-width:0;width:auto"'; var style = ' style="margin:0;flex:1;min-width:0;width:auto"';
// UNE ZONE DE TEXTE POUR CE QUI TIENT SUR PLUSIEURS LIGNES. Un <input>
// applique la sanitisation de HTML et retire les retours a la ligne en
// silence : le corps d'une note y perdait sa mise en forme, et la perte
// partait au depot a l'enregistrement suivant.
if (detailRows[k].m)
return '<textarea class="field-in" id="editval" rows="6"'
+ ' autocorrect="off" spellcheck="false"'
+ ' style="margin:0;flex:1;min-width:0;width:auto;height:auto;'
+ 'padding:10px 14px;resize:vertical"></textarea>';
if (editField !== 'folder') if (editField !== 'folder')
return '<input class="field-in" id="editval" autocomplete="off"' return '<input class="field-in" id="editval" autocomplete="off"'
+ ' type="' + (detailRows[k].s ? 'password' : 'text') + '"' + ' type="' + (detailRows[k].s ? 'password' : 'text') + '"'
@@ -891,6 +907,7 @@ function openEditor(k) {
editField = detailRows[k].e || ''; editField = detailRows[k].e || '';
if (!editField) return; if (!editField) return;
var line = row.parentNode; var line = row.parentNode;
line.classList.toggle('wrap', !!detailRows[k].m);
line.innerHTML = editorControl(k) line.innerHTML = editorControl(k)
// Show n'a de sens que sur un champ masque. Sur un site en clair il // Show n'a de sens que sur un champ masque. Sur un site en clair il
// n'aurait rien a reveler - et l'inverse : il le CACHERAIT. // n'aurait rien a reveler - et l'inverse : il le CACHERAIT.