From 618f72b31d57ac17f475dbe983a31627cff3b96e Mon Sep 17 00:00:00 2001
From: Wolfgang Müller
Date: Thu, 13 Feb 2025 19:04:12 +0100
Subject: frontend: Allow control-clicking to open the quick filter

This makes hircine's behaviour more consistent with standard browser
behaviour (that would also open a new tab when control-clicking).
---
 frontend/src/lib/Navigation.ts               |  6 ++++-
 frontend/src/lib/components/Cardlet.svelte   | 19 ++-------------
 frontend/src/lib/selection/Selectable.svelte | 36 ++++++++++++++++++++++------
 frontend/src/routes/artists/+page.svelte     |  9 ++++---
 frontend/src/routes/characters/+page.svelte  |  9 ++++---
 frontend/src/routes/circles/+page.svelte     |  9 ++++---
 frontend/src/routes/namespaces/+page.svelte  |  9 ++++---
 frontend/src/routes/tags/+page.svelte        |  9 ++++---
 frontend/src/routes/worlds/+page.svelte      |  9 ++++---
 9 files changed, 72 insertions(+), 43 deletions(-)

(limited to 'frontend/src')

diff --git a/frontend/src/lib/Navigation.ts b/frontend/src/lib/Navigation.ts
index f3bc413..4dcb998 100644
--- a/frontend/src/lib/Navigation.ts
+++ b/frontend/src/lib/Navigation.ts
@@ -1,5 +1,5 @@
 import { goto as svelteGoto } from '$app/navigation';
-import { SortDirection } from '$gql/graphql';
+import { SortDirection, type ComicFilter } from '$gql/graphql';
 import JsonURL from '@jsonurl/jsonurl';
 import { toastError } from './Toasts';
 import type { Key } from './Utils';
@@ -120,3 +120,7 @@ export function navigate(params: NavigationParameters<object>, current?: URLSear
 export function href<T>(base: string, params: NavigationParameters<T>) {
 	return `/${base}/?${parametersFrom(params).toString()}`;
 }
+
+export function quickComicFilter(id: number | string, filter: keyof ComicFilter) {
+	window.open(href('comics', { filter: { include: { [filter]: { all: [id] } } } }));
+}
diff --git a/frontend/src/lib/components/Cardlet.svelte b/frontend/src/lib/components/Cardlet.svelte
index d249cc8..d0c0509 100644
--- a/frontend/src/lib/components/Cardlet.svelte
+++ b/frontend/src/lib/components/Cardlet.svelte
@@ -1,30 +1,15 @@
 <script lang="ts">
-	import type { ComicFilter } from '$gql/graphql';
-	import { href } from '$lib/Navigation';
 	import type { Snippet } from 'svelte';
 
 	interface Props {
 		name: string;
 		title?: string | null;
-		filter?: keyof ComicFilter;
-		id?: number | string;
 		overlay?: Snippet;
 		onclick: (event: MouseEvent) => void;
+		onauxclick?: (event: MouseEvent) => void;
 	}
 
-	let {
-		name,
-		title = undefined,
-		filter = undefined,
-		id = undefined,
-		overlay,
-		onclick
-	}: Props = $props();
-
-	const onauxclick = (e: MouseEvent) => {
-		if (filter === undefined || id === undefined || e.button !== 1) return;
-		window.open(href('comics', { filter: { include: { [filter]: { all: [id] } } } }));
-	};
+	let { name, title = undefined, overlay, onclick, onauxclick = undefined }: Props = $props();
 </script>
 
 <button
diff --git a/frontend/src/lib/selection/Selectable.svelte b/frontend/src/lib/selection/Selectable.svelte
index 4705f44..439d6b7 100644
--- a/frontend/src/lib/selection/Selectable.svelte
+++ b/frontend/src/lib/selection/Selectable.svelte
@@ -2,25 +2,47 @@
 	import type { Snippet } from 'svelte';
 	import { getSelectionContext } from './Selection.svelte';
 
+	interface SnippetProps {
+		onclick: (event: MouseEvent) => void;
+		onauxclick: (event: MouseEvent) => void;
+		selected: boolean;
+	}
+
 	interface Props {
 		id: number;
 		index: number;
-		edit?: ((id: number) => void) | undefined;
-		children?: Snippet<[{ onclick: (event: MouseEvent) => void; selected: boolean }]>;
+		onclick?: (id: number) => void;
+		onauxclick?: (id: number) => void;
+		children?: Snippet<[SnippetProps]>;
 	}
 
-	let { id, index, edit = undefined, children }: Props = $props();
+	let {
+		id,
+		index,
+		onclick: onclick = undefined,
+		onauxclick = undefined,
+		children
+	}: Props = $props();
+
 	let selection = getSelectionContext();
 
-	const onclick = (event: MouseEvent) => {
+	const click = (event: MouseEvent) => {
 		if (selection.active) {
 			selection.update(index, event.shiftKey);
 			event.preventDefault();
-		} else if (edit) {
-			edit(id);
+		} else if (event.ctrlKey && onauxclick) {
+			onauxclick(id);
+		} else if (onclick) {
+			onclick(id);
 			event.preventDefault();
 		}
 	};
+
+	const auxclick = (event: MouseEvent) => {
+		if (event.button === 1 && onauxclick) {
+			onauxclick(id);
+		}
+	};
 </script>
 
-{@render children?.({ onclick, selected: selection.contains(id) })}
+{@render children?.({ onclick: click, onauxclick: auxclick, selected: selection.contains(id) })}
diff --git a/frontend/src/routes/artists/+page.svelte b/frontend/src/routes/artists/+page.svelte
index c907470..9f0d893 100644
--- a/frontend/src/routes/artists/+page.svelte
+++ b/frontend/src/routes/artists/+page.svelte
@@ -4,6 +4,7 @@
 	import type { Artist } from '$gql/graphql';
 	import { ArtistSortLabel } from '$lib/Enums';
 	import { BasicFilterContext } from '$lib/Filter.svelte';
+	import { quickComicFilter } from '$lib/Navigation';
 	import { toastFinally } from '$lib/Toasts';
 	import AddButton from '$lib/components/AddButton.svelte';
 	import Cardlet from '$lib/components/Cardlet.svelte';
@@ -53,6 +54,8 @@
 			.then((artist) => modals.open(EditArtist, { artist }))
 			.catch(toastFinally);
 	};
+
+	const quickFilter = (id: number) => quickComicFilter(id, 'artists');
 </script>
 
 <Head section="artists" />
@@ -78,9 +81,9 @@
 		<main>
 			<Cardlets>
 				{#each artists.edges as { id, name }, index (id)}
-					<Selectable {index} {id} {edit}>
-						{#snippet children({ onclick, selected })}
-							<Cardlet {name} {onclick} filter="artists" {id}>
+					<Selectable {index} {id} onclick={edit} onauxclick={quickFilter}>
+						{#snippet children({ onclick, onauxclick, selected })}
+							<Cardlet {name} {onclick} {onauxclick}>
 								{#snippet overlay()}
 									<SelectionOverlay position="right" centered {selected} />
 								{/snippet}
diff --git a/frontend/src/routes/characters/+page.svelte b/frontend/src/routes/characters/+page.svelte
index 04c72cb..3a4b737 100644
--- a/frontend/src/routes/characters/+page.svelte
+++ b/frontend/src/routes/characters/+page.svelte
@@ -4,6 +4,7 @@
 	import type { Character } from '$gql/graphql';
 	import { CharacterSortLabel } from '$lib/Enums';
 	import { BasicFilterContext } from '$lib/Filter.svelte';
+	import { quickComicFilter } from '$lib/Navigation';
 	import { toastFinally } from '$lib/Toasts';
 	import AddButton from '$lib/components/AddButton.svelte';
 	import Cardlet from '$lib/components/Cardlet.svelte';
@@ -53,6 +54,8 @@
 			.then((character) => modals.open(EditCharacter, { character }))
 			.catch(toastFinally);
 	};
+
+	const quickFilter = (id: number) => quickComicFilter(id, 'characters');
 </script>
 
 <Head section="characters" />
@@ -78,9 +81,9 @@
 		<main>
 			<Cardlets>
 				{#each characters.edges as { id, name }, index (id)}
-					<Selectable {index} {id} {edit}>
-						{#snippet children({ onclick, selected })}
-							<Cardlet {name} {onclick} filter="characters" {id}>
+					<Selectable {index} {id} onclick={edit} onauxclick={quickFilter}>
+						{#snippet children({ onclick, onauxclick, selected })}
+							<Cardlet {name} {onclick} {onauxclick}>
 								{#snippet overlay()}
 									<SelectionOverlay position="right" centered {selected} />
 								{/snippet}
diff --git a/frontend/src/routes/circles/+page.svelte b/frontend/src/routes/circles/+page.svelte
index 57520f8..8bac7ed 100644
--- a/frontend/src/routes/circles/+page.svelte
+++ b/frontend/src/routes/circles/+page.svelte
@@ -4,6 +4,7 @@
 	import type { Circle } from '$gql/graphql';
 	import { CircleSortLabel } from '$lib/Enums';
 	import { BasicFilterContext } from '$lib/Filter.svelte';
+	import { quickComicFilter } from '$lib/Navigation';
 	import { toastFinally } from '$lib/Toasts';
 	import AddButton from '$lib/components/AddButton.svelte';
 	import Cardlet from '$lib/components/Cardlet.svelte';
@@ -53,6 +54,8 @@
 			.then((circle) => modals.open(EditCircle, { circle }))
 			.catch(toastFinally);
 	};
+
+	const quickFilter = (id: number) => quickComicFilter(id, 'circles');
 </script>
 
 <Head section="circles" />
@@ -78,9 +81,9 @@
 		<main>
 			<Cardlets>
 				{#each circles.edges as { id, name }, index (id)}
-					<Selectable {index} {id} {edit}>
-						{#snippet children({ onclick, selected })}
-							<Cardlet {name} {onclick} filter="circles" {id}>
+					<Selectable {index} {id} onclick={edit} onauxclick={quickFilter}>
+						{#snippet children({ onclick, onauxclick, selected })}
+							<Cardlet {name} {onclick} {onauxclick}>
 								{#snippet overlay()}
 									<SelectionOverlay position="right" centered {selected} />
 								{/snippet}
diff --git a/frontend/src/routes/namespaces/+page.svelte b/frontend/src/routes/namespaces/+page.svelte
index 04f7737..d8e728d 100644
--- a/frontend/src/routes/namespaces/+page.svelte
+++ b/frontend/src/routes/namespaces/+page.svelte
@@ -4,6 +4,7 @@
 	import type { Namespace } from '$gql/graphql';
 	import { NamespaceSortLabel } from '$lib/Enums';
 	import { BasicFilterContext } from '$lib/Filter.svelte';
+	import { quickComicFilter } from '$lib/Navigation';
 	import { toastFinally } from '$lib/Toasts';
 	import AddButton from '$lib/components/AddButton.svelte';
 	import Cardlet from '$lib/components/Cardlet.svelte';
@@ -53,6 +54,8 @@
 			.then((namespace) => modals.open(EditNamespace, { namespace }))
 			.catch(toastFinally);
 	};
+
+	const quickFilter = (id: number) => quickComicFilter(`${id}:`, 'tags');
 </script>
 
 <Head section="Namespaces" />
@@ -78,9 +81,9 @@
 		<main>
 			<Cardlets>
 				{#each namespaces.edges as { id, name }, index (id)}
-					<Selectable {index} {id} {edit}>
-						{#snippet children({ onclick, selected })}
-							<Cardlet {name} {onclick} filter="tags" id={`${id}:`}>
+					<Selectable {index} {id} onclick={edit} onauxclick={quickFilter}>
+						{#snippet children({ onclick, onauxclick, selected })}
+							<Cardlet {name} {onclick} {onauxclick}>
 								{#snippet overlay()}
 									<SelectionOverlay position="right" centered {selected} />
 								{/snippet}
diff --git a/frontend/src/routes/tags/+page.svelte b/frontend/src/routes/tags/+page.svelte
index 30554c7..f71267f 100644
--- a/frontend/src/routes/tags/+page.svelte
+++ b/frontend/src/routes/tags/+page.svelte
@@ -4,6 +4,7 @@
 	import { type Tag } from '$gql/graphql';
 	import { TagSortLabel } from '$lib/Enums';
 	import { TagFilterContext } from '$lib/Filter.svelte';
+	import { quickComicFilter } from '$lib/Navigation';
 	import { toastFinally } from '$lib/Toasts';
 	import AddButton from '$lib/components/AddButton.svelte';
 	import Cardlet from '$lib/components/Cardlet.svelte';
@@ -58,6 +59,8 @@
 			.then((tag) => modals.open(EditTag, { tag }))
 			.catch(toastFinally);
 	};
+
+	const quickFilter = (id: number) => quickComicFilter(`:${id}`, 'tags');
 </script>
 
 <Head section="Tags" />
@@ -88,9 +91,9 @@
 		<main>
 			<Cardlets>
 				{#each tags.edges as { id, name, description }, index (id)}
-					<Selectable {index} {id} {edit}>
-						{#snippet children({ onclick, selected })}
-							<Cardlet {name} title={description} {onclick} filter="tags" id={`:${id}`}>
+					<Selectable {index} {id} onclick={edit} onauxclick={quickFilter}>
+						{#snippet children({ onclick, onauxclick, selected })}
+							<Cardlet {name} title={description} {onclick} {onauxclick}>
 								{#snippet overlay()}
 									<SelectionOverlay position="right" centered {selected} />
 								{/snippet}
diff --git a/frontend/src/routes/worlds/+page.svelte b/frontend/src/routes/worlds/+page.svelte
index f223a61..6b95142 100644
--- a/frontend/src/routes/worlds/+page.svelte
+++ b/frontend/src/routes/worlds/+page.svelte
@@ -4,6 +4,7 @@
 	import type { World } from '$gql/graphql';
 	import { WorldSortLabel } from '$lib/Enums';
 	import { BasicFilterContext } from '$lib/Filter.svelte';
+	import { quickComicFilter } from '$lib/Navigation';
 	import { toastFinally } from '$lib/Toasts';
 	import AddButton from '$lib/components/AddButton.svelte';
 	import Cardlet from '$lib/components/Cardlet.svelte';
@@ -53,6 +54,8 @@
 			.then((world) => modals.open(EditWorld, { world }))
 			.catch(toastFinally);
 	};
+
+	const quickFilter = (id: number) => quickComicFilter(id, 'worlds');
 </script>
 
 <Head section="worlds" />
@@ -78,9 +81,9 @@
 		<main>
 			<Cardlets>
 				{#each worlds.edges as { id, name }, index (id)}
-					<Selectable {index} {id} {edit}>
-						{#snippet children({ onclick, selected })}
-							<Cardlet {name} {onclick} filter="worlds" {id}>
+					<Selectable {index} {id} onclick={edit} onauxclick={quickFilter}>
+						{#snippet children({ onclick, onauxclick, selected })}
+							<Cardlet {name} {onclick} {onauxclick}>
 								{#snippet overlay()}
 									<SelectionOverlay position="right" centered {selected} />
 								{/snippet}
-- 
cgit v1.2.3-2-gb3c3