当前位置:   article > 正文

Vue3,使用monaco-editor实现sql对比编辑器,自定义高亮_monaco-editor vue3

monaco-editor vue3

前言:上篇文章讲了monaco-editor的基本使用,这篇文章讲解如何实现monaco-editor的对比效果。

一:安装monaco-editor

npm install monaco-editor

二:使用

  1. import * as monaco from "monaco-editor";
  2. import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";

三:封装基本组件

注意:原本monaco-editor的效果是左编辑器为原始sql,右编辑器为新sql。我这的需求是让左编辑器为可编辑,即左编辑器为新sql。因此我开启了左边的可编辑配置,且将右编辑器设置为只读。(需要根据需求自己更改配置以及传入的值)

  1. <template>
  2. <div class="codemirror">
  3. <div id="monacoEditorDiff" ref="editorDiffRef" class="monaco-editor"></div>
  4. </div>
  5. </template>
  6. <script setup>
  7. import * as monaco from "monaco-editor";
  8. import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
  9. // 定义从父组件接收的属性
  10. const props = defineProps({
  11. options: {
  12. type: Object,
  13. default: () => ({
  14. theme: "dark",
  15. }),
  16. },
  17. oldValue: {
  18. type: String,
  19. },
  20. newValue: {
  21. type: String,
  22. },
  23. });
  24. const emits = defineEmits(["newSqlCode"]);
  25. //解决 Monaco Editor 无法正确加载其所需的 Web Worker
  26. self.MonacoEnvironment = {
  27. getWorker(workerId, label) {
  28. return new editorWorker();
  29. },
  30. };
  31. let editor;
  32. let originalModel;
  33. let modifiedModel;
  34. const init = () => {
  35. editor = monaco.editor.createDiffEditor(
  36. document.getElementById("monacoEditorDiff"),
  37. {
  38. value: "", //值
  39. autoIndex: true, // 控制是否开启自动索引。当开启时,编辑器会自动创建索引以加速搜索和语义高亮。
  40. fontSize: 14, // 字体大小
  41. language: "sql", //语言
  42. theme: props.options.theme, //主题
  43. readOnly: true, // 是否只读
  44. overviewRulerBorder: false, // 滚动是否有边框
  45. cursorSmoothCaretAnimation: true, // 控制光标平滑动画的开启与关闭。当开启时,光标移动会有平滑的动画效果。
  46. mouseWheelZoom: true, //设置是否开启鼠标滚轮缩放功能
  47. folding: true, //控制是否开启代码折叠功能
  48. automaticLayout: true, // 控制编辑器是否自动调整布局以适应容器大小的变化
  49. minimap: {
  50. // 关闭代码缩略图
  51. enabled: false, // 是否启用预览图
  52. },
  53. scrollbar: {
  54. verticalScrollbarSize: 2, // 垂直滚动条宽度,默认px
  55. horizontalScrollbarSize: 2, // 水平滚动条高度
  56. },
  57. wordWrap: "on", // 开启自动换行
  58. roundedSelection: true, // 右侧不显示编辑器预览框
  59. originalEditable: true, // 是否允许修改原始文本
  60. ...props.options,
  61. }
  62. );
  63. //创建左右编辑器
  64. originalModel = monaco.editor.createModel(props.newValue, "sql");
  65. modifiedModel = monaco.editor.createModel(props.oldValue, "sql");
  66. editor.setModel({
  67. original: originalModel,
  68. modified: modifiedModel,
  69. });
  70. // 原始模型的内容发生变化时触发的操作
  71. originalModel.onDidChangeContent(() => {
  72. //获取修改后的文本
  73. // const modifiedText = modifiedModel.getValue();
  74. //获取原始文本
  75. const OriginalText = originalModel.getValue();
  76. emits("newSqlCode", OriginalText);
  77. });
  78. // 修改后模型的内容发生变化时触发的操作
  79. modifiedModel.onDidChangeContent(() => {});
  80. removeArrow();
  81. };
  82. /**
  83. * 移除 revert change(还原更改)箭头
  84. */
  85. const removeArrow = () => {
  86. setTimeout(() => {
  87. const revertChangeButtons = document.querySelectorAll(
  88. ".codicon-arrow-right"
  89. );
  90. revertChangeButtons.forEach((button) => {
  91. button.style.display = "none";
  92. });
  93. }, 500);
  94. };
  95. // 组件挂载后创建编辑器实例
  96. onMounted(() => {
  97. //初始化编辑器
  98. init();
  99. });
  100. onBeforeUnmount(() => {
  101. if (editor) {
  102. // 组件卸载前销毁编辑器实例
  103. editor.dispose();
  104. }
  105. });
  106. </script>
  107. <style scoped lang="less">
  108. .monaco-editor {
  109. height: 150px;
  110. width: 100%;
  111. }
  112. </style>

四:实现自定义单词高亮

实现目标需求:将传入的,需要高亮的关键词在编辑器中高亮。

注意:

一:sql里面大小写并不敏感,因此需要在匹配高亮关键词的时候,将大小写转换。

二:同一个span里面有可能出现匹配多个高亮关键词的情况

三:不能直接修改高亮词所在span的背景颜色,这样子会导致其他不匹配的字母也被高亮。应该找到对应的高亮词,替换成带有背景颜色的span。

  1. // 定义从父组件接收的属性
  2. const props = defineProps({
  3. options: {
  4. type: Object,
  5. default: () => ({
  6. theme: "dark",
  7. }),
  8. },
  9. oldValue: {
  10. type: String,
  11. },
  12. newValue: {
  13. type: String,
  14. },
  15. //需要高亮的关键词
  16. highLightWords: {
  17. type: Array,
  18. default: () => [],
  19. },
  20. });
  21. /**
  22. * 设置高亮
  23. */
  24. const setHighlight = () => {
  25. //有关键字的情况下 才需要高亮
  26. if (props.highLightWords.length > 0) {
  27. // 获取 Diff Editor 中原始编辑器的容器节点
  28. const originalEditorContainer = editor.getOriginalEditor().getDomNode();
  29. //获取第一个子节点
  30. const originalSonElement =
  31. originalEditorContainer && originalEditorContainer.firstChild;
  32. //获取第一个子节点的第一个子节点
  33. const originalGrandsonElement =
  34. originalSonElement && originalSonElement.firstChild;
  35. const linesContainer = originalEditorContainer.querySelector(".view-lines");
  36. // 获取所有行的容器节点
  37. const divLines = linesContainer.childNodes;
  38. // 遍历每一行的容器节点
  39. divLines.forEach((lineNode) => {
  40. //拿到每一行的总span节点
  41. const spanNodes = lineNode.childNodes;
  42. //拿到每一行的总span节点内容
  43. let originalSpanHtml = spanNodes[0].innerHTML;
  44. // 存储匹配到的关键字
  45. let successKeyWord = [];
  46. // 检查 span 元素的文本内容是否包含了 props.highLightWords 数组中的一个或多个关键字
  47. props.highLightWords.forEach((highlightWord) => {
  48. //创建正则表达式进行全局、忽略大小写匹配
  49. const regex = new RegExp(highlightWord, "gi");
  50. //如果匹配到,则将匹配成功的关键字存储起来
  51. if (regex.test(originalSpanHtml)) {
  52. successKeyWord.push(highlightWord);
  53. }
  54. });
  55. // 对 successKeyWord 数组进行去重操作
  56. successKeyWord = Array.from(new Set(successKeyWord));
  57. // 如果有任意一个关键字包含在 span 元素的文本内容中,则修改对应关键字的样式,而不是修改整个span
  58. if (successKeyWord.length > 0) {
  59. // 创建一个新的字符串来保存修改后的结果
  60. let modifiedHtml = originalSpanHtml;
  61. // 遍历匹配成功关键字数组中的每个字符串
  62. successKeyWord.forEach((str) => {
  63. // 创建带有黄色背景色的 <span> 元素
  64. const highlightedText = `<span style="background-color: yellow;">${str}</span>`;
  65. // 使用正则表达式进行全局、忽略大小写的替换
  66. modifiedHtml = modifiedHtml.replace(
  67. new RegExp(str, "gi"),
  68. highlightedText
  69. );
  70. });
  71. // 将修改后的 HTML 内容设置回原来的 span 元素中
  72. spanNodes[0].innerHTML = modifiedHtml;
  73. }
  74. });
  75. }
  76. };
  77. // 组件挂载后创建编辑器实例
  78. onMounted(() => {
  79. //初始化编辑器
  80. init();
  81. setHighlight();
  82. });

上面的代码已经实现了高亮,但是由于每次输入值,以及页面放大缩小,编辑器放大缩小等操作,编辑器都会重新加载渲染dom,导致高亮失效。

一:因此需要监听编辑器的dom变化,并在每次变化时都重新渲染高亮。

二:修改编辑器内容时,重新渲染高亮。

三:加入了防抖函数,避免在输入值的时候,频繁渲染高亮。

安装防抖:

npm install lodash

引入防抖:

  1. //引入防抖
  2. import debounce from "lodash/debounce";

使用

  1. // 原始模型的内容发生变化时触发的操作
  2. originalModel.onDidChangeContent(() => {
  3. //获取修改后的文本
  4. // const modifiedText = modifiedModel.getValue();
  5. //获取原始文本
  6. const OriginalText = originalModel.getValue();
  7. //内容发生变化 需要重新设置高亮
  8. setHighlight();
  9. emits("newSqlCode", OriginalText);
  10. });
  11. /**
  12. * 设置自定义高亮
  13. */
  14. let observer = null;
  15. let originalEditorContainer = null;
  16. //挂载高亮观察器
  17. const setLightObserver = () => {
  18. //有关键字的情况下 才需要高亮
  19. if (props.highLightWords.length > 0) {
  20. // 获取 Diff Editor 中原始编辑器的容器节点
  21. originalEditorContainer = editor.getOriginalEditor().getDomNode();
  22. //获取第一个子节点
  23. const originalSonElement =
  24. originalEditorContainer && originalEditorContainer.firstChild;
  25. //获取第一个子节点的第一个子节点
  26. const originalGrandsonElement =
  27. originalSonElement && originalSonElement.firstChild;
  28. //创建一个 MutationObserver 实例
  29. observer = new MutationObserver(() => {
  30. setHighlight();
  31. });
  32. // 配置观察器以监视特定类型的变化
  33. if (originalSonElement) {
  34. // 监视 originalSonElement 的样式变化,如果发生变化重新设置高亮(用于页面放大缩小时)
  35. observer.observe(originalSonElement, {
  36. attributes: true, // 监视属性的变化
  37. attributeFilter: ["style"], // 监视特定属性
  38. });
  39. // 监视 originalGrandsonElement 的样式变化,如果发生变化重新设置高亮(用于编辑器放大缩小滚动时)
  40. observer.observe(originalGrandsonElement, {
  41. attributes: true, // 监视属性的变化
  42. attributeFilter: ["style"], // 监视特定属性
  43. });
  44. }
  45. }
  46. };
  47. //使用防抖,避免多次渲染黄色背景
  48. const setHighlight = debounce(() => {
  49. const linesContainer = originalEditorContainer.querySelector(".view-lines");
  50. // 获取所有行的容器节点
  51. const divLines = linesContainer.childNodes;
  52. // 遍历每一行的容器节点
  53. divLines.forEach((lineNode) => {
  54. //拿到每一行的总span节点
  55. const spanNodes = lineNode.childNodes;
  56. //拿到每一行的总span节点内容
  57. let originalSpanHtml = spanNodes[0].innerHTML;
  58. // 存储匹配到的关键字
  59. let successKeyWord = [];
  60. // 检查 span 元素的文本内容是否包含了 props.highLightWords 数组中的一个或多个关键字
  61. props.highLightWords.forEach((highlightWord) => {
  62. //创建正则表达式进行全局、忽略大小写匹配
  63. const regex = new RegExp(highlightWord, "gi");
  64. //如果匹配到,则将匹配成功的关键字存储起来
  65. if (regex.test(originalSpanHtml)) {
  66. successKeyWord.push(highlightWord);
  67. }
  68. });
  69. // 对 successKeyWord 数组进行去重操作
  70. successKeyWord = Array.from(new Set(successKeyWord));
  71. // 如果有任意一个关键字包含在 span 元素的文本内容中,则修改对应关键字的样式,而不是修改整个span
  72. if (successKeyWord.length > 0) {
  73. // 创建一个新的字符串来保存修改后的结果
  74. let modifiedHtml = originalSpanHtml;
  75. // 遍历匹配成功关键字数组中的每个字符串
  76. successKeyWord.forEach((str) => {
  77. // 创建带有黄色背景色的 <span> 元素
  78. const highlightedText = `<span style="background-color: yellow;">${str}</span>`;
  79. // 使用正则表达式进行全局、忽略大小写的替换
  80. modifiedHtml = modifiedHtml.replace(
  81. new RegExp(str, "gi"),
  82. highlightedText
  83. );
  84. });
  85. // 将修改后的 HTML 内容设置回原来的 span 元素中
  86. spanNodes[0].innerHTML = modifiedHtml;
  87. }
  88. });
  89. }, 500);
  90. // 组件挂载后创建编辑器实例
  91. onMounted(() => {
  92. //初始化编辑器
  93. init();
  94. //挂载高亮观察器
  95. setLightObserver();
  96. });
  97. onBeforeUnmount(() => {
  98. if (editor) {
  99. // 组件卸载前销毁编辑器实例
  100. editor.dispose();
  101. // 组件卸载时断开 MutationObserver
  102. if (observer) {
  103. observer.disconnect();
  104. }
  105. }
  106. });

五:完整代码

  1. <template>
  2. <div class="codemirror">
  3. <div id="monacoEditorDiff" ref="editorDiffRef" class="monaco-editor"></div>
  4. </div>
  5. </template>
  6. <script setup>
  7. import { ref, onMounted, onBeforeUnmount } from "vue";
  8. import debounce from "lodash/debounce";
  9. import * as monaco from "monaco-editor";
  10. import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
  11. // 定义从父组件接收的属性
  12. const props = defineProps({
  13. options: {
  14. type: Object,
  15. default: () => ({
  16. theme: "dark",
  17. }),
  18. },
  19. oldValue: {
  20. type: String,
  21. },
  22. newValue: {
  23. type: String,
  24. },
  25. //需要高亮的关键词
  26. highLightWords: {
  27. type: Array,
  28. default: () => [],
  29. },
  30. });
  31. const emits = defineEmits(["newSqlCode"]);
  32. //解决 Monaco Editor 无法正确加载其所需的 Web Worker
  33. self.MonacoEnvironment = {
  34. getWorker(workerId, label) {
  35. return new editorWorker();
  36. },
  37. };
  38. let editor;
  39. let originalModel;
  40. let modifiedModel;
  41. const init = () => {
  42. editor = monaco.editor.createDiffEditor(
  43. document.getElementById("monacoEditorDiff"),
  44. {
  45. value: "", //值
  46. autoIndex: true, // 控制是否开启自动索引。当开启时,编辑器会自动创建索引以加速搜索和语义高亮。
  47. fontSize: 14, // 字体大小
  48. language: "sql", //语言
  49. theme: props.options.theme, //主题
  50. readOnly: true, // 是否只读
  51. overviewRulerBorder: false, // 滚动是否有边框
  52. cursorSmoothCaretAnimation: true, // 控制光标平滑动画的开启与关闭。当开启时,光标移动会有平滑的动画效果。
  53. mouseWheelZoom: true, //设置是否开启鼠标滚轮缩放功能
  54. folding: true, //控制是否开启代码折叠功能
  55. automaticLayout: true, // 控制编辑器是否自动调整布局以适应容器大小的变化
  56. minimap: {
  57. // 关闭代码缩略图
  58. enabled: false, // 是否启用预览图
  59. },
  60. scrollbar: {
  61. verticalScrollbarSize: 2, // 垂直滚动条宽度,默认px
  62. horizontalScrollbarSize: 2, // 水平滚动条高度
  63. },
  64. wordWrap: "on", // 开启自动换行
  65. roundedSelection: true, // 右侧不显示编辑器预览框
  66. originalEditable: true, // 是否允许修改原始文本
  67. ...props.options,
  68. }
  69. );
  70. //创建左右编辑器
  71. originalModel = monaco.editor.createModel(props.newValue, "sql");
  72. modifiedModel = monaco.editor.createModel(props.oldValue, "sql");
  73. editor.setModel({
  74. original: originalModel,
  75. modified: modifiedModel,
  76. });
  77. // 原始模型的内容发生变化时触发的操作
  78. originalModel.onDidChangeContent(() => {
  79. //获取修改后的文本
  80. // const modifiedText = modifiedModel.getValue();
  81. //获取原始文本
  82. const OriginalText = originalModel.getValue();
  83. //内容发生变化 需要重新设置高亮
  84. setHighlight();
  85. emits("newSqlCode", OriginalText);
  86. });
  87. // 修改后模型的内容发生变化时触发的操作
  88. modifiedModel.onDidChangeContent(() => {});
  89. removeArrow();
  90. };
  91. /**
  92. * 移除 revert change(还原更改)箭头
  93. */
  94. const removeArrow = () => {
  95. setTimeout(() => {
  96. const revertChangeButtons = document.querySelectorAll(
  97. ".codicon-arrow-right"
  98. );
  99. revertChangeButtons.forEach((button) => {
  100. button.style.display = "none";
  101. });
  102. }, 500);
  103. };
  104. /**
  105. * 设置自定义高亮
  106. */
  107. let observer = null;
  108. let originalEditorContainer = null;
  109. //挂载高亮观察器
  110. const setLightObserver = () => {
  111. //有关键字的情况下 才需要高亮
  112. if (props.highLightWords.length > 0) {
  113. // 获取 Diff Editor 中原始编辑器的容器节点
  114. originalEditorContainer = editor.getOriginalEditor().getDomNode();
  115. //获取第一个子节点
  116. const originalSonElement =
  117. originalEditorContainer && originalEditorContainer.firstChild;
  118. //获取第一个子节点的第一个子节点
  119. const originalGrandsonElement =
  120. originalSonElement && originalSonElement.firstChild;
  121. //创建一个 MutationObserver 实例
  122. observer = new MutationObserver(() => {
  123. setHighlight();
  124. });
  125. // 配置观察器以监视特定类型的变化
  126. if (originalSonElement) {
  127. // 监视 originalSonElement 的样式变化,如果发生变化重新设置高亮(用于页面放大缩小时)
  128. observer.observe(originalSonElement, {
  129. attributes: true, // 监视属性的变化
  130. attributeFilter: ["style"], // 监视特定属性
  131. });
  132. // 监视 originalGrandsonElement 的样式变化,如果发生变化重新设置高亮(用于编辑器放大缩小滚动时)
  133. observer.observe(originalGrandsonElement, {
  134. attributes: true, // 监视属性的变化
  135. attributeFilter: ["style"], // 监视特定属性
  136. });
  137. }
  138. }
  139. };
  140. //使用防抖,避免多次渲染黄色背景
  141. const setHighlight = debounce(() => {
  142. const linesContainer = originalEditorContainer.querySelector(".view-lines");
  143. // 获取所有行的容器节点
  144. const divLines = linesContainer.childNodes;
  145. // 遍历每一行的容器节点
  146. divLines.forEach((lineNode) => {
  147. //拿到每一行的总span节点
  148. const spanNodes = lineNode.childNodes;
  149. //拿到每一行的总span节点内容
  150. let originalSpanHtml = spanNodes[0].innerHTML;
  151. // 存储匹配到的关键字
  152. let successKeyWord = [];
  153. // 检查 span 元素的文本内容是否包含了 props.highLightWords 数组中的一个或多个关键字
  154. props.highLightWords.forEach((highlightWord) => {
  155. //创建正则表达式进行全局、忽略大小写匹配
  156. const regex = new RegExp(highlightWord, "gi");
  157. //如果匹配到,则将匹配成功的关键字存储起来
  158. if (regex.test(originalSpanHtml)) {
  159. successKeyWord.push(highlightWord);
  160. }
  161. });
  162. // 对 successKeyWord 数组进行去重操作
  163. successKeyWord = Array.from(new Set(successKeyWord));
  164. // 如果有任意一个关键字包含在 span 元素的文本内容中,则修改对应关键字的样式,而不是修改整个span
  165. if (successKeyWord.length > 0) {
  166. // 创建一个新的字符串来保存修改后的结果
  167. let modifiedHtml = originalSpanHtml;
  168. // 遍历匹配成功关键字数组中的每个字符串
  169. successKeyWord.forEach((str) => {
  170. // 创建带有黄色背景色的 <span> 元素
  171. const highlightedText = `<span style="background-color: yellow;">${str}</span>`;
  172. // 使用正则表达式进行全局、忽略大小写的替换
  173. modifiedHtml = modifiedHtml.replace(
  174. new RegExp(str, "gi"),
  175. highlightedText
  176. );
  177. });
  178. // 将修改后的 HTML 内容设置回原来的 span 元素中
  179. spanNodes[0].innerHTML = modifiedHtml;
  180. }
  181. });
  182. }, 500);
  183. // 组件挂载后创建编辑器实例
  184. onMounted(() => {
  185. //初始化编辑器
  186. init();
  187. //挂载高亮观察器
  188. setLightObserver();
  189. });
  190. onBeforeUnmount(() => {
  191. if (editor) {
  192. // 组件卸载前销毁编辑器实例
  193. editor.dispose();
  194. // 组件卸载时断开 MutationObserver
  195. if (observer) {
  196. observer.disconnect();
  197. }
  198. }
  199. });
  200. </script>
  201. <style scoped lang="less">
  202. .monaco-editor {
  203. height: 150px;
  204. width: 100%;
  205. }
  206. </style>

效果图:

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号