UNPKG

@progress/kendo-angular-grid

Version:

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

494 lines (493 loc) 23.2 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { ChangeDetectorRef, Directive, ElementRef, Host, HostBinding, HostListener, Input, NgZone } from '@angular/core'; import { Subscription, of } from 'rxjs'; import { isBlank, isPresent, isTruthy } from '../utils'; import { ColumnBase } from '../columns/column-base'; import { expandColumns, leafColumns, columnsToRender } from '../columns/column-common'; import { DraggableDirective } from '@progress/kendo-angular-common'; import { ColumnResizingService } from './column-resizing.service'; import { delay, takeUntil, filter, take, tap, switchMap, map } from 'rxjs/operators'; import { ColumnInfoService } from '../common/column-info.service'; import { ContextService } from '../common/provider.service'; import * as i0 from "@angular/core"; import * as i1 from "@progress/kendo-angular-common"; import * as i2 from "./column-resizing.service"; import * as i3 from "../common/provider.service"; import * as i4 from "../common/column-info.service"; /** * @hidden */ const fromPercentage = (value, percent) => { const sign = percent < 0 ? -1 : 1; return Math.ceil((Math.abs(percent) / 100) * value) * sign; }; /** * @hidden */ const toPercentage = (value, whole) => (value / whole) * 100; /** * @hidden */ const headerWidth = (handle) => handle.nativeElement.parentElement.getBoundingClientRect().width; /** * @hidden */ const adjacentColumnWidth = (handle) => handle.nativeElement.parentElement.nextElementSibling?.getBoundingClientRect().width; /** * @hidden */ const adjacentColumnInGroupWidth = (handle, rowIndex, colIndex) => { const tableElement = handle.nativeElement.closest('.k-grid-header-table'); const selector = (rowAttribute) => `tr[${rowAttribute}="${rowIndex}"] th[aria-colindex="${colIndex}"]`; const thElement = tableElement.querySelector([selector('aria-rowindex'), selector('data-kendo-grid-row-index')]); return thElement.getBoundingClientRect().width; }; /** * @hidden */ const allLeafColumns = columns => expandColumns(columns) .filter(c => !c.isColumnGroup); /** * @hidden */ const createMoveStream = (service, draggable) => mouseDown => draggable.kendoDrag.pipe(takeUntil(draggable.kendoRelease.pipe(tap(() => service.end()))), map(({ pageX }) => ({ originalX: mouseDown.pageX, pageX }))); /** * @hidden */ const preventOnDblClick = release => mouseDown => of(mouseDown).pipe(delay(150), takeUntil(release)); /** * @hidden */ const isInSpanColumn = column => !!(column.parent && column.parent.isSpanColumn); /** * @hidden * * Calculates the column index. If the column is stated in `SpanColumn`, * the index for all child columns equals the index of the first child. */ const indexOf = (target, list) => { let index = 0; let ignore = 0; let skip = 0; while (index < list.length) { const current = list[index]; const isParentSpanColumn = isInSpanColumn(current); if (current === target) { break; } if ((ignore-- <= 0) && isParentSpanColumn) { ignore = current.parent.childColumns.length - 1; skip += ignore; } index++; } return index - skip; }; /** * @hidden */ export class ColumnHandleDirective { draggable; element; service; zone; cdr; ctx; columnInfoService; isLast; columns = []; column; get visible() { if (this.isConstrainedMode && (this.isLast || this.isLastInGroup(this.column))) { return 'none'; } return this.column.resizable ? 'block' : 'none'; } get leftStyle() { return isTruthy(this.rtl) ? 0 : null; } get rightStyle() { return isTruthy(this.rtl) ? null : 0; } get isConstrainedMode() { const isConstrainedMode = this.ctx.grid?.resizable === 'constrained'; const isUnconstrainedMode = this.ctx.grid?.resizable === true || this.ctx.grid?.resizable === 'unconstrained'; const constrainedNoShift = isConstrainedMode && !this.service.isShiftPressed; const unconstrainedWithShift = isUnconstrainedMode && this.service.isShiftPressed; return constrainedNoShift || unconstrainedWithShift; } subscriptions = new Subscription(); rtl = false; totalChildrenSum = 0; childrenColumns = []; minWidthTotal = 0; foundColumn; autoFit() { this.service.autoFitResize = true; const allLeafs = allLeafColumns(this.columns); const currentLeafs = leafColumns([this.column]).filter(column => isTruthy(column.resizable)); const columnInfo = currentLeafs.map(column => { const isParentSpan = isInSpanColumn(column); const isLastInSpan = isParentSpan ? column.parent.childColumns.last === column : false; const index = indexOf(column, allLeafs); return { column, headerIndex: this.columnsForLevel(column.level).indexOf(column), index, isLastInSpan, isParentSpan, level: column.level }; }); currentLeafs.forEach(column => column.width = 0); this.service.measureColumns(columnInfo); } constructor(draggable, element, service, zone, cdr, ctx, columnInfoService) { this.draggable = draggable; this.element = element; this.service = service; this.zone = zone; this.cdr = cdr; this.ctx = ctx; this.columnInfoService = columnInfoService; } ngOnInit() { if (isBlank(this.column.width)) { this.column.implicitWidth = headerWidth(this.element); } const service = this.service.changes.pipe(filter(() => this.column.resizable), filter(e => isPresent(e.columns.find(column => column === this.column)))); this.subscriptions.add(service.pipe(filter(e => e.type === 'start')) .subscribe(this.initState.bind(this))); this.subscriptions.add(service.pipe(filter(e => e.type === 'resizeColumn')) .subscribe(this.resize.bind(this))); this.subscriptions.add(this.service.changes.pipe(filter(e => e.type === 'start'), filter(this.shouldUpdate.bind(this)), take(1) //on first resize only ).subscribe(this.initColumnWidth.bind(this))); this.subscriptions.add(this.zone.runOutsideAngular(() => this.draggable.kendoPress.pipe(tap(this.stopPropagation), tap(() => this.service.start(this.column)), switchMap(preventOnDblClick(this.draggable.kendoRelease)), switchMap(createMoveStream(this.service, this.draggable))) .subscribe(({ pageX, originalX }) => { const delta = pageX - originalX; const percent = toPercentage(delta, this.column.resizeStartWidth || this.column.width); this.service.resizeColumns(percent); }))); this.subscriptions.add(service.pipe(filter(e => e.type === 'autoFitComplete')) .subscribe(this.sizeToFit.bind(this))); this.subscriptions.add(service.pipe(filter(e => e.type === 'triggerAutoFit')) .subscribe(this.autoFit.bind(this))); this.subscriptions.add(this.ctx.localization.changes.subscribe(({ rtl }) => this.rtl = rtl)); } ngOnDestroy() { if (this.subscriptions) { this.subscriptions.unsubscribe(); } } shouldUpdate() { return !allLeafColumns(this.columns) .map(column => column.width || (this.isConstrainedMode && !column.width && column.implicitWidth)) .some(isBlank); } initColumnWidth() { this.column.width = headerWidth(this.element); if (this.isConstrainedMode) { this.column.resizeStartWidth = this.column.width; } } initState() { this.column.resizeStartWidth = headerWidth(this.element); if (this.isConstrainedMode && !this.service.adjacentColumn) { this.setAdjacentColumn(); } this.service.resizedColumn({ column: this.column, oldWidth: this.column.resizeStartWidth }); } resize({ deltaPercent }) { let delta = fromPercentage(this.column.resizeStartWidth, deltaPercent); if (isTruthy(this.rtl)) { delta *= -1; } let newWidth = Math.max(this.column.resizeStartWidth + delta, this.column.minResizableWidth); if (isPresent(this.column.maxResizableWidth)) { newWidth = Math.min(newWidth, this.column.maxResizableWidth); } if (this.isConstrainedMode) { newWidth = this.calcNewColumnWidth(newWidth); } const tableDelta = this.getTableDelta(newWidth, delta); this.updateWidth(this.column, newWidth); this.service.resizeTable(this.column, tableDelta); } sizeToFit({ columns, widths }) { const index = columns.indexOf(this.column); const width = Math.max(...widths.map(w => w[index])) + 1; //add 1px for IE const tableDelta = width - this.column.resizeStartWidth; this.updateWidth(this.column, width); this.service.resizeTable(this.column, tableDelta); } updateWidth(column, width) { if (this.isConstrainedMode && this.service.adjacentColumn && !this.service.autoFitResize) { this.updateWidthsOfResizedColumns(column, width); } column.width = width; this.columnInfoService.hiddenColumns.forEach((col) => { if (isBlank(col.width) && isPresent(col.implicitWidth)) { // Resize hidden columns to their implicit width so they // can be displayed with the same width if made visible. col.width = col.implicitWidth; } }); this.cdr.markForCheck(); //force CD cycle } updateWidthsOfResizedColumns(column, width) { let adjacentColumnNewWidth = column.resizeStartWidth + this.service.adjacentColumn.resizeStartWidth - width; if (this.service.draggedGroupColumn && column.parent) { this.updateWidthOfDraggedColumn(column, width); this.setGroupWidths(this.service.draggedGroupColumn); } else if (!this.service.draggedGroupColumn && !column.parent && this.service.adjacentColumn.parent) { this.service.adjacentColumn.parent.width = column.width + this.service.adjacentColumn.parent.width - width; this.service.adjacentColumn.width = adjacentColumnNewWidth; } else if (!this.service.draggedGroupColumn && column.parent && this.service.adjacentColumn.parent) { adjacentColumnNewWidth = column.width + this.service.adjacentColumn.width - width; this.service.adjacentColumn.width = adjacentColumnNewWidth; const filteredColumns = this.service.adjacentColumn.parent.children.filter(c => c !== this.service.adjacentColumn); const filteredColumnsWidth = filteredColumns.reduce((acc, c) => acc + c.width, 0); this.service.adjacentColumn.parent.width = adjacentColumnNewWidth + filteredColumnsWidth; this.setGroupWidths(this.service.adjacentColumn.parent); } else if (adjacentColumnNewWidth > this.service.adjacentColumn.minResizableWidth) { this.service.adjacentColumn.width = adjacentColumnNewWidth; } } calcNewColumnWidth(newWidth) { let maxAllowedResizableWidth; if (!this.service.adjacentColumn.parent) { maxAllowedResizableWidth = this.column.width + this.service.adjacentColumn.width - this.service.adjacentColumn.minResizableWidth; if (!this.column.parent) { maxAllowedResizableWidth = this.column.resizeStartWidth + this.service.adjacentColumn.resizeStartWidth - this.service.adjacentColumn.minResizableWidth; if (this.service.adjacentColumn.maxResizableWidth) { const minResizableWidth = this.column.resizeStartWidth + this.service.adjacentColumn.resizeStartWidth - this.service.adjacentColumn.maxResizableWidth; maxAllowedResizableWidth = this.column.resizeStartWidth + this.service.adjacentColumn.resizeStartWidth - this.service.adjacentColumn.minResizableWidth; this.column.minResizableWidth = minResizableWidth; this.column.maxResizableWidth = maxAllowedResizableWidth; } } } else { maxAllowedResizableWidth = this.column.width + this.service.adjacentColumn.width; newWidth = Math.min(newWidth, maxAllowedResizableWidth); this.minWidthTotal = 0; const minResizableWidth = this.minAdjacentColumnWidth(this.service.adjacentColumn); maxAllowedResizableWidth -= minResizableWidth; } return Math.min(newWidth, maxAllowedResizableWidth - 1); } setAdjacentColumn() { const columnsForLevel = this.columnsForLevel(this.column.level); if (this.column.parent) { if (this.column.isReordered) { this.service.adjacentColumn = columnsForLevel.find(c => c.orderIndex === this.column.orderIndex + 1); this.service.adjacentColumn.resizeStartWidth = this.service.adjacentColumn.width; } else { const columnIndex = columnsForLevel.indexOf(this.column); this.service.adjacentColumn = columnsForLevel[columnIndex + 1]; this.service.adjacentColumn.resizeStartWidth = adjacentColumnWidth(this.element); const parentColumnChildren = Array.from(this.column.parent.children); const indexOfCurrentColumn = parentColumnChildren.indexOf(this.column); let adjacentColumn; if (indexOfCurrentColumn + 1 <= parentColumnChildren.length - 1) { adjacentColumn = parentColumnChildren[indexOfCurrentColumn + 1]; if (adjacentColumn?.isColumnGroup) { this.service.adjacentColumn = adjacentColumn; } } } if (this.service.adjacentColumn.isColumnGroup) { this.foundColumn = null; this.service.adjacentColumn = this.firstGroupChild(this.service.adjacentColumn); } if (this.column.isColumnGroup) { this.service.draggedGroupColumn = this.column; } } else if (this.column.isColumnGroup) { if (this.column.isReordered) { this.service.adjacentColumn = columnsForLevel.find(c => c.orderIndex === this.column.orderIndex + 1); } else { this.service.adjacentColumn = columnsForLevel[columnsForLevel.indexOf(this.column) + 1]; } this.service.adjacentColumn.resizeStartWidth = adjacentColumnWidth(this.element); if (this.service.adjacentColumn.isColumnGroup) { this.foundColumn = null; this.service.adjacentColumn = this.firstGroupChild(this.service.adjacentColumn); } this.service.adjacentColumn.resizeStartWidth = this.service.adjacentColumn.width; this.service.draggedGroupColumn = this.column; } else { if (this.column.isReordered) { this.service.adjacentColumn = columnsForLevel.find(col => col.orderIndex === this.column.orderIndex + 1); } else { let adjacentColumn = columnsForLevel.find(c => c.leafIndex === this.column.leafIndex + 1); if (!adjacentColumn) { const indexOfCurrentColumn = columnsForLevel.indexOf(this.column); adjacentColumn = columnsForLevel[indexOfCurrentColumn + 1]; } this.service.adjacentColumn = adjacentColumn; } if (!this.service.adjacentColumn.parent) { this.service.adjacentColumn.resizeStartWidth = adjacentColumnWidth(this.element); } if (this.service.adjacentColumn.isColumnGroup) { this.foundColumn = null; this.service.adjacentColumn = this.firstGroupChild(this.service.adjacentColumn); const rowIndex = this.service.adjacentColumn.level + 1; const colIndex = this.service.adjacentColumn.leafIndex + 1; this.service.adjacentColumn.resizeStartWidth = adjacentColumnInGroupWidth(this.element, rowIndex, colIndex); } } this.service.resizedColumn({ column: this.service.adjacentColumn, oldWidth: this.service.adjacentColumn.resizeStartWidth }); } firstGroupChild(column) { Array.from(column.children).sort((a, b) => a.orderIndex - b.orderIndex).forEach((c, idx) => { if (idx === 0 && !c.isColumnGroup) { if (!this.foundColumn) { this.foundColumn = c; } } else if (c.isColumnGroup) { this.firstGroupChild(c); } }); return this.foundColumn; } setGroupWidths(column) { const childrenWidths = column.children.reduce((acc, c) => acc + c.width, 0); column.width = childrenWidths; column.children.forEach(c => { if (c.isColumnGroup) { this.setGroupWidths(c); } }); } updateWidthOfDraggedColumn(column, width) { this.totalChildrenSum = 0; this.childrenColumns = []; this.calcChildrenWidth(this.service.draggedGroupColumn); const childrenWidthNotIncludingColumn = this.childrenColumns.reduce((acc, col) => { return col !== column ? acc + col.width : acc; }, 0); this.service.draggedGroupColumn.width = childrenWidthNotIncludingColumn + width; if (this.service.adjacentColumn.minResizableWidth <= this.totalChildrenSum + this.service.adjacentColumn.resizeStartWidth - width - childrenWidthNotIncludingColumn) { this.service.adjacentColumn.width = this.totalChildrenSum + this.service.adjacentColumn.resizeStartWidth - width - childrenWidthNotIncludingColumn; } } calcChildrenWidth(column) { const columnChildren = Array.from(column.children); const childrenNoGroups = columnChildren.filter(c => !c.isColumnGroup); const childrenGroups = columnChildren.filter(c => c.isColumnGroup); childrenNoGroups.forEach(col => { if (this.childrenColumns.indexOf(col) === -1) { this.childrenColumns.push(col); } }); this.totalChildrenSum += childrenNoGroups.reduce((acc, col) => acc + col.resizeStartWidth, 0); childrenGroups.forEach((col) => { this.calcChildrenWidth(col); }); } columnsForLevel(level) { return columnsToRender(this.columns ? this.columns.filter(column => column.level === level) : []); } minAdjacentColumnWidth(column) { if (column.isColumnGroup) { Array.from(column.children).forEach(c => { this.minAdjacentColumnWidth(c); }); } else { this.minWidthTotal += column.minResizableWidth; if (column.width < column.minResizableWidth) { column.width = column.minResizableWidth; } } return this.minWidthTotal; } getTableDelta(newWidth, delta) { const minWidth = this.column.minResizableWidth; const maxWidth = this.column.maxResizableWidth; const startWidth = this.column.resizeStartWidth; const isAboveMin = newWidth > minWidth; const isBelowMax = newWidth < maxWidth; const isInBoundaries = isPresent(maxWidth) ? isAboveMin && isBelowMax : isAboveMin; if (isInBoundaries) { return delta; } else if (newWidth <= minWidth) { return minWidth - startWidth; } else { return startWidth - maxWidth; } } stopPropagation = ({ originalEvent: event }) => { this.service.isShiftPressed = event.shiftKey; event.stopPropagation(); event.preventDefault(); }; isLastInGroup(column) { if (column.parent) { const groupChildren = Array.from(column.parent.children); const indexOfCurrentColumn = groupChildren.indexOf(column); if (column.isReordered || column.orderIndex > 0 || (column.isReordered && column.orderIndex === 0)) { return (column.orderIndex - groupChildren[0].orderIndex) === groupChildren.length - 1; } else { return indexOfCurrentColumn === groupChildren.length - 1; } } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ColumnHandleDirective, deps: [{ token: i1.DraggableDirective, host: true }, { token: i0.ElementRef }, { token: i2.ColumnResizingService }, { token: i0.NgZone }, { token: i0.ChangeDetectorRef }, { token: i3.ContextService }, { token: i4.ColumnInfoService }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: ColumnHandleDirective, isStandalone: true, selector: "[kendoGridColumnHandle]", inputs: { isLast: "isLast", columns: "columns", column: "column" }, host: { listeners: { "dblclick": "autoFit()" }, properties: { "style.display": "this.visible", "style.left": "this.leftStyle", "style.right": "this.rightStyle" } }, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ColumnHandleDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoGridColumnHandle]', standalone: true }] }], ctorParameters: function () { return [{ type: i1.DraggableDirective, decorators: [{ type: Host }] }, { type: i0.ElementRef }, { type: i2.ColumnResizingService }, { type: i0.NgZone }, { type: i0.ChangeDetectorRef }, { type: i3.ContextService }, { type: i4.ColumnInfoService }]; }, propDecorators: { isLast: [{ type: Input }], columns: [{ type: Input }], column: [{ type: Input }], visible: [{ type: HostBinding, args: ['style.display'] }], leftStyle: [{ type: HostBinding, args: ['style.left'] }], rightStyle: [{ type: HostBinding, args: ['style.right'] }], autoFit: [{ type: HostListener, args: ['dblclick'] }] } });