novo-elements
Version:
782 lines (781 loc) • 68.3 kB
JavaScript
import * as i2 from '@angular/cdk/a11y';
import { ActiveDescendantKeyManager, A11yModule } from '@angular/cdk/a11y';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { SelectionModel } from '@angular/cdk/collections';
import { hasModifierKey } from '@angular/cdk/keycodes';
import * as i0 from '@angular/core';
import { EventEmitter, signal, input, booleanAttribute, computed, QueryList, HostListener, Input, ViewChild, ViewChildren, ContentChildren, Output, Optional, Self, Inject, Component, inject, Directive, NgModule } from '@angular/core';
import * as i4 from '@angular/forms';
import { NgControl, FormsModule } from '@angular/forms';
import { Subscription, Subject, merge, of } from 'rxjs';
import { filter, map, takeUntil, startWith, take } from 'rxjs/operators';
import * as i1 from 'novo-elements/services';
import { Helpers } from 'novo-elements/utils';
import * as i3 from 'novo-elements/elements/common';
import { mixinOverlay, mixinTabIndex, mixinRequired, mixinDisabled, mixinErrorState, _countGroupLabelsBeforeOption, _getOptionScrollPosition, NovoOverlayTemplateComponent, NovoOption, NovoOptgroup, NOVO_OPTION_PARENT_COMPONENT, NovoOptionModule, NovoOverlayModule } from 'novo-elements/elements/common';
import * as i11 from 'novo-elements/elements/field';
import { NOVO_FORM_FIELD, NovoFieldControl } from 'novo-elements/elements/field';
import * as i5 from '@angular/common';
import { CommonModule } from '@angular/common';
import * as i6 from 'novo-elements/elements/button';
import { NovoButtonModule } from 'novo-elements/elements/button';
import * as i7 from 'novo-elements/elements/divider';
import { NovoDividerModule } from 'novo-elements/elements/divider';
import * as i8 from 'novo-elements/elements/tooltip';
import { NovoTooltipModule } from 'novo-elements/elements/tooltip';
import * as i9 from 'novo-elements/elements/icon';
import { NovoIconModule } from 'novo-elements/elements/icon';
import * as i10 from 'novo-elements/pipes';
import { NovoPipesModule } from 'novo-elements/pipes';
// NG
/** Change event object that is emitted when the select value has changed. */
class NovoSelectChange {
constructor(
/** Reference to the select that emitted the change event. */
source,
/** Current value of the select that emitted the event. */
value) {
this.source = source;
this.value = value;
}
}
// Create Base Class from Mixins
// Boilerplate for applying mixins
class NovoSelectBase {
constructor(_defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl) {
this._defaultErrorStateMatcher = _defaultErrorStateMatcher;
this._parentForm = _parentForm;
this._parentFormGroup = _parentFormGroup;
this.ngControl = ngControl;
}
}
const NovoSelectMixins = mixinOverlay(mixinTabIndex(mixinRequired(mixinDisabled(mixinErrorState(NovoSelectBase)))));
let nextId = 0;
class NovoSelectElement extends NovoSelectMixins {
/**
* Implemented as part of NovoFieldControl.
* @docs-private
*/
get value() {
return this._value;
}
set value(newValue) {
// Always re-assign an array, because it might have been mutated.
if (newValue !== this._value || (this._multiple && Array.isArray(newValue))) {
if (this.options) {
this._setSelectionByValue(newValue);
}
this._value = newValue;
this._lingeringDisplayValue = null;
}
}
/** Whether the user should be allowed to select multiple options. */
get multiple() {
return this._multiple;
}
set multiple(value) {
this._multiple = coerceBooleanProperty(value);
this.position = 'above-below';
}
/** Array of options to display in the select dropdown */
set options(options) {
this._options = options;
this._initLegacyOptions();
}
get options() {
return this._options;
}
/** Whether the select is focused. */
get focused() {
return this._focused || this.panelOpen;
}
/** Implemented as part of NovoFieldControl. */
get empty() {
return Helpers.isEmpty(this._value);
}
/** The currently selected option. */
get selected() {
return this.multiple ? this._selectionModel.selected : this._selectionModel.selected[0];
}
/** The value displayed in the trigger. */
get displayValue() {
if (this.empty) {
return '';
}
if (this._multiple) {
const selectedOptions = this._selectionModel.selected.map((option) => this._getDisplayValue(option));
return selectedOptions.join(', ');
}
return this._getDisplayValue(this._selectionModel.selected[0]);
}
constructor(elementRef, labels, ref, focusMonitor, ngZone, injector, defaultErrorStateMatcher, ngControl, _parentForm, _parentFormGroup, _fieldElement) {
super(defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl);
this.elementRef = elementRef;
this.labels = labels;
this.ref = ref;
this.focusMonitor = focusMonitor;
this.ngZone = ngZone;
this.injector = injector;
this._fieldElement = _fieldElement;
this._uniqueId = `novo-select-${++nextId}`;
this._stateChanges = Subscription.EMPTY;
this._activeOptionChanges = Subscription.EMPTY;
this._selectedOptionChanges = Subscription.EMPTY;
this._destroy = new Subject();
this.controlType = 'select';
/** @docs-private Implemented as part of NovoFieldControl. */
this.lastKeyValue = null;
/** User defined tab index. */
this._userTabIndex = null;
/**
* The display string for the current value, kept from when the associated <novo-option> has stopped rendering
* due to filtration
*/
this._lingeringDisplayValue = null;
this.id = this._uniqueId;
this.name = this._uniqueId;
this.placeholder = 'Select...';
this.position = 'above-below';
this.displayIcon = null;
this.onSelect = new EventEmitter();
/** Event emitted when the selected value has been changed by the user. */
this.selectionChange = new EventEmitter();
/** Event that emits whenever the raw value of the select changes.*/
this.valueChange = new EventEmitter();
/** Event emitted when the select panel has been toggled. */
this.openedChange = new EventEmitter();
/** Event emitted when the select has been opened. */
this._openedStream = this.openedChange.pipe(filter((o) => o), map(() => { }));
/** Event emitted when the select has been closed. */
this._closedStream = this.openedChange.pipe(filter((o) => !o), map(() => { }));
/** Function that maps an option's control value to its display value in the trigger. */
this.displayWith = null;
/** * Function to compare the option values with the selected values. */
this.compareWith = (o1, o2) => o1 === o2 || o1 === o2.id || (!Helpers.isEmpty(o1.id) && !Helpers.isEmpty(o2.id) && o1.id === o2.id);
this.header = {
open: false,
valid: true,
value: '',
};
this.onModelChange = () => { };
this.onModelTouched = () => { };
this.filterTerm = '';
this.filteredOptions = [];
this.disabled = false;
this.viewOptionsSignal = signal(null, ...(ngDevMode ? [{ debugName: "viewOptionsSignal" }] : []));
this.contentOptionsSignal = signal(null, ...(ngDevMode ? [{ debugName: "contentOptionsSignal" }] : []));
// This signal may be set programmatically by a SelectSearchComponent.
this.hideLegacyOptionsForSearch = signal(false, ...(ngDevMode ? [{ debugName: "hideLegacyOptionsForSearch" }] : []));
this.hideLegacyOptions = input(false, ...(ngDevMode ? [{ debugName: "hideLegacyOptions", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
this.displayLegacyOptions = computed(() => {
return !(this.hideLegacyOptionsForSearch() || this.hideLegacyOptions());
}, ...(ngDevMode ? [{ debugName: "displayLegacyOptions" }] : []));
this._value = null;
this._multiple = false;
this._focused = false;
this._optionsComputed = computed(() => {
return [...(this.viewOptionsSignal() || []), ...(this.contentOptionsSignal() || [])];
}, ...(ngDevMode ? [{ debugName: "_optionsComputed" }] : []));
if (ngControl) {
ngControl.valueAccessor = this;
}
this._selectionModel = new SelectionModel(this.multiple);
// Initialize KeyManager to manage keyboard events
this._initKeyManager();
}
ngOnInit() {
this.stateChanges.next();
this._initLegacyOptions();
this.focusMonitor.monitor(this.elementRef.nativeElement).subscribe((origin) => this.ngZone.run(() => {
this._focused = !!origin;
this.stateChanges.next();
}));
}
ngOnChanges(changes) {
// Updating the disabled state is handled by `mixinDisabled`, but we need to additionally let
// the parent form field know to run change detection when the disabled state changes.
if (changes?.disabled) {
this.stateChanges.next();
}
if (changes?.multiple) {
// TODO: copy selection over??
this._selectionModel = new SelectionModel(this.multiple);
}
this._initLegacyOptions();
}
ngAfterViewInit() {
// Subscribe to NovoOption selections
this._watchSelectionEvents();
// Set initial value
this._initializeSelection();
// Listen to selection changes to select and deselect options
this._selectionModel.changed.pipe(takeUntil(this._destroy)).subscribe((event) => {
event.added.forEach((option) => {
if (option.select) {
option.select();
}
});
event.removed.forEach((option) => {
if (option.deselect) {
option.deselect();
}
});
});
// Listen to QueryList changes
merge(this.contentOptions.changes, this.viewOptions.changes)
.pipe(takeUntil(this._destroy))
.subscribe(() => {
this._watchSelectionEvents();
this._initializeSelection();
});
this.contentOptions.changes.pipe(takeUntil(this._destroy), startWith(this.contentOptions))
.subscribe(contentOptions => {
this.contentOptionsSignal.set(contentOptions.toArray());
});
this.viewOptions.changes.pipe(takeUntil(this._destroy), startWith(this.viewOptions))
.subscribe(viewOptions => {
this.viewOptionsSignal.set(viewOptions.toArray());
});
merge(this.overlay.opening, this.overlay.closing)
.pipe(takeUntil(this._destroy))
.subscribe(() => {
this.openedChange.emit(this.panelOpen);
});
setTimeout(() => {
if (this._fieldElement?._labelElement) {
this._ariaLabelledBy = this._fieldElement._labelElement.id;
}
});
}
ngOnDestroy() {
this._destroy.next();
this._destroy.complete();
this._stateChanges.unsubscribe();
this._activeOptionChanges.unsubscribe();
this._selectedOptionChanges.unsubscribe();
this.focusMonitor.stopMonitoring(this.elementRef.nativeElement);
}
onClick() {
this.togglePanel();
return false;
}
openPanel() {
super.openPanel();
this._highlightCorrectOption();
}
_initializeSelection() {
// Defer setting the value in order to avoid the "Expression
// has changed after it was checked" errors from Angular.
Promise.resolve().then(() => {
this._setSelectionByValue(this.ngControl ? this.ngControl.value : this._value);
this.stateChanges.next();
});
}
/**
* Sets the selected option based on a value. If no option can be
* found with the designated value, the select trigger is cleared.
*/
_setSelectionByValue(value) {
this._selectionModel.selected.forEach((option) => {
if (option.setInactiveStyles) {
option.setInactiveStyles();
}
});
this._selectionModel.clear();
if (this.multiple && value) {
value.forEach((currentValue) => this._selectValue(currentValue));
this._sortValues();
}
else if (this._keyManager) {
const correspondingOption = this._selectValue(value);
// Shift focus to the active item. Note that we shouldn't do this in multiple
// mode, because we don't know what option the user interacted with last.
if (correspondingOption) {
this._keyManager.updateActiveItem(correspondingOption);
}
else if (!this.panelOpen) {
// Otherwise reset the highlighted option. Note that we only want to do this while
// closed, because doing it while open can shift the user's focus unnecessarily.
this._keyManager.updateActiveItem(-1);
}
}
this.ref.markForCheck();
}
/**
* Finds and selects and option based on its value.
* @returns Option that has the corresponding value.
*/
_selectValue(value) {
const allOptions = this._getOptions();
const correspondingOption = allOptions.find((option) => {
// Skip options that are already in the model. This allows us to handle cases
// where the same primitive value is selected multiple times.
if (this._selectionModel.isSelected(option)) {
return false;
}
return !Helpers.isEmpty(value) && !Helpers.isEmpty(option.value) && this.compareWith(option.value, value);
});
if (correspondingOption) {
this._selectionModel.select(correspondingOption);
}
else if (value && !correspondingOption && this.displayLegacyOptions()) {
// If searchComponent is present, we are using a text input filter; so if there is an
// option not in the list, it is just filtered out
// Double Check option not already added.
const legacyOption = this.filteredOptions.find((it) => it.value === value);
if (!legacyOption) {
// Add a disabled option to the list and select it
this._legacyOption = {
disabled: true,
tooltip: 'Value is not provided in list of valid options.',
label: value?.label || value,
value,
};
this.filteredOptions.push(this._legacyOption);
this.ref.detectChanges();
}
}
return correspondingOption;
}
select(option, i, fireEvents = true) {
console.warn('select() method is deprecated');
}
clear() {
console.warn('clear() method is deprecated');
}
/**
* If the item is not disabled, 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.
*/
handleSelection(option, isUserInput = false) {
const wasSelected = this._selectionModel.isSelected(option);
if (option.value == null && !this._multiple) {
option.deselect();
this._selectionModel.clear();
if (this.value != null) {
this._propagateChanges(option.value);
}
}
else {
if (wasSelected !== option.selected) {
option.selected ? this._selectionModel.select(option) : this._selectionModel.deselect(option);
}
if (isUserInput) {
this._keyManager.setActiveItem(option);
}
if (this.multiple) {
this._sortValues();
if (isUserInput) {
this.focus();
}
}
}
const legacyOptionIndex = this.filteredOptions.lastIndexOf(this._legacyOption);
if (legacyOptionIndex !== -1) {
this.filteredOptions.splice(legacyOptionIndex, 1);
}
if (wasSelected !== this._selectionModel.isSelected(option)) {
this._propagateChanges();
}
this.stateChanges.next();
this._watchSelectionEvents();
}
_getDisplayValue(option) {
if (!option) {
return this._lingeringDisplayValue ?? '';
}
let toDisplay = option.viewValue;
if (this.displayWith) {
toDisplay = this.displayWith(option.value);
}
// 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.
let displayValue = toDisplay != null ? toDisplay : '';
// eslint-disable-next-line eqeqeq
if (displayValue != '') {
this._lingeringDisplayValue = displayValue;
}
else if (this._lingeringDisplayValue) {
displayValue = this._lingeringDisplayValue;
}
return displayValue;
}
/**
* Clear any previous selected option and emit a selection change event for this option
*/
_clearPreviousSelectedOption(skip) {
this._getOptions().forEach((option) => {
if (option !== skip && option.selected) {
option.deselect();
}
});
}
_watchSelectionEvents() {
const options = this._getOptions();
const selectionEvents = options ? merge(...options.map((option) => option.onSelectionChange)) : of();
this._selectedOptionChanges.unsubscribe();
this._selectedOptionChanges = selectionEvents.pipe(take(1)).subscribe((event) => {
this.onModelTouched();
this.handleSelection(event.source, event.isUserInput);
if (event.isUserInput && !this.multiple && this.panelOpen) {
this.closePanel();
this.focus();
}
});
}
/** Handles all keydown events on the select. */
_handleKeydown(event) {
if (!this.disabled) {
this.panelOpen ? this._handleOpenKeydown(event) : this._handleClosedKeydown(event);
}
}
/** Handles keyboard events while the select is closed. */
_handleClosedKeydown(event) {
const key = event.key;
const isArrowKey = key === "ArrowDown" /* Key.ArrowDown */ || key === "ArrowUp" /* Key.ArrowUp */ || key === "ArrowLeft" /* Key.ArrowLeft */ || key === "ArrowRight" /* Key.ArrowRight */;
const isOpenKey = key === "Enter" /* Key.Enter */ || key === " " /* Key.Space */;
const manager = this._keyManager;
// Open the select on ALT + arrow key to match the native <select>
if ((!manager.isTyping() && isOpenKey && !hasModifierKey(event)) || ((this.multiple || event.altKey) && isArrowKey)) {
event.preventDefault(); // prevents the page from scrolling down when pressing space
this.openPanel();
}
}
/** Handles keyboard events when the selected is open. */
_handleOpenKeydown(event) {
const manager = this._keyManager;
const key = event.key;
const isArrowKey = key === "ArrowDown" /* Key.ArrowDown */ || key === "ArrowUp" /* Key.ArrowUp */;
const isTyping = manager.isTyping();
if (isArrowKey && event.altKey) {
// Close the select on ALT + arrow key to match the native <select>
event.preventDefault();
this.closePanel();
// Don't do anything in this case if the user is typing,
// because the typing sequence can include the space key.
}
else if (!isTyping && (key === "Enter" /* Key.Enter */ || key === " " /* Key.Space */) && manager.activeItem && !hasModifierKey(event)) {
event.preventDefault();
manager.activeItem._selectViaInteraction();
}
else if (!isTyping && this._multiple && ['a', 'A'].includes(key) && event.ctrlKey) {
event.preventDefault();
const hasDeselectedOptions = this.options.some((opt) => !opt.disabled && !opt.selected);
this.options.forEach((option) => {
if (!option.disabled) {
hasDeselectedOptions ? option.select() : option.deselect();
}
});
}
else if ("Escape" /* Key.Escape */ === key) {
this.closePanel();
}
else {
const previouslyFocusedIndex = manager.activeItemIndex;
manager.onKeydown(event);
if (this._multiple && isArrowKey && event.shiftKey && manager.activeItem && manager.activeItemIndex !== previouslyFocusedIndex) {
manager.activeItem._selectViaInteraction();
}
}
}
writeValue(value) {
this.value = value;
}
registerOnChange(fn) {
this.onModelChange = fn;
}
registerOnTouched(fn) {
this.onModelTouched = fn;
}
setDisabledState(disabled) {
this.disabled = disabled;
}
/** Implemented as part of NovoFieldControl. */
setDescribedByIds(ids) {
this._ariaDescribedby = ids.join(' ');
}
/** Implemented as part of NovoFieldControl. */
onContainerClick(event) {
this.focus();
}
/**
* Focuses the first non-disabled chip in this chip list, or the associated input when there
* are no eligible chips.
*/
focus(options) {
if (!this.disabled) {
this.elementRef.nativeElement.focus(options);
}
}
_getOptions() {
return [...(this.viewOptions || []), ...(this.contentOptions || [])];
}
/** Sorts the selected values in the selected based on their order in the panel. */
_sortValues() {
if (this.multiple) {
// TODO.
}
}
/** Emits change event to set the model value. */
_propagateChanges(fallbackValue) {
let valueToEmit = null;
if (this.multiple) {
valueToEmit = this.selected.map((option) => option.value);
}
else {
valueToEmit = this.selected ? this.selected.value : fallbackValue;
}
this._value = valueToEmit;
this.valueChange.emit(valueToEmit);
this.onModelChange(valueToEmit);
this.onSelect.emit({ selected: valueToEmit });
this.selectionChange.emit(this._makeChangeEvent(valueToEmit));
this.ref.markForCheck();
}
_makeChangeEvent(value) {
return new NovoSelectChange(this, value);
}
/** Scrolls the active option into view. */
_scrollOptionIntoView(index) {
const options = new QueryList();
options.reset(this._getOptions());
const labelCount = _countGroupLabelsBeforeOption(index, options, this.optionGroups);
const itemHeight = this._getItemHeight();
this.panel.nativeElement.scrollTop = _getOptionScrollPosition((index + labelCount) * itemHeight, itemHeight, this.panel.nativeElement.scrollTop, this.panel.nativeElement.offsetHeight);
}
/** Sets up a key manager to listen to keyboard events on the overlay panel. */
_initKeyManager() {
this._keyManager = new ActiveDescendantKeyManager(this._optionsComputed, this.injector).withTypeAhead(250).withHomeAndEnd();
this._keyManager.tabOut.pipe(takeUntil(this._destroy)).subscribe(() => {
if (this.panelOpen) {
// Select the active item when tabbing away. This is consistent with how the native
// select behaves. Note that we only want to do this in single selection mode.
if (!this.multiple && this._keyManager.activeItem) {
this._keyManager.activeItem._selectViaInteraction();
}
// Restore focus to the trigger before closing. Ensures that the focus
// position won't be lost if the user got focus into the overlay.
this.focus();
this.closePanel();
}
});
this._keyManager.change.pipe(takeUntil(this._destroy)).subscribe(() => {
if (this.panelOpen && this.overlay) {
this._scrollOptionIntoView(this._keyManager.activeItemIndex || 0);
}
else if (!this.panelOpen && !this.multiple && this._keyManager.activeItem) {
this._keyManager.activeItem._selectViaInteraction();
}
});
}
/**
* Highlights the selected item. If no option is selected, it will highlight
* the first item instead.
*/
_highlightCorrectOption() {
if (this._keyManager) {
if (this.empty) {
this._keyManager.setFirstItemActive();
}
else {
const options = this._getOptions();
// eslint-disable-next-line eqeqeq
const index = options.findIndex(option => option.value == this._value);
this._keyManager.setActiveItem(index);
}
}
}
/** Calculates the height of the select's options. */
_getItemHeight() {
const [first] = this._getOptions();
if (first) {
return first._getHostElement().offsetHeight;
}
return 0;
}
// TODO: Deprecate this
_initLegacyOptions() {
if (this.options && this.options.length && typeof this.options[0] === 'string') {
this.filteredOptions = this.options.map((item) => {
return { value: item, label: item };
});
}
else {
this.filteredOptions = (this.options || [])
.map((item) => {
return {
...item,
disabled: Boolean(item.readOnly || item.disabled),
};
})
.map((item) => {
return {
...item,
active: false,
};
});
}
}
/**
* TODO: Deprecate all header methods
*/
toggleHeader(event, forceValue = false) {
if (event) {
event.stopPropagation();
event.preventDefault();
}
// Reverse the active property (if forceValue, use that)
this.header = {
open: forceValue !== undefined ? forceValue : !this.header.open,
value: '',
valid: true,
};
}
/**
* @deprecated use highlight pipe
*/
highlight(match, query) {
// Replaces the capture string with a the same string inside of a "strong" tag
return query ? match.replace(new RegExp(this.escapeRegexp(query), 'gi'), '<strong>$&</strong>') : match;
}
escapeRegexp(queryToEscape) {
// Ex: if the capture is "a" the result will be \a
return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
}
saveHeader() {
if (this.header.value) {
this.headerConfig.onSave(this.header.value);
this.createdItem = this.header.value;
this.closePanel();
}
else {
this.header.valid = false;
}
}
/** Determines the `aria-activedescendant` to be set on the host. */
_getAriaActiveDescendant() {
if (this.panelOpen && this._keyManager && this._keyManager.activeItem) {
return this._keyManager.activeItem.id;
}
return null;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NovoSelectElement, deps: [{ token: i0.ElementRef }, { token: i1.NovoLabelService }, { token: i0.ChangeDetectorRef }, { token: i2.FocusMonitor }, { token: i0.NgZone }, { token: i0.Injector }, { token: i3.ErrorStateMatcher }, { token: i4.NgControl, optional: true, self: true }, { token: i4.NgForm, optional: true }, { token: i4.FormGroupDirective, optional: true }, { token: NOVO_FORM_FIELD, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.19", type: NovoSelectElement, isStandalone: false, selector: "novo-select", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: false, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: false, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: false, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: false, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: false, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: false, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: false, isRequired: false, transformFunction: booleanAttribute }, headerConfig: { classPropertyName: "headerConfig", publicName: "headerConfig", isSignal: false, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: false, isRequired: false, transformFunction: null }, overlayWidth: { classPropertyName: "overlayWidth", publicName: "overlayWidth", isSignal: false, isRequired: false, transformFunction: null }, overlayHeight: { classPropertyName: "overlayHeight", publicName: "overlayHeight", isSignal: false, isRequired: false, transformFunction: null }, displayIcon: { classPropertyName: "displayIcon", publicName: "displayIcon", isSignal: false, isRequired: false, transformFunction: null }, displayWith: { classPropertyName: "displayWith", publicName: "displayWith", isSignal: false, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: false, isRequired: false, transformFunction: null }, hideLegacyOptions: { classPropertyName: "hideLegacyOptions", publicName: "hideLegacyOptions", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: false, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: false, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { onSelect: "onSelect", selectionChange: "selectionChange", valueChange: "valueChange", openedChange: "openedChange", _openedStream: "opened", _closedStream: "closed" }, host: { attributes: { "role": "combobox", "aria-autocomplete": "none", "aria-haspopup": "true" }, listeners: { "click": "onClick()", "keydown": "_handleKeydown($event)" }, properties: { "attr.id": "id", "attr.aria-controls": "panelOpen ? id + \"-panel\" : null", "attr.aria-expanded": "panelOpen", "attr.aria-required": "required.toString()", "attr.aria-disabled": "disabled.toString()", "attr.aria-invalid": "errorState", "attr.aria-labelledby": "_ariaLabelledBy || null", "attr.aria-describedby": "_ariaDescribedby || null", "attr.aria-activedescendant": "_getAriaActiveDescendant()", "class.novo-select-disabled": "disabled", "class.novo-select-invalid": "errorState", "class.novo-select-required": "required", "class.novo-select-empty": "empty", "class.novo-select-multiple": "multiple", "tabindex": "disabled ? -1 : 0" }, classAttribute: "novo-select" }, providers: [
{ provide: NovoFieldControl, useExisting: NovoSelectElement },
{ provide: NOVO_OPTION_PARENT_COMPONENT, useExisting: NovoSelectElement },
], queries: [{ propertyName: "optionGroups", predicate: NovoOptgroup, descendants: true }, { propertyName: "contentOptions", predicate: NovoOption, descendants: true }], viewQueries: [{ propertyName: "overlay", first: true, predicate: NovoOverlayTemplateComponent, descendants: true, static: true }, { propertyName: "panel", first: true, predicate: ["panel"], descendants: true }, { propertyName: "viewOptions", predicate: NovoOption, descendants: true }], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: `
<div class="novo-select-trigger">
<span class="novo-select-placeholder" *ngIf="empty">{{ placeholder }}</span>
<span class="text-ellipsis" *ngIf="!empty"><novo-icon size="sm" style="margin: 0 0 .25rem .1rem" *ngIf="displayIcon">{{ displayIcon }}</novo-icon> {{ displayValue }}</span>
<i class="bhi-collapse"></i>
</div>
<novo-overlay-template
[parent]="elementRef"
[position]="position"
[width]="overlayWidth"
[height]="overlayHeight"
(closing)="elementRef.nativeElement.focus()"
>
<div #panel class="novo-select-list" tabIndex="-1" [class.has-header]="headerConfig" [class.active]="panelOpen">
<novo-option *ngIf="headerConfig" class="select-header" [class.open]="header.open">
<novo-button *ngIf="!header.open" icon="add-thin" (click)="toggleHeader($event); (false)" tabIndex="-1" class="header">
{{ headerConfig.label }}
</novo-button>
<div *ngIf="header.open" [ngClass]="{ active: header.open }">
<input
autofocus
type="text"
[placeholder]="headerConfig.placeholder"
[attr.id]="name"
autocomplete="off"
[value]="header.value"
[ngClass]="{ invalid: !header.valid }"
/>
<footer>
<novo-button (click)="toggleHeader($event, false)">{{ labels.cancel }}</novo-button>
<novo-button (click)="saveHeader()" class="primary">{{ labels.save }}</novo-button>
</footer>
</div>
</novo-option>
<!-- Declarative Content Goes Here -->
<ng-content></ng-content>
<!-- Data Driven Content Goes Here -->
<ng-container *ngFor="let option of filteredOptions; let i = index">
<novo-option
*ngIf="!option.divider; else divider"
class="select-item"
[disabled]="option.disabled"
[class.active]="option.active"
[attr.data-automation-value]="option.label"
[value]="option.value"
[tooltip]="option.tooltip"
[tooltipPosition]="option.tooltipPosition || 'right'"
>
<span [innerHtml]="option.label | highlight:filterTerm"></span> <i *ngIf="option.active" class="bhi-check"></i>
</novo-option>
<ng-template #divider>
<novo-divider class="select-item-divider" [class.with-label]="option.label" [class.without-label]="!option.label">
{{ option?.label }}
</novo-divider>
</ng-template>
</ng-container>
</div>
</novo-overlay-template>
`, isInline: true, styles: [":host{display:block;position:relative;width:100%;max-width:800px;min-width:180px;cursor:pointer}:host:focus .novo-select-trigger{border-bottom:1px solid #4a89dc}:host:focus .novo-select-trigger i{color:#000000ba}:host:focus{outline:none}:host .novo-select-trigger{display:inline;font-weight:400;color:inherit;font-size:var(--font-size-text);transition:color .2s ease-out,opacity .2s ease-out;vertical-align:middle;display:flex;justify-content:space-between;align-items:center;background-color:transparent;border:none;border-bottom:1px solid rgb(175.4891304348,184.7826086957,192.0108695652);color:var(--text-main, #3d464d);height:2.05rem;position:relative;text-align:left;text-shadow:none;z-index:1;cursor:pointer;text-transform:none;padding:0 1rem 0 .5rem;margin-bottom:-1px;-webkit-appearance:none}:host .novo-select-trigger.text-capitalize{text-transform:capitalize}:host .novo-select-trigger.text-uppercase{text-transform:uppercase}:host .novo-select-trigger.text-nowrap{white-space:nowrap}:host .novo-select-trigger.text-ellipsis{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}:host .novo-select-trigger.text-size-default{font-size:inherit}:host .novo-select-trigger.text-size-body{font-size:1.3rem}:host .novo-select-trigger.text-size-xs{font-size:1rem}:host .novo-select-trigger.text-size-sm{font-size:1.2rem}:host .novo-select-trigger.text-size-md{font-size:1.3rem}:host .novo-select-trigger.text-size-lg{font-size:1.6rem}:host .novo-select-trigger.text-size-xl{font-size:2rem}:host .novo-select-trigger.text-size-2xl{font-size:2.6rem}:host .novo-select-trigger.text-size-3xl{font-size:3.2rem}:host .novo-select-trigger.text-size-smaller{font-size:.8em}:host .novo-select-trigger.text-size-larger{font-size:1.2em}:host .novo-select-trigger.text-color-black{color:#000}:host .novo-select-trigger.text-color-white{color:#fff}:host .novo-select-trigger.text-color-gray{color:#9e9e9e}:host .novo-select-trigger.text-color-grey{color:#9e9e9e}:host .novo-select-trigger.text-color-offWhite{color:#f7f7f7}:host .novo-select-trigger.text-color-bright{color:#f7f7f7}:host .novo-select-trigger.text-color-light{color:#dbdbdb}:host .novo-select-trigger.text-color-neutral{color:#4f5361}:host .novo-select-trigger.text-color-dark{color:#3d464d}:host .novo-select-trigger.text-color-orange{color:#ff6900}:host .novo-select-trigger.text-color-navigation{color:#202945}:host .novo-select-trigger.text-color-skyBlue{color:#009bdf}:host .novo-select-trigger.text-color-steel{color:#5b6770}:host .novo-select-trigger.text-color-metal{color:#637893}:host .novo-select-trigger.text-color-sand{color:#f4f4f4}:host .novo-select-trigger.text-color-silver{color:#e2e2e2}:host .novo-select-trigger.text-color-stone{color:#bebebe}:host .novo-select-trigger.text-color-ash{color:#a0a0a0}:host .novo-select-trigger.text-color-slate{color:#707070}:host .novo-select-trigger.text-color-onyx{color:#526980}:host .novo-select-trigger.text-color-charcoal{color:#282828}:host .novo-select-trigger.text-color-moonlight{color:#1a242f}:host .novo-select-trigger.text-color-midnight{color:#202945}:host .novo-select-trigger.text-color-darkness{color:#161f27}:host .novo-select-trigger.text-color-navy{color:#0d2d42}:host .novo-select-trigger.text-color-aqua{color:#3bafda}:host .novo-select-trigger.text-color-ocean{color:#4a89dc}:host .novo-select-trigger.text-color-mint{color:#37bc9b}:host .novo-select-trigger.text-color-grass{color:#8cc152}:host .novo-select-trigger.text-color-sunflower{color:#f6b042}:host .novo-select-trigger.text-color-bittersweet{color:#eb6845}:host .novo-select-trigger.text-color-grapefruit{color:#da4453}:host .novo-select-trigger.text-color-carnation{color:#d770ad}:host .novo-select-trigger.text-color-lavender{color:#967adc}:host .novo-select-trigger.text-color-mountain{color:#9678b6}:host .novo-select-trigger.text-color-info{color:#4a89dc}:host .novo-select-trigger.text-color-positive{color:#4a89dc}:host .novo-select-trigger.text-color-success{color:#8cc152}:host .novo-select-trigger.text-color-negative{color:#da4453}:host .novo-select-trigger.text-color-danger{color:#da4453}:host .novo-select-trigger.text-color-error{color:#da4453}:host .novo-select-trigger.text-color-warning{color:#f6b042}:host .novo-select-trigger.text-color-empty{color:#cccdcc}:host .novo-select-trigger.text-color-disabled{color:#bebebe}:host .novo-select-trigger.text-color-background{color:#f7f7f7}:host .novo-select-trigger.text-color-backgroundDark{color:#e2e2e2}:host .novo-select-trigger.text-color-presentation{color:#5b6770}:host .novo-select-trigger.text-color-bullhorn{color:#ff6900}:host .novo-select-trigger.text-color-pulse{color:#3bafda}:host .novo-select-trigger.text-color-company{color:#39d}:host .novo-select-trigger.text-color-candidate{color:#4b7}:host .novo-select-trigger.text-color-lead{color:#a69}:host .novo-select-trigger.text-color-contact{color:#fa4}:host .novo-select-trigger.text-color-clientcontact{color:#fa4}:host .novo-select-trigger.text-color-opportunity{color:#625}:host .novo-select-trigger.text-color-job{color:#b56}:host .novo-select-trigger.text-color-joborder{color:#b56}:host .novo-select-trigger.text-color-submission{color:#a9adbb}:host .novo-select-trigger.text-color-sendout{color:#747884}:host .novo-select-trigger.text-color-placement{color:#0b344f}:host .novo-select-trigger.text-color-note{color:#747884}:host .novo-select-trigger.text-color-contract{color:#454ea0}:host .novo-select-trigger.text-color-task{color:#4f5361}:host .novo-select-trigger.text-color-jobCode{color:#696d79}:host .novo-select-trigger.text-color-earnCode{color:#696d79}:host .novo-select-trigger.text-color-invoiceStatement{color:#696d79}:host .novo-select-trigger.text-color-billableCharge{color:#696d79}:host .novo-select-trigger.text-color-payableCharge{color:#696d79}:host .novo-select-trigger.text-color-user{color:#696d79}:host .novo-select-trigger.text-color-corporateUser{color:#696d79}:host .novo-select-trigger.text-color-distributionList{color:#696d79}:host .novo-select-trigger.text-color-credential{color:#696d79}:host .novo-select-trigger.text-color-person{color:#696d79}:host .novo-select-trigger.margin-before{margin-top:.4rem}:host .novo-select-trigger.margin-after{margin-bottom:.8rem}:host .novo-select-trigger.text-length-small{max-width:40ch}:host .novo-select-trigger.text-length-medium{max-width:55ch}:host .novo-select-trigger.text-length-large{max-width:70ch}:host .novo-select-trigger.text-weight-hairline{font-weight:100}:host .novo-select-trigger.text-weight-thin{font-weight:200}:host .novo-select-trigger.text-weight-light{font-weight:300}:host .novo-select-trigger.text-weight-normal{font-weight:400}:host .novo-select-trigger.text-weight-medium{font-weight:500}:host .novo-select-trigger.text-weight-semibold{font-weight:600}:host .novo-select-trigger.text-weight-bold{font-weight:700}:host .novo-select-trigger.text-weight-extrabold{font-weight:800}:host .novo-select-trigger.text-weight-heavy{font-weight:900}:host .novo-select-trigger.text-weight-lighter{font-weight:lighter}:host .novo-select-trigger.text-weight-bolder{font-weight:bolder}:host .novo-select-trigger.empty{color:#a9a9a9}:host .novo-select-trigger:hover{border-bottom:1px solid rgb(94.8152173913,108.8043478261,119.6847826087)}:host .novo-select-trigger .novo-select-placeholder{color:var(--form-placeholder)}:host .novo-select-trigger i{font-size:.8em;color:#3d464d;position:absolute;right:0}:host[disabled],:host.novo-select-disabled{pointer-events:none}:host[disabled] div[type=button],:host.novo-select-disabled div[type=button]{color:#9e9e9e}:host[disabled] i,:host.novo-select-disabled i{color:#9e9e9e!important}:host[disabled] .novo-select-trigger,:host.novo-select-disabled .novo-select-trigger{color:#9e9e9e!important;border-bottom:1px dashed #9e9e9e!important}:host[disabled] .novo-select-trigger:hover,:host.novo-select-disabled .novo-select-trigger:hover{color:#9e9e9e!important;border-bottom:1px dashed #9e9e9e!important}.novo-select-list{background-color:var(--background-bright);cursor:default;list-style:none;overflow:auto;margin:0;padding:0;width:100%;box-shadow:0 -1px 3px -2px #0003,0 2px 2px #00000024,0 1px 5px #0000001f;font-size:1rem;z-index:-1;opacity:0}.novo-select-list.active{z-index:1000;max-height:219px;min-width:200px;width:100%;max-width:800px;width:inherit;overflow:auto;opacity:1}.novo-select-list .select-item{height:35px}.select-header button{text-transform:uppercase}.select-header button.header{color:#4a89dc;position:relative;text-align:left;cursor:pointer;height:3rem;margin:0;padding:.5rem 1.6rem 0 0;box-sizing:border-box;border:none;display:block;align-items:center;justify-content:space-between;font-size:1rem}.select-header button.header:focus,.select-header button.header:hover{color:#4f4f4f}.select-header button.header i{color:#4a89dc;padding-right:10px}.select-header button.header span{text-align:left}.select-header div.active{width:100%;float:right;padding:5px}.select-header div.active footer{float:right}.select-header div.active button{display:inline-block;border:none;float:left;width:auto;font-weight:500;font-size:.8rem;color:#acacac}.select-header div.active button:hover{color:#868686}.select-header div.active button.primary{color:#4a89dc}.select-header div.active button.primary:hover{color:#2363b6}.select-header div.active input{display:flex;justify-content:space-between;align-items:center;background-color:transparent;border:none;border-bottom:1px solid rgba(0,0,0,.12);color:#000000ba;height:2.05rem;position:relative;text-align:left;text-shadow:none;width:100%;z-index:1;cursor:pointer;text-transform:none;padding-top:10px;font-size:1rem}.select-header div.active input.empty{color:#a9a9a9}.select-header div.active input:focus{outline:none}.select-header div.active input:hover{border-bottom:1px solid #4a89dc}.select-header div.active input.invalid{border-bottom:1px solid #da4453}\n"], dependencies: [{ kind: "directive", type: i5.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i5.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i5.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i6.NovoButtonElement, selector: "novo-button,button[theme]", inputs: ["color", "side", "size", "theme", "loading", "icon", "secondIcon", "disabled"] }, { kind: "component", type: i7.NovoDividerComponent, selector: "novo-divider", inputs: ["vertical", "inset"] }, { kind: "component", type: i3.NovoOption, selector: "novo-option", inputs: ["selected", "keepOpen", "novoInert", "value", "disabled"], exportAs: ["novoOption"] }, { kind: "component", type: i3.NovoOverlayTemplateComponent, selector: "novo-overlay-template", inputs: ["position", "scrollStrategy", "width", "minWidth", "height", "closeOnSelect", "hasBackdrop", "parent"], outputs: ["select", "opening", "closing", "backDropClicked"] }, { kind: "directive", type: i8.TooltipDirective, selector: "[tooltip]", inputs: ["tooltip", "tooltipPosition", "tooltipType", "tooltipSize", "tooltipBounce", "tooltipNoAnimate", "tooltipRounded", "tooltipAlways", "tooltipPreline", "removeTooltipArrow", "tooltipAutoPosition", "tooltipIsHTML", "tooltipCloseOnClick", "tooltipOnOverflow", "tooltipActive"] }, { kind: "component", type: i9.NovoIconComponent, selector: "novo-icon", inputs: ["raised", "theme", "shape", "color", "size", "smaller", "larger", "alt", "name"] }, { kind: "pipe", type: i10.HighlightPipe, name: "highlight" }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.19", ngImport: i0, type: NovoSelectElement, decorators: [{
type: Component,
args: [{ selector: 'novo-select', inputs: ['disabled', 'required', 'tabIndex'], providers: [
{ provide: NovoFieldControl, useExisting: NovoSelectElement },
{ provide: NOVO_OPTION_PARENT_COMPONENT, useExisting: NovoSelectElement },
], template: `
<div class="novo-select-trigger">
<span class="novo-select-placeholder" *ngIf="empty">{{ placeholder }}</span>
<span class="text-ellipsis" *ngIf="!empty"><novo-icon size="sm" style="margin: 0 0 .25rem .1rem" *ngIf="displayIcon">{{ displayIcon }}</novo-icon> {{ displayValue }}</span>
<i class="bhi-collapse"></i>
</div>
<novo-overlay-template
[parent]="elementRef"
[position]="position"
[width]="overlayWidth"
[height]="overlayHeight"
(closing)="elementRef.nativeElement.focus()"
>
<div #panel class="novo-select-list" tabIndex="-1" [class.has-header]="headerConfig" [class.active]="panelOpen">
<novo-option *ngIf="headerConfig" class="select-header" [class.open]="header.open">
<novo-button *ngIf="!header.open" icon="add-thin" (click)="toggleHeader($event); (false)" tabIndex="-1" class="header">
{{ headerConfig.label }}
</novo-button>
<div *ngIf="header.open" [ngClass]="{ active: header.open }">
<input
autofocus
type="text"
[placeholder]="headerConfig.placeholder"
[attr.id]="name"
autocomplete="off"
[value]="header.value"
[ngClass]="{ invalid: !header.valid }"
/>
<footer>
<novo-button (click)="toggleHeader($event, false)">{{ labels.cancel }}</novo-button>
<novo-button (click)="saveHeader()" class="primary">{{ labels.save }}</novo-button>
</footer>
</div>
</novo-option>
<!-- Declarative Content Goes Here -->
<ng-content></ng-content>
<!-- Data Driven Content Goes Here -->
<ng-container *ngFor="let option of filteredOptions; let i = index">
<novo-option
*ngIf="!option.divider; else divider"
class="select-item"
[disabled]="option.disabled"
[class.active]="option.active"
[attr.data-automation-value]="option.label"
[value]="option.value"
[tooltip]="option.tooltip"
[tooltipPosition]="option.tooltipPosition || 'right'"
>
<span [innerHtml]="option.label | highlight:filterTerm"></span> <i *ngIf="option.active" class="bhi-check"></i>
</no