Indicare la strada da seguire

Sérgio Gomes

In passato, indicare qualcosa sul web era semplice. Avevi un mouse, lo spostavi, a volte premevi i pulsanti e basta. Tutto ciò che non era un mouse veniva emulato come tale e gli sviluppatori sapevano esattamente cosa aspettarsi.

Tuttavia, semplice non significa necessariamente buono. Nel tempo, è diventato sempre più importante che non tutto fosse (o pretendesse di essere) un mouse: potevi avere penne sensibili alla pressione e all'inclinazione, per una straordinaria libertà creativa; potevi usare le dita, quindi tutto ciò di cui avevi bisogno era il dispositivo e la tua mano; e, a proposito, perché non usare più di un dito?

Da un po' di tempo sono disponibili gli eventi tocco per aiutarci a risolvere il problema, ma si tratta di un'API completamente separata specifica per il tocco, che ti costringe a codificare due modelli di eventi distinti se vuoi supportare sia il mouse sia il tocco. Chrome 55 è dotato di uno standard più recente che unifica entrambi i modelli: gli eventi del cursore.

Un modello di singolo evento

Gli eventi del cursore unificano il modello di input del cursore per il browser, combinando tocchi, penne e mouse in un unico insieme di eventi. Ad esempio:

document.addEventListener('pointermove',
    ev => console.log('The pointer moved.'));
foo.addEventListener('pointerover',
    ev => console.log('The pointer is now over foo.'));

Ecco un elenco di tutti gli eventi disponibili, che dovrebbero essere abbastanza familiari se hai dimestichezza con gli eventi del mouse:

pointerover Il cursore è entrato nell'area delimitata dell'elemento. Questo accade immediatamente per i dispositivi che supportano il passaggio del mouse o prima di un pointerdown evento per i dispositivi che non lo supportano.
pointerenter Simile a pointerover, ma non genera una gerarchia e gestisce i discendenti in modo diverso. Dettagli sulle specifiche.
pointerdown Il cursore è entrato nello stato del pulsante attivo, con un pulsante premuto o il contatto stabilito, a seconda della semantica del dispositivo di input.
pointermove Il cursore ha cambiato posizione.
pointerup Il cursore ha lasciato lo stato del pulsante attivo.
pointercancel È successo qualcosa che fa pensare che il cursore non emetterà altri eventi. Ciò significa che devi annullare eventuali azioni in corso e tornare a uno stato di input neutro.
pointerout Il cursore ha lasciato il riquadro di delimitazione dell'elemento o della schermata. Anche dopo un pointerup, se il dispositivo non supporta il passaggio del mouse.
pointerleave Simile a pointerout, ma non genera una gerarchia e gestisce i discendenti in modo diverso. Dettagli sulle specifiche.
gotpointercapture L'elemento ha ricevuto il controllo del cursore.
lostpointercapture Il cursore che stava per essere acquisito è stato rilasciato.

Diversi tipi di input

In genere, gli eventi relativi al cursore ti consentono di scrivere codice in modo indipendente dall'input, senza dover registrare gestori di eventi separati per diversi dispositivi di input. Naturalmente, dovrai comunque tenere conto delle differenze tra i tipi di input, ad esempio se si applica il concetto di passaggio del mouse. Se vuoi distinguere i diversi tipi di dispositivi di input, ad esempio per fornire codice/funzionalità separati per input diversi, puoi farlo all'interno degli stessi gestori eventi utilizzando la proprietà pointerType dell'interfaccia PointerEvent. Ad esempio, se stai codificando un riquadro di navigazione laterale, potresti avere la seguente logica per l'evento pointermove:

switch(ev.pointerType) {
    case 'mouse':
    // Do nothing.
    break;
    case 'touch':
    // Allow drag gesture.
    break;
    case 'pen':
    // Also allow drag gesture.
    break;
    default:
    // Getting an empty string means the browser doesn't know
    // what device type it is. Let's assume mouse and do nothing.
    break;
}

Azioni predefinite

Nei browser con tocco, vengono utilizzati determinati gesti per scorrere, aumentare lo zoom o aggiornare la pagina. Nel caso degli eventi tocco, riceverai comunque eventi durante l'esecuzione di queste azioni predefinite. Ad esempio, touchmove verrà comunque attivato mentre l'utente scorre.

Con gli eventi del cursore, ogni volta che viene attivata un'azione predefinita come lo scorrimento o lo zoom, viene visualizzato un evento pointercancel per informarti che il browser ha assunto il controllo del cursore. Ad esempio:

document.addEventListener('pointercancel',
    ev => console.log('Go home, the browser is in charge now.'));

Velocità integrata: questo modello consente un rendimento migliore per impostazione predefinita rispetto agli eventi touch, per i quali è necessario utilizzare ascoltatori di eventi passivi per ottenere lo stesso livello di reattività.

Puoi impedire al browser di assumere il controllo con la proprietà CSS touch-action. Se lo imposti su none su un elemento, verranno disattivate tutte le azioni predefinite dal browser avviate su quell'elemento. Esistono però diversi altri valori per un controllo più granulare, ad esempio pan-x, per consentire al browser di reagire al movimento sull'asse x, ma non sull'asse y. Chrome 55 supporta i seguenti valori:

auto Valore predefinito: il browser può eseguire qualsiasi azione predefinita.
none Al browser non è consentito eseguire azioni predefinite.
pan-x Il browser può eseguire solo l'azione predefinita di scorrimento orizzontale.
pan-y Il browser è autorizzato a eseguire solo l'azione predefinita di scorrimento verticale.
pan-left Il browser è autorizzato a eseguire solo l'azione predefinita di scorrimento orizzontale e solo a eseguire la panoramica della pagina verso sinistra.
pan-right Il browser è autorizzato a eseguire solo l'azione predefinita di scorrimento orizzontale e solo a eseguire la panoramica della pagina verso destra.
pan-up Il browser è autorizzato a eseguire solo l'azione predefinita di scorrimento verticale e solo a eseguire la panoramica della pagina verso l'alto.
pan-down Il browser è autorizzato a eseguire solo l'azione predefinita di scorrimento verticale e solo a eseguire la panoramica della pagina verso il basso.
manipulation Il browser può eseguire solo azioni di scorrimento e zoom.

Acquisizione del cursore

Hai mai trascorso un'ora frustrante a eseguire il debug di un evento mouseup non funzionante, fino a quando non hai capito che il problema è che l'utente rilascia il pulsante al di fuori del target di clic? No? Ok, forse è solo un mio problema.

Tuttavia, finora non esisteva un modo davvero efficace per risolvere questo problema. Certo, puoi configurare l'handler mouseup nel documento e salvare un po' di stato nella tua applicazione per tenere traccia delle cose. Tuttavia, non è la soluzione più pulita, soprattutto se stai creando un componente web e stai cercando di mantenere tutto ben isolato.

Con gli eventi del cursore è disponibile una soluzione molto migliore: puoi acquisire il cursore, in modo da ricevere l'evento pointerup (o qualsiasi altro dei suoi amici sfuggenti).

const foo = document.querySelector('#foo');
foo.addEventListener('pointerdown', ev => {
    console.log('Button down, capturing!');
    // Every pointer has an ID, which you can read from the event.
    foo.setPointerCapture(ev.pointerId);
});

foo.addEventListener('pointerup', 
    ev => console.log('Button up. Every time!'));

Supporto browser

Al momento della stesura di questo articolo, gli eventi relativi al cursore sono supportati in Internet Explorer 11, Microsoft Edge, Chrome e Opera e parzialmente in Firefox. Puoi trovare un elenco aggiornato su caniuse.com.

Puoi utilizzare il polyfill Pointer Events per colmare le lacune. In alternativa, il controllo del supporto del browser in fase di esecuzione è semplice:

if (window.PointerEvent) {
    // Yay, we can use pointer events!
} else {
    // Back to mouse and touch events, I guess.
}

Gli eventi del cursore sono candidati ideali per il miglioramento progressivo: basta modificare i metodi di inizializzazione per eseguire il controllo riportato sopra, aggiungere gestori di eventi del cursore nel blocco if e spostare i gestori di eventi del mouse/touch nel blocco else.

Provali e facci sapere cosa ne pensi.