@smui/data-table
Version:
Svelte Material UI - Data Table
429 lines (386 loc) • 14.2 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 { MDCComponent } from '@smui/common/base/component';
import type { SpecificEventListener } from '@smui/common/base/types';
import { MDCCheckbox, type MDCCheckboxFactory } from '@smui/checkbox/component';
import { closest } from '@smui/common/dom/ponyfill';
import { MDCLinearProgress } from '@smui/linear-progress/component';
import type { MDCDataTableAdapter } from './adapter';
import {
cssClasses,
dataAttributes,
events,
messages,
selectors,
SortValue,
} from './constants';
import { MDCDataTableFoundation } from './foundation';
import type {
MDCDataTableRowSelectionChangedEventDetail,
RowClickEventData,
} from './types';
/** MDC Data Table */
export class MDCDataTable extends MDCComponent<MDCDataTableFoundation> {
static override attachTo(root: HTMLElement): MDCDataTable {
return new MDCDataTable(root);
}
private headerRowCheckbox!: MDCCheckbox;
private rowCheckboxList!: MDCCheckbox[];
private checkboxFactory!: MDCCheckboxFactory;
private linearProgress!: MDCLinearProgress;
private headerRow!: HTMLElement;
private content!: HTMLElement;
private handleHeaderRowCheckboxChange!: SpecificEventListener<'change'>;
private handleRowCheckboxChange!: SpecificEventListener<'change'>;
private headerRowClickListener!: SpecificEventListener<'click'>; // Assigned in `initialSyncWithDOM()`
private handleContentClick!: SpecificEventListener<'click'>; // Assigned in `initialSyncWithDOM()`
override initialize(
checkboxFactory: MDCCheckboxFactory = (el: HTMLElement) =>
new MDCCheckbox(el),
) {
this.checkboxFactory = checkboxFactory;
}
override initialSyncWithDOM() {
this.headerRow = this.root.querySelector<HTMLElement>(
`.${cssClasses.HEADER_ROW}`,
)!;
this.handleHeaderRowCheckboxChange = () => {
this.foundation.handleHeaderRowCheckboxChange();
};
this.headerRow.addEventListener(
'change',
this.handleHeaderRowCheckboxChange,
);
this.headerRowClickListener = (event) => {
this.handleHeaderRowClick(event);
};
this.headerRow.addEventListener('click', this.headerRowClickListener);
this.content = this.root.querySelector<HTMLElement>(
`.${cssClasses.CONTENT}`,
)!;
this.handleContentClick = (event) => {
const dataRowEl = closest(
event.target as Element,
selectors.ROW,
) as HTMLElement;
if (!dataRowEl) return;
this.foundation.handleRowClick({
rowId: this.getRowIdByRowElement(dataRowEl),
row: dataRowEl,
altKey: event.altKey,
ctrlKey: event.ctrlKey,
metaKey: event.metaKey,
shiftKey: event.shiftKey,
});
};
this.content.addEventListener('click', this.handleContentClick);
this.handleRowCheckboxChange = (event) => {
this.foundation.handleRowCheckboxChange(event);
};
this.content.addEventListener('change', this.handleRowCheckboxChange);
this.layout();
}
/**
* Re-initializes header row checkbox and row checkboxes when selectable rows
* are added or removed from table.
*/
layout() {
this.foundation.layout();
}
/**
* @return Returns array of header row cell elements.
*/
getHeaderCells() {
return Array.from(
this.root.querySelectorAll<HTMLElement>(selectors.HEADER_CELL),
);
}
/**
* @return Returns array of row elements.
*/
getRows(): HTMLElement[] {
return this.foundation.getRows();
}
/**
* @return Returns array of selected row ids.
*/
getSelectedRowIds(): Array<string | null> {
return this.foundation.getSelectedRowIds();
}
/**
* Sets selected row ids. Overwrites previously selected rows.
* @param rowIds Array of row ids that needs to be selected.
*/
setSelectedRowIds(rowIds: string[]) {
this.foundation.setSelectedRowIds(rowIds);
}
/**
* Shows progress indicator when data table is in loading state.
*/
showProgress() {
this.getLinearProgress().open();
this.foundation.showProgress();
}
/**
* Hides progress indicator after data table is finished loading.
*/
hideProgress() {
this.foundation.hideProgress();
this.getLinearProgress().close();
}
override destroy() {
if (this.handleHeaderRowCheckboxChange) {
this.headerRow.removeEventListener(
'change',
this.handleHeaderRowCheckboxChange,
);
}
if (this.headerRowClickListener) {
this.headerRow.removeEventListener('click', this.headerRowClickListener);
}
if (this.handleRowCheckboxChange) {
this.content.removeEventListener('change', this.handleRowCheckboxChange);
}
if (this.headerRowCheckbox) {
this.headerRowCheckbox.destroy();
}
if (this.rowCheckboxList) {
for (const checkbox of this.rowCheckboxList) {
checkbox.destroy();
}
}
if (this.handleContentClick) {
this.content.removeEventListener('click', this.handleContentClick);
}
}
override getDefaultFoundation() {
// DO NOT INLINE this variable. For backward compatibility, foundations take
// a Partial<MDCFooAdapter>. To ensure we don't accidentally omit any
// methods, we need a separate, strongly typed adapter variable.
// tslint:disable:object-literal-sort-keys Methods should be in the same order as the adapter interface.
const adapter: MDCDataTableAdapter = {
addClass: (className) => {
this.root.classList.add(className);
},
removeClass: (className) => {
this.root.classList.remove(className);
},
getHeaderCellElements: () => this.getHeaderCells(),
getHeaderCellCount: () => this.getHeaderCells().length,
getAttributeByHeaderCellIndex: (index, attribute) => {
return this.getHeaderCells()[index].getAttribute(attribute);
},
setAttributeByHeaderCellIndex: (index, attribute, value) => {
this.safeSetAttribute(this.getHeaderCells()[index], attribute, value);
},
setClassNameByHeaderCellIndex: (index, className) => {
this.getHeaderCells()[index].classList.add(className);
},
removeClassNameByHeaderCellIndex: (index, className) => {
this.getHeaderCells()[index].classList.remove(className);
},
notifySortAction: (data) => {
this.emit(events.SORTED, data, /** shouldBubble */ true);
},
getTableContainerHeight: () => {
const tableContainer = this.root.querySelector<HTMLElement>(
`.${cssClasses.TABLE_CONTAINER}`,
);
if (!tableContainer) {
throw new Error('MDCDataTable: Table container element not found.');
}
return tableContainer.getBoundingClientRect().height;
},
getTableHeaderHeight: () => {
const tableHeader = this.root.querySelector<HTMLElement>(
selectors.HEADER_ROW,
);
if (!tableHeader) {
throw new Error('MDCDataTable: Table header element not found.');
}
return tableHeader.getBoundingClientRect().height;
},
setProgressIndicatorStyles: (styles) => {
const progressIndicator = this.root.querySelector<HTMLElement>(
selectors.PROGRESS_INDICATOR,
);
if (!progressIndicator) {
throw new Error(
'MDCDataTable: Progress indicator element not found.',
);
}
progressIndicator.style.setProperty('height', styles.height);
progressIndicator.style.setProperty('top', styles.top);
},
addClassAtRowIndex: (rowIndex: number, className: string) => {
this.getRows()[rowIndex].classList.add(className);
},
getRowCount: () => this.getRows().length,
getRowElements: () =>
Array.from(this.root.querySelectorAll<HTMLElement>(selectors.ROW)),
getRowIdAtIndex: (rowIndex: number) =>
this.getRows()[rowIndex].getAttribute(dataAttributes.ROW_ID),
getRowIndexByChildElement: (el: Element) => {
return this.getRows().indexOf(
closest(el, selectors.ROW) as HTMLElement,
);
},
getSelectedRowCount: () =>
this.root.querySelectorAll<HTMLElement>(selectors.ROW_SELECTED).length,
isCheckboxAtRowIndexChecked: (rowIndex: number) =>
this.rowCheckboxList[rowIndex].checked,
isHeaderRowCheckboxChecked: () => this.headerRowCheckbox.checked,
isRowsSelectable: () =>
!!this.root.querySelector<HTMLElement>(selectors.ROW_CHECKBOX) ||
!!this.root.querySelector<HTMLElement>(selectors.HEADER_ROW_CHECKBOX),
notifyRowSelectionChanged: (
data: MDCDataTableRowSelectionChangedEventDetail,
) => {
this.emit(
events.ROW_SELECTION_CHANGED,
{
row: this.getRowByIndex(data.rowIndex),
rowId: this.getRowIdByIndex(data.rowIndex),
rowIndex: data.rowIndex,
selected: data.selected,
},
/** shouldBubble */ true,
);
},
notifySelectedAll: () => {
this.emit(events.SELECTED_ALL, {}, /** shouldBubble */ true);
},
notifyUnselectedAll: () => {
this.emit(events.UNSELECTED_ALL, {}, /** shouldBubble */ true);
},
notifyRowClick: (data: RowClickEventData) => {
this.emit(events.ROW_CLICK, data, /** shouldBubble */ true);
},
registerHeaderRowCheckbox: () => {
if (this.headerRowCheckbox) {
this.headerRowCheckbox.destroy();
}
const checkboxEl = this.root.querySelector<HTMLElement>(
selectors.HEADER_ROW_CHECKBOX,
)!;
this.headerRowCheckbox = this.checkboxFactory(checkboxEl);
},
registerRowCheckboxes: () => {
if (this.rowCheckboxList) {
this.rowCheckboxList.forEach((checkbox) => {
checkbox.destroy();
});
}
this.rowCheckboxList = [];
this.getRows().forEach((rowEl) => {
const checkbox = this.checkboxFactory(
rowEl.querySelector<HTMLElement>(selectors.ROW_CHECKBOX)!,
);
this.rowCheckboxList.push(checkbox);
});
},
removeClassAtRowIndex: (rowIndex: number, className: string) => {
this.getRows()[rowIndex].classList.remove(className);
},
setAttributeAtRowIndex: (
rowIndex: number,
attr: string,
value: string,
) => {
this.safeSetAttribute(this.getRows()[rowIndex], attr, value);
},
setHeaderRowCheckboxChecked: (checked: boolean) => {
this.headerRowCheckbox.checked = checked;
},
setHeaderRowCheckboxIndeterminate: (indeterminate: boolean) => {
this.headerRowCheckbox.indeterminate = indeterminate;
},
setRowCheckboxCheckedAtIndex: (rowIndex: number, checked: boolean) => {
this.rowCheckboxList[rowIndex].checked = checked;
},
setSortStatusLabelByHeaderCellIndex: (
columnIndex: number,
sortValue: SortValue,
) => {
const headerCell = this.getHeaderCells()[columnIndex];
const sortStatusLabel = headerCell.querySelector<HTMLElement>(
selectors.SORT_STATUS_LABEL,
);
if (!sortStatusLabel) return;
sortStatusLabel.textContent =
this.getSortStatusMessageBySortValue(sortValue);
},
};
return new MDCDataTableFoundation(adapter);
}
private getRowByIndex(index: number): Element {
return this.getRows()[index];
}
private getRowIdByIndex(index: number): string | null {
return this.getRowByIndex(index).getAttribute(dataAttributes.ROW_ID);
}
private handleHeaderRowClick(event: Event): void {
const headerCell = closest(
event.target as Element,
selectors.HEADER_CELL_WITH_SORT,
) as HTMLElement;
if (!headerCell) {
return;
}
const columnId = headerCell.getAttribute(dataAttributes.COLUMN_ID);
const columnIndex = this.getHeaderCells().indexOf(headerCell);
if (columnIndex === -1) {
return;
}
this.foundation.handleSortAction({ columnId, columnIndex, headerCell });
}
private getSortStatusMessageBySortValue(sortValue: SortValue): string {
switch (sortValue) {
case SortValue.ASCENDING:
return messages.SORTED_IN_ASCENDING;
case SortValue.DESCENDING:
return messages.SORTED_IN_DESCENDING;
default:
return '';
}
}
private getLinearProgressElement(): HTMLElement {
const el = this.root.querySelector<HTMLElement>(
`.${cssClasses.LINEAR_PROGRESS}`,
);
if (!el) {
throw new Error('MDCDataTable: linear progress element is not found.');
}
return el;
}
private getLinearProgress(): MDCLinearProgress {
if (!this.linearProgress) {
const el = this.getLinearProgressElement();
this.linearProgress = new MDCLinearProgress(el);
}
return this.linearProgress;
}
private getRowIdByRowElement(rowElement: HTMLElement): string | null {
return rowElement.getAttribute(dataAttributes.ROW_ID);
}
}