From 71b10a3c89978b3d1982aceef40952c4c8d41a0d Mon Sep 17 00:00:00 2001 From: Madeorsk Date: Sun, 28 Jul 2024 14:36:36 +0200 Subject: [PATCH] Reorganize columns sorting and filter systems. --- src/Smartable/AsyncManager.tsx | 2 +- src/Smartable/Column.tsx | 78 +----------------------- src/Smartable/Columns/ColumnFilter.tsx | 12 ++++ src/Smartable/Columns/ColumnHeading.tsx | 45 ++++++++++++++ src/Smartable/Filters/NumberFilter.tsx | 3 +- src/Smartable/Filters/StringFilter.tsx | 3 +- src/Smartable/Instance.tsx | 58 ++---------------- src/Smartable/Smartable.tsx | 3 +- src/Smartable/Sort.tsx | 79 +++++++++++++++++++++++++ 9 files changed, 146 insertions(+), 137 deletions(-) create mode 100644 src/Smartable/Columns/ColumnHeading.tsx create mode 100644 src/Smartable/Sort.tsx diff --git a/src/Smartable/AsyncManager.tsx b/src/Smartable/AsyncManager.tsx index 20abffd..ed71872 100644 --- a/src/Smartable/AsyncManager.tsx +++ b/src/Smartable/AsyncManager.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState} from "react"; +import React, {useEffect, useRef, useState} from "react"; import {ColumnKey} from "./Column"; import {normalizeRowDefinition, RowData, RowDefinition} from "./Row"; import {CellDefinition} from "./Cell"; diff --git a/src/Smartable/Column.tsx b/src/Smartable/Column.tsx index 20e7e6f..ad2a8cd 100644 --- a/src/Smartable/Column.tsx +++ b/src/Smartable/Column.tsx @@ -1,6 +1,7 @@ import React, {useCallback, useContext} from "react"; import {Smartable, useTable} from "./Smartable"; import {ColumnFilter} from "./Columns/ColumnFilter"; +import {SortState} from "./Sort"; /** * Basic column key type. @@ -61,31 +62,6 @@ 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. */ @@ -128,58 +104,6 @@ export function useColumn(smartable?: Smartable): Colu return useContext(ColumnContext); } -/** - * Hook to get current column filter state. - */ -export function useFilterState(initialValue: T): [T, (newFilterState: T) => void] -{ - // Get current column data. - const column = useColumn(); - // Return filter state array from current column data. - return [column.filterState ?? initialValue, column.setFilterState]; -} - -/** - * Default column heading component. - */ -export function ColumnHeading() -{ - // Get current column data. - 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} - - {sortState?.order && {sortState.order}} - - ); -} - /** * Auto column context provider from table context. */ diff --git a/src/Smartable/Columns/ColumnFilter.tsx b/src/Smartable/Columns/ColumnFilter.tsx index bba0d4e..243ca70 100644 --- a/src/Smartable/Columns/ColumnFilter.tsx +++ b/src/Smartable/Columns/ColumnFilter.tsx @@ -1,4 +1,5 @@ import React from "react"; +import {useColumn} from "../Column"; /** * Column filter definition. @@ -18,3 +19,14 @@ export interface ColumnFilter */ filter: (data: T, filterState: FilterState) => boolean; } + +/** + * Hook to get current column filter state. + */ +export function useFilterState(initialValue: T): [T, (newFilterState: T) => void] +{ + // Get current column data. + const column = useColumn(); + // Return filter state array from current column data. + return [column.filterState ?? initialValue, column.setFilterState]; +} diff --git a/src/Smartable/Columns/ColumnHeading.tsx b/src/Smartable/Columns/ColumnHeading.tsx new file mode 100644 index 0000000..6da8571 --- /dev/null +++ b/src/Smartable/Columns/ColumnHeading.tsx @@ -0,0 +1,45 @@ +import React, {useCallback} from "react"; +import {useColumn} from "../Column"; +import {useTable} from "../Smartable"; +import {SortType} from "../Sort"; + +/** + * Default column heading component. + */ +export function ColumnHeading() +{ + // Get current column data. + 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} + + {sortState?.order && {sortState.order}} + + ); +} diff --git a/src/Smartable/Filters/NumberFilter.tsx b/src/Smartable/Filters/NumberFilter.tsx index da6141d..68681ff 100644 --- a/src/Smartable/Filters/NumberFilter.tsx +++ b/src/Smartable/Filters/NumberFilter.tsx @@ -1,6 +1,5 @@ import React, {useCallback} from "react"; -import {ColumnFilter} from "../Columns/ColumnFilter"; -import {useFilterState} from "../Column"; +import {ColumnFilter, useFilterState} from "../Columns/ColumnFilter"; /** * Filter value regex. diff --git a/src/Smartable/Filters/StringFilter.tsx b/src/Smartable/Filters/StringFilter.tsx index c9507ff..37af92f 100644 --- a/src/Smartable/Filters/StringFilter.tsx +++ b/src/Smartable/Filters/StringFilter.tsx @@ -1,6 +1,5 @@ import React, {useCallback} from "react"; -import {ColumnFilter} from "../Columns/ColumnFilter"; -import {useFilterState} from "../Column"; +import {ColumnFilter, useFilterState} from "../Columns/ColumnFilter"; import {normalizeString} from "@kernelui/core"; /** diff --git a/src/Smartable/Instance.tsx b/src/Smartable/Instance.tsx index d6f0c50..22a6051 100644 --- a/src/Smartable/Instance.tsx +++ b/src/Smartable/Instance.tsx @@ -1,8 +1,10 @@ import React, {useMemo} from "react"; -import {AutoColumnContextProvider, Column, ColumnHeading, ColumnKey, Columns, SortState, SortType} from "./Column"; +import {AutoColumnContextProvider, Column, ColumnKey, Columns} from "./Column"; import {SmartableProperties, useTable} from "./Smartable"; import {RowInstance, RowLoader} from "./Row"; -import {CurrentRowData, useAsyncManager} from "./AsyncManager"; +import {useAsyncManager} from "./AsyncManager"; +import {ColumnHeading} from "./Columns/ColumnHeading"; +import {sortRows} from "./Sort"; /** * Smartable instance component properties. @@ -98,55 +100,3 @@ export function TableBody() ) ); } - -/** - * Sort the given rows from the columns sort state. - * @param rows Rows to sort. - * @param columns Columns definition. - * @param columnsSortState Columns current sort state. - */ -export function sortRows(rows: CurrentRowData[], columns: Columns, columnsSortState: Partial>): CurrentRowData[] -{ - // Normalize value to undefined when rows are not loaded. - if (!Array.isArray(rows)) return undefined; - - // Get the sort column states following their priority order. - const orderedSortColumns = (Object.entries(columnsSortState) as [CK, SortState][]).sort((a, b) => ( - a[1].order - b[1].order - )); - - // Sort the given rows. - return rows.toSorted((a, b) => { - for (const [columnKey, columnSort] of orderedSortColumns) - { - // Get the current comparison result for the given cells' data. - const comparison = - (columnSort.type == SortType.DESC ? 1 : -1) * - (columns[columnKey]?.compare ?? genericCompare)(a.cells[columnKey]?.data, b.cells[columnKey]?.data); - - if (comparison != 0) - // If an order can be determined from the current comparison, returning it. - return comparison; - } - - // The comparisons always returned 0. - return 0; - }); -} - -/** - * Generic comparison function for table data. - * @param a First data to compare. - * @param b Second data to compare. - */ -export function genericCompare(a: any, b: any): number -{ - if (typeof a == "number" && typeof a == typeof b) - { // Compare numbers. - return b - a; - } - else - { // Compare data as strings. - return String(b).localeCompare(String(a)); - } -} diff --git a/src/Smartable/Smartable.tsx b/src/Smartable/Smartable.tsx index 69e3f09..38069e5 100644 --- a/src/Smartable/Smartable.tsx +++ b/src/Smartable/Smartable.tsx @@ -1,8 +1,9 @@ import React, {useCallback, useContext, useMemo, useState} from "react"; import {Instance} from "./Instance"; -import {ColumnKey, Columns, SortState, SortType} from "./Column"; +import {ColumnKey, Columns} from "./Column"; import {RowDefinition} from "./Row"; import {Promisable} from "@kernelui/core"; +import {SortState, SortType} from "./Sort"; /** * Smartable data type. diff --git a/src/Smartable/Sort.tsx b/src/Smartable/Sort.tsx new file mode 100644 index 0000000..3d3a14e --- /dev/null +++ b/src/Smartable/Sort.tsx @@ -0,0 +1,79 @@ +import {ColumnKey, Columns} from "./Column"; +import {CurrentRowData} from "./AsyncManager"; + +/** + * 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; +} + +/** + * Sort the given rows from the columns sort state. + * @param rows Rows to sort. + * @param columns Columns definition. + * @param columnsSortState Columns current sort state. + */ +export function sortRows(rows: CurrentRowData[], columns: Columns, columnsSortState: Partial>): CurrentRowData[] +{ + // Normalize value to undefined when rows are not loaded. + if (!Array.isArray(rows)) return undefined; + + // Get the sort column states following their priority order. + const orderedSortColumns = (Object.entries(columnsSortState) as [CK, SortState][]).sort((a, b) => ( + a[1].order - b[1].order + )); + + // Sort the given rows. + return rows.toSorted((a, b) => { + for (const [columnKey, columnSort] of orderedSortColumns) + { + // Get the current comparison result for the given cells' data. + const comparison = + (columnSort.type == SortType.DESC ? 1 : -1) * + (columns[columnKey]?.compare ?? genericCompare)(a.cells[columnKey]?.data, b.cells[columnKey]?.data); + + if (comparison != 0) + // If an order can be determined from the current comparison, returning it. + return comparison; + } + + // The comparisons always returned 0. + return 0; + }); +} + +/** + * Generic comparison function for table data. + * @param a First data to compare. + * @param b Second data to compare. + */ +export function genericCompare(a: any, b: any): number +{ + if (typeof a == "number" && typeof a == typeof b) + { // Compare numbers. + return b - a; + } + else + { // Compare data as strings. + return String(b).localeCompare(String(a)); + } +}