diff --git a/src/Squidex/app/framework/angular/forms/autocomplete.component.html b/src/Squidex/app/framework/angular/forms/autocomplete.component.html index b35a6ca17..882956d72 100644 --- a/src/Squidex/app/framework/angular/forms/autocomplete.component.html +++ b/src/Squidex/app/framework/angular/forms/autocomplete.component.html @@ -5,8 +5,13 @@ autocorrect="off" autocapitalize="off"> -
-
+
+
{{item}} diff --git a/src/Squidex/app/framework/angular/forms/autocomplete.component.ts b/src/Squidex/app/framework/angular/forms/autocomplete.component.ts index 5d4592462..a8666a9cf 100644 --- a/src/Squidex/app/framework/angular/forms/autocomplete.component.ts +++ b/src/Squidex/app/framework/angular/forms/autocomplete.component.ts @@ -49,9 +49,8 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O @ContentChild(TemplateRef) public itemTemplate: TemplateRef; - public items: any[] = []; - - public selectedIndex = -1; + public suggestedItems: any[] = []; + public suggestedIndex = -1; public queryInput = new FormControl(); @@ -72,14 +71,13 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O this.reset(); } }), - distinctUntilChanged(), debounceTime(200), + distinctUntilChanged(), filter(query => !!query && !!this.source), - switchMap(query => this.source.find(query)), - catchError(error => of([]))) + switchMap(query => this.source.find(query)), catchError(() => of([]))) .subscribe(items => { - this.reset(); - this.items = items || []; + this.suggestedIndex = -1; + this.suggestedItems = items || []; }); } @@ -96,7 +94,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O this.reset(); return false; case KEY_ENTER: - if (this.items.length > 0) { + if (this.suggestedItems.length > 0) { this.selectItem(); return false; } @@ -110,7 +108,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O if (!obj) { this.resetForm(); } else { - const item = this.items.find(i => i === obj); + const item = this.suggestedItems.find(i => i === obj); if (item) { this.queryInput.setValue(obj.title || ''); @@ -144,11 +142,11 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O public selectItem(selection: any | null = null) { if (!selection) { - selection = this.items[this.selectedIndex]; + selection = this.suggestedItems[this.suggestedIndex]; } - if (!selection && this.items.length === 1) { - selection = this.items[0]; + if (!selection && this.suggestedItems.length === 1) { + selection = this.suggestedItems[0]; } if (selection) { @@ -170,19 +168,19 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O selection = 0; } - if (selection >= this.items.length) { - selection = this.items.length - 1; + if (selection >= this.suggestedItems.length) { + selection = this.suggestedItems.length - 1; } - this.selectedIndex = selection; + this.suggestedIndex = selection; } private up() { - this.selectIndex(this.selectedIndex - 1); + this.selectIndex(this.suggestedIndex - 1); } private down() { - this.selectIndex(this.selectedIndex + 1); + this.selectIndex(this.suggestedIndex + 1); } private resetForm() { @@ -190,7 +188,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O } private reset() { - this.items = []; - this.selectedIndex = -1; + this.suggestedItems = []; + this.suggestedIndex = -1; } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.html b/src/Squidex/app/framework/angular/forms/tag-editor.component.html index b55d1e014..737f2201a 100644 --- a/src/Squidex/app/framework/angular/forms/tag-editor.component.html +++ b/src/Squidex/app/framework/angular/forms/tag-editor.component.html @@ -1,18 +1,30 @@ -
- - {{item}} - + +
+ + {{item}} + - -
\ No newline at end of file + +
+ +
+
+ {{item}} +
+
+ \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.ts b/src/Squidex/app/framework/angular/forms/tag-editor.component.ts index 398016b00..ba7808850 100644 --- a/src/Squidex/app/framework/angular/forms/tag-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/tag-editor.component.ts @@ -5,14 +5,18 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core'; +import { Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Subscription } from 'rxjs'; +import { distinctUntilChanged, map, tap } from 'rxjs/operators'; import { Types } from '@app/framework/internal'; const KEY_COMMA = 188; const KEY_DELETE = 8; const KEY_ENTER = 13; +const KEY_UP = 38; +const KEY_DOWN = 40; export interface Converter { convert(input: string): any; @@ -73,7 +77,8 @@ export const SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR: any = { templateUrl: './tag-editor.component.html', providers: [SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR] }) -export class TagEditorComponent implements ControlValueAccessor { +export class TagEditorComponent implements ControlValueAccessor, OnDestroy, OnInit { + private subscription: Subscription; private callChange = (v: any) => { /* NOOP */ }; private callTouched = () => { /* NOOP */ }; @@ -81,11 +86,17 @@ export class TagEditorComponent implements ControlValueAccessor { public converter: Converter = new StringConverter(); @Input() - public useDefaultValue = true; + public undefinedWhenEmpty = true; @Input() public acceptEnter = false; + @Input() + public allowDuplicates = true; + + @Input() + public suggestions: string[] = []; + @Input() public class: string; @@ -100,10 +111,44 @@ export class TagEditorComponent implements ControlValueAccessor { public hasFocus = false; + public suggestedItems: string[] = []; + public suggestedIndex = 0; + public items: any[] = []; public addInput = new FormControl(); + public ngOnDestroy() { + this.subscription.unsubscribe(); + } + + public ngOnInit() { + this.subscription = + this.addInput.valueChanges.pipe( + tap(() => { + this.adjustSize(); + }), + map(query => query), + map(query => query ? query.trim() : query), + tap(query => { + if (!query) { + this.resetAutocompletion(); + } + }), + distinctUntilChanged(), + map(query => { + if (Types.isArray(this.suggestions) && query && query.length > 0) { + return this.suggestions.filter(s => s.indexOf(query) >= 0 && this.items.indexOf(s) < 0); + } else { + return []; + } + })) + .subscribe(items => { + this.suggestedIndex = -1; + this.suggestedItems = items || []; + }); + } + public writeValue(obj: any) { this.resetForm(); @@ -136,12 +181,6 @@ export class TagEditorComponent implements ControlValueAccessor { } } - private resetForm() { - this.addInput.reset(); - - this.adjustSize(); - } - public markTouched() { this.callTouched(); @@ -171,17 +210,13 @@ export class TagEditorComponent implements ControlValueAccessor { } public onKeyDown(event: KeyboardEvent) { - if (event.keyCode === KEY_COMMA || (event.keyCode === KEY_ENTER && this.acceptEnter)) { - const value = this.addInput.value; - - if (value && this.converter.isValidInput(value)) { - const converted = this.converter.convert(value); + const key = event.keyCode; - this.updateItems([...this.items, converted]); - this.resetForm(); + if (key === KEY_COMMA) { + if (this.selectValue(this.addInput.value)) { return false; } - } else if (event.keyCode === KEY_DELETE) { + } else if (key === KEY_DELETE) { const value = this.addInput.value; if (!value || value.length === 0) { @@ -189,15 +224,74 @@ export class TagEditorComponent implements ControlValueAccessor { return false; } + } else if (key === KEY_UP) { + this.up(); + return false; + } else if (key === KEY_DOWN) { + this.down(); + return false; + } else if (key === KEY_ENTER) { + if (this.suggestedIndex >= 0) { + if (this.selectValue(this.suggestedItems[this.suggestedIndex])) { + return false; + } + } else if (this.acceptEnter) { + if (this.selectValue(this.addInput.value)) { + return false; + } + } } return true; } - private updateItems(items: string[]) { + public selectValue(value: string) { + if (value && this.converter.isValidInput(value)) { + const converted = this.converter.convert(value); + + if (this.allowDuplicates || this.items.indexOf(converted) < 0) { + this.updateItems([...this.items, converted]); + } + + this.resetForm(); + this.resetAutocompletion(); + return true; + } + } + + private resetAutocompletion() { + this.suggestedItems = []; + this.suggestedIndex = -1; + } + + public selectIndex(selection: number) { + if (selection < 0) { + selection = 0; + } + + if (selection >= this.items.length) { + selection = this.items.length - 1; + } + + this.suggestedIndex = selection; + } + + private resetForm() { + this.addInput.reset(); + } + + private up() { + this.selectIndex(this.suggestedIndex - 1); + } + + private down() { + this.selectIndex(this.suggestedIndex + 1); + } + + private updateItems(items: any[]) { this.items = items; - if (items.length === 0 && this.useDefaultValue) { + if (items.length === 0 && this.undefinedWhenEmpty) { this.callChange(undefined); } else { this.callChange(this.items); diff --git a/src/Squidex/app/shared/components/asset.component.html b/src/Squidex/app/shared/components/asset.component.html index 243cc67f2..89b3a18a6 100644 --- a/src/Squidex/app/shared/components/asset.component.html +++ b/src/Squidex/app/shared/components/asset.component.html @@ -68,7 +68,13 @@
- + +
{{asset.pixelWidth}}x{{asset.pixelHeight}}px, {{asset.fileSize | sqxFileSize}} diff --git a/src/Squidex/app/shared/components/asset.component.ts b/src/Squidex/app/shared/components/asset.component.ts index e69f17f6e..58bb509aa 100644 --- a/src/Squidex/app/shared/components/asset.component.ts +++ b/src/Squidex/app/shared/components/asset.component.ts @@ -54,6 +54,9 @@ export class AssetComponent implements OnDestroy, OnInit { @Input() public isSelectable = false; + @Input() + public allTags: string[]; + @Output() public loaded = new EventEmitter(); diff --git a/src/Squidex/app/shared/components/assets-list.component.html b/src/Squidex/app/shared/components/assets-list.component.html index 1f9899e42..7b504e6ff 100644 --- a/src/Squidex/app/shared/components/assets-list.component.html +++ b/src/Squidex/app/shared/components/assets-list.component.html @@ -14,7 +14,7 @@
Drop file on existing item to replace the asset with a newer version.
-
+
@@ -25,6 +25,7 @@ [isDisabled]="isDisabled" [isSelectable]="selectedIds" [isSelected]="isSelected(asset)" + [allTags]="tags" (updated)="update($event)" (selected)="select($event)" (deleting)="delete($event)"> diff --git a/src/Squidex/app/shared/state/assets.state.ts b/src/Squidex/app/shared/state/assets.state.ts index 933924581..45457abc0 100644 --- a/src/Squidex/app/shared/state/assets.state.ts +++ b/src/Squidex/app/shared/state/assets.state.ts @@ -37,6 +37,10 @@ export class AssetsState extends State { this.changes.pipe(map(x => x.tags), distinctUntilChanged(), map(x => sort(x))); + public tagsNames = + this.tags.pipe( + distinctUntilChanged(), map(x => x.map(t => t.name))); + public assets = this.changes.pipe(map(x => x.assets), distinctUntilChanged());