צל DOM 201

CSS וסגנון

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

מבוא

בואו נודה בזה. אין שום דבר סקסי בתגי עיצוב ללא סגנון. למזלנו, האנשים המבריקים שמאחורי Web Components חזו את זה ולא השאירו אותנו ללא מענה. מודול ה-CSS Scoping מגדיר אפשרויות רבות לעיצוב תוכן בעץ צל.

כיסוי סגנון

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

<div><h3>Light DOM</h3></div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.innerHTML = `
  <style>
    h3 {
      color: red;
    }
  </style>
  <h3>Shadow DOM</h3>
`;
</script>

יש שתי הבחנות מעניינות לגבי ההדגמה הזו:

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

מה המסר של הסיפור? יש לנו אנקפסולציה של סגנון מהעולם החיצון. תודה Shadow DOM!

עיצוב של רכיב המארח

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

<button class="red">My Button</button>
<script>
var button = document.querySelector('button');
var root = button.createShadowRoot();
root.innerHTML = `
  <style>
    :host {
      text-transform: uppercase;
    }
  </style>
  <content></content>
`;
</script>

חשוב לזכור שכללי הדף ההורה ספציפיים יותר מכללי :host שמוגדרים באלמנט, אבל פחות ספציפיים ממאפיין style שמוגדר באלמנט המארח. כך המשתמשים יכולים לשנות את העיצוב שלכם מבחוץ. התכונה :host פועלת גם בהקשר של ShadowRoot, לכן אי אפשר להשתמש בו מחוץ ל-Sshadow DOM.

הצורה הפונקציונלית של :host(<selector>) מאפשרת לטרגט לרכיב המארח אם הוא תואם ל-<selector>.

דוגמה – התאמה רק אם לרכיב עצמו יש את הכיתה .different (למשל <x-foo class="different"></x-foo>):

:host(.different) {
    ...
}

תגובה למצבי משתמשים

תרחיש לדוגמה שבו משתמשים ב-:host הוא כשיוצרים רכיב מותאם אישית ורוצים להגיב למצבים שונים של משתמשים (:hover,‏ :focus,‏ :active וכו').

<style>
  :host {
    opacity: 0.4;
    transition: opacity 420ms ease-in-out;
  }
  :host(:hover) {
    opacity: 1;
  }
  :host(:active) {
    position: relative;
    top: 3px;
    left: 3px;
  }
</style>

התאמת עיצוב לאלמנט

פסאודו-הקלאס :host-context(<selector>) תואם לרכיב המארח אם הוא או אחד מהאבות שלו תואמים ל-<selector>.

שימוש נפוץ ב-:host-context() הוא להגדרת עיצוב של רכיב על סמך הרכיבים שמסביב לו. לדוגמה, הרבה אנשים משתמשים בנושאים על ידי החלת סיווג על <html> או על <body>:

<body class="different">
  <x-foo></x-foo>
</body>

אפשר להשתמש ב-:host-context(.different) כדי להגדיר סגנון ל-<x-foo> כשהוא צאצא של רכיב עם הכיתה .different:

:host-context(.different) {
  color: red;
}

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

תמיכה במספר סוגי מארחים מתוך root בצל אחד

אפשר להשתמש ב-:host גם אם יוצרים ספריית נושאים ורוצים לתמוך בעיצוב של הרבה סוגים של רכיבי מארח מתוך אותה ה-DOM של Shadow DOM.

:host(x-foo) {
    /* Applies if the host is a <x-foo> element.*/
}

:host(x-foo:host) {
    /* Same as above. Applies if the host is a <x-foo> element. */
}

:host(div) {
    /* Applies if the host element is a <div>. */
}

עיצוב רכיבים פנימיים של Shadow DOM מבחוץ

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

רכיב הפסאודו ::shadow

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

לדוגמה, אם רכיב מארח שורש צל, אפשר לכתוב #host::shadow span {} כדי להגדיר סגנון לכל ה-spans שבתוך עץ הצל שלו.

<style>
  #host::shadow span {
    color: red;
  }
</style>

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

<script>
  var host = document.querySelector('div');
  var root = host.createShadowRoot();
  root.innerHTML = `
    <span>Shadow DOM</span>
    <content></content>
  `;
</script>

דוגמה (רכיבים מותאמים אישית) – ל-<x-tabs> יש <x-panel> צאצאים ב-Shadow DOM שלו. לכל לוח יש עץ צללים משלו שמכיל כותרות h2. כדי לעצב את הכותרות האלה מהדף הראשי, אפשר לכתוב:

x-tabs::shadow x-panel::shadow h2 {
    ...
}

הקומבינטור ‎/deep/

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

הקומבינטור /deep/ שימושי במיוחד בעולם של רכיבים מותאמים אישית, שבו נפוץ להשתמש בכמה רמות של Shadow DOM. דוגמאות עיקריות לכך הן הטמעה של קבוצה של רכיבים מותאמים אישית (שכל אחד מהם מארח עץ צללים משלו) או יצירת רכיב שעובר בירושה מרכיב אחר באמצעות <shadow>.

דוגמה (רכיבים מותאמים אישית) – בוחרים את כל רכיבי <x-panel> שהם צאצאים של <x-tabs>, במקום כלשהו בעץ:

x-tabs /deep/ x-panel {
    ...
}

דוגמה – הוספת סגנון לכל הרכיבים עם המחלקה .library-theme, בכל מקום בעץ צל:

body /deep/ .library-theme {
    ...
}

עבודה עם querySelector()‎

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

// No fun.
document.querySelector('x-tabs').shadowRoot
        .querySelector('x-panel').shadowRoot
        .querySelector('#foo');

// Fun.
document.querySelector('x-tabs::shadow x-panel::shadow #foo');

עיצוב רכיבים מקומיים

רכיבי בקרה ב-HTML מקורי הם אתגר מבחינת העיצוב. הרבה אנשים פשוט מוותרים ומפתחים פתרון משלהם. עם זאת, באמצעות הפקודה ::shadow ו-/deep/, אפשר לעצב כל רכיב בפלטפורמת האינטרנט שמשתמש ב-shadow DOM. דוגמאות טובות לכך הן הסוגים <input> ו-<video>:

video /deep/ input[type="range"] {
  background: hotpink;
}

יצירת ווקרי סגנון

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

שימוש ב-::shadow וב-‎/deep/

יש הרבה כוח מאחורי /deep/. הוא מאפשר לכותבי רכיבים לציין אלמנטים ספציפיים שאפשר לעצב או קבוצה של אלמנטים שאפשר להתאים להם עיצוב לפי נושא.

דוגמה - עיצוב כל הרכיבים עם המחלקה .library-theme, תוך התעלמות מכל עצי הצל:

body /deep/ .library-theme {
    ...
}

שימוש ברכיבי פסאודו מותאמים אישית

גם ב-WebKit וגם ב-Firefox מוגדרים רכיבי פסאודו לסגנון של חלקים פנימיים ברכיבי דפדפן מקומיים. דוגמה טובה היא input[type=range]. ניתן לעצב את פס ההזזה <span style="color:blue">blue</span> באמצעות טירגוט ל-::-webkit-slider-thumb:

input[type=range].custom::-webkit-slider-thumb {
  -webkit-appearance: none;
  background-color: blue;
  width: 10px;
  height: 40px;
}

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

אפשר להגדיר אלמנט בתור פסאודו-אלמנט בהתאמה אישית באמצעות המאפיין pseudo. צריך להוסיף לתחילת הערך או השם את הסימן 'x-'. הפעולה הזו יוצרת שיוך לאלמנט הזה בעץ הצללים, ומספקת לגורמים מחוץ לארגון נתיב ייעודי לחציית גבול הצל.

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

<style>
  #host::x-slider-thumb {
    background-color: blue;
  }
</style>
<div id="host"></div>
<script>
  var root = document.querySelector('#host').createShadowRoot();
  root.innerHTML = `
    <div>
      <div pseudo="x-slider-thumb"></div>' +
    </div>
  `;
</script>

שימוש במשתני CSS

אחת הדרכים היעילות ליצור הוקים (hooks) לעיצוב היא באמצעות משתני CSS. בעיקרון, יוצרים 'placeholders' של סגנונות שמשתמשים אחרים יכולים למלא.

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

button {
  color: var(--button-text-color, pink); /* default color will be pink */
  font-family: var(--button-font);
}

לאחר מכן, מי שמטמיע את הרכיב מגדיר את הערכים האלה לפי העדפותיו. אולי כדי להתאים לעיצוב ה-Comic Sans המגניב של הדף שלהם:

#host {
  --button-text-color: green;
  --button-font: "Comic Sans MS", "Comic Sans", cursive;
}

בגלל האופן שבו משתני CSS עוברים בירושה, הכל עובד מצוין! התמונה כולה נראית כך:

<style>
  #host {
    --button-text-color: green;
    --button-font: "Comic Sans MS", "Comic Sans", cursive;
  }
</style>
<div id="host">Host node</div>
<script>
  var root = document.querySelector('#host').createShadowRoot();
  root.innerHTML = `
    <style>
      button {
        color: var(--button-text-color, pink);
        font-family: var(--button-font);
      }
    </style>
    <content></content>
  `;
</script>

איפוס הסגנונות

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

resetStyleInheritance

בהמשך מוצגת הדגמה שמראה איך שינוי של resetStyleInheritance משפיע על עץ הצללים:

<div>
  <h3>Light DOM</h3>
</div>

<script>
  var root = document.querySelector('div').createShadowRoot();
  root.resetStyleInheritance = <span id="code-resetStyleInheritance">false</span>;
  root.innerHTML = `
    <style>
      h3 {
        color: red;
      }
    </style>
    <h3>Shadow DOM</h3>
    <content select="h3"></content>
  `;
</script>

<div class="demoarea" style="width:225px;">
  <div id="style-ex-inheritance"><h3 class="border">Light DOM</div>
</div>
<div id="inherit-buttons">
  <button id="demo-resetStyleInheritance">resetStyleInheritance=false</button>
</div>

<script>
  var container = document.querySelector('#style-ex-inheritance');
  var root = container.createShadowRoot();
  //root.resetStyleInheritance = false;
  root.innerHTML = '<style>h3{ color: red; }</style><h3>Shadow DOM<content select="h3"></content>';

  document.querySelector('#demo-resetStyleInheritance').addEventListener('click', function(e) {
    root.resetStyleInheritance = !root.resetStyleInheritance;
    e.target.textContent = 'resetStyleInheritance=' + root.resetStyleInheritance;
    document.querySelector('#code-resetStyleInheritance').textContent = root.resetStyleInheritance;
  });
</script>
נכסים שעברו בירושה לכלי פיתוח

קצת יותר קשה להבין את .resetStyleInheritance, בעיקר מפני שהפעולה הזו משפיעה רק על מאפייני CSS שעוברים בירושה. ההסבר הוא: כשמחפשים מאפיין לירושה, בגבול בין הדף ל-ShadowRoot, לא יורשים ערכים מהמארח אלא משתמשים בערך initial במקום זאת (לפי מפרט CSS).

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

עיצוב צמתים מבוזרים

צמתים מבוזרים הם רכיבים שנעשים להם רינדור בנקודת הטמעה (רכיב <content>). הרכיב <content> מאפשר לבחור צמתים מ-DOM האור ולייצר אותם במיקומים מוגדרים מראש ב-Shadow DOM. הם לא נמצאים באופן לוגי ב-shadow DOM; הם עדיין צאצאים של הרכיב המארח. נקודות ההטמעה הן רק עניין של רינדור.

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

רכיב הסימון ::content

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

::content > h3 מגדיר סגנונות לכל תגי h3 שעוברים דרך נקודת הטמעה.

בואו נראה דוגמה:

<div>
  <h3>Light DOM</h3>
  <section>
    <div>I'm not underlined</div>
    <p>I'm underlined in Shadow DOM!</p>
  </section>
</div>

<script>
var div = document.querySelector('div');
var root = div.createShadowRoot();
root.innerHTML = `
  <style>
    h3 { color: red; }
      content[select="h3"]::content > h3 {
      color: green;
    }
    ::content section p {
      text-decoration: underline;
    }
  </style>
  <h3>Shadow DOM</h3>
  <content select="h3"></content>
  <content select="section"></content>
`;
</script>

איפוס הסגנונות בנקודות ההוספה

כשיוצרים ShadowRoot, יש אפשרות לאפס את הסגנונות שעברו בירושה. אפשרות זו זמינה גם בנקודות ההוספה <content> ו-<shadow>. כשמשתמשים ברכיבים האלה, צריך להגדיר את .resetStyleInheritance ב-JS או להשתמש במאפיין הבווליאני reset-style-inheritance ברכיב עצמו.

  • בנקודות ההוספה של ShadowRoot או <shadow>: הערך reset-style-inheritance מציין שמאפייני CSS שעוברים בירושה מוגדרים ל-initial במארח, לפני שהם מגיעים לתוכן הצל. המיקום הזה נקרא 'הגבול העליון'.

  • בנקודות ההוספה מסוג <content>: הערך reset-style-inheritance מציין שמאפייני CSS שעוברים בירושה מוגדרים לערך initial לפני שהצאצאים של המארח מופצים בנקודת ההוספה. המיקום הזה נקרא 'הגבול התחתון'.

סיכום

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

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