carbon-components-angular
Version:
Next generation components
1,216 lines (1,196 loc) • 174 kB
JavaScript
import * as i0 from '@angular/core';
import { EventEmitter, Component, Input, Output, HostBinding, Directive, HostListener, ContentChild, NgModule } from '@angular/core';
import * as i1 from 'carbon-components-angular/i18n';
import { I18nModule } from 'carbon-components-angular/i18n';
import * as i2 from '@angular/common';
import { CommonModule } from '@angular/common';
import * as i3 from 'carbon-components-angular/button';
import { ButtonModule } from 'carbon-components-angular/button';
import { Search, SearchModule } from 'carbon-components-angular/search';
import { NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms';
import * as i2$1 from 'carbon-components-angular/icon';
import { IconModule } from 'carbon-components-angular/icon';
import { Subject, BehaviorSubject, combineLatest, Subscription, fromEvent } from 'rxjs';
import { getFocusElementList, tabbableSelectorIgnoreTabIndex } from 'carbon-components-angular/common';
import { getScrollbarWidth, merge } from 'carbon-components-angular/utils';
import { map } from 'rxjs/operators';
import * as i3$1 from 'carbon-components-angular/checkbox';
import * as i3$2 from 'carbon-components-angular/radio';
import { NFormsModule } from 'carbon-components-angular/forms';
import { DialogModule } from 'carbon-components-angular/dialog';
/**
* The table toolbar is reserved for global table actions such as table settings, complex filter, export, or editing table data.
*
* ## Basic usage
*
* ```html
* <cds-table-toolbar [model]="model">
* <cds-table-toolbar-actions>
* <button cdsButton="primary">
* Delete
* <svg cdsIcon="trash-can" size="16" class="cds--btn__icon"></svg>
* </button>
* <button cdsButton="primary">
* Save
* <svg cdsIcon="save" size="16" class="cds--btn__icon"></svg>
* </button>
* <button cdsButton="primary">
* Download
* <svg cdsIcon="download" size="16" class="cds--btn__icon"></svg>
* </button>
* </cds-table-toolbar-actions>
* <cds-table-toolbar-content>
* <cds-table-toolbar-search [expandable]="true"></cds-table-toolbar-search>
* <button cdsButton="toolbar-action">
* <svg cdsIcon="settings" size="16" class="cds--toolbar-action__icon"></svg>
* </button>
* <button cdsButton="primary" size="sm">
* Primary Button
* <svg cdsIcon="add" size="20" class="cds--btn__icon"></svg>
* </button>
* </cds-table-toolbar-content>
* </cds-table-toolbar>
* ```
*
*/
class TableToolbar {
constructor(i18n) {
this.i18n = i18n;
this.size = "md";
this.cancel = new EventEmitter();
this.actionBarLabel = this.i18n.getOverridable("TABLE_TOOLBAR.ACTION_BAR");
this._cancelText = this.i18n.getOverridable("TABLE_TOOLBAR.CANCEL");
this._batchTextLegacy = this.i18n.getOverridable("TABLE_TOOLBAR.BATCH_TEXT");
this._batchTextSingle = this.i18n.getOverridable("TABLE_TOOLBAR.BATCH_TEXT_SINGLE");
this._batchTextMultiple = this.i18n.getOverridable("TABLE_TOOLBAR.BATCH_TEXT_MULTIPLE");
}
set batchText(value) {
if (typeof value === "object") {
this._batchTextSingle.override(value.SINGLE);
this._batchTextMultiple.override(value.MULTIPLE);
}
else {
// For compatibility with old code
this._batchTextLegacy.override(value);
}
}
set ariaLabel(value) {
this.actionBarLabel.override(value.ACTION_BAR);
}
set cancelText(value) {
this._cancelText.override(value.CANCEL);
}
get cancelText() {
return { CANCEL: this._cancelText.value };
}
get count() {
return this.model.totalDataLength > 0 ? this.model.rowsSelected.reduce((previous, current) => previous + (current ? 1 : 0), 0) : 0;
}
get selected() {
return this.model.totalDataLength > 0 ? this.model.rowsSelected.some(item => item) : false;
}
onCancel() {
this.model.selectAll(false);
this.cancel.emit();
}
}
TableToolbar.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TableToolbar, deps: [{ token: i1.I18n }], target: i0.ɵɵFactoryTarget.Component });
TableToolbar.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: TableToolbar, selector: "cds-table-toolbar, ibm-table-toolbar", inputs: { model: "model", batchText: "batchText", ariaLabel: "ariaLabel", cancelText: "cancelText", size: "size" }, outputs: { cancel: "cancel" }, ngImport: i0, template: `
<section
class="cds--table-toolbar"
[ngClass]="{'cds--table-toolbar--sm' : size === 'sm'}"
[attr.aria-label]="actionBarLabel.subject | async">
<div
*ngIf="model"
class="cds--batch-actions"
[ngClass]="{
'cds--batch-actions--active': selected
}">
<div class="cds--batch-summary">
<p class="cds--batch-summary__para" *ngIf="count as n">
<ng-container *ngIf="_batchTextLegacy.subject | async as legacyText; else batchTextBlock">
<span>{{n}}</span> {{legacyText}}
</ng-container>
<ng-template #batchTextBlock>
<span *ngIf="n === 1">{{_batchTextSingle.subject | async}}</span>
<span *ngIf="n !== 1">{{_batchTextMultiple.subject | i18nReplace: {count: n} | async}}</span>
</ng-template>
</p>
</div>
<div class="cds--action-list">
<ng-content select="cds-table-toolbar-actions,ibm-table-toolbar-actions"></ng-content>
<button
cdsButton="primary"
class="cds--batch-summary__cancel"
[tabindex]="selected ? 0 : -1"
(click)="onCancel()">
{{_cancelText.subject | async}}
</button>
</div>
</div>
<ng-content></ng-content>
</section>
`, isInline: true, dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3.Button, selector: "[cdsButton], [ibmButton]", inputs: ["ibmButton", "cdsButton", "size", "skeleton", "iconOnly", "isExpressive"] }, { kind: "pipe", type: i2.AsyncPipe, name: "async" }, { kind: "pipe", type: i1.ReplacePipe, name: "i18nReplace" }] });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TableToolbar, decorators: [{
type: Component,
args: [{
selector: "cds-table-toolbar, ibm-table-toolbar",
template: `
<section
class="cds--table-toolbar"
[ngClass]="{'cds--table-toolbar--sm' : size === 'sm'}"
[attr.aria-label]="actionBarLabel.subject | async">
<div
*ngIf="model"
class="cds--batch-actions"
[ngClass]="{
'cds--batch-actions--active': selected
}">
<div class="cds--batch-summary">
<p class="cds--batch-summary__para" *ngIf="count as n">
<ng-container *ngIf="_batchTextLegacy.subject | async as legacyText; else batchTextBlock">
<span>{{n}}</span> {{legacyText}}
</ng-container>
<ng-template #batchTextBlock>
<span *ngIf="n === 1">{{_batchTextSingle.subject | async}}</span>
<span *ngIf="n !== 1">{{_batchTextMultiple.subject | i18nReplace: {count: n} | async}}</span>
</ng-template>
</p>
</div>
<div class="cds--action-list">
<ng-content select="cds-table-toolbar-actions,ibm-table-toolbar-actions"></ng-content>
<button
cdsButton="primary"
class="cds--batch-summary__cancel"
[tabindex]="selected ? 0 : -1"
(click)="onCancel()">
{{_cancelText.subject | async}}
</button>
</div>
</div>
<ng-content></ng-content>
</section>
`
}]
}], ctorParameters: function () { return [{ type: i1.I18n }]; }, propDecorators: { model: [{
type: Input
}], batchText: [{
type: Input
}], ariaLabel: [{
type: Input
}], cancelText: [{
type: Input
}], size: [{
type: Input
}], cancel: [{
type: Output
}] } });
class TableToolbarActions {
}
TableToolbarActions.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TableToolbarActions, deps: [], target: i0.ɵɵFactoryTarget.Component });
TableToolbarActions.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: TableToolbarActions, selector: "cds-table-toolbar-actions, ibm-table-toolbar-actions", ngImport: i0, template: `<ng-content></ng-content>`, isInline: true });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TableToolbarActions, decorators: [{
type: Component,
args: [{
selector: "cds-table-toolbar-actions, ibm-table-toolbar-actions",
template: `<ng-content></ng-content>`
}]
}] });
class TableToolbarSearch extends Search {
constructor() {
super(...arguments);
this.tableSearch = true;
this.size = "lg";
this.hostClass = true;
}
ngAfterViewInit() {
setTimeout(() => {
if (this.value) {
this.openSearch();
}
});
}
}
TableToolbarSearch.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TableToolbarSearch, deps: null, target: i0.ɵɵFactoryTarget.Component });
TableToolbarSearch.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: TableToolbarSearch, selector: "cds-table-toolbar-search, ibm-table-toolbar-search", host: { properties: { "class.cds--toolbar-content": "this.hostClass" } }, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: TableToolbarSearch,
multi: true
}
], usesInheritance: true, ngImport: i0, template: "<div\n\tclass=\"cds--search\"\n\t[ngClass]=\"{\n\t\t'cds--search--sm': size === 'sm',\n\t\t'cds--search--md': size === 'md',\n\t\t'cds--search--lg': size === 'lg',\n\t\t'cds--search--light': theme === 'light',\n\t\t'cds--skeleton': skeleton,\n\t\t'cds--search--expandable': expandable && !tableSearch,\n\t\t'cds--search--expanded': expandable && !tableSearch && active,\n\t\t'cds--toolbar-search': toolbar && !expandable,\n\t\t'cds--toolbar-search--active': toolbar && !expandable && active,\n\t\t'cds--toolbar-search-container-persistent': tableSearch && !expandable,\n\t\t'cds--toolbar-search-container-expandable': tableSearch && expandable,\n\t\t'cds--toolbar-search-container-active': tableSearch && expandable && active\n\t}\"\n\trole=\"search\"\n\t[attr.aria-label]=\"ariaLabel\"\n\t(click)=\"openSearch()\">\n\t<label class=\"cds--label\" [for]=\"id\">{{label}}</label>\n\n\t<div *ngIf=\"skeleton; else enableInput\" class=\"cds--search-input\"></div>\n\t<ng-template #enableInput>\n\t\t<input\n\t\t\t#input\n\t\t\tclass=\"cds--search-input\"\n\t\t\t[type]=\"tableSearch || !toolbar ? 'text' : 'search'\"\n\t\t\t[id]=\"id\"\n\t\t\t[value]=\"value\"\n\t\t\t[autocomplete]=\"autocomplete\"\n\t\t\t[placeholder]=\"placeholder\"\n\t\t\t[disabled]=\"disabled\"\n\t\t\t[required]=\"required\"\n\t\t\t(input)=\"onSearch($event.target.value)\"\n\t\t\t(keyup.enter)=\"onEnter()\"/>\n\t\t<button\n\t\t\t*ngIf=\"!tableSearch && toolbar\"\n\t\t\tclass=\"cds--toolbar-search__btn\"\n\t\t\t(click)=\"openSearch()\"\n\t\t\taria-label=\"Open search\">\n\t\t\t<svg cdsIcon=\"search\" size=\"16\" class=\"cds--search-magnifier-icon\"></svg>\n\t\t</button>\n\t\t<svg\n\t\t\tcdsIcon=\"search\"\n\t\t\t*ngIf=\"tableSearch || !toolbar\"\n\t\t\tclass=\"cds--search-magnifier-icon\"\n\t\t\tsize=\"16\">\n\t\t</svg>\n\t</ng-template>\n\n\t<button\n\t\t*ngIf=\"tableSearch || !toolbar\"\n\t\tclass=\"cds--search-close\"\n\t\t[ngClass]=\"{\n\t\t\t'cds--search-close--hidden': !value || value.length === 0\n\t\t}\"\n\t\t[title]=\"clearButtonTitle\"\n\t\t(click)=\"clearSearch()\">\n\t\t<span class=\"cds--visually-hidden\">{{ clearButtonTitle }}</span>\n\t\t<svg cdsIcon=\"close\" size=\"16\"></svg>\n\t</button>\n</div>\n", dependencies: [{ kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$1.IconDirective, selector: "[cdsIcon], [ibmIcon]", inputs: ["ibmIcon", "cdsIcon", "size", "title", "ariaLabel", "ariaLabelledBy", "ariaHidden", "isFocusable"] }] });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TableToolbarSearch, decorators: [{
type: Component,
args: [{ selector: "cds-table-toolbar-search, ibm-table-toolbar-search", providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: TableToolbarSearch,
multi: true
}
], template: "<div\n\tclass=\"cds--search\"\n\t[ngClass]=\"{\n\t\t'cds--search--sm': size === 'sm',\n\t\t'cds--search--md': size === 'md',\n\t\t'cds--search--lg': size === 'lg',\n\t\t'cds--search--light': theme === 'light',\n\t\t'cds--skeleton': skeleton,\n\t\t'cds--search--expandable': expandable && !tableSearch,\n\t\t'cds--search--expanded': expandable && !tableSearch && active,\n\t\t'cds--toolbar-search': toolbar && !expandable,\n\t\t'cds--toolbar-search--active': toolbar && !expandable && active,\n\t\t'cds--toolbar-search-container-persistent': tableSearch && !expandable,\n\t\t'cds--toolbar-search-container-expandable': tableSearch && expandable,\n\t\t'cds--toolbar-search-container-active': tableSearch && expandable && active\n\t}\"\n\trole=\"search\"\n\t[attr.aria-label]=\"ariaLabel\"\n\t(click)=\"openSearch()\">\n\t<label class=\"cds--label\" [for]=\"id\">{{label}}</label>\n\n\t<div *ngIf=\"skeleton; else enableInput\" class=\"cds--search-input\"></div>\n\t<ng-template #enableInput>\n\t\t<input\n\t\t\t#input\n\t\t\tclass=\"cds--search-input\"\n\t\t\t[type]=\"tableSearch || !toolbar ? 'text' : 'search'\"\n\t\t\t[id]=\"id\"\n\t\t\t[value]=\"value\"\n\t\t\t[autocomplete]=\"autocomplete\"\n\t\t\t[placeholder]=\"placeholder\"\n\t\t\t[disabled]=\"disabled\"\n\t\t\t[required]=\"required\"\n\t\t\t(input)=\"onSearch($event.target.value)\"\n\t\t\t(keyup.enter)=\"onEnter()\"/>\n\t\t<button\n\t\t\t*ngIf=\"!tableSearch && toolbar\"\n\t\t\tclass=\"cds--toolbar-search__btn\"\n\t\t\t(click)=\"openSearch()\"\n\t\t\taria-label=\"Open search\">\n\t\t\t<svg cdsIcon=\"search\" size=\"16\" class=\"cds--search-magnifier-icon\"></svg>\n\t\t</button>\n\t\t<svg\n\t\t\tcdsIcon=\"search\"\n\t\t\t*ngIf=\"tableSearch || !toolbar\"\n\t\t\tclass=\"cds--search-magnifier-icon\"\n\t\t\tsize=\"16\">\n\t\t</svg>\n\t</ng-template>\n\n\t<button\n\t\t*ngIf=\"tableSearch || !toolbar\"\n\t\tclass=\"cds--search-close\"\n\t\t[ngClass]=\"{\n\t\t\t'cds--search-close--hidden': !value || value.length === 0\n\t\t}\"\n\t\t[title]=\"clearButtonTitle\"\n\t\t(click)=\"clearSearch()\">\n\t\t<span class=\"cds--visually-hidden\">{{ clearButtonTitle }}</span>\n\t\t<svg cdsIcon=\"close\" size=\"16\"></svg>\n\t</button>\n</div>\n" }]
}], propDecorators: { hostClass: [{
type: HostBinding,
args: ["class.cds--toolbar-content"]
}] } });
class TableToolbarContent {
constructor() {
this.class = true;
}
}
TableToolbarContent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TableToolbarContent, deps: [], target: i0.ɵɵFactoryTarget.Component });
TableToolbarContent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "14.3.0", type: TableToolbarContent, selector: "cds-table-toolbar-content, ibm-table-toolbar-content", host: { properties: { "class.cds--toolbar-content": "this.class" } }, ngImport: i0, template: `<ng-content></ng-content>`, isInline: true });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TableToolbarContent, decorators: [{
type: Component,
args: [{
selector: "cds-table-toolbar-content, ibm-table-toolbar-content",
template: `<ng-content></ng-content>`
}]
}], propDecorators: { class: [{
type: HostBinding,
args: ["class.cds--toolbar-content"]
}] } });
class TableHeaderDescription {
constructor() {
this.id = `table-description-${TableHeaderDescription.counter++}`;
this.descriptionClass = true;
}
}
TableHeaderDescription.counter = 0;
TableHeaderDescription.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TableHeaderDescription, deps: [], target: i0.ɵɵFactoryTarget.Directive });
TableHeaderDescription.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.3.0", type: TableHeaderDescription, selector: "[cdsTableHeaderDescription], [ibmTableHeaderDescription]", inputs: { id: "id" }, host: { properties: { "attr.id": "this.id", "class.cds--data-table-header__description": "this.descriptionClass" } }, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TableHeaderDescription, decorators: [{
type: Directive,
args: [{
selector: "[cdsTableHeaderDescription], [ibmTableHeaderDescription]"
}]
}], propDecorators: { id: [{
type: HostBinding,
args: ["attr.id"]
}, {
type: Input
}], descriptionClass: [{
type: HostBinding,
args: ["class.cds--data-table-header__description"]
}] } });
class TableHeaderTitle {
constructor() {
this.id = `table-title-${TableHeaderTitle.counter++}`;
this.titleClass = true;
}
}
TableHeaderTitle.counter = 0;
TableHeaderTitle.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TableHeaderTitle, deps: [], target: i0.ɵɵFactoryTarget.Directive });
TableHeaderTitle.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "14.3.0", type: TableHeaderTitle, selector: "[cdsTableHeaderTitle], [ibmTableHeaderTitle]", inputs: { id: "id" }, host: { properties: { "attr.id": "this.id", "class.cds--data-table-header__title": "this.titleClass" } }, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.3.0", ngImport: i0, type: TableHeaderTitle, decorators: [{
type: Directive,
args: [{
selector: "[cdsTableHeaderTitle], [ibmTableHeaderTitle]"
}]
}], propDecorators: { id: [{
type: HostBinding,
args: ["attr.id"]
}, {
type: Input
}], titleClass: [{
type: HostBinding,
args: ["class.cds--data-table-header__title"]
}] } });
class TableHeaderItem {
/**
* Creates an instance of TableHeaderItem.
*/
constructor(rawData) {
/**
* Defines if column under this TableHeaderItem should be displayed.
*
*/
this.visible = true;
/**
* Disables sorting by default.
*
*/
this.sorted = false;
/**
* Enables sorting on click by default.
* If false then this column won't show a sorting arrow at all.
*
*/
this.sortable = true;
/**
* Number of applied filters.
*
* `filter()` should set it to appropriate number.
*
*/
this.filterCount = 0;
/**
* The number of rows to span
* **NOTE:** not supported by the default carbon table
*/
this.rowSpan = 1;
/**
* The number of columns to span
*/
this.colSpan = 1;
/**
* Style for the column, applied to every element in the column.
*
* ngStyle-like format
*
*/
this.style = {};
this.sortDirection = "NONE";
// defaults so we dont leave things empty
const defaults = {
data: "",
visible: this.visible,
style: this.style,
filterCount: this.filterCount,
filterData: { data: "" }
};
// fill our object with provided props, and fallback to defaults
const data = Object.assign({}, defaults, rawData);
for (let property of Object.getOwnPropertyNames(data)) {
if (data.hasOwnProperty(property)) {
this[property] = data[property];
}
}
}
/**
* If true, sort is set to ascending, if false descending will be true.
*
*/
set ascending(asc) {
this.sortDirection = asc ? "ASCENDING" : "DESCENDING";
}
get ascending() {
return this.sortDirection === "ASCENDING";
}
/**
* If true, sort is set to descending, if false ascending will be true.
*
*/
set descending(desc) {
this.sortDirection = desc ? "DESCENDING" : "ASCENDING";
}
get descending() {
return this.sortDirection === "DESCENDING";
}
get title() {
if (this._title) {
return this._title;
}
if (!this.data) {
return "";
}
if (typeof this.data === "string") {
return this.data;
}
if (this.data.toString &&
this.data.constructor !== ({}).constructor) {
return this.data.toString();
}
// data can’t be reasonably converted to an end user readable string
return "";
}
set title(title) {
this._title = title;
}
/**
* Used for sorting rows of the table.
*
* Override to enable different sorting.
*
* < 0 if `one` should go before `two`
* > 0 if `one` should go after `two`
* 0 if it doesn't matter (they are the same)
*/
compare(one, two) {
if (!one || !two) {
return 0;
}
if (typeof one.data === "string") {
return one.data.localeCompare(two.data);
}
if (one.data < two.data) {
return -1;
}
else if (one.data > two.data) {
return 1;
}
else {
return 0;
}
}
/**
* Used to filter rows in the table.
*
* Override to make a custom filter.
*
* Even though there is just one filter function, there can be multiple filters.
* When implementing filter, set `filterCount` before returning.
*
* `true` to hide the row
* `false` to show the row
*/
filter(item) {
this.filterCount = 0;
return false;
}
}
class TableItem {
/**
* Creates an instance of TableItem.
*/
constructor(rawData) {
/**
* The number of rows to span
*/
this.rowSpan = 1;
/**
* The number of columns to span
*/
this.colSpan = 1;
// defaults so we dont leave things empty
const defaults = {
data: ""
};
// fill our object with provided props, and fallback to defaults
const data = Object.assign({}, defaults, rawData);
for (const property of Object.getOwnPropertyNames(data)) {
if (data.hasOwnProperty(property)) {
this[property] = data[property];
}
}
}
get title() {
if (typeof this._title === "string") {
return this._title;
}
if (!this.data) {
return "";
}
if (typeof this.data === "string") {
return this.data;
}
if (this.data.toString &&
this.data.constructor !== ({}).constructor) {
return this.data.toString();
}
// data can’t be reasonably converted to an end user readable string
return "";
}
set title(title) {
this._title = title;
}
}
/**
* TableModel represents a data model for two-dimensional data. It's used for all things table
* (table component, table toolbar, pagination, etc)
*
* TableModel manages its internal data integrity very well if you use the provided helper
* functions for modifying rows and columns and assigning header and data in that order.
*
* It also provides direct access to the data so you can read and modify it.
* If you change the structure of the data (by directly pushing into the arrays or otherwise),
* keep in mind to keep the data structure intact.
*
* Header length and length of every line in the data should be equal.
*
* If they are not consistent, unexpected things will happen.
*
* Use the provided functions when in doubt.
*/
class TableModel {
constructor() {
this.dataChange = new EventEmitter();
this.rowsSelectedChange = new EventEmitter();
this.rowsExpandedChange = new EventEmitter();
/**
* Gets emitted when `selectAll` is called. Emits false if all rows are deselected and true if
* all rows are selected.
*/
this.selectAllChange = new Subject();
/**
* Contains information about selection state of rows in the table.
*/
this.rowsSelected = [];
/**
* Contains information about expanded state of rows in the table.
*/
this.rowsExpanded = [];
/**
* Contains information about initial index of rows in the table
*/
this.rowsIndices = [];
/**
* Contains information about the context of the row.
*
* It affects styling of the row to reflect the context.
*
* string can be one of `"success" | "warning" | "info" | "error" | ""` and it's
* empty or undefined by default
*/
this.rowsContext = [];
/**
* Contains class name(s) of the row.
*
* It affects styling of the row to reflect the appended class name(s).
*
* It's empty or undefined by default
*/
this.rowsClass = [];
/**
* Contains information about the header cells of the table.
*/
this.header = [];
/**
* Tracks the current page.
*/
this.currentPage = 1;
/**
* Length of page.
*/
this.pageLength = 10;
/**
* Set to true when there is no more data to load in the table
*/
this.isEnd = false;
/**
* Set to true when lazy loading to show loading indicator
*/
this.isLoading = false;
/**
* Used in `data`
*/
this._data = [[]];
/**
* The number of models instantiated, this is to make sure each table has a different
* model count for unique id generation.
*/
this.tableModelCount = 0;
this.tableModelCount = TableModel.COUNT++;
}
/**
* Sets data of the table.
*
* Make sure all rows are the same length to keep the column count accurate.
*/
set data(newData) {
if (!newData || (Array.isArray(newData) && newData.length === 0)) {
newData = [[]];
}
this._data = newData;
// init rowsSelected
this.rowsSelected = new Array(this._data.length).fill(false);
this.rowsExpanded = new Array(this._data.length).fill(false);
// init rows indices
this.rowsIndices = [...Array(this._data.length).keys()];
// init rowsContext
this.rowsContext = new Array(this._data.length);
// init rowsClass
this.rowsClass = new Array(this._data.length);
// only create a fresh header if necessary (header doesn't exist or differs in length)
if (this.header == null || (this.header.length !== this._data[0].length && this._data[0].length > 0)) {
let header = new Array();
for (let i = 0; i < this._data[0].length; i++) {
header.push(new TableHeaderItem());
}
this.header = header;
}
this.dataChange.emit();
}
/**
* Gets the full data.
*
* You can use it to alter individual `TableItem`s but if you need to change
* table structure, use `addRow()` and/or `addColumn()`
*/
get data() {
return this._data;
}
/**
* Manually set data length in case the data in the table doesn't
* correctly reflect all the data that table is to display.
*
* Example: if you have multiple pages of data that table will display
* but you're loading one at a time.
*
* Set to `null` to reset to default behavior.
*/
set totalDataLength(length) {
// if this function is called without a parameter we need to set to null to avoid having undefined != null
this._totalDataLength = isNaN(length) ? null : length;
}
/**
* Total length of data that table has access to, or the amount manually set
*/
get totalDataLength() {
// if manually set data length
if (this._totalDataLength !== null && this._totalDataLength >= 0) {
return this._totalDataLength;
}
// if empty dataset
if (this.data && this.data.length === 1 && this.data[0].length === 0) {
return 0;
}
return this.data.length;
}
/**
* Returns an id for the given column
*
* @param column the column to generate an id for
* @param row the row of the header to generate an id for
*/
getId(column, row = 0) {
return `table-header-${row}-${column}-${this.tableModelCount}`;
}
/**
* Returns the id of the header. Used to link the cells with headers (or headers with headers)
*
* @param column the column to start getting headers for
* @param colSpan the number of columns to get headers for (defaults to 1)
*/
getHeaderId(column, colSpan = 1) {
if (column === "select" || column === "expand") {
return this.getId(column);
}
let ids = [];
for (let i = column; i >= 0; i--) {
if (this.header[i]) {
for (let j = 0; j < colSpan; j++) {
ids.push(this.getId(i + j));
}
break;
}
}
return ids.join(" ");
}
/**
* Finds closest header by trying the `column` and then working its way to the left
*
* @param column the target column
*/
getHeader(column) {
if (!this.header) {
return null;
}
for (let i = column; i >= 0; i--) {
const headerCell = this.header[i];
if (headerCell) {
return headerCell;
}
}
return null;
}
/**
* Returns how many rows is currently selected
*/
selectedRowsCount() {
let count = 0;
if (this.rowsSelected) {
this.rowsSelected.forEach(rowSelected => {
if (rowSelected) {
count++;
}
});
}
return count;
}
/**
* Returns how many rows is currently expanded
*/
expandedRowsCount() {
let count = 0;
if (this.rowsExpanded) {
this.rowsExpanded.forEach(rowExpanded => {
if (rowExpanded) {
count++;
}
});
}
return count;
}
/**
* Returns `index`th row of the table.
*
* Negative index starts from the end. -1 being the last element.
*
* @param index
*/
row(index) {
return this.data[this.realRowIndex(index)];
}
/**
* Adds a row to the `index`th row or appends to table if index not provided.
*
* If row is shorter than other rows or not provided, it will be padded with
* empty `TableItem` elements.
*
* If row is longer than other rows, others will be extended to match so no data is lost.
*
* If called on an empty table with no parameters, it creates a 1x1 table.
*
* Negative index starts from the end. -1 being the last element.
*
* @param [row]
* @param [index]
*/
addRow(row, index) {
// if table empty create table with row
if (!this.data || this.data.length === 0 || this.data[0].length === 0) {
let newData = new Array();
newData.push(row ? row : [new TableItem()]); // row or one empty one column row
this.data = newData;
return;
}
let realRow = row;
const columnCount = this.data[0].length;
if (row == null) {
realRow = new Array();
for (let i = 0; i < columnCount; i++) {
realRow.push(new TableItem());
}
}
if (realRow.length < columnCount) {
// extend the length of realRow
const difference = columnCount - realRow.length;
for (let i = 0; i < difference; i++) {
realRow.push(new TableItem());
}
}
else if (realRow.length > columnCount) {
// extend the length of header
let difference = realRow.length - this.header.length;
for (let j = 0; j < difference; j++) {
this.header.push(new TableHeaderItem());
}
// extend the length of every other row
for (let i = 0; i < this.data.length; i++) {
let currentRow = this.data[i];
difference = realRow.length - currentRow.length;
for (let j = 0; j < difference; j++) {
currentRow.push(new TableItem());
}
}
}
if (index == null) {
this.data.push(realRow);
// update rowsSelected property for length
this.rowsSelected.push(false);
// update rowsExpanded property for length
this.rowsExpanded.push(false);
// update rowsContext property for length
this.rowsContext.push(undefined);
// update rowsClass property for length
this.rowsClass.push(undefined);
// update rowsIndices property for length
this.rowsIndices.push(this.data.length - 1);
}
else {
const ri = this.realRowIndex(index);
this.data.splice(ri, 0, realRow);
// update rowsSelected property for length
this.rowsSelected.splice(ri, 0, false);
// update rowsExpanded property for length
this.rowsExpanded.splice(ri, 0, false);
// update rowsContext property for length
this.rowsContext.splice(ri, 0, undefined);
// update rowsClass property for length
this.rowsClass.splice(ri, 0, undefined);
// update rowsIndices property for length
this.rowsIndices.splice(ri, 0, this.data.length - 1);
}
this.dataChange.emit();
}
/**
* Deletes `index`th row.
*
* Negative index starts from the end. -1 being the last element.
*
* @param index
*/
deleteRow(index) {
const rri = this.realRowIndex(index);
this.data.splice(rri, 1);
this.rowsSelected.splice(rri, 1);
this.rowsExpanded.splice(rri, 1);
this.rowsContext.splice(rri, 1);
this.rowsClass.splice(rri, 1);
const rowIndex = this.rowsIndices[rri];
this.rowsIndices.splice(rri, 1);
this.rowsIndices = this.rowsIndices.map((value) => (value > rowIndex) ? --value : value);
this.dataChange.emit();
}
hasExpandableRows() {
return this.data.some(data => data.some(d => d && d.expandedData)); // checking for some in 2D array
}
isRowExpandable(index) {
return this.data[index].some(d => d && d.expandedData);
}
isRowExpanded(index) {
return this.rowsExpanded[index];
}
getRowContext(index) {
return this.rowsContext[index];
}
/**
* Returns `index`th column of the table.
*
* Negative index starts from the end. -1 being the last element.
*
* @param index
*/
column(index) {
let column = new Array();
const ri = this.realColumnIndex(index);
const rc = this.data.length;
for (let i = 0; i < rc; i++) {
const row = this.data[i];
column.push(row[ri]);
}
return column;
}
/**
* Adds a column to the `index`th column or appends to table if index not provided.
*
* If column is shorter than other columns or not provided, it will be padded with
* empty `TableItem` elements.
*
* If column is longer than other columns, others will be extended to match so no data is lost.
*
* If called on an empty table with no parameters, it creates a 1x1 table.
*
* Negative index starts from the end. -1 being the last element.
*
* @param [column]
* @param [index]
*/
addColumn(column, index) {
// if table empty create table with row
if (!this.data || this.data.length === 0 || this.data[0].length === 0) {
let newData = new Array();
if (column == null) {
newData.push([new TableItem()]);
}
else {
for (let i = 0; i < column.length; i++) {
let item = column[i];
newData.push([item]);
}
}
this.data = newData;
return;
}
let rc = this.data.length; // row count
let ci = this.realColumnIndex(index);
// append missing rows
for (let i = 0; column != null && i < column.length - rc; i++) {
this.addRow();
}
rc = this.data.length;
if (index == null) {
// append to end
for (let i = 0; i < rc; i++) {
let row = this.data[i];
row.push(column == null || column[i] == null ? new TableItem() : column[i]);
}
// update header if not already set by user
if (this.header.length < this.data[0].length) {
this.header.push(new TableHeaderItem());
}
}
else {
if (index >= this.data[0].length) {
// if trying to append
ci++;
}
// insert
for (let i = 0; i < rc; i++) {
let row = this.data[i];
row.splice(ci, 0, column == null || column[i] == null ? new TableItem() : column[i]);
}
// update header if not already set by user
if (this.header.length < this.data[0].length) {
this.header.splice(ci, 0, new TableHeaderItem());
}
}
this.dataChange.emit();
}
/**
* Deletes `index`th column.
*
* Negative index starts from the end. -1 being the last element.
*
* @param index
*/
deleteColumn(index) {
const rci = this.realColumnIndex(index);
const rowCount = this.data.length;
for (let i = 0; i < rowCount; i++) {
this.data[i].splice(rci, 1);
}
// update header if not already set by user
if (this.header.length > this.data[0].length) {
this.header.splice(rci, 1);
}
this.dataChange.emit();
}
moveColumn(indexFrom, indexTo) {
const headerFrom = this.header[indexFrom];
this.addColumn(this.column(indexFrom), indexTo);
this.deleteColumn(indexFrom + (indexTo < indexFrom ? 1 : 0));
this.header[indexTo + (indexTo > indexFrom ? -1 : 0)] = headerFrom;
}
/**
* cycle through the three sort states
* @param index
*/
cycleSortState(index) {
// no sort provided so do the simple sort
switch (this.header[index].sortDirection) {
case "ASCENDING":
this.header[index].sortDirection = "DESCENDING";
break;
case "DESCENDING":
this.header[index].sortDirection = "NONE";
break;
default:
this.header[index].sortDirection = "ASCENDING";
break;
}
}
/**
* Sorts the data currently present in the model based on `compare()`
*
* Direction is set by `ascending` and `descending` properties of `TableHeaderItem`
* in `index`th column.
*
* @param index The column based on which it's sorting
*/
sort(index) {
this.pushRowStateToModelData();
const headerSorted = this.header[index].sorted;
// We only allow sorting by a single column, so reset sort state for all columns before specifying new sort state
this.header.forEach(column => column.sorted = false);
if (this.header[index].sortDirection === "NONE" && headerSorted) {
// Restore initial order of rows
const oldData = this._data;
this._data = [];
for (let i = 0; i < this.rowsIndices.length; i++) {
const ri = this.rowsIndices[i];
this._data[ri] = oldData[i];
}
}
else {
const descending = this.header[index].sortDirection === "DESCENDING" ? -1 : 1;
this.data.sort((a, b) => {
return descending * this.header[index].compare(a[index], b[index]);
});
this.header[index].sorted = true;
}
this.popRowStateFromModelData();
}
/**
* Appends `rowsSelected` and `rowsExpanded` info to model data.
*
* When sorting rows, do this first so information about row selection
* gets sorted with the other row info.
*
* Call `popRowSelectionFromModelData()` after sorting to make everything
* right with the world again.
*/
pushRowStateToModelData() {
for (let i = 0; i < this.data.length; i++) {
const rowSelectedMark = new TableItem();
rowSelectedMark.data = this.rowsSelected[i];
this.data[i].push(rowSelectedMark);
const rowExpandedMark = new TableItem();
rowExpandedMark.data = this.rowsExpanded[i];
this.data[i].push(rowExpandedMark);
const rowContext = new TableItem();
rowContext.data = this.rowsContext[i];
this.data[i].push(rowContext);
const rowClass = new TableItem();
rowClass.data = this.rowsClass[i];
this.data[i].push(rowClass);
const rowIndex = new TableItem();
rowIndex.data = this.rowsIndices[i];
this.data[i].push(rowIndex);
}
}
/**
* Restores `rowsSelected` from data pushed by `pushRowSelectionToModelData()`
*
* Call after sorting data (if you previously pushed to maintain selection order)
* to make everything right with the world again.
*/
popRowStateFromModelData() {
for (let i = 0; i < this.data.length; i++) {
this.rowsIndices[i] = this.data[i].pop().data;
this.rowsClass[i] = this.data[i].pop().data;
this.rowsContext[i] = this.data[i].pop().data;
this.rowsExpanded[i] = !!this.data[i].pop().data;
this.rowsSelected[i] = !!this.data[i].pop().data;
}
}
/**
* Checks if row is filtered out.
*
* @param index
* @returns true if any of the filters in header filters out the `index`th row
*/
isRowFiltered(index) {
const realIndex = this.realRowIndex(index);
return this.header.some((item, i) => item && item.filter(this.row(realIndex)[i]));
}
/**
* Select/deselect `index`th row based on value
*
* @param index index of the row to select
* @param value state to set the row to. Defaults to `true`
*/
selectRow(index, value = true) {
if (this.isRowDisabled(index)) {
return;
}
this.rowsSelected[index] = value;
this.rowsSelectedChange.emit(index);
}
/**
* Selects or deselects all rows in the model
*
* @param value state to set all rows to. Defaults to `true`
*/
selectAll(value = true) {
if (this.data.length >= 1 && this.data[0].length >= 1) {
for (let i = 0; i < this.rowsSelected.length; i++) {
this.selectRow(i, value);
}
}
this.selectAllChange.next(value);
}
isRowSelected(index) {
return this.rowsSelected[index];
}
/**
* Checks if row is disabled or not.
*/
isRowDisabled(index) {
const row = this.data[index];
return !!row.disabled;
}
/**
* Expands/Collapses `index`th row based on value
*
* @param index index of the row to expand or collapse
* @param value expanded state of the row. `true` is expanded and `false` is collapsed
*/
expandRow(index, value = true) {
this.rowsExpanded[index] = value;
this.rowsExpandedChange.emit(index);
}
/**
* Gets the true index of a row based on it's relative position.
* Like in Python, positive numbers start from the top and
* negative numbers start from the bottom.
*
* @param index
*/
realRowIndex(index) {
return this.realIndex(index, this.data.length);
}
/**
* Gets the true index of a column based on it's relative position.
* Like in Python, positive numbers start from the top and
* negative numbers start from the bottom.
*
* @param index
*/
realColumnIndex(index) {
return this.realIndex(index, this.data[0].length);
}
/**
* Generic function to calculate the real index of something.
* Used by `realRowIndex()` and `realColumnIndex()`
*
* @param index
* @param length
*/
realIndex(index, length) {
if (index == null) {
return length - 1;
}
else if (index >= 0) {
return index >= length ? length - 1 : index;
}
else {
return -index >= length ? 0 : length + index;
}
}
}
/**
* The number of models instantiated, used for (among other things) unique id generation
*/
TableModel.COUNT = 0;
/**
* `DataGridInteractionModel` provides centralized control over arbitrary 2d grids, following the w3 specs.
*
* Refs:
* - https://www.w3.org/TR/wai-aria-practices/examples/grid/dataGrids.html
* - https://www.w3.org/TR/wai-aria-practices/#grid
*
* Example usage (taken from `table.component`):
```typescript
// a standard HTML table
const table = this.elementRef.nativeElement.querySelector("table") as HTMLTableElement;
// `TableDomAdapter` implements `TableAdapter` and provides a consistent interface to query rows and columns in a table
const tableAdapter = new TableDomAdapter(table);
// the keydown events that we'll use for keyboard navigation of the table
const keydownEventStream = fromEvent<KeyboardEvent>(table, "keydown");
// the click events we'll use to ensure focus is updated correctly on click
const clickEventStream = fromEvent<MouseEvent>(table, "click");
// the `DataGridInteractionModel` instance!
this.interactionModel = new DataGridInteractionModel(keydownEventStream, clickEventStream, tableAdapter);
// subscribe to the combined position updates
this.interactionModel.position.subscribe(event => {
const [currentRow, currentColumn] = event.current;
const [previousRow, previousColumn] = event.previous;
// query the TableAdapter for the cell at the current row and column ...
const currentElement = tableAdapter.getCell(currentRow, currentColumn);
// ... and make it focusable it
Table.setTabIndex(currentElement, 0);
// if the model has just initialized don't focus or reset anything
if (previousRow === -1 || previousColumn === -1) { return; }
// query the TableAdapter for the cell at the previous row and column ...
const previousElement = tableAdapter.getCell(previousRow, previousColumn);
// ... and make it unfocusable (now there is only a single focusable cell)
Table.setTabIndex(previousElement, -1);
// finally, focus the current cell (skipped during initilzation)
Table.focus(currentElement);
});
```
*/
class DataGridInteractionModel {
/**
* `DataGridInteractionModel` requires knowledge of events, and a representation of your table/grid to be useful.
*
* @param keyboardEventStream an Observable of KeyboardEvents. Should be scoped to the table container.
* @param clickEventStream an Observable of ClickEvents. should only include clicks that take action on items known by the TableAdapter
* @param tableAdapter an instance of a concrete class that implements TableAdapter. The standard carbon table uses TableDomAdapter
*/
constructor(keyboardEventStream, clickEventStream, tableAdapter) {
this.keyboardEventStream = keyboardEventStream;
this.clickEventStream = clickEventStream;
this.tableAdapter = tableAdapter;
/**
* Internal subject to handle changes in row
*/
this.rowSubject = new BehaviorSubject({ current: 0, previous: -1 });
/**
* Internal subject to handle changes in column
*/
this.columnSubject = new BehaviorSubject({ current: 0, previous: -1 });
this.rowIndex = this.rowSubject.asObservable();
this.columnIndex = this.columnSubject.asObservable();
this.position = combineLatest(this.rowIndex, this.columnIndex).pipe(map(positions => {