0" [sqxModalTarget]="input" class="control-dropdown" #container position="bottomLeft">
+
{{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 @@
-
+
+ 0" [sqxModalTarget]="form" class="control-dropdown" #container position="bottomLeft">
+
+ {{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());