Skip to content

Commit

Permalink
Merge branch 'main' into add-mobile-support
Browse files Browse the repository at this point in the history
  • Loading branch information
Spikeysanju authored Nov 10, 2024
2 parents 6e5bd5d + bdab8a7 commit c8c62bd
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 60 deletions.
108 changes: 83 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# SvelteDnD

[![npm version](https://badge.fury.io/js/%40thisux%2Fsveltednd.svg)](https://www.npmjs.com/package/@thisux/sveltednd)

A lightweight drag and drop library for Svelte 5 applications. Built with TypeScript and Svelte's new runes system.

## Installation
Expand All @@ -14,7 +12,7 @@ npm install @thisux/sveltednd

```typescript
import { draggable, droppable, type DragDropState } from '@thisux/sveltednd';
import '@thisux/sveltednd/styles.css';
import '@thisux/sveltednd/styles.css'; // optional

// Create a list of items
let items = $state(['Item 1', 'Item 2', 'Item 3']);
Expand Down Expand Up @@ -145,9 +143,18 @@ interface DragDropState<T = unknown> {
<script lang="ts">
import { draggable, droppable, type DragDropState } from '@thisux/sveltednd';
let items = $state(['Item 1', 'Item 2', 'Item 3']);
interface Item {
id: string;
name: string;
}
let items = $state<Item[]>([
{ id: '1', name: 'Item 1' },
{ id: '2', name: 'Item 2' },
{ id: '3', name: 'Item 3' }
]);
function handleDrop(state: DragDropState<{ id: string }>) {
function handleDrop(state: DragDropState<Item>) {
const { draggedItem } = state;
items = [...items, draggedItem];
}
Expand All @@ -156,7 +163,7 @@ interface DragDropState<T = unknown> {
<div use:droppable={{ container: 'list', callbacks: { onDrop: handleDrop } }}>
{#each items as item}
<div use:draggable={{ container: 'list', dragData: item }}>
{item}
{item.name}
</div>
{/each}
</div>
Expand All @@ -168,17 +175,29 @@ interface DragDropState<T = unknown> {
<script lang="ts">
import { draggable, droppable, type DragDropState } from '@thisux/sveltednd';
let container1 = $state(['A', 'B']);
let container2 = $state(['C', 'D']);
interface Item {
id: string;
name: string;
}
let container1 = $state<Item[]>([
{ id: 'a', name: 'A' },
{ id: 'b', name: 'B' }
]);
let container2 = $state<Item[]>([
{ id: 'c', name: 'C' },
{ id: 'd', name: 'D' }
]);
function handleDrop(state: DragDropState<{ id: string }>) {
function handleDrop(state: DragDropState<Item>) {
const { sourceContainer, targetContainer, draggedItem } = state;
if (sourceContainer === 'container1') {
container1 = container1.filter((i) => i !== draggedItem);
container1 = container1.filter((i) => i.id !== draggedItem.id);
container2 = [...container2, draggedItem];
} else {
container2 = container2.filter((i) => i !== draggedItem);
container2 = container2.filter((i) => i.id !== draggedItem.id);
container1 = [...container1, draggedItem];
}
}
Expand All @@ -188,15 +207,15 @@ interface DragDropState<T = unknown> {
<div use:droppable={{ container: 'container1', callbacks: { onDrop: handleDrop } }}>
{#each container1 as item}
<div use:draggable={{ container: 'container1', dragData: item }}>
{item}
{item.name}
</div>
{/each}
</div>
<div use:droppable={{ container: 'container2', callbacks: { onDrop: handleDrop } }}>
{#each container2 as item}
<div use:draggable={{ container: 'container2', dragData: item }}>
{item}
{item.name}
</div>
{/each}
</div>
Expand All @@ -209,22 +228,61 @@ interface DragDropState<T = unknown> {
<script lang="ts">
import { draggable, droppable, type DragDropState } from '@thisux/sveltednd';
function handleDragOver(state: DragDropState) {
const { draggedItem } = state;
// Prevent dropping if item doesn't meet criteria
if (!isValidItem(draggedItem)) {
return false;
}
}
interface Item {
id: string;
name: string;
category: string;
}
let items = $state<Item[]>([
{ id: '1', name: 'Item 1', category: 'A' },
{ id: '2', name: 'Item 2', category: 'B' },
{ id: '3', name: 'Item 3', category: 'A' }
]);
function isValidItem(item: Item): boolean {
// Example criterion: Only allow dropping items from category 'A'
return item.category === 'A';
}
function handleDragOver(state: DragDropState<Item>) {
const { draggedItem } = state;
// Prevent dropping if item doesn't meet criteria
if (!isValidItem(draggedItem)) {
// Optionally, add a class to indicate invalid drop target
dndState.invalidDrop = true;
} else {
dndState.invalidDrop = false;
}
}
function handleDrop(state: DragDropState<Item>) {
const { draggedItem, targetContainer } = state;
if (!targetContainer || !isValidItem(draggedItem)) return;
// Handle the drop action
// For example, move the item to a different category or list
items = items.map((item) =>
item.id === draggedItem.id ? { ...item, category: targetContainer } : item
);
}
</script>
<div use:droppable={{
container: 'filtered',
callbacks: {
onDragOver: handleDragOver,
<div
use:droppable={{
container: 'filtered',
callbacks: {
onDragOver: handleDragOver,
onDrop: handleDrop
}
}}>
}}
>
{#each items as item}
<div use:draggable={{ container: 'filtered', dragData: item }}>
{item.name}
</div>
{/each}
</div>
```

### Additional Examples
Expand Down
18 changes: 2 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@thisux/sveltednd",
"version": "0.0.6",
"version": "0.0.10",
"private": false,
"description": "A lightweight, flexible drag and drop library for Svelte 5 applications.",
"author": "sanju <[email protected]>",
Expand Down Expand Up @@ -52,7 +52,6 @@
"svelte": "^5.0.0"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-cloudflare": "^4.7.4",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/package": "^2.0.0",
Expand Down
12 changes: 6 additions & 6 deletions src/lib/actions/draggable.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { dndState } from '$lib/stores/dnd.svelte.js';
import type { DragDropOptions } from '$lib/types/index.js';
import type { DragDropOptions, DragDropState } from '$lib/types/index.js';

export function draggable(node: HTMLElement, options: DragDropOptions) {
export function draggable<T>(node: HTMLElement, options: DragDropOptions<T>) {
function handleDragStart(event: DragEvent | TouchEvent) {

if (options.disabled) return;

// Update state using assignment (Svelte 5 style)
dndState.isDragging = true;
dndState.draggedItem = options.dragData;
dndState.sourceContainer = options.container;
Expand All @@ -17,12 +17,12 @@ export function draggable(node: HTMLElement, options: DragDropOptions) {
}

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

function handleDragEnd(event: DragEvent | TouchEvent) {
node.classList.remove('dragging');
options.callbacks?.onDragEnd?.(dndState);
options.callbacks?.onDragEnd?.(dndState as DragDropState<T>);

// Reset state
dndState.isDragging = false;
Expand All @@ -48,7 +48,7 @@ export function draggable(node: HTMLElement, options: DragDropOptions) {
node.addEventListener('touchend', handleTouchEnd);

return {
update(newOptions: DragDropOptions) {
update(newOptions: DragDropOptions<T>) {
options = newOptions;
node.draggable = !options.disabled;
},
Expand Down
22 changes: 12 additions & 10 deletions src/lib/actions/droppable.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { dndState } from '$lib/stores/dnd.svelte.js';
import type { DragDropOptions } from '$lib/types/index.js';

export function droppable(node: HTMLElement, options: DragDropOptions) {
let touchTimeout: number | null = null;
import type { DragDropOptions, DragDropState } from '$lib/types/index.js';

export function droppable<T>(node: HTMLElement, options: DragDropOptions<T>) {
let touchTimeout: number | null = null;

function handleDragEnter(event: DragEvent | TouchEvent) {

if (options.disabled) return;
event.preventDefault();

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

function handleDragLeave(event: DragEvent | TouchEvent) {
Expand All @@ -20,7 +21,7 @@ export function droppable(node: HTMLElement, options: DragDropOptions) {
if (!node.contains(target)) {
dndState.targetContainer = null;
node.classList.remove('drag-over');
options.callbacks?.onDragLeave?.(dndState);
options.callbacks?.onDragLeave?.(dndState as DragDropState<T>);
}
}

Expand All @@ -32,7 +33,7 @@ export function droppable(node: HTMLElement, options: DragDropOptions) {
event.dataTransfer.dropEffect = 'move';
}

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

async function handleDrop(event: DragEvent | TouchEvent) {
Expand All @@ -42,12 +43,13 @@ export function droppable(node: HTMLElement, options: DragDropOptions) {
node.classList.remove('drag-over');

try {

if (event instanceof DragEvent && event.dataTransfer) {
const dragData = JSON.parse(event.dataTransfer.getData('text/plain'));
const dragData = JSON.parse(event.dataTransfer.getData('text/plain')) as T;
dndState.draggedItem = dragData;
}

await options.callbacks?.onDrop?.(dndState);
await options.callbacks?.onDrop?.(dndState as DragDropState<T>);
} catch (error) {
console.error('Drop handling failed:', error);
}
Expand All @@ -70,7 +72,7 @@ export function droppable(node: HTMLElement, options: DragDropOptions) {
node.addEventListener('touchmove', handleTouchMove);

return {
update(newOptions: DragDropOptions) {
update(newOptions: DragDropOptions<T>) {
options = newOptions;
},

Expand Down
4 changes: 4 additions & 0 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@
<div
class="rounded-xl bg-white p-4 shadow-sm ring-1 ring-gray-200"
use:droppable={{
// The container is the status of the task. e.g. 'todo', 'in-progress', 'done'
container: status,
// When a task is dropped, the handleDrop function is called to update the task's status
callbacks: { onDrop: handleDrop }
}}
>
Expand All @@ -110,7 +112,9 @@
{#each items as task (task.id)}
<div
use:draggable={{
// The container is the status of the task. e.g. 'todo', 'in-progress', 'done'
container: status,
// The dragData is the task that is being dragged
dragData: task
}}
animate:flip={{ duration: 200 }}
Expand Down
2 changes: 1 addition & 1 deletion src/routes/nested/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@
use:droppable={{
container: `${group.id}:${itemIndex}`,
callbacks: {
onDrop: (state) => handleItemDrop(group.id, state)
onDrop: (state: DragDropState<DraggedItem>) => handleItemDrop(group.id, state)
}
}}
>
Expand Down

0 comments on commit c8c62bd

Please sign in to comment.