woop in a working state again

This commit is contained in:
Chris Kruining 2024-12-18 16:32:21 +01:00
parent 4a5f0cf2d1
commit ab68df340f
No known key found for this signature in database
GPG key ID: EB894A3560CCCAD2
15 changed files with 259 additions and 159 deletions

View file

@ -1,6 +1,6 @@
import { Accessor, createMemo } from "solid-js";
import { createStore, NotWrappable, StoreSetter } from "solid-js/store";
import { snapshot } from "vinxi/dist/types/runtime/storage";
import { Accessor, createEffect, createMemo } from "solid-js";
import { createStore, NotWrappable, StoreSetter, unwrap } from "solid-js/store";
import { CustomPartial } from "solid-js/store/types/store.js";
import { deepCopy, deepDiff, Mutation } from "~/utilities";
@ -17,54 +17,63 @@ export interface SortOptions<T extends Record<string, any>> {
with?: SortingFunction<T>;
}
export interface GroupingFunction<T> {
(nodes: DataSetRowNode<keyof T, T>[]): DataSetNode<keyof T, T>[];
export interface GroupingFunction<K, T> {
(nodes: DataSetRowNode<K, T>[]): DataSetNode<K, T>[];
}
export interface GroupOptions<T extends Record<string, any>> {
by: keyof T;
with: GroupingFunction<T>;
with?: GroupingFunction<number, T>;
}
interface DataSetState<T extends Record<string, any>> {
value: DataSetRowNode<keyof T, T>[];
snapshot: DataSetRowNode<keyof T, T>[];
value: (T | undefined)[];
snapshot: (T | undefined)[];
sorting?: SortOptions<T>;
grouping?: GroupOptions<T>;
}
export type Setter<T> =
| T
| CustomPartial<T>
| ((prevState: T) => T | CustomPartial<T>);
export interface DataSet<T extends Record<string, any>> {
data: T[];
value: Accessor<DataSetNode<keyof T, T>[]>;
mutations: Accessor<Mutation[]>;
sort: Accessor<SortOptions<T> | undefined>;
sorting: Accessor<SortOptions<T> | undefined>;
grouping: Accessor<GroupOptions<T> | undefined>;
// mutate<K extends keyof T>(index: number, value: T): void;
mutate<K extends keyof T>(index: number, prop: K, value: T[K]): void;
remove(indices: number[]): void;
insert(item: T, at?: number): void;
setSorting(options: SortOptions<T> | undefined): void;
setGrouping(options: GroupOptions<T> | undefined): void;
sort(options: Setter<SortOptions<T> | undefined>): DataSet<T>;
group(options: Setter<GroupOptions<T> | undefined>): DataSet<T>;
}
const defaultComparer = <T>(a: T, b: T) => a < b ? -1 : a > b ? 1 : 0;
function defaultGroupingFunction<T>(groupBy: keyof T): GroupingFunction<T> {
return (nodes: DataSetRowNode<keyof T, T>[]): DataSetNode<keyof T, T>[] => Object.entries(Object.groupBy(nodes, r => r.value[groupBy] as PropertyKey))
.map(([key, nodes]) => ({ kind: 'group', key, groupedBy: groupBy, nodes: nodes! } as DataSetGroupNode<keyof T, T>));
function defaultGroupingFunction<T>(groupBy: keyof T): GroupingFunction<number, T> {
return <K>(nodes: DataSetRowNode<K, T>[]): DataSetNode<K, T>[] => Object.entries(Object.groupBy(nodes, r => r.value[groupBy] as PropertyKey))
.map(([key, nodes]) => ({ kind: 'group', key, groupedBy: groupBy, nodes: nodes! } as DataSetGroupNode<K, T>));
}
export const createDataSet = <T extends Record<string, any>>(data: T[]): DataSet<T> => {
const nodes = data.map<DataSetRowNode<keyof T, T>>((value, key) => ({ kind: 'row', key: key as keyof T, value }));
export const createDataSet = <T extends Record<string, any>>(data: T[], initialOptions?: { sort?: SortOptions<T>, group?: GroupOptions<T> }): DataSet<T> => {
const nodes = data;
const [state, setState] = createStore<DataSetState<T>>({
value: deepCopy(nodes),
snapshot: nodes,
sorting: undefined,
grouping: undefined,
sorting: initialOptions?.sort,
grouping: initialOptions?.group,
});
const value = createMemo(() => {
const sorting = state.sorting;
const grouping = state.grouping;
let value = state.value as DataSetNode<keyof T, T>[];
let value: DataSetNode<number, T>[] = state.value
.map<DataSetRowNode<number, T> | undefined>((value, key) => value === undefined ? undefined : ({ kind: 'row', key, value }))
.filter(node => node !== undefined);
if (sorting) {
const comparer = sorting.with ?? defaultComparer;
@ -79,37 +88,57 @@ export const createDataSet = <T extends Record<string, any>>(data: T[]): DataSet
if (grouping) {
const implementation = grouping.with ?? defaultGroupingFunction(grouping.by);
value = implementation(value as DataSetRowNode<keyof T, T>[]);
value = implementation(value as DataSetRowNode<number, T>[]);
}
return value;
return value as DataSetNode<keyof T, T>[];
});
const mutations = createMemo(() => {
// enumerate all values to make sure the memo is recalculated on any change
Object.values(state.value).map(entry => Object.values(entry));
Object.values(state.value).map(entry => Object.values(entry ?? {}));
return deepDiff(state.snapshot, state.value).toArray();
});
const sort = createMemo(() => state.sorting);
return {
const sorting = createMemo(() => state.sorting);
const grouping = createMemo(() => state.grouping);
const set: DataSet<T> = {
data,
value,
mutations,
sort,
sorting,
grouping,
mutate(index, prop, value) {
console.log({ index, prop, value });
// setState('value', index, 'value', prop as any, value);
setState('value', index, prop as any, value);
},
setSorting(options) {
remove(indices) {
setState('value', value => value.map((item, i) => indices.includes(i) ? undefined : item));
},
insert(item, at) {
if (at === undefined) {
setState('value', state.value.length, item);
} else {
}
},
sort(options) {
setState('sorting', options);
return set;
},
setGrouping(options) {
group(options) {
setState('grouping', options)
return set;
},
};
return set
};

View file

@ -1,5 +1,5 @@
export type { Column, TableApi, CellEditor, CellEditors } from './table';
export type { Column, TableApi, CellRenderer, CellRenderers } from './table';
export type { DataSet, DataSetGroupNode, DataSetRowNode, DataSetNode, SortingFunction, SortOptions, GroupingFunction, GroupOptions } from './dataset';
export { SelectionMode, Table } from './table';
export { createDataSet } from './dataset';

View file

@ -32,7 +32,7 @@
& :is(.cell:first-child, .checkbox + .cell) {
position: sticky;
inset-inline-start: 1px;
padding-inline-start: calc(var(--depth, 0) * 1em + var(--padding-m));
padding-inline-start: calc(var(--depth, 0) * (1em + var(--padding-s)) + var(--padding-m));
z-index: 1;
&::after {
@ -61,7 +61,6 @@
}
& .caption {
/* grid-column: 1 / -1; */
position: sticky;
inset-inline-start: 0;
}
@ -148,40 +147,58 @@
font-weight: var(--text-bold);
}
& details {
& .group {
display: contents;
background-color: inherit;
&::details-content {
grid-column: 1 / -1;
display: block grid;
grid-template-columns: subgrid;
& > td {
display: contents;
background-color: inherit;
}
&:not([open])::details-content {
display: none;
}
& > table {
grid-column: 1 / -1;
grid-template-columns: subgrid;
background-color: inherit;
overflow: visible;
& > summary {
position: sticky;
inset-inline-start: 1px;
grid-column: 1;
padding: var(--padding-m);
padding-inline-start: calc(var(--depth) * 1em + var(--padding-m));
& > .header {
border-block-end-color: transparent;
&::after {
content: '';
position: absolute;
inset-inline-start: calc(100% - 1px);
inset-block-start: -.5px;
display: block;
inline-size: 2em;
block-size: 100%;
animation: column-scroll-shadow linear both;
animation-timeline: scroll(inline);
animation-range: 0 2em;
pointer-events: none;
& .cell {
justify-content: start;
column-gap: var(--padding-s);
& > label {
--state: 0;
display: contents;
& input[type="checkbox"] {
display: none;
}
& > svg {
rotate: calc(var(--state) * -.25turn);
transition: rotate .3s ease-in-out;
inline-size: 1em;
aspect-ratio: 1;
}
&:has(input:not(:checked)) {
--state: 1;
}
}
}
}
& > .main {
block-size: calc-size(auto, size);
transition: block-size .3s ease-in-out;
overflow: clip;
}
&:has(> .header input:not(:checked)) > .main {
block-size: 0;
}
}
}
}

View file

@ -1,7 +1,7 @@
import { Accessor, createContext, createEffect, createMemo, createSignal, For, JSX, Match, Show, Switch, useContext } from "solid-js";
import { selectable, SelectionItem, SelectionProvider, useSelection } from "~/features/selectable";
import { DataSetRowNode, DataSetNode, DataSet } from './dataset';
import { FaSolidSort, FaSolidSortDown, FaSolidSortUp } from "solid-icons/fa";
import { FaSolidAngleDown, FaSolidSort, FaSolidSortDown, FaSolidSortUp } from "solid-icons/fa";
import css from './table.module.css';
selectable;
@ -14,8 +14,8 @@ export type Column<T> = {
readonly groupBy?: (rows: DataSetRowNode<keyof T, T>[]) => DataSetNode<keyof T, T>[],
};
export type CellEditor<T extends Record<string, any>, K extends keyof T> = (cell: { row: number, column: K, value: T[K] }) => JSX.Element;
export type CellEditors<T extends Record<string, any>> = { [K in keyof T]?: CellEditor<T, K> };
export type CellRenderer<T extends Record<string, any>, K extends keyof T> = (cell: { row: number, column: K, value: T[K] }) => JSX.Element;
export type CellRenderers<T extends Record<string, any>> = { [K in keyof T]?: CellRenderer<T, K> };
export interface TableApi<T extends Record<string, any>> {
readonly selection: Accessor<SelectionItem<keyof T, T>[]>;
@ -30,7 +30,7 @@ interface TableContextType<T extends Record<string, any>> {
readonly columns: Accessor<Column<T>[]>,
readonly selection: Accessor<SelectionItem<keyof T, T>[]>,
readonly selectionMode: Accessor<SelectionMode>,
readonly cellRenderers: Accessor<CellEditors<T>>,
readonly cellRenderers: Accessor<CellRenderers<T>>,
}
const TableContext = createContext<TableContextType<any>>();
@ -48,7 +48,7 @@ type TableProps<T extends Record<string, any>> = {
rows: DataSet<T>,
columns: Column<T>[],
selectionMode?: SelectionMode,
children?: CellEditors<T>,
children?: CellRenderers<T>,
api?: (api: TableApi<T>) => any,
};
@ -58,7 +58,7 @@ export function Table<T extends Record<string, any>>(props: TableProps<T>) {
const rows = createMemo(() => props.rows);
const columns = createMemo<Column<T>[]>(() => props.columns ?? []);
const selectionMode = createMemo(() => props.selectionMode ?? SelectionMode.None);
const cellRenderers = createMemo<CellEditors<T>>(() => props.children ?? {});
const cellRenderers = createMemo<CellRenderers<T>>(() => props.children ?? {});
const context: TableContextType<T> = {
rows,
@ -86,11 +86,11 @@ function InnerTable<T extends Record<string, any>>(props: InnerTableProps<T>) {
const columnCount = createMemo(() => table.columns().length);
return <table class={`${css.table} ${selectable() ? css.selectable : ''} ${props.class}`} style={{ '--columns': columnCount() }}>
<Show when={(props.summary?.length ?? 0) > 0 ? props.summary : undefined}>{
{/* <Show when={(props.summary?.length ?? 0) > 0 ? props.summary : undefined}>{
summary => {
return <caption class={css.caption}>{summary()}</caption>;
}
}</Show>
}</Show> */}
<Groups />
<Head />
@ -165,7 +165,7 @@ function Head(props: {}) {
<For each={table.columns()}>{
({ id, label, sortable }) => {
const sort = createMemo(() => table.rows().sort());
const sort = createMemo(() => table.rows().sorting());
const by = String(id);
const onPointerDown = (e: PointerEvent) => {
@ -173,27 +173,27 @@ function Head(props: {}) {
return;
}
// table.setSort(current => {
// if (current?.by !== by) {
// return { by, reversed: false };
// }
table.rows().sort(current => {
if (current?.by !== by) {
return { by, reversed: false };
}
// if (current.reversed === true) {
// return undefined;
// }
if (current.reversed === true) {
return undefined;
}
// return { by, reversed: true };
// });
return { by, reversed: true };
});
};
return <th scope="col" class={`${css.cell} ${sort()?.by === by ? css.sorted : ''}`} onpointerdown={onPointerDown}>
{label}
{/* <Switch>
<Switch>
<Match when={sortable && sort()?.by !== by}><FaSolidSort /></Match>
<Match when={sortable && sort()?.by === by && sort()?.reversed !== true}><FaSolidSortUp /></Match>
<Match when={sortable && sort()?.by === by && sort()?.reversed === true}><FaSolidSortDown /></Match>
</Switch> */}
</Switch>
</th>;
}
}</For>
@ -239,13 +239,29 @@ function Row<T extends Record<string, any>>(props: { key: keyof T, value: T, dep
};
function Group<T extends Record<string, any>>(props: { key: keyof T, groupedBy: keyof T, nodes: DataSetNode<keyof T, T>[], depth: number }) {
return <details open>
<summary style={{ '--depth': props.depth }}>{String(props.key)}</summary>
const table = useTable();
<For each={props.nodes}>{
node => <Node node={node} depth={props.depth + 1} groupedBy={props.groupedBy} />
}</For>
</details>;
return <tr class={css.group}>
<td colSpan={table.columns().length}>
<table class={css.table}>
<thead class={css.header}>
<tr><th class={css.cell} colSpan={table.columns().length} style={{ '--depth': props.depth }}>
<label>
<input type="checkbox" checked name="collapse" />
<FaSolidAngleDown />
{String(props.key)}</label>
</th></tr>
</thead>
<tbody class={css.main}>
<For each={props.nodes}>{
node => <Node node={node} depth={props.depth + 1} groupedBy={props.groupedBy} />
}</For>
</tbody>
</table>
</td>
</tr>;
};
declare module "solid-js" {