diff options
Diffstat (limited to 'src/lib/CommandPalette')
| -rw-r--r-- | src/lib/CommandPalette/CommandPalette.svelte | 184 | ||||
| -rw-r--r-- | src/lib/CommandPalette/actions.ts | 306 |
2 files changed, 257 insertions, 233 deletions
diff --git a/src/lib/CommandPalette/CommandPalette.svelte b/src/lib/CommandPalette/CommandPalette.svelte index a42fab1a..8ace0222 100644 --- a/src/lib/CommandPalette/CommandPalette.svelte +++ b/src/lib/CommandPalette/CommandPalette.svelte @@ -1,13 +1,13 @@ <script lang="ts"> -import { onMount } from 'svelte'; -import { fly, fade } from 'svelte/transition'; -import { flip } from 'svelte/animate'; -import type { CommandPaletteAction } from './actions'; +import { onMount } from "svelte"; +import { fly, fade } from "svelte/transition"; +import { flip } from "svelte/animate"; +import type { CommandPaletteAction } from "./actions"; export let items: CommandPaletteAction[] = []; export let open = false; -let search = ''; +let search = ""; let filtered: (CommandPaletteAction & { id?: string })[] = []; let selectedIndex = -1; let inputRef: HTMLInputElement; @@ -16,121 +16,125 @@ let timeoutID: ReturnType<typeof setTimeout> | null = null; let itemIDs = new Map<string, number>(); $: { - items.forEach((item, index) => { - if (!itemIDs.has(item.url)) itemIDs.set(item.url, index); - }); - - const doesActionMatch = (action: CommandPaletteAction) => { - const doesActionIncludePattern = (query: string, action: string) => { - const normalise = (input: string) => input.toLowerCase().replace(/\s+/g, ''); - - return normalise(query).includes(normalise(action)); - }; - - return ( - doesActionIncludePattern(action.name, search) || - action.tags?.some((tag) => doesActionIncludePattern(tag, search)) - ); - }; - - filtered = []; - - items.forEach((action, idx) => { - const actionMatches = doesActionMatch(action); - let matchedParent = false; - - if (actionMatches) { - filtered.push({ ...action, id: `action-${idx}` }); - - matchedParent = true; - } - - if (action.actions) - action.actions.forEach((nestedAction, nestedIdx) => { - if (doesActionMatch(nestedAction)) - filtered.push({ - ...nestedAction, - id: `action-${idx}-nested-${nestedIdx}`, - name: `${matchedParent ? '↳' : `${action.name} >`} ${nestedAction.name}` - }); - }); - }); - - filtered = filtered.slice(0, 10); + items.forEach((item, index) => { + if (!itemIDs.has(item.url)) itemIDs.set(item.url, index); + }); + + const doesActionMatch = (action: CommandPaletteAction) => { + const doesActionIncludePattern = (query: string, action: string) => { + const normalise = (input: string) => + input.toLowerCase().replace(/\s+/g, ""); + + return normalise(query).includes(normalise(action)); + }; + + return ( + doesActionIncludePattern(action.name, search) || + action.tags?.some((tag) => doesActionIncludePattern(tag, search)) + ); + }; + + filtered = []; + + items.forEach((action, idx) => { + const actionMatches = doesActionMatch(action); + let matchedParent = false; + + if (actionMatches) { + filtered.push({ ...action, id: `action-${idx}` }); + + matchedParent = true; + } + + if (action.actions) + action.actions.forEach((nestedAction, nestedIdx) => { + if (doesActionMatch(nestedAction)) + filtered.push({ + ...nestedAction, + id: `action-${idx}-nested-${nestedIdx}`, + name: `${matchedParent ? "↳" : `${action.name} >`} ${nestedAction.name}`, + }); + }); + }); + + filtered = filtered.slice(0, 10); } $: if (selectedIndex >= filtered.length) selectedIndex = filtered.length - 1; $: if (selectedIndex < 0 && filtered.length > 0) selectedIndex = 0; $: if (open && !isVisible) { - isVisible = true; + isVisible = true; - if (timeoutID !== null) { - clearTimeout(timeoutID); + if (timeoutID !== null) { + clearTimeout(timeoutID); - timeoutID = null; - } + timeoutID = null; + } } else if (!open && isVisible) { - if (timeoutID === null) { - timeoutID = setTimeout(() => { - isVisible = false; - timeoutID = null; - }, 200); - } + if (timeoutID === null) { + timeoutID = setTimeout(() => { + isVisible = false; + timeoutID = null; + }, 200); + } } const executeItem = (item: CommandPaletteAction) => { - if (item.onClick) item.onClick(); - if (!item.preventDefault) window.location.href = item.url; + if (item.onClick) item.onClick(); + if (!item.preventDefault) window.location.href = item.url; - open = false; + open = false; }; const handleKey = (e: KeyboardEvent) => { - if (e.key === 'ArrowDown') { - e.preventDefault(); - - selectedIndex = (selectedIndex + 1) % filtered.length; - } else if (e.key === 'ArrowUp') { - e.preventDefault(); - - selectedIndex = (selectedIndex - 1 + filtered.length) % filtered.length; - } else if (e.key === 'Enter') { - if (filtered.length === 1) { - executeItem(filtered[0]); - } else if (filtered.length > 1 && selectedIndex >= 0) { - executeItem(filtered[selectedIndex]); - } - } else if (e.key === 'Escape') { - open = false; - } + if (e.key === "ArrowDown") { + e.preventDefault(); + + selectedIndex = (selectedIndex + 1) % filtered.length; + } else if (e.key === "ArrowUp") { + e.preventDefault(); + + selectedIndex = (selectedIndex - 1 + filtered.length) % filtered.length; + } else if (e.key === "Enter") { + if (filtered.length === 1) { + executeItem(filtered[0]); + } else if (filtered.length > 1 && selectedIndex >= 0) { + executeItem(filtered[selectedIndex]); + } + } else if (e.key === "Escape") { + open = false; + } }; onMount(() => { - window.addEventListener('keydown', handleGlobalKey); + window.addEventListener("keydown", handleGlobalKey); - return () => { - window.removeEventListener('keydown', handleGlobalKey); + return () => { + window.removeEventListener("keydown", handleGlobalKey); - if (timeoutID !== null) clearTimeout(timeoutID); - }; + if (timeoutID !== null) clearTimeout(timeoutID); + }; }); const handleClickOutside = (event: MouseEvent) => { - const target = event.target as HTMLElement; + const target = event.target as HTMLElement; - if (target.classList.contains('command-palette-overlay') || !target.closest('.dropdown')) - open = false; + if ( + target.classList.contains("command-palette-overlay") || + !target.closest(".dropdown") + ) + open = false; }; const handleGlobalKey = (e: KeyboardEvent) => { - if ((e.metaKey || e.ctrlKey) && e.key === 'k') { - e.preventDefault(); + if ((e.metaKey || e.ctrlKey) && e.key === "k") { + e.preventDefault(); - open = !open; + open = !open; - if (open) requestAnimationFrame(() => inputRef?.focus()); - } + if (open) requestAnimationFrame(() => inputRef?.focus()); + } }; </script> diff --git a/src/lib/CommandPalette/actions.ts b/src/lib/CommandPalette/actions.ts index 49efa536..01259130 100644 --- a/src/lib/CommandPalette/actions.ts +++ b/src/lib/CommandPalette/actions.ts @@ -1,148 +1,168 @@ export interface CommandPaletteAction { - name: string; - url: string; - onClick?: () => void; - preventDefault?: boolean; - tags?: string[]; - actions?: CommandPaletteAction[]; + name: string; + url: string; + onClick?: () => void; + preventDefault?: boolean; + tags?: string[]; + actions?: CommandPaletteAction[]; } export const defaultActions: CommandPaletteAction[] = [ - { - name: 'Home', - url: '/', - tags: ['main', 'manga', 'anime', 'light', 'dashboard', 'start', 'begin', 'novels', 'list'], - actions: [ - { - name: 'Upcoming Episodes', - url: '/', - tags: ['anime', 'list'] - }, - { - name: 'Not Yet Released', - url: '/', - tags: ['anime', 'schedule', 'list'] - }, - { - name: 'Due Episodes', - url: '/', - tags: ['anime', 'list'] - }, - { - name: 'Manga & Light Novels', - url: '/', - tags: ['novels', 'manga', 'list'] - } - ] - }, - { - name: 'Completed', - url: '/completed', - tags: ['finish', 'end', 'done', 'finish', 'end', 'done', 'anime', 'novels', 'manga'], - actions: [ - { - name: 'Anime', - url: '/completed', - tags: ['anime', 'list'] - }, - { - name: 'Manga & Light Novels', - url: '/completed', - tags: ['novels', 'manga', 'list'] - } - ] - }, - { - name: 'Subtitle Schedule', - url: '/schedule', - tags: ['anime', 'subs'] - }, - { - name: 'hololive Schedule', - url: '/hololive', - tags: ['vtuber', 'youtube', 'virtual', 'twitch', 'stream'] - }, - { - name: 'Character Birthdays', - url: '/birthdays', - tags: ['schedule', 'vtuber', 'date'] - }, - { - name: 'New Releases', - url: '/releases', - tags: ['novels', 'manga', 'date', 'schedule', 'time'] - }, - { - name: 'Settings', - url: '/settings', - tags: [ - 'sync', - 'display', - 'hide', - 'panels', - 'motion', - 'accessibility', - 'notifications', - 'rss', - 'warning', - 'show', - 'links', - 'sort', - 'calculation', - 'cache', - 'clear', - 'debug', - 'language', - 'locale' - ], - actions: [ - { - name: 'Settings Sync', - url: '/settings#sync', - tags: ['settings'] - }, - { - name: 'RSS Feeds', - url: '/settings#feeds', - tags: ['settings'] - }, - { - name: 'Display', - url: '/settings', - tags: ['settings'] - }, - { - name: 'Calculation', - url: '/settings', - tags: ['settings'] - }, - { - name: 'Cache', - url: '/settings', - tags: ['settings'] - }, - { - name: 'Debug', - url: '/settings#debug', - tags: ['settings'] - } - ] - }, - { - name: 'My Profile', - url: '/user', - tags: ['user', 'me', 'settings'], - actions: [ - { - name: 'User Preferences', - url: '/user', - tags: ['user', 'me', 'settings'] - } - ] - }, - { - name: 'My Badge Wall', - url: '/user?badges=1', - tags: ['user', 'me', 'settings'] - } + { + name: "Home", + url: "/", + tags: [ + "main", + "manga", + "anime", + "light", + "dashboard", + "start", + "begin", + "novels", + "list", + ], + actions: [ + { + name: "Upcoming Episodes", + url: "/", + tags: ["anime", "list"], + }, + { + name: "Not Yet Released", + url: "/", + tags: ["anime", "schedule", "list"], + }, + { + name: "Due Episodes", + url: "/", + tags: ["anime", "list"], + }, + { + name: "Manga & Light Novels", + url: "/", + tags: ["novels", "manga", "list"], + }, + ], + }, + { + name: "Completed", + url: "/completed", + tags: [ + "finish", + "end", + "done", + "finish", + "end", + "done", + "anime", + "novels", + "manga", + ], + actions: [ + { + name: "Anime", + url: "/completed", + tags: ["anime", "list"], + }, + { + name: "Manga & Light Novels", + url: "/completed", + tags: ["novels", "manga", "list"], + }, + ], + }, + { + name: "Subtitle Schedule", + url: "/schedule", + tags: ["anime", "subs"], + }, + { + name: "hololive Schedule", + url: "/hololive", + tags: ["vtuber", "youtube", "virtual", "twitch", "stream"], + }, + { + name: "Character Birthdays", + url: "/birthdays", + tags: ["schedule", "vtuber", "date"], + }, + { + name: "New Releases", + url: "/releases", + tags: ["novels", "manga", "date", "schedule", "time"], + }, + { + name: "Settings", + url: "/settings", + tags: [ + "sync", + "display", + "hide", + "panels", + "motion", + "accessibility", + "notifications", + "rss", + "warning", + "show", + "links", + "sort", + "calculation", + "cache", + "clear", + "debug", + "language", + "locale", + ], + actions: [ + { + name: "Settings Sync", + url: "/settings#sync", + tags: ["settings"], + }, + { + name: "RSS Feeds", + url: "/settings#feeds", + tags: ["settings"], + }, + { + name: "Display", + url: "/settings", + tags: ["settings"], + }, + { + name: "Calculation", + url: "/settings", + tags: ["settings"], + }, + { + name: "Cache", + url: "/settings", + tags: ["settings"], + }, + { + name: "Debug", + url: "/settings#debug", + tags: ["settings"], + }, + ], + }, + { + name: "My Profile", + url: "/user", + tags: ["user", "me", "settings"], + actions: [ + { + name: "User Preferences", + url: "/user", + tags: ["user", "me", "settings"], + }, + ], + }, + { + name: "My Badge Wall", + url: "/user?badges=1", + tags: ["user", "me", "settings"], + }, ]; |