quick snapshot

This commit is contained in:
Chris Kruining 2024-10-24 14:39:08 +02:00
parent b03f80f34f
commit 5367ed2f44
No known key found for this signature in database
GPG key ID: EB894A3560CCCAD2
10 changed files with 309 additions and 191 deletions

View file

@ -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 {

View file

@ -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) {

View file

@ -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()}
/>
};

View file

@ -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;

View file

@ -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>;

View file

@ -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