Add drag-drop system for grids
- Core composable with touch support & operation queue
- DraggableItem & DropZone components
- Proper position mapping for containers
- Cross-container swap/move operations
- Visual feedback states
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
627989bea1
commit
1d0495f1f2
3 changed files with 734 additions and 0 deletions
199
src/lib/components/dnd/DraggableItem.svelte
Normal file
199
src/lib/components/dnd/DraggableItem.svelte
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
<svelte:options runes={true} />
|
||||
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import type { GridItem, GridItemType, DragDropContext } from '$lib/composables/drag-drop.svelte'
|
||||
|
||||
interface Props {
|
||||
item: GridItem | undefined
|
||||
container: string
|
||||
position: number
|
||||
type: GridItemType
|
||||
canDrag?: boolean
|
||||
customPreview?: boolean
|
||||
children?: any
|
||||
}
|
||||
|
||||
let {
|
||||
item,
|
||||
container,
|
||||
position,
|
||||
type,
|
||||
canDrag = true,
|
||||
customPreview = false,
|
||||
children
|
||||
}: Props = $props()
|
||||
|
||||
const dragContext = getContext<DragDropContext>('drag-drop')
|
||||
let elementRef: HTMLElement | undefined = $state()
|
||||
let isDragging = $derived(
|
||||
dragContext?.state.isDragging &&
|
||||
dragContext?.state.draggedItem?.source.container === container &&
|
||||
dragContext?.state.draggedItem?.source.position === position
|
||||
)
|
||||
|
||||
function handleDragStart(e: DragEvent) {
|
||||
if (!canDrag || !item || !dragContext) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
e.dataTransfer!.effectAllowed = 'move'
|
||||
e.dataTransfer!.setData('application/json', JSON.stringify({
|
||||
item,
|
||||
container,
|
||||
position,
|
||||
type
|
||||
}))
|
||||
|
||||
if (customPreview && elementRef) {
|
||||
const ghost = createCustomGhost()
|
||||
e.dataTransfer!.setDragImage(ghost, e.offsetX, e.offsetY)
|
||||
requestAnimationFrame(() => ghost.remove())
|
||||
}
|
||||
|
||||
dragContext.startDrag(item, { container, position, type })
|
||||
}
|
||||
|
||||
function handleDragEnd(e: DragEvent) {
|
||||
if (!dragContext) return
|
||||
dragContext.endDrag()
|
||||
}
|
||||
|
||||
function createCustomGhost(): HTMLElement {
|
||||
if (!elementRef) throw new Error('Element ref not available')
|
||||
|
||||
const ghost = document.createElement('div')
|
||||
ghost.className = 'drag-ghost'
|
||||
ghost.style.position = 'absolute'
|
||||
ghost.style.top = '-1000px'
|
||||
ghost.style.left = '-1000px'
|
||||
ghost.style.transform = 'rotate(5deg) scale(1.05)'
|
||||
ghost.style.opacity = '0.8'
|
||||
|
||||
ghost.innerHTML = elementRef.innerHTML
|
||||
document.body.appendChild(ghost)
|
||||
|
||||
return ghost
|
||||
}
|
||||
|
||||
function handlePointerDown(e: PointerEvent) {
|
||||
if (!canDrag || !item || !dragContext) return
|
||||
dragContext.handlePointerDown(e, item, { container, position }, type)
|
||||
}
|
||||
|
||||
function handlePointerMove(e: PointerEvent) {
|
||||
if (!dragContext) return
|
||||
dragContext.handlePointerMove(e)
|
||||
|
||||
if (dragContext.state.isDragging && e.pointerType === 'touch') {
|
||||
e.preventDefault()
|
||||
dragContext.updateDragPreviewPosition(e.clientX, e.clientY)
|
||||
}
|
||||
}
|
||||
|
||||
function handlePointerUp(e: PointerEvent) {
|
||||
if (!dragContext) return
|
||||
dragContext.handlePointerUp()
|
||||
}
|
||||
|
||||
function handleTouchStart(e: TouchEvent) {
|
||||
if (!canDrag || !item) return
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
function handleTouchMove(e: TouchEvent) {
|
||||
if (!dragContext || !dragContext.state.isDragging) return
|
||||
e.preventDefault()
|
||||
const touch = e.touches[0]
|
||||
if (touch) {
|
||||
dragContext.updateDragPreviewPosition(touch.clientX, touch.clientY)
|
||||
}
|
||||
}
|
||||
|
||||
function handleTouchEnd(e: TouchEvent) {
|
||||
if (!dragContext || !dragContext.state.isDragging) return
|
||||
|
||||
const touch = e.changedTouches[0]
|
||||
if (touch) {
|
||||
const dropTarget = document.elementFromPoint(touch.clientX, touch.clientY)
|
||||
if (dropTarget) {
|
||||
const event = new CustomEvent('drop', {
|
||||
detail: { clientX: touch.clientX, clientY: touch.clientY }
|
||||
})
|
||||
dropTarget.dispatchEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
dragContext.endDrag()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!elementRef || !('ontouchstart' in window)) return
|
||||
|
||||
elementRef.addEventListener('touchstart', handleTouchStart, { passive: false })
|
||||
elementRef.addEventListener('touchmove', handleTouchMove, { passive: false })
|
||||
elementRef.addEventListener('touchend', handleTouchEnd)
|
||||
|
||||
return () => {
|
||||
if (elementRef) {
|
||||
elementRef.removeEventListener('touchstart', handleTouchStart)
|
||||
elementRef.removeEventListener('touchmove', handleTouchMove)
|
||||
elementRef.removeEventListener('touchend', handleTouchEnd)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={elementRef}
|
||||
draggable={canDrag && !!item}
|
||||
ondragstart={handleDragStart}
|
||||
ondragend={handleDragEnd}
|
||||
onpointerdown={handlePointerDown}
|
||||
onpointermove={handlePointerMove}
|
||||
onpointerup={handlePointerUp}
|
||||
class="draggable-item"
|
||||
class:dragging={isDragging}
|
||||
class:can-drag={canDrag && !!item}
|
||||
class:empty={!item}
|
||||
data-container={container}
|
||||
data-position={position}
|
||||
data-type={type}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.draggable-item {
|
||||
position: relative;
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
|
||||
&.dragging {
|
||||
opacity: 0.5;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
&.can-drag {
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
&.empty {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.drag-ghost) {
|
||||
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
</style>
|
||||
179
src/lib/components/dnd/DropZone.svelte
Normal file
179
src/lib/components/dnd/DropZone.svelte
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
<svelte:options runes={true} />
|
||||
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import type { GridItem, GridItemType, DragDropContext } from '$lib/composables/drag-drop.svelte'
|
||||
|
||||
interface Props {
|
||||
container: string
|
||||
position: number
|
||||
type: GridItemType
|
||||
item?: GridItem
|
||||
canDrop?: boolean
|
||||
onDrop?: (item: GridItem) => void
|
||||
children?: any
|
||||
}
|
||||
|
||||
let {
|
||||
container,
|
||||
position,
|
||||
type,
|
||||
item,
|
||||
canDrop = true,
|
||||
onDrop,
|
||||
children
|
||||
}: Props = $props()
|
||||
|
||||
const dragContext = getContext<DragDropContext>('drag-drop')
|
||||
let elementRef: HTMLElement | undefined = $state()
|
||||
|
||||
let isHovered = $derived(
|
||||
dragContext?.state.hoveredOver?.container === container &&
|
||||
dragContext?.state.hoveredOver?.position === position
|
||||
)
|
||||
|
||||
let isValidDrop = $derived(
|
||||
isHovered && dragContext?.state.validDrop
|
||||
)
|
||||
|
||||
let isInvalidDrop = $derived(
|
||||
isHovered && !dragContext?.state.validDrop
|
||||
)
|
||||
|
||||
function handleDragOver(e: DragEvent) {
|
||||
if (!canDrop || !dragContext) return
|
||||
|
||||
e.preventDefault()
|
||||
e.dataTransfer!.dropEffect = 'move'
|
||||
|
||||
if (dragContext.state.draggedItem) {
|
||||
const target = { container, position, type }
|
||||
dragContext.updateHover(target)
|
||||
|
||||
const isValid = dragContext.validateDrop(
|
||||
dragContext.state.draggedItem.source,
|
||||
target
|
||||
)
|
||||
|
||||
if (!isValid) {
|
||||
e.dataTransfer!.dropEffect = 'none'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragEnter(e: DragEvent) {
|
||||
if (!canDrop || !dragContext) return
|
||||
e.preventDefault()
|
||||
|
||||
const target = { container, position, type }
|
||||
dragContext.updateHover(target)
|
||||
}
|
||||
|
||||
function handleDragLeave(e: DragEvent) {
|
||||
if (!dragContext) return
|
||||
|
||||
if (e.relatedTarget && elementRef?.contains(e.relatedTarget as Node)) {
|
||||
return
|
||||
}
|
||||
|
||||
dragContext.updateHover(null)
|
||||
}
|
||||
|
||||
function handleDrop(e: DragEvent) {
|
||||
if (!canDrop || !dragContext) return
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const target = { container, position, type }
|
||||
|
||||
if (dragContext.state.draggedItem && dragContext.state.validDrop) {
|
||||
const success = dragContext.handleDrop(target, item)
|
||||
|
||||
if (success && onDrop && dragContext.state.draggedItem) {
|
||||
onDrop(dragContext.state.draggedItem.data)
|
||||
}
|
||||
}
|
||||
|
||||
dragContext.updateHover(null)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={elementRef}
|
||||
ondragover={handleDragOver}
|
||||
ondragenter={handleDragEnter}
|
||||
ondragleave={handleDragLeave}
|
||||
ondrop={handleDrop}
|
||||
class="drop-zone"
|
||||
class:hovered={isHovered}
|
||||
class:valid-drop={isValidDrop}
|
||||
class:invalid-drop={isInvalidDrop}
|
||||
class:can-drop={canDrop}
|
||||
data-container={container}
|
||||
data-position={position}
|
||||
data-type={type}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.drop-zone {
|
||||
position: relative;
|
||||
transition: all 0.2s ease-out;
|
||||
|
||||
&.hovered {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
&.valid-drop {
|
||||
border: 2px dashed #4CAF50;
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
border-radius: 8px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -4px;
|
||||
border: 2px solid #4CAF50;
|
||||
border-radius: 10px;
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
animation: pulse 1s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
&.invalid-drop {
|
||||
border: 2px dashed #F44336;
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
border-radius: 8px;
|
||||
opacity: 0.7;
|
||||
|
||||
&::after {
|
||||
content: '⚠';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 24px;
|
||||
color: #F44336;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 0.3;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
356
src/lib/composables/drag-drop.svelte.ts
Normal file
356
src/lib/composables/drag-drop.svelte.ts
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
import type { GridCharacter, GridWeapon, GridSummon } from '$lib/types/api/party'
|
||||
|
||||
export type GridItemType = 'character' | 'weapon' | 'summon'
|
||||
export type GridItem = GridCharacter | GridWeapon | GridSummon
|
||||
|
||||
export interface Position {
|
||||
container: string
|
||||
position: number
|
||||
}
|
||||
|
||||
export interface DragSource extends Position {
|
||||
type: GridItemType
|
||||
}
|
||||
|
||||
export interface DraggedItem {
|
||||
type: GridItemType
|
||||
data: GridItem
|
||||
source: DragSource
|
||||
}
|
||||
|
||||
export interface DropTarget extends Position {
|
||||
type: GridItemType
|
||||
}
|
||||
|
||||
export interface DragOperation {
|
||||
id: string
|
||||
type: 'move' | 'swap' | 'reorder'
|
||||
timestamp: number
|
||||
source: {
|
||||
container: string
|
||||
position: number
|
||||
itemId: string
|
||||
}
|
||||
target: {
|
||||
container: string
|
||||
position: number
|
||||
itemId?: string
|
||||
}
|
||||
status: 'pending' | 'synced' | 'failed'
|
||||
retryCount: number
|
||||
}
|
||||
|
||||
export interface TouchState {
|
||||
touchStartPos: { x: number; y: number } | null
|
||||
touchStartTime: number
|
||||
longPressTimer: number | null
|
||||
touchThreshold: number
|
||||
longPressDuration: number
|
||||
currentTouch: Touch | null
|
||||
}
|
||||
|
||||
export interface DragDropState {
|
||||
isDragging: boolean
|
||||
draggedItem: DraggedItem | null
|
||||
hoveredOver: DropTarget | null
|
||||
validDrop: boolean
|
||||
dragPreview: HTMLElement | null
|
||||
operationQueue: DragOperation[]
|
||||
lastError: Error | null
|
||||
touchState: TouchState
|
||||
}
|
||||
|
||||
export interface DragDropHandlers {
|
||||
onDrop?: (from: DragSource, to: DropTarget, item: GridItem) => void
|
||||
onSwap?: (from: DragSource, to: DropTarget, fromItem: GridItem, toItem: GridItem) => void
|
||||
onValidate?: (from: DragSource, to: DropTarget) => boolean
|
||||
onLocalUpdate?: (operation: DragOperation) => void
|
||||
}
|
||||
|
||||
export function createDragDropContext(handlers: DragDropHandlers = {}) {
|
||||
let state = $state<DragDropState>({
|
||||
isDragging: false,
|
||||
draggedItem: null,
|
||||
hoveredOver: null,
|
||||
validDrop: false,
|
||||
dragPreview: null,
|
||||
operationQueue: [],
|
||||
lastError: null,
|
||||
touchState: {
|
||||
touchStartPos: null,
|
||||
touchStartTime: 0,
|
||||
longPressTimer: null,
|
||||
touchThreshold: 10,
|
||||
longPressDuration: 500,
|
||||
currentTouch: null
|
||||
}
|
||||
})
|
||||
|
||||
function detectItemType(item: GridItem): GridItemType {
|
||||
if ('character' in item) return 'character'
|
||||
if ('weapon' in item) return 'weapon'
|
||||
if ('summon' in item) return 'summon'
|
||||
throw new Error('Unknown item type')
|
||||
}
|
||||
|
||||
function handlePointerDown(e: PointerEvent, item: GridItem, source: Position, type: GridItemType) {
|
||||
if (e.pointerType === 'touch') {
|
||||
initiateTouchDrag(e, item, source, type)
|
||||
} else {
|
||||
startDrag(item, { ...source, type })
|
||||
}
|
||||
}
|
||||
|
||||
function initiateTouchDrag(e: PointerEvent, item: GridItem, source: Position, type: GridItemType) {
|
||||
state.touchState.touchStartPos = { x: e.clientX, y: e.clientY }
|
||||
state.touchState.touchStartTime = Date.now()
|
||||
|
||||
state.touchState.longPressTimer = window.setTimeout(() => {
|
||||
startDrag(item, { ...source, type })
|
||||
if ('vibrate' in navigator) {
|
||||
navigator.vibrate(50)
|
||||
}
|
||||
}, state.touchState.longPressDuration)
|
||||
}
|
||||
|
||||
function handlePointerMove(e: PointerEvent) {
|
||||
if (!state.touchState.touchStartPos) return
|
||||
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(e.clientX - state.touchState.touchStartPos.x, 2) +
|
||||
Math.pow(e.clientY - state.touchState.touchStartPos.y, 2)
|
||||
)
|
||||
|
||||
if (distance > state.touchState.touchThreshold && state.touchState.longPressTimer) {
|
||||
clearTimeout(state.touchState.longPressTimer)
|
||||
state.touchState.longPressTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function handlePointerUp() {
|
||||
if (state.touchState.longPressTimer) {
|
||||
clearTimeout(state.touchState.longPressTimer)
|
||||
state.touchState.longPressTimer = null
|
||||
}
|
||||
state.touchState.touchStartPos = null
|
||||
}
|
||||
|
||||
function startDrag(item: GridItem, source: DragSource) {
|
||||
try {
|
||||
console.group('🚀 Drag Start')
|
||||
console.log('Item:', item)
|
||||
console.log('Source:', source)
|
||||
console.groupEnd()
|
||||
|
||||
state.isDragging = true
|
||||
state.draggedItem = {
|
||||
type: source.type,
|
||||
data: item,
|
||||
source
|
||||
}
|
||||
createDragPreview(item)
|
||||
} catch (error) {
|
||||
handleDragError(error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
function createDragPreview(item: GridItem) {
|
||||
const preview = document.createElement('div')
|
||||
preview.className = 'drag-preview'
|
||||
preview.style.position = 'fixed'
|
||||
preview.style.pointerEvents = 'none'
|
||||
preview.style.zIndex = '10000'
|
||||
preview.style.opacity = '0.8'
|
||||
|
||||
const itemName = 'character' in item ? item.character.name :
|
||||
'weapon' in item ? item.weapon.name :
|
||||
'summon' in item ? item.summon.name : 'Unknown'
|
||||
|
||||
preview.innerHTML = `
|
||||
<div class="drag-preview-content" style="padding: 8px; background: white; border: 2px solid #ccc; border-radius: 4px;">
|
||||
<span>${itemName || 'Item'}</span>
|
||||
</div>
|
||||
`
|
||||
document.body.appendChild(preview)
|
||||
state.dragPreview = preview
|
||||
}
|
||||
|
||||
function updateHover(target: DropTarget | null) {
|
||||
state.hoveredOver = target
|
||||
|
||||
if (target && state.draggedItem) {
|
||||
const isValid = validateDrop(state.draggedItem.source, target)
|
||||
state.validDrop = isValid
|
||||
} else {
|
||||
state.validDrop = false
|
||||
}
|
||||
}
|
||||
|
||||
function determineOperationType(source: Position, target: Position, targetHasItem: boolean): 'move' | 'swap' | 'reorder' {
|
||||
if (source.position === target.position && source.container === target.container) return 'reorder'
|
||||
if (targetHasItem) return 'swap'
|
||||
return 'move'
|
||||
}
|
||||
|
||||
function endDrag(targetHasItem: boolean = false) {
|
||||
try {
|
||||
console.group('🏁 Drag End')
|
||||
console.log('Final state:', { ...state })
|
||||
|
||||
if (state.validDrop && state.draggedItem && state.hoveredOver) {
|
||||
const operation: DragOperation = {
|
||||
id: crypto.randomUUID(),
|
||||
type: determineOperationType(state.draggedItem.source, state.hoveredOver, targetHasItem),
|
||||
timestamp: Date.now(),
|
||||
source: {
|
||||
container: state.draggedItem.source.container,
|
||||
position: state.draggedItem.source.position,
|
||||
itemId: state.draggedItem.data.id
|
||||
},
|
||||
target: {
|
||||
container: state.hoveredOver.container,
|
||||
position: state.hoveredOver.position,
|
||||
itemId: targetHasItem ? 'has-item' : undefined
|
||||
},
|
||||
status: 'pending',
|
||||
retryCount: 0
|
||||
}
|
||||
|
||||
state.operationQueue.push(operation)
|
||||
console.log('📝 Operation queued:', operation)
|
||||
|
||||
handlers.onLocalUpdate?.(operation)
|
||||
}
|
||||
|
||||
console.groupEnd()
|
||||
} catch (error) {
|
||||
handleDragError(error as Error)
|
||||
} finally {
|
||||
cleanupDragState()
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragError(error: Error) {
|
||||
console.error('🔥 Drag operation failed:', error)
|
||||
state.lastError = error
|
||||
cleanupDragState()
|
||||
}
|
||||
|
||||
function cleanupDragState() {
|
||||
state.isDragging = false
|
||||
state.draggedItem = null
|
||||
state.hoveredOver = null
|
||||
state.validDrop = false
|
||||
|
||||
if (state.dragPreview) {
|
||||
state.dragPreview.remove()
|
||||
state.dragPreview = null
|
||||
}
|
||||
|
||||
if (state.touchState.longPressTimer) {
|
||||
clearTimeout(state.touchState.longPressTimer)
|
||||
}
|
||||
state.touchState = {
|
||||
...state.touchState,
|
||||
touchStartPos: null,
|
||||
longPressTimer: null,
|
||||
currentTouch: null
|
||||
}
|
||||
}
|
||||
|
||||
function validateDrop(source: DragSource, target: DropTarget): boolean {
|
||||
console.group('🎯 Drop Validation')
|
||||
console.log('Source:', source)
|
||||
console.log('Target:', target)
|
||||
|
||||
// Can't drop on self
|
||||
if (source.container === target.container && source.position === target.position) {
|
||||
console.log('❌ Cannot drop on self')
|
||||
console.groupEnd()
|
||||
return false
|
||||
}
|
||||
|
||||
// Type mismatch check
|
||||
if (source.type !== target.type) {
|
||||
console.log('❌ Type mismatch:', source.type, 'vs', target.type)
|
||||
console.groupEnd()
|
||||
return false
|
||||
}
|
||||
|
||||
// Custom validation
|
||||
if (handlers.onValidate) {
|
||||
const customValid = handlers.onValidate(source, target)
|
||||
console.log(customValid ? '✅ Custom validation passed' : '❌ Custom validation failed')
|
||||
console.groupEnd()
|
||||
return customValid
|
||||
}
|
||||
|
||||
console.log('✅ Drop allowed')
|
||||
console.groupEnd()
|
||||
return true
|
||||
}
|
||||
|
||||
function handleDrop(target: DropTarget, targetItem?: GridItem) {
|
||||
if (!state.draggedItem || !state.validDrop) {
|
||||
console.log('❌ Invalid drop attempt')
|
||||
return false
|
||||
}
|
||||
|
||||
const source = state.draggedItem.source
|
||||
const item = state.draggedItem.data
|
||||
|
||||
console.group('💧 Handle Drop')
|
||||
console.log('From:', source)
|
||||
console.log('To:', target)
|
||||
console.log('Item:', item)
|
||||
console.log('Target Item:', targetItem)
|
||||
|
||||
if (targetItem) {
|
||||
// Swap items
|
||||
console.log('🔄 Swapping items')
|
||||
handlers.onSwap?.(source, target, item, targetItem)
|
||||
} else {
|
||||
// Move to empty slot
|
||||
console.log('📦 Moving to empty slot')
|
||||
handlers.onDrop?.(source, target, item)
|
||||
}
|
||||
|
||||
console.groupEnd()
|
||||
endDrag(!!targetItem)
|
||||
return true
|
||||
}
|
||||
|
||||
function updateDragPreviewPosition(x: number, y: number) {
|
||||
if (state.dragPreview) {
|
||||
state.dragPreview.style.left = `${x + 10}px`
|
||||
state.dragPreview.style.top = `${y - 20}px`
|
||||
}
|
||||
}
|
||||
|
||||
function getQueuedOperations() {
|
||||
return state.operationQueue.filter(op => op.status === 'pending')
|
||||
}
|
||||
|
||||
function clearQueue() {
|
||||
state.operationQueue = []
|
||||
}
|
||||
|
||||
return {
|
||||
get state() {
|
||||
return state
|
||||
},
|
||||
startDrag,
|
||||
updateHover,
|
||||
endDrag,
|
||||
validateDrop,
|
||||
handleDrop,
|
||||
handlePointerDown,
|
||||
handlePointerMove,
|
||||
handlePointerUp,
|
||||
updateDragPreviewPosition,
|
||||
getQueuedOperations,
|
||||
clearQueue
|
||||
}
|
||||
}
|
||||
|
||||
export type DragDropContext = ReturnType<typeof createDragDropContext>
|
||||
Loading…
Reference in a new issue