Roses are Red, Violets are Blue. Unexpected '{' on line 32.
上一頁
用 Highlight API 來幫文章畫重點
發佈於 2024年9月19日
這是一個用 <mark></mark> 標記的段落

如果你需要達到以上效果,過去在不使用任何 Library 的情況下,最簡單、只需要 HTML 的方法就是靠 <mark></mark> 標籤。否則就只能透過 CSS 分別對每個需要標記的段落進行樣式設定。

新版網站 satro hello 的搜尋功能是利用 fuse.js hello fuzzy searching,由於 fuse.js 可以拿到符合搜尋條件的位置資訊,所以我就想說,如果可以利用這個資訊,幫使用者標記出符合關鍵字的位置,那將會是一個很棒的事情。

當然,如果稍微寫一下程式,這並不是一件難事,但我就是想找到一個現成的 API、或是現成的 Library (沒錯,我就是懶)。

於是我就發現了已經被「大部分主流瀏覽器」支援的 highlight API hello

你要先知道

雖然說大部分主流瀏覽器,但是你會發現,如果你用的是 Firefox,進入這個網站時會跳出警告:

你正在使用不支援 Highlight API 的瀏覽器(例如:Firefox),這將會導致搜尋功能不會 幫你標示關鍵字,不用擔心,搜尋功能還是可以正常使用。

沒錯,Firefox 就是新 IE,全世界都支援了就剩他,歐盟要怪 Chromium 壟斷市場,Google 也是很冤枉,誰叫 Firefox 始終是這副德行呢?

highlight API Browser Compatibility 1

使用限制

這個 API 使用上還是有所限制:

  • 你不能不透過 JavaScript 來使用
  • 你需要取得 TextObject(通常會搭配 TreeWalker hello )或是把文字拆成一個個的 element,才能精準定位各個關鍵字的位置(因為你想要用位置資訊來標,不然你就不需要這個 API 了)
  • Firefox 不支援、一些比較老舊的瀏覽器也不支援(e.g. IE)

語法說明

建立 Range

// 我們先假設有一個叫做 #content 的 parent
const parentNode = document.querySelector('#content');
const range1 = new Range();
range1.setStart(parentNode, 10); // 這裡的 10 是指從 parentNode 的第 11 個 element 開始
range1.setEnd(parentNode, 20); // 這裡的 20 是指到 parentNode 的第 20 個 element 結束
const range2 = new Range();
range2.setStart(parentNode, 30);
range2.setEnd(parentNode, 40);

建立 highlight

1
// 通常你不會一個個建立變數,他可能會是一個 Array,所以我們先模擬這個情況
2
const ranges = [range1, range2];
3
4
const highlight = new Highlight(...ranges);

以防你不知道,... 是 ES6 的 Spread syntax hello ,可以將一個 Array 展開成多個參數。 所以上面的程式碼會等同於

const highlight = new Highlight(range1, range2);

註冊 highlight

5
CSS.highlights.set('highlight1', highlight)

如果你有很多組 highlight,這裏就沒辦法用 Spread syntax 了喔。你可以用 Array.prototype.forEach hello 來處理。

移除與清除 Highlight

6
CSS.highlights.delete('highlight1') // 移除 highlight1
7
// 或
8
CSS.highlights.clear() // 清除所有 highlight

幫 highlight 加上樣式

/* 你可以放在某個 .css 檔案中或是放在 <style></style> 中 */
::highlight(highlight1) {
background-color: yellow;
}

實際應用

用範圍來標記

假設我們有一個段落:

<div id="content">
Nam eget sem non sapien egestas pulvinar eu sit amet lorem.
</div>

如上,我們想要標記出 lorem 這個關鍵字。利用範圍的話,他是第 57 至 第 61 個字元(因為文字的前後含空格、換行之類的都算在內)。

const content = document.getElementById('#content');
const text = content.childNodes[0];
const range = new Range();
range.setStart(content, 56);
range.setEnd(content, 61);
const highlight = new Highlight(range);
CSS.highlights.set("content", highlight);

因為我們這裡只有一小段,就不用 TreeWalker 了,直接取得 childNodes 的第一個元素就是 TextObject。

最後再加上 CSS:

::highlight(content) {
background-color: yellow;
}

最終的成品大概會長這樣(出現 Highlight not support 的話,就是你的瀏覽器不支援):

做一個簡單的搜尋功能

<div id="content">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc non magna velit. Fusce non viverra turpis, sit amet faucibus enim. Sed tincidunt, diam vitae lobortis cursus, mauris neque cursus lacus, vel ornare nulla massa non augue. Morbi cursus id metus sagittis suscipit. In vitae venenatis libero. Nam facilisis tortor et nibh pretium finibus. Integer eget dolor sagittis, vestibulum enim feugiat, pulvinar ligula. Nunc hendrerit ac nunc nec dapibus. Aenean sit amet placerat est, eu congue leo. Morbi quis lectus id lorem maximus ultricies. Curabitur a sapien eget odio rutrum egestas eu at sem. Donec ut mauris eu diam euismod lobortis. Praesent risus arcu, viverra nec posuere eu, sodales nec leo. Mauris id interdum purus.
</p>
<p>
Curabitur pretium dapibus felis, eu congue sapien lobortis ac. Pellentesque convallis laoreet risus, a mattis dui sollicitudin eget. Aenean sodales sed purus in varius. Aliquam hendrerit aliquam fringilla. Morbi eu lacinia metus, non pretium lacus. In elementum massa dolor, a ullamcorper neque malesuada eu. Donec tristique, velit bibendum convallis maximus, orci ligula euismod quam, eu varius felis odio porttitor massa. Nulla feugiat, dolor quis laoreet gravida, turpis ex aliquet mi, ac varius libero sapien et quam.
</p>
<p>
Integer vulputate enim enim, a commodo dui varius nec. Morbi ut leo auctor, maximus tortor et, venenatis arcu. Suspendisse ac augue porttitor, molestie magna sit amet, gravida neque. Maecenas eu lorem sem. Aliquam ut velit tincidunt, bibendum dolor a, pharetra diam. In euismod ex vel tellus volutpat sollicitudin quis maximus mauris. Sed laoreet vestibulum leo.
</p>
</div>
<input type="text" id="search" placeholder="Search...">

如上,我想要在使用者輸入關鍵字時,將符合的文字標記起來。

首先我們要先取得 contentsearch

const content = document.getElementById("content");
const search = document.getElementById("search");

然後用 TreeWalker 來取得所有的 TextObject:

const treeWalker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT);
const nodes = [];
let node;
while ((node = treeWalker.nextNode())) nodes.push(node);

監聽 searchinput 事件,並搜尋符合的關鍵字:

search.addEventListener("input", (ev) => {
CSS.highlights.clear();
const ranges = [];
const keyword = ev.target.value.trim().toLowerCase();
nodes.forEach((node) => {
for (let i = 0; i < node.length; i++) {
const index = node.wholeText.toLowerCase().indexOf(keyword, i);
if (index !== -1) {
const range = new Range();
range.setStart(node, index);
range.setEnd(node, index + keyword.length);
ranges.push(range);
}
}
});
const highlight = new Highlight(...ranges);
CSS.highlights.set("content", highlight);
});

其中,3 的部分,我們使用了 String.prototype.indexOf 來搜尋關鍵字的位置,這個方法會回傳第一個找到的 index,如果找不到的話會回傳 -1。因此,我們除了傳入 keyword 之外,還要傳入 i

最後,也別忘了加上 CSS:

::highlight(content) {
background-color: yellow;
}

最終的結果會長這樣(出現 Highlight not support 的話,就是你的瀏覽器不支援):

結語

這個 API 雖然有些限制,尤其是 Firefox 不支援,但是對於一些簡單的應用來說,還是很方便的。當然,如果你有更多的需求,那你可能就要自己寫一些程式來滿足了。

Footnotes

  1. highlight API Browser Compatibility hello