[Feature] Add language #19
15 changed files with 259 additions and 159 deletions
|
@ -7,6 +7,10 @@ export default defineConfig({
|
|||
html: {
|
||||
cspNonce: 'KAAS_IS_AWESOME',
|
||||
},
|
||||
// css: {
|
||||
// postcss: {
|
||||
// },
|
||||
// },
|
||||
plugins: [
|
||||
solidSvg()
|
||||
// VitePWA({
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import { Accessor, createContext, createEffect, createMemo, createSignal, JSX, useContext } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { deepCopy, deepDiff, Mutation } from "~/utilities";
|
||||
import { SelectionMode, Table, Column as TableColumn, TableApi, CellEditors, CellEditor, createDataSet, DataSet, DataSetNode } from "~/components/table";
|
||||
import { Mutation } from "~/utilities";
|
||||
import { SelectionMode, Table, Column as TableColumn, TableApi, DataSet, CellRenderer } from "~/components/table";
|
||||
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> {
|
||||
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> {
|
||||
|
@ -16,7 +19,6 @@ export interface GridApi<T extends Record<string, any>> extends TableApi<T> {
|
|||
}
|
||||
|
||||
interface GridContextType<T extends Record<string, any>> {
|
||||
readonly rows: Accessor<DataSetNode<keyof T, T>[]>;
|
||||
readonly mutations: Accessor<Mutation[]>;
|
||||
readonly selection: TableApi<T>['selection'];
|
||||
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)!;
|
||||
|
||||
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 };
|
||||
|
||||
export function Grid<T extends Record<string, any>>(props: GridProps<T>) {
|
||||
const [table, setTable] = createSignal<TableApi<T>>();
|
||||
const data = createMemo(() => createDataSet(props.rows));
|
||||
|
||||
const rows = createMemo(() => data().value());
|
||||
const mutations = createMemo(() => data().mutations());
|
||||
const rows = createMemo(() => props.rows);
|
||||
const columns = createMemo(() => props.columns);
|
||||
const mutations = createMemo(() => rows().mutations());
|
||||
|
||||
const ctx: GridContextType<T> = {
|
||||
rows,
|
||||
mutations,
|
||||
selection: createMemo(() => table()?.selection() ?? []),
|
||||
|
||||
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[]) {
|
||||
// setState('rows', (r) => r.filter((_, i) => rows.includes(i) === false));
|
||||
remove(indices: number[]) {
|
||||
rows().remove(indices);
|
||||
table()?.clear();
|
||||
},
|
||||
|
||||
insert(row: T, at?: number) {
|
||||
if (at === undefined) {
|
||||
// setState('rows', state.rows.length, row);
|
||||
} else {
|
||||
|
||||
}
|
||||
rows().insert(row, at);
|
||||
},
|
||||
|
||||
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
|
||||
.filter(c => c.editor !== undefined)
|
||||
.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]) => {
|
||||
console.log('KAAS', { 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}>
|
||||
<Api api={props.api} table={table()} />
|
||||
|
||||
<form style="all: inherit; display: contents;">
|
||||
<Table api={setTable} class={`${css.grid} ${props.class}`} rows={data()} columns={columns()} selectionMode={SelectionMode.Multiple}>{
|
||||
cellEditors()
|
||||
}</Table>
|
||||
</form>
|
||||
<Table api={setTable} class={`${css.grid} ${props.class}`} rows={rows()} columns={columns()} selectionMode={SelectionMode.Multiple}>{
|
||||
cellEditors()
|
||||
}</Table>
|
||||
</GridContext.Provider>;
|
||||
};
|
||||
|
||||
|
@ -114,8 +107,8 @@ function Api<T extends Record<string, any>>(props: { api: undefined | ((api: Gri
|
|||
insert(row: T, at?: number) {
|
||||
gridContext.insert(row, at);
|
||||
},
|
||||
addColumn(column: keyof T, value: T[keyof T]): void {
|
||||
gridContext.addColumn(column, value);
|
||||
addColumn(column: keyof T): void {
|
||||
// gridContext.addColumn(column, value);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
|
||||
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';
|
|
@ -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';
|
||||
|
||||
export interface PromptApi {
|
||||
|
@ -72,4 +72,7 @@ export const Prompt: ParentComponent<{ api: (api: PromptApi) => any, title?: str
|
|||
</footer>
|
||||
</form>
|
||||
</dialog>;
|
||||
};
|
||||
};
|
||||
|
||||
let idCounter = 0;
|
||||
const createUniqueId = () => `prompt-${idCounter++}`;
|
|
@ -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
|
||||
};
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" {
|
||||
|
|
|
@ -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 { createStore } from "solid-js/store";
|
||||
import { CommandType, Command, useCommands } from "../command";
|
||||
|
@ -347,6 +347,9 @@ function SearchableList<T>(props: SearchableListProps<T>): JSX.Element {
|
|||
</search>;
|
||||
};
|
||||
|
||||
let keyCounter = 0;
|
||||
const createUniqueId = () => `key-${keyCounter++}`;
|
||||
|
||||
declare module "solid-js" {
|
||||
namespace JSX {
|
||||
interface HTMLAttributes<T> {
|
||||
|
|
|
@ -119,9 +119,9 @@ export function SelectionProvider<T extends object>(props: ParentProps<{ selecti
|
|||
|
||||
keyIdMap.set(key, id);
|
||||
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)!;
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { CommandPalette, CommandPaletteApi, Menu, MenuProvider } from "~/features/menu";
|
||||
import { A, RouteDefinition, useBeforeLeave } from "@solidjs/router";
|
||||
|
@ -121,3 +121,6 @@ const ErrorComp: Component<{ error: Error }> = (props) => {
|
|||
<a href="/">Return to start</a>
|
||||
</div>;
|
||||
};
|
||||
|
||||
let keyCounter = 0;
|
||||
const createUniqueId = () => `key-${keyCounter++}`;
|
|
@ -21,18 +21,27 @@
|
|||
flex-flow: column;
|
||||
gap: var(--padding-m);
|
||||
}
|
||||
|
||||
ol {
|
||||
margin-block: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& .content {
|
||||
display: block grid;
|
||||
grid: 1fr 1fr / 100%;
|
||||
background-color: var(--surface-500);
|
||||
border-top-left-radius: var(--radii-xl);
|
||||
|
||||
& > header {
|
||||
padding-inline-start: var(--padding-l);
|
||||
}
|
||||
padding: var(--padding-m);
|
||||
|
||||
& .table {
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
& > fieldset {
|
||||
border-radius: var(--radii-l);
|
||||
overflow: auto;
|
||||
background-color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,14 @@
|
|||
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 { 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 css from './grid.module.css';
|
||||
|
||||
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>[] = [
|
||||
{
|
||||
id: 'id',
|
||||
|
@ -24,35 +26,37 @@ export default function GridExperiment() {
|
|||
id: 'name',
|
||||
label: 'Name',
|
||||
sortable: true,
|
||||
editor,
|
||||
},
|
||||
{
|
||||
id: 'email',
|
||||
label: 'Email',
|
||||
sortable: true,
|
||||
editor: ({ value, mutate }) => <input value={value} oninput={debounce(e => {
|
||||
console.log('WHAAAAT????', e);
|
||||
return mutate(e.target.value.trim());
|
||||
}, 100)} />,
|
||||
editor,
|
||||
},
|
||||
{
|
||||
id: 'address',
|
||||
label: 'Address',
|
||||
sortable: true,
|
||||
editor,
|
||||
},
|
||||
{
|
||||
id: 'currency',
|
||||
label: 'Currency',
|
||||
sortable: true,
|
||||
editor,
|
||||
},
|
||||
{
|
||||
id: 'phone',
|
||||
label: 'Phone',
|
||||
sortable: true,
|
||||
editor,
|
||||
},
|
||||
{
|
||||
id: 'country',
|
||||
label: 'Country',
|
||||
sortable: true,
|
||||
editor,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -60,46 +64,55 @@ export default function GridExperiment() {
|
|||
|
||||
const mutations = createMemo(() => api()?.mutations() ?? [])
|
||||
|
||||
// createEffect(() => {
|
||||
// console.log(mutations());
|
||||
// });
|
||||
const rows = createDataSet(people.slice(0, 20), {
|
||||
// group: { by: 'country' },
|
||||
sort: { by: 'name', reversed: false },
|
||||
});
|
||||
|
||||
return <div class={css.root}>
|
||||
<Sidebar as="aside" label={'Grid options'} class={css.sidebar}>
|
||||
<fieldset>
|
||||
<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>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Selection ({api()?.selection().length})</legend>
|
||||
|
||||
<pre>{JSON.stringify(api()?.selection().map(i => i.key))}</pre>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Mutations ({mutations().length})</legend>
|
||||
|
||||
<Mutations mutations={mutations()} />
|
||||
<ol>
|
||||
<For each={api()?.selection()}>{
|
||||
item => <li value={item.key}>{item.value().name}</li>
|
||||
}</For>
|
||||
</ol>
|
||||
</fieldset>
|
||||
</Sidebar>
|
||||
|
||||
<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 >;
|
||||
}
|
||||
|
||||
type M = { kind: MutarionKind, key: string, original?: any, value?: any };
|
||||
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));
|
||||
|
||||
return <Table rows={rows()} columns={columns} groupBy='kind'>{{
|
||||
original: ({ value }) => <del>{value}</del>,
|
||||
value: ({ value }) => <ins>{value}</ins>,
|
||||
createEffect(() => {
|
||||
rows().group({ by: 'kind' });
|
||||
});
|
||||
|
||||
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>
|
||||
};
|
|
@ -3,9 +3,8 @@ import { Column, createDataSet, DataSetGroupNode, DataSetNode, DataSetRowNode, G
|
|||
import { createStore } from 'solid-js/store';
|
||||
import { Person, people } from './experimental.data';
|
||||
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 css from './table.module.css';
|
||||
|
||||
export default function TableExperiment() {
|
||||
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,
|
||||
group: undefined,
|
||||
sort: undefined,
|
||||
grouping: { by: 'country' },
|
||||
sorting: { by: 'country', reversed: false },
|
||||
});
|
||||
|
||||
const rows = createMemo(() => createDataSet(people));
|
||||
const rows = createMemo(() => createDataSet(people, {
|
||||
group: { by: 'country' },
|
||||
sort: { by: 'country', reversed: false },
|
||||
}));
|
||||
|
||||
createEffect(() => {
|
||||
rows().setGrouping(store.group);
|
||||
rows().group(store.grouping);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
rows().setSorting(store.sort);
|
||||
rows().sort(store.sorting);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
setStore('sorting', rows().sorting());
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
setStore('grouping', rows().grouping());
|
||||
});
|
||||
|
||||
return <div class={css.root}>
|
||||
|
@ -93,7 +103,7 @@ export default function TableExperiment() {
|
|||
<label>
|
||||
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>
|
||||
<For each={columns}>{
|
||||
column => <option value={column.id}>{column.label}</option>
|
||||
|
@ -108,7 +118,7 @@ export default function TableExperiment() {
|
|||
<label>
|
||||
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>
|
||||
<For each={columns}>{
|
||||
column => <option value={column.id}>{column.label}</option>
|
||||
|
@ -119,7 +129,7 @@ export default function TableExperiment() {
|
|||
<label>
|
||||
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>
|
||||
</fieldset>
|
||||
</Sidebar>
|
||||
|
|
|
@ -181,7 +181,7 @@ const bufferredIterator = <T extends readonly [string | number, any]>(subject: I
|
|||
done = res.done ?? false;
|
||||
|
||||
if (!done) {
|
||||
buffer.push(res.value)
|
||||
buffer.push(res.value);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue