Browse Source

Autocompletion for tags

pull/310/head
Sebastian 8 years ago
parent
commit
57cd8c7e52
  1. 9
      src/Squidex/app/framework/angular/forms/autocomplete.component.html
  2. 38
      src/Squidex/app/framework/angular/forms/autocomplete.component.ts
  3. 46
      src/Squidex/app/framework/angular/forms/tag-editor.component.html
  4. 132
      src/Squidex/app/framework/angular/forms/tag-editor.component.ts
  5. 8
      src/Squidex/app/shared/components/asset.component.html
  6. 3
      src/Squidex/app/shared/components/asset.component.ts
  7. 3
      src/Squidex/app/shared/components/assets-list.component.html
  8. 4
      src/Squidex/app/shared/state/assets.state.ts

9
src/Squidex/app/framework/angular/forms/autocomplete.component.html

@ -5,8 +5,13 @@
autocorrect="off"
autocapitalize="off">
<div *ngIf="items.length > 0" [sqxModalTarget]="input" class="control-dropdown" #container position="bottomLeft">
<div *ngFor="let item of items; let i = index;" class="control-dropdown-item control-dropdown-item-selectable" [class.active]="i === selectedIndex" (mousedown)="selectItem(item)" (mouseover)="selectIndex(i)" [sqxScrollActive]="i === selectedIndex" [container]="container">
<div *ngIf="suggestedItems.length > 0" [sqxModalTarget]="input" class="control-dropdown" #container position="bottomLeft">
<div *ngFor="let item of suggestedItems; let i = index;" class="control-dropdown-item control-dropdown-item-selectable"
[class.active]="i === suggestedIndex"
[container]="container"
(mousedown)="selectItem(item)"
(mouseover)="selectIndex(i)"
[sqxScrollActive]="i === suggestedIndex">
<ng-container *ngIf="!itemTemplate">{{item}}</ng-container>
<ng-template *ngIf="itemTemplate" [sqxTemplateWrapper]="itemTemplate" [item]="item" [index]="i"></ng-template>

38
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<any>;
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;
}
}

46
src/Squidex/app/framework/angular/forms/tag-editor.component.html

@ -1,18 +1,30 @@
<div class="form-control {{class}}" (click)="input.focus()" [class.focus]="hasFocus" [class.disabled]="addInput.disabled">
<span class="item" *ngFor="let item of items; let i = index" [class.disabled]="addInput.disabled">
{{item}} <i class="icon-close" (click)="remove(i)"></i>
</span>
<ng-container>
<div class="form-control {{class}}" #form (click)="input.focus()" [class.focus]="hasFocus" [class.disabled]="addInput.disabled">
<span class="item" *ngFor="let item of items; let i = index" [class.disabled]="addInput.disabled">
{{item}} <i class="icon-close" (click)="remove(i)"></i>
</span>
<input type="text" class="blank" #input
(blur)="markTouched()"
(focus)="focus()"
(input)="adjustSize()"
(keydown)="onKeyDown($event)"
[formControl]="addInput"
[attr.name]="inputName"
[attr.placeholder]="placeholder"
[class.hidden]="addInput.disabled"
autocomplete="off"
autocorrect="off"
autocapitalize="off">
</div>
<input type="text" class="blank" #input
(blur)="markTouched()"
(focus)="focus()"
(keydown)="onKeyDown($event)"
[formControl]="addInput"
[attr.name]="inputName"
[attr.placeholder]="placeholder"
[class.hidden]="addInput.disabled"
autocomplete="off"
autocorrect="off"
autocapitalize="off">
</div>
<div *ngIf="suggestedItems.length > 0" [sqxModalTarget]="form" class="control-dropdown" #container position="bottomLeft">
<div *ngFor="let item of suggestedItems; let i = index;" class="control-dropdown-item control-dropdown-item-selectable"
[class.active]="i === suggestedIndex"
[container]="container"
(mousedown)="selectValue(item)"
(mouseover)="selectIndex(i)"
[sqxScrollActive]="i === suggestedIndex">
<ng-container>{{item}}</ng-container>
</div>
</div>
</ng-container>

132
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 => <string>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 = <string>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 = <string>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);

8
src/Squidex/app/shared/components/asset.component.html

@ -68,7 +68,13 @@
</div>
</div>
<div class="file-tags tags">
<sqx-tag-editor [acceptEnter]="true" [useDefaultValue]="false" [formControl]="tagInput" class="blank"></sqx-tag-editor>
<sqx-tag-editor
[suggestions]="allTags"
[acceptEnter]="true"
[allowDuplicates]="false"
[undefinedWhenEmpty]="false"
[formControl]="tagInput" class="blank">
</sqx-tag-editor>
</div>
<div class="file-info">
<ng-container *ngIf="asset.pixelWidth">{{asset.pixelWidth}}x{{asset.pixelHeight}}px, </ng-container> {{asset.fileSize | sqxFileSize}}

3
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<AssetDto>();

3
src/Squidex/app/shared/components/assets-list.component.html

@ -14,7 +14,7 @@
<div class="file-drop-info">Drop file on existing item to replace the asset with a newer version.</div>
</div>
<div class="row assets">
<div class="row assets" *ngIf="state.tagsNames | async; let tags">
<sqx-asset *ngFor="let file of newFiles" [initFile]="file"
(failed)="remove(file)"
(loaded)="add(file, $event)">
@ -25,6 +25,7 @@
[isDisabled]="isDisabled"
[isSelectable]="selectedIds"
[isSelected]="isSelected(asset)"
[allTags]="tags"
(updated)="update($event)"
(selected)="select($event)"
(deleting)="delete($event)">

4
src/Squidex/app/shared/state/assets.state.ts

@ -37,6 +37,10 @@ export class AssetsState extends State<Snapshot> {
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());

Loading…
Cancel
Save