Maak kennis met visualViewport

Wat als ik je vertelde dat er meer dan één viewport is?

BRRRRAAAAAAAMMMMMMMMMM

En de viewport die u nu gebruikt, is eigenlijk een viewport binnen een viewport.

BRRRRAAAAAAAMMMMMMMMMM

En soms verwijzen de gegevens die de DOM u geeft naar een van die viewports en niet naar de andere.

BRRRRAAAAM... wacht wat?

Het is waar, kijk eens:

Lay-outvenster versus visuele venster

De video hierboven toont een webpagina waarop wordt gescrolld en geknuppeld, samen met een minikaart aan de rechterkant die de positie van viewports op de pagina laat zien.

Tijdens normaal scrollen zijn de zaken vrij eenvoudig. Het groene gebied vertegenwoordigt het lay-outvenster , op welke position: fixed items blijven plakken.

Het wordt raar als knijpzoomen wordt geïntroduceerd. Het rode vak vertegenwoordigt de visuele viewport , het deel van de pagina dat we daadwerkelijk kunnen zien. Dit zichtvenster kan tijdens position: fixed elementen blijven waar ze waren, vastgemaakt aan het lay-outvenster. Als we naar een grens van het lay-outvenster pannen, wordt het lay-outvenster meegesleept.

Compatibiliteit verbeteren

Helaas zijn web-API's inconsistent wat betreft de viewport waarnaar ze verwijzen, en ze zijn ook inconsistent in alle browsers.

element.getBoundingClientRect().y retourneert bijvoorbeeld de offset binnen het lay-outvenster . Dat is cool, maar we willen vaak de positie binnen de pagina, dus schrijven we:

element.getBoundingClientRect().y + window.scrollY

Veel browsers gebruiken echter de visuele viewport voor window.scrollY , wat betekent dat de bovenstaande code breekt wanneer de gebruiker knijpt in en uitzoomt.

Chrome 61 verandert window.scrollY om in plaats daarvan naar de lay-outviewport te verwijzen, wat betekent dat de bovenstaande code zelfs werkt als er met een knijpbeweging wordt ingezoomd. In feite veranderen browsers langzaam alle positionele eigenschappen om naar de lay-outvenster te verwijzen.

Met uitzondering van één nieuw pand...

De visuele viewport blootstellen aan script

Een nieuwe API stelt de visuele viewport beschikbaar als window.visualViewport . Het is een conceptspecificatie , met goedkeuring voor meerdere browsers , en komt terecht in Chrome 61.

console.log(window.visualViewport.width);

Dit is wat window.visualViewport ons geeft:

visualViewport eigenschappen
offsetLeft Afstand tussen de linkerrand van het visuele venster en het lay-outvenster, in CSS-pixels.
offsetTop Afstand tussen de bovenrand van het visuele venster en het lay-outvenster, in CSS-pixels.
pageLeft Afstand tussen de linkerrand van het visuele venster en de linkergrens van het document, in CSS-pixels.
pageTop Afstand tussen de bovenrand van het visuele venster en de bovenrand van het document, in CSS-pixels.
width Breedte van de visuele viewport in CSS-pixels.
height Hoogte van de visuele viewport in CSS-pixels.
scale De schaal die wordt toegepast door knijpzoomen. Als de inhoud door het zoomen twee keer zo groot is, zou dit 2 opleveren. Dit wordt niet beïnvloed door devicePixelRatio .

Er zijn ook een paar evenementen:

window.visualViewport.addEventListener('resize', listener);
visualViewport -gebeurtenissen
resize Wordt geactiveerd wanneer width , height of scale verandert.
scroll Wordt geactiveerd wanneer offsetLeft of offsetTop verandert.

Demo

De video aan het begin van dit artikel is gemaakt met visualViewport , bekijk hem eens in Chrome 61+ . Het maakt gebruik van visualViewport om de minikaart rechtsboven in de visuele viewport te laten staan, en past een omgekeerde schaal toe, zodat deze altijd even groot lijkt, ondanks knijpzoomen.

Gotcha's

Gebeurtenissen worden alleen geactiveerd als de visuele viewport verandert

Het voelt voor de hand liggend om te zeggen, maar het viel me op toen ik voor het eerst met visualViewport speelde.

Als het formaat van het lay-outvenster wordt gewijzigd, maar het visuele venster niet, krijgt u geen resize gebeurtenis. Het is echter ongebruikelijk dat het lay-outvenster wordt vergroot of verkleind zonder dat het visuele venster ook de breedte/hoogte verandert.

De echte valkuil is scrollen. Als er wordt gescrolld, maar de visuele viewport statisch blijft ten opzichte van de lay-outviewport , krijgt u geen scroll gebeurtenis op visualViewport en dit is heel gebruikelijk. Tijdens normaal scrollen door documenten blijft de visuele viewport linksboven in de lay-outviewport vergrendeld, dus scroll wordt niet geactiveerd op visualViewport .

Als je wilt horen over alle wijzigingen in de visuele viewport, inclusief pageTop en pageLeft , moet je ook naar de scroll-gebeurtenis van het venster luisteren:

visualViewport.addEventListener('scroll', update);
visualViewport.addEventListener('resize', update);
window.addEventListener('scroll', update);

Vermijd dubbel werk met meerdere luisteraars

Net als bij het luisteren naar scroll en vergroten of resize in het venster, zul je daardoor waarschijnlijk een soort "update" -functie oproepen. Het komt echter vaak voor dat veel van deze gebeurtenissen tegelijkertijd plaatsvinden. Als de gebruiker het formaat van het venster wijzigt, wordt resize geactiveerd, maar vaak wordt er ook scroll . Om de prestaties te verbeteren, moet u voorkomen dat u de wijziging meerdere keren doorvoert:

// Add listeners
visualViewport.addEventListener('scroll', update);
visualViewport.addEventListener('resize', update);
addEventListener('scroll', update);

let pendingUpdate = false;

function update() {
    // If we're already going to handle an update, return
    if (pendingUpdate) return;

    pendingUpdate = true;

    // Use requestAnimationFrame so the update happens before next render
    requestAnimationFrame(() => {
    pendingUpdate = false;

    // Handle update here
    });
}

Ik heb hiervoor een specificatieprobleem ingediend , omdat ik denk dat er misschien een betere manier is, zoals een enkele update gebeurtenis.

Gebeurtenishandlers werken niet

Vanwege een Chrome-bug werkt dit niet :

Niet doen

Buggy – gebruikt een gebeurtenishandler

visualViewport.onscroll = () => console.log('scroll!');

In plaats van:

Doen

Werkt – gebruikt een gebeurtenislistener

visualViewport.addEventListener('scroll', () => console.log('scroll'));

Offsetwaarden zijn afgerond

Ik denk (nou ja, ik hoop) dat dit weer een Chrome-bug is.

offsetLeft en offsetTop zijn afgerond, wat behoorlijk onnauwkeurig is zodra de gebruiker heeft ingezoomd. Je kunt de problemen hiermee zien tijdens de demo : als de gebruiker inzoomt en langzaam beweegt, springt de minikaart tussen niet-ingezoomde pixels .

Het aantal evenementen is laag

Net als andere resize en scroll , activeren deze niet elk frame, vooral niet op mobiel. Je kunt dit zien tijdens de demo : als je eenmaal inzoomt, heeft de minikaart moeite om vergrendeld te blijven in de viewport.

Toegankelijkheid

In de demo gebruikte ik visualViewport om de pinch-zoom van de gebruiker tegen te gaan. Het is logisch voor deze specifieke demo, maar je moet goed nadenken voordat je iets doet dat de wens van de gebruiker om in te zoomen teniet doet.

visualViewport kan worden gebruikt om de toegankelijkheid te verbeteren. Als de gebruiker bijvoorbeeld inzoomt, kunt u ervoor kiezen om de decoratieve position: fixed items, te verbergen, zodat deze niet in de weg staan ​​van de gebruiker. Maar nogmaals, wees voorzichtig dat u niet iets verbergt dat de gebruiker van dichterbij probeert te bekijken.

U kunt overwegen om op een analyseservice te posten wanneer de gebruiker inzoomt. Dit kan u helpen pagina's te identificeren waarmee gebruikers problemen ondervinden op het standaard zoomniveau.

visualViewport.addEventListener('resize', () => {
    if (visualViewport.scale > 1) {
    // Post data to analytics service
    }
});

En dat is het! visualViewport is een leuke kleine API die onderweg compatibiliteitsproblemen oplost.