File size: 3,840 Bytes
963e554
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
/**
 * Drag-handle list reordering (direct-manipulation primitive).
 *
 * Pointer-based (no DnD library): grab the handle, rows reorder live as you
 * drag, commit on release. Used for ordering query group-by chips, dashboard
 * widget stacking, and anywhere an ordered list needs rearranging.
 */
import { useRef, useState, type ReactNode } from 'react';
import Box from '@mui/material/Box';
import { GripVertical } from 'lucide-react';
import { tokens } from '../../theme';

export interface DragListItem {
  id: string;
  content: ReactNode;
}

interface DragListProps {
  items: DragListItem[];
  onReorder: (orderedIds: string[]) => void;
  dense?: boolean;
}

export default function DragList({ items, onReorder, dense = false }: DragListProps) {
  const [order, setOrder] = useState<string[] | null>(null); // non-null while dragging
  const [dragId, setDragId] = useState<string | null>(null);
  const containerRef = useRef<HTMLDivElement | null>(null);

  const ids = order ?? items.map((i) => i.id);
  const byId = new Map(items.map((i) => [i.id, i]));

  const indexFromPointer = (clientY: number): number => {
    const container = containerRef.current;
    if (!container) return 0;
    const rows = Array.from(container.querySelectorAll<HTMLElement>('[data-dragrow]'));
    for (let i = 0; i < rows.length; i += 1) {
      const r = rows[i].getBoundingClientRect();
      if (clientY < r.top + r.height / 2) return i;
    }
    return rows.length - 1;
  };

  return (
    <Box ref={containerRef} sx={{ display: 'flex', flexDirection: 'column', gap: dense ? 0.5 : 0.75 }}>
      {ids.map((id) => {
        const item = byId.get(id);
        if (!item) return null;
        const dragging = dragId === id;
        return (
          <Box
            key={id}
            data-dragrow
            sx={{
              display: 'flex',
              alignItems: 'center',
              gap: 1,
              px: 1,
              py: dense ? 0.5 : 0.75,
              borderRadius: 1,
              border: `1px solid ${dragging ? tokens.color.info : tokens.color.border}`,
              bgcolor: dragging ? tokens.color.surfaceRaised : tokens.color.surface,
              boxShadow: dragging ? tokens.elevation.raised : 'none',
              transition: `border-color ${tokens.motion.fast}, box-shadow ${tokens.motion.fast}`,
              userSelect: 'none',
            }}
          >
            <Box
              component="span"
              sx={{
                display: 'inline-flex',
                color: tokens.color.textFaint,
                cursor: 'grab',
                touchAction: 'none',
                '&:hover': { color: tokens.color.info },
              }}
              onPointerDown={(e) => {
                e.preventDefault();
                (e.target as Element).setPointerCapture(e.pointerId);
                setDragId(id);
                setOrder(items.map((i) => i.id));
              }}
              onPointerMove={(e) => {
                if (dragId !== id || order === null) return;
                const target = indexFromPointer(e.clientY);
                const current = order.indexOf(id);
                if (target !== current && target >= 0) {
                  const next = [...order];
                  next.splice(current, 1);
                  next.splice(target, 0, id);
                  setOrder(next);
                }
              }}
              onPointerUp={(e) => {
                (e.target as Element).releasePointerCapture(e.pointerId);
                if (order) onReorder(order);
                setOrder(null);
                setDragId(null);
              }}
            >
              <GripVertical size={15} />
            </Box>
            <Box sx={{ flex: 1, minWidth: 0 }}>{item.content}</Box>
          </Box>
        );
      })}
    </Box>
  );
}