diff --git a/src/components/EncodingPanel/EncodingShelf.tsx b/src/components/EncodingPanel/EncodingShelf.tsx index c91ca5d..1ca946b 100644 --- a/src/components/EncodingPanel/EncodingShelf.tsx +++ b/src/components/EncodingPanel/EncodingShelf.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import type { EncodingChannel, DetectedField, FieldType } from '../../types'; +import type { EncodingChannel, FieldType, DragPayload } from '../../types'; import { useApp } from '../../context/AppContext'; const TYPE_COLORS: Record = { @@ -25,12 +25,12 @@ export function EncodingShelf({ channel, label }: EncodingShelfProps) { const { state, assignField, removeField } = useApp(); const [isOver, setIsOver] = useState(false); const [isHoveredRemove, setIsHoveredRemove] = useState(false); + const [isDragging, setIsDragging] = useState(false); const assignedField = state.encodings[channel]; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); - e.dataTransfer.dropEffect = 'copy'; setIsOver(true); }; @@ -42,11 +42,46 @@ export function EncodingShelf({ channel, label }: EncodingShelfProps) { e.preventDefault(); setIsOver(false); - const fieldData = e.dataTransfer.getData('application/json'); - if (fieldData) { - const field = JSON.parse(fieldData) as DetectedField; - assignField(channel, field); + const data = e.dataTransfer.getData('application/json'); + if (!data) return; + + const payload = JSON.parse(data) as DragPayload; + + // Dropping on same channel - no-op + if (payload.sourceType === 'encodingShelf' && payload.sourceChannel === channel) { + return; + } + + // Move from another encoding shelf + if (payload.sourceType === 'encodingShelf' && payload.sourceChannel) { + const targetField = assignedField; + + if (targetField) { + // Swap: move target field to source channel + assignField(payload.sourceChannel, targetField); + } else { + // No field in target - just remove from source + removeField(payload.sourceChannel); + } } + + assignField(channel, payload.field); + }; + + const handlePillDragStart = (e: React.DragEvent) => { + if (!assignedField) return; + const payload: DragPayload = { + field: assignedField, + sourceType: 'encodingShelf', + sourceChannel: channel + }; + e.dataTransfer.setData('application/json', JSON.stringify(payload)); + e.dataTransfer.effectAllowed = 'move'; + setIsDragging(true); + }; + + const handlePillDragEnd = () => { + setIsDragging(false); }; const handleRemove = () => { @@ -115,6 +150,9 @@ export function EncodingShelf({ channel, label }: EncodingShelfProps) { > {assignedField ? (
diff --git a/src/components/FieldList/FieldPill.tsx b/src/components/FieldList/FieldPill.tsx index 0180c1c..68f65c1 100644 --- a/src/components/FieldList/FieldPill.tsx +++ b/src/components/FieldList/FieldPill.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import type { DetectedField, FieldType } from '../../types'; +import type { DetectedField, FieldType, DragPayload } from '../../types'; import { useApp } from '../../context/AppContext'; const TYPE_COLORS: Record = { @@ -27,7 +27,8 @@ export function FieldPill({ field, index }: FieldPillProps) { const [isDragging, setIsDragging] = useState(false); const handleDragStart = (e: React.DragEvent) => { - e.dataTransfer.setData('application/json', JSON.stringify(field)); + const payload: DragPayload = { field, sourceType: 'fieldList' }; + e.dataTransfer.setData('application/json', JSON.stringify(payload)); e.dataTransfer.effectAllowed = 'copy'; setIsDragging(true); }; diff --git a/src/types/index.ts b/src/types/index.ts index 1e61868..ad9345e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -8,6 +8,12 @@ export interface DetectedField { export type EncodingChannel = 'x' | 'y' | 'color' | 'size' | 'shape' | 'row' | 'column'; +export interface DragPayload { + field: DetectedField; + sourceType: 'fieldList' | 'encodingShelf'; + sourceChannel?: EncodingChannel; +} + export type EncodingState = { [K in EncodingChannel]?: DetectedField; };