UNPKG

novo-elements

Version:

1 lines 120 kB
{"version":3,"file":"novo-elements-elements-chips.mjs","sources":["../../../projects/novo-elements/src/elements/chips/Chip.ts","../../../projects/novo-elements/src/elements/chips/ChipDefaults.ts","../../../projects/novo-elements/src/elements/chips/ChipList.ts","../../../projects/novo-elements/src/elements/chips/ChipInput.ts","../../../projects/novo-elements/src/elements/chips/pipe/AvatarType.pipe.ts","../../../projects/novo-elements/src/elements/chips/Chips.ts","../../../projects/novo-elements/src/elements/chips/RowChips.ts","../../../projects/novo-elements/src/elements/chips/Chips.module.ts","../../../projects/novo-elements/src/elements/chips/novo-elements-elements-chips.ts"],"sourcesContent":["import { FocusableOption } from '@angular/cdk/a11y';\nimport { BooleanInput, coerceBooleanProperty, NumberInput } from '@angular/cdk/coercion';\nimport { Platform } from '@angular/cdk/platform';\n\nimport {\n Attribute,\n ChangeDetectorRef,\n Component,\n ContentChild,\n Directive,\n ElementRef,\n EventEmitter,\n Inject,\n InjectionToken,\n Input,\n NgZone,\n OnDestroy,\n Optional,\n Output,\n ViewEncapsulation,\n DOCUMENT,\n} from '@angular/core';\nimport { ANIMATION_MODULE_TYPE } from '@angular/platform-browser/animations';\nimport { Subject } from 'rxjs';\nimport { take } from 'rxjs/operators';\nimport { Key } from 'novo-elements/utils';\nimport { CanColor, CanColorCtor, CanSizeCtor, HasTabIndex, HasTabIndexCtor, mixinColor, mixinSize, mixinTabIndex } from 'novo-elements/elements/common';\n\nexport interface IRemovable {\n remove: () => void;\n removable: boolean;\n disabled: boolean;\n}\n\nexport const REMOVABLE_REF: InjectionToken<IRemovable> = new InjectionToken<IRemovable>('REMOVABLE_REF');\n\n/** Represents an event fired on an individual `novo-chip`. */\nexport interface NovoChipEvent {\n /** The chip the event was fired on. */\n chip: NovoChipElement;\n}\n\n/** Event object emitted by NovoChip when selected or deselected. */\nexport class NovoChipSelectionChange {\n constructor(\n /** Reference to the chip that emitted the event. */\n public source: NovoChipElement,\n /** Whether the chip that emitted the event is selected. */\n public selected: boolean,\n /** Whether the selection change was a result of a user interaction. */\n public isUserInput = false,\n ) {}\n}\n\n// Boilerplate for applying mixins to NovoChipElement.\n/** @docs-private */\nclass NovoChipBase {\n // abstract disabled: boolean;\n constructor(public _elementRef: ElementRef) {}\n}\n\nconst NovoChipMixinBase: CanSizeCtor & CanColorCtor & HasTabIndexCtor & typeof NovoChipBase = mixinSize(\n mixinTabIndex(mixinColor(NovoChipBase, null), -1),\n 'md',\n);\n\n/**\n * Dummy directive to add CSS class to chip avatar.\n * @docs-private\n */\n@Directive({\n selector: 'novo-chip-avatar, [novoChipAvatar]',\n host: { class: 'novo-chip-avatar' },\n standalone: false,\n})\nexport class NovoChipAvatar {}\n\n/**\n * Applies proper (click) support and adds styling for use with Bullhorn's \"x\" icon *\n * Example:\n *\n * `<novo-chip>\n * <novo-icon novoChipRemove>x</novo-icon>\n * </novo-chip>`\n *\n * You *may* use a custom icon, but you may need to override the `novo-chip-remove` positioning\n * styles to properly center the icon within the chip.\n */\n@Directive({\n selector: '[novoChipRemove]',\n host: {\n class: 'novo-chip-remove',\n '(click)': '_handleClick($event)',\n },\n standalone: false,\n})\nexport class NovoChipRemove {\n constructor(@Inject(REMOVABLE_REF) private _parentChip: IRemovable, elementRef: ElementRef<HTMLElement>) {\n if (elementRef.nativeElement.nodeName === 'BUTTON') {\n elementRef.nativeElement.setAttribute('type', 'button');\n }\n }\n\n /** Calls the parent chip's public `remove()` method if applicable. */\n _handleClick(event: Event): void {\n const parentChip = this._parentChip;\n if (parentChip.removable && !parentChip.disabled) {\n parentChip.remove();\n }\n\n // We need to stop event propagation because otherwise the event will bubble up to the\n // form field and cause the `onContainerClick` method to be invoked. This method would then\n // reset the focused chip that has been focused after chip removal. Usually the parent\n // the parent click listener of the `NovoChip` would prevent propagation, but it can happen\n // that the chip is being removed before the event bubbles up.\n event.stopPropagation();\n }\n}\n\n/**\n * Chip component. Used inside the NovoChipList component.\n */\n@Component({\n selector: 'novo-chip, [novo-chip]',\n template: '<ng-content></ng-content>',\n styleUrls: ['./Chip.scss'],\n encapsulation: ViewEncapsulation.None,\n inputs: ['color', 'tabIndex', 'size'],\n providers: [{ provide: REMOVABLE_REF, useExisting: NovoChipElement }],\n host: {\n class: 'novo-chip novo-focus-indicator',\n role: 'option',\n '[class.novo-chip-selectable]': 'selectable',\n '[class.novo-chip-selected]': 'selected',\n '[class.novo-chip-with-avatar]': 'avatar',\n '[class.novo-chip-with-trailing-icon]': 'removeIcon',\n '[class.novo-chip-disabled]': 'disabled',\n '[class._novo-animation-noopable]': '_animationsDisabled',\n '[attr.tabindex]': 'disabled ? null : tabIndex',\n '[attr.disabled]': 'disabled || null',\n '[attr.aria-disabled]': 'disabled.toString()',\n '[attr.aria-selected]': 'ariaSelected',\n '(click)': '_handleClick($event)',\n // '(mouseenter)': '_handleActivate($event)',\n // '(mouseleave)': '_handleDeactivate($event)',\n '(keydown)': '_handleKeydown($event)',\n '(focus)': 'focus()',\n '(blur)': '_blur()',\n },\n standalone: false,\n})\nexport class NovoChipElement extends NovoChipMixinBase implements FocusableOption, OnDestroy, CanColor, HasTabIndex {\n /** Whether the chip has focus. */\n _hasFocus: boolean = false;\n\n /** Whether animations for the chip are enabled. */\n _animationsDisabled: boolean;\n\n /** Whether the chip list is selectable */\n _chipListSelectable: boolean = true;\n\n /** Whether the chip list allows toggling */\n _chipListToggleable: boolean = true;\n\n /** Whether the chip list is in multi-selection mode. */\n _chipListMultiple: boolean = false;\n\n /** Whether the chip list as a whole is disabled. */\n _chipListDisabled: boolean = false;\n\n /** The chip avatar */\n @ContentChild(NovoChipAvatar) avatar: NovoChipAvatar;\n\n /** The chip's remove toggler. */\n @ContentChild(NovoChipRemove) removeIcon: NovoChipRemove;\n\n @Input() type: string;\n /** Whether the chip is selected. */\n @Input()\n get selected(): boolean {\n return this._selected;\n }\n set selected(value: boolean) {\n const coercedValue = coerceBooleanProperty(value);\n\n if (coercedValue !== this._selected) {\n this._selected = coercedValue;\n this._dispatchSelectionChange();\n }\n }\n protected _selected: boolean = false;\n\n /** The value of the chip. Defaults to the content inside `<novo-chip>` tags. */\n @Input()\n get value(): any {\n return this._value !== undefined ? this._value : this._elementRef.nativeElement.textContent;\n }\n set value(value: any) {\n this._value = value;\n }\n protected _value: any;\n\n /**\n * Whether or not the chip is selectable. When a chip is not selectable,\n * changes to its selected state are always ignored. By default a chip is\n * selectable, and it becomes non-selectable if its parent chip list is\n * not selectable.\n */\n @Input()\n get selectable(): boolean {\n return this._selectable && this._chipListSelectable;\n }\n set selectable(value: boolean) {\n this._selectable = coerceBooleanProperty(value);\n }\n protected _selectable: boolean = false;\n\n /** Whether the chip is disabled. */\n @Input()\n get disabled(): boolean {\n return this._chipListDisabled || this._disabled;\n }\n set disabled(value: boolean) {\n this._disabled = coerceBooleanProperty(value);\n }\n protected _disabled: boolean = false;\n\n /**\n * Determines whether or not the chip displays the remove styling and emits (removed) events.\n */\n @Input()\n get removable(): boolean {\n return this._removable;\n }\n set removable(value: boolean) {\n this._removable = coerceBooleanProperty(value);\n }\n protected _removable: boolean = true;\n\n /** Emits when the chip is focused. */\n readonly _onFocus = new Subject<NovoChipEvent>();\n\n /** Emits when the chip is blured. */\n readonly _onBlur = new Subject<NovoChipEvent>();\n\n /** Emitted when the chip is selected or deselected. */\n @Output() readonly selectionChange: EventEmitter<NovoChipSelectionChange> = new EventEmitter<NovoChipSelectionChange>();\n\n /** Emitted when the chip is destroyed. */\n @Output() readonly destroyed: EventEmitter<NovoChipEvent> = new EventEmitter<NovoChipEvent>();\n\n /** Emitted when a chip is to be removed. */\n @Output() readonly removed: EventEmitter<NovoChipEvent> = new EventEmitter<NovoChipEvent>();\n\n /** The ARIA selected applied to the chip. */\n get ariaSelected(): string | null {\n // Remove the `aria-selected` when the chip is deselected in single-selection mode, because\n // it adds noise to NVDA users where \"not selected\" will be read out for each chip.\n return this.selectable && (this._chipListMultiple || this.selected) ? this.selected.toString() : null;\n }\n\n constructor(\n public _elementRef: ElementRef<HTMLElement>,\n private _ngZone: NgZone,\n platform: Platform,\n @Optional()\n private _changeDetectorRef: ChangeDetectorRef,\n @Inject(DOCUMENT) _document: any,\n @Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string,\n @Attribute('tabindex') tabIndex?: string,\n ) {\n super(_elementRef);\n this._animationsDisabled = animationMode === 'NoopAnimations';\n this.tabIndex = tabIndex != null ? parseInt(tabIndex, 10) || -1 : -1;\n }\n\n ngOnDestroy() {\n this.destroyed.emit({ chip: this });\n }\n\n /** Selects the chip. */\n select(): void {\n if (!this._selected) {\n this._selected = true;\n this._dispatchSelectionChange();\n this._changeDetectorRef.markForCheck();\n }\n }\n\n /** Deselects the chip. */\n deselect(): void {\n if (this._selected) {\n this._selected = false;\n this._dispatchSelectionChange();\n this._changeDetectorRef.markForCheck();\n }\n }\n\n /** Select this chip and emit selected event */\n selectViaInteraction(): void {\n if (!this._selected) {\n this._selected = true;\n this._dispatchSelectionChange(true);\n this._changeDetectorRef.markForCheck();\n }\n }\n\n /** Toggles the current selected state of this chip. */\n toggleSelected(isUserInput: boolean = false): boolean {\n this._selected = !this.selected;\n this._dispatchSelectionChange(isUserInput);\n this._changeDetectorRef.markForCheck();\n return this.selected;\n }\n\n /** Allows for programmatic focusing of the chip. */\n focus(): void {\n if (!this._hasFocus) {\n this._elementRef.nativeElement.focus();\n this._onFocus.next({ chip: this });\n }\n this._hasFocus = true;\n }\n\n /**\n * Allows for programmatic removal of the chip. Called by the NovoChipList when the DELETE or\n * BACKSPACE keys are pressed.\n *\n * Informs any listeners of the removal request. Does not remove the chip from the DOM.\n */\n remove(): void {\n if (this.removable) {\n this.removed.emit({ chip: this });\n }\n }\n\n /** Handles click events on the chip. */\n _handleClick(event: Event) {\n if (this.disabled) {\n event.preventDefault();\n } else {\n event.stopPropagation();\n }\n if (this._chipListToggleable) {\n this.toggleSelected(true);\n }\n }\n\n /** Handle custom key presses. */\n _handleKeydown(event: KeyboardEvent): void {\n if (this.disabled) {\n return;\n }\n\n switch (event.key) {\n case Key.Delete:\n case Key.Backspace:\n // If we are removable, remove the focused chip\n this.remove();\n // Always prevent so page navigation does not occur\n event.preventDefault();\n break;\n case Key.Space:\n // If we are selectable, toggle the focused chip\n if (this.selectable) {\n this.toggleSelected(true);\n }\n // Always prevent space from scrolling the page since the list has focus\n event.preventDefault();\n break;\n default:\n // No default action for other keys\n break;\n }\n }\n\n _blur(): void {\n // When animations are enabled, Angular may end up removing the chip from the DOM a little\n // earlier than usual, causing it to be blurred and throwing off the logic in the chip list\n // that moves focus not the next item. To work around the issue, we defer marking the chip\n // as not focused until the next time the zone stabilizes.\n this._ngZone.onStable.pipe(take(1)).subscribe(() => {\n this._ngZone.run(() => {\n this._hasFocus = false;\n this._onBlur.next({ chip: this });\n });\n });\n }\n\n private _dispatchSelectionChange(isUserInput = false) {\n this.selectionChange.emit({\n source: this,\n isUserInput,\n selected: this._selected,\n });\n }\n\n static ngAcceptInputType_selected: BooleanInput;\n static ngAcceptInputType_selectable: BooleanInput;\n static ngAcceptInputType_removable: BooleanInput;\n static ngAcceptInputType_disabled: BooleanInput;\n static ngAcceptInputType_tabIndex: NumberInput;\n}\n","import { InjectionToken } from '@angular/core';\n\n/** Default options, for the chips module, that can be overridden. */\nexport interface NovoChipsDefaultOptions {\n /** The list of key codes that will trigger a chipEnd event. */\n separatorKeyCodes: readonly string[];\n}\n\n/** Injection token to be used to override the default options for the chips module. */\nexport const NOVO_CHIPS_DEFAULT_OPTIONS = new InjectionToken<NovoChipsDefaultOptions>('novo-chips-default-options');\n","import { FocusKeyManager } from '@angular/cdk/a11y';\nimport { Directionality } from '@angular/cdk/bidi';\nimport { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';\nimport { SelectionModel } from '@angular/cdk/collections';\nimport {\n AfterContentInit,\n AfterViewInit,\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n ContentChildren,\n DoCheck,\n ElementRef,\n EventEmitter,\n Input,\n OnDestroy,\n OnInit,\n Optional,\n Output,\n QueryList,\n Self,\n ViewEncapsulation,\n} from '@angular/core';\nimport { ControlValueAccessor, FormGroupDirective, NgControl, NgForm } from '@angular/forms';\nimport { merge, Observable, Subject, Subscription } from 'rxjs';\nimport { startWith, takeUntil } from 'rxjs/operators';\nimport { Key } from 'novo-elements/utils';\nimport { CanUpdateErrorState, CanUpdateErrorStateCtor, ErrorStateMatcher, mixinErrorState, NOVO_OPTION_PARENT_COMPONENT } from 'novo-elements/elements/common';\nimport { NovoFieldControl } from 'novo-elements/elements/field';\nimport { NovoChipElement, NovoChipEvent, NovoChipSelectionChange } from './Chip';\nimport { NovoChipTextControl } from './ChipTextControl';\n\n// Boilerplate for applying mixins to NovoChipList.\n/** @docs-private */\nclass NovoChipListBase {\n constructor(\n public _defaultErrorStateMatcher: ErrorStateMatcher,\n public _parentForm: NgForm,\n public _parentFormGroup: FormGroupDirective,\n /** @docs-private */\n public ngControl: NgControl,\n ) {}\n}\nconst _NovoChipListMixinBase: CanUpdateErrorStateCtor & typeof NovoChipListBase = mixinErrorState(NovoChipListBase);\n\n// Increasing integer for generating unique ids for chip-list components.\nlet nextUniqueId = 0;\n\n/** Change event object that is emitted when the chip list value has changed. */\nexport class NovoChipListChange {\n constructor(\n /** Chip list that emitted the event. */\n public source: NovoChipList,\n /** Value of the chip list when the event was emitted. */\n public value: any,\n ) {}\n}\n\n/**\n * A chip list component (named ChipList for its similarity to the List component).\n */\n@Component({\n selector: 'novo-chip-list',\n template: '<div class=\"novo-chip-list-wrapper\"><ng-content></ng-content></div>',\n styleUrls: ['./ChipList.scss'],\n exportAs: 'novoChipList',\n host: {\n '[attr.tabindex]': 'disabled ? null : _tabIndex',\n '[attr.aria-describedby]': '_ariaDescribedby || null',\n '[attr.aria-required]': 'role ? required : null',\n '[attr.aria-disabled]': 'disabled.toString()',\n '[attr.aria-invalid]': 'errorState',\n '[attr.aria-multiselectable]': 'multiple',\n '[attr.role]': 'role',\n '[class.novo-chip-list-empty]': 'empty',\n '[class.novo-chip-list-has-value]': '!empty',\n '[class.novo-chip-list-stacked]': 'stacked',\n '[class.novo-chip-list-focused]': 'focused',\n '[class.novo-chip-list-disabled]': 'disabled',\n '[class.novo-chip-list-invalid]': 'errorState',\n '[class.novo-chip-list-required]': 'required',\n '[attr.aria-orientation]': 'ariaOrientation',\n class: 'novo-chip-list',\n '(focus)': 'focus()',\n '(blur)': '_blur()',\n '(keydown)': '_keydown($event)',\n '[id]': '_uid',\n },\n providers: [\n { provide: NovoFieldControl, useExisting: NovoChipList },\n { provide: NOVO_OPTION_PARENT_COMPONENT, useExisting: NovoChipList },\n ],\n // styleUrls: ['./ChipList.scss'],\n encapsulation: ViewEncapsulation.None,\n changeDetection: ChangeDetectionStrategy.OnPush,\n standalone: false,\n})\nexport class NovoChipList\n extends _NovoChipListMixinBase\n implements NovoFieldControl<any>, ControlValueAccessor, AfterViewInit, AfterContentInit, DoCheck, OnInit, OnDestroy, CanUpdateErrorState\n{\n /**\n * Implemented as part of NovoFieldControl.\n * @docs-private\n */\n readonly controlType: string = 'chip-list';\n\n /**\n * When a chip is destroyed, we store the index of the destroyed chip until the chips\n * query list notifies about the update. This is necessary because we cannot determine an\n * appropriate chip that should receive focus until the array of chips updated completely.\n */\n private _lastDestroyedChipIndex: number | null = null;\n\n /** Subject that emits when the component has been destroyed. */\n private _destroyed = new Subject<void>();\n\n /** Subscription to focus changes in the chips. */\n private _chipFocusSubscription: Subscription | null;\n\n /** Subscription to blur changes in the chips. */\n private _chipBlurSubscription: Subscription | null;\n\n /** Subscription to selection changes in chips. */\n private _chipSelectionSubscription: Subscription | null;\n\n /** Subscription to remove changes in chips. */\n private _chipRemoveSubscription: Subscription | null;\n\n /** The chip input to add more chips */\n protected _chipInput: NovoChipTextControl;\n\n /** Uid of the chip list */\n _uid: string = `novo-chip-list-${nextUniqueId++}`;\n\n /** The aria-describedby attribute on the chip list for improved a11y. */\n _ariaDescribedby: string;\n\n /** Tab index for the chip list. */\n _tabIndex = 0;\n\n /**\n * User defined tab index.\n * When it is not null, use user defined tab index. Otherwise use _tabIndex\n */\n _userTabIndex: number | null = null;\n\n /** The FocusKeyManager which handles focus. */\n _keyManager: FocusKeyManager<NovoChipElement>;\n\n /** Function when touched */\n _onTouched = () => {};\n\n /** Function when changed */\n _onChange: (value: any) => void = () => {};\n\n _selectionModel: SelectionModel<NovoChipElement>;\n\n /** The array of selected chips inside chip list. */\n get selected(): NovoChipElement[] | NovoChipElement {\n return this.multiple ? this._selectionModel.selected : this._selectionModel.selected[0];\n }\n\n /** The ARIA role applied to the chip list. */\n get role(): string | null {\n return this.empty ? null : 'listbox';\n }\n\n /** An object used to control when error messages are shown. */\n @Input() errorStateMatcher: ErrorStateMatcher;\n\n /** Whether the user should be allowed to select multiple chips. */\n @Input()\n get multiple(): boolean {\n return this._multiple;\n }\n set multiple(value: boolean) {\n this._multiple = coerceBooleanProperty(value);\n this._syncChipsState();\n }\n private _multiple: boolean = false;\n\n /** Whether chips in this list can be toggled by user interaction */\n @Input()\n get chipsToggleable(): boolean {\n return this._chipsToggleable;\n }\n set chipsToggleable(value: boolean) {\n this._chipsToggleable = coerceBooleanProperty(value);\n this._syncChipsState();\n }\n private _chipsToggleable: boolean = true;\n\n /** Whether the chips should appear stacked instead of a row. */\n @Input()\n get stacked(): boolean {\n return this._stacked;\n }\n set stacked(value: boolean) {\n this._stacked = coerceBooleanProperty(value);\n }\n private _stacked: boolean = false;\n\n /**\n * A function to compare the option values with the selected values. The first argument\n * is a value from an option. The second is a value from the selection. A boolean\n * should be returned.\n */\n @Input()\n get compareWith(): (o1: any, o2: any) => boolean {\n return this._compareWith;\n }\n set compareWith(fn: (o1: any, o2: any) => boolean) {\n this._compareWith = fn;\n if (this._selectionModel) {\n // A different comparator means the selection could change.\n this._initializeSelection();\n }\n }\n private _compareWith = (o1: any, o2: any) => o1 === o2;\n\n /**\n * Implemented as part of NovoFieldControl.\n * @docs-private\n */\n @Input()\n get value(): any {\n return this._value;\n }\n set value(value: any) {\n this._value = value;\n this.writeValue(value);\n }\n protected _value: any;\n\n /**\n * Implemented as part of NovoFieldControl.\n * @docs-private\n */\n get id(): string {\n return this._chipInput ? this._chipInput.id : this._uid;\n }\n\n /**\n * Implemented as part of NovoFieldControl.\n * @docs-private\n */\n @Input()\n get required(): boolean {\n return this._required;\n }\n set required(value: boolean) {\n this._required = coerceBooleanProperty(value);\n this.stateChanges.next();\n }\n protected _required: boolean = false;\n\n /**\n * Implemented as part of NovoFieldControl.\n * @docs-private\n */\n @Input()\n get placeholder(): string {\n return this._chipInput ? this._chipInput.placeholder : this._placeholder;\n }\n set placeholder(value: string) {\n this._placeholder = value;\n this.stateChanges.next();\n }\n protected _placeholder: string;\n\n /** Whether any chips or the novoChipInput inside of this chip-list has focus. */\n get focused(): boolean {\n return (this._chipInput && this._chipInput.focused) || this._hasFocusedChip();\n }\n\n /**\n * Implemented as part of NovoFieldControl.\n * @docs-private\n */\n get empty(): boolean {\n return (!this._chipInput || this._chipInput.empty) && (!this.chips || this.chips.length === 0);\n }\n\n /**\n * Implemented as part of NovoFieldControl.\n * @docs-private\n */\n get shouldLabelFloat(): boolean {\n return !this.empty || this.focused;\n }\n\n /**\n * Implemented as part of NovoFieldControl.\n * @docs-private\n */\n @Input()\n get disabled(): boolean {\n return this.ngControl ? !!this.ngControl.disabled : this._disabled;\n }\n set disabled(value: boolean) {\n this._disabled = coerceBooleanProperty(value);\n this._syncChipsState();\n }\n protected _disabled: boolean = false;\n\n /** Orientation of the chip list. */\n @Input('aria-orientation') ariaOrientation: 'horizontal' | 'vertical' = 'horizontal';\n\n /**\n * Whether or not this chip list is selectable. When a chip list is not selectable,\n * the selected states for all the chips inside the chip list are always ignored.\n */\n @Input()\n get selectable(): boolean {\n return this._selectable;\n }\n set selectable(value: boolean) {\n this._selectable = coerceBooleanProperty(value);\n\n if (this.chips) {\n this.chips.forEach((chip) => (chip._chipListSelectable = this._selectable));\n }\n }\n protected _selectable: boolean = true;\n\n @Input()\n set tabIndex(value: number) {\n this._userTabIndex = value;\n this._tabIndex = value;\n }\n\n /** Combined stream of all of the child chips' selection change events. */\n get chipSelectionChanges(): Observable<NovoChipSelectionChange> {\n return merge(...this.chips.map((chip) => chip.selectionChange));\n }\n\n /** Combined stream of all of the child chips' focus change events. */\n get chipFocusChanges(): Observable<NovoChipEvent> {\n return merge(...this.chips.map((chip) => chip._onFocus));\n }\n\n /** Combined stream of all of the child chips' blur change events. */\n get chipBlurChanges(): Observable<NovoChipEvent> {\n return merge(...this.chips.map((chip) => chip._onBlur));\n }\n\n /** Combined stream of all of the child chips' remove change events. */\n get chipRemoveChanges(): Observable<NovoChipEvent> {\n return merge(...this.chips.map((chip) => chip.removed));\n }\n\n /** Event emitted when the selected chip list value has been changed by the user. */\n @Output() readonly change: EventEmitter<NovoChipListChange> = new EventEmitter<NovoChipListChange>();\n\n /**\n * Event that emits whenever the raw value of the chip-list changes. This is here primarily\n * to facilitate the two-way binding for the `value` input.\n * @docs-private\n */\n @Output() readonly valueChange: EventEmitter<any> = new EventEmitter<any>();\n\n /** The chip components contained within this chip list. */\n @ContentChildren(NovoChipElement, {\n // We need to use `descendants: true`, because Ivy will no longer match\n // indirect descendants if it's left as false.\n descendants: true,\n })\n chips: QueryList<NovoChipElement>;\n\n /** @docs-private Implemented as part of NovoFieldControl. */\n lastKeyValue: string = null;\n /** @docs-private Implemented as part of NovoFieldControl.*/\n lastCaretPosition: number | null;\n\n constructor(\n protected _elementRef: ElementRef<HTMLElement>,\n private _changeDetectorRef: ChangeDetectorRef,\n @Optional() private _dir: Directionality,\n @Optional() _parentForm: NgForm,\n @Optional() _parentFormGroup: FormGroupDirective,\n _defaultErrorStateMatcher: ErrorStateMatcher,\n /** @docs-private */\n @Optional() @Self() public ngControl: NgControl,\n ) {\n super(_defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl);\n if (this.ngControl) {\n this.ngControl.valueAccessor = this;\n }\n }\n\n ngAfterContentInit() {\n this._keyManager = new FocusKeyManager<NovoChipElement>(this.chips)\n .withWrap()\n .withVerticalOrientation()\n .withHomeAndEnd()\n .withHorizontalOrientation(this._dir ? this._dir.value : 'ltr');\n\n if (this._dir) {\n this._dir.change.pipe(takeUntil(this._destroyed)).subscribe((dir) => this._keyManager.withHorizontalOrientation(dir));\n }\n\n this._keyManager.tabOut.pipe(takeUntil(this._destroyed)).subscribe(() => {\n this._allowFocusEscape();\n });\n\n // When the list changes, re-subscribe\n this.chips.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => {\n Promise.resolve().then(() => {\n this._syncChipsState();\n });\n\n this._resetChips();\n\n if (this._value !== undefined) {\n Promise.resolve().then(() => {\n this._setSelectionByValue(this._value, false);\n });\n }\n\n // Check to see if we need to update our tab index\n this._updateTabIndex();\n\n // Check to see if we have a destroyed chip and need to refocus\n this._updateFocusForDestroyedChips();\n\n this.stateChanges.next();\n });\n }\n\n ngAfterViewInit() {\n // Reset chips selected/deselected status\n this._initializeSelection();\n }\n\n ngOnInit() {\n this._selectionModel = new SelectionModel<NovoChipElement>(this.multiple, undefined, false);\n this.stateChanges.next();\n }\n\n ngDoCheck() {\n if (this.ngControl) {\n // We need to re-evaluate this on every change detection cycle, because there are some\n // error triggers that we can't subscribe to (e.g. parent form submissions). This means\n // that whatever logic is in here has to be super lean or we risk destroying the performance.\n this.updateErrorState();\n\n if (this.ngControl.disabled !== this._disabled) {\n this.disabled = !!this.ngControl.disabled;\n }\n }\n }\n\n ngOnDestroy() {\n this._destroyed.next();\n this._destroyed.complete();\n this.stateChanges.complete();\n\n this._dropSubscriptions();\n }\n\n /** Associates an HTML input element with this chip list. */\n registerInput(inputElement: NovoChipTextControl): void {\n this._chipInput = inputElement;\n\n // We use this attribute to match the chip list to its input in test harnesses.\n // Set the attribute directly here to avoid \"changed after checked\" errors.\n this._elementRef.nativeElement.setAttribute('data-novo-chip-input', inputElement.id);\n }\n\n /**\n * Implemented as part of NovoFieldControl.\n * @docs-private\n */\n setDescribedByIds(ids: string[]) {\n this._ariaDescribedby = ids.join(' ');\n }\n\n // Implemented as part of ControlValueAccessor.\n writeValue(value: any): void {\n this._value = value;\n if (this.chips && this.chips.length > 0) {\n this._setSelectionByValue(value, false);\n }\n this.stateChanges.next();\n }\n\n addValue(value: any): void {\n this.value = [...this.value, value];\n this._chipInput.clearValue();\n }\n\n removeValue(value: any): void {\n if (this.value && Array.isArray(this.value)) {\n this.value = this.value.filter((it) => !this.compareWith(it, value));\n }\n }\n\n // Implemented as part of ControlValueAccessor.\n registerOnChange(fn: (value: any) => void): void {\n this._onChange = fn;\n }\n\n // Implemented as part of ControlValueAccessor.\n registerOnTouched(fn: () => void): void {\n this._onTouched = fn;\n }\n\n // Implemented as part of ControlValueAccessor.\n setDisabledState(isDisabled: boolean): void {\n this.disabled = isDisabled;\n this.stateChanges.next();\n }\n\n /**\n * Implemented as part of NovoFieldControl.\n * @docs-private\n */\n onContainerClick(event: MouseEvent) {\n if (!this._originatesFromChip(event)) {\n this.focus();\n }\n }\n\n /**\n * Focuses the first non-disabled chip in this chip list, or the associated input when there\n * are no eligible chips.\n */\n focus(options?: FocusOptions): void {\n if (this.disabled) {\n return;\n }\n\n // TODO: ARIA says this should focus the first `selected` chip if any are selected.\n // Focus on first element if there's no chipInput inside chip-list\n if (this._chipInput && this._chipInput.focused) {\n // do nothing\n } else if (this._chipInput) {\n Promise.resolve().then(() => this._focusInput(options));\n this.stateChanges.next();\n } else if (this.chips.length > 0) {\n this._keyManager.setFirstItemActive();\n this.stateChanges.next();\n }\n }\n\n /** Attempt to focus an input if we have one. */\n _focusInput(options?: FocusOptions) {\n if (this._chipInput) {\n this._chipInput.focus(options);\n }\n }\n\n /**\n * Pass events to the keyboard manager. Available here for tests.\n */\n _keydown(event: KeyboardEvent) {\n const target = event.target as HTMLElement;\n\n // If they are on an empty input and hit backspace, focus the last chip\n if (event.key === Key.Backspace && this._isInputEmpty(target)) {\n this._keyManager.setLastItemActive();\n event.preventDefault();\n } else if (target && target.classList.contains('novo-chip')) {\n this._keyManager.onKeydown(event);\n this.stateChanges.next();\n }\n }\n\n /**\n * Check the tab index as you should not be allowed to focus an empty list.\n */\n protected _updateTabIndex(): void {\n // If we have 0 chips, we should not allow keyboard focus\n this._tabIndex = this._userTabIndex || (this.chips.length === 0 ? -1 : 0);\n }\n\n /**\n * If the amount of chips changed, we need to update the\n * key manager state and focus the next closest chip.\n */\n protected _updateFocusForDestroyedChips() {\n // Move focus to the closest chip. If no other chips remain, focus the chip-list itself.\n if (this._lastDestroyedChipIndex != null) {\n if (this.chips.length) {\n const newChipIndex = Math.min(this._lastDestroyedChipIndex, this.chips.length - 1);\n this._keyManager.setActiveItem(newChipIndex);\n } else {\n this.focus();\n }\n }\n\n this._lastDestroyedChipIndex = null;\n }\n\n /**\n * Utility to ensure all indexes are valid.\n *\n * @param index The index to be checked.\n * @returns True if the index is valid for our list of chips.\n */\n private _isValidIndex(index: number): boolean {\n return index >= 0 && index < this.chips.length;\n }\n\n private _isInputEmpty(element: HTMLElement): boolean {\n if (element && element.nodeName.toLowerCase() === 'input') {\n const input = element as HTMLInputElement;\n return !input.value;\n }\n\n return false;\n }\n\n _setSelectionByValue(value: any, isUserInput: boolean = true) {\n this._clearSelection();\n this.chips.forEach((chip) => chip.deselect());\n\n if (Array.isArray(value)) {\n value.forEach((currentValue) => this._selectValue(currentValue, isUserInput));\n this._sortValues();\n } else {\n const correspondingChip = this._selectValue(value, isUserInput);\n\n // Shift focus to the active item. Note that we shouldn't do this in multiple\n // mode, because we don't know what chip the user interacted with last.\n if (correspondingChip) {\n if (isUserInput) {\n this._keyManager.setActiveItem(correspondingChip);\n }\n }\n }\n }\n\n /**\n * Finds and selects the chip based on its value.\n * @returns Chip that has the corresponding value.\n */\n private _selectValue(value: any, isUserInput: boolean = true): NovoChipElement | undefined {\n const correspondingChip = this.chips.find((chip) => {\n return chip.value != null && this._compareWith(chip.value, value);\n });\n\n if (correspondingChip) {\n isUserInput ? correspondingChip.selectViaInteraction() : correspondingChip.select();\n this._selectionModel.select(correspondingChip);\n }\n\n return correspondingChip;\n }\n\n private _initializeSelection(): void {\n // Defer setting the value in order to avoid the \"Expression\n // has changed after it was checked\" errors from Angular.\n Promise.resolve().then(() => {\n if (this.ngControl) {\n this.value = this.ngControl.value;\n }\n });\n }\n\n /**\n * Deselects every chip in the list.\n * @param skip Chip that should not be deselected.\n */\n private _clearSelection(skip?: NovoChipElement): void {\n this._selectionModel.clear();\n this.chips.forEach((chip) => {\n if (chip !== skip) {\n chip.deselect();\n }\n });\n this.stateChanges.next();\n }\n\n /**\n * Sorts the model values, ensuring that they keep the same\n * order that they have in the panel.\n */\n private _sortValues(): void {\n if (this._multiple) {\n this._selectionModel.clear();\n\n this.chips.forEach((chip) => {\n if (chip.selected) {\n this._selectionModel.select(chip);\n }\n });\n this.stateChanges.next();\n }\n }\n\n /** Emits change event to set the model value. */\n private _propagateChanges(fallbackValue?: any): void {\n let valueToEmit: any = null;\n\n if (Array.isArray(this.selected)) {\n valueToEmit = this.selected.map((chip) => chip.value);\n } else {\n valueToEmit = this.selected ? this.selected.value : fallbackValue;\n }\n this.change.emit(new NovoChipListChange(this, valueToEmit));\n this._onChange(valueToEmit);\n this._changeDetectorRef.markForCheck();\n }\n\n /** When blurred, mark the field as touched when focus moved outside the chip list. */\n _blur() {\n if (!this._hasFocusedChip()) {\n this._keyManager.setActiveItem(-1);\n }\n\n if (!this.disabled) {\n if (this._chipInput) {\n // If there's a chip input, we should check whether the focus moved to chip input.\n // If the focus is not moved to chip input, mark the field as touched. If the focus moved\n // to chip input, do nothing.\n // Timeout is needed to wait for the focus() event trigger on chip input.\n setTimeout(() => {\n if (!this.focused) {\n this._markAsTouched();\n }\n });\n } else {\n // If there's no chip input, then mark the field as touched.\n this._markAsTouched();\n }\n }\n }\n\n /** Mark the field as touched */\n _markAsTouched() {\n this._onTouched();\n this._changeDetectorRef.markForCheck();\n this.stateChanges.next();\n }\n\n /**\n * Removes the `tabindex` from the chip list and resets it back afterwards, allowing the\n * user to tab out of it. This prevents the list from capturing focus and redirecting\n * it back to the first chip, creating a focus trap, if it user tries to tab away.\n */\n _allowFocusEscape() {\n if (this._tabIndex !== -1) {\n this._tabIndex = -1;\n\n setTimeout(() => {\n this._tabIndex = this._userTabIndex || 0;\n this._changeDetectorRef.markForCheck();\n });\n }\n }\n\n private _resetChips() {\n this._dropSubscriptions();\n this._listenToChipsFocus();\n this._listenToChipsSelection();\n this._listenToChipsRemoved();\n }\n\n private _dropSubscriptions() {\n if (this._chipFocusSubscription) {\n this._chipFocusSubscription.unsubscribe();\n this._chipFocusSubscription = null;\n }\n\n if (this._chipBlurSubscription) {\n this._chipBlurSubscription.unsubscribe();\n this._chipBlurSubscription = null;\n }\n\n if (this._chipSelectionSubscription) {\n this._chipSelectionSubscription.unsubscribe();\n this._chipSelectionSubscription = null;\n }\n\n if (this._chipRemoveSubscription) {\n this._chipRemoveSubscription.unsubscribe();\n this._chipRemoveSubscription = null;\n }\n }\n\n /** Listens to user-generated selection events on each chip. */\n private _listenToChipsSelection(): void {\n this._chipSelectionSubscription = this.chipSelectionChanges.subscribe((event) => {\n event.source.selected ? this._selectionModel.select(event.source) : this._selectionModel.deselect(event.source);\n\n // For single selection chip list, make sure the deselected value is unselected.\n if (!this.multiple) {\n this.chips.forEach((chip) => {\n if (!this._selectionModel.isSelected(chip) && chip.selected) {\n chip.deselect();\n }\n });\n }\n\n if (event.isUserInput) {\n this._propagateChanges();\n }\n });\n }\n\n /** Listens to user-generated selection events on each chip. */\n private _listenToChipsFocus(): void {\n this._chipFocusSubscription = this.chipFocusChanges.subscribe((event) => {\n const chipIndex: number = this.chips.toArray().indexOf(event.chip);\n\n if (this._isValidIndex(chipIndex)) {\n this._keyManager.updateActiveItem(chipIndex);\n }\n this.stateChanges.next();\n });\n\n this._chipBlurSubscription = this.chipBlurChanges.subscribe(() => {\n this._blur();\n this.stateChanges.next();\n });\n }\n\n private _listenToChipsRemoved(): void {\n this._chipRemoveSubscription = this.chipRemoveChanges.subscribe((event) => {\n const chip = event.chip;\n const chipIndex = this.chips.toArray().indexOf(event.chip);\n this.removeValue(chip.value);\n // In case the chip that will be removed is currently focused, we temporarily store\n // the index in order to be able to determine an appropriate sibling chip that will\n // receive focus.\n if (this._isValidIndex(chipIndex) && chip._hasFocus) {\n this._lastDestroyedChipIndex = chipIndex;\n }\n this.stateChanges.next();\n });\n }\n\n /** Checks whether an event comes from inside a chip element. */\n private _originatesFromChip(event: Event): boolean {\n let currentElement = event.target as HTMLElement | null;\n\n while (currentElement && currentElement !== this._elementRef.nativeElement) {\n if (currentElement.classList.contains('novo-chip')) {\n return true;\n }\n\n currentElement = currentElement.parentElement;\n }\n\n return false;\n }\n\n /** Checks whether any of the chips is focused. */\n private _hasFocusedChip() {\n return this.chips && this.chips.some((chip) => chip._hasFocus);\n }\n\n /** Syncs the list's state with the individual chips. */\n private _syncChipsState() {\n if (this.chips) {\n this.chips.forEach((chip) => {\n chip._chipListDisabled = this._disabled;\n chip._chipListMultiple = this.multiple;\n chip._chipListSelectable = this.selectable;\n chip._chipListToggleable = this.chipsToggleable;\n });\n }\n }\n\n static ngAcceptInputType_multiple: BooleanInput;\n static ngAcceptInputType_required: BooleanInput;\n static ngAcceptInputType_disabled: BooleanInput;\n static ngAcceptInputType_selectable: BooleanInput;\n}\n","import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';\nimport { hasModifierKey } from '@angular/cdk/keycodes';\nimport { Directive, ElementRef, EventEmitter, forwardRef, Inject, Input, OnChanges, OnDestroy, Optional, Output, Self } from '@angular/core';\nimport { NgControl } from '@angular/forms';\nimport { Key } from 'novo-elements/utils';\nimport { NovoChipsDefaultOptions, NOVO_CHIPS_DEFAULT_OPTIONS } from './ChipDefaults';\nimport { NovoChipList } from './ChipList';\nimport { NovoChipTextControl } from './ChipTextControl';\nimport { NovoFieldElement } from 'novo-elements/elements/field';\nimport { Subject } from 'rxjs';\n\n/** Represents an input event on a `novoChipInput`. */\nexport interface NovoChipInputEvent {\n /** The native `<input>` element that the event is being fired for. */\n input: HTMLInputElement;\n\n /** The value of the input. */\n value: string;\n}\n\n// Increasing integer for generating unique ids.\nlet nextUniqueId = 0;\n\n/**\n * Directive that adds chip-specific behaviors to an input element inside `<novo-form-field>`.\n * May be placed inside or outside of an `<novo-chip-list>`.\n */\n@Directive({\n selector: 'input[novoChipInput]',\n exportAs: 'novoChipInput, novoChipInputFor',\n host: {\n class: 'novo-chip-input novo-input-element',\n '(keydown)': '_keydown($event)',\n '(blur)': '_blur($event)',\n '(focus)': '_focus()',\n '(input)': '_onInput()',\n '[id]': 'id',\n '[attr.disabled]': 'disabled || null',\n '[attr.placeholder]': 'placeholder || null',\n '[attr.aria-invalid]': 'chipList && chipList.ngControl ? chipList.ngControl.invalid : null',\n '[attr.aria-required]': 'chipList && chipList.required || null',\n },\n standalone: false,\n})\nexport class NovoChipInput implements NovoChipTextControl, OnChanges, OnDestroy {\n /** Whether the control is focused. */\n focused: boolean = false;\n\n /**\n * Whether or not the chipEnd event will be emitted when the input is blurred.\n */\n @Input('novoChipInputAddOnBlur')\n get addOnBlur(): boolean {\n return this._addOnBlur;\n }\n set addOnBlur(value: boolean) {\n this._addOnBlur = coerceBooleanProperty(value);\n }\n _addOnBlur: boolean = false;\n\n /**\n * The list of key codes that will trigger a chipEnd event.\n *\n * Defaults to `[Key.Enter]`.\n */\n @Input('novoChipInputSeparatorKeyCodes')\n separatorKeyCodes: readonly string[] = this._defaultOptions.separatorKeyCodes;\n\n /** Emitted when a chip is to be added. */\n @Output('novoChipInputTokenEnd')\n chipEnd: EventEmitter<NovoChipInputEvent> = new EventEmitter<NovoChipInputEvent>();\n\n /** The input's placeholder text. */\n @Input() placeholder: string = '';\n\n /** Unique id for the input. */\n @Input() id: string = `novo-chip-list-input-${nextUniqueId++}`;\n\n /** Whether the input is disabled. */\n @Input()\n get disabled(): boolean {\n return this._disabled || (this._chipList && this._chipList.disabled);\n }\n set disabled(value: boolean) {\n this._disabled = coerceBooleanProperty(value);\n }\n private _disabled: boolean = false;\n\n /** Whether the input is empty. */\n get empty(): boolean {\n return !this._inputElement.value;\n }\n\n /** Getter for accessing chipList in templates */\n get chipList(): NovoChipList {\n return this._chipList;\n }\n\n /** The native input element to which this directive is attached. */\n protected _inputElement: HTMLInputElement;\n\n destroy$ = new Subject<void>();\n\n constructor(\n protected _elementRef: ElementRef<HTMLInputElement>,\n @Inject(NOVO_CHIPS_DEFAULT_OPTIONS) private readonly _defaultOptions: NovoChipsDefaultOptions,\n @Optional() @Inject(NovoFieldElement) private readonly _field: NovoFieldElement,\n @Inject(forwardRef(() => NovoChipList)) private readonly _chipList: NovoChipList,\n @Optional() @Self() protected ngControl: NgControl,\n ) {\n this._inputElement = this._elementRef.nativeElement;\n this._chipList.registerInput(this);\n }\n\n ngOnChanges() {\n this._chipList.stateChanges.next();\n }\n\n ngOnDestroy(): void {\n this.destroy$.next();\n this.destroy$.complete();\n }\n\n /** Utility method to make host definition/tests more clear. */\n _keydown(event?: KeyboardEvent) {\n // Allow the user's focus to escape when they're tabbing forward. Note that we don't\n // want to do this when going backwards, because focus should go back to the first chip.\n if (event && event.key === Key.Tab && !hasModifierKey(event, 'shiftKey')) {\n this._chipList._allowFocusEscape();\n }\n\n this._emitChipEnd(event);\n }\n\n /** Checks to see if the blur should emit the (chipEnd) event. */\n _blur(blurEvent: FocusEvent) {\n if (this.addOnBlur) {\n this._emitChipEnd();\n } else if (!this._field.blurEventIsInField(blurEvent)) {\n this.clearValue();\n }\n this.focused = false;\n // Blur the chip list if it is not focused\n if (!this._chipList.focused) {\n this._chipList._blur();\n }\n this._chipList.stateChanges.next();\n }\n\n _focus() {\n this.focused = true;\n this._chipList.stateChanges.next();\n }\n\n /** Checks to see if the (chipEnd) event needs to be emitted. */\n _emitChipEnd(event?: KeyboardEvent) {\n if (!this._inputElement.value && !!event) {\n this._chipList._keydown(event);\n }\n if (!event || this._isSeparatorKey(event)) {\n this.chipEnd.emit({ input: this._inputElement, value: this._inputElement.value });\n\n if (event) {\n event.preventDefault();\n }\n }\n }\n\n _onInput() {\n // Let chip list know whenever the value changes.\n this._chipList.stateChanges.next();\n }\n\n /** Focuses the input. */\n focus(options?: FocusOptions): void {\n this._inputElement.focus(options);\n }\n\n /** Clears the input. */\n clearValue(): void {\n this._inputElement.value = '';\n this.ngControl?.control?.setValue('');\n }\n\n /** Checks whether a keycode is one of the configured separators. */\n private _isSeparatorKey(event: KeyboardEvent) {\n return !hasModifierKey(event) && new Set(this.separatorKeyCodes).has(event.key);\n }\n\n static readonly ngAcceptInputType_addOnBlur: BooleanInput;\n static readonly ngAcceptInputType_disabled: BooleanInput;\n}\n","import { Pipe, PipeTransform } from '@angular/core';\n\n@Pipe({\n name: 'avatarType',\n standalone: false,\n})\nexport class AvatarTypePipe implements PipeTransform {\n transform(item: any, type?: any): string {\n return (type || item?.value?.searchEntity || '').toLowerCase();\n }\n}\n","// NG2\nimport { coerceBooleanProperty } from '@angular/cdk/coercion';\nimport { ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, inject, Input, OnInit, Output, ViewChild, ViewContainerRef } from '@angular/core';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\n// Vendor\nimport { ReplaySubject } from 'rxjs';\nimport { ComponentUtils, NovoLabelService } from 'novo-elements/services';\nimport { Helpers, Key } from 'novo-elements/utils';\nimport { NovoPickerElement } from 'novo-elements/elements/picker';\nimport { ElementSize } from 'novo-elements/elements/common';\n\n// Value accessor for the component (supports ngModel)\nconst CHIPS_VALUE_ACCESSOR = {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => NovoChipsElement),\n multi: true,\n};\n\n@Component({\n selector: 'chips,novo-chips',\n providers: [CHIPS_VALUE_ACCESSOR],\n template: `\n <div class=\"novo-chip-container\">\n <novo-chip\n *ngFor=\"let item of _items | async | slice: 0:hiddenChipsLimit\"\n [ngClass]=\"getDynamicClasses(item)\"\n [class.selected]=\"item == selected\