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