UNPKG

ng-select2-component

Version:
1,298 lines (1,293 loc) 147 kB
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