import { html, PropertyValueMap, unsafeCSS } from "lit";
import { property } from "lit/decorators.js";
import { FTcell } from "../f-tcell/f-tcell";
import { FTrow } from "../f-trow/f-trow";
import { FRoot, flowElement } from "@nonfx/flow-core";
import globalStyle from "./f-table-global.scss?inline";
import { injectCss } from "@nonfx/flow-core-config";
injectCss("f-table", globalStyle);
export type FTableVariant = "stripped" | "outlined" | "underlined" | "bordered";
export type FTableSize = "medium" | "small";
export type FTableSelectable = "single" | "multiple" | "none";
@flowElement("f-table")
export class FTable extends FRoot {
/**
* css loaded from scss file
*/
static styles = [unsafeCSS(globalStyle)];
/**
* @attribute Variants are various representations of a table. For example a table can be stripped,outlined,underlined or bordered..
*/
@property({ type: String, reflect: true })
variant?: FTableVariant = "stripped";
/**
* @attribute size to apply on each cell
*/
@property({ type: String, reflect: true })
size?: FTableSize = "medium";
/**
* @attribute whether to display checkbox or radiobox
*/
@property({ type: String, reflect: true })
selectable?: FTableSelectable = "none";
/**
* @attribute highlight selected row, when selectable has value "single" or "multiple"
*/
@property({ type: Boolean, reflect: true, attribute: "highlight-selected" })
highlightSelected = false;
// fix for vue
set ["highlight-selected"](val: boolean) {
this.highlightSelected = val;
}
/**
* @attribute highlight on hover
*/
@property({ type: Boolean, reflect: true, attribute: "highlight-hover" })
highlightHover = false;
// fix for vue
set ["highlight-hover"](val: boolean) {
this.highlightHover = val;
}
/**
* @attribute highlight on column hover
*/
@property({ type: Boolean, reflect: true, attribute: "highlight-column-hover" })
highlightColumnHover = true;
// fix for vue
set ["highlight-column-hover"](val: boolean) {
this.highlightColumnHover = val;
}
protected willUpdate(changedProperties: PropertyValueMap | Map): void {
super.willUpdate(changedProperties);
this.role = "table";
}
render() {
return html`
`;
}
protected async updated(
changedProperties: PropertyValueMap | Map
): Promise {
super.updated(changedProperties);
await this.propogateProps();
}
async propogateProps() {
this.updateGridTemplateColumns();
this.applySelectable();
await this.updateComplete;
if (this.selectable === "multiple") {
await this.updateHeaderSelectionCheckboxState();
}
if (this.selectable === "single") {
const headerRow = Array.from(this.children).filter(
el =>
el.tagName.toLocaleLowerCase() === "f-trow" &&
el.getAttribute("slot") === "header" &&
el.hasAttribute("selected")
)[0] as FTrow;
if (headerRow) {
headerRow.selected = false;
}
const selectedRow = Array.from(this.children).filter(
el =>
el.tagName.toLocaleLowerCase() === "f-trow" &&
el.getAttribute("slot") !== "header" &&
el.hasAttribute("selected")
)[0] as FTrow;
if (selectedRow) {
this.updateRadioChecks(selectedRow);
}
}
}
updateGridTemplateColumns() {
const firstRow = Array.from(this.children).filter(
el => el.tagName.toLocaleLowerCase() === "f-trow"
)[0] as FTrow;
if (firstRow !== null) {
/**
* following query is not working vue app so replaced with firstRow.children
* firstRow?.querySelectorAll(":scope > f-tcell");
* */
const firstRowCells = firstRow.children;
const noOfCells = firstRowCells.length;
let gridColumnTemplate = ``;
for (let i = 0; i < noOfCells; i++) {
const cellElement = firstRowCells.item(i) as FTcell;
if (cellElement.width) {
gridColumnTemplate += `${cellElement?.width} `;
} else {
gridColumnTemplate += `auto `;
}
}
this.style.gridTemplateColumns = gridColumnTemplate;
}
}
/**
* apply checkbox or radio based on selection property
*/
applySelectable() {
const allRows = Array.from(this.children).filter(
el => el.tagName.toLocaleLowerCase() === "f-trow"
) as FTrow[];
allRows.forEach(row => {
const firstChild = Array.from(row.children).find(child => {
return child.tagName === "F-TCELL";
}) as FTcell;
if (firstChild) {
firstChild.selectable = this.selectable;
setTimeout(() => firstChild.setSelection(row.selected, Boolean(row.disableSelection)));
}
});
}
/**
* if checkbox from header got clicked
* @param event
*/
async handleHeaderRowSelection(event: CustomEvent) {
event.stopPropagation();
if (this.selectable === "multiple") {
const allRows = Array.from(this.children).filter(
el => el.tagName.toLocaleLowerCase() === "f-trow"
) as FTrow[];
if (event.detail.value === true) {
allRows.forEach(r => {
if (!r.disableSelection) {
r.selected = true;
}
});
} else {
allRows.forEach(r => {
if (!r.disableSelection) {
r.selected = false;
}
});
}
await this.updateComplete;
this.dispatchSelectedRowEvent();
}
}
/**
* if checkbox or radio clicked in row
* @param event
*/
async handleRowSelection(event: CustomEvent<{ element: HTMLElement }>) {
event.stopPropagation();
this.updateRadioChecks(event.detail.element);
await this.updateHeaderSelectionCheckboxState();
await this.updateComplete;
this.dispatchSelectedRowEvent();
}
dispatchSelectedRowEvent() {
const selectedRows = Array.from(this.children).filter(
el =>
el.tagName.toLocaleLowerCase() === "f-trow" &&
el.getAttribute("slot") !== "header" &&
el.hasAttribute("selected")
) as FTrow[];
const toggle = new CustomEvent("selected-rows", {
detail: Array.from(selectedRows),
bubbles: true,
composed: true
});
this.dispatchEvent(toggle);
}
/**
* only one radio should be selected
* @param element
*/
updateRadioChecks(element: HTMLElement) {
if (this.selectable === "single") {
const allRows = Array.from(this.children).filter(
el => el.tagName.toLocaleLowerCase() === "f-trow" && el.getAttribute("slot") !== "header"
) as FTrow[];
allRows.forEach(row => {
if (row.selected && row !== element) {
row.selected = false;
}
});
}
}
/**
* update header checkbox based on rest of the selection
*/
async updateHeaderSelectionCheckboxState() {
if (this.selectable === "multiple") {
const allRows = Array.from(this.children).filter(
el => el.tagName.toLocaleLowerCase() === "f-trow"
) as FTrow[];
const rowsWithoutHeader = Array.from(allRows).filter(
r => r.getAttribute("slot") !== "header"
);
const selectedRows = rowsWithoutHeader.filter(r => r.selected);
const headerRow = Array.from(this.children).filter(
el => el.tagName.toLocaleLowerCase() === "f-trow" && el.getAttribute("slot") === "header"
)[0] as FTrow;
if (headerRow) {
await headerRow.updateComplete;
const firstCell = Array.from(headerRow.children).filter(
el => el.tagName.toLocaleLowerCase() === "f-tcell"
)[0] as FTcell;
if (firstCell?.checkbox) {
if (selectedRows.length === 0) {
headerRow.selected = false;
firstCell.checkbox.value = "unchecked";
} else if (selectedRows.length === rowsWithoutHeader.length) {
headerRow.selected = true;
} else {
firstCell.checkbox.value = "indeterminate";
}
}
}
}
}
toggleColumnHighlight(event: CustomEvent) {
if (event.detail.columnIndex >= 0 && this.highlightColumnHover) {
const allRows = Array.from(this.children).filter(
el => el.tagName.toLocaleLowerCase() === "f-trow"
) as FTrow[];
allRows.forEach(row => {
const allCells = Array.from(row.children).filter(
el => el.tagName.toLocaleLowerCase() === "f-tcell"
) as FTcell[];
if (event.detail.type === "add") {
allCells[event.detail.columnIndex]?.classList.add("highlight");
} else {
allCells[event.detail.columnIndex]?.classList.remove("highlight");
}
});
}
}
toggleColumnSelected(event: CustomEvent) {
if (event.detail.columnIndex >= 0) {
const allRows = Array.from(this.children).filter(
el => el.tagName.toLocaleLowerCase() === "f-trow"
) as FTrow[];
allRows.forEach(row => {
const allCells = Array.from(row.children).filter(
el => el.tagName.toLocaleLowerCase() === "f-tcell"
) as FTcell[];
if (allCells[event.detail.columnIndex]) {
allCells[event.detail.columnIndex].selected =
!allCells[event.detail.columnIndex].selected;
}
allCells.forEach(cell => {
if (cell !== allCells[event.detail.columnIndex]) {
cell.selected = false;
}
});
});
}
}
}
/**
* Required for typescript
*/
declare global {
export interface HTMLElementTagNameMap {
"f-table": FTable;
}
}