Skip to content

Commit

Permalink
110088: example implementation of keyboard support
Browse files Browse the repository at this point in the history
  • Loading branch information
Jens Vannerum committed Dec 20, 2023
1 parent 92a10ce commit f114c1f
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
<button class="dropdown-item disabled" *ngIf="optionsList && optionsList.length == 0">{{'form.no-results' | translate}}</button>
<button class="dropdown-item collection-item text-truncate" *ngFor="let listEntry of optionsList"
(keydown.enter)="onSelect(listEntry); sdRef.close()" (mousedown)="onSelect(listEntry); sdRef.close()"
title="{{ listEntry.display }}" role="option"
[attr.id]="listEntry.display == (currentValue|async) ? ('combobox_' + id + '_selected') : null">
title="{{ listEntry.display }}" id="option-{{listEntry.display.replace(' ', '-')}}"
role="option">
{{inputFormatter(listEntry)}}
</button>
<div class="scrollable-dropdown-loading text-center" *ngIf="loading"><p>{{'form.loading' | translate}}</p></div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -62,7 +66,9 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
))
))
.subscribe((list: PaginatedList<VocabularyEntry>) => {
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);
}
Expand Down Expand Up @@ -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())
);
}

/**
Expand All @@ -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<VocabularyEntry>) => {
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;
}
}

Expand Down Expand Up @@ -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,"-"));

Check failure on line 209 in src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts

View workflow job for this annotation

GitHub Actions / tests (16.x)

Strings must use singlequote

Check failure on line 209 in src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts

View workflow job for this annotation

GitHub Actions / tests (18.x)

Strings must use singlequote
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]*$/)) {

Check failure on line 227 in src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts

View workflow job for this annotation

GitHub Actions / tests (16.x)

Expected '===' and instead saw '=='

Check failure on line 227 in src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts

View workflow job for this annotation

GitHub Actions / tests (18.x)

Expected '===' and instead saw '=='
return true;
}
return this.acceptableKeys.includes(keyPress);
}

}

0 comments on commit f114c1f

Please sign in to comment.