From 29752323a625ec31ddcacdfd732bf3c8625c089b Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 30 Jul 2019 15:21:50 +0200 Subject: [PATCH] Dropdown improvements. (#396) * Dropdown improvements. * Fix disable state. * Dropdown fix for up and down arrow key. --- .../shared/field-editor.component.html | 3 +- .../shared/references-dropdown.component.ts | 72 ++++++++---- .../workflows/workflow-step.component.html | 2 +- .../angular/forms/autocomplete.component.html | 4 +- .../angular/forms/dropdown.component.html | 18 ++- .../angular/forms/dropdown.component.scss | 15 +++ .../angular/forms/dropdown.component.ts | 103 +++++++++++++++--- .../angular/forms/tag-editor.component.html | 4 +- .../app/framework/angular/highlight.pipe.ts | 24 ++++ .../angular/scroll-active.directive.ts | 2 +- .../angular/template-wrapper.directive.ts | 22 +++- src/Squidex/app/framework/declarations.ts | 1 + src/Squidex/app/framework/module.ts | 3 + 13 files changed, 218 insertions(+), 55 deletions(-) create mode 100644 src/Squidex/app/framework/angular/highlight.pipe.ts diff --git a/src/Squidex/app/features/content/shared/field-editor.component.html b/src/Squidex/app/features/content/shared/field-editor.component.html index 8bbb536f0..1eb16060a 100644 --- a/src/Squidex/app/features/content/shared/field-editor.component.html +++ b/src/Squidex/app/features/content/shared/field-editor.component.html @@ -92,7 +92,8 @@ + [schemaId]="field.properties['schemaId']" + [isRequired]="field.properties['isRequired']"> diff --git a/src/Squidex/app/features/content/shared/references-dropdown.component.ts b/src/Squidex/app/features/content/shared/references-dropdown.component.ts index b27d4debc..d2344fc08 100644 --- a/src/Squidex/app/features/content/shared/references-dropdown.component.ts +++ b/src/Squidex/app/features/content/shared/references-dropdown.component.ts @@ -16,7 +16,6 @@ import { ContentDto, ContentsService, getContentValue, - ImmutableArray, MathHelper, SchemaDetailsDto, SchemasService, @@ -31,28 +30,35 @@ export const SQX_REFERENCES_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = { interface State { schema?: SchemaDetailsDto | null; - contentItems: ImmutableArray; - contentNames: ImmutableArray; + contentItems: ContentDto[]; + contentNames: ContentName[]; + + selectedItem?: ContentName; } -type ContentName = { name: string, id: string }; +type ContentName = { name: string, id?: string }; @Component({ selector: 'sqx-references-dropdown', template: ` - `, + + + + + `, providers: [SQX_REFERENCES_DROPDOWN_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) export class ReferencesDropdownComponent extends StatefulControlComponent implements OnInit { private languageField: AppLanguageDto; + private selectedId: string | undefined; @Input() public schemaId: string; + @Input() + public isRequired = false; + @Input() public set language(value: AppLanguageDto) { this.languageField = value; @@ -60,7 +66,7 @@ export class ReferencesDropdownComponent extends StatefulControlComponent ({ ...s, contentNames: this.createContentNames(s.schema, s.contentItems) })); } - public selectedId = new FormControl(''); + public selectionControl = new FormControl(''); constructor(changeDetector: ChangeDetectorRef, private readonly appsState: AppsState, @@ -69,16 +75,16 @@ export class ReferencesDropdownComponent extends StatefulControlComponent { - if (value) { + this.selectionControl.valueChanges + .subscribe((value: ContentName) => { + if (value && value.id) { this.callTouched(); - this.callChange([value]); + this.callChange([value.id]); } else { this.callTouched(); this.callChange([]); @@ -88,7 +94,7 @@ export class ReferencesDropdownComponent extends StatefulControlComponent ({ schema, contents }))) .subscribe(({ schema, contents }) => { - const contentItems = ImmutableArray.of(contents.items); + const contentItems = contents.items; const contentNames = this.createContentNames(schema, contentItems); this.next(s => ({ ...s, schema, contentItems, contentNames })); + + this.selectContent(); }, () => { - this.selectedId.disable(); + this.selectionControl.disable(); }); } public writeValue(obj: any) { if (Types.isArrayOfString(obj)) { - this.selectedId.setValue(obj[0], { emitEvent: false }); + this.selectedId = obj[0]; + + this.selectContent(); } else { - this.selectedId.setValue(undefined, { emitEvent: false }); + this.selectedId = undefined; + + this.unselectContent(); } } - private createContentNames(schema: SchemaDetailsDto | undefined | null, contents: ImmutableArray): ImmutableArray { + private selectContent() { + this.selectionControl.setValue(this.snapshot.contentNames.find(x => x.id === this.selectedId), { emitEvent: false }); + } + + private unselectContent() { + this.selectionControl.setValue(undefined, { emitEvent: false }); + } + + private createContentNames(schema: SchemaDetailsDto | undefined | null, contents: ContentDto[]): ContentName[] { if (contents.length === 0 || !schema) { - return ImmutableArray.empty(); + return []; } - return contents.map(content => { + const names = contents.map(content => { const name = schema.referenceFields .map(f => getContentValue(content, this.languageField, f, false)) @@ -132,6 +152,12 @@ export class ReferencesDropdownComponent extends StatefulControlComponent
- +
{{target.name}}
diff --git a/src/Squidex/app/framework/angular/forms/autocomplete.component.html b/src/Squidex/app/framework/angular/forms/autocomplete.component.html index 5f6ee92fa..2ffa48211 100644 --- a/src/Squidex/app/framework/angular/forms/autocomplete.component.html +++ b/src/Squidex/app/framework/angular/forms/autocomplete.component.html @@ -9,10 +9,10 @@
+ [sqxScrollActive]="i === snapshot.suggestedIndex" + [sqxScrollContainer]="container"> {{item}} diff --git a/src/Squidex/app/framework/angular/forms/dropdown.component.html b/src/Squidex/app/framework/angular/forms/dropdown.component.html index 72ba5a1d4..7a6e5a071 100644 --- a/src/Squidex/app/framework/angular/forms/dropdown.component.html +++ b/src/Squidex/app/framework/angular/forms/dropdown.component.html @@ -16,11 +16,19 @@
-
-
- {{item}} - - +
+
+ +
+ +
+
+ {{item}} + + +
diff --git a/src/Squidex/app/framework/angular/forms/dropdown.component.scss b/src/Squidex/app/framework/angular/forms/dropdown.component.scss index a601a86ba..ebd9fdf83 100644 --- a/src/Squidex/app/framework/angular/forms/dropdown.component.scss +++ b/src/Squidex/app/framework/angular/forms/dropdown.component.scss @@ -17,6 +17,21 @@ $color-input-disabled: #eef1f4; } } +.search-form { + padding: .5rem; +} + +.control-dropdown { + max-width: 40rem; + max-height: none; + overflow-y: hidden; +} + +.control-dropdown-items { + overflow-y: auto; + max-height: 15rem; +} + .selection { & { position: relative; diff --git a/src/Squidex/app/framework/angular/forms/dropdown.component.ts b/src/Squidex/app/framework/angular/forms/dropdown.component.ts index 3e4451566..d8e69d699 100644 --- a/src/Squidex/app/framework/angular/forms/dropdown.component.ts +++ b/src/Squidex/app/framework/angular/forms/dropdown.component.ts @@ -5,18 +5,26 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, forwardRef, Input, QueryList, TemplateRef } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, forwardRef, Input, OnChanges, OnInit, QueryList, SimpleChanges, TemplateRef } from '@angular/core'; +import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { map } from 'rxjs/operators'; -import { Keys, ModalModel, StatefulControlComponent } from '@app/framework/internal'; +import { + Keys, + ModalModel, + StatefulControlComponent, + Types +} from '@app/framework/internal'; export const SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DropdownComponent), multi: true }; interface State { + suggestedItems: any[]; selectedItem: any; selectedIndex: number; + query?: string; } @Component({ @@ -26,10 +34,16 @@ interface State { providers: [SQX_DROPDOWN_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class DropdownComponent extends StatefulControlComponent implements AfterContentInit, ControlValueAccessor { +export class DropdownComponent extends StatefulControlComponent implements AfterContentInit, ControlValueAccessor, OnChanges, OnInit { @Input() public items: any[] = []; + @Input() + public searchProperty = 'name'; + + @Input() + public canSearch = true; + @ContentChildren(TemplateRef) public templates: QueryList; @@ -38,13 +52,60 @@ export class DropdownComponent extends StatefulControlComponent im public templateSelection: TemplateRef; public templateItem: TemplateRef; + public queryInput = new FormControl(); + constructor(changeDetector: ChangeDetectorRef) { super(changeDetector, { selectedItem: undefined, - selectedIndex: -1 + selectedIndex: -1, + suggestedItems: [] }); } + public ngOnInit() { + this.own( + this.queryInput.valueChanges.pipe( + map((query: string) => { + if (!this.items || !query) { + return { query, items: this.items }; + } else { + query = query.trim().toLocaleLowerCase(); + + const items = this.items.filter(x => { + if (Types.isString(x)) { + return x.toLocaleLowerCase().indexOf(query) >= 0; + } else { + const value: string = x[this.searchProperty]; + + return value && value.toLocaleLowerCase().indexOf(query) >= 0; + } + }); + + return { query, items }; + } + })) + .subscribe(({ query, items }) => { + this.next(s => ({ + ...s, + suggestedIndex: 0, + suggestedItems: items || [], + query + })); + })); + } + + public ngOnChanges(changes: SimpleChanges) { + if (changes['items']) { + this.resetSearch(); + + this.next(s => ({ + ...s, + suggestedIndex: 0, + suggestedItems: this.items || [] + })); + } + } + public ngAfterContentInit() { if (this.templates.length === 1) { this.templateItem = this.templateSelection = this.templates.first; @@ -64,7 +125,7 @@ export class DropdownComponent extends StatefulControlComponent im } public writeValue(obj: any) { - this.selectIndex(this.items && obj ? this.items.indexOf(obj) : 0); + this.selectIndex(this.items && obj ? this.items.indexOf(obj) : 0, false); } public onKeyDown(event: KeyboardEvent) { @@ -75,8 +136,10 @@ export class DropdownComponent extends StatefulControlComponent im case Keys.DOWN: this.down(); return false; - case Keys.ESCAPE: case Keys.ENTER: + this.selectIndexAndClose(this.snapshot.selectedIndex); + return false; + case Keys.ESCAPE: if (this.dropdown.isOpen) { this.close(); return false; @@ -87,13 +150,17 @@ export class DropdownComponent extends StatefulControlComponent im } public open() { + if (!this.dropdown.isOpen) { + this.resetSearch(); + } + this.dropdown.show(); this.callTouched(); } public selectIndexAndClose(selectedIndex: number) { - this.selectIndex(selectedIndex); + this.selectIndex(selectedIndex, true); this.close(); } @@ -102,12 +169,16 @@ export class DropdownComponent extends StatefulControlComponent im this.dropdown.hide(); } - public selectIndex(selectedIndex: number) { + private resetSearch() { + this.queryInput.setValue(''); + } + + public selectIndex(selectedIndex: number, emitEvents: boolean) { if (selectedIndex < 0) { selectedIndex = 0; } - const items = this.items || []; + const items = this.snapshot.suggestedItems || []; if (selectedIndex >= items.length) { selectedIndex = items.length - 1; @@ -116,10 +187,10 @@ export class DropdownComponent extends StatefulControlComponent im const value = items[selectedIndex]; if (value !== this.snapshot.selectedItem) { - selectedIndex = selectedIndex; - - this.callChange(value); - this.callTouched(); + if (emitEvents) { + this.callChange(value); + this.callTouched(); + } this.next(s => ({ ...s, selectedIndex, selectedItem: value })); } @@ -127,10 +198,10 @@ export class DropdownComponent extends StatefulControlComponent im } private up() { - this.selectIndex(this.snapshot.selectedIndex - 1); + this.selectIndex(this.snapshot.selectedIndex - 1, true); } private down() { - this.selectIndex(this.snapshot.selectedIndex + 1); + this.selectIndex(this.snapshot.selectedIndex + 1, true); } } \ 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 f3f386244..6e337b0a7 100644 --- a/src/Squidex/app/framework/angular/forms/tag-editor.component.html +++ b/src/Squidex/app/framework/angular/forms/tag-editor.component.html @@ -26,10 +26,10 @@
+ [sqxScrollActive]="i === snapshot.suggestedIndex" + [sqxScrollContainer]="container"> {{item}}
diff --git a/src/Squidex/app/framework/angular/highlight.pipe.ts b/src/Squidex/app/framework/angular/highlight.pipe.ts new file mode 100644 index 000000000..fa50592b9 --- /dev/null +++ b/src/Squidex/app/framework/angular/highlight.pipe.ts @@ -0,0 +1,24 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +// tslint:disable: no-pipe-impure + +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'sqxHighlight', + pure: false +}) +export class HighlightPipe implements PipeTransform { + public transform(text: string, highlight: string): string { + if (!highlight) { + return text; + } + + return text.replace(new RegExp(highlight, 'i'), s => `${s}`); + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/scroll-active.directive.ts b/src/Squidex/app/framework/angular/scroll-active.directive.ts index 56a998c8a..507defbb0 100644 --- a/src/Squidex/app/framework/angular/scroll-active.directive.ts +++ b/src/Squidex/app/framework/angular/scroll-active.directive.ts @@ -14,7 +14,7 @@ export class ScrollActiveDirective implements AfterViewInit, OnChanges { @Input('sqxScrollActive') public isActive = false; - @Input() + @Input('sqxScrollContainer') public container: HTMLElement; constructor( diff --git a/src/Squidex/app/framework/angular/template-wrapper.directive.ts b/src/Squidex/app/framework/angular/template-wrapper.directive.ts index cdce76d12..a7f7f1d56 100644 --- a/src/Squidex/app/framework/angular/template-wrapper.directive.ts +++ b/src/Squidex/app/framework/angular/template-wrapper.directive.ts @@ -17,6 +17,9 @@ export class TemplateWrapperDirective implements OnDestroy, OnInit, OnChanges { @Input() public index: number; + @Input() + public context: any; + @Input('sqxTemplateWrapper') public templateRef: TemplateRef; @@ -34,19 +37,30 @@ export class TemplateWrapperDirective implements OnDestroy, OnInit, OnChanges { } public ngOnInit() { - this.view = this.viewContainer.createEmbeddedView(this.templateRef, { + const { index, context } = this; + + const data = { '\$implicit': this.item, - 'index': this.index - }); + index, + context + }; + + this.view = this.viewContainer.createEmbeddedView(this.templateRef, data); } public ngOnChanges(changes: SimpleChanges) { if (this.view) { if (changes.item) { this.view.context.$implicit = this.item; - } else if (changes.index) { + } + + if (changes.index) { this.view.context.index = this.index; } + + if (changes.context) { + this.view.context.context = this.context; + } } } } \ No newline at end of file diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index 689aeaf68..c4e533657 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -56,6 +56,7 @@ export * from './angular/routers/parent-link.directive'; export * from './angular/code.component'; export * from './angular/external-link.directive'; export * from './angular/hover-background.directive'; +export * from './angular/highlight.pipe'; export * from './angular/ignore-scrollbar.directive'; export * from './angular/image-source.directive'; export * from './angular/panel.component'; diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index eb5937a90..d33de2fc2 100644 --- a/src/Squidex/app/framework/module.ts +++ b/src/Squidex/app/framework/module.ts @@ -44,6 +44,7 @@ import { FormHintComponent, FromNowPipe, FullDateTimePipe, + HighlightPipe, HoverBackgroundDirective, IFrameEditorComponent, IgnoreScrollbarDirective, @@ -126,6 +127,7 @@ import { FormHintComponent, FromNowPipe, FullDateTimePipe, + HighlightPipe, HoverBackgroundDirective, IFrameEditorComponent, IgnoreScrollbarDirective, @@ -194,6 +196,7 @@ import { FormsModule, FromNowPipe, FullDateTimePipe, + HighlightPipe, HoverBackgroundDirective, IFrameEditorComponent, IgnoreScrollbarDirective,