| import { Directionality } from '@angular/cdk/bidi'; |
| import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
| import { isDataSource, _VIEW_REPEATER_STRATEGY, _DisposeViewRepeaterStrategy } from '@angular/cdk/collections'; |
| export { DataSource } from '@angular/cdk/collections'; |
| import { Platform } from '@angular/cdk/platform'; |
| import { ViewportRuler, ScrollingModule } from '@angular/cdk/scrolling'; |
| import { DOCUMENT } from '@angular/common'; |
| import { InjectionToken, Directive, TemplateRef, Inject, Optional, Input, ContentChild, ElementRef, Injectable, NgZone, IterableDiffers, ViewContainerRef, Component, ChangeDetectionStrategy, ViewEncapsulation, EmbeddedViewRef, ChangeDetectorRef, Attribute, SkipSelf, ViewChild, ContentChildren, NgModule } from '@angular/core'; |
| import { Subject, from, BehaviorSubject, isObservable, of } from 'rxjs'; |
| import { takeUntil, take } from 'rxjs/operators'; |
| |
| /** |
| * @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.io/license |
| */ |
| /** |
| * Mixin to provide a directive with a function that checks if the sticky input has been |
| * changed since the last time the function was called. Essentially adds a dirty-check to the |
| * sticky value. |
| * @docs-private |
| */ |
| function mixinHasStickyInput(base) { |
| return class extends base { |
| constructor(...args) { |
| super(...args); |
| this._sticky = false; |
| /** Whether the sticky input has changed since it was last checked. */ |
| this._hasStickyChanged = false; |
| } |
| /** Whether sticky positioning should be applied. */ |
| get sticky() { return this._sticky; } |
| set sticky(v) { |
| const prevValue = this._sticky; |
| this._sticky = coerceBooleanProperty(v); |
| this._hasStickyChanged = prevValue !== this._sticky; |
| } |
| /** Whether the sticky value has changed since this was last called. */ |
| hasStickyChanged() { |
| const hasStickyChanged = this._hasStickyChanged; |
| this._hasStickyChanged = false; |
| return hasStickyChanged; |
| } |
| /** Resets the dirty check for cases where the sticky state has been used without checking. */ |
| resetStickyChanged() { |
| this._hasStickyChanged = false; |
| } |
| }; |
| } |
| |
| /** |
| * @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.io/license |
| */ |
| /** |
| * Used to provide a table to some of the sub-components without causing a circular dependency. |
| * @docs-private |
| */ |
| const CDK_TABLE = new InjectionToken('CDK_TABLE'); |
| /** Injection token that can be used to specify the text column options. */ |
| const TEXT_COLUMN_OPTIONS = new InjectionToken('text-column-options'); |
| |
| /** |
| * @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.io/license |
| */ |
| /** |
| * Cell definition for a CDK table. |
| * Captures the template of a column's data row cell as well as cell-specific properties. |
| */ |
| class CdkCellDef { |
| constructor(/** @docs-private */ template) { |
| this.template = template; |
| } |
| } |
| CdkCellDef.decorators = [ |
| { type: Directive, args: [{ selector: '[cdkCellDef]' },] } |
| ]; |
| CdkCellDef.ctorParameters = () => [ |
| { type: TemplateRef } |
| ]; |
| /** |
| * Header cell definition for a CDK table. |
| * Captures the template of a column's header cell and as well as cell-specific properties. |
| */ |
| class CdkHeaderCellDef { |
| constructor(/** @docs-private */ template) { |
| this.template = template; |
| } |
| } |
| CdkHeaderCellDef.decorators = [ |
| { type: Directive, args: [{ selector: '[cdkHeaderCellDef]' },] } |
| ]; |
| CdkHeaderCellDef.ctorParameters = () => [ |
| { type: TemplateRef } |
| ]; |
| /** |
| * Footer cell definition for a CDK table. |
| * Captures the template of a column's footer cell and as well as cell-specific properties. |
| */ |
| class CdkFooterCellDef { |
| constructor(/** @docs-private */ template) { |
| this.template = template; |
| } |
| } |
| CdkFooterCellDef.decorators = [ |
| { type: Directive, args: [{ selector: '[cdkFooterCellDef]' },] } |
| ]; |
| CdkFooterCellDef.ctorParameters = () => [ |
| { type: TemplateRef } |
| ]; |
| // Boilerplate for applying mixins to CdkColumnDef. |
| /** @docs-private */ |
| class CdkColumnDefBase { |
| } |
| const _CdkColumnDefBase = mixinHasStickyInput(CdkColumnDefBase); |
| /** |
| * Column definition for the CDK table. |
| * Defines a set of cells available for a table column. |
| */ |
| class CdkColumnDef extends _CdkColumnDefBase { |
| constructor(_table) { |
| super(); |
| this._table = _table; |
| this._stickyEnd = false; |
| } |
| /** Unique name for this column. */ |
| get name() { return this._name; } |
| set name(name) { this._setNameInput(name); } |
| /** |
| * Whether this column should be sticky positioned on the end of the row. Should make sure |
| * that it mimics the `CanStick` mixin such that `_hasStickyChanged` is set to true if the value |
| * has been changed. |
| */ |
| get stickyEnd() { |
| return this._stickyEnd; |
| } |
| set stickyEnd(v) { |
| const prevValue = this._stickyEnd; |
| this._stickyEnd = coerceBooleanProperty(v); |
| this._hasStickyChanged = prevValue !== this._stickyEnd; |
| } |
| /** |
| * Overridable method that sets the css classes that will be added to every cell in this |
| * column. |
| * In the future, columnCssClassName will change from type string[] to string and this |
| * will set a single string value. |
| * @docs-private |
| */ |
| _updateColumnCssClassName() { |
| this._columnCssClassName = [`cdk-column-${this.cssClassFriendlyName}`]; |
| } |
| /** |
| * This has been extracted to a util because of TS 4 and VE. |
| * View Engine doesn't support property rename inheritance. |
| * TS 4.0 doesn't allow properties to override accessors or vice-versa. |
| * @docs-private |
| */ |
| _setNameInput(value) { |
| // If the directive is set without a name (updated programmatically), then this setter will |
| // trigger with an empty string and should not overwrite the programmatically set value. |
| if (value) { |
| this._name = value; |
| this.cssClassFriendlyName = value.replace(/[^a-z0-9_-]/ig, '-'); |
| this._updateColumnCssClassName(); |
| } |
| } |
| } |
| CdkColumnDef.decorators = [ |
| { type: Directive, args: [{ |
| selector: '[cdkColumnDef]', |
| inputs: ['sticky'], |
| providers: [{ provide: 'MAT_SORT_HEADER_COLUMN_DEF', useExisting: CdkColumnDef }], |
| },] } |
| ]; |
| CdkColumnDef.ctorParameters = () => [ |
| { type: undefined, decorators: [{ type: Inject, args: [CDK_TABLE,] }, { type: Optional }] } |
| ]; |
| CdkColumnDef.propDecorators = { |
| name: [{ type: Input, args: ['cdkColumnDef',] }], |
| stickyEnd: [{ type: Input, args: ['stickyEnd',] }], |
| cell: [{ type: ContentChild, args: [CdkCellDef,] }], |
| headerCell: [{ type: ContentChild, args: [CdkHeaderCellDef,] }], |
| footerCell: [{ type: ContentChild, args: [CdkFooterCellDef,] }] |
| }; |
| /** Base class for the cells. Adds a CSS classname that identifies the column it renders in. */ |
| class BaseCdkCell { |
| constructor(columnDef, elementRef) { |
| // If IE 11 is dropped before we switch to setting a single class name, change to multi param |
| // with destructuring. |
| const classList = elementRef.nativeElement.classList; |
| for (const className of columnDef._columnCssClassName) { |
| classList.add(className); |
| } |
| } |
| } |
| /** Header cell template container that adds the right classes and role. */ |
| class CdkHeaderCell extends BaseCdkCell { |
| constructor(columnDef, elementRef) { |
| super(columnDef, elementRef); |
| } |
| } |
| CdkHeaderCell.decorators = [ |
| { type: Directive, args: [{ |
| selector: 'cdk-header-cell, th[cdk-header-cell]', |
| host: { |
| 'class': 'cdk-header-cell', |
| 'role': 'columnheader', |
| }, |
| },] } |
| ]; |
| CdkHeaderCell.ctorParameters = () => [ |
| { type: CdkColumnDef }, |
| { type: ElementRef } |
| ]; |
| /** Footer cell template container that adds the right classes and role. */ |
| class CdkFooterCell extends BaseCdkCell { |
| constructor(columnDef, elementRef) { |
| super(columnDef, elementRef); |
| } |
| } |
| CdkFooterCell.decorators = [ |
| { type: Directive, args: [{ |
| selector: 'cdk-footer-cell, td[cdk-footer-cell]', |
| host: { |
| 'class': 'cdk-footer-cell', |
| 'role': 'gridcell', |
| }, |
| },] } |
| ]; |
| CdkFooterCell.ctorParameters = () => [ |
| { type: CdkColumnDef }, |
| { type: ElementRef } |
| ]; |
| /** Cell template container that adds the right classes and role. */ |
| class CdkCell extends BaseCdkCell { |
| constructor(columnDef, elementRef) { |
| super(columnDef, elementRef); |
| } |
| } |
| CdkCell.decorators = [ |
| { type: Directive, args: [{ |
| selector: 'cdk-cell, td[cdk-cell]', |
| host: { |
| 'class': 'cdk-cell', |
| 'role': 'gridcell', |
| }, |
| },] } |
| ]; |
| CdkCell.ctorParameters = () => [ |
| { type: CdkColumnDef }, |
| { type: ElementRef } |
| ]; |
| |
| /** |
| * @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.io/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(_ngZone) { |
| this._ngZone = _ngZone; |
| this._currentSchedule = null; |
| this._destroyed = new Subject(); |
| } |
| /** |
| * 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); |
| } |
| /** Prevent any further tasks from running. */ |
| ngOnDestroy() { |
| this._destroyed.next(); |
| this._destroyed.complete(); |
| } |
| _createScheduleIfNeeded() { |
| if (this._currentSchedule) { |
| return; |
| } |
| this._currentSchedule = new _Schedule(); |
| this._getScheduleObservable().pipe(takeUntil(this._destroyed)).subscribe(() => { |
| 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; |
| }); |
| } |
| _getScheduleObservable() { |
| // Use onStable when in the context of an ongoing change detection cycle so that we |
| // do not accidentally trigger additional cycles. |
| return this._ngZone.isStable ? |
| from(Promise.resolve(undefined)) : |
| this._ngZone.onStable.pipe(take(1)); |
| } |
| } |
| _CoalescedStyleScheduler.decorators = [ |
| { type: Injectable } |
| ]; |
| _CoalescedStyleScheduler.ctorParameters = () => [ |
| { type: NgZone } |
| ]; |
| |
| /** |
| * @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.io/license |
| */ |
| /** |
| * The row template that can be used by the mat-table. Should not be used outside of the |
| * material library. |
| */ |
| const CDK_ROW_TEMPLATE = `<ng-container cdkCellOutlet></ng-container>`; |
| /** |
| * Base class for the CdkHeaderRowDef and CdkRowDef that handles checking their columns inputs |
| * for changes and notifying the table. |
| */ |
| class BaseRowDef { |
| constructor( |
| /** @docs-private */ template, _differs) { |
| this.template = template; |
| this._differs = _differs; |
| } |
| ngOnChanges(changes) { |
| // Create a new columns differ if one does not yet exist. Initialize it based on initial value |
| // of the columns property or an empty array if none is provided. |
| if (!this._columnsDiffer) { |
| const columns = (changes['columns'] && changes['columns'].currentValue) || []; |
| this._columnsDiffer = this._differs.find(columns).create(); |
| this._columnsDiffer.diff(columns); |
| } |
| } |
| /** |
| * Returns the difference between the current columns and the columns from the last diff, or null |
| * if there is no difference. |
| */ |
| getColumnsDiff() { |
| return this._columnsDiffer.diff(this.columns); |
| } |
| /** Gets this row def's relevant cell template from the provided column def. */ |
| extractCellTemplate(column) { |
| if (this instanceof CdkHeaderRowDef) { |
| return column.headerCell.template; |
| } |
| if (this instanceof CdkFooterRowDef) { |
| return column.footerCell.template; |
| } |
| else { |
| return column.cell.template; |
| } |
| } |
| } |
| BaseRowDef.decorators = [ |
| { type: Directive } |
| ]; |
| BaseRowDef.ctorParameters = () => [ |
| { type: TemplateRef }, |
| { type: IterableDiffers } |
| ]; |
| // Boilerplate for applying mixins to CdkHeaderRowDef. |
| /** @docs-private */ |
| class CdkHeaderRowDefBase extends BaseRowDef { |
| } |
| const _CdkHeaderRowDefBase = mixinHasStickyInput(CdkHeaderRowDefBase); |
| /** |
| * Header row definition for the CDK table. |
| * Captures the header row's template and other header properties such as the columns to display. |
| */ |
| class CdkHeaderRowDef extends _CdkHeaderRowDefBase { |
| constructor(template, _differs, _table) { |
| super(template, _differs); |
| this._table = _table; |
| } |
| // Prerender fails to recognize that ngOnChanges in a part of this class through inheritance. |
| // Explicitly define it so that the method is called as part of the Angular lifecycle. |
| ngOnChanges(changes) { |
| super.ngOnChanges(changes); |
| } |
| } |
| CdkHeaderRowDef.decorators = [ |
| { type: Directive, args: [{ |
| selector: '[cdkHeaderRowDef]', |
| inputs: ['columns: cdkHeaderRowDef', 'sticky: cdkHeaderRowDefSticky'], |
| },] } |
| ]; |
| CdkHeaderRowDef.ctorParameters = () => [ |
| { type: TemplateRef }, |
| { type: IterableDiffers }, |
| { type: undefined, decorators: [{ type: Inject, args: [CDK_TABLE,] }, { type: Optional }] } |
| ]; |
| // Boilerplate for applying mixins to CdkFooterRowDef. |
| /** @docs-private */ |
| class CdkFooterRowDefBase extends BaseRowDef { |
| } |
| const _CdkFooterRowDefBase = mixinHasStickyInput(CdkFooterRowDefBase); |
| /** |
| * Footer row definition for the CDK table. |
| * Captures the footer row's template and other footer properties such as the columns to display. |
| */ |
| class CdkFooterRowDef extends _CdkFooterRowDefBase { |
| constructor(template, _differs, _table) { |
| super(template, _differs); |
| this._table = _table; |
| } |
| // Prerender fails to recognize that ngOnChanges in a part of this class through inheritance. |
| // Explicitly define it so that the method is called as part of the Angular lifecycle. |
| ngOnChanges(changes) { |
| super.ngOnChanges(changes); |
| } |
| } |
| CdkFooterRowDef.decorators = [ |
| { type: Directive, args: [{ |
| selector: '[cdkFooterRowDef]', |
| inputs: ['columns: cdkFooterRowDef', 'sticky: cdkFooterRowDefSticky'], |
| },] } |
| ]; |
| CdkFooterRowDef.ctorParameters = () => [ |
| { type: TemplateRef }, |
| { type: IterableDiffers }, |
| { type: undefined, decorators: [{ type: Inject, args: [CDK_TABLE,] }, { type: Optional }] } |
| ]; |
| /** |
| * Data row definition for the CDK table. |
| * Captures the header row's template and other row properties such as the columns to display and |
| * a when predicate that describes when this row should be used. |
| */ |
| class CdkRowDef extends BaseRowDef { |
| // TODO(andrewseguin): Add an input for providing a switch function to determine |
| // if this template should be used. |
| constructor(template, _differs, _table) { |
| super(template, _differs); |
| this._table = _table; |
| } |
| } |
| CdkRowDef.decorators = [ |
| { type: Directive, args: [{ |
| selector: '[cdkRowDef]', |
| inputs: ['columns: cdkRowDefColumns', 'when: cdkRowDefWhen'], |
| },] } |
| ]; |
| CdkRowDef.ctorParameters = () => [ |
| { type: TemplateRef }, |
| { type: IterableDiffers }, |
| { type: undefined, decorators: [{ type: Inject, args: [CDK_TABLE,] }, { type: Optional }] } |
| ]; |
| /** |
| * Outlet for rendering cells inside of a row or header row. |
| * @docs-private |
| */ |
| class CdkCellOutlet { |
| constructor(_viewContainer) { |
| this._viewContainer = _viewContainer; |
| CdkCellOutlet.mostRecentCellOutlet = this; |
| } |
| ngOnDestroy() { |
| // If this was the last outlet being rendered in the view, remove the reference |
| // from the static property after it has been destroyed to avoid leaking memory. |
| if (CdkCellOutlet.mostRecentCellOutlet === this) { |
| CdkCellOutlet.mostRecentCellOutlet = null; |
| } |
| } |
| } |
| /** |
| * Static property containing the latest constructed instance of this class. |
| * Used by the CDK table when each CdkHeaderRow and CdkRow component is created using |
| * createEmbeddedView. After one of these components are created, this property will provide |
| * a handle to provide that component's cells and context. After init, the CdkCellOutlet will |
| * construct the cells with the provided context. |
| */ |
| CdkCellOutlet.mostRecentCellOutlet = null; |
| CdkCellOutlet.decorators = [ |
| { type: Directive, args: [{ selector: '[cdkCellOutlet]' },] } |
| ]; |
| CdkCellOutlet.ctorParameters = () => [ |
| { type: ViewContainerRef } |
| ]; |
| /** Header template container that contains the cell outlet. Adds the right class and role. */ |
| class CdkHeaderRow { |
| } |
| CdkHeaderRow.decorators = [ |
| { type: Component, args: [{ |
| selector: 'cdk-header-row, tr[cdk-header-row]', |
| template: CDK_ROW_TEMPLATE, |
| host: { |
| 'class': 'cdk-header-row', |
| 'role': 'row', |
| }, |
| // See note on CdkTable for explanation on why this uses the default change detection strategy. |
| // tslint:disable-next-line:validate-decorators |
| changeDetection: ChangeDetectionStrategy.Default, |
| encapsulation: ViewEncapsulation.None |
| },] } |
| ]; |
| /** Footer template container that contains the cell outlet. Adds the right class and role. */ |
| class CdkFooterRow { |
| } |
| CdkFooterRow.decorators = [ |
| { type: Component, args: [{ |
| selector: 'cdk-footer-row, tr[cdk-footer-row]', |
| template: CDK_ROW_TEMPLATE, |
| host: { |
| 'class': 'cdk-footer-row', |
| 'role': 'row', |
| }, |
| // See note on CdkTable for explanation on why this uses the default change detection strategy. |
| // tslint:disable-next-line:validate-decorators |
| changeDetection: ChangeDetectionStrategy.Default, |
| encapsulation: ViewEncapsulation.None |
| },] } |
| ]; |
| /** Data row template container that contains the cell outlet. Adds the right class and role. */ |
| class CdkRow { |
| } |
| CdkRow.decorators = [ |
| { type: Component, args: [{ |
| selector: 'cdk-row, tr[cdk-row]', |
| template: CDK_ROW_TEMPLATE, |
| host: { |
| 'class': 'cdk-row', |
| 'role': 'row', |
| }, |
| // See note on CdkTable for explanation on why this uses the default change detection strategy. |
| // tslint:disable-next-line:validate-decorators |
| changeDetection: ChangeDetectionStrategy.Default, |
| encapsulation: ViewEncapsulation.None |
| },] } |
| ]; |
| /** Row that can be used to display a message when no data is shown in the table. */ |
| class CdkNoDataRow { |
| constructor(templateRef) { |
| this.templateRef = templateRef; |
| } |
| } |
| CdkNoDataRow.decorators = [ |
| { type: Directive, args: [{ |
| selector: 'ng-template[cdkNoDataRow]' |
| },] } |
| ]; |
| CdkNoDataRow.ctorParameters = () => [ |
| { type: TemplateRef } |
| ]; |
| |
| /** |
| * @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.io/license |
| */ |
| /** |
| * List of all possible directions that can be used for sticky positioning. |
| * @docs-private |
| */ |
| const STICKY_DIRECTIONS = ['top', 'bottom', 'left', 'right']; |
| /** |
| * Applies and removes sticky positioning styles to the `CdkTable` rows and columns cells. |
| * @docs-private |
| */ |
| class StickyStyler { |
| /** |
| * @param _isNativeHtmlTable Whether the sticky logic should be based on a table |
| * that uses the native `<table>` element. |
| * @param _stickCellCss The CSS class that will be applied to every row/cell that has |
| * sticky positioning applied. |
| * @param direction The directionality context of the table (ltr/rtl); affects column positioning |
| * by reversing left/right positions. |
| * @param _isBrowser Whether the table is currently being rendered on the server or the client. |
| * @param _needsPositionStickyOnElement Whether we need to specify position: sticky on cells |
| * using inline styles. If false, it is assumed that position: sticky is included in |
| * the component stylesheet for _stickCellCss. |
| * @param _positionListener A listener that is notified of changes to sticky rows/columns |
| * and their dimensions. |
| */ |
| constructor(_isNativeHtmlTable, _stickCellCss, direction, |
| /** |
| * @deprecated `_coalescedStyleScheduler` parameter to become required. |
| * @breaking-change 11.0.0 |
| */ |
| _coalescedStyleScheduler, _isBrowser = true, _needsPositionStickyOnElement = true, _positionListener) { |
| this._isNativeHtmlTable = _isNativeHtmlTable; |
| this._stickCellCss = _stickCellCss; |
| this.direction = direction; |
| this._coalescedStyleScheduler = _coalescedStyleScheduler; |
| this._isBrowser = _isBrowser; |
| this._needsPositionStickyOnElement = _needsPositionStickyOnElement; |
| this._positionListener = _positionListener; |
| this._cachedCellWidths = []; |
| this._borderCellCss = { |
| 'top': `${_stickCellCss}-border-elem-top`, |
| 'bottom': `${_stickCellCss}-border-elem-bottom`, |
| 'left': `${_stickCellCss}-border-elem-left`, |
| 'right': `${_stickCellCss}-border-elem-right`, |
| }; |
| } |
| /** |
| * Clears the sticky positioning styles from the row and its cells by resetting the `position` |
| * style, setting the zIndex to 0, and unsetting each provided sticky direction. |
| * @param rows The list of rows that should be cleared from sticking in the provided directions |
| * @param stickyDirections The directions that should no longer be set as sticky on the rows. |
| */ |
| clearStickyPositioning(rows, stickyDirections) { |
| const elementsToClear = []; |
| for (const row of rows) { |
| // If the row isn't an element (e.g. if it's an `ng-container`), |
| // it won't have inline styles or `children` so we skip it. |
| if (row.nodeType !== row.ELEMENT_NODE) { |
| continue; |
| } |
| elementsToClear.push(row); |
| for (let i = 0; i < row.children.length; i++) { |
| elementsToClear.push(row.children[i]); |
| } |
| } |
| // Coalesce with sticky row/column updates (and potentially other changes like column resize). |
| this._scheduleStyleChanges(() => { |
| for (const element of elementsToClear) { |
| this._removeStickyStyle(element, stickyDirections); |
| } |
| }); |
| } |
| /** |
| * Applies sticky left and right positions to the cells of each row according to the sticky |
| * states of the rendered column definitions. |
| * @param rows The rows that should have its set of cells stuck according to the sticky states. |
| * @param stickyStartStates A list of boolean states where each state represents whether the cell |
| * in this index position should be stuck to the start of the row. |
| * @param stickyEndStates A list of boolean states where each state represents whether the cell |
| * in this index position should be stuck to the end of the row. |
| * @param recalculateCellWidths Whether the sticky styler should recalculate the width of each |
| * column cell. If `false` cached widths will be used instead. |
| */ |
| updateStickyColumns(rows, stickyStartStates, stickyEndStates, recalculateCellWidths = true) { |
| if (!rows.length || !this._isBrowser || !(stickyStartStates.some(state => state) || |
| stickyEndStates.some(state => state))) { |
| if (this._positionListener) { |
| this._positionListener.stickyColumnsUpdated({ sizes: [] }); |
| this._positionListener.stickyEndColumnsUpdated({ sizes: [] }); |
| } |
| return; |
| } |
| const firstRow = rows[0]; |
| const numCells = firstRow.children.length; |
| const cellWidths = this._getCellWidths(firstRow, recalculateCellWidths); |
| const startPositions = this._getStickyStartColumnPositions(cellWidths, stickyStartStates); |
| const endPositions = this._getStickyEndColumnPositions(cellWidths, stickyEndStates); |
| const lastStickyStart = stickyStartStates.lastIndexOf(true); |
| const firstStickyEnd = stickyEndStates.indexOf(true); |
| // Coalesce with sticky row updates (and potentially other changes like column resize). |
| this._scheduleStyleChanges(() => { |
| const isRtl = this.direction === 'rtl'; |
| const start = isRtl ? 'right' : 'left'; |
| const end = isRtl ? 'left' : 'right'; |
| for (const row of rows) { |
| for (let i = 0; i < numCells; i++) { |
| const cell = row.children[i]; |
| if (stickyStartStates[i]) { |
| this._addStickyStyle(cell, start, startPositions[i], i === lastStickyStart); |
| } |
| if (stickyEndStates[i]) { |
| this._addStickyStyle(cell, end, endPositions[i], i === firstStickyEnd); |
| } |
| } |
| } |
| if (this._positionListener) { |
| this._positionListener.stickyColumnsUpdated({ |
| sizes: lastStickyStart === -1 ? |
| [] : |
| cellWidths |
| .slice(0, lastStickyStart + 1) |
| .map((width, index) => stickyStartStates[index] ? width : null) |
| }); |
| this._positionListener.stickyEndColumnsUpdated({ |
| sizes: firstStickyEnd === -1 ? |
| [] : |
| cellWidths |
| .slice(firstStickyEnd) |
| .map((width, index) => stickyEndStates[index + firstStickyEnd] ? width : null) |
| .reverse() |
| }); |
| } |
| }); |
| } |
| /** |
| * Applies sticky positioning to the row's cells if using the native table layout, and to the |
| * row itself otherwise. |
| * @param rowsToStick The list of rows that should be stuck according to their corresponding |
| * sticky state and to the provided top or bottom position. |
| * @param stickyStates A list of boolean states where each state represents whether the row |
| * should be stuck in the particular top or bottom position. |
| * @param position The position direction in which the row should be stuck if that row should be |
| * sticky. |
| * |
| */ |
| stickRows(rowsToStick, stickyStates, position) { |
| // Since we can't measure the rows on the server, we can't stick the rows properly. |
| if (!this._isBrowser) { |
| return; |
| } |
| // If positioning the rows to the bottom, reverse their order when evaluating the sticky |
| // position such that the last row stuck will be "bottom: 0px" and so on. Note that the |
| // sticky states need to be reversed as well. |
| const rows = position === 'bottom' ? rowsToStick.slice().reverse() : rowsToStick; |
| const states = position === 'bottom' ? stickyStates.slice().reverse() : stickyStates; |
| // Measure row heights all at once before adding sticky styles to reduce layout thrashing. |
| const stickyOffsets = []; |
| const stickyCellHeights = []; |
| const elementsToStick = []; |
| for (let rowIndex = 0, stickyOffset = 0; rowIndex < rows.length; rowIndex++) { |
| stickyOffsets[rowIndex] = stickyOffset; |
| if (!states[rowIndex]) { |
| continue; |
| } |
| const row = rows[rowIndex]; |
| elementsToStick[rowIndex] = this._isNativeHtmlTable ? |
| Array.from(row.children) : [row]; |
| const height = row.getBoundingClientRect().height; |
| stickyOffset += height; |
| stickyCellHeights[rowIndex] = height; |
| } |
| const borderedRowIndex = states.lastIndexOf(true); |
| // Coalesce with other sticky row updates (top/bottom), sticky columns updates |
| // (and potentially other changes like column resize). |
| this._scheduleStyleChanges(() => { |
| var _a, _b; |
| for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { |
| if (!states[rowIndex]) { |
| continue; |
| } |
| const offset = stickyOffsets[rowIndex]; |
| const isBorderedRowIndex = rowIndex === borderedRowIndex; |
| for (const element of elementsToStick[rowIndex]) { |
| this._addStickyStyle(element, position, offset, isBorderedRowIndex); |
| } |
| } |
| if (position === 'top') { |
| (_a = this._positionListener) === null || _a === void 0 ? void 0 : _a.stickyHeaderRowsUpdated({ sizes: stickyCellHeights, elements: elementsToStick }); |
| } |
| else { |
| (_b = this._positionListener) === null || _b === void 0 ? void 0 : _b.stickyFooterRowsUpdated({ sizes: stickyCellHeights, elements: elementsToStick }); |
| } |
| }); |
| } |
| /** |
| * When using the native table in Safari, sticky footer cells do not stick. The only way to stick |
| * footer rows is to apply sticky styling to the tfoot container. This should only be done if |
| * all footer rows are sticky. If not all footer rows are sticky, remove sticky positioning from |
| * the tfoot element. |
| */ |
| updateStickyFooterContainer(tableElement, stickyStates) { |
| if (!this._isNativeHtmlTable) { |
| return; |
| } |
| const tfoot = tableElement.querySelector('tfoot'); |
| // Coalesce with other sticky updates (and potentially other changes like column resize). |
| this._scheduleStyleChanges(() => { |
| if (stickyStates.some(state => !state)) { |
| this._removeStickyStyle(tfoot, ['bottom']); |
| } |
| else { |
| this._addStickyStyle(tfoot, 'bottom', 0, false); |
| } |
| }); |
| } |
| /** |
| * Removes the sticky style on the element by removing the sticky cell CSS class, re-evaluating |
| * the zIndex, removing each of the provided sticky directions, and removing the |
| * sticky position if there are no more directions. |
| */ |
| _removeStickyStyle(element, stickyDirections) { |
| for (const dir of stickyDirections) { |
| element.style[dir] = ''; |
| element.classList.remove(this._borderCellCss[dir]); |
| } |
| // If the element no longer has any more sticky directions, remove sticky positioning and |
| // the sticky CSS class. |
| // Short-circuit checking element.style[dir] for stickyDirections as they |
| // were already removed above. |
| const hasDirection = STICKY_DIRECTIONS.some(dir => stickyDirections.indexOf(dir) === -1 && element.style[dir]); |
| if (hasDirection) { |
| element.style.zIndex = this._getCalculatedZIndex(element); |
| } |
| else { |
| // When not hasDirection, _getCalculatedZIndex will always return ''. |
| element.style.zIndex = ''; |
| if (this._needsPositionStickyOnElement) { |
| element.style.position = ''; |
| } |
| element.classList.remove(this._stickCellCss); |
| } |
| } |
| /** |
| * Adds the sticky styling to the element by adding the sticky style class, changing position |
| * to be sticky (and -webkit-sticky), setting the appropriate zIndex, and adding a sticky |
| * direction and value. |
| */ |
| _addStickyStyle(element, dir, dirValue, isBorderElement) { |
| element.classList.add(this._stickCellCss); |
| if (isBorderElement) { |
| element.classList.add(this._borderCellCss[dir]); |
| } |
| element.style[dir] = `${dirValue}px`; |
| element.style.zIndex = this._getCalculatedZIndex(element); |
| if (this._needsPositionStickyOnElement) { |
| element.style.cssText += 'position: -webkit-sticky; position: sticky; '; |
| } |
| } |
| /** |
| * Calculate what the z-index should be for the element, depending on what directions (top, |
| * bottom, left, right) have been set. It should be true that elements with a top direction |
| * should have the highest index since these are elements like a table header. If any of those |
| * elements are also sticky in another direction, then they should appear above other elements |
| * that are only sticky top (e.g. a sticky column on a sticky header). Bottom-sticky elements |
| * (e.g. footer rows) should then be next in the ordering such that they are below the header |
| * but above any non-sticky elements. Finally, left/right sticky elements (e.g. sticky columns) |
| * should minimally increment so that they are above non-sticky elements but below top and bottom |
| * elements. |
| */ |
| _getCalculatedZIndex(element) { |
| const zIndexIncrements = { |
| top: 100, |
| bottom: 10, |
| left: 1, |
| right: 1, |
| }; |
| let zIndex = 0; |
| // Use `Iterable` instead of `Array` because TypeScript, as of 3.6.3, |
| // loses the array generic type in the `for of`. But we *also* have to use `Array` because |
| // typescript won't iterate over an `Iterable` unless you compile with `--downlevelIteration` |
| for (const dir of STICKY_DIRECTIONS) { |
| if (element.style[dir]) { |
| zIndex += zIndexIncrements[dir]; |
| } |
| } |
| return zIndex ? `${zIndex}` : ''; |
| } |
| /** Gets the widths for each cell in the provided row. */ |
| _getCellWidths(row, recalculateCellWidths = true) { |
| if (!recalculateCellWidths && this._cachedCellWidths.length) { |
| return this._cachedCellWidths; |
| } |
| const cellWidths = []; |
| const firstRowCells = row.children; |
| for (let i = 0; i < firstRowCells.length; i++) { |
| let cell = firstRowCells[i]; |
| cellWidths.push(cell.getBoundingClientRect().width); |
| } |
| this._cachedCellWidths = cellWidths; |
| return cellWidths; |
| } |
| /** |
| * Determines the left and right positions of each sticky column cell, which will be the |
| * accumulation of all sticky column cell widths to the left and right, respectively. |
| * Non-sticky cells do not need to have a value set since their positions will not be applied. |
| */ |
| _getStickyStartColumnPositions(widths, stickyStates) { |
| const positions = []; |
| let nextPosition = 0; |
| for (let i = 0; i < widths.length; i++) { |
| if (stickyStates[i]) { |
| positions[i] = nextPosition; |
| nextPosition += widths[i]; |
| } |
| } |
| return positions; |
| } |
| /** |
| * Determines the left and right positions of each sticky column cell, which will be the |
| * accumulation of all sticky column cell widths to the left and right, respectively. |
| * Non-sticky cells do not need to have a value set since their positions will not be applied. |
| */ |
| _getStickyEndColumnPositions(widths, stickyStates) { |
| const positions = []; |
| let nextPosition = 0; |
| for (let i = widths.length; i > 0; i--) { |
| if (stickyStates[i]) { |
| positions[i] = nextPosition; |
| nextPosition += widths[i]; |
| } |
| } |
| return positions; |
| } |
| /** |
| * Schedules styles to be applied when the style scheduler deems appropriate. |
| * @breaking-change 11.0.0 This method can be removed in favor of calling |
| * `CoalescedStyleScheduler.schedule` directly once the scheduler is a required parameter. |
| */ |
| _scheduleStyleChanges(changes) { |
| if (this._coalescedStyleScheduler) { |
| this._coalescedStyleScheduler.schedule(changes); |
| } |
| else { |
| changes(); |
| } |
| } |
| } |
| |
| /** |
| * @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.io/license |
| */ |
| /** |
| * Returns an error to be thrown when attempting to find an unexisting column. |
| * @param id Id whose lookup failed. |
| * @docs-private |
| */ |
| function getTableUnknownColumnError(id) { |
| return Error(`Could not find column with id "${id}".`); |
| } |
| /** |
| * Returns an error to be thrown when two column definitions have the same name. |
| * @docs-private |
| */ |
| function getTableDuplicateColumnNameError(name) { |
| return Error(`Duplicate column definition name provided: "${name}".`); |
| } |
| /** |
| * Returns an error to be thrown when there are multiple rows that are missing a when function. |
| * @docs-private |
| */ |
| function getTableMultipleDefaultRowDefsError() { |
| return Error(`There can only be one default row without a when predicate function.`); |
| } |
| /** |
| * Returns an error to be thrown when there are no matching row defs for a particular set of data. |
| * @docs-private |
| */ |
| function getTableMissingMatchingRowDefError(data) { |
| return Error(`Could not find a matching row definition for the` + |
| `provided row data: ${JSON.stringify(data)}`); |
| } |
| /** |
| * Returns an error to be thrown when there is no row definitions present in the content. |
| * @docs-private |
| */ |
| function getTableMissingRowDefsError() { |
| return Error('Missing definitions for header, footer, and row; ' + |
| 'cannot determine which columns should be rendered.'); |
| } |
| /** |
| * Returns an error to be thrown when the data source does not match the compatible types. |
| * @docs-private |
| */ |
| function getTableUnknownDataSourceError() { |
| return Error(`Provided data source did not match an array, Observable, or DataSource`); |
| } |
| /** |
| * Returns an error to be thrown when the text column cannot find a parent table to inject. |
| * @docs-private |
| */ |
| function getTableTextColumnMissingParentTableError() { |
| return Error(`Text column could not find a parent table for registration.`); |
| } |
| /** |
| * Returns an error to be thrown when a table text column doesn't have a name. |
| * @docs-private |
| */ |
| function getTableTextColumnMissingNameError() { |
| return Error(`Table text column must have a name.`); |
| } |
| |
| /** |
| * @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.io/license |
| */ |
| /** The injection token used to specify the StickyPositioningListener. */ |
| const STICKY_POSITIONING_LISTENER = new InjectionToken('CDK_SPL'); |
| |
| /** |
| * @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.io/license |
| */ |
| /** |
| * Provides a handle for the table to grab the view container's ng-container to insert data rows. |
| * @docs-private |
| */ |
| class DataRowOutlet { |
| constructor(viewContainer, elementRef) { |
| this.viewContainer = viewContainer; |
| this.elementRef = elementRef; |
| } |
| } |
| DataRowOutlet.decorators = [ |
| { type: Directive, args: [{ selector: '[rowOutlet]' },] } |
| ]; |
| DataRowOutlet.ctorParameters = () => [ |
| { type: ViewContainerRef }, |
| { type: ElementRef } |
| ]; |
| /** |
| * Provides a handle for the table to grab the view container's ng-container to insert the header. |
| * @docs-private |
| */ |
| class HeaderRowOutlet { |
| constructor(viewContainer, elementRef) { |
| this.viewContainer = viewContainer; |
| this.elementRef = elementRef; |
| } |
| } |
| HeaderRowOutlet.decorators = [ |
| { type: Directive, args: [{ selector: '[headerRowOutlet]' },] } |
| ]; |
| HeaderRowOutlet.ctorParameters = () => [ |
| { type: ViewContainerRef }, |
| { type: ElementRef } |
| ]; |
| /** |
| * Provides a handle for the table to grab the view container's ng-container to insert the footer. |
| * @docs-private |
| */ |
| class FooterRowOutlet { |
| constructor(viewContainer, elementRef) { |
| this.viewContainer = viewContainer; |
| this.elementRef = elementRef; |
| } |
| } |
| FooterRowOutlet.decorators = [ |
| { type: Directive, args: [{ selector: '[footerRowOutlet]' },] } |
| ]; |
| FooterRowOutlet.ctorParameters = () => [ |
| { type: ViewContainerRef }, |
| { type: ElementRef } |
| ]; |
| /** |
| * Provides a handle for the table to grab the view |
| * container's ng-container to insert the no data row. |
| * @docs-private |
| */ |
| class NoDataRowOutlet { |
| constructor(viewContainer, elementRef) { |
| this.viewContainer = viewContainer; |
| this.elementRef = elementRef; |
| } |
| } |
| NoDataRowOutlet.decorators = [ |
| { type: Directive, args: [{ selector: '[noDataRowOutlet]' },] } |
| ]; |
| NoDataRowOutlet.ctorParameters = () => [ |
| { type: ViewContainerRef }, |
| { type: ElementRef } |
| ]; |
| /** |
| * The table template that can be used by the mat-table. Should not be used outside of the |
| * material library. |
| * @docs-private |
| */ |
| const CDK_TABLE_TEMPLATE = |
| // Note that according to MDN, the `caption` element has to be projected as the **first** |
| // element in the table. See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/caption |
| ` |
| <ng-content select="caption"></ng-content> |
| <ng-content select="colgroup, col"></ng-content> |
| <ng-container headerRowOutlet></ng-container> |
| <ng-container rowOutlet></ng-container> |
| <ng-container noDataRowOutlet></ng-container> |
| <ng-container footerRowOutlet></ng-container> |
| `; |
| /** |
| * Class used to conveniently type the embedded view ref for rows with a context. |
| * @docs-private |
| */ |
| class RowViewRef extends EmbeddedViewRef { |
| } |
| /** |
| * A data table that can render a header row, data rows, and a footer row. |
| * Uses the dataSource input to determine the data to be rendered. The data can be provided either |
| * as a data array, an Observable stream that emits the data array to render, or a DataSource with a |
| * connect function that will return an Observable stream that emits the data array to render. |
| */ |
| class CdkTable { |
| constructor(_differs, _changeDetectorRef, _elementRef, role, _dir, _document, _platform, |
| /** |
| * @deprecated `_coalescedStyleScheduler`, `_viewRepeater` and `_viewportRuler` |
| * parameters to become required. |
| * @breaking-change 11.0.0 |
| */ |
| _viewRepeater, _coalescedStyleScheduler, _stickyPositioningListener, |
| // Optional for backwards compatibility. The viewport ruler is provided in root. Therefore, |
| // this property will never be null. |
| // tslint:disable-next-line: lightweight-tokens |
| _viewportRuler) { |
| this._differs = _differs; |
| this._changeDetectorRef = _changeDetectorRef; |
| this._elementRef = _elementRef; |
| this._dir = _dir; |
| this._platform = _platform; |
| this._viewRepeater = _viewRepeater; |
| this._coalescedStyleScheduler = _coalescedStyleScheduler; |
| this._stickyPositioningListener = _stickyPositioningListener; |
| this._viewportRuler = _viewportRuler; |
| /** Subject that emits when the component has been destroyed. */ |
| this._onDestroy = new Subject(); |
| /** |
| * Map of all the user's defined columns (header, data, and footer cell template) identified by |
| * name. Collection populated by the column definitions gathered by `ContentChildren` as well as |
| * any custom column definitions added to `_customColumnDefs`. |
| */ |
| this._columnDefsByName = new Map(); |
| /** |
| * Column definitions that were defined outside of the direct content children of the table. |
| * These will be defined when, e.g., creating a wrapper around the cdkTable that has |
| * column definitions as *its* content child. |
| */ |
| this._customColumnDefs = new Set(); |
| /** |
| * Data row definitions that were defined outside of the direct content children of the table. |
| * These will be defined when, e.g., creating a wrapper around the cdkTable that has |
| * built-in data rows as *its* content child. |
| */ |
| this._customRowDefs = new Set(); |
| /** |
| * Header row definitions that were defined outside of the direct content children of the table. |
| * These will be defined when, e.g., creating a wrapper around the cdkTable that has |
| * built-in header rows as *its* content child. |
| */ |
| this._customHeaderRowDefs = new Set(); |
| /** |
| * Footer row definitions that were defined outside of the direct content children of the table. |
| * These will be defined when, e.g., creating a wrapper around the cdkTable that has a |
| * built-in footer row as *its* content child. |
| */ |
| this._customFooterRowDefs = new Set(); |
| /** |
| * Whether the header row definition has been changed. Triggers an update to the header row after |
| * content is checked. Initialized as true so that the table renders the initial set of rows. |
| */ |
| this._headerRowDefChanged = true; |
| /** |
| * Whether the footer row definition has been changed. Triggers an update to the footer row after |
| * content is checked. Initialized as true so that the table renders the initial set of rows. |
| */ |
| this._footerRowDefChanged = true; |
| /** |
| * Whether the sticky column styles need to be updated. Set to `true` when the visible columns |
| * change. |
| */ |
| this._stickyColumnStylesNeedReset = true; |
| /** |
| * Whether the sticky styler should recalculate cell widths when applying sticky styles. If |
| * `false`, cached values will be used instead. This is only applicable to tables with |
| * {@link fixedLayout} enabled. For other tables, cell widths will always be recalculated. |
| */ |
| this._forceRecalculateCellWidths = true; |
| /** |
| * Cache of the latest rendered `RenderRow` objects as a map for easy retrieval when constructing |
| * a new list of `RenderRow` objects for rendering rows. Since the new list is constructed with |
| * the cached `RenderRow` objects when possible, the row identity is preserved when the data |
| * and row template matches, which allows the `IterableDiffer` to check rows by reference |
| * and understand which rows are added/moved/removed. |
| * |
| * Implemented as a map of maps where the first key is the `data: T` object and the second is the |
| * `CdkRowDef<T>` object. With the two keys, the cache points to a `RenderRow<T>` object that |
| * contains an array of created pairs. The array is necessary to handle cases where the data |
| * array contains multiple duplicate data objects and each instantiated `RenderRow` must be |
| * stored. |
| */ |
| this._cachedRenderRowsMap = new Map(); |
| /** |
| * CSS class added to any row or cell that has sticky positioning applied. May be overriden by |
| * table subclasses. |
| */ |
| this.stickyCssClass = 'cdk-table-sticky'; |
| /** |
| * Whether to manually add positon: sticky to all sticky cell elements. Not needed if |
| * the position is set in a selector associated with the value of stickyCssClass. May be |
| * overridden by table subclasses |
| */ |
| this.needsPositionStickyOnElement = true; |
| /** Whether the no data row is currently showing anything. */ |
| this._isShowingNoDataRow = false; |
| this._multiTemplateDataRows = false; |
| this._fixedLayout = false; |
| // TODO(andrewseguin): Remove max value as the end index |
| // and instead calculate the view on init and scroll. |
| /** |
| * Stream containing the latest information on what rows are being displayed on screen. |
| * Can be used by the data source to as a heuristic of what data should be provided. |
| * |
| * @docs-private |
| */ |
| this.viewChange = new BehaviorSubject({ start: 0, end: Number.MAX_VALUE }); |
| if (!role) { |
| this._elementRef.nativeElement.setAttribute('role', 'grid'); |
| } |
| this._document = _document; |
| this._isNativeHtmlTable = this._elementRef.nativeElement.nodeName === 'TABLE'; |
| } |
| /** |
| * Tracking function that will be used to check the differences in data changes. Used similarly |
| * to `ngFor` `trackBy` function. Optimize row operations by identifying a row based on its data |
| * relative to the function to know if a row should be added/removed/moved. |
| * Accepts a function that takes two parameters, `index` and `item`. |
| */ |
| get trackBy() { |
| return this._trackByFn; |
| } |
| set trackBy(fn) { |
| if ((typeof ngDevMode === 'undefined' || ngDevMode) && fn != null && typeof fn !== 'function') { |
| console.warn(`trackBy must be a function, but received ${JSON.stringify(fn)}.`); |
| } |
| this._trackByFn = fn; |
| } |
| /** |
| * The table's source of data, which can be provided in three ways (in order of complexity): |
| * - Simple data array (each object represents one table row) |
| * - Stream that emits a data array each time the array changes |
| * - `DataSource` object that implements the connect/disconnect interface. |
| * |
| * If a data array is provided, the table must be notified when the array's objects are |
| * added, removed, or moved. This can be done by calling the `renderRows()` function which will |
| * render the diff since the last table render. If the data array reference is changed, the table |
| * will automatically trigger an update to the rows. |
| * |
| * When providing an Observable stream, the table will trigger an update automatically when the |
| * stream emits a new array of data. |
| * |
| * Finally, when providing a `DataSource` object, the table will use the Observable stream |
| * provided by the connect function and trigger updates when that stream emits new data array |
| * values. During the table's ngOnDestroy or when the data source is removed from the table, the |
| * table will call the DataSource's `disconnect` function (may be useful for cleaning up any |
| * subscriptions registered during the connect process). |
| */ |
| get dataSource() { |
| return this._dataSource; |
| } |
| set dataSource(dataSource) { |
| if (this._dataSource !== dataSource) { |
| this._switchDataSource(dataSource); |
| } |
| } |
| /** |
| * Whether to allow multiple rows per data object by evaluating which rows evaluate their 'when' |
| * predicate to true. If `multiTemplateDataRows` is false, which is the default value, then each |
| * dataobject will render the first row that evaluates its when predicate to true, in the order |
| * defined in the table, or otherwise the default row which does not have a when predicate. |
| */ |
| get multiTemplateDataRows() { |
| return this._multiTemplateDataRows; |
| } |
| set multiTemplateDataRows(v) { |
| this._multiTemplateDataRows = coerceBooleanProperty(v); |
| // In Ivy if this value is set via a static attribute (e.g. <table multiTemplateDataRows>), |
| // this setter will be invoked before the row outlet has been defined hence the null check. |
| if (this._rowOutlet && this._rowOutlet.viewContainer.length) { |
| this._forceRenderDataRows(); |
| this.updateStickyColumnStyles(); |
| } |
| } |
| /** |
| * Whether to use a fixed table layout. Enabling this option will enforce consistent column widths |
| * and optimize rendering sticky styles for native tables. No-op for flex tables. |
| */ |
| get fixedLayout() { |
| return this._fixedLayout; |
| } |
| set fixedLayout(v) { |
| this._fixedLayout = coerceBooleanProperty(v); |
| // Toggling `fixedLayout` may change column widths. Sticky column styles should be recalculated. |
| this._forceRecalculateCellWidths = true; |
| this._stickyColumnStylesNeedReset = true; |
| } |
| ngOnInit() { |
| this._setupStickyStyler(); |
| if (this._isNativeHtmlTable) { |
| this._applyNativeTableSections(); |
| } |
| // Set up the trackBy function so that it uses the `RenderRow` as its identity by default. If |
| // the user has provided a custom trackBy, return the result of that function as evaluated |
| // with the values of the `RenderRow`'s data and index. |
| this._dataDiffer = this._differs.find([]).create((_i, dataRow) => { |
| return this.trackBy ? this.trackBy(dataRow.dataIndex, dataRow.data) : dataRow; |
| }); |
| // Table cell dimensions may change after resizing the window. Signal the sticky styler to |
| // refresh its cache of cell widths the next time sticky styles are updated. |
| // @breaking-change 11.0.0 Remove null check for _viewportRuler once it's a required parameter. |
| if (this._viewportRuler) { |
| this._viewportRuler.change().pipe(takeUntil(this._onDestroy)).subscribe(() => { |
| this._forceRecalculateCellWidths = true; |
| }); |
| } |
| } |
| ngAfterContentChecked() { |
| // Cache the row and column definitions gathered by ContentChildren and programmatic injection. |
| this._cacheRowDefs(); |
| this._cacheColumnDefs(); |
| // Make sure that the user has at least added header, footer, or data row def. |
| if (!this._headerRowDefs.length && !this._footerRowDefs.length && !this._rowDefs.length && |
| (typeof ngDevMode === 'undefined' || ngDevMode)) { |
| throw getTableMissingRowDefsError(); |
| } |
| // Render updates if the list of columns have been changed for the header, row, or footer defs. |
| const columnsChanged = this._renderUpdatedColumns(); |
| const rowDefsChanged = columnsChanged || this._headerRowDefChanged || this._footerRowDefChanged; |
| // Ensure sticky column styles are reset if set to `true` elsewhere. |
| this._stickyColumnStylesNeedReset = this._stickyColumnStylesNeedReset || rowDefsChanged; |
| this._forceRecalculateCellWidths = rowDefsChanged; |
| // If the header row definition has been changed, trigger a render to the header row. |
| if (this._headerRowDefChanged) { |
| this._forceRenderHeaderRows(); |
| this._headerRowDefChanged = false; |
| } |
| // If the footer row definition has been changed, trigger a render to the footer row. |
| if (this._footerRowDefChanged) { |
| this._forceRenderFooterRows(); |
| this._footerRowDefChanged = false; |
| } |
| // If there is a data source and row definitions, connect to the data source unless a |
| // connection has already been made. |
| if (this.dataSource && this._rowDefs.length > 0 && !this._renderChangeSubscription) { |
| this._observeRenderChanges(); |
| } |
| else if (this._stickyColumnStylesNeedReset) { |
| // In the above case, _observeRenderChanges will result in updateStickyColumnStyles being |
| // called when it row data arrives. Otherwise, we need to call it proactively. |
| this.updateStickyColumnStyles(); |
| } |
| this._checkStickyStates(); |
| } |
| ngOnDestroy() { |
| this._rowOutlet.viewContainer.clear(); |
| this._noDataRowOutlet.viewContainer.clear(); |
| this._headerRowOutlet.viewContainer.clear(); |
| this._footerRowOutlet.viewContainer.clear(); |
| this._cachedRenderRowsMap.clear(); |
| this._onDestroy.next(); |
| this._onDestroy.complete(); |
| if (isDataSource(this.dataSource)) { |
| this.dataSource.disconnect(this); |
| } |
| } |
| /** |
| * Renders rows based on the table's latest set of data, which was either provided directly as an |
| * input or retrieved through an Observable stream (directly or from a DataSource). |
| * Checks for differences in the data since the last diff to perform only the necessary |
| * changes (add/remove/move rows). |
| * |
| * If the table's data source is a DataSource or Observable, this will be invoked automatically |
| * each time the provided Observable stream emits a new data array. Otherwise if your data is |
| * an array, this function will need to be called to render any changes. |
| */ |
| renderRows() { |
| this._renderRows = this._getAllRenderRows(); |
| const changes = this._dataDiffer.diff(this._renderRows); |
| if (!changes) { |
| this._updateNoDataRow(); |
| return; |
| } |
| const viewContainer = this._rowOutlet.viewContainer; |
| // @breaking-change 11.0.0 Remove null check for `_viewRepeater` and the |
| // `else` clause once `_viewRepeater` is turned into a required parameter. |
| if (this._viewRepeater) { |
| this._viewRepeater.applyChanges(changes, viewContainer, (record, _adjustedPreviousIndex, currentIndex) => this._getEmbeddedViewArgs(record.item, currentIndex), (record) => record.item.data, (change) => { |
| if (change.operation === 1 /* INSERTED */ && change.context) { |
| this._renderCellTemplateForItem(change.record.item.rowDef, change.context); |
| } |
| }); |
| } |
| else { |
| changes.forEachOperation((record, prevIndex, currentIndex) => { |
| if (record.previousIndex == null) { |
| const renderRow = record.item; |
| const rowDef = renderRow.rowDef; |
| const context = { $implicit: renderRow.data }; |
| this._renderRow(this._rowOutlet, rowDef, currentIndex, context); |
| } |
| else if (currentIndex == null) { |
| viewContainer.remove(prevIndex); |
| } |
| else { |
| const view = viewContainer.get(prevIndex); |
| viewContainer.move(view, currentIndex); |
| } |
| }); |
| } |
| // Update the meta context of a row's context data (index, count, first, last, ...) |
| this._updateRowIndexContext(); |
| // Update rows that did not get added/removed/moved but may have had their identity changed, |
| // e.g. if trackBy matched data on some property but the actual data reference changed. |
| changes.forEachIdentityChange((record) => { |
| const rowView = viewContainer.get(record.currentIndex); |
| rowView.context.$implicit = record.item.data; |
| }); |
| this._updateNoDataRow(); |
| this.updateStickyColumnStyles(); |
| } |
| /** Adds a column definition that was not included as part of the content children. */ |
| addColumnDef(columnDef) { |
| this._customColumnDefs.add(columnDef); |
| } |
| /** Removes a column definition that was not included as part of the content children. */ |
| removeColumnDef(columnDef) { |
| this._customColumnDefs.delete(columnDef); |
| } |
| /** Adds a row definition that was not included as part of the content children. */ |
| addRowDef(rowDef) { |
| this._customRowDefs.add(rowDef); |
| } |
| /** Removes a row definition that was not included as part of the content children. */ |
| removeRowDef(rowDef) { |
| this._customRowDefs.delete(rowDef); |
| } |
| /** Adds a header row definition that was not included as part of the content children. */ |
| addHeaderRowDef(headerRowDef) { |
| this._customHeaderRowDefs.add(headerRowDef); |
| this._headerRowDefChanged = true; |
| } |
| /** Removes a header row definition that was not included as part of the content children. */ |
| removeHeaderRowDef(headerRowDef) { |
| this._customHeaderRowDefs.delete(headerRowDef); |
| this._headerRowDefChanged = true; |
| } |
| /** Adds a footer row definition that was not included as part of the content children. */ |
| addFooterRowDef(footerRowDef) { |
| this._customFooterRowDefs.add(footerRowDef); |
| this._footerRowDefChanged = true; |
| } |
| /** Removes a footer row definition that was not included as part of the content children. */ |
| removeFooterRowDef(footerRowDef) { |
| this._customFooterRowDefs.delete(footerRowDef); |
| this._footerRowDefChanged = true; |
| } |
| /** Sets a no data row definition that was not included as a part of the content children. */ |
| setNoDataRow(noDataRow) { |
| this._customNoDataRow = noDataRow; |
| } |
| /** |
| * Updates the header sticky styles. First resets all applied styles with respect to the cells |
| * sticking to the top. Then, evaluating which cells need to be stuck to the top. This is |
| * automatically called when the header row changes its displayed set of columns, or if its |
| * sticky input changes. May be called manually for cases where the cell content changes outside |
| * of these events. |
| */ |
| updateStickyHeaderRowStyles() { |
| const headerRows = this._getRenderedRows(this._headerRowOutlet); |
| const tableElement = this._elementRef.nativeElement; |
| // Hide the thead element if there are no header rows. This is necessary to satisfy |
| // overzealous a11y checkers that fail because the `rowgroup` element does not contain |
| // required child `row`. |
| const thead = tableElement.querySelector('thead'); |
| if (thead) { |
| thead.style.display = headerRows.length ? '' : 'none'; |
| } |
| const stickyStates = this._headerRowDefs.map(def => def.sticky); |
| this._stickyStyler.clearStickyPositioning(headerRows, ['top']); |
| this._stickyStyler.stickRows(headerRows, stickyStates, 'top'); |
| // Reset the dirty state of the sticky input change since it has been used. |
| this._headerRowDefs.forEach(def => def.resetStickyChanged()); |
| } |
| /** |
| * Updates the footer sticky styles. First resets all applied styles with respect to the cells |
| * sticking to the bottom. Then, evaluating which cells need to be stuck to the bottom. This is |
| * automatically called when the footer row changes its displayed set of columns, or if its |
| * sticky input changes. May be called manually for cases where the cell content changes outside |
| * of these events. |
| */ |
| updateStickyFooterRowStyles() { |
| const footerRows = this._getRenderedRows(this._footerRowOutlet); |
| const tableElement = this._elementRef.nativeElement; |
| // Hide the tfoot element if there are no footer rows. This is necessary to satisfy |
| // overzealous a11y checkers that fail because the `rowgroup` element does not contain |
| // required child `row`. |
| const tfoot = tableElement.querySelector('tfoot'); |
| if (tfoot) { |
| tfoot.style.display = footerRows.length ? '' : 'none'; |
| } |
| const stickyStates = this._footerRowDefs.map(def => def.sticky); |
| this._stickyStyler.clearStickyPositioning(footerRows, ['bottom']); |
| this._stickyStyler.stickRows(footerRows, stickyStates, 'bottom'); |
| this._stickyStyler.updateStickyFooterContainer(this._elementRef.nativeElement, stickyStates); |
| // Reset the dirty state of the sticky input change since it has been used. |
| this._footerRowDefs.forEach(def => def.resetStickyChanged()); |
| } |
| /** |
| * Updates the column sticky styles. First resets all applied styles with respect to the cells |
| * sticking to the left and right. Then sticky styles are added for the left and right according |
| * to the column definitions for each cell in each row. This is automatically called when |
| * the data source provides a new set of data or when a column definition changes its sticky |
| * input. May be called manually for cases where the cell content changes outside of these events. |
| */ |
| updateStickyColumnStyles() { |
| const headerRows = this._getRenderedRows(this._headerRowOutlet); |
| const dataRows = this._getRenderedRows(this._rowOutlet); |
| const footerRows = this._getRenderedRows(this._footerRowOutlet); |
| // For tables not using a fixed layout, the column widths may change when new rows are rendered. |
| // In a table using a fixed layout, row content won't affect column width, so sticky styles |
| // don't need to be cleared unless either the sticky column config changes or one of the row |
| // defs change. |
| if ((this._isNativeHtmlTable && !this._fixedLayout) |
| || this._stickyColumnStylesNeedReset) { |
| // Clear the left and right positioning from all columns in the table across all rows since |
| // sticky columns span across all table sections (header, data, footer) |
| this._stickyStyler.clearStickyPositioning([...headerRows, ...dataRows, ...footerRows], ['left', 'right']); |
| this._stickyColumnStylesNeedReset = false; |
| } |
| // Update the sticky styles for each header row depending on the def's sticky state |
| headerRows.forEach((headerRow, i) => { |
| this._addStickyColumnStyles([headerRow], this._headerRowDefs[i]); |
| }); |
| // Update the sticky styles for each data row depending on its def's sticky state |
| this._rowDefs.forEach(rowDef => { |
| // Collect all the rows rendered with this row definition. |
| const rows = []; |
| for (let i = 0; i < dataRows.length; i++) { |
| if (this._renderRows[i].rowDef === rowDef) { |
| rows.push(dataRows[i]); |
| } |
| } |
| this._addStickyColumnStyles(rows, rowDef); |
| }); |
| // Update the sticky styles for each footer row depending on the def's sticky state |
| footerRows.forEach((footerRow, i) => { |
| this._addStickyColumnStyles([footerRow], this._footerRowDefs[i]); |
| }); |
| // Reset the dirty state of the sticky input change since it has been used. |
| Array.from(this._columnDefsByName.values()).forEach(def => def.resetStickyChanged()); |
| } |
| /** |
| * Get the list of RenderRow objects to render according to the current list of data and defined |
| * row definitions. If the previous list already contained a particular pair, it should be reused |
| * so that the differ equates their references. |
| */ |
| _getAllRenderRows() { |
| const renderRows = []; |
| // Store the cache and create a new one. Any re-used RenderRow objects will be moved into the |
| // new cache while unused ones can be picked up by garbage collection. |
| const prevCachedRenderRows = this._cachedRenderRowsMap; |
| this._cachedRenderRowsMap = new Map(); |
| // For each data object, get the list of rows that should be rendered, represented by the |
| // respective `RenderRow` object which is the pair of `data` and `CdkRowDef`. |
| for (let i = 0; i < this._data.length; i++) { |
| let data = this._data[i]; |
| const renderRowsForData = this._getRenderRowsForData(data, i, prevCachedRenderRows.get(data)); |
| if (!this._cachedRenderRowsMap.has(data)) { |
| this._cachedRenderRowsMap.set(data, new WeakMap()); |
| } |
| for (let j = 0; j < renderRowsForData.length; j++) { |
| let renderRow = renderRowsForData[j]; |
| const cache = this._cachedRenderRowsMap.get(renderRow.data); |
| if (cache.has(renderRow.rowDef)) { |
| cache.get(renderRow.rowDef).push(renderRow); |
| } |
| else { |
| cache.set(renderRow.rowDef, [renderRow]); |
| } |
| renderRows.push(renderRow); |
| } |
| } |
| return renderRows; |
| } |
| /** |
| * Gets a list of `RenderRow<T>` for the provided data object and any `CdkRowDef` objects that |
| * should be rendered for this data. Reuses the cached RenderRow objects if they match the same |
| * `(T, CdkRowDef)` pair. |
| */ |
| _getRenderRowsForData(data, dataIndex, cache) { |
| const rowDefs = this._getRowDefs(data, dataIndex); |
| return rowDefs.map(rowDef => { |
| const cachedRenderRows = (cache && cache.has(rowDef)) ? cache.get(rowDef) : []; |
| if (cachedRenderRows.length) { |
| const dataRow = cachedRenderRows.shift(); |
| dataRow.dataIndex = dataIndex; |
| return dataRow; |
| } |
| else { |
| return { data, rowDef, dataIndex }; |
| } |
| }); |
| } |
| /** Update the map containing the content's column definitions. */ |
| _cacheColumnDefs() { |
| this._columnDefsByName.clear(); |
| const columnDefs = mergeArrayAndSet(this._getOwnDefs(this._contentColumnDefs), this._customColumnDefs); |
| columnDefs.forEach(columnDef => { |
| if (this._columnDefsByName.has(columnDef.name) && |
| (typeof ngDevMode === 'undefined' || ngDevMode)) { |
| throw getTableDuplicateColumnNameError(columnDef.name); |
| } |
| this._columnDefsByName.set(columnDef.name, columnDef); |
| }); |
| } |
| /** Update the list of all available row definitions that can be used. */ |
| _cacheRowDefs() { |
| this._headerRowDefs = mergeArrayAndSet(this._getOwnDefs(this._contentHeaderRowDefs), this._customHeaderRowDefs); |
| this._footerRowDefs = mergeArrayAndSet(this._getOwnDefs(this._contentFooterRowDefs), this._customFooterRowDefs); |
| this._rowDefs = mergeArrayAndSet(this._getOwnDefs(this._contentRowDefs), this._customRowDefs); |
| // After all row definitions are determined, find the row definition to be considered default. |
| const defaultRowDefs = this._rowDefs.filter(def => !def.when); |
| if (!this.multiTemplateDataRows && defaultRowDefs.length > 1 && |
| (typeof ngDevMode === 'undefined' || ngDevMode)) { |
| throw getTableMultipleDefaultRowDefsError(); |
| } |
| this._defaultRowDef = defaultRowDefs[0]; |
| } |
| /** |
| * Check if the header, data, or footer rows have changed what columns they want to display or |
| * whether the sticky states have changed for the header or footer. If there is a diff, then |
| * re-render that section. |
| */ |
| _renderUpdatedColumns() { |
| const columnsDiffReducer = (acc, def) => acc || !!def.getColumnsDiff(); |
| // Force re-render data rows if the list of column definitions have changed. |
| const dataColumnsChanged = this._rowDefs.reduce(columnsDiffReducer, false); |
| if (dataColumnsChanged) { |
| this._forceRenderDataRows(); |
| } |
| // Force re-render header/footer rows if the list of column definitions have changed. |
| const headerColumnsChanged = this._headerRowDefs.reduce(columnsDiffReducer, false); |
| if (headerColumnsChanged) { |
| this._forceRenderHeaderRows(); |
| } |
| const footerColumnsChanged = this._footerRowDefs.reduce(columnsDiffReducer, false); |
| if (footerColumnsChanged) { |
| this._forceRenderFooterRows(); |
| } |
| return dataColumnsChanged || headerColumnsChanged || footerColumnsChanged; |
| } |
| /** |
| * Switch to the provided data source by resetting the data and unsubscribing from the current |
| * render change subscription if one exists. If the data source is null, interpret this by |
| * clearing the row outlet. Otherwise start listening for new data. |
| */ |
| _switchDataSource(dataSource) { |
| this._data = []; |
| if (isDataSource(this.dataSource)) { |
| this.dataSource.disconnect(this); |
| } |
| // Stop listening for data from the previous data source. |
| if (this._renderChangeSubscription) { |
| this._renderChangeSubscription.unsubscribe(); |
| this._renderChangeSubscription = null; |
| } |
| if (!dataSource) { |
| if (this._dataDiffer) { |
| this._dataDiffer.diff([]); |
| } |
| this._rowOutlet.viewContainer.clear(); |
| } |
| this._dataSource = dataSource; |
| } |
| /** Set up a subscription for the data provided by the data source. */ |
| _observeRenderChanges() { |
| // If no data source has been set, there is nothing to observe for changes. |
| if (!this.dataSource) { |
| return; |
| } |
| let dataStream; |
| if (isDataSource(this.dataSource)) { |
| dataStream = this.dataSource.connect(this); |
| } |
| else if (isObservable(this.dataSource)) { |
| dataStream = this.dataSource; |
| } |
| else if (Array.isArray(this.dataSource)) { |
| dataStream = of(this.dataSource); |
| } |
| if (dataStream === undefined && (typeof ngDevMode === 'undefined' || ngDevMode)) { |
| throw getTableUnknownDataSourceError(); |
| } |
| this._renderChangeSubscription = dataStream.pipe(takeUntil(this._onDestroy)) |
| .subscribe(data => { |
| this._data = data || []; |
| this.renderRows(); |
| }); |
| } |
| /** |
| * Clears any existing content in the header row outlet and creates a new embedded view |
| * in the outlet using the header row definition. |
| */ |
| _forceRenderHeaderRows() { |
| // Clear the header row outlet if any content exists. |
| if (this._headerRowOutlet.viewContainer.length > 0) { |
| this._headerRowOutlet.viewContainer.clear(); |
| } |
| this._headerRowDefs.forEach((def, i) => this._renderRow(this._headerRowOutlet, def, i)); |
| this.updateStickyHeaderRowStyles(); |
| } |
| /** |
| * Clears any existing content in the footer row outlet and creates a new embedded view |
| * in the outlet using the footer row definition. |
| */ |
| _forceRenderFooterRows() { |
| // Clear the footer row outlet if any content exists. |
| if (this._footerRowOutlet.viewContainer.length > 0) { |
| this._footerRowOutlet.viewContainer.clear(); |
| } |
| this._footerRowDefs.forEach((def, i) => this._renderRow(this._footerRowOutlet, def, i)); |
| this.updateStickyFooterRowStyles(); |
| } |
| /** Adds the sticky column styles for the rows according to the columns' stick states. */ |
| _addStickyColumnStyles(rows, rowDef) { |
| const columnDefs = Array.from(rowDef.columns || []).map(columnName => { |
| const columnDef = this._columnDefsByName.get(columnName); |
| if (!columnDef && (typeof ngDevMode === 'undefined' || ngDevMode)) { |
| throw getTableUnknownColumnError(columnName); |
| } |
| return columnDef; |
| }); |
| const stickyStartStates = columnDefs.map(columnDef => columnDef.sticky); |
| const stickyEndStates = columnDefs.map(columnDef => columnDef.stickyEnd); |
| this._stickyStyler.updateStickyColumns(rows, stickyStartStates, stickyEndStates, !this._fixedLayout || this._forceRecalculateCellWidths); |
| } |
| /** Gets the list of rows that have been rendered in the row outlet. */ |
| _getRenderedRows(rowOutlet) { |
| const renderedRows = []; |
| for (let i = 0; i < rowOutlet.viewContainer.length; i++) { |
| const viewRef = rowOutlet.viewContainer.get(i); |
| renderedRows.push(viewRef.rootNodes[0]); |
| } |
| return renderedRows; |
| } |
| /** |
| * Get the matching row definitions that should be used for this row data. If there is only |
| * one row definition, it is returned. Otherwise, find the row definitions that has a when |
| * predicate that returns true with the data. If none return true, return the default row |
| * definition. |
| */ |
| _getRowDefs(data, dataIndex) { |
| if (this._rowDefs.length == 1) { |
| return [this._rowDefs[0]]; |
| } |
| let rowDefs = []; |
| if (this.multiTemplateDataRows) { |
| rowDefs = this._rowDefs.filter(def => !def.when || def.when(dataIndex, data)); |
| } |
| else { |
| let rowDef = this._rowDefs.find(def => def.when && def.when(dataIndex, data)) || this._defaultRowDef; |
| if (rowDef) { |
| rowDefs.push(rowDef); |
| } |
| } |
| if (!rowDefs.length && (typeof ngDevMode === 'undefined' || ngDevMode)) { |
| throw getTableMissingMatchingRowDefError(data); |
| } |
| return rowDefs; |
| } |
| _getEmbeddedViewArgs(renderRow, index) { |
| const rowDef = renderRow.rowDef; |
| const context = { $implicit: renderRow.data }; |
| return { |
| templateRef: rowDef.template, |
| context, |
| index, |
| }; |
| } |
| /** |
| * Creates a new row template in the outlet and fills it with the set of cell templates. |
| * Optionally takes a context to provide to the row and cells, as well as an optional index |
| * of where to place the new row template in the outlet. |
| */ |
| _renderRow(outlet, rowDef, index, context = {}) { |
| // TODO(andrewseguin): enforce that one outlet was instantiated from createEmbeddedView |
| const view = outlet.viewContainer.createEmbeddedView(rowDef.template, context, index); |
| this._renderCellTemplateForItem(rowDef, context); |
| return view; |
| } |
| _renderCellTemplateForItem(rowDef, context) { |
| for (let cellTemplate of this._getCellTemplates(rowDef)) { |
| if (CdkCellOutlet.mostRecentCellOutlet) { |
| CdkCellOutlet.mostRecentCellOutlet._viewContainer.createEmbeddedView(cellTemplate, context); |
| } |
| } |
| this._changeDetectorRef.markForCheck(); |
| } |
| /** |
| * Updates the index-related context for each row to reflect any changes in the index of the rows, |
| * e.g. first/last/even/odd. |
| */ |
| _updateRowIndexContext() { |
| const viewContainer = this._rowOutlet.viewContainer; |
| for (let renderIndex = 0, count = viewContainer.length; renderIndex < count; renderIndex++) { |
| const viewRef = viewContainer.get(renderIndex); |
| const context = viewRef.context; |
| context.count = count; |
| context.first = renderIndex === 0; |
| context.last = renderIndex === count - 1; |
| context.even = renderIndex % 2 === 0; |
| context.odd = !context.even; |
| if (this.multiTemplateDataRows) { |
| context.dataIndex = this._renderRows[renderIndex].dataIndex; |
| context.renderIndex = renderIndex; |
| } |
| else { |
| context.index = this._renderRows[renderIndex].dataIndex; |
| } |
| } |
| } |
| /** Gets the column definitions for the provided row def. */ |
| _getCellTemplates(rowDef) { |
| if (!rowDef || !rowDef.columns) { |
| return []; |
| } |
| return Array.from(rowDef.columns, columnId => { |
| const column = this._columnDefsByName.get(columnId); |
| if (!column && (typeof ngDevMode === 'undefined' || ngDevMode)) { |
| throw getTableUnknownColumnError(columnId); |
| } |
| return rowDef.extractCellTemplate(column); |
| }); |
| } |
| /** Adds native table sections (e.g. tbody) and moves the row outlets into them. */ |
| _applyNativeTableSections() { |
| const documentFragment = this._document.createDocumentFragment(); |
| const sections = [ |
| { tag: 'thead', outlets: [this._headerRowOutlet] }, |
| { tag: 'tbody', outlets: [this._rowOutlet, this._noDataRowOutlet] }, |
| { tag: 'tfoot', outlets: [this._footerRowOutlet] }, |
| ]; |
| for (const section of sections) { |
| const element = this._document.createElement(section.tag); |
| element.setAttribute('role', 'rowgroup'); |
| for (const outlet of section.outlets) { |
| element.appendChild(outlet.elementRef.nativeElement); |
| } |
| documentFragment.appendChild(element); |
| } |
| // Use a DocumentFragment so we don't hit the DOM on each iteration. |
| this._elementRef.nativeElement.appendChild(documentFragment); |
| } |
| /** |
| * Forces a re-render of the data rows. Should be called in cases where there has been an input |
| * change that affects the evaluation of which rows should be rendered, e.g. toggling |
| * `multiTemplateDataRows` or adding/removing row definitions. |
| */ |
| _forceRenderDataRows() { |
| this._dataDiffer.diff([]); |
| this._rowOutlet.viewContainer.clear(); |
| this.renderRows(); |
| } |
| /** |
| * Checks if there has been a change in sticky states since last check and applies the correct |
| * sticky styles. Since checking resets the "dirty" state, this should only be performed once |
| * during a change detection and after the inputs are settled (after content check). |
| */ |
| _checkStickyStates() { |
| const stickyCheckReducer = (acc, d) => { |
| return acc || d.hasStickyChanged(); |
| }; |
| // Note that the check needs to occur for every definition since it notifies the definition |
| // that it can reset its dirty state. Using another operator like `some` may short-circuit |
| // remaining definitions and leave them in an unchecked state. |
| if (this._headerRowDefs.reduce(stickyCheckReducer, false)) { |
| this.updateStickyHeaderRowStyles(); |
| } |
| if (this._footerRowDefs.reduce(stickyCheckReducer, false)) { |
| this.updateStickyFooterRowStyles(); |
| } |
| if (Array.from(this._columnDefsByName.values()).reduce(stickyCheckReducer, false)) { |
| this._stickyColumnStylesNeedReset = true; |
| this.updateStickyColumnStyles(); |
| } |
| } |
| /** |
| * Creates the sticky styler that will be used for sticky rows and columns. Listens |
| * for directionality changes and provides the latest direction to the styler. Re-applies column |
| * stickiness when directionality changes. |
| */ |
| _setupStickyStyler() { |
| const direction = this._dir ? this._dir.value : 'ltr'; |
| this._stickyStyler = new StickyStyler(this._isNativeHtmlTable, this.stickyCssClass, direction, this._coalescedStyleScheduler, this._platform.isBrowser, this.needsPositionStickyOnElement, this._stickyPositioningListener); |
| (this._dir ? this._dir.change : of()) |
| .pipe(takeUntil(this._onDestroy)) |
| .subscribe(value => { |
| this._stickyStyler.direction = value; |
| this.updateStickyColumnStyles(); |
| }); |
| } |
| /** Filters definitions that belong to this table from a QueryList. */ |
| _getOwnDefs(items) { |
| return items.filter(item => !item._table || item._table === this); |
| } |
| /** Creates or removes the no data row, depending on whether any data is being shown. */ |
| _updateNoDataRow() { |
| const noDataRow = this._customNoDataRow || this._noDataRow; |
| if (noDataRow) { |
| const shouldShow = this._rowOutlet.viewContainer.length === 0; |
| if (shouldShow !== this._isShowingNoDataRow) { |
| const container = this._noDataRowOutlet.viewContainer; |
| shouldShow ? container.createEmbeddedView(noDataRow.templateRef) : container.clear(); |
| this._isShowingNoDataRow = shouldShow; |
| } |
| } |
| } |
| } |
| CdkTable.decorators = [ |
| { type: Component, args: [{ |
| selector: 'cdk-table, table[cdk-table]', |
| exportAs: 'cdkTable', |
| template: CDK_TABLE_TEMPLATE, |
| host: { |
| 'class': 'cdk-table', |
| '[class.cdk-table-fixed-layout]': 'fixedLayout', |
| }, |
| encapsulation: ViewEncapsulation.None, |
| // The "OnPush" status for the `MatTable` component is effectively a noop, so we are removing it. |
| // The view for `MatTable` consists entirely of templates declared in other views. As they are |
| // declared elsewhere, they are checked when their declaration points are checked. |
| // tslint:disable-next-line:validate-decorators |
| changeDetection: ChangeDetectionStrategy.Default, |
| providers: [ |
| { provide: CDK_TABLE, useExisting: CdkTable }, |
| { provide: _VIEW_REPEATER_STRATEGY, useClass: _DisposeViewRepeaterStrategy }, |
| { provide: _COALESCED_STYLE_SCHEDULER, useClass: _CoalescedStyleScheduler }, |
| // Prevent nested tables from seeing this table's StickyPositioningListener. |
| { provide: STICKY_POSITIONING_LISTENER, useValue: null }, |
| ], |
| styles: [".cdk-table-fixed-layout{table-layout:fixed}\n"] |
| },] } |
| ]; |
| CdkTable.ctorParameters = () => [ |
| { type: IterableDiffers }, |
| { type: ChangeDetectorRef }, |
| { type: ElementRef }, |
| { type: String, decorators: [{ type: Attribute, args: ['role',] }] }, |
| { type: Directionality, decorators: [{ type: Optional }] }, |
| { type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] }, |
| { type: Platform }, |
| { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [_VIEW_REPEATER_STRATEGY,] }] }, |
| { type: _CoalescedStyleScheduler, decorators: [{ type: Optional }, { type: Inject, args: [_COALESCED_STYLE_SCHEDULER,] }] }, |
| { type: undefined, decorators: [{ type: Optional }, { type: SkipSelf }, { type: Inject, args: [STICKY_POSITIONING_LISTENER,] }] }, |
| { type: ViewportRuler, decorators: [{ type: Optional }] } |
| ]; |
| CdkTable.propDecorators = { |
| trackBy: [{ type: Input }], |
| dataSource: [{ type: Input }], |
| multiTemplateDataRows: [{ type: Input }], |
| fixedLayout: [{ type: Input }], |
| _rowOutlet: [{ type: ViewChild, args: [DataRowOutlet, { static: true },] }], |
| _headerRowOutlet: [{ type: ViewChild, args: [HeaderRowOutlet, { static: true },] }], |
| _footerRowOutlet: [{ type: ViewChild, args: [FooterRowOutlet, { static: true },] }], |
| _noDataRowOutlet: [{ type: ViewChild, args: [NoDataRowOutlet, { static: true },] }], |
| _contentColumnDefs: [{ type: ContentChildren, args: [CdkColumnDef, { descendants: true },] }], |
| _contentRowDefs: [{ type: ContentChildren, args: [CdkRowDef, { descendants: true },] }], |
| _contentHeaderRowDefs: [{ type: ContentChildren, args: [CdkHeaderRowDef, { |
| descendants: true |
| },] }], |
| _contentFooterRowDefs: [{ type: ContentChildren, args: [CdkFooterRowDef, { |
| descendants: true |
| },] }], |
| _noDataRow: [{ type: ContentChild, args: [CdkNoDataRow,] }] |
| }; |
| /** Utility function that gets a merged list of the entries in an array and values of a Set. */ |
| function mergeArrayAndSet(array, set) { |
| return array.concat(Array.from(set)); |
| } |
| |
| /** |
| * @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.io/license |
| */ |
| /** |
| * Column that simply shows text content for the header and row cells. Assumes that the table |
| * is using the native table implementation (`<table>`). |
| * |
| * By default, the name of this column will be the header text and data property accessor. |
| * The header text can be overridden with the `headerText` input. Cell values can be overridden with |
| * the `dataAccessor` input. Change the text justification to the start or end using the `justify` |
| * input. |
| */ |
| class CdkTextColumn { |
| constructor( |
| // `CdkTextColumn` is always requiring a table, but we just assert it manually |
| // for better error reporting. |
| // tslint:disable-next-line: lightweight-tokens |
| _table, _options) { |
| this._table = _table; |
| this._options = _options; |
| /** Alignment of the cell values. */ |
| this.justify = 'start'; |
| this._options = _options || {}; |
| } |
| /** Column name that should be used to reference this column. */ |
| get name() { |
| return this._name; |
| } |
| set name(name) { |
| this._name = name; |
| // With Ivy, inputs can be initialized before static query results are |
| // available. In that case, we defer the synchronization until "ngOnInit" fires. |
| this._syncColumnDefName(); |
| } |
| ngOnInit() { |
| this._syncColumnDefName(); |
| if (this.headerText === undefined) { |
| this.headerText = this._createDefaultHeaderText(); |
| } |
| if (!this.dataAccessor) { |
| this.dataAccessor = |
| this._options.defaultDataAccessor || ((data, name) => data[name]); |
| } |
| if (this._table) { |
| // Provide the cell and headerCell directly to the table with the static `ViewChild` query, |
| // since the columnDef will not pick up its content by the time the table finishes checking |
| // its content and initializing the rows. |
| this.columnDef.cell = this.cell; |
| this.columnDef.headerCell = this.headerCell; |
| this._table.addColumnDef(this.columnDef); |
| } |
| else if (typeof ngDevMode === 'undefined' || ngDevMode) { |
| throw getTableTextColumnMissingParentTableError(); |
| } |
| } |
| ngOnDestroy() { |
| if (this._table) { |
| this._table.removeColumnDef(this.columnDef); |
| } |
| } |
| /** |
| * Creates a default header text. Use the options' header text transformation function if one |
| * has been provided. Otherwise simply capitalize the column name. |
| */ |
| _createDefaultHeaderText() { |
| const name = this.name; |
| if (!name && (typeof ngDevMode === 'undefined' || ngDevMode)) { |
| throw getTableTextColumnMissingNameError(); |
| } |
| if (this._options && this._options.defaultHeaderTextTransform) { |
| return this._options.defaultHeaderTextTransform(name); |
| } |
| return name[0].toUpperCase() + name.slice(1); |
| } |
| /** Synchronizes the column definition name with the text column name. */ |
| _syncColumnDefName() { |
| if (this.columnDef) { |
| this.columnDef.name = this.name; |
| } |
| } |
| } |
| CdkTextColumn.decorators = [ |
| { type: Component, args: [{ |
| selector: 'cdk-text-column', |
| template: ` |
| <ng-container cdkColumnDef> |
| <th cdk-header-cell *cdkHeaderCellDef [style.text-align]="justify"> |
| {{headerText}} |
| </th> |
| <td cdk-cell *cdkCellDef="let data" [style.text-align]="justify"> |
| {{dataAccessor(data, name)}} |
| </td> |
| </ng-container> |
| `, |
| encapsulation: ViewEncapsulation.None, |
| // Change detection is intentionally not set to OnPush. This component's template will be provided |
| // to the table to be inserted into its view. This is problematic when change detection runs since |
| // the bindings in this template will be evaluated _after_ the table's view is evaluated, which |
| // mean's the template in the table's view will not have the updated value (and in fact will cause |
| // an ExpressionChangedAfterItHasBeenCheckedError). |
| // tslint:disable-next-line:validate-decorators |
| changeDetection: ChangeDetectionStrategy.Default |
| },] } |
| ]; |
| CdkTextColumn.ctorParameters = () => [ |
| { type: CdkTable, decorators: [{ type: Optional }] }, |
| { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [TEXT_COLUMN_OPTIONS,] }] } |
| ]; |
| CdkTextColumn.propDecorators = { |
| name: [{ type: Input }], |
| headerText: [{ type: Input }], |
| dataAccessor: [{ type: Input }], |
| justify: [{ type: Input }], |
| columnDef: [{ type: ViewChild, args: [CdkColumnDef, { static: true },] }], |
| cell: [{ type: ViewChild, args: [CdkCellDef, { static: true },] }], |
| headerCell: [{ type: ViewChild, args: [CdkHeaderCellDef, { static: true },] }] |
| }; |
| |
| /** |
| * @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.io/license |
| */ |
| const EXPORTED_DECLARATIONS = [ |
| CdkTable, |
| CdkRowDef, |
| CdkCellDef, |
| CdkCellOutlet, |
| CdkHeaderCellDef, |
| CdkFooterCellDef, |
| CdkColumnDef, |
| CdkCell, |
| CdkRow, |
| CdkHeaderCell, |
| CdkFooterCell, |
| CdkHeaderRow, |
| CdkHeaderRowDef, |
| CdkFooterRow, |
| CdkFooterRowDef, |
| DataRowOutlet, |
| HeaderRowOutlet, |
| FooterRowOutlet, |
| CdkTextColumn, |
| CdkNoDataRow, |
| NoDataRowOutlet, |
| ]; |
| class CdkTableModule { |
| } |
| CdkTableModule.decorators = [ |
| { type: NgModule, args: [{ |
| exports: EXPORTED_DECLARATIONS, |
| declarations: EXPORTED_DECLARATIONS, |
| imports: [ScrollingModule] |
| },] } |
| ]; |
| |
| /** |
| * @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.io/license |
| */ |
| |
| /** |
| * Generated bundle index. Do not edit. |
| */ |
| |
| export { BaseCdkCell, BaseRowDef, CDK_ROW_TEMPLATE, CDK_TABLE, CDK_TABLE_TEMPLATE, CdkCell, CdkCellDef, CdkCellOutlet, CdkColumnDef, CdkFooterCell, CdkFooterCellDef, CdkFooterRow, CdkFooterRowDef, CdkHeaderCell, CdkHeaderCellDef, CdkHeaderRow, CdkHeaderRowDef, CdkNoDataRow, CdkRow, CdkRowDef, CdkTable, CdkTableModule, CdkTextColumn, DataRowOutlet, FooterRowOutlet, HeaderRowOutlet, NoDataRowOutlet, STICKY_DIRECTIONS, STICKY_POSITIONING_LISTENER, StickyStyler, TEXT_COLUMN_OPTIONS, _COALESCED_STYLE_SCHEDULER, _CoalescedStyleScheduler, _Schedule, mixinHasStickyInput }; |
| //# sourceMappingURL=table.js.map |