refactor command feature
This commit is contained in:
parent
824d98b9c8
commit
6e17401992
8 changed files with 216 additions and 378 deletions
51
src/features/command/command.ts
Normal file
51
src/features/command/command.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { DictionaryKey } from "../i18n";
|
||||||
|
|
||||||
|
export enum Modifier {
|
||||||
|
None = 0,
|
||||||
|
Shift = 1 << 0,
|
||||||
|
Control = 1 << 1,
|
||||||
|
Meta = 1 << 2,
|
||||||
|
Alt = 1 << 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommandType<T extends (...args: any[]) => any = (...args: any[]) => any> {
|
||||||
|
(...args: Parameters<T>): Promise<ReturnType<T>>;
|
||||||
|
label: DictionaryKey;
|
||||||
|
shortcut?: {
|
||||||
|
key: string;
|
||||||
|
modifier: Modifier;
|
||||||
|
};
|
||||||
|
withLabel(label: string): CommandType<T>;
|
||||||
|
with<A extends any[], B extends any[]>(this: (this: ThisParameterType<T>, ...args: [...A, ...B]) => ReturnType<T>, ...args: A): CommandType<(...args: B) => ReturnType<T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createCommand = <T extends (...args: any[]) => any>(label: DictionaryKey, command: T, shortcut?: CommandType['shortcut']): CommandType<T> => {
|
||||||
|
return Object.defineProperties(((...args: Parameters<T>) => command(...args)) as any, {
|
||||||
|
label: {
|
||||||
|
value: label,
|
||||||
|
configurable: false,
|
||||||
|
writable: false,
|
||||||
|
},
|
||||||
|
shortcut: {
|
||||||
|
value: shortcut ? { key: shortcut.key.toLowerCase(), modifier: shortcut.modifier } : undefined,
|
||||||
|
configurable: false,
|
||||||
|
writable: false,
|
||||||
|
},
|
||||||
|
withLabel: {
|
||||||
|
value(label: DictionaryKey) {
|
||||||
|
return createCommand(label, command, shortcut);
|
||||||
|
},
|
||||||
|
configurable: false,
|
||||||
|
writable: false,
|
||||||
|
},
|
||||||
|
with: {
|
||||||
|
value<A extends any[], B extends any[]>(this: (this: ThisParameterType<T>, ...args: [...A, ...B]) => ReturnType<T>, ...args: A): CommandType<(...args: B) => ReturnType<T>> {
|
||||||
|
return createCommand(label, command.bind(undefined, ...args), shortcut);
|
||||||
|
},
|
||||||
|
configurable: false,
|
||||||
|
writable: false,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const noop = createCommand('noop' as any, () => { });
|
154
src/features/command/context.tsx
Normal file
154
src/features/command/context.tsx
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
import { Accessor, children, Component, createContext, createEffect, createMemo, For, JSX, ParentComponent, ParentProps, Show, useContext } from 'solid-js';
|
||||||
|
import { useI18n } from '../i18n';
|
||||||
|
import { createStore } from 'solid-js/store';
|
||||||
|
import { CommandType, Modifier } from './command';
|
||||||
|
|
||||||
|
interface CommandContextType {
|
||||||
|
readonly commands: Accessor<CommandType[]>;
|
||||||
|
set(commands: CommandType<any>[]): void;
|
||||||
|
addContextualArguments<T extends (...args: any[]) => any = any>(command: CommandType<T>, target: EventTarget, args: Accessor<Parameters<T>>): void;
|
||||||
|
execute<T extends (...args: any[]) => any = any>(command: CommandType<T>, event: Event): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CommandContextStateType {
|
||||||
|
commands: CommandType[];
|
||||||
|
contextualArguments: Map<CommandType, WeakMap<EventTarget, Accessor<any[]>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CommandContext = createContext<CommandContextType>();
|
||||||
|
|
||||||
|
export const useCommands = () => useContext(CommandContext);
|
||||||
|
|
||||||
|
const Root: ParentComponent<{ commands: CommandType[] }> = (props) => {
|
||||||
|
const [store, setStore] = createStore<CommandContextStateType>({ commands: [], contextualArguments: new Map() });
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
commands: createMemo(() => store.commands),
|
||||||
|
|
||||||
|
set(commands: CommandType<any>[]): void {
|
||||||
|
setStore('commands', existing => new Set([...existing, ...commands]).values().toArray());
|
||||||
|
},
|
||||||
|
|
||||||
|
addContextualArguments<T extends (...args: any[]) => any = any>(command: CommandType<T>, target: EventTarget, args: Accessor<Parameters<T>>): void {
|
||||||
|
setStore('contextualArguments', prev => {
|
||||||
|
if (prev.has(command) === false) {
|
||||||
|
prev.set(command, new WeakMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
prev.get(command)?.set(target, args);
|
||||||
|
|
||||||
|
return new Map(prev);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
execute<T extends (...args: any[]) => any = any>(command: CommandType<T>, event: Event): boolean | undefined {
|
||||||
|
const args = ((): Parameters<T> => {
|
||||||
|
const contexts = store.contextualArguments.get(command);
|
||||||
|
|
||||||
|
if (contexts === undefined) {
|
||||||
|
return [] as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = event.composedPath().find(el => contexts.has(el));
|
||||||
|
|
||||||
|
if (element === undefined) {
|
||||||
|
return [] as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = contexts.get(element)! as Accessor<Parameters<T>>;
|
||||||
|
|
||||||
|
return args();
|
||||||
|
})();
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
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 = store.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>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Add: Component<{ command: CommandType, commands: undefined } | { commands: CommandType[] }> = (props) => {
|
||||||
|
const context = useCommands();
|
||||||
|
const commands = createMemo<CommandType[]>(() => props.commands ?? [props.command]);
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
context?.set(commands());
|
||||||
|
});
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Context = <T extends (...args: any[]) => any = any>(props: ParentProps<{ for: CommandType<T>, with: Parameters<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) => {
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
return <>
|
||||||
|
{String(t(props.command.label))}
|
||||||
|
|
||||||
|
<Show when={props.command.shortcut}>{
|
||||||
|
shortcut => {
|
||||||
|
const modifier = shortcut().modifier;
|
||||||
|
const modifierMap: Record<number, string> = {
|
||||||
|
[Modifier.Shift]: 'Shft',
|
||||||
|
[Modifier.Control]: 'Ctrl',
|
||||||
|
[Modifier.Meta]: 'Meta',
|
||||||
|
[Modifier.Alt]: 'Alt',
|
||||||
|
};
|
||||||
|
|
||||||
|
return <samp>
|
||||||
|
<For each={Object.values(Modifier).filter((m): m is number => typeof m === 'number').filter(m => modifier & m)}>{
|
||||||
|
(m) => <><kbd>{modifierMap[m]}</kbd>+</>
|
||||||
|
}</For>
|
||||||
|
<kbd>{shortcut().key}</kbd>
|
||||||
|
</samp>;
|
||||||
|
}
|
||||||
|
}</Show>
|
||||||
|
</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Command = { Root, Handle, Add, Context };
|
|
@ -1,5 +1,5 @@
|
||||||
import { Accessor, Component, createContext, createEffect, createMemo, createSignal, For, JSX, ParentComponent, splitProps, useContext } from "solid-js";
|
import { Accessor, Component, createContext, createEffect, createMemo, createSignal, For, JSX, ParentComponent, splitProps, useContext } from "solid-js";
|
||||||
import { CommandType } from "./index";
|
import { CommandType } from "./command";
|
||||||
import css from "./contextMenu.module.css";
|
import css from "./contextMenu.module.css";
|
||||||
|
|
||||||
interface ContextMenuType {
|
interface ContextMenuType {
|
||||||
|
|
|
@ -1,208 +1,7 @@
|
||||||
import { Accessor, children, Component, createContext, createEffect, createMemo, For, JSX, ParentComponent, ParentProps, Show, useContext } from 'solid-js';
|
export type { CommandType } from './command';
|
||||||
import { Dictionary, DictionaryKey, useI18n } from '../i18n';
|
|
||||||
import { createStore, produce } from 'solid-js/store';
|
|
||||||
|
|
||||||
interface CommandContextType {
|
|
||||||
readonly commands: Accessor<CommandType[]>;
|
|
||||||
set(commands: CommandType<any>[]): void;
|
|
||||||
addContextualArguments<T extends (...args: any[]) => any = any>(command: CommandType<T>, target: EventTarget, args: Accessor<Parameters<T>>): void;
|
|
||||||
execute<T extends (...args: any[]) => any = any>(command: CommandType<T>, event: Event): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CommandContextStateType {
|
|
||||||
commands: CommandType[];
|
|
||||||
contextualArguments: Map<CommandType, WeakMap<EventTarget, Accessor<any[]>>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CommandContext = createContext<CommandContextType>();
|
|
||||||
|
|
||||||
export const useCommands = () => useContext(CommandContext);
|
|
||||||
|
|
||||||
const Root: ParentComponent<{ commands: CommandType[] }> = (props) => {
|
|
||||||
const [store, setStore] = createStore<CommandContextStateType>({ commands: [], contextualArguments: new Map() });
|
|
||||||
|
|
||||||
const context = {
|
|
||||||
commands: createMemo(() => store.commands),
|
|
||||||
|
|
||||||
set(commands: CommandType<any>[]): void {
|
|
||||||
setStore('commands', existing => new Set([...existing, ...commands]).values().toArray());
|
|
||||||
},
|
|
||||||
|
|
||||||
addContextualArguments<T extends (...args: any[]) => any = any>(command: CommandType<T>, target: EventTarget, args: Accessor<Parameters<T>>): void {
|
|
||||||
setStore('contextualArguments', prev => {
|
|
||||||
if (prev.has(command) === false) {
|
|
||||||
prev.set(command, new WeakMap());
|
|
||||||
}
|
|
||||||
|
|
||||||
prev.get(command)?.set(target, args);
|
|
||||||
|
|
||||||
return new Map(prev);
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
execute<T extends (...args: any[]) => any = any>(command: CommandType<T>, event: Event): boolean | undefined {
|
|
||||||
const args = ((): Parameters<T> => {
|
|
||||||
const contexts = store.contextualArguments.get(command);
|
|
||||||
|
|
||||||
if (contexts === undefined) {
|
|
||||||
return [] as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const element = event.composedPath().find(el => contexts.has(el));
|
|
||||||
|
|
||||||
if (element === undefined) {
|
|
||||||
return [] as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const args = contexts.get(element)! as Accessor<Parameters<T>>;
|
|
||||||
|
|
||||||
return args();
|
|
||||||
})();
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
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 = store.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>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Add: Component<{ command: CommandType, commands: undefined } | { commands: CommandType[] }> = (props) => {
|
|
||||||
const context = useCommands();
|
|
||||||
const commands = createMemo<CommandType[]>(() => props.commands ?? [props.command]);
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
context?.set(commands());
|
|
||||||
});
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Context = <T extends (...args: any[]) => any = any>(props: ParentProps<{ for: CommandType<T>, with: Parameters<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) => {
|
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
return <>
|
|
||||||
{String(t(props.command.label))}
|
|
||||||
|
|
||||||
<Show when={props.command.shortcut}>{
|
|
||||||
shortcut => {
|
|
||||||
const modifier = shortcut().modifier;
|
|
||||||
const modifierMap: Record<number, string> = {
|
|
||||||
[Modifier.Shift]: 'Shft',
|
|
||||||
[Modifier.Control]: 'Ctrl',
|
|
||||||
[Modifier.Meta]: 'Meta',
|
|
||||||
[Modifier.Alt]: 'Alt',
|
|
||||||
};
|
|
||||||
|
|
||||||
return <samp>
|
|
||||||
<For each={Object.values(Modifier).filter((m): m is number => typeof m === 'number').filter(m => modifier & m)}>{
|
|
||||||
(m) => <><kbd>{modifierMap[m]}</kbd>+</>
|
|
||||||
}</For>
|
|
||||||
<kbd>{shortcut().key}</kbd>
|
|
||||||
</samp>;
|
|
||||||
}
|
|
||||||
}</Show>
|
|
||||||
</>;
|
|
||||||
};
|
|
||||||
|
|
||||||
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<T extends (...args: any[]) => any = (...args: any[]) => any> {
|
|
||||||
(...args: Parameters<T>): Promise<ReturnType<T>>;
|
|
||||||
label: DictionaryKey;
|
|
||||||
shortcut?: {
|
|
||||||
key: string;
|
|
||||||
modifier: Modifier;
|
|
||||||
};
|
|
||||||
withLabel(label: string): CommandType<T>;
|
|
||||||
with<A extends any[], B extends any[]>(this: (this: ThisParameterType<T>, ...args: [...A, ...B]) => ReturnType<T>, ...args: A): CommandType<(...args: B) => ReturnType<T>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createCommand = <T extends (...args: any[]) => any>(label: DictionaryKey, command: T, shortcut?: CommandType['shortcut']): CommandType<T> => {
|
|
||||||
return Object.defineProperties(((...args: Parameters<T>) => command(...args)) as any, {
|
|
||||||
label: {
|
|
||||||
value: label,
|
|
||||||
configurable: false,
|
|
||||||
writable: false,
|
|
||||||
},
|
|
||||||
shortcut: {
|
|
||||||
value: shortcut ? { key: shortcut.key.toLowerCase(), modifier: shortcut.modifier } : undefined,
|
|
||||||
configurable: false,
|
|
||||||
writable: false,
|
|
||||||
},
|
|
||||||
withLabel: {
|
|
||||||
value(label: DictionaryKey) {
|
|
||||||
return createCommand(label, command, shortcut);
|
|
||||||
},
|
|
||||||
configurable: false,
|
|
||||||
writable: false,
|
|
||||||
},
|
|
||||||
with: {
|
|
||||||
value<A extends any[], B extends any[]>(this: (this: ThisParameterType<T>, ...args: [...A, ...B]) => ReturnType<T>, ...args: A): CommandType<(...args: B) => ReturnType<T>> {
|
|
||||||
return createCommand(label, command.bind(undefined, ...args), shortcut);
|
|
||||||
},
|
|
||||||
configurable: false,
|
|
||||||
writable: false,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const noop = createCommand('noop' as any, () => { });
|
|
||||||
|
|
||||||
|
|
||||||
export type { CommandPaletteApi } from './palette';
|
export type { CommandPaletteApi } from './palette';
|
||||||
|
|
||||||
|
export { createCommand, noop, Modifier } from './command';
|
||||||
|
export { useCommands, Command } from './context';
|
||||||
export { Context } from './contextMenu';
|
export { Context } from './contextMenu';
|
||||||
export { CommandPalette } from './palette';
|
export { CommandPalette } from './palette';
|
|
@ -1,7 +1,8 @@
|
||||||
import { Accessor, Component, createEffect, createMemo, createSignal, For, JSX, Show } from "solid-js";
|
import { Accessor, Component, createEffect, createMemo, createSignal, For, JSX, Show } from "solid-js";
|
||||||
import { CommandType, useCommands } from ".";
|
|
||||||
import css from "./palette.module.css";
|
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../i18n";
|
||||||
|
import { CommandType } from "./command";
|
||||||
|
import { useCommands } from "./context";
|
||||||
|
import css from "./palette.module.css";
|
||||||
|
|
||||||
export interface CommandPaletteApi {
|
export interface CommandPaletteApi {
|
||||||
readonly open: Accessor<boolean>;
|
readonly open: Accessor<boolean>;
|
||||||
|
@ -168,10 +169,3 @@ function SearchableList<T>(props: SearchableListProps<T>): JSX.Element {
|
||||||
let keyCounter = 0;
|
let keyCounter = 0;
|
||||||
const createUniqueId = () => `key-${keyCounter++}`;
|
const createUniqueId = () => `key-${keyCounter++}`;
|
||||||
|
|
||||||
declare module "solid-js" {
|
|
||||||
namespace JSX {
|
|
||||||
interface HTMLAttributes<T> {
|
|
||||||
anchor?: string | undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -75,7 +75,7 @@ const useMenu = () => {
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ItemProps<T extends (...args: any[]) => any> = { label: string, children: JSX.Element } | { command: CommandType<T> };
|
type ItemProps<T extends (...args: any[]) => any> = { label: string, children: JSX.Element, command: undefined } | { command: CommandType<T> };
|
||||||
|
|
||||||
function Item<T extends (...args: any[]) => any>(props: ItemProps<T>) {
|
function Item<T extends (...args: any[]) => any>(props: ItemProps<T>) {
|
||||||
const id = createUniqueId();
|
const id = createUniqueId();
|
||||||
|
@ -187,166 +187,6 @@ const Mount: Component = (props) => {
|
||||||
|
|
||||||
export const Menu = { Mount, Root, Item, Separator } as const;
|
export const Menu = { Mount, Root, Item, Separator } as const;
|
||||||
|
|
||||||
export interface CommandPaletteApi {
|
|
||||||
readonly open: Accessor<boolean>;
|
|
||||||
show(): void;
|
|
||||||
hide(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CommandPalette: Component<{ api?: (api: CommandPaletteApi) => any, onSubmit?: SubmitHandler<CommandType> }> = (props) => {
|
|
||||||
const [open, setOpen] = createSignal<boolean>(false);
|
|
||||||
const [root, setRoot] = createSignal<HTMLDialogElement>();
|
|
||||||
const [search, setSearch] = createSignal<SearchContext<CommandType>>();
|
|
||||||
const context = useMenu();
|
|
||||||
|
|
||||||
const api = {
|
|
||||||
open,
|
|
||||||
show() {
|
|
||||||
setOpen(true);
|
|
||||||
},
|
|
||||||
hide() {
|
|
||||||
setOpen(false);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
props.api?.(api);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const isOpen = open();
|
|
||||||
|
|
||||||
if (isOpen) {
|
|
||||||
search()?.clear();
|
|
||||||
root()?.showModal();
|
|
||||||
} else {
|
|
||||||
root()?.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = (command: CommandType) => {
|
|
||||||
setOpen(false);
|
|
||||||
props.onSubmit?.(command);
|
|
||||||
|
|
||||||
command();
|
|
||||||
};
|
|
||||||
|
|
||||||
return <dialog ref={setRoot} class={css.commandPalette} onClose={() => setOpen(false)}>
|
|
||||||
<SearchableList title="command palette" items={context.commands()} keySelector={item => item.label} context={setSearch} onSubmit={onSubmit}>{
|
|
||||||
(item, ctx) => <For each={item.label.split(ctx.filter())}>{
|
|
||||||
(part, index) => <>
|
|
||||||
<Show when={index() !== 0}><b>{ctx.filter()}</b></Show>
|
|
||||||
{part}
|
|
||||||
</>
|
|
||||||
}</For>
|
|
||||||
}</SearchableList>
|
|
||||||
</dialog>;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface SubmitHandler<T> {
|
|
||||||
(item: T): any;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchContext<T> {
|
|
||||||
readonly filter: Accessor<string>;
|
|
||||||
readonly results: Accessor<T[]>;
|
|
||||||
readonly value: Accessor<T | undefined>;
|
|
||||||
searchFor(term: string): void;
|
|
||||||
clear(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchableListProps<T> {
|
|
||||||
items: T[];
|
|
||||||
title?: string;
|
|
||||||
keySelector(item: T): string;
|
|
||||||
filter?: (item: T, search: string) => boolean;
|
|
||||||
children(item: T, context: SearchContext<T>): JSX.Element;
|
|
||||||
context?: (context: SearchContext<T>) => any,
|
|
||||||
onSubmit?: SubmitHandler<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SearchableList<T>(props: SearchableListProps<T>): JSX.Element {
|
|
||||||
const [term, setTerm] = createSignal<string>('');
|
|
||||||
const [input, setInput] = createSignal<HTMLInputElement>();
|
|
||||||
const [selected, setSelected] = createSignal<number>(0);
|
|
||||||
const id = createUniqueId();
|
|
||||||
|
|
||||||
const results = createMemo(() => {
|
|
||||||
const search = term();
|
|
||||||
|
|
||||||
if (search === '') {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return props.items.filter(item => props.filter ? props.filter(item, search) : props.keySelector(item).includes(search));
|
|
||||||
});
|
|
||||||
|
|
||||||
const value = createMemo(() => results().at(selected()));
|
|
||||||
|
|
||||||
const ctx = {
|
|
||||||
filter: term,
|
|
||||||
results,
|
|
||||||
value,
|
|
||||||
searchFor(term: string) {
|
|
||||||
setTerm(term);
|
|
||||||
},
|
|
||||||
clear() {
|
|
||||||
setTerm('');
|
|
||||||
setSelected(0);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
props.context?.(ctx);
|
|
||||||
});
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const length = results().length - 1;
|
|
||||||
|
|
||||||
setSelected(current => Math.min(current, length));
|
|
||||||
});
|
|
||||||
|
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'ArrowUp') {
|
|
||||||
setSelected(current => Math.max(0, current - 1));
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === 'ArrowDown') {
|
|
||||||
setSelected(current => Math.min(results().length - 1, current + 1));
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = (e: SubmitEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const v = value();
|
|
||||||
|
|
||||||
if (v === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.clear();
|
|
||||||
props.onSubmit?.(v);
|
|
||||||
};
|
|
||||||
|
|
||||||
return <search title={props.title}>
|
|
||||||
<form method="dialog" class={css.search} onkeydown={onKeyDown} onsubmit={onSubmit}>
|
|
||||||
<input id={`search-${id}`} ref={setInput} value={term()} oninput={(e) => setTerm(e.target.value)} placeholder="start typing for command" autofocus autocomplete="off" enterkeyhint="go" />
|
|
||||||
|
|
||||||
<output for={`search-${id}`}>
|
|
||||||
<For each={results()}>{
|
|
||||||
(result, index) => <div classList={{ [css.selected]: index() === selected() }}>{props.children(result, ctx)}</div>
|
|
||||||
}</For>
|
|
||||||
</output>
|
|
||||||
</form>
|
|
||||||
</search>;
|
|
||||||
};
|
|
||||||
|
|
||||||
let keyCounter = 0;
|
let keyCounter = 0;
|
||||||
const createUniqueId = () => `key-${keyCounter++}`;
|
const createUniqueId = () => `key-${keyCounter++}`;
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Accessor, children, createContext, createEffect, createMemo, createRenderEffect, createSignal, onCleanup, onMount, ParentComponent, ParentProps, Setter, Signal, useContext } from "solid-js";
|
import { Accessor, children, createContext, createEffect, createMemo, createRenderEffect, createSignal, onCleanup, onMount, ParentComponent, ParentProps, Signal, useContext } from "solid-js";
|
||||||
import { createStore } from "solid-js/store";
|
import { createStore } from "solid-js/store";
|
||||||
import { isServer } from "solid-js/web";
|
import { isServer } from "solid-js/web";
|
||||||
import css from "./index.module.css";
|
import css from "./index.module.css";
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Sidebar } from "~/components/sidebar";
|
||||||
import { emptyFolder, FolderEntry, walk as fileTreeWalk, Tree } from "~/components/filetree";
|
import { emptyFolder, FolderEntry, walk as fileTreeWalk, Tree } from "~/components/filetree";
|
||||||
import { Menu } from "~/features/menu";
|
import { Menu } from "~/features/menu";
|
||||||
import { Grid, load, useFiles } from "~/features/file";
|
import { Grid, load, useFiles } from "~/features/file";
|
||||||
import { Command, CommandType, Context, createCommand, Modifier, noop, useCommands } from "~/features/command";
|
import { Command, CommandType, Context, createCommand, Modifier } from "~/features/command";
|
||||||
import { Entry, GridApi } from "~/features/file/grid";
|
import { Entry, GridApi } from "~/features/file/grid";
|
||||||
import { Tab, Tabs } from "~/components/tabs";
|
import { Tab, Tabs } from "~/components/tabs";
|
||||||
import { isServer } from "solid-js/web";
|
import { isServer } from "solid-js/web";
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue