diff --git a/bun.lockb b/bun.lockb index 9b87d97..9c2dca4 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 6f0ea11..192e6ce 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,14 @@ "name": "calque", "dependencies": { "@solidjs/meta": "^0.29.4", - "@solidjs/router": "^0.15.1", + "@solidjs/router": "^0.15.2", "@solidjs/start": "^1.0.10", "dexie": "^4.0.10", "iterator-helpers-polyfill": "^3.0.1", "sitemap": "^8.0.0", "solid-icons": "^1.1.0", "solid-js": "^1.9.3", - "ts-pattern": "^5.5.0", + "ts-pattern": "^5.6.0", "vinxi": "^0.4.3" }, "engines": { diff --git a/src/app.css b/src/app.css index 7a72a54..e3878fa 100644 --- a/src/app.css +++ b/src/app.css @@ -150,6 +150,16 @@ code { border-radius: var(--radii-m); } +ins { + background-color: oklch(from var(--succ) l c h / .1); + color: oklch(from var(--succ) .1 .2 h); +} + +del { + background-color: oklch(from var(--fail) l c h / .1); + color: oklch(from var(--fail) .1 .2 h); +} + @property --hue { syntax: ''; inherits: false; diff --git a/src/components/filetree.tsx b/src/components/filetree.tsx index 34ec976..9515cd1 100644 --- a/src/components/filetree.tsx +++ b/src/components/filetree.tsx @@ -71,14 +71,14 @@ export const Tree: Component<{ entries: Entry[], children: readonly [(folder: Ac const _Tree: Component<{ entries: Entry[], children: readonly [(folder: Accessor) => JSX.Element, (file: Accessor) => JSX.Element] }> = (props) => { const context = useContext(TreeContext); - return { + return { entry => <> { folder => } { - file => context?.open(file().meta)}> {props.children[1](file)} + file => context?.open(file().meta)}> {props.children[1](file)} } } diff --git a/src/components/grid/grid.tsx b/src/components/grid/grid.tsx index 21beb73..64aca68 100644 --- a/src/components/grid/grid.tsx +++ b/src/components/grid/grid.tsx @@ -1,28 +1,28 @@ 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 } from "~/components/table"; +import { SelectionMode, Table, Column as TableColumn, TableApi, CellEditors, CellEditor, createDataSet, DataSet, DataSetNode } from "~/components/table"; import css from './grid.module.css'; export interface Column> extends TableColumn { - editor?: (cell: { id: keyof T, value: T[keyof T] }) => JSX.Element; + editor?: (cell: { row: number, column: keyof T, value: T[keyof T], mutate: (next: T[keyof T]) => any }) => JSX.Element; } export interface GridApi> extends TableApi { readonly mutations: Accessor; - remove(keys: string[]): void; - insert(prop: string): void; - addColumn(name: string): void; + remove(keys: number[]): void; + insert(row: T, at?: number): void; + addColumn(column: keyof T): void; } interface GridContextType> { - readonly rows: Accessor; + readonly rows: Accessor[]>; readonly mutations: Accessor; - // readonly selection: Accessor; - mutate(prop: string, value: string): void; - remove(props: string[]): void; - insert(prop: string): void; - addColumn(name: string): void; + readonly selection: TableApi['selection']; + mutate(row: number, column: K, value: T[K]): void; + remove(rows: number[]): void; + insert(row: T, at?: number): void; + addColumn(column: keyof T, value: T[keyof T]): void; } const GridContext = createContext>(); @@ -30,65 +30,68 @@ const GridContext = createContext>(); const useGrid = () => useContext(GridContext)!; type GridProps> = { class?: string, groupBy?: keyof T, columns: Column[], rows: T[], api?: (api: GridApi) => any }; +// type GridState> = { data: DataSet, columns: Column[], numberOfRows: number }; export function Grid>(props: GridProps) { const [table, setTable] = createSignal>(); - const [state, setState] = createStore<{ rows: T[], columns: Column[], snapshot: T[], numberOfRows: number }>({ - rows: [], - columns: [], - snapshot: [], - numberOfRows: 0, - }); + const data = createMemo(() => createDataSet(props.rows)); - const mutations = createMemo(() => { - // enumerate all values to make sure the memo is recalculated on any change - Object.values(state.rows).map(entry => Object.values(entry)); - - return deepDiff(state.snapshot, state.rows).toArray(); - }); - - createEffect(() => { - setState('rows', Object.fromEntries(deepCopy(props.rows).entries())); - setState('snapshot', props.rows); - }); - - createEffect(() => { - setState('columns', props.columns); - }); - - createEffect(() => { - setState('numberOfRows', Object.keys(state.rows).length); - }); - - const rows = createMemo(() => state.rows); - const columns = createMemo(() => state.columns); + const rows = createMemo(() => data().value()); + const mutations = createMemo(() => data().mutations()); + const columns = createMemo(() => props.columns); const ctx: GridContextType = { rows, mutations, - // selection, + selection: createMemo(() => table()?.selection() ?? []), - mutate(prop: string, value: string) { + mutate(row: number, column: K, value: T[K]) { + data().mutate(row, column, value); }, - remove(props: string[]) { + remove(rows: number[]) { + // setState('rows', (r) => r.filter((_, i) => rows.includes(i) === false)); }, - insert(prop: string) { + insert(row: T, at?: number) { + if (at === undefined) { + // setState('rows', state.rows.length, row); + } else { + + } }, - addColumn(id: keyof T): void { + addColumn(column: keyof T, value: T[keyof T]): void { + // setState('rows', { from: 0, to: state.rows.length - 1 }, column as any, value); }, }; - const cellEditors = createMemo(() => Object.fromEntries(state.columns.filter(c => c.editor !== undefined).map(c => [c.id, c.editor!] as const))); + const cellEditors = createMemo(() => Object.fromEntries( + props.columns + .filter(c => c.editor !== undefined) + .map(c => { + const Editor: CellEditor = ({ row, column, value }) => { + const mutate = (next: T[keyof T]) => { + console.log('KAAS', { next }) + + ctx.mutate(row, column, next); + }; + + return c.editor!({ row, column, value, mutate }); + }; + + return [c.id, Editor] as const; + }) + ) as any); return - { - cellEditors() - }
+
+ { + cellEditors() + }
+
; }; @@ -105,14 +108,14 @@ function Api>(props: { api: undefined | ((api: Gri return { ...table, mutations: gridContext.mutations, - remove(props: string[]) { - gridContext.remove(props); + remove(rows: number[]) { + gridContext.remove(rows); }, - insert(prop: string) { - gridContext.insert(prop); + insert(row: T, at?: number) { + gridContext.insert(row, at); }, - addColumn(name: string): void { - gridContext.addColumn(name); + addColumn(column: keyof T, value: T[keyof T]): void { + gridContext.addColumn(column, value); }, }; }); diff --git a/src/components/grid/index.tsx b/src/components/grid/index.tsx index bca9921..44b0d44 100644 --- a/src/components/grid/index.tsx +++ b/src/components/grid/index.tsx @@ -1,4 +1,4 @@ -export type { DataSetRowNode, DataSetGroupNode, DataSetNode, SelectionMode } from '../table'; +export type { DataSetRowNode, DataSetGroupNode, DataSetNode, SelectionMode, SortingFunction, SortOptions, GroupingFunction, GroupOptions } from '../table'; export type { GridApi, Column } from './grid'; export { Grid } from './grid'; \ No newline at end of file diff --git a/src/components/table/dataset.ts b/src/components/table/dataset.ts index 270bbaf..056f1f7 100644 --- a/src/components/table/dataset.ts +++ b/src/components/table/dataset.ts @@ -1,27 +1,115 @@ +import { Accessor, createMemo } from "solid-js"; +import { createStore, NotWrappable, StoreSetter } from "solid-js/store"; +import { snapshot } from "vinxi/dist/types/runtime/storage"; +import { deepCopy, deepDiff, Mutation } from "~/utilities"; -export type DataSetRowNode = { kind: 'row', key: string, value: T } -export type DataSetGroupNode = { kind: 'group', key: string, groupedBy: keyof T, nodes: DataSetNode[] }; -export type DataSetNode = DataSetRowNode | DataSetGroupNode; +export type DataSetRowNode = { kind: 'row', key: K, value: T } +export type DataSetGroupNode = { kind: 'group', key: K, groupedBy: keyof T, nodes: DataSetNode[] }; +export type DataSetNode = DataSetRowNode | DataSetGroupNode; -export type DataSet> = DataSetNode[]; +export interface SortingFunction { + (a: T, b: T): -1 | 0 | 1; +} +export interface SortOptions> { + by: keyof T; + reversed: boolean; + with?: SortingFunction; +} -export const createDataSet = >(data: T[]): DataSetNode[] => { - return Object.entries(data).map>(([key, value]) => ({ kind: 'row', key, value })); -}; +export interface GroupingFunction { + (nodes: DataSetRowNode[]): DataSetNode[]; +} +export interface GroupOptions> { + by: keyof T; + with: GroupingFunction; +} +interface DataSetState> { + value: DataSetRowNode[]; + snapshot: DataSetRowNode[]; + sorting?: SortOptions; + grouping?: GroupOptions; +} -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])); +export interface DataSet> { + data: T[]; + value: Accessor[]>; + mutations: Accessor; + sort: Accessor | undefined>; - if (sort.reversed) { - sorted.reverse(); - } + // mutate(index: number, value: T): void; + mutate(index: number, prop: K, value: T[K]): void; - return sorted; -}; + setSorting(options: SortOptions | undefined): void; + setGrouping(options: GroupOptions | undefined): void; +} -type GroupingFunction = (nodes: DataSetRowNode[]) => DataSetNode[]; -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 +const defaultComparer = (a: T, b: T) => a < b ? -1 : a > b ? 1 : 0; +function defaultGroupingFunction(groupBy: keyof T): GroupingFunction { + return (nodes: DataSetRowNode[]): DataSetNode[] => Object.entries(Object.groupBy(nodes, r => r.value[groupBy] as PropertyKey)) + .map(([key, nodes]) => ({ kind: 'group', key, groupedBy: groupBy, nodes: nodes! } as DataSetGroupNode)); +} + +export const createDataSet = >(data: T[]): DataSet => { + const nodes = data.map>((value, key) => ({ kind: 'row', key: key as keyof T, value })); + + const [state, setState] = createStore>({ + value: deepCopy(nodes), + snapshot: nodes, + sorting: undefined, + grouping: undefined, + }); + + const value = createMemo(() => { + const sorting = state.sorting; + const grouping = state.grouping; + + let value = state.value as DataSetNode[]; + + if (sorting) { + const comparer = sorting.with ?? defaultComparer; + + value = value.filter(entry => entry.kind === 'row').toSorted((a, b) => comparer(a.value[sorting.by], b.value[sorting.by])); + + if (sorting.reversed) { + value.reverse(); + } + } + + if (grouping) { + const implementation = grouping.with ?? defaultGroupingFunction(grouping.by); + + value = implementation(value as DataSetRowNode[]); + } + + return value; + }); + + 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)); + + return deepDiff(state.snapshot, state.value).toArray(); + }); + const sort = createMemo(() => state.sorting); + + return { + data, + value, + mutations, + sort, + + mutate(index, prop, value) { + console.log({ index, prop, value }); + // setState('value', index, 'value', prop as any, value); + }, + + setSorting(options) { + setState('sorting', options); + }, + + setGrouping(options) { + setState('grouping', options) + }, + }; +}; \ No newline at end of file diff --git a/src/components/table/index.tsx b/src/components/table/index.tsx index eaa7d79..d575f94 100644 --- a/src/components/table/index.tsx +++ b/src/components/table/index.tsx @@ -1,5 +1,5 @@ -export type { Column, TableApi } from './table'; -export type { DataSet, DataSetGroupNode, DataSetRowNode, DataSetNode } from './dataset'; +export type { Column, TableApi, CellEditor, CellEditors } from './table'; +export type { DataSet, DataSetGroupNode, DataSetRowNode, DataSetNode, SortingFunction, SortOptions, GroupingFunction, GroupOptions } from './dataset'; export { SelectionMode, Table } from './table'; -export { createDataSet, toSorted, toGrouped } from './dataset'; \ No newline at end of file +export { createDataSet } from './dataset'; \ No newline at end of file diff --git a/src/components/table/table.tsx b/src/components/table/table.tsx index 76ee0bb..bb035bd 100644 --- a/src/components/table/table.tsx +++ b/src/components/table/table.tsx @@ -1,50 +1,42 @@ import { Accessor, createContext, createEffect, createMemo, createSignal, For, JSX, Match, Show, Switch, useContext } from "solid-js"; -import { selectable, SelectionProvider, useSelection } from "~/features/selectable"; -import { DataSetRowNode, DataSetGroupNode, DataSetNode, createDataSet, toSorted, toGrouped } from './dataset'; -import { createStore } from "solid-js/store"; +import { selectable, SelectionItem, SelectionProvider, useSelection } from "~/features/selectable"; +import { DataSetRowNode, DataSetNode, DataSet } from './dataset'; import { FaSolidSort, FaSolidSortDown, FaSolidSortUp } from "solid-icons/fa"; import css from './table.module.css'; -selectable +selectable; export type Column = { id: keyof T, label: string, sortable?: boolean, group?: string, - readonly groupBy?: (rows: DataSetRowNode[]) => DataSetNode[], + readonly groupBy?: (rows: DataSetRowNode[]) => DataSetNode[], }; -type SelectionItem = { key: string, value: Accessor, element: WeakRef }; -export type CellEditors> = { [K in keyof T]?: (cell: { value: T[K] }) => JSX.Element }; +export type CellEditor, K extends keyof T> = (cell: { row: number, column: K, value: T[K] }) => JSX.Element; +export type CellEditors> = { [K in keyof T]?: CellEditor }; export interface TableApi> { - readonly selection: Accessor[]>; - readonly rows: Accessor; + readonly selection: Accessor[]>; + readonly rows: Accessor>; readonly columns: Accessor[]>; selectAll(): void; clear(): void; } -const TableContext = createContext<{ - readonly rows: Accessor, - readonly columns: Accessor[]>, - readonly selection: Accessor, +interface TableContextType> { + readonly rows: Accessor>, + readonly columns: Accessor[]>, + readonly selection: Accessor[]>, readonly selectionMode: Accessor, - readonly groupBy: Accessor, - readonly sort: Accessor<{ by: string, reversed?: boolean } | undefined>, - readonly cellRenderers: Accessor>, - - setSort(setter: (current: { by: string, reversed?: boolean } | undefined) => { by: string, reversed: boolean } | undefined): void; -}>(); - -const useTable = () => useContext(TableContext)! - -function defaultGroupingFunction(groupBy: keyof T) { - return (nodes: DataSetRowNode[]): DataSetNode[] => Object.entries(Object.groupBy>(nodes, r => r.value[groupBy])) - .map>(([key, nodes]) => ({ kind: 'group', key, groupedBy: groupBy, nodes: nodes! })); + readonly cellRenderers: Accessor>, } +const TableContext = createContext>(); + +const useTable = >() => useContext(TableContext)! as TableContextType + export enum SelectionMode { None, Single, @@ -53,46 +45,27 @@ export enum SelectionMode { type TableProps> = { class?: string, summary?: string, - rows: T[], + rows: DataSet, columns: Column[], - groupBy?: keyof T, - sort?: { - by: keyof T, - reversed?: boolean, - }, selectionMode?: SelectionMode, children?: CellEditors, api?: (api: TableApi) => any, }; export function Table>(props: TableProps) { - const [selection, setSelection] = createSignal([]); - const [state, setState] = createStore({ - sort: props.sort ? { by: props.sort.by as string, reversed: props.sort.reversed } : undefined, - }); + const [selection, setSelection] = createSignal[]>([]); - createEffect(() => { - setState('sort', props.sort ? { by: props.sort.by as string, reversed: props.sort.reversed } : undefined); - }); - - const rows = createMemo(() => props.rows ?? []); + const rows = createMemo(() => props.rows); const columns = createMemo[]>(() => props.columns ?? []); const selectionMode = createMemo(() => props.selectionMode ?? SelectionMode.None); - const groupBy = createMemo(() => props.groupBy as string | undefined); const cellRenderers = createMemo>(() => props.children ?? {}); - const context = { + const context: TableContextType = { rows, columns, selection, selectionMode, - groupBy, - sort: createMemo(() => state.sort), cellRenderers, - - setSort(setter: (current: { by: string, reversed?: boolean } | undefined) => { by: string, reversed: boolean } | undefined) { - setState('sort', setter); - }, }; return @@ -104,30 +77,13 @@ export function Table>(props: TableProps) { ; }; -type InnerTableProps> = { class?: string, summary?: string, rows: T[] }; +type InnerTableProps> = { class?: string, summary?: string, rows: DataSet }; function InnerTable>(props: InnerTableProps) { - const table = useTable(); + const table = useTable(); const selectable = createMemo(() => table.selectionMode() !== SelectionMode.None); const columnCount = createMemo(() => table.columns().length); - const nodes = createMemo[]>(() => { - const columns = table.columns(); - const groupBy = table.groupBy(); - const sort = table.sort(); - - let dataset = createDataSet(props.rows); - - if (sort) { - dataset = toSorted(dataset, { by: sort.by, reversed: sort.reversed ?? false, with: (a, b) => a < b ? -1 : a > b ? 1 : 0 }) - } - - if (groupBy) { - dataset = toGrouped(dataset, { by: groupBy, with: columns.find(({ id }) => id === groupBy)?.groupBy ?? defaultGroupingFunction(groupBy) }); - } - - return dataset; - }); return { @@ -138,7 +94,7 @@ function InnerTable>(props: InnerTableProps) { - { + { node => } @@ -154,13 +110,11 @@ function InnerTable>(props: InnerTableProps) { }; function Api>(props: { api: undefined | ((api: TableApi) => any) }) { - const table = useTable(); - const selectionContext = useSelection>(); + const table = useTable(); + const selectionContext = useSelection(); const api: TableApi = { - selection: createMemo(() => { - return selectionContext.selection(); - }), + selection: selectionContext.selection, rows: table.rows, columns: table.columns, selectAll() { @@ -209,7 +163,7 @@ function Head(props: {}) { { ({ id, label, sortable }) => { - const sort = createMemo(() => table.sort()); + const sort = createMemo(() => table.rows().sort()); const by = String(id); const onPointerDown = (e: PointerEvent) => { @@ -245,7 +199,7 @@ function Head(props: {}) { ; }; -function Node>(props: { node: DataSetNode, depth: number, groupedBy?: keyof T }) { +function Node>(props: { node: DataSetNode, depth: number, groupedBy?: keyof T }) { return { row => @@ -257,11 +211,11 @@ function Node>(props: { node: DataSetNode, dept ; } -function Row>(props: { key: string, value: T, depth: number, groupedBy?: keyof T }) { - const table = useTable(); - const context = useSelection(); +function Row>(props: { key: keyof T, value: T, depth: number, groupedBy?: keyof T }) { + const table = useTable(); + const context = useSelection(); + const columns = table.columns; - const values = createMemo(() => Object.entries(props.value)); const isSelected = context.isSelected(props.key); return @@ -271,17 +225,17 @@ function Row>(props: { key: string, value: T, dept - { - ([k, value]) => + { + ({ id }) => }; }; -function Group>(props: { key: string, groupedBy: keyof T, nodes: DataSetNode[], depth: number }) { +function Group>(props: { key: keyof T, groupedBy: keyof T, nodes: DataSetNode[], depth: number }) { const table = useTable(); return
- {props.key} + {String(props.key)} { node => diff --git a/src/entry-server.tsx b/src/entry-server.tsx index 51b314e..f56257b 100644 --- a/src/entry-server.tsx +++ b/src/entry-server.tsx @@ -36,7 +36,7 @@ export default createHandler(({ nonce }) => { // style: `${base} data: `, } as const; - event.response.headers.append('Content-Security-Policy', Object.entries(policies).map(([p, v]) => `${p}-src ${v}`).join('; ')) + // event.response.headers.append('Content-Security-Policy', Object.entries(policies).map(([p, v]) => `${p}-src ${v}`).join('; ')) return { nonce }; }); diff --git a/src/features/selectable/index.tsx b/src/features/selectable/index.tsx index 458434b..ae54a29 100644 --- a/src/features/selectable/index.tsx +++ b/src/features/selectable/index.tsx @@ -1,4 +1,4 @@ -import { Accessor, children, createContext, createEffect, createMemo, createRenderEffect, createSignal, createUniqueId, onCleanup, onMount, ParentComponent, Setter, Signal, useContext } from "solid-js"; +import { Accessor, children, createContext, createEffect, createMemo, createRenderEffect, createSignal, createUniqueId, onCleanup, onMount, ParentComponent, ParentProps, Setter, Signal, useContext } from "solid-js"; import { createStore } from "solid-js/store"; import { isServer } from "solid-js/web"; import css from "./index.module.css"; @@ -16,26 +16,33 @@ enum SelectionMode { Toggle, } -export interface SelectionContextType { - readonly selection: Accessor; +export interface SelectionItem { + key: K; + value: Accessor; + element: WeakRef; +}; + +export interface SelectionContextType { + readonly selection: Accessor[]>; readonly length: Accessor; - select(selection: string[], options?: Partial<{ mode: SelectionMode }>): void; + select(selection: (keyof T)[], options?: Partial<{ mode: SelectionMode }>): void; selectAll(): void; clear(): void; - isSelected(key: string): Accessor; + isSelected(key: keyof T): Accessor; } -interface InternalSelectionContextType { +interface InternalSelectionContextType { readonly latest: Signal, readonly modifier: Signal, readonly selectables: Signal, - add(key: string, value: object, element: HTMLElement): void; + readonly keyMap: Map, + add(key: keyof T, value: Accessor, element: HTMLElement): string; } -export interface SelectionHandler { +export interface SelectionHandler { (selection: T[]): any; } -const SelectionContext = createContext(); -const InternalSelectionContext = createContext(); +const SelectionContext = createContext>(); +const InternalSelectionContext = createContext>(); export function useSelection(): SelectionContextType { const context = useContext(SelectionContext); @@ -46,15 +53,17 @@ export function useSelection(): SelectionContextType< return context as SelectionContextType; }; -const useInternalSelection = () => useContext(InternalSelectionContext)!; - -interface State { - selection: string[]; - data: { key: string, value: Accessor, element: WeakRef }[]; +function useInternalSelection() { + return useContext(InternalSelectionContext)! as InternalSelectionContextType; } -export const SelectionProvider: ParentComponent<{ selection?: SelectionHandler, multiSelect?: boolean }> = (props) => { - const [state, setState] = createStore({ selection: [], data: [] }); +interface State { + selection: (keyof T)[]; + data: SelectionItem[]; +} + +export function SelectionProvider(props: ParentProps<{ selection?: SelectionHandler, multiSelect?: boolean }>) { + const [state, setState] = createStore>({ selection: [], data: [] }); const selection = createMemo(() => state.data.filter(({ key }) => state.selection.includes(key))); const length = createMemo(() => state.data.length); @@ -62,7 +71,7 @@ export const SelectionProvider: ParentComponent<{ selection?: SelectionHandler, props.selection?.(selection().map(({ value }) => value())); }); - const context: SelectionContextType = { + const context: SelectionContextType = { selection, length, select(selection, { mode = SelectionMode.Normal } = {}) { @@ -92,17 +101,29 @@ export const SelectionProvider: ParentComponent<{ selection?: SelectionHandler, clear() { setState('selection', []); }, - isSelected(key: string) { + isSelected(key) { return createMemo(() => state.selection.includes(key)); }, }; - const internal: InternalSelectionContextType = { + const keyIdMap = new Map(); + const idKeyMap = new Map(); + const internal: InternalSelectionContextType = { modifier: createSignal(Modifier.None), latest: createSignal(), selectables: createSignal([]), - add(key: string, value: Accessor, element: HTMLElement) { - setState('data', data => [...data, { key, value, element: new WeakRef(element) }]); + keyMap: idKeyMap, + add(key, value, element) { + if (keyIdMap.has(key) === false) { + const id = createUniqueId(); + + keyIdMap.set(key, id); + idKeyMap.set(id, key); + } + + setState('data', state.data.length, { key, value, element: new WeakRef(element) }); + + return keyIdMap.get(key)!; }, }; @@ -180,31 +201,31 @@ const Root: ParentComponent = (props) => { return
{c()}
; }; -export const selectable = (element: HTMLElement, options: Accessor<{ value: object, key?: string }>) => { - const context = useSelection(); - const internal = useInternalSelection(); +export function selectable(element: HTMLElement, options: Accessor<{ value: T, key: keyof T }>) { + const context = useSelection(); + const internal = useInternalSelection(); - const key = options().key ?? createUniqueId(); + const key = options().key; const value = createMemo(() => options().value); const isSelected = context.isSelected(key); - internal.add(key, value, element); + const selectionKey = internal.add(key, value, element); - const createRange = (a?: HTMLElement, b?: HTMLElement): string[] => { + const createRange = (a?: HTMLElement, b?: HTMLElement): (keyof T)[] => { if (!a && !b) { return []; } if (!a) { - return [b!.dataset.selecatableKey!]; + return [b!.dataset.selecatableKey! as keyof T]; } if (!b) { - return [a!.dataset.selecatableKey!]; + return [a!.dataset.selecatableKey! as keyof T]; } if (a === b) { - return [a!.dataset.selecatableKey!]; + return [a!.dataset.selecatableKey! as keyof T]; } const nodes = internal.selectables[0](); @@ -212,7 +233,7 @@ export const selectable = (element: HTMLElement, options: Accessor<{ value: obje const bIndex = nodes.indexOf(b); const selection = nodes.slice(Math.min(aIndex, bIndex), Math.max(aIndex, bIndex) + 1); - return selection.map(n => n.dataset.selectionKey!); + return selection.map(n => internal.keyMap.get(n.dataset.selecatableKey!)!); }; createRenderEffect(() => { @@ -256,13 +277,13 @@ export const selectable = (element: HTMLElement, options: Accessor<{ value: obje }); element.classList.add(css.selectable); - element.dataset.selectionKey = key; + element.dataset.selectionKey = selectionKey; }; declare module "solid-js" { namespace JSX { interface Directives { - selectable: { value: object, key?: string }; + selectable: { value: object, key: any }; } } } \ No newline at end of file diff --git a/src/global.d.ts b/src/global.d.ts index 41957d9..82db5e8 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -4,10 +4,10 @@ interface FileSystemHandle { getUniqueId(): Promise; } -declare module "solid-js" { - namespace JSX { - interface InputHTMLAttributes extends HTMLAttributes { - indeterminate?: boolean; - } - } -} \ No newline at end of file +// declare module "solid-js" { +// namespace JSX { +// interface InputHTMLAttributes extends HTMLAttributes { +// indeterminate?: boolean; +// } +// } +// } \ No newline at end of file diff --git a/src/routes/(editor).tsx b/src/routes/(editor).tsx index faa92bf..b607078 100644 --- a/src/routes/(editor).tsx +++ b/src/routes/(editor).tsx @@ -92,11 +92,15 @@ export default function Editor(props: ParentProps) {
- }> + + {props.children} + + + {/* }> {props.children} - + */}
@@ -112,6 +116,8 @@ const ErrorComp: Component<{ error: Error }> = (props) => { cause => <>{cause().description} } + {props.error.stack} + Return to start ; }; diff --git a/src/routes/(editor)/experimental/grid.module.css b/src/routes/(editor)/experimental/grid.module.css index 40f3bde..0243457 100644 --- a/src/routes/(editor)/experimental/grid.module.css +++ b/src/routes/(editor)/experimental/grid.module.css @@ -8,6 +8,8 @@ z-index: 1; padding: var(--padding-xl); background-color: var(--surface-300); + max-inline-size: 25vw; + overflow: auto; & > ul { padding: 0; diff --git a/src/routes/(editor)/experimental/grid.tsx b/src/routes/(editor)/experimental/grid.tsx index 05fac16..c7c882d 100644 --- a/src/routes/(editor)/experimental/grid.tsx +++ b/src/routes/(editor)/experimental/grid.tsx @@ -1,8 +1,10 @@ import { Sidebar } from '~/components/sidebar'; import { 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 { createDataSet, Table } from '~/components/table'; import css from './grid.module.css'; -import { createMemo, createSignal } from 'solid-js'; export default function GridExperiment() { const columns: Column[] = [ @@ -27,7 +29,10 @@ export default function GridExperiment() { id: 'email', label: 'Email', sortable: true, - editor: ({ value }) => , + editor: ({ value, mutate }) => { + console.log('WHAAAAT????', e); + return mutate(e.target.value.trim()); + }, 100)} />, }, { id: 'address', @@ -55,13 +60,46 @@ export default function GridExperiment() { const mutations = createMemo(() => api()?.mutations() ?? []) + // createEffect(() => { + // console.log(mutations()); + // }); + return
- - {mutations().length} + +
+ Commands + + + +
+ +
+ Selection ({api()?.selection().length}) + +
{JSON.stringify(api()?.selection().map(i => i.key))}
+
+ +
+ Mutations ({mutations().length}) + + +
; -} \ No newline at end of file +} + +type M = { kind: MutarionKind, key: string, original?: any, value?: any }; +const Mutations: Component<{ mutations: Mutation[] }> = (props) => { + const columns: Column[] = [{ id: 'key', label: 'Key' }, { id: 'original', label: 'original' }, { id: 'value', label: 'Value' }]; + + const rows = createMemo(() => createDataSet(props.mutations)); + + return
{table.cellRenderers()[k]?.({ value }) ?? value}{table.cellRenderers()[id]?.({ row: props.key as number, column: id, value: props.value[id] }) ?? props.value[id]}
{{ + original: ({ value }) => {value}, + value: ({ value }) => {value}, + }}
+}; \ No newline at end of file diff --git a/src/routes/(editor)/experimental/table.tsx b/src/routes/(editor)/experimental/table.tsx index a7c81a3..e36c89e 100644 --- a/src/routes/(editor)/experimental/table.tsx +++ b/src/routes/(editor)/experimental/table.tsx @@ -1,19 +1,20 @@ import { Sidebar } from '~/components/sidebar'; -import css from './table.module.css'; -import { Column, DataSetGroupNode, DataSetNode, DataSetRowNode, SelectionMode, Table } from '~/components/table'; +import { Column, createDataSet, DataSetGroupNode, DataSetNode, DataSetRowNode, GroupOptions, SelectionMode, SortOptions, Table } from '~/components/table'; import { createStore } from 'solid-js/store'; import { Person, people } from './experimental.data'; +import css from './table.module.css'; +import { createEffect, createMemo, For } from 'solid-js'; export default function TableExperiment() { const columns: Column[] = [ { id: 'id', label: '#', - groupBy(rows: DataSetRowNode[]) { - const group = (nodes: (DataSetRowNode & { _key: string })[]): DataSetNode[] => nodes.every(n => n._key.includes('.') === false) + groupBy(rows: DataSetRowNode[]) { + const group = (nodes: (DataSetRowNode & { _key: string })[]): DataSetNode[] => 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: 'id', nodes: group(nodes!.map(n => ({ ...n, _key: n._key.slice(key.length + 1) }))) })); + .map>(([key, nodes]) => ({ kind: 'group', key, groupedBy: 'id', nodes: group(nodes!.map(n => ({ ...n, _key: n._key.slice(key.length + 1) }))) })); return group(rows.map(row => ({ ...row, _key: row.value.id }))); }, @@ -50,10 +51,20 @@ export default function TableExperiment() { }, ]; - const [store, setStore] = createStore<{ selectionMode: SelectionMode, groupBy?: keyof Person, sort?: { by: keyof Person, reversed?: boolean } }>({ + const [store, setStore] = createStore<{ selectionMode: SelectionMode, group?: GroupOptions, sort?: SortOptions }>({ selectionMode: SelectionMode.None, - // groupBy: 'value', - // sortBy: 'key' + group: undefined, + sort: undefined, + }); + + const rows = createMemo(() => createDataSet(people)); + + createEffect(() => { + rows().setGrouping(store.group); + }); + + createEffect(() => { + rows().setSorting(store.sort); }); return
@@ -74,7 +85,7 @@ export default function TableExperiment() {