src/components/sq-select-multi-tags/sq-select-multi-tags.component.ts
Represents a multi-tag select component.
Example :<sq-select-multi-tags
[name]="'tags'"
[value]="selectedTags"
[options]="tagOptions"
(valueChange)="handleTagSelection($event)"
>
</sq-select-multi-tags>
OnChanges
| changeDetection | ChangeDetectionStrategy.OnPush |
| selector | sq-select-multi-tags |
| standalone | true |
| imports |
NgClass
NgStyle
NgTemplateOutlet
AsyncPipe
FormsModule
ScrollingModule
SqLoaderComponent
SqTooltipComponent
SqTagComponent
SqDataTestDirective
SqSelectorComponent
UniversalSafePipe
TranslateInternalPipe
SearchPipe
SearchValidValuesPipe
SqClickOutsideDirective
|
| styleUrls | ./sq-select-multi-tags.component.scss |
| templateUrl | ./sq-select-multi-tags.component.html |
constructor(element: ElementRef, translate: TranslateService, changeDetector: ChangeDetectorRef)
|
||||||||||||||||
|
Constructs a new SqSelectMultiTagsComponent.
Parameters :
|
| backgroundColor | |
Type : string
|
|
Default value : ''
|
|
|
Background color for the multi-tag select input. |
|
| borderColor | |
Type : string
|
|
Default value : ''
|
|
|
Border color for the multi-tag select input. |
|
| customClass | |
Type : string
|
|
Default value : ''
|
|
|
Custom CSS class for styling the component. |
|
| disabled | |
Type : boolean
|
|
Default value : false
|
|
|
Indicates whether the multi-tag select input is disabled. |
|
| errorSpan | |
Type : boolean
|
|
Default value : true
|
|
|
Indicates whether to display an error span. |
|
| externalError | |
Type : string
|
|
Default value : ''
|
|
|
External error message for the multi-tag select input. |
|
| externalIcon | |
Type : string
|
|
Default value : ''
|
|
|
External icon for the multi-tag select input. |
|
| hideSearch | |
Type : boolean
|
|
Default value : false
|
|
|
Indicates whether to hide the search input. |
|
| id | |
Type : string
|
|
|
The id attribute for the multi-tag select input. |
|
| label | |
Type : string
|
|
Default value : ''
|
|
|
The label for the multi-tag select input. |
|
| labelColor | |
Type : string
|
|
Default value : ''
|
|
|
Color for the label of the multi-tag select input. |
|
| loading | |
Type : boolean
|
|
Default value : false
|
|
|
Indicates whether the multi-tag select input is in a loading state. |
|
| maxHeight | |
Type : string
|
|
Default value : '100%'
|
|
|
Maximum height for the multi-tag values. |
|
| maxTags | |
Type : number
|
|
|
Maximum number of tags that can be chosen. |
|
| minCharactersToSearch | |
Type : number
|
|
Default value : 0
|
|
|
Minimum number of characters to perform the searchChange. |
|
| minTags | |
Type : number
|
|
|
Minimum number of tags that can be chosen. |
|
| name | |
Type : string
|
|
Default value : `random-name-${(1 + Date.now() + Math.random()).toString().replace('.', '')}`
|
|
|
The name attribute for the multi-tag select input. |
|
| options | |
Type : Array<OptionMulti>
|
|
Default value : []
|
|
|
Options available for selection. |
|
| placeholder | |
Type : string
|
|
Default value : ''
|
|
|
Placeholder text for the input field. |
|
| placeholderSearch | |
Type : string
|
|
Default value : ''
|
|
|
Placeholder text for the search input field. |
|
| readonly | |
Type : boolean
|
|
Default value : false
|
|
|
Indicates whether the multi-tag select input is readonly. |
|
| required | |
Type : boolean
|
|
Default value : false
|
|
|
Indicates whether the multi-tag select input is required. |
|
| selectHandleDataTest | |
Type : string
|
|
Default value : 'select-multi-tags'
|
|
|
The data-test attribute value for the select handle element. |
|
| selectInputDataTest | |
Type : string
|
|
Default value : 'input-multi-tags'
|
|
|
The data-test attribute value for the select input element. |
|
| SelectOptionDataTest | |
Type : string
|
|
Default value : 'option-multi-tags'
|
|
|
The data-test attribute value for the select option elements. |
|
| showInside | |
Type : boolean
|
|
Default value : true
|
|
|
Indicates whether to show selected tags inside the input. |
|
| timeToChange | |
Type : number
|
|
Default value : 800
|
|
|
The time interval for input timeout in ms. |
|
| tooltipColor | |
Type : string
|
|
Default value : 'inherit'
|
|
|
Tooltip color for the multi-tag select input. |
|
| tooltipIcon | |
Type : string
|
|
Default value : ''
|
|
|
Tooltip icon for the multi-tag select input. |
|
| tooltipMessage | |
Type : string
|
|
Default value : ''
|
|
|
Tooltip message for the multi-tag select input. |
|
| tooltipPlacement | |
Type : "center top" | "center bottom" | "left center" | "right center"
|
|
Default value : 'right center'
|
|
|
Tooltip placement for the multi-tag select input. |
|
| useFormErrors | |
Type : boolean
|
|
Default value : true
|
|
|
Indicates whether to use form errors for validation. |
|
| value | |
Type : OptionMulti[]
|
|
Default value : []
|
|
|
The selected values for the multi-tag select input. |
|
| closeChange | |
Type : EventEmitter<boolean>
|
|
|
Event emitted when the multi-tag select dropdown is closed. |
|
| removeTag | |
Type : EventEmitter<OptionMulti>
|
|
|
Event emitted when a tag is removed. |
|
| searchChange | |
Type : EventEmitter<string>
|
|
|
Event emitted when the search input value changes. |
|
| valid | |
Type : EventEmitter<boolean>
|
|
|
Event emitted when the multi-tag select input becomes valid or invalid. |
|
| valueChange | |
Type : EventEmitter<Array<OptionMulti>>
|
|
|
Event emitted when the selected values change. |
|
| closeDropdown |
closeDropdown()
|
|
Closes the multi-tag select dropdown.
Returns :
void
|
| Async doDropDownAction |
doDropDownAction()
|
|
Do action to open or close thw dropdown list
Returns :
any
|
| emit | ||||||||||||
emit(object: OptionMulti, checked: boolean)
|
||||||||||||
|
Emits a change event with the specified object and checked state.
Parameters :
Returns :
void
|
| handleCollapse | ||||||||
handleCollapse(item: OptionMulti)
|
||||||||
|
Handles the collapse of an item and set the cdkItemSize if the item is open.
Parameters :
Returns :
void
|
| Async modelChange | ||||||
modelChange(event: any)
|
||||||
|
Change searchtext with timeout and detect detectChanges
Parameters :
Returns :
any
|
| Async ngOnChanges | ||||||||
ngOnChanges(changes: SimpleChanges)
|
||||||||
|
Lifecycle hook called when any input properties change.
Parameters :
Returns :
any
|
| removeItem | ||||||||||||
removeItem(item: OptionMulti, event: any)
|
||||||||||||
|
Removes an item from the selected values.
Parameters :
Returns :
void
|
| Async setError | |||||||||||||||
setError(key: string, interpolateParams: Object)
|
|||||||||||||||
|
Sets an error message.
Parameters :
Returns :
any
|
| validate |
validate()
|
|
Validates the multi-tag select input and sets the error state.
Returns :
void
|
| verifyNewOptions |
verifyNewOptions()
|
|
Verify new options and set the cdkVirtualScrollViewportHeight
Returns :
void
|
| _options |
Type : Array<OptionMulti>
|
Default value : []
|
|
Control options to render |
| cdkItemSize |
Type : string | null
|
Default value : '32'
|
|
The size for the cdk-virtual-scroll-viewport (default 32px). |
| cdkVirtualScrollViewportHeight |
Type : string
|
Default value : '305px'
|
|
The height for the cdk-virtual-scroll-viewport (default 305px). |
| Public element |
Type : ElementRef
|
|
- The element reference.
|
| error |
Type : boolean | string
|
Default value : ''
|
|
Error message associated with the multi-tag select input. |
| isMaxTags |
Default value : false
|
|
Control the readonly on reach the maxTags |
| labelTemplate |
Type : TemplateRef<HTMLElement> | null
|
Default value : null
|
Decorators :
@ContentChild('labelTemplate')
|
|
The label template for the search-based select input. |
| nativeElement |
Type : ElementRef
|
|
Native element reference. |
| open |
Default value : false
|
|
Indicates whether the multi-tag select dropdown is open. |
| renderOptionsList |
Default value : false
|
|
Indicates when is the time to render the multi-tag select dropdown. |
| searchText |
Type : string
|
Default value : ''
|
|
Text entered in the search input field. |
| selectEmptyTemplate |
Type : TemplateRef<HTMLElement> | null
|
Default value : null
|
Decorators :
@ContentChild('selectEmptyTemplate')
|
|
The select empty template for the search-based select input. |
| timeouted |
Default value : false
|
|
Indicates whether a timeout has occurred for input changes. |
| timeoutInput |
Type : ReturnType<>
|
|
Timeout for input changes. |
| trackByOptValue |
Type : TrackByFunction<any>
|
Default value : useMemo((index, opt) => opt.value)
|
|
Return trackBy for ngFor |
| valueChanged |
Default value : false
|
|
Indicates whether the value has changed. |
| verifyIfHasChildrenInValue | ||||
Default value : useMemo((item: OptionMulti, value?: Array<OptionMulti>) => {
if (item.children?.length) {
const hasAllChildren = item.children.every(child => this.findItemInValue(child, value));
if (hasAllChildren && !this.findItemInValue(item, value) && !this.timeouted) {
this.timeouted = true;
setTimeout(() => {
this.emit(item, true);
this.timeouted = false;
}, 0);
}
return !!item.children.find(child => this.findItemInValue(child, value));
}
return false;
})
|
||||
|
Verifies if an item has children that are selected. |
||||
|
Parameters :
|
| verifyIfOptionsHasChildren | ||||
Default value : useMemo((options: OptionMulti[]) => {
return options.some(item => item.children?.length);
})
|
||||
|
Verifies if any options have children. |
||||
|
Parameters :
|
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild,
ElementRef,
EventEmitter,
Input,
OnChanges,
Optional,
Output,
SimpleChanges,
TemplateRef,
TrackByFunction,
} from '@angular/core';
import { NgClass, NgStyle, NgTemplateOutlet, AsyncPipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { TranslateService } from '@ngx-translate/core';
import { useMemo } from '../../helpers/memo.helper';
import { OptionMulti } from '../../interfaces/option.interface';
import { SqLoaderComponent } from '../sq-loader/sq-loader.component';
import { SqTooltipComponent } from '../sq-tooltip/sq-tooltip.component';
import { SqTagComponent } from '../sq-tag/sq-tag.component';
import { SqSelectorComponent } from '../sq-selector/sq-selector.component';
import { UniversalSafePipe } from '../../pipes/universal-safe/universal-safe.pipe';
import { TranslateInternalPipe } from '../../pipes/translate-internal/translate-internal.pipe';
import { SearchPipe } from '../../pipes/search/search.pipe';
import { SearchValidValuesPipe } from '../../pipes/search-valid-values/search-valid-values.pipe';
import { SqClickOutsideDirective } from '../../directives/sq-click-outside/sq-click-outside.directive';
import { SqDataTestDirective } from '../../directives/sq-data-test/sq-data-test.directive';
/**
* Represents a multi-tag select component.
*
* @example
* <sq-select-multi-tags
* [name]="'tags'"
* [value]="selectedTags"
* [options]="tagOptions"
* (valueChange)="handleTagSelection($event)"
* >
* </sq-select-multi-tags>
*
* @implements {OnChanges}
*/
@Component({
selector: 'sq-select-multi-tags',
templateUrl: './sq-select-multi-tags.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['./sq-select-multi-tags.component.scss'],
standalone: true,
imports: [
NgClass,
NgStyle,
NgTemplateOutlet,
AsyncPipe,
FormsModule,
ScrollingModule,
SqLoaderComponent,
SqTooltipComponent,
SqTagComponent,
SqDataTestDirective,
SqSelectorComponent,
UniversalSafePipe,
TranslateInternalPipe,
SearchPipe,
SearchValidValuesPipe,
SqClickOutsideDirective,
],
providers: [],
})
export class SqSelectMultiTagsComponent implements OnChanges {
/**
* The name attribute for the multi-tag select input.
*
* @default 'random-name-[hash-random-code]'
*/
@Input() name = `random-name-${(1 + Date.now() + Math.random()).toString().replace('.', '')}`;
/**
* The selected values for the multi-tag select input.
*/
@Input() value?: OptionMulti[] = [];
/**
* The id attribute for the multi-tag select input.
*/
@Input() id?: string;
/**
* The data-test attribute value for the select handle element.
*
* @default 'select-multi-tags'
*/
@Input() selectHandleDataTest = 'select-multi-tags';
/**
* The data-test attribute value for the select input element.
*
* @default 'input-multi-tags'
*/
@Input() selectInputDataTest = 'input-multi-tags';
/**
* The data-test attribute value for the select option elements.
*
* @default 'option-multi-tags'
*/
@Input() SelectOptionDataTest = 'option-multi-tags';
/**
* The label for the multi-tag select input.
*/
@Input() label = '';
/**
* Custom CSS class for styling the component.
*/
@Input() customClass = '';
/**
* Placeholder text for the input field.
*/
@Input() placeholder = '';
/**
* External error message for the multi-tag select input.
*/
@Input() externalError = '';
/**
* External icon for the multi-tag select input.
*/
@Input() externalIcon = '';
/**
* Placeholder text for the search input field.
*/
@Input() placeholderSearch = '';
/**
* Indicates whether the multi-tag select input is disabled.
*/
@Input() disabled = false;
/**
* Indicates whether the multi-tag select input is readonly.
*/
@Input() readonly = false;
/**
* Indicates whether the multi-tag select input is required.
*/
@Input() required = false;
/**
* Indicates whether the multi-tag select input is in a loading state.
*/
@Input() loading = false;
/**
* Indicates whether to use form errors for validation.
*/
@Input() useFormErrors = true;
/**
* Indicates whether to display an error span.
*/
@Input() errorSpan = true;
/**
* Background color for the multi-tag select input.
*/
@Input() backgroundColor = '';
/**
* Border color for the multi-tag select input.
*/
@Input() borderColor = '';
/**
* Color for the label of the multi-tag select input.
*/
@Input() labelColor = '';
/**
* Maximum height for the multi-tag values.
*/
@Input() maxHeight = '100%';
/**
* Minimum number of characters to perform the searchChange.
*/
@Input() minCharactersToSearch = 0;
/**
* The time interval for input timeout in ms.
*/
@Input() timeToChange = 800;
/**
* Options available for selection.
*/
@Input() options: Array<OptionMulti> = [];
/**
* Maximum number of tags that can be chosen.
*/
@Input() maxTags?: number;
/**
* Minimum number of tags that can be chosen.
*/
@Input() minTags?: number;
/**
* Indicates whether to show selected tags inside the input.
*/
@Input() showInside = true;
/**
* Indicates whether to hide the search input.
*/
@Input() hideSearch = false;
/**
* Tooltip message for the multi-tag select input.
*/
@Input() tooltipMessage = '';
/**
* Tooltip placement for the multi-tag select input.
*/
@Input() tooltipPlacement: 'center top' | 'center bottom' | 'left center' | 'right center' = 'right center';
/**
* Tooltip color for the multi-tag select input.
*/
@Input() tooltipColor = 'inherit';
/**
* Tooltip icon for the multi-tag select input.
*/
@Input() tooltipIcon = '';
/**
* Event emitted when the selected values change.
*/
@Output() valueChange: EventEmitter<Array<OptionMulti>> = new EventEmitter();
/**
* Event emitted when the search input value changes.
*/
@Output() searchChange: EventEmitter<string> = new EventEmitter();
/**
* Event emitted when the multi-tag select dropdown is closed.
*/
@Output() closeChange: EventEmitter<boolean> = new EventEmitter();
/**
* Event emitted when a tag is removed.
*/
@Output() removeTag: EventEmitter<OptionMulti> = new EventEmitter();
/**
* Event emitted when the multi-tag select input becomes valid or invalid.
*/
@Output() valid: EventEmitter<boolean> = new EventEmitter();
/**
* The label template for the search-based select input.
*/
@ContentChild('labelTemplate')
labelTemplate: TemplateRef<HTMLElement> | null = null;
/**
* The select empty template for the search-based select input.
*/
@ContentChild('selectEmptyTemplate')
selectEmptyTemplate: TemplateRef<HTMLElement> | null = null;
/**
* Indicates when is the time to render the multi-tag select dropdown.
*/
renderOptionsList = false;
/**
* Indicates whether the multi-tag select dropdown is open.
*/
open = false;
/**
* Text entered in the search input field.
*/
searchText = '';
/**
* Indicates whether the value has changed.
*/
valueChanged = false;
/**
* Indicates whether a timeout has occurred for input changes.
*/
timeouted = false;
/**
* Error message associated with the multi-tag select input.
*/
error: boolean | string = '';
/**
* Native element reference.
*/
nativeElement: ElementRef;
/**
* Control options to render
*/
_options: Array<OptionMulti> = [];
/**
* Control the readonly on reach the maxTags
*/
isMaxTags = false;
/**
* Timeout for input changes.
*/
timeoutInput!: ReturnType<typeof setTimeout>;
/**
* The height for the cdk-virtual-scroll-viewport (default 305px).
*/
cdkVirtualScrollViewportHeight = '305px';
/**
* The size for the cdk-virtual-scroll-viewport (default 32px).
*/
cdkItemSize: string | null = '32';
/**
* Constructs a new SqSelectMultiTagsComponent.
*
* @param {ElementRef} element - The element reference.
* @param {TranslateService} translate - The optional TranslateService for internationalization.
* @param {ChangeDetectorRef} changeDetector - Base class that provides change detection functionality.
*/
constructor(
public element: ElementRef,
@Optional() private translate: TranslateService,
private changeDetector: ChangeDetectorRef
) {
this.nativeElement = element.nativeElement;
}
/**
* Lifecycle hook called when any input properties change.
*
* @param changes - The changes detected in the component's input properties.
*/
async ngOnChanges(changes: SimpleChanges) {
if (this.open && changes.hasOwnProperty('options')) {
this.verifyNewOptions();
}
if (changes.hasOwnProperty('value') || changes.hasOwnProperty('minTags') || changes.hasOwnProperty('maxTags')) {
this.validate();
}
}
/**
* Determines if an item exists in the selected values.
*
* @param {OptionMulti} item - The item to search for.
* @returns {boolean} True if the item exists in the selected values; otherwise, false.
*/
findItemInValue = useMemo((item: OptionMulti, value?: Array<OptionMulti>) => {
return !!value?.find(value => value.value === item.value);
});
/**
* Verifies if any options have children.
*
* @param {OptionMulti[]} options - The options to check.
* @returns {boolean} True if any option has children; otherwise, false.
*/
verifyIfOptionsHasChildren = useMemo((options: OptionMulti[]) => {
return options.some(item => item.children?.length);
});
/**
* Verifies if an item has children that are selected.
*
* @param {OptionMulti} item - The item to check.
* @returns {boolean} True if the item has selected children; otherwise, false.
*/
verifyIfHasChildrenInValue = useMemo((item: OptionMulti, value?: Array<OptionMulti>) => {
if (item.children?.length) {
const hasAllChildren = item.children.every(child => this.findItemInValue(child, value));
if (hasAllChildren && !this.findItemInValue(item, value) && !this.timeouted) {
this.timeouted = true;
setTimeout(() => {
this.emit(item, true);
this.timeouted = false;
}, 0);
}
return !!item.children.find(child => this.findItemInValue(child, value));
}
return false;
});
/**
* Removes an item from the selected values.
*
* @param {OptionMulti} item - The item to remove.
*/
removeItem(item: OptionMulti, event: any) {
event?.stopPropagation();
if (!this.readonly && !this.disabled) {
if (item.children?.length) {
item.children.forEach(child => {
this.value = this.value?.filter(value => value.value !== child.value);
});
}
this.value = this.value?.filter(value => value.value !== item.value);
this.valueChange.emit(this.value);
this.removeTag.emit(item);
this.validate();
}
}
/**
* Emits a change event with the specified object and checked state.
*
* @param {OptionMulti} object - The object to emit.
* @param {boolean} checked - The checked state.
*/
emit(object: OptionMulti, checked: boolean) {
if (checked) {
this.value?.push(object);
// This code adds all children of a parent to value. Commented out for now as it is not the desired behavior.
// if (object.children?.length) {
// object.children.forEach((item) => {
// if (!this.value.find((child) => child.value === item.value)) {
// this.value.push(item)
// }
// })
// }
} else {
this.value = this.value?.filter(item => item.value !== object.value);
if (object.children?.length) {
object.children.forEach(item => {
this.value = this.value?.filter(child => child.value !== item.value);
});
}
}
this.valueChanged = true;
this.valueChange.emit(this.value);
this.validate();
}
/**
* Do action to open or close thw dropdown list
*/
async doDropDownAction() {
if (this.open) {
this.closeDropdown();
this.renderOptionsList = await new Promise<boolean>(resolve =>
setTimeout(() => {
resolve(false);
}, 300)
);
this.changeDetector.detectChanges();
} else {
this.verifyNewOptions();
this.renderOptionsList = true;
this.open = await new Promise<boolean>(resolve =>
setTimeout(() => {
resolve(true);
}, 100)
);
this.changeDetector.detectChanges();
}
}
/**
* Closes the multi-tag select dropdown.
*/
closeDropdown() {
this.open = false;
this._options = [];
this.searchText = '';
this.closeChange.emit(this.valueChanged);
this.valueChanged = false;
}
/**
* Handles the collapse of an item and set the cdkItemSize if the item is open.
*
* @param {OptionMulti} item - The item to collapse.
*/
handleCollapse(item: OptionMulti) {
item.open = !item.open;
if (item.children) {
if (this.options.find(option => option.open) || item.open) {
this.cdkItemSize = null;
} else {
this.cdkItemSize = '32';
}
}
}
/**
* Sets an error message.
*
* @param {string} key - The translation key for the error message.
*/
async setError(key: string, interpolateParams: Object = {}) {
if (this.useFormErrors && this.translate) {
this.error = await this.translate.instant(key, interpolateParams);
}
}
/**
* Validates the multi-tag select input and sets the error state.
*/
validate() {
if (this.externalError) {
this.error = false;
} else if (this.required && !this.value?.length) {
this.setError('forms.required');
this.valid.emit(false);
} else if (this.minTags && this.value && this.value?.length < this.minTags) {
this.setError('forms.minimumRequired', { minTags: this.minTags });
this.valid.emit(false);
} else if (this.maxTags && this.value && this.value?.length === this.maxTags) {
this.renderOptionsList = false;
this.isMaxTags = true;
this.error = '';
this.valid.emit(true);
} else {
this.isMaxTags = false;
this.error = '';
this.valid.emit(true);
}
}
/**
* Return trackBy for ngFor
*/
trackByOptValue: TrackByFunction<any> = useMemo((index, opt) => opt.value);
/**
* Change searchtext with timeout and detect detectChanges
*/
async modelChange(event: any) {
if (!this.minCharactersToSearch || !event.length || event.length >= this.minCharactersToSearch) {
clearTimeout(this.timeoutInput);
this.searchText =
(await new Promise<string>(
resolve =>
(this.timeoutInput = setTimeout(() => {
resolve(event);
}, this.timeToChange))
)) || '';
this.searchChange.emit(event);
this.changeDetector.detectChanges();
}
}
/**
* Verify new options and set the cdkVirtualScrollViewportHeight
*/
verifyNewOptions() {
this._options = this.options;
if (!this._options.length) {
this.cdkVirtualScrollViewportHeight = '16px';
} else if (this._options.length < 15) {
this.cdkVirtualScrollViewportHeight = this._options.length * 32 + 'px';
} else {
this.cdkVirtualScrollViewportHeight = '305px';
}
}
}
<div class="wrapper-all-inside-input {{ customClass }}">
@if (label?.length || labelTemplate || tooltipMessage) {
<label
class="display-flex align-items-center"
[ngClass]="{
readonly: readonly || isMaxTags,
}"
[for]="id"
>
@if (label && !labelTemplate) {
<div [ngStyle]="{ color: labelColor }" [innerHtml]="label | universalSafe"></div>
}
@if (labelTemplate) {
<span>
<ng-container *ngTemplateOutlet="labelTemplate"></ng-container>
</span>
}
@if (tooltipMessage) {
<sq-tooltip
class="ml-1"
[message]="tooltipMessage"
[placement]="tooltipPlacement"
[color]="tooltipColor"
[icon]="tooltipIcon"
></sq-tooltip>
}
</label>
}
<div
class="wrapper-select-multi"
[ngClass]="{
error: (externalError && externalError !== '') || (error && error !== ''),
disabled: disabled,
readonly: readonly || isMaxTags,
loading: loading,
}"
>
<div
[dataTest]="selectHandleDataTest"
[class]="'input-fake col border-' + borderColor"
style="min-height: auto"
[ngStyle]="{ 'border-color': borderColor }"
[ngClass]="{
'no-label': !(label && label.length > 0),
'has-icon': error || externalError,
disabled: disabled,
readonly: readonly || isMaxTags,
}"
[ngStyle]="{
'background-color': backgroundColor,
'border-color': borderColor,
}"
[clickOutsideEnabled]="open"
(clickOutside)="closeDropdown()"
>
<div
class="input-fake-content"
[ngClass]="{
disabled: disabled,
readonly: readonly,
}"
[style.maxHeight]="maxHeight"
(click)="doDropDownAction()"
>
@if (loading) {
<div class="loading-wrapper">
<sq-loader></sq-loader>
</div>
}
@if (!value?.length || !showInside) {
<span>{{ placeholder }}</span>
}
@if (showInside && value?.length) {
<div class="input-fake-content-text">
@for (opt of value; track opt; let i = $index) {
<span class="tag" (click)="removeItem(opt, $event)">
{{ opt?.label }}
@if (!readonly) {
<i class="fas fa-times" [style.minWidth]="'auto'"></i>
}
</span>
}
</div>
}
@if (value?.length) {
<span class="badge">{{ value!.length }}</span>
}
@if (!loading) {
<i class="icon-down fas fa-chevron-down"></i>
}
</div>
@if (!loading && !disabled && !readonly && !isMaxTags && renderOptionsList) {
<div
id="sq-select-multi-tags-scroll"
class="input-window scrollbar"
[ngClass]="{
open: !loading && !disabled && !isMaxTags && renderOptionsList && open,
}"
>
<div class="input-search">
<div class="wrapper-all-inside-input">
<div class="p-0 wrapper-input wrapper-input-squid text-ellipsisarea">
<input
[dataTest]="selectInputDataTest"
[name]="name"
[id]="id"
[placeholder]="placeholderSearch || ('forms.search' | translateInternal | async) || ''"
class="col input"
[ngModel]="searchText"
(ngModelChange)="modelChange($event)"
/>
</div>
<span class="icon icon-external textarea-icon"><i class="fas fa-search"></i></span>
</div>
</div>
<cdk-virtual-scroll-viewport
[itemSize]="cdkItemSize"
[ngStyle]="{ height: cdkVirtualScrollViewportHeight }"
class="list scrollbar"
>
<ng-container
*cdkVirtualFor="let opt of _options | search: searchText; i as index; trackBy: trackByOptValue"
>
<ng-template *ngTemplateOutlet="option; context: { opt: opt, i: index }"></ng-template>
</ng-container>
</cdk-virtual-scroll-viewport>
@if (!_options?.length) {
@if (!selectEmptyTemplate) {
<p class="mb-0 mt-3">
{{ 'forms.searchSelectEmpty' | translateInternal | async }}
</p>
}
@if (selectEmptyTemplate) {
<span>
<ng-container *ngTemplateOutlet="selectEmptyTemplate"></ng-container>
</span>
}
}
</div>
}
</div>
@if (errorSpan) {
<div
class="box-validation box-invalid show"
[ngClass]="{
'visibility-hidden-force':
((!externalError || externalError === '') && (!error || error === '')) || disabled || readonly,
}"
>
<i
[ngClass]="{
'visibility-hidden-force': !error && !externalError,
}"
class="fa-solid fa-triangle-exclamation"
></i>
{{ externalError ? externalError : '' }}
{{ error && !externalError ? error : '' }}
</div>
}
<ng-template #option let-opt="opt" let-i="i">
<li>
<div class="label">
<i
class="icon-collapse fas fa-chevron-down"
[ngClass]="{ 'fa-rotate-by': !opt.open }"
[ngStyle]="{ color: !opt?.children?.length || opt?.disabled ? 'transparent' : '' }"
(click)="handleCollapse(opt)"
style="--fa-rotate-angle: -90deg"
></i>
<sq-selector
[dataTest]="SelectOptionDataTest + '-' + (i || opt.value)"
[style.minWidth]="'auto'"
[id]="(id || name) + '-checkbox-' + opt?.value + '-' + i"
[name]="name + '-checkbox'"
[value]="opt?.value"
[disabled]="opt?.disabled"
[checked]="findItemInValue(opt, value)"
[indeterminate]="verifyIfHasChildrenInValue(opt, value)"
(valueChange)="emit(opt, $event.checked)"
></sq-selector>
<span
class="text m-0 display-inline-block"
[ngClass]="{ 'cursor-pointer': opt?.children?.length }"
(click)="handleCollapse(opt)"
>{{ opt?.label }}</span
>
</div>
@if (opt?.children?.length) {
<ul class="children" [ngClass]="{ open: !opt?.disabled && opt?.open }">
@for (
child of opt?.children | searchValidValues: searchText;
track trackByOptValue(j, child);
let j = $index
) {
<ng-template *ngTemplateOutlet="option; context: { opt: child, i: j }"></ng-template>
}
</ul>
}
</li>
</ng-template>
</div>
</div>
./sq-select-multi-tags.component.scss
.wrapper-select-multi {
margin: 0;
padding: 0;
text-align: left;
display: flex;
flex-direction: column;
position: relative;
&.loading {
cursor: wait;
}
.input-fake-content-text {
display: flex;
justify-content: flex-start;
align-items: center;
flex-wrap: wrap;
gap: 5px;
max-width: calc(100% - 40px);
min-height: 51px;
.tag {
margin: 0;
border: 1px solid var(--border_color);
border-radius: 5px;
padding: 0.2rem 0.35rem;
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
transition: var(--transition);
background: var(--background_secondary);
&:hover {
background: var(--border_color);
}
}
}
.input-fake-content {
min-height: 44px;
border: 1px solid var(--border_color);
background: var(--background);
padding: 0.75rem 1rem;
transition: var(--transition);
color: var(--text_color);
border-radius: 5px;
overflow-x: auto;
.icon-down {
right: 20px;
bottom: calc(50% - 6px);
position: absolute;
font-size: 0.86rem;
}
&.readonly {
cursor: not-allowed;
pointer-events: none;
background-color: var(--color_border_input);
border-color: var(--color_border_input_disabled);
}
}
.loading-wrapper {
position: absolute;
right: 5px;
bottom: 10px;
}
.badge {
position: absolute;
bottom: 15px;
right: 20px;
margin: 0;
border-radius: 50%;
background-color: var(--border_color);
color: var(--black);
font-size: 0.86rem;
font-weight: 700;
padding: 0 0.35rem;
border-radius: 50%;
min-width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
transition: var(--transition);
}
.input-search {
display: inline-block;
position: sticky;
top: 0;
z-index: 1;
width: 100%;
padding-top: 1.1rem;
margin-bottom: 0.7rem;
background-color: var(--background_secondary);
.wrapper-all-inside-input {
.icon {
margin: 0;
font-size: 1rem;
font-weight: 700;
position: absolute;
right: 11px;
top: 27px;
}
.icon-external {
color: inherit !important;
}
}
}
.input-window {
width: 100%;
position: absolute;
top: 100%;
max-height: 0;
height: 0;
transition: var(--transition);
overflow: hidden;
z-index: 1;
&.open {
height: auto;
max-height: 400px;
background: var(--background_secondary);
padding: 0 0.7rem 1.1rem;
overflow-y: auto;
box-shadow: var(--box_shadow);
}
}
.icon-collapse {
transition: var(--transition);
cursor: pointer;
padding: 0.35rem;
position: relative;
}
.list,
.children {
padding: 0;
list-style: none;
margin: 0;
width: 100%;
li {
display: grid;
.label {
margin: 0 0 0.5rem;
display: flex;
flex-wrap: nowrap;
align-items: baseline;
justify-content: flex-start;
transition: var(--transition);
}
}
}
.children {
max-height: 0;
overflow: hidden;
transition: var(--transition);
padding-left: 1.4rem;
&.open {
max-height: 9999px;
}
}
}