Viewing File: /home/markqprx/iniasli.pro/client/ui/tables/table.tsx

import React, {
  cloneElement,
  ComponentPropsWithoutRef,
  Fragment,
  JSXElementConstructor,
  MutableRefObject,
  ReactElement,
  useCallback,
  useContext,
  useMemo,
} from 'react';
import {useControlledState} from '@react-stately/utils';
import {SortDescriptor} from './types/sort-descriptor';
import {useGridNavigation} from './navigate-grid';
import {RowElementProps, TableRow} from './table-row';
import {
  TableContext,
  TableContextValue,
  TableSelectionStyle,
} from './table-context';
import {ColumnConfig} from '../../datatable/column-config';
import {TableDataItem} from './types/table-data-item';
import clsx from 'clsx';
import {useInteractOutside} from '@react-aria/interactions';
import {mergeProps, useObjectRef} from '@react-aria/utils';
import {isCtrlKeyPressed} from '@common/utils/keybinds/is-ctrl-key-pressed';
import {useIsMobileMediaQuery} from '@common/utils/hooks/is-mobile-media-query';
import {CheckboxColumnConfig} from '@common/ui/tables/checkbox-column-config';
import {TableHeaderRow} from '@common/ui/tables/table-header-row';

export interface TableProps<T extends TableDataItem>
  extends ComponentPropsWithoutRef<'table'> {
  className?: string;
  columns: ColumnConfig<T>[];
  hideHeaderRow?: boolean;
  data: T[];
  meta?: any;
  tableRef?: MutableRefObject<HTMLTableElement>;
  selectedRows?: (number | string)[];
  defaultSelectedRows?: (number | string)[];
  onSelectionChange?: (keys: (number | string)[]) => void;
  sortDescriptor?: SortDescriptor;
  onSortChange?: (descriptor: SortDescriptor) => any;
  enableSorting?: boolean;
  onDelete?: (items: T[]) => void;
  enableSelection?: boolean;
  selectionStyle?: TableSelectionStyle;
  ariaLabelledBy?: string;
  onAction?: (item: T, index: number) => void;
  selectRowOnContextMenu?: boolean;
  renderRowAs?: JSXElementConstructor<RowElementProps<T>>;
  tableBody?: ReactElement<TableBodyProps>;
  hideBorder?: boolean;
  closeOnInteractOutside?: boolean;
  collapseOnMobile?: boolean;
  cellHeight?: string;
  headerCellHeight?: string;
}
export function Table<T extends TableDataItem>({
  className,
  columns: userColumns,
  collapseOnMobile = true,
  hideHeaderRow = false,
  hideBorder = false,
  data,
  selectedRows: propsSelectedRows,
  defaultSelectedRows: propsDefaultSelectedRows,
  onSelectionChange: propsOnSelectionChange,
  sortDescriptor: propsSortDescriptor,
  onSortChange: propsOnSortChange,
  enableSorting = true,
  onDelete,
  enableSelection = true,
  selectionStyle = 'checkbox',
  ariaLabelledBy,
  selectRowOnContextMenu,
  onAction,
  renderRowAs,
  tableBody,
  meta,
  tableRef: propsTableRef,
  closeOnInteractOutside = false,
  cellHeight,
  headerCellHeight,
  ...domProps
}: TableProps<T>) {
  const isMobile = useIsMobileMediaQuery();
  const isCollapsedMode = !!isMobile && collapseOnMobile;
  if (isCollapsedMode) {
    hideHeaderRow = true;
    hideBorder = true;
  }

  const [selectedRows, onSelectionChange] = useControlledState(
    propsSelectedRows,
    propsDefaultSelectedRows || [],
    propsOnSelectionChange,
  );

  const [sortDescriptor, onSortChange] = useControlledState(
    propsSortDescriptor,
    undefined,
    propsOnSortChange,
  );

  const toggleRow = useCallback(
    (item: TableDataItem) => {
      const newValues = [...selectedRows];
      if (!newValues.includes(item.id)) {
        newValues.push(item.id);
      } else {
        const index = newValues.indexOf(item.id);
        newValues.splice(index, 1);
      }
      onSelectionChange(newValues);
    },
    [selectedRows, onSelectionChange],
  );

  const selectRow = useCallback(
    // allow deselecting all rows by passing in null
    (item: TableDataItem | null, merge?: boolean) => {
      let newValues: (string | number)[] = [];
      if (item) {
        newValues = merge
          ? [...selectedRows?.filter(id => id !== item.id), item.id]
          : [item.id];
      }
      onSelectionChange(newValues);
    },
    [selectedRows, onSelectionChange],
  );

  // add checkbox columns to config, if selection is enabled
  const columns = useMemo(() => {
    const filteredColumns = userColumns.filter(c => {
      const visibleInMode = c.visibleInMode || 'regular';
      if (visibleInMode === 'all') {
        return true;
      }
      if (visibleInMode === 'compact' && isCollapsedMode) {
        return true;
      }
      if (visibleInMode === 'regular' && !isCollapsedMode) {
        return true;
      }
    });
    const showCheckboxCell =
      enableSelection && selectionStyle !== 'highlight' && !isMobile;
    if (showCheckboxCell) {
      filteredColumns.unshift(CheckboxColumnConfig);
    }
    return filteredColumns;
  }, [isMobile, userColumns, enableSelection, selectionStyle, isCollapsedMode]);

  const contextValue: TableContextValue<T> = {
    isCollapsedMode,
    cellHeight,
    headerCellHeight,
    hideBorder,
    hideHeaderRow,
    selectedRows,
    onSelectionChange,
    enableSorting,
    enableSelection,
    selectionStyle,
    data,
    columns,
    sortDescriptor,
    onSortChange,
    toggleRow,
    selectRow,
    onAction,
    selectRowOnContextMenu,
    meta,
    collapseOnMobile,
  };

  const navProps = useGridNavigation({
    cellCount: enableSelection ? columns.length + 1 : columns.length,
    rowCount: data.length + 1,
  });

  const tableBodyProps: TableBodyProps = {
    renderRowAs: renderRowAs as any,
  };

  if (!tableBody) {
    tableBody = <BasicTableBody {...tableBodyProps} />;
  } else {
    tableBody = cloneElement(tableBody, tableBodyProps);
  }

  // deselect rows when clicking outside the table
  const tableRef = useObjectRef(propsTableRef);
  useInteractOutside({
    ref: tableRef,
    onInteractOutside: e => {
      if (
        closeOnInteractOutside &&
        enableSelection &&
        selectedRows?.length &&
        // don't deselect if clicking on a dialog (for example is table row has a context menu)
        !(e.target as HTMLElement).closest('[role="dialog"]')
      ) {
        onSelectionChange([]);
      }
    },
  });

  return (
    <TableContext.Provider value={contextValue as any}>
      <div
        {...mergeProps(domProps, navProps, {
          onKeyDown: (e: KeyboardEvent) => {
            if (e.key === 'Escape') {
              e.preventDefault();
              e.stopPropagation();
              if (selectedRows?.length) {
                onSelectionChange([]);
              }
            } else if (e.key === 'Delete') {
              e.preventDefault();
              e.stopPropagation();
              if (selectedRows?.length) {
                onDelete?.(
                  data.filter(item => selectedRows?.includes(item.id)),
                );
              }
            } else if (isCtrlKeyPressed(e) && e.key === 'a') {
              e.preventDefault();
              e.stopPropagation();
              if (enableSelection) {
                onSelectionChange(data.map(item => item.id));
              }
            }
          },
        })}
        role="grid"
        tabIndex={0}
        aria-rowcount={data.length + 1}
        aria-colcount={columns.length + 1}
        ref={tableRef}
        aria-multiselectable={enableSelection ? true : undefined}
        aria-labelledby={ariaLabelledBy}
        className={clsx(
          className,
          'isolate select-none text-sm outline-none focus-visible:ring-2',
        )}
      >
        {!hideHeaderRow && <TableHeaderRow />}
        {tableBody}
      </div>
    </TableContext.Provider>
  );
}

export interface TableBodyProps {
  renderRowAs?: TableProps<TableDataItem>['renderRowAs'];
}
function BasicTableBody({renderRowAs}: TableBodyProps) {
  const {data} = useContext(TableContext);
  return (
    <Fragment>
      {data.map((item, rowIndex) => (
        <TableRow
          item={item}
          index={rowIndex}
          key={item.id}
          renderAs={renderRowAs}
        />
      ))}
    </Fragment>
  );
}
Back to Directory File Manager