diff --git a/src/app/components/listbox/listbox.interface.ts b/src/app/components/listbox/listbox.interface.ts index 506c9286a74..25583d7011b 100644 --- a/src/app/components/listbox/listbox.interface.ts +++ b/src/app/components/listbox/listbox.interface.ts @@ -30,6 +30,21 @@ export interface ListboxChangeEvent { */ value: any; } +/** + * Custom change event. + * @see {@link Listbox.onSelectAllChange} + * @group Events + */ +export interface ListboxSelectAllChangeEvent { + /** + * Browser event. + */ + originalEvent: Event; + /** + * Boolean value indicates whether all data is selected. + */ + checked: boolean; +} /** * Custom filter event. * @see {@link Listbox.onFilter} diff --git a/src/app/components/listbox/listbox.ts b/src/app/components/listbox/listbox.ts index 5e96cb38e85..cc4f27e39a4 100755 --- a/src/app/components/listbox/listbox.ts +++ b/src/app/components/listbox/listbox.ts @@ -31,7 +31,7 @@ import { Subscription } from 'rxjs'; import { SearchIcon } from 'primeng/icons/search'; import { CheckIcon } from 'primeng/icons/check'; import { Nullable } from 'primeng/ts-helpers'; -import { ListboxChangeEvent, ListboxClickEvent, ListboxDoubleClickEvent, ListboxFilterEvent, ListboxFilterOptions } from './listbox.interface'; +import { ListboxChangeEvent, ListboxClickEvent, ListboxDoubleClickEvent, ListboxFilterEvent, ListboxFilterOptions, ListboxSelectAllChangeEvent } from './listbox.interface'; import { Scroller, ScrollerModule } from 'primeng/scroller'; export const LISTBOX_VALUE_ACCESSOR: any = { @@ -473,6 +473,16 @@ export class Listbox implements AfterContentInit, OnInit, ControlValueAccessor, set filterValue(val: string) { this._filterValue.set(val); } + /** + * Whether all data is selected. + * @group Props + */ + @Input() get selectAll(): boolean | undefined | null { + return this._selectAll; + } + set selectAll(value: boolean | undefined | null) { + this._selectAll = value; + } /** * Callback to invoke on value change. * @param {ListboxChangeEvent} event - Custom change event. @@ -509,6 +519,12 @@ export class Listbox implements AfterContentInit, OnInit, ControlValueAccessor, * @group Emits */ @Output() onBlur: EventEmitter = new EventEmitter(); + /** + * Callback to invoke when all data is selected. + * @param {ListboxSelectAllChangeEvent} event - Custom select event. + * @group Emits + */ + @Output() onSelectAllChange: EventEmitter = new EventEmitter(); @ViewChild('headerchkbox') headerCheckboxViewChild: Nullable; @@ -630,6 +646,8 @@ export class Listbox implements AfterContentInit, OnInit, ControlValueAccessor, searchTimeout: any; + _selectAll: boolean | undefined | null = null; + _options = signal(null); startRangeIndex = signal(-1); @@ -745,8 +763,11 @@ export class Listbox implements AfterContentInit, OnInit, ControlValueAccessor, this.onOptionSelect(null, this.visibleOptions()[this.focusedOptionIndex()]); } } - - updateModel(value, event?) { + /** + * Updates the model value. + * @group Method + */ + public updateModel(value, event?) { this.value = value; this.modelValue.set(value); this.onModelChange(value); @@ -841,20 +862,28 @@ export class Listbox implements AfterContentInit, OnInit, ControlValueAccessor, } DomHandler.focus(this.headerCheckboxViewChild.nativeElement); - const value = this.allSelected() - ? [] - : this.visibleOptions() - .filter((option) => this.isValidOption(option)) - .map((option) => this.getOptionValue(option)); - this.updateModel(value, event); + if(this.selectAll !== null) { + this.onSelectAllChange.emit({ + originalEvent: event, + checked: !this.allSelected() + }) + } else { + const value = this.allSelected() + ? [] + : this.visibleOptions() + .filter((option) => this.isValidOption(option)) + .map((option) => this.getOptionValue(option)); + + this.updateModel(value, event); + this.onChange.emit({ originalEvent: event, value: this.value }); + } event.preventDefault(); - event.stopPropagation(); + // event.stopPropagation(); } allSelected() { - const allSelected = this.visibleOptions().length > 0 && this.visibleOptions().every((option) => this.isOptionGroup(option) || this.isOptionDisabled(option) || this.isSelected(option)); - return ObjectUtils.isNotEmpty(this.visibleOptions()) && allSelected; + return this.selectAll !== null ? this.selectAll : ObjectUtils.isNotEmpty(this.visibleOptions()) && this.visibleOptions().every((option) => this.isOptionGroup(option) || this.isOptionDisabled(option) || this.isSelected(option)); } onOptionTouchEnd() { diff --git a/src/app/components/multiselect/multiselect.interface.ts b/src/app/components/multiselect/multiselect.interface.ts index b410c752634..4721c38be4e 100644 --- a/src/app/components/multiselect/multiselect.interface.ts +++ b/src/app/components/multiselect/multiselect.interface.ts @@ -28,6 +28,21 @@ export interface MultiSelectChangeEvent { */ itemValue?: any; } +/** + * Custom change event. + * @see {@link MultiSelect.onSelectAllChange} + * @group Events + */ +export interface MultiSelectSelectAllChangeEvent { + /** + * Browser event. + */ + originalEvent: Event; + /** + * Boolean value indicates whether all data is selected. + */ + checked: boolean; +} /** * Custom filter event. * @see {@link MultiSelect.onFilter} diff --git a/src/app/components/multiselect/multiselect.ts b/src/app/components/multiselect/multiselect.ts index a2bc8809e7b..6b8f3b04c10 100755 --- a/src/app/components/multiselect/multiselect.ts +++ b/src/app/components/multiselect/multiselect.ts @@ -22,6 +22,7 @@ import { QueryList, Renderer2, signal, + SimpleChanges, TemplateRef, ViewChild, ViewEncapsulation @@ -41,7 +42,7 @@ import { TimesCircleIcon } from 'primeng/icons/timescircle'; import { TimesIcon } from 'primeng/icons/times'; import { ChevronDownIcon } from 'primeng/icons/chevrondown'; import { Nullable } from 'primeng/ts-helpers'; -import { MultiSelectRemoveEvent, MultiSelectFilterOptions, MultiSelectFilterEvent, MultiSelectBlurEvent, MultiSelectChangeEvent, MultiSelectFocusEvent, MultiSelectLazyLoadEvent } from './multiselect.interface'; +import { MultiSelectRemoveEvent, MultiSelectFilterOptions, MultiSelectFilterEvent, MultiSelectBlurEvent, MultiSelectChangeEvent, MultiSelectFocusEvent, MultiSelectLazyLoadEvent, MultiSelectSelectAllChangeEvent } from './multiselect.interface'; export const MULTISELECT_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, @@ -251,7 +252,7 @@ export class MultiSelectItem { - + @@ -268,6 +269,7 @@ export class MultiSelectItem { [value]="_filterValue() || ''" (input)="onFilterInputChange($event)" (keydown)="onFilterKeyDown($event)" + (click)="onInputClick($event)" (blur)="onFilterBlur($event)" class="p-multiselect-filter p-inputtext p-component" [disabled]="disabled" @@ -758,6 +760,16 @@ export class MultiSelect implements OnInit, AfterViewInit, AfterContentInit, Aft this._itemSize = val; console.warn('The itemSize property is deprecated, use virtualScrollItemSize property instead.'); } + /** + * Whether all data is selected. + * @group Props + */ + @Input() get selectAll(): boolean | undefined | null { + return this._selectAll; + } + set selectAll(value: boolean | undefined | null) { + this._selectAll = value; + } /** * Fields used when filtering the options, defaults to optionLabel. * @group Props @@ -835,6 +847,12 @@ export class MultiSelect implements OnInit, AfterViewInit, AfterContentInit, Aft * @group Emits */ @Output() onRemove: EventEmitter = new EventEmitter(); + /** + * Callback to invoke when all data is selected. + * @param {MultiSelectSelectAllChangeEvent} event - Custom select event. + * @group Emits + */ + @Output() onSelectAllChange: EventEmitter = new EventEmitter(); @ViewChild('container') containerViewChild: Nullable; @@ -864,6 +882,8 @@ export class MultiSelect implements OnInit, AfterViewInit, AfterContentInit, Aft searchTimeout: any; + _selectAll: boolean | undefined | null = null; + _autoZIndex: boolean | undefined; _baseZIndex: number | undefined; @@ -880,7 +900,7 @@ export class MultiSelect implements OnInit, AfterViewInit, AfterContentInit, Aft _selectionLimit: number | undefined; - public value: any[] | undefined | null; + value: any[]; public _filteredOptions: any[] | undefined | null; @@ -1066,7 +1086,7 @@ export class MultiSelect implements OnInit, AfterViewInit, AfterContentInit, Aft return ObjectUtils.isNotEmpty(this.maxSelectedLabels) && this.modelValue() && this.modelValue().length > this.maxSelectedLabels ? this.modelValue().slice(0, this.maxSelectedLabels) : this.modelValue(); }); - constructor(public el: ElementRef, public renderer: Renderer2, public cd: ChangeDetectorRef, public zone: NgZone, public filterService: FilterService, public config: PrimeNGConfig, public overlayService: OverlayService) {} + constructor( public el: ElementRef, public renderer: Renderer2, public cd: ChangeDetectorRef, public zone: NgZone, public filterService: FilterService, public config: PrimeNGConfig, public overlayService: OverlayService) {} ngOnInit() { this.id = this.id || UniqueComponentId(); @@ -1187,12 +1207,22 @@ export class MultiSelect implements OnInit, AfterViewInit, AfterContentInit, Aft } } - updateModel(value, event?) { + /** + * Updates the model value. + * @group Method + */ + public updateModel(value, event?) { this.value = value; this.onModelChange(value); this.modelValue.set(value); } + onInputClick(event) { + event.stopPropagation(); + event.preventDefault(); + this.focusedOptionIndex.set(-1); + } + onOptionSelect(event, isFocus = false, index = -1) { const { originalEvent, option } = event; if (this.disabled || this.isOptionDisabled(option)) { @@ -1314,7 +1344,6 @@ export class MultiSelect implements OnInit, AfterViewInit, AfterContentInit, Aft isSelected(option) { const optionValue = this.getOptionValue(option); - return (this.modelValue() || []).some((value) => ObjectUtils.equals(value, optionValue, this.equalityKey())); } @@ -1648,7 +1677,7 @@ export class MultiSelect implements OnInit, AfterViewInit, AfterContentInit, Aft } this.focusInputViewChild?.nativeElement.focus({ preventScroll: true }); this.onClick.emit(event); - this.cd.detectChanges(); + this.cd.detectChanges() } onFirstHiddenFocus(event) { @@ -1738,15 +1767,22 @@ export class MultiSelect implements OnInit, AfterViewInit, AfterContentInit, Aft return; } - DomHandler.focus(this.headerCheckboxViewChild.nativeElement); - - const value = this.allSelected() + if(this.selectAll !== null) { + this.onSelectAllChange.emit({ + originalEvent: event, + checked: !this.allSelected() + }) + } else { + const value = this.allSelected() ? [] : this.visibleOptions() .filter((option) => this.isValidOption(option)) .map((option) => this.getOptionValue(option)); - this.updateModel(value, event); - this.onChange.emit({ originalEvent: event, value: this.value }); + + this.updateModel(value, event) + } + + DomHandler.focus(this.headerCheckboxViewChild.nativeElement); this.headerCheckboxFocus = true; event.preventDefault(); @@ -1797,11 +1833,11 @@ export class MultiSelect implements OnInit, AfterViewInit, AfterContentInit, Aft this.cd.markForCheck(); } - registerOnChange(fn: Function): void { + public registerOnChange(fn: Function): void { this.onModelChange = fn; } - registerOnTouched(fn: Function): void { + public registerOnTouched(fn: Function): void { this.onModelTouched = fn; } @@ -1811,8 +1847,7 @@ export class MultiSelect implements OnInit, AfterViewInit, AfterContentInit, Aft } allSelected() { - const allSelected = this.visibleOptions().length > 0 && this.visibleOptions().every((option) => this.isOptionGroup(option) || this.isOptionDisabled(option) || this.isSelected(option)); - return ObjectUtils.isNotEmpty(this.visibleOptions()) && allSelected; + return this.selectAll !== null ? this.selectAll : ObjectUtils.isNotEmpty(this.visibleOptions()) && this.visibleOptions().every((option) => this.isOptionGroup(option) || this.isOptionDisabled(option) || this.isSelected(option)); } /** diff --git a/src/app/showcase/doc/listbox/virtualscrolldoc.ts b/src/app/showcase/doc/listbox/virtualscrolldoc.ts index a54b1f1dcfd..237de69f5db 100644 --- a/src/app/showcase/doc/listbox/virtualscrolldoc.ts +++ b/src/app/showcase/doc/listbox/virtualscrolldoc.ts @@ -12,9 +12,10 @@ import { Code } from '../../domain/code';
` }) -export class VirtualScrollDoc implements OnInit { +export class VirtualScrollDoc { @Input() id: string; @Input() title: string; - virtualItems!: any[]; + items = Array.from({ length: 100000 }, (_, i) => ({ label: `Item #${i}`, value: i })) selectedItems!: any[]; - ngOnInit() { - this.virtualItems = []; - for (let i = 0; i < 10000; i++) { - this.virtualItems.push({ name: 'Item ' + i, code: 'Item ' + i }); - } + selectAll: boolean = false; + + onSelectAllChange(event) { + this.selectedItems = event.checked ? [...this.items] : []; + this.selectAll = event.checked; + } + + onChange(event) { + const { value } = event + if(value) this.selectAll = value.length === this.items.length; } code: Code = { basic: ` -`, +`, html: `
- +
`, typescript: ` @@ -61,16 +104,22 @@ import { Component, OnInit } from '@angular/core'; selector: 'listbox-virtual-scroll-demo', templateUrl: './listbox-virtual-scroll-demo.html' }) -export class ListboxVirtualScrollDemo implements OnInit { - virtualItems!: any[]; +export class ListboxVirtualScrollDemo { + items = Array.from({ length: 100000 }, (_, i) => ({ label: \`Item #\${i}\`, value: i })) - selectedItems!: any; + selectedItems!: any[]; + + selectAll = false; + + onSelectAllChange(event) { + this.selectedItems = event.checked ? [...this.items] : []; + this.selectAll = event.checked; + event.updateModel(this.selectedItems, event.originalEvent) + } - ngOnInit() { - this.virtualItems = []; - for (let i = 0; i < 10000; i++) { - this.virtualItems.push({ name: 'Item ' + i, code: 'Item ' + i }); - } + onChange(event) { + const { originalEvent, value } = event + if(value) this.selectAll = value.length === this.items.length; } }` diff --git a/src/app/showcase/doc/multiselect/virtualscrolldoc.ts b/src/app/showcase/doc/multiselect/virtualscrolldoc.ts index 99b1b75051e..6156be455e8 100644 --- a/src/app/showcase/doc/multiselect/virtualscrolldoc.ts +++ b/src/app/showcase/doc/multiselect/virtualscrolldoc.ts @@ -12,15 +12,18 @@ import { Code } from '../../domain/code';
@@ -31,24 +34,55 @@ export class VirtualScrollDoc { @Input() title: string; - virtualItems!: any[]; + items = Array.from({ length: 100000 }, (_, i) => ({ label: `Item #${i}`, value: i })) selectedItems!: any[]; - constructor() { - this.virtualItems = []; - for (let i = 0; i < 10000; i++) { - this.virtualItems.push({ name: 'Item ' + i, code: 'Item ' + i }); - } + selectAll: boolean = false; + + onSelectAllChange(event) { + this.selectedItems = event.checked ? [...this.items] : []; + this.selectAll = event.checked; + } + + onChange(event) { + const { value } = event + if(value) this.selectAll = value.length === this.items.length; } code: Code = { basic: ` -`, +`, html: `
- +
`, typescript: ` @@ -59,17 +93,23 @@ import { Component } from '@angular/core'; templateUrl: './multi-select-virtual-scroll-demo.html' }) export class MultiSelectVirtualScrollDemo { - - virtualItems!: any[]; + items = Array.from({ length: 100000 }, (_, i) => ({ label: \`Item #\${i}\`, value: i })) selectedItems!: any[]; - constructor() { - this.virtualItems = []; - for (let i = 0; i < 10000; i++) { - this.virtualItems.push({ name: 'Item ' + i, code: 'Item ' + i }); - } + selectAll = false; + + onSelectAllChange(event) { + this.selectedItems = event.checked ? [...this.items] : []; + this.selectAll = event.checked; + event.updateModel(this.selectedItems, event.originalEvent) + } + + onChange(event) { + const { originalEvent, value } = event + if(value) this.selectAll = value.length === this.items.length; } + }` }; }