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

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

Browser Support

  • Chrome: 118.
  • Edge: 118.
  • Firefox: behind a flag.
  • Safari: 17.4.

Source

الفن الدقيق لكتابة أدوات اختيار لغة 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

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

عند مقارنة البيانات التي تظهر في قواعد الأنماط بجذور تحديد نطاق مختلفة، يتم اختيار البيان الذي يتضمّن أقل عدد من القفزات بين العنصر الشقيق أو العنصر من الجيل التالي وجذر تحديد النطاق وموضوع قاعدة الأنماط ذات النطاق.

تكون هذه الخطوة الجديدة مفيدة عند دمج عدّة صيغ لمكوّن معيّن. إليك هذا المثال الذي لا يستخدم @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)