feat(frontend): make filter expression tokens draggable for reordering
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
27d8215a0a
commit
63ea1a4d6a
@ -17,7 +17,13 @@
|
|||||||
let tags = $state<Tag[]>([]);
|
let tags = $state<Tag[]>([]);
|
||||||
let search = $state('');
|
let search = $state('');
|
||||||
let tokens = $state<string[]>(parseDslFilter(value));
|
let tokens = $state<string[]>(parseDslFilter(value));
|
||||||
let tagNames = $derived(new Map(tags.filter((t) => t.id && t.name).map((t) => [t.id as string, t.name as string])));
|
let tagNames = $derived(
|
||||||
|
new Map(
|
||||||
|
tags
|
||||||
|
.filter((t) => t.id && t.name)
|
||||||
|
.map((t) => [t.id as string, t.name as string]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
tokens = parseDslFilter(value ?? null);
|
tokens = parseDslFilter(value ?? null);
|
||||||
@ -52,6 +58,39 @@
|
|||||||
search = '';
|
search = '';
|
||||||
onApply(null);
|
onApply(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Drag-and-drop reordering ---
|
||||||
|
let dragIndex = $state<number | null>(null);
|
||||||
|
let dropIndex = $state<number | null>(null);
|
||||||
|
|
||||||
|
function onDragStart(i: number, e: DragEvent) {
|
||||||
|
dragIndex = i;
|
||||||
|
e.dataTransfer!.effectAllowed = 'move';
|
||||||
|
// Set minimal drag image so the token itself acts as the ghost
|
||||||
|
e.dataTransfer!.setData('text/plain', String(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragOver(i: number, e: DragEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer!.dropEffect = 'move';
|
||||||
|
dropIndex = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrop(i: number, e: DragEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (dragIndex === null || dragIndex === i) return;
|
||||||
|
const next = [...tokens];
|
||||||
|
const [moved] = next.splice(dragIndex, 1);
|
||||||
|
next.splice(i, 0, moved);
|
||||||
|
tokens = next;
|
||||||
|
dragIndex = null;
|
||||||
|
dropIndex = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragEnd() {
|
||||||
|
dragIndex = null;
|
||||||
|
dropIndex = null;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="bar">
|
<div class="bar">
|
||||||
@ -61,9 +100,24 @@
|
|||||||
<span class="hint">No filter — tap a tag or operator below to build one</span>
|
<span class="hint">No filter — tap a tag or operator below to build one</span>
|
||||||
{:else}
|
{:else}
|
||||||
{#each tokens as token, i (i)}
|
{#each tokens as token, i (i)}
|
||||||
<button class="token active-token" onclick={() => removeToken(i)} title="Remove">
|
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||||
|
<div
|
||||||
|
class="token active-token"
|
||||||
|
class:dragging={dragIndex === i}
|
||||||
|
class:drop-before={dropIndex === i && dragIndex !== null && dragIndex !== i}
|
||||||
|
draggable="true"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
title="Drag to reorder · Click to remove"
|
||||||
|
ondragstart={(e) => onDragStart(i, e)}
|
||||||
|
ondragover={(e) => onDragOver(i, e)}
|
||||||
|
ondrop={(e) => onDrop(i, e)}
|
||||||
|
ondragend={onDragEnd}
|
||||||
|
onclick={() => removeToken(i)}
|
||||||
|
onkeydown={(e) => e.key === 'Delete' && removeToken(i)}
|
||||||
|
>
|
||||||
{tokenLabel(token, tagNames)}
|
{tokenLabel(token, tagNames)}
|
||||||
</button>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@ -155,12 +209,26 @@
|
|||||||
background-color: var(--color-accent);
|
background-color: var(--color-accent);
|
||||||
color: var(--color-bg-primary);
|
color: var(--color-bg-primary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
transition: opacity 0.15s, outline 0.1s;
|
||||||
|
outline: 2px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.active-token:hover {
|
.active-token:hover {
|
||||||
background-color: var(--color-accent-hover);
|
background-color: var(--color-accent-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.active-token.dragging {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-token.drop-before {
|
||||||
|
outline: 2px solid var(--color-accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.op-token {
|
.op-token {
|
||||||
background-color: color-mix(in srgb, var(--color-accent) 18%, var(--color-bg-elevated));
|
background-color: color-mix(in srgb, var(--color-accent) 18%, var(--color-bg-elevated));
|
||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user