CogitoWeb

6 luglio 2010

Box Model: alla fine avevano ragione “loro”

Filed under: CSS — MaxArt @ 02:14
Tags: , , ,
Se vi dovessero chiedere la lunghezza della vostra auto, molto probabilmente voi prendereste un metro e misurereste la distanza tra il paraurti anteriore e quello posteriore. Non vi verrebbe mai in mente di dire che la lunghezza è la distanza tra il cruscotto ed il lunotto posteriore. Similmente, se vi chiedessero le dimensioni di una scatola di scarpe, voi la misurereste dai bordi esterni, e non quelli interni sebbene la differenza sia minima.

A questo devono aver pensato alla Microsoft tanto, tanto tempo fa, sviluppando la prima versione di Internet Explorer (era il 1995). La domanda era: come dobbiamo intendere le dimensioni di un elemento HTML? A cosa si definiscono i valori di width e di height? Come qualsiasi sviluppatore web saprà, un elemento HTML è caratterizzato da tre “spessori”:

  • margin: si tratta di uno spazio vuoto che viene frapposto tra l’elemento e quelli che lo circondano;
  • border: il bordo dell’elemento, disegnato in vari modi e colori;
  • padding: un ulteriore spazio che specifica la distanza tra il bordo dell’elemento ed il suo contenuto.

L’idea di Microsoft era che width e height si dovessero riferire alle dimensioni dell’elemento includendo bordi e padding, andando cioè così a delimitare l’area effettivamente visibile dell’elemento. Questa in effetti pare la scelta più logica, ma così non sembrò alla Netscape, sviluppatore del browser concorrente Navigator: per loro, la scelta migliore era definire le dimensioni come quelle del contenuto dell’elemento. Fu così che, per Internet Explorer, un elemento largo 100 pixel, con 5 pixel di bordo e 10 di padding, visualmente era largo effettivamente 100 pixel, mentre era largo 130 pixel per Netscape Navigator. E non c’era modo di conciliare la cosa: inutile dire che era un brutto periodo per i webmaster…

A questo si aggiunse che il consorzio W3 per la definizione degli standard web decise, a sorpresa, di dare ragione all’idea di Netscape: larghezza e altezza sono quelli del contenuto e non della scatola. Beh, poco male: in fondo l’importante era che venisse presa una decisione definitiva, perché in sostanza i due box model sono equivalenti, giusto? Sbagliato! Perché l’uso del box model di casa Microsoft, oltre ad essere intuitivamente più ragionevole, consente di “giocare” molto più liberamente con le dimensioni, permettendo in sostanza di mischiare le unità di misura. Cosa vuol dire questo?

Supponiamo di voler mettere un elemento <div> all’interno di un altro, in modo che occupi tutto lo spazio possibile in larghezza. Se conoscessimo la dimensione in pixel (o qualche altra unità di misura) la cosa sarebbe banale, ma in caso contrario si può sempre ricorrere alle percentuali, in questo modo:

<div>
   ...
   <div style="width: 100%">Testo inserito</div>
   ...
</div>

L’effetto è quello voluto:

Testo inserito

Ma se volessimo definire un padding di qualche pixel all’interno del div interno? Cosa accadrebbe? Forse qualcuno avrà già intuito il problema, ma partiamo direttamente dal risultato:

Testo inserito

Il <div> ha sbordato! E, per la precisione, di 20 pixel (è stato messo un padding di 10 pixel). Tutto questo, a ben pensarci, è ovvio: l’indicazione width: 100% si riferirà all’area del contenuto, ed è quella che sarà definita larga quanto l’elemento contenitore. La definizione del padding aggiunge in sostanza uno spessore esterno a quest’area, ed il risultato è così spiegato. Col box model di Microsoft, invece, non avremmo avuto questo problema.

Inutile pensarci su: non c’è alcun modo di risolvere questo problema, almeno con il solo uso dei fogli di stile. Se abbiamo usato le percentuali, saremo costretti a mettere dei padding con le percentuali, sottraendo il loro valore da quello di width. L’unico modo per ottenere l’effetto desiderato è quello di calcolare trmite JavaScript la larghezza in pixel dell’elemento contenitore ed in base a questo definire la larghezza del nostro <div> interno. Bella rogna, eh? Tutto per un effetto così semplice. E la cosa diventa ancora più ridicola se magari l’elemento contenitore ha una larghezza variabile e modificabile dall’utente (ad esempio, in un sito la cui struttura è stata definita con le percentuali): per ottenere un effetto di fluidità, bisogna effettuare un ricalcolo “al volo” delle dimensioni, con evidenti rallentamenti in caso di computer più vecchi (o di uno smartphone), quando un lavoro del genere è tipico del motore di rendering del browser, solitamente ben più efficiente.

Un altro esempio potrebbe essere, per l’appunto, quello di un sito la cui struttura è stata sviluppata usando le percentuali. Se volessimo avere, ad esempio, due sezioni larghe al 50% una accanto all’altra, non potremmo metterci né bordi, né padding, perché alla fine risulterebbero più larghe della metà della pagina e quindi la seconda andrebbe “a capo” (cioè sotto la prima).

Nel 2001, Internet Explorer 6 venne pubblicato e garantì il supporto al box model definito dal W3C, a patto che venisse messa una dichiarazione <!DOCTYPE> all’inizio del documento; nel caso contrario, usava il box model tradizionale di casa Microsoft. Gran parte dei webmaster si adeguarono al W3C, ma almeno da questo punto di vista fu una gran perdita. Tant’è vero che ora il W3C sembra essere tornato sui suoi passi, o quantomeno è intenzionato a lasciare la libertà agli sviluppatori di decidere che box model usare: con le nuove specifiche CSS3, infatti, verrà introdotta la proprietà box-sizing, che potrà assumere i valori content-box (di default, per lo standard W3C) o border-box (per il box model tradizionale).

Purtroppo, le specifiche del CSS3 sembrano non diventare mai definitive, ed ogni volta la loro uscita viene ritardata. Mentre le specifiche CSS1 vennero rilasciate nel 1996 e quelle CSS2 nel 1998, le specifiche CSS3 videro cominciare il loro sviluppo nel 1999 ma a distanza di ben 11 anni ancora non sono definitive. Fortunatamente, il W3C pubblica di continuo i lavori di sviluppo e molti browser si sono già adattati ad usare parte delle nuove specifiche già prima del tempo (col rovescio della medaglia per cui talvolta i risultati sono errati o fuori standard). In particolare, box-sixing (col prefisso -moz-) è stata adottata da Firefox già dalla versione 1.0, da Safari (col prefisso -webkit-) dalla versione 3, da Chrome (sempre con -webkit-), da Opera dalla versione 8.5 e Internet Explorer 8 (questi due senza prefissi). Ironicamente, quindi, proprio Internet Explorer è stato l’ultimo browser a seguire il “ritorno alle origini”…

Se avete uno dei suddetti browser, il risultato dovrebbe assomigliare a questo:

Questo è il codice relativo:

<div>
   ...
   <div style="width: 100%; box-sizing: border-box; -moz-box-sizing: border-box; -webkit-box-sizing: border-box;">Testo inserito</div>
   ...
</div>

Si noti la specifica multipla della proprietà per Firefox e Safari/Chrome: in futuro non sarà più necessaria.

Nota: per modificare la proprietà con JavaScript, si agisce sulla proprietà boxSizing dell’oggetto di stile (MozBoxSizing in Firefox, WebkitBoxSizing in Safari/Chrome). Si noti che, sebbene sia uguale a “” e non a undefined, in Internet Explorer 7 la proprietà non è supportata!

3 luglio 2010

addEvent: una (altra) soluzione / a(nother) solution

Filed under: JavaScript — MaxArt @ 14:36
Tags: ,
English Version English version 

Una delle differenze più importanti tra il codice JavaScript che si scrive per Internet Explorer e quello per un browser aderente alle direttive W3C consiste nei metodi che si usano per definire un evento su un elemento della struttura della pagina. Mentre Internet Explorer usa i metodi attachEvent e removeEvent, gli standard indicano la strada in addEventListener e removeEventListener, rispettivamente, come funzioni per aggiungere e togliere un evento. Al di là delle differenze di funzionamento degli eventi tra i browser (Internet Explorer non supporta l’event capturing, motivo per il quale, del resto, è una tecnica poco sfruttata), i due metodi fanno pressoché la stessa cosa. Ma questo non vuol dire che fanno la stessa cosa.

Quello che si vorrebbe, magari, è una funzione che possa utilizzare l’uno o l’altro metodo in automatico a seconda del supporto che viene fornito dal browser. Una soluzione largamente adottata fu fornita da Scott Andrew LePera oltre 9 anni fa:

function addEvent(obj, evType, fn, useCapture){
  if (obj.addEventListener){
    obj.addEventListener(evType, fn, useCapture);
    return true;
  } else if (obj.attachEvent){
    var r = obj.attachEvent("on"+evType, fn);
    return r;
  } else {
    alert("Handler could not be attached");
  }
}

Questa semplice soluzione, insieme con la controparte removeEvent, funziona discretamente fino ad un certo punto. La differenza tra addEventListener e attachEvent consiste nel fatto che, nella funzione passata come gestore dell’evento, secondo gli standard la parola chiave this deve riferirsi, come logica vorrebbe, all’elemento HTML su cui è definito l’evento, ma in Internet Explorer this fa riferimento all’oggetto window. Una bella seccatura!

Un primo approccio alla soluzione

Una soluzione consiste nell’usare un wrapping del gestore dell’evento nel caso di Internet Explorer:

...
  } else if (obj.attachEvent){
    var r = obj.attachEvent("on"+evType, function() {fn.call(obj)});
  ...

Grazie all’uso di call, siamo sicuri che this all’interno della funzione farà riferimento all’elemento su cui è definito l’evento e non all’oggetto window. Basta questo? Macché! Questo può bastare finché gli eventi così definiti non debbano essere in seguito eliminati con removeEvent, perché il codice relativo è questo:

  obj.detachEvent("on"+evType, fn);

Ebbene, la rimozione dell’evento non funzionerà, perché si cercherà di rimuovere la funzione fn come gestore, ma non è tale funzione ad essere stata definita come gestore dell’evento, bensì il suo wrapper function() {fn.call(obj)}! Dovremmo quindi dare il riferimento al wrapper della funzione, ma questo ci è impossibile una volta usciti dalla funzione addEvent.

L’idea di John Resig

A tale proposito, il guru Peter-Paul Koch, autore del fenomenale (per quantità di risorse) sito quirksmode.org, indispensabile per aiutare tutti gli sviluppatori del web a districarsi tra le differenze di comportamento dei più comuni browser, nel 2005 indisse un concorso per definire un’accoppiata di funzioni addEvent/removeEvent che risolvesse la questione di this in Internet Explorer.

Il concorso fu vinto da un altro guru, John Resig, l’autore della notissima libreria jQuery. Questo è il codice che ha usato:

function addEvent( obj, type, fn ) {
  if ( obj.attachEvent ) {
    obj['e'+type+fn] = fn;
    obj[type+fn] = function(){obj['e'+type+fn]( window.event );}
    obj.attachEvent( 'on'+type, obj[type+fn] );
  } else
    obj.addEventListener( type, fn, false );
}
function removeEvent( obj, type, fn ) {
  if ( obj.detachEvent ) {
    obj.detachEvent( 'on'+type, obj[type+fn] );
    obj[type+fn] = null;
  } else
    obj.removeEventListener( type, fn, false );
}

Comincio col dire che la soluzione, ovviamente, funziona benissimo. Anche in questo caso si va costruire un wrapper della funzione originale (obj['e'+type+fn]), che viene però richiamato come funzione locale. Ho però alcune osservazioni a riguardo:

  • le funzioni eseguono un controllo sulla presenza di attachEvent/detachEvent ogni volta che vengono chiamate: questo è superfluo, perché è chiaro se tali metodi saranno presenti una volta, lo saranno sempre (non è che il motore JavaScript cambia nel bel mezzo dell’esecuzione!)
  • in ogni caso viene effettuato prima un controllo su attachEvent e non su addEventListener, come sarebbe più logico (prima gli standard);
  • mi è sempre parso un metodo piuttosto invasivo, che “sporca” l’elemento HTML con la definizione di attributi (perché così vengono considerati da Internet Explorer i membri definiti gli elementi HTML) con nomi assurdamente lunghi, con garanzia di unicità non completa e per giunta non vengono ripuliti del tutto da removeEvent.

Si tratta, si badi, di argomentazioni abbastanza secondarie, se non la prima che può avere un certo marginale impatto prestazionale. Tuttavia, credo che si possano risolvere in toto.

Una nuova soluzione

Questa è l’alternativa che propongo:

if (document.addEventListener) {
  addEvent=function addEvent(obj, type, fn) {obj.addEventListener(type, fn, false);}
  removeEvent=function removeEvent(obj, type, fn) {obj.removeEventListener(type, fn, false);}
} else {
  addEvent = function addEvent(obj, type, fn) {
    fn[type+fn] = function() {return fn.call(obj, window.event);};
    obj.attachEvent("on"+type, fn[type+fn]);
  };
  removeEvent = function removeEvent(obj, type, fn) {obj.detachEvent("on"+type, fn[type+fn]);}
}

La differenza con la soluzione di John Resig sta nel fatto che la mia versione di addEvent va a salvare il riferimento al wrapper non sull’oggetto elemento HTML, ma sull’oggetto fn che viene passato come argomento. In questo modo, removeEvent sa sempre dove andare a prendere il wrapper e rimuoverlo.

Posso dire di essere piuttosto sorpreso di aver sviluppato qualcosa che mi sia piaciuto più della versione di un mostro sacro come John Resig, ed ogni tanto mi chiedo se ciò che ho scritto non abbia delle controindicazioni a me non visibili. Finora, però, non m’è parso.


addEvent: a(nother) solution


One of the main differences between the JavaScript code you write per Internet Explorer and the one for a W3C standard compliant browser is about the methods you use to define an event on an element of the page structure. While Internet Explorer uses the methods attachEvent and removeEvent, the standards have their way in addEventListener and removeEventListener, rispectively, ad function to add and remove an event. Leaving apart the working differences among browsers (Internet Explorer doesn’t support event capturing, and that’s actually the reasing why this technique is not used often), these two methods almost do the same thing. But this doesn’t mean that they do the same thing.

What we actually want is a function that calls one method or the other automatically, depending on the browser support. A widely used solution was given by Scott Andrew LePera more than 9 years ago:

function addEvent(obj, evType, fn, useCapture){
  if (obj.addEventListener){
    obj.addEventListener(evType, fn, useCapture);
    return true;
  } else if (obj.attachEvent){
    var r = obj.attachEvent("on"+evType, fn);
    return r;
  } else {
    alert("Handler could not be attached");
  }
}

This simple solution, together with the removeEvent counterpart, works fairly well under certain conditions. The difference between addEventListener and attachEvent is in the fact that, while running the function passed as event listener, by the standards the key word this must refer, as logic should hint, to the HTML element on which the event is defined, but on Internet Explorer this always refers to the window object. A real pain!

<h3A first approach to the solution

A solution consists of the using of a wrapper of the event listener in the Internet Explorer case:

...
  } else if (obj.attachEvent){
    var r = obj.attachEvent("on"+evType, function() {fn.call(obj)});
  ...

Thanks to call, we’re sure that this inside the function will refer to the element on which the event is defined and not to the window object. Is that enough? Not at all! This can be enough until the events defined in this way are not to be removed later by removeEvent, because this is the relative part:

  obj.detachEvent("on"+evType, fn);

Therefore, removing the event will not work, because it will try to remove the function fn as the listener, but that not the function defined as the event listener, but rather it’s its wrapper function() {fn.call(obj)}! So we should give the reference to the function wrapper, but that’s impossible once we’ve returned from the addEvent function.

John Resig’s idea

By this way, guru Peter-Paul Koch, author of the fantastic (for resource quantity) site quirksmode.org, essential to help web developers to have their way among behaviour differences of the most common browsers, in 2005 called a contest to define an addEvent/removeEvent functions couple to solve the this problem in Internet Explorer.

Not surprisingly, the contest was won by another guru, John Resig, author of the arch-famous library jQuery. This is the code he used:

function addEvent( obj, type, fn ) {
  if ( obj.attachEvent ) {
    obj['e'+type+fn] = fn;
    obj[type+fn] = function(){obj['e'+type+fn]( window.event );}
    obj.attachEvent( 'on'+type, obj[type+fn] );
  } else
    obj.addEventListener( type, fn, false );
}
function removeEvent( obj, type, fn ) {
  if ( obj.detachEvent ) {
    obj.detachEvent( 'on'+type, obj[type+fn] );
    obj[type+fn] = null;
  } else
    obj.removeEventListener( type, fn, false );
}

Let’s start to say that the solution, obviously, works just fine. In this case, too, we build a wrapper of the original function (obj['e'+type+fn]), which is instead called as a “local” method. I have some notes about it:

  • those functions checks the presence of addEvent/removeEvent every time they’re called: this is redundant, because it’s obvious that if those methods are definded once, they always are (you don’t change the JavaScript engine in the middle of the execution!)
  • anyway, attachEvent is checked first, not addEventListener, as logic would suggest (standards first);
  • it always seemed to me an obtrusive way, that “spoils” the HTML element defining attributes (because it’s the way Internet Explorer deals with members defined on HTML elements) with ridicously long names, without the complete uniqueness warranty and, moreover, not entirely cleaned by removeEvent.

These are, mind you, quite minor arguments, with the exception of the first one which can have some marginal performance influence. Anyway, I think that they can be completely solved.

A new solution

This is the alternative I propose:

if (document.addEventListener) {
  addEvent=function addEvent(obj, type, fn) {obj.addEventListener(type, fn, false);}
  removeEvent=function removeEvent(obj, type, fn) {obj.removeEventListener(type, fn, false);}
} else {
  addEvent = function addEvent(obj, type, fn) {
    fn[type+fn] = function() {return fn.call(obj, window.event);};
    obj.attachEvent("on"+type, fn[type+fn]);
  };
  removeEvent = function removeEvent(obj, type, fn) {obj.detachEvent("on"+type, fn[type+fn]);}
}

The difference with the solution of John Resig is that my version of addEvent saves the wrapper reference not on the HTML element but rather on the fn object that is passed as argument. This way, removeEvent always knows where to get the wrapper and remove it.

I must say I’m quite surprised to have developed something that I liked more than the version of a sacred genius as John Resig, and sometimes I wonder if what I wrote has some issues that I don’t see. It didn’t seem to me… so far.

Blog su WordPress.com.