finished refactoring. all the code is now way more clearly seperated and reusable

This commit is contained in:
Chris Kruining 2024-12-19 11:42:03 +01:00
parent c2b7a9ccf3
commit 998a788baa
No known key found for this signature in database
GPG key ID: EB894A3560CCCAD2
13 changed files with 141 additions and 231 deletions

View file

@ -0,0 +1,19 @@
.textarea {
resize: vertical;
min-block-size: max(2em, 100%);
max-block-size: 50em;
background-color: var(--surface-600);
color: var(--text-1);
border-color: var(--text-2);
border-radius: var(--radii-s);
&:has(::spelling-error, ::grammar-error) {
border-color: var(--fail);
}
& ::spelling-error {
outline: 1px solid var(--fail);
text-decoration: yellow underline;
}
}

View file

@ -1,185 +1,70 @@
import { Accessor, Component, createContext, createEffect, createMemo, createSignal, For, ParentComponent, Show, useContext } from "solid-js";
import { createStore, produce, unwrap } from "solid-js/store";
import { debounce, deepCopy, deepDiff, Mutation, splitAt } from "~/utilities";
import { DataSetRowNode, DataSetNode, SelectionMode, Table } from "~/components/table";
import css from './grid.module.css';
type Rows = Map<string, Record<string, string>>;
export interface GridContextType {
readonly rows: Accessor<Rows>;
readonly mutations: Accessor<Mutation[]>;
// readonly selection: Accessor<SelectionItem[]>;
mutate(prop: string, value: string): void;
remove(props: string[]): void;
insert(prop: string): void;
addColumn(name: string): void;
}
import { Accessor, Component, createEffect, createMemo, createSignal } from "solid-js";
import { debounce, Mutation } from "~/utilities";
import { Column, GridApi as GridCompApi, Grid as GridComp } from "~/components/grid";
import { createDataSet, DataSetNode, DataSetRowNode } from "~/components/table";
import css from "./grid.module.css"
export type Entry = { key: string } & { [lang: string]: string };
export interface GridApi {
readonly selection: Accessor<Record<string, Record<string, string>>>;
readonly rows: Accessor<Record<string, Record<string, string>>>;
readonly mutations: Accessor<Mutation[]>;
selectAll(): void;
clear(): void;
remove(keys: string[]): void;
insert(prop: string): void;
addColumn(name: string): void;
remove(indices: number[]): void;
addKey(key: string): void;
addLocale(locale: string): void;
};
const groupBy = (rows: DataSetRowNode<number, Entry>[]) => {
type R = DataSetRowNode<number, Entry> & { _key: string };
const group = (nodes: R[]): DataSetNode<number, Entry>[] => Object
.entries(Object.groupBy(nodes, r => r._key.split('.').at(0)!) as Record<number, R[]>)
.map<any>(([key, nodes]) => nodes.at(0)?._key === key
? nodes[0]
: ({ kind: 'group', key, groupedBy: 'key', nodes: group(nodes.map(n => ({ ...n, _key: n._key.slice(key.length + 1) }))) })
);
return group(rows.map<R>(r => ({ ...r, _key: r.value.key }))) as any;
}
const GridContext = createContext<GridContextType>();
export function Grid(props: { class?: string, rows: Entry[], api?: (api: GridApi) => any }) {
const rows = createMemo(() => createDataSet<Entry>(props.rows, { group: { by: 'key', with: groupBy } }));
const locales = createMemo(() => Object.keys(rows().value().at(0) ?? {}).filter(k => k !== 'key'));
const columns = createMemo<Column<Entry>[]>(() => [
{
id: 'key',
label: 'Key',
renderer: ({ value }) => value.split('.').at(-1),
},
...locales().map<Column<Entry>>(lang => ({
id: lang,
label: lang,
renderer: ({ row, column, value, mutate }) => {
const entry = rows().value()[row]!;
const useGrid = () => useContext(GridContext)!;
export const Grid: Component<{ class?: string, columns: string[], rows: Rows, api?: (api: GridApi) => any }> = (props) => {
const [table, setTable] = createSignal();
const [state, setState] = createStore<{ rows: Record<string, Record<string, string>>, 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();
});
type Entry = { key: string, [lang: string]: string };
const groupBy = (rows: DataSetRowNode<Entry>[]) => {
const group = (nodes: DataSetRowNode<Entry>[]): DataSetNode<Entry>[] => Object
.entries(Object.groupBy(nodes, r => r.key.split('.').at(0)!) as Record<string, DataSetRowNode<Entry>[]>)
.map<DataSetNode<Entry>>(([key, nodes]) => nodes.at(0)?.key === key
? { ...nodes[0], key: nodes[0].value.key, value: { ...nodes[0].value, key: nodes[0].key } }
: ({ kind: 'group', key, groupedBy: 'key', nodes: group(nodes.map(n => ({ ...n, key: n.key.slice(key.length + 1) }))) })
);
return group(rows.map(r => ({ ...r, key: r.value.key })));
}
const rows = createMemo(() => Object.entries(state.rows).map(([key, values]) => ({ key, ...values })));
const columns = createMemo(() => [
{ id: 'key', label: 'Key', groupBy },
...state.columns.map(c => ({ id: c, label: c })),
return <TextArea row={row} key={entry.key} lang={String(column)} value={value} oninput={e => mutate(e.data ?? '')} />;
},
}))
]);
createEffect(() => {
setState('rows', Object.fromEntries(deepCopy(props.rows).entries()));
setState('snapshot', props.rows);
const r = rows();
props.api?.({
mutations: r.mutations,
remove: r.remove,
addKey(key) {
r.insert({ key, ...Object.fromEntries(locales().map(l => [l, ''])) });
},
addLocale(locale) {
r.mutateEach(entry => ({ ...entry, [locale]: '' }));
},
});
});
createEffect(() => {
setState('columns', [...props.columns]);
});
createEffect(() => {
setState('numberOfRows', Object.keys(state.rows).length);
});
const ctx: GridContextType = {
rows,
mutations,
// selection,
mutate(prop: string, value: string) {
const [key, lang] = splitAt(prop, prop.lastIndexOf('.'));
setState('rows', key, lang, value);
},
remove(props: string[]) {
setState('rows', produce(rows => {
for (const prop of props) {
delete rows[prop];
}
return rows;
}));
},
insert(prop: string) {
setState('rows', prop, Object.fromEntries(state.columns.map(lang => [lang, ''])));
},
addColumn(name: string): void {
if (state.columns.includes(name)) {
return;
}
setState(produce(state => {
state.columns.push(name);
state.rows = Object.fromEntries(Object.entries(state.rows).map(([key, row]) => [key, { ...row, [name]: '' }]));
return state;
}))
},
};
return <GridContext.Provider value={ctx}>
<Api api={props.api} table={table()} />
<Table api={setTable} class={props.class} rows={rows()} columns={columns()} groupBy="key" selectionMode={SelectionMode.Multiple}>{
Object.fromEntries(state.columns.map(c => [c, ({ key, value }: any) => {
return <TextArea key={key} value={value} oninput={(e) => ctx.mutate(key, e.data ?? '')} />;
}]))
}</Table>
</GridContext.Provider>;
return <GridComp rows={rows()} columns={columns()} />;
};
const Api: Component<{ api: undefined | ((api: GridApi) => any), table?: any }> = (props) => {
const gridContext = useGrid();
const api = createMemo<GridApi | undefined>(() => {
const table = props.table;
if (!table) {
return;
}
return {
selection: createMemo(() => {
const selection = props.table?.selection() ?? [];
return Object.fromEntries(selection.map(({ key, value }) => [key, value()] as const));
}),
rows: createMemo(() => props.table?.rows ?? []),
mutations: gridContext.mutations,
selectAll() {
props.table.selectAll();
},
clear() {
props.table.clear();
},
remove(props: string[]) {
gridContext.remove(props);
},
insert(prop: string) {
gridContext.insert(prop);
},
addColumn(name: string): void {
gridContext.addColumn(name);
},
};
});
createEffect(() => {
const value = api();
if (value) {
props.api?.(value);
}
});
return null;
};
const TextArea: Component<{ key: string, value: string, oninput?: (event: InputEvent) => any }> = (props) => {
const TextArea: Component<{ row: number, key: string, lang: string, value: string, oninput?: (event: InputEvent) => any }> = (props) => {
const [element, setElement] = createSignal<HTMLTextAreaElement>();
const key = createMemo(() => props.key.slice(0, props.key.lastIndexOf('.')));
const lang = createMemo(() => props.key.slice(props.key.lastIndexOf('.') + 1));
const resize = () => {
const el = element();
@ -205,10 +90,11 @@ const TextArea: Component<{ key: string, value: string, oninput?: (event: InputE
return <textarea
ref={setElement}
class={css.textarea}
value={props.value}
lang={lang()}
placeholder={`${key()} in ${lang()}`}
name={`${key()}:${lang()}`}
lang={props.lang}
placeholder={`${props.key} in ${props.lang}`}
name={`${props.row}[${props.lang}]`}
spellcheck={true}
wrap="soft"
onkeyup={onKeyUp}