1- import { ChangeDetectorRef , Component , EventEmitter , Input , OnInit , Output } from '@angular/core' ;
1+ import {
2+ ChangeDetectorRef ,
3+ Component ,
4+ EventEmitter ,
5+ Input ,
6+ OnInit ,
7+ Output ,
8+ ViewChild ,
9+ ElementRef
10+ } from '@angular/core' ;
211import { UntypedFormGroup } from '@angular/forms' ;
312
413import { Observable , of as observableOf } from 'rxjs' ;
5- import { catchError , distinctUntilChanged , map , tap } from 'rxjs/operators' ;
14+ import { catchError , map , tap } from 'rxjs/operators' ;
615import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' ;
716import { DynamicFormLayoutService , DynamicFormValidationService } from '@ng-dynamic-forms/core' ;
817
@@ -28,6 +37,8 @@ import { FormFieldMetadataValueObject } from '../../../models/form-field-metadat
2837 templateUrl : './dynamic-scrollable-dropdown.component.html'
2938} )
3039export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyComponent implements OnInit {
40+ @ViewChild ( 'dropdownMenu' , { read : ElementRef } ) dropdownMenu : ElementRef ;
41+
3142 @Input ( ) bindId = true ;
3243 @Input ( ) group : UntypedFormGroup ;
3344 @Input ( ) model : DynamicScrollableDropdownModel ;
@@ -40,6 +51,9 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
4051 public loading = false ;
4152 public pageInfo : PageInfo ;
4253 public optionsList : any ;
54+ public inputText : string = null ;
55+ public selectedIndex = 0 ;
56+ public acceptableKeys = [ 'Space' , 'NumpadMultiply' , 'NumpadAdd' , 'NumpadSubtract' , 'NumpadDecimal' , 'Semicolon' , 'Equal' , 'Comma' , 'Minus' , 'Period' , 'Quote' , 'Backquote' ] ;
4357
4458 constructor ( protected vocabularyService : VocabularyService ,
4559 protected cdr : ChangeDetectorRef ,
@@ -54,32 +68,26 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
5468 */
5569 ngOnInit ( ) {
5670 this . updatePageInfo ( this . model . maxOptions , 1 ) ;
57- this . vocabularyService . getVocabularyEntries ( this . model . vocabularyOptions , this . pageInfo ) . pipe (
71+ this . loadOptions ( ) ;
72+ }
73+
74+ loadOptions ( ) {
75+ this . loading = true ;
76+ this . vocabularyService . getVocabularyEntriesByValue ( this . inputText , false , this . model . vocabularyOptions , this . pageInfo ) . pipe (
5877 getFirstSucceededRemoteDataPayload ( ) ,
59- catchError ( ( ) => observableOf ( buildPaginatedList (
60- new PageInfo ( ) ,
61- [ ]
62- ) )
63- ) )
64- . subscribe ( ( list : PaginatedList < VocabularyEntry > ) => {
65- this . optionsList = list . page ;
66- if ( this . model . value ) {
67- this . setCurrentValue ( this . model . value , true ) ;
68- }
69-
70- this . updatePageInfo (
71- list . pageInfo . elementsPerPage ,
72- list . pageInfo . currentPage ,
73- list . pageInfo . totalElements ,
74- list . pageInfo . totalPages
75- ) ;
76- this . cdr . detectChanges ( ) ;
77- } ) ;
78-
79- this . group . get ( this . model . id ) . valueChanges . pipe ( distinctUntilChanged ( ) )
80- . subscribe ( ( value ) => {
81- this . setCurrentValue ( value ) ;
82- } ) ;
78+ catchError ( ( ) => observableOf ( buildPaginatedList ( new PageInfo ( ) , [ ] ) ) ) ,
79+ tap ( ( ) => this . loading = false )
80+ ) . subscribe ( ( list : PaginatedList < VocabularyEntry > ) => {
81+ this . optionsList = list . page ;
82+ this . updatePageInfo (
83+ list . pageInfo . elementsPerPage ,
84+ list . pageInfo . currentPage ,
85+ list . pageInfo . totalElements ,
86+ list . pageInfo . totalPages
87+ ) ;
88+ this . selectedIndex = 0 ;
89+ this . cdr . detectChanges ( ) ;
90+ } ) ;
8391 }
8492
8593 /**
@@ -94,10 +102,30 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
94102 openDropdown ( sdRef : NgbDropdown ) {
95103 if ( ! this . model . readOnly ) {
96104 this . group . markAsUntouched ( ) ;
105+ this . inputText = null ;
106+ this . updatePageInfo ( this . model . maxOptions , 1 ) ;
107+ this . loadOptions ( ) ;
97108 sdRef . open ( ) ;
98109 }
99110 }
100111
112+ navigateDropdown ( event : KeyboardEvent ) {
113+ if ( event . key === 'ArrowDown' ) {
114+ this . selectedIndex = Math . min ( this . selectedIndex + 1 , this . optionsList . length - 1 ) ;
115+ } else if ( event . key === 'ArrowUp' ) {
116+ this . selectedIndex = Math . max ( this . selectedIndex - 1 , 0 ) ;
117+ }
118+ this . scrollToSelected ( ) ;
119+ }
120+
121+ scrollToSelected ( ) {
122+ const dropdownItems = this . dropdownMenu . nativeElement . querySelectorAll ( '.dropdown-item' ) ;
123+ const selectedItem = dropdownItems [ this . selectedIndex ] ;
124+ if ( selectedItem ) {
125+ selectedItem . scrollIntoView ( { block : 'nearest' } ) ;
126+ }
127+ }
128+
101129 /**
102130 * KeyDown handler to allow toggling the dropdown via keyboard
103131 * @param event KeyboardEvent
@@ -106,13 +134,54 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
106134 selectOnKeyDown ( event : KeyboardEvent , sdRef : NgbDropdown ) {
107135 const keyName = event . key ;
108136
109- if ( keyName === ' ' || keyName === ' Enter') {
137+ if ( keyName === 'Enter' ) {
110138 event . preventDefault ( ) ;
111139 event . stopPropagation ( ) ;
112- sdRef . toggle ( ) ;
140+ if ( sdRef . isOpen ( ) ) {
141+ this . onSelect ( this . optionsList [ this . selectedIndex ] ) ;
142+ sdRef . close ( ) ;
143+ } else {
144+ sdRef . open ( ) ;
145+ }
113146 } else if ( keyName === 'ArrowDown' || keyName === 'ArrowUp' ) {
114- this . openDropdown ( sdRef ) ;
147+ event . preventDefault ( ) ;
148+ event . stopPropagation ( ) ;
149+ this . navigateDropdown ( event ) ;
150+ } else if ( keyName === 'Backspace' ) {
151+ this . removeKeyFromInput ( ) ;
152+ } else if ( this . isAcceptableKey ( keyName ) ) {
153+ this . addKeyToInput ( keyName ) ;
154+ }
155+ }
156+
157+ addKeyToInput ( keyName : string ) {
158+ if ( this . inputText === null ) {
159+ this . inputText = '' ;
160+ }
161+ this . inputText += keyName ;
162+ // When a new key is added, we need to reset the page info
163+ this . updatePageInfo ( this . model . maxOptions , 1 ) ;
164+ this . loadOptions ( ) ;
165+ }
166+
167+ removeKeyFromInput ( ) {
168+ if ( this . inputText !== null ) {
169+ this . inputText = this . inputText . slice ( 0 , - 1 ) ;
170+ if ( this . inputText === '' ) {
171+ this . inputText = null ;
172+ }
173+ this . loadOptions ( ) ;
174+ }
175+ }
176+
177+
178+ isAcceptableKey ( keyPress : string ) : boolean {
179+ // allow all letters and numbers
180+ if ( keyPress . length === 1 && keyPress . match ( / ^ [ a - z A - Z 0 - 9 ] * $ / ) ) {
181+ return true ;
115182 }
183+ // Some other characters like space, dash, etc should be allowed as well
184+ return this . acceptableKeys . includes ( keyPress ) ;
116185 }
117186
118187 /**
@@ -127,7 +196,7 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
127196 this . pageInfo . totalElements ,
128197 this . pageInfo . totalPages
129198 ) ;
130- this . vocabularyService . getVocabularyEntries ( this . model . vocabularyOptions , this . pageInfo ) . pipe (
199+ this . vocabularyService . getVocabularyEntriesByValue ( this . inputText , false , this . model . vocabularyOptions , this . pageInfo ) . pipe (
131200 getFirstSucceededRemoteDataPayload ( ) ,
132201 catchError ( ( ) => observableOf ( buildPaginatedList (
133202 new PageInfo ( ) ,
0 commit comments