Add column sort state.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
0efb60bb66
commit
3dd366b51d
5 changed files with 192 additions and 17 deletions
|
@ -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}
|
||||||
|
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>
|
||||||
|
|
|
@ -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,
|
|
||||||
column: column as Column,
|
|
||||||
}}>
|
|
||||||
<ColumnHeading />
|
<ColumnHeading />
|
||||||
</ColumnContext.Provider>
|
</AutoColumnContextProvider>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -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
64
src/styles/_headings.less
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
@import "_headings";
|
Loading…
Reference in a new issue