ng-select2-component
Version:
An Angular select2 component.
1,298 lines (1,293 loc) • 147 kB
JavaScript
import * as i0 from '@angular/core';
import { Directive, input, numberAttribute, booleanAttribute, computed, output, viewChild, viewChildren, signal, TemplateRef, Component, ChangeDetectionStrategy, Optional, Self, Attribute, HostBinding, HostListener } from '@angular/core';
import { moveItemInArray, CdkDropList, CdkDrag } from '@angular/cdk/drag-drop';
import { ConnectionPositionPair, CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
import { NgTemplateOutlet } from '@angular/common';
import { toObservable } from '@angular/core/rxjs-interop';
import { InfiniteScrollDirective } from 'ngx-infinite-scroll';
import { Subject, Subscription } from 'rxjs';
import * as i1 from '@angular/cdk/scrolling';
import * as i2 from '@angular/forms';
const timeout = 200;
const unicodePatterns = [
{ l: 'a', s: /[ⓐaẚàáâầấẫẩãāăằắẵẳȧǡäǟảåǻǎȁȃạậặḁąⱥɐ]/gi },
{ l: 'aa', s: /ꜳ/gi },
{ l: 'ae', s: /[æǽǣ]/gi },
{ l: 'ao', s: /ꜵ/gi },
{ l: 'au', s: /ꜷ/gi },
{ l: 'av', s: /[ꜹꜻ]/gi },
{ l: 'ay', s: /ꜽ/gi },
{ l: 'b', s: /[ⓑbḃḅḇƀƃɓ]/gi },
{ l: 'c', s: /[ⓒcćĉċčçḉƈȼꜿↄ]/gi },
{ l: 'd', s: /[ⓓdḋďḍḑḓḏđƌɖɗꝺ]/gi },
{ l: 'dz', s: /[dzdž]/gi },
{ l: 'e', s: /[ⓔeèéêềếễểẽēḕḗĕėëẻěȅȇẹệȩḝęḙḛɇɛǝ]/gi },
{ l: 'f', s: /[ⓕfḟƒꝼ]/gi },
{ l: 'g', s: /[ⓖgǵĝḡğġǧģǥɠꞡᵹꝿ]/gi },
{ l: 'h', s: /[ⓗhĥḣḧȟḥḩḫẖħⱨⱶɥ]/gi },
{ l: 'hv', s: /ƕ/gi },
{ l: 'i', s: /[ⓘiìíîĩīĭİïḯỉǐȉȋịįḭɨı]/gi },
{ l: 'j', s: /[ⓙjĵǰɉ]/gi },
{ l: 'k', s: /[ⓚkḱǩḳķḵƙⱪꝁꝃꝅꞣ]/gi },
{ l: 'l', s: /[ⓛlŀĺľḷḹļḽḻſłƚɫⱡꝉꞁꝇꝆ]/gi },
{ l: 'lj', s: /lj/gi },
{ l: 'm', s: /[ⓜmḿṁṃɱɯ]/gi },
{ l: 'n', s: /[ⓝnǹńñṅňṇņṋṉƞɲʼnꞑꞥ]/gi },
{ l: 'nj', s: /nj/gi },
{ l: 'o', s: /[ⓞoòóôồốỗổõṍȭṏōṑṓŏȯȱöȫỏőǒȍȏơờớỡởợọộǫǭøǿɔƟꝋꝍɵ]/gi },
{ l: 'oi', s: /ƣ/gi },
{ l: 'oe', s: /œ/gi },
{ l: 'oo', s: /ꝏ/gi },
{ l: 'ou', s: /ȣ/gi },
{ l: 'p', s: /[ⓟpṕṗƥᵽꝑꝓꝕ]/gi },
{ l: 'q', s: /[ⓠqɋꝗꝙ]/gi },
{ l: 'r', s: /[ⓡrŕṙřȑȓṛṝŗṟɍɽꝛꞧꞃ]/gi },
{ l: 's', s: /[ⓢsßẞśṥŝṡšṧṣṩșşȿꞩꞅẛ]/gi },
{ l: 't', s: /[ⓣtṫẗťṭțţṱṯŧƭʈⱦꞇ]/gi },
{ l: 'tz', s: /ꜩ/gi },
{ l: 'u', s: /[ⓤuùúûũṹūṻŭüǜǘǖǚủůűǔȕȗưừứữửựụṳųṷṵʉ]/gi },
{ l: 'v', s: /[ⓥvṽṿʋꝟʌ]/gi },
{ l: 'vy', s: /ꝡ/gi },
{ l: 'w', s: /[ⓦwẁẃŵẇẅẘẉⱳ]/gi },
{ l: 'x', s: /[ⓧxẋẍ]/gi },
{ l: 'y', s: /[ⓨyỳýŷỹȳẏÿỷẙỵƴɏỿ]/gi },
{ l: 'z', s: /[ⓩzźẑżžẓẕƶȥɀⱬꝣ]/gi },
];
const defaultMinCountForSearch = 6;
const protectRegexp = new RegExp('[\\-\\[\\]\\/\\{\\}\\(\\)\\*\\+\\?\\.\\\\\\^\\$\\|]', 'g');
class Select2Hint {
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.4", ngImport: i0, type: Select2Hint, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
/** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.4", type: Select2Hint, isStandalone: true, selector: "select2-hint, ng-select2-hint", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.4", ngImport: i0, type: Select2Hint, decorators: [{
type: Directive,
args: [{ selector: 'select2-hint, ng-select2-hint', standalone: true }]
}] });
class Select2Label {
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.4", ngImport: i0, type: Select2Label, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
/** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.4", type: Select2Label, isStandalone: true, selector: "select2-label, ng-select2-label", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.4", ngImport: i0, type: Select2Label, decorators: [{
type: Directive,
args: [{ selector: 'select2-label, ng-select2-label', standalone: true }]
}] });
class Select2Utils {
static getOptionByValue(data, value) {
if (Array.isArray(data)) {
for (const groupOrOption of data) {
const options = groupOrOption.options;
if (options) {
for (const option of options) {
if (option.value === value) {
return option;
}
}
}
else if (groupOrOption.value === value) {
return groupOrOption;
}
}
}
return null;
}
static getOptionsByValue(data, value, multiple) {
if (multiple) {
const values = Array.isArray(value) ? value : [];
const result = [];
for (const v of values) {
const option = Select2Utils.getOptionByValue(data, v);
if (option) {
result.push(option);
}
}
return result;
}
return Select2Utils.getOptionByValue(data, value);
}
static getFirstAvailableOption(data) {
if (Array.isArray(data)) {
for (const groupOrOption of data) {
const options = groupOrOption.options;
if (options) {
for (const option of options) {
if (!option.disabled) {
return option;
}
}
}
else {
const option = groupOrOption;
if (!option.disabled) {
return option;
}
}
}
}
return null;
}
static optionIsNotInFilteredData(filteredData, option) {
if (Select2Utils.isNullOrUndefined(option)) {
return true;
}
for (const groupOrOption of filteredData) {
const options = groupOrOption.options;
if (options && options.includes(option)) {
return false;
}
else if (groupOrOption === option) {
return false;
}
}
return true;
}
static getPreviousOption(filteredData, hoveringOption) {
let findIt = Select2Utils.isNullOrUndefined(hoveringOption);
for (let i = filteredData.length - 1; i >= 0; i--) {
const groupOrOption = filteredData[i];
const options = groupOrOption.options;
if (options) {
for (let j = options.length - 1; j >= 0; j--) {
const option = options[j];
if (findIt && !option.disabled && !option.hide) {
return option;
}
if (!findIt) {
findIt = option === hoveringOption;
}
}
}
else {
const option = groupOrOption;
if (findIt && !option.disabled && !option.hide) {
return option;
}
if (!findIt) {
findIt = option === hoveringOption;
}
}
}
return null;
}
static getNextOption(filteredData, hoveringOption) {
let findIt = Select2Utils.isNullOrUndefined(hoveringOption);
if (filteredData) {
for (const groupOrOption of filteredData) {
const options = groupOrOption.options;
if (options) {
for (const option of options) {
if (findIt) {
if (!option.disabled && !option.hide) {
return option;
}
}
else if (!findIt) {
findIt = option === hoveringOption;
}
}
}
else {
const option = groupOrOption;
if (findIt) {
if (!option.disabled && !option.hide) {
return option;
}
}
else if (!findIt) {
findIt = option === hoveringOption;
}
}
}
}
return null;
}
static getFirstOption(filteredData) {
const firstElement = filteredData[0];
if (this.isOption(firstElement)) {
return firstElement ?? null;
}
else {
return firstElement.options[0] ?? null;
}
}
static getLastOption(filteredData) {
const lastElement = filteredData.at(-1);
if (!lastElement) {
return null;
}
if (this.isOption(lastElement)) {
return lastElement;
}
else {
return lastElement.options.at(-1) ?? null;
}
}
static isGroup(element) {
return !!element.options;
}
static isOption(element) {
return !this.isGroup(element);
}
static getReduceData(data, maxResults = 0) {
if (maxResults > 0) {
let counter = 0;
const result = [];
// debugger;
for (const groupOrOption of data) {
const options = groupOrOption.options;
if (options) {
const group = {
...groupOrOption,
options: [],
};
result.push(group);
for (const item of options) {
group.options.push(item);
counter++;
if (counter === maxResults) {
return { result, reduce: true };
}
}
}
else {
result.push(groupOrOption);
counter++;
}
if (counter === maxResults) {
return { result, reduce: true };
}
}
return { result, reduce: false };
}
else {
return { result: data, reduce: false };
}
}
static getFilteredData(data, searchText, editPattern) {
if (searchText) {
const result = [];
for (const groupOrOption of data) {
const options = groupOrOption.options;
if (options) {
if (options.some(group => Select2Utils.containSearchText(group.label, searchText, editPattern))) {
const filteredOptions = options.filter(group => Select2Utils.containSearchText(group.label, searchText, editPattern));
result.push({
...groupOrOption,
options: filteredOptions,
});
}
}
else if (Select2Utils.containSearchText(groupOrOption.label, searchText, editPattern)) {
result.push(groupOrOption);
}
}
return result;
}
else {
return data;
}
}
static getFilteredSelectedData(data, selectedOptions) {
const result = [];
for (const groupOrOption of data) {
const options = groupOrOption.options;
if (options) {
const filteredOptions = options.filter(group => Select2Utils.isSelected(selectedOptions, group, true) === 'false');
if (filteredOptions.length) {
result.push({
...groupOrOption,
options: filteredOptions,
});
}
}
else if (Select2Utils.isSelected(selectedOptions, groupOrOption, true) === 'false') {
result.push(groupOrOption);
}
}
return result;
}
static isSearchboxHidden(data, minCountForSearch) {
if (minCountForSearch === undefined || minCountForSearch === null || isNaN(+minCountForSearch)) {
minCountForSearch = defaultMinCountForSearch;
}
const optionCount = Select2Utils.getOptionsCount(data);
return optionCount < +minCountForSearch;
}
static isSelected(options, option, multiple) {
return multiple
? options && options.some(op => op.value === option.value)
? 'true'
: 'false'
: options && option.value === options.value
? 'true'
: 'false';
}
static removeSelection(options, option) {
for (let i = 0; i < options.length; i++) {
if (options[i].value === option.value) {
options.splice(i, 1);
return;
}
}
}
static getOptionsCount(data) {
let count = 0;
if (Array.isArray(data)) {
for (const groupOrOption of data) {
const options = groupOrOption.options;
count += options ? options.length : 1;
}
}
return count;
}
static isNullOrUndefined(value) {
return value === null || value === undefined;
}
static containSearchText(label, searchText, editPattern) {
return searchText
? Select2Utils.formatSansUnicode(label).match(new RegExp(Select2Utils.formatPattern(searchText, editPattern), 'i')) !== null
: true;
}
static protectPattern(str) {
return str.replace(protectRegexp, '\\$&');
}
static formatSansUnicode(str) {
for (const unicodePattern of unicodePatterns) {
str = str.replace(unicodePattern.s, unicodePattern.l);
}
return str;
}
static formatPattern(str, editPattern) {
str = Select2Utils.formatSansUnicode(Select2Utils.protectPattern(str));
if (editPattern && typeof editPattern === 'function') {
str = editPattern(str);
}
return str;
}
}
let nextUniqueId = 0;
const OPEN_KEYS_NATIVE = ['Enter', ' '];
const CLOSE_KEYS_NATIVE = ['ArrowDown', 'ArrowUp', 'Home', 'End', 'PageUp', 'PageDown'];
const OPEN_KEYS = ['ArrowDown', 'ArrowUp', 'Enter', ' ', 'Home', 'End', 'PageUp', 'PageDown'];
const ON_OPEN_KEYS = ['Home', 'End', 'PageUp', 'PageDown'];
const CLOSE_KEYS = ['Escape', 'Tab', { key: 'ArrowUp', altKey: true }];
class Select2 {
// ----------------------- HostBinding
get classMaterial() {
return this.styleMode() === 'material';
}
get classNostyle() {
return this.styleMode() === 'noStyle';
}
get classBorderless() {
return this.styleMode() === 'borderless';
}
get select2above() {
return !this.overlay() ? this.listPosition() === 'above' : this._isAbobeOverlay();
}
get select2Options() {
return this.multiple() ? (this.selectedOption ?? []) : [];
}
get select2Option() {
return this.multiple() ? null : this.selectedOption;
}
get searchText() {
return this.innerSearchText;
}
set searchText(text) {
this.innerSearchText = text;
}
get disabledState() {
return this._control?.disabled ?? this._disabled;
}
get _positions() {
switch (this.listPosition()) {
case 'above':
return [
new ConnectionPositionPair({ originX: 'start', originY: 'top' }, { overlayX: 'start', overlayY: 'bottom' }),
];
case 'auto':
return [
new ConnectionPositionPair({ originX: 'start', originY: 'bottom' }, { overlayX: 'start', overlayY: 'top' }),
new ConnectionPositionPair({ originX: 'start', originY: 'top' }, { overlayX: 'start', overlayY: 'bottom' }),
];
default:
return [
new ConnectionPositionPair({ originX: 'start', originY: 'bottom' }, { overlayX: 'start', overlayY: 'top' }),
];
}
}
get resultsElement() {
return this.resultContainer()?.nativeElement;
}
/** Tab index for the element. */
get _tabIndex() {
return this.disabledState ? -1 : this.tabIndex();
}
constructor(_viewportRuler, _changeDetectorRef, _parentForm, _parentFormGroup, _control, tabIndex) {
this._viewportRuler = _viewportRuler;
this._changeDetectorRef = _changeDetectorRef;
this._parentForm = _parentForm;
this._parentFormGroup = _parentFormGroup;
this._control = _control;
this._uid = `select2-${nextUniqueId++}`;
// ----------------------- signal-input
/** data of options & option groups */
this.data = input.required();
/** minimum characters to start filter search */
this.minCharForSearch = input(0, { transform: numberAttribute });
/** text placeholder */
this.displaySearchStatus = input(undefined);
/** text placeholder */
this.placeholder = input(undefined);
/** in multiple: maximum selection element (0 = no limit) */
this.limitSelection = input(0, { transform: numberAttribute });
/** dropdown position */
this.listPosition = input('below');
/** overlay with CDK Angular position */
this.overlay = input(false, { transform: booleanAttribute });
/** select one or more item */
this.multiple = input(false, { transform: booleanAttribute });
/** drag'n drop list of items in multiple */
this.multipleDrag = input(false, { transform: booleanAttribute });
/** use the material style */
this.styleMode = input('default');
/** message when no result */
this.noResultMessage = input();
/** maximum results limit (0 = no limit) */
this.maxResults = input(0, { transform: numberAttribute });
/** message when maximum results */
this.maxResultsMessage = input('Too many results…');
/** infinite scroll distance */
this.infiniteScrollDistance = input(1.5, { transform: numberAttribute });
/** infinite scroll distance */
this.infiniteScrollThrottle = input(150, { transform: numberAttribute });
/** infinite scroll activated */
this.infiniteScroll = input(false, { transform: booleanAttribute });
/** auto create if not exist */
this.autoCreate = input(false, { transform: booleanAttribute });
/** no template for label selection */
this.noLabelTemplate = input(false, { transform: booleanAttribute });
/** use it for change the pattern of the filter search */
this.editPattern = input(undefined);
/** template(s) for formatting */
this.templates = input(undefined);
/** template for formatting selected option */
this.templateSelection = input(undefined);
/** the max height of the results container when opening the select */
this.resultMaxHeight = input('200px');
/** Active Search event */
this.customSearchEnabled = input(false, { transform: booleanAttribute });
/** minimal data of show the search field */
this.minCountForSearch = input(undefined, { transform: numberAttribute });
/** Unique id of the element. */
this.id = input(this._uid);
/** Unique id of label element. */
this.idLabel = computed(() => `${this.id()}-label`);
/** Unique id of combo element. */
this.idCombo = computed(() => `${this.id()}-combo`);
/** Unique id of options element. */
this.idOptions = computed(() => `${this.id()}-options`);
/** Unique id of overlay element. */
this.idOverlay = computed(() => `${this.id()}-overlay`);
/** Whether the element is required. */
this.required = input(false, { transform: booleanAttribute });
/** Whether selected items should be hidden. */
this.disabled = input(false, { transform: booleanAttribute });
/** Whether items are hidden when has. */
this.hideSelectedItems = input(false, { transform: booleanAttribute });
/** Whether the element is readonly. */
this.readonly = input(false, { transform: booleanAttribute });
/** The input element's value. */
this.value = input();
/** Tab index for the select2 element. */
this.tabIndex = input(0, { transform: numberAttribute });
/** reset with no selected value */
this.resettable = input(false, { transform: booleanAttribute });
/** selected value when × is clicked */
this.resetSelectedValue = input(undefined);
/** like native select keyboard navigation (only single mode) */
this.nativeKeyboard = input(false, { transform: booleanAttribute });
/** grid: item by line
* * 0 = no grid
* * number = item by line (4)
* * string = minimal size item (100px)
*/
this.grid = input('');
/**
* Replace selection by a text
* * if string: `%size%` = total selected options
* * if function: juste show the string
*/
this.selectionOverride = input(undefined);
/** force selection on one line */
this.selectionNoWrap = input(false, { transform: booleanAttribute });
/** Add an option to select or remove all (if all is selected) */
this.showSelectAll = input(false, { transform: booleanAttribute });
/** Text for remove all options */
this.removeAllText = input('Remove all');
/** Text for select all options */
this.selectAllText = input('Select all');
// -- WAI related inputs ---
/** title attribute applied to the input */
this.title = input();
/** aria-labelledby attribute applied to the input, to specify en external label */
this.ariaLabelledby = input();
/** aria-describedby attribute applied to the input */
this.ariaDescribedby = input();
/** aria-invalid attribute applied to the input, to force error state */
this.ariaInvalid = input(false, { transform: booleanAttribute });
/** description of the reset button when using 'resettable'. Default value : 'Reset' */
this.ariaResetButtonDescription = input('Reset');
// ----------------------- output
this.update = output();
this.autoCreateItem = output();
this.open = output();
this.close = output();
this.focus = output();
this.blur = output();
this.search = output();
this.scroll = output();
this.removeOption = output();
// ----------------------- signal viewChild
this.cdkConnectedOverlay = viewChild.required(CdkConnectedOverlay);
this.selection = viewChild.required('selection');
this.resultContainer = viewChild('results');
this.results = viewChildren('result');
this.searchInput = viewChild('searchInput');
this.dropdown = viewChild('dropdown');
// ----------------------- internal var
this.selectedOption = null;
this.isOpen = false;
/** Whether the element is focused or not. */
this.focused = false;
this.filteredData = signal(undefined);
this.overlayWidth = '';
this.overlayHeight = '';
this.hoveringOption = signal(null);
this.hoveringOptionId = computed(() => this.getElementId(this.hoveringOption()));
this.innerSearchText = '';
this._stateChanges = new Subject();
this._data = [];
this._disabled = false;
this._value = null;
this.toObservable = new Subscription();
/** View -> model callback called when select has been touched */
this._onTouched = () => {
// do nothing
};
/** View -> model callback called when value changes */
this._onChange = () => {
// do nothing
};
if (this._control) {
this._control.valueAccessor = this;
}
this.toObservable.add(toObservable(this.multiple).subscribe(_multiple => {
this.ngOnInit();
}));
this.toObservable.add(toObservable(this.minCountForSearch).subscribe(minCountForSearch => {
this.updateSearchBox();
}));
this.toObservable.add(toObservable(this.disabled).subscribe(disabled => {
this._disabled = disabled;
}));
}
ngOnChanges(changes) {
let updateFilterData;
if (changes['data']) {
this._data = changes['data'].currentValue;
updateFilterData = true;
}
if (changes['value']) {
const value = changes['value'].currentValue;
if (this.testValueChange(this._value, value)) {
if (this._value === undefined) {
this._value = value;
}
this.writeValue(value);
updateFilterData = true;
}
}
// now call update filter data so that _data and the current selected values are set
if (updateFilterData) {
this.updateFilteredData();
}
}
clickDetection(e) {
if (this.isOpen) {
const target = e.target;
if (!this.ifParentContainsClass(target, 'selection')) {
if ((!this.ifParentContainsClass(target, 'select2-dropdown') &&
!this.multiple() &&
!this.hideSelectedItems()) ||
!(this.ifParentContainsClass(target, 'select2-results__option') ||
this.ifParentContainsClass(target, 'select2-dropdown'))) {
this.toggleOpenAndClose();
}
if (!this.overlay() && !this.ifParentContainsId(target, this.id())) {
this.clickExit();
}
}
else if (!this.ifParentContainsId(target, this.id())) {
this.toggleOpenAndClose();
this.clickExit();
}
}
else if (this.focused) {
const target = e.target;
this._focus(this.clickOnSelect2Element(target));
}
}
ngOnInit() {
this._viewportRuler.change(100).subscribe(() => {
if (this.isOpen) {
this.triggerRect();
}
});
const option = Select2Utils.getOptionsByValue(this._data, this._control ? this._control.value : this.value(), this.multiple());
if (option !== null) {
this.selectedOption = option ?? null;
}
if (!Array.isArray(option)) {
this.hoveringOption.set(Select2Utils.getOptionByValue(this._data, this.value));
}
this.updateSearchBox();
}
ngAfterViewInit() {
this.cdkConnectedOverlay().positionChange.subscribe((posChange) => {
if (this.listPosition() === 'auto' &&
posChange.connectionPair?.originY &&
this._overlayPosition !== posChange.connectionPair.originY) {
this.triggerRect();
this._overlayPosition = posChange.connectionPair.originY;
this._changeDetectorRef.detectChanges();
}
});
this.selectionElement = this.selection().nativeElement;
this.triggerRect();
}
ngDoCheck() {
this.updateSearchBox();
this._dirtyCheckNativeValue();
if (this._triggerRect) {
if (this.overlayWidth !== this._triggerRect.width) {
this.overlayWidth = this._triggerRect.width;
this._changeDetectorRef.detectChanges();
}
if (this._dropdownRect &&
this._dropdownRect.height > 0 &&
this.overlayHeight !== this._dropdownRect.height) {
this.overlayHeight = this.listPosition() === 'auto' ? this._dropdownRect.height : 0;
this.fixValue();
}
}
}
ngOnDestroy() {
this.toObservable.unsubscribe();
}
fixValue() {
if (!Array.isArray(this.selectedOption) && this.multiple()) {
const selectedOption = this.selectedOption;
this.selectedOption = [];
setTimeout(() => {
this.select(selectedOption);
this._changeDetectorRef.detectChanges();
});
}
else if (Array.isArray(this.selectedOption) && !this.multiple()) {
const selectedOption = this.selectedOption[0];
this.selectedOption = null;
setTimeout(() => {
this.select(selectedOption);
this._changeDetectorRef.detectChanges();
});
}
else {
this._changeDetectorRef.detectChanges();
}
}
updateSearchBox() {
if (this.autoCreate() && !this.multiple()) {
this.isSearchboxHidden = false;
}
else {
const hidden = this.displaySearchStatus() === 'hidden' ||
(this.displaySearchStatus() !== 'always' &&
!this.customSearchEnabled() &&
Select2Utils.isSearchboxHidden(this._data, this.minCountForSearch()));
if (this.isSearchboxHidden !== hidden) {
this.isSearchboxHidden = hidden;
}
}
}
getOptionStyle(option) {
return ('select2-results__option ' +
(option.hide ? 'select2-results__option--hide ' : '') +
(option === this.hoveringOption() ? 'select2-results__option--highlighted ' : '') +
(option.classes || ''));
}
mouseenter(option) {
if (!option.disabled) {
this.hoveringOption.set(option);
}
}
click(option) {
if (this.testSelection(option)) {
this.select(option);
}
}
reset(event) {
const resetSelectedValue = this.resetSelectedValue();
this.select(resetSelectedValue !== undefined
? (Select2Utils.getOptionByValue(this._data, resetSelectedValue) ?? null)
: null);
if (event) {
this.stopEvent(event);
}
this._focus(true);
}
prevChange(event) {
event.stopPropagation();
}
stopEvent(event) {
event.preventDefault();
event.stopPropagation();
}
toggleOpenAndClose(focus = true, open, event) {
if (this.disabledState || this.readonly()) {
return;
}
this._focus(focus);
const onOpenAction = event && this._testKey(event, ON_OPEN_KEYS);
const changeEmit = this.isOpen !== (open ?? !this.isOpen);
this.isOpen = open ?? !this.isOpen;
if (this.isOpen) {
if (!this.isSearchboxHidden) {
this.innerSearchText = '';
this.updateFilteredData();
this._focusSearchbox(focus);
}
if (this.isSearchboxHidden && !changeEmit && event) {
this.keyDown(event);
}
else {
if (this.selectedOption) {
const option = Array.isArray(this.selectedOption) ? this.selectedOption[0] : this.selectedOption;
this.updateScrollFromOption(option);
}
else if (this.resultsElement) {
this.resultsElement.scrollTop = 0;
}
if (onOpenAction) {
this.keyDown(event);
}
this._changeDetectorRef.detectChanges();
this.triggerRect();
this.cdkConnectedOverlay().overlayRef?.updatePosition();
}
if (changeEmit) {
this.open.emit(this);
}
}
else if (changeEmit) {
this.close.emit(this);
}
this._changeDetectorRef.markForCheck();
}
hasTemplate(option, defaultValue, select = false) {
const templates = this.templates();
const templatesValue = this.templates();
return ((select
? templates?.[option.templateSelectionId ?? ''] instanceof TemplateRef ||
templates?.[`${defaultValue}Selection`] instanceof TemplateRef ||
templates?.[`templateSelection`] instanceof TemplateRef ||
this.templateSelection() instanceof TemplateRef
: false) ||
templatesValue?.[option.templateId ?? ''] instanceof TemplateRef ||
templatesValue?.[defaultValue] instanceof TemplateRef ||
templatesValue?.['template'] instanceof TemplateRef ||
templatesValue instanceof TemplateRef ||
false);
}
getTemplate(option, defaultValue, select = false) {
const templates = this.templates();
const templatesValue = this.templates();
return this.hasTemplate(option, defaultValue, select)
? (select
? templates?.[option.templateSelectionId ?? ''] ||
templates?.[`${defaultValue}Selection`] ||
templates?.[`templateSelection`] ||
this.templateSelection()
: undefined) ||
templatesValue?.[option.templateId ?? ''] ||
templatesValue?.[defaultValue] ||
templatesValue?.['template'] ||
templatesValue
: undefined;
}
triggerRect() {
this._triggerRect = this.selectionElement?.getBoundingClientRect();
const dropdown = this.dropdown();
this._dropdownRect = dropdown?.nativeElement ? dropdown.nativeElement.getBoundingClientRect() : undefined;
}
isNumber(o) {
return !isNaN(o - 0) && o !== null && o !== '' && o !== false;
}
selectAll() {
if (this.multiple()) {
if (!this.selectAllTest()) {
const options = [];
this._data.forEach(e => {
if (Select2Utils.isGroup(e)) {
e.options.forEach(f => {
if (!f.disabled && !f.hide) {
options.push(f);
}
});
}
else if (!e.disabled && !e.hide) {
options.push(e);
}
});
this.selectedOption = options;
this.writeValue(options.map(e => e.value));
}
else {
this.selectedOption = [];
this.writeValue([]);
}
this.isOpen = false;
this.close.emit(this);
}
}
selectAllTest() {
if (this.multiple() && Array.isArray(this.selectedOption) && this.selectedOption.length) {
let options = 0;
this._data.forEach(e => {
if (Select2Utils.isGroup(e)) {
e.options.forEach(f => {
if (!f.disabled && !f.hide) {
options++;
}
});
}
else if (!e.disabled && !e.hide) {
options++;
}
});
return this.selectedOption.length === options;
}
return false;
}
testSelection(option) {
if (option.disabled) {
return false;
}
const limitSelection = this.limitSelection();
return !this.multiple() || !limitSelection || this.optionsSize() < limitSelection;
}
testValueChange(value1, value2) {
if (((value1 === null || value1 === undefined) && (value2 === null || value2 === undefined)) ||
value1 === value2) {
return false;
}
if (this.multiple() &&
value1?.length &&
value2?.length &&
value1.length === value2.length) {
for (const e1 of value1) {
const test = value2.indexOf(e1) > -1;
if (!test) {
return true;
}
}
return false;
}
return true;
}
updateFilteredData() {
let result = this._data;
if (this.multiple() && this.hideSelectedItems()) {
result = Select2Utils.getFilteredSelectedData(result, this.selectedOption);
}
if (!this.customSearchEnabled() && this.searchText && this.searchText.length >= +this.minCharForSearch()) {
result = Select2Utils.getFilteredData(result, this.searchText, this.editPattern());
}
if (this.maxResults() > 0) {
const data = Select2Utils.getReduceData(result, +this.maxResults());
result = data.result;
this.maxResultsExceeded = data.reduce;
}
else {
this.maxResultsExceeded = false;
}
if (Select2Utils.optionIsNotInFilteredData(result, this.hoveringOption())) {
this.hoveringOption.set(Select2Utils.getFirstAvailableOption(result));
}
this.filteredData.set(result);
// replace selected options when data change
if (this.multiple() && Array.isArray(this.selectedOption) && this.selectedOption.length) {
const options = [];
const value = this.selectedOption.map(e => e.value);
this._data.forEach(e => {
if (Select2Utils.isGroup(e)) {
e.options.forEach(f => {
if (value.includes(f.value)) {
options.push(f);
}
});
}
else if (value.includes(e.value)) {
options.push(e);
}
});
// preserve selection order
this.selectedOption = this.selectedOption.map(e => options.find(f => f.value === e.value));
}
else if (!Array.isArray(this.selectedOption) && this.selectedOption) {
let option = null;
this._data.forEach(e => {
if (Select2Utils.isGroup(e)) {
e.options.forEach(f => {
if (this.selectedOption.value === f.value) {
option = f;
}
});
}
else if (this.selectedOption.value === e.value) {
option = e;
}
});
this.selectedOption = option;
}
this._changeDetectorRef.detectChanges();
}
clickExit() {
this._focus(false);
}
isInSelect(elt) {
return this.ifParentContainsId(elt, this.id()) || this.ifParentContainsId(elt, this.idOverlay());
}
ifParentContainsClass(element, cssClass) {
return this.getParentElementByClass(element, cssClass) !== null;
}
ifParentContainsId(element, id) {
return this.getParentElementById(element, id) !== null;
}
getParentElementByClass(element, cssClass) {
return this.containClasses(element, cssClass.trim().split(/\s+/))
? element
: element.parentElement
? this.getParentElementByClass(element.parentElement, cssClass)
: null;
}
getParentElementById(element, id) {
return element.id === id
? element
: element.parentElement
? this.getParentElementById(element.parentElement, id)
: null;
}
containClasses(element, cssClasses) {
if (!element.classList) {
return false;
}
for (const cssClass of cssClasses) {
if (!element.classList.contains(cssClass)) {
return false;
}
}
return true;
}
containAlmostOneClasses(element, cssClasses) {
if (!element.classList) {
return false;
}
let containAlmostOne = false;
for (const cssClass of cssClasses) {
if (element.classList.contains(cssClass)) {
containAlmostOne = true;
}
}
return containAlmostOne;
}
clickOnSelect2Element(element) {
return this.containAlmostOneClasses(element, [
'select2-overlay-backdrop',
'select2-label-content',
'select2-selection__rendered',
'select2-results__option',
]);
}
focusin(options) {
if (!this.disabledState) {
this._focus(true, options);
}
}
focusout(event) {
if (!event.relatedTarget || !this.isInSelect(event.relatedTarget)) {
this._focus(false);
this._onTouched();
}
}
select(option, emit = true, closeOnSelect = true) {
let value;
if (option !== null && option !== undefined) {
if (this.multiple()) {
this.selectedOption ??= [];
const options = this.selectedOption;
const index = options.findIndex(op => op.value === option.value);
if (index === -1) {
options.push(option);
}
else {
options.splice(index, 1);
}
value = this.selectedOption.map(op => op.value);
}
else {
this.selectedOption = option;
if (closeOnSelect && this.isOpen) {
this.isOpen = false;
this.close.emit(this);
this.selectionElement?.focus();
}
value = this.selectedOption.value;
if (!option && this._value === null) {
this._value = value;
}
}
}
else {
// when remove value
if (Array.isArray(this.selectedOption) ? this.selectedOption?.length : this.selectedOption) {
value = '';
}
this.selectedOption = null;
}
if (this.multiple() && this.hideSelectedItems()) {
this.updateFilteredData();
}
emit &&= value !== undefined && this.testDiffValue(this._value, value);
if (this._control) {
this._onChange(value);
}
if (emit) {
this.writeValue(value);
setTimeout(() => {
this.updateEvent(value);
});
}
}
testDiffValue(val1, val2) {
return Array.isArray(val1) ? val1?.length !== val2?.length : val1 !== val2;
}
keyDown(event, create = false) {
if (create && this._testKey(event, ['Enter'])) {
this.createAndAdd(event);
}
else if (this._testKey(event, [{ key: 'ArrowDown', altKey: false }])) {
this.moveDown();
this.actionAfterKeyDownMoveAction(event);
}
else if (this._testKey(event, [{ key: 'ArrowUp', altKey: false }])) {
this.moveUp();
this.actionAfterKeyDownMoveAction(event);
}
else if (this._testKey(event, ['Home'])) {
this.moveStart();
this.actionAfterKeyDownMoveAction(event);
}
else if (this._testKey(event, ['End'])) {
this.moveEnd();
this.actionAfterKeyDownMoveAction(event);
}
else if (this._testKey(event, ['PageUp'])) {
this.moveUp(10);
this.actionAfterKeyDownMoveAction(event);
}
else if (this._testKey(event, ['PageDown'])) {
this.moveDown(10);
this.actionAfterKeyDownMoveAction(event);
}
else if (this._testKey(event, ['Enter']) || (this.isSearchboxHidden && this._testKey(event, [' ']))) {
this.selectByEnter(true);
event.preventDefault();
}
else if (this._testKey(event, CLOSE_KEYS) && this.isOpen) {
this.toggleOpenAndClose();
this._focus(true);
}
}
actionAfterKeyDownMoveAction(event) {
event.preventDefault();
if (this.nativeKeyboard() && !this.multiple()) {
this.selectByEnter(false);
}
}
openKey(event, create = false) {
if (create && this._testKey(event, ['Enter'])) {
this.createAndAdd(event);
}
else if (this._testKey(event, this.nativeKeyboard() && !this.multiple() ? OPEN_KEYS_NATIVE : OPEN_KEYS)) {
this.toggleOpenAndClose(true, true, event);
event.preventDefault();
}
else if (this.nativeKeyboard() && !this.multiple() && this._testKey(event, CLOSE_KEYS_NATIVE)) {
this.updateScrollFromOption(this.select2Option);
this.keyDown(event, create);
}
else if (this._testKey(event, CLOSE_KEYS)) {
if (this.isOpen) {
this.toggleOpenAndClose();
this._onTouched();
}
}
}
searchUpdate(e) {
this.searchText = e.target.value;
if (!this.customSearchEnabled()) {
this.updateFilteredData();
}
else {
this.search.emit({
component: this,
value: this._value,
search: this.searchText,
data: this._data,
filteredData: (data) => {
this.filteredData.set(data);
this._changeDetectorRef.markForCheck();
},
});
}
}
isSelected(option) {
return Select2Utils.isSelected(this.selectedOption, option, this.multiple());
}
isDisabled(option) {
return option.disabled ? 'true' : 'false';
}
removeSelection(e, option) {
if (this.readonly() || this.disabledState) {
return;
}
Select2Utils.removeSelection(this.selectedOption, option);
if (this.multiple() && this.hideSelectedItems()) {
this.updateFilteredData();
}
const value = this.selectedOption.map(op => op.value);
if (this._control) {
this._onChange(value);
}
else {
this._value = value ?? null;
}
this.updateEvent(value);
this.removeOption.emit({
component: this,
value: value,
removedOption: option,
});
e.preventDefault();
e.stopPropagation();
if (this.isOpen) {
this._focusSearchbox();
}
else {
this._focus(true);
}
}
/**
* Sets the model value. Implemented as part of ControlValueAccessor.
* @param value
*/
writeValue(value) {
this.selectedOption = null;
this._setSelectionByValue(this.multiple() ? (value ?? []) : value);
if (this.testValueChange(this._value, value)) {
this._value = value;
}
}
/**
* Saves a callback function to be invoked when the select's value
* changes from user input. Part of the ControlValueAccessor interface
* required to integrate with Angular's core forms API.
*
* @param fn Callback to be triggered when the value changes.
*/
registerOnChange(fn) {
this._onChange = fn;
}
/**
* Saves a callback function to be invoked when the select is blurred
* by the user. Part of the ControlValueAccessor interface required
* to integrate with Angular's core forms API.
*
* @param fn Callback to be triggered when the component has been touched.
*/
registerOnTouched(fn) {
this._onTouched = fn;
}
/**
* Sets whether the component should be disabled.
* Implemented as part of ControlValueAccessor.
* @param isDisabled
*/
setDisabledState(isDisabled) {
this._disabled = isDisabled;
}
onScroll(way) {
this.scroll.emit({
component: this,
way,
search: this.innerSearchText,
data: this._data,
});
}
drop(event) {
if (Array.isArray(this.selectedOption)) {
moveItemInArray(this.selectedOption, event.previousIndex, event.currentIndex);
const value = this.selectedOption.map(e => e.value);
this.writeValue(value);
this._value = value;
this.updateEvent(value);
}
}
_isErrorState() {
const isInvalid = this._control?.invalid;
const isTouched = this._control?.touched;
const isSubmitted = this._parentFormGroup?.submitted || this._parentForm?.submitted;
return !!(isInvalid && (isTouched || isSubmitted));
}
_selectionOverrideLabel() {
const selectionOverride = this.selectionOverride();
if (typeof selectionOverride === 'function') {
return selectionOverride({
size: this.optionsSize(),
options: Array.isArray(this.selectedOption)
? this.selectedOption
: this.selectedOption
? [this.selectedOption]
: null,
});
}
else if (typeof selectionOverride === 'string') {
return selectionOverride.replaceAll('%size%', `${this.optionsSize()}`);
}
return undefined;
}
getElementId(elt) {
if (!elt) {
return elt;
}
else if (elt.id) {
return elt.id;
}
const [i, j] = this._getElementPath(elt);
const toSuffix = (index) => (index !== undefined ? `-${index}` : '');
elt.id ??= `${this.id()}-option${toSuffix(i)}${toSuffix(j)}`;
return elt.id;
}
_getElementPath(elt) {
for (let i = 0; i < this._da