当前位置: 首页 > news >正文

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

在这里插入图片描述

相关文章:

  • 脱壳工具:BlackDex的使用详解
  • 【数据挖掘】2022年京东算法工程师笔试题(23届)
  • Unet医学细胞分割实战
  • 2022年9月4日:面向初学者的 web 开发--JavaScript 数组和循环(没有完全搞懂)
  • Vue模板语法2
  • CUDA编程学习(3)
  • 【教程】visual studio debug 技巧总结
  • JS-获取DOM元素的五种方法
  • Linux下编译main.c文件,命令中的gcc -o -c是什么意思
  • 【12. 文件系统管理】
  • 【C++】模板基础 + STL
  • Solidity中的calldata,storage,memory
  • 【SQL刷题】Day13----SQL分组数据专项练习
  • JVM阶段(6)-方法区回收
  • 《工程伦理与学术道德》之《工程中的风险、安全与责任》
  • 【译】理解JavaScript:new 关键字
  • bootstrap创建登录注册页面
  • hadoop入门学习教程--DKHadoop完整安装步骤
  • JDK9: 集成 Jshell 和 Maven 项目.
  • JS数组方法汇总
  • SegmentFault 2015 Top Rank
  • unity如何实现一个固定宽度的orthagraphic相机
  • Vue2 SSR 的优化之旅
  • vue-cli在webpack的配置文件探究
  • vue中实现单选
  • 对话 CTO〡听神策数据 CTO 曹犟描绘数据分析行业的无限可能
  • 对话:中国为什么有前途/ 写给中国的经济学
  • 基于MaxCompute打造轻盈的人人车移动端数据平台
  • 利用DataURL技术在网页上显示图片
  • 人脸识别最新开发经验demo
  • 入门到放弃node系列之Hello Word篇
  • 使用 Xcode 的 Target 区分开发和生产环境
  • 微信小程序:实现悬浮返回和分享按钮
  • 优化 Vue 项目编译文件大小
  • 主流的CSS水平和垂直居中技术大全
  • LevelDB 入门 —— 全面了解 LevelDB 的功能特性
  • 进程与线程(三)——进程/线程间通信
  • 如何用纯 CSS 创作一个菱形 loader 动画
  • ​卜东波研究员:高观点下的少儿计算思维
  • #Linux(帮助手册)
  • #我与Java虚拟机的故事#连载06:收获颇多的经典之作
  • #我与Java虚拟机的故事#连载07:我放弃了对JVM的进一步学习
  • (06)Hive——正则表达式
  • (4)(4.6) Triducer
  • (5)STL算法之复制
  • (Redis使用系列) SpringBoot中Redis的RedisConfig 二
  • (vue)el-checkbox 实现展示区分 label 和 value(展示值与选中获取值需不同)
  • (翻译)terry crowley: 写给程序员
  • (附源码)springboot优课在线教学系统 毕业设计 081251
  • (十五)使用Nexus创建Maven私服
  • (未解决)jmeter报错之“请在微信客户端打开链接”
  • (一)eclipse Dynamic web project 工程目录以及文件路径问题
  • (已解决)什么是vue导航守卫
  • *_zh_CN.properties 国际化资源文件 struts 防乱码等
  • .NET 4.0中使用内存映射文件实现进程通讯