15 dicembre 2021

Coordinate

Per spostare gli elementi dovremmo prendere familiarità con le coordinate.

La gran parte dei metodi JavaScript ha a che fare con uno di questi due sistemi di coordinate:

  1. Coordinate relative alla finestra – paragonabili a position:fixed, calcolate dal bordo superiore sinistro della finestra.
    • indicheremo queste coordinate con clientX/clientY, il ragionamento per tale nome diventerà evidente in seguito, quando studieremo le proprietà degli eventi.
  2. Coordinate relative al documento – paragonabili a position:absolute riferito alla radice del documento, calcolate dal bordo superiore sinistro del documento.
    • le indicheremo con pageX/pageY.

Quando ci troviamo all’inizio della pagina, così che l’angolo superiore sinistro della finestra coincide esattamente con l’angolo superiore sinistro del documento, queste coordinate sono uguali tra loro. Ma dopo che si scorre la pagina, le coordinate relative alla finestra cambiano via via che gli elementi si spostano all’interno di questa, mentre le coordinate relative al documento rimangono invariate.

In questa immagine consideriamo un punto nel documento e mostriamo le sue coordinate prima dello scorrimento (riquadro a sinistra) e dopo di esso (riquadro a destra):

Quando il documento scorre:

  • pageY – la coordinata relativa al documento non cambia, si prende a riferimento la parte superiore del documento (che ora fuori dall’area visibile di scorrimento).
  • clientY – la coordinata relativa alla finestra è cambiata (la freccia è diventata più corta), dal momento che lo stesso punto è più vicino al bordo superiore della finestra.

Le coordinate di un elemento: getBoundingClientRect

Il metodo elem.getBoundingClientRect() restituisce le coordinate relative alla finestra del rettangolo minimo che racchiude elem come oggetto della classe nativa DOMRect.

Ecco le principali proprietà di DOMRect:

  • x/y – le coordinate X/Y dell’origine rettangolo relative alla finestra,
  • width/height – larghezza/altezza del rettangolo (possono avere valori negativi).

Ci sono, inoltre, proprietà derivate:

  • top/bottom – la coordinata Y per i bordi superiore/inferiore del rettangolo,
  • left/right – la coordinata X per i bordi sinistro/destro del rettangolo.

Clicca, per esempio, su questo pulsante per conoscere le sue coordinate relative alla finestra:

Se scorrete la pagina e ripetete il test, noterete che quando cambia la posizione relativa alla finestra del pulsante, cambiano anche le sue coordinate relative alla finestra (y/top/bottom se scorri verticalmente).

Di seguito un’immagine descrittiva dell’output di elem.getBoundingClientRect():

Come potete osservare, x/y e width/height descrivono pienamente il rettangolo. A partire da queste si possono calcolare agevolmente le proprietà derivate:

  • left = x
  • top = y
  • right = x + width
  • bottom = y + height

Nota bene:

  • Le coordinate possono avere valori decimali, come 10.5. È normale, il browser internamente usa frazioni nei calcoli. Non dobbiamo arrotondare quando assegniamo i valori a style.left/top.
  • Le coordinate possono essere negative. Per esempio se la pagina scorre in modo che elem sia al di sopra del bordo della finestra, allora elem.getBoundingClientRect().top è negativa.
Perché le proprietà derivate sono necessarie? Perché esistono top/left se ci sono già x/y?

In matematica un rettangolo è definito unicamente dalla sua origine (x,y) e dal vettore di direzione (width,height). Le proprietà aggiuntive derivate esistono quindi per comodità.

Tecnicamente è possibile per width/height essere negativi in base alla “direzione” del rettangolo, ad esempio per rappresentare la selezione del mouse con l’inizio e la fine contrassegnati adeguatamente.

Valori negativi per width/height comportano che il rettangolo abbia inizio dal suo angolo in basso a destra e si sviluppi a sinistra verso l’alto.

Ecco un rettangolo con width e height negativi (es. width=-200, height=-100):

Come potete notare, in casi del genere left/top non sono equivalenti a x/y.

Ma in pratica elem.getBoundingClientRect() restituisce sempre valori positivi per width/height. Qui menzioniamo i valori negativi per width/height solo per farvi comprendere il motivo per cui queste proprietà apparentemente duplicate in realtà non lo siano.

Internet Explorer non supporta x/y

Internet Explorer non supporta le proprietà x/y per ragioni storiche.

Possiamo quindi ricorrere ad un polyfill (aggiungendo dei getter in DomRect.prototype) o utilizzare semplicemente top/left, dal momento che, queste ultime, corrispondono sempre a x/y per i valori positivi di width/height restituiti da elem.getBoundingClientRect().

Le coordinate right/bottom sono differenti dalle proprietà di posizione CSS

Ci sono delle evidenti somiglianze tra le coordinate relative alla finestra e position:fixed dei CSS.

Nel posizionamento CSS, tuttavia, la proprietà right indica la distanza dal bordo destro, e la proprietà bottom indica la distanza dal bordo in basso.

Se diamo una semplice occhiata all’immagine sopra, possiamo notare che in JavaScript non è così. Tutte le coordinate relative alla finestra sono calcolate a partire dall’angolo superiore sinistro e queste non fanno eccezione.

elementFromPoint(x, y)

La chiamata a document.elementFromPoint(x, y) restituisce l’elemento più annidato alle coordinate (x, y) relative alla finestra.

La sintassi è:

let elem = document.elementFromPoint(x, y);

Il codice sotto, ad esempio, evidenzia e mostra il tag dell’elemento che si trova adesso al centro della finestra:

let centerX = document.documentElement.clientWidth / 2;
let centerY = document.documentElement.clientHeight / 2;

let elem = document.elementFromPoint(centerX, centerY);

elem.style.background = "red";
alert(elem.tagName);

Dal momento che usa le coordinate relative alla finestra, l’elemento può variare in base alla posizione di scorrimento corrente.

Per coordinate al di fuori della finestra elementFromPoint restituisce null

Il metodo document.elementFromPoint(x,y) funziona solo se (x,y) si trovano all’interno dell’area visibile.

Se una delle coordinate è negativa o eccede le dimensioni della finestra, restituisce null.

Ecco un tipico errore che può verificarsi se non prestiamo attenzione a questa eventualità:

let elem = document.elementFromPoint(x, y);
// se le coordinate sono fuori dalla finestra elem = null
elem.style.background = ''; // Error!

Utilizzo con il posizionamento “fixed”

La maggior parte delle volte per posizionare qualcosa abbiamo bisogno delle coordinate.

Per mostrare qualcosa vicino un elemento, possiamo usare getBoundingClientRect per ricavare le sue coordinate e successivamente utilizzare la proprietà CSS position insieme a left/top (o right/bottom).

Per esempio la funzione createMessageUnder(elem, html) in basso, mostra un messaggio sotto elem:

let elem = document.getElementById("coords-show-mark");

function createMessageUnder(elem, html) {
  // crea l'elemento messaggio
  let message = document.createElement('div');
  // per assegnare degli stili sarebbe preferibile usare una classe CSS
  message.style.cssText = "position:fixed; color: red";

  // assegna le coordinate, non dimenticare "px"!
  let coords = elem.getBoundingClientRect();

  message.style.left = coords.left + "px";
  message.style.top = coords.bottom + "px";

  message.innerHTML = html;

  return message;
}

// Esempio d'uso:
// aggiunge il messaggio al documento per 5 secondi
let message = createMessageUnder(elem, 'Hello, world!');
document.body.append(message);
setTimeout(() => message.remove(), 5000);

Clicca il pulsante per eseguire:

Il codice può essere modificato per mostrare il messaggio a sinistra, a destra, sopra, per applicare animazioni CSS di dissolvenza e così via. Dal momento che disponiamo di tutte le coordinate e dimensioni dell’elemento, è piuttosto semplice.

Fate attenzione, tuttavia, ad un dettaglio importante: quando la pagina scorre, il pulsante si allontana dal messaggio.

Il motivo è ovvio: il messaggio si basa su position:fixed, quindi rimane nello stessa posizione relativamente alla finestra mentre la pagina scorre via.

Per cambiare questo comportamento, dobbiamo usare coordinate relative al documento e position:absolute.

Coordinate relative al documento

Le coordinate relative al documento hanno come riferimento l’angolo superiore sinistro del documento, non della finestra.

Nei CSS, le coordinate relative alla finestra corrispondono a position:fixed, mentre le coordinate relative al documento sono assimilabili a position:absolute riferito alla radice del documento.

Possiamo usare position:absolute e top/left per posizionare qualcosa in un determinato punto del documento, in modo che rimanga lì durante lo scorrimento di pagina. Ma prima abbiamo bisogno di conoscerne le coordinate corrette.

Non esiste un metodo standard per ottenere le coordinate di un elemento relative al documento, però è facile ricavarle.

I due sistemi di coordinate sono correlati dalla formula:

  • pageY = clientY + altezza della parte verticale del documento fuori dall’area visibile di scorrimento.
  • pageX = clientX + larghezza della parte orizzontale del documento fuori dall’area visibile di scorrimento.

La funzione getCoords(elem) ricaverà le coordinate relative alla finestra da elem.getBoundingClientRect() ed aggiungerà a queste lo scorrimento di pagina corrente:

// ottiene le coordinate relative al documento di un elemento
function getCoords(elem) {
  let box = elem.getBoundingClientRect();

  return {
    top: box.top + window.pageYOffset,
    right: box.right + window.pageXOffset,
    bottom: box.bottom + window.pageYOffset,
    left: box.left + window.pageXOffset
  };
}

Se nell’esempio sopra l’avessimo usata con position:absolute, il messaggio sarebbe rimasto vicino l’elemento durante lo scorrimento.

Ecco la funzione createMessageUnder adattata:

function createMessageUnder(elem, html) {
  let message = document.createElement('div');
  message.style.cssText = "position:absolute; color: red";

  let coords = getCoords(elem);

  message.style.left = coords.left + "px";
  message.style.top = coords.bottom + "px";

  message.innerHTML = html;

  return message;
}

Riepilogo

Ogni punto sulla pagina ha delle coordinate:

  1. relative alla finestra – elem.getBoundingClientRect().
  2. relative al documento – elem.getBoundingClientRect() più lo scorrimento di pagina corrente.

Le coordinate relative alla finestra sono ottime per un utilizzo con position:fixed e le coordinate relative al documento vanno bene con position:absolute.

Entrambi i sistemi di coordinate hanno i loro vantaggi e svantaggi; ci sono circostanze in cui abbiamo bisogno dell’uno o dell’altro, proprio come per la proprietà CSS position absolute e fixed.

Esercizi

importanza: 5

Nell’iframe sotto potete osservare un documento con un “campo” verde.

Usate JavaScript per trovare le coordinate relative alla finestra degli angoli indicati dalle frecce.

Per comodità è stata implementata una semplice funzionalità nel documento: un click in un punto qualsiasi mostrerà le coordinate.

Il vostro codice dovrebbe usare il DOM per ottenere le coordinate relative alla finestra di:

  1. angolo esterno superiore sinistro (è semplice).
  2. angolo esterno inferiore destro (semplice anche questo).
  3. angolo interno superiore sinistro (un po’ più difficile).
  4. angolo interno inferiore destro (esistono vari modi, sceglietene uno).

Le coordinate che calcolate dovrebbero essere le stesse di quelle mostrate al click del mouse.

P.S. Il codice dovrebbe funzionare anche se l’elemento ha un’altra dimensione o un altro bordo, non deve dipendere da valori fissi.

Apri una sandbox per l'esercizio.

Angoli esterni

Gli angoli esterni sono fondamentalmente quello che otteniamo da elem.getBoundingClientRect().

Le coordinate dell’angolo superiore sinistro answer1 e quelle dell’angolo inferiore destro answer2:

let coords = elem.getBoundingClientRect();

let answer1 = [coords.left, coords.top];
let answer2 = [coords.right, coords.bottom];

Angolo interno superiore sinistro

Questo differisce dall’angolo esterno solo per la larghezza del bordo. Un modo affidabile per calcolare la distanza è clientLeft/clientTop:

let answer3 = [coords.left + field.clientLeft, coords.top + field.clientTop];

Angolo interno inferiore destro

Nel nostro caso possiamo sottrarre la misura del bordo dalle coordinate esterne.

Potremmo utilizzare il valore CSS:

let answer4 = [
  coords.right - parseInt(getComputedStyle(field).borderRightWidth),
  coords.bottom - parseInt(getComputedStyle(field).borderBottomWidth)
];

Un’alternativa sarebbe aggiungere clientWidth/clientHeight alle coordinate dell’angolo interno superiore sinistro. Questa è probabilmente la soluzione migliore:

let answer4 = [
  coords.left + elem.clientLeft + elem.clientWidth,
  coords.top + elem.clientTop + elem.clientHeight
];

Apri la soluzione in una sandbox.

importanza: 5

Create una funzione positionAt(anchor, position, elem) che posizioni elem vicino l’elemento anchor in base a position.

Il parametro position deve essere una stringa con uno dei 3 valori seguenti:

  • "top" – posiziona elem proprio sopra anchor
  • "right" – posiziona elem subito a destra di anchor
  • "bottom" – posiziona elem esattamente sotto anchor

Il codice che scriverete viene richiamato dalla funzione showNote(anchor, position, html), che trovate nel codice sorgente dell’esercizio e che crea una nota con l’html passato come parametro e lo mostra nella posizione assegnata position vicino all’elemento anchor.

Ecco un esempio:

Apri una sandbox per l'esercizio.

In questo esercizio dobbiamo solo calcolare accuratamente le coordinate. Guardate il codice per i dettagli.

Nota bene: gli elementi devono essere visibili nel documento per leggere offsetHeight e le altre proprietà. Un elemento nascosto (display:none) o fuori dal documento non ha dimensioni.

Apri la soluzione in una sandbox.

importanza: 5

Modificate la soluzione dell’esercizio precedente affinché la nota utilizzi position:absolute invece di position:fixed.

In questo modo eviteremo che si allontani dall’elemento quando la pagina scorre.

Prendete la soluzione dell’esecizio precedente come punto di partenza. Per testare lo scorrimento aggiungete lo stile <body style="height: 2000px">.

La soluzione è in realtà piuttosto semplice:

  • Utilizzate position:absolute nel file CSS invece di position:fixed per .note.
  • Utilizzate la funzione getCoords() della sezione Coordinate per ottenere le coordinate relative al documento.

Apri la soluzione in una sandbox.

importanza: 5

Estendete l’esercizio precedente Mostrate una nota vicino l'elemento (position:absolute): fate in modo che la funzione positionAt(anchor, position, elem) inserisca elem all’interno di anchor.

Nuovi valori per position:

  • top-out, right-out, bottom-out – funzionano come prima, inseriscono elem sopra/a destra/sotto anchor.
  • top-in, right-in, bottom-in – inseriscono elem all’interno di anchor: posizionandolo nel bordo superiore/a destra/inferiore.

Per esempio:

// mostra la nota sopra blockquote
positionAt(blockquote, "top-out", note);

// mostra la nota all'interno di blockquote, nel bordo superiore
positionAt(blockquote, "top-in", note);

Ecco il risultato:

Prendete la soluzione dell’esercizio Mostrate una nota vicino l'elemento (position:absolute) come codice sorgente.

Mappa del tutorial