Skip to content

Added custom classes and resolved issue #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,8 @@ vite.config.js.timestamp-*
vite.config.ts.timestamp-*

# .github
.github/copilot-instructions.md
.github/copilot-instructions.md

# pnpm

pnpm-lock.yaml
12 changes: 8 additions & 4 deletions src/lib/actions/draggable.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { dndState } from '$lib/stores/dnd.svelte.js';
import type { DragDropOptions, DragDropState } from '$lib/types/index.js';

const DEFAULT_DRAGGING_CLASS = 'dragging';

export function draggable<T>(node: HTMLElement, options: DragDropOptions<T>) {
const draggingClass = (options.attributes?.draggingClass || DEFAULT_DRAGGING_CLASS).split(' ');

function handleDragStart(event: DragEvent) {
if (options.disabled) return;

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

node.classList.add('dragging');
node.classList.add(...draggingClass);
options.callbacks?.onDragStart?.(dndState as DragDropState<T>);
}

function handleDragEnd() {
node.classList.remove('dragging');
node.classList.remove(...draggingClass);
options.callbacks?.onDragEnd?.(dndState as DragDropState<T>);

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

node.setPointerCapture(event.pointerId);
node.classList.add('dragging');
node.classList.add(...draggingClass);
options.callbacks?.onDragStart?.(dndState as DragDropState<T>);
}

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

node.releasePointerCapture(event.pointerId);
node.classList.remove('dragging');
node.classList.remove(...draggingClass);
options.callbacks?.onDragEnd?.(dndState as DragDropState<T>);

// Reset state
Expand Down
33 changes: 23 additions & 10 deletions src/lib/actions/droppable.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
import { dndState } from '$lib/stores/dnd.svelte.js';
import type { DragDropOptions, DragDropState } from '$lib/types/index.js';

const DEFAULT_DRAG_OVER_CLASS = 'drag-over';

export function droppable<T>(node: HTMLElement, options: DragDropOptions<T>) {
const dragOverClass = (options.attributes?.draggingClass || DEFAULT_DRAG_OVER_CLASS).split(' ');

function handleDragEnter(event: DragEvent) {
if (options.disabled) return;
event.preventDefault();

const target = event.target as HTMLElement;

dndState.targetContainer = options.container;
node.classList.add('drag-over');
dndState.targetElement = target;

node.classList.add(...dragOverClass);
options.callbacks?.onDragEnter?.(dndState as DragDropState<T>);
}

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

const target = event.target as HTMLElement;
if (!node.contains(target)) {
dndState.targetContainer = null;
node.classList.remove('drag-over');
options.callbacks?.onDragLeave?.(dndState as DragDropState<T>);
}

// check if element is still being dragged over
if (!dndState.targetElement?.isSameNode(target)) return;

node.classList.remove(...dragOverClass);

options.callbacks?.onDragLeave?.(dndState as DragDropState<T>);

dndState.targetContainer = null;
dndState.targetElement = null;
}

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

node.classList.remove('drag-over');
node.classList.remove(...dragOverClass);

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

dndState.targetContainer = options.container;
node.classList.add('drag-over');
node.classList.add(...dragOverClass);
options.callbacks?.onDragEnter?.(dndState as DragDropState<T>);
}

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

dndState.targetContainer = null;
node.classList.remove('drag-over');
node.classList.remove(...dragOverClass);
options.callbacks?.onDragLeave?.(dndState as DragDropState<T>);
}

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

node.classList.remove('drag-over');
node.classList.remove(...dragOverClass);
options.callbacks?.onDrop?.(dndState as DragDropState<T>);
}

Expand Down
3 changes: 2 additions & 1 deletion src/lib/stores/dnd.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export const dndState = $state<DragDropState>({
isDragging: false,
draggedItem: null,
sourceContainer: '',
targetContainer: null
targetContainer: null,
targetElement: null
});
7 changes: 7 additions & 0 deletions src/lib/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export interface DragDropState<T = unknown> {
draggedItem: T;
sourceContainer: string;
targetContainer: string | null;
targetElement: HTMLElement | null;
}

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

export interface DragDropAttributes {
draggingClass?: string;
dragOverClass?: string;
}

export interface DragDropOptions<T = unknown> {
dragData?: T;
container: string;
disabled?: boolean;
callbacks?: DragDropCallbacks<T>;
attributes?: DragDropAttributes;
}
110 changes: 110 additions & 0 deletions src/routes/custom-classes/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<script lang="ts">
import { draggable, droppable, type DragDropState } from '$lib/index.js';
import { flip } from 'svelte/animate';
import { fade } from 'svelte/transition';

interface Item {
id: string;
title: string;
description: string;
priority: 'low' | 'medium' | 'high';
}

const items = $state<Item[]>([
{
id: '1',
title: 'Design System Updates',
description: 'Update color palette and component library',
priority: 'high'
},
{
id: '2',
title: 'User Research',
description: 'Conduct interviews with 5 key customers',
priority: 'medium'
},
{
id: '3',
title: 'API Documentation',
description: 'Document new endpoints and examples',
priority: 'low'
}
]);

function handleDrop(state: DragDropState<Item>) {
const { draggedItem, targetContainer } = state;
const dragIndex = items.findIndex((item: Item) => item.id === draggedItem.id);
const dropIndex = parseInt(targetContainer ?? '0');

if (dragIndex !== -1 && !isNaN(dropIndex)) {
const [item] = items.splice(dragIndex, 1);
items.splice(dropIndex, 0, item);
}
}

const getPriorityColor = (priority: Item['priority']) => {
return {
low: 'bg-blue-50 text-blue-700',
medium: 'bg-yellow-50 text-yellow-700',
high: 'bg-red-50 text-red-700'
}[priority];
};
</script>

<div class="min-h-screen bg-gray-50 p-8">
<div class="mb-8 flex flex-col gap-2">
<h1 class="text-2xl font-bold text-gray-900">Sortable List</h1>
<p class="text-gray-600">Drag and drop items to reorder them in the list.</p>
</div>

<div class="w-80">
<div class="rounded-xl bg-gray-100 p-4 shadow-sm ring-1 ring-gray-200">
<div class="space-y-3">
{#each items as item, index (item.id)}
<div
use:draggable={{ container: index.toString(), dragData: item }}
use:droppable={{
container: index.toString(),
callbacks: { onDrop: handleDrop },
attributes: {
draggingClass: 'border border-blue-500',
dragOverClass: 'border border-red-500'
}
}}
animate:flip={{ duration: 200 }}
in:fade={{ duration: 150 }}
out:fade={{ duration: 150 }}
class="svelte-dnd-touch-feedback cursor-move rounded-lg bg-white p-3 shadow-sm
ring-gray-200 transition-all duration-200 hover:shadow-md hover:ring-2 hover:ring-blue-200"
>
<div class="mb-2 flex items-start justify-between gap-2">
<h3 class="font-medium text-gray-900">
{item.title}
</h3>
<span
class={`rounded-full px-2 py-0.5 text-xs font-medium ${getPriorityColor(
item.priority
)}`}
>
{item.priority}
</span>
</div>
<p class="text-sm text-gray-500">
{item.description}
</p>
</div>
{/each}
</div>
</div>
</div>
</div>

<style>
:global(.dragging) {
@apply opacity-50 shadow-lg ring-2 ring-blue-400;
}

:global(.drag-over) {
@apply bg-blue-50;
}
</style>