diff --git a/src/Smartable/Column.tsx b/src/Smartable/Column.tsx index 7566096..a1eae36 100644 --- a/src/Smartable/Column.tsx +++ b/src/Smartable/Column.tsx @@ -1,4 +1,4 @@ -import React, {useContext} from "react"; +import React, {useCallback, useContext} from "react"; import {Smartable, useTable} from "./Smartable"; /** @@ -48,6 +48,31 @@ export function createColumn(key: K, column: Column): [K, C return [key, column]; } +/** + * Column sort type. + */ +export enum SortType +{ + ASC = "asc", + DESC = "desc", +} + +/** + * Column sort state. + */ +export interface SortState +{ + /** + * Sort type (ascending or descending). + */ + type: SortType; + + /** + * Sort order. + */ + order: number; +} + /** * Table column context data. */ @@ -62,6 +87,11 @@ export interface ColumnContextData * Column definition. */ column: Column; + + /** + * Column sort state. + */ + sortState?: SortState; } export const ColumnContext = React.createContext>(undefined); @@ -80,8 +110,37 @@ export function useColumn(smartable?: Smartable): Colu export function ColumnHeading() { // Get current column data. - const {column} = useColumn(); - return {column.title}; + const {key, column, sortState} = useColumn(); + + // Get column sort state setter. + const {setColumnSortState} = useTable(); + + // Initialize handle click function. + const handleClick = useCallback((event: React.MouseEvent) => { + if (event.button == 0) + { // Normal click (usually left click). + // Toggle sort type. + setColumnSortState(key, sortState?.type == SortType.ASC ? SortType.DESC : SortType.ASC); + } + else if (event.button == 2 || event.button == 1) + { // Alt click (usually right or middle click). + // Reset sort type. + setColumnSortState(key, null); + } + }, [key, sortState, setColumnSortState]); + + // Disable context menu function. + const disableContextMenu = useCallback((event: React.MouseEvent) => { + event.preventDefault(); + return false; + }, []); + + return ( + + {column.title} + + ); } /** @@ -97,6 +156,8 @@ export function AutoColumnContextProvider({columnKey, children}: React.PropsWith key: columnKey, // Get current column data from table data. column: table.columns[columnKey], + // Get current column sort state from table data. + sortState: table.columnsSortState?.[columnKey], }}> {children} diff --git a/src/Smartable/Instance.tsx b/src/Smartable/Instance.tsx index c807793..0a6dbb3 100644 --- a/src/Smartable/Instance.tsx +++ b/src/Smartable/Instance.tsx @@ -1,5 +1,5 @@ import React from "react"; -import {Column, ColumnContext, ColumnHeading, ColumnKey, Columns} from "./Column"; +import {AutoColumnContextProvider, Column, ColumnContext, ColumnHeading, ColumnKey, Columns} from "./Column"; import {SmartableProperties, useTable} from "./Smartable"; import {Async, Promisable} from "@kernelui/core"; import {RowDefinition, RowInstance} from "./Row"; @@ -40,13 +40,10 @@ export function ColumnsHeadings({columns}: {columns: Colum return ( { // Showing title of each column. - Object.entries(columns).map(([key, column]) => ( - - - + Object.keys(columns).map((key) => ( + + + )) } diff --git a/src/Smartable/Smartable.tsx b/src/Smartable/Smartable.tsx index 2b8a49a..fedf10a 100644 --- a/src/Smartable/Smartable.tsx +++ b/src/Smartable/Smartable.tsx @@ -1,6 +1,6 @@ -import React, {useContext} from "react"; +import React, {useCallback, useContext, useMemo, useState} from "react"; import {Instance} from "./Instance"; -import {ColumnKey, Columns} from "./Column"; +import {ColumnKey, Columns, SortState, SortType} from "./Column"; import {RowDefinition} from "./Row"; import {Promisable} from "@kernelui/core"; @@ -80,14 +80,54 @@ export function createSmartable({columns}: { return { Table: (props: SmartableProperties) => { + // Initialize sort state. + const [sortState, setSortState] = useState({} as Record); + // Filter columns from the given property. const filteredColumns = props.shownColumns ? filterColumns(columns, props.shownColumns) : columns; + // Set sort state of a specific column. + const setColumnSortState = useCallback((key: CK, sortType: SortType|null): void => { + // Copy current sort state. + let newSortState = {...sortState}; + + if (sortType) + // A new sort type for given column has been set. + newSortState[key] = { + // Setting new sort type. + type: sortType, + // Keeping current order, or creating a new one (from the current state size). + order: newSortState?.[key]?.order ?? (Object.keys(sortState).length + 1), + }; + else if (newSortState[key]) + { // Sort type for given column has been reset, removing it. + const removedOrderKey = newSortState[key]?.order; + delete newSortState[key]; + + // Decrement all remaining greater orders by one, as there is now one less sorted column. + newSortState = Object.fromEntries((Object.entries(newSortState) as [CK, SortState][]).map( + // For each column sort state... + ([columnKey, {order: columnSortOrder, ...columnSortState}]) => ( + //... copy the current column sort state, just decrement its order. + [columnKey, {order: columnSortOrder - (removedOrderKey < columnSortOrder ? 1 : 0), ...columnSortState} as SortState] + ) + )) as Record; + } + + // Set new sort state. + setSortState(newSortState); + }, [sortState, setSortState]); + + // Initialize table context value. + const contextValue = useMemo>(() => ({ + columns: filteredColumns, + columnsSortState: sortState, + setColumnSortState: setColumnSortState, + ...props, + }), [filteredColumns, sortState, setSortState, props]); + return ( - + ); @@ -105,6 +145,18 @@ export interface TableContextData extends SmartablePropert * Current table columns. */ columns: Columns; + + /** + * Current table columns sort state. + */ + columnsSortState: Record; + + /** + * Set current table columns sort state. + * @param key The column key for which to set the sort type. + * @param sortType The sort type to set for the given column. NULL to reset sort state. + */ + setColumnSortState: (key: CK, sortType: SortType|null) => void; } /** diff --git a/src/styles/_headings.less b/src/styles/_headings.less new file mode 100644 index 0000000..de82337 --- /dev/null +++ b/src/styles/_headings.less @@ -0,0 +1,64 @@ +tr.headings +{ + th + { + position: relative; + + cursor: pointer; + + &::before, &::after + { // Sorting order indicator. + transition: height 0.2s ease, background 0.2s ease, top 0.2s ease, bottom 0.2s ease; + + content: ""; + position: absolute; + top: 0; + bottom: 0; + + display: block; + margin: auto; + box-sizing: border-box; + + background: var(--background-darkest); + } + + &::before + { + right: calc(0.33em - 1px); + + width: 2px; + height: 0; + border-radius: 2px; + } + &::after + { + right: calc(0.33em - 3px); + + width: 6px; + height: 6px; + border-radius: 6px; + } + + &.asc, &.desc + { + &::after, &::before + { + background: var(--primary); + } + + &::before + { + height: 0.8em; + } + } + + &.asc::after + { + top: 0.5em; + } + &.desc::after + { + bottom: 0.5em; + } + } +} diff --git a/src/styles/smartable.less b/src/styles/smartable.less index e69de29..c560608 100644 --- a/src/styles/smartable.less +++ b/src/styles/smartable.less @@ -0,0 +1 @@ +@import "_headings";