This commit is contained in:
+360
@@ -0,0 +1,360 @@
|
||||
import { type WritableBoxedValues, type ReadableBoxedValues } from "svelte-toolbelt";
|
||||
import type { CommandState } from "./types.js";
|
||||
import type { BitsKeyboardEvent, BitsMouseEvent, BitsPointerEvent, RefAttachment, WithRefOpts } from "../../internal/types.js";
|
||||
interface GridItem {
|
||||
index: number;
|
||||
firstRowOfGroup: boolean;
|
||||
ref: HTMLElement;
|
||||
}
|
||||
type ItemsGrid = GridItem[][];
|
||||
interface CommandRootStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
||||
filter: (value: string, search: string, keywords?: string[]) => number;
|
||||
shouldFilter: boolean;
|
||||
loop: boolean;
|
||||
vimBindings: boolean;
|
||||
columns: number | null;
|
||||
disablePointerSelection: boolean;
|
||||
disableInitialScroll: boolean;
|
||||
onStateChange?: (state: Readonly<CommandState>) => void;
|
||||
}>, WritableBoxedValues<{
|
||||
value: string;
|
||||
}> {
|
||||
}
|
||||
export declare class CommandRootState {
|
||||
#private;
|
||||
static create(opts: CommandRootStateOpts): CommandRootState;
|
||||
readonly opts: CommandRootStateOpts;
|
||||
readonly attachment: RefAttachment;
|
||||
sortAfterTick: boolean;
|
||||
sortAndFilterAfterTick: boolean;
|
||||
allItems: Set<string>;
|
||||
allGroups: Map<string, Set<string>>;
|
||||
allIds: Map<string, {
|
||||
value: string;
|
||||
keywords?: string[];
|
||||
}>;
|
||||
key: number;
|
||||
viewportNode: HTMLElement | null;
|
||||
inputNode: HTMLElement | null;
|
||||
labelNode: HTMLElement | null;
|
||||
commandState: CommandState;
|
||||
_commandState: CommandState;
|
||||
setState<K extends keyof CommandState>(key: K, value: CommandState[K], preventScroll?: boolean): void;
|
||||
constructor(opts: CommandRootStateOpts);
|
||||
/**
|
||||
* Sets current value and triggers re-render if cleared.
|
||||
*
|
||||
* @param value - New value to set
|
||||
*/
|
||||
setValue(value: string, opts?: boolean): void;
|
||||
/**
|
||||
* Gets all non-disabled, visible command items.
|
||||
*
|
||||
* @returns Array of valid item elements
|
||||
* @remarks Exposed for direct item access and bound checking
|
||||
*/
|
||||
getValidItems(): HTMLElement[];
|
||||
/**
|
||||
* Gets all visible command items.
|
||||
*
|
||||
* @returns Array of valid item elements
|
||||
* @remarks Exposed for direct item access and bound checking
|
||||
*/
|
||||
getVisibleItems(): HTMLElement[];
|
||||
/** Returns all visible items in a matrix structure
|
||||
*
|
||||
* @remarks Returns empty if the command isn't configured as a grid
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
get itemsGrid(): ItemsGrid;
|
||||
/**
|
||||
* Sets selection to item at specified index in valid items array.
|
||||
* If index is out of bounds, does nothing.
|
||||
*
|
||||
* @param index - Zero-based index of item to select
|
||||
* @remarks
|
||||
* Uses `getValidItems()` to get selectable items, filtering out disabled/hidden ones.
|
||||
* Access valid items directly via `getValidItems()` to check bounds before calling.
|
||||
*
|
||||
* @example
|
||||
* // get valid items length for bounds check
|
||||
* const items = getValidItems()
|
||||
* if (index < items.length) {
|
||||
* updateSelectedToIndex(index)
|
||||
* }
|
||||
*/
|
||||
updateSelectedToIndex(index: number): void;
|
||||
/**
|
||||
* Updates selected item by moving up/down relative to current selection.
|
||||
* Handles wrapping when loop option is enabled.
|
||||
*
|
||||
* @param change - Direction to move: 1 for next item, -1 for previous item
|
||||
* @remarks
|
||||
* The loop behavior wraps:
|
||||
* - From last item to first when moving next
|
||||
* - From first item to last when moving previous
|
||||
*
|
||||
* Uses `getValidItems()` to get all selectable items, which filters out disabled/hidden items.
|
||||
* You can call `getValidItems()` directly to get the current valid items array.
|
||||
*
|
||||
* @example
|
||||
* // select next item
|
||||
* updateSelectedByItem(1)
|
||||
*
|
||||
* // get all valid items
|
||||
* const items = getValidItems()
|
||||
*/
|
||||
updateSelectedByItem(change: number): void;
|
||||
/**
|
||||
* Moves selection to the first valid item in the next/previous group.
|
||||
* If no group is found, falls back to selecting the next/previous item globally.
|
||||
*
|
||||
* @param change - Direction to move: 1 for next group, -1 for previous group
|
||||
* @example
|
||||
* // move to first item in next group
|
||||
* updateSelectedByGroup(1)
|
||||
*
|
||||
* // move to first item in previous group
|
||||
* updateSelectedByGroup(-1)
|
||||
*/
|
||||
updateSelectedByGroup(change: 1 | -1): void;
|
||||
/**
|
||||
* Maps item id to display value and search keywords.
|
||||
* Returns cleanup function to remove mapping.
|
||||
*
|
||||
* @param id - Unique item identifier
|
||||
* @param value - Display text
|
||||
* @param keywords - Optional search boost terms
|
||||
* @returns Cleanup function
|
||||
*/
|
||||
registerValue(value: string, keywords?: string[]): () => void;
|
||||
/**
|
||||
* Registers item in command list and its group.
|
||||
* Handles filtering, sorting and selection updates.
|
||||
*
|
||||
* @param id - Item identifier
|
||||
* @param groupId - Optional group to add item to
|
||||
* @returns Cleanup function that handles selection
|
||||
*/
|
||||
registerItem(id: string, groupId: string | undefined): () => void;
|
||||
/**
|
||||
* Creates empty group if not exists.
|
||||
*
|
||||
* @param id - Group identifier
|
||||
* @returns Cleanup function
|
||||
*/
|
||||
registerGroup(id: string): () => void;
|
||||
get isGrid(): boolean;
|
||||
onkeydown(e: BitsKeyboardEvent): void;
|
||||
props: {
|
||||
readonly id: string;
|
||||
readonly role: "application";
|
||||
readonly tabindex: -1;
|
||||
readonly onkeydown: (e: BitsKeyboardEvent) => void;
|
||||
};
|
||||
}
|
||||
interface CommandEmptyStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
||||
forceMount: boolean;
|
||||
}> {
|
||||
}
|
||||
export declare class CommandEmptyState {
|
||||
#private;
|
||||
static create(opts: CommandEmptyStateOpts): CommandEmptyState;
|
||||
readonly opts: CommandEmptyStateOpts;
|
||||
readonly root: CommandRootState;
|
||||
readonly attachment: RefAttachment;
|
||||
readonly shouldRender: boolean;
|
||||
constructor(opts: CommandEmptyStateOpts, root: CommandRootState);
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly role: "presentation";
|
||||
};
|
||||
}
|
||||
interface CommandGroupContainerStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
||||
value: string;
|
||||
forceMount: boolean;
|
||||
}> {
|
||||
}
|
||||
export declare class CommandGroupContainerState {
|
||||
static create(opts: CommandGroupContainerStateOpts): CommandGroupContainerState;
|
||||
readonly opts: CommandGroupContainerStateOpts;
|
||||
readonly root: CommandRootState;
|
||||
readonly attachment: RefAttachment;
|
||||
readonly shouldRender: boolean;
|
||||
headingNode: HTMLElement | null;
|
||||
trueValue: string;
|
||||
constructor(opts: CommandGroupContainerStateOpts, root: CommandRootState);
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly role: "presentation";
|
||||
readonly hidden: true | undefined;
|
||||
readonly "data-value": string;
|
||||
};
|
||||
}
|
||||
interface CommandGroupHeadingStateOpts extends WithRefOpts {
|
||||
}
|
||||
export declare class CommandGroupHeadingState {
|
||||
static create(opts: CommandGroupHeadingStateOpts): CommandGroupHeadingState;
|
||||
readonly opts: CommandGroupHeadingStateOpts;
|
||||
readonly group: CommandGroupContainerState;
|
||||
readonly attachment: RefAttachment;
|
||||
constructor(opts: CommandGroupHeadingStateOpts, group: CommandGroupContainerState);
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
};
|
||||
}
|
||||
interface CommandGroupItemsStateOpts extends WithRefOpts {
|
||||
}
|
||||
export declare class CommandGroupItemsState {
|
||||
static create(opts: CommandGroupItemsStateOpts): CommandGroupItemsState;
|
||||
readonly opts: CommandGroupItemsStateOpts;
|
||||
readonly group: CommandGroupContainerState;
|
||||
readonly attachment: RefAttachment;
|
||||
constructor(opts: CommandGroupItemsStateOpts, group: CommandGroupContainerState);
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly role: "group";
|
||||
readonly "aria-labelledby": string | undefined;
|
||||
};
|
||||
}
|
||||
interface CommandInputStateOpts extends WithRefOpts, WritableBoxedValues<{
|
||||
value: string;
|
||||
}>, ReadableBoxedValues<{
|
||||
autofocus: boolean;
|
||||
}> {
|
||||
}
|
||||
export declare class CommandInputState {
|
||||
#private;
|
||||
static create(opts: CommandInputStateOpts): CommandInputState;
|
||||
readonly opts: CommandInputStateOpts;
|
||||
readonly root: CommandRootState;
|
||||
readonly attachment: RefAttachment;
|
||||
constructor(opts: CommandInputStateOpts, root: CommandRootState);
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly type: "text";
|
||||
readonly autocomplete: "off";
|
||||
readonly autocorrect: "off";
|
||||
readonly spellcheck: false;
|
||||
readonly "aria-autocomplete": "list";
|
||||
readonly role: "combobox";
|
||||
readonly "aria-expanded": "true" | "false";
|
||||
readonly "aria-controls": string | undefined;
|
||||
readonly "aria-labelledby": string | undefined;
|
||||
readonly "aria-activedescendant": string | undefined;
|
||||
};
|
||||
}
|
||||
interface CommandItemStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
||||
value: string;
|
||||
disabled: boolean;
|
||||
onSelect: () => void;
|
||||
forceMount: boolean;
|
||||
keywords: string[];
|
||||
}> {
|
||||
group: CommandGroupContainerState | null;
|
||||
}
|
||||
export declare class CommandItemState {
|
||||
#private;
|
||||
static create(opts: Omit<CommandItemStateOpts, "group">): CommandItemState;
|
||||
readonly opts: CommandItemStateOpts;
|
||||
readonly root: CommandRootState;
|
||||
readonly attachment: RefAttachment;
|
||||
readonly shouldRender: boolean;
|
||||
readonly isSelected: boolean;
|
||||
trueValue: string;
|
||||
constructor(opts: CommandItemStateOpts, root: CommandRootState);
|
||||
onpointermove(_: BitsPointerEvent): void;
|
||||
onclick(_: BitsMouseEvent): void;
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly "aria-disabled": "true" | "false";
|
||||
readonly "aria-selected": "true" | "false";
|
||||
readonly "data-disabled": "" | undefined;
|
||||
readonly "data-selected": "" | undefined;
|
||||
readonly "data-value": string;
|
||||
readonly "data-group": string | undefined;
|
||||
readonly role: "option";
|
||||
readonly onpointermove: (_: BitsPointerEvent) => void;
|
||||
readonly onclick: (_: BitsMouseEvent) => void;
|
||||
};
|
||||
}
|
||||
interface CommandLoadingStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
||||
progress: number;
|
||||
}> {
|
||||
}
|
||||
export declare class CommandLoadingState {
|
||||
static create(opts: CommandLoadingStateOpts): CommandLoadingState;
|
||||
readonly opts: CommandLoadingStateOpts;
|
||||
readonly attachment: RefAttachment;
|
||||
constructor(opts: CommandLoadingStateOpts);
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly role: "progressbar";
|
||||
readonly "aria-valuenow": number;
|
||||
readonly "aria-valuemin": 0;
|
||||
readonly "aria-valuemax": 100;
|
||||
readonly "aria-label": "Loading...";
|
||||
};
|
||||
}
|
||||
interface CommandSeparatorStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
||||
forceMount: boolean;
|
||||
}> {
|
||||
}
|
||||
export declare class CommandSeparatorState {
|
||||
static create(opts: CommandSeparatorStateOpts): CommandSeparatorState;
|
||||
readonly opts: CommandSeparatorStateOpts;
|
||||
readonly root: CommandRootState;
|
||||
readonly attachment: RefAttachment;
|
||||
readonly shouldRender: boolean;
|
||||
constructor(opts: CommandSeparatorStateOpts, root: CommandRootState);
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly "aria-hidden": "true";
|
||||
};
|
||||
}
|
||||
interface CommandListStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
||||
ariaLabel: string;
|
||||
}> {
|
||||
}
|
||||
export declare class CommandListState {
|
||||
static create(opts: CommandListStateOpts): CommandListState;
|
||||
readonly opts: CommandListStateOpts;
|
||||
readonly root: CommandRootState;
|
||||
readonly attachment: RefAttachment;
|
||||
constructor(opts: CommandListStateOpts, root: CommandRootState);
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly role: "listbox";
|
||||
readonly "aria-label": string;
|
||||
};
|
||||
}
|
||||
interface CommandLabelStateOpts extends WithRefOpts, ReadableBoxedValues<{
|
||||
for?: string;
|
||||
}> {
|
||||
}
|
||||
export declare class CommandLabelState {
|
||||
static create(opts: CommandLabelStateOpts): CommandLabelState;
|
||||
readonly opts: CommandLabelStateOpts;
|
||||
readonly root: CommandRootState;
|
||||
readonly attachment: RefAttachment;
|
||||
constructor(opts: CommandLabelStateOpts, root: CommandRootState);
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly for: string | undefined;
|
||||
readonly style: import("svelte-toolbelt").StyleProperties;
|
||||
};
|
||||
}
|
||||
interface CommandViewportStateOpts extends WithRefOpts {
|
||||
}
|
||||
export declare class CommandViewportState {
|
||||
static create(opts: CommandViewportStateOpts): CommandViewportState;
|
||||
readonly opts: CommandViewportStateOpts;
|
||||
readonly list: CommandListState;
|
||||
readonly attachment: RefAttachment;
|
||||
constructor(opts: CommandViewportStateOpts, list: CommandListState);
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
};
|
||||
}
|
||||
export {};
|
||||
+1325
File diff suppressed because it is too large
Load Diff
+31
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { boxWith } from "svelte-toolbelt";
|
||||
import { CommandLabelState } from "../command.svelte.js";
|
||||
|
||||
import type { WithChildren } from "../../../internal/types.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import { mergeProps } from "svelte-toolbelt";
|
||||
import type { BitsPrimitiveLabelAttributes, WithElementRef } from "../../../shared/index.js";
|
||||
|
||||
const uid = $props.id();
|
||||
let {
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
...restProps
|
||||
}: WithChildren<WithElementRef<BitsPrimitiveLabelAttributes>> = $props();
|
||||
|
||||
const labelState = CommandLabelState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, labelState.props));
|
||||
</script>
|
||||
|
||||
<label {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</label>
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import type { WithChildren } from "../../../internal/types.js";
|
||||
import type { BitsPrimitiveLabelAttributes, WithElementRef } from "../../../shared/index.js";
|
||||
declare const CommandLabel: import("svelte").Component<WithChildren<WithElementRef<BitsPrimitiveLabelAttributes>>, {}, "ref">;
|
||||
type CommandLabel = ReturnType<typeof CommandLabel>;
|
||||
export default CommandLabel;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { CommandEmptyProps } from "../types.js";
|
||||
import { CommandEmptyState } from "../command.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
child,
|
||||
forceMount = false,
|
||||
...restProps
|
||||
}: CommandEmptyProps = $props();
|
||||
|
||||
const emptyState = CommandEmptyState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
forceMount: boxWith(() => forceMount),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(emptyState.props, restProps));
|
||||
</script>
|
||||
|
||||
{#if emptyState.shouldRender}
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { CommandEmptyProps } from "../types.js";
|
||||
declare const CommandEmpty: import("svelte").Component<CommandEmptyProps, {}, "ref">;
|
||||
type CommandEmpty = ReturnType<typeof CommandEmpty>;
|
||||
export default CommandEmpty;
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import { CommandGroupHeadingState } from "../command.svelte.js";
|
||||
import type { CommandGroupHeadingProps } from "../types.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
child,
|
||||
...restProps
|
||||
}: CommandGroupHeadingProps = $props();
|
||||
|
||||
const headingState = CommandGroupHeadingState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, headingState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { CommandGroupHeadingProps } from "../types.js";
|
||||
declare const CommandGroupHeading: import("svelte").Component<CommandGroupHeadingProps, {}, "ref">;
|
||||
type CommandGroupHeading = ReturnType<typeof CommandGroupHeading>;
|
||||
export default CommandGroupHeading;
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { CommandGroupItemsProps } from "../types.js";
|
||||
import { CommandGroupItemsState } from "../command.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
child,
|
||||
...restProps
|
||||
}: CommandGroupItemsProps = $props();
|
||||
|
||||
const groupItemsState = CommandGroupItemsState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, groupItemsState.props));
|
||||
</script>
|
||||
|
||||
<div style="display: contents;">
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { CommandGroupItemsProps } from "../types.js";
|
||||
declare const CommandGroupItems: import("svelte").Component<CommandGroupItemsProps, {}, "ref">;
|
||||
type CommandGroupItems = ReturnType<typeof CommandGroupItems>;
|
||||
export default CommandGroupItems;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { CommandGroupProps } from "../types.js";
|
||||
import { CommandGroupContainerState } from "../command.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
value = "",
|
||||
forceMount = false,
|
||||
children,
|
||||
child,
|
||||
...restProps
|
||||
}: CommandGroupProps = $props();
|
||||
|
||||
const groupState = CommandGroupContainerState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
forceMount: boxWith(() => forceMount),
|
||||
value: boxWith(() => value),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, groupState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { CommandGroupProps } from "../types.js";
|
||||
declare const CommandGroup: import("svelte").Component<CommandGroupProps, {}, "ref">;
|
||||
type CommandGroup = ReturnType<typeof CommandGroup>;
|
||||
export default CommandGroup;
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { CommandInputProps } from "../types.js";
|
||||
import { CommandInputState } from "../command.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
value = $bindable(""),
|
||||
autofocus = false,
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
child,
|
||||
...restProps
|
||||
}: CommandInputProps = $props();
|
||||
|
||||
const inputState = CommandInputState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
value: boxWith(
|
||||
() => value,
|
||||
(v) => {
|
||||
value = v;
|
||||
}
|
||||
),
|
||||
autofocus: boxWith(() => autofocus ?? false),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, inputState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<input {...mergedProps} bind:value />
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { CommandInputProps } from "../types.js";
|
||||
declare const CommandInput: import("svelte").Component<CommandInputProps, {}, "value" | "ref">;
|
||||
type CommandInput = ReturnType<typeof CommandInput>;
|
||||
export default CommandInput;
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { CommandItemProps } from "../types.js";
|
||||
import { CommandItemState } from "../command.svelte.js";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
value = "",
|
||||
disabled = false,
|
||||
children,
|
||||
child,
|
||||
onSelect = noop,
|
||||
forceMount = false,
|
||||
keywords = [],
|
||||
...restProps
|
||||
}: CommandItemProps = $props();
|
||||
|
||||
const itemState = CommandItemState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
value: boxWith(() => value),
|
||||
disabled: boxWith(() => disabled),
|
||||
onSelect: boxWith(() => onSelect),
|
||||
forceMount: boxWith(() => forceMount),
|
||||
keywords: boxWith(() => keywords),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, itemState.props));
|
||||
</script>
|
||||
|
||||
{#key itemState.root.key}
|
||||
<div style="display: contents;" data-item-wrapper data-value={itemState.trueValue}>
|
||||
{#if itemState.shouldRender}
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/key}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { CommandItemProps } from "../types.js";
|
||||
declare const CommandItem: import("svelte").Component<CommandItemProps, {}, "ref">;
|
||||
type CommandItem = ReturnType<typeof CommandItem>;
|
||||
export default CommandItem;
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { CommandLinkItemProps } from "../types.js";
|
||||
import { CommandItemState } from "../command.svelte.js";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
value = "",
|
||||
disabled = false,
|
||||
children,
|
||||
child,
|
||||
onSelect = noop,
|
||||
forceMount = false,
|
||||
keywords = [],
|
||||
...restProps
|
||||
}: CommandLinkItemProps = $props();
|
||||
|
||||
const itemState = CommandItemState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
value: boxWith(() => value),
|
||||
disabled: boxWith(() => disabled),
|
||||
onSelect: boxWith(() => onSelect),
|
||||
forceMount: boxWith(() => forceMount),
|
||||
keywords: boxWith(() => keywords),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, itemState.props));
|
||||
</script>
|
||||
|
||||
{#key itemState.root.key}
|
||||
<div style="display: contents;">
|
||||
{#if itemState.shouldRender}
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<a {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/key}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { CommandLinkItemProps } from "../types.js";
|
||||
declare const CommandLinkItem: import("svelte").Component<CommandLinkItemProps, {}, "ref">;
|
||||
type CommandLinkItem = ReturnType<typeof CommandLinkItem>;
|
||||
export default CommandLinkItem;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { CommandListProps } from "../types.js";
|
||||
import { CommandListState } from "../command.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
child,
|
||||
children,
|
||||
"aria-label": ariaLabel,
|
||||
...restProps
|
||||
}: CommandListProps = $props();
|
||||
|
||||
const listState = CommandListState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
ariaLabel: boxWith(() => ariaLabel ?? "Suggestions..."),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, listState.props));
|
||||
</script>
|
||||
|
||||
{#key listState.root._commandState.search === ""}
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
{/key}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { CommandListProps } from "../types.js";
|
||||
declare const CommandList: import("svelte").Component<CommandListProps, {}, "ref">;
|
||||
type CommandList = ReturnType<typeof CommandList>;
|
||||
export default CommandList;
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { CommandLoadingProps } from "../types.js";
|
||||
import { CommandLoadingState } from "../command.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
progress = 0,
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
child,
|
||||
...restProps
|
||||
}: CommandLoadingProps = $props();
|
||||
|
||||
const loadingState = CommandLoadingState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
progress: boxWith(() => progress),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, loadingState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { CommandLoadingProps } from "../types.js";
|
||||
declare const CommandLoading: import("svelte").Component<CommandLoadingProps, {}, "ref">;
|
||||
type CommandLoading = ReturnType<typeof CommandLoading>;
|
||||
export default CommandLoading;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { CommandSeparatorProps } from "../types.js";
|
||||
import { CommandSeparatorState } from "../command.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
forceMount = false,
|
||||
children,
|
||||
child,
|
||||
...restProps
|
||||
}: CommandSeparatorProps = $props();
|
||||
|
||||
const separatorState = CommandSeparatorState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
forceMount: boxWith(() => forceMount),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, separatorState.props));
|
||||
</script>
|
||||
|
||||
{#if separatorState.shouldRender}
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { CommandSeparatorProps } from "../types.js";
|
||||
declare const CommandSeparator: import("svelte").Component<CommandSeparatorProps, {}, "ref">;
|
||||
type CommandSeparator = ReturnType<typeof CommandSeparator>;
|
||||
export default CommandSeparator;
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import { CommandViewportState } from "../command.svelte.js";
|
||||
import type { CommandViewportProps } from "../types.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
child,
|
||||
...restProps
|
||||
}: CommandViewportProps = $props();
|
||||
|
||||
const listViewportState = CommandViewportState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, listViewportState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { CommandViewportProps } from "../types.js";
|
||||
declare const CommandViewport: import("svelte").Component<CommandViewportProps, {}, "ref">;
|
||||
type CommandViewport = ReturnType<typeof CommandViewport>;
|
||||
export default CommandViewport;
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import { CommandRootState } from "../command.svelte.js";
|
||||
import type { CommandRootProps } from "../types.js";
|
||||
import CommandLabel from "./_command-label.svelte";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import { computeCommandScore } from "../index.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
value = $bindable(""),
|
||||
onValueChange = noop,
|
||||
onStateChange = noop,
|
||||
loop = false,
|
||||
shouldFilter = true,
|
||||
filter = computeCommandScore,
|
||||
label = "",
|
||||
vimBindings = true,
|
||||
disablePointerSelection = false,
|
||||
disableInitialScroll = false,
|
||||
columns = null,
|
||||
children,
|
||||
child,
|
||||
...restProps
|
||||
}: CommandRootProps = $props();
|
||||
|
||||
const rootState = CommandRootState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
filter: boxWith(() => filter),
|
||||
shouldFilter: boxWith(() => shouldFilter),
|
||||
loop: boxWith(() => loop),
|
||||
value: boxWith(
|
||||
() => value,
|
||||
(v) => {
|
||||
if (value !== v) {
|
||||
value = v;
|
||||
onValueChange(v);
|
||||
}
|
||||
}
|
||||
),
|
||||
vimBindings: boxWith(() => vimBindings),
|
||||
disablePointerSelection: boxWith(() => disablePointerSelection),
|
||||
disableInitialScroll: boxWith(() => disableInitialScroll),
|
||||
onStateChange: boxWith(() => onStateChange),
|
||||
columns: boxWith(() => columns),
|
||||
});
|
||||
|
||||
// Imperative APIs - DO NOT REMOVE OR RENAME
|
||||
|
||||
/**
|
||||
* Sets selection to item at specified index in valid items array.
|
||||
* If index is out of bounds, does nothing.
|
||||
*
|
||||
* @param index - Zero-based index of item to select
|
||||
* @remarks
|
||||
* Uses `getValidItems()` to get selectable items, filtering out disabled/hidden ones.
|
||||
* Access valid items directly via `getValidItems()` to check bounds before calling.
|
||||
*
|
||||
* @example
|
||||
* // get valid items length for bounds check
|
||||
* const items = getValidItems()
|
||||
* if (index < items.length) {
|
||||
* updateSelectedToIndex(index)
|
||||
* }
|
||||
*/
|
||||
export const updateSelectedToIndex: (typeof rootState)["updateSelectedToIndex"] = (i) =>
|
||||
rootState.updateSelectedToIndex(i);
|
||||
/**
|
||||
* Moves selection to the first valid item in the next/previous group.
|
||||
* If no group is found, falls back to selecting the next/previous item globally.
|
||||
*
|
||||
* @param change - Direction to move: 1 for next group, -1 for previous group
|
||||
* @example
|
||||
* // move to first item in next group
|
||||
* updateSelectedByGroup(1)
|
||||
*
|
||||
* // move to first item in previous group
|
||||
* updateSelectedByGroup(-1)
|
||||
*/
|
||||
export const updateSelectedByGroup: (typeof rootState)["updateSelectedByGroup"] = (c) =>
|
||||
rootState.updateSelectedByGroup(c);
|
||||
/**
|
||||
* Updates selected item by moving up/down relative to current selection.
|
||||
* Handles wrapping when loop option is enabled.
|
||||
*
|
||||
* @param change - Direction to move: 1 for next item, -1 for previous item
|
||||
* @remarks
|
||||
* The loop behavior wraps:
|
||||
* - From last item to first when moving next
|
||||
* - From first item to last when moving previous
|
||||
*
|
||||
* Uses `getValidItems()` to get all selectable items, which filters out disabled/hidden items.
|
||||
* You can call `getValidItems()` directly to get the current valid items array.
|
||||
*
|
||||
* @example
|
||||
* // select next item
|
||||
* updateSelectedByItem(1)
|
||||
*
|
||||
* // get all valid items
|
||||
* const items = getValidItems()
|
||||
*/
|
||||
export const updateSelectedByItem: (typeof rootState)["updateSelectedByItem"] = (c) =>
|
||||
rootState.updateSelectedByItem(c);
|
||||
/**
|
||||
* Gets all non-disabled, visible command items.
|
||||
*
|
||||
* @returns Array of valid item elements
|
||||
* @remarks Exposed for direct item access and bound checking
|
||||
*/
|
||||
export const getValidItems = () => rootState.getValidItems();
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, rootState.props));
|
||||
</script>
|
||||
|
||||
{#snippet Label()}
|
||||
<CommandLabel>
|
||||
{label}
|
||||
</CommandLabel>
|
||||
{/snippet}
|
||||
|
||||
{#if child}
|
||||
{@render Label()}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render Label()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import type { CommandRootProps } from "../types.js";
|
||||
declare const Command: import("svelte").Component<CommandRootProps, {
|
||||
/**
|
||||
* Sets selection to item at specified index in valid items array.
|
||||
* If index is out of bounds, does nothing.
|
||||
*
|
||||
* @param index - Zero-based index of item to select
|
||||
* @remarks
|
||||
* Uses `getValidItems()` to get selectable items, filtering out disabled/hidden ones.
|
||||
* Access valid items directly via `getValidItems()` to check bounds before calling.
|
||||
*
|
||||
* @example
|
||||
* // get valid items length for bounds check
|
||||
* const items = getValidItems()
|
||||
* if (index < items.length) {
|
||||
* updateSelectedToIndex(index)
|
||||
* }
|
||||
*/ updateSelectedToIndex: (index: number) => void;
|
||||
/**
|
||||
* Moves selection to the first valid item in the next/previous group.
|
||||
* If no group is found, falls back to selecting the next/previous item globally.
|
||||
*
|
||||
* @param change - Direction to move: 1 for next group, -1 for previous group
|
||||
* @example
|
||||
* // move to first item in next group
|
||||
* updateSelectedByGroup(1)
|
||||
*
|
||||
* // move to first item in previous group
|
||||
* updateSelectedByGroup(-1)
|
||||
*/ updateSelectedByGroup: (change: 1 | -1) => void;
|
||||
/**
|
||||
* Updates selected item by moving up/down relative to current selection.
|
||||
* Handles wrapping when loop option is enabled.
|
||||
*
|
||||
* @param change - Direction to move: 1 for next item, -1 for previous item
|
||||
* @remarks
|
||||
* The loop behavior wraps:
|
||||
* - From last item to first when moving next
|
||||
* - From first item to last when moving previous
|
||||
*
|
||||
* Uses `getValidItems()` to get all selectable items, which filters out disabled/hidden items.
|
||||
* You can call `getValidItems()` directly to get the current valid items array.
|
||||
*
|
||||
* @example
|
||||
* // select next item
|
||||
* updateSelectedByItem(1)
|
||||
*
|
||||
* // get all valid items
|
||||
* const items = getValidItems()
|
||||
*/ updateSelectedByItem: (change: number) => void;
|
||||
/**
|
||||
* Gets all non-disabled, visible command items.
|
||||
*
|
||||
* @returns Array of valid item elements
|
||||
* @remarks Exposed for direct item access and bound checking
|
||||
*/ getValidItems: () => HTMLElement[];
|
||||
}, "value" | "ref">;
|
||||
type Command = ReturnType<typeof Command>;
|
||||
export default Command;
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Given a command, a search query, and (optionally) a list of keywords for the command,
|
||||
* computes a score between 0 and 1 that represents how well the search query matches the
|
||||
* abbreviation and keywords. 1 is a perfect match, 0 is no match.
|
||||
*
|
||||
* The score is calculated based on the following rules:
|
||||
* - The scores are arranged so that a continuous match of characters will result in a total
|
||||
* score of 1. The best case, this character is a match, and either this is the start of the string
|
||||
* or the previous character was also a match.
|
||||
* - A new match at the start of a word scores better than a new match elsewhere as it's more likely
|
||||
* that the user will type the starts of fragments.
|
||||
* - Word jumps between spaces are scored slightly higher than slashes, brackets, hyphens, etc.
|
||||
* - A continuous match of characters will result in a total score of 1.
|
||||
* - A new match at the start of a word scores better than a new match elsewhere as it's more likely that the user will type the starts of fragments.
|
||||
* - Any other match isn't ideal, but we include it for completeness.
|
||||
* - If the user transposed two letters, it should be significantly penalized.
|
||||
* - The goodness of a match should decay slightly with each missing character.
|
||||
* - Match higher for letters closer to the beginning of the word.
|
||||
*
|
||||
* @param command - The value to score against the search string (e.g. a command name like "Calculator")
|
||||
* @param search - The search string to score against the value/aliases
|
||||
* @param commandKeywords - An optional list of aliases/keywords to score against the search string - e.g. ["math", "add", "divide", "multiply", "subtract"]
|
||||
* @returns A score between 0 and 1 that represents how well the search string matches the
|
||||
* command (and keywords)
|
||||
*/
|
||||
export declare function computeCommandScore(command: string, search: string, commandKeywords?: string[]): number;
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
// oxlint-disable ban-ts-comment
|
||||
// @ts-nocheck
|
||||
// The scores are arranged so that a continuous match of characters will
|
||||
// result in a total score of 1.
|
||||
//
|
||||
// The best case, this character is a match, and either this is the start
|
||||
// of the string, or the previous character was also a match.
|
||||
const SCORE_CONTINUE_MATCH = 1;
|
||||
// A new match at the start of a word scores better than a new match
|
||||
// elsewhere as it's more likely that the user will type the starts
|
||||
// of fragments.
|
||||
// NOTE: We score word jumps between spaces slightly higher than slashes, brackets
|
||||
// hyphens, etc.
|
||||
const SCORE_SPACE_WORD_JUMP = 0.9;
|
||||
const SCORE_NON_SPACE_WORD_JUMP = 0.8;
|
||||
// Any other match isn't ideal, but we include it for completeness.
|
||||
const SCORE_CHARACTER_JUMP = 0.17;
|
||||
// If the user transposed two letters, it should be significantly penalized.
|
||||
//
|
||||
// i.e. "ouch" is more likely than "curtain" when "uc" is typed.
|
||||
const SCORE_TRANSPOSITION = 0.1;
|
||||
// The goodness of a match should decay slightly with each missing
|
||||
// character.
|
||||
//
|
||||
// i.e. "bad" is more likely than "bard" when "bd" is typed.
|
||||
//
|
||||
// This will not change the order of suggestions based on SCORE_* until
|
||||
// 100 characters are inserted between matches.
|
||||
const PENALTY_SKIPPED = 0.999;
|
||||
// The goodness of an exact-case match should be higher than a
|
||||
// case-insensitive match by a small amount.
|
||||
//
|
||||
// i.e. "HTML" is more likely than "haml" when "HM" is typed.
|
||||
//
|
||||
// This will not change the order of suggestions based on SCORE_* until
|
||||
// 1000 characters are inserted between matches.
|
||||
const PENALTY_CASE_MISMATCH = 0.9999;
|
||||
// Match higher for letters closer to the beginning of the word
|
||||
// If the word has more characters than the user typed, it should
|
||||
// be penalized slightly.
|
||||
//
|
||||
// i.e. "html" is more likely than "html5" if I type "html".
|
||||
//
|
||||
// However, it may well be the case that there's a sensible secondary
|
||||
// ordering (like alphabetical) that it makes sense to rely on when
|
||||
// there are many prefix matches, so we don't make the penalty increase
|
||||
// with the number of tokens.
|
||||
const PENALTY_NOT_COMPLETE = 0.99;
|
||||
const IS_GAP_REGEXP = /[\\/_+.#"@[({&]/;
|
||||
const COUNT_GAPS_REGEXP = /[\\/_+.#"@[({&]/g;
|
||||
const IS_SPACE_REGEXP = /[\s-]/;
|
||||
const COUNT_SPACE_REGEXP = /[\s-]/g;
|
||||
function computeCommandScoreInner(string, abbreviation, lowerString, lowerAbbreviation, stringIndex, abbreviationIndex, memoizedResults) {
|
||||
if (abbreviationIndex === abbreviation.length) {
|
||||
if (stringIndex === string.length)
|
||||
return SCORE_CONTINUE_MATCH;
|
||||
return PENALTY_NOT_COMPLETE;
|
||||
}
|
||||
const memoizeKey = `${stringIndex},${abbreviationIndex}`;
|
||||
if (memoizedResults[memoizeKey] !== undefined)
|
||||
return memoizedResults[memoizeKey];
|
||||
const abbreviationChar = lowerAbbreviation.charAt(abbreviationIndex);
|
||||
let index = lowerString.indexOf(abbreviationChar, stringIndex);
|
||||
let highScore = 0;
|
||||
let score, transposedScore, wordBreaks, spaceBreaks;
|
||||
while (index >= 0) {
|
||||
score = computeCommandScoreInner(string, abbreviation, lowerString, lowerAbbreviation, index + 1, abbreviationIndex + 1, memoizedResults);
|
||||
if (score > highScore) {
|
||||
if (index === stringIndex) {
|
||||
score *= SCORE_CONTINUE_MATCH;
|
||||
}
|
||||
else if (IS_GAP_REGEXP.test(string.charAt(index - 1))) {
|
||||
score *= SCORE_NON_SPACE_WORD_JUMP;
|
||||
wordBreaks = string.slice(stringIndex, index - 1).match(COUNT_GAPS_REGEXP);
|
||||
if (wordBreaks && stringIndex > 0) {
|
||||
score *= PENALTY_SKIPPED ** wordBreaks.length;
|
||||
}
|
||||
}
|
||||
else if (IS_SPACE_REGEXP.test(string.charAt(index - 1))) {
|
||||
score *= SCORE_SPACE_WORD_JUMP;
|
||||
spaceBreaks = string.slice(stringIndex, index - 1).match(COUNT_SPACE_REGEXP);
|
||||
if (spaceBreaks && stringIndex > 0) {
|
||||
score *= PENALTY_SKIPPED ** spaceBreaks.length;
|
||||
}
|
||||
}
|
||||
else {
|
||||
score *= SCORE_CHARACTER_JUMP;
|
||||
if (stringIndex > 0) {
|
||||
score *= PENALTY_SKIPPED ** (index - stringIndex);
|
||||
}
|
||||
}
|
||||
if (string.charAt(index) !== abbreviation.charAt(abbreviationIndex)) {
|
||||
score *= PENALTY_CASE_MISMATCH;
|
||||
}
|
||||
}
|
||||
if ((score < SCORE_TRANSPOSITION &&
|
||||
lowerString.charAt(index - 1) ===
|
||||
lowerAbbreviation.charAt(abbreviationIndex + 1)) ||
|
||||
(lowerAbbreviation.charAt(abbreviationIndex + 1) ===
|
||||
lowerAbbreviation.charAt(abbreviationIndex) &&
|
||||
lowerString.charAt(index - 1) !== lowerAbbreviation.charAt(abbreviationIndex))) {
|
||||
transposedScore = computeCommandScoreInner(string, abbreviation, lowerString, lowerAbbreviation, index + 1, abbreviationIndex + 2, memoizedResults);
|
||||
if (transposedScore * SCORE_TRANSPOSITION > score) {
|
||||
score = transposedScore * SCORE_TRANSPOSITION;
|
||||
}
|
||||
}
|
||||
if (score > highScore) {
|
||||
highScore = score;
|
||||
}
|
||||
index = lowerString.indexOf(abbreviationChar, index + 1);
|
||||
}
|
||||
memoizedResults[memoizeKey] = highScore;
|
||||
return highScore;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param string
|
||||
* @returns
|
||||
*/
|
||||
function formatInput(string) {
|
||||
// convert all valid space characters to space so they match each other
|
||||
return string.toLowerCase().replace(COUNT_SPACE_REGEXP, " ");
|
||||
}
|
||||
/**
|
||||
* Given a command, a search query, and (optionally) a list of keywords for the command,
|
||||
* computes a score between 0 and 1 that represents how well the search query matches the
|
||||
* abbreviation and keywords. 1 is a perfect match, 0 is no match.
|
||||
*
|
||||
* The score is calculated based on the following rules:
|
||||
* - The scores are arranged so that a continuous match of characters will result in a total
|
||||
* score of 1. The best case, this character is a match, and either this is the start of the string
|
||||
* or the previous character was also a match.
|
||||
* - A new match at the start of a word scores better than a new match elsewhere as it's more likely
|
||||
* that the user will type the starts of fragments.
|
||||
* - Word jumps between spaces are scored slightly higher than slashes, brackets, hyphens, etc.
|
||||
* - A continuous match of characters will result in a total score of 1.
|
||||
* - A new match at the start of a word scores better than a new match elsewhere as it's more likely that the user will type the starts of fragments.
|
||||
* - Any other match isn't ideal, but we include it for completeness.
|
||||
* - If the user transposed two letters, it should be significantly penalized.
|
||||
* - The goodness of a match should decay slightly with each missing character.
|
||||
* - Match higher for letters closer to the beginning of the word.
|
||||
*
|
||||
* @param command - The value to score against the search string (e.g. a command name like "Calculator")
|
||||
* @param search - The search string to score against the value/aliases
|
||||
* @param commandKeywords - An optional list of aliases/keywords to score against the search string - e.g. ["math", "add", "divide", "multiply", "subtract"]
|
||||
* @returns A score between 0 and 1 that represents how well the search string matches the
|
||||
* command (and keywords)
|
||||
*/
|
||||
export function computeCommandScore(command, search, commandKeywords) {
|
||||
/**
|
||||
* NOTE: We used to do lower-casing on each recursive call, but this meant that `toLowerCase()`
|
||||
* was the dominating cost in the algorithm. Passing both is a little ugly, but considerably
|
||||
* faster.
|
||||
*/
|
||||
command =
|
||||
commandKeywords && commandKeywords.length > 0
|
||||
? `${`${command} ${commandKeywords?.join(" ")}`}`
|
||||
: command;
|
||||
return computeCommandScoreInner(command, search, formatInput(command), formatInput(search), 0, 0, {});
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
export { default as Root } from "./components/command.svelte";
|
||||
export { default as Empty } from "./components/command-empty.svelte";
|
||||
export { default as Group } from "./components/command-group.svelte";
|
||||
export { default as GroupHeading } from "./components/command-group-heading.svelte";
|
||||
export { default as GroupItems } from "./components/command-group-items.svelte";
|
||||
export { default as Input } from "./components/command-input.svelte";
|
||||
export { default as Item } from "./components/command-item.svelte";
|
||||
export { default as LinkItem } from "./components/command-link-item.svelte";
|
||||
export { default as List } from "./components/command-list.svelte";
|
||||
export { default as Viewport } from "./components/command-viewport.svelte";
|
||||
export { default as Loading } from "./components/command-loading.svelte";
|
||||
export { default as Separator } from "./components/command-separator.svelte";
|
||||
export type { CommandRootProps as RootProps, CommandEmptyProps as EmptyProps, CommandGroupProps as GroupProps, CommandGroupHeadingProps as GroupHeadingProps, CommandGroupItemsProps as GroupItemsProps, CommandItemProps as ItemProps, CommandLinkItemProps as LinkItemProps, CommandInputProps as InputProps, CommandSeparatorProps as SeparatorProps, CommandListProps as ListProps, CommandLoadingProps as LoadingProps, CommandViewportProps as ViewportProps, } from "./types.js";
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
export { default as Root } from "./components/command.svelte";
|
||||
export { default as Empty } from "./components/command-empty.svelte";
|
||||
export { default as Group } from "./components/command-group.svelte";
|
||||
export { default as GroupHeading } from "./components/command-group-heading.svelte";
|
||||
export { default as GroupItems } from "./components/command-group-items.svelte";
|
||||
export { default as Input } from "./components/command-input.svelte";
|
||||
export { default as Item } from "./components/command-item.svelte";
|
||||
export { default as LinkItem } from "./components/command-link-item.svelte";
|
||||
export { default as List } from "./components/command-list.svelte";
|
||||
export { default as Viewport } from "./components/command-viewport.svelte";
|
||||
export { default as Loading } from "./components/command-loading.svelte";
|
||||
export { default as Separator } from "./components/command-separator.svelte";
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export * as Command from "./exports.js";
|
||||
export { computeCommandScore } from "./compute-command-score.js";
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export * as Command from "./exports.js";
|
||||
export { computeCommandScore } from "./compute-command-score.js";
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
import type { BitsPrimitiveAnchorAttributes, BitsPrimitiveDivAttributes, BitsPrimitiveInputAttributes, WithChild, Without } from "../../shared/index.js";
|
||||
export type CommandState = {
|
||||
/** The value of the search query */
|
||||
search: string;
|
||||
/** The value of the selected command menu item */
|
||||
value: string;
|
||||
/** The filtered items */
|
||||
filtered: {
|
||||
/** The count of all visible items. */
|
||||
count: number;
|
||||
/** Map from visible item value to its search store. */
|
||||
items: Map<string, number>;
|
||||
/** Set of groups with at least one visible item. */
|
||||
groups: Set<string>;
|
||||
};
|
||||
};
|
||||
export type CommandRootPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* An accessible label for the command menu.
|
||||
* Not visible & only used for screen readers.
|
||||
*/
|
||||
label?: string;
|
||||
/**
|
||||
* Optionally set to `false` to turn off the automatic filtering
|
||||
* and sorting. If `false`, you must conditionally render valid
|
||||
* items yourself.
|
||||
*/
|
||||
shouldFilter?: boolean;
|
||||
/**
|
||||
* A custom filter function for whether each command item should
|
||||
* match the query. It should return a number between `0` and `1`,
|
||||
* with `1` being a perfect match, and `0` being no match, resulting
|
||||
* in the item being hidden entirely.
|
||||
*
|
||||
* By default, it will use the `computeCommandScore` function exported
|
||||
* by this package to compute the score.
|
||||
*/
|
||||
filter?: (value: string, search: string, keywords?: string[]) => number;
|
||||
/**
|
||||
* A function that is called when the command state changes.
|
||||
*/
|
||||
onStateChange?: (state: Readonly<CommandState>) => void;
|
||||
/**
|
||||
* Optionally provide or bind to the selected command menu item.
|
||||
*/
|
||||
value?: string;
|
||||
/**
|
||||
* A function that is called when the selected command menu item
|
||||
* changes. It receives the new value as an argument.
|
||||
*/
|
||||
onValueChange?: (value: string) => void;
|
||||
/**
|
||||
* Optionally set to `true` to enable looping through the items
|
||||
* when the user reaches the end of the list using the keyboard.
|
||||
*/
|
||||
loop?: boolean;
|
||||
/**
|
||||
* Optionally set to `true` to disable selection via pointer events.
|
||||
*/
|
||||
disablePointerSelection?: boolean;
|
||||
/**
|
||||
* Set this prop to `false` to disable the option to use ctrl+n/j/p/k (vim style) navigation.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
vimBindings?: boolean;
|
||||
/**
|
||||
* The number of columns in a grid layout.
|
||||
*
|
||||
* @default null
|
||||
*/
|
||||
columns?: number | null;
|
||||
/**
|
||||
* Whether to disable scrolling the selected item into view on initial mount.
|
||||
* When `true`, prevents automatic scrolling when the command menu first renders
|
||||
* and selects its first item, but still allows scrolling on subsequent selections.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
disableInitialScroll?: boolean;
|
||||
}>;
|
||||
export type CommandRootProps = CommandRootPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, CommandRootPropsWithoutHTML>;
|
||||
export type CommandEmptyPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* Whether to force mount the group container regardless of
|
||||
* filtering logic.
|
||||
*/
|
||||
forceMount?: boolean;
|
||||
}>;
|
||||
export type CommandEmptyProps = CommandEmptyPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, CommandEmptyPropsWithoutHTML>;
|
||||
export type CommandGroupPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* A unique value for the group.
|
||||
*/
|
||||
value?: string;
|
||||
/**
|
||||
* Whether to force mount the group container regardless of
|
||||
* filtering logic.
|
||||
*/
|
||||
forceMount?: boolean;
|
||||
}>;
|
||||
export type CommandGroupProps = CommandGroupPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, CommandGroupPropsWithoutHTML>;
|
||||
export type CommandGroupHeadingPropsWithoutHTML = WithChild;
|
||||
export type CommandGroupHeadingProps = CommandGroupHeadingPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, CommandGroupHeadingPropsWithoutHTML>;
|
||||
export type CommandGroupItemsPropsWithoutHTML = WithChild;
|
||||
export type CommandGroupItemsProps = CommandGroupItemsPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, CommandGroupItemsPropsWithoutHTML>;
|
||||
export type CommandItemPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* Whether the item is disabled.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* A callback that is fired when the item is selected, either via
|
||||
* click or keyboard selection.
|
||||
*/
|
||||
onSelect?: () => void;
|
||||
/**
|
||||
* A unique value for this item that will be used when filtering
|
||||
* and ranking the items. If not provided, an attempt will be made
|
||||
* to use the `textContent` of the item. If the `textContent` is dynamic,
|
||||
* you will need to provide a stable unique value for the item.
|
||||
*/
|
||||
value?: string;
|
||||
/**
|
||||
* A list of keywords that will be used to filter the item.
|
||||
*/
|
||||
keywords?: string[];
|
||||
/**
|
||||
* Whether to always mount the item regardless of filtering logic.
|
||||
*/
|
||||
forceMount?: boolean;
|
||||
}>;
|
||||
export type CommandItemProps = CommandItemPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, CommandItemPropsWithoutHTML>;
|
||||
export type CommandLinkItemPropsWithoutHTML = CommandItemPropsWithoutHTML;
|
||||
export type CommandLinkItemProps = CommandLinkItemPropsWithoutHTML & Without<BitsPrimitiveAnchorAttributes, CommandLinkItemPropsWithoutHTML>;
|
||||
export type CommandInputPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* The value of the input element, used to search/filter items.
|
||||
*/
|
||||
value?: string;
|
||||
}>;
|
||||
export type CommandInputProps = CommandInputPropsWithoutHTML & Without<BitsPrimitiveInputAttributes, CommandInputPropsWithoutHTML>;
|
||||
export type CommandListPropsWithoutHTML = WithChild;
|
||||
export type CommandListProps = CommandListPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, CommandListPropsWithoutHTML>;
|
||||
export type CommandSeparatorPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* Whether to force mount the separator container regardless of
|
||||
* filtering logic.
|
||||
*/
|
||||
forceMount?: boolean;
|
||||
}>;
|
||||
export type CommandSeparatorProps = CommandSeparatorPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, CommandSeparatorPropsWithoutHTML>;
|
||||
export type CommandLoadingPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* The current progress of the loading state.
|
||||
* This is a number between `0` and `100`.
|
||||
*/
|
||||
progress?: number;
|
||||
}>;
|
||||
export type CommandLoadingProps = CommandLoadingPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, CommandLoadingPropsWithoutHTML>;
|
||||
export type CommandViewportPropsWithoutHTML = WithChild;
|
||||
export type CommandViewportProps = CommandViewportPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, CommandViewportPropsWithoutHTML>;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export declare function findNextSibling(el: Element, selector: string): Element | undefined;
|
||||
export declare function findPreviousSibling(el: Element, selector: string): Element | undefined;
|
||||
export declare function findFirstStartMarkerWithImmediateSiblingAsEnd(el: Element, type: "item" | "group"): HTMLElement | null;
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
export function findNextSibling(el, selector) {
|
||||
let sibling = el.nextElementSibling;
|
||||
while (sibling) {
|
||||
if (sibling.matches(selector))
|
||||
return sibling;
|
||||
sibling = sibling.nextElementSibling;
|
||||
}
|
||||
}
|
||||
export function findPreviousSibling(el, selector) {
|
||||
let sibling = el.previousElementSibling;
|
||||
while (sibling) {
|
||||
if (sibling.matches(selector))
|
||||
return sibling;
|
||||
sibling = sibling.previousElementSibling;
|
||||
}
|
||||
}
|
||||
export function findFirstStartMarkerWithImmediateSiblingAsEnd(el, type) {
|
||||
const startMarkers = el.querySelectorAll(`[data-bits-command-${type}-start]`);
|
||||
for (const startMarker of startMarkers) {
|
||||
const endMarker = startMarker.nextElementSibling;
|
||||
if (endMarker && endMarker.hasAttribute(`data-bits-command-${type}-end`)) {
|
||||
return startMarker;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user