@nebular/theme
Version:
@nebular/theme
356 lines • 12.9 kB
JavaScript
/**
* @license
* Copyright Akveo. All Rights Reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/
import { ChangeDetectorRef, Directive, ElementRef, forwardRef, HostBinding, HostListener, Input, Renderer2, } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { merge, Subject } from 'rxjs';
import { filter, startWith, switchMap, takeUntil, tap } from 'rxjs/operators';
import { NbTrigger, NbTriggerStrategyBuilderService } from '../cdk/overlay/overlay-trigger';
import { NbOverlayService } from '../cdk/overlay/overlay-service';
import { ENTER, ESCAPE } from '../cdk/keycodes/keycodes';
import { NbAdjustment, NbPosition, NbPositionBuilderService, } from '../cdk/overlay/overlay-position';
import { NbActiveDescendantKeyManagerFactoryService, NbKeyManagerActiveItemMode, } from '../cdk/a11y/descendant-key-manager';
import { convertToBoolProperty } from '../helpers';
import { NbAutocompleteComponent } from './autocomplete.component';
/**
* The `NbAutocompleteDirective` provides a capability to expand input with
* `NbAutocompleteComponent` overlay containing options to select and fill input with.
*
* @stacked-example(Showcase, autocomplete/autocomplete-showcase.component)
*
* ### Installation
*
* Import `NbAutocompleteModule` to your feature module.
* ```ts
* @NgModule({
* imports: [
* // ...
* NbAutocompleteModule,
* ],
* })
* export class PageModule { }
* ```
* ### Usage
*
* You can bind control with form controls or ngModel.
*
* @stacked-example(Autocomplete form binding, autocomplete/autocomplete-form.component)
*
* Options in the autocomplete may be grouped using `nb-option-group` component.
*
* @stacked-example(Grouping, autocomplete/autocomplete-group.component)
*
* Autocomplete may change selected option value via provided function.
*
* @stacked-example(Custom display, autocomplete/autocomplete-custom-display.component)
*
* Also, autocomplete may make first option in option list active automatically.
*
* @stacked-example(Active first, autocomplete/autocomplete-active-first.component)
*
* */
export class NbAutocompleteDirective {
constructor(hostRef, overlay, cd, triggerStrategyBuilder, positionBuilder, activeDescendantKeyManagerFactory, renderer) {
this.hostRef = hostRef;
this.overlay = overlay;
this.cd = cd;
this.triggerStrategyBuilder = triggerStrategyBuilder;
this.positionBuilder = positionBuilder;
this.activeDescendantKeyManagerFactory = activeDescendantKeyManagerFactory;
this.renderer = renderer;
this.destroy$ = new Subject();
this._onChange = () => { };
this._onTouched = () => { };
/**
* Determines options overlay offset (in pixels).
**/
this.overlayOffset = 8;
this._focusInputOnValueChange = true;
/**
* Determines options overlay scroll strategy.
**/
this.scrollStrategy = 'block';
this.role = 'combobox';
this.ariaAutocomplete = 'list';
this.hasPopup = 'true';
}
/**
* Determines is autocomplete overlay opened.
* */
get isOpen() {
return this.overlayRef && this.overlayRef.hasAttached();
}
/**
* Determines is autocomplete overlay closed.
* */
get isClosed() {
return !this.isOpen;
}
/**
* Provides autocomplete component.
* */
get autocomplete() {
return this._autocomplete;
}
set autocomplete(autocomplete) {
this._autocomplete = autocomplete;
}
/**
* Determines if the input will be focused when the control value is changed
* */
get focusInputOnValueChange() {
return this._focusInputOnValueChange;
}
set focusInputOnValueChange(value) {
this._focusInputOnValueChange = convertToBoolProperty(value);
}
get top() {
return this.isOpen && this.autocomplete.options.length && this.autocomplete.overlayPosition === NbPosition.TOP;
}
get bottom() {
return this.isOpen && this.autocomplete.options.length && this.autocomplete.overlayPosition === NbPosition.BOTTOM;
}
get ariaExpanded() {
return this.isOpen && this.isOpen.toString();
}
get ariaOwns() {
return this.isOpen ? this.autocomplete.id : null;
}
get ariaActiveDescendant() {
return (this.isOpen && this.keyManager.activeItem) ? this.keyManager.activeItem.id : null;
}
ngAfterViewInit() {
this.triggerStrategy = this.createTriggerStrategy();
this.subscribeOnTriggers();
}
ngOnDestroy() {
if (this.triggerStrategy) {
this.triggerStrategy.destroy();
}
if (this.positionStrategy) {
this.positionStrategy.dispose();
}
if (this.overlayRef) {
this.overlayRef.dispose();
}
this.destroy$.next();
this.destroy$.complete();
}
handleInput() {
const currentValue = this.hostRef.nativeElement.value;
this._onChange(currentValue);
this.setHostInputValue(this.getDisplayValue(currentValue));
this.show();
}
handleKeydown() {
this.show();
}
handleBlur() {
this._onTouched();
}
show() {
if (this.isClosed) {
this.attachToOverlay();
this.setActiveItem();
}
}
hide() {
if (this.isOpen) {
this.overlayRef.detach();
// Need to update class via @HostBinding
this.cd.markForCheck();
}
}
writeValue(value) {
this.handleInputValueUpdate(value);
}
registerOnChange(fn) {
this._onChange = fn;
}
registerOnTouched(fn) {
this._onTouched = fn;
}
setDisabledState(disabled) {
this.renderer.setProperty(this.hostRef.nativeElement, 'disabled', disabled);
}
subscribeOnOptionClick() {
/**
* If the user changes provided options list in the runtime we have to handle this
* and resubscribe on options selection changes event.
* Otherwise, the user will not be able to select new options.
* */
this.autocomplete.options.changes
.pipe(tap(() => this.setActiveItem()), startWith(this.autocomplete.options), switchMap((options) => {
return merge(...options.map(option => option.click));
}), takeUntil(this.destroy$))
.subscribe((clickedOption) => this.handleInputValueUpdate(clickedOption.value));
}
subscribeOnPositionChange() {
this.positionStrategy.positionChange
.pipe(takeUntil(this.destroy$))
.subscribe((position) => {
this.autocomplete.overlayPosition = position;
this.cd.detectChanges();
});
}
getActiveItem() {
return this.keyManager.activeItem;
}
setupAutocomplete() {
this.autocomplete.setHost(this.customOverlayHost || this.hostRef);
}
getDisplayValue(value) {
const displayFn = this.autocomplete.handleDisplayFn;
return displayFn ? displayFn(value) : value;
}
getContainer() {
return this.overlayRef && this.isOpen && {
location: {
nativeElement: this.overlayRef.overlayElement,
},
};
}
handleInputValueUpdate(value) {
if (value === undefined || value === null) {
return;
}
this.setHostInputValue(value);
this._onChange(value);
if (this.focusInputOnValueChange) {
this.hostRef.nativeElement.focus();
}
this.autocomplete.emitSelected(value);
this.hide();
}
subscribeOnTriggers() {
this.triggerStrategy.show$
.pipe(filter(() => this.isClosed))
.subscribe(() => this.show());
this.triggerStrategy.hide$
.pipe(filter(() => this.isOpen))
.subscribe(() => this.hide());
}
createTriggerStrategy() {
return this.triggerStrategyBuilder
.trigger(NbTrigger.FOCUS)
.host(this.hostRef.nativeElement)
.container(() => this.getContainer())
.build();
}
createKeyManager() {
this.keyManager = this.activeDescendantKeyManagerFactory
.create(this.autocomplete.options);
}
setHostInputValue(value) {
this.hostRef.nativeElement.value = this.getDisplayValue(value);
}
createPositionStrategy() {
return this.positionBuilder
.connectedTo(this.customOverlayHost || this.hostRef)
.position(NbPosition.BOTTOM)
.offset(this.overlayOffset)
.adjustment(NbAdjustment.VERTICAL);
}
subscribeOnOverlayKeys() {
this.overlayRef.keydownEvents()
.pipe(takeUntil(this.destroy$))
.subscribe((event) => {
if (event.keyCode === ESCAPE && this.isOpen) {
event.preventDefault();
this.hostRef.nativeElement.focus();
this.hide();
}
else if (event.keyCode === ENTER) {
event.preventDefault();
const activeItem = this.getActiveItem();
if (!activeItem) {
return;
}
this.handleInputValueUpdate(activeItem.value);
}
else {
this.keyManager.onKeydown(event);
}
});
}
setActiveItem() {
// If autocomplete has activeFirst input set to true,
// keyManager set first option active, otherwise - reset active option.
const mode = this.autocomplete.activeFirst
? NbKeyManagerActiveItemMode.FIRST_ACTIVE
: NbKeyManagerActiveItemMode.RESET_ACTIVE;
this.keyManager.setActiveItem(mode);
this.cd.detectChanges();
}
attachToOverlay() {
if (!this.overlayRef) {
this.setupAutocomplete();
this.initOverlay();
}
this.overlayRef.attach(this.autocomplete.portal);
}
createOverlay() {
const scrollStrategy = this.createScrollStrategy();
this.overlayRef = this.overlay.create({ positionStrategy: this.positionStrategy, scrollStrategy, panelClass: this.autocomplete.optionsPanelClass });
}
initOverlay() {
this.positionStrategy = this.createPositionStrategy();
this.createKeyManager();
this.subscribeOnPositionChange();
this.subscribeOnOptionClick();
this.checkOverlayVisibility();
this.createOverlay();
this.subscribeOnOverlayKeys();
}
checkOverlayVisibility() {
this.autocomplete.options.changes
.pipe(takeUntil(this.destroy$)).subscribe(() => {
if (!this.autocomplete.options.length) {
this.hide();
}
});
}
createScrollStrategy() {
return this.overlay.scrollStrategies[this.scrollStrategy]();
}
}
NbAutocompleteDirective.decorators = [
{ type: Directive, args: [{
selector: 'input[nbAutocomplete]',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NbAutocompleteDirective),
multi: true,
}],
},] }
];
NbAutocompleteDirective.ctorParameters = () => [
{ type: ElementRef },
{ type: NbOverlayService },
{ type: ChangeDetectorRef },
{ type: NbTriggerStrategyBuilderService },
{ type: NbPositionBuilderService },
{ type: NbActiveDescendantKeyManagerFactoryService },
{ type: Renderer2 }
];
NbAutocompleteDirective.propDecorators = {
autocomplete: [{ type: Input, args: ['nbAutocomplete',] }],
overlayOffset: [{ type: Input }],
focusInputOnValueChange: [{ type: Input }],
scrollStrategy: [{ type: Input }],
customOverlayHost: [{ type: Input }],
top: [{ type: HostBinding, args: ['class.nb-autocomplete-position-top',] }],
bottom: [{ type: HostBinding, args: ['class.nb-autocomplete-position-bottom',] }],
role: [{ type: HostBinding, args: ['attr.role',] }],
ariaAutocomplete: [{ type: HostBinding, args: ['attr.aria-autocomplete',] }],
hasPopup: [{ type: HostBinding, args: ['attr.haspopup',] }],
ariaExpanded: [{ type: HostBinding, args: ['attr.aria-expanded',] }],
ariaOwns: [{ type: HostBinding, args: ['attr.aria-owns',] }],
ariaActiveDescendant: [{ type: HostBinding, args: ['attr.aria-activedescendant',] }],
handleInput: [{ type: HostListener, args: ['input',] }],
handleKeydown: [{ type: HostListener, args: ['keydown.arrowDown',] }, { type: HostListener, args: ['keydown.arrowUp',] }],
handleBlur: [{ type: HostListener, args: ['blur',] }]
};
//# sourceMappingURL=autocomplete.directive.js.map