mirror of
https://github.com/valitydev/control-center.git
synced 2024-11-06 02:25:17 +00:00
FE-737: Monaco editor (#36)
This commit is contained in:
parent
24c7d26f2d
commit
835ce8d41a
233
angular.json
233
angular.json
@ -1,129 +1,118 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"control-center": {
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"projectType": "application",
|
||||
"prefix": "app",
|
||||
"schematics": {},
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": [
|
||||
"./node_modules/keycloak-js/dist/keycloak.js"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"control-center": {
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"projectType": "application",
|
||||
"prefix": "app",
|
||||
"schematics": {},
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "./node_modules/monaco-editor/min/vs",
|
||||
"output": "libs/vs"
|
||||
},
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": ["src/styles.css"],
|
||||
"scripts": ["./node_modules/keycloak-js/dist/keycloak.js"]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "control-center:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "control-center:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "control-center:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "src/tsconfig.spec.json",
|
||||
"karmaConfig": "src/karma.conf.js",
|
||||
"styles": ["src/styles.css"],
|
||||
"scripts": [],
|
||||
"assets": ["src/favicon.ico", "src/assets"]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"],
|
||||
"exclude": ["**/node_modules/**"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "control-center:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "control-center:build:production"
|
||||
"control-center-e2e": {
|
||||
"root": "e2e/",
|
||||
"projectType": "application",
|
||||
"architect": {
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "e2e/protractor.conf.js",
|
||||
"devServerTarget": "control-center:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "control-center:serve:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": "e2e/tsconfig.e2e.json",
|
||||
"exclude": ["**/node_modules/**"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "control-center:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "src/tsconfig.spec.json",
|
||||
"karmaConfig": "src/karma.conf.js",
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": [],
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"src/tsconfig.app.json",
|
||||
"src/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"control-center-e2e": {
|
||||
"root": "e2e/",
|
||||
"projectType": "application",
|
||||
"architect": {
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "e2e/protractor.conf.js",
|
||||
"devServerTarget": "control-center:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "control-center:serve:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": "e2e/tsconfig.e2e.json",
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "control-center"
|
||||
}
|
||||
"defaultProject": "control-center"
|
||||
}
|
||||
|
5
package-lock.json
generated
5
package-lock.json
generated
@ -7437,6 +7437,11 @@
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz",
|
||||
"integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y="
|
||||
},
|
||||
"monaco-editor": {
|
||||
"version": "0.15.6",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.15.6.tgz",
|
||||
"integrity": "sha512-JoU9V9k6KqT9R9Tiw1RTU8ohZ+Xnf9DMg6Ktqqw5hILumwmq7xqa/KLXw513uTUsWbhtnHoSJYYR++u3pkyxJg=="
|
||||
},
|
||||
"move-concurrently": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||
|
@ -34,6 +34,7 @@
|
||||
"keycloak-js": "4.5.0",
|
||||
"lodash-es": "^4.17.10",
|
||||
"moment": "^2.22.2",
|
||||
"monaco-editor": "^0.15.6",
|
||||
"rxjs": "^6.3.3",
|
||||
"thrift-ts": "git+ssh://git@github.com/rbkmoney/thrift-ts.git#17cea6fc8a57f899a3042917ab67761f2314aff1",
|
||||
"uuid": "^3.3.2",
|
||||
|
23
src/app/domain/details-container.service.ts
Normal file
23
src/app/domain/details-container.service.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatSidenav } from '@angular/material';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class DetailsContainerService {
|
||||
opened$: Subject<boolean> = new Subject();
|
||||
|
||||
private detailsContainer: MatSidenav;
|
||||
|
||||
set container(sidenav: MatSidenav) {
|
||||
this.detailsContainer = sidenav;
|
||||
this.detailsContainer.openedChange.subscribe(opened => this.opened$.next(opened));
|
||||
}
|
||||
|
||||
open() {
|
||||
this.detailsContainer.open();
|
||||
}
|
||||
|
||||
close() {
|
||||
this.detailsContainer.close();
|
||||
}
|
||||
}
|
@ -1,4 +1,11 @@
|
||||
<form fxLayout fxLayoutGap="20px">
|
||||
<form
|
||||
fxLayout
|
||||
fxLayout.sm="column"
|
||||
fxLayout.xs="column"
|
||||
fxLayoutGap="20px"
|
||||
fxLayoutGap.sm="0"
|
||||
fxLayoutGap.xs="0"
|
||||
>
|
||||
<cc-domain-objects-type-selector
|
||||
fxFlex="50"
|
||||
[group]="group"
|
||||
@ -6,7 +13,15 @@
|
||||
></cc-domain-objects-type-selector>
|
||||
<mat-form-field fxFlex="50">
|
||||
<span matPrefix>/</span>
|
||||
<input matInput placeholder="RegExp pattern" (keyup)="patternChange($event.target.value)" />
|
||||
<input
|
||||
matInput
|
||||
placeholder="RegExp pattern"
|
||||
[value]="pattern"
|
||||
(keyup)="patternChange($event.target.value)"
|
||||
/>
|
||||
<span matSuffix>/</span>
|
||||
<button mat-button matSuffix mat-icon-button aria-label="Clear" (click)="clearPattern()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
|
@ -11,11 +11,19 @@ export class GroupControlComponent {
|
||||
@Output() typeSelectionChange: EventEmitter<string[]> = new EventEmitter();
|
||||
@Output() regExpPatternChange: EventEmitter<string> = new EventEmitter();
|
||||
|
||||
pattern = '';
|
||||
|
||||
selectionChange(selectedTypes: string[]) {
|
||||
this.typeSelectionChange.emit(selectedTypes);
|
||||
}
|
||||
|
||||
patternChange(pattern: string) {
|
||||
this.regExpPatternChange.emit(pattern);
|
||||
this.pattern = pattern;
|
||||
this.regExpPatternChange.emit(this.pattern);
|
||||
}
|
||||
|
||||
clearPattern() {
|
||||
this.pattern = '';
|
||||
this.regExpPatternChange.emit(this.pattern);
|
||||
}
|
||||
}
|
||||
|
@ -23,14 +23,18 @@
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="details" stickyEnd>
|
||||
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
||||
<td mat-cell *matCellDef="let object">
|
||||
<button mat-icon-button (click)="openDetails(object.json)">
|
||||
<td mat-cell *matCellDef="let object; let index = index">
|
||||
<button mat-icon-button (click)="openDetails(object, index)">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="cols"></tr>
|
||||
<tr mat-row *matRowDef="let object; columns: cols"></tr>
|
||||
<tr
|
||||
mat-row
|
||||
*matRowDef="let object; columns: cols; let index = index"
|
||||
[class.selected-row]="selectedIndex === index && detailsOpened"
|
||||
></tr>
|
||||
</table>
|
||||
</div>
|
||||
<mat-paginator [pageSizeOptions]="[10, 20, 50, 100]" showFirstLastButtons></mat-paginator>
|
||||
|
@ -11,7 +11,7 @@ table {
|
||||
}
|
||||
|
||||
.mat-column-type {
|
||||
padding-right: 8px
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.mat-column-ref {
|
||||
@ -39,3 +39,7 @@ table {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.selected-row {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { Component, OnInit, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
|
||||
import { MatTableDataSource, MatPaginator, MatSort } from '@angular/material';
|
||||
|
||||
import { AbstractDomainObject, DomainGroup } from '../domain-group';
|
||||
import { DomainGroup } from '../domain-group';
|
||||
import { DomainDetailsService } from '../../domain-details.service';
|
||||
import { toTableGroup, toDataSource } from './table-group';
|
||||
import { sortData } from './sort-table-data';
|
||||
import { filterPredicate } from './filter-predicate';
|
||||
import { TableDataSource, TableGroup } from './model';
|
||||
import { DetailsContainerService } from '../../details-container.service';
|
||||
|
||||
@Component({
|
||||
selector: 'cc-group-table',
|
||||
@ -21,9 +22,14 @@ export class GroupTableComponent implements OnInit, OnChanges {
|
||||
|
||||
dataSource: MatTableDataSource<TableDataSource> = new MatTableDataSource();
|
||||
cols = ['name', 'ref', 'data', 'details'];
|
||||
selectedIndex: number;
|
||||
detailsOpened: boolean;
|
||||
private tableGroup: TableGroup[];
|
||||
|
||||
constructor(private detailsService: DomainDetailsService) {}
|
||||
constructor(
|
||||
private detailsService: DomainDetailsService,
|
||||
private detailsContainerService: DetailsContainerService
|
||||
) {}
|
||||
|
||||
ngOnChanges({ group }: SimpleChanges) {
|
||||
if (group && group.currentValue) {
|
||||
@ -36,10 +42,13 @@ export class GroupTableComponent implements OnInit, OnChanges {
|
||||
this.dataSource.sort = this.sort;
|
||||
this.dataSource.filterPredicate = filterPredicate;
|
||||
this.dataSource.sortData = sortData;
|
||||
this.detailsContainerService.opened$.subscribe(opened => (this.detailsOpened = opened));
|
||||
}
|
||||
|
||||
openDetails(obj: AbstractDomainObject) {
|
||||
this.detailsService.emit(obj);
|
||||
openDetails({ json }: TableDataSource, index: number) {
|
||||
console.log(index);
|
||||
this.selectedIndex = index;
|
||||
this.detailsService.emit(json);
|
||||
}
|
||||
|
||||
applyFilter(filterValue: string) {
|
||||
|
@ -0,0 +1 @@
|
||||
<cc-monaco-editor [file]="file$ | async" [options]="options"></cc-monaco-editor>
|
@ -0,0 +1,4 @@
|
||||
cc-monaco-editor {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { DomainDetailsService } from '../domain-details.service';
|
||||
import { MonacoFile, MonacoEditorOptions } from '../../monaco-editor';
|
||||
|
||||
@Component({
|
||||
selector: 'cc-domain-obj-details',
|
||||
templateUrl: './domain-obj-details.component.html',
|
||||
styleUrls: ['./domain-obj-details.component.scss']
|
||||
})
|
||||
export class DomainObjDetailsComponent implements OnInit {
|
||||
file$: Observable<MonacoFile>;
|
||||
options: MonacoEditorOptions = {
|
||||
readOnly: true
|
||||
};
|
||||
|
||||
constructor(private detailsService: DomainDetailsService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.file$ = this.detailsService.detailedObject$.pipe(
|
||||
map(o => ({
|
||||
uri: 'index.json',
|
||||
language: 'json',
|
||||
content: JSON.stringify(o, null, 2)
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
1
src/app/domain/domain-obj-details/index.ts
Normal file
1
src/app/domain/domain-obj-details/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './domain-obj-details.component';
|
@ -7,8 +7,8 @@
|
||||
fixedTopGap="64"
|
||||
>
|
||||
<div class="details-container" fxLayout="column" fxLayoutGap="20px">
|
||||
<cc-domain-obj-details fxFlex="95"></cc-domain-obj-details>
|
||||
<button mat-stroked-button (click)="closeDetails()">CLOSE</button>
|
||||
<cc-pretty-json [object]="detailedDomainObj$ | async"></cc-pretty-json>
|
||||
</div>
|
||||
</mat-sidenav>
|
||||
<mat-sidenav-content>
|
||||
|
@ -4,4 +4,5 @@ mat-sidenav {
|
||||
|
||||
.details-container {
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -1,37 +1,35 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { MatSnackBar, MatSidenav } from '@angular/material';
|
||||
import { Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
|
||||
import { DomainService } from './domain.service';
|
||||
import { AbstractDomainObject } from './domain-group/domain-group';
|
||||
import { DomainDetailsService } from './domain-details.service';
|
||||
import { DetailsContainerService } from './details-container.service';
|
||||
|
||||
@Component({
|
||||
templateUrl: './domain.component.html',
|
||||
styleUrls: ['../shared/container.css', './domain.component.scss']
|
||||
styleUrls: ['../shared/container.css', './domain.component.scss'],
|
||||
providers: [DomainDetailsService, DetailsContainerService]
|
||||
})
|
||||
export class DomainComponent implements OnInit {
|
||||
initialized = false;
|
||||
isLoading: boolean;
|
||||
detailedDomainObj$: Observable<AbstractDomainObject>;
|
||||
@ViewChild('domainObjDetails') detailsContainer: MatSidenav;
|
||||
|
||||
constructor(
|
||||
private domainService: DomainService,
|
||||
private snackBar: MatSnackBar,
|
||||
private detailsService: DomainDetailsService
|
||||
private detailsService: DomainDetailsService,
|
||||
private detailsContainerService: DetailsContainerService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.initialize();
|
||||
this.detailedDomainObj$ = this.detailsService.detailedObject$.pipe(
|
||||
tap(() => this.detailsContainer.open())
|
||||
);
|
||||
this.detailsContainerService.container = this.detailsContainer;
|
||||
this.detailsService.detailedObject$.subscribe(() => this.detailsContainerService.open());
|
||||
}
|
||||
|
||||
closeDetails() {
|
||||
this.detailsContainer.close();
|
||||
this.detailsContainerService.close();
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
|
||||
import { DomainComponent } from './domain.component';
|
||||
import { DomainObjDetailsComponent } from './domain-obj-details';
|
||||
import { DomainRoutingModule } from './domain-routing.module';
|
||||
import { DomainService } from './domain.service';
|
||||
import { ThriftModule } from '../thrift/thrift.module';
|
||||
@ -19,9 +20,10 @@ import { DomainGroupModule } from './domain-group';
|
||||
import { MetadataLoader } from './metadata-loader';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { DomainDetailsService } from './domain-details.service';
|
||||
import { MonacoEditorModule } from '../monaco-editor';
|
||||
|
||||
@NgModule({
|
||||
declarations: [DomainComponent],
|
||||
declarations: [DomainComponent, DomainObjDetailsComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
DomainRoutingModule,
|
||||
@ -33,10 +35,11 @@ import { DomainDetailsService } from './domain-details.service';
|
||||
MatButtonModule,
|
||||
MatInputModule,
|
||||
MatProgressSpinnerModule,
|
||||
MonacoEditorModule,
|
||||
ThriftModule,
|
||||
DomainGroupModule,
|
||||
SharedModule
|
||||
],
|
||||
providers: [DomainService, MetadataLoader, DomainDetailsService]
|
||||
providers: [DomainService, MetadataLoader]
|
||||
})
|
||||
export class DamainModule {}
|
||||
|
17
src/app/monaco-editor/bootstrap.ts
Normal file
17
src/app/monaco-editor/bootstrap.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { shareReplay } from 'rxjs/operators';
|
||||
|
||||
declare const window: any;
|
||||
|
||||
export const bootstrap$ = Observable.create(observer => {
|
||||
const script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.src = 'libs/vs/loader.js';
|
||||
script.onload = () => {
|
||||
window.require.config({ paths: { vs: 'libs/vs' } });
|
||||
window.require(['vs/editor/editor.main'], () => {
|
||||
observer.next();
|
||||
});
|
||||
};
|
||||
document.body.appendChild(script);
|
||||
}).pipe(shareReplay(1));
|
14
src/app/monaco-editor/from-disposable.ts
Normal file
14
src/app/monaco-editor/from-disposable.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/// <reference path="../../../node_modules/monaco-editor/monaco.d.ts" />
|
||||
import { Observable, Observer } from 'rxjs';
|
||||
|
||||
export function fromDisposable<T>(
|
||||
source: (listener: (e: T) => void) => monaco.IDisposable
|
||||
): Observable<T> {
|
||||
return Observable.create((observer: Observer<T>) => {
|
||||
const disposable = source(e => {
|
||||
observer.next(e);
|
||||
});
|
||||
|
||||
return () => disposable.dispose();
|
||||
});
|
||||
}
|
2
src/app/monaco-editor/index.ts
Normal file
2
src/app/monaco-editor/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './monaco-editor.module';
|
||||
export * from './model';
|
7
src/app/monaco-editor/model.ts
Normal file
7
src/app/monaco-editor/model.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export type MonacoEditorOptions = monaco.editor.IEditorOptions;
|
||||
|
||||
export interface MonacoFile {
|
||||
uri: string;
|
||||
content: string;
|
||||
language?: string;
|
||||
}
|
69
src/app/monaco-editor/monaco-editor.directive.ts
Normal file
69
src/app/monaco-editor/monaco-editor.directive.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import {
|
||||
OnInit,
|
||||
Input,
|
||||
ElementRef,
|
||||
Output,
|
||||
EventEmitter,
|
||||
Directive,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
HostListener,
|
||||
OnDestroy
|
||||
} from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { filter, map, distinctUntilChanged, debounceTime, takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { MonacoFile, MonacoEditorOptions } from './model';
|
||||
import { MonacoEditorService } from './monaco-editor.service';
|
||||
|
||||
@Directive({
|
||||
selector: 'cc-monaco-editor,[ccMonacoEditor]'
|
||||
})
|
||||
export class MonacoEditorDirective implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() file: MonacoFile;
|
||||
@Input() options: MonacoEditorOptions;
|
||||
|
||||
@Output() ready = new EventEmitter();
|
||||
|
||||
private resize$ = new Subject();
|
||||
private destroy$ = new Subject();
|
||||
private initialized = false;
|
||||
|
||||
constructor(private monacoEditorService: MonacoEditorService, private editorRef: ElementRef) {}
|
||||
|
||||
@HostListener('window:resize') onResize() {
|
||||
this.resize$.next();
|
||||
}
|
||||
|
||||
ngOnChanges({ options, file }: SimpleChanges) {
|
||||
if (options) {
|
||||
this.monacoEditorService.updateOptions(options.currentValue);
|
||||
}
|
||||
if (file) {
|
||||
this.monacoEditorService.open(file.currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.monacoEditorService.init(this.editorRef, this.options).subscribe(() => {
|
||||
this.ready.emit();
|
||||
this.initialized = true;
|
||||
});
|
||||
this.resize$
|
||||
.pipe(
|
||||
filter(() => this.initialized),
|
||||
map(() => {
|
||||
const { clientWidth, clientHeight } = this.editorRef.nativeElement;
|
||||
return { width: clientWidth, height: clientHeight };
|
||||
}),
|
||||
distinctUntilChanged((a, b) => a.width === b.width && a.height === b.height),
|
||||
debounceTime(50),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe(dimension => this.monacoEditorService.layout(dimension));
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
}
|
||||
}
|
13
src/app/monaco-editor/monaco-editor.module.ts
Normal file
13
src/app/monaco-editor/monaco-editor.module.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { MonacoEditorDirective } from './monaco-editor.directive';
|
||||
import { MonacoEditorService } from './monaco-editor.service';
|
||||
|
||||
@NgModule({
|
||||
declarations: [MonacoEditorDirective],
|
||||
imports: [CommonModule],
|
||||
exports: [MonacoEditorDirective],
|
||||
providers: [MonacoEditorService]
|
||||
})
|
||||
export class MonacoEditorModule {}
|
93
src/app/monaco-editor/monaco-editor.service.ts
Normal file
93
src/app/monaco-editor/monaco-editor.service.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { Injectable, ElementRef, NgZone } from '@angular/core';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { tap, map, takeUntil, take } from 'rxjs/operators';
|
||||
|
||||
import { MonacoEditorOptions, MonacoFile } from './model';
|
||||
import { fromDisposable } from './from-disposable';
|
||||
import { bootstrap$ } from './bootstrap';
|
||||
|
||||
declare const window: any;
|
||||
|
||||
@Injectable()
|
||||
export class MonacoEditorService {
|
||||
fileChange$ = new Subject<MonacoFile>();
|
||||
|
||||
private editor: monaco.editor.ICodeEditor;
|
||||
private file: MonacoFile;
|
||||
|
||||
constructor(private zone: NgZone) {}
|
||||
|
||||
init({ nativeElement }: ElementRef, options: MonacoEditorOptions = {}): Observable<void> {
|
||||
return bootstrap$.pipe(
|
||||
tap(() => {
|
||||
this.disposeModels();
|
||||
this.editor = monaco.editor.create(nativeElement, {
|
||||
...options
|
||||
});
|
||||
if (this.file) {
|
||||
this.open(this.file);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
updateOptions(options: MonacoEditorOptions) {
|
||||
if (this.editor) {
|
||||
this.editor.updateOptions(options);
|
||||
}
|
||||
}
|
||||
|
||||
open(file: MonacoFile) {
|
||||
this.file = file;
|
||||
if (!this.editor) {
|
||||
return;
|
||||
}
|
||||
const uri = monaco.Uri.file(file.uri);
|
||||
let model = monaco.editor.getModel(uri);
|
||||
if (model) {
|
||||
if (file.language && model.getModeId() !== file.language) {
|
||||
model.dispose();
|
||||
model = undefined;
|
||||
} else {
|
||||
model.setValue(file.content);
|
||||
}
|
||||
}
|
||||
if (!model) {
|
||||
model = monaco.editor.createModel(file.content, file.language, uri);
|
||||
this.registerModelChangeListener(file, model);
|
||||
}
|
||||
this.editor.setModel(model);
|
||||
}
|
||||
|
||||
layout(dimension?: monaco.editor.IDimension) {
|
||||
if (this.editor) {
|
||||
this.editor.layout(dimension);
|
||||
}
|
||||
}
|
||||
|
||||
private registerModelChangeListener(file: MonacoFile, model: monaco.editor.IModel) {
|
||||
const destroy = fromDisposable(model.onWillDispose.bind(model)).pipe(take(1));
|
||||
fromDisposable(model.onDidChangeContent.bind(model))
|
||||
.pipe(
|
||||
map(() => model.getValue()),
|
||||
takeUntil(destroy)
|
||||
)
|
||||
.subscribe((content: string) =>
|
||||
this.zone.run(() =>
|
||||
this.fileChange$.next({
|
||||
...file,
|
||||
content
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private disposeModels() {
|
||||
if (!window.monaco) {
|
||||
return;
|
||||
}
|
||||
for (const model of monaco.editor.getModels()) {
|
||||
model.dispose();
|
||||
}
|
||||
}
|
||||
}
|
@ -5,29 +5,31 @@ import { Component, Input } from '@angular/core';
|
||||
templateUrl: './pretty-json.component.html',
|
||||
styles: [
|
||||
`
|
||||
:host /deep/ * {
|
||||
font-family: Menlo, Monaco, 'Courier New', monospace;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
letter-spacing: 0px;
|
||||
}
|
||||
:host /deep/ .string {
|
||||
color: #008000;
|
||||
font-weight: bold;
|
||||
color: #0451a5;
|
||||
}
|
||||
|
||||
:host /deep/ .number {
|
||||
color: #0000ff;
|
||||
font-weight: bold;
|
||||
color: #09885a;
|
||||
}
|
||||
|
||||
:host /deep/ .boolean {
|
||||
color: #000080;
|
||||
font-weight: bold;
|
||||
color: #0451a5;
|
||||
}
|
||||
|
||||
:host /deep/ .null {
|
||||
color: magenta;
|
||||
font-weight: bold;
|
||||
color: #0451a5;
|
||||
}
|
||||
|
||||
:host /deep/ .key {
|
||||
color: #660e7a;
|
||||
font-weight: bold;
|
||||
color: #a31515;
|
||||
}
|
||||
|
||||
pre {
|
||||
|
Loading…
Reference in New Issue
Block a user