vue2中,codemirror編輯器的使用

交互說明?

在編輯器中輸入{時,會自動彈出選項彈窗,然后可以選值插入。

代碼

父組件

<variable-editorv-model="content":variables="variables"placeholder="請輸入模板內容..."@blur="handleBlur"
/>data() {return {content: "這是一個示例 {user.name}",variables: [{id: "user",label: "user",type: "object",children: [{ id: "user.name", label: "name", type: "string" },{ id: "user.age", label: "age", type: "number" },],},{id: "items",label: "items",type: "array<object>",children: [{ id: "items.title", label: "title", type: "string" },{ id: "items.price", label: "price", type: "number" },],},],};},handleBlur(val) {console.log("編輯器內容已更新:", val);
},

子組件?

<template><div class="variable-editor"><div ref="editorRef" class="editor-container"></div><el-popoverv-if="variables && variables.length > 0"ref="popover"placement="left-start":value="popoverOpen":visible-arrow="false"trigger="manual"@after-enter="handleAfterOpen"><divclass="tree-wrap my-variable-popover"tabindex="-1"@keydown="handleKeyDown"@keydown.capture="handleKeyDownCapture"ref="treeRef"><el-tree:data="variables":props="defaultProps"default-expand-all:highlight-current="true":current-node-key="selectedKeys[0]"@current-change="handleCurrentChange"@node-click="handleVariableInsert"ref="tree"node-key="id"><div slot-scope="{ node, data }" class="flex-row-center"><i v-if="getTypeIcon(data)" :class="getTypeIcon(data)"></i><span class="ml-1">{{ node.label }}</span></div></el-tree></div><span slot="reference" ref="anchorRef" class="anchor-point"></span></el-popover></div>
</template><script>
import {EditorView,ViewPlugin,placeholder,Decoration,keymap,
} from "@codemirror/view";
import { EditorState, RangeSetBuilder, StateEffect } from "@codemirror/state";
import { defaultKeymap, insertNewlineAndIndent } from "@codemirror/commands";// 扁平化樹結構
const flattenTree = (nodes, result = []) => {for (const node of nodes) {result.push({ key: node.id, title: node.label });if (node.children) {flattenTree(node.children, result);}}return result;
};export default {name: "VariableEditor",props: {value: {type: String,default: "",},variables: {type: Array,default: () => [],},placeholder: {type: String,default: "請輸入內容...",},},data() {return {popoverOpen: false,selectedKeys: [],editorView: null,lastCursorPos: null,flattenedTree: [],defaultProps: {children: "children",label: "label",},// 類型圖標映射typeIcons: {string: "el-icon-document",number: "el-icon-tickets",boolean: "el-icon-switch-button",object: "el-icon-folder","array<object>": "el-icon-collection",},};},computed: {currentIndex() {return this.flattenedTree.findIndex((node) => node.key === this.selectedKeys[0]);},},mounted() {this.flattenedTree = flattenTree(this.variables);this.initEditor();},beforeDestroy() {if (this.editorView) {this.editorView.destroy();}},watch: {variables: {handler(newVal) {this.flattenedTree = flattenTree(newVal);if (this.editorView) {// 重新配置編輯器以更新插件this.editorView.dispatch({effects: StateEffect.reconfigure.of(this.createExtensions()),});}},deep: true,},value(newVal) {if (this.editorView && newVal !== this.editorView.state.doc.toString()) {this.editorView.dispatch({changes: {from: 0,to: this.editorView.state.doc.length,insert: newVal,},});}},popoverOpen(val) {if (val && this.flattenedTree.length > 0) {this.selectedKeys = [this.flattenedTree[0].key];this.$nextTick(() => {if (this.$refs.tree) {this.$refs.tree.setCurrentKey(this.selectedKeys[0]);}});}},},methods: {getTypeIcon(data) {return this.typeIcons[data.type] || this.typeIcons.string;},initEditor() {if (!this.$refs.editorRef) return;this.editorView = new EditorView({doc: this.value,parent: this.$refs.editorRef,extensions: this.createExtensions(),});// 添加失焦事件this.$refs.editorRef.addEventListener("blur", this.onEditorBlur);},createExtensions() {return [placeholder(this.placeholder || "請輸入內容..."),EditorView.editable.of(true),EditorView.lineWrapping,keymap.of([...defaultKeymap,{ key: "Enter", run: insertNewlineAndIndent },]),EditorState.languageData.of(() => {return [{ autocomplete: () => [] }];}),this.createUpdateListener(),this.createVariablePlugin(),this.createInterpolationPlugin(this.variables),];},createUpdateListener() {return EditorView.updateListener.of((update) => {if (update.docChanged) {// const content = update.state.doc.toString();// 不要在每次更改時都觸發,而是在失焦時觸發}});},createVariablePlugin() {const self = this;return ViewPlugin.fromClass(class {constructor(view) {this.view = view;}update(update) {if (update.docChanged || update.selectionSet) {const pos = update.state.selection.main.head;const doc = update.state.doc.toString();// 只有當光標位置真正變化時才更新if (self.lastCursorPos !== pos) {self.lastCursorPos = pos;// 延遲更新 Popover 位置setTimeout(() => {self.$refs.popover &&self.$refs.popover.$el &&self.$refs.popover.updatePopper();}, 10);}// 1. 正則查找所有的 {xxx}const regex = /\{(.*?)\}/g;let match;let inInterpolation = false;while ((match = regex.exec(doc)) !== null) {const start = match.index;const end = start + match[0].length;if (pos > start && pos < end) {// 光標在插值表達式內inInterpolation = true;setTimeout(() => {const coords = this.view.coordsAtPos(pos);const editorRect = this.view.dom.getBoundingClientRect();if (coords) {self.$refs.anchorRef.style.position = "absolute";self.$refs.anchorRef.style.left = `${coords.left - editorRect.left - 10}px`;self.$refs.anchorRef.style.top = `${coords.top - editorRect.top}px`;self.$refs.anchorRef.dataset.start = start;self.$refs.anchorRef.dataset.end = end;self.popoverOpen = true;}}, 0);break;}}if (!inInterpolation) {// 檢測輸入 { 的情況const prev = update.state.sliceDoc(pos - 1, pos);if (prev === "{") {setTimeout(() => {const coords = this.view.coordsAtPos(pos);const editorRect = this.view.dom.getBoundingClientRect();if (coords) {self.$refs.anchorRef.style.position = "absolute";self.$refs.anchorRef.style.left = `${coords.left - editorRect.left - 10}px`;self.$refs.anchorRef.style.top = `${coords.top - editorRect.top}px`;self.$refs.anchorRef.dataset.start = pos;self.$refs.anchorRef.dataset.end = pos;self.popoverOpen = true;}}, 0);} else {self.popoverOpen = false;}}}}});},createInterpolationPlugin(variables) {const self = this;return ViewPlugin.fromClass(class {constructor(view) {this.decorations = this.buildDecorations(view);}update(update) {if (update.docChanged || update.viewportChanged) {this.decorations = this.buildDecorations(update.view);}}buildDecorations(view) {const builder = new RangeSetBuilder();const doc = view.state.doc;const text = doc.toString();const regex = /\{(.*?)\}/g;let match;while ((match = regex.exec(text)) !== null) {const [full, expr] = match;const start = match.index;const end = start + full.length;const isValid = self.validatePath(variables, expr.trim());const deco = Decoration.mark({class: isValid? "cm-decoration-interpolation-valid": "cm-decoration-interpolation-invalid",});builder.add(start, end, deco);}return builder.finish();}},{decorations: (v) => v.decorations,});},validatePath(schema, rawPath) {const segments = rawPath.replace(/\[(\d+)\]/g, "[$1]").split(".");// 遞歸匹配function match(nodes, index) {if (index >= segments.length) return true;const currentKey = segments[index];for (const node of nodes) {const { label: title, type, children } = node;// 匹配數組字段,如 abc[0]if (/\[\d+\]$/.test(currentKey)) {const name = currentKey.replace(/\[\d+\]$/, "");if (title === name && type === "array<object>" && children) {return match(children, index + 1);}}// 匹配普通字段if (title === currentKey) {if ((type === "object" || type === "array<object>") && children) {return match(children, index + 1);}// 如果不是object類型,且已經是最后一個字段return index === segments.length - 1;}}return false;}return match(schema, 0);},handleAfterOpen() {if (this.$refs.treeRef) {this.$refs.treeRef.focus();}},handleCurrentChange(data) {if (data) {this.selectedKeys = [data.id];}},handleVariableInsert(data) {const key = data.id;this.selectedKeys = [key];const view = this.editorView;if (!view) return;const state = view.state;const pos = state.selection.main.head;const doc = state.doc.toString();let insertText = `{${key}}`;let targetFrom = pos;let targetTo = pos;let foundInBraces = false;// 檢查光標是否在 {...} 內部const regex = /\{[^}]*\}/g;let match;while ((match = regex.exec(doc)) !== null) {const [full] = match;const start = match.index;const end = start + full.length;if (pos > start && pos < end) {targetFrom = start;targetTo = end;foundInBraces = true;break;}}// 如果不在 {...} 中,但光標前是 `{`,只插入 `${key}}`,不要加多一個 `{`if (!foundInBraces && doc[pos - 1] === "{") {targetFrom = pos;insertText = `${key}}`; // 前面已經有 {,只補后半段}const transaction = state.update({changes: {from: targetFrom,to: targetTo,insert: insertText,},selection: { anchor: targetFrom + insertText.length },});view.dispatch(transaction);view.focus();this.popoverOpen = false;},onEditorBlur(e) {const related = e.relatedTarget;// 如果焦點轉移到了 Popover 內部,則不處理 blurif (related && related.closest(".my-variable-popover")) {return;}const view = this.editorView;if (view) {this.$emit("input", view.state.doc.toString());this.$emit("blur");}},handleKeyDownCapture(e) {if (!["ArrowUp", "ArrowDown", "Enter"].includes(e.key)) {e.stopPropagation();}},handleKeyDown(e) {if (!["ArrowUp", "ArrowDown", "Enter"].includes(e.key)) return;if (e.key === "ArrowDown") {let nextKey;if (this.currentIndex < this.flattenedTree.length - 1) {nextKey = this.flattenedTree[this.currentIndex + 1].key;} else {nextKey = this.flattenedTree[0].key;}this.selectedKeys = [nextKey];this.$refs.tree.setCurrentKey(nextKey);} else if (e.key === "ArrowUp") {let prevKey;if (this.currentIndex > 0) {prevKey = this.flattenedTree[this.currentIndex - 1].key;} else {prevKey = this.flattenedTree[this.flattenedTree.length - 1].key;}this.selectedKeys = [prevKey];this.$refs.tree.setCurrentKey(prevKey);} else if (e.key === "Enter" && this.selectedKeys[0]) {// 查找對應的節點數據const findNodeData = (key, nodes) => {for (const node of nodes) {if (node.id === key) return node;if (node.children) {const found = findNodeData(key, node.children);if (found) return found;}}return null;};const nodeData = findNodeData(this.selectedKeys[0], this.variables);if (nodeData) {this.handleVariableInsert(nodeData);}}},},
};
</script><style scoped>
.variable-editor {position: relative;width: 100%;
}.editor-container {width: 100%;border: 1px solid #dcdfe6;border-radius: 4px;min-height: 150px;overflow: hidden;transition: border-color 0.2s, box-shadow 0.2s;
}/* CodeMirror 6 編輯器樣式 */
:global(.cm-editor) {height: 150px !important;min-height: 150px !important;overflow-y: auto;
}/* 編輯器獲取焦點時的樣式 */
:global(.cm-editor.cm-focused) {outline: none;
}/* 使用更具體的選擇器確保只有一層邊框高亮 */
.editor-container:focus-within {border-color: #409eff !important;
}.anchor-point {position: absolute;z-index: 10;
}.tree-wrap {min-width: 200px;
}.flex-row-center {display: flex;align-items: center;flex-wrap: nowrap;
}.ml-1 {margin-left: 4px;
}/* 添加到全局樣式中 */
:global(.cm-decoration-interpolation-valid) {color: #409eff;background-color: rgba(64, 158, 255, 0.1);
}:global(.cm-decoration-interpolation-invalid) {color: #f56c6c;background-color: rgba(245, 108, 108, 0.1);text-decoration: wavy underline #f56c6c;
}
</style>

依賴安裝?

npm install @codemirror/state @codemirror/view @codemirror/commands

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/907345.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/907345.shtml
英文地址,請注明出處:http://en.pswp.cn/news/907345.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Kafka自定義分區策略實戰避坑指南

文章目錄 概要代碼示例小結 概要 kafka生產者發送消息默認根據總分區數和設置的key計算哈希取余數&#xff0c;key不變就默認存放在一個分區&#xff0c;沒有key則隨機數分區&#xff0c;明顯默認的是最不好用的&#xff0c;那kafka也提供了一個輪詢分區策略&#xff0c;我自己…

WPF 全屏顯示實現(無標題欄按鈕 + 自定義退出按鈕)

WPF 全屏顯示實現&#xff08;無標題欄按鈕 自定義退出按鈕&#xff09; 完整實現代碼 MainWindow.xaml <Window x:Class"FullScreenApp.MainWindow"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas…

sqli_labs第二十九/三十/三十一關——hpp注入

一&#xff1a;HTTP參數污染&#xff1a; hpp&#xff08;http parameter pollution)注入中&#xff0c;可以通過在hppt的請求中注入多個同名參數來繞過安全過濾 原理&#xff1a;php默認只取最后一個同名參數 比如在這一關里&#xff0c;可能對第一個id參數進行消毒處理&a…

【STM32】按鍵控制LED 光敏傳感器控制蜂鳴器

&#x1f50e;【博主簡介】&#x1f50e; &#x1f3c5;CSDN博客專家 &#x1f3c5;2021年博客之星物聯網與嵌入式開發TOP5 &#x1f3c5;2022年博客之星物聯網與嵌入式開發TOP4 &#x1f3c5;2021年2022年C站百大博主 &#x1f3c5;華為云開發…

華為OD機試真題——斗地主之順子(2025B卷:100分)Java/python/JavaScript/C/C++/GO最佳實現

2025 B卷 100分 題型 本專欄內全部題目均提供Java、python、JavaScript、C、C++、GO六種語言的最佳實現方式; 并且每種語言均涵蓋詳細的問題分析、解題思路、代碼實現、代碼詳解、3個測試用例以及綜合分析; 本文收錄于專欄:《2025華為OD真題目錄+全流程解析+備考攻略+經驗分…

Qt找不到windows API報錯:error: LNK2019: 無法解析的外部符號 __imp_OpenClipboard

筆者在開發中出現的bug完整報錯如下&#xff1a; spcm_ostools_win.obj:-1: error: LNK2019: 無法解析的外部符號 __imp_OpenClipboard&#xff0c;函數 "void __cdecl spcmdrv::vCopyToClipboard(char const *,unsigned __int64)" (?vCopyToClipboardspcmdrvYAXPE…

4.8.4 利用Spark SQL實現分組排行榜

在本次實戰中&#xff0c;我們的目標是利用Spark SQL實現分組排行榜&#xff0c;特別是計算每個學生分數最高的前3個成績。任務的原始數據由一組學生成績組成&#xff0c;每個學生可能有多個成績記錄。我們首先將這些數據讀入Spark DataFrame&#xff0c;然后按學生姓名分組&am…

[PyMySQL]

掌握pymysql對數據庫實現增刪改查數據庫工具類封裝,數據庫操作應用場景數據庫操作應用場景 校驗測試數據 : 刪除員工 :構造測試數據 : 測試數據使用一次就失效,不能重復使用 : 添加員工(is_delete)測試數據在展開測試前無法確定是否存在 : 查詢,修改,刪除員工操作步驟:!~~~~~~~…

cs224w課程學習筆記-第12課

cs224w課程學習筆記-第12課 知識圖譜問答 前言一、問答類型分類二、路徑查詢(Path queries)2.1 直觀查詢方法2.2 TransE 擴展2.3 TransE 能力分析 三、連詞查詢(conjunctive queries)3.1 Query2box 原理1)、投影2)、交集查詢&#xff08;AND 操作)3)、聯合查詢&#xff08;OR 操…

AI任務相關解決方案2-基于WOA-CNN-BIGRU-Transformer模型解決光纖通信中的非線性問題

文章目錄 1. 項目背景與研究意義1.1 光纖通信中的非線性問題1.2 神經網絡在光纖非線性補償中的應用現狀 2. 現有模型 CNN-BIGRU-attention 分析2.1 模型架構與工作原理2.2 模型性能評估與局限性 3. 新模型優化方案3.1 WOA算法原理與優勢3.2 WOA-CNN-BIGRU-MHA模型構建3.3 WOA-C…

HTTP Accept簡介

一、HTTP Accept是什么 HTTP協議是一個客戶端和服務器之間進行通信的標準協議&#xff0c;它定義了發送請求和響應的格式。而HTTP Accept是HTTP協議中的一個HTTP頭部&#xff0c;用于告訴服務器請求方所期望的響應格式。這些格式可以是媒體類型、字符集、語言等信息。 HTTP A…

39-居住證管理系統(小程序)

技術棧: springBootVueMysqlUni-app 功能點: 群眾端 警方端 管理員端 群眾端: 1.首頁: 輪播圖展示、公告信息列表 2.公告欄: 公告查看及評論 3.我的: 聯系我們: 可在線咨詢管理員問題 實時回復 居住證登記申請 回執單查看 領證信息查看 4.個人中心: 個人信息查看及修改…

鴻蒙OSUniApp 開發的滑動圖片墻組件#三方框架 #Uniapp

UniApp 開發的滑動圖片墻組件 前言 在移動應用中&#xff0c;圖片墻是一種極具視覺沖擊力的內容展示方式&#xff0c;廣泛應用于相冊、商品展示、社交分享等場景。一個優秀的滑動圖片墻組件不僅要支持流暢的滑動瀏覽&#xff0c;還要兼容不同設備的分辨率和性能&#xff0c;尤…

碰一碰系統源碼搭建==saas系統

搭建“碰一碰”系統&#xff08;通常指基于NFC或藍牙的短距離交互功能&#xff09;的源碼實現&#xff0c;需結合具體技術棧和功能需求。以下是關鍵步驟和示例代碼&#xff1a; 技術選型 NFC模式&#xff1a;適用于Android/iOS設備的近場通信&#xff0c;需處理NDEF協議。藍牙…

自動駕駛決策規劃框架詳解:從理論到實踐

歡迎來到《自動駕駛決策規劃框架詳解:從理論到實踐》的第二章。在本章中,我們將深入探討自動駕駛系統中至關重要的“大腦”——決策規劃模塊。我們將從基本概念入手,逐步解析主流的決策規劃框架,包括經典的路徑速度解耦方法、工業界廣泛應用的Apollo Planning框架、應對復雜…

服務器定時任務查看和編輯

在 Ubuntu 系統中&#xff0c;查看當前系統中已開啟的定時任務主要有以下幾種方式&#xff0c;分別針對不同類型的定時任務管理方式&#xff08;如 crontab、systemd timer 等&#xff09;&#xff1a; 查看服務器定時任務 一、查看用戶級別的 Crontab 任務 每個用戶都可以配…

小白的進階之路系列之四----人工智能從初步到精通pytorch自定義數據集下

本篇涵蓋的內容 在之前的文章中,我們已經討論了如何獲取數據,轉換數據以及如何準備自定義數據集,本篇文章將涵蓋更加深入的問題,希望通過詳細的代碼示例,幫助大家了解PyTorch自定義數據集是如何應對各種復雜實際情況中,數據處理的。 更加詳細的,我們將討論下面一些內容…

DeepSeek實戰:打造智能數據分析與可視化系統

DeepSeek實戰:打造智能數據分析與可視化系統 1. 數據智能時代:DeepSeek數據分析系統入門 在數據驅動的決策時代,智能數據分析系統正成為企業核心競爭力。本節將使用DeepSeek構建一個從數據清洗到可視化分析的全流程智能系統。 1.1 系統核心功能架構 class DataAnalysisS…

力扣100題---字母異位詞分組

1.字母異位詞分組 給你一個字符串數組&#xff0c;請你將 字母異位詞 組合在一起。可以按任意順序返回結果列表。 字母異位詞 是由重新排列源單詞的所有字母得到的一個新單詞。 方法一&#xff1a;字母排序 class Solution {public List<List<String>> groupAnagr…

使用子查詢在 SQL Server 中進行數據操作

在 SQL Server 中&#xff0c;子查詢&#xff08;Subquery&#xff09;是一種在查詢中嵌套另一個查詢的技術&#xff0c;可以用來執行復雜的查詢、過濾數據或進行數據計算。子查詢通常被用在 SELECT、INSERT、UPDATE 或 DELETE 語句中&#xff0c;可以幫助我們高效地解決問題。…