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

前言

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

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

文本选中样式设置

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

::selection {
    
    
    background: #ff9632;
}

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

通过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()
}

在这里插入图片描述

清除

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

实现选中

全部选中

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);
};
  • 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>

如果起始节点类型是 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>

在这里插入图片描述

跨段文字特定位置选中

<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>

在这里插入图片描述

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>

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

<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>

在这里插入图片描述

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

局限

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

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_41897680/article/details/126672714