@alyle/ui
Version:
Minimal Design, a set of components for Angular
1,143 lines • 53.8 kB
JavaScript
import { __decorate } from 'tslib';
import { trigger, transition, animate, keyframes, style } from '@angular/animations';
import * as i0 from '@angular/core';
import { Directive, isDevMode, EventEmitter, Input, Output, ContentChild, forwardRef, ContentChildren, ViewChild, Optional, Self, ChangeDetectionStrategy, Component, HostListener, NgModule } from '@angular/core';
import * as i2 from '@alyle/ui/field';
import { STYLES as STYLES$1, LyFieldControlBase } from '@alyle/ui/field';
import * as i1 from '@alyle/ui';
import { shadowBuilder, st2c, StyleCollection, mixinTabIndex, Dir, LyOverlayPosition, YPosition, XPosition, StyleRenderer, mixinDisableRipple, Style, LyCommonModule, LyOverlayModule } from '@alyle/ui';
import { Subject, defer, merge } from 'rxjs';
import { startWith, switchMap, take, filter, mapTo, distinctUntilChanged, takeUntil } from 'rxjs/operators';
import { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
import { ESCAPE, DOWN_ARROW, UP_ARROW, LEFT_ARROW, RIGHT_ARROW, ENTER, SPACE, hasModifierKey, A } from '@angular/cdk/keycodes';
import { coerceNumberProperty, coerceBooleanProperty } from '@angular/cdk/coercion';
import { SelectionModel } from '@angular/cdk/collections';
import * as i3 from '@angular/cdk/scrolling';
import * as i4 from '@angular/forms';
import * as i5 from '@angular/common';
import { CommonModule } from '@angular/common';
import * as i6 from '@angular/cdk/platform';
import * as i7 from '@alyle/ui/checkbox';
import { LyCheckboxModule } from '@alyle/ui/checkbox';
/**
* Returns an exception to be thrown when attempting to assign a non-array value to a select
* in `multiple` mode. Note that `undefined` and `null` are still valid values to allow for
* resetting the value.
* @docs-private
*/
function getLySelectNonArrayValueError() {
return Error('Value must be an array in multiple-selection mode.');
}
/**
* Returns an exception to be thrown when assigning a non-function value to the comparator
* used to determine if a value corresponds to an option. Note that whether the function
* actually takes two values and returns a boolean is not checked.
*/
function getLySelectNonFunctionValueError() {
return Error('`compareWith` must be a function.');
}
const DEFAULT_DISABLE_RIPPLE = false;
const STYLE_PRIORITY = -2;
const STYLES = (theme, ref) => {
ref.renderStyleSheet(STYLES$1);
const field = ref.selectorsOf(STYLES$1);
const select = ref.selectorsOf(STYLES);
const { after } = theme;
return {
$priority: STYLE_PRIORITY,
$name: LySelect.и,
root: () => (_className) => `${_className}{display:block;position:relative;padding-${after}:1em;min-height:1em;-webkit-tap-highlight-color:transparent;}${st2c(((theme.select
&& theme.select.root
&& (theme.select.root instanceof StyleCollection
? theme.select.root.setTransformer(fn => fn(select))
: theme.select.root(select)))), `${_className}`)}`,
container: (_className) => `${_className}{background:${theme.background.primary.default};border-radius:2px;box-shadow:${shadowBuilder(4)};display:block;transform-origin:inherit;pointer-events:all;overflow:auto;max-height:256px;}`,
valueText: (_className) => `${_className}{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}`,
option: (_className) => `${_className}{display:flex;font-family:${theme.typography.fontFamily};color:${theme.text.default};-webkit-tap-highlight-color:transparent;background-color:rgba(0,0,0,0);border:0;padding:0 1em;margin:0;outline:none;box-sizing:border-box;position:relative;justify-content:flex-start;align-items:center;align-content:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;line-height:1.125;height:3em;cursor:pointer;}`,
optionActive: (_className) => `${_className}{background:${theme.hover};}`,
optionText: (_className) => `ly-checkbox ~ ${_className}{display:flex;align-items:inherit;align-content:inherit;}`,
content: (_className) => `${_className}{padding:0;display:flex;justify-content:inherit;align-items:inherit;align-content:inherit;width:100%;height:100%;box-sizing:border-box;}`,
/** Style for field */
_cursorPointer: () => (_className) => `${_className}:not(${field.disabled}) ${field.container}{cursor:pointer;}`,
};
};
/** Change event object that is emitted when the select value has changed. */
class LySelectChange {
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;
}
}
/** @docs-private */
const ANIMATIONS = [
trigger('selectEnter', [
transition('void => in', [
animate('125ms cubic-bezier(0, 0, 0.2, 1)', keyframes([
style({
opacity: 0,
transform: 'scaleY(0.9)'
}),
style({
opacity: 1,
transform: 'scaleY(1)'
})
]))
]),
transition('* => void', animate('100ms 25ms linear', style({ opacity: 0 })))
]),
];
/** @docs-private */
class LySelectBase {
}
/** @docs-private */
const LySelectMixinBase = mixinTabIndex(LySelectBase);
/**
* Allows the user to customize the trigger that is displayed when the select has a value.
*/
class LySelectTrigger {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: LySelectTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.10", type: LySelectTrigger, isStandalone: false, selector: "ly-select-trigger", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: LySelectTrigger, decorators: [{
type: Directive,
args: [{
selector: 'ly-select-trigger',
standalone: false
}]
}] });
class LySelect extends LySelectMixinBase {
/** @docs-private */
static { this.и = 'LySelect'; }
_onFocus() {
if (!this.disabled) {
this._focused = true;
this.stateChanges.next();
}
}
_onBlur() {
this._focused = false;
if (!this.disabled && !this._opened) {
this.onTouched();
this._cd.markForCheck();
this.stateChanges.next();
}
}
/** Time to wait in milliseconds after the last keystroke before moving focus to an item. */
get typeaheadDebounceInterval() { return this._typeaheadDebounceInterval; }
set typeaheadDebounceInterval(value) {
const newVal = coerceNumberProperty(value);
if (this._typeaheadDebounceInterval !== newVal && this._keyManager) {
this._typeaheadDebounceInterval = newVal;
this._keyManager.withTypeAhead(newVal);
}
}
/** Value of the select control. */
get value() { return this._value; }
set value(newValue) {
if (newValue !== this._value) {
if (this.options) {
this._setSelectionByValue(newValue);
}
this._value = newValue;
}
}
/** Whether the input is disabled. */
set disabled(val) {
if (val !== this._disabled) {
this._disabled = coerceBooleanProperty(val);
if (this._field) {
if (!val && this._hasDisabledClass) {
this._renderer.removeClass(this._field._getHostElement(), this._field.classes.disabled);
this._field.sRenderer.addClass(this.classes._cursorPointer);
this._hasDisabledClass = undefined;
}
else if (val) {
this._renderer.addClass(this._field._getHostElement(), this._field.classes.disabled);
this._field.sRenderer.removeClass(this.classes._cursorPointer);
this._hasDisabledClass = true;
}
}
this.stateChanges.next();
}
}
get disabled() {
if (this.ngControl && this.ngControl.disabled !== null) {
return this.ngControl.disabled;
}
return this._disabled;
}
set required(value) {
this._required = coerceBooleanProperty(value);
}
get required() { return this._required; }
set multiple(value) {
this._multiple = coerceBooleanProperty(value);
}
get multiple() { return this._multiple; }
/**
* @deprecated has been deprecated in favor of `compareWith`
*/
set valueKey(fn) {
this._valueKey = fn;
console.warn('LySelect: `[valueKey]` has been deprecated in favor of `[compareWith]`');
}
get valueKey() { return this._valueKey; }
set placeholder(val) {
this._placeholder = val;
}
get placeholder() { return this._placeholder; }
/**
* Function to compare the option values with the selected values. The first argument
* is a value from an option. The second is a value from the selection. A boolean
* should be returned.
*/
get compareWith() { return this._compareWith; }
set compareWith(fn) {
if (typeof fn !== 'function' && isDevMode()) {
throw getLySelectNonFunctionValueError();
}
this._compareWith = fn;
if (this._selectionModel) {
// A different comparator means the selection could change.
this._initializeSelection();
}
}
get focused() {
return this._focused;
}
get empty() {
return !this._selectionModel || this._selectionModel.isEmpty();
}
get floatingLabel() {
return this._opened ? true : !this.empty;
}
/** The value displayed in the trigger. */
get triggerValue() {
if (this._multiple) {
const selectedOptions = this._selectionModel.selected.map(option => option.viewValue);
if (this._theme.variables.direction === Dir.rtl) {
selectedOptions.reverse();
}
return selectedOptions.join(', ');
}
return this._selectionModel.selected[0].viewValue;
}
/** Current selecteds */
get selected() {
const selected = this._selectionModel?.selected;
return this.multiple ? (selected || []) : selected[0];
}
constructor(_theme, sRenderer, _renderer, _el, _overlay,
/** @internal */
_field,
/** @internal */
_cd, _ngZone, _viewportRuler,
/** @docs-private */
ngControl, _parentForm, _parentFormGroup) {
super();
this._theme = _theme;
this.sRenderer = sRenderer;
this._renderer = _renderer;
this._el = _el;
this._overlay = _overlay;
this._field = _field;
this._cd = _cd;
this._ngZone = _ngZone;
this._viewportRuler = _viewportRuler;
this.ngControl = ngControl;
this._parentForm = _parentForm;
this._parentFormGroup = _parentFormGroup;
/** @docs-private */
this.classes = this.sRenderer.renderSheet(STYLES);
/** The cached font-size of the trigger element. */
this._triggerFontSize = 0;
this._disabled = false;
this._required = false;
this.stateChanges = new Subject();
this._form = this._parentForm || this._parentFormGroup;
this._valueKey = same;
this._previousOffsetY = 0;
this._focused = false;
this.errorState = false;
/** Emits when the panel element is finished transforming in. */
this._panelDoneAnimatingStream = new Subject();
/** Comparison function to specify which option is displayed. Defaults to object equality. */
this._compareWith = (o1, o2) => o1 === o2;
/** Emits whenever the component is destroyed. */
this._destroy = new Subject();
/** Combined stream of all of the child options' change events. */
this.optionSelectionChanges = defer(() => {
const options = this.options;
if (options) {
return options.changes.pipe(startWith(options), switchMap(() => merge(...options.map(option => option.onSelectionChange))));
}
return this._ngZone.onStable
.pipe(take(1), switchMap(() => this.optionSelectionChanges));
});
/** 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), mapTo(null));
/** Event emitted when the select has been closed. */
this._closedStream = this.openedChange.pipe(filter(o => !o), mapTo(null));
/** 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 is here primarily
* to facilitate the two-way binding for the `value` input.
* @docs-private
*/
this.valueChange = new EventEmitter();
/**
* The registered callback function called when a change event occurs on the input element.
*/
this.onChange = (_) => { };
/**
* The registered callback function called when a blur event occurs on the input element.
*/
this.onTouched = () => { };
if (this.ngControl) {
// Note: we provide the value accessor through here, instead of
// the `providers` to avoid running into a circular import.
this.ngControl.valueAccessor = this;
}
this._field?.sRenderer.addClass(this.classes._cursorPointer);
}
ngOnInit() {
this._selectionModel = new SelectionModel(this.multiple);
this.stateChanges.next();
// We need `distinctUntilChanged` here, because some browsers will
// fire the animation end event twice for the same animation. See:
// https://github.com/angular/angular/issues/24084
this._panelDoneAnimatingStream
.pipe(distinctUntilChanged((prev, curr) => prev.state === curr.state), takeUntil(this._destroy))
.subscribe((re) => {
if (this._opened) {
this.openedChange.emit(true);
}
if (re.state === 'void') {
if (!re.overlay.isDestroyed) {
re.overlay.destroy();
this.openedChange.emit(false);
this.stateChanges.next();
this._cd.markForCheck();
}
}
});
const ngControl = this.ngControl;
// update styles on disabled
if (ngControl && ngControl.statusChanges) {
ngControl.statusChanges.pipe(takeUntil(this._destroy)).subscribe(() => {
this.disabled = !!ngControl.disabled;
});
}
// apply class {selectArrow} to `<select>`
this._renderer.addClass(this._field._getHostElement(), this._field.classes.selectArrow);
// apply default styles
this._renderer.addClass(this._el.nativeElement, this._field.classes.inputNative);
this._renderer.addClass(this._el.nativeElement, this.classes.root);
}
ngDoCheck() {
const ngControl = this.ngControl;
if (ngControl) {
// The disabled state might go out of sync if the form group is swapped out. See #17860.
if (this._previousControl !== ngControl.control) {
if (this._previousControl !== undefined &&
ngControl.disabled !== null &&
ngControl.disabled !== this.disabled) {
this.disabled = ngControl.disabled;
}
this._previousControl = ngControl.control;
}
const oldVal = this.errorState;
const newVal = !!(this.ngControl && this.ngControl.invalid && (this.ngControl.touched || (this._form && this._form.submitted)));
if (newVal !== oldVal) {
this.errorState = newVal;
if (this._field) {
const errorClass = this._field.classes.errorState;
if (newVal) {
this._renderer.addClass(this._field._getHostElement(), errorClass);
this._errorClass = errorClass;
}
else if (this._errorClass) {
this._renderer.removeClass(this._field._getHostElement(), errorClass);
this._errorClass = undefined;
}
this.stateChanges.next();
}
}
}
}
ngAfterContentInit() {
this._initKeyManager();
this.options.changes.pipe(startWith(null), takeUntil(this._destroy)).subscribe(() => {
this._resetOptions();
this._initializeSelection();
});
this._selectionModel.changed.pipe(takeUntil(this._destroy)).subscribe(event => {
event.added.forEach(option => option.select());
event.removed.forEach(option => option.deselect());
});
Promise.resolve().then(() => {
this.value = this.ngControl ? this.ngControl.value : this._value;
this.stateChanges.next();
this._cd.markForCheck();
});
this._keyManager.change.pipe(takeUntil(this._destroy)).subscribe(() => {
if (!this._opened && !this.multiple && this._keyManager.activeItem) {
this._keyManager.activeItem._selectViaInteraction();
}
});
}
ngOnDestroy() {
this._destroy.next();
this._destroy.complete();
this.stateChanges.complete();
this._panelDoneAnimatingStream.complete();
if (this._overlayRef) {
this._overlayRef.destroy();
this._overlayRef = null;
}
}
open() {
if (this.disabled || !this.options || !this.options.length || this._opened) {
return;
}
if (this._overlayRef) {
this._overlayRef.destroy();
}
this._opened = true;
const overlayRef = this._overlay.create(this.templateRef, {
$implicit: () => overlayRef
}, {
styles: {
top: 0,
left: 0,
pointerEvents: null
},
fnDestroy: () => {
this._closeOverlay(overlayRef);
keydownEventsSuscription.unsubscribe();
this.focus();
},
onResizeScroll: this._updatePlacement.bind(this)
});
this._overlayRef = overlayRef;
const keydownEvents = overlayRef.keydownEvents();
const keydownEventsSuscription = keydownEvents.subscribe((event) => {
if (event.keyCode === ESCAPE) {
this._closeOverlay(overlayRef);
keydownEventsSuscription.unsubscribe();
}
});
this._keyManager.withHorizontalOrientation(null);
this._triggerFontSize = parseInt(getComputedStyle(this._getHostElement()).fontSize || '0');
this._highlightCorrectOption();
this._cd.markForCheck();
this.stateChanges.next();
this._ngZone.onStable.pipe(take(1)).subscribe(() => this._updatePlacement(true));
}
close() {
const overlayRef = this._overlayRef;
if (overlayRef) {
this._overlayRef = null;
this._closeOverlay(overlayRef);
}
}
_closeOverlay(overlay) {
const overlayRef = overlay;
if (overlayRef && !overlay.isDestroyed) {
this._opened = false;
this._keyManager.withHorizontalOrientation(this._theme.variables.direction);
overlayRef.detach();
this._cd.markForCheck();
this.onTouched();
// Make sure to destroy the overlayRef
this._ngZone.runOutsideAngular(() => {
setTimeout(() => {
overlayRef.destroy();
}, 250);
});
}
}
/** @docs-private */
onContainerClick() {
this.focus();
this.open();
}
/** Focuses the select element. */
focus(options) {
this._getHostElement().focus(options);
}
_getHostElement() {
return this._el.nativeElement;
}
/**
* Sets the "value" property on the input element.
*
* @param value The checked value
*/
writeValue(value) {
this.value = value;
}
/**
* Registers a function called when the control value changes.
*
* @param fn The callback function
*/
registerOnChange(fn) {
this.onChange = (_valueString) => {
fn(this.value);
};
}
/**
* Registers a function called when the control is touched.
*
* @param fn The callback function
*/
registerOnTouched(fn) { this.onTouched = fn; }
/**
* Disables the select. Part of the ControlValueAccessor interface required
* to integrate with Angular's core forms API.
*
* @param isDisabled Sets whether the component is disabled.
*/
setDisabledState(isDisabled) {
this.disabled = isDisabled;
this._cd.markForCheck();
this.stateChanges.next();
}
/** Handles all keydown events on the select. */
_handleKeydown(event) {
if (!this.disabled) {
this._opened ? this._handleOpenKeydown(event) : this._handleClosedKeydown(event);
}
}
/** Handles keyboard events while the select is closed. */
_handleClosedKeydown(event) {
const keyCode = event.keyCode;
const isArrowKey = keyCode === DOWN_ARROW || keyCode === UP_ARROW ||
keyCode === LEFT_ARROW || keyCode === RIGHT_ARROW;
const isOpenKey = keyCode === ENTER || keyCode === 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.open();
}
else if (!this.multiple) {
manager.onKeydown(event);
}
}
/** Handles keyboard events when the selected is open. */
_handleOpenKeydown(event) {
const manager = this._keyManager;
const keyCode = event.keyCode;
const isArrowKey = keyCode === DOWN_ARROW || keyCode === UP_ARROW;
const isTyping = manager.isTyping();
if (isArrowKey && event.altKey) {
// Close the select on ALT + arrow key to match the native <select>
event.preventDefault();
this.close();
// Don't do anything in this case if the user is typing,
// because the typing sequence can include the space key.
}
else if (!isTyping && (keyCode === ENTER || keyCode === SPACE) && manager.activeItem &&
!hasModifierKey(event)) {
event.preventDefault();
manager.activeItem._selectViaInteraction();
}
else if (!isTyping && this._multiple && keyCode === A && 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 {
const previouslyFocusedIndex = manager.activeItemIndex;
manager.onKeydown(event);
if (this._multiple && isArrowKey && event.shiftKey && manager.activeItem &&
manager.activeItemIndex !== previouslyFocusedIndex) {
manager.activeItem._selectViaInteraction();
}
}
}
_initializeSelection() {
// Defer setting the value in order to avoid the "Expression
// has changed after it was checked" errors from Angular.
Promise.resolve().then(() => {
if (this.ngControl) {
this._value = this.ngControl.value;
}
this._setSelectionByValue(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) {
if (this.multiple && value) {
if (!Array.isArray(value) && isDevMode()) {
throw getLySelectNonArrayValueError();
}
this._selectionModel.clear();
value.forEach((currentValue) => this._selectValue(currentValue));
this._sortValues();
}
else {
this._selectionModel.clear();
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._opened) {
// 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._cd.markForCheck();
}
/**
* Finds and selects and option based on its value.
* @returns Option that has the corresponding value.
*/
_selectValue(value) {
const correspondingOption = this.options.find((option) => {
if (this._valueKey !== same) {
return this.valueKey(option.value) === this.valueKey(value);
}
try {
// Treat null as a special reset value.
return option.value != null && this._compareWith(option.value, value);
}
catch (error) {
if (isDevMode()) {
// Notify developers of errors in their comparator.
console.warn(error);
}
return false;
}
});
if (correspondingOption) {
this._selectionModel.select(correspondingOption);
}
return correspondingOption;
}
_updatePlacement(needAutoscroll) {
const hostOverlay = this._overlayRef.containerElement;
const container = hostOverlay.querySelector('div');
const triggerFontSize = this._triggerFontSize;
const { nativeElement } = this.valueTextDivRef;
let panelWidth;
if (this.multiple) {
panelWidth = nativeElement.offsetWidth + triggerFontSize * 4;
}
else {
panelWidth = nativeElement.offsetWidth + triggerFontSize * 2;
}
// reset height & width
this._renderer.setStyle(container, 'height', 'initial');
this._renderer.setStyle(container, 'width', `${panelWidth}px`);
let selectedElement = this._selectionModel.isEmpty()
? hostOverlay.querySelector('ly-option')
: this._selectionModel.selected[0]._getHostElement();
if (!selectedElement) {
selectedElement = (hostOverlay.firstElementChild.firstElementChild || hostOverlay.firstElementChild);
}
const offset = {
y: -(-(nativeElement.offsetHeight / 2) + selectedElement.offsetTop + (selectedElement.offsetHeight / 2)),
x: -triggerFontSize
};
// scroll to selected option
if (container.scrollHeight !== container.offsetHeight) {
if (needAutoscroll) {
container.scrollTop = container.scrollTop
- (container.offsetHeight / 2 - (selectedElement.offsetTop - container.scrollTop)) + selectedElement.offsetHeight / 2;
offset.y = container.scrollTop + offset.y;
}
else {
offset.y = this._previousOffsetY;
}
}
if (this.multiple) {
offset.x -= 24;
}
this._previousOffsetY = offset.y;
const position = new LyOverlayPosition(this._theme, this._viewportRuler, nativeElement, hostOverlay)
.setYAnchor(YPosition.above)
.setXAnchor(XPosition.before)
.setXAxis(XPosition.after)
.setYAxis(YPosition.below)
.setXOffset(offset.x)
.setYOffset(offset.y)
.build();
if (needAutoscroll) {
const triggerRect = position.triggerRect;
const newScrollTop = Math.round((selectedElement.offsetTop
+ (selectedElement.offsetHeight / 2))
- (triggerRect.top + (triggerRect.height / 2) - position.rawY));
const scrollTopForOptionAtEnd = (selectedElement.offsetTop + selectedElement.offsetHeight) - container.offsetHeight;
if (newScrollTop > selectedElement.offsetTop) {
container.scrollTop = selectedElement.offsetTop;
}
else if (scrollTopForOptionAtEnd > newScrollTop) {
container.scrollTop = scrollTopForOptionAtEnd;
}
else {
container.scrollTop = newScrollTop;
}
}
this._renderer.setStyle(hostOverlay, 'left', `${position.x}px`);
this._renderer.setStyle(hostOverlay, 'top', `${position.y}px`);
this._renderer.setStyle(container, 'width', position.width ? `${position.width}px` : `${panelWidth}px`);
this._renderer.setStyle(container, 'height', position.height ? `${position.height}px` : `100%`);
this._renderer.setStyle(hostOverlay, 'transform-origin', `${position.xo}px ${position.yo}px 0`);
}
/** Sets up a key manager to listen to keyboard events on the overlay panel. */
_initKeyManager() {
this._keyManager = new ActiveDescendantKeyManager(this.options)
.withTypeAhead(this._typeaheadDebounceInterval)
.withVerticalOrientation()
.withHorizontalOrientation(this._theme.variables.direction)
.withHomeAndEnd()
.withAllowedModifierKeys(['shiftKey']);
this._keyManager.tabOut.pipe(takeUntil(this._destroy)).subscribe(() => {
if (this._opened) {
// 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.close();
}
});
this._keyManager.change.pipe(takeUntil(this._destroy)).subscribe(() => {
if (this._opened) {
this._scrollActiveOptionIntoView();
}
else if (!this._opened && !this.multiple && this._keyManager.activeItem) {
this._keyManager.activeItem._selectViaInteraction();
}
});
}
/** Sorts the selected values in the selected based on their order in the panel. */
_sortValues() {
if (this.multiple) {
const options = this.options.toArray();
this._selectionModel.sort((a, b) => {
return this.sortComparator ? this.sortComparator(a, b, options) :
options.indexOf(a) - options.indexOf(b);
});
this.stateChanges.next();
}
}
_resetOptions() {
const changedOrDestroyed = merge(this.options.changes, this._destroy);
this.optionSelectionChanges.pipe(takeUntil(changedOrDestroyed)).subscribe(event => {
this._onSelect(event.source, event.isUserInput);
if (event.isUserInput && !this.multiple && this._opened) {
this.close();
this.focus();
}
});
}
/** Invoked when an option is clicked. */
_onSelect(option, isUserInput) {
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) {
// In case the user selected the option with their mouse, we
// want to restore focus back to the trigger, in order to
// prevent the select keyboard controls from clashing with
// the ones from `ly-option`.
this.focus();
}
}
}
if (wasSelected !== this._selectionModel.isSelected(option)) {
this._propagateChanges();
}
this.stateChanges.next();
}
/** 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.onChange(valueToEmit);
this.selectionChange.emit(new LySelectChange(this, valueToEmit));
this._cd.markForCheck();
}
/**
* 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 {
this._keyManager.setActiveItem(this._selectionModel.selected[0]);
}
}
}
/** Scrolls the active option into view. */
_scrollActiveOptionIntoView() {
const el = this._overlayRef.containerElement;
const container = el.querySelector('div');
const activeOption = this._keyManager.activeItem._getHostElement();
// const containerRect = container.getBoundingClientRect();
// const activeOptionRect = ;
if (typeof activeOption.scrollIntoView === 'function') {
if (container.scrollTop > activeOption.offsetTop) {
container.scrollTop = activeOption.offsetTop;
}
else if (container.scrollTop + container.offsetHeight < activeOption.offsetTop + activeOption.offsetHeight) {
container.scrollTop = activeOption.offsetTop - container.offsetHeight + activeOption.offsetHeight;
}
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: LySelect, deps: [{ token: i1.LyTheme2 }, { token: i1.StyleRenderer }, { token: i0.Renderer2 }, { token: i0.ElementRef }, { token: i1.LyOverlay }, { token: i2.LyField, optional: true }, { token: i0.ChangeDetectorRef }, { token: i0.NgZone }, { token: i3.ViewportRuler }, { token: i4.NgControl, optional: true, self: true }, { token: i4.NgForm, optional: true }, { token: i4.FormGroupDirective, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.10", type: LySelect, isStandalone: false, selector: "ly-select", inputs: { tabIndex: "tabIndex", typeaheadDebounceInterval: "typeaheadDebounceInterval", value: "value", disabled: "disabled", required: "required", multiple: "multiple", valueKey: "valueKey", placeholder: "placeholder", compareWith: "compareWith", sortComparator: "sortComparator" }, outputs: { openedChange: "openedChange", _openedStream: "opened", _closedStream: "closed", selectionChange: "selectionChange", valueChange: "valueChange" }, host: { listeners: { "keydown": "_handleKeydown($event)", "focus": "_onFocus()", "blur": "_onBlur()" }, properties: { "attr.tabindex": "tabIndex" } }, providers: [
StyleRenderer,
{ provide: LyFieldControlBase, useExisting: LySelect }
], queries: [{ propertyName: "customTrigger", first: true, predicate: LySelectTrigger, descendants: true }, { propertyName: "options", predicate: i0.forwardRef(() => LyOption), descendants: true }], viewQueries: [{ propertyName: "templateRef", first: true, predicate: ["templateRef"], descendants: true }, { propertyName: "valueTextDivRef", first: true, predicate: ["valueText"], descendants: true }, { propertyName: "_options", first: true, predicate: i0.forwardRef(() => LyOption), descendants: true }], exportAs: ["lySelect"], usesInheritance: true, ngImport: i0, template: "<div #valueText [ngSwitch]=\"empty\">\n <div [className]=\"classes.valueText\" *ngSwitchCase=\"true\">{{ '\\u00A0' }}</div>\n <div [className]=\"classes.valueText\" *ngSwitchDefault [ngSwitch]=\"!!customTrigger\">\n <span *ngSwitchDefault>{{ triggerValue || '\\u00A0' }}</span>\n <ng-content select=\"ly-select-trigger\" *ngSwitchCase=\"true\"></ng-content>\n </div>\n</div>\n\n<ng-template #templateRef let-getOverlay>\n <div\n #container\n tabindex=\"-1\"\n role=\"listbox\"\n [className]=\"classes.container\"\n [@selectEnter]=\"'in'\"\n (@selectEnter.done)=\"_panelDoneAnimatingStream.next({state: $event.toState, overlay: getOverlay()})\"\n (keydown)=\"_handleKeydown($event)\"\n [style.font-size.px]=\"_triggerFontSize\"\n >\n <ng-content></ng-content>\n </div>\n</ng-template>", dependencies: [{ kind: "directive", type: i5.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i5.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i5.NgSwitchDefault, selector: "[ngSwitchDefault]" }], animations: [...ANIMATIONS], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: LySelect, decorators: [{
type: Component,
args: [{ selector: 'ly-select', changeDetection: ChangeDetectionStrategy.OnPush, exportAs: 'lySelect', host: {
'[attr.tabindex]': 'tabIndex',
'(keydown)': '_handleKeydown($event)',
'(focus)': '_onFocus()',
'(blur)': '_onBlur()',
}, animations: [...ANIMATIONS], inputs: ['tabIndex'], providers: [
StyleRenderer,
{ provide: LyFieldControlBase, useExisting: LySelect }
], standalone: false, template: "<div #valueText [ngSwitch]=\"empty\">\n <div [className]=\"classes.valueText\" *ngSwitchCase=\"true\">{{ '\\u00A0' }}</div>\n <div [className]=\"classes.valueText\" *ngSwitchDefault [ngSwitch]=\"!!customTrigger\">\n <span *ngSwitchDefault>{{ triggerValue || '\\u00A0' }}</span>\n <ng-content select=\"ly-select-trigger\" *ngSwitchCase=\"true\"></ng-content>\n </div>\n</div>\n\n<ng-template #templateRef let-getOverlay>\n <div\n #container\n tabindex=\"-1\"\n role=\"listbox\"\n [className]=\"classes.container\"\n [@selectEnter]=\"'in'\"\n (@selectEnter.done)=\"_panelDoneAnimatingStream.next({state: $event.toState, overlay: getOverlay()})\"\n (keydown)=\"_handleKeydown($event)\"\n [style.font-size.px]=\"_triggerFontSize\"\n >\n <ng-content></ng-content>\n </div>\n</ng-template>" }]
}], ctorParameters: () => [{ type: i1.LyTheme2 }, { type: i1.StyleRenderer }, { type: i0.Renderer2 }, { type: i0.ElementRef }, { type: i1.LyOverlay }, { type: i2.LyField, decorators: [{
type: Optional
}] }, { type: i0.ChangeDetectorRef }, { type: i0.NgZone }, { type: i3.ViewportRuler }, { type: i4.NgControl, decorators: [{
type: Optional
}, {
type: Self
}] }, { type: i4.NgForm, decorators: [{
type: Optional
}] }, { type: i4.FormGroupDirective, decorators: [{
type: Optional
}] }], propDecorators: { templateRef: [{
type: ViewChild,
args: ['templateRef']
}], valueTextDivRef: [{
type: ViewChild,
args: ['valueText']
}], _options: [{
type: ViewChild,
args: [forwardRef(() => LyOption)]
}], options: [{
type: ContentChildren,
args: [forwardRef(() => LyOption), { descendants: true }]
}], customTrigger: [{
type: ContentChild,
args: [LySelectTrigger]
}], openedChange: [{
type: Output
}], _openedStream: [{
type: Output,
args: ['opened']
}], _closedStream: [{
type: Output,
args: ['closed']
}], selectionChange: [{
type: Output
}], valueChange: [{
type: Output
}], typeaheadDebounceInterval: [{
type: Input
}], value: [{
type: Input
}], disabled: [{
type: Input
}], required: [{
type: Input
}], multiple: [{
type: Input
}], valueKey: [{
type: Input
}], placeholder: [{
type: Input
}], compareWith: [{
type: Input
}], sortComparator: [{
type: Input
}] } });
/** Event object emitted by LyOption when selected or deselected. */
class LyOptionSelectionChange {
constructor(
/** Reference to the option that emitted the event. */
source,
/** Whether the change in the option's value was a result of a user action. */
isUserInput = false) {
this.source = source;
this.isUserInput = isUserInput;
}
}
/** @docs-private */
class LyOptionBase {
constructor(_theme, _ngZone, _platform) {
this._theme = _theme;
this._ngZone = _ngZone;
this._platform = _platform;
}
}
/** @docs-private */
const LyOptionMixinBase = mixinDisableRipple(LyOptionBase);
/**
* @dynamic
*/
class LyOption extends LyOptionMixinBase {
_onClick() {
this._selectViaInteraction();
}
/** Whether or not the option is currently selected. */
get selected() { return this._selected; }
/** Whether the wrapping component is in multiple selection mode. */
get multiple() { return this._select && this._select.multiple; }
/**
* Tracks simple string values bound to the option element.
*/
set value(value) {
this._value = value;
}
get value() {
return this._value;
}
/** Whether the option is disabled. */
set disabled(value) {
this._disabled = coerceBooleanProperty(value);
}
get disabled() {
return this._disabled;
}
/** The displayed value of the option. */
get viewValue() {
return (this._getHostElement().textContent || '').trim();
}
/** The color of Select option */
get _color() {
return this._selected ? this._select._field.color : null;
}
/**
* @deprecated use instead `selected`
*/
get isSelected() {
return this._selected;
}
constructor(sRenderer,
/** @internal */
_select, _el,
/** @internal */
_rippleService, _renderer, _theme,
/** @internal */
_cd, _ngZone, platform) {
super(_theme, _ngZone, platform);
this.sRenderer = sRenderer;
this._select = _select;
this._el = _el;
this._rippleService = _rippleService;
this._cd = _cd;
/** @docs-private */
this.classes = this._theme.addStyleSheet(STYLES, STYLE_PRIORITY);
this._selected = false;
this._disabled = false;
/** Event emitted when the option is selected or deselected. */
// tslint:disable-next-line: no-output-on-prefix
this.onSelectionChange = new EventEmitter();
_renderer.addClass(_el.nativeElement, this.classes.option);
this._triggerElement = _el;
}
ngOnInit() {
if (this.disableRipple == null) {
this.disableRipple = DEFAULT_DISABLE_RIPPLE;
}
}
ngOnChanges() { }
/** Applies the styles for an active item to this item. */
setActiveStyles() {
this.sRenderer.addClass(this.classes.optionActive);
}
/** Applies the styles for an inactive item to this item. */
setInactiveStyles() {
this.sRenderer.removeClass(this.classes.optionActive);
}
/** Gets the label to be used when determining whether the option should be focused. */
getLabel() {
return this.viewValue;
}
/** Selects the option. */
select() {
if (!this._selected) {
this._selected = true;
this._selectedColor = this._color;
this._cd.markForCheck();
this._emitSelectionChangeEvent();
}
}
/** Deselects the option. */
deselect() {
if (this._selected) {
this._selected = false;
this._selectedColor = null;
this._cd.markForCheck();
this._emitSelectionChangeEvent();
}
}
/** Sets focus onto this option. */
focus(_origin, options) {
const element = this._getHostElement();
if (typeof element.focus === 'function') {
element.focus(options);
}
}
/** Ensures the option is selected when activated from the keyboard. */
_handleKeydown(event) {
// tslint:disable-next-line: deprecation
if ((event.keyCode === ENTER || event.keyCode === SPACE) && !hasModifierKey(event)) {
this._selectViaInteraction();
// Prevent the page from scrolling down and form submits.
event.preventDefault();
}
}
/**
* `Selects the option while indicating the selection came from the user. Used to
* determine if the select's view -> model callback should be invoked.`
*/
_selectViaInteraction() {
if (!this.disabled) {
this._selected = this.multiple ? !this._selected : true;
this._selectedColor = this._color;
this._cd.markForCheck();
this._emitSelectionChangeEvent(true);
}
}
/** @internal */
_getHostElement() {
return this._el.nativeElement;
}
/** Returns the correct tabindex for the option depending on disabled state. */
_getTabIndex() {
return this.disabled ? '-1' : '0';
}
/** Emits the selection change event. */
_emitSelectionChangeEvent(isUserInput = false) {
this.onSelectionChange.emit(new LyOptionSelectionChange(this, isUserInput));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: LyOption, deps: [{ token: i1.StyleRenderer }, { token: LySelect }, { token: i0.ElementRef }, { token: i1.LyRippleService }, { token: i0.Renderer2 }, { token: i1.LyTheme2 }, { token: i0.ChangeDetectorRef }, { token: i0.NgZone }, { token: i6.Platform }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.10", type: LyOption, isStandalone: false, selector: "ly-option", inputs: { disableRipple: "disableRipple", value: "value", disabled: "disabled" }, outputs: { onSelectionChange: "onSelectionChange" }, host: { attributes: { "role": "option" }, listeners: { "keydown": "_handleKeydown($event)", "click": "_onClick()" }, properties: { "attr.tabindex": "_getTabIndex()" } }, providers: [
StyleRenderer
], viewQueries: [{ propertyName: "_rippleContainer", first: true, predicate: ["rippleContainer"], descendants: true }], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: "<span [className]=\"classes.content\">\n <ly-checkbox [disabled]=\"disabled\"\n [color]=\"_color\"\n [checked]=\"isSelected\"\n *ngIf=\"_select.multiple\"\n (click)=\"$event.preventDefault()\"\n ></ly-checkbox>\n <span [className]=\"classes.optionText\"><ng-content></ng-content></span>\n</span>\n<div #rippleContainer [className]=\"_rippleService.classes.container\"></div>", dependencies: [{ kind: "directive", type: i5.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i7.LyCheckbox, selector: "ly-checkbox", inputs: ["disableRipple", "value", "name", "color", "checked", "indeterminate", "required", "disabled"], outputs: ["change", "indeterminateChange"], exportAs: ["lyCheckbox"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
}
__decorate([
Style(value => (theme) => (_className) => `${_className}{color:${theme.colorOf(value)};}`)
], LyOption.prototype, "_selectedColor", void 0);
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: LyOption, decorators: [{
type: Component,
args: [{ selector: 'ly-option', changeDetection: ChangeDetectionStrategy.OnPush, host: {
'role': 'option',
'(keydown)': '_handleKeydown($event)',
'[attr.tabindex]': '_getTabIndex()',
}, inputs: [
'disableRipple'
], providers: [
StyleRenderer
], standalone: false, template: "<span [className]=\"classes.content\">\n <ly-checkbox [disabled]=\"disabled\"\n [color]=\"_color\"\n [checked]=\"isSelected\"\n *ngIf=\"_select.multiple\"\n (click)=\"$event.preventDefault()\"\n ></ly-checkbox>\n <span [className]=\"classes.optionText\"><ng-content></ng-content></span>\n</span>\n<div #rippleContainer [className]=\"_rippleService.classes.container\"></div>" }]
}], ctorParameters: () => [{ type: i1.StyleRenderer }, { type: LySelect }, { type: i0.ElementRef }, { type: i1.LyRippleService }, { type: i0.Renderer2 }, { type: i1.LyTheme2 }, { type: i0.ChangeDetectorRef }, { type: i0.NgZone }, { type: i6.Platform }], propDecorators: { _rippleContainer: [{
type: ViewChild,
args: ['rippleContainer']
}], onSelectionChange: [{
type: Output
}], _onClick: [{
type: HostListener,
args: ['click']
}], value: [{
type: Input,
args: ['value']
}], disabled: [{
type: Input
}], _selectedColor: [] } });
function same(o) {
return o;
}
class LySelectModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: LySelectModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.10", ngImport: i0, type: LySelectModule, declarations: [LySelect, LyOption, LySelectTrigger], imports: [CommonModule,
LyCommonModule,
LyCheckboxModule,
LyOverlayModule], exports: [LySelect, LyOption, LySelectTrigger, LyCommonModule] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: LySelectModule, imports: [CommonModule,
LyCommonModule,
LyCheckboxModule,
LyOverlayModule, LyCommonModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: LySelectModule, decorators: [{
type: NgModule,
args: [{
declarations: [LySelect, LyOption, LySelectTrigger],
imports: [
CommonModule,
LyCommonModule,
LyCheckboxModule,
LyOverlayModule
],
exports: [LySelect, LyOption, LySelectTrigger, LyCommonModule]
}]
}] });
/**
* Generated bundle index. Do not edit.
*/
export { LyOption, LyOptionBase, LyOptionMixinBase, LyOptionSelectionChange, LySelect, LySelectBase, LySelectChange, LySelectMixinBase, LySelectModule, LySelectTrigger, STYLES };
//# sourceMappingURL=alyle-ui-select.mjs.map