はじめに
私たちを取り巻くWeb技術は、もはや社会的なインフラとしてめまぐるしく進化しています。HTMLやCSSはもちろんのこと、JavaScriptやライブラリ、フレームワークなど、それぞれがニーズにキャッチアップする形で、機能強化を繰り返しています。その中でも、Web技術の中核に位置するにもかかわらず、意外と見過ごされがちなのがHTMLの進化です。本連載は、このHTMLと関連するJavaScript APIにフォーカスして、その新機能を手軽に試していただこうというものです。理解も利用もたやすいHTMLなので、ライトな気持ちで「こんなことができるようになったのか」を感じていただきます。
[NOTE]サンプルについて
本記事の配布サンプルは、以下のURLから入手できます。新機能を試していくので、ブラウザは最新である方がよいでしょう。本記事のサンプルは、執筆時点で最新のGoogle Chromeで動作することを確認しています。
https://github.com/wateryinhare62/mynavi_html
連載第7回の目的
この回では、DOM(Document Object Model)の書き換えなしにテキストの一部の色などを変更できるCSS Custom Highlight APIを試します。検索結果で該当キーワードを強調表示できれば便利ですが、これをDOMの書き換えなしに実現する例を紹介します。
Custom Highlightとは
Custom Highlightとは、Webページの特定の文字範囲をハイライト表示する機能です。ハイライト表示は特定の文字範囲をspan要素などで囲み、CSSでスタイルを設定するのが一般的です。しかしながら、ハイライト表示したい文字範囲がページの表示中に変化する場合には、span要素の挿入や削除にはDOMの書き換えを伴い、コーディングと実行の両面でその負担はけして小さなものではありません。
Custom Highlightを使うと、DOMの書き換えを必要とせず、簡単なプログラミング処理とスタイル設定のみで、特定の文字範囲をハイライト表示できます。DOMを変更しないので速度面で優れており、リアルタイム検索結果の表示、誤った用語用法の指摘、プログラムコードのエラー箇所の表示など、即時性が重要な用途でCustom Highlightによるハイライト表示は威力を発揮するでしょう(図1)。
Custom Highlightは、基本的にはCSS Custom Highlight APIを中心に動作します。文字範囲を表すオブジェクトを生成し、それをCustom Highlight専用のリポジトリに登録することで、その範囲に特定のスタイルを適用できます。疑似属性も拡張されており、スタイルの指定はいつも通りと簡便です。
Custom Highlightのブラウザサポートは基本的にWidely availableです(一部の機能は、2025年にサポートされたばかりのNewly available)。Firefoxで一部サポートされない機能もありますが、ほとんどの機能が多くのブラウザで利用できると思ってよいでしょう。
以降、いくつかのシンプルな例を通じてCustom Highlightの基本的な使い方を紹介していきます。
基本的なCustom Highlight
Custom Highlightの基本は、以下の流れとなります。
- 文字範囲を指定するRangeオブジェクトを(複数)生成する
- RangeオブジェクトからHighlightオブジェクトを(複数)生成する
- Highlightオブジェクトを登録する
- ::highlight疑似要素でスタイルを設定する
これを見ると、基本的にはJavaScriptによるプログラミングが中心で、それにスタイル設定が加わる感じです。HTMLは、あくまでもハイライト表示のための対象を用意するだけの存在です。
[NOTE]CSS Custom Highlight API
CSS Custom Highlight APIは、後述するHighlightオブジェクトとHighlightRepositoryオブジェクト、それにCSS.highlightsプロパティを指します。Rangeオブジェクトは範囲保持のための汎用的なオブジェクトであり、CSS Custom Highlight APIはそれを利用してハイライト表示するといった関係になっています。
適当なC#のコードをハイライト表示する例を通じて、Custom Highlightの使い方を見ていきましょう(図2)。
HTMLファイルの用意
まずは、対象となるHTMLファイルを用意します。内容は、前述の適当なC#のコードです(リスト1)。これに、CSS(ch_basic.css)読み込みのためのlink要素とJavaScript(ch_basic.js)読み込みのためのscript要素があるだけの単純なものです。
リスト1:ch_basic.html
<h1>Custom Highlightの基本</h1>
<pre>
public static void BubbleSortArray(int[] array)
{
…略…
}
</pre>
Rangeオブジェクトの生成
ハイライト表示には、対象の文字範囲を表すRangeオブジェクトを生成する必要があります。Rangeオブジェクトは、基本的にはTextノードに対して、文字範囲の始点と終点を指定するという形で生成します。Rangeオブジェクト自体は汎用的なものですが、Custom Highlightにおいては重要な役割を果たすので、少し詳しく解説します。Rangeオブジェクト生成のJavaScriptコードは、リスト2の通りです。
リスト2:ch_basic.js
// (1)Custom Highlightがサポートされているかチェック
if (!CSS.highlights) { (1)
alert('CSS Custom Highlightをサポートしていません。');
} else {
// (2)ハイライト対象のTextノードを取得
const node = document.querySelector('pre').firstChild;
// (3)範囲オブジェクトの生成
const range = new Range(); (4)
range.setStart(node, 14); (5)
range.setEnd(node, 18); (6)
まず重要なのは、(1)のCSS Custom Highlightサポートのチェックです。CSS Custom Highlightは広く利用可能にはなっていますが、サポートの有無をチェックするのは常道なので、CSS.highlightsオブジェクトが非nullであるかで確認しています。なお、このオブジェクトは後ほどHighlightオブジェクトの登録に使いますが、以降のリストではチェック部分の掲載を省略しますので注意してください。
(2)では、ハイライト対象のTextノードを取得しています。pre要素の子ノードはTextノード(テキストに相当する「public static void BubbleSortArray(int[] array)…}」)のみなので、firstChildメソッドで最初の子ノードのみを取り出しています。
(3)以降はRangeオブジェクトの生成です。Rangeオブジェクト自体は(4)のようにRangeクラスのコンストラクタか、あるいはDocumentオブジェクトのcreateRangeメソッドで生成します。どちらでも結果としてRangeオブジェクトが得られます。
Rangeオブジェクトに始点と終点を設定するのは、setStartメソッド(5)とsetEndメソッド(6)です。それぞれ、対象のTextノードと0からの文字位置を受け取って、範囲として設定します。setEndメソッドに指定するのは、範囲最後の文字の、次の位置となることに注意します。本来はきちんと検索して位置を決めるのですが、ここではRangeオブジェクトの生成方法に集中するために、固定位置としています。
[NOTE]ノードの種類とRangeオブジェクト
Rangeオブジェクトに指定するノードの種類によって、範囲の意味が変わってくるので注意してください。ここではTextノードを指定したので、setStart/setEndメソッドで示している範囲は文字単位となります。ノードがHTML要素など文字以外の場合、範囲はノード単位となります。ハイライトの趣旨からいうとノード単位を指定する局面は少ないと思うので、本稿では基本的に文字範囲を指定するものとして解説します。
Highlightオブジェクトの生成
もちろん、このままではハイライト表示されません。RangeオブジェクトからHighlightオブジェクトを生成し、それをCustom Highlightのためのリポジトリ(格納庫)に登録する必要があります。このJavaScriptコードはリスト3の通りです。
リスト3:ch_basic.js
// ハイライトオブジェクトの生成と登録
const highlight = new Highlight(range); (1)
CSS.highlights.set("highlight", highlight); (2)
(1)がHighlightオブジェクトの生成です。コンストラクタ引数にRangeオブジェクトを指定することで、その範囲でHighlightオブジェクトが生成されます。
(2)がHighlightオブジェクトの登録であり、CSS.highlights.setメソッドの呼び出しで、HighlightRepositoryというリポジトリに登録されます。1番目の引数はCSSで参照するときの名前、2番目の引数がHighlightオブジェクトです。HighlightRepositoryはマップのような構造を持っているので、setで追加するほかgetで取得したり、keys、valuesなどのメソッドで名前やHighlightオブジェクトのリストを取得することもできます。
スタイルの設定
ここまでで、ひとまず必要なコードは整いました。最後に、CSSファイルにスタイルを設定します。スタイルの設定は、::highlight疑似要素でスタイルを設定するだけです(リスト4)。
リスト4:ch_basic.css
::highlight(highlight) {
background-color: lightgray;
color: red;
font-weight: bold;
}
疑似要素の引数に、CSS.highlights.setメソッドで引数に与えた名前が指定されていることに注目です。これにより、複数のハイライトに異なるスタイルを適用することができます。
文字列を検索して複数のハイライト表示
HighlightRepositoryはリポジトリであるので、複数のHighlightオブジェクトを登録することで、それぞれに異なるハイライト表示が可能です。前節のサンプルを改変して、複数の文字列を検索、それぞれに異なる背景と文字色を与える例を紹介します(図3)。
大枠のJavaScriptコードはリスト5のようになります(HTMLは基本的に前節と同じ)。文字列を検索してハイライト表示する部分は別掲します。
リスト5:ch_highlights.js
// (1)検索対象のTextノードとテキストを取得
const node = document.querySelector('pre').firstChild;
const text = node.textContent;
// (2)ハイライトする検索文字列の配列
const searchWords = [
{index: 1, text: 'int'},
{index: 2, text: 'array'},
{index: 3, text: 'for'}
];
// (3)Textノード内で指定されたテキストを検索し、ハイライトを適用する関数
const createHighlight = (index, searchText) => {
…別掲…
};
// (4)それぞれの検索文字列についてハイライトを作成
searchWords.forEach(({index, text}) => {
createHighlight(index, text);
});
(1)のように対象のTextノードを用意するのは前節と変わりませんが、実際に検索するので内包するテキストも取得しています。また、文字位置をハードコードする替わりに、(2)のようにインデックスと検索文字列からなる配列を用意します。(4)ではそれぞれの要素について、(3)の関数を呼び出してハイライトを作成します。重要なのは(3)で、リスト6のようになります。これは、次節で紹介するリアルタイム検索のハイライト表示でも使う内容です。
リスト6:ch_highlights.js
// Textノード内で指定されたテキストを検索し、ハイライトを適用する関数
const createHighlight = (index, searchText) => {
// (1)指定されたテキストが出現するすべての位置を検索
const indices = [];
let startPos = 0;
while (startPos < text.length) {
const index = text.indexOf(searchText, startPos);
if (index === -1) break;
indices.push(index);
startPos = index + searchText.length;
}
// (2)見つかった位置に対してRangeオブジェクトの配列を生成
const ranges = indices.map(i => {
const range = new Range();
range.setStart(node, i);
range.setEnd(node, i + searchText.length);
return range;
});
// (3)Highlightオブジェクトを生成して登録
const highlight = new Highlight(...ranges);
CSS.highlights.set(`highlight-${index}`, highlight);
};
createHighlightは、引数にインデックス(検索文字列の番号)と検索文字列を受け取って、見つかった検索文字列に対してハイライトを作成する関数です。
まず、Textノードから文字列をindexOfメソッドで検索し、その位置の配列を作成します(1)。配列とするのは、複数箇所に文字列が見つかる可能性があるためです。そして、(2)のように発見位置の配列から対応するRangeオブジェクトの配列を生成し、(3)でハイライト登録するという流れです(Rangeオブジェクトを渡す際にスプレッド構文を使っていることに注意)。setメソッドによる登録の際には、検索文字列に応じて異なるスタイルが適用されるように、インデックスを登録のキーに使用します。
これに合わせて、CSS側も3つのスタイルを用意し、例えば「::highlight(highlight-1)」といったセレクタで参照できるようにしています(CSSファイルの全容は配布サンプルを参照)。
複数ノードからリアルタイム検索してハイライト表示
本節ではまとめとして、テキスト入力ボックスから入力した複数の文字列を、複数のTextノードからリアルタイムで検索して、それぞれをハイライト表示する例を紹介します(図4)。とはいえ、ほとんどの部分はこれまでと変わるものではありません。ここでは、リアルタイム検索で変わるポイントに絞って解説します。
HTMLファイルの用意
HTMLファイルにはテキスト入力ボックスを2個配置して、検索対象全体をdiv要素で囲みます(リスト7)。入力ボックスを2個にして、それぞれの検索文字列に異なるハイライト表示を行います。
リスト7:ch_realtime.html
<label>検索テキスト1:<input id="search_text1" type="text"></label><br>
<label>検索テキスト2:<input id="search_text2" type="text"></label>
<div id="target">
<h1>夜の庭</h1>
…略…
</div>
Textノードとテキストの配列を生成
JavaScriptコードは長くなるので、まずは大枠を示します(リスト8)。
リスト8:ch_realtime.js
// (1)検索文字列入力ボックスと検索対象の要素を取得
const textBox1 = document.getElementById('search_text1');
const textBox2 = document.getElementById('search_text2');
const target = document.querySelector('div#target');
// (2)Textノードとテキストの配列を生成
const tw = document.createTreeWalker(target, NodeFilter.SHOW_TEXT);
const textNodes = [];
let currentNode = tw.nextNode();
while (currentNode) {
textNodes.push({node: currentNode, text: currentNode.textContent});
currentNode = tw.nextNode();
}
// (3)Textノード配列内で指定されたテキストを検索し、ハイライトを適用する関数
const createHighlight = (index, searchText) => {
…別掲…
};
// (4)検索テキストボックスのイベントハンドラ
function inputListener() {
// HighlightRegistryをクリア
CSS.highlights.clear();
// 個別にハイライトを作成
createHighlight('1', textBox1.value.trim());
createHighlight('2', textBox2.value.trim());
}
// (5)イベントリスナを登録
textBox1.addEventListener('input', inputListener);
textBox2.addEventListener('input', inputListener);
(1)の要素オブジェクトの取得は特に説明は不要でしょう。
(2)では、Textノードが複数になるので、文字列の検索を簡単に、かつRangeオブジェクトの生成時に渡すために、全てのTextノードと内包するテキストの配列textNodesを生成しています。配列の生成には、createTreeWalkerメソッドでTreeWalkerオブジェクト(ドキュメントのサブツリーのノードおよびその位置を表すオブジェクト)を生成して全ノードを取り出すのが簡単です。このとき、オプションのSHOW_TEXTで、Textノードのみの取り出しを指定します。あとは、nextNodeメソッドでTextノードを順番に取り出し、配列textNodesに内包するテキストであるtextContentプロパティの値とともに格納します。
(3)のcreateHighlight関数は前節でも登場したものですが、複数のTextノード対応版として後ほど別掲します。
(4)と(5)についての説明は特に不要と思いますが、(4)のclearメソッドに注目です。全てのハイライト表示を消去するメソッドで、この処理がないと、過去のハイライト表示が蓄積されて、見た目が訳のわからないことになってしまうためです。
ハイライト登録関数
最後は、別掲としていたcreateHighlight関数です(リスト8)。基本は前節で示したとおりなので、ここでは複数のTextノード対応版としての違いに絞って解説します。
リスト8:ch_search.js
// Textノード配列内で指定されたテキストを検索し、ハイライトを適用する関数
function createHighlight(i, search_text) {
// (1)検索テキストが空の場合は処理を終了
if (search_text === '') {
return;
}
// (2)すべてのTextノードから検索テキストを探す
const ranges = textNodes.map(({node, text}) => {
…前節のch_highlights.jsと同じ…
return ranges; (3)
});
// (4)Highlightオブジェクトを生成して登録
const highlight = new Highlight(...ranges.flat()); (5)
CSS.highlights.set(`highlight-${index}`, highlight);
}
まず、リアルタイム検索するので、検索文字列が空の場合もありえます。このときは検索そのものを省略して無駄を省きます(1)。
次に、複数のTextノードに対応するために、textNodes配列のそれぞれについてRangeオブジェクトを生成して配列とする処理を(2)のように挿入します。繰り返し処理の最後に(3)のようにreturn文を追加して、個々のRangeオブジェクトを配列化します。
(4)も大きく変わるものではありませんが、一つ異なるのは、Rangeオブジェクトの配列に対して(5)のようにflatメソッドを適用してから展開している点です。これは、Rangeオブジェクトの配列が2次元となるので、それを1次元に平坦化するのが目的です。これに伴い、検索文字列が見つからなかった場合の空の要素も削除されます。
まとめ
Custom Highlightはいかがでしたでしょうか。JavaScriptでの実装がメインになるのでやや難しい印象ですが、手順はほぼ定型なので、アイデア次第でさまざまな用途に活用できそうなのをお伝えできたのではないかと思います。次回は、メディアやビューポートの条件に応じて最適な画像をブラウザが表示するsrcset属性とsizes属性を紹介します。
WINGSプロジェクト 山内直(著) 山田 祥寛(監修)
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
RSS
X:@WingsPro_info(公式)、@WingsPro_info/wings(メンバーリスト)<著者について>
WINGSプロジェクト所属のテクニカルライター。出版社を経てフリーランスとして独立。ライター、エディター、デベロッパー、講師業に従事。屋号は「たまデジ。」。



