@ng-matero/extensions
Version:
Angular Material Extensions
1,054 lines (1,036 loc) • 47.7 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, inject, Renderer2, Input, Directive, Injectable, NgZone, CSP_NONCE, DOCUMENT, ElementRef, NgModule, Injector } from '@angular/core';
import { _IdGenerator } from '@angular/cdk/a11y';
import { Subject, merge, combineLatest, Observable } from 'rxjs';
import { mapTo, take, takeUntil, startWith, pairwise, distinctUntilChanged, share, map, skip, filter } from 'rxjs/operators';
import { CdkTable } from '@angular/cdk/table';
import { coerceCssPixelValue } from '@angular/cdk/coercion';
import { ComponentPortal } from '@angular/cdk/portal';
import { ESCAPE } from '@angular/cdk/keycodes';
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
// TODO: Figure out how to remove `mat-` classes from the CDK part of the
// column resize implementation.
const HEADER_CELL_SELECTOR = '.cdk-header-cell, .mat-header-cell';
const HEADER_ROW_SELECTOR = '.cdk-header-row, .mat-header-row';
const RESIZE_OVERLAY_SELECTOR = '.mat-column-resize-overlay-thumb';
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/** closest implementation that is able to start from non-Element Nodes. */
function closest(element, selector) {
if (!(element instanceof Node)) {
return null;
}
let curr = element;
while (curr != null && !(curr instanceof Element)) {
curr = curr.parentNode;
}
return curr?.closest(selector) ?? null;
}
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
const HOVER_OR_ACTIVE_CLASS = 'cdk-column-resize-hover-or-active';
const WITH_RESIZED_COLUMN_CLASS = 'cdk-column-resize-with-resized-column';
const COLUMN_RESIZE_OPTIONS = new InjectionToken('CdkColumnResizeOptions');
/**
* Base class for ColumnResize directives which attach to mat-table elements to
* provide common events and services for column resizing.
*/
class ColumnResize {
constructor() {
this._renderer = inject(Renderer2);
this.destroyed = new Subject();
/** Unique ID for this table instance. */
this.selectorId = inject(_IdGenerator).getId('cdk-column-resize-');
/** @docs-private Whether a call to updateStickyColumnStyles is pending after a resize. */
this._flushPending = false;
/**
* Whether to update the column's width continuously as the mouse position
* changes, or to wait until mouseup to apply the new size.
*/
this.liveResizeUpdates = inject(COLUMN_RESIZE_OPTIONS, { optional: true })?.liveResizeUpdates ?? true;
}
ngAfterViewInit() {
this.elementRef.nativeElement.classList.add(this.getUniqueCssClass());
this._listenForRowHoverEvents();
this._listenForResizeActivity();
this._listenForHoverActivity();
}
ngOnDestroy() {
this._eventCleanups?.forEach(cleanup => cleanup());
this.destroyed.next();
this.destroyed.complete();
}
/** Gets the unique CSS class name for this table instance. */
getUniqueCssClass() {
return this.selectorId;
}
/** Gets the ID for this table used for column size persistance. */
getTableId() {
return String(this.elementRef.nativeElement.id);
}
/** Called when a column in the table is resized. Applies a css class to the table element. */
setResized() {
this.elementRef.nativeElement.classList.add(WITH_RESIZED_COLUMN_CLASS);
}
_listenForRowHoverEvents() {
this.ngZone.runOutsideAngular(() => {
const element = this.elementRef.nativeElement;
this._eventCleanups = [
this._renderer.listen(element, 'mouseover', (event) => {
this.eventDispatcher.headerCellHovered.next(closest(event.target, HEADER_CELL_SELECTOR));
}),
this._renderer.listen(element, 'mouseleave', (event) => {
if (event.relatedTarget &&
!event.relatedTarget.matches(RESIZE_OVERLAY_SELECTOR)) {
this.eventDispatcher.headerCellHovered.next(null);
}
}),
];
});
}
_listenForResizeActivity() {
merge(this.eventDispatcher.overlayHandleActiveForCell.pipe(mapTo(undefined)), this.notifier.triggerResize.pipe(mapTo(undefined)), this.notifier.resizeCompleted.pipe(mapTo(undefined)))
.pipe(take(1), takeUntil(this.destroyed))
.subscribe(() => {
this.setResized();
});
}
_listenForHoverActivity() {
this.eventDispatcher.headerRowHoveredOrActiveDistinct
.pipe(startWith(null), pairwise(), takeUntil(this.destroyed))
.subscribe(([previousRow, hoveredRow]) => {
if (hoveredRow) {
hoveredRow.classList.add(HOVER_OR_ACTIVE_CLASS);
}
if (previousRow) {
previousRow.classList.remove(HOVER_OR_ACTIVE_CLASS);
}
});
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ColumnResize, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
/** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.5", type: ColumnResize, isStandalone: true, inputs: { liveResizeUpdates: "liveResizeUpdates" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ColumnResize, decorators: [{
type: Directive
}], propDecorators: { liveResizeUpdates: [{
type: Input
}] } });
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/**
* Originating source of column resize events within a table.
* @docs-private
*/
class ColumnResizeNotifierSource {
constructor() {
/** Emits when an in-progress resize is canceled. */
this.resizeCanceled = new Subject();
/** Emits when a resize is applied. */
this.resizeCompleted = new Subject();
/** Triggers a resize action. */
this.triggerResize = new Subject();
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ColumnResizeNotifierSource, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ColumnResizeNotifierSource }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ColumnResizeNotifierSource, decorators: [{
type: Injectable
}] });
/** Service for triggering column resizes imperatively or being notified of them. */
class ColumnResizeNotifier {
constructor() {
this._source = inject(ColumnResizeNotifierSource);
/** Emits whenever a column is resized. */
this.resizeCompleted = this._source.resizeCompleted;
}
/** Instantly resizes the specified column. */
resize(columnId, size) {
this._source.triggerResize.next({
columnId,
size,
completeImmediately: true,
isStickyColumn: true,
});
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ColumnResizeNotifier, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ColumnResizeNotifier }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ColumnResizeNotifier, decorators: [{
type: Injectable
}] });
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/** Coordinates events between the column resize directives. */
class HeaderRowEventDispatcher {
constructor() {
this._ngZone = inject(NgZone);
/**
* Emits the currently hovered header cell or null when no header cells are hovered.
* Exposed publicly for events to feed in, but subscribers should use headerCellHoveredDistinct,
* defined below.
*/
this.headerCellHovered = new Subject();
/**
* Emits the header cell for which a user-triggered resize is active or null
* when no resize is in progress.
*/
this.overlayHandleActiveForCell = new Subject();
/** Distinct and shared version of headerCellHovered. */
this.headerCellHoveredDistinct = this.headerCellHovered.pipe(distinctUntilChanged(), share());
/**
* Emits the header that is currently hovered or hosting an active resize event (with active
* taking precedence).
*/
this.headerRowHoveredOrActiveDistinct = combineLatest([
this.headerCellHoveredDistinct.pipe(map(cell => closest(cell, HEADER_ROW_SELECTOR)), startWith(null), distinctUntilChanged()),
this.overlayHandleActiveForCell.pipe(map(cell => closest(cell, HEADER_ROW_SELECTOR)), startWith(null), distinctUntilChanged()),
]).pipe(skip(1), // Ignore initial [null, null] emission.
map(([hovered, active]) => active || hovered), distinctUntilChanged(), share());
this._headerRowHoveredOrActiveDistinctReenterZone = this.headerRowHoveredOrActiveDistinct.pipe(this._enterZone(), share());
// Optimization: Share row events observable with subsequent callers.
// At startup, calls will be sequential by row (and typically there's only one).
this._lastSeenRow = null;
this._lastSeenRowHover = null;
}
/**
* Emits whether the specified row should show its overlay controls.
* Emission occurs within the NgZone.
*/
resizeOverlayVisibleForHeaderRow(row) {
if (row !== this._lastSeenRow) {
this._lastSeenRow = row;
this._lastSeenRowHover = this._headerRowHoveredOrActiveDistinctReenterZone.pipe(map(hoveredRow => hoveredRow === row), distinctUntilChanged(), share());
}
return this._lastSeenRowHover;
}
_enterZone() {
return (source) => new Observable(observer => source.subscribe({
next: value => this._ngZone.run(() => observer.next(value)),
error: err => observer.error(err),
complete: () => observer.complete(),
}));
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: HeaderRowEventDispatcher, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: HeaderRowEventDispatcher }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: HeaderRowEventDispatcher, decorators: [{
type: Injectable
}] });
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/**
* @docs-private
*/
class _Schedule {
constructor() {
this.tasks = [];
this.endTasks = [];
}
}
/** Injection token used to provide a coalesced style scheduler. */
const _COALESCED_STYLE_SCHEDULER = new InjectionToken('_COALESCED_STYLE_SCHEDULER');
/**
* Allows grouping up CSSDom mutations after the current execution context.
* This can significantly improve performance when separate consecutive functions are
* reading from the CSSDom and then mutating it.
*
* @docs-private
*/
class _CoalescedStyleScheduler {
constructor() {
this._currentSchedule = null;
this._ngZone = inject(NgZone);
}
/**
* Schedules the specified task to run at the end of the current VM turn.
*/
schedule(task) {
this._createScheduleIfNeeded();
this._currentSchedule.tasks.push(task);
}
/**
* Schedules the specified task to run after other scheduled tasks at the end of the current
* VM turn.
*/
scheduleEnd(task) {
this._createScheduleIfNeeded();
this._currentSchedule.endTasks.push(task);
}
_createScheduleIfNeeded() {
if (this._currentSchedule) {
return;
}
this._currentSchedule = new _Schedule();
this._ngZone.runOutsideAngular(() =>
// TODO(mmalerba): Scheduling this using something that runs less frequently
// (e.g. requestAnimationFrame, setTimeout, etc.) causes noticeable jank with the column
// resizer. We should audit the usages of schedule / scheduleEnd in that component and see
// if we can refactor it so that we don't need to flush the tasks quite so frequently.
queueMicrotask(() => {
while (this._currentSchedule.tasks.length || this._currentSchedule.endTasks.length) {
const schedule = this._currentSchedule;
// Capture new tasks scheduled by the current set of tasks.
this._currentSchedule = new _Schedule();
for (const task of schedule.tasks) {
task();
}
for (const task of schedule.endTasks) {
task();
}
}
this._currentSchedule = null;
}));
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: _CoalescedStyleScheduler, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: _CoalescedStyleScheduler }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: _CoalescedStyleScheduler, decorators: [{
type: Injectable
}] });
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/**
* Provides an implementation for resizing a column.
* The details of how resizing works for tables for flex mat-tables are quite different.
*/
class ResizeStrategy {
constructor() {
this._pendingResizeDelta = null;
}
/** Adjusts the width of the table element by the specified delta. */
updateTableWidthAndStickyColumns(delta) {
if (this._pendingResizeDelta === null) {
const tableElement = this.columnResize.elementRef.nativeElement;
const tableWidth = this.getElementWidth(tableElement);
this.styleScheduler.schedule(() => {
tableElement.style.width = coerceCssPixelValue(tableWidth + this._pendingResizeDelta);
this._pendingResizeDelta = null;
});
this.styleScheduler.scheduleEnd(() => {
this.table.updateStickyColumnStyles();
});
}
this._pendingResizeDelta = (this._pendingResizeDelta ?? 0) + delta;
}
/** Gets the style.width pixels on the specified element if present, otherwise its offsetWidth. */
getElementWidth(element) {
// Optimization: Check style.width first as we probably set it already before reading
// offsetWidth which triggers layout.
return coercePixelsFromCssValue(element.style.width) || element.offsetWidth;
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ResizeStrategy, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ResizeStrategy }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ResizeStrategy, decorators: [{
type: Injectable
}] });
/**
* The optimally performing resize strategy for <table> elements with table-layout: fixed.
* Tested against and outperformed:
* CSS selector
* CSS selector w/ CSS variable
* Updating all cell nodes
*/
class TableLayoutFixedResizeStrategy extends ResizeStrategy {
constructor() {
super(...arguments);
this.columnResize = inject(ColumnResize);
this.styleScheduler = inject(_COALESCED_STYLE_SCHEDULER);
this.table = inject(CdkTable);
}
applyColumnSize(_, columnHeader, sizeInPx, previousSizeInPx) {
const delta = sizeInPx - (previousSizeInPx ?? this.getElementWidth(columnHeader));
if (delta === 0) {
return;
}
this.styleScheduler.schedule(() => {
columnHeader.style.width = coerceCssPixelValue(sizeInPx);
});
this.updateTableWidthAndStickyColumns(delta);
}
applyMinColumnSize(_, columnHeader, sizeInPx) {
const currentWidth = this.getElementWidth(columnHeader);
const newWidth = Math.max(currentWidth, sizeInPx);
this.applyColumnSize(_, columnHeader, newWidth, currentWidth);
}
applyMaxColumnSize(_, columnHeader, sizeInPx) {
const currentWidth = this.getElementWidth(columnHeader);
const newWidth = Math.min(currentWidth, sizeInPx);
this.applyColumnSize(_, columnHeader, newWidth, currentWidth);
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: TableLayoutFixedResizeStrategy, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: TableLayoutFixedResizeStrategy }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: TableLayoutFixedResizeStrategy, decorators: [{
type: Injectable
}] });
/**
* The optimally performing resize strategy for flex mat-tables.
* Tested against and outperformed:
* CSS selector w/ CSS variable
* Updating all mat-cell nodes
*/
class CdkFlexTableResizeStrategy extends ResizeStrategy {
constructor() {
super(...arguments);
this.columnResize = inject(ColumnResize);
this.styleScheduler = inject(_COALESCED_STYLE_SCHEDULER);
this.table = inject(CdkTable);
this._nonce = inject(CSP_NONCE, { optional: true });
this._document = inject(DOCUMENT);
this._columnIndexes = new Map();
this._columnProperties = new Map();
this._indexSequence = 0;
this.defaultMinSize = 0;
this.defaultMaxSize = Number.MAX_SAFE_INTEGER;
}
applyColumnSize(cssFriendlyColumnName, columnHeader, sizeInPx, previousSizeInPx) {
// Optimization: Check applied width first as we probably set it already before reading
// offsetWidth which triggers layout.
const delta = sizeInPx -
(previousSizeInPx ??
(this._getAppliedWidth(cssFriendlyColumnName) || columnHeader.offsetWidth));
if (delta === 0) {
return;
}
const cssSize = coerceCssPixelValue(sizeInPx);
this._applyProperty(cssFriendlyColumnName, 'flex', `0 0.01 ${cssSize}`);
this.updateTableWidthAndStickyColumns(delta);
}
applyMinColumnSize(cssFriendlyColumnName, _, sizeInPx) {
const cssSize = coerceCssPixelValue(sizeInPx);
this._applyProperty(cssFriendlyColumnName, 'min-width', cssSize, sizeInPx !== this.defaultMinSize);
this.updateTableWidthAndStickyColumns(0);
}
applyMaxColumnSize(cssFriendlyColumnName, _, sizeInPx) {
const cssSize = coerceCssPixelValue(sizeInPx);
this._applyProperty(cssFriendlyColumnName, 'max-width', cssSize, sizeInPx !== this.defaultMaxSize);
this.updateTableWidthAndStickyColumns(0);
}
getColumnCssClass(cssFriendlyColumnName) {
return `cdk-column-${cssFriendlyColumnName}`;
}
ngOnDestroy() {
this._styleElement?.remove();
this._styleElement = undefined;
}
_getPropertyValue(cssFriendlyColumnName, key) {
const properties = this._getColumnPropertiesMap(cssFriendlyColumnName);
return properties.get(key);
}
_getAppliedWidth(cssFriendslyColumnName) {
return coercePixelsFromFlexValue(this._getPropertyValue(cssFriendslyColumnName, 'flex'));
}
_applyProperty(cssFriendlyColumnName, key, value, enable = true) {
const properties = this._getColumnPropertiesMap(cssFriendlyColumnName);
this.styleScheduler.schedule(() => {
if (enable) {
properties.set(key, value);
}
else {
properties.delete(key);
}
this._applySizeCss(cssFriendlyColumnName);
});
}
_getStyleSheet() {
if (!this._styleElement) {
this._styleElement = this._document.createElement('style');
if (this._nonce) {
this._styleElement.setAttribute('nonce', this._nonce);
}
this._styleElement.appendChild(this._document.createTextNode(''));
this._document.head.appendChild(this._styleElement);
}
return this._styleElement.sheet;
}
_getColumnPropertiesMap(cssFriendlyColumnName) {
let properties = this._columnProperties.get(cssFriendlyColumnName);
if (properties === undefined) {
properties = new Map();
this._columnProperties.set(cssFriendlyColumnName, properties);
}
return properties;
}
_applySizeCss(cssFriendlyColumnName) {
const properties = this._getColumnPropertiesMap(cssFriendlyColumnName);
const propertyKeys = Array.from(properties.keys());
let index = this._columnIndexes.get(cssFriendlyColumnName);
if (index === undefined) {
if (!propertyKeys.length) {
// Nothing to set or unset.
return;
}
index = this._indexSequence++;
this._columnIndexes.set(cssFriendlyColumnName, index);
}
else {
this._getStyleSheet().deleteRule(index);
}
const columnClassName = this.getColumnCssClass(cssFriendlyColumnName);
const tableClassName = this.columnResize.getUniqueCssClass();
const selector = `.${tableClassName} .${columnClassName}`;
const body = propertyKeys.map(key => `${key}:${properties.get(key)}`).join(';');
this._getStyleSheet().insertRule(`${selector} {${body}}`, index);
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: CdkFlexTableResizeStrategy, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: CdkFlexTableResizeStrategy }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: CdkFlexTableResizeStrategy, decorators: [{
type: Injectable
}] });
/** Converts CSS pixel values to numbers, eg "123px" to 123. Returns NaN for non pixel values. */
function coercePixelsFromCssValue(cssValue) {
return Number(cssValue.match(/(\d+)px/)?.[1]);
}
/**
* Converts CSS flex values as set in CdkFlexTableResizeStrategy to numbers,
* eg "0 0.01 123px" to 123.
*/
function coercePixelsFromFlexValue(flexValue) {
return Number(flexValue?.match(/0 0\.01 (\d+)px/)?.[1]);
}
const TABLE_LAYOUT_FIXED_RESIZE_STRATEGY_PROVIDER = {
provide: ResizeStrategy,
useClass: TableLayoutFixedResizeStrategy,
};
const FLEX_RESIZE_STRATEGY_PROVIDER = {
provide: ResizeStrategy,
useClass: CdkFlexTableResizeStrategy,
};
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
const PROVIDERS = [
ColumnResizeNotifier,
HeaderRowEventDispatcher,
ColumnResizeNotifierSource,
{ provide: _COALESCED_STYLE_SCHEDULER, useClass: _CoalescedStyleScheduler },
];
const TABLE_PROVIDERS = [
...PROVIDERS,
TABLE_LAYOUT_FIXED_RESIZE_STRATEGY_PROVIDER,
];
const FLEX_PROVIDERS = [...PROVIDERS, FLEX_RESIZE_STRATEGY_PROVIDER];
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/**
* Explicitly enables column resizing for a table-based cdk-table.
* Individual columns must be annotated specifically.
*/
class CdkColumnResize extends ColumnResize {
constructor() {
super(...arguments);
this.columnResizeNotifier = inject(ColumnResizeNotifier);
this.elementRef = inject(ElementRef);
this.eventDispatcher = inject(HeaderRowEventDispatcher);
this.ngZone = inject(NgZone);
this.notifier = inject(ColumnResizeNotifierSource);
this.table = inject(CdkTable);
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: CdkColumnResize, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
/** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.5", type: CdkColumnResize, isStandalone: true, selector: "table[cdk-table][columnResize]", providers: [...TABLE_PROVIDERS, { provide: ColumnResize, useExisting: CdkColumnResize }], usesInheritance: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: CdkColumnResize, decorators: [{
type: Directive,
args: [{
selector: 'table[cdk-table][columnResize]',
providers: [...TABLE_PROVIDERS, { provide: ColumnResize, useExisting: CdkColumnResize }],
}]
}] });
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/**
* Explicitly enables column resizing for a flexbox-based cdk-table.
* Individual columns must be annotated specifically.
*/
class CdkColumnResizeFlex extends ColumnResize {
constructor() {
super(...arguments);
this.columnResizeNotifier = inject(ColumnResizeNotifier);
this.elementRef = inject(ElementRef);
this.eventDispatcher = inject(HeaderRowEventDispatcher);
this.ngZone = inject(NgZone);
this.notifier = inject(ColumnResizeNotifierSource);
this.table = inject(CdkTable);
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: CdkColumnResizeFlex, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
/** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.5", type: CdkColumnResizeFlex, isStandalone: true, selector: "cdk-table[columnResize]", providers: [...FLEX_PROVIDERS, { provide: ColumnResize, useExisting: CdkColumnResizeFlex }], usesInheritance: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: CdkColumnResizeFlex, decorators: [{
type: Directive,
args: [{
selector: 'cdk-table[columnResize]',
providers: [...FLEX_PROVIDERS, { provide: ColumnResize, useExisting: CdkColumnResizeFlex }],
}]
}] });
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/**
* One of two NgModules for use with CdkColumnResize.
* When using this module, columns are not resizable by default.
*/
class CdkColumnResizeModule {
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: CdkColumnResizeModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
/** @nocollapse */ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.5", ngImport: i0, type: CdkColumnResizeModule, imports: [CdkColumnResize, CdkColumnResizeFlex], exports: [CdkColumnResize, CdkColumnResizeFlex] }); }
/** @nocollapse */ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: CdkColumnResizeModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: CdkColumnResizeModule, decorators: [{
type: NgModule,
args: [{
imports: [CdkColumnResize, CdkColumnResizeFlex],
exports: [CdkColumnResize, CdkColumnResizeFlex],
}]
}] });
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/**
* Can be provided by the host application to enable persistence of column resize state.
*/
class ColumnSizeStore {
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ColumnSizeStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ColumnSizeStore }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ColumnSizeStore, decorators: [{
type: Injectable
}] });
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/** Tracks state of resize events in progress. */
class ResizeRef {
constructor(origin, overlayRef, minWidthPx, maxWidthPx, liveUpdates = true) {
this.origin = origin;
this.overlayRef = overlayRef;
this.minWidthPx = minWidthPx;
this.maxWidthPx = maxWidthPx;
this.liveUpdates = liveUpdates;
}
}
const OVERLAY_ACTIVE_CLASS = 'cdk-resizable-overlay-thumb-active';
/**
* Base class for Resizable directives which are applied to column headers to make those columns
* resizable.
*/
class Resizable {
constructor() {
this.isResizable = true;
this.minWidthPxInternal = 0;
this.maxWidthPxInternal = Number.MAX_SAFE_INTEGER;
this.destroyed = new Subject();
this._viewInitialized = false;
this._isDestroyed = false;
}
/** The minimum width to allow the column to be sized to. */
get minWidthPx() {
return this.minWidthPxInternal;
}
set minWidthPx(value) {
if (value) {
this.minWidthPxInternal = value;
}
this.columnResize.setResized();
if (this.elementRef.nativeElement && this._viewInitialized) {
this._applyMinWidthPx();
}
}
/** The maximum width to allow the column to be sized to. */
get maxWidthPx() {
return this.maxWidthPxInternal;
}
set maxWidthPx(value) {
if (value) {
this.maxWidthPxInternal = value;
}
this.columnResize.setResized();
if (this.elementRef.nativeElement && this._viewInitialized) {
this._applyMaxWidthPx();
}
}
ngAfterViewInit() {
if (this.isResizable) {
this._listenForRowHoverEvents();
this._listenForResizeEvents();
this._appendInlineHandle();
this.styleScheduler.scheduleEnd(() => {
if (this._isDestroyed)
return;
this._viewInitialized = true;
this._applyMinWidthPx();
this._applyMaxWidthPx();
});
}
}
ngOnDestroy() {
this._isDestroyed = true;
this.destroyed.next();
this.destroyed.complete();
this.inlineHandle?.remove();
this.overlayRef?.dispose();
}
_createOverlayForHandle() {
// Use of overlays allows us to properly capture click events spanning parts
// of two table cells and is also useful for displaying a resize thumb
// over both cells and extending it down the table as needed.
const isRtl = this.directionality.value === 'rtl';
const positionStrategy = this.overlay
.position()
.flexibleConnectedTo(this.elementRef.nativeElement)
.withFlexibleDimensions(false)
.withGrowAfterOpen(false)
.withPush(false)
.withDefaultOffsetX(isRtl ? 1 : 0)
.withPositions([
{
originX: isRtl ? 'start' : 'end',
originY: 'top',
overlayX: 'center',
overlayY: 'top',
},
]);
return this.overlay.create({
// Always position the overlay based on left-indexed coordinates.
direction: 'ltr',
disposeOnNavigation: true,
positionStrategy,
scrollStrategy: this.overlay.scrollStrategies.reposition(),
width: '16px',
});
}
_listenForRowHoverEvents() {
const element = this.elementRef.nativeElement;
const takeUntilDestroyed = takeUntil(this.destroyed);
this.eventDispatcher
.resizeOverlayVisibleForHeaderRow(closest(element, HEADER_ROW_SELECTOR))
.pipe(takeUntilDestroyed)
.subscribe(hoveringRow => {
if (hoveringRow) {
if (!this.overlayRef) {
this.overlayRef = this._createOverlayForHandle();
}
this._showHandleOverlay();
}
else if (this.overlayRef) {
// todo - can't detach during an active resize - need to work that out
this.overlayRef.detach();
}
});
}
_listenForResizeEvents() {
const takeUntilDestroyed = takeUntil(this.destroyed);
merge(this.resizeNotifier.resizeCanceled, this.resizeNotifier.triggerResize)
.pipe(takeUntilDestroyed, filter(columnSize => columnSize.columnId === this.columnDef.name))
.subscribe(({ size, previousSize, completeImmediately }) => {
this.elementRef.nativeElement.classList.add(OVERLAY_ACTIVE_CLASS);
this._applySize(size, previousSize);
if (completeImmediately) {
this._completeResizeOperation();
}
});
merge(this.resizeNotifier.resizeCanceled, this.resizeNotifier.resizeCompleted)
.pipe(takeUntilDestroyed)
.subscribe(columnSize => {
this._cleanUpAfterResize(columnSize);
});
}
_completeResizeOperation() {
this.ngZone.run(() => {
this.resizeNotifier.resizeCompleted.next({
columnId: this.columnDef.name,
size: this.elementRef.nativeElement.offsetWidth,
});
});
}
_cleanUpAfterResize(columnSize) {
this.elementRef.nativeElement.classList.remove(OVERLAY_ACTIVE_CLASS);
if (this.overlayRef && this.overlayRef.hasAttached()) {
this._updateOverlayHandleHeight();
this.overlayRef.updatePosition();
if (columnSize.columnId === this.columnDef.name) {
this.inlineHandle.focus();
}
}
}
_createHandlePortal() {
const injector = Injector.create({
parent: this.injector,
providers: [
{
provide: ResizeRef,
useValue: new ResizeRef(this.elementRef, this.overlayRef, this.minWidthPx, this.maxWidthPx),
},
],
});
return new ComponentPortal(this.getOverlayHandleComponentType(), this.viewContainerRef, injector);
}
_showHandleOverlay() {
this._updateOverlayHandleHeight();
this.overlayRef.attach(this._createHandlePortal());
// Needed to ensure that all of the lifecycle hooks inside the overlay run immediately.
this.changeDetectorRef.markForCheck();
}
_updateOverlayHandleHeight() {
this.overlayRef.updateSize({ height: this.elementRef.nativeElement.offsetHeight });
}
_applySize(sizeInPixels, previousSize) {
const sizeToApply = Math.min(Math.max(sizeInPixels, this.minWidthPx, 0), this.maxWidthPx);
this.resizeStrategy.applyColumnSize(this.columnDef.cssClassFriendlyName, this.elementRef.nativeElement, sizeToApply, previousSize);
}
_applyMinWidthPx() {
this.resizeStrategy.applyMinColumnSize(this.columnDef.cssClassFriendlyName, this.elementRef.nativeElement, this.minWidthPx);
}
_applyMaxWidthPx() {
this.resizeStrategy.applyMaxColumnSize(this.columnDef.cssClassFriendlyName, this.elementRef.nativeElement, this.maxWidthPx);
}
_appendInlineHandle() {
this.styleScheduler.schedule(() => {
this.inlineHandle = this.document.createElement('div');
this.inlineHandle.tabIndex = 0;
this.inlineHandle.className = this.getInlineHandleCssClassName();
// TODO: Apply correct aria role (probably slider) after a11y spec questions resolved.
this.elementRef.nativeElement.appendChild(this.inlineHandle);
});
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: Resizable, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
/** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.5", type: Resizable, isStandalone: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: Resizable, decorators: [{
type: Directive
}] });
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
// TODO: Take another look at using cdk drag drop. IIRC I ran into a couple
// good reasons for not using it but I don't remember what they were at this point.
/**
* Base class for a component shown over the edge of a resizable column that is responsible
* for handling column resize mouse events and displaying any visible UI on the column edge.
*/
class ResizeOverlayHandle {
constructor() {
this._renderer = inject(Renderer2);
this.destroyed = new Subject();
this._cumulativeDeltaX = 0;
}
ngAfterViewInit() {
this._listenForMouseEvents();
}
ngOnDestroy() {
this.destroyed.next();
this.destroyed.complete();
}
_listenForMouseEvents() {
this.ngZone.runOutsideAngular(() => {
this._observableFromEvent(this.elementRef.nativeElement, 'mouseenter')
.pipe(mapTo(this.resizeRef.origin.nativeElement), takeUntil(this.destroyed))
.subscribe(cell => this.eventDispatcher.headerCellHovered.next(cell));
this._observableFromEvent(this.elementRef.nativeElement, 'mouseleave')
.pipe(map(event => event.relatedTarget && closest(event.relatedTarget, HEADER_CELL_SELECTOR)), takeUntil(this.destroyed))
.subscribe(cell => this.eventDispatcher.headerCellHovered.next(cell));
this._observableFromEvent(this.elementRef.nativeElement, 'mousedown')
.pipe(takeUntil(this.destroyed))
.subscribe(mousedownEvent => {
this._dragStarted(mousedownEvent);
});
});
}
_dragStarted(mousedownEvent) {
// Only allow dragging using the left mouse button.
if (mousedownEvent.button !== 0) {
return;
}
const mouseup = this._observableFromEvent(this.document, 'mouseup');
const mousemove = this._observableFromEvent(this.document, 'mousemove');
const escape = this._observableFromEvent(this.document, 'keyup').pipe(filter(event => event.keyCode === ESCAPE));
const startX = mousedownEvent.screenX;
const initialSize = this._getOriginWidth();
let overlayOffset = 0;
let originOffset = this._getOriginOffset();
let size = initialSize;
let overshot = 0;
this._cumulativeDeltaX = 0;
this.updateResizeActive(true);
mouseup.pipe(takeUntil(merge(escape, this.destroyed))).subscribe(({ screenX }) => {
this.styleScheduler.scheduleEnd(() => {
this._notifyResizeEnded(size, screenX !== startX);
});
});
escape.pipe(takeUntil(merge(mouseup, this.destroyed))).subscribe(() => {
this._notifyResizeEnded(initialSize);
});
mousemove
.pipe(map(({ screenX }) => screenX), startWith(startX), distinctUntilChanged(), pairwise(), takeUntil(merge(mouseup, escape, this.destroyed)))
.subscribe(([prevX, currX]) => {
let deltaX = currX - prevX;
if (!this.resizeRef.liveUpdates) {
this._cumulativeDeltaX += deltaX;
const sizeDelta = this._computeNewSize(size, this._cumulativeDeltaX) - size;
this._updateOverlayOffset(sizeDelta);
return;
}
// If the mouse moved further than the resize was able to match, limit the
// movement of the overlay to match the actual size and position of the origin.
if (overshot !== 0) {
if ((overshot < 0 && deltaX < 0) || (overshot > 0 && deltaX > 0)) {
overshot += deltaX;
return;
}
else {
const remainingOvershot = overshot + deltaX;
overshot =
overshot > 0 ? Math.max(remainingOvershot, 0) : Math.min(remainingOvershot, 0);
deltaX = remainingOvershot - overshot;
if (deltaX === 0) {
return;
}
}
}
this._triggerResize(size, deltaX);
this.styleScheduler.scheduleEnd(() => {
const originNewSize = this._getOriginWidth();
const originNewOffset = this._getOriginOffset();
const originOffsetDeltaX = originNewOffset - originOffset;
const originSizeDeltaX = originNewSize - size;
size = originNewSize;
originOffset = originNewOffset;
overshot += deltaX + (this._isLtr() ? -originSizeDeltaX : originSizeDeltaX);
overlayOffset += originOffsetDeltaX + (this._isLtr() ? originSizeDeltaX : 0);
this._updateOverlayOffset(overlayOffset);
});
});
}
updateResizeActive(active) {
this.eventDispatcher.overlayHandleActiveForCell.next(active ? this.resizeRef.origin.nativeElement : null);
}
_triggerResize(startSize, deltaX) {
this.resizeNotifier.triggerResize.next({
columnId: this.columnDef.name,
size: this._computeNewSize(startSize, deltaX),
previousSize: startSize,
isStickyColumn: this.columnDef.sticky || this.columnDef.stickyEnd,
});
}
_computeNewSize(startSize, deltaX) {
let computedNewSize = startSize + (this._isLtr() ? deltaX : -deltaX);
computedNewSize = Math.min(Math.max(computedNewSize, this.resizeRef.minWidthPx, 0), this.resizeRef.maxWidthPx);
return computedNewSize;
}
_getOriginWidth() {
return this.resizeRef.origin.nativeElement.offsetWidth;
}
_getOriginOffset() {
return this.resizeRef.origin.nativeElement.offsetLeft;
}
_updateOverlayOffset(offset) {
this.resizeRef.overlayRef.overlayElement.style.transform = `translateX(${coerceCssPixelValue(offset)})`;
}
_isLtr() {
return this.directionality.value === 'ltr';
}
_notifyResizeEnded(size, completedSuccessfully = false) {
this.updateResizeActive(false);
this.ngZone.run(() => {
const sizeMessage = {
columnId: this.columnDef.name,
size: this._computeNewSize(size, this._cumulativeDeltaX),
};
if (completedSuccessfully) {
if (!this.resizeRef.liveUpdates) {
this._triggerResize(size, this._cumulativeDeltaX);
}
this.resizeNotifier.resizeCompleted.next(sizeMessage);
}
else {
this.resizeNotifier.resizeCanceled.next(sizeMessage);
}
});
}
_observableFromEvent(element, name) {
return new Observable(subscriber => {
const handler = (event) => subscriber.next(event);
const cleanup = this._renderer.listen(element, name, handler);
return () => {
cleanup();
subscriber.complete();
};
});
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ResizeOverlayHandle, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
/** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.5", type: ResizeOverlayHandle, isStandalone: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: ResizeOverlayHandle, decorators: [{
type: Directive
}] });
/**
* Generated bundle index. Do not edit.
*/
export { COLUMN_RESIZE_OPTIONS, CdkColumnResize, CdkColumnResizeFlex, CdkColumnResizeModule, CdkFlexTableResizeStrategy, ColumnResize, ColumnResizeNotifier, ColumnResizeNotifierSource, ColumnSizeStore, FLEX_PROVIDERS, FLEX_RESIZE_STRATEGY_PROVIDER, HeaderRowEventDispatcher, Resizable, ResizeOverlayHandle, ResizeRef, ResizeStrategy, TABLE_LAYOUT_FIXED_RESIZE_STRATEGY_PROVIDER, TABLE_PROVIDERS, TableLayoutFixedResizeStrategy, _COALESCED_STYLE_SCHEDULER, _CoalescedStyleScheduler, _Schedule };
//# sourceMappingURL=mtxColumnResize.mjs.map