Firefox 关键词高亮插件的简单实现

目录

[1、配置 manifest.json 文件](#1、配置 manifest.json 文件)

2、编写侧边栏结构

3、查找关键词并高亮的方法

[3-1) 如果直接使用 innerHTML 进行替换](#3-1) 如果直接使用 innerHTML 进行替换)

4、清除关键词高亮

5、页面脚本代码

6、参考


1、配置 manifest.json 文件

javascript 复制代码
{
    "manifest_version": 2,
    "name": "key_word_plugin",
    "version": "1.0",
  
    "description": "find_key_word",
    // 添加权限
    "permissions":[
        "*://*/*",
        "activeTab"
    ],
    "icons": {
      "48": "icons/flower.jpg"
    },
  
    "content_scripts": [
        {
            "matches": ["*://*/*"],
            "js": ["index.js"],
            "run_at":"document_idle"
        }
    ],
    // 侧边栏
    "sidebar_action": {
        "default_title": "My tool",
        "default_panel": "./sidebar/sidebar.html",
        "default_icon": "./sidebar/sidebar_icon.png"
    },
    // 背景脚本
    "background": {
        "scripts": ["bg.js"],
        "persistent": false,
        "type": "module"
    }
}

2、编写侧边栏结构

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        /* 略 */
    </style>
    <link rel="stylesheet" href="./top_area.css">
</head>
<body>
    <div class="container">
        <!-- 关键词查找 -->
        <div class="top-area">
            <section class="inp-area">
                <input class="inp" type="text" maxlength="10">
                <button class="find-btn">查找</button>
            </section>
            <section class="result-area">
                <p>共找到</p>
                <p class="count">
                    <!-- 将查找到的结果条目数量写入此处 -->
                </p>
                <p>处;</p>
            </section>
            <section class="btn-area">
                <input type="number" step="1" min="1" class="goto-keyword-inp usable">
                <button class="usable goto-btn">跳转</button>
            </section>
            <section class="btn-area">
                <button class="usable last-btn">上一个</button>
                <button class="usable next-btn">下一个</button>
                <button class="clear">清除所有标记</button>
            </section>
        </div>
    </div>
</body>
<script src="keyword.js"></script>
</html>

效果图

3、查找关键词并高亮的方法

javascript 复制代码
    // 获取当前激活的标签页面 使用 tabs 需要权限 activeTab,在 manifest 中配置
    browser.tabs.query({active: true, currentWindow: true}).then((logTabs,onError)=>{
        // 然后往当前页面中注入内容脚本,document将是当前页面的 document
        browser.tabs.executeScript({
            code:`
(function action(keyword, nodes){
    Array.from(nodes).forEach(node =>{
        let {nodeType, data : content} = node;
        if(nodeType === 3 && content.includes(keyword)){
            let parentNode = node.parentNode;
			let split_arr = node.data.trim().replaceAll(keyword,'-' + keyword + '-').split('-').filter(e => e);
            for(let item of split_arr){
                if(item === keyword){
                    let strong = document.createElement('strong')
                    strong.innerText = keyword;
                    strong.classList.add('${KEYWORD_CLASS_NAME}')
                    strong.style = '${__style}'
                    parentNode.insertBefore(strong, node)
                }else{
                    let text = document.createTextNode(item);
                    parentNode.insertBefore(text, node)
                }
            }
            parentNode.removeChild(node)
        }else if(nodeType === 1 && node.textContent.includes(keyword)){
            action(keyword, node.childNodes)
        }
    })
})('${keyword}', Array.from(document.body.childNodes).filter((e)=>{
    return (
        e.textContent.includes('${keyword}') &&
        e.tagName !== 'SCRIPT'
    )
}))
document.querySelectorAll('.__keyword_word__').length
            `
        }).then((onExecuted, onError)=>{
// onExecuted[0] 的内容就是document.querySelectorAll('.__keyword_word__').length的结果
            total = onExecuted[0]
        })
    })

点击查找关键词后,页面脚本向当前的页面注入一段JavaScript代码。该代码包含一个立即执行的函数 和 一个关键词数量的获取。

该立即执行的函数 action,接收一个 要匹配的关键词 keyword 和 当前搜索节点数组 nodes 作为参数。

遍历每一个节点,取出节点的类型-->nodeType 和节点的文本内容 -->content。

如果是纯文本节点,则该节点的 nodeType 为3,如果是元素节点,则为 1。

如果有纯文本节点,并且该纯文本节点中的内容包含了关键词,那么构造出一个数组,使用该数组来区分非关键词内容和关键词内容,以及他们之间的位置关系。

javascript 复制代码
let split_arr = content.trim()
    .replaceAll(keyword, '-' + keyword + '-')
    .split('-')
    .filter(e => e);

如关键词为 ++我们++,纯文本节点的内容为:

我们的征途是星辰大海,请和我们一起,永远相信美好的事情即将发生

那么构造的数组为:

[ " ++我们++ ", " 的征途是星辰大海,请和*", "** ++我们++ ", " 一起,永远相信美好的事情即将发生**"**]*

javascript 复制代码
    if(nodeType === 3 && content.includes(keyword)){
            let parentNode = node.parentNode;
			let split_arr = content.trim()
                .replaceAll(keyword,'-' + keyword + '-')
                .split('-        ')
                .filter(e => e);
            for(let item of split_arr){
                if(item === keyword){
                    let strong = document.createElement('strong')
                    strong.innerText = keyword;
                    strong.classList.add('${KEYWORD_CLASS_NAME}')
                    strong.style = '${__style}'
                    parentNode.insertBefore(strong, node)
                }else{
                    let text = document.createTextNode(item);
                    parentNode.insertBefore(text, node)
                }
            }
            parentNode.removeChild(node)
        }

遍历构造的数组中的内容,如果当前值等于关键词,那么构造一个强调标签 Strong 将关键词作为 innerText,并添加指定的样式和样式类名,然后加入到当前所遍历的节点之前;如果该当前值与关键词不相等,则直接构造一个文本节点,将其添加到当前所遍历的节点之前......

当遍历完构造的数组后,将当前遍历的节点从其父节点中删除。这样就将纯文本节点中的内容全部高亮处理了。

没有包含关键词的纯文本节点直接跳过。

如果该节点不是纯文本结点,那么判断其 textContent 中是否包含关键词,如果是,那么让其所有子节点再参与 action 处理。否则就不用继续递归。

3-1) 如果直接使用 innerHTML 进行替换

如果标签中的属性出现了关键词,则会出现标签结构混乱的问题:

原代码:

html 复制代码
<body>
    <div class="my_name">
        <img src="https://tse1-mm.cn.bing.net/th/id/OIP-C.duz6S7Fvygrqd6Yj_DcXAQHaF7?rs=1&pid=ImgDetMain" alt="我的图片">
        <p>我的图片</p>
        <div>
            你的图片
            <p>我们的图片</p>
            <span>都是</span>
            图片
        </div>
    </div>
    <script>
        document.body.innerHTML = document.body.innerHTML.replaceAll('图片','<strong style="color:red">图片</strong>')
    </script>
</body>

4、清除关键词高亮

javascript 复制代码
browser.tabs.query({active: true, currentWindow: true}).then(()=>{
        browser.tabs.executeScript({
            code:`
(function action(keyword){
    document.querySelectorAll('.${KEYWORD_CLASS_NAME}').forEach(e =>{
        let parent = e.parentNode;
        let textNode = document.createTextNode(keyword);
        parent.replaceChild(textNode, e)
    })
})('${keyword}')
            `
        })
    })

获取到所有 strong 强调标签(根据自定义的 class 名称),然后进行遍历,获取到每一个strong 的父元素。使用 createTextNode 创建一个纯文本节点,其内容就是关键词。然后将该文本节点替换掉 strong 标签即可。

5、页面脚本代码

javascript 复制代码
// 简单封装document.querySelector
const getFirstEle = sign => document.querySelector(sign);

// 关键词
var KEYWORD = '';

// 总共找到多少处
var total = 0;  
const count_ele = getFirstEle('.count')
count_ele.innerText = '____'

const KEYWORD_CLASS_NAME = '__keyword_word__'
const __style = `color: #b60404; background-color: #f9f906; text-decoration: underline; text-decoration-style: double;`
var INDEX = null;               // 当前记录的关键词索引,用于跳转 [1 ~ total]

const find_btn = getFirstEle('.find-btn');
const clear_btn = getFirstEle('.clear');
const last_btn = getFirstEle('.last-btn');
const next_btn = getFirstEle('.next-btn');
const goto_keyword_inp = getFirstEle('.goto-keyword-inp')
const goto_btn = getFirstEle('.goto-btn')

// 控制关键词跳转是否可用
const usables = document.querySelectorAll('.usable');
const set_usable = (res)=>{ usables.forEach(e => { e.disabled = !res; }) }

// 默认不可用
set_usable(false);

// 点击查找关键词
find_btn.addEventListener('click', (e)=>{
    // 获取用户的输入
    let keyword = document.querySelector('.inp').value.trim()
    if(!keyword) return;

    // 获取上次的关键词
    let last_keyword = sessionStorage.getItem('_keyword_');

    // 如果上次查找的关键词存在并且与当前的关键词相等
    if(last_keyword && last_keyword === keyword){ return; }
    // 如果上次的关键词与当前的关键词不相等,那么页面的高亮没有被清理
    // 因为上次的关键词session中没有被清除。先清理页面残留
    else if(last_keyword && last_keyword !== keyword){
        clear_action(last_keyword, false, false, false)
    }

    // 更新关键词
    sessionStorage.setItem('_keyword_', keyword)
    KEYWORD = keyword;
    
    // 获取当前激活的标签页面 使用 tabs 需要权限 activeTab,在 manifest 中配置
    browser.tabs.query({active: true, currentWindow: true}).then((logTabs,onError)=>{
        // 然后往当前页面中注入内容脚本,document将是当前页面的 document
        browser.tabs.executeScript({
            code:`
(function action(keyword, nodes){
    Array.from(nodes).forEach(node =>{
        let {nodeType, textContent : content} = node;
        if(nodeType === 3 && content.includes(keyword)){
            let parentNode = node.parentNode;
			let split_arr = content.trim().replaceAll(keyword,'-' + keyword + '-').split('-').filter(e => e);
            for(let item of split_arr){
                if(item === keyword){
                    let strong = document.createElement('strong')
                    strong.innerText = keyword;
                    strong.classList.add('${KEYWORD_CLASS_NAME}')
                    strong.style = '${__style}'
                    parentNode.insertBefore(strong, node)
                }else{
                    let text = document.createTextNode(item);
                    parentNode.insertBefore(text, node)
                }
            }
            parentNode.removeChild(node)
        }else if(nodeType === 1 && content.includes(keyword)){
            action(keyword, node.childNodes)
        }
    })
})('${keyword}', Array.from(document.body.childNodes).filter((e)=>{
    return (
        e.textContent.includes('${keyword}') &&
        e.tagName !== 'SCRIPT'
    )
}))
document.querySelectorAll('.__keyword_word__').length
            `
        }).then((onExecuted, onError)=>{
            total = onExecuted[0]
            count_ele.innerText = total;
            // 开启跳转功能
            if(total > 0) set_usable(true);
        })
    })
})

// 点击清除按钮 回归页面原始的状态
clear_btn.addEventListener('click', ()=>{
    let keyword = sessionStorage.getItem('_keyword_');
    clear_action(keyword)
})

// 清除关键词标记
const clear_action = (keyword, clear_inp=true, clear_keyword_session=true, clear_count=true)=>{
    browser.tabs.query({active: true, currentWindow: true}).then(()=>{
        browser.tabs.executeScript({
            code:`
(function action(keyword){
    document.querySelectorAll('.${KEYWORD_CLASS_NAME}').forEach(e =>{
        let parent = e.parentNode;
        let textNode = document.createTextNode(keyword);
        parent.replaceChild(textNode, e)
    })
})('${keyword}')
            `
        })
    })
    if(clear_inp) document.querySelector('.inp').value = '';
    if(clear_keyword_session) sessionStorage.setItem('_keyword_', '');
    if(clear_count) count_ele.innerText = '_____';
    set_usable(false)
    KEYWORD = ''
    goto_keyword_inp.value = ''
}

// 跳转到上一个关键词位置
last_btn.addEventListener('click', ()=>{
    if(!INDEX) INDEX = 1;
    else if(INDEX <= 1 ) INDEX = total;
    else if(INDEX >= total) INDEX = total - 1;
    else INDEX --;
    goto_keyword_site(INDEX - 1, KEYWORD);
})

// 跳转到下一个关键词位置
next_btn.addEventListener('click', ()=>{
    if(!INDEX) INDEX = 1;
    else if(INDEX <= 1) INDEX = 2;
    else if(INDEX >= total) INDEX = 1;
    else INDEX ++;
    goto_keyword_site(INDEX - 1, KEYWORD);
})

// 跳转到指定的位置
goto_btn.addEventListener('click', ()=>{
    let index = parseInt(goto_keyword_inp.value)
    if(!index) return;
    if(index > total) index = total;
    else if(index < 1) index = 1;
    goto_keyword_site(index - 1)
    INDEX = index;
})


// 跳转到具体的关键词位置
const goto_keyword_site = (index) =>{
    goto_keyword_inp.value = index + 1;
    browser.tabs.query({active: true, currentWindow: true}).then((logTabs,onError)=>{
        // 然后往当前页面中注入内容脚本,document将是当前页面的 document
        browser.tabs.executeScript({
            code:`
document.querySelectorAll(".${KEYWORD_CLASS_NAME}")[${index}].scrollIntoView({
        behavior:'smooth'
})            
            `
        })
    })
}

6、参考

1\]: [扩展是什么? - Mozilla \| MDN](https://developer.mozilla.org/zh-CN/docs/Mozilla/Add-ons/WebExtensions/What_are_WebExtensions "扩展是什么? - Mozilla | MDN") \[2\]: [Firefox插件(拓展)开发_火狐浏览器插件开发-CSDN博客](https://blog.csdn.net/hao_13/article/details/130979265 "Firefox插件(拓展)开发_火狐浏览器插件开发-CSDN博客")

相关推荐
LYFlied几秒前
Vue Vapor模式与AI时代前端发展的思考:虚拟DOM与框架的未来
前端·vue.js·人工智能·前端框架
江公望2 分钟前
VUE3 动态Prop 10分钟讲清楚
前端·javascript·vue.js
不会写DN3 分钟前
JavaScript call、apply、bind 方法解析
开发语言·前端·javascript·node.js
如果你好13 分钟前
理解 Proxy 原理及如何拦截 Map、Set 等集合方法调用实现自定义拦截和日志——含示例代码解析
javascript
AAA简单玩转程序设计15 分钟前
Java Map遍历的“优雅”合集
java·前端
timeweaver16 分钟前
React Server Components 再曝高危漏洞:拒绝服务与源码泄露接踵而至
前端·安全
狗哥哥21 分钟前
企业级 Vue 3 项目图标系统重构实践:从多源混乱到单一数据源
前端·vue.js·架构
czhc114007566322 分钟前
c# winform1212
java·javascript·c#
一锤捌拾23 分钟前
漫谈 JS 解析与作用域锁定
javascript
GISer_Jing24 分钟前
AI赋能前端营销领域全解析:业务、技术、应用场景等
前端·人工智能