diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html index 1ac38e9943c..ca854ae9a2c 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html @@ -44,8 +44,8 @@

{{'form.loading' | translate}}

diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts index a4ca2101934..d02bb88261f 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts @@ -40,6 +40,9 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom public loading = false; public pageInfo: PageInfo; public optionsList: any; + public allOptionsList: any; + public inputText = ''; + public acceptableKeys = ['Space', 'NumpadMultiply', 'NumpadAdd', 'NumpadSubtract', 'NumpadDecimal', 'Semicolon', 'Equal', 'Comma', 'Minus', 'Period', 'Quote', 'Backquote']; constructor(protected vocabularyService: VocabularyService, protected cdr: ChangeDetectorRef, @@ -53,7 +56,8 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom * Initialize the component, setting up the init form value */ ngOnInit() { - this.updatePageInfo(this.model.maxOptions, 1); + // Obtain ALL the entries from the vocabulary, needed to search for the first matching option + this.updatePageInfo(9999, 1); this.vocabularyService.getVocabularyEntries(this.model.vocabularyOptions, this.pageInfo).pipe( getFirstSucceededRemoteDataPayload(), catchError(() => observableOf(buildPaginatedList( @@ -62,7 +66,9 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom )) )) .subscribe((list: PaginatedList) => { - this.optionsList = list.page; + // Save all entries, slice the first page to display, we only load more entries when scrolling / searching + this.allOptionsList = list.page; + this.optionsList = list.page.slice(0, this.model.maxOptions); if (this.model.value) { this.setCurrentValue(this.model.value, true); } @@ -113,6 +119,42 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom } else if (keyName === 'ArrowDown' || keyName === 'ArrowUp') { this.openDropdown(sdRef); } + + if (keyName === 'Backspace') { + this.inputText = this.inputText.slice(0, -1); + } else if (this.isAcceptableKey(keyName)) { + this.inputText += keyName; + } + + const matchingOption = this.findMatchingOption(this.allOptionsList, this.inputText); + + if (matchingOption) { + // the matching option is not in the list of options to display, so we need to load more options + // TODO: Not sure if at this point it's even worth it to paginate the dropdown options + const maxAttempts = 3; + const waitForOption = async () => { + for (let attempt = 0; attempt < maxAttempts; attempt++) { + if (this.optionsList.includes(matchingOption)) { + this.scrollToMatchingOption(matchingOption.display); + break; + } + this.pageInfo.currentPage++; + this.optionsList = this.allOptionsList.slice(0, this.pageInfo.elementsPerPage * this.pageInfo.currentPage); + await new Promise(resolve => setTimeout(resolve, 100)); + } + }; + waitForOption(); + } else { + this.setCurrentValue(null); + } + } + + findMatchingOption(options: any, inputText: string): any { + if (isEmpty(inputText)) { + return null; + } + return options.find(option => option.display.toLowerCase().startsWith(this.inputText.toLowerCase()) + ); } /** @@ -121,30 +163,9 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom onScroll() { if (!this.loading && this.pageInfo.currentPage <= this.pageInfo.totalPages) { this.loading = true; - this.updatePageInfo( - this.pageInfo.elementsPerPage, - this.pageInfo.currentPage + 1, - this.pageInfo.totalElements, - this.pageInfo.totalPages - ); - this.vocabularyService.getVocabularyEntries(this.model.vocabularyOptions, this.pageInfo).pipe( - getFirstSucceededRemoteDataPayload(), - catchError(() => observableOf(buildPaginatedList( - new PageInfo(), - [] - )) - ), - tap(() => this.loading = false)) - .subscribe((list: PaginatedList) => { - this.optionsList = this.optionsList.concat(list.page); - this.updatePageInfo( - list.pageInfo.elementsPerPage, - list.pageInfo.currentPage, - list.pageInfo.totalElements, - list.pageInfo.totalPages - ); - this.cdr.detectChanges(); - }); + this.pageInfo.currentPage++; + this.optionsList = this.allOptionsList.slice(0, this.pageInfo.elementsPerPage * this.pageInfo.currentPage); + this.loading = false; } } @@ -183,4 +204,30 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom this.currentValue = result; } + scrollToMatchingOption(matchingOption: any): void { + console.log('scrollToMatchingOption', matchingOption); + const element = document.getElementById('option-' + matchingOption.replace(/ /g,"-")); + console.log('element', element.id); + if (element) { + // remove active class from all options + const activeElements = document.getElementsByClassName('active'); + for (let i = 0; i < activeElements.length; i++) { + activeElements[i].classList.remove('active'); + } + // scroll to the matching option and center it in the dropdown + element.scrollIntoView({block: 'center'}); + // add active class to the matching option + element.className += ' active'; + this.setCurrentValue(matchingOption); + } + } + + isAcceptableKey(keyPress: string): boolean { + // allow all letters and numbers + if (keyPress.length == 1 && keyPress.match(/^[a-zA-Z0-9]*$/)) { + return true; + } + return this.acceptableKeys.includes(keyPress); + } + }