Skip to content

Commit

Permalink
feat: basic dnd
Browse files Browse the repository at this point in the history
  • Loading branch information
ubermanu committed Jun 2, 2023
1 parent 20094a6 commit b43ef5a
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export * from './components/Tabs/tabs.js'
export * from './components/ToggleButton/toggleButton.js'
export * from './components/Toolbar/toolbar.js'
export * from './components/Tooltip/tooltip.js'
export * from './interactions/DragAndDrop/drag.js'
export * from './interactions/DragAndDrop/drop.js'
export * from './interactions/FocusVisible/focusVisible.js'
export * from './interactions/FocusWithin/focusWithin.js'
export * from './interactions/Hover/hover.js'
Expand Down
57 changes: 57 additions & 0 deletions src/lib/interactions/DragAndDrop/drag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { Action } from 'svelte/action'
import type { SvelteComponent } from 'svelte/internal'
import { get, readable, readonly, writable } from 'svelte/store'

export type DragConfig = {
onDragStart?: (e: DragEvent) => void
onDragEnd?: (e: DragEvent) => void
preview?: SvelteComponent | string
}

// TODO: Add keyboard support
// TODO: Add drag image support (rendered component)
export const useDrag = (config?: DragConfig) => {
const { onDragStart, onDragEnd } = { ...config }

const dragging$ = writable(false)
const data$ = writable<string>('')

const draggableAttrs = readable({
draggable: true,
})

const onDragStartHandler = async (event: DragEvent) => {
dragging$.set(true)
event.dataTransfer?.setData('text/plain', get(data$))
onDragStart?.(event)
}

const onDragEndHandler = (event: DragEvent) => {
dragging$.set(false)
onDragEnd?.(event)
}

const drag: Action = (node, data: string = '') => {
data$.set(data)

node.addEventListener('dragstart', onDragStartHandler)
node.addEventListener('dragend', onDragEndHandler)

return {
update(data: string) {
data$.set(data)
},
destroy() {
dragging$.set(false)
node.removeEventListener('dragstart', onDragStartHandler)
node.removeEventListener('dragend', onDragEndHandler)
},
}
}

return {
dragging: readonly(dragging$),
draggableAttrs,
drag,
}
}
49 changes: 49 additions & 0 deletions src/lib/interactions/DragAndDrop/drop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { Action } from 'svelte/action'
import { readonly, writable } from 'svelte/store'

export type DropConfig = {
accept?: string[]
onDrop?: (e: DragEvent) => void
}

// TODO: Add keyboard support
export const useDrop = (config?: DropConfig) => {
const { onDrop } = { ...config }

const hovering$ = writable(false)

const onDragOverHandler = (e: DragEvent) => {
e.preventDefault()
hovering$.set(true)
}

const onDragLeaveHandler = (e: DragEvent) => {
e.preventDefault()
hovering$.set(false)
}

const onDropHandler = (e: DragEvent) => {
e.preventDefault()
hovering$.set(false)
onDrop?.(e)
}

const drop: Action = (node) => {
node.addEventListener('dragover', onDragOverHandler)
node.addEventListener('dragleave', onDragLeaveHandler)
node.addEventListener('drop', onDropHandler)

return {
destroy() {
node.removeEventListener('dragover', onDragOverHandler)
node.removeEventListener('dragleave', onDragLeaveHandler)
node.removeEventListener('drop', onDropHandler)
},
}
}

return {
hovering: readonly(hovering$),
drop,
}
}
28 changes: 28 additions & 0 deletions src/routes/(docs)/interaction/drag-and-drop/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script lang="ts">
import Page from './example/+page.svelte'
import PageSource from './example/+page.svelte?raw'
import ProductSource from './example/Product.svelte?raw'
import BasketSource from './example/Basket.svelte?raw'
import Snippet from '../../Snippet.svelte'
</script>

<h1>Drag and Drop</h1>

<p>The Drag and Drop interactions allows data transfer between components.</p>

<h2>Example</h2>

<div>
<Page />
</div>

<h2>Sources</h2>

<p>Page</p>
<Snippet code={PageSource} />

<p>Product</p>
<Snippet code={ProductSource} />

<p>Basket</p>
<Snippet code={BasketSource} />
15 changes: 15 additions & 0 deletions src/routes/(docs)/interaction/drag-and-drop/example/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script lang="ts">
import Basket from './Basket.svelte'
import Product from './Product.svelte'
const products = ['🧀', '🧴', '🪥', '🥑', '🥖', '', '🍺', '🍅', '🐟']
</script>

<div class="mb-4 flex w-fit gap-2 rounded">
{#each products as value}
<Product {value} />
{/each}
</div>

<p class="mb-2 text-xs opacity-50">Drag a product into your basket</p>
<Basket items={['🥑', '🐟']} />
28 changes: 28 additions & 0 deletions src/routes/(docs)/interaction/drag-and-drop/example/Basket.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script lang="ts">
import { useDrop } from '$lib'
export let items: string[] = []
const { drop, hovering } = useDrop({
onDrop: (event) => {
const item = event.dataTransfer.getData('text/plain')
items = [...items, item]
},
})
</script>

<div
use:drop
class="mb-2 flex min-h-[3rem] max-w-md flex-wrap gap-2 rounded-md border-2 border-transparent bg-amber-50 p-2 shadow dark:bg-amber-950"
class:is-hovering={$hovering}
>
{#each items as item}
<div class="select-none p-1">{item}</div>
{/each}
</div>

<style lang="postcss">
.is-hovering {
@apply border-dashed border-amber-500 transition-colors;
}
</style>
22 changes: 22 additions & 0 deletions src/routes/(docs)/interaction/drag-and-drop/example/Product.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<script lang="ts">
import { useDrag } from '$lib'
export let value: string
const { dragging, drag, draggableAttrs } = useDrag()
</script>

<button
use:drag={value}
{...$draggableAttrs}
class="rounded border-2 border-transparent p-2 transition-colors transition-opacity hover:bg-neutral-200 dark:hover:bg-neutral-700"
class:is-dragging={$dragging}
>
{value}
</button>

<style lang="postcss">
.is-dragging {
@apply border-amber-500 opacity-50;
}
</style>

0 comments on commit b43ef5a

Please sign in to comment.