UNPKG

@ngstack/code-editor

Version:

Code editor component for Angular applications.

633 lines (623 loc) 24.7 kB
import * as i0 from '@angular/core'; import { InjectionToken, Injectable, Optional, Inject, EventEmitter, inject, Component, ChangeDetectionStrategy, ViewEncapsulation, ViewChild, Input, Output, HostListener, APP_INITIALIZER, NgModule } from '@angular/core'; import { Subject, BehaviorSubject } from 'rxjs'; import { CommonModule } from '@angular/common'; const EDITOR_SETTINGS = new InjectionToken('EDITOR_SETTINGS'); class CodeEditorService { /** * Returns the global `monaco` instance */ get monaco() { return this._monaco; } constructor(settings) { this.typingsLoaded = new Subject(); this.loaded = new BehaviorSubject({ monaco: null }); this.loadingTypings = new BehaviorSubject(false); const editorVersion = settings?.editorVersion || 'latest'; this.baseUrl = settings?.baseUrl || `https://cdn.jsdelivr.net/npm/monaco-editor@${editorVersion}/min`; this.typingsWorkerUrl = settings?.typingsWorkerUrl || ``; } loadTypingsWorker() { if (!this.typingsWorker && window.Worker) { if (this.typingsWorkerUrl.startsWith('http')) { const proxyScript = `importScripts('${this.typingsWorkerUrl}');`; const proxy = URL.createObjectURL(new Blob([proxyScript], { type: 'text/javascript' })); this.typingsWorker = new Worker(proxy); } else { this.typingsWorker = new Worker(this.typingsWorkerUrl); } this.typingsWorker.addEventListener('message', (e) => { this.loadingTypings.next(false); this.typingsLoaded.next(e.data); }); } return this.typingsWorker; } loadTypings(dependencies) { if (dependencies && dependencies.length > 0) { const worker = this.loadTypingsWorker(); if (worker) { this.loadingTypings.next(true); worker.postMessage({ dependencies }); } } } loadEditor() { return new Promise((resolve) => { const onGotAmdLoader = () => { window.require.config({ paths: { vs: `${this.baseUrl}/vs` } }); if (this.baseUrl.startsWith('http')) { const proxyScript = ` self.MonacoEnvironment = { baseUrl: "${this.baseUrl}" }; importScripts('${this.baseUrl}/vs/base/worker/workerMain.js'); `; const proxy = URL.createObjectURL(new Blob([proxyScript], { type: 'text/javascript' })); window['MonacoEnvironment'] = { getWorkerUrl: function () { return proxy; } }; } window.require(['vs/editor/editor.main'], () => { this._monaco = window['monaco']; this.loaded.next({ monaco: this._monaco }); resolve(); }); }; if (!window.require) { const loaderScript = document.createElement('script'); loaderScript.type = 'text/javascript'; loaderScript.src = `${this.baseUrl}/vs/loader.js`; loaderScript.addEventListener('load', onGotAmdLoader); document.body.appendChild(loaderScript); } else { onGotAmdLoader(); } }); } /** * Switches to a theme. * @param themeName name of the theme */ setTheme(themeName) { this.monaco.editor.setTheme(themeName); } createEditor(containerElement, options) { return this.monaco.editor.create(containerElement, options); } createModel(value, language, uri) { return this.monaco.editor.createModel(value, language, this.monaco.Uri.file(uri)); } setModelLanguage(model, mimeTypeOrLanguageId) { if (this.monaco && model) { this.monaco.editor.setModelLanguage(model, mimeTypeOrLanguageId); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.10", ngImport: i0, type: CodeEditorService, deps: [{ token: EDITOR_SETTINGS, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.10", ngImport: i0, type: CodeEditorService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.10", ngImport: i0, type: CodeEditorService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [EDITOR_SETTINGS] }] }] }); class TypescriptDefaultsService { constructor(codeEditorService) { codeEditorService.loaded.subscribe(event => { this.setup(event.monaco); }); codeEditorService.typingsLoaded.subscribe(typings => { this.updateTypings(typings); }); } setup(monaco) { if (!monaco) { return; } this.monaco = monaco; const defaults = monaco.languages.typescript.typescriptDefaults; defaults.setCompilerOptions({ target: monaco.languages.typescript.ScriptTarget.ES6, module: 'commonjs', noEmit: true, noLib: true, emitDecoratorMetadata: true, experimentalDecorators: true, allowNonTsExtensions: true, declaration: true, lib: ['es2017', 'dom'], baseUrl: '.', paths: {} }); defaults.setMaximumWorkerIdleTime(-1); defaults.setEagerModelSync(true); /* defaults.setDiagnosticsOptions({ noSemanticValidation: true, noSyntaxValidation: true }); */ } updateTypings(typings) { if (typings) { this.addExtraLibs(typings.files); this.addLibraryPaths(typings.entryPoints); } } addExtraLibs(libs = []) { if (!this.monaco || !libs || libs.length === 0) { return; } const defaults = this.monaco.languages.typescript.typescriptDefaults; // undocumented API const registeredLibs = defaults.getExtraLibs(); libs.forEach(lib => { if (!registeredLibs[lib.path]) { // needs performance improvements, recreates its worker each time // defaults.addExtraLib(lib.content, lib.path); // undocumented API defaults._extraLibs[lib.path] = lib.content; } }); // undocumented API defaults._onDidChange.fire(defaults); } addLibraryPaths(paths = {}) { if (!this.monaco) { return; } const defaults = this.monaco.languages.typescript.typescriptDefaults; const compilerOptions = defaults.getCompilerOptions(); compilerOptions.paths = compilerOptions.paths || {}; Object.keys(paths).forEach(key => { compilerOptions.paths[key] = [paths[key]]; }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.10", ngImport: i0, type: TypescriptDefaultsService, deps: [{ token: CodeEditorService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.10", ngImport: i0, type: TypescriptDefaultsService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.10", ngImport: i0, type: TypescriptDefaultsService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: CodeEditorService }] }); class JavascriptDefaultsService { constructor(codeEditorService) { codeEditorService.loaded.subscribe(event => { this.setup(event.monaco); }); codeEditorService.typingsLoaded.subscribe(typings => { this.updateTypings(typings); }); } setup(monaco) { if (!monaco) { return; } this.monaco = monaco; const defaults = monaco.languages.typescript.javascriptDefaults; defaults.setCompilerOptions({ target: monaco.languages.typescript.ScriptTarget.ES6, module: 'commonjs', allowNonTsExtensions: true, baseUrl: '.', paths: {} }); defaults.setMaximumWorkerIdleTime(-1); defaults.setEagerModelSync(true); /* defaults.setDiagnosticsOptions({ noSemanticValidation: false, noSyntaxValidation: false }); */ } updateTypings(typings) { if (typings) { this.addExtraLibs(typings.files); this.addLibraryPaths(typings.entryPoints); } } addExtraLibs(libs = []) { if (!this.monaco || !libs || libs.length === 0) { return; } const defaults = this.monaco.languages.typescript.javascriptDefaults; // undocumented API const registeredLibs = defaults.getExtraLibs(); libs.forEach(lib => { if (!registeredLibs[lib.path]) { // needs performance improvements, recreates its worker each time // defaults.addExtraLib(lib.content, lib.path); // undocumented API defaults._extraLibs[lib.path] = lib.content; } }); // undocumented API defaults._onDidChange.fire(defaults); } addLibraryPaths(paths = {}) { if (!this.monaco) { return; } const defaults = this.monaco.languages.typescript.javascriptDefaults; const compilerOptions = defaults.getCompilerOptions(); compilerOptions.paths = compilerOptions.paths || {}; Object.keys(paths).forEach(key => { compilerOptions.paths[key] = [paths[key]]; }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.10", ngImport: i0, type: JavascriptDefaultsService, deps: [{ token: CodeEditorService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.10", ngImport: i0, type: JavascriptDefaultsService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.10", ngImport: i0, type: JavascriptDefaultsService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: CodeEditorService }] }); class JsonDefaultsService { constructor(codeEditorService) { codeEditorService.loaded.subscribe(event => { this.setup(event.monaco); }); } setup(monaco) { if (!monaco) { return; } this.monaco = monaco; const defaults = monaco.languages.json.jsonDefaults; defaults.setDiagnosticsOptions({ validate: true, allowComments: true, schemas: [ ...defaults._diagnosticsOptions.schemas, { uri: 'http://myserver/foo-schema.json', // fileMatch: [id], // fileMatch: ['*.json'], schema: { type: 'object', properties: { p1: { enum: ['v1', 'v2'] }, p2: { $ref: 'http://myserver/bar-schema.json' } } } }, { uri: 'http://myserver/bar-schema.json', // fileMatch: [id], // fileMatch: ['*.json'], schema: { type: 'object', properties: { q1: { enum: ['x1', 'x2'] } } } } ] }); } addSchemas(id, definitions = []) { const defaults = this.monaco.languages.json.jsonDefaults; const options = defaults.diagnosticsOptions; const schemas = {}; if (options && options.schemas && options.schemas.length > 0) { options.schemas.forEach(schema => { schemas[schema.uri] = schema; }); } for (const { uri, schema } of definitions) { schemas[uri] = { uri, schema, fileMatch: [id || '*.json'] }; } // console.log(schemas); // console.log(Object.values(schemas)); options.schemas = Object.values(schemas); defaults.setDiagnosticsOptions(options); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.10", ngImport: i0, type: JsonDefaultsService, deps: [{ token: CodeEditorService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.10", ngImport: i0, type: JsonDefaultsService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.10", ngImport: i0, type: JsonDefaultsService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: CodeEditorService }] }); class CodeEditorComponent { constructor() { // private _value = ''; this.defaultOptions = { lineNumbers: 'on', contextmenu: false, minimap: { enabled: false } }; // @Input() // set value(v: string) { // if (v !== this._value) { // this._value = v; // this.setEditorValue(v); // this.valueChanged.emit(v); // } // } // get value(): string { // return this._value; // } /** * Editor theme. Defaults to `vs`. * * Allowed values: `vs`, `vs-dark` or `hc-black`. * @memberof CodeEditorComponent */ this.theme = 'vs'; /** * Editor options. * * See https://microsoft.github.io/monaco-editor/docs.html#interfaces/editor.IStandaloneEditorConstructionOptions.html for more details. * * @memberof CodeEditorComponent */ this.options = {}; /** * Toggle readonly state of the editor. * * @memberof CodeEditorComponent */ this.readOnly = false; /** * An event emitted when the text content of the model have changed. */ this.valueChanged = new EventEmitter(); /** * An event emitted when the code model value is changed. */ this.codeModelChanged = new EventEmitter(); /** * An event emitted when the contents of the underlying editor model have changed. */ this.modelContentChanged = new EventEmitter(); /** * Raised when editor finished loading all its components. */ this.loaded = new EventEmitter(); this.editorService = inject(CodeEditorService); this.typescriptDefaults = inject(TypescriptDefaultsService); this.javascriptDefaults = inject(JavascriptDefaultsService); this.jsonDefaults = inject(JsonDefaultsService); } /** * The instance of the editor. */ get editor() { return this._editor; } set editor(value) { this._editor = value; } ngOnDestroy() { if (this.editor) { this.editor.dispose(); this.editor = null; } if (this._model) { this._model.dispose(); this._model = null; } } ngOnChanges(changes) { const codeModel = changes['codeModel']; const readOnly = changes['readOnly']; const theme = changes['theme']; if (codeModel && !codeModel.firstChange) { this.updateModel(codeModel.currentValue); } if (readOnly && !readOnly.firstChange) { if (this.editor) { this.editor.updateOptions({ readOnly: readOnly.currentValue }); } } if (theme && !theme.firstChange) { this.editorService.setTheme(theme.currentValue); } } onResize() { if (this.editor) { this.editor.layout(); } } async ngAfterViewInit() { this.setupEditor(); this.loaded.emit(this); } setupEditor() { const domElement = this.editorContent.nativeElement; const settings = { value: '', language: 'text', uri: `code-${Date.now()}`, ...this.codeModel }; this._model = this.editorService.createModel(settings.value, settings.language, settings.uri); const options = Object.assign({}, this.defaultOptions, this.options, { readOnly: this.readOnly, theme: this.theme, model: this._model }); this.editor = this.editorService.createEditor(domElement, options); this._model.onDidChangeContent((e) => { this.modelContentChanged.emit(e); const newValue = this._model.getValue(); if (this.codeModel) { this.codeModel.value = newValue; } this.valueChanged.emit(newValue); }); this.setupDependencies(this.codeModel); this.codeModelChanged.emit({ sender: this, value: this.codeModel }); } runEditorAction(id, args) { this.editor.getAction(id)?.run(args); } formatDocument() { this.runEditorAction('editor.action.formatDocument'); } setupDependencies(model) { if (!model) { return; } const { language } = model; if (language) { const lang = language.toLowerCase(); switch (lang) { case 'typescript': if (model.dependencies) { this.editorService.loadTypings(model.dependencies); } break; case 'javascript': if (model.dependencies) { this.editorService.loadTypings(model.dependencies); } break; case 'json': if (model.schemas) { this.jsonDefaults.addSchemas(model.uri, model.schemas); } break; default: break; } } } setEditorValue(value) { // Fix for value change while dispose in process. setTimeout(() => { if (this._model) { this._model.setValue(value); } }); } updateModel(model) { if (model) { this.setEditorValue(model.value); this.editorService.setModelLanguage(this._model, model.language); this.setupDependencies(model); this.codeModelChanged.emit({ sender: this, value: model }); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.10", ngImport: i0, type: CodeEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.10", type: CodeEditorComponent, isStandalone: true, selector: "ngs-code-editor", inputs: { codeModel: "codeModel", theme: "theme", options: "options", readOnly: "readOnly" }, outputs: { valueChanged: "valueChanged", codeModelChanged: "codeModelChanged", modelContentChanged: "modelContentChanged", loaded: "loaded" }, host: { listeners: { "window:resize": "onResize()" }, classAttribute: "ngs-code-editor" }, viewQueries: [{ propertyName: "editorContent", first: true, predicate: ["editor"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: "<div id=\"editor\" #editor class=\"monaco-editor editor\"></div>\n", styles: [".editor{width:100%;height:inherit;min-height:200px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.10", ngImport: i0, type: CodeEditorComponent, decorators: [{ type: Component, args: [{ selector: 'ngs-code-editor', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: { class: 'ngs-code-editor' }, template: "<div id=\"editor\" #editor class=\"monaco-editor editor\"></div>\n", styles: [".editor{width:100%;height:inherit;min-height:200px}\n"] }] }], propDecorators: { editorContent: [{ type: ViewChild, args: ['editor', { static: true }] }], codeModel: [{ type: Input }], theme: [{ type: Input }], options: [{ type: Input }], readOnly: [{ type: Input }], valueChanged: [{ type: Output }], codeModelChanged: [{ type: Output }], modelContentChanged: [{ type: Output }], loaded: [{ type: Output }], onResize: [{ type: HostListener, args: ['window:resize'] }] } }); function setupEditorService(service) { return () => service.loadEditor(); } function provideCodeEditor(settings) { return [ { provide: EDITOR_SETTINGS, useValue: settings }, CodeEditorService, TypescriptDefaultsService, JavascriptDefaultsService, JsonDefaultsService, { provide: APP_INITIALIZER, useFactory: setupEditorService, deps: [CodeEditorService], multi: true, }, ]; } /** @deprecated use `provideCodeEditor(settings)` instead */ class CodeEditorModule { static forRoot(settings) { return { ngModule: CodeEditorModule, providers: [ { provide: EDITOR_SETTINGS, useValue: settings }, CodeEditorService, { provide: APP_INITIALIZER, useFactory: setupEditorService, deps: [CodeEditorService], multi: true, }, ], }; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.10", ngImport: i0, type: CodeEditorModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.2.10", ngImport: i0, type: CodeEditorModule, imports: [CommonModule, CodeEditorComponent], exports: [CodeEditorComponent] }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.10", ngImport: i0, type: CodeEditorModule, imports: [CommonModule] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.10", ngImport: i0, type: CodeEditorModule, decorators: [{ type: NgModule, args: [{ imports: [CommonModule, CodeEditorComponent], exports: [CodeEditorComponent], }] }] }); /* * Public API Surface of code-editor */ /** * Generated bundle index. Do not edit. */ export { CodeEditorComponent, CodeEditorModule, CodeEditorService, EDITOR_SETTINGS, JavascriptDefaultsService, TypescriptDefaultsService, provideCodeEditor, setupEditorService }; //# sourceMappingURL=ngstack-code-editor.mjs.map