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

Vue3+codemirror6实现公式(规则)编辑器

实现截图

在这里插入图片描述

实现/带实现功能

  • 插入标签
  • 插入公式
  • 提示补全
  • 公式验证
  • 公式计算

需要的依赖

    "@codemirror/autocomplete": "^6.18.4","@codemirror/lang-javascript": "^6.2.2","@codemirror/state": "^6.5.2","@codemirror/view": "^6.36.2","codemirror": "^6.0.1",

初始化编辑器

// index.ts
export const useCodemirror = () => {const code = ref("");const view = shallowRef<EditorView>();const editorRef = ref<InstanceType<typeof HTMLDivElement>>();const extensions = [placeholderTag, //插入tagplaceholderFn, //插入函数baseTheme, //基础样式EditorView.lineWrapping, //换行basicSetup, //基础配置javascript(), //js语言支持autocompletion({ override: [myCompletions] }), //补全提示];/*** @description 初始化编辑器*/const init = () => {if (editorRef.value) {view.value = new EditorView({parent: editorRef.value,state: EditorState.create({doc: code.value,extensions: extensions,}),});setTimeout(() => {view.value?.focus();}, 0);}};/*** @description 销毁编辑器*/const destroyed = () => {view.value?.destroy();view.value = undefined;};/*** @description 插入文本并设置光标位置*/const insertText = (text: string, type: "fn" | "tag" = "tag") => {if (view.value) {let content = type === "tag" ? `[[${text}]]` : `{{${text}}}()`;const selection = view.value.state.selection;if (!selection.main.empty) {// 如果选中文本,则替换选中文本const from = selection.main.from;const to = selection.main.to;const anchor =type === "tag" ? from + content.length : from + content.length - 1;const transaction = view.value!.state.update({changes: { from, to, insert: content }, // 在当前光标位置插入标签selection: {anchor,}, // 指定新光标位置});view.value.dispatch(transaction);} else {// 如果没有选中文本,则插入标签const pos = selection.main.head;const anchor =type === "tag" ? pos + content.length : pos + content.length - 1;const transaction = view.value.state.update({changes: { from: pos, to: pos, insert: content }, // 在当前光标位置插入标签selection: {anchor: anchor,}, // 指定新光标位置});view.value.dispatch(transaction);}setTimeout(() => {view.value?.focus();}, 0);}};return {code,view,editorRef,init,destroyed,insertText,};
};
<template><MyDialogv-model="state.visible"title="Editor":width="800"center:close-on-click-modal="false":destroy-on-close="true"@close="close"><div class="editor-container"><TreeComclass="editor-tree":data="state.paramsData"@node-click="insertTag"></TreeCom><div class="editor-content"><div class="editor-main" ref="editorRef"></div><div class="fn"><div class="fn-list"><TreeCom:default-expand-all="true":data="state.fnData"@node-click="insertFn"@mouseenter="hoverFn"></TreeCom></div><div class="fn-desc"><DescCom v-bind="state.info"></DescCom></div></div></div></div><template #footer><div><el-button @click="close">取消</el-button><el-button type="primary" @click="submit">确认</el-button></div></template></MyDialog>
</template><script lang="ts">
export default { name: "Editor" };
</script>
<script lang="ts" setup>
import { nextTick, reactive } from "vue";
import TreeCom from "./components/tree.vue";
import DescCom from "./components/desc.vue";
import { useCodemirror, functionDescription } from ".";
import { Tree } from "@/types/common";const state = reactive({visible: false,paramsData: [{label: "参数1",id: "1",},{label: "参数2",id: "2",},{label: "参数3",id: "3",},],fnData: [{label: "常用函数",id: "1",children: [{label: "SUM",desc: "求和",id: "1-1",},{label: "IF",desc: "条件判断",id: "1-2",},],},],info: {},
});const { code, view, editorRef, init, destroyed, insertText } = useCodemirror();
/*** @description 插入标签*/
const insertTag = (data: Tree) => {if (!data.children) {insertText(`${data.id}.${data.label}`);}
};
/*** @description 插入函数*/
const insertFn = (data: Tree) => {if (!data.children) {insertText(`${data.label}`, "fn");}
};
/*** @description 鼠标悬停展示函数描述*/
const hoverFn = (data: Tree) => {const info = functionDescription(data.label);if (info) {state.info = info;}
};
/*** @description 获取数据*/
const submit = () => {const data = view.value?.state.doc;console.log(data);
};
const open = () => {state.visible = true;nextTick(() => {init();});
};
const close = () => {destroyed();state.visible = false;
};defineExpose({open,
});
</script><style lang="scss" scoped>
.editor-container {position: relative;.editor-tree {width: 200px;position: absolute;left: 0;top: 0;height: 100%;}.editor-content {margin-left: 210px;display: flex;flex-direction: column;.editor-main {border: 1px solid #ccc;height: 200px;}.fn {display: flex;height: 200px;> div {flex: 1;border: 1px solid #ccc;}}}
}
:deep(.cm-focused) {outline: none;
}
:deep(.cm-gutters) {display: none;
}
</style>

插入标签的实现

根据官网例子以及部分大佬思路改编

  1. 插入标签使用[[${id}.${label}]]
  /*** @description 插入文本并设置光标位置*/const insertText = (text: string, type: "fn" | "tag" = "tag") => {if (view.value) {let content = type === "tag" ? `[[${text}]]` : `{{${text}}}()`;const selection = view.value.state.selection;if (!selection.main.empty) {// 如果选中文本,则替换选中文本const from = selection.main.from;const to = selection.main.to;const anchor =type === "tag" ? from + content.length : from + content.length - 1;const transaction = view.value!.state.update({changes: { from, to, insert: content }, // 在当前光标位置插入标签selection: {anchor,}, // 指定新光标位置});view.value.dispatch(transaction);} else {// 如果没有选中文本,则插入标签const pos = selection.main.head;const anchor =type === "tag" ? pos + content.length : pos + content.length - 1;const transaction = view.value.state.update({changes: { from: pos, to: pos, insert: content }, // 在当前光标位置插入标签selection: {anchor: anchor,}, // 指定新光标位置});view.value.dispatch(transaction);}setTimeout(() => {view.value?.focus();}, 0);}};
  1. 然后去匹配[[]]中的内容,取出来用span包裹
/*** @description 插入tag*/
const placeholderTagMatcher = new MatchDecorator({regexp: /\[\[(.+?)\]\]/g,decoration: (match) => {return Decoration.replace({ widget: new PlaceholderTag(match[1]) });},
});
// 定义一个 PlaceholderTag 类,继承自 WidgetType
class PlaceholderTag extends WidgetType {// 定义一个字符串类型的 id 属性,默认值为空字符串id: string = "";// 定义一个字符串类型的 text 属性,默认值为空字符串text: string = "";// 构造函数,接收一个字符串类型的 text 参数constructor(text: string) {// 调用父类的构造函数super();// 被替换的数据处理if (text) {const [id, ...texts] = text.split(".");if (id && texts.length) {this.text = texts.join(".");this.id = id;console.log(this.text, "id:", this.id);}}}eq(other: PlaceholderTag) {return this.text == other.text;}// 此处是我们的渲染方法toDOM() {let elt = document.createElement("span");if (!this.text) return elt;elt.className = "cm-tag";elt.textContent = this.text;return elt;}ignoreEvent() {return true;}
}
// 导出一个名为placeholders的常量,它是一个ViewPlugin实例,通过fromClass方法创建
const placeholderTag = ViewPlugin.fromClass(// 定义一个匿名类,该类继承自ViewPlugin的基类class {// 定义一个属性placeholders,用于存储装饰集placeholders: DecorationSet;// 构造函数,接收一个EditorView实例作为参数constructor(view: EditorView) {// 调用placeholderMatcher.createDeco方法,根据传入的view创建装饰集,并赋值给placeholders属性this.placeholders = placeholderTagMatcher.createDeco(view);}// update方法,用于在视图更新时更新装饰集update(update: ViewUpdate) {// 调用placeholderMatcher.updateDeco方法,根据传入的update和当前的placeholders更新装饰集,并重新赋值给placeholders属性this.placeholders = placeholderTagMatcher.updateDeco(update,this.placeholders);}},// 配置对象,用于定义插件的行为{// decorations属性,返回当前实例的placeholders属性,用于提供装饰集decorations: (v) => v.placeholders,// provide属性,返回一个函数,该函数返回一个EditorView.atomicRanges的提供者provide: (plugin) =>EditorView.atomicRanges.of((view) => {// 从view中获取当前插件的placeholders属性,如果不存在则返回Decoration.nonereturn view.plugin(plugin)?.placeholders || Decoration.none;}),}
);
  1. 设置样式
const baseTheme = EditorView.baseTheme({".cm-tag": {paddingLeft: "6px",paddingRight: "6px",paddingTop: "3px",paddingBottom: "3px",marginLeft: "3px",marginRight: "3px",backgroundColor: "#ffcdcc",borderRadius: "4px",},".cm-fn": {color: "#01a252",},
});
  1. 使用插件
    在这里插入图片描述

插入公式的实现

同理,我只是把[[]]换成了{{}},然后样式也修改了

注意:我们插入标签和公式的时候要指定光标位置,不然会出现问题,使用起来也不方便

提示补全的实现

也是根据官网例子改编,注意要先下载依赖@codemirror/autocomplete

/*** @description 补全提示*/
const completions = [{label: "SUM",apply: insetCompletion,},{label: "IF",apply: insetCompletion,},
];
/*** @description 补全提示* @param {CompletionContext} context* @return {*}*/
function myCompletions(context: CompletionContext) {// 匹配到以s或su或sum或i或if开头的单词let before = context.matchBefore(/[s](?:u(?:m)?)?|[i](?:f)?/gi);if (!context.explicit && !before) return null;return {from: before ? before.from : context.pos,options: completions,};
}
/*** @description 插入补全* @param {EditorView} view* @param {Completion} completion* @param {number} from* @param {number} to*/
function insetCompletion(view: EditorView,completion: Completion,from: number,to: number
) {const content = `{{${completion.label}}}()`;const anchor = from + content.length - 1;const transaction = view.state.update({changes: { from, to, insert: content }, // 在当前光标位置插入标签selection: {anchor: anchor,}, // 指定新光标位置});view.dispatch(transaction);
}

使用插件
在这里插入图片描述
仓库地址
在线预览

相关文章:

Vue3+codemirror6实现公式(规则)编辑器

实现截图 实现/带实现功能 插入标签 插入公式 提示补全 公式验证 公式计算 需要的依赖 "codemirror/autocomplete": "^6.18.4","codemirror/lang-javascript": "^6.2.2","codemirror/state": "^6.5.2","cod…...

Lua中文语言编程源码-第十一节,其它小改动汉化过程

__tostring 汉化过程 liolib.c metameth[] {"__转换为字符串", f_tostring}, lauxlib.c luaL_callmeta(L, idx, "__转换为字符串") lua.c luaL_callmeta(L, 1, "__转换为字符串") __len 汉化过程 ltm.c luaT_eventname[] ltablib.c c…...

Safari常用快捷键

一、书签边栏 1、显示或隐藏书签边栏&#xff1a;Control-Command-1 2、选择下一个书签或文件夹&#xff1a;向上头键或向下头键 3、打开所选书签&#xff1a;空格键 4、打开所选文件夹&#xff1a;空格键或右箭头键 5、关闭所选文件夹&#xff1a;空格键或左箭头键 6、更…...

Git登录并解决 CAPTCHA

修改公司域账户密码之后&#xff0c;导致今天pull代码时显示&#xff1a;remote error: CAPTCHA required 本文将介绍如何解决 Git 中的常见错误“fatal: Authentication failed for git”。该问题通常出现在尝试访问远程 Git 仓库时&#xff0c;表示身份验证失败。以下是几种常…...

Websocket从原理到实战

引言 WebSocket 是一种在单个 TCP 连接上进行全双工通信的网络协议&#xff0c;它使得客户端和服务器之间能够进行实时、双向的通信&#xff0c;既然是通信协议一定要从发展历史到协议内容到应用场景最后到实战全方位了解 发展历史 WebSocket 最初是为了解决 HTTP 协议在实时…...

Ubuntu 下 nginx-1.24.0 源码分析 - ngx_get_options函数

声明 就在 main函数所在的 nginx.c 中&#xff1a; static ngx_int_t ngx_get_options(int argc, char *const *argv); 实现 static ngx_int_t ngx_get_options(int argc, char *const *argv) {u_char *p;ngx_int_t i;for (i 1; i < argc; i) {p (u_char *) argv[i]…...

判断您的Mac当前使用的是Zsh还是Bash:echo $SHELL、echo $0

要判断您的Mac当前使用的是Zsh还是Bash&#xff0c;可以使用以下方法&#xff1a; 查看默认Shell: 打开“终端”应用程序&#xff0c;然后输入以下命令&#xff1a; echo $SHELL这将显示当前默认使用的Shell。例如&#xff0c;如果输出是/bin/zsh&#xff0c;则说明您使用的是Z…...

Centos执行yum命令报错

错误描述 错误&#xff1a;为仓库 ‘appstream’ 下载元数据失败 : Cannot prepare internal mirrorlist: Curl error (6): Couldn’t resolve host name for http://mirrorlist.centos.org/?release8&archx86_64&repoAppStream&infrastock [Could not resolve h…...

订单超时设计(1)--- 如何使用redis实现订单超时实时关闭功能

如何使用redis实现订单超时实时关闭功能 准备工作实现步骤解释注意事项&#xff08;重点&#xff09; 使用Redis实现订单超时实时关闭功能&#xff0c;可以利用Redis的延时队列&#xff08;使用Sorted Set实现&#xff09;和过期键&#xff08;使用TTL和Keyspace Notifications…...

485网关数据收发测试

目录 1.UDP SERVER数据收发测试 使用产品&#xff1a; || ZQWL-GW1600NM 产品||【智嵌物联】智能网关型串口服务器 1.UDP SERVER数据收发测试 A&#xff08;TX&#xff09;连接RX B&#xff08;RX&#xff09;连接TX 打开1个网络调试助手&#xff0c;模拟用户的UDP客户端设…...

RabbitMQ快速上手及入门

概念 概念&#xff1a; publisher&#xff1a;生产者&#xff0c;也就是发送消息的一方 consumer&#xff1a;消费者&#xff0c;也就是消费消息的一方 queue&#xff1a;队列&#xff0c;存储消息。生产者投递的消息会暂存在消息队列中&#xff0c;等待消费者处理 exchang…...

4种架构的定义和关联

文章目录 **1. 各架构的定义****业务架构&#xff08;Business Architecture&#xff09;****应用架构&#xff08;Application Architecture&#xff09;****数据架构&#xff08;Data Architecture&#xff09;****技术架构&#xff08;Technology Architecture&#xff09;*…...

109,【1】攻防世界 web 题目名称-文件包含

进入靶场 直接显示源代码 提示我们通过get方式传递名为filename的参数&#xff0c;同时给出了文件名check.php filenamecheck.php 显示使用了正确的用法&#xff0c;错误的方法 filename./check.php 还是一样的回显 傻了&#xff0c;题目名称是文件包含&#xff0c;需要用到…...

leetcode90 子集II

1. 题意 给一个可能含有重复元素的数组&#xff0c;求这个数组的所有子集。 2. 题解 跟leetcode 72 子集的差别在于&#xff0c;我们需要将重复的元素给去掉。那如何去重呢&#xff0c;实际上我们可以先排序将重复的元素给放在一起。然后在回溯后&#xff0c;找到下一个不与…...

DeepSeek模型构建与训练

在完成数据预处理之后,下一步就是构建和训练深度学习模型。DeepSeek提供了简洁而强大的API,使得模型构建和训练变得非常直观。无论是简单的全连接网络,还是复杂的卷积神经网络(CNN)或循环神经网络(RNN),DeepSeek都能轻松应对。本文将带你一步步构建一个深度学习模型,并…...

PyTorch torch.unbind、torch.split 和 torch.chunk函数介绍

pytorch中 torch.unbind、torch.split 和 torch.chunk等函数可用于张量的拆分操作。 1. torch.unbind 功能说明&#xff1a; torch.unbind 沿指定的维度将张量“解包”为多个张量&#xff0c;返回一个元组。解包后被操作的那个维度会消失&#xff0c;每个输出张量的维度数会比…...

【愚公系列】《循序渐进Vue.js 3.x前端开发实践》061-Vue Router的动态路由

标题详情作者简介愚公搬代码头衔华为云特约编辑&#xff0c;华为云云享专家&#xff0c;华为开发者专家&#xff0c;华为产品云测专家&#xff0c;CSDN博客专家&#xff0c;CSDN商业化专家&#xff0c;阿里云专家博主&#xff0c;阿里云签约作者&#xff0c;腾讯云优秀博主&…...

杭州某小厂面试

问的都是基础知识&#xff0c;主要是三个部分&#xff1a;计网&#xff0c;数据库&#xff0c;java。计网答得挺好&#xff0c;数据答得一般&#xff0c;Java答得一坨。 目录 1.TCP/IP协议的5层模型 2.3次握手和4次挥手 3.操作系统中的进程和线程的区别 4.lunix top 命令看…...

C基础寒假练习(8)

一、终端输入10个学生成绩&#xff0c;使用冒泡排序对学生成绩从低到高排序 #include <stdio.h> int main(int argc, const char *argv[]) {int arr[10]; // 定义一个长度为10的整型数组&#xff0c;用于存储学生成绩int len sizeof(arr) / sizeof(arr[0]); // 计算数组…...

设计模式 ->模板方法模式(Template Method Pattern)

模板方法模式 模板方法模式是一种行为设计模式&#xff0c;它在一个方法中定义一个操作的算法骨架&#xff0c;而将一些步骤延迟到子类中实现。它允许子类在不改变算法结构的情况下重新定义算法中的某些步骤 特点 算法骨架&#xff1a; 在基类中定义算法的框架延迟实现&…...

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…...

JavaSec-RCE

简介 RCE(Remote Code Execution)&#xff0c;可以分为:命令注入(Command Injection)、代码注入(Code Injection) 代码注入 1.漏洞场景&#xff1a;Groovy代码注入 Groovy是一种基于JVM的动态语言&#xff0c;语法简洁&#xff0c;支持闭包、动态类型和Java互操作性&#xff0c…...

idea大量爆红问题解决

问题描述 在学习和工作中&#xff0c;idea是程序员不可缺少的一个工具&#xff0c;但是突然在有些时候就会出现大量爆红的问题&#xff0c;发现无法跳转&#xff0c;无论是关机重启或者是替换root都无法解决 就是如上所展示的问题&#xff0c;但是程序依然可以启动。 问题解决…...

Linux简单的操作

ls ls 查看当前目录 ll 查看详细内容 ls -a 查看所有的内容 ls --help 查看方法文档 pwd pwd 查看当前路径 cd cd 转路径 cd .. 转上一级路径 cd 名 转换路径 …...

Vue2 第一节_Vue2上手_插值表达式{{}}_访问数据和修改数据_Vue开发者工具

文章目录 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染2. 插值表达式{{}}3. 访问数据和修改数据4. vue响应式5. Vue开发者工具--方便调试 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染 准备容器引包创建Vue实例 new Vue()指定配置项 ->渲染数据 准备一个容器,例如: …...

ESP32 I2S音频总线学习笔记(四): INMP441采集音频并实时播放

简介 前面两期文章我们介绍了I2S的读取和写入&#xff0c;一个是通过INMP441麦克风模块采集音频&#xff0c;一个是通过PCM5102A模块播放音频&#xff0c;那如果我们将两者结合起来&#xff0c;将麦克风采集到的音频通过PCM5102A播放&#xff0c;是不是就可以做一个扩音器了呢…...

Android 之 kotlin 语言学习笔记三(Kotlin-Java 互操作)

参考官方文档&#xff1a;https://developer.android.google.cn/kotlin/interop?hlzh-cn 一、Java&#xff08;供 Kotlin 使用&#xff09; 1、不得使用硬关键字 不要使用 Kotlin 的任何硬关键字作为方法的名称 或字段。允许使用 Kotlin 的软关键字、修饰符关键字和特殊标识…...

云原生玩法三问:构建自定义开发环境

云原生玩法三问&#xff1a;构建自定义开发环境 引言 临时运维一个古董项目&#xff0c;无文档&#xff0c;无环境&#xff0c;无交接人&#xff0c;俗称三无。 运行设备的环境老&#xff0c;本地环境版本高&#xff0c;ssh不过去。正好最近对 腾讯出品的云原生 cnb 感兴趣&…...

08. C#入门系列【类的基本概念】:开启编程世界的奇妙冒险

C#入门系列【类的基本概念】&#xff1a;开启编程世界的奇妙冒险 嘿&#xff0c;各位编程小白探险家&#xff01;欢迎来到 C# 的奇幻大陆&#xff01;今天咱们要深入探索这片大陆上至关重要的 “建筑”—— 类&#xff01;别害怕&#xff0c;跟着我&#xff0c;保准让你轻松搞…...

RSS 2025|从说明书学习复杂机器人操作任务:NUS邵林团队提出全新机器人装配技能学习框架Manual2Skill

视觉语言模型&#xff08;Vision-Language Models, VLMs&#xff09;&#xff0c;为真实环境中的机器人操作任务提供了极具潜力的解决方案。 尽管 VLMs 取得了显著进展&#xff0c;机器人仍难以胜任复杂的长时程任务&#xff08;如家具装配&#xff09;&#xff0c;主要受限于人…...