diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7456665 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + - run: npm ci + - run: npm run check + - run: npm run lint + - run: npm test + + publish: + needs: test + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + - run: npm ci + - run: npm run build + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..715b548 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +node_modules + +# Output +.output +.vercel +/.svelte-kit +/build +/dist + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..ab78a95 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..7ebb855 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,15 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..f13e9ec --- /dev/null +++ b/README.md @@ -0,0 +1,257 @@ +# SvelteDnD + +A lightweight drag and drop library for Svelte 5 applications. Built with TypeScript and Svelte's new runes system. + +## Installation + +```bash +npm install sveltednd +``` + +## Quick Start + +```typescript +import { draggable, droppable } from 'sveltednd'; +import 'sveltednd/styles.css'; + +// Create a list of items +let items = $state(['Item 1', 'Item 2', 'Item 3']); + +// Handle drops between containers +function handleDrop(state) { + const { draggedItem, sourceContainer, targetContainer } = state; + if (!targetContainer || sourceContainer === targetContainer) return; + + items = items.filter((item) => item !== draggedItem); + items = [...items, draggedItem]; +} +``` + +```svelte + +
+ {#each items as item} + +
+ {item} +
+ {/each} +
+``` + +## Core Concepts + +### 1. Draggable Items + +- Add `use:draggable` to make elements draggable +- Specify container ID and data to transfer +- Optional callbacks for drag start/end + +### 2. Droppable Containers + +- Add `use:droppable` to create drop zones +- Handle drops via callbacks +- Visual feedback during drag operations + +### 3. State Management + +- Built-in state tracking via Svelte 5 runes +- Access current drag state via `dndState` store +- Automatic cleanup and memory management + +## API Reference + +### Draggable Action + +```typescript +interface DraggableOptions { + container: string; // Container identifier + dragData: any; // Data to transfer + disabled?: boolean; // Disable dragging + callbacks?: { + onDragStart?: (state: DragDropState) => void; + onDragEnd?: (state: DragDropState) => void; + } +} + +// Usage +
console.log('Started dragging', state) + } +}}> +``` + +### Droppable Action + +```typescript +interface DroppableOptions { + container: string; // Container identifier + disabled?: boolean; // Disable dropping + callbacks?: { + onDragEnter?: (state: DragDropState) => void; + onDragLeave?: (state: DragDropState) => void; + onDragOver?: (state: DragDropState) => void; + onDrop?: (state: DragDropState) => Promise | void; + } +} + +// Usage +
handleDrop(state) + } +}}> +``` + +### DragDropState Interface + +```typescript +interface DragDropState { + isDragging: boolean; // Current drag status + draggedItem: any; // Item being dragged + sourceContainer: string; // Origin container ID + targetContainer: string | null; // Current target container ID +} +``` + +## Examples + +### Basic List + +```svelte + + +
+ {#each items as item} +
+ {item} +
+ {/each} +
+``` + +### Multiple Containers + +```svelte + + +
+
+ {#each container1 as item} +
+ {item} +
+ {/each} +
+ +
+ {#each container2 as item} +
+ {item} +
+ {/each} +
+
+``` + +### Conditional Dropping + +```svelte + + +
+``` + +## Styling + +The library provides CSS classes for styling drag and drop states: + +```css +/* Base styles */ +.svelte-dnd-draggable { + cursor: grab; +} + +/* Active dragging */ +.svelte-dnd-dragging { + opacity: 0.5; + cursor: grabbing; +} + +/* Valid drop target */ +.svelte-dnd-drop-target { + outline: 2px dashed #4caf50; +} + +/* Invalid drop target */ +.svelte-dnd-invalid-target { + outline: 2px dashed #f44336; +} +``` + +## TypeScript Support + +The library is written in TypeScript and provides full type definitions. Use interfaces to type your dragged items: + +```typescript +interface Task { + id: string; + title: string; +} + +function handleDrop(state: DragDropState) { + const draggedTask = state.draggedItem as Task; + // TypeScript now knows the shape of draggedTask +} +``` + +## Performance Tips + +1. Use unique IDs as keys in loops +2. Keep drag data minimal +3. Avoid expensive operations in drag callbacks +4. Use `$derived` for computed values + +## License + +MIT diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..7f4e3ea Binary files /dev/null and b/bun.lockb differ diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..a526565 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,33 @@ +import prettier from 'eslint-config-prettier'; +import js from '@eslint/js'; +import svelte from 'eslint-plugin-svelte'; +import globals from 'globals'; +import ts from 'typescript-eslint'; + +export default ts.config( + js.configs.recommended, + ...ts.configs.recommended, + ...svelte.configs['flat/recommended'], + prettier, + ...svelte.configs['flat/prettier'], + { + languageOptions: { + globals: { + ...globals.browser, + ...globals.node + } + } + }, + { + files: ['**/*.svelte'], + + languageOptions: { + parserOptions: { + parser: ts.parser + } + } + }, + { + ignores: ['build/', '.svelte-kit/', 'dist/'] + } +); diff --git a/package.json b/package.json new file mode 100644 index 0000000..fd19805 --- /dev/null +++ b/package.json @@ -0,0 +1,75 @@ +{ + "name": "sveltednd", + "version": "0.0.1", + "description": "A lightweight, flexible drag and drop library for Svelte 5 applications.", + "author": "sanju ", + "contributors": [ + "sanju " + ], + "license": "MIT", + "keywords": [ + "svelte", + "drag", + "drop", + "dnd", + "drag-and-drop", + "typescript", + "svelte5" + ], + "scripts": { + "dev": "vite dev", + "build": "vite build && npm run package", + "preview": "vite preview", + "package": "svelte-kit sync && svelte-package && publint", + "prepublishOnly": "npm run package", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "format": "prettier --write .", + "lint": "prettier --check . && eslint .", + "test:unit": "vitest", + "test": "npm run test:unit -- --run" + }, + "files": [ + "dist", + "!dist/**/*.test.*", + "!dist/**/*.spec.*" + ], + "sideEffects": [ + "**/*.css" + ], + "svelte": "./dist/index.js", + "types": "./dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "svelte": "./dist/index.js" + } + }, + "peerDependencies": { + "svelte": "^5.0.0" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^3.0.0", + "@sveltejs/kit": "^2.0.0", + "@sveltejs/package": "^2.0.0", + "@sveltejs/vite-plugin-svelte": "^4.0.0", + "@types/eslint": "^9.6.0", + "autoprefixer": "^10.4.20", + "eslint": "^9.7.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-svelte": "^2.36.0", + "globals": "^15.0.0", + "prettier": "^3.3.2", + "prettier-plugin-svelte": "^3.2.6", + "prettier-plugin-tailwindcss": "^0.6.5", + "publint": "^0.2.0", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "tailwindcss": "^3.4.9", + "typescript": "^5.0.0", + "typescript-eslint": "^8.0.0", + "vite": "^5.0.11", + "vitest": "^2.0.4" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..0f77216 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/src/app.css b/src/app.css new file mode 100644 index 0000000..a31e444 --- /dev/null +++ b/src/app.css @@ -0,0 +1,3 @@ +@import 'tailwindcss/base'; +@import 'tailwindcss/components'; +@import 'tailwindcss/utilities'; diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..f22aeaa --- /dev/null +++ b/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/src/demo.spec.ts b/src/demo.spec.ts new file mode 100644 index 0000000..e07cbbd --- /dev/null +++ b/src/demo.spec.ts @@ -0,0 +1,7 @@ +import { describe, it, expect } from 'vitest'; + +describe('sum test', () => { + it('adds 1 + 2 to equal 3', () => { + expect(1 + 2).toBe(3); + }); +}); diff --git a/src/lib/actions/draggable.ts b/src/lib/actions/draggable.ts new file mode 100644 index 0000000..636a280 --- /dev/null +++ b/src/lib/actions/draggable.ts @@ -0,0 +1,49 @@ +import { dndState } from '$lib/stores/dnd.svelte.js'; +import type { DragDropOptions } from '$lib/types/index.js'; + +export function draggable(node: HTMLElement, options: DragDropOptions) { + function handleDragStart(event: DragEvent) { + if (options.disabled) return; + + // Update state using assignment (Svelte 5 style) + dndState.isDragging = true; + dndState.draggedItem = options.dragData; + dndState.sourceContainer = options.container; + dndState.targetContainer = null; + + if (event.dataTransfer) { + event.dataTransfer.effectAllowed = 'move'; + event.dataTransfer.setData('text/plain', JSON.stringify(options.dragData)); + } + + node.classList.add('dragging'); + options.callbacks?.onDragStart?.(dndState); + } + + function handleDragEnd() { + node.classList.remove('dragging'); + options.callbacks?.onDragEnd?.(dndState); + + // Reset state + dndState.isDragging = false; + dndState.draggedItem = null; + dndState.sourceContainer = ''; + dndState.targetContainer = null; + } + + node.draggable = !options.disabled; + node.addEventListener('dragstart', handleDragStart); + node.addEventListener('dragend', handleDragEnd); + + return { + update(newOptions: DragDropOptions) { + options = newOptions; + node.draggable = !options.disabled; + }, + + destroy() { + node.removeEventListener('dragstart', handleDragStart); + node.removeEventListener('dragend', handleDragEnd); + } + }; +} diff --git a/src/lib/actions/droppable.ts b/src/lib/actions/droppable.ts new file mode 100644 index 0000000..132b40a --- /dev/null +++ b/src/lib/actions/droppable.ts @@ -0,0 +1,71 @@ +import { dndState } from '$lib/stores/dnd.svelte.js'; +import type { DragDropOptions } from '$lib/types/index.js'; + +export function droppable(node: HTMLElement, options: DragDropOptions) { + function handleDragEnter(event: DragEvent) { + if (options.disabled) return; + event.preventDefault(); + + dndState.targetContainer = options.container; + node.classList.add('drag-over'); + options.callbacks?.onDragEnter?.(dndState); + } + + 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); + } + } + + function handleDragOver(event: DragEvent) { + if (options.disabled) return; + event.preventDefault(); + + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'move'; + } + + options.callbacks?.onDragOver?.(dndState); + } + + async function handleDrop(event: DragEvent) { + if (options.disabled) return; + event.preventDefault(); + + node.classList.remove('drag-over'); + + try { + if (event.dataTransfer) { + const dragData = JSON.parse(event.dataTransfer.getData('text/plain')); + dndState.draggedItem = dragData; + } + + await options.callbacks?.onDrop?.(dndState); + } catch (error) { + console.error('Drop handling failed:', error); + } + } + + node.addEventListener('dragenter', handleDragEnter); + node.addEventListener('dragleave', handleDragLeave); + node.addEventListener('dragover', handleDragOver); + node.addEventListener('drop', handleDrop); + + return { + update(newOptions: DragDropOptions) { + options = newOptions; + }, + + destroy() { + node.removeEventListener('dragenter', handleDragEnter); + node.removeEventListener('dragleave', handleDragLeave); + node.removeEventListener('dragover', handleDragOver); + node.removeEventListener('drop', handleDrop); + } + }; +} diff --git a/src/lib/actions/index.ts b/src/lib/actions/index.ts new file mode 100644 index 0000000..10edd50 --- /dev/null +++ b/src/lib/actions/index.ts @@ -0,0 +1,2 @@ +export { draggable } from './draggable.js'; +export { droppable } from './droppable.js'; diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..c9450e3 --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1,11 @@ +// Actions +export { draggable, droppable } from './actions/index.js'; + +// Store +export { dndState } from './stores/dnd.svelte.js'; + +// Types +export type * from './types/index.js'; + +// Styles +import './styles/dnd.css'; diff --git a/src/lib/stores/dnd.svelte.ts b/src/lib/stores/dnd.svelte.ts new file mode 100644 index 0000000..4bb7456 --- /dev/null +++ b/src/lib/stores/dnd.svelte.ts @@ -0,0 +1,9 @@ +import type { DragDropState } from '$lib/types/index.js'; + +// Global DnD state using Svelte 5's state rune +export const dndState = $state({ + isDragging: false, + draggedItem: null, + sourceContainer: '', + targetContainer: null +}); diff --git a/src/lib/styles/dnd.css b/src/lib/styles/dnd.css new file mode 100644 index 0000000..790b2be --- /dev/null +++ b/src/lib/styles/dnd.css @@ -0,0 +1,37 @@ +/* Base draggable styles */ +.svelte-dnd-draggable { + touch-action: none; /* Prevents touch scrolling while dragging */ + user-select: none; /* Prevents text selection during drag */ +} + +/* Active dragging state */ +.svelte-dnd-dragging { + opacity: 0.5; + cursor: grabbing; +} + +/* Draggable hover state */ +.svelte-dnd-draggable:hover { + cursor: grab; +} + +/* Droppable area styles */ +.svelte-dnd-droppable { + position: relative; +} + +/* Active drop target */ +.svelte-dnd-drop-target { + outline: 2px dashed #4caf50; +} + +/* Invalid drop target */ +.svelte-dnd-invalid-target { + outline: 2px dashed #f44336; +} + +/* Drop preview/placeholder */ +.svelte-dnd-placeholder { + opacity: 0.3; + border: 2px dashed #9e9e9e; +} diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts new file mode 100644 index 0000000..6810d40 --- /dev/null +++ b/src/lib/types/index.ts @@ -0,0 +1,22 @@ +export interface DragDropState { + isDragging: boolean; + draggedItem: any; + sourceContainer: string; + targetContainer: string | null; +} + +export interface DragDropCallbacks { + onDragStart?: (state: DragDropState) => void; + onDragEnter?: (state: DragDropState) => void; + onDragLeave?: (state: DragDropState) => void; + onDragOver?: (state: DragDropState) => void; + onDrop?: (state: DragDropState) => Promise | void; + onDragEnd?: (state: DragDropState) => void; +} + +export interface DragDropOptions { + dragData?: any; + container: string; + disabled?: boolean; + callbacks?: DragDropCallbacks; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..33ddc16 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,60 @@ + + +
+
+ + {@render children()} + +
+
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..c5d6df7 --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,153 @@ + + +
+
+

Kanban Board

+

Drag and drop tasks between columns to reorder them in the board.

+
+ +
+ {#each tasksByStatus as { status, items }} +
+
+
+

+ {status.replace('-', ' ')} +

+ + {items.length} + +
+ +
+ {#each items as task (task.id)} +
+
+

+ {task.title} +

+ + {task.priority} + +
+

+ {task.description} +

+
+ {/each} +
+
+
+ {/each} +
+
+ + diff --git a/src/routes/grid-sort/+page.svelte b/src/routes/grid-sort/+page.svelte new file mode 100644 index 0000000..8d097ed --- /dev/null +++ b/src/routes/grid-sort/+page.svelte @@ -0,0 +1,58 @@ + + +
+
+
+

Sortable List

+

Drag and drop items to reorder them in the list.

+
+ +
+ {#each cards as card, index (card.id)} +
+
+
+ {card.icon} +
+
+
+ Position {index + 1} +
+
{/each} +
+
+
diff --git a/src/routes/horizontal-scroll/+page.svelte b/src/routes/horizontal-scroll/+page.svelte new file mode 100644 index 0000000..199856b --- /dev/null +++ b/src/routes/horizontal-scroll/+page.svelte @@ -0,0 +1,60 @@ + + +
+
+

Horizontal Image Gallery

+

Drag and drop images to rearrange them in the gallery.

+
+ +
+ {#each images as image, index (image.id)} +
+
+ + +
+ + +
+ {index + 1} +
+
+ {/each} +
+
diff --git a/src/routes/nested/+page.svelte b/src/routes/nested/+page.svelte new file mode 100644 index 0000000..d516678 --- /dev/null +++ b/src/routes/nested/+page.svelte @@ -0,0 +1,188 @@ + + +
+

Nested Containers

+ +
+ {#each groups as group, groupIndex (group.id)} +
+
+ +
+
+

{group.title}

+

{group.description}

+
+ + {group.items.length} + +
+ + +
+ {#each group.items as item, itemIndex (item.id)} +
handleItemDrop(group.id, state) + } + }} + > +
+
+

{item.title}

+ + {item.priority} + +
+

{item.description}

+
+
+ {/each} +
+
+
+ {/each} +
+
+ + diff --git a/src/routes/simple-list/+page.svelte b/src/routes/simple-list/+page.svelte new file mode 100644 index 0000000..4ae1274 --- /dev/null +++ b/src/routes/simple-list/+page.svelte @@ -0,0 +1,105 @@ + + +
+
+

Sortable List

+

Drag and drop items to reorder them in the list.

+
+ +
+
+
+ {#each items as item, index (item.id)} +
+
+

+ {item.title} +

+ + {item.priority} + +
+

+ {item.description} +

+
+ {/each} +
+
+
+
+ + diff --git a/static/favicon.png b/static/favicon.png new file mode 100644 index 0000000..825b9e6 Binary files /dev/null and b/static/favicon.png differ diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..1295460 --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,18 @@ +import adapter from '@sveltejs/adapter-auto'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://svelte.dev/docs/kit/adapters for more information about adapters. + adapter: adapter() + } +}; + +export default config; diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..aa4bc77 --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,11 @@ +import type { Config } from 'tailwindcss'; + +export default { + content: ['./src/**/*.{html,js,svelte,ts}'], + + theme: { + extend: {} + }, + + plugins: [] +} satisfies Config; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6f788f1 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "module": "NodeNext", + "moduleResolution": "NodeNext" + } +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..d76fc8a --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; +import { sveltekit } from '@sveltejs/kit/vite'; + +export default defineConfig({ + plugins: [sveltekit()], + + test: { + include: ['src/**/*.{test,spec}.{js,ts}'] + } +});