couple of bug fixes

This commit is contained in:
Chris Kruining 2024-12-19 15:54:11 +01:00
parent f6af76f0ba
commit c33e99b105
No known key found for this signature in database
GPG key ID: EB894A3560CCCAD2
5 changed files with 53 additions and 45 deletions

View file

@ -32,13 +32,12 @@ const GridContext = createContext<GridContextType<any>>();
const useGrid = () => useContext(GridContext)!; const useGrid = () => useContext(GridContext)!;
type GridProps<T extends Record<string, any>> = { class?: string, groupBy?: keyof T, columns: Column<T>[], rows: DataSet<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>) { export function Grid<T extends Record<string, any>>(props: GridProps<T>) {
const [table, setTable] = createSignal<TableApi<T>>(); const [table, setTable] = createSignal<TableApi<T>>();
const rows = createMemo(() => props.rows); const rows = createMemo(() => props.rows);
const columns = createMemo(() => props.columns); const columns = createMemo(() => props.columns as TableColumn<T>[]);
const mutations = createMemo(() => rows().mutations()); const mutations = createMemo(() => rows().mutations());
const ctx: GridContextType<T> = { const ctx: GridContextType<T> = {

View file

@ -19,7 +19,7 @@ export interface Column<T extends Record<string, any>> {
}; };
export interface TableApi<T extends Record<string, any>> { export interface TableApi<T extends Record<string, any>> {
readonly selection: Accessor<SelectionItem<keyof T, T>[]>; readonly selection: Accessor<SelectionItem<number, T>[]>;
readonly rows: Accessor<DataSet<T>>; readonly rows: Accessor<DataSet<T>>;
readonly columns: Accessor<Column<T>[]>; readonly columns: Accessor<Column<T>[]>;
selectAll(): void; selectAll(): void;
@ -29,7 +29,7 @@ export interface TableApi<T extends Record<string, any>> {
interface TableContextType<T extends Record<string, any>> { interface TableContextType<T extends Record<string, any>> {
readonly rows: Accessor<DataSet<T>>, readonly rows: Accessor<DataSet<T>>,
readonly columns: Accessor<Column<T>[]>, readonly columns: Accessor<Column<T>[]>,
readonly selection: Accessor<SelectionItem<keyof T, T>[]>, readonly selection: Accessor<SelectionItem<number, T>[]>,
readonly selectionMode: Accessor<SelectionMode>, readonly selectionMode: Accessor<SelectionMode>,
readonly cellRenderers: Accessor<CellRenderers<T>>, readonly cellRenderers: Accessor<CellRenderers<T>>,
} }
@ -54,7 +54,7 @@ type TableProps<T extends Record<string, any>> = {
}; };
export function Table<T extends Record<string, any>>(props: TableProps<T>) { export function Table<T extends Record<string, any>>(props: TableProps<T>) {
const [selection, setSelection] = createSignal<SelectionItem<keyof T, T>[]>([]); const [selection, setSelection] = createSignal<SelectionItem<number, T>[]>([]);
const rows = createMemo(() => props.rows); const rows = createMemo(() => props.rows);
const columns = createMemo<Column<T>[]>(() => props.columns ?? []); const columns = createMemo<Column<T>[]>(() => props.columns ?? []);
@ -97,7 +97,7 @@ function InnerTable<T extends Record<string, any>>(props: InnerTableProps<T>) {
<Head /> <Head />
<tbody class={css.main}> <tbody class={css.main}>
<For each={props.rows.nodes()}>{ <For each={props.rows.nodes() as DataSetNode<number, T>[]}>{
node => <Node node={node} depth={0} /> node => <Node node={node} depth={0} />
}</For> }</For>
</tbody> </tbody>
@ -114,7 +114,7 @@ function InnerTable<T extends Record<string, any>>(props: InnerTableProps<T>) {
function Api<T extends Record<string, any>>(props: { api: undefined | ((api: TableApi<T>) => any) }) { function Api<T extends Record<string, any>>(props: { api: undefined | ((api: TableApi<T>) => any) }) {
const table = useTable<T>(); const table = useTable<T>();
const selectionContext = useSelection<T>(); const selectionContext = useSelection<number, T>();
const api: TableApi<T> = { const api: TableApi<T> = {
selection: selectionContext.selection, selection: selectionContext.selection,
@ -202,7 +202,7 @@ function Head(props: {}) {
</thead>; </thead>;
}; };
function Node<T extends Record<string, any>>(props: { node: DataSetNode<keyof T, T>, depth: number, groupedBy?: keyof T }) { function Node<K extends number | string, T extends Record<string, any>>(props: { node: DataSetNode<K, T>, depth: number, groupedBy?: keyof T }) {
return <Switch> return <Switch>
<Match when={props.node.kind === 'row' ? props.node : undefined}>{ <Match when={props.node.kind === 'row' ? props.node : undefined}>{
row => <Row key={row().key} value={row().value} depth={props.depth} groupedBy={props.groupedBy} /> row => <Row key={row().key} value={row().value} depth={props.depth} groupedBy={props.groupedBy} />
@ -214,9 +214,9 @@ function Node<T extends Record<string, any>>(props: { node: DataSetNode<keyof T,
</Switch>; </Switch>;
} }
function Row<T extends Record<string, any>>(props: { key: keyof T, value: T, depth: number, groupedBy?: keyof T }) { function Row<K extends number | string, T extends Record<string, any>>(props: { key: K, value: T, depth: number, groupedBy?: keyof T }) {
const table = useTable<T>(); const table = useTable<T>();
const context = useSelection<T>(); const context = useSelection<K, T>();
const columns = table.columns; const columns = table.columns;
const isSelected = context.isSelected(props.key); const isSelected = context.isSelected(props.key);
@ -239,7 +239,7 @@ function Row<T extends Record<string, any>>(props: { key: keyof T, value: T, dep
</tr>; </tr>;
}; };
function Group<T extends Record<string, any>>(props: { key: keyof T, groupedBy: keyof T, nodes: DataSetNode<keyof T, T>[], depth: number }) { function Group<K extends number | string, T extends Record<string, any>>(props: { key: K, groupedBy: keyof T, nodes: DataSetNode<K, T>[], depth: number }) {
const table = useTable(); const table = useTable();
return <tr class={css.group}> return <tr class={css.group}>

View file

@ -2,11 +2,13 @@ import { Accessor, Component, createEffect, createMemo, createSignal } from "sol
import { debounce, Mutation } from "~/utilities"; import { debounce, Mutation } from "~/utilities";
import { Column, GridApi as GridCompApi, Grid as GridComp } from "~/components/grid"; import { Column, GridApi as GridCompApi, Grid as GridComp } from "~/components/grid";
import { createDataSet, DataSetNode, DataSetRowNode } from "~/components/table"; import { createDataSet, DataSetNode, DataSetRowNode } from "~/components/table";
import { SelectionItem } from "../selectable";
import css from "./grid.module.css" import css from "./grid.module.css"
export type Entry = { key: string } & { [lang: string]: string }; export type Entry = { key: string } & { [lang: string]: string };
export interface GridApi { export interface GridApi {
readonly mutations: Accessor<Mutation[]>; readonly mutations: Accessor<Mutation[]>;
readonly selection: Accessor<SelectionItem<number, Entry>[]>;
remove(indices: number[]): void; remove(indices: number[]): void;
addKey(key: string): void; addKey(key: string): void;
addLocale(locale: string): void; addLocale(locale: string): void;
@ -25,9 +27,9 @@ const groupBy = (rows: DataSetRowNode<number, Entry>[]) => {
return group(rows.map<R>(r => ({ ...r, _key: r.value.key }))) as any; return group(rows.map<R>(r => ({ ...r, _key: r.value.key }))) as any;
} }
export function Grid(props: { class?: string, rows: Entry[], api?: (api: GridApi) => any }) { export function Grid(props: { class?: string, rows: Entry[], locales: string[], api?: (api: GridApi) => any }) {
const rows = createMemo(() => createDataSet<Entry>(props.rows, { group: { by: 'key', with: groupBy } })); 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 locales = createMemo(() => props.locales);
const columns = createMemo<Column<Entry>[]>(() => [ const columns = createMemo<Column<Entry>[]>(() => [
{ {
id: 'key', id: 'key',
@ -45,11 +47,14 @@ export function Grid(props: { class?: string, rows: Entry[], api?: (api: GridApi
})) }))
]); ]);
const [api, setApi] = createSignal<GridCompApi<Entry>>();
createEffect(() => { createEffect(() => {
const r = rows(); const r = rows();
props.api?.({ props.api?.({
mutations: r.mutations, mutations: r.mutations,
selection: createMemo(() => api()?.selection() ?? []),
remove: r.remove, remove: r.remove,
addKey(key) { addKey(key) {
r.insert({ key, ...Object.fromEntries(locales().map(l => [l, ''])) }); r.insert({ key, ...Object.fromEntries(locales().map(l => [l, ''])) });
@ -60,7 +65,7 @@ export function Grid(props: { class?: string, rows: Entry[], api?: (api: GridApi
}); });
}); });
return <GridComp rows={rows()} columns={columns()} />; return <GridComp rows={rows()} columns={columns()} api={setApi} />;
}; };
const TextArea: Component<{ row: number, key: string, lang: string, value: string, oninput?: (event: InputEvent) => any }> = (props) => { const TextArea: Component<{ row: number, key: string, lang: string, value: string, oninput?: (event: InputEvent) => any }> = (props) => {

View file

@ -22,48 +22,48 @@ export interface SelectionItem<K, T> {
element: WeakRef<HTMLElement>; element: WeakRef<HTMLElement>;
}; };
export interface SelectionContextType<T extends object> { export interface SelectionContextType<K, T extends object> {
readonly selection: Accessor<SelectionItem<keyof T, T>[]>; readonly selection: Accessor<SelectionItem<K, T>[]>;
readonly length: Accessor<number>; readonly length: Accessor<number>;
select(selection: (keyof T)[], options?: Partial<{ mode: SelectionMode }>): void; select(selection: K[], options?: Partial<{ mode: SelectionMode }>): void;
selectAll(): void; selectAll(): void;
clear(): void; clear(): void;
isSelected(key: keyof T): Accessor<boolean>; isSelected(key: K): Accessor<boolean>;
} }
interface InternalSelectionContextType<T extends object> { interface InternalSelectionContextType<K, T extends object> {
readonly latest: Signal<HTMLElement | undefined>, readonly latest: Signal<HTMLElement | undefined>,
readonly modifier: Signal<Modifier>, readonly modifier: Signal<Modifier>,
readonly selectables: Signal<HTMLElement[]>, readonly selectables: Signal<HTMLElement[]>,
readonly keyMap: Map<string, keyof T>, readonly keyMap: Map<string, K>,
add(key: keyof T, value: Accessor<T>, element: HTMLElement): string; add(key: K, value: Accessor<T>, element: HTMLElement): string;
} }
export interface SelectionHandler<T extends object> { export interface SelectionHandler<T extends object> {
(selection: T[]): any; (selection: T[]): any;
} }
const SelectionContext = createContext<SelectionContextType<any>>(); const SelectionContext = createContext<SelectionContextType<any, any>>();
const InternalSelectionContext = createContext<InternalSelectionContextType<any>>(); const InternalSelectionContext = createContext<InternalSelectionContextType<any, any>>();
export function useSelection<T extends object = object>(): SelectionContextType<T> { export function useSelection<K, T extends object = object>() {
const context = useContext(SelectionContext); const context = useContext(SelectionContext);
if (context === undefined) { if (context === undefined) {
throw new Error('selection context is used outside of a provider'); throw new Error('selection context is used outside of a provider');
} }
return context as SelectionContextType<T>; return context as SelectionContextType<K, T>;
}; };
function useInternalSelection<T extends object>() { function useInternalSelection<K, T extends object>() {
return useContext(InternalSelectionContext)! as InternalSelectionContextType<T>; return useContext(InternalSelectionContext)! as InternalSelectionContextType<K, T>;
} }
interface State<T extends object> { interface State<K, T extends object> {
selection: (keyof T)[]; selection: K[];
data: SelectionItem<keyof T, T>[]; data: SelectionItem<K, T>[];
} }
export function SelectionProvider<T extends object>(props: ParentProps<{ selection?: SelectionHandler<T>, multiSelect?: boolean }>) { export function SelectionProvider<K, T extends object>(props: ParentProps<{ selection?: SelectionHandler<T>, multiSelect?: boolean }>) {
const [state, setState] = createStore<State<T>>({ selection: [], data: [] }); const [state, setState] = createStore<State<K, T>>({ selection: [], data: [] });
const selection = createMemo(() => state.data.filter(({ key }) => state.selection.includes(key))); const selection = createMemo(() => state.data.filter(({ key }) => state.selection.includes(key)));
const length = createMemo(() => state.data.length); const length = createMemo(() => state.data.length);
@ -71,7 +71,7 @@ export function SelectionProvider<T extends object>(props: ParentProps<{ selecti
props.selection?.(selection().map(({ value }) => value())); props.selection?.(selection().map(({ value }) => value()));
}); });
const context: SelectionContextType<T> = { const context: SelectionContextType<K, T> = {
selection, selection,
length, length,
select(selection, { mode = SelectionMode.Normal } = {}) { select(selection, { mode = SelectionMode.Normal } = {}) {
@ -106,9 +106,9 @@ export function SelectionProvider<T extends object>(props: ParentProps<{ selecti
}, },
}; };
const keyIdMap = new Map<keyof T, string>(); const keyIdMap = new Map<K, string>();
const idKeyMap = new Map<string, keyof T>(); const idKeyMap = new Map<string, K>();
const internal: InternalSelectionContextType<T> = { const internal: InternalSelectionContextType<K, T> = {
modifier: createSignal<Modifier>(Modifier.None), modifier: createSignal<Modifier>(Modifier.None),
latest: createSignal<HTMLElement>(), latest: createSignal<HTMLElement>(),
selectables: createSignal<HTMLElement[]>([]), selectables: createSignal<HTMLElement[]>([]),
@ -201,9 +201,9 @@ const Root: ParentComponent = (props) => {
return <div ref={setRoot} tabIndex={0} onKeyDown={onKeyboardEvent} onKeyUp={onKeyboardEvent} class={css.root}>{c()}</div>; return <div ref={setRoot} tabIndex={0} onKeyDown={onKeyboardEvent} onKeyUp={onKeyboardEvent} class={css.root}>{c()}</div>;
}; };
export function selectable<T extends object>(element: HTMLElement, options: Accessor<{ value: T, key: keyof T }>) { export function selectable<K, T extends object>(element: HTMLElement, options: Accessor<{ value: T, key: K }>) {
const context = useSelection<T>(); const context = useSelection<K, T>();
const internal = useInternalSelection<T>(); const internal = useInternalSelection<K, T>();
const key = options().key; const key = options().key;
const value = createMemo(() => options().value); const value = createMemo(() => options().value);
@ -211,17 +211,17 @@ export function selectable<T extends object>(element: HTMLElement, options: Acce
const selectionKey = internal.add(key, value, element); const selectionKey = internal.add(key, value, element);
const createRange = (a?: HTMLElement, b?: HTMLElement): (keyof T)[] => { const createRange = (a?: HTMLElement, b?: HTMLElement): K[] => {
if (!a && !b) { if (!a && !b) {
return []; return [];
} }
if (!a) { if (!a) {
return [b!.dataset.selecatableKey! as keyof T]; return [b!.dataset.selecatableKey! as K];
} }
if (!b) { if (!b) {
return [a!.dataset.selecatableKey! as keyof T]; return [a!.dataset.selecatableKey! as K];
} }
if (a === b) { if (a === b) {

View file

@ -125,7 +125,8 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
} }
case MutarionKind.Delete: { case MutarionKind.Delete: {
return files.values().map(file => ({ kind: MutarionKind.Delete, key: m.key, file })).toArray(); const entry = entries.get(index as any)!;
return files.values().map(file => ({ kind: MutarionKind.Delete, key: entry.key, file })).toArray();
} }
default: throw new Error('unreachable code'); default: throw new Error('unreachable code');
@ -283,7 +284,7 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
return; return;
} }
remove(Object.keys(selection())); remove(selection().map(s => s.key));
}, { key: 'delete', modifier: Modifier.None }), }, { key: 'delete', modifier: Modifier.None }),
inserNewKey: createCommand('insert new key', async () => { inserNewKey: createCommand('insert new key', async () => {
const formData = await newKeyPrompt()?.showModal(); const formData = await newKeyPrompt()?.showModal();
@ -380,6 +381,7 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
const Content: Component<{ directory: FileSystemDirectoryHandle, api?: Setter<GridApi | undefined>, entries?: Setter<Entries> }> = (props) => { const Content: Component<{ directory: FileSystemDirectoryHandle, api?: Setter<GridApi | undefined>, entries?: Setter<Entries> }> = (props) => {
const [entries, setEntries] = createSignal<Entries>(new Map()); const [entries, setEntries] = createSignal<Entries>(new Map());
const [locales, setLocales] = createSignal<string[]>([]);
const [rows, setRows] = createSignal<Entry[]>([]); const [rows, setRows] = createSignal<Entry[]>([]);
const [api, setApi] = createSignal<GridApi>(); const [api, setApi] = createSignal<GridApi>();
@ -412,6 +414,8 @@ const Content: Component<{ directory: FileSystemDirectoryHandle, api?: Setter<Gr
); );
const template = contents.map(({ lang, handle }) => [lang, { handle, value: '' }]); const template = contents.map(({ lang, handle }) => [lang, { handle, value: '' }]);
setLocales(contents.map(({ lang }) => lang));
const merged = contents.reduce((aggregate, { id, handle, lang, entries }) => { const merged = contents.reduce((aggregate, { id, handle, lang, entries }) => {
for (const [key, value] of entries.entries()) { for (const [key, value] of entries.entries()) {
if (!aggregate.has(key)) { if (!aggregate.has(key)) {
@ -429,7 +433,7 @@ const Content: Component<{ directory: FileSystemDirectoryHandle, api?: Setter<Gr
})(); })();
}); });
return <Grid rows={rows()} api={setApi} />; return <Grid rows={rows()} locales={locales()} api={setApi} />;
}; };
const Blank: Component<{ open: CommandType }> = (props) => { const Blank: Component<{ open: CommandType }> = (props) => {