464 lines
20 KiB
JavaScript
464 lines
20 KiB
JavaScript
import { Platform } from '@angular/cdk/platform';
|
|
import { NgTemplateOutlet } from '@angular/common';
|
|
import * as i0 from '@angular/core';
|
|
import { inject, DOCUMENT, Injectable, NgZone, DestroyRef, EventEmitter, ElementRef, forwardRef, booleanAttribute, Output, Input, ViewEncapsulation, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
|
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
import { ReplaySubject, BehaviorSubject, of, Subject, combineLatest } from 'rxjs';
|
|
import { tap, map, debounceTime, filter, distinctUntilChanged } from 'rxjs/operators';
|
|
import { warn, PREFIX } from 'ng-zorro-antd/core/logger';
|
|
import { inNextTick, fromEventOutsideAngular } from 'ng-zorro-antd/core/util';
|
|
import { NzSpinComponent } from 'ng-zorro-antd/spin';
|
|
import { NzConfigService, onConfigChangeEventForComponent } from 'ng-zorro-antd/core/config';
|
|
|
|
/**
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
|
|
*/
|
|
const NzCodeEditorLoadingStatus = {
|
|
UNLOAD: 'unload',
|
|
LOADING: 'loading',
|
|
LOADED: 'LOADED'
|
|
};
|
|
|
|
/**
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
|
|
*/
|
|
const NZ_CONFIG_MODULE_NAME = 'codeEditor';
|
|
function tryTriggerFunc(fn) {
|
|
return (...args) => {
|
|
if (fn) {
|
|
fn(...args);
|
|
}
|
|
};
|
|
}
|
|
// Caretaker note: previously, these were `NzCodeEditorService` properties.
|
|
// They're kept as static variables because this will allow loading Monaco only once.
|
|
// This applies to micro frontend apps with multiple Angular apps or a single Angular app
|
|
// that can be bootstrapped and destroyed multiple times (e.g. using Webpack module federation).
|
|
// Root providers are re-initialized each time the app is bootstrapped. Platform providers aren't.
|
|
// We can't make the `NzCodeEditorService` to be a platform provider (`@Injectable({ providedIn: 'platform' })`)
|
|
// since it depends on other root providers.
|
|
const loaded$ = new ReplaySubject(1);
|
|
let loadingStatus = NzCodeEditorLoadingStatus.UNLOAD;
|
|
class NzCodeEditorService {
|
|
nzConfigService = inject(NzConfigService);
|
|
document = inject(DOCUMENT);
|
|
firstEditorInitialized = false;
|
|
option = {};
|
|
config;
|
|
option$ = new BehaviorSubject(this.option);
|
|
constructor() {
|
|
const globalConfig = this.nzConfigService.getConfigForComponent(NZ_CONFIG_MODULE_NAME);
|
|
this.config = { ...globalConfig };
|
|
if (this.config.monacoEnvironment) {
|
|
window.MonacoEnvironment = { ...this.config.monacoEnvironment };
|
|
}
|
|
this.option = this.config.defaultEditorOption || {};
|
|
onConfigChangeEventForComponent(NZ_CONFIG_MODULE_NAME, () => {
|
|
const newGlobalConfig = this.nzConfigService.getConfigForComponent(NZ_CONFIG_MODULE_NAME);
|
|
if (newGlobalConfig) {
|
|
this._updateDefaultOption(newGlobalConfig.defaultEditorOption);
|
|
}
|
|
});
|
|
}
|
|
_updateDefaultOption(option) {
|
|
this.option = { ...this.option, ...option };
|
|
this.option$.next(this.option);
|
|
if ('theme' in option && option.theme) {
|
|
monaco.editor.setTheme(option.theme);
|
|
}
|
|
}
|
|
requestToInit() {
|
|
if (loadingStatus === NzCodeEditorLoadingStatus.LOADED) {
|
|
this.onInit();
|
|
return of(this.getLatestOption());
|
|
}
|
|
if (loadingStatus === NzCodeEditorLoadingStatus.UNLOAD) {
|
|
if (this.config.useStaticLoading && typeof monaco === 'undefined') {
|
|
warn('You choose to use static loading but it seems that you forget ' +
|
|
'to config webpack plugin correctly. Please refer to our official website' +
|
|
'for more details about static loading.');
|
|
}
|
|
else {
|
|
this.loadMonacoScript();
|
|
}
|
|
}
|
|
return loaded$.pipe(tap(() => this.onInit()), map(() => this.getLatestOption()));
|
|
}
|
|
loadMonacoScript() {
|
|
if (this.config.useStaticLoading) {
|
|
Promise.resolve().then(() => this.onLoad());
|
|
return;
|
|
}
|
|
if (loadingStatus === NzCodeEditorLoadingStatus.LOADING) {
|
|
return;
|
|
}
|
|
loadingStatus = NzCodeEditorLoadingStatus.LOADING;
|
|
const assetsRoot = this.config.assetsRoot;
|
|
const vs = assetsRoot ? `${assetsRoot}/vs` : 'assets/vs';
|
|
const windowAsAny = window;
|
|
const loadScript = this.document.createElement('script');
|
|
loadScript.type = 'text/javascript';
|
|
loadScript.src = `${vs}/loader.js`;
|
|
const onLoad = () => {
|
|
cleanup();
|
|
windowAsAny.require.config({
|
|
paths: { vs },
|
|
...this.config.extraConfig
|
|
});
|
|
windowAsAny.require(['vs/editor/editor.main'], () => {
|
|
this.onLoad();
|
|
});
|
|
};
|
|
const onError = () => {
|
|
cleanup();
|
|
throw new Error(`${PREFIX} cannot load assets of monaco editor from source "${vs}".`);
|
|
};
|
|
const cleanup = () => {
|
|
// Caretaker note: we have to remove these listeners once the `<script>` is loaded successfully
|
|
// or not since the `onLoad` listener captures `this`, which will prevent the `NzCodeEditorService`
|
|
// from being garbage collected.
|
|
loadScript.removeEventListener('load', onLoad);
|
|
loadScript.removeEventListener('error', onError);
|
|
// We don't need to keep the `<script>` element within the `<body>` since JavaScript has
|
|
// been executed and Monaco is available globally. E.g. Webpack, always removes `<script>`
|
|
// elements after loading chunks (see its `LoadScriptRuntimeModule`).
|
|
this.document.documentElement.removeChild(loadScript);
|
|
};
|
|
loadScript.addEventListener('load', onLoad);
|
|
loadScript.addEventListener('error', onError);
|
|
this.document.documentElement.appendChild(loadScript);
|
|
}
|
|
onLoad() {
|
|
loadingStatus = NzCodeEditorLoadingStatus.LOADED;
|
|
loaded$.next(true);
|
|
loaded$.complete();
|
|
tryTriggerFunc(this.config.onLoad)();
|
|
}
|
|
onInit() {
|
|
if (!this.firstEditorInitialized) {
|
|
this.firstEditorInitialized = true;
|
|
tryTriggerFunc(this.config.onFirstEditorInit)();
|
|
}
|
|
tryTriggerFunc(this.config.onInit)();
|
|
}
|
|
getLatestOption() {
|
|
return { ...this.option };
|
|
}
|
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: NzCodeEditorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: NzCodeEditorService, providedIn: 'root' });
|
|
}
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: NzCodeEditorService, decorators: [{
|
|
type: Injectable,
|
|
args: [{
|
|
providedIn: 'root'
|
|
}]
|
|
}], ctorParameters: () => [] });
|
|
|
|
/**
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
|
|
*/
|
|
class NzCodeEditorComponent {
|
|
nzCodeEditorService = inject(NzCodeEditorService);
|
|
ngZone = inject(NgZone);
|
|
platform = inject(Platform);
|
|
destroyRef = inject(DestroyRef);
|
|
nzEditorMode = 'normal';
|
|
nzOriginalText = '';
|
|
nzLoading = false;
|
|
nzFullControl = false;
|
|
nzToolkit;
|
|
set nzEditorOption(value) {
|
|
this.editorOption$.next(value);
|
|
}
|
|
nzEditorInitialized = new EventEmitter();
|
|
editorOptionCached = {};
|
|
el = inject((ElementRef)).nativeElement;
|
|
resize$ = new Subject();
|
|
editorOption$ = new BehaviorSubject({});
|
|
editorInstance = null;
|
|
value = '';
|
|
modelSet = false;
|
|
onDidChangeContentDisposable = null;
|
|
constructor() {
|
|
this.el.classList.add('ant-code-editor');
|
|
this.destroyRef.onDestroy(() => {
|
|
if (this.onDidChangeContentDisposable) {
|
|
this.onDidChangeContentDisposable.dispose();
|
|
this.onDidChangeContentDisposable = null;
|
|
}
|
|
if (this.editorInstance) {
|
|
this.editorInstance.dispose();
|
|
this.editorInstance = null;
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Initialize a monaco editor instance.
|
|
*/
|
|
ngAfterViewInit() {
|
|
if (!this.platform.isBrowser) {
|
|
return;
|
|
}
|
|
this.nzCodeEditorService
|
|
.requestToInit()
|
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
.subscribe(option => this.setup(option));
|
|
}
|
|
writeValue(value) {
|
|
this.value = value;
|
|
this.setValue();
|
|
}
|
|
registerOnChange(fn) {
|
|
this.onChange = fn;
|
|
}
|
|
registerOnTouched(fn) {
|
|
this.onTouch = fn;
|
|
}
|
|
onChange = (_value) => { };
|
|
onTouch = () => { };
|
|
layout() {
|
|
this.resize$.next();
|
|
}
|
|
setup(option) {
|
|
// The `setup()` is invoked when the Monaco editor is loaded. This may happen asynchronously for the first
|
|
// time, and it'll always happen synchronously afterwards. The first `setup()` invokation is outside the Angular
|
|
// zone, but further invokations will happen within the Angular zone. We call the `setModel()` on the editor
|
|
// instance, which tells Monaco to add event listeners lazily internally (`mousemove`, `mouseout`, etc.).
|
|
// We should avoid adding them within the Angular zone since this will drastically affect the performance.
|
|
this.ngZone.runOutsideAngular(() => inNextTick()
|
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
.subscribe(() => {
|
|
this.editorOptionCached = option;
|
|
this.registerOptionChanges();
|
|
this.initMonacoEditorInstance();
|
|
this.registerResizeChange();
|
|
this.setValue();
|
|
if (!this.nzFullControl) {
|
|
this.setValueEmitter();
|
|
}
|
|
if (this.nzEditorInitialized.observers.length) {
|
|
this.ngZone.run(() => this.nzEditorInitialized.emit(this.editorInstance));
|
|
}
|
|
}));
|
|
}
|
|
registerOptionChanges() {
|
|
combineLatest([this.editorOption$, this.nzCodeEditorService.option$])
|
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
.subscribe(([selfOpt, defaultOpt]) => {
|
|
this.editorOptionCached = {
|
|
...this.editorOptionCached,
|
|
...defaultOpt,
|
|
...selfOpt
|
|
};
|
|
this.updateOptionToMonaco();
|
|
});
|
|
}
|
|
initMonacoEditorInstance() {
|
|
this.ngZone.runOutsideAngular(() => {
|
|
this.editorInstance =
|
|
this.nzEditorMode === 'normal'
|
|
? monaco.editor.create(this.el, { ...this.editorOptionCached })
|
|
: monaco.editor.createDiffEditor(this.el, {
|
|
...this.editorOptionCached
|
|
});
|
|
});
|
|
}
|
|
registerResizeChange() {
|
|
fromEventOutsideAngular(window, 'resize')
|
|
.pipe(debounceTime(300), takeUntilDestroyed(this.destroyRef))
|
|
.subscribe(() => {
|
|
this.layout();
|
|
});
|
|
this.resize$
|
|
.pipe(takeUntilDestroyed(this.destroyRef), filter(() => !!this.editorInstance), map(() => ({
|
|
width: this.el.clientWidth,
|
|
height: this.el.clientHeight
|
|
})), distinctUntilChanged((a, b) => a.width === b.width && a.height === b.height), debounceTime(50))
|
|
.subscribe(() => {
|
|
this.editorInstance.layout();
|
|
});
|
|
}
|
|
setValue() {
|
|
if (!this.editorInstance) {
|
|
return;
|
|
}
|
|
if (this.nzFullControl && this.value) {
|
|
warn(`should not set value when you are using full control mode! It would result in ambiguous data flow!`);
|
|
return;
|
|
}
|
|
if (this.nzEditorMode === 'normal') {
|
|
if (this.modelSet) {
|
|
const model = this.editorInstance.getModel();
|
|
this.preservePositionAndSelections(() => model.setValue(this.value));
|
|
}
|
|
else {
|
|
this.editorInstance.setModel(monaco.editor.createModel(this.value, this.editorOptionCached.language));
|
|
this.modelSet = true;
|
|
}
|
|
}
|
|
else {
|
|
if (this.modelSet) {
|
|
const model = this.editorInstance.getModel();
|
|
this.preservePositionAndSelections(() => {
|
|
model.modified.setValue(this.value);
|
|
model.original.setValue(this.nzOriginalText);
|
|
});
|
|
}
|
|
else {
|
|
const language = this.editorOptionCached.language;
|
|
this.editorInstance.setModel({
|
|
original: monaco.editor.createModel(this.nzOriginalText, language),
|
|
modified: monaco.editor.createModel(this.value, language)
|
|
});
|
|
this.modelSet = true;
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* {@link editor.ICodeEditor}#setValue resets the cursor position to the start of the document.
|
|
* This helper memorizes the cursor position and selections and restores them after the given
|
|
* function has been called.
|
|
*/
|
|
preservePositionAndSelections(fn) {
|
|
if (!this.editorInstance) {
|
|
fn();
|
|
return;
|
|
}
|
|
const position = this.editorInstance.getPosition();
|
|
const selections = this.editorInstance.getSelections();
|
|
fn();
|
|
if (position) {
|
|
this.editorInstance.setPosition(position);
|
|
}
|
|
if (selections) {
|
|
this.editorInstance.setSelections(selections);
|
|
}
|
|
}
|
|
setValueEmitter() {
|
|
const model = (this.nzEditorMode === 'normal'
|
|
? this.editorInstance.getModel()
|
|
: this.editorInstance.getModel().modified);
|
|
// The `onDidChangeContent` returns a disposable object (an object with `dispose()` method) which will cleanup
|
|
// the listener. The callback, that we pass to `onDidChangeContent`, captures `this`. This leads to a circular reference
|
|
// (`nz-code-editor -> monaco -> nz-code-editor`) and prevents the `nz-code-editor` from being GC'd.
|
|
this.onDidChangeContentDisposable = model.onDidChangeContent(() => {
|
|
this.emitValue(model.getValue());
|
|
});
|
|
}
|
|
emitValue(value) {
|
|
if (this.value === value) {
|
|
// If the value didn't change there's no reason to send an update.
|
|
// Specifically this may happen during an update from the model (writeValue) where sending an update to the model would actually be incorrect.
|
|
return;
|
|
}
|
|
this.value = value;
|
|
// We're re-entering the Angular zone only if the value has been changed since there's a `return` expression previously.
|
|
// This won't cause "dead" change detections (basically when the `tick()` has been run, but there's nothing to update).
|
|
this.ngZone.run(() => {
|
|
this.onChange(value);
|
|
});
|
|
}
|
|
updateOptionToMonaco() {
|
|
if (this.editorInstance) {
|
|
this.editorInstance.updateOptions({ ...this.editorOptionCached });
|
|
}
|
|
}
|
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: NzCodeEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.0", type: NzCodeEditorComponent, isStandalone: true, selector: "nz-code-editor", inputs: { nzEditorMode: "nzEditorMode", nzOriginalText: "nzOriginalText", nzLoading: ["nzLoading", "nzLoading", booleanAttribute], nzFullControl: ["nzFullControl", "nzFullControl", booleanAttribute], nzToolkit: "nzToolkit", nzEditorOption: "nzEditorOption" }, outputs: { nzEditorInitialized: "nzEditorInitialized" }, providers: [
|
|
{
|
|
provide: NG_VALUE_ACCESSOR,
|
|
useExisting: forwardRef(() => NzCodeEditorComponent),
|
|
multi: true
|
|
}
|
|
], exportAs: ["nzCodeEditor"], ngImport: i0, template: `
|
|
@if (nzLoading) {
|
|
<div class="ant-code-editor-loading">
|
|
<nz-spin />
|
|
</div>
|
|
}
|
|
@if (nzToolkit) {
|
|
<div class="ant-code-editor-toolkit">
|
|
<ng-template [ngTemplateOutlet]="nzToolkit" />
|
|
</div>
|
|
}
|
|
`, isInline: true, dependencies: [{ kind: "component", type: NzSpinComponent, selector: "nz-spin", inputs: ["nzIndicator", "nzSize", "nzTip", "nzDelay", "nzSimple", "nzSpinning"], exportAs: ["nzSpin"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
}
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: NzCodeEditorComponent, decorators: [{
|
|
type: Component,
|
|
args: [{
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
encapsulation: ViewEncapsulation.None,
|
|
selector: 'nz-code-editor',
|
|
exportAs: 'nzCodeEditor',
|
|
template: `
|
|
@if (nzLoading) {
|
|
<div class="ant-code-editor-loading">
|
|
<nz-spin />
|
|
</div>
|
|
}
|
|
@if (nzToolkit) {
|
|
<div class="ant-code-editor-toolkit">
|
|
<ng-template [ngTemplateOutlet]="nzToolkit" />
|
|
</div>
|
|
}
|
|
`,
|
|
providers: [
|
|
{
|
|
provide: NG_VALUE_ACCESSOR,
|
|
useExisting: forwardRef(() => NzCodeEditorComponent),
|
|
multi: true
|
|
}
|
|
],
|
|
imports: [NzSpinComponent, NgTemplateOutlet]
|
|
}]
|
|
}], ctorParameters: () => [], propDecorators: { nzEditorMode: [{
|
|
type: Input
|
|
}], nzOriginalText: [{
|
|
type: Input
|
|
}], nzLoading: [{
|
|
type: Input,
|
|
args: [{ transform: booleanAttribute }]
|
|
}], nzFullControl: [{
|
|
type: Input,
|
|
args: [{ transform: booleanAttribute }]
|
|
}], nzToolkit: [{
|
|
type: Input
|
|
}], nzEditorOption: [{
|
|
type: Input
|
|
}], nzEditorInitialized: [{
|
|
type: Output
|
|
}] } });
|
|
|
|
/**
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
|
|
*/
|
|
class NzCodeEditorModule {
|
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: NzCodeEditorModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.0", ngImport: i0, type: NzCodeEditorModule, imports: [NzCodeEditorComponent], exports: [NzCodeEditorComponent] });
|
|
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: NzCodeEditorModule, imports: [NzCodeEditorComponent] });
|
|
}
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: NzCodeEditorModule, decorators: [{
|
|
type: NgModule,
|
|
args: [{
|
|
imports: [NzCodeEditorComponent],
|
|
exports: [NzCodeEditorComponent]
|
|
}]
|
|
}] });
|
|
|
|
/**
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
|
|
*/
|
|
|
|
/**
|
|
* Generated bundle index. Do not edit.
|
|
*/
|
|
|
|
export { NzCodeEditorComponent, NzCodeEditorLoadingStatus, NzCodeEditorModule, NzCodeEditorService };
|
|
//# sourceMappingURL=ng-zorro-antd-code-editor.mjs.map
|