מכוונים לאן שרוצים

Sérgio Gomes

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

עם זאת, פשוט לא בהכרח אומר טוב. עם הזמן, התברר שחשוב לא להשתמש רק בעכבר (או במכשיר שמתחזה לעכבר): אפשר להשתמש בעטים עם חיישן לחץ וחיישן הטיה, כדי ליהנות מחופש יצירתי מדהים. אפשר להשתמש באצבעות, כך שכל מה שצריך הוא המכשיר והיד. וחוץ מזה, למה לא להשתמש ביותר מאצבע אחת?

כבר הרבה זמן יש לנו אירועי מגע שיעזרו לנו בכך, אבל הם ממשק API נפרד לגמרי שמיועד במיוחד למגע. לכן, אם רוצים לתמוך גם בעכבר וגם במגע, צריך לכתוב קוד של שני מודלים נפרדים של אירועים. גרסת Chrome 55 מגיעה עם תקן חדש יותר שמאחד את שני המודלים: אירועי סמן.

מודל של אירוע יחיד

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

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

לפניכם רשימה של כל האירועים הזמינים, שדומים מאוד לאירועי העכבר:

pointerover הסמן נכנס לתיבת הגבול של הרכיב. האירוע הזה מתרחש באופן מיידי במכשירים שתומכים בהחזקה מעל, או לפני אירוע pointerdown במכשירים שלא תומכים בהחזקה מעל.
pointerenter דומה ל-pointerover, אבל לא עולה למעלה (bubble) ומטפל בצאצאים באופן שונה. פרטים על המפרט
pointerdown הסמן נכנס למצב לחצן פעיל, כשלוחצים על לחצן או כאשר מתקיים מגע, בהתאם לסמנטיקה של מכשיר הקלט.
pointermove הסמן שינה מיקום.
pointerup הסמן עזב את מצב הלחצן הפעיל.
pointercancel קרה משהו שגורם לכך שסביר להניח שהעכבר לא ישדר יותר אירועים. כלומר, צריך לבטל את כל הפעולות שבתהליך ולחזור למצב קלט נייטרלי.
pointerout הסמן יצא מתיבת הגבול של האובייקט או המסך. גם אחרי pointerup, אם המכשיר לא תומך בהחזקה מעל.
pointerleave דומה ל-pointerout, אבל לא עולה למעלה (bubble) ומטפל בצאצאים באופן שונה. פרטים על המפרט
gotpointercapture הרכיב קיבל צילום של סמן.
lostpointercapture הפונקציה שתועדה שוחררה.

סוגים שונים של קלט

באופן כללי, אירועי Pointer מאפשרים לכתוב קוד באופן לא תלוי-קלט, בלי צורך לרשום מנהלים נפרדים של אירועים למכשירי קלט שונים. כמובן, עדיין תצטרכו לשים לב להבדלים בין סוגי הקלט, למשל אם המושג 'החזקה מעל' רלוונטי. אם אתם רוצים להבדיל בין סוגים שונים של מכשירי קלט – אולי כדי לספק קוד או פונקציונליות נפרדים לקלטים שונים – תוכלו לעשות זאת בתוך אותם פונקציות לטיפול באירועים באמצעות המאפיין pointerType של הממשק PointerEvent. לדוגמה, אם תכנתם תפריט ניווט צדדי, תוכלו להשתמש בלוגיקה הבאה באירוע 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;
}

פעולות ברירת מחדל

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

כשמשתמשים באירועי סמן, בכל פעם שמופעל פעולת ברירת מחדל כמו גלילה או הגדלת התצוגה, מתקבל אירוע pointercancel כדי להודיע לכם שהדפדפן השתלט על הסמן. לדוגמה:

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

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

אפשר למנוע מהדפדפן להשתלט על האירוע באמצעות מאפיין ה-CSS touch-action. הגדרת הערך none ברכיב תשבית את כל הפעולות שהוגדרו בדפדפן והופעלו דרך הרכיב הזה. אבל יש מספר ערכים אחרים לצורך שליטה ברמת פירוט גבוהה יותר, כמו pan-x, שמאפשרים לדפדפן להגיב לתנועה בציר x אבל לא בציר y. ב-Chrome 55 יש תמיכה בערכים הבאים:

auto ברירת מחדל. הדפדפן יכול לבצע כל פעולת ברירת מחדל.
none הדפדפן לא רשאי לבצע פעולות ברירת מחדל.
pan-x הדפדפן רשאי לבצע רק את פעולת ברירת המחדל של גלילה אופקית.
pan-y הדפדפן רשאי לבצע רק את פעולת ברירת המחדל של גלילה אנכית.
pan-left הדפדפן רשאי לבצע רק את פעולת ברירת המחדל של גלילה אופקית, ורק כדי להזיז את הדף שמאלה.
pan-right הדפדפן רשאי לבצע רק את פעולת ברירת המחדל של גלילה אופקית, ורק כדי להזיז את הדף ימינה.
pan-up הדפדפן רשאי לבצע רק את פעולת ברירת המחדל של גלילה אנכית, ורק כדי להזיז את הדף למעלה.
pan-down הדפדפן רשאי לבצע רק את פעולת ברירת המחדל של גלילה אנכית, ורק כדי להזיז את הדף למטה.
manipulation הדפדפן רשאי לבצע רק פעולות גלילה ושינוי מרחק התצוגה.

לכידת מצביע

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

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

אירועי סמן הם פתרון הרבה יותר טוב: אפשר לתעד את הסמן, כך שבטוח תקבלו את האירוע pointerup (או כל אחד מחבריו החמקמקים).

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!'));

תמיכה בדפדפנים

נכון למועד כתיבת המאמר, יש תמיכה באירועי סמן (Pointer Events) ב-Internet Explorer 11,‏ Microsoft Edge,‏ Chrome ו-Opera, ותמיכה חלקית ב-Firefox. כאן תוכלו למצוא רשימה מעודכנת.

אפשר להשתמש ב-Pointer Events polyfill כדי למלא את הפערים. לחלופין, אפשר לבדוק את תמיכת הדפדפן בזמן הריצה:

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

אירועי סמן הם מועמדים מצוינים לשיפור הדרגתי: פשוט משנים את שיטות האי initialization כדי לבצע את הבדיקה שלמעלה, מוסיפים מנהלים של אירועי סמן בבלוק if ומעבירים את המנהלים של אירועי העכבר/המגע לבלוק else.

אז כדאי לנסות אותן ולספר לנו מה חשבתם.