@kwiz/fluentui
Version:
KWIZ common controls for FluentUI
348 lines • 21.8 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { createTableColumn, makeStyles, Subtitle1, Table, TableBody, TableCell, TableCellActions, TableCellLayout, TableHeader, TableHeaderCell, TableRow, TableSelectionCell, tokens, useArrowNavigationGroup, useTableFeatures, useTableSort } from "@fluentui/react-components";
import { CheckboxCheckedRegular, CheckboxUncheckedRegular, ChevronCircleLeftFilled, ChevronCircleLeftRegular, ChevronCircleRightFilled, ChevronCircleRightRegular, EqualCircleFilled, EqualCircleRegular, FilterDismissRegular, FilterFilled, FilterRegular, MoreVerticalRegular } from "@fluentui/react-icons";
import { CommonLogger, dateFormat, filterEmptyEntries, firstOrNull, isBoolean, isDate, isFunction, isNotEmptyArray, isNotEmptyString, isNullOrEmptyArray, isNullOrEmptyString, isNullOrNaN, isNullOrUndefined, isNumber, isPrimitiveValue, isString, stopEvent } from "@kwiz/common";
import { useCallback, useMemo, useState } from "react";
import { useShowOnHover } from "../helpers";
import { KnownClassNames, mergeClassesEX } from "../styles/styles";
import { ButtonEX } from "./button";
import { DatePickerEx } from "./date";
import { Horizontal } from "./horizontal";
import { InputEx, InputNumberEx } from "./input";
import { MenuEx } from "./menu";
import { Vertical } from "./vertical";
const logger = new CommonLogger("table");
const cssNames = {
sortIcon: "sort-icon",
selectableTable: "selectable-table"
};
const selectionCellWidth = 32;
const backgroundFix = {
backgroundColor: tokens.colorNeutralBackground1,
":hover": {
color: tokens.colorNeutralForeground1Hover,
backgroundColor: tokens.colorSubtleBackgroundHover
}
};
const useStyles = makeStyles({
stickySelectionCell: Object.assign({ position: "sticky", left: 0, top: 0, zIndex: 30 }, backgroundFix),
stickySelectionHeaderCell: {
zIndex: 40
},
singleSelect: {
'&>*': { visibility: "hidden" }
},
firstCell: Object.assign(Object.assign({ position: "sticky", left: 0, zIndex: 10 }, backgroundFix), { [`&.${cssNames.selectableTable}`]: {
left: `${selectionCellWidth}px`
} }),
secondCellCover: Object.assign(Object.assign({ position: "sticky", left: '0', zIndex: 10, maxWidth: '200px' }, backgroundFix), { [`&.${cssNames.selectableTable}`]: {
left: `${selectionCellWidth}px`
} }),
secondCellSmall: Object.assign(Object.assign({ position: "sticky", left: '40px', zIndex: 10, maxWidth: '200px' }, backgroundFix), { [`&.${cssNames.selectableTable}`]: {
left: `${selectionCellWidth + 40}px`
} }),
secondCellMedium: Object.assign(Object.assign({ position: "sticky", left: '80px', zIndex: 10, maxWidth: '200px' }, backgroundFix), { [`&.${cssNames.selectableTable}`]: {
left: `${selectionCellWidth + 80}px`
} }),
th: Object.assign(Object.assign({ position: "sticky", top: 0, zIndex: 20, whiteSpace: "nowrap" }, backgroundFix), { '::after': {
content: '""',
position: "absolute",
backgroundColor: tokens.colorNeutralStroke1,
height: "1px",
left: 0, right: 0, bottom: 0
} }),
stickyColumnCell: {
'::after': {
content: '""',
position: "absolute",
backgroundColor: tokens.colorNeutralStroke1,
width: "1px",
top: 0, right: 0, bottom: 0
},
//make sure content covers the selection cell when it is not showing
[`&.${cssNames.selectableTable}::before`]: {
content: '""',
position: "absolute",
backgroundColor: tokens.colorNeutralBackground1,
width: "40px",
top: 0, left: '-40px', bottom: 0
}
},
stickyColumnCellPre: {
'::before': {
content: '""',
position: "absolute",
backgroundColor: tokens.colorNeutralStroke2,
width: "1px",
top: 0, left: 0, bottom: 0
}
},
first2THWhenSticky: {
zIndex: 30
},
table: {
tableLayout: "auto"
},
nowrap: {
whiteSpace: "nowrap"
},
emptyLabel: {
paddingTop: tokens.spacingVerticalM,
paddingBottom: tokens.spacingVerticalM,
}
});
export function TableEX(props) {
const { items, columns, getItemMenu, selectionMode, folders, getFolderMenu } = props;
const css = useStyles();
const showOnHover = useShowOnHover();
const fProps = props;
const freezed = fProps.stickyTop || fProps.stickyLeft || isString(props.maxHeight);
const keyboardNavAttr = useArrowNavigationGroup({ axis: "grid" });
const normalizedCols = useMemo(() => columns.map(c => isString(c) ? {
key: c,
renderer: c,
sortable: true,
} : Object.assign(Object.assign({}, c), { renderer: c.renderer || c.key })), [columns]);
// #region Styles
const secondCellClass = fProps.stickyLeftGap === "cover" ? css.secondCellCover : fProps.stickyLeftGap === "medium" ? css.secondCellMedium : css.secondCellSmall;
const headerCellClasses = fProps.stickyTop ? [css.th, showOnHover.hoverParent] : [showOnHover.hoverParent];
const firstHeaderCellClasses = headerCellClasses.slice();
const secondHeaderCellClasses = headerCellClasses.slice();
const firstCellClasses = fProps.stickyLeft ? [css.firstCell] : [];
const secondCellClasses = fProps.stickyLeft === 2 ? [secondCellClass] : [];
const selectionCellHeaderClasses = [css.stickySelectionHeaderCell];
const selectionCellClasses = [];
if (fProps.stickyLeft) {
firstHeaderCellClasses.splice(0, 0, css.firstCell);
firstHeaderCellClasses.push(css.first2THWhenSticky);
if (fProps.stickyLeft === 2) {
secondHeaderCellClasses.splice(0, 0, secondCellClass);
secondHeaderCellClasses.push(css.first2THWhenSticky);
secondCellClasses.push(css.stickyColumnCell);
secondCellClasses.push(css.stickyColumnCellPre);
}
else
firstCellClasses.push(css.stickyColumnCell);
}
if (isNotEmptyString(selectionMode)) {
selectionCellClasses.push(css.stickySelectionCell);
if (selectionMode === "single")
selectionCellHeaderClasses.push(css.singleSelect);
firstCellClasses.push(cssNames.selectableTable);
secondCellClasses.push(cssNames.selectableTable);
firstHeaderCellClasses.push(cssNames.selectableTable);
secondHeaderCellClasses.push(cssNames.selectableTable);
}
// #endregion
const menuIndex = fProps.stickyLeft === 2 ? 1 : 0;
const hasMenu = useMemo(() => isFunction(getItemMenu), [getItemMenu]);
const hasFolderMenu = useMemo(() => isFunction(getFolderMenu), [getItemMenu]);
function normalizeColValue(colValue) {
const normalizedColValue = isPrimitiveValue(colValue)
? {
primitiveValue: colValue, renderer: isNumber(colValue)
? colValue.toString(10)
: isDate(colValue)
? colValue.toDateString()
: isBoolean(colValue)
? () => (colValue
? _jsx(CheckboxCheckedRegular, {})
: _jsx(CheckboxUncheckedRegular, {}))
: colValue
}
: colValue;
return normalizedColValue;
}
function toPrimitiveValue(colValue) {
if (isPrimitiveValue(colValue))
return colValue;
else if (!isNullOrUndefined(colValue.primitiveValue))
return colValue.primitiveValue;
else if (isString(colValue.renderer))
return colValue.renderer;
else
return null;
}
// #region Filter
const [filter, setFilter] = useState();
const [filterop, setFilterop] = useState("eq");
const filteredItems = useMemo(() => {
if (!filter)
return items;
const filterCol = firstOrNull(normalizedCols, c => c.key === filter.column);
if (!(filterCol === null || filterCol === void 0 ? void 0 : filterCol.filter))
return items;
switch (filterCol.filter) {
case "string": {
const fValue = filter.value.toLowerCase();
return items.filter(i => {
const v = toPrimitiveValue(i[filterCol.key]);
if (isNotEmptyString(v))
return v.toLowerCase().includes(fValue);
else
return false;
});
}
case "bool": {
const fValue = filter.value === true;
return items.filter(i => {
const v = toPrimitiveValue(i[filterCol.key]);
if (fValue)
return v === true;
else
return v !== true; //false/null as false
});
}
case "number": {
const fValue = filter.value;
return items.filter(i => {
const v = toPrimitiveValue(i[filterCol.key]);
if (isNullOrNaN(v))
return false;
if (filterop === "gt")
return v >= fValue;
else if (filterop === "lt")
return v <= fValue;
else
return v === fValue;
});
}
case "date": {
const fValue = dateFormat(filter.value, "yyyyMMdd");
return items.filter(i => {
const v = toPrimitiveValue(i[filterCol.key]);
if (!isDate(v))
return false;
const vFormat = dateFormat(v, "yyyyMMdd");
if (filterop === "gt")
return vFormat >= fValue;
else if (filterop === "lt")
return vFormat <= fValue;
else
return vFormat === fValue;
});
}
}
}, [items, filter, normalizedCols, filterop]);
const filterMenuItemOperator = useMemo(() => ({
title: "Op", onClick: () => { },
as: _jsxs(Horizontal, { hSpaced: true, children: [_jsx(ButtonEX, { icon: filterop === "lt" ? _jsx(ChevronCircleLeftFilled, {}) : _jsx(ChevronCircleLeftRegular, {}), title: "Less than", onClick: (e) => { stopEvent(e); setFilterop("lt"); } }), _jsx(ButtonEX, { icon: filterop === "eq" ? _jsx(EqualCircleFilled, {}) : _jsx(EqualCircleRegular, {}), title: "Equals", onClick: (e) => { stopEvent(e); setFilterop("eq"); } }), _jsx(ButtonEX, { icon: filterop === "gt" ? _jsx(ChevronCircleRightFilled, {}) : _jsx(ChevronCircleRightRegular, {}), title: "Greater than", onClick: (e) => { stopEvent(e); setFilterop("gt"); } })] }, "filterMenuItemOperator")
}), [filterop]);
// #endregion
// #region Sort
const table_columns = useMemo(() => normalizedCols.map(col => createTableColumn({
columnId: col.key,
compare: (a, b) => {
const var1 = toPrimitiveValue(a[col.key]);
const var2 = toPrimitiveValue(b[col.key]);
if (isNullOrUndefined(var1) && isNullOrUndefined(var2))
return 0;
else if (isNullOrUndefined(var1))
return -1;
else if (isNullOrUndefined(var2))
return 1;
const var1x = isDate(var1) ? var1.getTime() : isBoolean(var1) ? var1 ? 1 : 0 : var1;
const var2x = isDate(var2) ? var2.getTime() : isBoolean(var2) ? var2 ? 1 : 0 : var2;
//both not null and must be same value type
return isString(var1x)
? var1x.localeCompare(var2)
: var1x - var2x;
}
})), [normalizedCols]);
const table_features = useTableFeatures({ columns: table_columns, items: filteredItems }, [
useTableSort({
defaultSortState: props.noDefaultSort ? undefined : { sortColumn: normalizedCols[0].key, sortDirection: "ascending" }
})
]);
const headerSortProps = (columnId) => ({
onClick: (e) => {
table_features.sort.toggleColumnSort(e, columnId);
},
sortDirection: table_features.sort.getSortDirection(columnId),
});
const rows = table_features.sort.sort(table_features.getRows());
// #endregion
// #selection
const { onSelect, selection, getItemKey } = Object.assign({ selection: [] }, props);
const allSelected = rows.length > 0 && selection.length === rows.length;
const toggleRow = useCallback((item) => {
if (isNotEmptyString(selectionMode)) {
const key = getItemKey(item);
if (!isNullOrUndefined(key)) {
if (!selection.includes(key)) {
if (selectionMode === "multiselect")
onSelect([...selection, key]); //add
else
onSelect([key]); //change - select one
}
else //selected
{
if (selectionMode === "multiselect") //remove selected
{
onSelect(selection.filter(s => s !== key));
}
}
}
}
}, [selection, onSelect, selectionMode]);
// #endregion
const tbl = _jsxs(Table, Object.assign({ className: mergeClassesEX(css.table, props.css) }, keyboardNavAttr, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [isNotEmptyString(selectionMode) && _jsx(TableSelectionCell, { type: "checkbox", className: mergeClassesEX(selectionCellClasses, selectionCellHeaderClasses), checked: allSelected ? true : selection.length > 0 ? "mixed" : false, onClick: selectionMode === "multiselect"
? () => onSelect(filterEmptyEntries(allSelected ? [] : rows.map(r => getItemKey(r.item)))) : undefined, onKeyDown: selectionMode === "multiselect"
? (e) => {
if (e.key === " ") {
stopEvent(e);
onSelect(filterEmptyEntries(allSelected ? [] : rows.map(r => getItemKey(r.item))));
}
} : undefined }), normalizedCols.map((col, coli) => _jsxs(TableHeaderCell, Object.assign({ className: mergeClassesEX((coli === 0 ? firstHeaderCellClasses : coli === 1 ? secondHeaderCellClasses : headerCellClasses), col.headerCss, col.nowrap ? css.nowrap : undefined) }, (col.sortable ? headerSortProps(col.key) : {}), { children: [isString(col.renderer) ? col.renderer : col.renderer(), col.filter
? _jsx(MenuEx, { trigger: {
icon: col.key === (filter === null || filter === void 0 ? void 0 : filter.column) ? _jsx(FilterFilled, {}) : _jsx(FilterRegular, {}),
title: "Filtering", className: col.key === (filter === null || filter === void 0 ? void 0 : filter.column) ? '' : showOnHover.showOnHover
}, items: [...(col.filter === "string"
? [
{ title: "Filter text", onClick: () => { }, as: _jsx(InputEx, { value: (filter === null || filter === void 0 ? void 0 : filter.value) || "", onClick: e => stopEvent(e), onChange: (e, data) => setFilter(isNullOrEmptyString(data.value) ? null : { column: col.key, value: data.value }) }, "filterInput") },
]
: col.filter === "number"
? [
{ title: "Filter number", onClick: () => { }, as: _jsx(InputNumberEx, { defaultValue: filter === null || filter === void 0 ? void 0 : filter.value, onClick: e => stopEvent(e), onChange: (num) => setFilter(isNullOrNaN(num) ? null : { column: col.key, value: num }) }, "filterInput") },
filterMenuItemOperator
]
: col.filter === "bool"
? [
{ title: "On", icon: _jsx(CheckboxCheckedRegular, {}), onClick: (e) => { stopEvent(e); setFilter({ column: col.key, value: true }); } },
{ title: "Off", icon: _jsx(CheckboxUncheckedRegular, {}), onClick: (e) => { stopEvent(e); setFilter({ column: col.key, value: false }); } }
]
: col.filter === "date"
? [
{
title: "Filter date", onClick: () => { }, as: _jsx(DatePickerEx, { value: filter === null || filter === void 0 ? void 0 : filter.value, onDateChange: date => {
//console.log(date);
setFilter(!isDate(date) ? null : { column: col.key, value: date });
} }, "filterInput")
},
filterMenuItemOperator
]
: []),
{ title: "Clear filter", icon: _jsx(FilterDismissRegular, {}), onClick: (e) => { stopEvent(e); setFilter(null); } },
] })
: undefined] }), `h${coli}`))] }) }), _jsxs(TableBody, { children: [(!filter && isNotEmptyArray(folders)) ? folders.map((folder, folderIndex) => _jsxs(TableRow, { className: mergeClassesEX(props.rowCss), children: [isNotEmptyString(selectionMode) && _jsx("td", {}), normalizedCols.map((col, coli) => {
const normalizedColValue = normalizeColValue(folder[col.key]);
const menuItems = (hasFolderMenu && menuIndex === coli) ? getFolderMenu(folder, folderIndex) : [];
return _jsxs(TableCell, { tabIndex: 0, role: "gridcell", className: mergeClassesEX((coli === 0 ? firstCellClasses : coli === 1 ? secondCellClasses : []), col.css, col.nowrap ? css.nowrap : undefined), children: [_jsx(TableCellLayout, { media: normalizedColValue.media, appearance: col.primary ? "primary" : undefined, description: normalizedColValue.description, children: isFunction(normalizedColValue.renderer) ? normalizedColValue.renderer() : normalizedColValue.renderer }), (isNotEmptyArray(menuItems)) && _jsx(TableCellActions, { children: _jsx(MenuEx, { trigger: { icon: _jsx(MoreVerticalRegular, {}), title: "more" }, items: menuItems }) })] }, `h${coli}`);
})] }, `f${folderIndex}`)) : undefined, rows.map((row, rowi) => _jsxs(TableRow, { className: mergeClassesEX(props.rowCss), onClick: (e) => toggleRow(row.item), onKeyDown: (e) => {
if (e.key === " ") {
stopEvent(e);
toggleRow(row.item);
}
}, children: [isNotEmptyString(selectionMode) && _jsx(TableSelectionCell, { subtle: true, className: mergeClassesEX(selectionCellClasses), checked: selection.includes(getItemKey(row.item)), type: selectionMode === "single" ? "radio" : "checkbox", radioIndicator: { "aria-label": "Select row" } }), normalizedCols.map((col, coli) => {
const normalizedColValue = normalizeColValue(row.item[col.key]);
const menuItems = (hasMenu && menuIndex === coli) ? getItemMenu(row.item, rowi) : [];
return _jsxs(TableCell, { tabIndex: 0, role: "gridcell", className: mergeClassesEX((coli === 0 ? firstCellClasses : coli === 1 ? secondCellClasses : []), col.css, col.nowrap ? css.nowrap : undefined), children: [_jsx(TableCellLayout, { media: normalizedColValue.media, appearance: col.primary ? "primary" : undefined, description: normalizedColValue.description, children: isFunction(normalizedColValue.renderer) ? normalizedColValue.renderer() : normalizedColValue.renderer }), isNotEmptyArray(menuItems) && _jsx(TableCellActions, { children: _jsx(MenuEx, { trigger: { icon: _jsx(MoreVerticalRegular, {}), title: "more" }, items: menuItems }) })] }, `h${coli}`);
})] }, `i${rowi}`))] })] }));
const tableControl = freezed
? _jsx("div", { className: KnownClassNames.overflowContent, style: { maxHeight: fProps.maxHeight, overflow: "auto" }, children: tbl })
: tbl;
return (isNullOrEmptyArray(props.items) && isNotEmptyString(props.emptyLabel))
? _jsxs(Vertical, { children: [tableControl, _jsx(Horizontal, { hCentered: true, css: [css.emptyLabel], children: _jsx(Subtitle1, { children: props.emptyLabel }) })] })
: tableControl;
}
//# sourceMappingURL=table.js.map