@clr/angular
Version:
Angular components for Clarity
1 lines • 116 kB
Source Map (JSON)
{"version":3,"file":"clr-angular-forms-combobox.mjs","sources":["../../../projects/angular/forms/combobox/providers/combobox-container.service.ts","../../../projects/angular/forms/combobox/combobox-container.ts","../../../projects/angular/forms/combobox/model/combobox.model.ts","../../../projects/angular/forms/combobox/model/multi-select-combobox.model.ts","../../../projects/angular/forms/combobox/model/single-select-combobox.model.ts","../../../projects/angular/forms/combobox/option-selected.directive.ts","../../../projects/angular/forms/combobox/model/pseudo-focus.model.ts","../../../projects/angular/forms/combobox/providers/option-selection.service.ts","../../../projects/angular/forms/combobox/providers/combobox-focus-handler.service.ts","../../../projects/angular/forms/combobox/option.ts","../../../projects/angular/forms/combobox/options.ts","../../../projects/angular/forms/combobox/combobox.ts","../../../projects/angular/forms/combobox/combobox.html","../../../projects/angular/forms/combobox/option-items.directive.ts","../../../projects/angular/forms/combobox/option-group.ts","../../../projects/angular/forms/combobox/combobox.module.ts","../../../projects/angular/forms/combobox/index.ts","../../../projects/angular/forms/combobox/clr-angular-forms-combobox.ts"],"sourcesContent":["/*\n * Copyright (c) 2016-2026 Broadcom. All Rights Reserved.\n * The term \"Broadcom\" refers to Broadcom Inc. and/or its subsidiaries.\n * This software is released under MIT license.\n * The full license information can be found in LICENSE in the root directory of this project.\n */\n\nimport { Injectable } from '@angular/core';\n\n@Injectable()\nexport class ComboboxContainerService {\n labelOffset = 0;\n labelText: string;\n}\n","/*\n * Copyright (c) 2016-2026 Broadcom. All Rights Reserved.\n * The term \"Broadcom\" refers to Broadcom Inc. and/or its subsidiaries.\n * This software is released under MIT license.\n * The full license information can be found in LICENSE in the root directory of this project.\n */\n\nimport { AfterContentInit, AfterViewInit, Component, ElementRef, Optional, ViewChild } from '@angular/core';\nimport {\n ClrAbstractContainer,\n ControlClassService,\n ControlIdService,\n LayoutService,\n NgControlService,\n} from '@clr/angular/forms/common';\n\nimport { ComboboxContainerService } from './providers/combobox-container.service';\n@Component({\n selector: 'clr-combobox-container',\n template: `\n <ng-content select=\"label\"></ng-content>\n @if (!label && addGrid()) {\n <label></label>\n }\n <div class=\"clr-control-container\" [ngClass]=\"controlClass()\" #controlContainer>\n <ng-content select=\"clr-combobox\"></ng-content>\n @if (showHelper) {\n <ng-content select=\"clr-control-helper\"></ng-content>\n }\n @if (showInvalid) {\n <ng-content select=\"clr-control-error\"></ng-content>\n }\n @if (showValid) {\n <ng-content select=\"clr-control-success\"></ng-content>\n }\n </div>\n `,\n host: {\n '[class.clr-form-control]': 'true',\n '[class.clr-combobox-form-control]': 'true',\n '[class.clr-form-control-disabled]': 'control?.disabled',\n '[class.clr-row]': 'addGrid()',\n },\n providers: [NgControlService, ControlIdService, ControlClassService, ComboboxContainerService],\n standalone: false,\n})\nexport class ClrComboboxContainer extends ClrAbstractContainer implements AfterContentInit, AfterViewInit {\n @ViewChild('controlContainer') controlContainer: ElementRef<HTMLElement>;\n\n constructor(\n @Optional() layoutService: LayoutService,\n controlClassService: ControlClassService,\n ngControlService: NgControlService,\n private containerService: ComboboxContainerService,\n public el: ElementRef<HTMLElement>\n ) {\n super(layoutService, controlClassService, ngControlService);\n }\n\n ngAfterContentInit() {\n if (this.label) {\n this.containerService.labelText = this.label.labelText;\n }\n }\n\n ngAfterViewInit() {\n this.containerService.labelOffset =\n this.controlContainer.nativeElement.offsetHeight - this.el.nativeElement.offsetHeight;\n }\n}\n","/*\n * Copyright (c) 2016-2026 Broadcom. All Rights Reserved.\n * The term \"Broadcom\" refers to Broadcom Inc. and/or its subsidiaries.\n * This software is released under MIT license.\n * The full license information can be found in LICENSE in the root directory of this project.\n */\n\nexport type ClrComboboxIdentityFunction<T> = (item: T) => any;\n\nexport type ClrComboboxResolverFunction<T> = (input: string) => T;\n\nexport abstract class ComboboxModel<T> {\n model: T | T[];\n displayField?: string;\n identityFn: ClrComboboxIdentityFunction<T> = (item: T) => item;\n abstract containsItem(item: T): boolean;\n abstract select(item: T): void;\n abstract unselect(item: T): void;\n abstract toString(displayField?: string, index?: number): string;\n abstract isEmpty(): boolean;\n abstract pop(): T; // pops the last item\n}\n","/*\n * Copyright (c) 2016-2026 Broadcom. All Rights Reserved.\n * The term \"Broadcom\" refers to Broadcom Inc. and/or its subsidiaries.\n * This software is released under MIT license.\n * The full license information can be found in LICENSE in the root directory of this project.\n */\n\nimport { ComboboxModel } from './combobox.model';\n\nexport class MultiSelectComboboxModel<T> extends ComboboxModel<T> {\n override model: T[];\n override displayField: string;\n\n containsItem(item: T): boolean {\n if (this.model === null || this.model === undefined) {\n return false;\n }\n return this.model.some(m => this.identityFn(m) === this.identityFn(item));\n }\n\n select(item: T): void {\n this.addItem(item);\n }\n\n unselect(item: T): void {\n this.removeItem(item);\n }\n\n isEmpty(): boolean {\n return !(this.model && this.model.length > 0);\n }\n\n pop(): T {\n let item;\n if (this.model && this.model.length > 0) {\n item = this.model[this.model.length - 1];\n this.removeItem(item);\n }\n return item;\n }\n\n toString(displayField?: string, index = -1): string {\n let displayString = '';\n\n if (this.model) {\n // If the model is array, we can use a specific item from it, to retrieve the display value.\n if (index > -1) {\n if (this.model[index]) {\n // If we have a defined display field, we'll use it's value as display value\n if (displayField && (this.model[index] as any)[displayField]) {\n displayString += (this.model[index] as any)[displayField];\n } else {\n // If we don't have a defined display field, we'll use the toString representation of the\n // item as display value.\n displayString += this.model[index].toString();\n }\n }\n } else {\n this.model.forEach((model: T) => {\n // If we have a defined display field, we'll use it's value as display value\n if (displayField && (model as any)[displayField]) {\n displayString += (model as any)[displayField];\n } else {\n // If we don't have a defined display field, we'll use the toString representation of the\n // model as display value.\n displayString += model.toString();\n }\n displayString += ' ';\n });\n }\n }\n\n return displayString.trim();\n }\n\n private addItem(item: T) {\n if (!this.containsItem(item)) {\n this.model = this.model || [];\n this.model.push(item);\n }\n }\n\n private removeItem(item: T) {\n if (this.model === null || this.model === undefined) {\n return;\n }\n\n const index = this.model.findIndex(m => this.identityFn(m) === this.identityFn(item));\n\n if (index > -1) {\n this.model.splice(index, 1);\n }\n\n // we intentionally set the model to null for form validation\n if (this.model.length === 0) {\n this.model = null;\n }\n }\n}\n","/*\n * Copyright (c) 2016-2026 Broadcom. All Rights Reserved.\n * The term \"Broadcom\" refers to Broadcom Inc. and/or its subsidiaries.\n * This software is released under MIT license.\n * The full license information can be found in LICENSE in the root directory of this project.\n */\n\nimport { ComboboxModel } from './combobox.model';\n\nexport class SingleSelectComboboxModel<T> extends ComboboxModel<T> {\n override model: T;\n\n containsItem(item: T): boolean {\n if (this.model === null || this.model === undefined) {\n return false;\n }\n return this.identityFn(this.model) === this.identityFn(item);\n }\n\n select(item: T): void {\n this.model = item;\n }\n\n unselect(item: T): void {\n if (this.containsItem(item)) {\n this.model = null;\n }\n }\n\n isEmpty(): boolean {\n return !this.model;\n }\n\n pop(): T {\n const item = this.model;\n this.model = null;\n return item;\n }\n\n toString(displayField?: string): string {\n if (!this.model) {\n return '';\n }\n if (displayField && (this.model as any)[displayField]) {\n return (this.model as any)[displayField];\n } else {\n return this.model.toString();\n }\n }\n}\n","/*\n * Copyright (c) 2016-2026 Broadcom. All Rights Reserved.\n * The term \"Broadcom\" refers to Broadcom Inc. and/or its subsidiaries.\n * This software is released under MIT license.\n * The full license information can be found in LICENSE in the root directory of this project.\n */\n\nimport { Directive, Input, TemplateRef } from '@angular/core';\n\n@Directive({\n selector: '[clrOptionSelected]',\n standalone: false,\n})\nexport class ClrOptionSelected<T> {\n @Input('clrOptionSelected') selected: T;\n\n constructor(public template: TemplateRef<{ $implicit: T }>) {}\n}\n","/*\n * Copyright (c) 2016-2026 Broadcom. All Rights Reserved.\n * The term \"Broadcom\" refers to Broadcom Inc. and/or its subsidiaries.\n * This software is released under MIT license.\n * The full license information can be found in LICENSE in the root directory of this project.\n */\n\nimport { BehaviorSubject, Observable } from 'rxjs';\n\nimport { SingleSelectComboboxModel } from './single-select-combobox.model';\n\nexport class PseudoFocusModel<T> extends SingleSelectComboboxModel<T> {\n private _focusChanged = new BehaviorSubject<T>(null);\n get focusChanged(): Observable<T> {\n return this._focusChanged.asObservable();\n }\n\n override select(item: T): void {\n if (this.model !== item) {\n this.model = item;\n this._focusChanged.next(item);\n }\n }\n}\n","/*\n * Copyright (c) 2016-2026 Broadcom. All Rights Reserved.\n * The term \"Broadcom\" refers to Broadcom Inc. and/or its subsidiaries.\n * This software is released under MIT license.\n * The full license information can be found in LICENSE in the root directory of this project.\n */\n\nimport { Injectable } from '@angular/core';\nimport { BehaviorSubject, Observable, ReplaySubject, Subject } from 'rxjs';\n\nimport { ClrComboboxIdentityFunction, ClrComboboxResolverFunction, ComboboxModel } from '../model/combobox.model';\nimport { MultiSelectComboboxModel } from '../model/multi-select-combobox.model';\n\n@Injectable()\nexport class OptionSelectionService<T> {\n loading = false;\n editable = false;\n showSelectAll = false;\n selectionModel: ComboboxModel<T>;\n inputChanged: Observable<string>;\n // Display all options on first open, even if filter text exists.\n // https://github.com/vmware-clarity/ng-clarity/issues/386\n showAllOptions = true;\n\n private _currentInput = '';\n private _displayField: string;\n private _inputChanged = new BehaviorSubject('');\n private _selectionChanged = new ReplaySubject<ComboboxModel<T>>(1);\n private _selectAllRequested = new Subject<void>();\n\n constructor() {\n this.inputChanged = this._inputChanged.asObservable();\n }\n\n get displayField() {\n return this._displayField;\n }\n set displayField(value: string) {\n this._displayField = value;\n if (this.selectionModel) {\n this.selectionModel.displayField = value;\n }\n }\n\n get currentInput(): string {\n return this._currentInput;\n }\n set currentInput(input) {\n // clear value in single selection model when input is empty\n if (input === '' && !this.multiselectable) {\n this.setSelectionValue(null);\n }\n this._currentInput = input;\n this._inputChanged.next(input);\n }\n\n // This observable is for notifying the ClrOption to update its\n // selection by comparing the value\n get selectionChanged(): Observable<ComboboxModel<T>> {\n return this._selectionChanged.asObservable();\n }\n\n get multiselectable(): boolean {\n return this.selectionModel instanceof MultiSelectComboboxModel;\n }\n\n get identityFn(): ClrComboboxIdentityFunction<T> {\n return this._identityFn;\n }\n\n set identityFn(value: ClrComboboxIdentityFunction<T>) {\n this._identityFn = value || ((item: T) => item);\n if (this.selectionModel) {\n this.selectionModel.identityFn = this._identityFn;\n }\n }\n\n get selectAllRequested(): Observable<void> {\n return this._selectAllRequested.asObservable();\n }\n\n requestSelectAll() {\n this._selectAllRequested.next();\n }\n\n editableResolver: ClrComboboxResolverFunction<T> = (input: string) => input as T;\n\n select(item: T) {\n if (item === null || item === undefined || this.selectionModel.containsItem(item)) {\n return;\n }\n this.selectionModel.select(item);\n this._selectionChanged.next(this.selectionModel);\n }\n\n toggle(item: T) {\n if (item === null || item === undefined) {\n return;\n }\n if (this.selectionModel.containsItem(item)) {\n this.selectionModel.unselect(item);\n } else {\n this.selectionModel.select(item);\n }\n this._selectionChanged.next(this.selectionModel);\n }\n\n selectMany(items: T[]) {\n let changed = false;\n for (const item of items) {\n if (!this.selectionModel.containsItem(item)) {\n this.selectionModel.select(item);\n changed = true;\n }\n }\n if (changed) {\n this._selectionChanged.next(this.selectionModel);\n }\n }\n\n unselectMany(items: T[]) {\n if (!this.selectionModel || this.selectionModel.isEmpty()) {\n return;\n }\n\n let changed = false;\n for (const item of items) {\n if (this.selectionModel.containsItem(item)) {\n this.selectionModel.unselect(item);\n changed = true;\n }\n }\n\n if (changed) {\n this._selectionChanged.next(this.selectionModel);\n }\n }\n\n unselect(item: T) {\n if (item === null || item === undefined || !this.selectionModel.containsItem(item)) {\n return;\n }\n this.selectionModel.unselect(item);\n this._selectionChanged.next(this.selectionModel);\n }\n\n /**\n * Checks whether all given items are currently selected, using identityFn for comparison.\n */\n containsAll(items: T[]): boolean {\n if (!items.length || this.selectionModel.isEmpty()) {\n return false;\n }\n return items.every(item => this.selectionModel.containsItem(item));\n }\n\n setSelectionValue(value: T | T[]): void {\n if (!this.selectionModel) {\n return;\n }\n\n const current = this.selectionModel.model;\n if (this.valuesEqualByIdentity(current, value)) {\n return;\n }\n\n this.selectionModel.model = value;\n this._selectionChanged.next(this.selectionModel);\n }\n\n private _identityFn: ClrComboboxIdentityFunction<T> = (item: T) => item;\n\n private valuesEqualByIdentity(current: T | T[], value: T | T[]): boolean {\n if (current === value) {\n return true;\n }\n // Check if both are null or undefined or empty string.\n if (\n (current === null || current === undefined || current === '') &&\n (value === null || value === undefined || value === '')\n ) {\n return true;\n }\n // Check if one is null or undefined or empty string and the other is not.\n if (\n current === null ||\n current === undefined ||\n current === '' ||\n value === null ||\n value === undefined ||\n value === ''\n ) {\n return false;\n }\n\n if (this.multiselectable) {\n const cur = current as T[];\n const val = value as T[];\n if (cur.length !== val.length) {\n return false;\n }\n // We only consider values equal if they are ordered the same way.\n const curIds = cur.map(this._identityFn);\n const valIds = val.map(this._identityFn);\n return curIds.every((id, i) => id === valIds[i]);\n } else {\n return this._identityFn(current as T) === this._identityFn(value as T);\n }\n }\n}\n","/*\n * Copyright (c) 2016-2026 Broadcom. All Rights Reserved.\n * The term \"Broadcom\" refers to Broadcom Inc. and/or its subsidiaries.\n * This software is released under MIT license.\n * The full license information can be found in LICENSE in the root directory of this project.\n */\n\nimport { isPlatformBrowser } from '@angular/common';\nimport { Inject, Injectable, PLATFORM_ID, Renderer2, RendererFactory2 } from '@angular/core';\nimport { ClrPopoverService } from '@clr/angular/popover/common';\nimport { ArrowKeyDirection, customFocusableItemProvider, Keys } from '@clr/angular/utils';\nimport { take } from 'rxjs/operators';\n\nimport { OptionSelectionService } from './option-selection.service';\nimport { PseudoFocusModel } from '../model/pseudo-focus.model';\nimport { SELECT_ALL_ID } from '../options';\n\n@Injectable()\nexport class ComboboxFocusHandler<T> {\n pseudoFocus: PseudoFocusModel<OptionData<T>> = new PseudoFocusModel<OptionData<T>>();\n\n private renderer: Renderer2;\n private _trigger: HTMLElement;\n private _listbox: HTMLElement;\n private _textInput: HTMLElement;\n private optionData: OptionData<T>[] = [];\n\n constructor(\n rendererFactory: RendererFactory2,\n private popoverService: ClrPopoverService,\n private selectionService: OptionSelectionService<T>,\n @Inject(PLATFORM_ID) private platformId: any\n ) {\n this.handleFocusSubscription();\n // Direct renderer injection can be problematic and leads to failing tests at least\n this.renderer = rendererFactory.createRenderer(null, null);\n }\n\n get trigger() {\n return this._trigger;\n }\n set trigger(el: HTMLElement) {\n this._trigger = el;\n this.addFocusOnBlurListener(el);\n }\n\n get listbox() {\n return this._listbox;\n }\n set listbox(el: HTMLElement) {\n this._listbox = el;\n this.addFocusOnBlurListener(el);\n }\n\n get textInput() {\n return this._textInput;\n }\n set textInput(el: HTMLElement) {\n this._textInput = el;\n this.renderer.listen(el, 'keydown', event => !this.handleTextInput(event));\n this.addFocusOnBlurListener(el);\n }\n\n focusInput() {\n if (this.textInput && isPlatformBrowser(this.platformId)) {\n this.textInput.focus({ preventScroll: true });\n }\n }\n\n focusFirstActive() {\n if (this.optionData.length > 0) {\n if (this.selectionService.selectionModel.isEmpty()) {\n this.pseudoFocus.select(this.optionData[0]);\n } else {\n let firstActive: T;\n if (this.selectionService.multiselectable) {\n firstActive = (this.selectionService.selectionModel.model as T[])[0];\n } else {\n firstActive = this.selectionService.selectionModel.model as T;\n }\n const activeProxy = this.optionData.find(option => option.value === firstActive);\n if (activeProxy) {\n // active element is visible\n this.pseudoFocus.select(activeProxy);\n } else {\n // we have active element, but it's filtered out\n this.pseudoFocus.select(this.optionData[0]);\n }\n this.scrollIntoSelectedModel('auto');\n }\n }\n }\n\n addOptionValues(options: OptionData<T>[]) {\n this.optionData = options;\n }\n\n focusOption(option: OptionData<T>) {\n this.pseudoFocus.select(option);\n }\n\n private handleFocusSubscription() {\n this.popoverService.openChange.subscribe(open => {\n if (!open) {\n this.pseudoFocus.model = null;\n }\n });\n }\n\n private moveFocusTo(direction: ArrowKeyDirection) {\n let index = this.optionData.findIndex(option => option.equals(this.pseudoFocus.model));\n if (direction === ArrowKeyDirection.UP) {\n if (index === -1 || index === 0) {\n index = this.optionData.length - 1;\n } else {\n index--;\n }\n } else if (direction === ArrowKeyDirection.DOWN) {\n if (index === -1 || index === this.optionData.length - 1) {\n index = 0;\n } else {\n index++;\n }\n }\n this.pseudoFocus.select(this.optionData[index]);\n this.scrollIntoSelectedModel();\n }\n\n private openAndMoveTo(direction: ArrowKeyDirection) {\n if (!this.popoverService.open) {\n this.popoverService.openChange.pipe(take(1)).subscribe(open => {\n if (open) {\n this.moveFocusTo(direction);\n }\n });\n this.popoverService.open = true;\n } else {\n this.moveFocusTo(direction);\n }\n }\n\n // this service is only interested in keys that may move the focus\n private handleTextInput(event: KeyboardEvent): boolean {\n let preventDefault = false;\n const key = event.key;\n if (event) {\n switch (key) {\n case Keys.Enter:\n if (this.popoverService.open && this.pseudoFocus.model) {\n if (this.selectionService.multiselectable) {\n if (this.pseudoFocus.model.id === SELECT_ALL_ID) {\n this.selectionService.requestSelectAll();\n } else {\n this.selectionService.toggle(this.pseudoFocus.model.value);\n }\n } else {\n this.selectionService.select(this.pseudoFocus.model.value);\n }\n preventDefault = true;\n }\n break;\n case Keys.Space:\n if (!this.popoverService.open) {\n this.popoverService.open = true;\n preventDefault = true;\n }\n break;\n case Keys.ArrowUp:\n this.preventViewportScrolling(event);\n this.openAndMoveTo(ArrowKeyDirection.UP);\n preventDefault = true;\n break;\n case Keys.ArrowDown:\n this.preventViewportScrolling(event);\n this.openAndMoveTo(ArrowKeyDirection.DOWN);\n preventDefault = true;\n break;\n default:\n // Any other keypress\n if (\n event.key !== Keys.Tab &&\n !(this.selectionService.multiselectable && event.key === Keys.Backspace) &&\n !(event.key === Keys.Escape) &&\n !this.popoverService.open\n ) {\n this.popoverService.open = true;\n }\n break;\n }\n }\n return preventDefault;\n }\n\n private scrollIntoSelectedModel(behavior: ScrollBehavior = 'smooth') {\n if (this.pseudoFocus.model && this.pseudoFocus.model.el) {\n this.pseudoFocus.model.el.scrollIntoView({ behavior, block: 'center', inline: 'nearest' });\n }\n }\n\n private preventViewportScrolling(event: KeyboardEvent) {\n event.preventDefault();\n event.stopImmediatePropagation();\n }\n\n private addFocusOnBlurListener(el: HTMLElement) {\n if (isPlatformBrowser(this.platformId)) {\n this.renderer.listen(el, 'blur', event => {\n if (this.focusOutOfComponent(event)) {\n this.popoverService.open = false;\n }\n });\n }\n }\n\n private focusOutOfComponent(event: FocusEvent): boolean {\n const target = event.relatedTarget as Node;\n return !(this.textInput.contains(target) || this.trigger.contains(target) || this.listbox.contains(target));\n }\n}\n\nexport const COMBOBOX_FOCUS_HANDLER_PROVIDER = customFocusableItemProvider(ComboboxFocusHandler);\n\nexport class OptionData<T> {\n el: HTMLElement;\n\n constructor(\n public id: string,\n public value: T\n ) {}\n\n equals(other: OptionData<T>): boolean {\n if (!other) {\n return false;\n }\n return this.id === other.id && this.value === other.value;\n }\n}\n","/*\n * Copyright (c) 2016-2026 Broadcom. All Rights Reserved.\n * The term \"Broadcom\" refers to Broadcom Inc. and/or its subsidiaries.\n * This software is released under MIT license.\n * The full license information can be found in LICENSE in the root directory of this project.\n */\n\nimport { Component, ElementRef, HostBinding, HostListener, Input, OnInit } from '@angular/core';\nimport { ClrCommonStringsService, uniqueIdFactory } from '@clr/angular/utils';\n\nimport { ComboboxFocusHandler, OptionData as OptionProxy } from './providers/combobox-focus-handler.service';\nimport { OptionSelectionService } from './providers/option-selection.service';\n@Component({\n selector: 'clr-option',\n template: `\n <ng-content></ng-content>\n @if (selected) {\n <span class=\"clr-sr-only\">{{ commonStrings.keys.comboboxSelected }}</span>\n }\n `,\n host: {\n '[class.clr-combobox-option]': 'true',\n '[attr.role]': '\"option\"',\n // Do not remove. Or click-selection will not work.\n '[attr.tabindex]': '-1',\n '[attr.id]': 'optionId',\n },\n standalone: false,\n})\nexport class ClrOption<T> implements OnInit {\n // A proxy with only the necessary data to be used for a11y and the focus handler service.\n optionProxy: OptionProxy<T> = new OptionProxy(null, null);\n\n private _id: string;\n private _value: T;\n\n constructor(\n public elRef: ElementRef<HTMLElement>,\n public commonStrings: ClrCommonStringsService,\n private focusHandler: ComboboxFocusHandler<T>,\n private optionSelectionService: OptionSelectionService<T>\n ) {\n this.optionProxy.el = elRef.nativeElement;\n }\n\n @Input('id')\n get optionId() {\n return this._id;\n }\n set optionId(id: string) {\n this._id = id;\n this.optionProxy.id = this._id;\n }\n\n @Input('clrValue')\n get value(): T {\n return this._value;\n }\n set value(value: T) {\n this._value = value;\n this.optionProxy.value = value;\n }\n\n @HostBinding('class.active')\n get selected() {\n return (\n this.optionSelectionService.selectionModel && this.optionSelectionService.selectionModel.containsItem(this.value)\n );\n }\n\n @HostBinding('class.clr-focus')\n get focusClass() {\n return this.focusHandler.pseudoFocus.containsItem(this.optionProxy);\n }\n\n ngOnInit() {\n if (!this._id) {\n this._id = 'clr-option-' + uniqueIdFactory();\n this.optionProxy.id = this._id;\n }\n }\n\n @HostListener('click', ['$event'])\n onClick(event: MouseEvent) {\n event.stopPropagation();\n if (this.optionSelectionService.multiselectable) {\n this.optionSelectionService.toggle(this.value);\n } else {\n this.optionSelectionService.select(this.value);\n }\n // As the popover stays open in multi-select mode now, we have to take focus back to the input\n // This way we achieve two things:\n // - do not lose focus\n // - we're still able to use onBlur for \"outside-click\" handling\n this.focusHandler.focusOption(this.optionProxy);\n this.focusHandler.focusInput();\n }\n}\n","/*\n * Copyright (c) 2016-2026 Broadcom. All Rights Reserved.\n * The term \"Broadcom\" refers to Broadcom Inc. and/or its subsidiaries.\n * This software is released under MIT license.\n * The full license information can be found in LICENSE in the root directory of this project.\n */\n\nimport {\n AfterViewInit,\n Component,\n ContentChildren,\n DOCUMENT,\n ElementRef,\n Inject,\n Input,\n OnDestroy,\n Optional,\n QueryList,\n ViewChild,\n} from '@angular/core';\nimport { ClrPopoverService, POPOVER_HOST_ORIGIN } from '@clr/angular/popover/common';\nimport { ClrCommonStringsService, ClrLoadingState, IF_ACTIVE_ID, LoadingListener } from '@clr/angular/utils';\nimport { Subscription } from 'rxjs';\n\nimport { ClrOption } from './option';\nimport { ComboboxFocusHandler, OptionData } from './providers/combobox-focus-handler.service';\nimport { OptionSelectionService } from './providers/option-selection.service';\n\nlet nbOptionsComponents = 0;\n\nexport const SELECT_ALL_ID = 'select-all-id';\n\n@Component({\n selector: 'clr-options',\n template: `\n @if (optionSelectionService.loading) {\n <div class=\"clr-combobox-options-loading\">\n <clr-spinner clrInline>\n {{ commonStrings.keys.loading }}\n </clr-spinner>\n <span class=\"clr-combobox-options-text\">\n {{ searchText(optionSelectionService.currentInput) }}\n </span>\n </div>\n }\n\n @if (showSelectAll) {\n <div class=\"clr-combobox-select-all\">\n <button\n #selectAllBtn\n type=\"button\"\n tabindex=\"-1\"\n class=\"btn btn-link clr-combobox-select-all-btn clr-combobox-option\"\n [class.clr-focus]=\"isSelectAllFocused\"\n (click)=\"toggleSelectAll($event)\"\n >\n {{ allVisibleSelected ? commonStrings.keys.comboboxUnselectAll : commonStrings.keys.comboboxSelectAll }}\n </button>\n </div>\n }\n\n <!-- Rendered if data set is empty -->\n @if (emptyOptions) {\n <div [id]=\"noResultsElementId\" role=\"option\">\n <span class=\"clr-combobox-options-empty-text\">\n {{ commonStrings.keys.comboboxNoResults }}\n </span>\n </div>\n }\n\n <!--Option Groups and Options will be projected here-->\n <ng-content></ng-content>\n `,\n providers: [{ provide: LoadingListener, useExisting: ClrOptions }],\n host: {\n '[class.clr-combobox-options]': 'true',\n '[class.clr-combobox-options-multi]': 'optionSelectionService.multiselectable',\n '[class.clr-combobox-options-hidden]': 'emptyOptions && editable',\n '[attr.role]': '\"listbox\"',\n '[id]': 'optionsId',\n },\n standalone: false,\n})\nexport class ClrOptions<T> implements AfterViewInit, LoadingListener, OnDestroy {\n @Input('id') optionsId: string;\n\n loading = false;\n _items: QueryList<ClrOption<T>>;\n\n private subscriptions: Subscription[] = [];\n private _selectAllOption: OptionData<T>;\n\n constructor(\n public optionSelectionService: OptionSelectionService<T>,\n @Inject(IF_ACTIVE_ID) public id: number,\n public el: ElementRef<HTMLElement>,\n public commonStrings: ClrCommonStringsService,\n private focusHandler: ComboboxFocusHandler<T>,\n private popoverService: ClrPopoverService,\n @Optional()\n @Inject(POPOVER_HOST_ORIGIN)\n parentHost: ElementRef<HTMLElement>,\n @Inject(DOCUMENT) private document: any\n ) {\n if (!parentHost) {\n throw new Error('clr-options should only be used inside of a clr-combobox');\n }\n\n if (!this.optionsId) {\n this.optionsId = 'clr-options-' + nbOptionsComponents++;\n }\n }\n\n @ViewChild('selectAllBtn')\n set selectAllBtn(value: ElementRef) {\n if (value) {\n this._selectAllOption = new OptionData<T>(SELECT_ALL_ID, null);\n this._selectAllOption.el = value.nativeElement;\n } else {\n this._selectAllOption = null;\n }\n this.updateFocusableItems();\n }\n\n @ContentChildren(ClrOption, { descendants: true })\n get items(): QueryList<ClrOption<T>> {\n return this._items;\n }\n set items(items: QueryList<ClrOption<T>>) {\n this._items = items;\n this.updateFocusableItems();\n }\n\n /**\n * Tests if the list of options is empty, meaning it doesn't contain any items\n */\n get emptyOptions() {\n return !this.optionSelectionService.loading && this.items.length === 0;\n }\n\n get editable() {\n return this.optionSelectionService.editable;\n }\n\n get noResultsElementId() {\n return `${this.optionsId}-no-results`;\n }\n\n get showSelectAll(): boolean {\n return (\n this.optionSelectionService.showSelectAll &&\n this.optionSelectionService.multiselectable &&\n !this.optionSelectionService.loading &&\n this.items.length > 0\n );\n }\n\n get allVisibleSelected(): boolean {\n if (!this.items || this.items.length === 0) {\n return false;\n }\n return this.optionSelectionService.containsAll(this.items.map(option => option.value));\n }\n\n get isSelectAllFocused() {\n return this.focusHandler.pseudoFocus.model?.id === SELECT_ALL_ID;\n }\n\n toggleSelectAll(event: Event = null) {\n if (event) {\n event.stopPropagation();\n this.focusHandler.focusInput();\n }\n const visibleValues = this.items.map(option => option.value);\n\n if (this.allVisibleSelected) {\n this.optionSelectionService.unselectMany(visibleValues);\n } else {\n this.optionSelectionService.selectMany(visibleValues);\n }\n }\n\n ngAfterViewInit() {\n this.focusHandler.listbox = this.el.nativeElement;\n\n this.subscriptions.push(\n this.items.changes.subscribe(items => {\n if (items.length) {\n setTimeout(() => {\n this.focusHandler.focusFirstActive();\n });\n } else {\n this.focusHandler.pseudoFocus.pop();\n }\n }),\n this.optionSelectionService.selectAllRequested.subscribe(() => {\n this.toggleSelectAll();\n })\n );\n }\n\n ngOnDestroy() {\n this.subscriptions.forEach(sub => sub.unsubscribe());\n }\n\n searchText(input: string) {\n return this.commonStrings.parse(this.commonStrings.keys.comboboxSearching, { INPUT: input });\n }\n\n loadingStateChange(state: ClrLoadingState): void {\n this.loading = state === ClrLoadingState.LOADING;\n }\n\n private updateFocusableItems() {\n const focusList: OptionData<T>[] = [];\n\n if (this._selectAllOption) {\n focusList.push(this._selectAllOption);\n }\n\n if (this._items) {\n const itemOptions = this._items.map(option => option.optionProxy);\n focusList.push(...itemOptions);\n }\n\n this.focusHandler.addOptionValues(focusList);\n }\n}\n","/*\n * Copyright (c) 2016-2026 Broadcom. All Rights Reserved.\n * The term \"Broadcom\" refers to Broadcom Inc. and/or its subsidiaries.\n * This software is released under MIT license.\n * The full license information can be found in LICENSE in the root directory of this project.\n */\n\nimport { isPlatformBrowser } from '@angular/common';\nimport {\n AfterContentInit,\n booleanAttribute,\n ChangeDetectorRef,\n Component,\n ContentChild,\n ElementRef,\n EventEmitter,\n Host,\n HostListener,\n Inject,\n Injector,\n Input,\n NgZone,\n Optional,\n Output,\n PLATFORM_ID,\n QueryList,\n Renderer2,\n Self,\n ViewChild,\n ViewChildren,\n ViewContainerRef,\n} from '@angular/core';\nimport { ControlValueAccessor, NgControl } from '@angular/forms';\nimport { WrappedFormControl } from '@clr/angular/forms/common';\nimport {\n ClrPopoverHostDirective,\n ClrPopoverPosition,\n ClrPopoverService,\n ClrPopoverType,\n} from '@clr/angular/popover/common';\nimport {\n ClrCommonStringsService,\n ClrLoadingState,\n FOCUS_SERVICE_PROVIDER,\n IF_ACTIVE_ID_PROVIDER,\n Keys,\n LoadingListener,\n} from '@clr/angular/utils';\nimport { debounceTime, Subject } from 'rxjs';\n\nimport { ClrComboboxContainer } from './combobox-container';\nimport { ClrComboboxIdentityFunction, ClrComboboxResolverFunction, ComboboxModel } from './model/combobox.model';\nimport { MultiSelectComboboxModel } from './model/multi-select-combobox.model';\nimport { SingleSelectComboboxModel } from './model/single-select-combobox.model';\nimport { ClrOptionSelected } from './option-selected.directive';\nimport { ClrOptions } from './options';\nimport { ComboboxContainerService } from './providers/combobox-container.service';\nimport { COMBOBOX_FOCUS_HANDLER_PROVIDER, ComboboxFocusHandler } from './providers/combobox-focus-handler.service';\nimport { OptionSelectionService } from './providers/option-selection.service';\n\n@Component({\n selector: 'clr-combobox',\n templateUrl: './combobox.html',\n providers: [\n OptionSelectionService,\n { provide: LoadingListener, useExisting: ClrCombobox },\n IF_ACTIVE_ID_PROVIDER,\n FOCUS_SERVICE_PROVIDER,\n COMBOBOX_FOCUS_HANDLER_PROVIDER,\n ],\n hostDirectives: [ClrPopoverHostDirective],\n host: {\n '[class.aria-required]': 'true',\n '[class.clr-combobox]': 'true',\n '[class.clr-combobox-disabled]': 'control?.disabled',\n },\n standalone: false,\n})\nexport class ClrCombobox<T>\n extends WrappedFormControl<ClrComboboxContainer>\n implements ControlValueAccessor, LoadingListener, AfterContentInit\n{\n @Input('placeholder') placeholder = '';\n\n @Output('clrInputChange') clrInputChange = new EventEmitter<string>(false);\n @Output('clrOpenChange') clrOpenChange = this.popoverService.openChange;\n\n /**\n * This output should be used to set up a live region using aria-live and populate it with updates that reflect each combobox change.\n */\n @Output('clrSelectionChange') clrSelectionChange = this.optionSelectionService.selectionChanged;\n\n @ViewChild('textboxInput') textbox: ElementRef<HTMLInputElement>;\n @ViewChild('trigger') trigger: ElementRef<HTMLButtonElement>;\n @ContentChild(ClrOptionSelected) optionSelected: ClrOptionSelected<T>;\n\n @ViewChild('truncationButton') truncationButton: ElementRef;\n @ViewChild('wrapper', { static: true }) wrapper: ElementRef;\n @ViewChildren('pill') calculationPills: QueryList<ElementRef<HTMLElement>>;\n\n focused = false;\n\n popoverPosition = ClrPopoverPosition.BOTTOM_LEFT;\n\n protected override index = 1;\n\n protected popoverType = ClrPopoverType.DROPDOWN;\n\n protected containerWidth = null;\n protected selectionExpanded = false;\n protected calculatedLimit: number | undefined;\n protected shouldCalculate = true;\n protected isTotalSelection = false;\n\n private resizeObserver: ResizeObserver;\n private containerWidthChange = new Subject();\n @ContentChild(ClrOptions) private options: ClrOptions<T>;\n\n private _searchText = '';\n private onTouchedCallback: () => any;\n private onChangeCallback: (model: T | T[]) => any;\n\n constructor(\n vcr: ViewContainerRef,\n injector: Injector,\n @Self()\n @Optional()\n public control: NgControl,\n protected override renderer: Renderer2,\n protected override el: ElementRef<HTMLElement>,\n public optionSelectionService: OptionSelectionService<T>,\n public commonStrings: ClrCommonStringsService,\n private popoverService: ClrPopoverService,\n @Optional() private containerService: ComboboxContainerService,\n @Inject(PLATFORM_ID) private platformId: any,\n private focusHandler: ComboboxFocusHandler<T>,\n private cdr: ChangeDetectorRef,\n private zone: NgZone,\n @Optional() @Host() private container: ClrComboboxContainer\n ) {\n super(vcr, ClrComboboxContainer, injector, control, renderer, el);\n if (control) {\n control.valueAccessor = this;\n }\n\n // default to SingleSelectComboboxModel, in case the optional input [ClrMulti] isn't used\n this.multiSelect = false;\n }\n\n @Input({ alias: 'showSelectAll', transform: booleanAttribute })\n get showSelectAll() {\n return this.optionSelectionService.showSelectAll;\n }\n set showSelectAll(value: boolean) {\n this.optionSelectionService.showSelectAll = value;\n }\n\n @Input('clrEditable')\n get editable() {\n return this.optionSelectionService.editable;\n }\n set editable(value: boolean) {\n this.optionSelectionService.editable = value;\n }\n\n @Input('clrEditableResolverFn')\n set editableResolver(value: ClrComboboxResolverFunction<T> | undefined) {\n this.optionSelectionService.editableResolver = value;\n }\n\n @Input('clrComboboxIdentityFn')\n set identityFn(value: ClrComboboxIdentityFunction<T>) {\n this.optionSelectionService.identityFn = value;\n }\n\n @Input('clrMulti')\n get multiSelect() {\n return this.optionSelectionService.multiselectable;\n }\n set multiSelect(value: boolean | string) {\n if (value) {\n this.optionSelectionService.selectionModel = new MultiSelectComboboxModel<T>();\n } else {\n // in theory, setting this again should not cause errors even though we already set it in constructor,\n // since the initial call to writeValue (caused by [ngModel] input) should happen after this\n this.optionSelectionService.selectionModel = new SingleSelectComboboxModel<T>();\n }\n this.optionSelectionService.selectionModel.identityFn = this.optionSelectionService.identityFn;\n this.updateControlValue();\n }\n\n // Override the id of WrappedFormControl, as we want to move it to the embedded input.\n // Otherwise, the label/component connection does not work and screen readers do not read the label.\n override get id() {\n return this.controlIdService.id + '-combobox';\n }\n override set id(id: string) {\n super.id = id;\n }\n\n get searchText(): string {\n return this._searchText;\n }\n set searchText(text: string) {\n // if input text has changed since last time, fire a change event so application can react to it\n if (text !== this._searchText) {\n if (this.popoverService.open) {\n this.optionSelectionService.showAllOptions = false;\n }\n this._searchText = text;\n this.clrInputChange.emit(this.searchText);\n }\n // We need to trigger this even if unchanged, so the option-items directive will update its list\n // based on the \"showAllOptions\" variable which may have changed in the openChange subscription below.\n // The option-items directive does not listen to openChange, but it listens to currentInput changes.\n this.optionSelectionService.currentInput = this.searchText;\n }\n\n get openState(): boolean {\n return this.popoverService.open;\n }\n\n get multiSelectModel(): T[] {\n if (!this.multiSelect) {\n throw Error('multiSelectModel is not available in single selection context');\n }\n return (this.optionSelectionService.selectionModel as MultiSelectComboboxModel<T>).model;\n }\n\n get ariaControls(): string {\n return this.options?.optionsId;\n }\n\n get ariaOwns(): string {\n return this.options?.optionsId;\n }\n\n get ariaDescribedBySelection(): string {\n return 'selection-' + this.id;\n }\n\n get displayField(): string {\n return this.optionSelectionService.displayField;\n }\n\n get showAllText() {\n return this.commonStrings.parse(this.commonStrings.keys.comboboxShowAll, {\n ITEMS: this.multiSelectModel?.length.toString(),\n });\n }\n\n get allSelectedText() {\n return this.commonStrings.parse(this.commonStrings.keys.comboboxAllSelected, {\n ITEMS: this.multiSelectModel?.length.toString(),\n });\n }\n\n get showIndividualPills(): boolean {\n return !this.isTotalSelection || this.selectionExpanded;\n }\n\n get showTruncationToggle(): boolean {\n return (\n this.selectionExpanded ||\n this.isTotalSelection ||\n (this.calculatedLimit !== null && this.calculatedLimit < this.multiSelectModel.length)\n );\n }\n\n private get disabled() {\n return this.control?.disabled;\n }\n\n ngAfterContentInit() {\n this.initializeSubscriptions();\n\n // Initialize with preselected value\n if (!this.optionSelectionService.selectionModel.isEmpty()) {\n this.updateInputValue(this.optionSelectionService.selectionModel);\n }\n }\n\n ngAfterViewInit() {\n this.focusHandler.textInput = this.textbox.nativeElement;\n this.focusHandler.trigger = this.trigger.nativeElement;\n // The text input is the actual element we are wrapping\n // This assignment is needed by the wrapper, so it can set\n // the aria properties on the input element, not on the component.\n\n // We calculate on the initial load to prevent flickering\n this.el = this.textbox;\n if (this.showSelectAll) {\n if (this.multiSelect && this.multiSelectModel?.length > 0) {\n this.calculateLimit();\n }\n this.initialiseObserver();\n }\n }\n\n override ngOnDestroy(): void {\n super.ngOnDestroy();\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n }\n }\n\n clearSelection() {\n this.focusHandler.focusInput();\n // Clear the array model directly\n this.optionSelectionService.setSelectionValue([]);\n }\n\n @HostListener('keydown', ['$event'])\n onKeyUp(event: KeyboardEvent) {\n // if BACKSPACE in multiselect mode, delete the last pill if text is empty\n if (this.multiSelect) {\n const multiModel: T[] = this.optionSelectionService.selectionModel.model as T[];\n switch (event.key) {\n case Keys.Backspace:\n if (!this._searchText.length) {\n if (multiModel && multiModel.length > 0) {\n const lastItem: T = multiModel[multiModel.length - 1];\n this.control?.control.markAsTouched();\n this.optionSelectionService.unselect(lastItem);\n }\n }\n break;\n case Keys.Enter:\n if (this.editable && this._searchText.length > 0 && this.options.emptyOptions) {\n const parsedInput = this.optionSelectionService.editableResolver(this._searchText);\n this.control?.control.markAsTouched();\n this.optionSelectionService.select(parsedInput);\n this.searchText = '';\n }\n break;\n }\n }\n }\n\n inputId(): string {\n return this.controlIdService.id;\n }\n\n loadingStateChange(state: ClrLoadingState): void {\n this.optionSelectionService.loading = state === ClrLoadingState.LOADING;\n\n if (state !== ClrLoadingState.LOADING && isPlatformBrowser(this.platformId)) {\n setTimeout(() => {\n this.popoverService?.resetPositions();\n });\n this.focusFirstActive();\n }\n }\n\n unselect(item: T) {\n if (!this.disabled) {\n this.optionSelectionService.unselect(item);\n }\n }\n\n onBlur(event) {\n if (!event.relatedTarget || !this.options.el?.nativeElement.contains(event.relatedTarget)) {\n this.onTouchedCallback?.();\n this.triggerValidation();\n this.focused = false;\n }\n }\n\n onFocus() {\n this.focused = true;\n\n // fix for \"expression changed\" error when focus is returned to a combobox after a modal is closed\n // https://github.com/vmware-clarity/ng-clarity/issues/663\n this.cdr.detectChanges();\n }\n\n onChange() {\n if (this.editable && !this.multiSelect && this.options.emptyOptions) {\n const parsedInput = this.optionSelectionService.editableResolver(this._searchText);\n this.optionSelectionService.setSelectionValue(parsedInput);\n }\n }\n\n getSelectionAriaLabel() {\n if (this.containerService && this.containerService.labelText) {\n return `${this.containerService.labelText} ${this.commonStrings.keys.comboboxSelection}`;\n }\n return this.commonStrings.keys.comboboxSelection;\n }\n\n focusFirstActive() {\n setTimeout(() => {\n this.focusHandler.focusFirstActive();\n });\n }\n\n writeValue(value: T | T[]): void {\n this.optionSelectionService.selectionModel.model = value;\n this.updateInputValue(this.optionSelectionService.selectionModel);\n }\n\n registerOnTouched(onTouched: any): void {\n this.onTouchedCallback = onTouched;\n }\n\n registerOnChange(onChange: any): void {\n this.onChangeCallback = onChange;\n }\n\n getActiveDescendant() {\n const model = this.focusHandler.pseudoFocus.model;\n return model ? model.id : this.options?.noResultsElementId;\n }\n\n setDisabledState(): void {\n // do nothing\n }\n\n onWrapperClick(event) {\n if (this.disabled) {\n return;\n }\n this.focusHandler.focusInput();\n if (this.editable || (!this.editable && this.trigger.nativeElement.contains(event.target))) {\n this.popoverService.toggleWithEvent(event);\n }\n }\n\n toggleSelectionExpand() {\n this.selectionExpanded = !this.selectionExpanded;\n if (this.selectionExpanded) {\n this.applyLimit(this.multiSelectModel.length);\n } else {\n this.containerWidthChange.next(this.containerWidth);\n }\n }\n\n private initialiseObserver() {\n const container = this.container ? this.container.el.nativeElement : this.el.nativeElement.parentElement;\n this.containerWidth = container.offsetWidth;\n this.resizeObserver = new ResizeObserver(entries => {\n this.zone.runOutsideAngular(() => {\n entries.forEach(entry => {\n const entryWidth = entry.contentRect.width;\n switch (entry.target) {\n case container:\n if (this.containerWidth !== entryWidth) {\n this.containerWidth = entryWidth;\n this.containerWidthChange.next(entryWidth);\n }\n break;\n case this.wrapper.nativeElement:\n this.containerWidthChange.next(null);\n break;\n }\n });\n });\n });\n this.resizeObserver.observe(container);\n this.resizeObserver.observe(this.wrapper.nativeElement);\n }\n\n private calculateLimit() {\n this.shouldCalculate = true;\n this.cdr.detectChanges();\n if (!this.calculationPills || this.calculationPills.length === 0) {\n this.applyLimit();\n return;\n }\n const pillDimensions = this.calculationPills.map(p => ({\n top: p.nativeElement.offsetTop,\n width: p.nativeElement.offsetWidth,\n left: p.nativeElement.offsetLeft,\n }));\n\n const firstPill = pillDimensions[0];\n const buttonWidth = this.truncationButton?.nativeElement?.offsetWidth || 100;\n const textboxWidth = this.textbox.nativeElement.offsetWidth;\n const expectedWidth = this.containerWidth - textbo