initial attempt with pwa
This commit is contained in:
parent
ddf4519f41
commit
b27abe928d
16 changed files with 382 additions and 220 deletions
|
@ -11,7 +11,7 @@ interface ContextMenuType {
|
|||
|
||||
const ContextMenu = createContext<ContextMenuType>()
|
||||
|
||||
const Root: ParentComponent<{ commands: CommandType[] }> = (props) => {
|
||||
const Root: ParentComponent<{ commands: CommandType<any[]>[] }> = (props) => {
|
||||
const [target, setTarget] = createSignal<HTMLElement>();
|
||||
|
||||
const context = {
|
||||
|
|
|
@ -1,48 +1,115 @@
|
|||
import { Component, Show } from 'solid-js';
|
||||
import { Accessor, children, Component, createContext, createEffect, createMemo, JSX, ParentComponent, ParentProps, Show, useContext } from 'solid-js';
|
||||
|
||||
export enum Modifier {
|
||||
None = 0,
|
||||
Shift = 1 << 0,
|
||||
Control = 1 << 1,
|
||||
Meta = 1 << 2,
|
||||
Alt = 1 << 3,
|
||||
interface CommandContextType {
|
||||
set(commands: CommandType<any[]>[]): void;
|
||||
addContextualArguments<T extends any[] = any[]>(command: CommandType<T>, target: EventTarget, args: Accessor<T>): void;
|
||||
execute<TArgs extends any[] = []>(command: CommandType<TArgs>, event: Event): void;
|
||||
}
|
||||
|
||||
export interface CommandType {
|
||||
(): any;
|
||||
label: string;
|
||||
shortcut?: {
|
||||
key: string;
|
||||
modifier: Modifier;
|
||||
};
|
||||
}
|
||||
const CommandContext = createContext<CommandContextType>();
|
||||
|
||||
export const createCommand = (label: string, command: () => any, shortcut?: CommandType['shortcut']): CommandType => {
|
||||
return Object.defineProperties(command as CommandType, {
|
||||
label: {
|
||||
value: label,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
export const useCommands = () => useContext(CommandContext);
|
||||
|
||||
const Root: ParentComponent<{ commands: CommandType[] }> = (props) => {
|
||||
// const commands = () => props.commands ?? [];
|
||||
const contextualArguments = new Map<CommandType, WeakMap<EventTarget, Accessor<any[]>>>();
|
||||
const commands = new Set<CommandType<any[]>>();
|
||||
|
||||
const context = {
|
||||
set(c: CommandType<any[]>[]): void {
|
||||
for (const command of c) {
|
||||
commands.add(command);
|
||||
}
|
||||
},
|
||||
shortcut: {
|
||||
value: shortcut ? { key: shortcut.key.toLowerCase(), modifier: shortcut.modifier } : undefined,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
}
|
||||
|
||||
addContextualArguments<T extends any[] = any[]>(command: CommandType<T>, target: EventTarget, args: Accessor<T>): void {
|
||||
if (contextualArguments.has(command) === false) {
|
||||
contextualArguments.set(command, new WeakMap());
|
||||
}
|
||||
|
||||
contextualArguments.get(command)?.set(target, args);
|
||||
},
|
||||
|
||||
execute<T extends any[] = any[]>(command: CommandType<T>, event: Event): boolean | undefined {
|
||||
const contexts = contextualArguments.get(command);
|
||||
|
||||
if (contexts === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = event.composedPath().find(el => contexts.has(el));
|
||||
|
||||
if (element === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const args = contexts.get(element)! as Accessor<T>;
|
||||
|
||||
command(...args());
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
context.set(props.commands ?? []);
|
||||
});
|
||||
|
||||
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 command = commands.values().find(c => c.shortcut?.key === key && (c.shortcut.modifier === undefined || c.shortcut.modifier === modifiers));
|
||||
|
||||
if (command === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
return context.execute(command, e);
|
||||
};
|
||||
|
||||
return <CommandContext.Provider value={context}>
|
||||
<div tabIndex={0} style="display: contents;" onKeyDown={listener}>{props.children}</div>
|
||||
</CommandContext.Provider>;
|
||||
};
|
||||
|
||||
export const noop = Object.defineProperties(createCommand('noop', () => { }), {
|
||||
withLabel: {
|
||||
value(label: string) {
|
||||
return createCommand(label, () => { });
|
||||
},
|
||||
configurable: false,
|
||||
writable: false,
|
||||
},
|
||||
}) as CommandType & { withLabel(label: string): CommandType };
|
||||
const Add: Component<{ command: CommandType<any[]> } | { commands: CommandType<any[]>[] }> = (props) => {
|
||||
const context = useCommands();
|
||||
const commands = createMemo<CommandType<any[]>[]>(() => props.commands ?? [props.command]);
|
||||
|
||||
export const Command: Component<{ command: CommandType }> = (props) => {
|
||||
createEffect(() => {
|
||||
context?.set(commands());
|
||||
});
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const Context = <T extends any[] = any[]>(props: ParentProps<{ for: CommandType<T>, with: T }>): JSX.Element => {
|
||||
const resolved = children(() => props.children);
|
||||
const context = useCommands();
|
||||
const args = createMemo(() => props.with);
|
||||
|
||||
createEffect(() => {
|
||||
const children = resolved();
|
||||
|
||||
if (Array.isArray(children) || !(children instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
context?.addContextualArguments(props.for, children, args);
|
||||
});
|
||||
|
||||
return <>{resolved()}</>;
|
||||
};
|
||||
|
||||
const Handle: Component<{ command: CommandType }> = (props) => {
|
||||
return <>
|
||||
{props.command.label}
|
||||
<Show when={props.command.shortcut}>{
|
||||
|
@ -58,4 +125,67 @@ export const Command: Component<{ command: CommandType }> = (props) => {
|
|||
</>;
|
||||
};
|
||||
|
||||
export const Command = { Root, Handle, Add, Context };
|
||||
|
||||
export enum Modifier {
|
||||
None = 0,
|
||||
Shift = 1 << 0,
|
||||
Control = 1 << 1,
|
||||
Meta = 1 << 2,
|
||||
Alt = 1 << 3,
|
||||
}
|
||||
|
||||
export interface CommandType<TArgs extends any[] = []> {
|
||||
(...args: TArgs): any;
|
||||
label: string;
|
||||
shortcut?: {
|
||||
key: string;
|
||||
modifier: Modifier;
|
||||
};
|
||||
}
|
||||
|
||||
export const createCommand = <TArgs extends any[] = []>(label: string, command: (...args: TArgs) => any, shortcut?: CommandType['shortcut']): CommandType<TArgs> => {
|
||||
return Object.defineProperties(command as CommandType<TArgs>, {
|
||||
label: {
|
||||
value: label,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
},
|
||||
shortcut: {
|
||||
value: shortcut ? { key: shortcut.key.toLowerCase(), modifier: shortcut.modifier } : undefined,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const commandArguments = <T extends any[] = any[]>(element: Element, commandAndArgs: Accessor<[CommandType<T>, T]>) => {
|
||||
const ctx = useContext(CommandContext);
|
||||
const args = createMemo(() => commandAndArgs()[1]);
|
||||
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.addContextualArguments(commandAndArgs()[0], element, args);
|
||||
}
|
||||
|
||||
export const noop = Object.defineProperties(createCommand('noop', () => { }), {
|
||||
withLabel: {
|
||||
value(label: string) {
|
||||
return createCommand(label, () => { });
|
||||
},
|
||||
configurable: false,
|
||||
writable: false,
|
||||
},
|
||||
}) as CommandType & { withLabel(label: string): CommandType };
|
||||
|
||||
declare module "solid-js" {
|
||||
namespace JSX {
|
||||
interface Directives {
|
||||
commandArguments<T extends any[] = any[]>(): [CommandType<T>, T];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { Context } from './contextMenu';
|
|
@ -3,7 +3,6 @@ import { Portal } from "solid-js/web";
|
|||
import { createStore } from "solid-js/store";
|
||||
import { CommandType, Command } from "../command";
|
||||
import css from "./index.module.css";
|
||||
import { join } from "vinxi/dist/types/lib/path";
|
||||
|
||||
export interface MenuContextType {
|
||||
ref: Accessor<Node | undefined>;
|
||||
|
@ -61,7 +60,9 @@ export const MenuProvider: ParentComponent<{ commands?: CommandType[] }> = (prop
|
|||
},
|
||||
};
|
||||
|
||||
return <MenuContext.Provider value={ctx}>{props.children}</MenuContext.Provider>;
|
||||
return <Command.Root commands={ctx.commands()}>
|
||||
<MenuContext.Provider value={ctx}>{props.children}</MenuContext.Provider>
|
||||
</Command.Root>;
|
||||
}
|
||||
|
||||
const useMenu = () => {
|
||||
|
@ -127,7 +128,7 @@ const Root: ParentComponent<{}> = (props) => {
|
|||
|
||||
const Child: Component<{ command: CommandType }> = (props) => {
|
||||
return <button class={css.item} type="button" onpointerdown={onExecute(props.command)}>
|
||||
<Command command={props.command} />
|
||||
<Command.Handle command={props.command} />
|
||||
</button>
|
||||
};
|
||||
|
||||
|
@ -177,43 +178,10 @@ const Root: ParentComponent<{}> = (props) => {
|
|||
</Portal>
|
||||
};
|
||||
|
||||
declare module "solid-js" {
|
||||
namespace JSX {
|
||||
interface HTMLAttributes<T> {
|
||||
anchor?: string | undefined;
|
||||
}
|
||||
|
||||
interface Directives {
|
||||
asMenuRoot: true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Mount: Component = (props) => {
|
||||
const menu = useMenu();
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
return <div class={css.root} ref={menu.setRef} onKeyDown={listener}></div>;
|
||||
return <div class={css.root} ref={menu.setRef} />;
|
||||
};
|
||||
|
||||
export const Menu = { Mount, Root, Item, Separator } as const;
|
||||
|
@ -374,3 +342,11 @@ function SearchableList<T>(props: SearchableListProps<T>): JSX.Element {
|
|||
</output>
|
||||
</form>;
|
||||
};
|
||||
|
||||
declare module "solid-js" {
|
||||
namespace JSX {
|
||||
interface HTMLAttributes<T> {
|
||||
anchor?: string | undefined;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -177,22 +177,7 @@ const Root: ParentComponent = (props) => {
|
|||
});
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener('keydown', onKeyboardEvent);
|
||||
document.addEventListener('keyup', onKeyboardEvent);
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
if (isServer) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.removeEventListener('keydown', onKeyboardEvent);
|
||||
document.removeEventListener('keyup', onKeyboardEvent);
|
||||
});
|
||||
|
||||
return <div ref={setRoot} style={{ 'display': 'contents' }}>{c()}</div>;
|
||||
// return <div ref={setRoot}>{c()}</div>;
|
||||
return <div ref={setRoot} tabIndex={0} onKeyDown={onKeyboardEvent} onKeyUp={onKeyboardEvent} style={{ 'display': 'contents' }}>{c()}</div>;
|
||||
};
|
||||
|
||||
export const selectable = (element: HTMLElement, options: Accessor<{ value: object, key?: string }>) => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue