• 实现一个简单的 ctrl+ f 搜索


    前言

    浏览器可以通过ctrl + f 来实现,这个功能真的很不错,但是如何实现类似的功能呢?想了很久,感觉可以基于文本选中来实现

    复制时的效果是这样的
    在这里插入图片描述
    搜索时的效果
    在这里插入图片描述
    是不是除了颜色不一样,其他都一样呢

    文本选中样式设置

    其实文本选中的样式是可以被自定义的,可以通过CSS3的伪类选择器::selection来设置文本被选中时的状态。比如:

    ::selection {
        background: #ff9632;
    }
    
    • 1
    • 2
    • 3

    在这里插入图片描述
    这样是不是就根搜索时的样式一样了

    通过js来实现文本选中

    参考:

    https://developer.mozilla.org/zh-CN/docs/Web/API/Range

    javascript里文字选中/选中文字

    获取、清除选中的文字

    获取

    //获取选中的文字
    document.getElementById("get").onclick = function () {
        var txt = window.getSelection ? window.getSelection() : document.selection.createRange().text;
        alert(txt);//alert默认调用了toString()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述

    清除

     //清除选中的文字
     document.getElementById("set").onclick = function () {
         window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty();
     }
    
    • 1
    • 2
    • 3
    • 4

    实现选中

    全部选中

    const selectAll = () => {
        // 创建range对象
        const range = document.createRange();
        // 获取要选中的文本
        const node = document.getElementsByClassName('container')[0];
        range.selectNodeContents(node);
    
        const selection = window.getSelection();
        selection?.removeAllRanges();
        selection?.addRange(range);
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • Range.selectNodeContents() 方法用于设置 Range,使其包含一个 Node 的内容。
    • Window.getSelection 返回一个 Selection 对象,表示用户选择的文本范围或光标的当前位置。
    • Selection.removeAllRanges() 方法会从当前 selection 对象中移除所有的 range 对象,取消所有的选择只 留下anchorNodefocusNode属性并将其设置为 null
    • addRange:向选区添加一个区域

    在这里插入图片描述

    设置开始截止位置选中

    <template>
      <el-button type="primary" @click="selectAll">全部选中</el-button>
      <div class="container">
         <p>这是一段默认有开始截止位置的文本</p>
      </div>
    </template>
    
    <script setup lang="ts">
    const selectAll = () => {
        // 创建range对象
        const range = document.createRange();
        // 获取开始节点
        const startNode = document.getElementsByTagName('p').item(0)?.firstChild;
        if (startNode) {
            range.setStart(startNode, 2);
            range.setEnd(startNode, 5);
    
            const selection = window.getSelection();
            selection?.removeAllRanges();
            selection?.addRange(range);
        }
    };
    
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    如果起始节点类型是 TextComment, or CDATASection之一, 那么startOffset指的是从起始节点算起字符的偏移量。

    在这里插入图片描述
    跨段选中

    <template>
      <el-button type="primary" @click="selectAll">跨行选中</el-button>
      <div class="container">
          <span>太阳当空照,<br></span>
          <span>花儿对我笑。<br></span>
          <span>小鸟说:“早,早,早,<br></span>
          <span>你为什么背上小书包?”<br></span>
          <span>我去上学校,<br></span>
          <span>天天不迟到。<br></span>
          <span>爱学习,爱劳动,<br></span>
          <span>长大要为祖国立功劳。<br></span>
      </div>
    </template>
    
    <script setup lang="ts">
    const selectAll = () => {
        // 创建range对象
        const range = document.createRange();
        // 获取开始节点
        const startNode = document.getElementsByClassName('container')[0];
    
        // childNode会将html换行加进去
        for (var i = 0; i < startNode.childNodes.length; i++) {
            console.log(`节点:${i + 1}`, startNode.childNodes[i], startNode.childNodes[i].nodeName);
            if (startNode.childNodes[i].nodeName == '#text' && !/\s/.test(startNode.childNodes.nodeValue)) {
                startNode.removeChild(startNode.childNodes[i]);
            }
        }
    
        var startOffset = 1;
        range.setStart(startNode, startOffset);
        var endOffset = startNode.childNodes.length - 2;
        range.setEnd(startNode, endOffset);
    
        const selection = window.getSelection();
        selection?.removeAllRanges();
        selection?.addRange(range);
    };
    
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    在这里插入图片描述

    跨段文字特定位置选中

    <template>
      <el-button type="primary" @click="selectAll">跨行选中</el-button>
      <div class="container">
          <span>太阳当空照,<br></span>
          <span>花儿对我笑。<br></span>
          <span>小鸟说:“早,早,早,<br></span>
          <span>你为什么背上小书包?”<br></span>
          <span>我去上学校,<br></span>
          <span>天天不迟到。<br></span>
          <span>爱学习,爱劳动,<br></span>
          <span>长大要为祖国立功劳。<br></span>
      </div>
    </template>
    
    <script setup lang="ts">
    const selectAll = () => {
        // 创建range对象
        const range = document.createRange();
        // 获取开始节点
        const startNode = document.getElementsByClassName('container')[0];
    
        // childNode会将html换行加进去
        for (var i = 0; i < startNode.childNodes.length; i++) {
            console.log(`节点:${i + 1}`, startNode.childNodes[i], startNode.childNodes[i].nodeName);
            if (startNode.childNodes[i].nodeName == '#text' && !/\s/.test(startNode.childNodes.nodeValue)) {
                startNode.removeChild(startNode.childNodes[i]);
            }
        }
    
        range.setStart(startNode.childNodes[5].firstChild, 2);
        range.setEnd(startNode.childNodes[7].firstChild, 8);
    
        const selection = window.getSelection();
        selection?.removeAllRanges();
        selection?.addRange(range);
    };
    
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    在这里插入图片描述

    ctrl + f 简单demo

    <template>
      <div>
        <el-input v-model="inputValue" placeholder="请输入" />
        <el-button type="primary" @click="search">搜索</el-button>
        <el-button type="primary" @click="previous">上一个</el-button>
        <el-button type="primary" @click="next">下一个</el-button>
        <div class="container" id="container">
          <p> 床前明月光,疑是地上霜。</p>
          <p>举头望明月,低头思故乡。</p>
        </div>
      </div>
    </template>
    
    <script setup lang="ts">
    import { ElMessage } from 'element-plus';
    import { ref } from 'vue';
    
    const inputValue = ref('');
    
    const findNode = ref();
    
    const index = ref(-1);
    
    const search = () => {
        // 获取容器
        const container = document.getElementById('container');
        // 获取所有文本
        const allText = container?.innerText;
        if (allText && allText.includes(inputValue.value)) {
            // 获取所有节点
            const containerAllNode = container.childNodes;
            console.log(containerAllNode);
            // 用于保存找到的节点
            findNode.value = [];
            for (let i = 0; i < containerAllNode.length; i++) {
                // 遍历查询节点
                if (containerAllNode[i].textContent?.includes(inputValue.value)) {
                    findNode.value.push(containerAllNode[i]);
                }
            }
            // 默认选中第一个
            if (findNode.value && findNode.value.length > 0) {
                index.value = 0;
                setSelect(0);
            }
        } else {
            ElMessage.warning('未匹配到');
        }
    };
    
    const previous = () => {
        if (index.value > 0) {
            index.value -= 1;
        } else {
            index.value = findNode.value.length - 1;
        }
        setSelect(index.value);
    };
    
    const next = () => {
        if (index.value < findNode.value.length - 1) {
            index.value += 1;
        } else {
            index.value = 0;
        }
        setSelect(index.value);
    };
    
    const setSelect = (index) => {
        const range = document.createRange();
        range.selectNodeContents(findNode.value[index]);
        const selection = window.getSelection();
        selection?.removeAllRanges();
        selection?.addRange(range);
    };
    
    </script>
    <style lang="scss" scoped>
    .container {
        width: 400px;
        height: 200px;
        margin-left: 100px;
        border: 1px solid red;
    
        ::selection {
            background: #ff9632;
        }
    }
    </style>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89

    在这里插入图片描述
    改进版

    <template>
      <div>
        <el-input v-model="inputValue" placeholder="请输入" />
        <el-button type="primary" @click="search">搜索</el-button>
        <el-button type="primary" @click="previous">上一个</el-button>
        <el-button type="primary" @click="next">下一个</el-button>
        <div class="container" id="container">
          <p> 床前明月光,疑是地上霜。</p>
          <p>举头望明月,低头思故乡。</p>
        </div>
      </div>
    </template>
    
    <script setup lang="ts">
    import { ElMessage } from 'element-plus';
    import { ref } from 'vue';
    
    const inputValue = ref('');
    
    const findNode = ref();
    
    const index = ref(-1);
    
    const search = () => {
        // 获取容器
        const container = document.getElementById('container');
        // 获取所有文本
        const allText = container?.innerText;
        if (allText && allText.includes(inputValue.value)) {
            // 获取所有节点
            const containerAllNode = container.childNodes;
            console.log(containerAllNode);
            // 用于保存找到的节点
            findNode.value = [];
            for (let i = 0; i < containerAllNode.length; i++) {
                // 遍历查询节点
                if (containerAllNode[i].textContent?.includes(inputValue.value)) {
                    findNode.value.push(containerAllNode[i]);
                }
            }
            // 默认选中第一个
            if (findNode.value && findNode.value.length > 0) {
                index.value = 0;
                for (let j = 0; j < findNode.value.length; j++) {
                    findNode.value[j].classList.add('abc');
                }
                setSelect(0);
            }
        } else {
            ElMessage.warning('未匹配到');
        }
    };
    
    const previous = () => {
        if (index.value > 0) {
            index.value -= 1;
        } else {
            index.value = findNode.value.length - 1;
        }
        setSelect(index.value);
    };
    
    const next = () => {
        if (index.value < findNode.value.length - 1) {
            index.value += 1;
        } else {
            index.value = 0;
        }
        setSelect(index.value);
    };
    
    const setSelect = (index) => {
        const range = document.createRange();
        console.log(findNode.value[index]);
        range.selectNodeContents(findNode.value[index]);
        const selection = window.getSelection();
        selection?.removeAllRanges();
        selection?.addRange(range);
    };
    
    </script>
    <style lang="scss" scoped>
    .container {
        width: 400px;
        height: 200px;
        margin-left: 100px;
        border: 1px solid red;
    
        ::selection {
            background: #ff9632;
        }
    }
    
    .abc {
        background-color: #ff0;
    }
    </style>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97

    在这里插入图片描述

    如果想实现只选中输入的文字,可以看 设置开始截止位置选中 这块内容,找到文字对应的起始位置和结束位置

    局限

    • ctrl + f 搜索不是基于文本选中实现的,从下面的图片可以看出来
    • 如果真要实现类似浏览器的搜索功能是很复杂的,需要判断节点的类型,文字在哪一个节点里面;但是如果节点类型固定的话,还是可以简单试试

    在这里插入图片描述

    补充

    最近突然有了些想法,浏览器的搜索功能应该不是通过正则替换来实现的。但是如果只是具体的需求,完全可以通过正则来实现

    比如:a是关键词,b是加有样式的关键词。当搜索时将a都替换成b;当清空时再由b替换后a;如果是change事件的话,每次改变时先由b提换回a,再更新a和b,更新后再将a都替换成b。

  • 相关阅读:
    Java对象的相等判定问题与equals方法解析
    高通camera之对camx架构的浅析
    MySQL数据库管理
    react事件系统(新老版本)
    Splunk UBA audit log 发送到 Splunk ES
    Postman汉化教程
    【校招VIP】前端ES6相关之Symbol
    干货 | 师兄手把手教你如何踏上科研道路
    最快的开源UDP传输工具:Kcptun
    Bootloader程序刷写
  • 原文地址:https://blog.csdn.net/weixin_41897680/article/details/126672714