UNPKG

@progress/kendo-angular-grid

Version:

Kendo UI Grid for Angular - high performance data grid with paging, filtering, virtualization, CRUD, and more.

383 lines (382 loc) 18 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { Component, HostBinding, Input, ElementRef, NgZone, Renderer2, Output, EventEmitter, ViewChild, ViewChildren, QueryList } from '@angular/core'; import { ColumnMenuService } from './column-menu.service'; import { ColumnListKeyboardNavigation } from './column-list-kb-nav.service'; import { ColumnMenuChooserItemCheckedDirective } from './column-chooser-item-checked.directive'; import { Keys } from '@progress/kendo-angular-common'; import { Subscription } from 'rxjs'; import { NgFor, NgIf, NgClass } from '@angular/common'; import { CheckBoxComponent } from '@progress/kendo-angular-inputs'; import { take } from 'rxjs/operators'; import { arrowRotateCcwIcon, checkIcon } from '@progress/kendo-svg-icons'; import { ButtonDirective } from '@progress/kendo-angular-buttons'; import * as i0 from "@angular/core"; import * as i1 from "./column-list-kb-nav.service"; /** * @hidden */ export class ColumnListComponent { element; ngZone; renderer; listNavigationService; checkIcon = checkIcon; arrowRotateCcwIcon = arrowRotateCcwIcon; className = true; reset = new EventEmitter(); apply = new EventEmitter(); columnChange = new EventEmitter(); set columns(value) { this._columns = value.filter(column => column.includeInChooser !== false); this.allColumns = value; this.updateColumnState(); } get columns() { return this._columns; } autoSync = true; ariaLabel; allowHideAll = false; applyText; resetText; actionsClass = 'k-actions k-actions-stretched k-actions-horizontal'; isLast; isExpanded; service; resetButton; applyButton; options; checkboxes; hasLocked; hasVisibleLocked; unlockedCount = 0; hasUnlockedFiltered; hasFiltered; _columns; allColumns; domSubscriptions = new Subscription(); constructor(element, ngZone, renderer, listNavigationService) { this.element = element; this.ngZone = ngZone; this.renderer = renderer; this.listNavigationService = listNavigationService; } ngOnInit() { if (!this.element) { return; } this.ngZone.runOutsideAngular(() => { this.domSubscriptions.add(this.renderer.listen(this.element.nativeElement, 'click', (e) => { this.ngZone.onStable.pipe(take(1)).subscribe(() => { this.handleCheckBoxClick(e); }); })); this.domSubscriptions.add(this.renderer.listen(this.element.nativeElement, 'keydown', this.onKeydown)); }); } ngAfterViewInit() { this.ngZone.onStable.pipe(take(1)).subscribe(() => { this.listNavigationService.items = this.options.toArray(); this.listNavigationService.toggle(0, true); this.updateDisabled(); }); } ngOnChanges(changes) { if (!this.service) { return; } if (changes['isLast'] && this.isLast) { this.service.menuTabbingService.lastFocusable = this.applyButton.nativeElement; } if (changes['isExpanded'] && this.isExpanded && this.isLast && this.applyButton) { this.service.menuTabbingService.lastFocusable = this.applyButton.nativeElement; } } ngOnDestroy() { this.domSubscriptions.unsubscribe(); } isDisabled(column) { return !(this.allowHideAll || this.hasFiltered || column.hidden || this.columns.find(current => current !== column && !current.hidden)) || (this.hasVisibleLocked && !this.hasUnlockedFiltered && this.unlockedCount === 1 && !column.locked && !column.hidden); } cancelChanges() { this.checkboxes.forEach((element, index) => { element.checkedState = !this.columns[index].hidden; }); this.updateDisabled(); this.reset.emit(); } applyChanges() { const changed = []; this.checkboxes.forEach((item, index) => { const column = this.columns[index]; const hidden = !item.checkedState; if (Boolean(column.hidden) !== hidden) { column.hidden = hidden; changed.push(column); } }); this.updateDisabled(); this.apply.emit(changed); } onTab(e) { if (this.isLast) { e.preventDefault(); if (this.service) { this.service.menuTabbingService.firstFocusable.focus(); } else { this.listNavigationService.toggle(this.listNavigationService.activeIndex, true); } } } onKeydown = (e) => { if (e.keyCode !== Keys.Tab) { e.preventDefault(); } if (e.key === 'Tab' && !e.shiftKey && this.autoSync) { e.preventDefault(); } if (e.key === 'Tab' && e.shiftKey) { this.ngZone.run(() => { if (e.target.matches('.k-column-list-item')) { e.preventDefault(); this.resetButton?.nativeElement.focus(); } }); } if (e.keyCode === Keys.ArrowDown) { this.listNavigationService.next(); } else if (e.keyCode === Keys.ArrowUp) { this.listNavigationService.prev(); } else if (e.keyCode === Keys.Space && e.target.classList.contains('k-column-list-item')) { this.listNavigationService.toggleCheckedState(); } }; updateDisabled() { if (this.allowHideAll && !this.hasLocked) { return; } // Cache visible columns to avoid repeated checks const visibleColumns = []; const columnMap = new Map(); this.checkboxes.forEach((checkbox, index) => { // Reset all disabled states first this.setDisabledState(checkbox, false); if (checkbox.checkedState) { visibleColumns.push({ checkbox, index }); columnMap.set(index, this.columns[index]); } }); // Only apply disabled states where needed if (!this.allowHideAll && visibleColumns.length === 1 && !this.hasFiltered) { this.setDisabledState(visibleColumns[0].checkbox, true); } else if (this.hasLocked && !this.hasUnlockedFiltered) { const checkedUnlocked = visibleColumns.filter(item => !columnMap.get(item.index).locked); if (checkedUnlocked.length === 1) { this.setDisabledState(checkedUnlocked[0].checkbox, true); } } } updateColumnState() { this.hasLocked = this.allColumns.filter(column => column.locked && (!column.hidden || column.includeInChooser !== false)).length > 0; this.hasVisibleLocked = this.allColumns.filter(column => column.locked && !column.hidden).length > 0; this.unlockedCount = this.columns.filter(column => !column.locked && !column.hidden).length; const filteredColumns = this.allColumns.filter(column => column.includeInChooser === false && !column.hidden); if (filteredColumns.length) { this.hasFiltered = filteredColumns.length > 0; this.hasUnlockedFiltered = filteredColumns.filter(column => !column.locked).length > 0; } else { this.hasFiltered = false; this.hasUnlockedFiltered = false; } } setDisabledState(checkbox, disabled) { if (checkbox.disabled !== disabled) { this.ngZone.runOutsideAngular(() => { checkbox.disabled = disabled; const checkboxElement = checkbox.hostElement.nativeElement; const parentElement = checkboxElement.parentElement; if (disabled) { this.renderer.addClass(parentElement, 'k-disabled'); this.renderer.setAttribute(parentElement, 'aria-disabled', 'true'); } else { this.renderer.removeClass(parentElement, 'k-disabled'); this.renderer.removeAttribute(parentElement, 'aria-disabled'); } }); } } handleCheckBoxClick = (e) => { const closestItem = e.target.closest('.k-column-list-item'); if (closestItem) { const checkboxElement = closestItem.querySelector('.k-checkbox-wrap'); const index = parseInt(checkboxElement.firstElementChild.getAttribute('data-index'), 10); const checkbox = this.checkboxes.toArray()[index]; if (e.target === checkbox.input.nativeElement) { closestItem.focus(); e.target.classList.remove('k-focus'); } if (this.autoSync) { if (!this.columns[index]) { return; } const column = this.columns[index]; const hidden = !checkbox.checkedState; if (Boolean(column.hidden) !== hidden) { this.ngZone.runOutsideAngular(() => { column.hidden = hidden; this.ngZone.run(() => { this.columnChange.emit([column]); }); }); } } else { this.ngZone.run(() => this.updateDisabled()); } if (index !== this.listNavigationService.activeIndex) { this.listNavigationService.toggle(this.listNavigationService.activeIndex, false); this.listNavigationService.activeIndex = index; this.listNavigationService.toggle(index, true); } } }; static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ColumnListComponent, deps: [{ token: i0.ElementRef }, { token: i0.NgZone }, { token: i0.Renderer2 }, { token: i1.ColumnListKeyboardNavigation }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: ColumnListComponent, isStandalone: true, selector: "kendo-grid-columnlist", inputs: { columns: "columns", autoSync: "autoSync", ariaLabel: "ariaLabel", allowHideAll: "allowHideAll", applyText: "applyText", resetText: "resetText", actionsClass: "actionsClass", isLast: "isLast", isExpanded: "isExpanded", service: "service" }, outputs: { reset: "reset", apply: "apply", columnChange: "columnChange" }, host: { properties: { "class.k-column-list-wrapper": "this.className" } }, providers: [ColumnListKeyboardNavigation], viewQueries: [{ propertyName: "resetButton", first: true, predicate: ["resetButton"], descendants: true }, { propertyName: "applyButton", first: true, predicate: ["applyButton"], descendants: true }, { propertyName: "options", predicate: ColumnMenuChooserItemCheckedDirective, descendants: true }, { propertyName: "checkboxes", predicate: CheckBoxComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: ` <div class="k-column-list" role="listbox" aria-multiselectable="true" [attr.aria-label]="ariaLabel"> <label *ngFor="let column of columns; let index = index;" class='k-column-list-item' [kendoColumnMenuChooserItemChecked]="!column.hidden" role="option"> <kendo-checkbox [inputAttributes]="{'data-index': index.toString()}" [tabindex]="-1" [checkedState]="!column.hidden" [disabled]="isDisabled(column)" ></kendo-checkbox> <span class="k-checkbox-label">{{ column.displayTitle }}</span> </label> </div> <div [ngClass]="actionsClass" *ngIf="!autoSync"> <button #applyButton kendoButton type="button" themeColor="primary" (click)="applyChanges()" (keydown.enter)="$event.preventDefault(); $event.stopPropagation(); applyChanges();" (keydown.space)="$event.preventDefault(); $event.stopPropagation(); applyChanges();">{{ applyText }}</button> <button #resetButton kendoButton type="button" (keydown.tab)="onTab($event)" (click)="cancelChanges()" (keydown.enter)="$event.preventDefault(); $event.stopPropagation(); cancelChanges();" (keydown.space)="$event.preventDefault(); $event.stopPropagation(); cancelChanges();">{{ resetText }}</button> </div> `, isInline: true, dependencies: [{ kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: ColumnMenuChooserItemCheckedDirective, selector: "[kendoColumnMenuChooserItemChecked]", inputs: ["kendoColumnMenuChooserItemChecked"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: CheckBoxComponent, selector: "kendo-checkbox", inputs: ["checkedState", "rounded"], outputs: ["checkedStateChange"], exportAs: ["kendoCheckBox"] }, { kind: "component", type: ButtonDirective, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ColumnListComponent, decorators: [{ type: Component, args: [{ selector: 'kendo-grid-columnlist', providers: [ColumnListKeyboardNavigation], template: ` <div class="k-column-list" role="listbox" aria-multiselectable="true" [attr.aria-label]="ariaLabel"> <label *ngFor="let column of columns; let index = index;" class='k-column-list-item' [kendoColumnMenuChooserItemChecked]="!column.hidden" role="option"> <kendo-checkbox [inputAttributes]="{'data-index': index.toString()}" [tabindex]="-1" [checkedState]="!column.hidden" [disabled]="isDisabled(column)" ></kendo-checkbox> <span class="k-checkbox-label">{{ column.displayTitle }}</span> </label> </div> <div [ngClass]="actionsClass" *ngIf="!autoSync"> <button #applyButton kendoButton type="button" themeColor="primary" (click)="applyChanges()" (keydown.enter)="$event.preventDefault(); $event.stopPropagation(); applyChanges();" (keydown.space)="$event.preventDefault(); $event.stopPropagation(); applyChanges();">{{ applyText }}</button> <button #resetButton kendoButton type="button" (keydown.tab)="onTab($event)" (click)="cancelChanges()" (keydown.enter)="$event.preventDefault(); $event.stopPropagation(); cancelChanges();" (keydown.space)="$event.preventDefault(); $event.stopPropagation(); cancelChanges();">{{ resetText }}</button> </div> `, standalone: true, imports: [NgFor, ColumnMenuChooserItemCheckedDirective, NgIf, NgClass, CheckBoxComponent, ButtonDirective] }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.NgZone }, { type: i0.Renderer2 }, { type: i1.ColumnListKeyboardNavigation }]; }, propDecorators: { className: [{ type: HostBinding, args: ["class.k-column-list-wrapper"] }], reset: [{ type: Output }], apply: [{ type: Output }], columnChange: [{ type: Output }], columns: [{ type: Input }], autoSync: [{ type: Input }], ariaLabel: [{ type: Input }], allowHideAll: [{ type: Input }], applyText: [{ type: Input }], resetText: [{ type: Input }], actionsClass: [{ type: Input }], isLast: [{ type: Input }], isExpanded: [{ type: Input }], service: [{ type: Input }], resetButton: [{ type: ViewChild, args: ['resetButton', { static: false }] }], applyButton: [{ type: ViewChild, args: ['applyButton', { static: false }] }], options: [{ type: ViewChildren, args: [ColumnMenuChooserItemCheckedDirective] }], checkboxes: [{ type: ViewChildren, args: [CheckBoxComponent] }] } });