import {html} from 'lit';
import {property, query, queryAssignedElements} from 'lit/decorators.js';
import {DataTableColumn, DataTableRow} from './mwa-data-table';
import {
MDCDataTable,
MDCDataTableAdapter,
MDCDataTableFoundation,
MDCDataTableRowSelectionChangedEventDetail,
ProgressIndicatorStyles,
RowClickEventData,
SortActionEventDetail
} from '@material/data-table';
import {cssClasses, messages, SortValue, strings} from '@material/data-table/constants';
import {BaseElement} from '@material/mwc-base';
import {observer} from '@material/mwc-base/observer';
import {Select} from '@material/mwc-select';
import {SelectedDetail} from '@material/mwc-list';
import {IconButton} from '@material/mwc-icon-button';
import '@material/mwc-icon-button';
import '@material/mwc-linear-progress';
import '@material/mwc-select';
import {LinearProgress} from '@material/mwc-linear-progress';
import {FilterTextFieldInputEventDetail} from './mwa-data-table-column';
export interface RowSelectionChangedDetail {
row: DataTableRow,
rowIndex: number,
selected: boolean
}
export interface FilteredDetail {
column: DataTableColumn,
text: string,
caseSensitive: boolean
columnIndex: number
}
export class DataTableBase extends BaseElement {
/**
* Enable/disable pagination.
*/
@property({type: Boolean, reflect: true}) paginated = false;
/**
* JSON array with the page sizes to be used in the pagination and shown as page size select options.
*/
@property({type: String, reflect: true}) pageSizes = '[10, 25, 100]';
/**
* Label to show before the page size select.
*/
@property({type: String, reflect: true}) pageSizesLabel = 'Rows per page:';
/**
* Index of the first row to be shown on the current page.
*/
@property({type: Number, reflect: true}) firstRowOfPage = 1;
/** @internal */
protected pageSizesArray: number[] = JSON.parse(this.pageSizes);
/**
* Size of the current page.
*/
@property({type: Number, reflect: true}) currentPageSize = this.pageSizesArray[0];
/**
* Index of the last row to be shown on the current page.
*/
@property({type: Number, reflect: true}) lastRowOfPage = this.currentPageSize;
/**
* Label pattern to show after the page size select that indicates the current rows shown in the page.
* It should contain the following parameters: `:firstRow`, `:lastRow`, `:totalRows`
*/
@property({type: String}) paginationTotalLabel = ':firstRow-:lastRow of :totalRows';
/**
* Whether the loading indicator is active.
*/
@property({type: Boolean, reflect: true})
@observer(function(this: DataTableBase, value: boolean) {
if (value) {
this.mdcFoundation.showProgress();
} else {
this.mdcFoundation.hideProgress();
}
})
inProgress = false;
/**
* Overall height of the table. Available in three different measures.
*/
@property({type: String, reflect: true})
@observer(function(this: DataTableBase, value, ) {
for (const row of this.rows) {
row.setAttribute('density', value);
}
for (const column of this.columns) {
column.setAttribute('density', value);
}
})
density?: '' | 'tight' | 'comfortable' | 'dense' | 'compact';
/** @internal */
@queryAssignedElements({slot: 'header-cell', selector: 'mwa-data-table-column'}) columns!: DataTableColumn[];
/** @internal */
@queryAssignedElements({slot: 'row', selector: 'mwa-data-table-row'}) rows!: DataTableRow[];
/** @internal */
@query('.mdc-data-table') protected tableElement!: HTMLTableElement;
/** @internal */
@query('.mdc-data-table__table-container') protected tableContainerElement!: HTMLTableElement;
/** @internal */
@query('.mdc-data-table__header-row') protected headerRowElement!: HTMLTableSectionElement;
/** @internal */
@query('.mdc-data-table__progress-indicator') protected progressIndicator!: LinearProgress;
/** @internal */
protected mdcDataTable?: MDCDataTable;
/** @internal */
protected mdcRoot: HTMLDivElement = this.tableElement;
/** @internal */
// @ts-ignore (TypeScript bug)
protected readonly mdcFoundationClass = MDCDataTableFoundation;
/** @internal */
// @ts-ignore (TypeScript bug)
protected mdcFoundation!: MDCDataTableFoundation;
/** @internal */
protected get headerCheckboxRow() {
return this.columns.filter((column) => column.checkbox !== undefined)[0];
}
/** @internal */
protected get headerCheckbox() {
return this.headerCheckboxRow?.checkbox;
}
render() {
return html`
${this.renderPagination()}
`;
}
/** @internal */
rowCallback = (e: Event) => this.mdcFoundation.handleRowCheckboxChange(e);
/** @internal */
headerRowCallback = () => this.mdcFoundation.handleHeaderRowCheckboxChange();
/** @internal */
filterColumnCallback = (e: Event) => {
const event = e as CustomEvent;
const index = this.columns.indexOf(event.detail.column);
let {text} = event.detail;
for (const row of this.rows) {
row.hidden = true;
}
const rowsToShow = this.rows.filter((row) => {
let cellText = row.cells[index].textContent ?? '';
if (!event.detail.caseSensitive) {
cellText = cellText.toLowerCase();
text = text.toLowerCase();
}
return cellText.search(text) !== -1;
});
this.showRows(rowsToShow);
/**
* Event emitted when the data table has been filtered.
*
* Event detail: `FilteredDetail`;
*/
this.dispatchEvent(new CustomEvent('filtered', {
detail: {
column: event.detail.column,
text,
caseSensitive: event.detail.caseSensitive,
columnIndex: index
}
}));
};
protected renderPagination() {
if (this.paginated) {
const initialPageLabel = this.firstRowOfPage < 1 ? 1 : this.firstRowOfPage;
const lastPageLabel = this.lastRowOfPage > this.rows.length ? this.rows.length : this.lastRowOfPage;
return html`
`;
}
return;
}
protected onPageSizeSelected(e: CustomEvent) {
const select = e.target as Select;
this.currentPageSize = Number.parseInt(select.value);
this.paginate('first');
}
protected onPaginationButtonClicked(event: Event) {
let button = event.target as HTMLElement;
if (!(button instanceof IconButton)) {
button = button.closest('mwc-icon-button') as IconButton;
}
const action = button.dataset.page as 'first' | 'previous' | 'next' | 'last';
this.paginate(action);
}
protected paginate(action: 'current' | 'first' | 'previous' | 'next' | 'last' = 'current') {
this.pageSizesArray = JSON.parse(this.pageSizes);
if (!this.pageSizesArray.includes(this.currentPageSize)) {
this.currentPageSize = this.pageSizesArray[0];
}
if (this.paginated) {
this.hideRows();
switch (action) {
case 'first':
this.firstRowOfPage = 1;
this.lastRowOfPage = this.currentPageSize;
break;
case 'previous':
this.firstRowOfPage -= this.currentPageSize;
this.lastRowOfPage -= this.currentPageSize;
break;
case 'next':
this.firstRowOfPage += this.currentPageSize;
this.lastRowOfPage += this.currentPageSize;
break;
case 'last':
this.firstRowOfPage = this.rows.length - this.currentPageSize + 1;
this.lastRowOfPage = this.rows.length;
break;
}
const rowsToShow = this.rows.slice(this.firstRowOfPage - 1, this.lastRowOfPage);
this.showRows(rowsToShow);
}
}
protected firstUpdated() {
super.firstUpdated();
this.paginate('first');
}
protected updated(_changedProperties) {
super.updated(_changedProperties);
for (const row of this.rows) {
row.removeEventListener('selected', this.rowCallback);
row.addEventListener('selected', this.rowCallback);
}
for (const column of this.columns) {
column.removeEventListener('filter', this.filterColumnCallback);
column.addEventListener('filter', this.filterColumnCallback);
}
this.headerCheckboxRow?.removeEventListener('checked', this.headerRowCallback);
this.headerCheckboxRow?.addEventListener('checked', this.headerRowCallback);
this.paginate();
this.mdcFoundation.layout();
}
protected createAdapter(): MDCDataTableAdapter {
type ClassName = typeof cssClasses[keyof typeof cssClasses];
return {
addClass: (className: ClassName) => {
switch (className) {
case cssClasses.IN_PROGRESS:
this.tableElement.classList.add(cssClasses.IN_PROGRESS);
break;
}
},
removeClass: (className: ClassName) => {
switch (className) {
case cssClasses.IN_PROGRESS:
this.tableElement.classList.remove(cssClasses.IN_PROGRESS);
break;
}
},
addClassAtRowIndex: (rowIndex: number, className: ClassName) => {
switch (className) {
case cssClasses.ROW_SELECTED:
this.rows[rowIndex].selected = true;
}
},
getRowCount: () => this.rows.length,
getRowElements: () => this.rows,
getRowIdAtIndex: (rowIndex: number) => this.rows?.[rowIndex].id ?? null,
getRowIndexByChildElement: (el: Element) => this.rows.findIndex((row) => row.contains(el)),
getSelectedRowCount: () => this.rows.filter((row) => row.selected).length,
isCheckboxAtRowIndexChecked: (rowIndex: number) => this.rows[rowIndex].selected,
isHeaderRowCheckboxChecked: () => this.headerCheckbox?.checked ?? false,
isRowsSelectable: () => this.headerCheckbox !== undefined ||
this.rows.filter((row) => row.checkboxCell !== undefined).length > 0,
notifyRowSelectionChanged: (data: MDCDataTableRowSelectionChangedEventDetail) => {
/**
* Event emitted when row checkbox is checked or unchecked.
*
* Event detail: `RowSelectionChangedDetail`.
*/
this.dispatchEvent(new CustomEvent(
'rowSelectionChanged',
{
detail: {
row: this.rows[data.rowIndex],
rowIndex: data.rowIndex,
selected: data.selected,
}
}
));
},
notifySelectedAll: () => {
/**
* Event emitted when header row checkbox is checked.
*/
this.dispatchEvent(new CustomEvent('selectedAll'));
},
notifyUnselectedAll: () => {
/**
* Event emitted when header row checkbox is unchecked.
*/
this.dispatchEvent(new CustomEvent('unselectedAll'));
},
notifyRowClick: (detail: RowClickEventData) => {
/**
* Event emitted when a row has been checked or unchecked.
*
* Event detail: `RowClickEventData`.
*/
this.dispatchEvent(new CustomEvent('rowClick', {detail}));
},
registerHeaderRowCheckbox: () => {},
registerRowCheckboxes: () => {},
removeClassAtRowIndex: (rowIndex: number, className: ClassName) => {
switch (className) {
case cssClasses.ROW_SELECTED:
this.rows[rowIndex].selected = false;
}
},
setAttributeAtRowIndex: (rowIndex: number, attr: string, value: string) => {
const row = this.rows[rowIndex];
if (row) {
row.setAttribute(attr, value);
switch (attr) {
case strings.ARIA_SELECTED:
row.selected = value === 'true';
break;
}
}
},
setHeaderRowCheckboxChecked: (checked: boolean) => {
if (this.headerCheckbox) {
this.headerCheckbox.checked = checked;
}
},
setHeaderRowCheckboxIndeterminate: (indeterminate: boolean) => {
if (this.headerCheckbox) {
this.headerCheckbox.indeterminate = indeterminate;
}
},
setRowCheckboxCheckedAtIndex: (rowIndex: number, checked: boolean) => {
const row = this.rows[rowIndex];
if (row) {
row.selected = checked;
}
},
getHeaderCellCount: () => this.columns.length,
getHeaderCellElements: () => this.columns,
getAttributeByHeaderCellIndex: (columnIndex: number, attribute: string) => this.columns[columnIndex].getAttribute(attribute),
setAttributeByHeaderCellIndex: (columnIndex: number, attribute: string, value: string) => {
this.columns[columnIndex].setAttribute(attribute, value);
},
setClassNameByHeaderCellIndex: (columnIndex: number, className: string) => {
const attributesMapping = {
[cssClasses.HEADER_CELL_SORTED]: 'sorted',
[cssClasses.HEADER_CELL_SORTED_DESCENDING]: 'sortedDescending'
};
this.columns[columnIndex].toggleAttribute(attributesMapping[className], true);
},
removeClassNameByHeaderCellIndex: (columnIndex: number, className: string) => {
const attributesMapping = {
[cssClasses.HEADER_CELL_SORTED]: 'sorted',
[cssClasses.HEADER_CELL_SORTED_DESCENDING]: 'sortedDescending'
};
this.columns[columnIndex].toggleAttribute(attributesMapping[className], false);
},
notifySortAction: (data: SortActionEventDetail) => {
/**
* Event emitted when a column has been sorted.
*
* Event detail: `SortActionEventDetail`.
*/
this.dispatchEvent(new CustomEvent('sorted', {detail: data}));
},
getTableContainerHeight: () => this.tableContainerElement.getBoundingClientRect().height,
getTableHeaderHeight: () => this.headerRowElement.getBoundingClientRect().height,
setProgressIndicatorStyles: (styles: ProgressIndicatorStyles) => {
this.progressIndicator.style.top = styles.top;
this.progressIndicator.style.height = styles.height;
},
setSortStatusLabelByHeaderCellIndex: (columnIndex: number, sortValue: SortValue) => {
const column = this.columns[columnIndex];
if (column.sortable) {
const mappings = {
[SortValue.ASCENDING]: messages.SORTED_IN_ASCENDING,
[SortValue.DESCENDING]: messages.SORTED_IN_DESCENDING,
};
column.sortButton!.ariaLabel = mappings[sortValue];
}
}
};
}
protected renderTemplate(template: string, params: Object) {
for (const [key, value] of Object.entries(params)) {
template = template.replace(`:${key}`, value);
}
return template;
}
protected hideRows(rows: DataTableRow[] = this.rows) {
for (const row of rows) {
row.hidden = true;
row.classList.remove('without-bottom-border');
}
}
protected showRows(rows: DataTableRow[] = this.rows) {
for (const row of rows) {
row.hidden = false;
}
// Add bottom border to the last row
rows.slice(-1)[0]?.classList.add('without-bottom-border');
}
}