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