חדש:VisualViewport

Jake Archibald
Jake Archibald

מה יקרה אם אספר לכם שיש יותר ממרחב צפייה אחד?

BRRRRAAAAAAAMMMMMMMMMM

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

BRRRRAAAAAAAMMMMMMMMMM

לפעמים הנתונים שמקבלים מ-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!');

במקום זאת:

מה צריך לעשות

פועל – משתמש ב-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 קטן ונוח שמטפל בבעיות תאימות לאורך הדרך.