diff --git a/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.ts index 373bc680a..e952312da 100644 --- a/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/dictionary-overview/dictionary-overview-screen.component.ts @@ -89,7 +89,7 @@ export class DictionaryOverviewScreenComponent extends ComponentHasChanges imple if (file) { fileReader.onload = () => { - this._dictionaryManager.editorValue = fileReader.result; + this._dictionaryManager.editorValue = fileReader.result as string; this._fileInput.nativeElement.value = null; }; fileReader.readAsText(file); diff --git a/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.html b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.html index ce0578af2..ada7c69d8 100644 --- a/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.html +++ b/apps/red-ui/src/app/modules/shared/components/dictionary-manager/dictionary-manager.component.html @@ -16,7 +16,7 @@
- {{ currentMatch + '/' + searchPositions.length }} + {{ currentMatch + '/' + searchMatches.length }}
- - - - - - - - - - - + +
- - - - - - - - - -
(); - editDecorations: IModelDeltaDecoration[] = []; - searchDecorations: IModelDeltaDecoration[] = []; - searchPositions: ISearchPosition[] = []; - currentMatch = 1; + currentMatch = 0; + searchMatches: FindMatch[] = []; currentEntries: string[] = []; - changedLines: number[] = []; - editorOptions = { theme: 'vs-light', language: 'text/plain', automaticLayout: true }; + editorOptions: IStandaloneEditorConstructionOptions = { + theme: 'vs', + language: 'text/plain', + automaticLayout: true, + readOnly: !this.canEdit + }; diffEditorText = ''; + showDiffEditor = false; searchText = ''; selectRuleSet = { name: 'dictionary-overview.compare.select-ruleset' }; @@ -55,13 +50,15 @@ export class DictionaryManagerComponent implements OnChanges { private _codeEditor: ICodeEditor; private _diffEditor: IDiffEditor; + private _decorations: string[] = []; + private _searchDecorations: string[] = []; constructor( private readonly _dictionaryControllerService: DictionaryControllerService, private readonly _appStateService: AppStateService, - private readonly _formBuilder: FormBuilder, - private readonly _changeDetector: ChangeDetectorRef + private readonly _formBuilder: FormBuilder ) { + this.currentEntries = this.initialEntries; this.form = this._formBuilder.group({ active: [false], ruleSet: [{ value: this.selectRuleSet, disabled: true }], @@ -83,8 +80,17 @@ export class DictionaryManagerComponent implements OnChanges { this._onRuleSetChanged(); }); + this.form.controls.active.valueChanges.subscribe((value) => { + this.showDiffEditor = + value && this.form.get('dictionary').value !== this.selectDictionary; + }); + this.form.controls.dictionary.valueChanges.subscribe((dictionary) => { - this._onDictionaryChanged(dictionary); + this._onDictionaryChanged(dictionary).subscribe((data) => { + this.diffEditorText = data; + this.showDiffEditor = dictionary !== this.selectDictionary; + if (this.showDiffEditor) this._diffEditor?.getOriginalEditor().setValue(data); + }); }); } @@ -94,21 +100,23 @@ export class DictionaryManagerComponent implements OnChanges { onCodeEditorInit(editor: ICodeEditor): void { this._codeEditor = editor; + (window as any).monaco.editor.defineTheme('redaction', { + base: 'vs', + inherit: true, + rules: [], + colors: { + 'editor.lineHighlightBackground': '#f4f5f7' + } + }); + (window as any).monaco.editor.setTheme('redaction'); } - get showDiffEditor(): boolean { - return ( - this.form.get('active').value && - this.form.get('dictionary').value !== this.selectDictionary - ); + get editorValue(): string { + return this._codeEditor?.getModel()?.getValue(); } - get editorValue() { - return this._codeEditor.getModel().getValue(); - } - - set editorValue(value: any) { - this._codeEditor.getModel().setValue(value); + set editorValue(value: string) { + this._codeEditor?.getModel()?.setValue(value); } revert() { @@ -119,71 +127,88 @@ export class DictionaryManagerComponent implements OnChanges { @debounce() searchChanged(text: string) { this.searchText = text.toLowerCase(); + this.searchMatches = this._getMatches(this.searchText); this._applySearchDecorations(); + this.currentMatch = 0; this.nextSearchMatch(); } + private _applySearchDecorations() { + this._searchDecorations = this._codeEditor.deltaDecorations(this._searchDecorations, []); + + const decorations = this.searchMatches.map( + (match) => + ({ + range: match.range, + options: { inlineClassName: 'search-marker' } + } as IModelDeltaDecoration) + ); + + this._searchDecorations = this._codeEditor.deltaDecorations( + this._searchDecorations, + decorations + ); + } + + private _getMatches(text: string): FindMatch[] { + const model = this._codeEditor?.getModel(); + return model.findMatches(text, false, false, false, null, false) || []; + } + get codeEditorText(): string { return this.currentEntries.join('\n'); } set codeEditorText(text: string) { - this._applySearchDecorations(); this.currentEntries = text.split('\n'); - this.changedLines = []; - this.editDecorations = []; - this._codeEditor.deltaDecorations([], this.editDecorations); - - this.currentEntries.forEach((entry, index) => { - if (this.initialEntries.indexOf(entry) < 0) { - this.changedLines.push(index); - } - }); - - this.changedLines.forEach((line) => this._makeDecorations(line)); - this._codeEditor.deltaDecorations([], this.editDecorations); + this.codeEditorTextChanged(); } - private _makeDecorations(line: number) { - const entry = this.currentEntries[line]; - if (entry.trim().length === 0) return; - const wholeLine = new monaco.Range(line, 0, line, 1); - const decoration: IModelDeltaDecoration = { - range: wholeLine, - options: { isWholeLine: true, inlineClassName: 'changed-row-marker' } - }; - this.editDecorations.push(decoration); - if (entry.trim().length < MIN_WORD_LENGTH) { - this.editDecorations.push({ - range: wholeLine, - options: { isWholeLine: true, inlineClassName: 'too-short-marker' } - }); - } + @debounce() + codeEditorTextChanged() { + const newDecorations = this.currentEntries + .filter((entry) => this._isNew(entry)) + .map((entry) => this._makeDecorationFor(entry)); + + this._decorations = this._codeEditor.deltaDecorations(this._decorations, newDecorations); } - get hasChanges() { + private _isNew(entry: string): boolean { + return this.initialEntries.indexOf(entry) < 0 && entry?.trim().length > 0; + } + + private _makeDecorationFor(entry: string): IModelDeltaDecoration { + const line = this.currentEntries.indexOf(entry) + 1; + const cssClass = entry.length < MIN_WORD_LENGTH ? 'too-short-marker' : 'changed-row-marker'; + + return { + range: new monaco.Range(line, 1, line, 1), + options: { isWholeLine: true, className: cssClass } + } as IModelDeltaDecoration; + } + + get hasChanges(): boolean { return ( this.currentEntries.length && - (this.editDecorations.length > 0 || - this.currentEntries.filter((e) => e.trim().length > 0).length < - this.initialEntries.length) + this.currentEntries.filter((e) => e.trim().length > 0).length < + this.initialEntries.length ); } - nextSearchMatch() { - if (this.searchPositions.length > 0) { + nextSearchMatch(): void { + if (this.searchMatches?.length > 0) { this.currentMatch = - this.currentMatch < this.searchPositions.length ? this.currentMatch + 1 : 1; - this._gotoLine(); + this.currentMatch < this.searchMatches.length ? this.currentMatch + 1 : 1; + this._scrollToCurrentMatch(); } } - previousSearchMatch() { - if (this.searchPositions.length > 0) { + previousSearchMatch(): void { + if (this.searchMatches.length > 0) { this.currentMatch = - this.currentMatch > 1 ? this.currentMatch - 1 : this.searchPositions.length; - this._gotoLine(); + this.currentMatch > 1 ? this.currentMatch - 1 : this.searchMatches.length; + this._scrollToCurrentMatch(); } } @@ -206,33 +231,11 @@ export class DictionaryManagerComponent implements OnChanges { ]; } - private _applySearchDecorations() { - this.searchPositions = this._getSearchPositions(); - this.searchDecorations = this.searchPositions.map(({ row, column, length }) => ({ - range: new monaco.Range(row, column, row, column + length), - options: { inlineClassName: 'search-marker' } - })); + private _scrollToCurrentMatch(): void { + const range = this.searchMatches[this.currentMatch - 1].range; - this._codeEditor.deltaDecorations([], this.searchDecorations); - } - - private _getSearchPositions(): ISearchPosition[] { - const searchText = this.searchText.toLowerCase(); - return this.currentEntries - .map((val, index) => { - const columnIndex = val.toLowerCase().indexOf(searchText); - if (columnIndex >= 0) { - return { row: index, column: columnIndex, length: searchText.length }; - } - }) - .filter((entry) => !!entry); - } - - private _gotoLine() { - const position = this.searchPositions[this.currentMatch - 1]; - this._codeEditor.setScrollPosition({ scrollTop: position.row }); - // this._editorComponent.getEditor().scrollToLine(position.row, true, true, () => {}); - // this._editorComponent.getEditor().gotoLine(position.row + 1, position.column, true); + this._codeEditor.setSelection(range); + this._codeEditor.revealLineInCenter(range.startLineNumber, SMOOTH_SCROLL); } private _onRuleSetChanged() { @@ -240,20 +243,19 @@ export class DictionaryManagerComponent implements OnChanges { this.form.patchValue({ dictionary: this.selectDictionary }); } - private _onDictionaryChanged(dictionary: TypeValue) { + private _onDictionaryChanged(dictionary: TypeValue): Observable { if (dictionary === this.selectDictionary) { - return; + return of(''); } - this._dictionaryControllerService + return this._dictionaryControllerService .getDictionaryForType(dictionary.ruleSetId, dictionary.type) - .subscribe((data) => { - this.diffEditorText = data.entries - .sort((str1, str2) => - str1.localeCompare(str2, undefined, { sensitivity: 'accent' }) - ) - .join('\n'); - this._changeDetector.detectChanges(); - }); + .pipe(map((data) => this._toString(data.entries))); + } + + private _toString(entries: string[]) { + return entries + .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'accent' })) + .join('\n'); } saveEntries() { diff --git a/apps/red-ui/src/assets/styles/red-editor.scss b/apps/red-ui/src/assets/styles/red-editor.scss index cb53946c0..920726438 100644 --- a/apps/red-ui/src/assets/styles/red-editor.scss +++ b/apps/red-ui/src/assets/styles/red-editor.scss @@ -2,19 +2,16 @@ @import 'red-mixins'; .changed-row-marker { - position: absolute; background: rgba($primary, 0.1); z-index: 20; } .too-short-marker { - position: absolute; background: rgba($primary, 0.5); z-index: 30; } .search-marker { - position: absolute; background: rgba($blue-5, 0.3); z-index: 40; }