mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge pull request #2764 from alanorth/backport-2733-to-dspace-7_x
[Port dspace-7_x] Use keyboard to select values
This commit is contained in:
@@ -27,7 +27,7 @@
|
|||||||
(keydown)="selectOnKeyDown($event, sdRef)">
|
(keydown)="selectOnKeyDown($event, sdRef)">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ngbDropdownMenu
|
<div #dropdownMenu ngbDropdownMenu
|
||||||
class="dropdown-menu scrollable-dropdown-menu w-100"
|
class="dropdown-menu scrollable-dropdown-menu w-100"
|
||||||
[attr.aria-label]="model.placeholder">
|
[attr.aria-label]="model.placeholder">
|
||||||
<div class="scrollable-menu"
|
<div class="scrollable-menu"
|
||||||
@@ -41,7 +41,8 @@
|
|||||||
[scrollWindow]="false">
|
[scrollWindow]="false">
|
||||||
|
|
||||||
<button class="dropdown-item disabled" *ngIf="optionsList && optionsList.length == 0">{{'form.no-results' | translate}}</button>
|
<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"
|
<button class="dropdown-item collection-item text-truncate" *ngFor="let listEntry of optionsList; let i = index"
|
||||||
|
[class.active]="i === selectedIndex"
|
||||||
(keydown.enter)="onSelect(listEntry); sdRef.close()" (mousedown)="onSelect(listEntry); sdRef.close()"
|
(keydown.enter)="onSelect(listEntry); sdRef.close()" (mousedown)="onSelect(listEntry); sdRef.close()"
|
||||||
title="{{ listEntry.display }}" role="option"
|
title="{{ listEntry.display }}" role="option"
|
||||||
[attr.id]="listEntry.display == (currentValue|async) ? ('combobox_' + id + '_selected') : null">
|
[attr.id]="listEntry.display == (currentValue|async) ? ('combobox_' + id + '_selected') : null">
|
||||||
|
@@ -1,8 +1,17 @@
|
|||||||
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
import {
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
OnInit,
|
||||||
|
Output,
|
||||||
|
ViewChild,
|
||||||
|
ElementRef
|
||||||
|
} from '@angular/core';
|
||||||
import { UntypedFormGroup } from '@angular/forms';
|
import { UntypedFormGroup } from '@angular/forms';
|
||||||
|
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { catchError, distinctUntilChanged, map, tap } from 'rxjs/operators';
|
import { catchError, map, tap } from 'rxjs/operators';
|
||||||
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
|
import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
|
||||||
|
|
||||||
@@ -28,6 +37,8 @@ import { FormFieldMetadataValueObject } from '../../../models/form-field-metadat
|
|||||||
templateUrl: './dynamic-scrollable-dropdown.component.html'
|
templateUrl: './dynamic-scrollable-dropdown.component.html'
|
||||||
})
|
})
|
||||||
export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyComponent implements OnInit {
|
export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyComponent implements OnInit {
|
||||||
|
@ViewChild('dropdownMenu', { read: ElementRef }) dropdownMenu: ElementRef;
|
||||||
|
|
||||||
@Input() bindId = true;
|
@Input() bindId = true;
|
||||||
@Input() group: UntypedFormGroup;
|
@Input() group: UntypedFormGroup;
|
||||||
@Input() model: DynamicScrollableDropdownModel;
|
@Input() model: DynamicScrollableDropdownModel;
|
||||||
@@ -40,6 +51,9 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
|
|||||||
public loading = false;
|
public loading = false;
|
||||||
public pageInfo: PageInfo;
|
public pageInfo: PageInfo;
|
||||||
public optionsList: any;
|
public optionsList: any;
|
||||||
|
public inputText: string = null;
|
||||||
|
public selectedIndex = 0;
|
||||||
|
public acceptableKeys = ['Space', 'NumpadMultiply', 'NumpadAdd', 'NumpadSubtract', 'NumpadDecimal', 'Semicolon', 'Equal', 'Comma', 'Minus', 'Period', 'Quote', 'Backquote'];
|
||||||
|
|
||||||
constructor(protected vocabularyService: VocabularyService,
|
constructor(protected vocabularyService: VocabularyService,
|
||||||
protected cdr: ChangeDetectorRef,
|
protected cdr: ChangeDetectorRef,
|
||||||
@@ -54,32 +68,26 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
|
|||||||
*/
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.updatePageInfo(this.model.maxOptions, 1);
|
this.updatePageInfo(this.model.maxOptions, 1);
|
||||||
this.vocabularyService.getVocabularyEntries(this.model.vocabularyOptions, this.pageInfo).pipe(
|
this.loadOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadOptions() {
|
||||||
|
this.loading = true;
|
||||||
|
this.vocabularyService.getVocabularyEntriesByValue(this.inputText, false, this.model.vocabularyOptions, this.pageInfo).pipe(
|
||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
catchError(() => observableOf(buildPaginatedList(
|
catchError(() => observableOf(buildPaginatedList(new PageInfo(), []))),
|
||||||
new PageInfo(),
|
tap(() => this.loading = false)
|
||||||
[]
|
).subscribe((list: PaginatedList<VocabularyEntry>) => {
|
||||||
))
|
this.optionsList = list.page;
|
||||||
))
|
this.updatePageInfo(
|
||||||
.subscribe((list: PaginatedList<VocabularyEntry>) => {
|
list.pageInfo.elementsPerPage,
|
||||||
this.optionsList = list.page;
|
list.pageInfo.currentPage,
|
||||||
if (this.model.value) {
|
list.pageInfo.totalElements,
|
||||||
this.setCurrentValue(this.model.value, true);
|
list.pageInfo.totalPages
|
||||||
}
|
);
|
||||||
|
this.selectedIndex = 0;
|
||||||
this.updatePageInfo(
|
this.cdr.detectChanges();
|
||||||
list.pageInfo.elementsPerPage,
|
});
|
||||||
list.pageInfo.currentPage,
|
|
||||||
list.pageInfo.totalElements,
|
|
||||||
list.pageInfo.totalPages
|
|
||||||
);
|
|
||||||
this.cdr.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.group.get(this.model.id).valueChanges.pipe(distinctUntilChanged())
|
|
||||||
.subscribe((value) => {
|
|
||||||
this.setCurrentValue(value);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,10 +102,30 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
|
|||||||
openDropdown(sdRef: NgbDropdown) {
|
openDropdown(sdRef: NgbDropdown) {
|
||||||
if (!this.model.readOnly) {
|
if (!this.model.readOnly) {
|
||||||
this.group.markAsUntouched();
|
this.group.markAsUntouched();
|
||||||
|
this.inputText = null;
|
||||||
|
this.updatePageInfo(this.model.maxOptions, 1);
|
||||||
|
this.loadOptions();
|
||||||
sdRef.open();
|
sdRef.open();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
navigateDropdown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'ArrowDown') {
|
||||||
|
this.selectedIndex = Math.min(this.selectedIndex + 1, this.optionsList.length - 1);
|
||||||
|
} else if (event.key === 'ArrowUp') {
|
||||||
|
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
|
||||||
|
}
|
||||||
|
this.scrollToSelected();
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToSelected() {
|
||||||
|
const dropdownItems = this.dropdownMenu.nativeElement.querySelectorAll('.dropdown-item');
|
||||||
|
const selectedItem = dropdownItems[this.selectedIndex];
|
||||||
|
if (selectedItem) {
|
||||||
|
selectedItem.scrollIntoView({ block: 'nearest' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KeyDown handler to allow toggling the dropdown via keyboard
|
* KeyDown handler to allow toggling the dropdown via keyboard
|
||||||
* @param event KeyboardEvent
|
* @param event KeyboardEvent
|
||||||
@@ -106,15 +134,56 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
|
|||||||
selectOnKeyDown(event: KeyboardEvent, sdRef: NgbDropdown) {
|
selectOnKeyDown(event: KeyboardEvent, sdRef: NgbDropdown) {
|
||||||
const keyName = event.key;
|
const keyName = event.key;
|
||||||
|
|
||||||
if (keyName === ' ' || keyName === 'Enter') {
|
if (keyName === 'Enter') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
sdRef.toggle();
|
if (sdRef.isOpen()) {
|
||||||
|
this.onSelect(this.optionsList[this.selectedIndex]);
|
||||||
|
sdRef.close();
|
||||||
|
} else {
|
||||||
|
sdRef.open();
|
||||||
|
}
|
||||||
} else if (keyName === 'ArrowDown' || keyName === 'ArrowUp') {
|
} else if (keyName === 'ArrowDown' || keyName === 'ArrowUp') {
|
||||||
this.openDropdown(sdRef);
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
this.navigateDropdown(event);
|
||||||
|
} else if (keyName === 'Backspace') {
|
||||||
|
this.removeKeyFromInput();
|
||||||
|
} else if (this.isAcceptableKey(keyName)) {
|
||||||
|
this.addKeyToInput(keyName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addKeyToInput(keyName: string) {
|
||||||
|
if (this.inputText === null) {
|
||||||
|
this.inputText = '';
|
||||||
|
}
|
||||||
|
this.inputText += keyName;
|
||||||
|
// When a new key is added, we need to reset the page info
|
||||||
|
this.updatePageInfo(this.model.maxOptions, 1);
|
||||||
|
this.loadOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeKeyFromInput() {
|
||||||
|
if (this.inputText !== null) {
|
||||||
|
this.inputText = this.inputText.slice(0, -1);
|
||||||
|
if (this.inputText === '') {
|
||||||
|
this.inputText = null;
|
||||||
|
}
|
||||||
|
this.loadOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
isAcceptableKey(keyPress: string): boolean {
|
||||||
|
// allow all letters and numbers
|
||||||
|
if (keyPress.length === 1 && keyPress.match(/^[a-zA-Z0-9]*$/)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Some other characters like space, dash, etc should be allowed as well
|
||||||
|
return this.acceptableKeys.includes(keyPress);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads any new entries
|
* Loads any new entries
|
||||||
*/
|
*/
|
||||||
@@ -127,7 +196,7 @@ export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyCom
|
|||||||
this.pageInfo.totalElements,
|
this.pageInfo.totalElements,
|
||||||
this.pageInfo.totalPages
|
this.pageInfo.totalPages
|
||||||
);
|
);
|
||||||
this.vocabularyService.getVocabularyEntries(this.model.vocabularyOptions, this.pageInfo).pipe(
|
this.vocabularyService.getVocabularyEntriesByValue(this.inputText, false, this.model.vocabularyOptions, this.pageInfo).pipe(
|
||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
catchError(() => observableOf(buildPaginatedList(
|
catchError(() => observableOf(buildPaginatedList(
|
||||||
new PageInfo(),
|
new PageInfo(),
|
||||||
|
Reference in New Issue
Block a user