UNPKG

@progress/kendo-angular-grid

Version:

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

476 lines (469 loc) 23.1 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { ChangeDetectionStrategy, Component, EventEmitter, HostBinding, Input, Output, QueryList, ViewChildren, ChangeDetectorRef, NgZone, ViewChild, TemplateRef, Renderer2 } from '@angular/core'; import { Subscription, from, fromEvent, merge } from "rxjs"; import { and, isNullOrEmptyString, isUniversal, observe, or } from '../utils'; import { GroupInfoService } from './group-info.service'; import { DropTargetDirective } from '../dragdrop/drop-target.directive'; import { DragHintService } from '../dragdrop/drag-hint.service'; import { DropCueService } from '../dragdrop/drop-cue.service'; import { position, isTargetBefore } from '../dragdrop/common'; import { tap, filter, switchMapTo, takeUntil } from 'rxjs/operators'; import { arrowLeftIcon, arrowRightIcon, sortAscSmallIcon, sortDescSmallIcon } from '@progress/kendo-svg-icons'; import { ContextService } from '../common/provider.service'; import { PopupService } from '@progress/kendo-angular-popup'; import { ChipComponent, ChipListComponent } from '@progress/kendo-angular-buttons'; import { closest } from '../rendering/common/dom-queries'; import { DraggableDirective, EventsOutsideAngularDirective, Keys } from '@progress/kendo-angular-common'; import { IconWrapperComponent } from '@progress/kendo-angular-icons'; import { DraggableColumnDirective } from '../dragdrop/draggable-column.directive'; import { NgIf, NgFor } from '@angular/common'; import * as i0 from "@angular/core"; import * as i1 from "../dragdrop/drag-hint.service"; import * as i2 from "../dragdrop/drop-cue.service"; import * as i3 from "./group-info.service"; import * as i4 from "../common/provider.service"; import * as i5 from "@progress/kendo-angular-popup"; const withoutField = ({ field }) => isNullOrEmptyString(field); const alreadyGrouped = ({ groups, field }) => groups.some(group => group.field === field); const overSameTarget = ({ target, field }) => target.field === field; const overLastTarget = ({ target }) => target.lastTarget; const isLastGroup = ({ groups, field }) => groups.map(group => group.field).indexOf(field) === groups.length - 1; const isNotGroupable = (groupsService) => ({ field }) => !groupsService.isGroupable(field); const columnRules = (groupService) => or(withoutField, alreadyGrouped, isNotGroupable(groupService)); const indicatorRules = or(overSameTarget, and(overLastTarget, isLastGroup)); /** * @hidden */ export class GroupPanelComponent { hint; cue; groupInfoService; ctx; cd; popupService; ngZone; renderer; change = new EventEmitter(); get groupHeaderClass() { return true; } set text(value) { this.emptyText = value; } get text() { return this.emptyText ? this.emptyText : this.ctx.localization.get('groupPanelEmpty'); } navigable; groups = []; dropTargets = new QueryList(); defaultTemplate; groupTitles = []; isChipMenuOpen = false; get gridId() { return this.ctx.grid?.ariaRootId; } rtl = false; first; last; arrowLeftIcon = arrowLeftIcon; arrowRightIcon = arrowRightIcon; emptyText; subscription; targetSubscription; popupSubs; popupRef; activeItem; constructor(hint, cue, groupInfoService, ctx, cd, popupService, ngZone, renderer) { this.hint = hint; this.cue = cue; this.groupInfoService = groupInfoService; this.ctx = ctx; this.cd = cd; this.popupService = popupService; this.ngZone = ngZone; this.renderer = renderer; } ngAfterViewInit() { this.subscription = this.ctx.localization.changes.subscribe(({ rtl }) => { this.rtl = rtl; this.cd.markForCheck(); }); this.subscription.add(observe(this.dropTargets) .subscribe(this.attachTargets.bind(this))); } ngDoCheck() { const currentTitles = this.groups.map(group => this.groupInfoService.groupTitle(group)); if (currentTitles.length !== this.groupTitles.length || currentTitles.some((current, idx) => current !== this.groupTitles[idx])) { this.groupTitles = currentTitles; this.cd.markForCheck(); } } ngOnDestroy() { if (this.subscription) { this.subscription.unsubscribe(); } if (this.targetSubscription) { this.targetSubscription.unsubscribe(); } this.destroyMenu(); } messageFor(token) { return this.ctx.localization.get(token); } getTitle(group) { return this.messageFor(group.dir === 'desc' ? 'sortedDescending' : 'sortedAscending'); } getDirectionIcon(group) { return group.dir === 'desc' ? 'sort-desc-sm' : 'sort-asc-sm'; } getDirectionSvgIcon(group) { return group.dir === 'desc' ? sortDescSmallIcon : sortAscSmallIcon; } directionChange(group) { group.dir = group.dir ? group.dir : "asc"; group.dir = group.dir === 'asc' ? 'desc' : 'asc'; const index = this.groups.findIndex(x => x.field === group.field); const groups = [...this.groups.slice(0, index), group, ...this.groups.slice(index + 1)]; this.change.emit(groups); } insert(field, index) { const groups = this.groups.filter(x => x.field !== field); if (groups.length || this.groups.length === 0) { this.change.emit([...groups.slice(0, index), { field: field }, ...groups.slice(index)]); } } remove(group) { this.destroyMenu(); this.change.emit(this.groups.filter(x => x.field !== group.field)); } toggleMenu(chip, first, last, field) { const anchor = chip.element.nativeElement.querySelector('.k-chip-action'); if (this.popupRef) { const popupAnchor = this.popupRef.popup.instance.anchor; this.destroyMenu(); if (anchor === popupAnchor) { return; } } this.first = first; this.last = last; const direction = this.ctx.localization.rtl ? 'right' : 'left'; this.popupRef = this.popupService.open({ anchor: anchor, content: this.defaultTemplate, anchorAlign: { vertical: 'bottom', horizontal: direction }, popupAlign: { vertical: 'top', horizontal: direction }, positionMode: 'absolute' }); this.activeItem = this.dropTargets.find(dt => dt.context.field === field); this.renderer.setAttribute(this.popupRef.popupElement, 'dir', this.ctx.localization.rtl ? 'rtl' : 'ltr'); const menuItems = Array.from(this.popupRef.popupElement.querySelectorAll('.k-menu-item')); this.activateMenuItem(menuItems[1], 'previous'); this.popupSubs = this.popupRef.popupAnchorViewportLeave.subscribe(() => { this.destroyMenu(true); }); if (isUniversal()) { return; } this.ngZone.runOutsideAngular(() => { this.popupSubs.add(fromEvent(document, 'click') .pipe(filter((event) => !closest(event.target, (node) => node === this.popupRef.popupElement || (node.matches && node.matches('.k-chip-action'))))).subscribe(() => { this.destroyMenu(); })); }); } handleKeyDown = (e) => { if (e.keyCode === Keys.ArrowDown || e.keyCode === Keys.ArrowUp) { e.preventDefault(); const relatedItemType = e.target.matches(':first-child') ? 'next' : 'previous'; this.activateMenuItem(e.target, relatedItemType); } else if (e.keyCode === Keys.Escape) { this.destroyMenu(true); } else if (e.keyCode === Keys.Tab) { this.destroyMenu(true); } else if (e.keyCode === Keys.Space || e.keyCode === Keys.Enter) { this.handleMenuClick(e); } }; handleClick = (e) => { e.preventDefault(); const menuItemEl = e.target.closest('.k-menu-item'); if (!menuItemEl.matches('[aria-disabled="true"]')) { this.handleMenuClick(e); return; } if (menuItemEl.getAttribute('tabindex') === '0') { return; } const activeMenuItem = menuItemEl.closest('.k-menu-group').querySelector('[tabindex="0"]'); const relatedItemType = activeMenuItem.matches(':first-child') ? 'next' : 'previous'; this.activateMenuItem(activeMenuItem, relatedItemType); }; canDrop(draggable, target) { const isIndicator = draggable.type === 'groupIndicator'; const rules = isIndicator ? indicatorRules : columnRules(this.groupInfoService); return !rules({ field: draggable.field, groups: this.groups, target }); } attachTargets() { if (this.targetSubscription) { this.targetSubscription.unsubscribe(); } this.targetSubscription = new Subscription(); const enterStream = this.dropTargets .reduce((acc, target) => merge(acc, target.enter), from([])); const leaveStream = this.dropTargets .reduce((acc, target) => merge(acc, target.leave), from([])); const dropStream = this.dropTargets .reduce((acc, target) => merge(acc, target.drop), from([])); this.targetSubscription.add(enterStream.pipe(tap(() => { this.hint.removeLock(); this.destroyMenu(); }), filter(({ draggable, target }) => this.canDrop(draggable.context, target.context)), tap(this.enter.bind(this)), switchMapTo(dropStream.pipe(takeUntil(leaveStream.pipe(tap(this.leave.bind(this))))))).subscribe(this.drop.bind(this))); } enter({ draggable, target }) { this.hint.enable(); let before = target.context.lastTarget || isTargetBefore(draggable.element.nativeElement, target.element.nativeElement); if (this.ctx.localization.rtl) { before = !before; } this.cue.position(position(target.element.nativeElement, before)); } leave() { this.hint.disable(); this.cue.hide(); } drop({ target, draggable }) { const field = draggable.context.field; const index = this.dropTargets.toArray().indexOf(target); this.insert(field, index); } destroyMenu(focusAnchor) { if (this.popupRef) { this.popupRef.close(); this.popupRef = null; this.popupSubs && this.popupSubs.unsubscribe(); focusAnchor && this.activeItem.context.target.focus(); } } activateMenuItem(item, relatedItemType) { this.renderer.setAttribute(item, 'tabindex', '-1'); this.renderer.removeClass(item, 'k-focus'); const relatedItem = item[`${relatedItemType}ElementSibling`]; this.renderer.setAttribute(relatedItem, 'tabindex', '0'); this.renderer.addClass(relatedItem, 'k-focus'); this.ngZone.runOutsideAngular(() => setTimeout(() => relatedItem.focus())); } handleMenuClick(e) { e.preventDefault(); if (e.target.getAttribute('aria-disabled') !== 'true') { const chips = this.dropTargets.toArray().slice(0, this.dropTargets.length - 1); let groupChip, groupChipIndex; for (let i = 0; i < chips.length; i++) { if (chips[i].element.nativeElement === this.popupRef.popup.instance.anchor.closest('.k-chip')) { groupChip = chips[i]; groupChipIndex = i; break; } } const isPrev = e.target.closest('.k-menu-item').matches(':first-child'); if (isPrev && groupChipIndex > 0) { this.insert(groupChip.context.field, groupChipIndex - 1); } else if (!isPrev && groupChipIndex < chips.length - 1) { this.insert(groupChip.context.field, groupChipIndex + 1); } this.destroyMenu(true); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: GroupPanelComponent, deps: [{ token: i1.DragHintService }, { token: i2.DropCueService }, { token: i3.GroupInfoService }, { token: i4.ContextService }, { token: i0.ChangeDetectorRef }, { token: i5.PopupService }, { token: i0.NgZone }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: GroupPanelComponent, isStandalone: true, selector: "kendo-grid-group-panel", inputs: { text: "text", navigable: "navigable", groups: "groups" }, outputs: { change: "change" }, host: { properties: { "class.k-grouping-header": "this.groupHeaderClass" } }, viewQueries: [{ propertyName: "defaultTemplate", first: true, predicate: ["defaultTemplate"], descendants: true, read: TemplateRef, static: true }, { propertyName: "dropTargets", predicate: DropTargetDirective, descendants: true }], ngImport: i0, template: ` <div *ngIf="groups.length === 0" class="k-grouping-drop-container" [context]="{ lastTarget: true }" kendoDropTarget > {{ text }} </div> <kendo-chiplist *ngIf="groups.length !== 0" [navigable]="navigable" role="none"> <kendo-chip *ngFor="let group of groups; let index = index; let first = first; let last = last;" #chip kendoDropTarget kendoDraggableColumn kendoDraggable [title]="getTitle(group)" [enableDrag]="true" [context]="{ field: group.field, type: 'groupIndicator', hint: groupTitles[index], target: chip }" [label]="groupTitles[index]" [removable]="true" [hasMenu]="true" [icon]="getDirectionIcon(group)" [svgIcon]="getDirectionSvgIcon(group)" [attr.aria-haspopup]="'menu'" [attr.aria-expanded]="isChipMenuOpen" [attr.aria-controls]="gridId" (contentClick)="directionChange(group)" (remove)="remove(group)" (menuToggle)="toggleMenu(chip, first, last, group.field)" (keydown.alt.arrowdown)="$event.preventDefault(); toggleMenu(chip, first, last, group.field)" > </kendo-chip> </kendo-chiplist> <div *ngIf="groups.length !== 0" class="k-grouping-drop-container" [context]="{ lastTarget: true }" kendoDropTarget >&nbsp;</div> <ng-template #defaultTemplate> <ul unselectable="on" role="menu" class="k-group k-menu-group k-reset k-menu-group-md" [kendoEventsOutsideAngular]="{ keydown: handleKeyDown, click: handleClick }"> <li role="menuitem" unselectable="on" class="k-item k-menu-item" [attr.aria-disabled]="first"> <span class="k-link k-menu-link" [class.k-disabled]="first"> <kendo-icon-wrapper [name]="rtl ? 'arrow-right' : 'arrow-left'" [svgIcon]="rtl ? arrowRightIcon : arrowLeftIcon"></kendo-icon-wrapper> <span class="k-menu-link-text">{{messageFor('groupChipMenuPrevious')}}</span> </span> </li> <li role="menuitem" unselectable="on" class="k-item k-menu-item" [attr.aria-disabled]="last"> <span class="k-link k-menu-link" [class.k-disabled]="last"> <kendo-icon-wrapper [name]="rtl ? 'arrow-left' : 'arrow-right'" [svgIcon]="rtl ? arrowLeftIcon : arrowRightIcon"></kendo-icon-wrapper> <span class="k-menu-link-text">{{messageFor('groupChipMenuNext')}}</span> </span> </li> </ul> </ng-template> `, isInline: true, dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: DropTargetDirective, selector: "[kendoDropTarget]", inputs: ["context"], outputs: ["enter", "leave", "drop"] }, { kind: "component", type: ChipListComponent, selector: "kendo-chiplist, kendo-chip-list", inputs: ["selection", "size", "role", "navigable"], outputs: ["selectedChange", "remove"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "component", type: ChipComponent, selector: "kendo-chip", inputs: ["label", "icon", "svgIcon", "iconClass", "avatarSettings", "selected", "removable", "removeIcon", "removeSvgIcon", "hasMenu", "menuIcon", "menuSvgIcon", "disabled", "size", "rounded", "fillMode", "themeColor"], outputs: ["remove", "menuToggle", "contentClick"] }, { kind: "directive", type: DraggableColumnDirective, selector: "[kendoDraggableColumn]", inputs: ["context", "enableDrag"], outputs: ["drag"] }, { kind: "directive", type: DraggableDirective, selector: "[kendoDraggable]", inputs: ["enableDrag"], outputs: ["kendoPress", "kendoDrag", "kendoRelease"] }, { kind: "directive", type: EventsOutsideAngularDirective, selector: "[kendoEventsOutsideAngular]", inputs: ["kendoEventsOutsideAngular", "scope"] }, { kind: "component", type: IconWrapperComponent, selector: "kendo-icon-wrapper", inputs: ["name", "svgIcon", "innerCssClass", "customFontClass", "size"], exportAs: ["kendoIconWrapper"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: GroupPanelComponent, decorators: [{ type: Component, args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'kendo-grid-group-panel', template: ` <div *ngIf="groups.length === 0" class="k-grouping-drop-container" [context]="{ lastTarget: true }" kendoDropTarget > {{ text }} </div> <kendo-chiplist *ngIf="groups.length !== 0" [navigable]="navigable" role="none"> <kendo-chip *ngFor="let group of groups; let index = index; let first = first; let last = last;" #chip kendoDropTarget kendoDraggableColumn kendoDraggable [title]="getTitle(group)" [enableDrag]="true" [context]="{ field: group.field, type: 'groupIndicator', hint: groupTitles[index], target: chip }" [label]="groupTitles[index]" [removable]="true" [hasMenu]="true" [icon]="getDirectionIcon(group)" [svgIcon]="getDirectionSvgIcon(group)" [attr.aria-haspopup]="'menu'" [attr.aria-expanded]="isChipMenuOpen" [attr.aria-controls]="gridId" (contentClick)="directionChange(group)" (remove)="remove(group)" (menuToggle)="toggleMenu(chip, first, last, group.field)" (keydown.alt.arrowdown)="$event.preventDefault(); toggleMenu(chip, first, last, group.field)" > </kendo-chip> </kendo-chiplist> <div *ngIf="groups.length !== 0" class="k-grouping-drop-container" [context]="{ lastTarget: true }" kendoDropTarget >&nbsp;</div> <ng-template #defaultTemplate> <ul unselectable="on" role="menu" class="k-group k-menu-group k-reset k-menu-group-md" [kendoEventsOutsideAngular]="{ keydown: handleKeyDown, click: handleClick }"> <li role="menuitem" unselectable="on" class="k-item k-menu-item" [attr.aria-disabled]="first"> <span class="k-link k-menu-link" [class.k-disabled]="first"> <kendo-icon-wrapper [name]="rtl ? 'arrow-right' : 'arrow-left'" [svgIcon]="rtl ? arrowRightIcon : arrowLeftIcon"></kendo-icon-wrapper> <span class="k-menu-link-text">{{messageFor('groupChipMenuPrevious')}}</span> </span> </li> <li role="menuitem" unselectable="on" class="k-item k-menu-item" [attr.aria-disabled]="last"> <span class="k-link k-menu-link" [class.k-disabled]="last"> <kendo-icon-wrapper [name]="rtl ? 'arrow-left' : 'arrow-right'" [svgIcon]="rtl ? arrowLeftIcon : arrowRightIcon"></kendo-icon-wrapper> <span class="k-menu-link-text">{{messageFor('groupChipMenuNext')}}</span> </span> </li> </ul> </ng-template> `, standalone: true, imports: [NgIf, DropTargetDirective, ChipListComponent, NgFor, ChipComponent, DraggableColumnDirective, DraggableDirective, EventsOutsideAngularDirective, IconWrapperComponent] }] }], ctorParameters: function () { return [{ type: i1.DragHintService }, { type: i2.DropCueService }, { type: i3.GroupInfoService }, { type: i4.ContextService }, { type: i0.ChangeDetectorRef }, { type: i5.PopupService }, { type: i0.NgZone }, { type: i0.Renderer2 }]; }, propDecorators: { change: [{ type: Output }], groupHeaderClass: [{ type: HostBinding, args: ["class.k-grouping-header"] }], text: [{ type: Input }], navigable: [{ type: Input }], groups: [{ type: Input }], dropTargets: [{ type: ViewChildren, args: [DropTargetDirective] }], defaultTemplate: [{ type: ViewChild, args: ['defaultTemplate', { static: true, read: TemplateRef }] }] } });