常常遇到项目中需要全局搜索🔍,但是不想用浏览器自带的搜索功能,这时候懒得✍️,一直没时间写, 抽出空来,还是自己实现一个吧,方便自己,也方便他人。
要能移动位置、以及修改样式,这两点比较重要
代码放在了github上面,需要自取,修改一下样式就行
功能
主要功能:
- 创建一个自定义搜索框 允许用户输入搜索内容。
- 高亮显示匹配的文本 并且能够通过上下按钮跳转到不同的匹配项。
- 支持拖拽功能 用户可以自由移动搜索框。
- 自定义高亮颜色 当前高亮项为橙色,其它高亮项为黄色。
- 快捷键支持 通过监听ctrl+f打开搜索框
实现步骤
1. 创建 TextSearch 类
首先,我们定义 TextSearch 类的构造函数,初始化默认配置,并在页面加载时自动创建搜索框。
class TextSearch {
constructor(options = {}) {
this.options = {
backgroundColor: '#fff',
textColor: '#000',
highlightColor: 'yellow', // 默认黄色
currentHighlightColor: 'orange', // 当前高亮为橙色
searchBoxWidth: '96%',
...options,
};
this.matches = [];
this.currentIndex = -1;
this.createSearchBox();
this.listenForSearch();
this.clearHighlights();
}
}
- options:允许用户传入自定义配置,比如背景色、文字颜色和高亮颜色等。
- matches:存储所有匹配的文本节点。
- currentIndex:当前高亮的匹配项索引。
2. 创建搜索框
接下来,我们创建一个固定在页面上的搜索框,并为其添加输入框、搜索按钮和关闭按钮。 代码就不展示了,很简单
效果如下:
3. 高亮匹配文本
当用户输入搜索内容时,我们将遍历页面上的所有文本节点,使用正则表达式匹配符合条件的文本,并通过 span 标签包裹它们来实现高亮显示。
searchText(query) {
this.clearHighlights(); // 清除之前的高亮
if (!query.trim()) {
return;
}
const regex = new RegExp(query, 'gi');
const textNodes = this.getTextNodesUnder(document.body);
textNodes.forEach((node) => {
const matches = node.textContent.match(regex);
if (matches) {
this.highlightText(node, matches, query);
}
});
}
highlightText(node, matches, query) {
const regex = new RegExp(`(${query})`, 'gi');
const parts = node.textContent.split(regex);
const parentNode = node.parentNode;
const fragment = document.createDocumentFragment();
parts.forEach((part) => {
const span = document.createElement('span');
if (part.toLowerCase() === query.toLowerCase()) {
span.className = 'highlighted';
span.style.backgroundColor = this.options.highlightColor;
span.style.fontWeight = 'bold';
span.textContent = part;
fragment.appendChild(span);
this.matches.push(span);
} else {
fragment.appendChild(document.createTextNode(part));
}
});
parentNode.replaceChild(fragment, node);
}
- getTextNodesUnder:获取页面中所有的文本节点。
- highlightText:使用
span标签包裹匹配的文本,并为其添加背景色。
4. 跳转到下一个/上一个匹配项
通过上下按钮,用户可以在所有匹配项中进行跳转。通过维护一个 currentIndex 来记录当前高亮项的位置。
highlightNext() {
if (this.matches.length === 0) return;
this.currentIndex = (this.currentIndex + 1) % this.matches.length;
this.updateHighlightColors();
this.scrollToMatch(this.matches[this.currentIndex]);
}
highlightPrevious() {
if (this.matches.length === 0) return;
this.currentIndex = (this.currentIndex - 1 + this.matches.length) % this.matches.length;
this.updateHighlightColors();
this.scrollToMatch(this.matches[this.currentIndex]);
}
updateHighlightColors() {
this.matches.forEach((match, index) => {
if (index === this.currentIndex) {
match.style.backgroundColor = this.options.currentHighlightColor;
} else {
match.style.backgroundColor = this.options.highlightColor;
}
});
}
scrollToMatch(match) {
match.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
- highlightNext 和 highlightPrevious:分别用于切换到下一个和上一个匹配项。
- updateHighlightColors:更新所有匹配项的高亮颜色,确保当前项为橙色,其它项为黄色。
5. 拖拽功能
为了让用户更加灵活地操作搜索框,为搜索框添加了拖拽功能。
makeDraggable(element) {
let isDragging = false;
let offsetX, offsetY;
element.addEventListener('mousedown', (e) => {
isDragging = true;
offsetX = e.clientX - element.getBoundingClientRect().left;
offsetY = e.clientY - element.getBoundingClientRect().top;
});
window.addEventListener('mousemove', (e) => {
if (isDragging) {
element.style.top = `${e.clientY - offsetY}px`;
element.style.left = `${e.clientX - offsetX}px`;
}
});
window.addEventListener('mouseup', () => {
isDragging = false;
});
}
6. 监听 Ctrl+F 快捷键
listenForSearch() {
window.addEventListener('keydown', (event) => {
if ((event.ctrlKey || event.metaKey) && event.key === 'f') {
event.preventDefault();
this.createSearchBox();
}
});
}
最终效果
完整代码
github: ctrl-f