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

目录

1、配置 manifest.json 文件

2、编写侧边栏结构

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

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

4、清除关键词高亮

5、页面脚本代码

6、参考


1、配置 manifest.json 文件

{
    "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、编写侧边栏结构

<!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、查找关键词并高亮的方法

    // 获取当前激活的标签页面 使用 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。

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

let split_arr = content.trim()
    .replaceAll(keyword, '-' + keyword + '-')
    .split('-')
    .filter(e => e);

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

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

        那么构造的数组为:

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

    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 进行替换

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

原代码: 

<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、清除关键词高亮

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、页面脚本代码

// 简单封装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

[2]: Firefox插件(拓展)开发_火狐浏览器插件开发-****博客 

上一篇:Ubuntu 20.04 LTS 在3588安卓主板上测试yolov8-1.0版本的yolov8n-seg模型


下一篇:MySQL-视图:视图概述、创建、查看、更新、修改、删除-4. 查看视图