צל DOM 201

CSS וסגנון

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

מבוא

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

סיכום סגנון

אחת מהתכונות העיקריות של 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>

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

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

מוסר הסיפור? יש לנו תקצירי סגנון מבחוץ. תודה צל 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>

getcha אחד הוא שלכללים בדף ההורה יש ספציפיות גבוהה יותר מכללי ה-:host שהוגדרו ברכיב, אבל ספציפיות נמוכה יותר ממאפיין style שמוגדר ברכיב המארח. כך המשתמשים יכולים לשנות את הסגנון מבחוץ. :host פועל גם רק בהקשר של ShadowRoot, ולכן אי אפשר להשתמש בו מחוץ ל-DOM 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;
}

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

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

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

: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>. */
}

עיצוב פנימי של DOM מסוג Shadow מבחוץ

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

רכיב ::צללית פסאודו-אלמנט

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

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

<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> צאצאים ב-DOM של Shadow. כל חלונית מארחת עץ צללים משלה עם h2 כותרות. כדי לעצב את הכותרות האלה מהדף הראשי, צריך לכתוב:

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

השילוב /deep/

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

השילוב /deep/ שימושי במיוחד בעולם של 'רכיבים מותאמים אישית', שבו מקובל להשתמש במספר רמות של DOM מסוג Shadow. דוגמאות ראשוניות כוללות מספר אלמנטים מותאמים אישית (שכל אחד מהם מארח עץ צללים משלו) או יוצר אלמנט העובר בירושה באמצעות <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/, אפשר לסגנן כל רכיב בפלטפורמת האינטרנט שמשתמש ב-DOM של צל. דוגמאות נהדרות הן <input> ו<video>:

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

יצירת קטעי הוּק (hooks) לסגנון

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

שימוש בתווים ::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;
}

בדומה לאופן שבו דפדפנים מספקים קטעי הוק (hooks) לפריטי תוכן פנימיים מסוימים, המחברים של תוכן 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. למעשה, יצירת "ערכי placeholder של סגנון" שמשתמשים אחרים ימלאו.

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

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>

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

סגנונות שעברו בירושה, כמו גופנים, צבעים וגובה שורות, ממשיכים להשפיע על אלמנטים ב-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 של Light ולעבד אותם במיקומים שהוגדרו מראש ב-DOM של Shadow. הם לא נמצאים באופן לוגי ב-DOM של הצל; הם עדיין צאצאים של הרכיב המארח. נקודות הוספה הן רק פעולה של עיבוד.

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

::רכיב פסאודו-תוכן

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

::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 לפני שצאצאי המארח מופצים בנקודת ההוספה. המיקום הזה נקרא הגבול התחתון.

סיכום

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

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