fix(entries): size the multiline field from its content instead of a fixed height
A manual resize was lost on reopen: the row is rebuilt on every open, and remembering the height would not survive a restart either since localStorage is wiped when the ephemeral port changes the origin. Deriving it from the content removes the state entirely - the field is always the right size.
This commit is contained in:
+1
-1
@@ -78,7 +78,7 @@ Préparer : 3 entrées `A`, `B`, `C`, dont `A` dans le dossier `Work` avec le ta
|
|||||||
|
|
||||||
### A12 · pas de fausse alerte (non-régression d'A11)
|
### A12 · pas de fausse alerte (non-régression d'A11)
|
||||||
Aucun de ces enchaînements ne doit afficher « Entry changed elsewhere » :
|
Aucun de ces enchaînements ne doit afficher « Entry changed elsewhere » :
|
||||||
- **A12a** : `A` ouverte + dirty → Save → modifier encore → Save (le rechargement post-save re-cale la référence)
|
- **A12a** : `A` ouverte + dirty → Save → modifier encore → Save (le rechargement post-save re-cale la référence)✅
|
||||||
- **A12b** : `A` ouverte + dirty → batch *Add tag* / *Move to folder* sur `A` → Save (l'éditeur a absorbé le changement)
|
- **A12b** : `A` ouverte + dirty → batch *Add tag* / *Move to folder* sur `A` → Save (l'éditeur a absorbé le changement)
|
||||||
- **A12c** : `A` ouverte + dirty → supprimer son dossier → Save (cf. A7)
|
- **A12c** : `A` ouverte + dirty → supprimer son dossier → Save (cf. A7)
|
||||||
- **A12d** : `A` ouverte + dirty → *Fetch missing* favicons → Save (l'icône est hors payload éditeur)
|
- **A12d** : `A` ouverte + dirty → *Fetch missing* favicons → Save (l'icône est hors payload éditeur)
|
||||||
|
|||||||
+2
-1
@@ -1604,11 +1604,12 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
/* Multiline custom field (Notes). The row stays a grid, so the textarea just
|
/* Multiline custom field (Notes). The row stays a grid, so the textarea just
|
||||||
grows in its own column and the buttons keep their places. */
|
grows in its own column and the buttons keep their places. */
|
||||||
.so-custom-multiline {
|
.so-custom-multiline {
|
||||||
min-height: 72px;
|
min-height: 56px;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
overflow-y: auto; /* JS caps the auto-grow; past that it scrolls */
|
||||||
}
|
}
|
||||||
/* Editable combobox for option-backed custom fields (card brand, expiry
|
/* Editable combobox for option-backed custom fields (card brand, expiry
|
||||||
year…). Input fills the cell; the arrow drops a menu of ALL options. */
|
year…). Input fills the cell; the arrow drops a menu of ALL options. */
|
||||||
|
|||||||
@@ -4161,6 +4161,12 @@ function soCustomFieldsField() {
|
|||||||
return wrap;
|
return wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Grow a textarea to fit its content, up to a cap past which it scrolls.
|
||||||
|
function autoSizeTextarea(ta, maxPx = 320) {
|
||||||
|
ta.style.height = 'auto';
|
||||||
|
ta.style.height = Math.min(ta.scrollHeight, maxPx) + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
function buildCustomFieldRow(field, idx, rerender) {
|
function buildCustomFieldRow(field, idx, rerender) {
|
||||||
const row = el('div', { class: 'so-custom-row' });
|
const row = el('div', { class: 'so-custom-row' });
|
||||||
const labelInput = el('input', {
|
const labelInput = el('input', {
|
||||||
@@ -4189,10 +4195,18 @@ function buildCustomFieldRow(field, idx, rerender) {
|
|||||||
if (field.multiline) {
|
if (field.multiline) {
|
||||||
valueInput = el('textarea', {
|
valueInput = el('textarea', {
|
||||||
class: 'so-input so-custom-value so-custom-multiline',
|
class: 'so-input so-custom-value so-custom-multiline',
|
||||||
rows: 5, placeholder: 'Value',
|
rows: 3, placeholder: 'Value',
|
||||||
autocomplete: 'off', autocorrect: 'off', spellcheck: 'false',
|
autocomplete: 'off', autocorrect: 'off', spellcheck: 'false',
|
||||||
});
|
});
|
||||||
valueInput.classList.toggle('is-masked', !!field.is_secret);
|
valueInput.classList.toggle('is-masked', !!field.is_secret);
|
||||||
|
// Height follows the content instead of being remembered: a manual
|
||||||
|
// resize would be lost anyway (re-rendered on every open, and
|
||||||
|
// localStorage is wiped each launch when the port changes).
|
||||||
|
const grow = () => autoSizeTextarea(valueInput);
|
||||||
|
valueInput.addEventListener('input', grow);
|
||||||
|
// scrollHeight reads 0 until the element is in the DOM — same
|
||||||
|
// render-time vs DOM-attachment trap as renderSoChips (CLAUDE.md).
|
||||||
|
setTimeout(grow, 0);
|
||||||
} else {
|
} else {
|
||||||
valueInput = el('input', {
|
valueInput = el('input', {
|
||||||
type: field.is_secret ? 'password' : 'text',
|
type: field.is_secret ? 'password' : 'text',
|
||||||
|
|||||||
Reference in New Issue
Block a user