quick snapshot
This commit is contained in:
parent
b03f80f34f
commit
5367ed2f44
10 changed files with 309 additions and 191 deletions
|
@ -9,12 +9,12 @@
|
|||
|
||||
margin: 0;
|
||||
gap: var(--padding-m);
|
||||
padding: var(--padding-m);
|
||||
padding: var(--padding-m) 0;
|
||||
font-size: var(--text-s);
|
||||
|
||||
background-color: var(--surface-1);
|
||||
color: var(--text-1);
|
||||
border: 1px solid var(--text-1);
|
||||
border: 1px solid var(--surface-5);
|
||||
border-radius: var(--radii-m);
|
||||
|
||||
& > li {
|
||||
|
@ -23,10 +23,16 @@
|
|||
grid-template-columns: subgrid;
|
||||
align-items: center;
|
||||
|
||||
padding: var(--padding-s) var(--padding-m);
|
||||
|
||||
& > sub {
|
||||
color: var(--text-2);
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--surface-4);
|
||||
}
|
||||
}
|
||||
|
||||
&:popover-open {
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
& textarea {
|
||||
resize: vertical;
|
||||
min-block-size: 2em;
|
||||
min-block-size: max(2em, 100%);
|
||||
max-block-size: 50em;
|
||||
|
||||
background-color: var(--surface-1);
|
||||
|
@ -39,10 +39,6 @@
|
|||
& > span {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
& > textarea {
|
||||
min-block-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
& :is(.header, .main, .footer) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Accessor, Component, createContext, createEffect, createMemo, createRenderEffect, createSignal, For, onMount, ParentComponent, Show, useContext } from "solid-js";
|
||||
import { Accessor, Component, createContext, createEffect, createMemo, createRenderEffect, createSignal, createUniqueId, For, onMount, ParentComponent, Show, useContext } from "solid-js";
|
||||
import { createStore, unwrap } from "solid-js/store";
|
||||
import { SelectionProvider, useSelection, selectable } from "../selectable";
|
||||
import { debounce, deepCopy, deepDiff, Mutation } from "~/utilities";
|
||||
|
@ -10,15 +10,17 @@ interface Leaf extends Record<string, string> { }
|
|||
export interface Entry extends Record<string, Entry | Leaf> { }
|
||||
|
||||
type Rows = Map<string, Record<string, string>>;
|
||||
type SelectionItem = { key: string, value: Accessor<Record<string, string>>, element: WeakRef<HTMLElement> };
|
||||
|
||||
export interface GridContextType {
|
||||
readonly rows: Accessor<Record<string, Record<string, string>>>;
|
||||
readonly mutations: Accessor<Mutation[]>;
|
||||
readonly selection: Accessor<object[]>;
|
||||
readonly selection: Accessor<SelectionItem[]>;
|
||||
mutate(prop: string, lang: string, value: string): void;
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -31,7 +33,7 @@ const isLeaf = (entry: Entry | Leaf): entry is Leaf => Object.values(entry).some
|
|||
const useGrid = () => useContext(GridContext)!;
|
||||
|
||||
const GridProvider: ParentComponent<{ rows: Rows }> = (props) => {
|
||||
const [selection, setSelection] = createSignal<object[]>([]);
|
||||
const [selection, setSelection] = createSignal<SelectionItem[]>([]);
|
||||
const [state, setState] = createStore<{ rows: Record<string, Record<string, string>>, snapshot: Rows, numberOfRows: number }>({
|
||||
rows: {},
|
||||
snapshot: new Map,
|
||||
|
@ -106,9 +108,14 @@ export const Grid: Component<{ class?: string, columns: string[], rows: Rows, ap
|
|||
|
||||
const Api: Component<{ api: undefined | ((api: GridApi) => any) }> = (props) => {
|
||||
const gridContext = useGrid();
|
||||
const selectionContext = useSelection();
|
||||
const selectionContext = useSelection<{ key: string, value: Accessor<Record<string, string>>, element: WeakRef<HTMLElement> }>();
|
||||
|
||||
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() {
|
||||
|
@ -160,7 +167,7 @@ const Row: Component<{ entry: Entry, path?: string[] }> = (props) => {
|
|||
return <Show when={isLeaf(value)} fallback={<Group key={key} entry={value as Entry} path={path} />}>
|
||||
<div class={css.row} use:selectable={{ value, key: k }}>
|
||||
<div class={css.cell}>
|
||||
<input type="checkbox" checked={isSelected()} on:input={() => context.select([k])} />
|
||||
<input type="checkbox" checked={isSelected()} on:input={() => context.select([k])} on:pointerdown={e => e.stopPropagation()} />
|
||||
</div>
|
||||
|
||||
<div class={css.cell}>
|
||||
|
@ -217,6 +224,13 @@ const TextArea: Component<{ key: string, value: string, lang: string, oninput?:
|
|||
resize();
|
||||
});
|
||||
|
||||
const observer = new MutationObserver((e) => {
|
||||
if (element()?.isConnected) {
|
||||
resize();
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
return <textarea
|
||||
ref={setElement}
|
||||
value={props.value}
|
||||
|
@ -226,5 +240,7 @@ const TextArea: Component<{ key: string, value: string, lang: string, oninput?:
|
|||
spellcheck
|
||||
wrap="soft"
|
||||
onkeyup={onKeyUp}
|
||||
on:keydown={e => e.stopPropagation()}
|
||||
on:pointerdown={e => e.stopPropagation()}
|
||||
/>
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
.item {
|
||||
padding: .5em 1em;
|
||||
padding: var(--padding-m) var(--padding-l);
|
||||
|
||||
background-color: inherit;
|
||||
color: var(--text-1);
|
||||
|
@ -34,6 +34,13 @@
|
|||
display: grid;
|
||||
}
|
||||
|
||||
& > .separator {
|
||||
grid-column: span 2;
|
||||
inline-size: calc(100% - (2 * var(--padding-m)));
|
||||
margin-block: 0;
|
||||
border: 1px solid var(--surface-5);
|
||||
}
|
||||
|
||||
& > .item {
|
||||
grid-column: span 2;
|
||||
display: grid;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Accessor, Component, For, JSX, ParentComponent, Setter, Show, children, createContext, createEffect, createMemo, createSignal, createUniqueId, mergeProps, onCleanup, onMount, useContext } from "solid-js";
|
||||
import { Accessor, Component, For, JSX, Match, ParentComponent, Setter, Show, Switch, children, createContext, createEffect, createMemo, createSignal, createUniqueId, mergeProps, onCleanup, onMount, useContext } from "solid-js";
|
||||
import { Portal } from "solid-js/web";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { CommandType, Command } from "../command";
|
||||
|
@ -9,8 +9,8 @@ export interface MenuContextType {
|
|||
ref: Accessor<Node | undefined>;
|
||||
setRef: Setter<Node | undefined>;
|
||||
|
||||
addItems(items: (Item | ItemWithChildren)[]): void;
|
||||
items: Accessor<(Item | ItemWithChildren)[]>;
|
||||
addItems(items: (Item | Separator | ItemWithChildren)[]): void;
|
||||
items: Accessor<(Item | Separator | ItemWithChildren)[]>;
|
||||
commands(): CommandType[];
|
||||
};
|
||||
|
||||
|
@ -21,11 +21,15 @@ export interface Item {
|
|||
command: CommandType;
|
||||
}
|
||||
|
||||
export interface Separator {
|
||||
kind: 'separator';
|
||||
}
|
||||
|
||||
export interface ItemWithChildren {
|
||||
kind: 'node';
|
||||
id: string;
|
||||
label: string;
|
||||
children: Item[];
|
||||
children: (Item | Separator)[];
|
||||
}
|
||||
|
||||
const MenuContext = createContext<MenuContextType>();
|
||||
|
@ -51,7 +55,7 @@ export const MenuProvider: ParentComponent<{ commands?: CommandType[] }> = (prop
|
|||
},
|
||||
commands() {
|
||||
return Object.values(store.items)
|
||||
.map(item => item.kind === 'node' ? item.children.map(c => c.command) : item.command)
|
||||
.map(item => item.kind === 'node' ? item.children.filter(c => c.kind === 'leaf').map(c => c.command) : item.command)
|
||||
.flat()
|
||||
.concat(props.commands ?? []);
|
||||
},
|
||||
|
@ -90,6 +94,10 @@ const Item: Component<ItemProps> = (props) => {
|
|||
}) as unknown as JSX.Element;
|
||||
}
|
||||
|
||||
const Separator: Component = (props) => {
|
||||
return mergeProps(props, { kind: 'separator' }) as unknown as JSX.Element;
|
||||
}
|
||||
|
||||
const Root: ParentComponent<{}> = (props) => {
|
||||
const menu = useMenu();
|
||||
const [current, setCurrent] = createSignal<HTMLElement>();
|
||||
|
@ -125,34 +133,46 @@ const Root: ParentComponent<{}> = (props) => {
|
|||
|
||||
return <Portal mount={menu.ref()}>
|
||||
<For each={items}>{
|
||||
item => <Show when={Object.hasOwn(item, 'children') ? item as ItemWithChildren : undefined} fallback={<Child command={item.command} />}>{
|
||||
item => <>
|
||||
<div
|
||||
class={css.child}
|
||||
id={`child-${item().id}`}
|
||||
style={`position-anchor: --menu-${item().id};`}
|
||||
popover
|
||||
on:toggle={(e: ToggleEvent) => {
|
||||
if (e.newState === 'open' && e.target !== null) {
|
||||
return setCurrent(e.target as HTMLElement);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<For each={item().children}>
|
||||
{(child) => <Child command={child.command} />}
|
||||
</For>
|
||||
</div>
|
||||
item => <Switch>
|
||||
<Match when={item.kind === 'node' ? item as ItemWithChildren : undefined}>{
|
||||
item => <>
|
||||
<div
|
||||
class={css.child}
|
||||
id={`child-${item().id}`}
|
||||
style={`position-anchor: --menu-${item().id};`}
|
||||
popover
|
||||
on:toggle={(e: ToggleEvent) => {
|
||||
if (e.newState === 'open' && e.target !== null) {
|
||||
return setCurrent(e.target as HTMLElement);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<For each={item().children}>{
|
||||
child => <Switch>
|
||||
<Match when={child.kind === 'leaf' ? child as Item : undefined}>{
|
||||
item => <Child command={item().command} />
|
||||
}</Match>
|
||||
|
||||
<button
|
||||
class={css.item}
|
||||
type="button"
|
||||
popovertarget={`child-${item().id}`}
|
||||
style={`anchor-name: --menu-${item().id};`}
|
||||
>
|
||||
{item().label}
|
||||
</button>
|
||||
</>
|
||||
}</Show>
|
||||
<Match when={child.kind === 'separator'}><hr class={css.separator} /></Match>
|
||||
</Switch>
|
||||
}</For>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class={css.item}
|
||||
type="button"
|
||||
popovertarget={`child-${item().id}`}
|
||||
style={`anchor-name: --menu-${item().id};`}
|
||||
>
|
||||
{item().label}
|
||||
</button>
|
||||
</>
|
||||
}</Match>
|
||||
|
||||
<Match when={item.kind === 'leaf' ? item as Item : undefined}>{
|
||||
item => <Child command={item().command} />
|
||||
}</Match>
|
||||
</Switch>
|
||||
}</For>
|
||||
</Portal>
|
||||
};
|
||||
|
@ -207,7 +227,7 @@ export const asMenuRoot = (element: Element) => {
|
|||
menu.setRef(element);
|
||||
};
|
||||
|
||||
export const Menu = { Root, Item } as const;
|
||||
export const Menu = { Root, Item, Separator } as const;
|
||||
|
||||
export interface CommandPaletteApi {
|
||||
readonly open: Accessor<boolean>;
|
||||
|
|
|
@ -2,7 +2,6 @@ import { Accessor, children, createContext, createEffect, createMemo, createRend
|
|||
import { createStore } from "solid-js/store";
|
||||
import { isServer } from "solid-js/web";
|
||||
import css from "./index.module.css";
|
||||
import { isFocusable } from "~/utilities";
|
||||
|
||||
enum Modifier {
|
||||
None = 0,
|
||||
|
@ -17,8 +16,8 @@ enum SelectionMode {
|
|||
Toggle,
|
||||
}
|
||||
|
||||
export interface SelectionContextType {
|
||||
readonly selection: Accessor<object[]>;
|
||||
export interface SelectionContextType<T extends object = object> {
|
||||
readonly selection: Accessor<T[]>;
|
||||
readonly length: Accessor<number>;
|
||||
select(selection: string[], options?: Partial<{ mode: SelectionMode }>): void;
|
||||
selectAll(): void;
|
||||
|
@ -31,19 +30,21 @@ interface InternalSelectionContextType {
|
|||
readonly selectables: Signal<HTMLElement[]>,
|
||||
add(key: string, value: object, element: HTMLElement): void;
|
||||
}
|
||||
export type SelectionHandler = (selection: object[]) => any;
|
||||
export interface SelectionHandler<T extends object = object> {
|
||||
(selection: T[]): any;
|
||||
}
|
||||
|
||||
const SelectionContext = createContext<SelectionContextType>();
|
||||
const InternalSelectionContext = createContext<InternalSelectionContextType>();
|
||||
|
||||
export const useSelection = () => {
|
||||
export function useSelection<T extends object = object>(): SelectionContextType<T> {
|
||||
const context = useContext(SelectionContext);
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error('selection context is used outside of a provider');
|
||||
}
|
||||
|
||||
return context;
|
||||
return context as SelectionContextType<T>;
|
||||
};
|
||||
const useInternalSelection = () => useContext(InternalSelectionContext)!;
|
||||
|
||||
|
@ -69,8 +70,6 @@ export const SelectionProvider: ParentComponent<{ selection?: SelectionHandler,
|
|||
mode = SelectionMode.Toggle;
|
||||
}
|
||||
|
||||
console.log(selection, mode);
|
||||
|
||||
setState('selection', existing => {
|
||||
switch (mode) {
|
||||
case SelectionMode.Toggle: {
|
||||
|
@ -127,15 +126,17 @@ const Root: ParentComponent = (props) => {
|
|||
|
||||
if (!isServer && r) {
|
||||
const findSelectables = () => {
|
||||
setSelectables(Array.from((function* () {
|
||||
const iterator = document.createTreeWalker(r, NodeFilter.SHOW_ELEMENT, {
|
||||
acceptNode: (node: HTMLElement) => node.dataset.selectionKey ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP,
|
||||
});
|
||||
setTimeout(() => {
|
||||
setSelectables(Array.from((function* () {
|
||||
const iterator = document.createTreeWalker(r, NodeFilter.SHOW_ELEMENT, {
|
||||
acceptNode: (node: HTMLElement) => node.dataset.selectionKey ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP,
|
||||
});
|
||||
|
||||
while (iterator.nextNode()) {
|
||||
yield iterator.currentNode as HTMLElement;
|
||||
}
|
||||
})()));
|
||||
while (iterator.nextNode()) {
|
||||
yield iterator.currentNode as HTMLElement;
|
||||
}
|
||||
})()));
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const observer = new MutationObserver(entries => {
|
||||
|
@ -191,6 +192,7 @@ const Root: ParentComponent = (props) => {
|
|||
});
|
||||
|
||||
return <div ref={setRoot} style={{ 'display': 'contents' }}>{c()}</div>;
|
||||
// return <div ref={setRoot}>{c()}</div>;
|
||||
};
|
||||
|
||||
export const selectable = (element: HTMLElement, options: Accessor<{ value: object, key?: string }>) => {
|
||||
|
@ -228,14 +230,11 @@ export const selectable = (element: HTMLElement, options: Accessor<{ value: obje
|
|||
return selection.map(n => n.dataset.selectionKey!);
|
||||
};
|
||||
|
||||
|
||||
createRenderEffect(() => {
|
||||
element.dataset.selected = isSelected() ? 'true' : undefined;
|
||||
});
|
||||
|
||||
const onPointerDown = (e: Event) => {
|
||||
// TODO :: find out if the cell clicked is editable and early exit after that
|
||||
|
||||
const [latest, setLatest] = internal.latest
|
||||
const [modifier] = internal.modifier
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue