ניתוח מעמיק של אירוע JavaScript

preventDefault ו-stopPropagation: מתי להשתמש ומתי בדיוק כל שיטה פועלת.

סטיבן סטצ'ור
סטיבן סטצ'ור

Event.stopPropagation() ו-Event.preventDefault()

הטיפול באירועי JavaScript הוא בדרך כלל פשוט. הדבר נכון במיוחד בעת טיפול במבנה HTML פשוט (יחסי יחסית). אבל העניינים הופכים להיות יותר מעורבים כשהאירועים עוברים (או מתפשטים) דרך היררכיה של אלמנטים. בדרך כלל זה קורה כשמפתחים פונים ל-stopPropagation() או ל-preventDefault() כדי לפתור את הבעיות שהם נתקלים בהן. אם אי פעם חשבתם לעצמכם "אני פשוט אנסה את preventDefault() ואם זה לא יעבוד אני אנסה stopPropagation() ואם זה לא יעבוד, אני אנסה את שניהם", המאמר הזה מיועד לכם. אסביר בדיוק מה עושה כל שיטה ומתי להשתמש בכל שיטה, ואתן לכם מגוון דוגמאות עבודה שתוכלו לחקור. המטרה שלי היא לסיים את הבלבול אחת ולתמיד.

עם זאת, לפני שנצלול יותר מדי לעומק, חשוב לגעת בקצרה בשני הסוגים של טיפול באירועים שאפשר להשתמש ב-JavaScript (בכל הדפדפנים המודרניים - Internet Explorer שלפני גרסה 9 לא תמך כלל בתיעוד אירועים).

סגנונות אירוע (צילום ובבלון)

כל הדפדפנים המודרניים תומכים בצילום אירועים, אבל המפתחים משתמשים בו לעיתים רחוקות מאוד. באופן מפתיע, זו הייתה הדרך היחידה ליצור אירועים שבהם Netscape תמכה במקור. היריבה הגדולה ביותר של Netscape, Microsoft Internet Explorer, לא תמכה בכלל בצילום אירועים, אלא רק תמכה בסגנון אירועים אחר שנקרא 'בועות אירועים'. כשה-W3C נוצר, הם מצאו שיש יתרונות בשני הסגנונות של האירועים. הם גם הצהירו שהדפדפנים צריכים לתמוך בשניהם, באמצעות פרמטר שלישי ל-method addEventListener. במקור, הפרמטר הזה היה בוליאני בלבד, אבל כל הדפדפנים המודרניים תומכים באובייקט options בתור הפרמטר השלישי, ואפשר להשתמש בו כדי לציין (בין היתר) אם רוצים להשתמש בתיעוד אירועים או לא:

someElement.addEventListener('click', myClickHandler, { capture: true | false });

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

תיעוד אירועים

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

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

<html>
  <body>
    <div id="A">
      <div id="B">
        <div id="C"></div>
      </div>
    </div>
  </body>
</html>
document.getElementById('C').addEventListener(
  'click',
  function (e) {
    console.log('#C was clicked');
  },
  true,
);

כשמשתמש לוחץ על האלמנט #C, אירוע שמקורו ב-window נשלח. לאחר מכן, האירוע הזה יופץ דרך הצאצאים שלו באופן הבא:

window => document => <html> => <body> => וכן הלאה, עד להגעה ליעד.

לא משנה אם שום דבר לא מאזין לאירוע מסוג קליק ברכיב window או document או ברכיב <html> או ברכיב <body> (או בכל רכיב אחר בדרכו ליעד). האירוע עדיין מתחיל ב-window ומתחיל את התהליך כמו שמתואר למעלה.

בדוגמה שלנו, אירוע הקליק יופץ (זו מילה חשובה כי היא קשורה ישירות לאופן הפעולה של השיטה stopPropagation() ותוסבר בהמשך במסמך זה) מה-window לאלמנט היעד שלו (במקרה הזה, #C) דרך כל רכיב בין window ל-#C.

המשמעות היא שאירוע הקליק יתחיל ב-window והדפדפן ישאל את השאלות הבאות:

"האם יש משהו מאזין לאירוע קליק בwindow בשלב הצילום?" במקרה כזה, הגורמים המטפלים באירועים המתאימים יופעלו. בדוגמה שלנו, שום דבר הוא לא דבר, כך שאף handler לא יופעל.

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

בשלב הבא האירוע יופץ לאלמנט <html> והדפדפן ישאל: "האם מישהו מאזין ללחיצה על הרכיב <html> בשלב הצילום?" במקרה כזה, הגורמים המטפלים באירועים המתאימים יופעלו.

בשלב הבא האירוע יופץ לאלמנט <body> ובדפדפן ישאל: "האם מתבצעת האזנה לאירוע מסוג קליק על האלמנט <body> בשלב הצילום?" במקרה כזה, הגורמים המטפלים באירועים המתאימים יופעלו.

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

לאחר מכן, האירוע יופץ לרכיב #B (ואותה השאלה תישאל).

לבסוף, האירוע יגיע ליעד והדפדפן ישאל: "האם משהו מאזין לאירוע קליק על האלמנט #C בשלב הצילום?" התשובה הפעם היא 'כן!' פרק הזמן הקצר הזה שבו האירוע נמצא ביעד, נקרא 'שלב היעד'. בשלב הזה, הגורם המטפל באירועים יופעל, הדפדפן console.log "בוצעה קליק על #C" ואז סיימנו, נכון? לא נכון! לא סיימנו בכלל. התהליך נמשך, אבל עכשיו הוא משתנה לשלב הבבוע.

מבעבע באירוע

הדפדפן ישאל:

"האם יש משהו שמאזין לאירוע קליק על #C בשלב הבועות?" חשוב לשים לב כאן. ניתן לחלוטין להאזין לקליקים (או לכל סוג אירוע) גם בשלב הצילום וגם בשלב הבבוע. אם חיווטת רכיבי handler של אירועים בשני השלבים (למשל, על ידי קריאה ל-.addEventListener() פעמיים, פעם אחת עם capture = true ופעם אחת עם capture = false), אז כן, שני הגורמים המטפלים באירועים יופעלו באופן מוחלט עבור אותו אלמנט. אבל חשוב גם לשים לב שהן מופעלות בשלבים שונים (אחת בשלב הצילום והשנייה בשלב הבעבוע).

בשלב הבא האירוע יופץ (נקרא בדרך כלל "bubble" כי נראה שהאירוע מועבר "במעלה" עץ ה-DOM) אל רכיב ההורה שלו, #B, והדפדפן ישאל: "Isanythinging for click events on #B in the bubbbing level?" בדוגמה שלנו, שום דבר לא קורה, ולכן אף handler לא יופעל.

בשלב הבא האירוע יוצג בבועות עם הכיתוב #A, והדפדפן ישאל: "האם משהו מאזין לאירועי קליקים ב-#A בשלב הבועות?"

לאחר מכן, האירוע יופיע בבועות עם הכיתוב <body>: "האם יש משהו שמאזין לאירועי קליק ברכיב <body> בשלב הבבוע?"

בשלב הבא, האלמנט <html>: "האם יש משהו שמאזין לאירועי קליק ברכיב <html> בשלב הבבוע?"

בשלב הבא, document: "האם יש פריט שמאזין לאירועי קליק על document בשלב הבועה?"

לבסוף, window: "האם יש משהו שמאזין לאירועי קליק על החלון בשלב הבועות?"

סוף סוף! זה היה מסע ארוך, והאירוע כנראה כבר עייף מאוד, אבל תאמינו או לא, זה המסע שכל אירוע עובר! בדרך כלל זה אף פעם לא שמים לב לכך, כי המפתחים מתעניינים בדרך כלל רק בשלב אחד של האירוע (ובדרך כלל מדובר בשלב הבבוע).

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

<html>
  <body>
    <div id="A">
      <div id="B">
        <div id="C"></div>
      </div>
    </div>
  </body>
</html>
document.addEventListener(
  'click',
  function (e) {
    console.log('click on document in capturing phase');
  },
  true,
);
// document.documentElement == <html>
document.documentElement.addEventListener(
  'click',
  function (e) {
    console.log('click on <html> in capturing phase');
  },
  true,
);
document.body.addEventListener(
  'click',
  function (e) {
    console.log('click on <body> in capturing phase');
  },
  true,
);
document.getElementById('A').addEventListener(
  'click',
  function (e) {
    console.log('click on #A in capturing phase');
  },
  true,
);
document.getElementById('B').addEventListener(
  'click',
  function (e) {
    console.log('click on #B in capturing phase');
  },
  true,
);
document.getElementById('C').addEventListener(
  'click',
  function (e) {
    console.log('click on #C in capturing phase');
  },
  true,
);

document.addEventListener(
  'click',
  function (e) {
    console.log('click on document in bubbling phase');
  },
  false,
);
// document.documentElement == <html>
document.documentElement.addEventListener(
  'click',
  function (e) {
    console.log('click on <html> in bubbling phase');
  },
  false,
);
document.body.addEventListener(
  'click',
  function (e) {
    console.log('click on <body> in bubbling phase');
  },
  false,
);
document.getElementById('A').addEventListener(
  'click',
  function (e) {
    console.log('click on #A in bubbling phase');
  },
  false,
);
document.getElementById('B').addEventListener(
  'click',
  function (e) {
    console.log('click on #B in bubbling phase');
  },
  false,
);
document.getElementById('C').addEventListener(
  'click',
  function (e) {
    console.log('click on #C in bubbling phase');
  },
  false,
);

פלט המסוף יהיה תלוי ברכיב שעליו לוחצים. אם לוחצים על הרכיב 'העמוק ביותר' בעץ ה-DOM (הרכיב #C), תראו כל הפעלה של כל אחד מהגורמים שמטפלים באירועים האלה. עם קצת עיצוב CSS כדי שיהיה ברור יותר איזה רכיב הוא, זהו רכיב הפלט #C של המסוף (עם צילום מסך גם):

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"
"click on <body> in bubbling phase"
"click on <html> in bubbling phase"
"click on document in bubbling phase"

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

event.stopPropagation()

כשאנחנו מבינים מאיפה מגיעים האירועים ואיך הם עוברים (כלומר מופצים) דרך ה-DOM בשלב הצילום וגם בשלב הבבוע, אנחנו יכולים להפנות את תשומת הלב שלנו לevent.stopPropagation().

אפשר להפעיל את השיטה stopPropagation() באירועי DOM מקוריים (ברוב). אני אומר "הרוב", כי יש כמה דרכים שבהן קריאה לשיטה הזו לא תעזור (כי האירוע לא מתחיל). בקטגוריה הזו נכללים אירועים כמו focus, blur, load, scroll ועוד כמה אירועים. אפשר לקרוא ל-stopPropagation() אבל לא יקרה שום דבר מעניין, כי האירועים האלה לא מתפשטים.

אבל מה stopPropagation עושה?

זה פועל, פחות או יותר, רק מה שכתוב. כשקוראים לאירוע, מנקודה זו ואילך הוא יפסיק להפיץ את האירוע לכל רכיב אחר שאליו הוא היה מועבר. הדבר נכון לשני הכיוונים (צילום ומבעבוע). כך שאם תקראו ל-stopPropagation() במקום כלשהו בשלב הצילום, האירוע אף פעם לא יגיע לשלב היעד או לשלב הבועות. אם תקראו לו בשלב הבועה, הוא כבר עבר את שלב הצילום אבל הוא יפסיק "לבוע" מהנקודה שבה קראתם לו.

נחזור לתגי העיצוב לדוגמה שלנו. מה יקרה אם נקרא stopPropagation() בשלב הצילום ברכיב #B?

כתוצאה מכך יתקבל הפלט הבא:

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"

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

מה דעתך להפסיק את ההפצה ב-#A בשלב הבבוע? כתוצאה מכך, יתקבל הפלט הבא:

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"

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

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

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"

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

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

אני ממליץ לכם להתנסות בכל אחת מההדגמות חיות האלה. נסו ללחוץ על האלמנט #A בלבד או על האלמנט body בלבד. נסו לחזות מה יקרה ולאחר מכן בודקים אם צדקתם. בשלב הזה ניתן לחזות את מספר ההמרות באופן די מדויק.

event.stopImmediatePropagation()

מהי השיטה המוזרה והבלתי נפוצה? השיטה דומה ל-stopPropagation, אבל במקום למנוע מאירוע לעבור לצאצאים (לצילום) או לאות אב (מבעבוע), השיטה הזו רלוונטית רק כשיש יותר מ-handler אחד של אירועים שמחובר לרכיב אחד. ב-addEventListener() יש תמיכה בסגנון Multicast של אירועים, ולכן אפשר לחלוטין לחבר גורם handler של אירועים לרכיב יחיד יותר מפעם אחת. כשזה קורה, (ברוב הדפדפנים), הגורמים המטפלים באירועים מופעלים לפי הסדר שבו מחברים את המכשירים. קריאה ל-stopImmediatePropagation() מונעת הפעלה של רכיבי handler נוספים. לדוגמה:

<html>
  <body>
    <div id="A">I am the #A element</div>
  </body>
</html>
document.getElementById('A').addEventListener(
  'click',
  function (e) {
    console.log('When #A is clicked, I shall run first!');
  },
  false,
);

document.getElementById('A').addEventListener(
  'click',
  function (e) {
    console.log('When #A is clicked, I shall run second!');
    e.stopImmediatePropagation();
  },
  false,
);

document.getElementById('A').addEventListener(
  'click',
  function (e) {
    console.log('When #A is clicked, I would have run third, if not for stopImmediatePropagation');
  },
  false,
);

הדוגמה שלמעלה תוביל לפלט הבא של המסוף:

"When #A is clicked, I shall run first!"
"When #A is clicked, I shall run second!"

הערה: הגורם המטפל באירועים השלישי אף פעם לא פועל בגלל העובדה שהגורם המטפל באירועים השני קורא ל-e.stopImmediatePropagation(). אם במקום זאת היינו קוראים ל-e.stopPropagation(), ה-handler השלישי עדיין היה פועל.

event.preventDefault()

אם stopPropagation() מונע מאירוע להתקדם 'למטה' (לצלם) או 'למעלה' (מבעבוע), מה אז עושה preventDefault()? נשמע שהוא עושה משהו דומה. האם זה נכון?

לא ממש. בדרך כלל קיים בלבול בין שתי הפלטפורמות, אבל אין ביניהן הרבה קשר. כשמופיעה המילה preventDefault() בראש, צריך להוסיף את המילה 'פעולה'. חשוב "למנוע את פעולת ברירת המחדל".

ומהי פעולת ברירת המחדל שאפשר לשאול? לצערנו, התשובה לכך לא כל כך ברורה כי היא תלויה במידה רבה בשילוב של האלמנט + האירוע הנדון. ואם הנושא מבלבל עוד יותר, לפעמים אין שום פעולת ברירת מחדל!

נתחיל בדוגמה פשוטה מאוד להבנה. מה אתם מצפים שיקרה כשאתם לוחצים על קישור בדף אינטרנט? כמובן, אתם מצפים שהדפדפן ינווט לכתובת ה-URL שצוינה בקישור הזה. במקרה הזה, האלמנט הוא תג עוגן והאירוע הוא אירוע מסוג קליק. לשילוב הזה (<a> + click) יש 'פעולת ברירת מחדל' של ניווט ל- href של הקישור. מה קורה אם רוצים למנוע מהדפדפן לבצע את פעולת ברירת המחדל הזו? כלומר, נניח שאתם רוצים למנוע מהדפדפן לנווט לכתובת ה-URL שצוינה במאפיין href של האלמנט <a>. זה מה ש-preventDefault() יעשה בשבילכם. עיין בדוגמה זו:

<a id="avett" href="https://www.theavettbrothers.com/welcome">The Avett Brothers</a>
document.getElementById('avett').addEventListener(
  'click',
  function (e) {
    e.preventDefault();
    console.log('Maybe we should just play some of their music right here instead?');
  },
  false,
);

אפשר לשחק עם התכונה הזו באופן אינטראקטיבי בהדגמה החיה שלמטה. לוחצים על הקישור The Avett Brothers וקוראים את פלט המסוף (ואת העובדה שלא הופניתם לאתר של Avett Brothers).

בדרך כלל, לחיצה על הקישור 'האחים Avett' תעביר אתכם אל www.theavettbrothers.com. עם זאת, במקרה הזה חיברנו את הגורם המטפל באירועים של קליקים לרכיב <a> וציינו שצריך למנוע את פעולת ברירת המחדל. לכן, כשמשתמש ילחץ על קישור זה, לא יועבר לו ניווט לשום מקום, ובמקום זאת המסוף פשוט יתעד את עצמו: "אולי כדאי שפשוט תפעיל חלק מהמוזיקה שלו כאן?"

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

  • הרכיב <form> + אירוע "submit": הערך preventDefault() לשילוב הזה ימנע שליחה של טופס. זו אפשרות שימושית אם רוצים לבצע אימות ואם משהו ייכשל, תוכלו לקרוא באופן מותנה ל-preventDefault להפסיק את שליחת הטופס.

  • אלמנט <a> + אירוע "click": preventDefault() עבור השילוב הזה מונע מהדפדפן לנווט לכתובת ה-URL שצוינה במאפיין href של הרכיב <a>.

  • document + האירוע "גלגל עכבר": preventDefault() בשילוב הזה מונע גלילה בדף באמצעות גלגל העכבר (עם זאת, גלילה באמצעות המקלדת עדיין תפעל).
    ↜ לביצוע הפעולה הזו יש להתקשר אל addEventListener() באמצעות { passive: false }.

  • document + אירוע "keydown": preventDefault() לשילוב הזה הוא קטלני. זה חוסך את השימוש בדף, ומונע גלילה במקלדת, שימוש בכרטיסיות והדגשת המקלדת.

  • document + אירוע "mousedown": preventDefault() עבור השילוב הזה ימנעו הדגשה של טקסט באמצעות העכבר וכל פעולת "ברירת מחדל" אחרת שתתבצע באמצעות העכבר למטה.

  • הרכיב <input> + אירוע "לחיצה על מקש": preventDefault() לשילוב הזה ימנעו מתווים שהוזנו על ידי המשתמש להגיע לאלמנט הקלט (אבל אל תעשו זאת; לעיתים רחוקות, אם יש סיבה חוקית לכך).

  • document + אירוע "contextmenu": הערך preventDefault() עבור השילוב הזה מונע את הצגת תפריט ההקשר של הדפדפן כשמשתמש לוחץ לחיצה ימנית או לחיצה ארוכה (או כל דרך אחרת שבה עשוי להופיע תפריט הקשר).

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

בדיחה מעשית משעשעת?

מה קורה אם stopPropagation() וגם preventDefault() בשלב הצילום, החל מהמסמך? מתרחש תוכן מצחיק! קטע הקוד הבא יעבד כל דף אינטרנט חסר תועלת כמעט:

function preventEverything(e) {
  e.preventDefault();
  e.stopPropagation();
  e.stopImmediatePropagation();
}

document.addEventListener('click', preventEverything, true);
document.addEventListener('keydown', preventEverything, true);
document.addEventListener('mousedown', preventEverything, true);
document.addEventListener('contextmenu', preventEverything, true);
document.addEventListener('mousewheel', preventEverything, { capture: true, passive: false });

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

המקור של כל האירועים הוא window, ולכן בקטע הזה אנחנו עוצרים את השינויים, כל האירועים ב-click, ב-keydown, ב-mousedown, ב-contextmenu וב-mousewheel כדי לא להגיע לרכיבים שעשויים להאזין להם. אנחנו גם קוראים ל-stopImmediatePropagation כדי שגם handlers שחיברתם למסמך אחרי המכשיר הזה יוסתרו.

שים לב: stopPropagation() ו-stopImmediatePropagation() הם לא (לפחות לא בעיקר) שמעבדים את הדף ללא תועלת. הם פשוט מונעים מאירועים להגיע לאן שהם היו מגיעים אחרת.

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

הדגמות בשידור חי

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

אישורים

תמונה ראשית (Hero) מאת Tom Wilson באתר UnFlood.