Browse Source

Dropdown improvements. (#396)

* Dropdown improvements.

* Fix disable state.

* Dropdown fix for up and down arrow key.
pull/397/head
Sebastian Stehle 7 years ago
committed by GitHub
parent
commit
29752323a6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      src/Squidex/app/features/content/shared/field-editor.component.html
  2. 72
      src/Squidex/app/features/content/shared/references-dropdown.component.ts
  3. 2
      src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html
  4. 4
      src/Squidex/app/framework/angular/forms/autocomplete.component.html
  5. 18
      src/Squidex/app/framework/angular/forms/dropdown.component.html
  6. 15
      src/Squidex/app/framework/angular/forms/dropdown.component.scss
  7. 103
      src/Squidex/app/framework/angular/forms/dropdown.component.ts
  8. 4
      src/Squidex/app/framework/angular/forms/tag-editor.component.html
  9. 24
      src/Squidex/app/framework/angular/highlight.pipe.ts
  10. 2
      src/Squidex/app/framework/angular/scroll-active.directive.ts
  11. 22
      src/Squidex/app/framework/angular/template-wrapper.directive.ts
  12. 1
      src/Squidex/app/framework/declarations.ts
  13. 3
      src/Squidex/app/framework/module.ts

3
src/Squidex/app/features/content/shared/field-editor.component.html

@ -92,7 +92,8 @@
<sqx-references-dropdown
[formControl]="control"
[language]="language"
[schemaId]="field.properties['schemaId']">
[schemaId]="field.properties['schemaId']"
[isRequired]="field.properties['isRequired']">
</sqx-references-dropdown>
</ng-container>
</ng-container>

72
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<ContentDto>;
contentNames: ImmutableArray<ContentName>;
contentItems: ContentDto[];
contentNames: ContentName[];
selectedItem?: ContentName;
}
type ContentName = { name: string, id: string };
type ContentName = { name: string, id?: string };
@Component({
selector: 'sqx-references-dropdown',
template: `
<select class="form-control" [formControl]="selectedId">
<option [ngValue]="null"></option>
<option *ngFor="let content of snapshot.contentNames" [ngValue]="content.id">{{content.name}}</option>
</select>`,
<sqx-dropdown [formControl]="selectionControl" [items]="snapshot.contentNames">
<ng-template let-content="$implicit" let-context="context">
<span class="truncate" [innerHTML]="content.name | sqxHighlight:context"></span>
</ng-template>
</sqx-dropdown>`,
providers: [SQX_REFERENCES_DROPDOWN_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ReferencesDropdownComponent extends StatefulControlComponent<State, string[]> 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<State,
this.next(s => ({ ...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<State,
) {
super(changeDetector, {
schema: null,
contentItems: ImmutableArray.empty(),
contentNames: ImmutableArray.empty()
contentItems: [],
contentNames: []
});
this.own(
this.selectedId.valueChanges
.subscribe(value => {
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<State,
public ngOnInit() {
if (this.schemaId === MathHelper.EMPTY_GUID) {
this.selectedId.disable();
this.selectionControl.disable();
return;
}
@ -101,29 +107,43 @@ export class ReferencesDropdownComponent extends StatefulControlComponent<State,
}
}, (schema, contents) => ({ 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<ContentDto>): ImmutableArray<ContentName> {
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<State,
return { name, id: content.id };
});
if (!this.isRequired) {
return [{ name: '- No Reference -' }, ...names];
}
return names;
}
public trackByContent(content: ContentDto) {

2
src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html

@ -49,7 +49,7 @@
<i class="icon-corner-down-right text-decent"></i>
</div>
<div class="col col-step">
<sqx-dropdown [items]="openSteps" [(ngModel)]="openStep">
<sqx-dropdown [items]="openSteps" [(ngModel)]="openStep" [canSearch]="false">
<ng-template let-target="$implicit">
<div class="color-circle" [style.background]="target.color"></div> {{target.name}}
</ng-template>

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

@ -9,10 +9,10 @@
<div class="control-dropdown" [sqxAnchoredTo]="input" position="bottom-left" #container @fade>
<div *ngFor="let item of snapshot.suggestedItems; let i = index" class="control-dropdown-item control-dropdown-item-selectable"
[class.active]="i === snapshot.suggestedIndex"
[container]="container"
(mousedown)="selectItem(item)"
(mouseover)="selectIndex(i)"
[sqxScrollActive]="i === snapshot.suggestedIndex">
[sqxScrollActive]="i === snapshot.suggestedIndex"
[sqxScrollContainer]="container">
<ng-container *ngIf="!itemTemplate">{{item}}</ng-container>

18
src/Squidex/app/framework/angular/forms/dropdown.component.html

@ -16,11 +16,19 @@
<div class="items-container">
<ng-container *sqxModal="dropdown">
<div class="control-dropdown" [sqxAnchoredTo]="input" position="bottom-left" #container>
<div *ngFor="let item of items; let i = index;" class="control-dropdown-item control-dropdown-item-selectable" [class.active]="i === snapshot.selectedIndex" (mousedown)="selectIndexAndClose(i)" [sqxScrollActive]="i === snapshot.selectedIndex" [container]="container">
<ng-container *ngIf="!templateItem">{{item}}</ng-container>
<ng-template *ngIf="templateItem" [sqxTemplateWrapper]="templateItem" [item]="item" [index]="i"></ng-template>
<div class="control-dropdown" [sqxAnchoredTo]="input" position="bottom-left">
<div *ngIf="canSearch" class="search-form">
<input class="form-control search" [formControl]="queryInput" [disabled]="snapshot.isDisabled" placeholder="Search" (keydown)="onKeyDown($event)" sqxFocusOnInit />
</div>
<div class="control-dropdown-items" #container>
<div *ngFor="let item of snapshot.suggestedItems; let i = index;" class="control-dropdown-item control-dropdown-item-selectable" [class.active]="i === snapshot.selectedIndex" (mousedown)="selectIndexAndClose(i)"
[sqxScrollActive]="i === snapshot.selectedIndex"
[sqxScrollContainer]="container">
<ng-container *ngIf="!templateItem">{{item}}</ng-container>
<ng-template *ngIf="templateItem" [sqxTemplateWrapper]="templateItem" [item]="item" [index]="i" [context]="snapshot.query"></ng-template>
</div>
</div>
</div>
</ng-container>

15
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;

103
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<State, any[]> implements AfterContentInit, ControlValueAccessor {
export class DropdownComponent extends StatefulControlComponent<State, any[]> implements AfterContentInit, ControlValueAccessor, OnChanges, OnInit {
@Input()
public items: any[] = [];
@Input()
public searchProperty = 'name';
@Input()
public canSearch = true;
@ContentChildren(TemplateRef)
public templates: QueryList<any>;
@ -38,13 +52,60 @@ export class DropdownComponent extends StatefulControlComponent<State, any[]> im
public templateSelection: TemplateRef<any>;
public templateItem: TemplateRef<any>;
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<State, any[]> 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<State, any[]> 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<State, any[]> 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<State, any[]> 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<State, any[]> 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<State, any[]> 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);
}
}

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

@ -26,10 +26,10 @@
<div class="control-dropdown" [sqxAnchoredTo]="form" position="bottom-left" #container @fade>
<div *ngFor="let item of snapshot.suggestedItems; let i = index" class="control-dropdown-item control-dropdown-item-selectable"
[class.active]="i === snapshot.suggestedIndex"
[container]="container"
(mousedown)="selectValue(item)"
(mouseover)="selectIndex(i)"
[sqxScrollActive]="i === snapshot.suggestedIndex">
[sqxScrollActive]="i === snapshot.suggestedIndex"
[sqxScrollContainer]="container">
<ng-container>{{item}}</ng-container>
</div>
</div>

24
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 => `<b>${s}</b>`);
}
}

2
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(

22
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<any>;
@ -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;
}
}
}
}

1
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';

3
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,

Loading…
Cancel
Save