تمييز نطاقات النص

تتيح لك واجهة CSS Custom Highlight API تصميم نطاقات نصية عشوائية على صفحة معيّنة بدون تعديل بنية DOM. يتيح ذلك تمييز نتائج البحث وتلوين البنية ومؤشرات التعديل التعاوني أو علامات الأخطاء الإملائية والنحوية بدون التفاف النص في عناصر إضافية أو الاعتماد على معالجة innerHTML.

التنفيذ الأساسي

لتمييز نطاقات النص، يجب جمع عُقد النص المستهدَف وإنشاء كائنَي Range وHighlight وتسجيلهما في HighlightRegistry، ثم تنسيقهما باستخدام العنصر الزائف ::highlight().

1. جمع عُقد النص وإنشاء النطاقات

استخدِم TreeWalker لجمع كل عُقد النص في العنصر المستهدَف، ثم أنشئ عناصر Range تشير إلى إزاحات الأحرف التي تريد تمييزها.

const article = document.querySelector("article");

// MANDATORY: Use TreeWalker to collect text nodes — do not manipulate innerHTML.
const treeWalker = document.createTreeWalker(article, NodeFilter.SHOW_TEXT);
const allTextNodes = [];
let currentNode = treeWalker.nextNode();
while (currentNode) {
  allTextNodes.push(currentNode);
  currentNode = treeWalker.nextNode();
}

// MANDATORY: Set range start/end on text nodes, not element nodes.
const range = new Range();
range.setStart(textNode, matchStartIndex);
range.setEnd(textNode, matchEndIndex);

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

2. إنشاء لحظة مميّزة من النطاقات

يمكنك تجميع عنصر واحد أو أكثر من عناصر Range في Highlight. تنتمي النطاقات المتعددة التي تشترك في النمط نفسه إلى تمييز واحد.

const searchHighlight = new Highlight(...matchingRanges);

3- تسجيل اللحظة المميّزة في السجلّ

سجِّل كل Highlight باسم مخصّص باستخدام CSS.highlights، وهو HighlightRegistry يشبه Map.

// MANDATORY: Clear previous highlights before registering new ones
// to avoid stale ranges persisting on the page.
CSS.highlights.clear();

CSS.highlights.set("search-results", searchHighlight);

عند تداخل عدّة نصوص مميّزة، استخدِم السمة priority للتحكّم في ترتيب التكديس. يتم عرض النقاط البارزة ذات الأولوية الأعلى في الأعلى.

const primary = new Highlight(...primaryRanges);
primary.priority = 1;

const secondary = new Highlight(...secondaryRanges);
secondary.priority = 0; // painted first (behind primary)

CSS.highlights.set("primary", primary);
CSS.highlights.set("secondary", secondary);

4. التنسيق باستخدام ::highlight()

استخدِم العنصر الزائف ::highlight() في CSS لتنسيق كل تمييز مسجَّل حسب الاسم.

::highlight(search-results) {
  background-color: #ffdd00;
  color: black;
}

لا تعمل سوى مجموعة محدودة من خصائص CSS داخل ::highlight(): color وbackground-color وtext-decoration وخصائصها الطويلة وtext-shadow و-webkit-text-stroke-color و-webkit-text-fill-color و-webkit-text-stroke-width. يتم تجاهل السمات مثل background-image أو font-size أو padding.

تسهيل الاستخدام

AVOID استخدام ميزة "تمييز النص المخصّص" كبديل لترميز HTML الدلالي.

تُعدّ ميزات التمييز المخصّصة مخصّصة للعرض فقط ولا يتم عرضها في شجرة تسهيل الاستخدام. إذا كان النص المميّز ذا صلة دلالية بالمستند (على سبيل المثال، فقرة اختارها المستخدم)، استخدِم <mark> بدلاً من ذلك. احتفِظ بميزة "تمييز النص المخصّص" للتأثيرات المؤقتة والمرئية فقط، مثل نتائج البحث أو تلوين البنية.

يجب ألا تعتمد النقاط البارزة على اللون فقط لنقل المعنى. إذا كان التمييز يشير إلى خطأ، يمكنك إقرانه بمؤشر مرئي آخر مثل text-decoration: wavy underline أو تصنيف نصي مجاور. تأكَّد من توفّر تباين كافٍ بين خلفية النص المميَّز ولون النص لتلبية متطلبات WCAG 2.1 (‏4.5:1 على الأقل للنص العادي).

استراتيجيات احتياطية

بالنسبة إلى المتصفّحات التي لا تتوافق مع CSS Custom Highlight API، يجب توفير تجربة أساسية فعّالة يظل فيها النص مقروءًا، حتى بدون التمييز المرئي.

يمكنك رصد مدى توفّر واجهة برمجة التطبيقات قبل استخدامها:

if (CSS.highlights) {
  // CSS Custom Highlight API is supported.
} else {
  // Fallback: wrap matches in <mark> elements.
}

إذا كان تمييز النص مهمًا لتجربة المستخدم، يمكنك استخدام عناصر <mark> لتضمين النص المطابق. يؤدي ذلك إلى تعديل نموذج العناصر في المستند (DOM)، لذا يجب الحرص على الحفاظ على متتبِّعات الأحداث وتجنُّب إيقاف بنية المستند.

if (!CSS.highlights) {
  // Walk text nodes and wrap matches in <mark>, preserving structure.
  const walker = document.createTreeWalker(article, NodeFilter.SHOW_TEXT);
  const nodes = [];
  for (let n = walker.nextNode(); n; n = walker.nextNode()) nodes.push(n);

  const term = searchTerm.toLowerCase();
  for (const textNode of nodes) {
    const text = textNode.textContent;
    let pos = text.toLowerCase().indexOf(term);
    if (pos === -1) continue;

    const frag = document.createDocumentFragment();
    let last = 0;
    while (pos !== -1) {
      frag.append(text.slice(last, pos));
      const mark = document.createElement("mark");
      // textContent assignment avoids HTML injection.
      mark.textContent = text.slice(pos, pos + term.length);
      frag.append(mark);
      last = pos + term.length;
      pos = text.toLowerCase().indexOf(term, last);
    }
    frag.append(text.slice(last));
    textNode.replaceWith(frag);
  }
}