UNPKG

@kwiz/fluentui

Version:
348 lines 21.8 kB
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