UNPKG

@ng-matero/extensions

Version:
1,054 lines (1,036 loc) 47.7 kB
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 &lt;table&gt; 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