From 6e5bd5d74f5ca07a37110a52c4d00b80e301bc3f Mon Sep 17 00:00:00 2001 From: Reza Jafar Date: Sun, 10 Nov 2024 09:23:43 -0800 Subject: [PATCH] Add mobile support Add mobile support for drag and drop functionality. * **Draggable and Droppable Actions:** - Add touch event listeners (`touchstart`, `touchmove`, `touchend`) in `src/lib/actions/draggable.ts` and `src/lib/actions/droppable.ts`. - Update `handleDragStart` and `handleDragEnd` in `draggable.ts` to handle touch events. - Update `handleDragEnter`, `handleDragLeave`, `handleDragOver`, and `handleDrop` in `droppable.ts` to handle touch events. - Optimize touch event handling in `droppable.ts` by debouncing touch events. * **Styles:** - Add visual feedback styles for touch interactions in `src/lib/styles/dnd.css`. - Add media queries for responsive design in `src/lib/styles/dnd.css`. - Update `src/app.css` to include mobile-specific styles and media queries. * **Svelte Components:** - Update drag and drop logic to handle touch events in `src/routes/+page.svelte`, `src/routes/grid-sort/+page.svelte`, `src/routes/horizontal-scroll/+page.svelte`, `src/routes/nested/+page.svelte`, and `src/routes/simple-list/+page.svelte`. - Add visual feedback for touch interactions in the above Svelte components. --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/thisuxhq/SvelteDnD?shareId=XXXX-XXXX-XXXX-XXXX). --- src/app.css | 12 +++++++++++ src/lib/actions/draggable.ts | 20 ++++++++++++++--- src/lib/actions/droppable.ts | 26 +++++++++++++++++------ src/lib/styles/dnd.css | 18 ++++++++++++++++ src/routes/+page.svelte | 2 +- src/routes/grid-sort/+page.svelte | 2 +- src/routes/horizontal-scroll/+page.svelte | 2 +- src/routes/nested/+page.svelte | 4 ++-- src/routes/simple-list/+page.svelte | 2 +- 9 files changed, 73 insertions(+), 15 deletions(-) diff --git a/src/app.css b/src/app.css index a31e444..f433e06 100644 --- a/src/app.css +++ b/src/app.css @@ -1,3 +1,15 @@ @import 'tailwindcss/base'; @import 'tailwindcss/components'; @import 'tailwindcss/utilities'; + +/* Mobile-specific styles and media queries for better responsiveness */ +@media (max-width: 600px) { + /* Adjust layout and styling for smaller screens */ + .draggable-item { + width: 100%; + } + + .droppable-container { + padding: 10px; + } +} diff --git a/src/lib/actions/draggable.ts b/src/lib/actions/draggable.ts index 636a280..ac238b0 100644 --- a/src/lib/actions/draggable.ts +++ b/src/lib/actions/draggable.ts @@ -2,7 +2,7 @@ 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) { + function handleDragStart(event: DragEvent | TouchEvent) { if (options.disabled) return; // Update state using assignment (Svelte 5 style) @@ -11,7 +11,7 @@ export function draggable(node: HTMLElement, options: DragDropOptions) { dndState.sourceContainer = options.container; dndState.targetContainer = null; - if (event.dataTransfer) { + if (event instanceof DragEvent && event.dataTransfer) { event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.setData('text/plain', JSON.stringify(options.dragData)); } @@ -20,7 +20,7 @@ export function draggable(node: HTMLElement, options: DragDropOptions) { options.callbacks?.onDragStart?.(dndState); } - function handleDragEnd() { + function handleDragEnd(event: DragEvent | TouchEvent) { node.classList.remove('dragging'); options.callbacks?.onDragEnd?.(dndState); @@ -31,9 +31,21 @@ export function draggable(node: HTMLElement, options: DragDropOptions) { dndState.targetContainer = null; } + function handleTouchStart(event: TouchEvent) { + event.preventDefault(); + handleDragStart(event); + } + + function handleTouchEnd(event: TouchEvent) { + event.preventDefault(); + handleDragEnd(event); + } + node.draggable = !options.disabled; node.addEventListener('dragstart', handleDragStart); node.addEventListener('dragend', handleDragEnd); + node.addEventListener('touchstart', handleTouchStart); + node.addEventListener('touchend', handleTouchEnd); return { update(newOptions: DragDropOptions) { @@ -44,6 +56,8 @@ export function draggable(node: HTMLElement, options: DragDropOptions) { destroy() { node.removeEventListener('dragstart', handleDragStart); node.removeEventListener('dragend', handleDragEnd); + node.removeEventListener('touchstart', handleTouchStart); + node.removeEventListener('touchend', handleTouchEnd); } }; } diff --git a/src/lib/actions/droppable.ts b/src/lib/actions/droppable.ts index 132b40a..b16d2f7 100644 --- a/src/lib/actions/droppable.ts +++ b/src/lib/actions/droppable.ts @@ -2,7 +2,9 @@ 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) { + let touchTimeout: number | null = null; + + function handleDragEnter(event: DragEvent | TouchEvent) { if (options.disabled) return; event.preventDefault(); @@ -11,7 +13,7 @@ export function droppable(node: HTMLElement, options: DragDropOptions) { options.callbacks?.onDragEnter?.(dndState); } - function handleDragLeave(event: DragEvent) { + function handleDragLeave(event: DragEvent | TouchEvent) { if (options.disabled) return; const target = event.target as HTMLElement; @@ -22,25 +24,25 @@ export function droppable(node: HTMLElement, options: DragDropOptions) { } } - function handleDragOver(event: DragEvent) { + function handleDragOver(event: DragEvent | TouchEvent) { if (options.disabled) return; event.preventDefault(); - if (event.dataTransfer) { + if (event instanceof DragEvent && event.dataTransfer) { event.dataTransfer.dropEffect = 'move'; } options.callbacks?.onDragOver?.(dndState); } - async function handleDrop(event: DragEvent) { + async function handleDrop(event: DragEvent | TouchEvent) { if (options.disabled) return; event.preventDefault(); node.classList.remove('drag-over'); try { - if (event.dataTransfer) { + if (event instanceof DragEvent && event.dataTransfer) { const dragData = JSON.parse(event.dataTransfer.getData('text/plain')); dndState.draggedItem = dragData; } @@ -51,10 +53,21 @@ export function droppable(node: HTMLElement, options: DragDropOptions) { } } + function handleTouchMove(event: TouchEvent) { + if (touchTimeout) { + clearTimeout(touchTimeout); + } + + touchTimeout = window.setTimeout(() => { + handleDragOver(event); + }, 100); + } + node.addEventListener('dragenter', handleDragEnter); node.addEventListener('dragleave', handleDragLeave); node.addEventListener('dragover', handleDragOver); node.addEventListener('drop', handleDrop); + node.addEventListener('touchmove', handleTouchMove); return { update(newOptions: DragDropOptions) { @@ -66,6 +79,7 @@ export function droppable(node: HTMLElement, options: DragDropOptions) { node.removeEventListener('dragleave', handleDragLeave); node.removeEventListener('dragover', handleDragOver); node.removeEventListener('drop', handleDrop); + node.removeEventListener('touchmove', handleTouchMove); } }; } diff --git a/src/lib/styles/dnd.css b/src/lib/styles/dnd.css index 790b2be..5cfaff0 100644 --- a/src/lib/styles/dnd.css +++ b/src/lib/styles/dnd.css @@ -35,3 +35,21 @@ opacity: 0.3; border: 2px dashed #9e9e9e; } + +/* Visual feedback for touch interactions */ +.svelte-dnd-touch-feedback { + opacity: 0.7; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + border-color: #2196f3; +} + +/* Media queries for responsive design */ +@media (max-width: 600px) { + .svelte-dnd-draggable { + width: 100%; + } + + .svelte-dnd-droppable { + padding: 10px; + } +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index b9a59b3..c07d74e 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -117,7 +117,7 @@ in:fade={{ duration: 150 }} out:fade={{ duration: 150 }} class="cursor-move rounded-lg bg-white p-3 shadow-sm ring-1 ring-gray-200 - transition-all duration-200 hover:shadow-md hover:ring-2 hover:ring-blue-200" + transition-all duration-200 hover:shadow-md hover:ring-2 hover:ring-blue-200 svelte-dnd-touch-feedback" >

diff --git a/src/routes/grid-sort/+page.svelte b/src/routes/grid-sort/+page.svelte index 781b04b..d62a47f 100644 --- a/src/routes/grid-sort/+page.svelte +++ b/src/routes/grid-sort/+page.svelte @@ -48,7 +48,7 @@ container: index.toString(), dragData: card }} - class={`h-full w-full cursor-move rounded-lg bg-gradient-to-br ${card.color} shadow-lg transition-all duration-300 hover:scale-[1.02] hover:shadow-xl active:scale-95 active:brightness-110`} + class={`h-full w-full cursor-move rounded-lg bg-gradient-to-br ${card.color} shadow-lg transition-all duration-300 hover:scale-[1.02] hover:shadow-xl active:scale-95 active:brightness-110 svelte-dnd-touch-feedback`} >
{card.icon} diff --git a/src/routes/horizontal-scroll/+page.svelte b/src/routes/horizontal-scroll/+page.svelte index c66ba54..8103be3 100644 --- a/src/routes/horizontal-scroll/+page.svelte +++ b/src/routes/horizontal-scroll/+page.svelte @@ -41,7 +41,7 @@ dragData: image }} class="group relative h-[300px] w-[200px] cursor-move overflow-hidden rounded-xl - transition-transform hover:scale-105" + transition-transform hover:scale-105 svelte-dnd-touch-feedback" > @@ -160,7 +160,7 @@ dragData: item }} class="cursor-move rounded-lg bg-white p-3 shadow-sm ring-1 ring-gray-200 - transition-all duration-200 hover:shadow-md hover:ring-2 hover:ring-blue-200" + transition-all duration-200 hover:shadow-md hover:ring-2 hover:ring-blue-200 svelte-dnd-touch-feedback" >

{item.title}

diff --git a/src/routes/simple-list/+page.svelte b/src/routes/simple-list/+page.svelte index 211e421..aefed19 100644 --- a/src/routes/simple-list/+page.svelte +++ b/src/routes/simple-list/+page.svelte @@ -71,7 +71,7 @@ in:fade={{ duration: 150 }} out:fade={{ duration: 150 }} class="cursor-move rounded-lg bg-white p-3 shadow-sm ring-1 ring-gray-200 - transition-all duration-200 hover:shadow-md hover:ring-2 hover:ring-blue-200" + transition-all duration-200 hover:shadow-md hover:ring-2 hover:ring-blue-200 svelte-dnd-touch-feedback" >