UNPKG

@ribajs/monaco-editor

Version:

Monaco-editor module for Riba.js

283 lines (258 loc) 7.89 kB
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; } } }