Выделение фрагментов текста

Функции для сайтов: Специальные выделения

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

Внутри ::highlight() работает только ограниченный набор свойств CSS: 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, необходимо обеспечить базовую функциональность, при которой текст остается читаемым даже без визуального выделения.

Вы можете проверить поддержку, прежде чем использовать 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);
  }
}