Skip to content

Commit 299c1ec

Browse files
authored
Merge pull request #4 from Bxnq/main
Added custom classes and resolved issue
2 parents 7ae42a7 + bbb2460 commit 299c1ec

File tree

6 files changed

+155
-16
lines changed

6 files changed

+155
-16
lines changed

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,8 @@ vite.config.js.timestamp-*
2222
vite.config.ts.timestamp-*
2323

2424
# .github
25-
.github/copilot-instructions.md
25+
.github/copilot-instructions.md
26+
27+
# pnpm
28+
29+
pnpm-lock.yaml

src/lib/actions/draggable.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { dndState } from '$lib/stores/dnd.svelte.js';
22
import type { DragDropOptions, DragDropState } from '$lib/types/index.js';
33

4+
const DEFAULT_DRAGGING_CLASS = 'dragging';
5+
46
export function draggable<T>(node: HTMLElement, options: DragDropOptions<T>) {
7+
const draggingClass = (options.attributes?.draggingClass || DEFAULT_DRAGGING_CLASS).split(' ');
8+
59
function handleDragStart(event: DragEvent) {
610
if (options.disabled) return;
711

@@ -15,12 +19,12 @@ export function draggable<T>(node: HTMLElement, options: DragDropOptions<T>) {
1519
event.dataTransfer.setData('text/plain', JSON.stringify(options.dragData));
1620
}
1721

18-
node.classList.add('dragging');
22+
node.classList.add(...draggingClass);
1923
options.callbacks?.onDragStart?.(dndState as DragDropState<T>);
2024
}
2125

2226
function handleDragEnd() {
23-
node.classList.remove('dragging');
27+
node.classList.remove(...draggingClass);
2428
options.callbacks?.onDragEnd?.(dndState as DragDropState<T>);
2529

2630
// Reset state
@@ -39,7 +43,7 @@ export function draggable<T>(node: HTMLElement, options: DragDropOptions<T>) {
3943
dndState.targetContainer = null;
4044

4145
node.setPointerCapture(event.pointerId);
42-
node.classList.add('dragging');
46+
node.classList.add(...draggingClass);
4347
options.callbacks?.onDragStart?.(dndState as DragDropState<T>);
4448
}
4549

@@ -53,7 +57,7 @@ export function draggable<T>(node: HTMLElement, options: DragDropOptions<T>) {
5357
if (!dndState.isDragging) return;
5458

5559
node.releasePointerCapture(event.pointerId);
56-
node.classList.remove('dragging');
60+
node.classList.remove(...draggingClass);
5761
options.callbacks?.onDragEnd?.(dndState as DragDropState<T>);
5862

5963
// Reset state

src/lib/actions/droppable.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,38 @@
11
import { dndState } from '$lib/stores/dnd.svelte.js';
22
import type { DragDropOptions, DragDropState } from '$lib/types/index.js';
33

4+
const DEFAULT_DRAG_OVER_CLASS = 'drag-over';
5+
46
export function droppable<T>(node: HTMLElement, options: DragDropOptions<T>) {
7+
const dragOverClass = (options.attributes?.draggingClass || DEFAULT_DRAG_OVER_CLASS).split(' ');
8+
59
function handleDragEnter(event: DragEvent) {
610
if (options.disabled) return;
711
event.preventDefault();
812

13+
const target = event.target as HTMLElement;
14+
915
dndState.targetContainer = options.container;
10-
node.classList.add('drag-over');
16+
dndState.targetElement = target;
17+
18+
node.classList.add(...dragOverClass);
1119
options.callbacks?.onDragEnter?.(dndState as DragDropState<T>);
1220
}
1321

1422
function handleDragLeave(event: DragEvent) {
1523
if (options.disabled) return;
1624

1725
const target = event.target as HTMLElement;
18-
if (!node.contains(target)) {
19-
dndState.targetContainer = null;
20-
node.classList.remove('drag-over');
21-
options.callbacks?.onDragLeave?.(dndState as DragDropState<T>);
22-
}
26+
27+
// check if element is still being dragged over
28+
if (!dndState.targetElement?.isSameNode(target)) return;
29+
30+
node.classList.remove(...dragOverClass);
31+
32+
options.callbacks?.onDragLeave?.(dndState as DragDropState<T>);
33+
34+
dndState.targetContainer = null;
35+
dndState.targetElement = null;
2336
}
2437

2538
function handleDragOver(event: DragEvent) {
@@ -37,7 +50,7 @@ export function droppable<T>(node: HTMLElement, options: DragDropOptions<T>) {
3750
if (options.disabled) return;
3851
event.preventDefault();
3952

40-
node.classList.remove('drag-over');
53+
node.classList.remove(...dragOverClass);
4154

4255
try {
4356
if (event.dataTransfer) {
@@ -55,22 +68,22 @@ export function droppable<T>(node: HTMLElement, options: DragDropOptions<T>) {
5568
if (options.disabled || !dndState.isDragging) return;
5669

5770
dndState.targetContainer = options.container;
58-
node.classList.add('drag-over');
71+
node.classList.add(...dragOverClass);
5972
options.callbacks?.onDragEnter?.(dndState as DragDropState<T>);
6073
}
6174

6275
function handlePointerOut(event: PointerEvent) {
6376
if (options.disabled || !dndState.isDragging) return;
6477

6578
dndState.targetContainer = null;
66-
node.classList.remove('drag-over');
79+
node.classList.remove(...dragOverClass);
6780
options.callbacks?.onDragLeave?.(dndState as DragDropState<T>);
6881
}
6982

7083
function handlePointerUp(event: PointerEvent) {
7184
if (options.disabled || !dndState.isDragging) return;
7285

73-
node.classList.remove('drag-over');
86+
node.classList.remove(...dragOverClass);
7487
options.callbacks?.onDrop?.(dndState as DragDropState<T>);
7588
}
7689

src/lib/stores/dnd.svelte.ts

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

src/lib/types/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export interface DragDropState<T = unknown> {
33
draggedItem: T;
44
sourceContainer: string;
55
targetContainer: string | null;
6+
targetElement: HTMLElement | null;
67
}
78

89
export interface DragDropCallbacks<T = unknown> {
@@ -14,9 +15,15 @@ export interface DragDropCallbacks<T = unknown> {
1415
onDragEnd?: (state: DragDropState<T>) => void;
1516
}
1617

18+
export interface DragDropAttributes {
19+
draggingClass?: string;
20+
dragOverClass?: string;
21+
}
22+
1723
export interface DragDropOptions<T = unknown> {
1824
dragData?: T;
1925
container: string;
2026
disabled?: boolean;
2127
callbacks?: DragDropCallbacks<T>;
28+
attributes?: DragDropAttributes;
2229
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<script lang="ts">
2+
import { draggable, droppable, type DragDropState } from '$lib/index.js';
3+
import { flip } from 'svelte/animate';
4+
import { fade } from 'svelte/transition';
5+
6+
interface Item {
7+
id: string;
8+
title: string;
9+
description: string;
10+
priority: 'low' | 'medium' | 'high';
11+
}
12+
13+
const items = $state<Item[]>([
14+
{
15+
id: '1',
16+
title: 'Design System Updates',
17+
description: 'Update color palette and component library',
18+
priority: 'high'
19+
},
20+
{
21+
id: '2',
22+
title: 'User Research',
23+
description: 'Conduct interviews with 5 key customers',
24+
priority: 'medium'
25+
},
26+
{
27+
id: '3',
28+
title: 'API Documentation',
29+
description: 'Document new endpoints and examples',
30+
priority: 'low'
31+
}
32+
]);
33+
34+
function handleDrop(state: DragDropState<Item>) {
35+
const { draggedItem, targetContainer } = state;
36+
const dragIndex = items.findIndex((item: Item) => item.id === draggedItem.id);
37+
const dropIndex = parseInt(targetContainer ?? '0');
38+
39+
if (dragIndex !== -1 && !isNaN(dropIndex)) {
40+
const [item] = items.splice(dragIndex, 1);
41+
items.splice(dropIndex, 0, item);
42+
}
43+
}
44+
45+
const getPriorityColor = (priority: Item['priority']) => {
46+
return {
47+
low: 'bg-blue-50 text-blue-700',
48+
medium: 'bg-yellow-50 text-yellow-700',
49+
high: 'bg-red-50 text-red-700'
50+
}[priority];
51+
};
52+
</script>
53+
54+
<div class="min-h-screen bg-gray-50 p-8">
55+
<div class="mb-8 flex flex-col gap-2">
56+
<h1 class="text-2xl font-bold text-gray-900">Sortable List</h1>
57+
<p class="text-gray-600">Drag and drop items to reorder them in the list.</p>
58+
</div>
59+
60+
<div class="w-80">
61+
<div class="rounded-xl bg-gray-100 p-4 shadow-sm ring-1 ring-gray-200">
62+
<div class="space-y-3">
63+
{#each items as item, index (item.id)}
64+
<div
65+
use:draggable={{ container: index.toString(), dragData: item }}
66+
use:droppable={{
67+
container: index.toString(),
68+
callbacks: { onDrop: handleDrop },
69+
attributes: {
70+
draggingClass: 'border border-blue-500',
71+
dragOverClass: 'border border-red-500'
72+
}
73+
}}
74+
animate:flip={{ duration: 200 }}
75+
in:fade={{ duration: 150 }}
76+
out:fade={{ duration: 150 }}
77+
class="svelte-dnd-touch-feedback cursor-move rounded-lg bg-white p-3 shadow-sm
78+
ring-gray-200 transition-all duration-200 hover:shadow-md hover:ring-2 hover:ring-blue-200"
79+
>
80+
<div class="mb-2 flex items-start justify-between gap-2">
81+
<h3 class="font-medium text-gray-900">
82+
{item.title}
83+
</h3>
84+
<span
85+
class={`rounded-full px-2 py-0.5 text-xs font-medium ${getPriorityColor(
86+
item.priority
87+
)}`}
88+
>
89+
{item.priority}
90+
</span>
91+
</div>
92+
<p class="text-sm text-gray-500">
93+
{item.description}
94+
</p>
95+
</div>
96+
{/each}
97+
</div>
98+
</div>
99+
</div>
100+
</div>
101+
102+
<style>
103+
:global(.dragging) {
104+
@apply opacity-50 shadow-lg ring-2 ring-blue-400;
105+
}
106+
107+
:global(.drag-over) {
108+
@apply bg-blue-50;
109+
}
110+
</style>

0 commit comments

Comments
 (0)