@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
JavaScript
/**-----------------------------------------------------------------------------------------
* 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]
}] } });