mirror of
https://github.com/christianvidalwolf-prog/Craze-Data-check.git
synced 2026-08-03 14:35:23 +02:00
feat: implement excel-style drag-to-select and shift-click in Dimensions and filters
This commit is contained in:
@@ -27,6 +27,7 @@ export function ColumnFilterPopover({
|
|||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [dragStart, setDragStart] = useState<number | null>(null);
|
const [dragStart, setDragStart] = useState<number | null>(null);
|
||||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||||
|
const [lastClickedIndex, setLastClickedIndex] = useState<number | null>(null);
|
||||||
const listRef = React.useRef<HTMLDivElement>(null);
|
const listRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const filteredValues = useMemo(() => {
|
const filteredValues = useMemo(() => {
|
||||||
@@ -134,10 +135,23 @@ export function ColumnFilterPopover({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onMouseDown={(e) => { e.preventDefault(); handleMouseDown(idx); }}
|
onMouseDown={(e) => { e.preventDefault(); handleMouseDown(idx); }}
|
||||||
onMouseEnter={() => handleMouseEnter(idx)}
|
onMouseEnter={() => handleMouseEnter(idx)}
|
||||||
onClick={(e) => { if (!isDragging) onToggle(val); }}
|
onClick={(e) => {
|
||||||
onKeyDown={(e) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); onToggle(val); } }}
|
if (isDragging) return;
|
||||||
|
|
||||||
|
if (e.shiftKey && lastClickedIndex !== null) {
|
||||||
|
const start = Math.min(lastClickedIndex, idx);
|
||||||
|
const end = Math.max(lastClickedIndex, idx);
|
||||||
|
const itemsToSelect = filteredValues.slice(start, end + 1);
|
||||||
|
const newSelected = new Set([...selectedValues, ...itemsToSelect]);
|
||||||
|
onSelectAll(Array.from(newSelected));
|
||||||
|
} else {
|
||||||
|
onToggle(val);
|
||||||
|
}
|
||||||
|
setLastClickedIndex(idx);
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); onToggle(val); setLastClickedIndex(idx); } }}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 p-1.5 rounded cursor-pointer group transition-colors",
|
"flex items-center gap-2 p-1.5 rounded cursor-pointer group transition-colors select-none",
|
||||||
isDragSelected ? "bg-blue-600/40" : "hover:bg-slate-700/50"
|
isDragSelected ? "bg-blue-600/40" : "hover:bg-slate-700/50"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -55,6 +55,53 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat
|
|||||||
const [lineFilter, setLineFilter] = useState<string[]>([]);
|
const [lineFilter, setLineFilter] = useState<string[]>([]);
|
||||||
const [classFilter, setClassFilter] = useState<string[]>([]);
|
const [classFilter, setClassFilter] = useState<string[]>([]);
|
||||||
const [openFilter, setOpenFilter] = useState<'line' | 'class' | null>(null);
|
const [openFilter, setOpenFilter] = useState<'line' | 'class' | null>(null);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [dragStart, setDragStart] = useState<number | null>(null);
|
||||||
|
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||||
|
const [lastClickedIndex, setLastClickedIndex] = useState<number | null>(null);
|
||||||
|
const [activeClusterKey, setActiveClusterKey] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const isDraggingRef = React.useRef(false);
|
||||||
|
const dragStartRef = React.useRef<number | null>(null);
|
||||||
|
const hoveredIndexRef = React.useRef<number | null>(null);
|
||||||
|
const activeClusterKeyRef = React.useRef<string | null>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handleGlobalMouseUp = () => {
|
||||||
|
if (isDraggingRef.current && activeClusterKeyRef.current) {
|
||||||
|
const clusterKey = activeClusterKeyRef.current;
|
||||||
|
const start = dragStartRef.current;
|
||||||
|
const end = hoveredIndexRef.current;
|
||||||
|
|
||||||
|
if (start !== null && end !== null) {
|
||||||
|
const cluster = nearDuplicateClusters.find(c => c.groups[0].key === clusterKey);
|
||||||
|
if (cluster) {
|
||||||
|
const allClusterRows = cluster.groups.flatMap(g => g.rows);
|
||||||
|
const s = Math.min(start, end);
|
||||||
|
const e = Math.max(start, end);
|
||||||
|
const indicesToSelect = allClusterRows.slice(s, e + 1).map(r => r.index);
|
||||||
|
|
||||||
|
setClusterSelections(prev => {
|
||||||
|
const next = new Set(prev[clusterKey] ?? []);
|
||||||
|
indicesToSelect.forEach(idx => next.add(idx));
|
||||||
|
return { ...prev, [clusterKey]: next };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isDraggingRef.current = false;
|
||||||
|
dragStartRef.current = null;
|
||||||
|
hoveredIndexRef.current = null;
|
||||||
|
activeClusterKeyRef.current = null;
|
||||||
|
setIsDragging(false);
|
||||||
|
setDragStart(null);
|
||||||
|
setHoveredIndex(null);
|
||||||
|
setActiveClusterKey(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('mouseup', handleGlobalMouseUp);
|
||||||
|
return () => window.removeEventListener('mouseup', handleGlobalMouseUp);
|
||||||
|
}, [nearDuplicateClusters]);
|
||||||
|
|
||||||
const groups = useMemo(() => {
|
const groups = useMemo(() => {
|
||||||
const groupMap = new Map<string, { row: ExcelRow; index: number }[]>();
|
const groupMap = new Map<string, { row: ExcelRow; index: number }[]>();
|
||||||
@@ -492,24 +539,64 @@ export function DimensionsView({ data, headers, onEdit, onSaveRow, onCaptureStat
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="divide-y divide-violet-500/10">
|
<div className="divide-y divide-violet-500/10">
|
||||||
{allClusterRows.map(({ row, index }) => {
|
{allClusterRows.map(({ row, index }, rowIdx) => {
|
||||||
const isSelected = selection.has(index);
|
const isSelected = selection.has(index);
|
||||||
|
const isInDragRange = isDragging && activeClusterKey === clusterKey && dragStart !== null && hoveredIndex !== null &&
|
||||||
|
((rowIdx >= dragStart && rowIdx <= hoveredIndex) || (rowIdx <= dragStart && rowIdx >= hoveredIndex));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-3 px-4 py-2.5 hover:bg-violet-500/5 cursor-pointer transition-colors",
|
"flex items-center gap-3 px-4 py-2.5 hover:bg-violet-500/5 cursor-pointer transition-colors select-none",
|
||||||
isSelected && "bg-violet-500/10"
|
isSelected && "bg-violet-500/10",
|
||||||
|
isInDragRange && "bg-violet-500/20 ring-1 ring-violet-500/30"
|
||||||
)}
|
)}
|
||||||
onClick={() => setClusterSelections(prev => {
|
onMouseDown={(e) => {
|
||||||
const current = new Set(prev[clusterKey] ?? []);
|
// Only handle left click
|
||||||
if (current.has(index)) current.delete(index); else current.add(index);
|
if (e.button !== 0) return;
|
||||||
return { ...prev, [clusterKey]: current };
|
e.preventDefault();
|
||||||
})}
|
isDraggingRef.current = true;
|
||||||
|
dragStartRef.current = rowIdx;
|
||||||
|
hoveredIndexRef.current = rowIdx;
|
||||||
|
activeClusterKeyRef.current = clusterKey;
|
||||||
|
setIsDragging(true);
|
||||||
|
setDragStart(rowIdx);
|
||||||
|
setHoveredIndex(rowIdx);
|
||||||
|
setActiveClusterKey(clusterKey);
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
if (isDraggingRef.current && activeClusterKeyRef.current === clusterKey) {
|
||||||
|
hoveredIndexRef.current = rowIdx;
|
||||||
|
setHoveredIndex(rowIdx);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (!isDragging) {
|
||||||
|
if (e.shiftKey && lastClickedIndex !== null) {
|
||||||
|
const start = Math.min(lastClickedIndex, rowIdx);
|
||||||
|
const end = Math.max(lastClickedIndex, rowIdx);
|
||||||
|
const indicesToSelect = allClusterRows.slice(start, end + 1).map(r => r.index);
|
||||||
|
|
||||||
|
setClusterSelections(prev => {
|
||||||
|
const next = new Set(prev[clusterKey] ?? []);
|
||||||
|
indicesToSelect.forEach(idx => next.add(idx));
|
||||||
|
return { ...prev, [clusterKey]: next };
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setClusterSelections(prev => {
|
||||||
|
const current = new Set(prev[clusterKey] ?? []);
|
||||||
|
if (current.has(index)) current.delete(index); else current.add(index);
|
||||||
|
return { ...prev, [clusterKey]: current };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setLastClickedIndex(rowIdx);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={isSelected}
|
checked={isSelected || isInDragRange}
|
||||||
readOnly
|
readOnly
|
||||||
className="rounded border-slate-600 bg-slate-700 text-violet-600 focus:ring-violet-500 shrink-0 pointer-events-none"
|
className="rounded border-slate-600 bg-slate-700 text-violet-600 focus:ring-violet-500 shrink-0 pointer-events-none"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user