From 977670b9e0b705cafa93f37dbaf494dea4ac7f96 Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Tue, 10 Dec 2024 15:07:45 +0100 Subject: [PATCH] made tables more feature complete and started splitting of all the data handling to dataset.ts --- src/components/table.tsx | 114 ------------ src/components/table/dataset.ts | 27 +++ src/components/table/index.tsx | 5 + src/components/{ => table}/table.module.css | 13 +- src/components/table/table.tsx | 193 ++++++++++++++++++++ src/routes/(editor)/experimental.module.css | 6 + src/routes/(editor)/experimental.tsx | 90 ++++++++- 7 files changed, 325 insertions(+), 123 deletions(-) delete mode 100644 src/components/table.tsx create mode 100644 src/components/table/dataset.ts create mode 100644 src/components/table/index.tsx rename src/components/{ => table}/table.module.css (87%) create mode 100644 src/components/table/table.tsx diff --git a/src/components/table.tsx b/src/components/table.tsx deleted file mode 100644 index a2a7e9e..0000000 --- a/src/components/table.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { Component, createMemo, createSignal, For, Match, Show, Switch } from "solid-js"; -import { selectable, SelectionProvider, useSelection } from "~/features/selectable"; -import css from './table.module.css'; - -selectable - -type Row = { kind: 'row', key: string, value: T } -type Group = { kind: 'group', key: string, nodes: Node[] }; -type Node = Row | Group; - -export function Table>(props: { class?: string, rows: T[], selectable?: boolean }) { - const [selection, setSelection] = createSignal([]); - const columns = createMemo(() => ['#', ...Object.keys(props.rows.at(0) ?? {})]); - const selectable = createMemo(() => props.selectable ?? false); - - return <> - - {/* */} - - <_Table class={props.class} columns={columns()} rows={props.rows} /> - - ; -}; - -type TableProps> = { class?: string, columns: (keyof T)[], rows: T[] }; - -function _Table>(props: TableProps) { - const columnCount = createMemo(() => props.columns.length - 1); - const nodes = createMemo[]>(() => { - const rows = Object.entries(props.rows).map>(([i, row]) => ({ kind: 'row', key: row['key'], value: row })); - - const group = (nodes: Row[]): Node[] => nodes.every(n => n.key.includes('.') === false) - ? nodes - : Object.entries(Object.groupBy(nodes, r => String(r.key).split('.').at(0)!)) - .map>(([key, nodes]) => ({ kind: 'group', key, nodes: group(nodes!.map(n => ({ ...n, key: n.key.slice(key.length + 1) }))) })); - - const grouped = group(rows); - - return grouped; - }); - - return
- - -
- { - node => - } - -
-
-}; - -function Head>(props: { headers: (keyof T)[] }) { - const context = useSelection(); - - return
-
- 0 && context.selection().length === context.length()} - indeterminate={context.selection().length !== 0 && context.selection().length !== context.length()} - on:input={(e: InputEvent) => e.target.checked ? context.selectAll() : context.clear()} - /> -
- - { - header => {header.toString()} - } -
; -}; - -function Node>(props: { node: Node, depth: number }) { - return - { - row => - } - - { - group => - } - ; -} - -function Row>(props: { key: string, value: T, depth: number }) { - const context = useSelection(); - - const values = createMemo(() => Object.entries(props.value)); - const isSelected = context.isSelected(props.key); - - return
-
- context.select([props.key])} on:pointerdown={e => e.stopPropagation()} /> -
- -
- {props.key} -
- - { - ([k, v]) =>
{v}
- }
-
; -}; - -function Group>(props: { key: string, nodes: Node[], depth: number }) { - return
- {props.key} - - { - node => - } -
; -}; \ No newline at end of file diff --git a/src/components/table/dataset.ts b/src/components/table/dataset.ts new file mode 100644 index 0000000..193d956 --- /dev/null +++ b/src/components/table/dataset.ts @@ -0,0 +1,27 @@ + + +export type RowNode = { kind: 'row', key: string, value: T } +export type GroupNode = { kind: 'group', key: string, groupedBy: keyof T, nodes: Node[] }; +export type Node = RowNode | GroupNode; + +export type DataSet> = Node[]; + +export const createDataSet = >(data: T[]): Node[] => { + return Object.entries(data).map>(([key, value]) => ({ kind: 'row', key, value })); +}; + +type SortingFunction = (a: T, b: T) => -1 | 0 | 1; +type SortOptions> = { by: keyof T, reversed: boolean, with: SortingFunction }; +export const toSorted = >(dataSet: DataSet, sort: SortOptions): DataSet => { + const sorted = dataSet.toSorted((a, b) => sort.with(a.value[sort.by], b.value[sort.by])); + + if (sort.reversed) { + sorted.reverse(); + } + + return sorted; +}; + +type GroupingFunction = (nodes: RowNode[]) => Node[]; +type GroupOptions> = { by: keyof T, with: GroupingFunction }; +export const toGrouped = >(dataSet: DataSet, group: GroupOptions): DataSet => group.with(dataSet as any); \ No newline at end of file diff --git a/src/components/table/index.tsx b/src/components/table/index.tsx new file mode 100644 index 0000000..7d858b2 --- /dev/null +++ b/src/components/table/index.tsx @@ -0,0 +1,5 @@ + +export type { Column } from './table'; +export type { DataSet, GroupNode, RowNode, Node } from './dataset'; +export { SelectionMode, Table } from './table'; +export { createDataSet, toSorted, toGrouped } from './dataset'; \ No newline at end of file diff --git a/src/components/table.module.css b/src/components/table/table.module.css similarity index 87% rename from src/components/table.module.css rename to src/components/table/table.module.css index 97bf858..349dcce 100644 --- a/src/components/table.module.css +++ b/src/components/table/table.module.css @@ -1,20 +1,24 @@ .table { position: relative; display: grid; - grid-template-columns: 2em minmax(10em, max-content) repeat(var(--columns), auto); + grid-template-columns: repeat(var(--columns), minmax(0, auto)); align-content: start; padding-inline: 1px; margin-inline: -1px; block-size: 100%; + &.selectable { + grid-template-columns: 2em repeat(calc(var(--columns) - 1), minmax(0, auto)); + } + & input[type="checkbox"] { margin: .1em; } & .cell { display: grid; - padding: .5em; + padding: var(--padding-m); border: 1px solid transparent; border-radius: var(--radii-m); @@ -89,14 +93,13 @@ } & > summary { - grid-column: 2 / span calc(1 + var(--columns)); padding: .5em; padding-inline-start: calc(var(--depth) * 1em + .5em); } - & > .row > .cell > span { - padding-inline-start: calc(var(--depth) * 1em); + & > .row > .cell { + padding-inline-start: calc(var(--depth) * 1em + var(--padding-m)); } } } diff --git a/src/components/table/table.tsx b/src/components/table/table.tsx new file mode 100644 index 0000000..12a9f91 --- /dev/null +++ b/src/components/table/table.tsx @@ -0,0 +1,193 @@ +import { Accessor, createContext, createEffect, createMemo, createSignal, For, JSX, Match, Show, Switch, useContext } from "solid-js"; +import { selectable, SelectionProvider, useSelection } from "~/features/selectable"; +import { type RowNode, type GroupNode, type Node, createDataSet, toSorted, toGrouped } from './dataset'; +import css from './table.module.css'; + +selectable + +export type Column = { + id: keyof T, + label: string, + readonly groupBy?: (rows: RowNode[]) => Node[], +}; + +const TableContext = createContext<{ + readonly columns: Accessor[]>, + readonly selectionMode: Accessor, + readonly groupBy: Accessor, + readonly sort: Accessor<{ by: string, reversed?: boolean } | undefined>, + readonly cellRenderers: Accessor JSX.Element>>, +}>(); + +const useTable = () => useContext(TableContext)! + +function defaultGroupingFunction(groupBy: keyof T) { + return (nodes: RowNode[]): Node[] => Object.entries(Object.groupBy>(nodes, r => r.value[groupBy])) + .map>(([key, nodes]) => ({ kind: 'group', key, groupedBy: groupBy, nodes: nodes! })); +} + +export enum SelectionMode { + None, + Single, + Multiple +} +type TableProps> = { + class?: string, + rows: T[], + columns: Column[], + groupBy?: keyof T, + sort?: { + by: keyof T, + reversed?: boolean, + }, + selectionMode?: SelectionMode, + children?: { [K in keyof T]?: (cell: { value: T[K] }) => JSX.Element }, +}; + +export function Table>(props: TableProps) { + const [selection, setSelection] = createSignal([]); + const columns = createMemo[]>(() => props.columns ?? []); + const selectionMode = createMemo(() => props.selectionMode ?? SelectionMode.None); + const groupBy = createMemo(() => props.groupBy as string | undefined); + const sort = createMemo(() => props.sort as any); + const cellRenderers = createMemo(() => props.children ?? {}); + + return + + + + ; +}; + +type InnerTableProps> = { class?: string, rows: T[] }; + +function InnerTable>(props: InnerTableProps) { + const table = useTable(); + + const selectable = createMemo(() => table.selectionMode() !== SelectionMode.None); + const columnCount = createMemo(() => table.columns().length + (selectable() ? 0 : -1)); + const nodes = createMemo[]>(() => { + const columns = table.columns(); + const groupBy = table.groupBy(); + const sort = table.sort(); + + let kaas = createDataSet(props.rows); + + if (sort) { + kaas = toSorted(kaas, { by: sort.by, reversed: sort.reversed ?? false, with: (a, b) => a < b ? -1 : a > b ? 1 : 0 }) + } + + if (groupBy) { + kaas = toGrouped(kaas, { by: groupBy, with: columns.find(({ id }) => id === groupBy)?.groupBy ?? defaultGroupingFunction(groupBy) }); + } + + console.log(kaas); + + const rows = props.rows; + + if (sort) { + rows.sort((a, b) => a[sort.by] < b[sort.by] ? -1 : a[sort.by] > b[sort.by] ? 1 : 0); + + if (sort.reversed === true) { + rows.reverse(); + } + } + + const nodes = Object.entries(rows).map>(([i, row]) => ({ kind: 'row', key: i, value: row })); + + if (groupBy === undefined) { + return nodes; + } + + const groupingFunction = columns.find(({ id }) => id === groupBy)?.groupBy ?? defaultGroupingFunction(groupBy); + + return groupingFunction(nodes); + }); + + + + return
+ + +
+ { + node => + } + +
+
+}; + +function Head>(props: {}) { + const table = useTable(); + const context = useSelection(); + + return
+ +
+ 0 && context.selection().length === context.length()} + indeterminate={context.selection().length !== 0 && context.selection().length !== context.length()} + on:input={(e: InputEvent) => e.target.checked ? context.selectAll() : context.clear()} + /> +
+
+ + { + column => {column.label} + } +
; +}; + +function Node>(props: { node: Node, depth: number, groupedBy?: keyof T }) { + return + { + row => + } + + { + group => + } + ; +} + +function Row>(props: { key: string, value: T, depth: number, groupedBy?: keyof T }) { + const table = useTable(); + const context = useSelection(); + + const values = createMemo(() => Object.entries(props.value)); + const isSelected = context.isSelected(props.key); + + return
+ +
+ context.select([props.key])} on:pointerdown={e => e.stopPropagation()} /> +
+
+ + { + ([k, v]) =>
{table.cellRenderers()[k]?.({ value: v }) ?? v}
+ }
+
; +}; + +function Group>(props: { key: string, groupedBy: keyof T, nodes: Node[], depth: number }) { + const table = useTable(); + + const gridColumn = createMemo(() => { + const groupedBy = props.groupedBy; + const columns = table.columns(); + const selectable = table.selectionMode() !== SelectionMode.None; + + return columns.findIndex(({ id }) => id === groupedBy) + (selectable ? 2 : 1); + }); + + return
+ {props.key} + + { + node => + } +
; +}; \ No newline at end of file diff --git a/src/routes/(editor)/experimental.module.css b/src/routes/(editor)/experimental.module.css index 5872a3d..004cc10 100644 --- a/src/routes/(editor)/experimental.module.css +++ b/src/routes/(editor)/experimental.module.css @@ -13,6 +13,12 @@ padding: 0; margin: 0; } + + & fieldset { + display: flex; + flex-flow: column; + gap: var(--padding-m); + } } & .content { diff --git a/src/routes/(editor)/experimental.tsx b/src/routes/(editor)/experimental.tsx index 7d75300..c94d745 100644 --- a/src/routes/(editor)/experimental.tsx +++ b/src/routes/(editor)/experimental.tsx @@ -1,6 +1,8 @@ -import { Table } from "~/components/table"; +import { Column, GroupNode, RowNode, Node, SelectionMode, Table } from "~/components/table"; import css from "./experimental.module.css"; import { Sidebar } from "~/components/sidebar"; +import { createStore } from "solid-js/store"; +import { createEffect, For } from "solid-js"; export default function Experimental() { const rows = [ @@ -23,13 +25,93 @@ export default function Experimental() { { key: 'key2.c.a', value: 70 }, { key: 'key2.c.b', value: 80 }, { key: 'key2.c.c', value: 90 }, + + { key: 'aaaa', value: 200 }, ]; + type Entry = typeof rows[0]; + const columns: Column[] = [ + { + id: 'key', + label: 'Key', + groupBy(rows: RowNode[]) { + const group = (nodes: (RowNode & { _key: string })[]): Node[] => nodes.every(n => n._key.includes('.') === false) + ? nodes + : Object.entries(Object.groupBy(nodes, r => String(r._key).split('.').at(0)!)) + .map>(([key, nodes]) => ({ kind: 'group', key, groupedBy: 'key', nodes: group(nodes!.map(n => ({ ...n, _key: n._key.slice(key.length + 1) }))) })); + + return group(rows.map(row => ({ ...row, _key: row.value.key }))); + }, + }, + { + id: 'value', + label: 'Value', + }, + ]; + + const [store, setStore] = createStore<{ selectionMode: SelectionMode, groupBy?: keyof Entry, sort?: { by: keyof Entry, reversed?: boolean } }>({ + selectionMode: SelectionMode.None, + // groupBy: 'value', + // sortBy: 'key' + }); + + createEffect(() => { + console.log({ ...store }); + }); + return
- + +
+ table properties + + + + +
+ +
+ table sorting + + + + +
+
- +
{{ + value: (cell) => , + }}
-
; + ; } \ No newline at end of file