【element-tiptap】如何实现查找替换功能?

这是一个稍微复杂的功能了,因为 element-tiptap 中没有查找替换功能,需要从零开始开发。但是,在万能的github上有一个开源的库,我们可以借用一下 tiptap-search-and-replace
不过这个库是没有UI的,只有一个扩展的方法。但是这个关键的方法只要有了,剩下的就简单多了
searchAndReplace.ts
我的项目的目录名文件名都已经从首字母大写改成了全部小写的写法,不影响大家阅读哦
我们首先把这个源码放到 scr/extensions 目录下面
然后UI我们可以参考在线WPS的UI
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
先分析一下需求。我发现开发之前的需求分析真的很重要,可能这就是所谓的“慢即是快”,看似有些繁琐并且浪费时间,但是可以在开发的时候少走很多弯路,可以让自己开发的思路更加的清晰,效率也会更高。

  • 新建一个扩展
  • 需要一个下拉框组件,点击按钮出现查找和替换两个菜单项;点击查找、替换菜单项的时候,都需要弹出弹出框组件,并且把状态传递给弹出框组件
  • 需要一个弹出框组件,有两种状态,表示当前是查找还是替换;弹出框组件分为两个tab栏;查找可以找上一个、下一个;替换可以找上一个、下一个、全部替换、替换当前
  • 查找可以用快捷键⌘F唤醒,替换可以用快捷键⌘H唤醒
    更多选项里面的功能就先不做了

1、新建一个扩展

① 新建一个扩展,src/extensions/search-replace.ts,然后把上面的文件的源码放进去,当然,我们后续还需要对它进行稍微的改造,这里先不管
② 在 src/extensions/index.ts 文件中模仿其他扩展,也导出这个扩展

export { default as SearchAndReplace } from './search-replace';

③ 在 src/components/editor.vue 文件中的扩展列表 extensions 增加一项,根据官网的提示,需要增加 configure 配置项

import {SearchAndReplace
} from '../extensions';
SearchAndReplace.configure({searchResultClass: "search-result", // class to give to found items. default 'search-result'caseSensitive: false, // no need to explaindisableRegex: false, // also no need to explain
}),

2、下拉框组件

① 创建下拉框组件 src/components/menu-commands/search-replace/search-replace.dropdown.vue
ps:我这里的文件命名自己有根据项目需要修改过,大家自行修改哈
代码说明:

  • 下拉框菜单有两个:查找、替换
  • 点击查找或者替换的时候,会将对应的标识字符串传递到回调函数中,以此标识当前的操作类型
  • 一个弹出框组件,当点击查找或替换的时候都会被激活,并且接受表示操作类型的参数
<template><el-dropdown placement="bottom" trigger="click" @command="handleCommand" popper-class="my-dropdown":popper-options="{ modifiers: [{ name: 'computeStyles', options: { adaptive: false } }] }"><div><command-button :enable-tooltip="enableTooltip" :tooltip="t('editor.extensions.searchAndReplace.tooltip')"icon="search" :button-icon="buttonIcon" /></div><template #dropdown><el-dropdown-menu class="el-tiptap-dropdown-menu"><el-dropdown-item command="search"><span>查找</span></el-dropdown-item><el-dropdown-item command="replace"><span>替换</span></el-dropdown-item></el-dropdown-menu></template></el-dropdown><search-replace-popup v-if="showPopup" :mode="popupMode" :editor="editor" @close="showPopup = false" />
</template><script lang="ts">
import { defineComponent, inject, ref } from 'vue';
import { Editor } from '@tiptap/vue-3';
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus';
import CommandButton from '../command.button.vue';
import searchReplacePopup from './search-replace.popup.vue';export default defineComponent({name: 'searchAndReplaceDropdown',components: {ElDropdown,ElDropdownMenu,ElDropdownItem,CommandButton,searchReplacePopup,},props: {editor: {type: Object as () => Editor,required: true,},buttonIcon: {default: '',type: String}},setup(props) {const t = inject('t') as (key: string) => string;const enableTooltip = inject('enableTooltip', true);const showPopup = ref(false);const popupMode = ref<'search' | 'replace'>('search');const handleCommand = (command: string) => {popupMode.value = command as 'search' | 'replace';showPopup.value = true;};return {t,enableTooltip,showPopup,popupMode,handleCommand};},
});
</script><style scoped>
.dropdown-title {font-size: 14px;font-weight: 500;margin: 5px;
}.el-tiptap-dropdown-menu__item {margin-left: 5px;
}
</style>

② 这里的图标 search 需要我们自己添加
src/icons/search.svg

<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke-width="1.5"><g id="group-0" stroke="#333333" fill="#333333"><path d="M11.2426 11.2426L14.5 14.5M13 7C13 10.3137 10.3137 13 7 13C3.68629 13 1 10.3137 1 7C1 3.68629 3.68629 1 7 1C10.3137 1 13 3.68629 13 7Z" stroke-linecap="round" stroke-linejoin="miter" fill="none" vector-effect="non-scaling-stroke"></path></g></svg>

③ 扩展文件修改,应用下拉框组件
主要是 addOptions 方法

import searchAndReplaceDropdown from "@/components/menu-commands/search-replace/search-replace.dropdown.vue";
addOptions() {return {// 保留父扩展的所有选项...this.parent?.(),button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) {return {component: searchAndReplaceDropdown,componentProps: {editor,},};},};
},

3、弹出框组件

src/components/menu-commands/search-replace/search-replace.popup.vue
① 创建弹出框组件
组件说明:

  • UI 参考WPS编辑器的效果
  • 查找、替换、上一个、下一个、替换全部 这些功能已经被我们的扩展文件添加到了 editor.commands 上,所以直接通过命令调用即可
  • 输入框中文字改变的时候,就需要执行查找
  • 点击右上角的叉号关闭弹出框的时候,要去除所有的选中状态
<template><div v-if="drawerVisible" class="search-replace-container" :style="containerStyle"><span class="search-replace-close" type="text" @click="handleClose">×</span><el-tabs v-model="activeTab"><el-tab-pane label="查找" name="search"><div class="search-replace-title">查找</div><el-input v-model="searchTerm"@input="onSearchTermChange"size="default":placeholder="t('editor.extensions.searchAndReplace.searchPlaceholder')"></el-input><div class="search-replace-actions"><el-button size="default" @click="findPrevious">上一个</el-button><el-button size="default" @click="findNext">下一个</el-button></div></el-tab-pane><el-tab-pane label="替换" name="replace"><div class="search-replace-title">查找</div><el-input v-model="searchTerm"@input="onSearchTermChange"size="default":placeholder="t('editor.extensions.searchAndReplace.searchPlaceholder')"></el-input><div class="search-replace-title">替换为</div><el-input v-model="replaceTerm"size="default":placeholder="t('editor.extensions.searchAndReplace.replacePlaceholder')"></el-input><div class="search-replace-actions"><el-button size="default" @click="findPrevious">上一个</el-button><el-button size="default" @click="findNext">下一个</el-button><el-button size="default" @click="replaceCurrent">替换</el-button><el-button size="default" @click="replaceAll">替换全部</el-button></div></el-tab-pane></el-tabs></div>
</template><script lang="ts">
import { defineComponent, ref, watch, inject, computed } from 'vue';
import { Editor } from '@tiptap/vue-3';
import { ElTabs, ElTabPane, ElInput, ElButton } from 'element-plus';export default defineComponent({name: 'searchAndReplacePopup',components: {ElTabs,ElTabPane,ElInput,ElButton,},props: {mode: {type: String as () => 'search' | 'replace',required: true,},editor: {type: Object as () => Editor,required: true,},},emits: ['close'],setup(props, { emit }) {const t = inject('t') as (key: string) => string;const drawerVisible = ref(true);const activeTab = ref(props.mode);const searchTerm = ref('');const replaceTerm = ref('');watch(() => props.mode, (newMode) => {activeTab.value = newMode;});const handleClose = () => {emit('close');searchTerm.value = '';replaceTerm.value = '';props.editor.commands.setSearchTerm('');props.editor.commands.resetIndex();};const findNext = () => {props.editor.commands.setSearchTerm(searchTerm.value);props.editor.commands.nextSearchResult();};const findPrevious = () => {props.editor.commands.setSearchTerm(searchTerm.value);props.editor.commands.previousSearchResult();};const replaceCurrent = () => {props.editor.commands.setReplaceTerm(replaceTerm.value);props.editor.commands.replace();};const replaceAll = () => {props.editor.commands.setReplaceTerm(replaceTerm.value);props.editor.commands.replaceAll();};const onSearchTermChange = () => {props.editor.commands.setSearchTerm(searchTerm.value);};// 动态计算容器宽度const containerStyle = computed(() => ({width: activeTab.value === 'search' ? '358px' : '478px',}));return {t,drawerVisible,activeTab,searchTerm,replaceTerm,handleClose,findNext,findPrevious,replaceCurrent,replaceAll,onSearchTermChange,containerStyle,};},
});
</script><style lang="scss">
@import '../../../styles/variables.scss';
.search-replace-container {position: absolute;top: 10%;right: 10%;width: 30%;background-color: white;border: 1px solid #ccc;box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);padding: 16px;z-index: 1000;border-radius: 5px;transition: width 0.3s ease; /* 添加过渡效果 */
}.search-replace-container .el-tabs__item {font-size: 16px;
}
.search-replace-container .el-tabs__nav-wrap::after {background-color: transparent;
}
.search-replace-close {cursor: pointer;font-size: 18px;color: #60646c;font-weight: 600;position: absolute;width: 24px;height: 24px;display: flex;align-items: center;justify-content: center;border-radius: 4px;top: 22px;right: 18px;z-index: 3;
}
.search-replace-close:hover {background-color: #f0f0f0;
}.search-replace-title {font-weight: 600;font-size: 14px;line-height: 22px;color: hsla(0, 0%, 5%, .9);margin-top: 8px;margin-bottom: 3px;
}.search-result {background: $lighter-primary-color;
}.search-result.search-result-current {background: $tiptap-search-result-current-color;
}.search-replace-actions {margin-top: 16px;display: flex;justify-content: flex-end;
}
</style>

② 背景颜色定义
在这里插入图片描述
如上图所示,查找的时候会查找到很多结果,所有的结果都会被添加类名 search-result,不过当前选中的结果还会被添加 search-result-current 类名。
上面的代码中,我们给这两个类添加了背景颜色,背景颜色是通过 @import '../../../styles/variables.scss'; 引入进来的,在这个文件中,还需要增加一个定义
src/styles/variables.scss

$tiptap-search-result-current-color:rgb(193, 243, 181);

这样就可以实现如下效果
在这里插入图片描述

4、替换方法改造

这里有一个小问题,就是替换功能总是会替换第一个查找结果,但是期望结果应该是替换我们选中的结果
在这里插入图片描述
此时选中的是第三个 Content,点击替换的时候,还是会替换第一个 Content
这其实是源码中的 replace 方法的问题,我们看一下这个方法的定义

const replace = (replaceTerm: string,results: Range[],{ state, dispatch }: { state: EditorState; dispatch: Dispatch },
) => {const firstResult = results[0];if (!firstResult) return;const { from, to } = results[0];if (dispatch) dispatch(state.tr.insertText(replaceTerm, from, to));
};

可以看到确实是进行的是结果列表中第一个元素的替换
这个方法的调用在 addCommands 里:

replace:() =>({ editor, state, dispatch }) => {const { replaceTerm, results } = editor.storage.searchAndReplace;console.log(editor.storage.searchAndReplace);console.log(results);replace(replaceTerm, results, { state, dispatch });return false;},

控制台输出一下 editor.storage.searchAndReplace ,我们可以发现,有一个属性可以标识当前选中的是哪一个结果
在这里插入图片描述
索引为 2,也就是第三个。
那么我们可以改造一下 replace 函数,接收一个索引的参数,来指定替换哪一个

// 替换当前搜索结果
const replace = (replaceTerm: string,results: Range[],resultIndex: number,{ state, dispatch }: { state: EditorState; dispatch: Dispatch },
) => {if (resultIndex < 0 || resultIndex >= results.length) return;const { from, to } = results[resultIndex];if (dispatch) dispatch(state.tr.insertText(replaceTerm, from, to));
};

这样就可以实现 替换当前选中的结果 了

5、快捷键

src/extensions/search-replace.ts 增加快捷键
如果当前鼠标选中的区域有文本的话,就需要获取到选中区域的文本,传递给 setSearchTerm 命令

addKeyboardShortcuts() {return {'Mod-f': () => {const { state } = this.editor;const { from, to } = state.selection;const selectedText = state.doc.textBetween(from, to);this.editor.commands.setSearchTerm(selectedText);this.editor.emit('openSearchReplacePopup', 'search');return true;},'Mod-h': () => {const { state } = this.editor;const { from, to } = state.selection;const selectedText = state.doc.textBetween(from, to);this.editor.commands.setSearchTerm(selectedText);this.editor.commands.setReplaceTerm('');this.editor.emit('openSearchReplacePopup', 'replace');return true;},};
},

src/components/menu-commands/search-replace/search-replace.dropdown.vue
我们知道,弹出框显示与否,是通过这个组件里面的 showPopup 属性控制的,快捷键按下的时候,会使用 emit 触发 openSearchReplacePopup 事件。那么在下拉框组件中,我们需要监听openSearchReplacePopup 事件,在回调函数中将showPopup 属性设置为 true

onMounted(() => {props.editor.on('openSearchReplacePopup', (mode: 'search' | 'replace') => {popupMode.value = mode;showPopup.value = true;});
});

src/components/menu-commands/search-replace/search-replace.popup.vue
在这个组件中,searchTerm 我们之前是初始化为''的,但是现在这个值,需要从编辑器的属性中获取

const searchTerm = ref(props.editor.storage.searchAndReplace.searchTerm);

另外,还有一个小细节,如图,我们选中了第三个 Content,并且按下了快捷键
在这里插入图片描述
此时会发生什么?会发现当前的查找结果是第一个 Content
在这里插入图片描述
这不是我想要的。
应该是,咱们鼠标选中的内容作为当前查找结果。
那么此时我们就需要在查找结果数组中,找到和我们鼠标选中区域的位置一样的查找结果,并且选中这个查找结果

const selection = props.editor.state.selection;
const results = props.editor.storage.searchAndReplace.results;
if(results.length > 0) {// 找到 results 中,from 和 selection.from 一样的结果,然后设置为当前结果for(let i = 0; i < results.length; i++) {if(results[i].from !== selection.from) {// 向后找props.editor.commands.nextSearchResult();}else{break;}}
}

然后你会发现,现在选中第三个 Content 按下快捷键是这种效果
在这里插入图片描述

其实不是bug了,是因为当前我们用鼠标选中了第三个 Content,并且鼠标选中的背景色跟我们设置的选中结果的背景色一样😂😂😂,就这个我以为是bug看了好久好久。。。。。。
不如改个颜色直观一些吧
src/styles/variables.scss

$tiptap-search-result-color:#FDFF00;
$tiptap-search-result-current-color:#F79632;

src/components/menu-commands/search-replace/search-replace.popup.vue

.search-result {display: inline-block;background: $tiptap-search-result-color;
}.search-result.search-result-current {background: $tiptap-search-result-current-color;
}

在这里插入图片描述
随便点击一下页面
在这里插入图片描述
长呼一口气,这个还真是有点复杂。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/883334.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Linux】线程池详解及其基本架构与单例模式实现

目录 1.关于线程池的基本理论 1.1.线程池是什么&#xff1f; 1.2.线程池的应用场景&#xff1a; 2.线程池的基本架构 2.1.线程容器 2.2.任务队列 2.3.线程函数&#xff08;HandlerTask&#xff09; 2.4.线程唤醒机制 3.添加单例模式 3.1.单例模式是什么&…

【 thinkphp8 】00006 启动 内、外置服务器

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 【 t…

Linux文件类型和根目录结构

Linux文件类型和根目录结构 1.文件类型 字符文件类型说明~普通文件类似于Windows的记事本d目录文件类似于windows文件夹c字符设备文件串行端口设备&#xff0c;顺序读写&#xff0c;键盘b块设备文件可供存储的接口设备&#xff0c;随机读写&#xff0c;硬盘p管道文件用于进程…

jmeter中请求参数:Parameters、Body Data的区别

使用jmeter发送请求&#xff0c;常常要伴随传递参数。有两种请求参数: Parameters, Body Data, 它们的使用方式有很大不同。 先看下get和post请求的区别。 get请求&#xff1a;顾名思义是从服务器获取资源。 post请求&#xff1a;顾名思义是往服务器提交要处理的数据。 直观…

【算法刷题指南】双指针

&#x1f308;个人主页&#xff1a; 南桥几晴秋 &#x1f308;C专栏&#xff1a; 南桥谈C &#x1f308;C语言专栏&#xff1a; C语言学习系列 &#x1f308;Linux学习专栏&#xff1a; 南桥谈Linux &#x1f308;数据结构学习专栏&#xff1a; 数据结构杂谈 &#x1f308;数据…

JavaSE要点 1】Java基础

目录 一、编译和运行 二、JDK,JRE和JVM 三、Java中的基本类型 1. 基本类型的大小 四、JVM内存模型 1. 内存模型 五、JVM虚拟机的组成 1. 虚拟机的5个组成部分 2. 虚拟机栈 3. 栈帧 六、值传递和引用传递 七、变量的默认值 八、String的不可变 九. 包装类和常量池&#xff08;-…

基于SSM的网上购物系统的设计与实现

技术介绍 本系统运用了JSP技术、SSM框架、B/S架构和myspl数据库 MySQL 介绍 MySQL是一种关系型的数据库管理系统&#xff0c;属于Oracle旗下的产品。MySQL的语言是非结构化的&#xff0c;使用的用户可以在数据上进行工作。这个数据库管理系统一经问世就受到了社会的广泛关注…

【WebGis开发 - Cesium】三维可视化项目教程---图层管理拓展图层顺序调整功能

目录 引言一、为什么要开发图层顺序调整功能二、开发思路整理1. 拖拽库方案选择2. cesium图层api查询 三、代码编写1. 编写拖拽组件代码2. 修改原有图层管理代码2.1 图层加载移除的调整2.2 图层顺序与拖拽列表的矛盾 3. 编写图层移动代码 四、总结 引言 本教程主要是围绕Cesium…

深入理解InnoDB底层原理:从数据结构到逻辑架构

💡 无论你是刚刚踏入编程世界的新人,还是希望进一步提升自己的资深开发者,在这里都能找到适合你的内容。我们共同探讨技术难题,一起进步,携手度过互联网行业的每一个挑战。 📣 如果你觉得我的文章对你有帮助,请不要吝啬你的点赞👍分享💕和评论哦! 让我们一起打造…

ctfshow(259->261)--反序列化漏洞--原生类与更多魔术方法

Web259 进入界面&#xff0c;回显如下&#xff1a; highlight_file(__FILE__);$vip unserialize($_GET[vip]); //vip can get flag one key $vip->getFlag();题干里还提示了网站有一个flag.php界面&#xff0c;源代码如下&#xff1a; $xff explode(,, $_SERVER[HTTP_X…

常见的音视频格式介绍

目录 aac格式介绍&#xff08;ADTS&#xff09;h264格式分析FLV和MP4格式介绍 aac格式介绍&#xff08;ADTS&#xff09; aac的格式有两种&#xff1a;ADIF不常用&#xff0c;ADTS是主流&#xff0c;所以这里主要讲解ADTS。简单来说&#xff0c;ADTS可以在任意帧解码&#xff…

校园表白墙源码修复版

此校园表白墙源码基于thinkphp&#xff0c;因为时代久远有不少bug&#xff0c;经本人修复已去除大部分bug&#xff0c;添加了美化元素。 https://pan.quark.cn/s/1f9b3564c84b https://pan.baidu.com/s/1bb9vu9VV2jJoo9-GF6W3xw?pwd7293 https://caiyun.139.com/m/i?2hoTc…

多款云存储平台存在安全漏洞,影响超2200万用户

据苏黎世联邦理工学院研究人员Jonas Hofmann和Kien Tuong Turong的发现&#xff0c;端到端加密&#xff08;E2EE&#xff09;云存储平台存在一系列安全问题&#xff0c;可能会使用户数据暴露给恶意行为者。在通过密码学分析后&#xff0c;研究人员揭示了Sync、pCloud、Icedrive…

三维管线管网建模工具MagicPipe3D V3.5.3

经纬管网建模系统MagicPipe3D&#xff0c;本地离线参数化构建地下管网三维模型&#xff08;包括管道、接头、附属设施等&#xff09;&#xff0c;输出标准3DTiles、Obj模型等格式&#xff0c;支持Cesium、Unreal、Unity、Osg等引擎加载进行三维可视化、语义查询、专题分析&…

【Android】MVP架构

MVP架构简介 MVP&#xff08;Model-View-Presenter&#xff09;是一种常见的软件架构模式&#xff0c;尤其在Android应用开发中被广泛使用。它将应用程序分为三层&#xff1a;Model、View 和 Presenter&#xff0c;以实现职责分离&#xff0c;提高代码的可维护性和可测试性。 …

基于自适应VSG控制的光伏混合储能构网型逆变系统MATLAB仿真模型

模型简介 此模型源侧部分采用光伏发电系统与混合储能系统&#xff08;蓄电池超级电容&#xff09;&#xff0c;并网逆变器采用虚拟同步发电机&#xff08;VSG&#xff09;控制&#xff0c;为系统提供惯量阻尼支撑。同时对VSG控制部分进行了改进&#xff0c;采用构造函数法对虚…

论文阅读(二十九):Multi-scale Interactive Network for Salient Object Detection

文章目录 Abstract1.Introduction2.Scale VariationProposed Method3.1Network Overview3.2Aggregate Interaction Module3.3 Self-Interaction Module3.4Consistency-Enhanced Loss 4.Experiments4.1Implementation Details4.2 Comparison with State-of-the-arts4.3Ablation …

【力扣】[Java版] 刷题笔记-21. 合并两个有序链表

题目&#xff1a; 21. 合并两个有序链表 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 解题思路 从题目和示例可以看出&#xff0c;应该是要循环遍历链表进行比较&#xff0c;然后组成新的链表。 第一种&#xff1a;递归…

存储过程(SQL)

1.存储过程 存储过程&#xff08;Stored Procedure&#xff09;是一组为了完成特定功能的SQL语句集&#xff0c;经编译后存储在数据库中&#xff0c;用户通过指定存储过程的名字并给定参数&#xff08;如果该存储过程带有参数&#xff09;来调用执行它。 2.MySQL存储过程创建…

PostgreSQL的前世今生

PostgreSQL的起源可以追溯到1977年的加州大学伯克利分校&#xff08;UC Berkeley&#xff09;的Ingres项目。该项目由著名的数据库科学家Michael Stonebraker领导&#xff0c;他是2015年图灵奖的获得者。以下是PostgreSQL起源的详细概述&#xff1a; 一、早期发展 Ingres项目…