import { Component, Inject, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatTooltipModule } from '@angular/material/tooltip'; import { BreakpointObserver } from '@angular/cdk/layout'; import { I18nService } from '../services/i18n.service'; import { ThemeService } from '../services/theme.service'; import { CommonService } from '../services/common.service'; import { DialogCancelButtonComponent } from '../components/dialog-cancel-button.component'; import { RedisStateService } from '../services/redis-state.service'; import { SettingsService } from '../services/settings.service'; export interface JsonEditorDialogData { value: string; hideFormatSave?: boolean; } @Component({ selector: 'p3xr-json-editor-dialog', standalone: true, imports: [ CommonModule, MatDialogModule, MatButtonModule, MatIconModule, MatToolbarModule, MatTooltipModule, DialogCancelButtonComponent, ], template: ` edit {{ strings().intention?.jsonViewEditor || 'JSON Editor' }} @if (isJson) {
} @else { {{ strings().label?.jsonViewNotParsable || 'Not valid JSON' }} } @if (isJson && !isReadonly) { @if (!hideFormatSave) { } } `, styles: [` .hide-sm { display: inline; } .p3xr-dialog-content-editor { padding: 0 !important; overflow: hidden !important; max-height: none !important; } .p3xr-dialog-content-message { min-height: 320px; } .p3xr-codemirror-host { height: calc(90vh - 100px); } .p3xr-codemirror-host .cm-editor { height: 100% !important; max-height: 100% !important; } .p3xr-codemirror-host .cm-scroller { overflow: auto !important; min-height: 0 !important; } @media (max-width: 959px) { .hide-sm { display: none; } } `], }) export class JsonEditorDialogComponent implements OnInit, AfterViewInit, OnDestroy { @ViewChild('editorContainer') editorContainer!: ElementRef; isJson = false; isReadonly = false; hideFormatSave = false; lineWrap = true; minHeight = '400px'; strings; private editorView: any; private wrapCompartment: any; private EditorViewClass: any; private obj: any; private resizeHandler: any; constructor( @Inject(MatDialogRef) private dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) private data: JsonEditorDialogData, @Inject(I18nService) private i18n: I18nService, @Inject(ThemeService) private theme: ThemeService, @Inject(CommonService) private common: CommonService, @Inject(BreakpointObserver) private breakpointObserver: BreakpointObserver, @Inject(RedisStateService) private state: RedisStateService, @Inject(SettingsService) private settings: SettingsService, ) { this.strings = this.i18n.strings; } ngOnInit(): void { try { this.obj = JSON.parse(this.data.value); this.isJson = true; } catch (e) { this.obj = undefined; this.isJson = false; } this.isReadonly = this.state.connection()?.readonly === true; this.hideFormatSave = this.data.hideFormatSave === true; this.updateMinHeight(); } async ngAfterViewInit(): Promise { if (!this.isJson || !this.editorContainer) return; const { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, highlightActiveLine, rectangularSelection, crosshairCursor } = await import( /* webpackChunkName: "codemirror-view" */ '@codemirror/view' ); const { EditorState, Compartment } = await import( /* webpackChunkName: "codemirror-state" */ '@codemirror/state' ); this.wrapCompartment = new Compartment(); const { json } = await import( /* webpackChunkName: "codemirror-lang-json" */ '@codemirror/lang-json' ); const { defaultKeymap, history, historyKeymap } = await import( /* webpackChunkName: "codemirror-commands" */ '@codemirror/commands' ); const { bracketMatching, foldGutter, foldKeymap, indentOnInput, syntaxHighlighting, defaultHighlightStyle } = await import( /* webpackChunkName: "codemirror-language" */ '@codemirror/language' ); const { closeBrackets, closeBracketsKeymap } = await import( /* webpackChunkName: "codemirror-autocomplete" */ '@codemirror/autocomplete' ); const { searchKeymap, highlightSelectionMatches } = await import( /* webpackChunkName: "codemirror-search" */ '@codemirror/search' ); const { lintKeymap } = await import( /* webpackChunkName: "codemirror-lint" */ '@codemirror/lint' ); let themeExtension; if (this.theme.isDark()) { const { oneDark } = await import( /* webpackChunkName: "codemirror-theme-dark" */ '@codemirror/theme-one-dark' ); themeExtension = oneDark; } else { const { githubLight } = await import( /* webpackChunkName: "codemirror-theme-light" */ '@uiw/codemirror-theme-github' ); themeExtension = githubLight; } const doc = JSON.stringify(this.obj, null, this.settings.jsonFormat() ?? 2); this.EditorViewClass = EditorView; this.editorView = new EditorView({ state: EditorState.create({ doc: doc, extensions: [ lineNumbers(), highlightActiveLineGutter(), highlightSpecialChars(), history(), foldGutter(), drawSelection(), indentOnInput(), syntaxHighlighting(defaultHighlightStyle, { fallback: true }), bracketMatching(), closeBrackets(), rectangularSelection(), crosshairCursor(), highlightActiveLine(), highlightSelectionMatches(), keymap.of([ ...closeBracketsKeymap, ...defaultKeymap, ...searchKeymap, ...historyKeymap, ...foldKeymap, ...lintKeymap, ]), json(), themeExtension, EditorView.theme({ '&': { 'height': 'calc(90vh - 100px)', 'max-height': 'calc(90vh - 100px)', }, '.cm-scroller': { 'overflow': 'auto', 'scrollbar-width': 'auto', }, '.cm-scroller::-webkit-scrollbar': { 'height': '12px', 'display': 'block', }, '.cm-scroller::-webkit-scrollbar-track': { 'background': 'rgba(128, 128, 128, 0.1)', }, '.cm-scroller::-webkit-scrollbar-thumb': { 'background': 'rgba(128, 128, 128, 0.4)', 'border-radius': '6px', }, '.cm-scroller::-webkit-scrollbar-thumb:hover': { 'background': 'rgba(128, 128, 128, 0.6)', }, }), this.wrapCompartment.of(this.lineWrap ? EditorView.lineWrapping : []), EditorState.readOnly.of(this.isReadonly), ], }), parent: this.editorContainer.nativeElement, }); // Resize handler this.resizeHandler = () => { this.updateMinHeight(); }; window.addEventListener('resize', this.resizeHandler); } ngOnDestroy(): void { if (this.resizeHandler) { window.removeEventListener('resize', this.resizeHandler); } if (this.editorView) { this.editorView.destroy(); this.editorView = undefined; } } toggleWrap(): void { this.lineWrap = !this.lineWrap; if (this.editorView && this.wrapCompartment && this.EditorViewClass) { this.editorView.dispatch({ effects: this.wrapCompartment.reconfigure(this.lineWrap ? this.EditorViewClass.lineWrapping : []), }); } } save(format: boolean): void { try { const text = this.editorView.state.doc.toString(); const parsed = JSON.parse(text); const result = JSON.stringify(parsed, null, format ? (this.settings.jsonFormat() ?? 2) : 0); this.dialogRef.close({ obj: result }); } catch (e) { this.common.generalHandleError(e); } } close(): void { this.dialogRef.close(undefined); } private updateMinHeight(): void { const isMobile = this.breakpointObserver.isMatched('(max-width: 959px)'); this.minHeight = isMobile ? '100%' : `${Math.max(10, window.innerHeight - 100)}px`; } }