import { Accessor, Component, createContext, createEffect, createMemo, createSignal, For, ParentComponent, Show, useContext } from "solid-js"; import { createStore, produce, unwrap } from "solid-js/store"; import { SelectionProvider, useSelection, selectable } from "../features/selectable"; import { debounce, deepCopy, deepDiff, Mutation } from "~/utilities"; import css from './grid.module.css'; selectable // prevents removal of import type Rows = Map>; type SelectionItem = { key: string, value: Accessor>, element: WeakRef }; type Insertion = { kind: 'row', key: string } | { kind: 'column', value: string }; export interface GridContextType { readonly rows: Accessor>>; readonly mutations: Accessor; readonly selection: Accessor; mutate(prop: string, lang: string, value: string): void; remove(props: string[]): void; insert(insertion: Insertion): void; } export interface GridApi { readonly selection: Accessor>>; readonly rows: Accessor>>; readonly mutations: Accessor; selectAll(): void; clear(): void; remove(keys: string[]): void; insert(insertion: Insertion): void; } const GridContext = createContext(); const useGrid = () => useContext(GridContext)!; export const Grid: Component<{ class?: string, columns: string[], rows: Rows, api?: (api: GridApi) => any }> = (props) => { const [selection, setSelection] = createSignal([]); const [state, setState] = createStore<{ rows: Record>, columns: string[], snapshot: Rows, numberOfRows: number }>({ rows: {}, columns: [], snapshot: new Map, numberOfRows: 0, }); 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(); }); const rows = createMemo(() => Object.fromEntries(Object.entries(state.rows).map(([key, row]) => [key, unwrap(row)] as const))); const columns = createMemo(() => state.columns); 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 ctx: GridContextType = { rows, mutations, selection, mutate(prop: string, lang: string, value: string) { setState('rows', prop, lang, value); }, remove(props: string[]) { setState('rows', produce(rows => { for (const prop of props) { delete rows[prop]; } return rows; })); }, insert(prop: string) { setState('rows', produce(rows => { rows[prop] = Object.fromEntries(state.columns.slice(1).map(lang => [lang, ''])); return rows })) }, addColumn(name: string): void { setState(produce(state => { state.columns.push(name); state.rows = Object.fromEntries(Object.entries(state.rows).map(([key, row]) => [key, { ...row, [name]: '' }])); return state; })) }, }; return <_Grid class={props.class} columns={columns()} rows={rows()} /> ; }; const _Grid: Component<{ class?: string, columns: string[], rows: Record> }> = (props) => { const columnCount = createMemo(() => props.columns.length - 1); const root = createMemo(() => Object.entries(props.rows) .reduce((aggregate, [key, value]) => { let obj: any = aggregate; const parts = key.split('.'); for (const [i, part] of parts.entries()) { if (Object.hasOwn(obj, part) === false) { obj[part] = {}; } if (i === (parts.length - 1)) { obj[part] = value; } else { obj = obj[part]; } } return aggregate; }, {})); return
}; const Api: Component<{ api: undefined | ((api: GridApi) => any) }> = (props) => { const gridContext = useGrid(); const selectionContext = useSelection<{ key: string, value: Accessor>, element: WeakRef }>(); const api: GridApi = { selection: createMemo(() => { const selection = selectionContext.selection(); return Object.fromEntries(selection.map(({ key, value }) => [key, value()] as const)); }), rows: gridContext.rows, mutations: gridContext.mutations, selectAll() { selectionContext.selectAll(); }, clear() { selectionContext.clear(); }, remove(props: string[]) { gridContext.remove(props); }, insert(prop: string) { gridContext.insert(prop); }, addColumn(name: string): void { gridContext.addColumn(name); }, }; createEffect(() => { props.api?.(api); }); return null; }; const Head: Component<{ headers: string[] }> = (props) => { const context = useSelection(); return
0 && context.selection().length === context.length()} indeterminate={context.selection().length !== 0 && context.selection().length !== context.length()} on:input={(e: InputEvent) => e.target.checked ? context.selectAll() : context.clear()} />
{ header => {header} }
; }; const Row: Component<{ entry: Entry, path?: string[] }> = (props) => { const grid = useGrid(); return { ([key, value]) => { const values = Object.entries(value); const path = [...(props.path ?? []), key]; const k = path.join('.'); const context = useSelection(); const isSelected = context.isSelected(k); return }>
context.select([k])} on:pointerdown={e => e.stopPropagation()} />
{key}
{ ([lang, value]) =>