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