started implementation of commands
This commit is contained in:
parent
4b3d91d6cd
commit
99509c9ba1
4 changed files with 197 additions and 65 deletions
|
@ -1,62 +1,89 @@
|
||||||
import { Accessor, Component, For, JSX, ParentComponent, Setter, Show, children, createContext, createRenderEffect, createSignal, createUniqueId, mergeProps, onCleanup, useContext } from "solid-js";
|
import { Accessor, Component, For, JSX, ParentComponent, Setter, Show, children, createContext, createMemo, createSignal, createUniqueId, mergeProps, onCleanup, onMount, splitProps, useContext } from "solid-js";
|
||||||
import { Portal, isServer, ssr, useAssets } from "solid-js/web";
|
import { Portal, isServer } from "solid-js/web";
|
||||||
|
import './style.css';
|
||||||
|
import { createStore } from "solid-js/store";
|
||||||
|
|
||||||
export interface MenuContextType {
|
export interface MenuContextType {
|
||||||
ref(): JSX.Element|undefined;
|
ref: Accessor<JSX.Element|undefined>;
|
||||||
// setRef(ref: JSX.Element|undefined): void;
|
setRef: Setter<JSX.Element|undefined>;
|
||||||
|
|
||||||
|
addItems(items: (Item|ItemWithChildren)[]): void;
|
||||||
|
items: Accessor<(Item|ItemWithChildren)[]>;
|
||||||
|
commands(): Command[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum Modifier {
|
||||||
|
Shift = 1 << 0,
|
||||||
|
Control = 1 << 1,
|
||||||
|
Meta = 1 << 2,
|
||||||
|
Alt = 1 << 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Command {
|
||||||
|
(): any;
|
||||||
|
shortcut?: {
|
||||||
|
key: string;
|
||||||
|
modifier?: Modifier;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface Item {
|
export interface Item {
|
||||||
ref?: Element;
|
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
children?: Omit<Item, 'children'>[];
|
command: Command;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ItemWithChildren {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
children: Item[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const MenuContext = createContext<MenuContextType>();
|
const MenuContext = createContext<MenuContextType>();
|
||||||
|
|
||||||
// const initClientProvider = (): MenuContextType => {
|
export const createCommand = (command: () => any, shortcut?: Command['shortcut']): Command => {
|
||||||
// const root = document.querySelector('[data-app-menu="root"]');
|
if(shortcut) {
|
||||||
// const items = JSON.parse(document.querySelector('[data-app-menu="ssr-items"]')?.textContent ?? '[]');
|
(command as Command).shortcut = { key: shortcut.key.toLowerCase(), modifier: shortcut.modifier };
|
||||||
|
}
|
||||||
|
|
||||||
// console.log(items);
|
return command;
|
||||||
|
};
|
||||||
|
|
||||||
// let _ref!: JSX.Element;
|
export const MenuProvider: ParentComponent = (props) => {
|
||||||
|
const [ ref, setRef ] = createSignal<JSX.Element|undefined>();
|
||||||
|
const [ _items, setItems ] = createSignal<Map<string, Item&{ children?: Map<string, Item> }>>(new Map());
|
||||||
|
|
||||||
// // useAssets(() => ssr(`<script type="application/json" data-app-menu="ssr-items">${JSON.stringify(items)}</script>`) as any);
|
const [ store, setStore ] = createStore<{ items: Record<string, Item|ItemWithChildren> }>({ items: {} });
|
||||||
|
|
||||||
// return {
|
const addItems = (items: (Item|ItemWithChildren)[]) => setStore('items', values => {
|
||||||
// ref() {
|
for (const item of items) {
|
||||||
// return _ref;
|
// const existing = values.get(item.id);
|
||||||
// },
|
|
||||||
// setRef(ref: JSX.Element) {
|
|
||||||
// _ref = ref;
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const initServerProvider = (): MenuContextType => {
|
// if(item.children && existing?.children instanceof Map) {
|
||||||
// let _ref!: JSX.Element;
|
// for (const child of item.children) {
|
||||||
|
// existing.children.set(child.id, child);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// else if (item.children && existing === undefined){
|
||||||
|
// values.set(item.id, { ...item, children: new Map(item.children.map(c => [ c.id, c ])) });
|
||||||
|
// }
|
||||||
|
// else {
|
||||||
|
// values.set(item.id, item as Item);
|
||||||
|
// }
|
||||||
|
values[item.id] = item;
|
||||||
|
}
|
||||||
|
|
||||||
// // useAssets(() => ssr(`<script type="application/json" data-app-menu="ssr-items">${JSON.stringify(items)}</script>`) as any);
|
return values;
|
||||||
|
});
|
||||||
|
const items = createMemo<(Item|ItemWithChildren)[]>(() =>
|
||||||
|
Array.from(
|
||||||
|
Object.values(store.items),
|
||||||
|
// item => item.children instanceof Map ? { ...item, children: Array.from(item.children.values()) } : item
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const commands = createMemo(() => items().map(item => item.children instanceof Array ? item.children.map(c => c.command) : item.command).flat());
|
||||||
|
|
||||||
// return {
|
return <MenuContext.Provider value={{ ref, setRef, addItems, items, commands }}>{props.children}</MenuContext.Provider>;
|
||||||
// ref() {
|
|
||||||
// return _ref;
|
|
||||||
// },
|
|
||||||
// setRef(ref: JSX.Element) {
|
|
||||||
// _ref = ref;
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
// };
|
|
||||||
|
|
||||||
export const MenuProvider: ParentComponent<{ root?: JSX.Element }> = (props) => {
|
|
||||||
// const ctx = isServer ? initServerProvider() : initClientProvider();
|
|
||||||
|
|
||||||
// const [ ref, setRef ] = createSignal<JSX.Element>();
|
|
||||||
// const ctx = {ref, setRef};
|
|
||||||
|
|
||||||
return <MenuContext.Provider value={{ ref: () => props.root }}>{props.children}</MenuContext.Provider>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const useMenu = () => {
|
const useMenu = () => {
|
||||||
|
@ -69,10 +96,19 @@ const useMenu = () => {
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Item: ParentComponent<{ label: string }> = (props) => {
|
type ItemProps = { label: string, children: JSX.Element }|{ label: string, command: Command };
|
||||||
|
|
||||||
|
const Item: Component<ItemProps> = (props) => {
|
||||||
|
const id = createUniqueId();
|
||||||
|
|
||||||
|
if(props.command) {
|
||||||
|
return mergeProps(props, { id }) as unknown as JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
const childItems = children(() => props.children);
|
const childItems = children(() => props.children);
|
||||||
|
|
||||||
return mergeProps(props, {
|
return mergeProps(props, {
|
||||||
|
id,
|
||||||
get children() {
|
get children() {
|
||||||
return childItems();
|
return childItems();
|
||||||
}
|
}
|
||||||
|
@ -81,19 +117,25 @@ const Item: ParentComponent<{ label: string }> = (props) => {
|
||||||
|
|
||||||
const Root: ParentComponent<{}> = (props) => {
|
const Root: ParentComponent<{}> = (props) => {
|
||||||
const menu = useMenu();
|
const menu = useMenu();
|
||||||
const items: { label: string, children?: { label: string }[] }[] = (isServer
|
|
||||||
|
menu.addItems((isServer
|
||||||
? props.children
|
? props.children
|
||||||
: props.children?.map(c => c())) ?? [];
|
: props.children?.map(c => c())) ?? [])
|
||||||
|
|
||||||
|
const Button: Component<{ label: string, command: Command }|{ [key: string]: any }> = (props) => {
|
||||||
|
const [ local, rest ] = splitProps(props, ['label', 'command']);
|
||||||
|
return <button class="item" on:pointerDown={local.command} {...rest}>{local.label}</button>;
|
||||||
|
};
|
||||||
|
|
||||||
return <Portal mount={menu.ref()}>
|
return <Portal mount={menu.ref()}>
|
||||||
<For each={items}>
|
<For each={menu.items()}>
|
||||||
{(item) => <>
|
{(item) => <>
|
||||||
<button {...(item.children ? { popovertarget: item.label } : {})}>{item.label}</button>
|
<Button label={item.label} {...(item.children ? { popovertarget: `child-${item.id}`, id: `menu-${item.id}`, command: item.command } : {})} />
|
||||||
|
|
||||||
<Show when={item.children}>
|
<Show when={item.children}>
|
||||||
<div id={item.label} popover>
|
<div class="child" id={`child-${item.id}`} anchor={`menu-${item.id}`} style="inset: unset;" popover>
|
||||||
<For each={item.children}>
|
<For each={item.children}>
|
||||||
{(child) => <span>{child.label}</span>}
|
{(child) => <Button label={child.label} command={child.command} />}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -103,4 +145,54 @@ const Root: ParentComponent<{}> = (props) => {
|
||||||
</Portal>
|
</Portal>
|
||||||
};
|
};
|
||||||
|
|
||||||
|
declare module "solid-js" {
|
||||||
|
namespace JSX {
|
||||||
|
interface HTMLAttributes<T> {
|
||||||
|
anchor?: string|undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Directives {
|
||||||
|
asMenuRoot: true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const asMenuRoot = (element: Element) => {
|
||||||
|
const menu = useMenu();
|
||||||
|
|
||||||
|
const c = 'menu-root';
|
||||||
|
const listener = (e: KeyboardEvent) => {
|
||||||
|
const key = e.key.toLowerCase();
|
||||||
|
const modifiers =
|
||||||
|
(e.shiftKey ? 1 : 0) << 0 |
|
||||||
|
(e.ctrlKey ? 1 : 0) << 1 |
|
||||||
|
(e.metaKey ? 1 : 0) << 2 |
|
||||||
|
(e.altKey ? 1 : 0) << 3 ;
|
||||||
|
|
||||||
|
const commands = menu.commands();
|
||||||
|
const command = commands.find(c => c.shortcut?.key === key && (c.shortcut.modifier === undefined || c.shortcut.modifier === modifiers));
|
||||||
|
|
||||||
|
if(command === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
command();
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
element.classList.add(c);
|
||||||
|
document.addEventListener('keydown', listener);
|
||||||
|
});
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
element.classList.remove(c);
|
||||||
|
document.removeEventListener('keydown', listener);
|
||||||
|
});
|
||||||
|
|
||||||
|
menu.setRef(element);
|
||||||
|
};
|
||||||
|
|
||||||
export const Menu = { Root, Item } as const;
|
export const Menu = { Root, Item } as const;
|
28
src/features/menu/style.css
Normal file
28
src/features/menu/style.css
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
|
||||||
|
.menu-root {
|
||||||
|
& > div {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.child {
|
||||||
|
position: fixed;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
gap: .5em;
|
||||||
|
padding: .5em 0;
|
||||||
|
|
||||||
|
inset-inline-start: anchor(start);
|
||||||
|
inset-block-start: anchor(end);
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
margin: unset;
|
||||||
|
|
||||||
|
&:popover-open {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,19 +1,14 @@
|
||||||
import { Title } from "@solidjs/meta";
|
import { Title } from "@solidjs/meta";
|
||||||
import { JSX, createSignal } from "solid-js";
|
|
||||||
import { FilesProvider } from "~/features/file";
|
import { FilesProvider } from "~/features/file";
|
||||||
import { MenuProvider, Menu } from "~/features/menu";
|
import { MenuProvider, asMenuRoot } from "~/features/menu";
|
||||||
|
|
||||||
|
asMenuRoot // prevents removal of import
|
||||||
|
|
||||||
export default function Editor(props) {
|
export default function Editor(props) {
|
||||||
const [ref, setRef] = createSignal<JSX.Element>();
|
return <MenuProvider>
|
||||||
|
|
||||||
return <MenuProvider root={ref()}>
|
|
||||||
<Title>Translation-Tool</Title>
|
<Title>Translation-Tool</Title>
|
||||||
|
|
||||||
<nav ref={setRef}>
|
<nav use:asMenuRoot />
|
||||||
<a href="/">Index</a>
|
|
||||||
<a href="/about">About</a>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<FilesProvider>
|
<FilesProvider>
|
||||||
|
|
|
@ -1,20 +1,37 @@
|
||||||
import { Menu } from "~/features/menu";
|
import { createCommand, Menu, Modifier } from "~/features/menu";
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
|
const commands = {
|
||||||
|
open: createCommand(() => {
|
||||||
|
console.log('Open a file');
|
||||||
|
}, { key: 'o', modifier: Modifier.Control}),
|
||||||
|
save: createCommand(() => {
|
||||||
|
console.log('save');
|
||||||
|
}, { key: 's', modifier: Modifier.Control }),
|
||||||
|
saveAll: createCommand(() => {
|
||||||
|
console.log('save all');
|
||||||
|
}, { key: 's', modifier: Modifier.Control|Modifier.Shift }),
|
||||||
|
edit: createCommand(() => {}),
|
||||||
|
selection: createCommand(() => {}),
|
||||||
|
view: createCommand(() => {}),
|
||||||
|
} as const;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Menu.Root>
|
<Menu.Root>
|
||||||
<Menu.Item label="file">
|
<Menu.Item label="file">
|
||||||
<Menu.Item label="open" />
|
<Menu.Item label="open" command={commands.open} />
|
||||||
|
|
||||||
<Menu.Item label="save" />
|
<Menu.Item label="save" command={commands.save} />
|
||||||
|
|
||||||
|
<Menu.Item label="save all" command={commands.saveAll} />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|
||||||
<Menu.Item label="edit" />
|
<Menu.Item label="edit" command={commands.edit} />
|
||||||
|
|
||||||
<Menu.Item label="selection" />
|
<Menu.Item label="selection" command={commands.selection} />
|
||||||
|
|
||||||
<Menu.Item label="view" />
|
<Menu.Item label="view" command={commands.view} />
|
||||||
</Menu.Root>
|
</Menu.Root>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue