Add column sort state.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
Madeorsk 2024-07-20 14:05:35 +02:00
parent 0efb60bb66
commit 792624fa73
Signed by: Madeorsk
SSH key fingerprint: SHA256:J9G0ofIOLKf7kyS2IfrMqtMaPdfsk1W02+oGueZzDDU
5 changed files with 192 additions and 17 deletions

View file

@ -1,4 +1,4 @@
import React, {useContext} from "react"; import React, {useCallback, useContext} from "react";
import {Smartable, useTable} from "./Smartable"; import {Smartable, useTable} from "./Smartable";
/** /**
@ -48,6 +48,31 @@ export function createColumn<K extends ColumnKey>(key: K, column: Column): [K, C
return [key, column]; 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. * Table column context data.
*/ */
@ -62,6 +87,11 @@ export interface ColumnContextData<CK extends ColumnKey>
* Column definition. * Column definition.
*/ */
column: Column; column: Column;
/**
* Column sort state.
*/
sortState?: SortState;
} }
export const ColumnContext = React.createContext<ColumnContextData<ColumnKey>>(undefined); export const ColumnContext = React.createContext<ColumnContextData<ColumnKey>>(undefined);
@ -80,8 +110,37 @@ export function useColumn<CK extends ColumnKey>(smartable?: Smartable<CK>): Colu
export function ColumnHeading() export function ColumnHeading()
{ {
// Get current column data. // Get current column data.
const {column} = useColumn(); const {key, column, sortState} = useColumn();
return <th>{column.title}</th>;
// 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 (
<th className={sortState?.type ?? undefined} data-sort-order={sortState?.order ?? undefined}
onMouseDown={handleClick} onContextMenu={disableContextMenu}>
{column.title}
</th>
);
} }
/** /**
@ -97,6 +156,8 @@ export function AutoColumnContextProvider({columnKey, children}: React.PropsWith
key: columnKey, key: columnKey,
// Get current column data from table data. // Get current column data from table data.
column: table.columns[columnKey], column: table.columns[columnKey],
// Get current column sort state from table data.
sortState: table.columnsSortState?.[columnKey],
}}> }}>
{children} {children}
</ColumnContext.Provider> </ColumnContext.Provider>

View file

@ -1,5 +1,5 @@
import React from "react"; 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 {SmartableProperties, useTable} from "./Smartable";
import {Async, Promisable} from "@kernelui/core"; import {Async, Promisable} from "@kernelui/core";
import {RowDefinition, RowInstance} from "./Row"; import {RowDefinition, RowInstance} from "./Row";
@ -40,13 +40,10 @@ export function ColumnsHeadings<CK extends ColumnKey>({columns}: {columns: Colum
return ( return (
<tr className={"headings"}> <tr className={"headings"}>
{ // Showing title of each column. { // Showing title of each column.
Object.entries(columns).map(([key, column]) => ( Object.keys(columns).map((key) => (
<ColumnContext.Provider key={key} value={{ <AutoColumnContextProvider key={key} columnKey={key}>
key: key, <ColumnHeading />
column: column as Column, </AutoColumnContextProvider>
}}>
<ColumnHeading/>
</ColumnContext.Provider>
)) ))
} }
</tr> </tr>

View file

@ -1,6 +1,6 @@
import React, {useContext} from "react"; import React, {useCallback, useContext, useMemo, useState} from "react";
import {Instance} from "./Instance"; import {Instance} from "./Instance";
import {ColumnKey, Columns} from "./Column"; import {ColumnKey, Columns, SortState, SortType} from "./Column";
import {RowDefinition} from "./Row"; import {RowDefinition} from "./Row";
import {Promisable} from "@kernelui/core"; import {Promisable} from "@kernelui/core";
@ -80,14 +80,54 @@ export function createSmartable<CK extends ColumnKey>({columns}: {
return { return {
Table: (props: SmartableProperties<CK>) => { Table: (props: SmartableProperties<CK>) => {
// Initialize sort state.
const [sortState, setSortState] = useState({} as Record<CK, SortState>);
// Filter columns from the given property. // Filter columns from the given property.
const filteredColumns = props.shownColumns ? filterColumns(columns, props.shownColumns) : columns; const filteredColumns = props.shownColumns ? filterColumns(columns, props.shownColumns) : columns;
return ( // Set sort state of a specific column.
<TableContext.Provider value={{ 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<CK, SortState>;
}
// Set new sort state.
setSortState(newSortState);
}, [sortState, setSortState]);
// Initialize table context value.
const contextValue = useMemo<TableContextData<CK>>(() => ({
columns: filteredColumns, columns: filteredColumns,
columnsSortState: sortState,
setColumnSortState: setColumnSortState,
...props, ...props,
}}> }), [filteredColumns, sortState, setSortState, props]);
return (
<TableContext.Provider value={contextValue}>
<Instance columns={filteredColumns} {...props} /> <Instance columns={filteredColumns} {...props} />
</TableContext.Provider> </TableContext.Provider>
); );
@ -105,6 +145,18 @@ export interface TableContextData<CK extends ColumnKey> extends SmartablePropert
* Current table columns. * Current table columns.
*/ */
columns: Columns<CK>; columns: Columns<CK>;
/**
* Current table columns sort state.
*/
columnsSortState: Record<CK, SortState>;
/**
* 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;
} }
/** /**

64
src/styles/_headings.less Normal file
View file

@ -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;
}
}
}

View file

@ -0,0 +1 @@
@import "_headings";