צל DOM 301

מושגים מתקדמים וממשקי API של DOM

במאמר הזה נפרט על הדברים המדהימים שאפשר לעשות עם Shadow DOM. הוא מבוסס על המושגים שעליהם אנחנו מדברים ב-Shadow DOM 101 וב-Shadow DOM 201.

שימוש בשורשי צל מרובים

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

נראה מה יקרה אם ננסה לצרף כמה שורשי צל למארח:

<div id="example1">Light DOM</div>
<script>
  var container = document.querySelector('#example1');
  var root1 = container.createShadowRoot();
  var root2 = container.createShadowRoot();
  root1.innerHTML = '<div>Root 1 FTW</div>';
  root2.innerHTML = '<div>Root 2 FTW</div>';
</script>

מה שמוצג הוא "Root 2 FTW", למרות העובדה שכבר צירפנו עץ צל. הסיבה לכך היא שעץ הצללים האחרון שנוסף למארח מנצח. זו ערימת LIFO כמו בנוגע לעיבוד. בדיקה של כלי הפיתוח מאמתת את ההתנהגות הזו.

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

נקודות להכנסת האזורים הכהים

"נקודות הוספה של צל" (<shadow>) דומות לנקודות הוספה רגילות (<content>) בכך שהן placeholders. עם זאת, במקום לשמש כ-placeholders של התוכן של המארח, הם מוגדרים כמארחים של עצי צל אחרים. זה Shadow DOM Inception!

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

נחזור לדוגמה המקורית שלנו. הצללית הראשונה root1 הוסרה מרשימת ההזמנות. הוספה של נקודת הזנה של <shadow> מחזירה אותה:

<div id="example2">Light DOM</div>
<script>
var container = document.querySelector('#example2');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();
root1.innerHTML = '<div>Root 1 FTW</div><content></content>';
**root2.innerHTML = '<div>Root 2 FTW</div><shadow></shadow>';**
</script>

בדוגמה הזו יש כמה דברים מעניינים:

  1. עדיין מתבצע עיבוד של "Root 2 FTW" מעל "Root 1 FTW". הסיבה לכך היא הנקודה שבה הצבנו את נקודת ההזנה <shadow>. אם רוצים להשתמש בכיוון ההפוך, מזיזים את נקודת ההוספה: root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';.
  2. שימו לב שעכשיו יש נקודת הוספה של <content> ברמה הבסיסית (root1). כך צומת הטקסט "Light DOM" יופיע בטרנד הרינדור.

מה מוצג ב-<shadow>?

לפעמים כדאי לדעת מהו עץ הצלליות הישן שמעובד ב-<shadow>. אפשר לקבל הפניה לעץ הזה דרך .olderShadowRoot:

**root2.olderShadowRoot** === root1 //true

קבלת שורש הצל של המארח

אם רכיב מארח DOM של Shadow, ניתן לגשת לשורש הצללים הקטן ביותר באמצעות .shadowRoot:

var root = host.createShadowRoot();
console.log(host.shadowRoot === root); // true
console.log(document.body.shadowRoot); // null

אם אתה חושש שאנשים יחצו את הצללים שלך, צריך להגדיר מחדש את .shadowRoot כ-null:

Object.defineProperty(host, 'shadowRoot', {
  get: function() { return null; },
  set: function(value) { }
});

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

בניית DOM של צל ב-JS

אם אתם מעדיפים ליצור DOM ב-JS, ל-HTMLContentElement ול-HTMLShadowElement יש ממשקים בשביל זה.

<div id="example3">
  <span>Light DOM</span>
</div>
<script>
var container = document.querySelector('#example3');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();

var div = document.createElement('div');
div.textContent = 'Root 1 FTW';
root1.appendChild(div);

 // HTMLContentElement
var content = document.createElement('content');
content.select = 'span'; // selects any spans the host node contains
root1.appendChild(content);

var div = document.createElement('div');
div.textContent = 'Root 2 FTW';
root2.appendChild(div);

// HTMLShadowElement
var shadow = document.createElement('shadow');
root2.appendChild(shadow);
</script>

הדוגמה הזו כמעט זהה לדוגמה בקטע הקודם. ההבדל היחיד הוא שעכשיו אני משתמש ב-select כדי לשלוף את <span> שנוסף לאחרונה.

עבודה עם נקודות הכנסה

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

מה שמשני באופן עקרוני בנקודות הכנסה הוא שהן לא מעבירות DOM פיזית. הצמתים של המארח נשארים ללא שינוי. נקודות הוספה רק מקדמות מחדש צמתים מהמארח אל עץ הצללים. מדובר במצגת/עיבוד: "העבר את הצמתים האלה לכאן" "עבד את הצמתים האלה במיקום הזה".

למשל:

<div><h2>Light DOM</h2></div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.innerHTML = '<content select="h2"></content>';

var h2 = document.querySelector('h2');
console.log(root.querySelector('content[select="h2"] h2')); // null;
console.log(root.querySelector('content').contains(h2)); // false
</script>

והנה, השדה h2 הוא לא צאצא של ה-DOM של הצל. זה מוביל לעוד פרט:

Element.getDistributedNodes()

לא ניתן לעבור אל <content>, אבל ה-API של .getDistributedNodes() מאפשר לנו לשלוח שאילתה לצמתים המבוזרים בנקודת הכנסה:

<div id="example4">
  <h2>Eric</h2>
  <h2>Bidelman</h2>
  <div>Digital Jedi</div>
  <h4>footer text</h4>
</div>

<template id="sdom">
  <header>
    <content select="h2"></content>
  </header>
  <section>
    <content select="div"></content>
  </section>
  <footer>
    <content select="h4:first-of-type"></content>
  </footer>
</template>

<script>
var container = document.querySelector('#example4');

var root = container.createShadowRoot();

var t = document.querySelector('#sdom');
var clone = document.importNode(t.content, true);
root.appendChild(clone);

var html = [];
[].forEach.call(root.querySelectorAll('content'), function(el) {
  html.push(el.outerHTML + ': ');
  var nodes = el.getDistributedNodes();
  [].forEach.call(nodes, function(node) {
    html.push(node.outerHTML);
  });
  html.push('\n');
});
</script>

Element.getDestinationInsertionPoints()

בדומה ל-.getDistributedNodes(), אפשר לבדוק באילו נקודות הכנסה צומת מחולק על ידי קריאה ל-.getDestinationInsertionPoints():

<div id="host">
  <h2>Light DOM
</div>

<script>
  var container = document.querySelector('div');

  var root1 = container.createShadowRoot();
  var root2 = container.createShadowRoot();
  root1.innerHTML = '<content select="h2"></content>';
  root2.innerHTML = '<shadow></shadow>';

  var h2 = document.querySelector('#host h2');
  var insertionPoints = h2.getDestinationInsertionPoints();
  [].forEach.call(insertionPoints, function(contentEl) {
    console.log(contentEl);
  });
</script>

כלי: Shadow DOM Visualizer

קשה להבין את הקסם השחור שהוא DOM של Shadow. אני זוכרת שניסיתי לעטוף את הראש סביבי בפעם הראשונה.

כדי להמחיש את אופן הפעולה של עיבוד DOM של Shadow, פיתחתי כלי באמצעות d3.js. ניתן לערוך את שתי תיבות הסימון בצד ימין. אתם יכולים להדביק את תגי העיצוב שלכם ולשחק כדי לראות איך דברים פועלים ונקודות ההוספה מערבבים צמתים של מארחים לתוך עץ הצל.

תצוגה חזותית של צל DOM
הפעלת Shadow DOM Visualizer

רוצה לנסות? אשמח לשמוע מה דעתך.

מודל האירוע

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

פעולה 1 ב-Play

  • זה מעניין. אמור להופיע mouseout מהרכיב המארח (<div data-host>) לצומת הכחול. למרות שזה צומת מבוזר, הוא עדיין נמצא במארח, ולא ב-ShadowDOM. העברת העכבר כלפי מטה לתוך צבע צהוב יוצרת שוב mouseout בצומת הכחול.

פעולה 2 ב-Play

  • יש mouseout אחד שמופיע במארח (בסוף). בדרך כלל תראו את הטריגר של אירועים mouseout לכל הבלוקים הצהובים. עם זאת, במקרה הזה הרכיבים האלה הם פנימיים ל-DOM של הצל, והאירוע לא יופיע בבועות דרך הגבולות העליונים שלו.

פעולה 3 ב-Play

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

אירועים שמופסקים תמיד

האירועים הבאים אף פעם לא חוצים את גבולות הצל:

  • לבטל
  • error
  • בחירה
  • שינוי
  • משקל
  • אפס
  • resize
  • scroll
  • Selectstart (התחלה של בחירה)

סיכום

אני מקווה שתביעו הסכמה לכך ש-Shadow DOM הוא חזק במיוחד. לראשונה, יש לנו אנקפסולציה מתאימה בלי המטען המיותר של <iframe> או טכניקות ישנות יותר.

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

למידע נוסף, קראו את מאמר המבוא Shadow DOM 101 של Dominic ובמאמר Shadow DOM 201: CSS & Styling.