@smui/data-table
Version:
Svelte Material UI - Data Table
373 lines (332 loc) • 11.5 kB
text/typescript
/**
* @license
* Copyright 2019 Google Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import { MDCFoundation } from '@smui/common/base/foundation';
import type { MDCDataTableAdapter } from './adapter';
import { cssClasses, SortValue, attributes } from './constants';
import type { RowClickEventData, SortActionEventData } from './types';
/**
* The Foundation of data table component containing pure business logic, any
* logic requiring DOM manipulation are delegated to adapter methods.
*/
export class MDCDataTableFoundation extends MDCFoundation<MDCDataTableAdapter> {
static override get defaultAdapter(): MDCDataTableAdapter {
return {
addClass: () => undefined,
addClassAtRowIndex: () => undefined,
getAttributeByHeaderCellIndex: () => '',
getHeaderCellCount: () => 0,
getHeaderCellElements: () => [],
getRowCount: () => 0,
getRowElements: () => [],
getRowIdAtIndex: () => '',
getRowIndexByChildElement: () => 0,
getSelectedRowCount: () => 0,
getTableContainerHeight: () => 0,
getTableHeaderHeight: () => 0,
isCheckboxAtRowIndexChecked: () => false,
isHeaderRowCheckboxChecked: () => false,
isRowsSelectable: () => false,
notifyRowSelectionChanged: () => undefined,
notifySelectedAll: () => undefined,
notifySortAction: () => undefined,
notifyUnselectedAll: () => undefined,
notifyRowClick: () => undefined,
registerHeaderRowCheckbox: () => undefined,
registerRowCheckboxes: () => undefined,
removeClass: () => undefined,
removeClassAtRowIndex: () => undefined,
removeClassNameByHeaderCellIndex: () => undefined,
setAttributeAtRowIndex: () => undefined,
setAttributeByHeaderCellIndex: () => undefined,
setClassNameByHeaderCellIndex: () => undefined,
setHeaderRowCheckboxChecked: () => undefined,
setHeaderRowCheckboxIndeterminate: () => undefined,
setProgressIndicatorStyles: () => undefined,
setRowCheckboxCheckedAtIndex: () => undefined,
setSortStatusLabelByHeaderCellIndex: () => undefined,
};
}
constructor(adapter?: Partial<MDCDataTableAdapter>) {
super({ ...MDCDataTableFoundation.defaultAdapter, ...adapter });
}
/**
* Re-initializes header row checkbox and row checkboxes when selectable rows
* are added or removed from table. Use this if registering checkbox is
* synchronous.
*/
layout() {
if (this.adapter.isRowsSelectable()) {
this.adapter.registerHeaderRowCheckbox();
this.adapter.registerRowCheckboxes();
this.setHeaderRowCheckboxState();
}
}
/**
* Re-initializes header row checkbox and row checkboxes when selectable rows
* are added or removed from table. Use this if registering checkbox is
* asynchronous.
*/
async layoutAsync(): Promise<void> {
if (this.adapter.isRowsSelectable()) {
await this.adapter.registerHeaderRowCheckbox();
await this.adapter.registerRowCheckboxes();
this.setHeaderRowCheckboxState();
}
}
/**
* @return Returns array of row elements.
*/
getRows(): HTMLElement[] {
return this.adapter.getRowElements();
}
/**
* @return Array of header cell elements.
*/
getHeaderCells(): Element[] {
return this.adapter.getHeaderCellElements();
}
/**
* Sets selected row ids. Overwrites previously selected rows.
* @param rowIds Array of row ids that needs to be selected.
*/
setSelectedRowIds(rowIds: string[]) {
for (let rowIndex = 0; rowIndex < this.adapter.getRowCount(); rowIndex++) {
const rowId = this.adapter.getRowIdAtIndex(rowIndex);
let isSelected = false;
if (rowId && rowIds.indexOf(rowId) >= 0) {
isSelected = true;
}
this.adapter.setRowCheckboxCheckedAtIndex(rowIndex, isSelected);
this.selectRowAtIndex(rowIndex, isSelected);
}
this.setHeaderRowCheckboxState();
}
/**
* @return Returns array of all row ids.
*/
getRowIds(): Array<string | null> {
const rowIds = [];
for (let rowIndex = 0; rowIndex < this.adapter.getRowCount(); rowIndex++) {
rowIds.push(this.adapter.getRowIdAtIndex(rowIndex));
}
return rowIds;
}
/**
* @return Returns array of selected row ids.
*/
getSelectedRowIds(): Array<string | null> {
const selectedRowIds: Array<string | null> = [];
for (let rowIndex = 0; rowIndex < this.adapter.getRowCount(); rowIndex++) {
if (this.adapter.isCheckboxAtRowIndexChecked(rowIndex)) {
selectedRowIds.push(this.adapter.getRowIdAtIndex(rowIndex));
}
}
return selectedRowIds;
}
/**
* Handles header row checkbox change event.
*/
handleHeaderRowCheckboxChange() {
const isHeaderChecked = this.adapter.isHeaderRowCheckboxChecked();
for (let rowIndex = 0; rowIndex < this.adapter.getRowCount(); rowIndex++) {
this.adapter.setRowCheckboxCheckedAtIndex(rowIndex, isHeaderChecked);
this.selectRowAtIndex(rowIndex, isHeaderChecked);
}
if (isHeaderChecked) {
this.adapter.notifySelectedAll();
} else {
this.adapter.notifyUnselectedAll();
}
}
/**
* Handles change event originated from row checkboxes.
*/
handleRowCheckboxChange(event: Event) {
const rowIndex = this.adapter.getRowIndexByChildElement(
event.target as HTMLInputElement,
);
if (rowIndex === -1) {
return;
}
const selected = this.adapter.isCheckboxAtRowIndexChecked(rowIndex);
this.selectRowAtIndex(rowIndex, selected);
this.setHeaderRowCheckboxState();
const rowId = this.adapter.getRowIdAtIndex(rowIndex);
this.adapter.notifyRowSelectionChanged({ rowId, rowIndex, selected });
}
/**
* Handles sort action on sortable header cell.
*/
handleSortAction(eventData: SortActionEventData) {
const { columnId, columnIndex, headerCell } = eventData;
// Reset sort attributes / classes on other header cells.
for (let index = 0; index < this.adapter.getHeaderCellCount(); index++) {
if (index === columnIndex) {
continue;
}
this.adapter.removeClassNameByHeaderCellIndex(
index,
cssClasses.HEADER_CELL_SORTED,
);
this.adapter.removeClassNameByHeaderCellIndex(
index,
cssClasses.HEADER_CELL_SORTED_DESCENDING,
);
this.adapter.setAttributeByHeaderCellIndex(
index,
attributes.ARIA_SORT,
SortValue.NONE,
);
this.adapter.setSortStatusLabelByHeaderCellIndex(index, SortValue.NONE);
}
// Set appropriate sort attributes / classes on target header cell.
this.adapter.setClassNameByHeaderCellIndex(
columnIndex,
cssClasses.HEADER_CELL_SORTED,
);
const currentSortValue = this.adapter.getAttributeByHeaderCellIndex(
columnIndex,
attributes.ARIA_SORT,
);
let sortValue = SortValue.NONE;
// Set to descending if sorted on ascending order.
if (currentSortValue === SortValue.ASCENDING) {
this.adapter.setClassNameByHeaderCellIndex(
columnIndex,
cssClasses.HEADER_CELL_SORTED_DESCENDING,
);
this.adapter.setAttributeByHeaderCellIndex(
columnIndex,
attributes.ARIA_SORT,
SortValue.DESCENDING,
);
sortValue = SortValue.DESCENDING;
// Set to ascending if sorted on descending order.
} else if (currentSortValue === SortValue.DESCENDING) {
this.adapter.removeClassNameByHeaderCellIndex(
columnIndex,
cssClasses.HEADER_CELL_SORTED_DESCENDING,
);
this.adapter.setAttributeByHeaderCellIndex(
columnIndex,
attributes.ARIA_SORT,
SortValue.ASCENDING,
);
sortValue = SortValue.ASCENDING;
} else {
// Set to ascending by default when not sorted.
this.adapter.setAttributeByHeaderCellIndex(
columnIndex,
attributes.ARIA_SORT,
SortValue.ASCENDING,
);
sortValue = SortValue.ASCENDING;
}
this.adapter.setSortStatusLabelByHeaderCellIndex(columnIndex, sortValue);
this.adapter.notifySortAction({
columnId,
columnIndex,
headerCell,
sortValue,
});
}
/**
* Handles data table row click event.
*/
handleRowClick({
rowId,
row,
altKey,
ctrlKey,
metaKey,
shiftKey,
}: RowClickEventData) {
this.adapter.notifyRowClick({
rowId,
row,
altKey,
ctrlKey,
metaKey,
shiftKey,
});
}
/**
* Shows progress indicator blocking only the table body content when in
* loading state.
*/
showProgress() {
const tableHeaderHeight = this.adapter.getTableHeaderHeight();
// Calculate the height of table content (Not scroll content) excluding
// header row height.
const height = this.adapter.getTableContainerHeight() - tableHeaderHeight;
const top = tableHeaderHeight;
this.adapter.setProgressIndicatorStyles({
height: `${height}px`,
top: `${top}px`,
});
this.adapter.addClass(cssClasses.IN_PROGRESS);
}
/**
* Hides progress indicator when data table is finished loading.
*/
hideProgress() {
this.adapter.removeClass(cssClasses.IN_PROGRESS);
}
/**
* Updates header row checkbox state based on number of rows selected.
*/
private setHeaderRowCheckboxState() {
if (this.adapter.getSelectedRowCount() === 0) {
this.adapter.setHeaderRowCheckboxChecked(false);
this.adapter.setHeaderRowCheckboxIndeterminate(false);
} else if (
this.adapter.getSelectedRowCount() === this.adapter.getRowCount()
) {
this.adapter.setHeaderRowCheckboxChecked(true);
this.adapter.setHeaderRowCheckboxIndeterminate(false);
} else {
this.adapter.setHeaderRowCheckboxIndeterminate(true);
this.adapter.setHeaderRowCheckboxChecked(false);
}
}
/**
* Sets the attributes of row element based on selection state.
*/
private selectRowAtIndex(rowIndex: number, selected: boolean) {
if (selected) {
this.adapter.addClassAtRowIndex(rowIndex, cssClasses.ROW_SELECTED);
this.adapter.setAttributeAtRowIndex(
rowIndex,
attributes.ARIA_SELECTED,
'true',
);
} else {
this.adapter.removeClassAtRowIndex(rowIndex, cssClasses.ROW_SELECTED);
this.adapter.setAttributeAtRowIndex(
rowIndex,
attributes.ARIA_SELECTED,
'false',
);
}
}
}