الحدّ من مدى وصول أدوات الاختيار باستخدام 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 على أنّه جذر النطاق لقاعدة at-rule الخاصة بـ @scope.

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

لا يمكن لقاعدة الأسلوب على مستوى النطاق img { … } اختيار عناصر <img> إلا إذا كانت ضمن النطاق للعنصر .card المطابق.

لمنع اختيار عناصر <img> داخل منطقة محتوى البطاقة (.card__content)، يمكنك جعل أداة اختيار img أكثر تحديدًا. هناك طريقة أخرى لإجراء ذلك وهي استخدام حقيقة أنّ قاعدة at-rule‏ @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.

@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 في التسلسل

ضمن تسلسل 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 على Unsplash)