aboutsummaryrefslogtreecommitdiff
path: root/src/lib/CommandPalette
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/CommandPalette')
-rw-r--r--src/lib/CommandPalette/CommandPalette.svelte184
-rw-r--r--src/lib/CommandPalette/actions.ts306
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"],
+ },
];