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

@ -7,6 +7,10 @@ export default defineConfig({
html: { html: {
cspNonce: 'KAAS_IS_AWESOME', cspNonce: 'KAAS_IS_AWESOME',
}, },
// css: {
// postcss: {
// },
// },
plugins: [ plugins: [
solidSvg() solidSvg()
// VitePWA({ // VitePWA({

View file

@ -1,11 +1,14 @@
import { Accessor, createContext, createEffect, createMemo, createSignal, JSX, useContext } from "solid-js"; import { Accessor, createContext, createEffect, createMemo, createSignal, JSX, useContext } from "solid-js";
import { createStore } from "solid-js/store"; import { Mutation } from "~/utilities";
import { deepCopy, deepDiff, Mutation } from "~/utilities"; import { SelectionMode, Table, Column as TableColumn, TableApi, DataSet, CellRenderer } from "~/components/table";
import { SelectionMode, Table, Column as TableColumn, TableApi, CellEditors, CellEditor, createDataSet, DataSet, DataSetNode } from "~/components/table";
import css from './grid.module.css'; import css from './grid.module.css';
export interface CellEditor<T extends Record<string, any>, K extends keyof T> {
(cell: Parameters<CellRenderer<T, K>>[0] & { mutate: (next: T[K]) => any }): JSX.Element;
}
export interface Column<T extends Record<string, any>> extends TableColumn<T> { export interface Column<T extends Record<string, any>> extends TableColumn<T> {
editor?: (cell: { row: number, column: keyof T, value: T[keyof T], mutate: (next: T[keyof T]) => any }) => JSX.Element; editor?: CellEditor<T, keyof T>;
} }
export interface GridApi<T extends Record<string, any>> extends TableApi<T> { export interface GridApi<T extends Record<string, any>> extends TableApi<T> {
@ -16,7 +19,6 @@ export interface GridApi<T extends Record<string, any>> extends TableApi<T> {
} }
interface GridContextType<T extends Record<string, any>> { interface GridContextType<T extends Record<string, any>> {
readonly rows: Accessor<DataSetNode<keyof T, T>[]>;
readonly mutations: Accessor<Mutation[]>; readonly mutations: Accessor<Mutation[]>;
readonly selection: TableApi<T>['selection']; readonly selection: TableApi<T>['selection'];
mutate<K extends keyof T>(row: number, column: K, value: T[K]): void; mutate<K extends keyof T>(row: number, column: K, value: T[K]): void;
@ -29,36 +31,31 @@ const GridContext = createContext<GridContextType<any>>();
const useGrid = () => useContext(GridContext)!; const useGrid = () => useContext(GridContext)!;
type GridProps<T extends Record<string, any>> = { class?: string, groupBy?: keyof T, columns: Column<T>[], rows: T[], api?: (api: GridApi<T>) => any }; type GridProps<T extends Record<string, any>> = { class?: string, groupBy?: keyof T, columns: Column<T>[], rows: DataSet<T>, api?: (api: GridApi<T>) => any };
// type GridState<T extends Record<string, any>> = { data: DataSet<T>, columns: Column<T>[], numberOfRows: number }; // type GridState<T extends Record<string, any>> = { data: DataSet<T>, columns: Column<T>[], numberOfRows: number };
export function Grid<T extends Record<string, any>>(props: GridProps<T>) { export function Grid<T extends Record<string, any>>(props: GridProps<T>) {
const [table, setTable] = createSignal<TableApi<T>>(); const [table, setTable] = createSignal<TableApi<T>>();
const data = createMemo(() => createDataSet(props.rows));
const rows = createMemo(() => data().value()); const rows = createMemo(() => props.rows);
const mutations = createMemo(() => data().mutations());
const columns = createMemo(() => props.columns); const columns = createMemo(() => props.columns);
const mutations = createMemo(() => rows().mutations());
const ctx: GridContextType<T> = { const ctx: GridContextType<T> = {
rows,
mutations, mutations,
selection: createMemo(() => table()?.selection() ?? []), selection: createMemo(() => table()?.selection() ?? []),
mutate<K extends keyof T>(row: number, column: K, value: T[K]) { mutate<K extends keyof T>(row: number, column: K, value: T[K]) {
data().mutate(row, column, value); rows().mutate(row, column, value);
}, },
remove(rows: number[]) { remove(indices: number[]) {
// setState('rows', (r) => r.filter((_, i) => rows.includes(i) === false)); rows().remove(indices);
table()?.clear();
}, },
insert(row: T, at?: number) { insert(row: T, at?: number) {
if (at === undefined) { rows().insert(row, at);
// setState('rows', state.rows.length, row);
} else {
}
}, },
addColumn(column: keyof T, value: T[keyof T]): void { addColumn(column: keyof T, value: T[keyof T]): void {
@ -70,10 +67,8 @@ export function Grid<T extends Record<string, any>>(props: GridProps<T>) {
props.columns props.columns
.filter(c => c.editor !== undefined) .filter(c => c.editor !== undefined)
.map(c => { .map(c => {
const Editor: CellEditor<T, keyof T> = ({ row, column, value }) => { const Editor: CellRenderer<T, keyof T> = ({ row, column, value }) => {
const mutate = (next: T[keyof T]) => { const mutate = (next: T[keyof T]) => {
console.log('KAAS', { next })
ctx.mutate(row, column, next); ctx.mutate(row, column, next);
}; };
@ -87,11 +82,9 @@ export function Grid<T extends Record<string, any>>(props: GridProps<T>) {
return <GridContext.Provider value={ctx}> return <GridContext.Provider value={ctx}>
<Api api={props.api} table={table()} /> <Api api={props.api} table={table()} />
<form style="all: inherit; display: contents;"> <Table api={setTable} class={`${css.grid} ${props.class}`} rows={rows()} columns={columns()} selectionMode={SelectionMode.Multiple}>{
<Table api={setTable} class={`${css.grid} ${props.class}`} rows={data()} columns={columns()} selectionMode={SelectionMode.Multiple}>{ cellEditors()
cellEditors() }</Table>
}</Table>
</form>
</GridContext.Provider>; </GridContext.Provider>;
}; };
@ -114,8 +107,8 @@ function Api<T extends Record<string, any>>(props: { api: undefined | ((api: Gri
insert(row: T, at?: number) { insert(row: T, at?: number) {
gridContext.insert(row, at); gridContext.insert(row, at);
}, },
addColumn(column: keyof T, value: T[keyof T]): void { addColumn(column: keyof T): void {
gridContext.addColumn(column, value); // gridContext.addColumn(column, value);
}, },
}; };
}); });

View file

@ -1,4 +1,4 @@
export type { DataSetRowNode, DataSetGroupNode, DataSetNode, SelectionMode, SortingFunction, SortOptions, GroupingFunction, GroupOptions } from '../table'; export type { DataSetRowNode, DataSetGroupNode, DataSetNode, SelectionMode, SortingFunction, SortOptions, GroupingFunction, GroupOptions } from '../table';
export type { GridApi, Column } from './grid'; export type { GridApi, Column, CellEditor } from './grid';
export { Grid } from './grid'; export { Grid } from './grid';

View file

@ -1,4 +1,4 @@
import { createEffect, createSignal, createUniqueId, JSX, onMount, ParentComponent, Show } from "solid-js"; import { createEffect, createSignal, JSX, ParentComponent, Show } from "solid-js";
import css from './prompt.module.css'; import css from './prompt.module.css';
export interface PromptApi { export interface PromptApi {
@ -72,4 +72,7 @@ export const Prompt: ParentComponent<{ api: (api: PromptApi) => any, title?: str
</footer> </footer>
</form> </form>
</dialog>; </dialog>;
}; };
let idCounter = 0;
const createUniqueId = () => `prompt-${idCounter++}`;

View file

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

View file

@ -32,7 +32,7 @@
& :is(.cell:first-child, .checkbox + .cell) { & :is(.cell:first-child, .checkbox + .cell) {
position: sticky; position: sticky;
inset-inline-start: 1px; 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; z-index: 1;
&::after { &::after {
@ -61,7 +61,6 @@
} }
& .caption { & .caption {
/* grid-column: 1 / -1; */
position: sticky; position: sticky;
inset-inline-start: 0; inset-inline-start: 0;
} }
@ -148,40 +147,58 @@
font-weight: var(--text-bold); font-weight: var(--text-bold);
} }
& details { & .group {
display: contents; display: contents;
background-color: inherit; background-color: inherit;
&::details-content { & > td {
grid-column: 1 / -1; display: contents;
display: block grid;
grid-template-columns: subgrid;
background-color: inherit; background-color: inherit;
}
&:not([open])::details-content { & > table {
display: none; grid-column: 1 / -1;
} grid-template-columns: subgrid;
background-color: inherit;
overflow: visible;
& > summary { & > .header {
position: sticky; border-block-end-color: transparent;
inset-inline-start: 1px;
grid-column: 1;
padding: var(--padding-m);
padding-inline-start: calc(var(--depth) * 1em + var(--padding-m));
&::after { & .cell {
content: ''; justify-content: start;
position: absolute; column-gap: var(--padding-s);
inset-inline-start: calc(100% - 1px);
inset-block-start: -.5px; & > label {
display: block; --state: 0;
inline-size: 2em; display: contents;
block-size: 100%;
animation: column-scroll-shadow linear both; & input[type="checkbox"] {
animation-timeline: scroll(inline); display: none;
animation-range: 0 2em; }
pointer-events: 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 { Accessor, createContext, createEffect, createMemo, createSignal, For, JSX, Match, Show, Switch, useContext } from "solid-js";
import { selectable, SelectionItem, SelectionProvider, useSelection } from "~/features/selectable"; import { selectable, SelectionItem, SelectionProvider, useSelection } from "~/features/selectable";
import { DataSetRowNode, DataSetNode, DataSet } from './dataset'; 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'; import css from './table.module.css';
selectable; selectable;
@ -14,8 +14,8 @@ export type Column<T> = {
readonly groupBy?: (rows: DataSetRowNode<keyof T, T>[]) => DataSetNode<keyof T, 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 CellRenderer<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 CellRenderers<T extends Record<string, any>> = { [K in keyof T]?: CellRenderer<T, K> };
export interface TableApi<T extends Record<string, any>> { export interface TableApi<T extends Record<string, any>> {
readonly selection: Accessor<SelectionItem<keyof T, T>[]>; readonly selection: Accessor<SelectionItem<keyof T, T>[]>;
@ -30,7 +30,7 @@ interface TableContextType<T extends Record<string, any>> {
readonly columns: Accessor<Column<T>[]>, readonly columns: Accessor<Column<T>[]>,
readonly selection: Accessor<SelectionItem<keyof T, T>[]>, readonly selection: Accessor<SelectionItem<keyof T, T>[]>,
readonly selectionMode: Accessor<SelectionMode>, readonly selectionMode: Accessor<SelectionMode>,
readonly cellRenderers: Accessor<CellEditors<T>>, readonly cellRenderers: Accessor<CellRenderers<T>>,
} }
const TableContext = createContext<TableContextType<any>>(); const TableContext = createContext<TableContextType<any>>();
@ -48,7 +48,7 @@ type TableProps<T extends Record<string, any>> = {
rows: DataSet<T>, rows: DataSet<T>,
columns: Column<T>[], columns: Column<T>[],
selectionMode?: SelectionMode, selectionMode?: SelectionMode,
children?: CellEditors<T>, children?: CellRenderers<T>,
api?: (api: TableApi<T>) => any, 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 rows = createMemo(() => props.rows);
const columns = createMemo<Column<T>[]>(() => props.columns ?? []); const columns = createMemo<Column<T>[]>(() => props.columns ?? []);
const selectionMode = createMemo(() => props.selectionMode ?? SelectionMode.None); 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> = { const context: TableContextType<T> = {
rows, rows,
@ -86,11 +86,11 @@ function InnerTable<T extends Record<string, any>>(props: InnerTableProps<T>) {
const columnCount = createMemo(() => table.columns().length); const columnCount = createMemo(() => table.columns().length);
return <table class={`${css.table} ${selectable() ? css.selectable : ''} ${props.class}`} style={{ '--columns': columnCount() }}> 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 => { summary => {
return <caption class={css.caption}>{summary()}</caption>; return <caption class={css.caption}>{summary()}</caption>;
} }
}</Show> }</Show> */}
<Groups /> <Groups />
<Head /> <Head />
@ -165,7 +165,7 @@ function Head(props: {}) {
<For each={table.columns()}>{ <For each={table.columns()}>{
({ id, label, sortable }) => { ({ id, label, sortable }) => {
const sort = createMemo(() => table.rows().sort()); const sort = createMemo(() => table.rows().sorting());
const by = String(id); const by = String(id);
const onPointerDown = (e: PointerEvent) => { const onPointerDown = (e: PointerEvent) => {
@ -173,27 +173,27 @@ function Head(props: {}) {
return; return;
} }
// table.setSort(current => { table.rows().sort(current => {
// if (current?.by !== by) { if (current?.by !== by) {
// return { by, reversed: false }; return { by, reversed: false };
// } }
// if (current.reversed === true) { if (current.reversed === true) {
// return undefined; return undefined;
// } }
// return { by, reversed: true }; return { by, reversed: true };
// }); });
}; };
return <th scope="col" class={`${css.cell} ${sort()?.by === by ? css.sorted : ''}`} onpointerdown={onPointerDown}> return <th scope="col" class={`${css.cell} ${sort()?.by === by ? css.sorted : ''}`} onpointerdown={onPointerDown}>
{label} {label}
{/* <Switch> <Switch>
<Match when={sortable && sort()?.by !== by}><FaSolidSort /></Match> <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}><FaSolidSortUp /></Match>
<Match when={sortable && sort()?.by === by && sort()?.reversed === true}><FaSolidSortDown /></Match> <Match when={sortable && sort()?.by === by && sort()?.reversed === true}><FaSolidSortDown /></Match>
</Switch> */} </Switch>
</th>; </th>;
} }
}</For> }</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 }) { function Group<T extends Record<string, any>>(props: { key: keyof T, groupedBy: keyof T, nodes: DataSetNode<keyof T, T>[], depth: number }) {
return <details open> const table = useTable();
<summary style={{ '--depth': props.depth }}>{String(props.key)}</summary>
<For each={props.nodes}>{ return <tr class={css.group}>
node => <Node node={node} depth={props.depth + 1} groupedBy={props.groupedBy} /> <td colSpan={table.columns().length}>
}</For> <table class={css.table}>
</details>; <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" { declare module "solid-js" {

View file

@ -1,4 +1,4 @@
import { Accessor, Component, For, JSX, Match, ParentComponent, Setter, Show, Switch, children, createContext, createEffect, createMemo, createSignal, createUniqueId, mergeProps, onCleanup, onMount, useContext } from "solid-js"; import { Accessor, Component, For, JSX, Match, ParentComponent, Setter, Show, Switch, children, createContext, createEffect, createMemo, createSignal, mergeProps, useContext } from "solid-js";
import { Portal } from "solid-js/web"; import { Portal } from "solid-js/web";
import { createStore } from "solid-js/store"; import { createStore } from "solid-js/store";
import { CommandType, Command, useCommands } from "../command"; import { CommandType, Command, useCommands } from "../command";
@ -347,6 +347,9 @@ function SearchableList<T>(props: SearchableListProps<T>): JSX.Element {
</search>; </search>;
}; };
let keyCounter = 0;
const createUniqueId = () => `key-${keyCounter++}`;
declare module "solid-js" { declare module "solid-js" {
namespace JSX { namespace JSX {
interface HTMLAttributes<T> { interface HTMLAttributes<T> {

View file

@ -119,9 +119,9 @@ export function SelectionProvider<T extends object>(props: ParentProps<{ selecti
keyIdMap.set(key, id); keyIdMap.set(key, id);
idKeyMap.set(id, key); idKeyMap.set(id, key);
}
setState('data', state.data.length, { key, value, element: new WeakRef(element) }); setState('data', state.data.length, { key, value, element: new WeakRef(element) });
}
return keyIdMap.get(key)!; return keyIdMap.get(key)!;
}, },

View file

@ -1,5 +1,5 @@
import { Link, Meta, Title } from "@solidjs/meta"; import { Link, Meta, Title } from "@solidjs/meta";
import { Component, createMemo, createSignal, createUniqueId, ErrorBoundary, ParentProps, Show } from "solid-js"; import { Component, createMemo, createSignal, ParentProps, Show } from "solid-js";
import { FilesProvider } from "~/features/file"; import { FilesProvider } from "~/features/file";
import { CommandPalette, CommandPaletteApi, Menu, MenuProvider } from "~/features/menu"; import { CommandPalette, CommandPaletteApi, Menu, MenuProvider } from "~/features/menu";
import { A, RouteDefinition, useBeforeLeave } from "@solidjs/router"; import { A, RouteDefinition, useBeforeLeave } from "@solidjs/router";
@ -121,3 +121,6 @@ const ErrorComp: Component<{ error: Error }> = (props) => {
<a href="/">Return to start</a> <a href="/">Return to start</a>
</div>; </div>;
}; };
let keyCounter = 0;
const createUniqueId = () => `key-${keyCounter++}`;

View file

@ -21,18 +21,27 @@
flex-flow: column; flex-flow: column;
gap: var(--padding-m); gap: var(--padding-m);
} }
ol {
margin-block: 0;
}
} }
& .content { & .content {
display: block grid;
grid: 1fr 1fr / 100%;
background-color: var(--surface-500); background-color: var(--surface-500);
border-top-left-radius: var(--radii-xl); border-top-left-radius: var(--radii-xl);
padding: var(--padding-m);
& > header {
padding-inline-start: var(--padding-l);
}
& .table { & .table {
border-radius: inherit; border-radius: inherit;
} }
& > fieldset {
border-radius: var(--radii-l);
overflow: auto;
background-color: inherit;
}
} }
} }

View file

@ -1,12 +1,14 @@
import { Sidebar } from '~/components/sidebar'; import { Sidebar } from '~/components/sidebar';
import { Column, DataSetGroupNode, DataSetNode, DataSetRowNode, Grid, GridApi } from '~/components/grid'; import { CellEditor, Column, DataSetGroupNode, DataSetNode, DataSetRowNode, Grid, GridApi } from '~/components/grid';
import { people, Person } from './experimental.data'; import { people, Person } from './experimental.data';
import { Component, createEffect, createMemo, createSignal, For, Match, Switch } from 'solid-js'; import { Component, createEffect, createMemo, createSignal, For, Match, Switch } from 'solid-js';
import { Created, debounce, Deleted, MutarionKind, Mutation, Updated } from '~/utilities'; import { debounce, MutarionKind, Mutation } from '~/utilities';
import { createDataSet, Table } from '~/components/table'; import { createDataSet, Table } from '~/components/table';
import css from './grid.module.css'; import css from './grid.module.css';
export default function GridExperiment() { export default function GridExperiment() {
const editor: CellEditor<any, any> = ({ value, mutate }) => <input value={value} oninput={debounce(e => mutate(e.target.value.trim()), 300)} />
const columns: Column<Person>[] = [ const columns: Column<Person>[] = [
{ {
id: 'id', id: 'id',
@ -24,35 +26,37 @@ export default function GridExperiment() {
id: 'name', id: 'name',
label: 'Name', label: 'Name',
sortable: true, sortable: true,
editor,
}, },
{ {
id: 'email', id: 'email',
label: 'Email', label: 'Email',
sortable: true, sortable: true,
editor: ({ value, mutate }) => <input value={value} oninput={debounce(e => { editor,
console.log('WHAAAAT????', e);
return mutate(e.target.value.trim());
}, 100)} />,
}, },
{ {
id: 'address', id: 'address',
label: 'Address', label: 'Address',
sortable: true, sortable: true,
editor,
}, },
{ {
id: 'currency', id: 'currency',
label: 'Currency', label: 'Currency',
sortable: true, sortable: true,
editor,
}, },
{ {
id: 'phone', id: 'phone',
label: 'Phone', label: 'Phone',
sortable: true, sortable: true,
editor,
}, },
{ {
id: 'country', id: 'country',
label: 'Country', label: 'Country',
sortable: true, sortable: true,
editor,
}, },
]; ];
@ -60,46 +64,55 @@ export default function GridExperiment() {
const mutations = createMemo(() => api()?.mutations() ?? []) const mutations = createMemo(() => api()?.mutations() ?? [])
// createEffect(() => { const rows = createDataSet(people.slice(0, 20), {
// console.log(mutations()); // group: { by: 'country' },
// }); sort: { by: 'name', reversed: false },
});
return <div class={css.root}> return <div class={css.root}>
<Sidebar as="aside" label={'Grid options'} class={css.sidebar}> <Sidebar as="aside" label={'Grid options'} class={css.sidebar}>
<fieldset> <fieldset>
<legend>Commands</legend> <legend>Commands</legend>
<button onclick={() => api()?.insert({ id: 'some guid', name: 'new person', address: '', country: '', currency: '', email: 'some@email.email', phone: '' })}>add row</button> <button onclick={() => api()?.insert({ id: crypto.randomUUID(), name: '', address: '', country: '', currency: '', email: '', phone: '' })}>add row</button>
<button onclick={() => api()?.remove(api()?.selection()?.map(i => i.key as any) ?? [])} disabled={api()?.selection().length === 0}>Remove {api()?.selection().length} items</button> <button onclick={() => api()?.remove(api()?.selection()?.map(i => i.key as any) ?? [])} disabled={api()?.selection().length === 0}>Remove {api()?.selection().length} items</button>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>Selection ({api()?.selection().length})</legend> <legend>Selection ({api()?.selection().length})</legend>
<pre>{JSON.stringify(api()?.selection().map(i => i.key))}</pre> <ol>
</fieldset> <For each={api()?.selection()}>{
item => <li value={item.key}>{item.value().name}</li>
<fieldset> }</For>
<legend>Mutations ({mutations().length})</legend> </ol>
<Mutations mutations={mutations()} />
</fieldset> </fieldset>
</Sidebar> </Sidebar>
<div class={css.content}> <div class={css.content}>
<Grid api={setApi} rows={people} columns={columns} groupBy="country" /> <Grid class={css.table} api={setApi} rows={rows} columns={columns} groupBy="country" />
<fieldset class={css.mutaions}>
<legend>Mutations ({mutations().length})</legend>
<Mutations mutations={mutations()} />
</fieldset>
</div> </div>
</div >; </div >;
} }
type M = { kind: MutarionKind, key: string, original?: any, value?: any }; type M = { kind: MutarionKind, key: string, original?: any, value?: any };
const Mutations: Component<{ mutations: Mutation[] }> = (props) => { const Mutations: Component<{ mutations: Mutation[] }> = (props) => {
const columns: Column<M>[] = [{ id: 'key', label: 'Key' }, { id: 'original', label: 'original' }, { id: 'value', label: 'Value' }]; const columns: Column<M>[] = [{ id: 'key', label: 'Key' }, { id: 'original', label: 'Old' }, { id: 'value', label: 'New' }];
const rows = createMemo(() => createDataSet<M>(props.mutations)); const rows = createMemo(() => createDataSet<M>(props.mutations));
return <Table rows={rows()} columns={columns} groupBy='kind'>{{ createEffect(() => {
original: ({ value }) => <del>{value}</del>, rows().group({ by: 'kind' });
value: ({ value }) => <ins>{value}</ins>, });
return <Table rows={rows()} columns={columns}>{{
original: ({ value }) => value ? <del><pre>{JSON.stringify(value, null, 2)}</pre></del> : null,
value: ({ value }) => value ? <ins><pre>{JSON.stringify(value, null, 2)}</pre></ins> : null,
}}</Table> }}</Table>
}; };

View file

@ -3,9 +3,8 @@ import { Column, createDataSet, DataSetGroupNode, DataSetNode, DataSetRowNode, G
import { createStore } from 'solid-js/store'; import { createStore } from 'solid-js/store';
import { Person, people } from './experimental.data'; import { Person, people } from './experimental.data';
import { createEffect, createMemo, For } from 'solid-js'; import { createEffect, createMemo, For } from 'solid-js';
import css from './table.module.css';
import { Menu } from '~/features/menu';
import { Command, createCommand, Modifier } from '~/features/command'; import { Command, createCommand, Modifier } from '~/features/command';
import css from './table.module.css';
export default function TableExperiment() { export default function TableExperiment() {
const columns: Column<Person>[] = [ const columns: Column<Person>[] = [
@ -53,20 +52,31 @@ export default function TableExperiment() {
}, },
]; ];
const [store, setStore] = createStore<{ selectionMode: SelectionMode, group?: GroupOptions<Person>, sort?: SortOptions<Person> }>({ const [store, setStore] = createStore<{ selectionMode: SelectionMode, grouping?: GroupOptions<Person>, sorting?: SortOptions<Person> }>({
selectionMode: SelectionMode.None, selectionMode: SelectionMode.None,
group: undefined, grouping: { by: 'country' },
sort: undefined, sorting: { by: 'country', reversed: false },
}); });
const rows = createMemo(() => createDataSet(people)); const rows = createMemo(() => createDataSet(people, {
group: { by: 'country' },
sort: { by: 'country', reversed: false },
}));
createEffect(() => { createEffect(() => {
rows().setGrouping(store.group); rows().group(store.grouping);
}); });
createEffect(() => { createEffect(() => {
rows().setSorting(store.sort); rows().sort(store.sorting);
});
createEffect(() => {
setStore('sorting', rows().sorting());
});
createEffect(() => {
setStore('grouping', rows().grouping());
}); });
return <div class={css.root}> return <div class={css.root}>
@ -93,7 +103,7 @@ export default function TableExperiment() {
<label> <label>
Group by Group by
<select value={store.group?.by ?? ''} oninput={e => setStore('group', 'by', (e.target.value || undefined) as any)}> <select value={store.grouping?.by ?? ''} oninput={e => setStore('grouping', e.target.value ? { by: e.target.value as keyof Person } : undefined)}>
<option value=''>None</option> <option value=''>None</option>
<For each={columns}>{ <For each={columns}>{
column => <option value={column.id}>{column.label}</option> column => <option value={column.id}>{column.label}</option>
@ -108,7 +118,7 @@ export default function TableExperiment() {
<label> <label>
by by
<select value={store.sort?.by ?? ''} oninput={e => setStore('sort', prev => e.target.value ? { by: e.target.value as keyof Person, reversed: prev?.reversed } : undefined)}> <select value={store.sorting?.by ?? ''} oninput={e => setStore('sorting', prev => e.target.value ? { by: e.target.value as keyof Person, reversed: prev?.reversed } : undefined)}>
<option value=''>None</option> <option value=''>None</option>
<For each={columns}>{ <For each={columns}>{
column => <option value={column.id}>{column.label}</option> column => <option value={column.id}>{column.label}</option>
@ -119,7 +129,7 @@ export default function TableExperiment() {
<label> <label>
reversed reversed
<input type="checkbox" checked={store.sort?.reversed ?? false} oninput={e => setStore('sort', prev => prev !== undefined ? { by: prev.by, reversed: e.target.checked || undefined } : undefined)} /> <input type="checkbox" checked={store.sorting?.reversed ?? false} oninput={e => setStore('sorting', prev => prev !== undefined ? { by: prev.by, reversed: e.target.checked || undefined } : undefined)} />
</label> </label>
</fieldset> </fieldset>
</Sidebar> </Sidebar>

View file

@ -181,7 +181,7 @@ const bufferredIterator = <T extends readonly [string | number, any]>(subject: I
done = res.done ?? false; done = res.done ?? false;
if (!done) { if (!done) {
buffer.push(res.value) buffer.push(res.value);
} }
}; };