@ribajs/monaco-editor
Version:
Monaco-editor module for Riba.js
283 lines (258 loc) • 7.89 kB
text/typescript
import { Component, TemplateFunction, ScopeBase } from "@ribajs/core";
import MonacoLoader from "@monaco-editor/loader";
import { hasChildNodesTrim } from "@ribajs/utils/src/dom.js";
import pugTemplate from "./monaco-editor.component.pug";
import type { editor } from "monaco-editor";
interface Scope extends ScopeBase {
dataValue: string;
language?: string;
theme: "vs" | "vs-dark" | "hc-black";
readOnly: boolean;
automaticLayout: boolean;
minimapEnabled: boolean;
/**
* Autoformat document on set value
*/
formatDocument: boolean;
htmlFormatTabSize: number;
htmlFormatInsertSpaces: boolean;
htmlFormatIndentHandlebars: boolean;
/*
* Maximum amount of characters per line.
*/
htmlFormatWrapLineLength: number;
/**
* List of tags that shouldn't be reformatted.
*/
htmlFormatUnformatted: string;
/**
* List of tags, comma separated, where the content shouldn't be reformatted.
*/
htmlFormatContentUnformatted: string;
/**
* List of tags that should have an extra newline before them.
*/
htmlFormatExtraLiners: string;
/**
* Whether existing line breaks before elements should be preserved.
*/
htmlFormatPreserveNewLines: boolean;
/**
* Maximum number of line breaks to be preserved in one chunk.
*/
htmlFormatMaxPreserveNewLines: number;
/**
* End with a newline.
*/
htmlFormatEndWithNewline: boolean;
/**
* Indent <head> and <body> sections.
*/
htmlFormatIndentInnerHtml: boolean;
/**
* Wrapping strategy for attributes:
* * `auto`: Wrap when the line length is exceeded
* * `force`: Wrap all attributes, except first
* * `force-aligned`: Wrap all attributes, except first, and align attributes
* * `force-expand-multiline`: Wrap all attributes
*/
htmlFormatWrapAttributes:
| "auto"
| "force"
| "force-aligned"
| "force-expand-multiline";
}
export class MonacoEditorComponent extends Component {
public static tagName = "monaco-editor";
protected autobind = true;
public _debug = false;
protected editor?: editor.IStandaloneCodeEditor;
static get observedAttributes(): string[] {
return [
"data-value",
"language",
"theme",
"read-only",
"automatic-layout",
"minimap-enabled",
"format-document",
"html-format-tabSize",
"html-format-wrapLineLength",
"html-format-unformatted",
"html-format-contentUnformatted",
"html-format-extraLiners",
"html-format-preserveNewLines",
"html-format-maxPreserveNewLines",
"html-format-endWithNewline",
"html-format-indentInnerHtml",
"html-format-wrapAttributes",
];
}
public scope: Scope = {
dataValue: "{}",
/**
* monaco editor options
* for all options
* @see https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.ieditorconstructionoptions.html
*/
language: undefined,
theme: "vs-dark",
readOnly: true,
automaticLayout: false,
minimapEnabled: false,
formatDocument: false,
htmlFormatTabSize: 4,
htmlFormatInsertSpaces: true,
htmlFormatIndentHandlebars: true,
htmlFormatWrapLineLength: 0,
htmlFormatUnformatted: "inline",
htmlFormatContentUnformatted: "pre",
htmlFormatExtraLiners: "head,body,/html",
htmlFormatPreserveNewLines: true,
htmlFormatMaxPreserveNewLines: 20,
htmlFormatEndWithNewline: true,
htmlFormatIndentInnerHtml: true,
htmlFormatWrapAttributes: "force-aligned",
};
constructor() {
super();
}
protected connectedCallback() {
super.connectedCallback();
this.init(MonacoEditorComponent.observedAttributes);
}
protected async beforeBind() {
await super.beforeBind();
}
protected async afterBind() {
await this.createEditor(this.scope.dataValue);
await super.afterBind();
}
protected async createEditor(value: any) {
if (typeof value === "object") {
value = JSON.stringify(value, null, 4);
} else if (typeof value !== "string") {
value = String(value);
}
if (this.editor) {
return;
}
const monaco = await MonacoLoader.init();
monaco.editor.onDidCreateEditor((/*editor*/) => {
this.debug("onDidCreateEditor");
// FIXME remove timeout
setTimeout(() => {
this.formatDocument();
}, 2000);
});
monaco.languages.html.htmlDefaults.setOptions({
format: {
tabSize: this.scope.htmlFormatTabSize,
insertSpaces: this.scope.htmlFormatInsertSpaces,
indentHandlebars: this.scope.htmlFormatIndentHandlebars,
wrapLineLength: this.scope.htmlFormatWrapLineLength,
unformatted: this.scope.htmlFormatUnformatted,
contentUnformatted: this.scope.htmlFormatContentUnformatted,
extraLiners: this.scope.htmlFormatExtraLiners,
preserveNewLines: this.scope.htmlFormatPreserveNewLines,
maxPreserveNewLines: this.scope.htmlFormatMaxPreserveNewLines,
endWithNewline: this.scope.htmlFormatEndWithNewline,
indentInnerHtml: this.scope.htmlFormatIndentInnerHtml,
wrapAttributes: this.scope.htmlFormatWrapAttributes,
},
});
this.editor = monaco.editor.create(this, {
value: value as string,
language: this.scope.language,
theme: this.scope.theme,
readOnly: this.scope.readOnly,
automaticLayout: this.scope.automaticLayout,
formatOnType: true, // TODO
formatOnPaste: true, // TODO
minimap: {
enabled: this.scope.minimapEnabled,
},
});
this.editor?.onDidChangeModelContent((/*event*/) => {
if (this.editor) {
this.scope.dataValue = this.editor.getValue();
if (this.scope.dataValue) {
this.dispatchEvent(
new CustomEvent<string>("change", {
detail: this.scope.dataValue,
}),
);
}
// this.debug('onDidChangeContent', this.scope.dataValue);
}
});
}
protected formatDocument() {
if (this.editor && this.scope.formatDocument && this.scope.dataValue) {
// this.editor.trigger(this.scope.dataValue, 'editor.action.formatDocument', {});
this.debug("Format document");
this.editor
.getAction("editor.action.formatDocument")
?.run()
.then(() => {
this.debug("Document formatted");
})
.catch((error) => {
console.error(error);
});
}
}
protected setValue(value: any) {
if (typeof value === "object") {
value = JSON.stringify(value, null, 4);
} else if (typeof value !== "string") {
value = String(value);
}
this.debug("setValue");
if (this.editor) {
this.editor.setValue(value);
this.formatDocument();
}
}
protected requiredAttributes(): string[] {
return ["language", "dataValue"];
}
protected parsedAttributeChangedCallback(
attributeName: string,
oldValue: any,
newValue: any,
namespace: string | null,
) {
super.parsedAttributeChangedCallback(
attributeName,
oldValue,
newValue,
namespace,
);
this.debug("attributeName", attributeName);
if (attributeName === "dataValue") {
if (this.editor) {
// Make it disable that this value updates automatically
this.setValue(this.scope.dataValue);
}
}
}
// deconstruction
protected disconnectedCallback() {
if (this.editor) {
this.editor.dispose();
}
super.disconnectedCallback();
}
protected template(): ReturnType<TemplateFunction> {
let template: string | null = null;
// Only set the component template if there no childs already
if (this && hasChildNodesTrim(this)) {
return null;
} else {
template = pugTemplate(this.scope);
this.debug("Use template", template);
return template;
}
}
}