חדש:VisualViewport

ג'ייק ארצ'יבלד
ג'ייק ארצ'יבלד

ואם אמרתי לכם, יש יותר מאזור תצוגה אחד.

BRRRRAAAAAAAMMMMMMMM'

ואזור התצוגה שבו אתם משתמשים כרגע הוא למעשה אזור תצוגה בתוך אזור תצוגה.

BRRRRAAAAAAAMMMMMMMM'

ולפעמים הנתונים שה-DOM נותן לכם מתייחסים לאחד מאזורי התצוגה האלה ולא לשני.

BRRRRAAAAM... לחכות מה?

זה נכון, כדאי לראות:

אזור התצוגה של הפריסה לעומת אזור התצוגה החזותי

בסרטון שלמעלה רואים דף אינטרנט שמתבצעת גלילה ושינוי מרחק התצוגה, יחד עם מיני-מפה בצד שמאל שמראה את המיקום של אזורי התצוגה בדף.

העניינים זזים ממש קדימה במהלך גלילה רגילה. האזור הירוק מייצג את אזור התצוגה של הפריסה, שאליו נשמרים position: fixed פריטים.

הדברים נעשים מוזרים כשמכניסים את מרחק התצוגה באמצעות צביטה. התיבה האדומה מייצגת את אזור התצוגה הוויזואלי, שהוא החלק בדף שאנחנו יכולים לראות בפועל. אזור התצוגה הזה יכול לנוע, ורכיבי position: fixed נשארים במיקום שבו היו מחוברים לאזור התצוגה של הפריסה. אם מזיזים את הגבולות של אזור התצוגה של הפריסה, גוררים איתה את אזור התצוגה.

שיפור התאימות

לצערי ממשקי ה-API לאינטרנט לא עקביים מבחינת אזור התצוגה שאליו הם מתייחסים, והם גם לא עקביים בדפדפנים שונים.

לדוגמה, element.getBoundingClientRect().y מחזיר את ההיסט בתוך אזור התצוגה של הפריסה. זה מגניב, אבל הרבה פעמים אנחנו רוצים את המיקום בדף, אז נכתוב:

element.getBoundingClientRect().y + window.scrollY

עם זאת, דפדפנים רבים משתמשים באזור התצוגה הוויזואלי עבור window.scrollY, כלומר הקוד שלמעלה נשבר כשהמשתמש מבצע תנועת צביטה להגדלת התצוגה.

Chrome 61 משנה את window.scrollY כדי להפנות לאזור התצוגה של הפריסה, כלומר הקוד שלמעלה פועל גם כשמשנים את מרחק התצוגה באמצעות צביטה. למעשה, הדפדפנים משנים באיטיות את כל מאפייני המיקום כדי להפנות לאזור התצוגה של הפריסה.

מלבד נכס חדש אחד...

חשיפת אזור התצוגה החזותי לסקריפט

API חדש חושף את אזור התצוגה הוויזואלי כ-window.visualViewport. זהו מפרט טיוטה, עם אישור חוצה-דפדפנים, והוא נוחת ב-Chrome 61.

console.log(window.visualViewport.width);

זה מה ש-window.visualViewport נותן לנו:

visualViewport מלונות
offsetLeft המרחק בין הקצה השמאלי של אזור התצוגה הוויזואלי לבין אזור התצוגה של הפריסה, בפיקסלים של CSS.
offsetTop המרחק בין הקצה העליון של אזור התצוגה הוויזואלי לבין אזור התצוגה של הפריסה, בפיקסלים של CSS.
pageLeft המרחק בין הקצה השמאלי של אזור התצוגה החזותי לבין הגבול השמאלי של המסמך, בפיקסלים של CSS.
pageTop המרחק בין הקצה העליון של אזור התצוגה החזותי לבין הגבול העליון של המסמך, בפיקסלים של CSS.
width הרוחב של אזור התצוגה החזותי בפיקסלים של CSS.
height הגובה של אזור התצוגה החזותי בפיקסלים של CSS.
scale שינוי קנה המידה לפי תנועת צביטה להגדלת התצוגה. אם התוכן גדול פי שניים עקב שינוי מרחק התצוגה, יוחזר הערך 2. אין לכך השפעה על devicePixelRatio.

יש גם כמה אירועים:

window.visualViewport.addEventListener('resize', listener);
visualViewport אירועים
resize מופעל כשwidth, height או scale השתנו.
scroll מופעל כשoffsetLeft או offsetTop משתנים.

הדגמה (דמו)

הסרטון בתחילת המאמר הזה נוצר באמצעות visualViewport, אפשר לראות אותו ב-Chrome 61+. הוא משתמש ב- visualViewport כדי שהמיני-מפה נצמדת לפינה השמאלית העליונה של אזור התצוגה הויזואלי, ותראה תמיד קנה מידה הפוך, כך שהיא תיווצר בהתאם.

הבנתי

אירועים מופעלים רק כשאזור התצוגה הוויזואלי משתנה

זה מרגיש כמו ברור מאליו, אבל זה תפס אותי כששיחקתי לראשונה עם visualViewport.

אם גודל אזור התצוגה משתנה, אבל אזור התצוגה החזותי לא משתנה, לא מתקבל אירוע resize. עם זאת, לא ניתן לשנות את גודל אזור התצוגה בפריסה בלי שאזור התצוגה הוויזואלי ישנה גם את הרוחב/גובה.

העיקרון האמיתי נגלל. אם מתבצעת גלילה, אבל אזור התצוגה החזותי נשאר סטטי יחסי לאזור התצוגה של הפריסה, לא מתקבל אירוע scroll ב-visualViewport, וזה נפוץ מאוד. במהלך גלילה רגילה של מסמכים, אזור התצוגה הוויזואלי נשאר נעול בפינה השמאלית העליונה של אזור התצוגה של הפריסה, כך ש-scroll לא יופעל ב-visualViewport.

אם אתם רוצים לקבל מידע על כל השינויים באזור התצוגה החזותי, כולל pageTop ו-pageLeft, תצטרכו להאזין גם לאירוע הגלילה של החלון:

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

הימנעות מכפילויות של תכנים במספר מאזינים

בדומה להאזנה ל-scroll ול-resize בחלון, סביר להניח שכתוצאה מכך יופעל סוג כלשהו של פונקציית "עדכון". עם זאת, הרבה מהאירועים האלה מתרחשים בו-זמנית. אם המשתמש ישנה את גודל החלון, פעולה זו תפעיל את resize, אבל לעיתים קרובות גם scroll. כדי לשפר את הביצועים, מומלץ להימנע מטיפול בשינוי מספר פעמים:

// 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
    });
}

דיווחתי על בעיה במפרט, כי נראה לי שיש דרך טובה יותר, למשל אירוע update יחיד.

הגורמים המטפלים באירועים לא פועלים

בגלל באג ב-Chrome, האפשרות הזו לא עובדת:

מה אסור לעשות

באגי – משתמש במטפל באירועים

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

במקום זאת:

מה מותר לעשות

Works – משתמש ב-event listener

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

ערכי הקיזוז מעוגלים

אני חושב (טוב, אני מקווה) שזה באג נוסף ב-Chrome.

הערכים offsetLeft ו-offsetTop מעוגלים, והדבר די לא מדויק כשהמשתמש מגדילים את התצוגה. אפשר לראות את הבעיות בהגדרה הזו במהלך ההדגמה – אם המשתמש מגדיל את התצוגה וזז לאט, המיני-מפה מוצמדת בין פיקסלים שלא הוגדלו.

קצב האירועים איטי

כמו אירועים אחרים של resize ו-scroll, הם לא מופעלים בכל פריים, במיוחד בנייד. תוכלו לראות זאת במהלך ההדגמה. ברגע שעושים תנועת צביטה על מרחק התצוגה, המיני-מפה לא מצליחה להישאר נעולה באזור התצוגה.

נגישות

בהדגמה השתמשתי ב-visualViewport כדי לבטל את שינוי המרחק מהתצוגה באמצעות צביטה. זה הגיוני עבור ההדגמה המסוימת הזו, אבל צריך לחשוב היטב לפני שמבצעים פעולה שגורמת לרצון המשתמש להגדיל את התצוגה.

אפשר להשתמש באפליקציה visualViewport כדי לשפר את הנגישות. לדוגמה, אם המשתמש מגדיל את התצוגה, תוכלו לבחור להסתיר פריטים דקורטיביים של position: fixed כדי להרחיק אותם מצידם. אבל שוב, היזהרו שלא להסתיר משהו שהמשתמש מנסה לראות מקרוב.

תוכלו לשקול לפרסם פוסט בשירות ניתוח נתונים כשהמשתמש מגדילים את התצוגה. כך תוכלו לזהות דפים שבהם המשתמשים מתקשים, ברמת ברירת המחדל של מרחק התצוגה.

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

זהו, סיימתם. visualViewport הוא API קטן ונחמד לפתרון בעיות תאימות לאורך הדרך.