Skip to content

Commit

Permalink
more work here
Browse files Browse the repository at this point in the history
huntabyte committed Jul 23, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 17a71eb commit 42942cb
Showing 6 changed files with 356 additions and 73 deletions.
35 changes: 31 additions & 4 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,33 @@
import config from '@huntabyte/eslint-config'
import config from '@huntabyte/eslint-config';

export default config({
ignores: [".DS_Store","**/.DS_Store/**","node_modules","**/node_modules/**","build","build/**",".svelte-kit",".svelte-kit/**","package","package/**",".env","**/.env/**",".env.*","**/.env.*/**","!.env.example","!**/.env.example/**","pnpm-lock.yaml","**/pnpm-lock.yaml/**","package-lock.json","**/package-lock.json/**","yarn.lock","**/yarn.lock/**","dist","dist/**",".changeset/","**/.changeset/**/"],
svelte: true,
})
ignores: [
'.DS_Store',
'**/.DS_Store/**',
'node_modules',
'**/node_modules/**',
'build',
'build/**',
'.svelte-kit',
'.svelte-kit/**',
'package',
'package/**',
'.env',
'**/.env/**',
'.env.*',
'**/.env.*/**',
'!.env.example',
'!**/.env.example/**',
'pnpm-lock.yaml',
'**/pnpm-lock.yaml/**',
'package-lock.json',
'**/package-lock.json/**',
'yarn.lock',
'**/yarn.lock/**',
'dist',
'dist/**',
'.changeset/',
'**/.changeset/**/'
],
svelte: true
});
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -64,9 +64,9 @@
"types": "./dist/index.d.ts",
"type": "module",
"dependencies": {
"bits-ui": "0.21.4",
"bits-ui": "https://pkg.pr.new/bits-ui@52b86fe",
"runed": "^0.15.0",
"svelte-toolbelt": "^0.0.2"
"svelte-toolbelt": "^0.1.0"
},
"packageManager": "[email protected]"
}
98 changes: 51 additions & 47 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

259 changes: 254 additions & 5 deletions src/lib/cmdk/command-state.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import type { CommandRoot } from './index.js';
import { a } from 'vitest/dist/suite-IbNSsUWN.js';
import { tick } from 'svelte';
import { commandScore } from '$lib/internal/command-score.js';
import type { ReadableBoxedValues, WritableBoxedValues } from '$lib/internal/types.js';
import { useRefById } from '$lib/internal/useRefById.svelte.js';
import { kbd } from '$lib/internal/index.js';

export const LIST_SELECTOR = `[data-cmdk-list-sizer]`;
export const GROUP_SELECTOR = `[data-cmdk-group]`;
@@ -20,6 +22,7 @@ type CommandRootStateProps = ReadableBoxedValues<{
id: string;
filter: (value: string, search: string) => number;
shouldFilter: boolean;
loop: boolean;
}> &
WritableBoxedValues<{
ref: HTMLElement | null;
@@ -33,6 +36,7 @@ class CommandRootState {
id: CommandRootStateProps['id'];
filter: CommandRootStateProps['filter'];
shouldFilter: CommandRootStateProps['shouldFilter'];
loop: CommandRootStateProps['loop'];
listNode = $state<HTMLElement | null>(null);
search = $state('');
value = $state('');
@@ -47,6 +51,7 @@ class CommandRootState {
this.commandRef = props.ref;
this.filter = props.filter;
this.shouldFilter = props.shouldFilter;
this.loop = props.loop;

useRefById({
id: this.id,
@@ -56,12 +61,12 @@ class CommandRootState {

#score(value: string | undefined, search: string) {
const lowerCaseAndTrimmedValue = value?.toLowerCase().trim();
const filterFn = this.filter.value;
const filterFn = this.filter.current;
return lowerCaseAndTrimmedValue ? filterFn(lowerCaseAndTrimmedValue, search) : 0;
}

#filterItems() {
const shouldFilter = this.shouldFilter.value;
const shouldFilter = this.shouldFilter.current;
if (!this.search || !shouldFilter) {
this.filtered.count = this.allItems.size;
return;
@@ -94,7 +99,7 @@ class CommandRootState {
}

#sort() {
const shouldFilter = this.shouldFilter.value;
const shouldFilter = this.shouldFilter.current;
if (!this.search || !shouldFilter) return;

const scores = this.filtered.items;
@@ -118,7 +123,133 @@ class CommandRootState {
// sort items within groups to bottom
// sort items outside of groups
// sort groups to bottom (pushed all non-grouped items to the top)
const commandNode = this.commandRef.value;
const commandNode = this.commandRef.current;
if (!commandNode) return;
const list = this.listNode;

const validItems = this.#getValidItems().sort((a, b) => {
const valueA = a.getAttribute(VALUE_ATTR) ?? '';
const valueB = b.getAttribute(VALUE_ATTR) ?? '';
return (scores.get(valueA) ?? 0) - (scores.get(valueB) ?? 0);
});

for (const item of validItems) {
const group = item.closest(GROUP_ITEMS_SELECTOR);
const closest = item.closest(`${GROUP_ITEMS_SELECTOR} > *`);
if (group) {
if (item.parentElement === group) {
group.appendChild(item);
} else {
if (!closest) continue;
group.appendChild(closest);
}
} else {
if (item.parentElement === list) {
list?.appendChild(item);
} else {
if (!closest) continue;
list?.appendChild(closest);
}
}
}

groups.sort((a, b) => b[1] - a[1]);

for (const group of groups) {
const el = commandNode.querySelector(`${GROUP_SELECTOR}[${VALUE_ATTR}="${group[0]}"]`);
el?.parentElement?.appendChild(el);
}
}

#getValidItems = () => {
const node = this.commandRef.current;
if (!node) return [];
return Array.from(node.querySelectorAll<HTMLElement>(VALID_ITEM_SELECTOR)).filter(
(el): el is HTMLElement => !!el
);
};

#getSelectedItem = () => {
const node = this.commandRef.current;
if (!node) return;
const selectedNode = node.querySelector<HTMLElement>(
`${VALID_ITEM_SELECTOR}[aria-selected="true"]`
);
if (!selectedNode) return;
return selectedNode;
};

#selectFirstItem = () => {
const item = this.#getValidItems().find((item) => !item.ariaDisabled);
if (!item) return;
const value = item.getAttribute(VALUE_ATTR) ?? '';
if (!value) return;
return value;
};

#scrollSelectedIntoView() {
const item = this.#getSelectedItem();
if (!item) return;

if (item.parentElement?.firstChild === item) {
tick().then(() => {
item
.closest(GROUP_SELECTOR)
?.querySelector(GROUP_HEADING_SELECTOR)
?.scrollIntoView({ block: 'nearest' });
});
}
tick().then(() => item.scrollIntoView({ block: 'nearest' }));
}

#updateSelectedToIndex(index: number) {
const node = this.commandRef.current;
if (!node) return;
const items = this.#getValidItems();
const item = items[index];
if (!item) return;
this.setValue(item.getAttribute(VALUE_ATTR) ?? '');
}

#updateSelectedByChange(change: 1 | -1) {
const selected = this.#getSelectedItem();
const items = this.#getValidItems();
const index = items.findIndex((item) => item === selected);

// get item at this index
let newSelected = items[index + change];
if (this.loop.current) {
if (index + change < 0) {
newSelected = items[items.length - 1];
} else if (index + change === items.length) {
newSelected = items[0];
} else {
newSelected = items[index + change];
}
}
if (newSelected) {
this.setValue(newSelected.getAttribute(VALUE_ATTR) ?? '');
}
}

#updateSelectedToGroup(change: 1 | -1) {
const selected = this.#getSelectedItem();
let group = selected?.closest(GROUP_SELECTOR);
let item: HTMLElement | undefined | null;

while (group && !item) {
group =
change > 0
? findNextSibling(group, GROUP_SELECTOR)
: findPreviousSibling(group, GROUP_SELECTOR);
item = group?.querySelector(VALID_ITEM_SELECTOR);
}

if (item) {
this.setValue(item.getAttribute(VALUE_ATTR) ?? '');
} else {
this.#updateSelectedByChange(change);
}
}

updateValue(id: string, value: string) {
@@ -139,5 +270,123 @@ class CommandRootState {
this.allGroups.get(groupId)?.add(id);
}
}

this.#filterItems();

if (!this.value) {
this.value = this.#selectFirstItem() ?? '';
}

return () => {
this.allIds.delete(id);
this.allItems.delete(id);
this.filtered.items.delete(id);
const selectedItem = this.#getSelectedItem();

this.#filterItems();

if (selectedItem?.getAttribute('id') === id) {
this.value = this.#selectFirstItem() ?? '';
}
};
}

updateGroup(id: string) {
if (!this.allGroups.has(id)) {
this.allGroups.set(id, new SvelteSet());
}

return () => {
this.allIds.delete(id);
this.allGroups.delete(id);
};
}

setSearch(search: string) {
this.search = search;
this.#filterItems();
this.#sort();
tick().then(() => {
this.value = this.#selectFirstItem() ?? '';
});
}

setValue(newVal: string, preventScroll?: boolean) {
if (preventScroll) return;
tick().then(() => this.#scrollSelectedIntoView());
}

#last = () => {
return this.#updateSelectedToIndex(this.#getValidItems().length - 1);
};

#next = (e: KeyboardEvent) => {
e.preventDefault();

if (e.metaKey) {
this.#last();
} else if (e.altKey) {
this.#updateSelectedToGroup(1);
} else {
this.#updateSelectedByChange(1);
}
};

#prev = (e: KeyboardEvent) => {
e.preventDefault();

if (e.metaKey) {
this.setValue(this.search);
} else if (e.altKey) {
this.#updateSelectedToGroup(-1);
} else {
this.#updateSelectedByChange(-1);
}
};

#handleRootKeydown = (e: KeyboardEvent) => {
switch (e.key) {
case kbd.ARROW_DOWN:
this.#next(e);
break;
case kbd.ARROW_UP:
this.#prev(e);
break;
case kbd.HOME:
// first item
e.preventDefault();
this.#updateSelectedToIndex(0);
break;
case kbd.END:
// last item
e.preventDefault();
this.#last();
break;
case kbd.ENTER: {
e.preventDefault();
const item = this.#getSelectedItem() as HTMLElement;
if (item) {
item?.click();
}
}
}
};
}

function findNextSibling(el: Element, selector: string) {
let sibling = el.nextElementSibling;

while (sibling) {
if (sibling.matches(selector)) return sibling;
sibling = sibling.nextElementSibling;
}
}

function findPreviousSibling(el: Element, selector: string) {
let sibling = el.previousElementSibling;

while (sibling) {
if (sibling.matches(selector)) return sibling;
sibling = sibling.previousElementSibling;
}
}
14 changes: 7 additions & 7 deletions src/lib/cmdk/command.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { getContext, setContext, tick } from 'svelte';
import { commandScore } from '$lib/internal/command-score.js';
import type { CommandProps, Context, Group, State, StateStore } from './types.js';
import { get, writable } from 'svelte/store';
import type { CommandProps, Context, Group, State, StateStore } from './types.js';
import { commandScore } from '$lib/internal/command-score.js';
import {
omit,
effect,
generateId,
toWritableStores,
isUndefined,
kbd,
omit,
removeUndefined,
effect
toWritableStores
} from '$lib/internal/index.js';

const NAME = 'Command';
@@ -394,7 +394,7 @@ export function createCommand(props: CommandProps) {
const rootEl = rootElement ?? document.getElementById(ids.root);
if (!rootEl) return [];
return Array.from(rootEl.querySelectorAll(VALID_ITEM_SELECTOR)).filter(
(el): el is HTMLElement => (el ? true : false)
(el): el is HTMLElement => !!el
);
}

@@ -441,7 +441,7 @@ export function createCommand(props: CommandProps) {
function updateSelectedToGroup(change: 1 | -1) {
const selected = getSelectedItem();
let group = selected?.closest(GROUP_SELECTOR);
let item: HTMLElement | undefined | null = undefined;
let item: HTMLElement | undefined | null;

while (group && !item) {
group =
19 changes: 11 additions & 8 deletions src/lib/cmdk/components/CommandGroup.svelte
Original file line number Diff line number Diff line change
@@ -5,20 +5,23 @@
import type { GroupProps } from '../types.js';
import { onMount } from 'svelte';
type $$Props = GroupProps;
export let heading: $$Props['heading'] = undefined;
export let value = '';
export let alwaysRender: $$Props['alwaysRender'] = false;
export let asChild: $$Props['asChild'] = false;
let {
value = '',
alwaysRender = false,
heading,
children,
child,
ref = $bindable(null),
...restProps
}: GroupProps = $props();
const { id } = createGroup(alwaysRender);
const context = getCtx();
const state = getState();
const cmdkState = getState();
const headingId = generateId();
const render = derived(state, ($state) => {
const render = derived(cmdkState, ($state) => {
if (alwaysRender) return true;
if (context.filter() === false) return true;
if (!$state.search) return true;

0 comments on commit 42942cb

Please sign in to comment.