Skip to content

Commit e961643

Browse files
authored
Merge pull request #10 from thisuxhq/add-drop-validation
feat: add conditional check example and update dependencies to versio…
2 parents ae8cc2f + 7742efc commit e961643

File tree

6 files changed

+203
-68
lines changed

6 files changed

+203
-68
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,7 @@ interface DragDropAttributes {
452452
- **[Nested Containers](https://github.com/thisuxhq/SvelteDnD/blob/main/src/routes/nested/+page.svelte)**: Explore the example in `src/routes/nested/+page.svelte`.
453453
- **[Custom Classes](https://github.com/thisuxhq/SvelteDnD/blob/main/src/routes/custom-classes/+page.svelte)**: Explore the example in `src/routes/custom-classes/+page.svelte`.
454454
- **[Interactive Elements](https://github.com/thisuxhq/SvelteDnD/blob/main/src/routes/interactive-elements/+page.svelte)**: Explore the example in `src/routes/interactive-elements/+page.svelte`.
455+
- **[Conditional Check](https://github.com/thisuxhq/SvelteDnD/blob/main/src/routes/conditional-check/+page.svelte)**: Explore the example in `src/routes/conditional-check/+page.svelte`.
455456

456457
## Styling
457458

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@thisux/sveltednd",
3-
"version": "0.0.17",
3+
"version": "0.0.18",
44
"private": false,
55
"description": "A lightweight, flexible drag and drop library for Svelte 5 applications.",
66
"author": "sanju <[email protected]>",
@@ -75,6 +75,6 @@
7575
"vitest": "^2.0.4"
7676
},
7777
"dependencies": {
78-
"@thisux/sveltednd": "^0.0.14"
78+
"@thisux/sveltednd": "^0.0.17"
7979
}
8080
}

src/lib/stores/dnd.svelte.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ export const dndState = $state<DragDropState>({
66
draggedItem: null,
77
sourceContainer: '',
88
targetContainer: null,
9-
targetElement: null
9+
targetElement: null,
10+
invalidDrop: false
1011
});

src/lib/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export interface DragDropState<T = unknown> {
44
sourceContainer: string;
55
targetContainer: string | null;
66
targetElement: HTMLElement | null;
7+
invalidDrop?: boolean;
78
}
89

910
export interface DragDropCallbacks<T = unknown> {

src/routes/+layout.svelte

Lines changed: 51 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -12,85 +12,71 @@
1212
{ path: '/nested', title: 'Nested Containers' },
1313
{ path: '/multiple', title: 'Multiple' },
1414
{ path: '/custom-classes', title: 'Custom Classes' },
15-
{ path: '/interactive-elements', title: 'Interactives' }
15+
{ path: '/interactive-elements', title: 'Interactives' },
16+
{ path: '/conditional-check', title: 'Conditional Check' }
1617
];
1718
1819
const cn = (...classes: string[]) => classes.filter(Boolean).join(' ');
1920
</script>
2021

21-
<div class="mx-auto min-h-screen w-full bg-gray-100">
22-
<div class="flex flex-col">
23-
<nav class="hidden border-b bg-white px-8 py-4 md:sticky md:top-0 md:block">
24-
<div class="flex items-center justify-between">
25-
<div class="flex gap-4 overflow-x-auto whitespace-nowrap">
26-
<div class="flex-1 overflow-hidden">
27-
<div class="flex gap-4 overflow-x-auto whitespace-nowrap">
28-
{#each examples as { path, title }}
29-
<a
30-
href={path}
31-
class={cn(
32-
'rounded px-3 py-1 text-sm hover:bg-gray-100',
33-
$page.url.pathname === path ? 'text-primary rounded-md bg-gray-100' : ''
34-
)}
35-
>
36-
{title}
37-
</a>
38-
{/each}
39-
</div>
40-
</div>
41-
</div>
42-
<div class="flex gap-2">
43-
<a
44-
href="https://github.com/thisuxhq/sveltednd"
45-
class="flex items-center gap-2 rounded-md bg-[#24292e] px-4 py-2 text-sm text-white hover:bg-[#1b1f23]"
46-
target="_blank"
47-
rel="noopener noreferrer"
22+
<div class="flex min-h-screen bg-gray-100">
23+
<!-- Sidebar -->
24+
<aside class="hidden w-64 border-r bg-white md:block">
25+
<div class="flex h-full flex-col">
26+
<!-- Logo section -->
27+
<div class="border-b p-4">
28+
<div class="flex flex-col space-y-1">
29+
<h1 class="text-primary text-xl font-semibold">SvelteDnD</h1>
30+
<a href="https://thisux.com" target="_blank" class="hover:text-primary text-xs"
31+
>by ThisUX</a
4832
>
49-
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
50-
<path
51-
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
52-
/>
53-
</svg>
54-
GitHub
55-
</a>
33+
</div>
34+
</div>
35+
36+
<!-- Navigation links -->
37+
<div class="flex-1 overflow-y-auto p-4">
38+
{#each examples as { path, title }}
5639
<a
57-
href="https://www.npmjs.com/package/@thisux/sveltednd"
58-
class="flex items-center gap-2 rounded-md bg-[#cb3837] px-4 py-2 text-sm text-white hover:bg-[#ab3231]"
59-
target="_blank"
60-
rel="noopener noreferrer"
40+
href={path}
41+
class={cn(
42+
'block rounded px-3 py-2 text-sm font-medium transition-colors',
43+
'hover:text-primary hover:bg-gray-100',
44+
$page.url.pathname === path ? 'text-primary bg-gray-100' : 'text-gray-600'
45+
)}
6146
>
62-
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
63-
<path
64-
d="M0 7.334v8h6.666v1.332H12v-1.332h12v-8H0zm6.666 6.664H5.334v-4H3.999v4H1.335V8.667h5.331v5.331zm4 0v1.336H8.001V8.667h5.334v5.332h-2.669v-.001zm12.001 0h-1.33v-4h-1.336v4h-1.335v-4h-1.33v4h-2.671V8.667h8.002v5.331zM10.665 10H12v2.667h-1.335V10z"
65-
/>
66-
</svg>
67-
NPM
47+
{title}
6848
</a>
69-
</div>
49+
{/each}
7050
</div>
71-
</nav>
51+
</div>
52+
</aside>
53+
54+
<!-- Main content -->
55+
<div class="flex-1">
7256
{@render children()}
73-
<nav class="sticky bottom-0 border-t bg-white px-8 py-4 md:hidden">
57+
</div>
58+
</div>
59+
60+
<!-- Mobile bottom navigation -->
61+
<nav class="sticky bottom-0 border-t bg-white px-8 py-4 md:hidden">
62+
<div class="flex gap-4 overflow-x-auto whitespace-nowrap">
63+
<div class="flex-1 overflow-hidden">
7464
<div class="flex gap-4 overflow-x-auto whitespace-nowrap">
75-
<div class="flex-1 overflow-hidden">
76-
<div class="flex gap-4 overflow-x-auto whitespace-nowrap">
77-
{#each examples as { path, title }}
78-
<a
79-
href={path}
80-
class={cn(
81-
'rounded px-3 py-1 text-sm hover:bg-gray-100',
82-
$page.url.pathname === path ? 'text-primary rounded-md bg-gray-100' : ''
83-
)}
84-
>
85-
{title}
86-
</a>
87-
{/each}
88-
</div>
89-
</div>
65+
{#each examples as { path, title }}
66+
<a
67+
href={path}
68+
class={cn(
69+
'rounded px-3 py-1 text-sm hover:bg-gray-100',
70+
$page.url.pathname === path ? 'text-primary rounded-md bg-gray-100' : ''
71+
)}
72+
>
73+
{title}
74+
</a>
75+
{/each}
9076
</div>
91-
</nav>
77+
</div>
9278
</div>
93-
</div>
79+
</nav>
9480

9581
<!-- Floating badge (hidden on mobile)-->
9682
<footer class="fixed bottom-8 right-8 z-50 hidden md:block">
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<script lang="ts">
2+
import { draggable, droppable } from '$lib/index.js';
3+
import { dndState } from '$lib/stores/dnd.svelte.js';
4+
import type { DragDropState } from '$lib/types/index.js';
5+
6+
interface Fruit {
7+
id: string;
8+
name: string;
9+
color: string;
10+
}
11+
12+
let sourceFruits = $state([
13+
{ id: '1', name: 'Apple', color: 'Red' },
14+
{ id: '2', name: 'Banana', color: 'Yellow' },
15+
{ id: '3', name: 'Grapes', color: 'Green' },
16+
{ id: '4', name: 'Orange', color: 'Orange' },
17+
{ id: '5', name: 'Pineapple', color: 'Yellow' },
18+
{ id: '6', name: 'Strawberry', color: 'Red' },
19+
{ id: '7', name: 'Watermelon', color: 'Green' }
20+
]);
21+
22+
let targetFruits = $state([]);
23+
24+
// Add a derived state for empty states
25+
let isTargetEmpty = $derived(targetFruits.length === 0);
26+
let isSourceEmpty = $derived(sourceFruits.length === 0);
27+
28+
// Validation function that sets invalidDrop state
29+
function validateDrop(state: DragDropState<Fruit>) {
30+
const fruit = state.draggedItem;
31+
if (!fruit) return;
32+
33+
// Set invalidDrop based on the color condition
34+
dndState.invalidDrop = fruit.color !== 'Red';
35+
}
36+
37+
const dragDropCallbacks = {
38+
onDragOver: (state: DragDropState<Fruit>) => {
39+
validateDrop(state);
40+
},
41+
onDrop: async (state: DragDropState<Fruit>) => {
42+
if (dndState.invalidDrop || !state.draggedItem) {
43+
return; // Prevent invalid drops
44+
}
45+
46+
// Move fruit to target container
47+
sourceFruits = sourceFruits.filter((fruit) => fruit.id !== state.draggedItem.id);
48+
targetFruits = [...targetFruits, state.draggedItem];
49+
},
50+
onDragEnd: () => {
51+
// Reset invalidDrop state when drag ends
52+
dndState.invalidDrop = false;
53+
}
54+
};
55+
</script>
56+
57+
<div class="container mx-auto p-8">
58+
<div class="mb-12 space-y-2">
59+
<h1 class="text-3xl font-bold tracking-tight">Fruit Sorter</h1>
60+
<p class="text-muted-foreground">
61+
Drop the red fruits in the target zone. Other colors will be rejected.
62+
</p>
63+
</div>
64+
65+
<div class="grid gap-8 md:grid-cols-2">
66+
<!-- Source Container -->
67+
<div class="space-y-4">
68+
<h2 class="text-sm font-medium uppercase tracking-wide text-muted-foreground">Available Fruits</h2>
69+
<div
70+
class="min-h-[400px] rounded-lg border bg-white p-4 shadow-sm transition-all"
71+
use:droppable={{ container: 'source' }}
72+
>
73+
{#if isSourceEmpty}
74+
<div class="flex h-full items-center justify-center">
75+
<p class="text-muted-foreground">All fruits have been sorted</p>
76+
</div>
77+
{:else}
78+
<div class="grid gap-2">
79+
{#each sourceFruits as fruit}
80+
<div
81+
use:draggable={{ container: 'source', dragData: fruit }}
82+
class={`group flex items-center justify-between rounded-md border p-3 shadow-sm transition-all hover:shadow
83+
${fruit.color === 'Red' ? 'border-red-200 bg-red-50/50' : 'border-muted bg-muted/5'}`}
84+
>
85+
<span class="font-medium">{fruit.name}</span>
86+
<span
87+
class={`rounded px-2 py-1 text-xs
88+
${fruit.color === 'Red' ? 'bg-red-100 text-red-700' : 'bg-muted/10 text-muted-foreground'}`}
89+
>
90+
{fruit.color}
91+
</span>
92+
</div>
93+
{/each}
94+
</div>
95+
{/if}
96+
</div>
97+
</div>
98+
99+
<!-- Target Container -->
100+
<div class="space-y-4">
101+
<h2 class="text-sm font-medium uppercase tracking-wide text-muted-foreground">Red Fruits Only</h2>
102+
<div
103+
class={`min-h-[400px] rounded-lg border bg-white p-4 shadow-sm transition-all
104+
${
105+
dndState.isDragging
106+
? dndState.invalidDrop
107+
? 'border-red-500/50 bg-red-50/5'
108+
: 'border-blue-500/50 bg-blue-50/5'
109+
: ''
110+
}`}
111+
112+
use:droppable={{
113+
container: 'target',
114+
callbacks: dragDropCallbacks,
115+
attributes: {
116+
dragOverClass: dndState.invalidDrop ? 'invalid-drop' : 'valid-drop'
117+
}
118+
}}
119+
>
120+
{#if isTargetEmpty}
121+
<div class="flex h-full items-center justify-center">
122+
<p class="text-muted-foreground">Drop red fruits here</p>
123+
</div>
124+
{:else}
125+
<div class="grid gap-2">
126+
{#each targetFruits as fruit}
127+
<div class="flex items-center justify-between rounded-md border-red-200 bg-red-50/50 p-3 shadow-sm">
128+
<span class="font-medium">{fruit.name}</span>
129+
<span class="rounded bg-red-100 px-2 py-1 text-xs text-red-700">{fruit.color}</span>
130+
</div>
131+
{/each}
132+
</div>
133+
{/if}
134+
</div>
135+
</div>
136+
</div>
137+
</div>
138+
139+
<style>
140+
.valid-drop {
141+
@apply border-blue-500/50 bg-blue-50/5 ring-2 ring-blue-500/20 ring-offset-2;
142+
}
143+
.invalid-drop {
144+
@apply border-red-500/50 bg-red-50/5 ring-2 ring-red-500/20 ring-offset-2;
145+
}
146+
</style>

0 commit comments

Comments
 (0)