الحدّ من مدى وصول أدوات الاختيار باستخدام CSS @scope at-rule

تعرَّف على كيفية استخدام @scope لتحديد العناصر فقط داخل شجرة فرعية محدودة في نموذج كائن المستند (DOM).

دعم المتصفح

  • Chrome: 118.
  • الحافة: 118.
  • متصفّح Firefox: خلف علم
  • Safari: الإصدار 17.4.

المصدر

الفن الدقيق لكتابة أدوات اختيار لغة CSS

عند كتابة محدّدات، قد تجد نفسك منقسمًا بين عالمين. من ناحية، تريد أن تكون محددًا جدًا بشأن العناصر التي تحددها. من ناحية أخرى، تريد أن تظل أدوات الاختيار الخاصة بك سهلة التجاوز وألا تكون مرتبطة بإحكام ببنية DOM.

على سبيل المثال، عندما تريد تحديد "صورة الجزء الرئيسي في منطقة المحتوى لمكون البطاقة"، وهو اختيار عنصر محدّد، لا تريد على الأرجح كتابة أداة اختيار مثل .card > .content > img.hero.

  • تحتوي أداة الاختيار هذه على خصوصية عالية جدًا لـ (0,3,1)، ما يجعل من الصعب تجاوزها مع زيادة الرمز البرمجي.
  • ومن خلال الاعتماد على المُنشئ الثانوي المباشر، يقترن ببنية DOM بإحكام. وإذا تغير الترميز، فيجب تغيير CSS أيضًا.

وتذكَّر أيضًا أنّك لا تريد كتابة img على أنّها أداة الاختيار لهذا العنصر فقط، لأنّ ذلك سيحدّد جميع عناصر الصورة على صفحتك.

وغالبًا ما يكون تحقيق التوازن المناسب في هذه اللعبة تحديًا كبيرًا. على مرّ السنين، ابتكر بعض المطوّرين حلولاً وحلولاً بديلة لمساعدتك في مثل هذه المواقف. على سبيل المثال:

  • وتنص منهجيات مثل BEM على منح هذا العنصر فئة card__img card__img--hero للحفاظ على مستوى الخصوصية منخفض مع السماح لك بالتركيز في الوقت نفسه على ما تختاره.
  • تعمل الحلول المستندة إلى JavaScript مثل CSS النطاق أو المكونات النمطية على إعادة كتابة جميع أدوات الاختيار من خلال إضافة سلاسل تم إنشاؤها عشوائيًا، مثل sc-596d7e0e-4، إلى أدوات الاختيار لمنعها من استهداف العناصر على الجانب الآخر من صفحتك.
  • تلغي بعض المكتبات أدوات الاختيار تمامًا وتطلب منك وضع مشغِّلات النمط مباشرةً في الترميز نفسه.

ولكن ماذا لو لم تكن بحاجة إلى أيٍّ منها؟ ماذا لو أعطاك CSS طريقة لتكون دقيقًا جدًا في ما يتعلق بالعناصر التي تحددها، بدون الحاجة إلى كتابة محددات عالية الدقة أو عناصر مرتبطة بإحكام بنموذج DOM الخاص بك؟ وهنا يأتي دور @scope، ما يوفّر لك طريقة لاختيار العناصر فقط داخل شجرة فرعية في DOM.

نقدّم لك @scope

باستخدام @scope، يمكنك الحدّ من مدى وصول أدوات الاختيار. ويمكنك إجراء ذلك من خلال ضبط جذر النطاق الذي يحدِّد الحدّ الأعلى للشجرة الفرعية التي تريد استهدافها. من خلال مجموعة جذر تحديد النطاق، لا يمكن لقواعد الأنماط المضمّنة، المسماة قواعد الأنماط ذات النطاق، الاختيار إلا من الشجرة الفرعية المحدودة في نموذج العناصر في المستند (DOM).

على سبيل المثال، لاستهداف عناصر <img> فقط في المكوِّن .card، يجب ضبط .card كجذر تحديد نطاق قاعدة @scope.

@scope (.card) {
    img {
        border-color: green;
    }
}

يمكن لقاعدة النمط ذات النطاق img { … } اختيار عناصر <img> التي تقع في نطاق عنصر .card المطابق بشكل فعّال.

لمنع اختيار عناصر <img> داخل منطقة محتوى البطاقة (.card__content)، يمكنك جعل أداة الاختيار img أكثر تحديدًا. ويمكن إجراء ذلك أيضًا باستخدام حقيقة أنّ قاعدة @scope تقبل أيضًا حد النطاق الذي يحدّد الحدّ الأدنى.

@scope (.card) to (.card__content) {
    img {
        border-color: green;
    }
}

لا تستهدف قاعدة النمط ذات النطاق الواسع هذه سوى عناصر <img> الموضوعة بين عناصر .card و.card__content في شجرة الأصل. غالبًا ما يُشار إلى هذا النوع من النطاقات ذات الحدود العليا والسفلية باسم نطاق الدونات.

أداة اختيار :scope

بشكل تلقائي، تكون جميع قواعد الأنماط ذات النطاق مرتبطة بجذر النطاق. ويمكن أيضًا استهداف عنصر الجذر نفسه لتحديد النطاق. لإجراء ذلك، يمكنك استخدام أداة اختيار ":scope".

@scope (.card) {
    :scope {
        /* Selects the matched .card itself */
    }
    img {
       /* Selects img elements that are a child of .card */
    }
}

تتم إضافة :scope ضمنًا إلى أدوات الاختيار داخل قواعد الأنماط ذات النطاق الفرعي. يمكنك أن تكون واضحًا وصريحًا بشأن ذلك، من خلال إضافة :scope بنفسك في بدايته. بدلاً من ذلك، يمكنك إضافة أداة اختيار & في البداية، من CSS Nesting.

@scope (.card) {
    img {
       /* Selects img elements that are a child of .card */
    }
    :scope img {
        /* Also selects img elements that are a child of .card */
    }
    & img {
        /* Also selects img elements that are a child of .card */
    }
}

يمكن لحدّ النطاق استخدام الفئة الزائفة :scope لطلب علاقة محدّدة بجذر النطاق:

/* .content is only a limit when it is a direct child of the :scope */
@scope (.media-object) to (:scope > .content) { ... }

ويمكن أن يشير حد النطاق أيضًا إلى عناصر خارج جذر النطاق باستخدام :scope. على سبيل المثال:

/* .content is only a limit when the :scope is inside .sidebar */
@scope (.media-object) to (.sidebar :scope .content) { ... }

تجدر الإشارة إلى أنّ قواعد النمط ذات النطاق الفرعي نفسها لا يمكن أن تتخطّى الشجرة الفرعية. الاختيارات مثل :scope + p غير صالحة لأنها تحاول اختيار عناصر خارج النطاق.

@scope والدقة

لا تؤثر المحددات التي تستخدمها في مقدمة @scope في خصوصية أدوات الاختيار المضمنة. في المثال أدناه، تظل خصوصية أداة الاختيار img هي (0,0,1).

@scope (#sidebar) {
    img { /* Specificity = (0,0,1) */
        …
    }
}

وإنّ خصوصية :scope هي خاصّة بفئة زائفة عادية، وهي (0,1,0).

@scope (#sidebar) {
    :scope img { /* Specificity = (0,1,0) + (0,0,1) = (0,1,1) */
        …
    }
}

في المثال التالي، تتم إعادة كتابة & داخليًا إلى أداة الاختيار المستخدَمة لجذر تحديد النطاق، ويتم التفافها داخل أداة اختيار :is(). في النهاية، سيستخدم المتصفّح :is(#sidebar, .card) img كأداة اختيار لإجراء المطابقة. تُعرف هذه العملية باسم إزالة المحتوى.

@scope (#sidebar, .card) {
    & img { /* desugars to `:is(#sidebar, .card) img` */
        …
    }
}

بما أنّه تتم إزالة تصنيف & باستخدام :is()، يتم احتساب خصوصية & باتّباع قواعد الخصوصية :is(): وتكون خصوصية & أكثر تحديدًا للوسيطة الخاصة بها.

وعند تطبيق هذا المثال، تكون خصوصية :is(#sidebar, .card) هي الوسيط الأكثر تحديدًا، وهي #sidebar، وبالتالي تصبح (1,0,0). ويمكنك دمجها مع خصوصية img، وهي (0,0,1)، وستحصل في النهاية على (1,0,1) كدقة خاصة لأداة الاختيار المركّبة بالكامل.

@scope (#sidebar, .card) {
    & img { /* Specificity = (1,0,0) + (0,0,1) = (1,0,1) */
        …
    }
}

الفرق بين :scope و& داخل @scope

إلى جانب الاختلافات في كيفية احتساب الخصوصية، هناك اختلاف آخر بين :scope و& هو أنّ :scope يمثل جذر النطاق المتطابق، بينما يمثّل & أداة الاختيار المستخدَمة لمطابقة جذر تحديد النطاق.

ولهذا السبب، من الممكن استخدام & عدة مرات. وهذا على عكس :scope الذي لا يمكنك استخدامه سوى مرة واحدة، لأنه لا يمكنك مطابقة جذر تحديد نطاق داخل جذر تحديد نطاق.

@scope (.card) {
  & & { /* Selects a `.card` in the matched root .card */
  }
  :scope :scope { /* ❌ Does not work */
    …
  }
}

نطاق بدون تمهيد

عند كتابة أنماط مضمَّنة باستخدام العنصر <style>، يمكنك تحديد نطاق قواعد النمط على العنصر الرئيسي المضمّن في العنصر <style> من خلال عدم تحديد أي جذر نطاق. ويمكنك إجراء ذلك من خلال حذف مقدمة @scope.

<div class="card">
  <div class="card__header">
    <style>
      @scope {
        img {
          border-color: green;
        }
      }
    </style>
    <h1>Card Title</h1>
    <img src="…" height="32" class="hero">
  </div>
  <div class="card__content">
    <p><img src="…" height="32"></p>
  </div>
</div>

في المثال أعلاه، لا تستهدف القواعد المحدّدة النطاق سوى العناصر داخل div باسم الفئة card__header، لأنّ div هو العنصر الرئيسي للعنصر <style>.

@scope in the cascade

داخل تتالي CSS، تضيف @scope أيضًا معيارًا جديدًا: تحديد النطاق. تأتي الخطوة بعد التحديد ولكن قبل ترتيب الظهور.

التمثيل البصري لـ CSS Cascade.

وفقًا للمواصفات:

عند مقارنة التعريفات التي تظهر في قواعد النمط ذات الجذور الصغيرة المختلفة، يُفوز الإعلان الذي يتضمن أقل عدد من العناصر التابعة أو العناصر التابعة للأجيال بين جذر النطاق وموضوع قاعدة النمط واسع النطاق.

تصبح هذه الخطوة الجديدة مفيدة عند دمج عدة أشكال للمكون. إليك هذا المثال الذي لا يستخدم @scope بعد:

<style>
    .light { background: #ccc; }
    .dark  { background: #333; }
    .light a { color: black; }
    .dark a { color: white; }
</style>
<div class="light">
    <p><a href="#">What color am I?</a></p>
    <div class="dark">
        <p><a href="#">What about me?</a></p>
        <div class="light">
            <p><a href="#">Am I the same as the first?</a></p>
        </div>
    </div>
</div>

عند عرض هذا الجزء الصغير من الترميز، سيكون الرابط الثالث هو white بدلاً من black، مع أنّه عنصر فرعي للعلامة div مع تطبيق الفئة .light عليه. ويرجع ذلك إلى ترتيب معيار الظهور الذي يستخدمه التتابع هنا لتحديد الفائز. يرى أنّ إعلان .dark a قد تم الإعلان عنه أخيرًا، لذا ستفوز من قاعدة .light a.

وقد تم حل ذلك الآن باستخدام معيار التقارب لتحديد النطاق:

@scope (.light) {
    :scope { background: #ccc; }
    a { color: black;}
}

@scope (.dark) {
    :scope { background: #333; }
    a { color: white; }
}

نظرًا لتوفُّر أداة اختيار a ذات النطاق نفسه لهما نفس الدقة، يتم بدء تنفيذ معيار التقارب لتحديد النطاق. وهي توازن كلا القائمين حسب القرب من جذر النطاق الخاص بهما. بالنسبة إلى عنصر a الثالث، يكون ناتجها قفزة واحدة فقط مع جذر تحديد نطاق .light، بينما تساوي قفزة واحدة مع قيمة .dark. وبالتالي، ستفوز أداة اختيار a في .light.

الملاحظة الختامية: عزل أداة الاختيار، وليس عزل النمط

يُرجى العِلم أنّ @scope يحدّ من مدى وصول المحدِّدات إلى الجمهور، ولا يتيح عزل النمط. ستظل المواقع الإلكترونية المكتسبة ضمن العناصر الفرعية مكتسبة، خارج الحد الأدنى للسمة @scope. وإحدى هذه السمات هي السمة color. عند الإعلان عن أن واحدًا داخل نطاق دائري مجوف، سيظل color يرث للأطفال داخل ثقب الكعكة.

@scope (.card) to (.card__content) {
  :scope {
    color: hotpink;
  }
}

في المثال أعلاه، يحتوي العنصر .card__content وعناصره الثانوية على لون hotpink لأنّها تكتسِب القيمة من .card.

(صورة غلاف من قناة rustam burkhanov على Unspark)