UNPKG

novo-elements

Version:

356 lines (351 loc) 19.2 kB
import { ActiveDescendantKeyManager } from '@angular/cdk/a11y'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { hasModifierKey } from '@angular/cdk/keycodes'; import * as i0 from '@angular/core'; import { EventEmitter, ViewChild, Input, Output, ContentChildren, Attribute, Optional, Inject, ChangeDetectionStrategy, ViewEncapsulation, Component, NgModule } from '@angular/core'; import { Subscription, fromEvent, merge, of, interval } from 'rxjs'; import { take, debounce } from 'rxjs/operators'; import { BooleanInput } from 'novo-elements/utils'; import * as i1 from 'novo-elements/elements/common'; import { mixinOverlay, mixinDisabled, NovoOverlayTemplateComponent, NovoOptgroup, NovoOption, NOVO_OPTION_PARENT_COMPONENT, NOVO_OVERLAY_CONTAINER, NovoOverlayModule, NovoOptionModule, NovoCommonModule } from 'novo-elements/elements/common'; import * as i3 from 'novo-elements/elements/field'; import { NOVO_FORM_FIELD, NovoFieldModule } from 'novo-elements/elements/field'; import * as i2 from '@angular/cdk/scrolling'; import { CommonModule } from '@angular/common'; import { NovoButtonModule } from 'novo-elements/elements/button'; import { NovoChipsModule } from 'novo-elements/elements/chips'; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; /** Event object that is emitted when an autocomplete option is selected. */ class NovoOptionSelectedEvent { constructor( /** Reference to the autocomplete panel that emitted the event. */ source, /** Option that was selected. */ option) { this.source = source; this.option = option; } } // Boilerplate for applying mixins class NovoAutocompleteBase { constructor() { } } const NovoAutocompleteMixins = mixinOverlay(mixinDisabled(NovoAutocompleteBase)); class NovoAutocompleteElement extends NovoAutocompleteMixins { /** Whether the user should be allowed to select multiple options. */ get multiple() { return this._multiple; } set multiple(value) { this._multiple = coerceBooleanProperty(value); } /** Whether the toggle button is disabled. */ get disabled() { if (this._disabled === undefined && this._formField?._control) { return this._formField._control.disabled; } return !!this._disabled; } set disabled(value) { this._disabled = coerceBooleanProperty(value); } constructor(_elementRef, cdr, defaultTabIndex, _formField) { super(); this._elementRef = _elementRef; this.cdr = cdr; this._formField = _formField; this._stateChanges = Subscription.EMPTY; this._activeOptionChanges = Subscription.EMPTY; this._selectedOptionChanges = Subscription.EMPTY; this._keyDownChanges = Subscription.EMPTY; /** Event that is emitted whenever an option from the list is selected. */ this.optionSelected = new EventEmitter(); /** Emits whenever an option is activated using the keyboard. */ this.optionActivated = new EventEmitter(); /** Key to use to trigger autocomplete. used for textarea. */ this.triggerOn = (control) => control.focused; /** Function that maps an option's control value to its display value in the trigger. */ this.displayWith = null; this._multiple = false; const parsedTabIndex = Number(defaultTabIndex); this.tabIndex = parsedTabIndex || parsedTabIndex === 0 ? parsedTabIndex : null; } ngOnChanges(changes) { this._watchStateChanges(); this._watchSelectionEvents(); } ngOnDestroy() { this._stateChanges.unsubscribe(); this._activeOptionChanges.unsubscribe(); this._selectedOptionChanges.unsubscribe(); this._keyDownChanges.unsubscribe(); } ngAfterContentInit() { this._keyManager = new ActiveDescendantKeyManager(this.options).withWrap(); this._activeOptionChanges = this._keyManager.change.subscribe((index) => { this.optionActivated.emit({ source: this, option: this.options.toArray()[index] || null }); }); this.element = this._formField.getConnectedOverlayOrigin() || this._elementRef; this._keyDownChanges = fromEvent(this.element.nativeElement, 'keydown').subscribe((event) => this._handleKeydown(event)); this.options.changes.subscribe(() => { this._watchStateChanges(); this._watchSelectionEvents(); Promise.resolve().then(() => { this.checkSelectedOptions(); if (this.makeFirstItemActive && this.options.length > 0) { this._keyManager.setFirstItemActive(); } }); }); } ngAfterViewInit() { this._watchStateChanges(); this._watchSelectionEvents(); } checkPanel() { const isTriggered = this.triggerOn(this._formField._control); if (isTriggered && this.element) { this.openPanel(); } } _setTriggerValue(option) { const toDisplay = this.displayWith ? this.displayWith(option) : option?.viewValue; // Simply falling back to an empty string if the display value is falsy does not work properly. // The display value can also be the number zero and shouldn't fall back to an empty string. const displayValue = toDisplay != null ? toDisplay : ''; const optionValue = option.value; // If it's used within a `NovoField`, we should set it through the property so it can go // through change detection. if (this._formField) { const { controlType, lastCaretPosition = 0 } = this._formField._control; if (controlType === 'textarea') { const currentValue = this._formField._control.value.split(''); currentValue.splice(lastCaretPosition, 0, displayValue); this._formField._control.value = currentValue.join(''); } else if (controlType === 'chip-list') { const chipList = this._formField._control; const currentValue = this._formField._control.value; if (currentValue.includes(optionValue)) { chipList.removeValue(optionValue); } else { chipList.addValue(optionValue); } } else { let valueToEmit = optionValue; if (this.multiple) { const currentValue = this._formField._control.value; if (Array.isArray(currentValue)) { if (currentValue.includes(optionValue)) { valueToEmit = currentValue.filter((it) => it === optionValue); } else { valueToEmit = [...currentValue, optionValue]; } } else if (currentValue === optionValue) { valueToEmit = []; } else { valueToEmit = [currentValue, optionValue]; } } this._formField._control.value = valueToEmit; } } else { console.warn('AutoComplete only intended to be used within a NovoField'); } this._previousValue = optionValue; } /** * Clear any previous selected option and emit a selection change event for this option */ _clearPreviousSelectedOption(skip) { this.options.forEach((option) => { if (option !== skip && option.selected) { option.deselect(); } }); } /** Emits the `select` event. */ _emitSelectEvent(option) { const event = new NovoOptionSelectedEvent(this, option); this.optionSelected.emit(event); } /** * This method closes the panel, and if a value is specified, also sets the associated * control to that value. It will also mark the control as dirty if this interaction * stemmed from the user. */ _setValueAndClose(event) { if (event && event.source) { if (!this.multiple) { this._clearPreviousSelectedOption(event.source); } this._setTriggerValue(event.source); this._emitSelectEvent(event.source); this._watchSelectionEvents(); } if (!this.multiple) { this.closePanel(); } } _watchSelectionEvents() { const selectionEvents = this.options ? merge(...this.options.map((option) => option.onSelectionChange)) : of(); this._selectedOptionChanges.unsubscribe(); this._selectedOptionChanges = selectionEvents.pipe(take(1)).subscribe((evt) => { this._setValueAndClose(evt); }); } _watchStateChanges() { const inputStateChanged = this._formField.stateChanges; this._stateChanges.unsubscribe(); this._stateChanges = merge(inputStateChanged) .pipe(debounce(() => interval(10))) .subscribe(() => { this.checkSelectedOptions(); this.checkPanel(); this.cdr.markForCheck(); }); } /** The currently active option, coerced to MatOption type. */ get activeOption() { if (this._keyManager) { return this._keyManager.activeItem; } return null; } _handleKeydown(event) { const key = event.key; // Prevent the default action on all escape key presses. This is here primarily to bring IE // in line with other browsers. By default, pressing escape on IE will cause it to revert // the input value to the one that it had on focus, however it won't dispatch any events // which means that the model value will be out of sync with the view. if (key === "Escape" /* Key.Escape */ && !hasModifierKey(event)) { event.preventDefault(); } if (this.activeOption && key === "Enter" /* Key.Enter */ && this.panelOpen) { this.activeOption._selectViaInteraction(); event.preventDefault(); } else { const prevActiveItem = this._keyManager.activeItem; const isArrowKey = key === "ArrowUp" /* Key.ArrowUp */ || key === "ArrowDown" /* Key.ArrowDown */; if (this.panelOpen || key === "Tab" /* Key.Tab */) { this._keyManager.onKeydown(event); } else if (isArrowKey && !this.overlay.panelOpen) { this.openPanel(); } } } checkSelectedOptions() { if (this.multiple && Array.isArray(this._formField._control.value)) { const value = this._formField._control.value; this.options.forEach((option) => option.deselect()); value.forEach((currentValue) => this._selectValue(currentValue)); } } /** * Finds and selects and option based on its value. * @returns Option that has the corresponding value. */ _selectValue(value) { const correspondingOption = this.options.find((option) => { return option.value === value; }); correspondingOption?.select(); return correspondingOption; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NovoAutocompleteElement, deps: [{ token: i0.ElementRef }, { token: i0.ChangeDetectorRef }, { token: 'tabindex', attribute: true }, { token: NOVO_FORM_FIELD, optional: true }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.19", type: NovoAutocompleteElement, isStandalone: false, selector: "novo-autocomplete", inputs: { tabIndex: "tabIndex", triggerOn: "triggerOn", displayWith: "displayWith", ariaLabel: ["aria-label", "ariaLabel"], multiple: "multiple", disabled: "disabled", makeFirstItemActive: "makeFirstItemActive" }, outputs: { optionSelected: "optionSelected", optionActivated: "optionActivated" }, host: { properties: { "attr.tabindex": "disabled ? null : -1" }, classAttribute: "novo-autocomplete" }, providers: [ { provide: NOVO_OPTION_PARENT_COMPONENT, useExisting: NovoAutocompleteElement }, { provide: NOVO_OVERLAY_CONTAINER, useExisting: NovoAutocompleteElement }, ], queries: [{ propertyName: "optionGroups", predicate: NovoOptgroup, descendants: true }, { propertyName: "options", predicate: NovoOption, descendants: true }], viewQueries: [{ propertyName: "overlay", first: true, predicate: NovoOverlayTemplateComponent, descendants: true }], exportAs: ["novoAutocomplete"], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: "<novo-overlay-template [parent]=\"element\" position=\"above-below\">\n <div class=\"novo-autocomplete-options\" cdk-scrollable>\n <ng-content></ng-content>\n </div>\n</novo-overlay-template>", styles: [".novo-autocomplete-options{background-color:var(--background-bright);cursor:default;list-style:none;padding-inline-start:0px!important;box-shadow:0 -1px 3px -2px #0003,0 2px 2px #00000024,0 1px 5px #0000001f}\n"], dependencies: [{ kind: "component", type: i1.NovoOverlayTemplateComponent, selector: "novo-overlay-template", inputs: ["position", "scrollStrategy", "width", "minWidth", "height", "closeOnSelect", "hasBackdrop", "parent"], outputs: ["select", "opening", "closing", "backDropClicked"] }, { kind: "directive", type: i2.CdkScrollable, selector: "[cdk-scrollable], [cdkScrollable]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); } } __decorate([ BooleanInput(), __metadata("design:type", Boolean), __metadata("design:paramtypes", [Boolean]) ], NovoAutocompleteElement.prototype, "disabled", null); __decorate([ BooleanInput(), __metadata("design:type", Boolean) ], NovoAutocompleteElement.prototype, "makeFirstItemActive", void 0); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NovoAutocompleteElement, decorators: [{ type: Component, args: [{ selector: 'novo-autocomplete', host: { class: 'novo-autocomplete', // Always set the tabindex to -1 so that it doesn't overlap with any custom tabindex the // consumer may have provided, while still being able to receive focus. '[attr.tabindex]': 'disabled ? null : -1', }, providers: [ { provide: NOVO_OPTION_PARENT_COMPONENT, useExisting: NovoAutocompleteElement }, { provide: NOVO_OVERLAY_CONTAINER, useExisting: NovoAutocompleteElement }, ], exportAs: 'novoAutocomplete', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<novo-overlay-template [parent]=\"element\" position=\"above-below\">\n <div class=\"novo-autocomplete-options\" cdk-scrollable>\n <ng-content></ng-content>\n </div>\n</novo-overlay-template>", styles: [".novo-autocomplete-options{background-color:var(--background-bright);cursor:default;list-style:none;padding-inline-start:0px!important;box-shadow:0 -1px 3px -2px #0003,0 2px 2px #00000024,0 1px 5px #0000001f}\n"] }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.ChangeDetectorRef }, { type: undefined, decorators: [{ type: Attribute, args: ['tabindex'] }] }, { type: i3.NovoFieldElement, decorators: [{ type: Optional }, { type: Inject, args: [NOVO_FORM_FIELD] }] }], propDecorators: { optionGroups: [{ type: ContentChildren, args: [NovoOptgroup, { descendants: true }] }], options: [{ type: ContentChildren, args: [NovoOption, { descendants: true }] }], optionSelected: [{ type: Output }], optionActivated: [{ type: Output }], tabIndex: [{ type: Input }], triggerOn: [{ type: Input }], displayWith: [{ type: Input }], ariaLabel: [{ type: Input, args: ['aria-label'] }], multiple: [{ type: Input }], disabled: [{ type: Input }], makeFirstItemActive: [{ type: Input }], overlay: [{ type: ViewChild, args: [NovoOverlayTemplateComponent] }] } }); class NovoAutoCompleteModule { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NovoAutoCompleteModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.3.19", ngImport: i0, type: NovoAutoCompleteModule, declarations: [NovoAutocompleteElement], imports: [CommonModule, NovoButtonModule, NovoOverlayModule, NovoOptionModule, NovoCommonModule, NovoFieldModule, NovoChipsModule], exports: [NovoAutocompleteElement] }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NovoAutoCompleteModule, imports: [CommonModule, NovoButtonModule, NovoOverlayModule, NovoOptionModule, NovoCommonModule, NovoFieldModule, NovoChipsModule] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NovoAutoCompleteModule, decorators: [{ type: NgModule, args: [{ imports: [CommonModule, NovoButtonModule, NovoOverlayModule, NovoOptionModule, NovoCommonModule, NovoFieldModule, NovoChipsModule], declarations: [NovoAutocompleteElement], exports: [NovoAutocompleteElement], }] }] }); /** * Generated bundle index. Do not edit. */ export { NovoAutoCompleteModule, NovoAutocompleteElement, NovoOptionSelectedEvent }; //# sourceMappingURL=novo-elements-elements-autocomplete.mjs.map