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

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

تاريخ النشر: 4 أكتوبر 2023

Browser Support

  • Chrome: 118.
  • Edge: 118.
  • Firefox: 146.
  • Safari: 17.4.

Source

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

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

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

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

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

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

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

نقدّم لكم ‎ @scope

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

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

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

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

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

@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 Cascade، يضيف @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.