[Feature] Add language #19
5 changed files with 53 additions and 45 deletions
|
@ -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> = {
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue