finished refactoring. all the code is now way more clearly seperated and reusable
This commit is contained in:
parent
c2b7a9ccf3
commit
998a788baa
13 changed files with 141 additions and 231 deletions
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue