lit-and-monaco-editor
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseLit and Monaco Editor
Lit与Monaco Editor
In this article I will go over how to set up a Lit web component and use it to wrap the Monaco Editor that powers VSCode.
To learn how to build an extension with VSCode and Lit check out the blog post here.
Prerequisites
前置条件
- Vscode
- Node >= 16
- Typescript
- VSCode
- Node >= 16
- TypeScript
Getting Started
开始上手
We can start off by navigating in terminal to the location of the project and run the following:
npm init @vitejs/app --template lit-tsThen enter a project name and now open the project in vscode and install the dependencies:
lit-code-editorcd lit-code-editor
npm i lit monaco-editor
npm i -D @types/node
code .Update the with the following:
vite.config.tsimport { defineConfig } from "vite";
import { resolve } from "path";
export default defineConfig({
base: "/lit-code-editor/",
build: {
rollupOptions: {
input: {
main: resolve(__dirname, "index.html"),
},
},
},
});我们可以先在终端导航到项目所在位置,然后运行以下命令:
npm init @vitejs/app --template lit-ts然后输入项目名称,现在在VSCode中打开项目并安装依赖:
lit-code-editorcd lit-code-editor
npm i lit monaco-editor
npm i -D @types/node
code .更新文件内容如下:
vite.config.tsimport { defineConfig } from "vite";
import { resolve } from "path";
export default defineConfig({
base: "/lit-code-editor/",
build: {
rollupOptions: {
input: {
main: resolve(__dirname, "index.html"),
},
},
},
});Template
模板设置
Open up the and update it with the following:
index.html<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lit Code Editor</title>
<script type="module" src="/src/code-editor.ts"></script>
<style>
body {
margin: 0;
padding: 0;
width: 100%;
height: 100vh;
}
</style>
</head>
<body>
<code-editor>
<script type="text/javascript">
function x() {
console.log("Hello world! :)");
}
</script>
</code-editor>
</body>
</html>We are setting up the to have a slot which will be the code for the editor to start with. The language can be set with the type or adding an attribute to the component.
lit-elementcode-editor打开文件并更新内容如下:
index.html<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lit Code Editor</title>
<script type="module" src="/src/code-editor.ts"></script>
<style>
body {
margin: 0;
padding: 0;
width: 100%;
height: 100vh;
}
</style>
</head>
<body>
<code-editor>
<script type="text/javascript">
function x() {
console.log("Hello world! :)");
}
</script>
</code-editor>
</body>
</html>我们为设置了一个插槽,用于作为编辑器的初始代码。可以通过类型属性或为组件添加属性来设置语言。
lit-elementcode-editorWeb Component
Web组件开发
Before we update our component we need to rename to
my-element.tscode-editor.tsOpen up and update it with the following:
code-editor.tsimport { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { createRef, Ref, ref } from "lit/directives/ref.js";
// -- Monaco Editor Imports --
import * as monaco from "monaco-editor";
import styles from "monaco-editor/min/vs/editor/editor.main.css";
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
// @ts-ignore
self.MonacoEnvironment = {
getWorker(_: any, label: string) {
if (label === "json") {
return new jsonWorker();
}
if (label === "css" || label === "scss" || label === "less") {
return new cssWorker();
}
if (label === "html" || label === "handlebars" || label === "razor") {
return new htmlWorker();
}
if (label === "typescript" || label === "javascript") {
return new tsWorker();
}
return new editorWorker();
},
};
@customElement("code-editor")
export class CodeEditor extends LitElement {
private container: Ref<HTMLElement> = createRef();
editor?: monaco.editor.IStandaloneCodeEditor;
@property() theme?: string;
@property() language?: string;
@property() code?: string;
static styles = css`
:host {
--editor-width: 100%;
--editor-height: 100vh;
}
main {
width: var(--editor-width);
height: var(--editor-height);
}
`;
render() {
return html`
<style>
${styles}
</style>
<main ${ref(this.container)}></main>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"code-editor": CodeEditor;
}
}Here we are just setting up some boilerplate to set up the web workers with vite and passing the reference from the container element to the template using the ref directive.
The styles from monaco editor are also passed as a style element load in the shadow root.
Now let's add some helper methods for accessing the code and language provided:
private getFile() {
if (this.children.length > 0) return this.children[0];
return null;
}
private getCode() {
if (this.code) return this.code;
const file = this.getFile();
if (!file) return;
return file.innerHTML.trim();
}
private getLang() {
if (this.language) return this.language;
const file = this.getFile();
if (!file) return;
const type = file.getAttribute("type")!;
return type.split("/").pop()!;
}
private getTheme() {
if (this.theme) return this.theme;
if (this.isDark()) return "vs-dark";
return "vs-light";
}
private isDark() {
return (
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
);
}These methods are checking the slot for the script tag with the language provided or looking for a property set on and then returning the value.
code-editorNow let's attach the editor to the container reference:
firstUpdated() {
this.editor = monaco.editor.create(this.container.value!, {
value: this.getCode(),
language: this.getLang(),
theme: this.getTheme(),
automaticLayout: true,
});
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", () => {
monaco.editor.setTheme(this.getTheme());
});
}Now the editor should be running and able to be interacted with:
When the system changes to dark mode it will switch as well!
To get and set the value from the editor we can add 2 helper methods:
setValue(value: string) {
this.editor!.setValue(value);
}
getValue() {
const value = this.editor!.getValue();
return value;
}Everything should work as expected now and the final code should look like the following:
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { createRef, Ref, ref } from "lit/directives/ref.js";
// -- Monaco Editor Imports --
import * as monaco from "monaco-editor";
import styles from "monaco-editor/min/vs/editor/editor.main.css";
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
// @ts-ignore
self.MonacoEnvironment = {
getWorker(_: any, label: string) {
if (label === "json") {
return new jsonWorker();
}
if (label === "css" || label === "scss" || label === "less") {
return new cssWorker();
}
if (label === "html" || label === "handlebars" || label === "razor") {
return new htmlWorker();
}
if (label === "typescript" || label === "javascript") {
return new tsWorker();
}
return new editorWorker();
},
};
@customElement("code-editor")
export class CodeEditor extends LitElement {
private container: Ref<HTMLElement> = createRef();
editor?: monaco.editor.IStandaloneCodeEditor;
@property() theme?: string;
@property() language?: string;
@property() code?: string;
static styles = css`
:host {
--editor-width: 100%;
--editor-height: 100vh;
}
main {
width: var(--editor-width);
height: var(--editor-height);
}
`;
render() {
return html`
<style>
${styles}
</style>
<main ${ref(this.container)}></main>
`;
}
private getFile() {
if (this.children.length > 0) return this.children[0];
return null;
}
private getCode() {
if (this.code) return this.code;
const file = this.getFile();
if (!file) return;
return file.innerHTML.trim();
}
private getLang() {
if (this.language) return this.language;
const file = this.getFile();
if (!file) return;
const type = file.getAttribute("type")!;
return type.split("/").pop()!;
}
private getTheme() {
if (this.theme) return this.theme;
if (this.isDark()) return "vs-dark";
return "vs-light";
}
private isDark() {
return (
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
);
}
setValue(value: string) {
this.editor!.setValue(value);
}
getValue() {
const value = this.editor!.getValue();
return value;
}
firstUpdated() {
this.editor = monaco.editor.create(this.container.value!, {
value: this.getCode(),
language: this.getLang(),
theme: this.getTheme(),
automaticLayout: true,
});
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", () => {
monaco.editor.setTheme(this.getTheme());
});
}
}
declare global {
interface HTMLElementTagNameMap {
"code-editor": CodeEditor;
}
}在更新组件之前,我们需要将重命名为
my-element.tscode-editor.ts打开文件并更新内容如下:
code-editor.tsimport { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { createRef, Ref, ref } from "lit/directives/ref.js";
// -- Monaco Editor 导入 --
import * as monaco from "monaco-editor";
import styles from "monaco-editor/min/vs/editor/editor.main.css";
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
// @ts-ignore
self.MonacoEnvironment = {
getWorker(_: any, label: string) {
if (label === "json") {
return new jsonWorker();
}
if (label === "css" || label === "scss" || label === "less") {
return new cssWorker();
}
if (label === "html" || label === "handlebars" || label === "razor") {
return new htmlWorker();
}
if (label === "typescript" || label === "javascript") {
return new tsWorker();
}
return new editorWorker();
},
};
@customElement("code-editor")
export class CodeEditor extends LitElement {
private container: Ref<HTMLElement> = createRef();
editor?: monaco.editor.IStandaloneCodeEditor;
@property() theme?: string;
@property() language?: string;
@property() code?: string;
static styles = css`
:host {
--editor-width: 100%;
--editor-height: 100vh;
}
main {
width: var(--editor-width);
height: var(--editor-height);
}
`;
render() {
return html`
<style>
${styles}
</style>
<main ${ref(this.container)}></main>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"code-editor": CodeEditor;
}
}这里我们只是设置了一些基础代码,用于通过vite配置Web Worker,并使用ref指令将容器元素的引用传递给模板。
Monaco Editor的样式也会作为style元素加载到shadow root中。
现在添加一些辅助方法来获取提供的代码和语言:
private getFile() {
if (this.children.length > 0) return this.children[0];
return null;
}
private getCode() {
if (this.code) return this.code;
const file = this.getFile();
if (!file) return;
return file.innerHTML.trim();
}
private getLang() {
if (this.language) return this.language;
const file = this.getFile();
if (!file) return;
const type = file.getAttribute("type")!;
return type.split("/").pop()!;
}
private getTheme() {
if (this.theme) return this.theme;
if (this.isDark()) return "vs-dark";
return "vs-light";
}
private isDark() {
return (
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
);
}这些方法会检查插槽中带有指定语言的script标签,或者查找组件上设置的属性,然后返回对应的值。
code-editor现在将编辑器附加到容器引用上:
firstUpdated() {
this.editor = monaco.editor.create(this.container.value!, {
value: this.getCode(),
language: this.getLang(),
theme: this.getTheme(),
automaticLayout: true,
});
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", () => {
monaco.editor.setTheme(this.getTheme());
});
}现在编辑器应该可以运行并交互了:
当系统切换到深色模式时,编辑器也会随之切换!
为了获取和设置编辑器的值,我们可以添加两个辅助方法:
setValue(value: string) {
this.editor!.setValue(value);
}
getValue() {
const value = this.editor!.getValue();
return value;
}现在所有功能都应该正常工作了,最终代码如下:
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { createRef, Ref, ref } from "lit/directives/ref.js";
// -- Monaco Editor 导入 --
import * as monaco from "monaco-editor";
import styles from "monaco-editor/min/vs/editor/editor.main.css";
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
// @ts-ignore
self.MonacoEnvironment = {
getWorker(_: any, label: string) {
if (label === "json") {
return new jsonWorker();
}
if (label === "css" || label === "scss" || label === "less") {
return new cssWorker();
}
if (label === "html" || label === "handlebars" || label === "razor") {
return new htmlWorker();
}
if (label === "typescript" || label === "javascript") {
return new tsWorker();
}
return new editorWorker();
},
};
@customElement("code-editor")
export class CodeEditor extends LitElement {
private container: Ref<HTMLElement> = createRef();
editor?: monaco.editor.IStandaloneCodeEditor;
@property() theme?: string;
@property() language?: string;
@property() code?: string;
static styles = css`
:host {
--editor-width: 100%;
--editor-height: 100vh;
}
main {
width: var(--editor-width);
height: var(--editor-height);
}
`;
render() {
return html`
<style>
${styles}
</style>
<main ${ref(this.container)}></main>
`;
}
private getFile() {
if (this.children.length > 0) return this.children[0];
return null;
}
private getCode() {
if (this.code) return this.code;
const file = this.getFile();
if (!file) return;
return file.innerHTML.trim();
}
private getLang() {
if (this.language) return this.language;
const file = this.getFile();
if (!file) return;
const type = file.getAttribute("type")!;
return type.split("/").pop()!;
}
private getTheme() {
if (this.theme) return this.theme;
if (this.isDark()) return "vs-dark";
return "vs-light";
}
private isDark() {
return (
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
);
}
setValue(value: string) {
this.editor!.setValue(value);
}
getValue() {
const value = this.editor!.getValue();
return value;
}
firstUpdated() {
this.editor = monaco.editor.create(this.container.value!, {
value: this.getCode(),
language: this.getLang(),
theme: this.getTheme(),
automaticLayout: true,
});
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", () => {
monaco.editor.setTheme(this.getTheme());
});
}
}
declare global {
interface HTMLElementTagNameMap {
"code-editor": CodeEditor;
}
}Usage
使用方法
To use this component it can have the code provided by slots:
<code-editor>
<script type="text/javascript">
function x() {
console.log("Hello world! :)");
}
</script>
</code-editor>Or for properties:
<code-editor
code="console.log('Hello World');"
language="javascript"
>
</code-editor>Or both:
<code-editor language="typescript">
<script>
function x() {
console.log("Hello world! :)");
}
</script>
</code-editor>The theme can also be manually set:
<code-editor theme="vs-light"> </code-editor>使用该组件时,可以通过插槽提供代码:
<code-editor>
<script type="text/javascript">
function x() {
console.log("Hello world! :)");
}
</script>
</code-editor>也可以通过属性传递:
<code-editor
code="console.log('Hello World');"
language="javascript"
>
</code-editor>或者两者结合:
<code-editor language="typescript">
<script>
function x() {
console.log("Hello world! :)");
}
</script>
</code-editor>也可以手动设置主题:
<code-editor theme="vs-light"> </code-editor>