@ngstack/code-editor
Version:
Code editor component for Angular applications.
633 lines (623 loc) • 24.7 kB
JavaScript
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