ช่วงข้อความไฮไลต์

ฟีเจอร์บนเว็บ: ไฮไลต์ที่กำหนดเอง

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 อย่างน้อย 1 รายการเป็น 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 และ Longhand, 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 คุณควรสร้างประสบการณ์การใช้งานพื้นฐานที่ใช้งานได้ซึ่งข้อความยังคงอ่านได้ แม้จะไม่มีไฮไลต์ภาพก็ตาม

คุณสามารถตรวจหาการรองรับก่อนใช้ API ได้ดังนี้

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

หากการไฮไลต์มีความสำคัญต่อประสบการณ์ของผู้ใช้ ให้กลับไปใช้การรวมข้อความที่ตรงกันในองค์ประกอบ <mark> การดำเนินการนี้จะแก้ไข DOM ดังนั้นโปรดระมัดระวังในการเก็บรักษา Listener เหตุการณ์และหลีกเลี่ยงการทำลายโครงสร้างเอกสาร

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);
  }
}