No. 216

Un'area di testo che si espande è un campo di inserimento testo multi-riga che si estende in altezza fino a contenere tutto il suo contenuto. Questo elemento di UI si trova comunemente nelle applicazioni sia desktop sia mobile, come il campo di composizione degli SMS su iPhone. Si possono trovare degli esempi nel Web, anche su Facebook, dove è molto usato. E' una buona scelta ovunque non sappiate esattamente quanto testo verrà inserito dall'utente e vogliate mantenere il layout compatto. In quanto tale, è specialmente utile nelle interfacce per smartphone.

Nonostante l'ubiquità di questo controllo, non c'è un modo per crearlo usando solo HTML e CSS. Mentre i normali elementi block-level (come un div, ad esempio) si espandono per adattarsi al proprio contenuto, l'umile textarea non lo fa, anche se gli assegnate lo stile display:block- Dal momento che questo è l'unico modo per accettare un input multi-linea da parte dell'utente (oltre ad usare contenteditable, un intero mondo di dolore in cui non mi avventurerò oggi), serve un po' di JavaScript per ridimensionarlo come desiderato.

Passando Internet al setaccio, potete trovare molti tentativi di creazione di text area espandibili, ma la maggior parte è affetta dai seguenti problemi:

  • L'altezza è calcolata cercando di indovinare dove possa avvenire la chiusura basandosi sull'attributo cols.
  • L'altezza è ricalcolata e impostata solo su eventi keyup (ed eventualmente cut/paste). Questo non funziona se la textarea ha una larghezza fluida e la finestra viene ridimensionata.
  • L'altezza richiesta è calcolata in base all'attributo scrollHeight, il quale non è specificato in nessuna specifica del W3C (è stato introdotto da IE) e pertanto non sorprende ci siano subdole differenze tra le varie implementazioni, che richiedono blocchi di codice inelegante e fragile per fare browser-sniffing.

La soluzione migliore che io abbia visto utilizza un elemento pre separato, posizionato assolutamente fuori dallo schermo, con lo stesso stile della textarea, che chiameremo mirror element. Utilizzanto setTimeout, la textarea viene poi interrogata ogni 200 ms circa ed ogni volta che si trova un nuovo valore, il contenuto del mirror element viene aggiornato. Questo poi si ridimensiona automaticamente per adattarsi al suo contenuto (come fa un normale elemento block-level), dopodiché potete estrarre la dimensione dalla proprietà offsetHeight ed applicarla alla textarea.

Questo metodo funziona, ma il "polling" è inefficiente, specialmente se avete più textarea. Peggio ancora, se supportate le textarea di larghezza flessibile dovete anche controllare che la larghezza della textarea non sia cambiata ogni volta che fate una "poll" (ed è costoso leggere da offsetWidth). Può essere difficile calcolare l'esatta larghezza della content-box nella textarea, perciò di solito c'è un “fudge factor” aggiunto all'altezza applicata alla textarea, proprio per essere sicuri che non sia un po' troppo corta, risultando in una box che sia poi un po' troppo grossa per il contenuto. Qui, vi mostrerò una soluzione migliore per questo problema, ossia assegnare una dimensione alla textarea utilizzando solo il più piccolo frammento di codice magico JavaScript insieme ad alcuni trucchi CSS.

La tecnica è un miglioramento del posizionamento fuori dallo schermo del mirror element. Il primo miglioramento che facciamo è collegato al modo in cui rileviamo l'input. L'evento change non è l'ideale perché si attiva solo quando la textarea perde il focus. L'evento keyup funziona il più delle volte, ma si attiva anche su eventi in cui non è stato fatto alcun vero cambiamento, come il cursore che si muove a destra e a sinistra e non si attiva su l'utente usa il mouse per fare copia/incolla. Quello che vogliamo in realtà è un evento che semplicemente si attivi ogniqualvolta cambi il valore della textarea. Fortunatamente, un tale evento esiste ed è incredibilmente utile, sebbene sia menzionato così raramente che sembrerebbe che molti non siano a conoscenza della sua esistenza. L'evento è semplicemente chiamato input e lo si usa proprio come qualunque altro evento:

textarea.addEventListener('input', function (event) {
/* Code to handle event */
}, false );

Quindi, il nostro primo miglioramento consiste nel fermare il "polling" utilizzando setTimeout ed invece usare l'evento molto più efficiente input. E' supportato da tutti i browser, perfino da Internet Explorer a partire dalla versione 9, sebbene ovviamente ci sia un bug in IE: non si attiva quando cancellate il testo, così l'area non si restringerà fino a quando non aggiungerete di nuovo del testo. Se questo vi crea problemi, potete guardare l'evento keyup in IE per coprire la maggior parte dei casi.

Per IE8 possiamo usare l'evento proprietario onpropertychange, anch'esso si attiva quando la proprietà value cambia. Questo evento è anche disponibile sulle versioni precedenti alla 8, ma probabilmente c'è bisogno di un po' di piccoli trucchi CSS per far funzionare in generale la textarea espandibile. Lascio il farlo funzionare in IE6 e IE7 come esercizio ai lettori sfortunati abbastanza da dover offrire il supporto a questi browser antichi.

Ora, alcuni di voi potrebbero aver notato che non stiamo più facendo polling: la textarea non si ridimensiona se ha una larghezza fluida e la finestra viene ridimensionata. Questo ci porta al nostro prossimo miglioramento: facciamo in modo che il browser ridimensioni la textarea automaticamente. Ma, vi sento gridare, pensavo che avreste chiesto se ciò fosse possibile. Beh, non proprio. Non potete farlo automaticamente solo con HTML e CSS, ma tutto quello che JS deve fare è aggiornare l'elemento mirror con il valore della textarea. Non deve misurare o esplicitamente impostare l'altezza. Il trucco consiste nel posizionare la textarea in cima all'elemento mirror, entrambe all'interno di un div contenitore posizionato relativamente. La textarea è posizionata assolutamente e ha una larghezza ed altezza pari al 100% per far sì che riempia il div. Il mirror è posizionato staticamente così che si espanda per adattarsi al suo contenuto. Il div contenitore si espanderà poi per adattarsi all'altezza del mirror e questo a sua volta farà in modo che la textarea posizionata assolutamente si ridimensioni per riempire il contenitore, rendendolo così dell'altezza giusta adatta al suo contenuto.

Spiegazione sufficiente, dacci il codice!

Ehi, frenate un attimo! Ci sto arrivando! E' davvero splendidamente semplice. Il markup è così:

<div class="expandingArea">
<pre><span></span><br></pre>
<textarea></textarea>
</div>

Il pre è il nostro mirror. Abbiamo bisogno di un br alla fine di questo per assicurarci che ogni spazio bianco copiato dalla textarea venga reso correttamente dal browser e non sgranocchiato. L'elemento span è perciò quello che in realtà andiamo ad aggiornare con il contenuto della textarea.

Ora, il CSS. Per prima cosa, un piccolo reset (probabilmente l'avete già scritto):

textarea, 
pre {
margin: 0;
padding: 0;
outline: 0;
border: 0;
}

Gli elementi contenitori hanno la classe expandingArea. Potete qui definire qualunque border o inset box-shadow, ecc., che volete usare per dare stile alla vostra textarea. Ho appena aggiunto un semplice border da 1px solid grigio. Potete anche impostare una proprietà min-height se volete e funzionerà come vi aspettate:

.expandingArea {
position: relative;
border: 1px solid #888;
background: #fff;
}

Potete impostare qualunque padding, line height e stile di font desideriate, assicuratevi solamente che siano gli stessi per la textarea e per l'elemento pre.

.expandingArea > textarea,
.expandingArea > pre {
padding: 5px;
background: transparent;
font: 400 13px/16px helvetica, arial, sans-serif;
/* Make the text soft-wrap */
white-space: pre-wrap;
word-wrap: break-word;
}
.expandingArea > textarea {
/* The border-box box model is used to allow
* padding whilst still keeping the overall width
* at exactly that of the containing element.
*/
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
width: 100%;
/* This height is used when JS is disabled */
height: 100px;
}
.expandingArea.active > textarea {
/* Hide any scrollbars */
overflow: hidden;
position: absolute;
top: 0;
left: 0;
height: 100%;
/* Remove WebKit user-resize widget */
resize: none;
}
.expandingArea > pre {
display: none;
}
.expandingArea.active > pre {
display: block;
/* Hide the text; just using it for sizing */
visibility: hidden;
}

Da ultimo, usiamo il seguente JavaScript. Per brevità ho omesso la solita feature detection che dovreste fare prima di usare querySelector() o querySelectorAll(). Nella miglior tradizione della graceful degradation, gli utenti con JavaScript disabilitato avranno una textarea di altezza fissa (con l'altezza impostata nel CSS), con delle scrollbar che appaiono quando il contenuto eccede:

function makeExpandingArea(container) {
var area = container.querySelector('textarea');
var span = container.querySelector('span');
if (area.addEventListener) {
area.addEventListener('input', function() {
span.textContent = area.value;
}, false);
span.textContent = area.value;
} else if (area.attachEvent) {
// IE8 compatibility
area.attachEvent('onpropertychange', function() {
span.innerText = area.value;
});
span.innerText = area.value;
}
// Enable extra CSS
container.className += ' active';
}

var areas = document.querySelectorAll('.expandingArea');
var l = areas.length;

while (l--) {
makeExpandingArea(areas[l]);
}

Una nota sulla delegation: potete facilmente impostarla con un singolo event listener nel document node ed usate l'event delegation per gestire efficientemente più text area espandibili, ma solo se non dovete supportare IE8 o inferiori, perché Microsoft, nella sua infinita saggezza, non ha implementato l'evento onpropertychange.

La demo obbligatoria


Note conclusive

A causa del modo in cui l'aggioramento della grafica viene effettuato in Opera per Mac OS X, si può verificare un leggero sfarfallio quando viene aggiunta una nuova riga al text field. Potete aggirare questo problema rendendolo sempre una riga più alta di quello che è necessario in Opera su Mac. Aggiungete semplicemente questo codice in cima alla funzione makeExpandingArea (tristemente non c'è modo di fare feature detection per questo):

if ( window.opera && /Mac OS X/.test( navigator.appVersion ) ) {
container.querySelector( 'pre' )
.appendChild(
document.createElement( 'br' )
);
}

Da ultimo, poiché la textarea è posizionata direttamente sopra a pre, potete estenderla per fare delle cose fuori dall'ordinario come sottolineare la sintassi mentre scrivete. Se fate il parsing del valore e lo dividete in diversi tag prima di aggiungerlo al pre, potete applicare diversi colori a diverse sezioni. Dovrete rimuovere la dichiarazione visibility: hidden da pre e invece aggiungere color: transparent a textarea. Usiamo questa tecnica in My Opera Mail per rendere più semplice scorrere i nomi nei campi A/CC/CCn dello schermo di composizione. L'insidia nascosta è che tutti i browser tranne Opera rendono il colore del cursore uguale al colore del testo, così il cursore sparisce quando si rende il colore trasparente. Non credo che ci sia alcuno standard W3C che copra questo argomento (per favore, fatemi sapere se ho torto), ma la piattaforma standard (basata sui text editor "che hanno a bordo") sembra essere l'opposto del colore di background in Windows e sempre nera in Mac, indipendentemente dal color del background o del foreground. Ma fino a che gli altri browser non vedranno la luce e sistemeranno questo comportamento potrete ancora applicare la sottolineatura della sintassi su blur e spegnerla mentre l'utente sta in effetti facendo editing o cambiando al contrario il colore di background.

E questo è tutto amici! Spero vi sia piaciuto leggere questo articolo e che magari abbiate imparato una tecnica utile. E' elegante ed efficiente e funziona bene tanto nei browser mobile moderni così come in quelli desktop. Felice hacking!

Illustrazioni: Carlo Brigatti

Share/Save/Bookmark
 

Discutiamone

Ti sembra interessante? Scrivi tu il primo commento


Cenni sull'autore

Neil Jenkins

Neil JenkinsNeil Jenkins lavora in Opera Software, dove dirige l'UX design e il front-end engineering per My Opera Mail. Quando non programma lo si vede spesso cantare, camminare su montagne remote o magari entrambe le cose.

Questo sito per poter funzionare utilizza i cookie. Per saperne di più visita la pagina relativa all' INFORMATIVA