前言
哇咔咔,这是我的第20篇Monaco教程,写完这一篇会暂时休息一段时间,练练字,存存稿,读读书,顺便修修文章。
目前全网成系统的monaco中文专栏应该只有我这一个,欢迎评论区打脸。自结束了GitLab CI/CD的专栏后,我就一直在利用业余时间学习Monaco相关的知识,一是为了弥补公司项目上的不足,二是看准了这东西以后肯定会大放异彩。不过目前它还是处于VS Code的光彩下。你说vscode大家都知道是啥,你说摩纳哥编辑器,大家就会问你,这是啥玩意。monaco editor是vscode的核心编辑器,vscode就是基于它开发出来的。
说实话,虽然写了20篇文章,但感觉还是只学到了一点皮毛,对于很多特性还没有时间去研究,去思考它们的使用场景,以及如何为项目赋能。 马上过年了,暂时休息一段时间,好好规划一下后面的内容。
在写这一篇文章之前其实有两个选题,一是介绍创建monaco的参数配置,从那100多个配置参数中找几十个常用参数 一一分析他们的作用和使用场景。另一个选题就是在编辑器中插入一下自定义的dom元素,包括实现一个评论区域,表单,弹层。前一个选题对新手很学习很有帮助,网上也有些零碎的文章介绍。后一个选题比较少。这也是我写这篇文章的原因。不为别的,只因很酷。
技术分析
前言写了500多字,在加300字就能当做高考作文了。
今天要解析的功能 是monaco中一个很重要的特性。就是在编辑器中加入一些自定义的dom元素。
如果你的公司使用gitlab 你可能在一个MR中见过这样的操作页面。
在一个合并请求中,会有一些人对某一行改动的代码添加注释,或者给出建议。当他点击行号左侧的对话框图标时,就会在该行的下方,出现一个评论区域,开发者可以在该区域填写表单,发表评论。
其实这一功能正是借助了monaco的内嵌dom元素来实现的。具体的插入dom元素的方法 至少有三种。
而本文主要讲解二种,IContentWidget
与 IOverlayWidget
。
下面看一下,monaco内置的组件。
鼠标左键菜单。
右上角的查询组件
自动完成的下拉选项
在编辑器中,点击鼠标左键出现的菜单项,是一种内置的dom元素,其类型是IContentWidget
。
IOverlayWidget
与 IContentWidget
有很多相同点,也有很大的区别点。下面我们就来揭开二者的面纱。使用他们看一让我们的WEB IDE 注入一些奇奇怪怪的东西。如果你想,你可以在输入一个tree单词时,在编辑器中间出现一棵动态的银杏树图片 ,或者你可以在输入一个dog单词后,显示一个拉布拉多犬图片,你甚至可以让它汪汪两声。所以说学会这一功能的使用,你可以在编辑器器中为所欲为。 哈哈。。。
核心方法
IOverlayWidget
我们先来看一下IOverlayWidget
这个接口,该接口的定义位置在这里
描述是 An overlay widgets renders on top of the text.
一个渲染在文本之上的浮窗部件。
(下文统一使用 OverlayWidget来表述)
正如字面意思,它的内容会渲染在编辑器文本之上,例如查找组件就是使用它来完成的。它的内容会遮盖编辑器中的文本。
解释上很好理解,我们看下它的属性有哪些。以及可使用的方法有那些。
OverlayWidget
只有三个方法,没有再多的属性,这相比上一篇的CompletionItem类型15个属性,真是让我轻松了不少。
- getDomNode 用于设置该组件要显示的dom内容,需要返回HTMLElement
- getId 一个独一无二的组件id,如
monaco.fizz.overlaywidget
- getPosition 组件的显示位置,返回参数的类型为
IOverlayWidgetPosition
,内部是一个枚举值,三个待选项,右下角,顶部中心,右上角三个位置可以选。如果返回null,则组件会自己放置自己。
以上是OverlayWidget
的定义,我们可以根据定义创建一个自己的部件,如下
const fizzOverlayWidget = {
getDomNode() {
const domNode = document.createElement('div')
domNode.innerHTML = `<p class="import-info">无所谓,我会出手 ${new Date()}</p>`
domNode.style.background = '#ffc107';
domNode.style.borderRadius = '4px';
domNode.style.right = '200px';
domNode.style.width = '130px';
domNode.style.padding = '0 8px';
domNode.style.top = '50px';
domNode.style.color = '#ff5722'
return domNode
},
getId() {
return 'monaco.fizz.overlaywidget'
},
getPosition() {
return {
// preference: monaco.editor.OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER
// preference: monaco.editor.OverlayWidgetPositionPreference.TOP_CENTER
preference: monaco.editor.OverlayWidgetPositionPreference.TOP_RIGHT_CORNER
}
// return null
}
}
以上就是OverlayWidget的定义,只需要定义三个参数即可。而且还有一个 getPosition 参数可以为空。
下面我们将自定义的OverlayWidget添加到编辑器,看看是什么效果
有关OverlayWidget的方法有这些。这些方法都在editor下。
removeOverlayWidget(widget: IOverlayWidget): void
移除一个OverlayWidgetlayoutOverlayWidget(widget: IOverlayWidget): void
重新渲染一个OverlayWidget,可以使用它来改变一个OverlayWidget的位置addOverlayWidget(widget: IOverlayWidget): void
添加一个OverlayWidget,组件必须有一个独一无二的id,否则将会变为更新,覆盖现有组件内容。
ok,现在让我们一个一个体验三个方法。
首先是
editor.addOverlayWidget(fizzOverlayWidget)
渲染到右上角
渲染到顶部中心
组件的样式表现
我们通过修改组件显示的位置,来查看不同的表现。
由多次尝试得知,当组件显示的位置为null时,我们可以设置组件的top,left,bottom,right,来自定义组件相对于编辑器的位置。其实使用OverlayWidgetPositionPreference,本质也是相对定位,设置right,bottom。果真是实践出真知。
看完addOverlayWidget
方法,在来看一下removeOverlayWidget方法,该方法需要传入一个完整的OverlayWidget。不过既然每个OverlayWidget都有一个id,是不是只设置id就可以了。我们试试。
移除组件时我们直接这样写,只定义getId。
editor.removeOverlayWidget({
getId() {
return 'monaco.fizz.overlaywidget'
},
})
实验之后,果然可以,NBNB。
看完了添加和移除还有一个layoutOverlayWidget方法。
试了一下。
我们首先在编辑器addOverlayWidget一个顶部中间的组件,然后修改该组件getPosition使其指向,右上角,
然后再调用layoutOverlayWidget方法,我们发现,组件的位置确实变了。
editor.layoutOverlayWidget(fizzOverlayWidget)
所以讲,layoutOverlayWidget是可以更新同一id的组件的位置,不能更新内容,不可添加新的组件。
addOverlayWidget可以添加多个同id,不同或相同的组件,可以同时存在。(不知道是不是bug)
IContentWidget
IContentWidget
的类型解释在此处可以查看到。
它的解释是A content widget renders inline with the text and can be easily placed 'near' an editor position.
会渲染到编辑器的行内内容组件,并且很容易将其防止在某个固定位置附近。
根据描述我们就知道,相比layoutOverlayWidget只能从三个渲染位置选择,IContentWidget可以渲染到某个固定的位置附近,那么鼠标左键的菜单,自动完成的候选项,都是可以使用该组件完成的。
为了方便描述,下文统一使用ContentWidget
ContentWidget类型有二个属性,五个方法
属性:
- allowEditorOverflow 是否允许组件溢出编辑器
- suppressMouseDown 是否支持鼠标按下操作,这是为了不让事件穿透。让用户可以点击组件的dom元素。
方法:
- afterRender 在渲染组件后,设置组件的显示位置,设置一个枚举值,可以在position的下方,上方,附近显示。
- beforeRender 设置ContentWidget组件的宽和高,将会被ContentWidget引用。
- getDomNode 与上面的layoutOverlayWidget组件一样,都是用户设置组件的dom元素,返回HTMLElement
- getId 组件唯一的id
- getPosition 组件显示的位置,类型为IContentWidgetPosition(有四个属性position,positionAffinity,preference,range)。
getPosition 中的四个属性:
position(类型IPosition), 组件被放置的坐标
positionAffinity(类型PositionAffinity), 可选参数
preference(类型为ContentWidgetPositionPreference[],可以设置多个枚举值,从三个选项),将组件放置在position的上方还是上方,或者精确的位置。
range,(类型IRange)可选参数,组件更为精确的影响范围。
由定义我们创建一个ContentWidget
const fizzContentWidget = {
allowEditorOverflow: true,
suppressMouseDown: false,
afterRender(position) {
console.log(position)
},
beforeRender() {
const dimension = {
height: 200,
width: 200,
}
return dimension
},
getDomNode() {
const domNode = document.createElement('div')
domNode.innerHTML = `<p class="import-info">无所谓,我会出手</p>`
domNode.style.background = '#ffc107';
domNode.style.borderRadius = '4px';
domNode.style.width = '130px';
domNode.style.padding = '0 8px';
domNode.style.color = '#ff5722'
return domNode
},
getId() {
return 'monaco.fizz.contentwidth'
},
getPosition() {
const contentWidgetPosition = {
position: {
column: 0,
lineNumber: 2
},
preference: [
monaco.editor.ContentWidgetPositionPreference.ABOVE,
monaco.editor.ContentWidgetPositionPreference.BELOW,
],
}
return contentWidgetPosition
},
}
我们设置 position
为 {column: 0,lineNumber: 2}
,该组件将会在地2行,第0列渲染。
调用editor.addContentWidget(fizzContentWidget)
将会被渲染到页面上。
效果图如下:
我们看到该组件的内容同样是渲染在文本上层的,并没有渲染在编辑器中,也没有撑开第2行与第3行。
那么在编辑器中添加评论表单,而不用遮挡底部的文本内容,到底应该怎么显示那?
其实这涉及到了另一个API,changeViewZones
。这里不展开讲这个API,后续会单独写一篇文章,详细讲解它的使用,与应用场景。
这里我们编写这样一个我们看到该组件的内容同样是渲染在文本上层的,并没有渲染在编辑器中,也没有撑开第2行与第3行。
那么在编辑器中添加评论表单,而不用遮挡底部的文本内容,到底应该怎么显示那?
其实这涉及到了另一个API,changeViewZones。由于该API过于强大,这里不展开讲这个API,后续会单独写一篇文章,详细讲解它的使用与应用场景。
这里我们编写这样一个函数
function addZone(){
var viewZoneId = null;
editor.changeViewZones(function (changeAccessor) {
var domNode = document.createElement('div');
domNode.style.background = 'lightgreen';
viewZoneId = changeAccessor.addZone({
afterLineNumber: 2,
heightInLines: 3,
domNode: domNode
});
});
}
重点是 afterLineNumber: 2, heightInLines: 3,
他表示,在编辑器中创建一个区域,该区域是在第2行,开始渲染,一共占据3行。这一区域我们可以设置一个dom元素,也可以使用一个ContentWidget来覆盖在上层。
下面让我来看一下这个案例,完整的效果
效果图
完整代码
<!DOCTYPE html>
<html>
<head>
<title>Hello World Monaco Editor(CSDN@拿我格子衫来)</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
</head>
<body>
<h2>Hello World Monaco Editor(CSDN@拿我格子衫来)</h2>
<button onclick="addOverlay()">添加IOverlayWidget</button>
<button onclick="layoutOverlay()">layout IOverlayWidget</button>
<button onclick="delOverlayWidget()">移除IOverlayWidget</button>
<button onclick="addContent()">添加IContentWidget</button>
<button onclick="layoutContent()">layout IContentWidget</button>
<button onclick="delContent()">移除IContentWidget</button>
<button onclick="addZone()">添加留白区域</button>
<div id="container" style="width: 800px; height: 600px; border: 1px solid grey"></div>
<script src="./monaco-editor/package/min/vs/loader.js"></script>
<script src="./const.js"></script>
<script>
require.config({ paths: { vs: './monaco-editor/package/min/vs' } });
let editor;
require(['vs/editor/editor.main'], function () {
editor = monaco.editor.create(document.getElementById('container'), {
value: `
function mo(){
console.log(12)
}
function fi(){
console.log(12)
}
function mo(){
console.log(12)
}
function fi(){
console.log(12)
}
function mo(){
console.log(12)
}
function fi(){
console.log(12)
}
function mo(){
console.log(12)
}
function fi(){
console.log(12)
}
function mo(){
console.log(12)
}
function fi(){
console.log(12)
}
function mo(){
console.log(12)
}
function fi(){
console.log(12)
}
function mo(){
console.log(12)
}
function fi(){
console.log(12)
}
function fi(){
console.log(12)
}
function mo(){
console.log(12)
}
function fi(){
console.log(12)
}
`,
language: 'javascript'
});
});
function addOverlay() {
editor.addOverlayWidget(createOverlayWidget())
}
function layoutOverlay() {
const lay = createOverlayWidget()
lay.getPosition = () => {
return { preference: monaco.editor.OverlayWidgetPositionPreference.TOP_RIGHT_CORNER }
}
lay.getDomNode = () => {
const domNode = document.createElement('div')
domNode.innerHTML = `<p class="import-info">无所谓,我会出手${new Date()}</p>`
return domNode
}
editor.layoutOverlayWidget(lay)
// editor.addOverlayWidget(lay)
}
function createOverlayWidget() {
const fizzOverlayWidget = {
getDomNode() {
const domNode = document.createElement('div')
domNode.innerHTML = `<p class="import-info">无所谓,我会出手</p>`
domNode.style.background = '#ffc107';
domNode.style.borderRadius = '4px';
domNode.style.right = '200px';
domNode.style.width = '130px';
domNode.style.padding = '0 8px';
domNode.style.top = '50px';
domNode.style.color = '#ff5722'
return domNode
},
getId() {
return 'monaco.fizz.overlaywidget'
},
getPosition() {
// return {
// // preference: monaco.editor.OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER
// preference: monaco.editor.OverlayWidgetPositionPreference.TOP_CENTER
// // preference: monaco.editor.OverlayWidgetPositionPreference.TOP_RIGHT_CORNER
// }
return null
}
}
return fizzOverlayWidget
}
function delOverlayWidget() {
editor.removeOverlayWidget({
getId() {
return 'monaco.fizz.overlaywidget'
},
})
}
const fizzContentWidget = {
allowEditorOverflow: true,
suppressMouseDown: false,
afterRender(position) {
console.log(position)
},
beforeRender() {
const dimension = {
height: 200,
width: 200,
}
return dimension
},
getDomNode() {
const domNode = document.createElement('div')
domNode.innerHTML = `<p class="import-info">无所谓,我会出手</p>`
domNode.style.background = '#ffc107';
domNode.style.borderRadius = '4px';
domNode.style.width = '130px';
domNode.style.padding = '0 8px';
domNode.style.color = '#ff5722'
return domNode
},
getId() {
return 'monaco.fizz.contentwidth'
},
getPosition() {
const contentWidgetPosition = {
position: {
column: 0,
lineNumber: 2
},
preference: [
monaco.editor.ContentWidgetPositionPreference.ABOVE,
monaco.editor.ContentWidgetPositionPreference.BELOW,
],
}
return contentWidgetPosition
},
}
function addContent() {
editor.addContentWidget(fizzContentWidget)
}
function layoutContent() {
fizzContentWidget.getPosition = () => {
const contentWidgetPosition = {
position: {
column: 0,
lineNumber: 4
},
preference: [
monaco.editor.ContentWidgetPositionPreference.ABOVE,
monaco.editor.ContentWidgetPositionPreference.BELOW,
],
}
return contentWidgetPosition
}
editor.layoutContentWidget(fizzContentWidget)
}
function delContent() {
editor.removeContentWidget({
getId() {
return 'monaco.fizz.contentwidth'
},
})
}
function addZone() {
var viewZoneId = null;
editor.changeViewZones(function (changeAccessor) {
var domNode = document.createElement('div');
domNode.style.background = 'lightgreen';
viewZoneId = changeAccessor.addZone({
afterLineNumber: 2,
heightInLines: 3,
domNode: domNode
});
});
}
</script>
</body>
</html>
后记
monaco的东西还有很多,让我们慢慢到来。