mirror of
https://github.com/its-a-feature/Mythic
synced 2026-06-08 14:55:38 +00:00
better column dragging for virtualized tables
This commit is contained in:
@@ -1,59 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import {classes} from './styles';
|
||||
import Draggable from 'react-draggable';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import DragHandleIcon from '@mui/icons-material/DragHandle';
|
||||
|
||||
const DraggableHandles = React.forwardRef(({ height, rowHeight, width, minColumnWidth, columnWidths, onStop }, ref) => {
|
||||
const [isDragging, setIsDragging] = useState(-1);
|
||||
const nodeRef = React.useRef(null);
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={classes.draggableHandlesContainer}
|
||||
style={{
|
||||
height: height,
|
||||
width: width,
|
||||
pointerEvents: isDragging >= 0 ? 'initial' : 'none',
|
||||
}}>
|
||||
{columnWidths.map((_, i) => {
|
||||
// leftOffset is the sum of the width of all columns left of i
|
||||
const leftOffset = columnWidths.slice(0, i).reduce((a, b) => a + b, 0);
|
||||
return (
|
||||
<Draggable
|
||||
key={i}
|
||||
nodeRef={nodeRef}
|
||||
axis='x'
|
||||
bounds={{
|
||||
left: minColumnWidth - columnWidths[i],
|
||||
right: Number.POSITIVE_INFINITY,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
offsetParent={document.body}
|
||||
|
||||
defaultPosition={{x: 0, y: 0}}
|
||||
position={{x: 0, y: 0}}
|
||||
onStart={() => {
|
||||
setIsDragging(i);
|
||||
}}
|
||||
onStop={(e, data) => {
|
||||
setIsDragging(-1);
|
||||
onStop(data.x, i);
|
||||
}}>
|
||||
<DragHandleIcon
|
||||
ref={nodeRef}
|
||||
className={isDragging === i ? classes.draggableHandlesClickAreaSelected : classes.draggableHandlesClickArea}
|
||||
style={{
|
||||
left: leftOffset + columnWidths[i] - 1 - 7,
|
||||
rotate: isDragging === i ? "0deg" : "90deg",
|
||||
}}>
|
||||
</DragHandleIcon>
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default DraggableHandles;
|
||||
@@ -14,6 +14,8 @@ const HeaderCell = ({
|
||||
sortIndicatorIndex,
|
||||
sortDirection,
|
||||
headerNameKey = "name",
|
||||
isResizing = false,
|
||||
onResizePointerDown = () => {},
|
||||
VariableSizeGridProps: { style, rowIndex, columnIndex, data },
|
||||
}) => {
|
||||
const dropdownAnchorRef = React.useRef(null);
|
||||
@@ -33,6 +35,29 @@ const HeaderCell = ({
|
||||
},
|
||||
[onDoubleClick, columnIndex]
|
||||
);
|
||||
const handleResizePointerDown = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onResizePointerDown(event, columnIndex);
|
||||
},
|
||||
[onResizePointerDown, columnIndex]
|
||||
);
|
||||
const handleResizeDoubleClick = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onDoubleClick(event, columnIndex);
|
||||
},
|
||||
[onDoubleClick, columnIndex]
|
||||
);
|
||||
const stopResizeClickPropagation = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
},
|
||||
[]
|
||||
);
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
const handleContextClick = useCallback(
|
||||
(event) => {
|
||||
@@ -71,6 +96,17 @@ const HeaderCell = ({
|
||||
setOpenContextMenu={setOpenContextMenu} handleMenuItemClick={handleMenuItemClick}
|
||||
/>
|
||||
</Box>
|
||||
{!item.disableResize &&
|
||||
<div
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
aria-label={`Resize ${item[headerNameKey]} column`}
|
||||
className={`${classes.headerResizeHandle} ${isResizing ? classes.headerResizeHandleActive : ""}`}
|
||||
onPointerDown={handleResizePointerDown}
|
||||
onDoubleClick={handleResizeDoubleClick}
|
||||
onClick={stopResizeClickPropagation}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+121
-37
@@ -5,7 +5,6 @@ import useScrollbarSize from 'react-scrollbar-size';
|
||||
import { VariableSizeGrid } from 'react-window';
|
||||
import HeaderCell from './HeaderCell';
|
||||
import Cell from './Cell';
|
||||
import DraggableHandles from './DraggableHandles';
|
||||
import {classes} from './styles';
|
||||
import {GetMythicSetting, useSetMythicSetting} from "../MythicSavedUserSetting";
|
||||
|
||||
@@ -44,6 +43,8 @@ const innerElementType = React.forwardRef(({ children, style }, ref) => {
|
||||
contextMenuOptions={HeaderCellData.contextMenuOptions}
|
||||
sortIndicatorIndex={HeaderCellData.sortIndicatorIndex}
|
||||
sortDirection={HeaderCellData.sortDirection}
|
||||
isResizing={HeaderCellData.resizingColumnIndex === i}
|
||||
onResizePointerDown={HeaderCellData.startColumnResize}
|
||||
VariableSizeGridProps={{
|
||||
style: {
|
||||
position: 'absolute',
|
||||
@@ -103,8 +104,18 @@ const ResizableGridWrapper = ({
|
||||
}));
|
||||
const gridUUID = React.useMemo( () => GetShortRandomString(), []);
|
||||
const gridRef = useRef(null);
|
||||
const dragHandlesRef = useRef(null);
|
||||
const columnWidthsRef = useRef(columnWidths);
|
||||
const lastResetColumnIndexRef = useRef(0);
|
||||
const activeResizeRef = useRef(null);
|
||||
const resizeFrameRef = useRef(null);
|
||||
const cleanupResizeListenersRef = useRef(null);
|
||||
const [resizingColumnIndex, setResizingColumnIndex] = useState(-1);
|
||||
const [updateSetting] = useSetMythicSetting();
|
||||
const setColumnWidthsAndRef = useCallback( (newColumnWidths, resetColumnIndex=0) => {
|
||||
lastResetColumnIndexRef.current = resetColumnIndex;
|
||||
columnWidthsRef.current = newColumnWidths;
|
||||
setColumnWidths(newColumnWidths);
|
||||
}, []);
|
||||
const getColumnWidth = useCallback(
|
||||
(index) => {
|
||||
return columnWidths[index] || MIN_COLUMN_WIDTH;
|
||||
@@ -146,35 +157,118 @@ const ResizableGridWrapper = ({
|
||||
}
|
||||
//updatedColumnWidths[updatedWidthIndex] += totalWidthDiff;
|
||||
}
|
||||
setColumnWidths(updatedColumnWidths);
|
||||
setColumnWidthsAndRef(updatedColumnWidths);
|
||||
if(name !== undefined){
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
|
||||
}
|
||||
|
||||
}, [scrollbarWidth, columns, AutoSizerProps.width, localColumnsRef.current]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [scrollbarWidth, columns, AutoSizerProps.width, localColumnsRef.current, setColumnWidthsAndRef]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useEffect(() => {
|
||||
gridRef.current.resetAfterColumnIndex(0, true);
|
||||
if(name !== undefined){
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: columnWidths});
|
||||
if(gridRef.current){
|
||||
gridRef.current.resetAfterColumnIndex(lastResetColumnIndexRef.current, true);
|
||||
}
|
||||
}, [columnWidths]);
|
||||
|
||||
/* Event Handlers */
|
||||
|
||||
const resizeColumn = useCallback( (x, columnIndex) => {
|
||||
const updatedColumnWidths = columnWidths.map((columnWidth, index) => {
|
||||
if (columnIndex === index) {
|
||||
return Math.floor(Math.max(columnWidth + x, MIN_COLUMN_WIDTH));
|
||||
}
|
||||
return Math.floor(columnWidth);
|
||||
});
|
||||
setColumnWidths(updatedColumnWidths);
|
||||
if(name !== undefined){
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
|
||||
const startColumnResize = useCallback( (event, columnIndex) => {
|
||||
if(columns[columnIndex]?.disableResize){
|
||||
return;
|
||||
}
|
||||
|
||||
}, [columnWidths]);
|
||||
const initialWidths = columnWidthsRef.current.map( (columnWidth) => Math.floor(columnWidth || MIN_COLUMN_WIDTH));
|
||||
const startClientX = event.clientX;
|
||||
const startWidth = initialWidths[columnIndex] || MIN_COLUMN_WIDTH;
|
||||
const previousCursor = document.body.style.cursor;
|
||||
const previousUserSelect = document.body.style.userSelect;
|
||||
const applyResizeWidth = (clientX) => {
|
||||
const activeResize = activeResizeRef.current;
|
||||
if(!activeResize){
|
||||
return;
|
||||
}
|
||||
activeResize.latestClientX = clientX;
|
||||
if(resizeFrameRef.current !== null){
|
||||
return;
|
||||
}
|
||||
resizeFrameRef.current = window.requestAnimationFrame(() => {
|
||||
resizeFrameRef.current = null;
|
||||
const currentResize = activeResizeRef.current;
|
||||
if(!currentResize){
|
||||
return;
|
||||
}
|
||||
const resizeDelta = currentResize.latestClientX - currentResize.startClientX;
|
||||
const nextColumnWidth = Math.floor(Math.max(currentResize.startWidth + resizeDelta, MIN_COLUMN_WIDTH));
|
||||
const updatedColumnWidths = currentResize.initialWidths.map( (columnWidth, index) => {
|
||||
return currentResize.columnIndex === index ? nextColumnWidth : columnWidth;
|
||||
});
|
||||
setColumnWidthsAndRef(updatedColumnWidths, currentResize.columnIndex);
|
||||
});
|
||||
};
|
||||
const cleanupResizeListeners = () => {
|
||||
window.removeEventListener("pointermove", handlePointerMove);
|
||||
window.removeEventListener("pointerup", handlePointerUp);
|
||||
window.removeEventListener("pointercancel", handlePointerUp);
|
||||
document.body.style.cursor = previousCursor;
|
||||
document.body.style.userSelect = previousUserSelect;
|
||||
cleanupResizeListenersRef.current = null;
|
||||
};
|
||||
const finishResize = (clientX) => {
|
||||
const currentResize = activeResizeRef.current;
|
||||
if(!currentResize){
|
||||
cleanupResizeListeners();
|
||||
return;
|
||||
}
|
||||
if(resizeFrameRef.current !== null){
|
||||
window.cancelAnimationFrame(resizeFrameRef.current);
|
||||
resizeFrameRef.current = null;
|
||||
}
|
||||
const resizeDelta = clientX - currentResize.startClientX;
|
||||
const nextColumnWidth = Math.floor(Math.max(currentResize.startWidth + resizeDelta, MIN_COLUMN_WIDTH));
|
||||
const updatedColumnWidths = currentResize.initialWidths.map( (columnWidth, index) => {
|
||||
return currentResize.columnIndex === index ? nextColumnWidth : columnWidth;
|
||||
});
|
||||
activeResizeRef.current = null;
|
||||
setColumnWidthsAndRef(updatedColumnWidths, currentResize.columnIndex);
|
||||
if(name !== undefined){
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
|
||||
}
|
||||
setResizingColumnIndex(-1);
|
||||
cleanupResizeListeners();
|
||||
};
|
||||
function handlePointerMove(pointerEvent){
|
||||
pointerEvent.preventDefault();
|
||||
applyResizeWidth(pointerEvent.clientX);
|
||||
}
|
||||
function handlePointerUp(pointerEvent){
|
||||
pointerEvent.preventDefault();
|
||||
finishResize(pointerEvent.clientX);
|
||||
}
|
||||
event.currentTarget.setPointerCapture?.(event.pointerId);
|
||||
activeResizeRef.current = {
|
||||
columnIndex,
|
||||
initialWidths,
|
||||
latestClientX: startClientX,
|
||||
startClientX,
|
||||
startWidth,
|
||||
};
|
||||
document.body.style.cursor = "col-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
setResizingColumnIndex(columnIndex);
|
||||
window.addEventListener("pointermove", handlePointerMove);
|
||||
window.addEventListener("pointerup", handlePointerUp);
|
||||
window.addEventListener("pointercancel", handlePointerUp);
|
||||
cleanupResizeListenersRef.current = cleanupResizeListeners;
|
||||
}, [columns, name, setColumnWidthsAndRef, updateSetting]);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cleanupResizeListenersRef.current?.();
|
||||
activeResizeRef.current = null;
|
||||
if(resizeFrameRef.current !== null){
|
||||
window.cancelAnimationFrame(resizeFrameRef.current);
|
||||
resizeFrameRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
const autosizeColumn = useCallback( ({columnIndex}) => {
|
||||
if(columns[columnIndex].disableDoubleClick){
|
||||
return
|
||||
@@ -264,9 +358,12 @@ const ResizableGridWrapper = ({
|
||||
}
|
||||
}
|
||||
if(updatedValues){
|
||||
setColumnWidths(() => updatedColumnWidths);
|
||||
setColumnWidthsAndRef(updatedColumnWidths, columnIndex);
|
||||
if(name !== undefined){
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
|
||||
}
|
||||
}
|
||||
}, [columnWidths]);
|
||||
}, [columnWidths, columns, items, name, setColumnWidthsAndRef, updateSetting]);
|
||||
|
||||
useEffect( () => {
|
||||
if(callbackTableGridRef){
|
||||
@@ -285,14 +382,10 @@ const ResizableGridWrapper = ({
|
||||
"sortIndicatorIndex": sortIndicatorIndex,
|
||||
"sortDirection": sortDirection,
|
||||
"getColumnWidth": getColumnWidth,
|
||||
"resizingColumnIndex": resizingColumnIndex,
|
||||
"startColumnResize": startColumnResize,
|
||||
"itemsWithHeader": itemsWithHeader,
|
||||
};
|
||||
const localOnScroll = React.useCallback( ({scrollLeft}) => {
|
||||
|
||||
if (dragHandlesRef.current) {
|
||||
dragHandlesRef.current.scrollTo({ left: scrollLeft });
|
||||
}
|
||||
}, [dragHandlesRef.current])
|
||||
return (
|
||||
<>
|
||||
<HeaderCellContext.Provider value={headerCellData}>
|
||||
@@ -311,19 +404,9 @@ const ResizableGridWrapper = ({
|
||||
innerElementType={innerElementType}
|
||||
overscanRowCount={20}
|
||||
useIsScrolling={false}
|
||||
onScroll={localOnScroll}
|
||||
ref={gridRef}>
|
||||
{CellRenderer}
|
||||
</VariableSizeGrid>
|
||||
<DraggableHandles
|
||||
height={AutoSizerProps.height}
|
||||
rowHeight={headerRowHeight}
|
||||
width={AutoSizerProps.width}
|
||||
minColumnWidth={MIN_COLUMN_WIDTH}
|
||||
columnWidths={columnWidths}
|
||||
onStop={resizeColumn}
|
||||
ref={dragHandlesRef}
|
||||
/>
|
||||
</HeaderCellContext.Provider>
|
||||
|
||||
</>
|
||||
@@ -381,6 +464,7 @@ MythicResizableGrid.propTypes = {
|
||||
name: PropTypes.string,
|
||||
width: PropTypes.number,
|
||||
disableAutosize: PropTypes.bool,
|
||||
disableResize: PropTypes.bool,
|
||||
disableSort: PropTypes.bool
|
||||
})
|
||||
).isRequired,
|
||||
|
||||
@@ -2,11 +2,9 @@ const PREFIX = 'MythicResizableGrid';
|
||||
export const classes = {
|
||||
headerCellRow: `${PREFIX}-headerCellRow`,
|
||||
headerCell: `${PREFIX}-headerCell`,
|
||||
headerResizeHandle: `${PREFIX}-headerResizeHandle`,
|
||||
headerResizeHandleActive: `${PREFIX}-headerResizeHandleActive`,
|
||||
hoveredRow: `${PREFIX}-hoveredRow`,
|
||||
cell: `${PREFIX}-cell`,
|
||||
cellInner: `${PREFIX}-cellInner`,
|
||||
draggableHandlesContainer: `${PREFIX}-draggableHandlesContainer`,
|
||||
draggableHandlesClickArea: `${PREFIX}-draggableHandlesClickArea`,
|
||||
draggableHandlesClickAreaSelected: `${PREFIX}-draggableHandlesClickAreaSelected`,
|
||||
draggableHandlesIndicator: `${PREFIX}-draggableHandlesIndicator`,
|
||||
}
|
||||
|
||||
@@ -200,6 +200,7 @@ tspan {
|
||||
.MythicResizableGrid-headerCell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 0 0.5em;
|
||||
box-sizing: border-box;
|
||||
justify-content: space-between;
|
||||
@@ -215,6 +216,41 @@ tspan {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.MythicResizableGrid-headerResizeHandle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 5px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
z-index: 2;
|
||||
}
|
||||
.MythicResizableGrid-headerResizeHandle::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
width: 0px;
|
||||
height: 100%;
|
||||
border-left: 1px solid ${(props) => props.theme.palette.text.main};
|
||||
border-right: 1px solid ${(props) => props.theme.palette.text.main};
|
||||
opacity: 0.85;
|
||||
}
|
||||
.MythicResizableGrid-headerResizeHandle:hover,
|
||||
.MythicResizableGrid-headerResizeHandleActive {
|
||||
background-color: ${(props) => props.theme.palette.info.main + "22"};
|
||||
}
|
||||
.MythicResizableGrid-headerResizeHandle:hover::after,
|
||||
.MythicResizableGrid-headerResizeHandleActive::after {
|
||||
background-color: ${(props) => props.theme.palette.info.main};
|
||||
opacity: 1;
|
||||
}
|
||||
.MythicResizableGrid-headerResizeHandle:hover::before,
|
||||
.MythicResizableGrid-headerResizeHandleActive::before {
|
||||
border-color: ${(props) => props.theme.palette.info.main};
|
||||
opacity: 1;
|
||||
}
|
||||
.MythicResizableGrid-hoveredRow {
|
||||
background-color: ${(props) => props.theme.tableHover + "CC"};
|
||||
}
|
||||
@@ -233,32 +269,6 @@ tspan {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.MythicResizableGrid-draggableHandlesContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.MythicResizableGrid-draggableHandlesClickArea {
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
cursor: col-resize;
|
||||
pointer-events: initial;
|
||||
}
|
||||
.MythicResizableGrid-draggableHandlesClickAreaSelected {
|
||||
position: absolute;
|
||||
cursor: col-resize;
|
||||
width: 10px;
|
||||
pointer-events: initial;
|
||||
background-color: ${(props) => props.theme.palette.info.main};
|
||||
opacity: 0.5;
|
||||
height: 100%;
|
||||
}
|
||||
.MythicResizableGrid-draggableHandlesIndicator {
|
||||
position: relative;
|
||||
width: 10px;
|
||||
color: red;
|
||||
height: 100px;
|
||||
}
|
||||
.Toastify__toast {
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap !important;
|
||||
|
||||
Reference in New Issue
Block a user