@linzjs/step-ag-grid
Version:
[](https://github.com/semantic-release/semantic-release) > Reusable [ag-grid](https://www.ag-grid.com/) component for LINZ / Toitū te whenua.
270 lines (237 loc) • 9 kB
text/typescript
import { CellMouseDownEvent, CellMouseOverEvent, IRowNode } from 'ag-grid-community';
import { MutableRefObject, RefObject, useCallback, useEffect, useRef, useState } from 'react';
import { useInterval } from '../../utils/useInterval';
import { GridBaseRow } from '../types';
export interface CellLocation {
rowId: string;
colId: string;
clickLocation: [number, number];
}
export interface GridRanges<TData extends GridBaseRow> {
selectedColIds: string[];
selectedNodes: IRowNode<TData>[];
}
export const useGridRangeSelection = <TData extends GridBaseRow>({
enableRangeSelection,
gridDivRef,
rangeStartRef,
rangeEndRef,
hasSelectedMoreThanOneCellRef,
rangeSortedNodesRef,
}: {
enableRangeSelection: boolean;
gridDivRef: RefObject<HTMLDivElement | null>;
rangeStartRef: MutableRefObject<CellLocation | null>;
rangeEndRef: MutableRefObject<CellLocation | null>;
hasSelectedMoreThanOneCellRef: MutableRefObject<boolean>;
rangeSortedNodesRef: MutableRefObject<IRowNode<TData>[] | null>;
}) => {
const [refreshIntervalEnabled, setRefreshIntervalEnabled] = useState(false);
const ranges = useCallback((): GridRanges<TData> => {
const gridElement = gridDivRef.current!;
const rangeStart = rangeStartRef.current!;
const rangeEnd = rangeEndRef.current!;
const rangeSortedNodes = rangeSortedNodesRef.current!;
if (!gridElement || !rangeStart || !rangeEnd || !rangeSortedNodes) {
return {
selectedColIds: [],
selectedNodes: [],
};
}
const elStyleLeftComparator = (el1: Element, el2: Element) => elStyleLeft(el1) - elStyleLeft(el2);
const elStyleLeft = (el: Element): number => parseFloat((el as HTMLElement).style.left) ?? 0;
const getSortedColIds = (): string[] => {
//
const leftHeaders = [...gridElement.querySelectorAll('.ag-pinned-left-header .ag-header-cell')].sort(
elStyleLeftComparator,
);
const centerHeaders = [...gridElement.querySelectorAll('.ag-header-viewport .ag-header-cell')].sort(
elStyleLeftComparator,
);
const rightHeaders = [...gridElement.querySelectorAll('.ag-pinned-right-header .ag-header-cell')].sort(
elStyleLeftComparator,
);
return [...leftHeaders, ...centerHeaders, ...rightHeaders].map((el, i) => el.getAttribute('col-id') ?? String(i));
};
const sortedColIds = getSortedColIds();
const startColIndex = sortedColIds.indexOf(rangeStart.colId);
const endColIndex = sortedColIds.indexOf(rangeEnd.colId);
const selectedColIds = sortedColIds.slice(
Math.min(startColIndex, endColIndex),
Math.max(startColIndex, endColIndex) + 1,
);
const startRowIndex = rangeSortedNodes.findIndex((node) => node.data!.id === rangeStart.rowId);
const endRowIndex = rangeSortedNodes.findIndex((node) => node.data!.id === rangeEnd.rowId);
const selectedNodes = rangeSortedNodes.slice(
Math.min(startRowIndex, endRowIndex),
Math.max(startRowIndex, endRowIndex) + 1,
);
return { selectedColIds, selectedNodes };
}, [gridDivRef, rangeSortedNodesRef, rangeStartRef, rangeEndRef]);
const redrawSelectedRanges = useCallback(() => {
const gridElement = gridDivRef.current!;
const { selectedColIds, selectedNodes } = ranges();
selectedColIds.forEach((colId, colIndex) => {
selectedNodes.forEach((node, rowIndex) => {
const rowId = node.data!.id;
const cell = gridElement.querySelector(
`.ag-row[row-id=${JSON.stringify(String(rowId))}] .ag-cell[col-id=${JSON.stringify(colId)}`,
);
cell?.classList.add('rangeSelect');
if (colIndex === 0) {
cell?.classList.add('rangeSelectLeft');
}
if (colIndex === selectedColIds.length - 1) {
cell?.classList.add('rangeSelectRight');
}
if (rowIndex === 0) {
cell?.classList.add('rangeSelectTop');
}
if (rowIndex === selectedNodes.length - 1) {
cell?.classList.add('rangeSelectBottom');
}
});
});
}, [ranges, gridDivRef]);
const updateRangeSelectionCellClasses = useCallback(
(justRefresh?: boolean) => {
//
// Get all grid cols, sort by pinned, then style: left
const gridElement = gridDivRef.current;
if (!gridElement) {
return;
}
// Clear all selections
if (!justRefresh) {
gridElement
.querySelectorAll('.rangeSelect,.rangeSelectLeft,.rangeSelectTop,.rangeSelectRight,.rangeSelectBottom')
.forEach((el) => {
el.classList.remove(
'rangeSelect',
'rangeSelectLeft',
'rangeSelectTop',
'rangeSelectRight',
'rangeSelectBottom',
);
});
}
// if range selection multiple add .Grid-container.rangeSelectingMultiple
const rangeStart = rangeStartRef.current;
const rangeEnd = rangeEndRef.current;
if (
rangeStart !== null &&
rangeEnd !== null &&
(hasSelectedMoreThanOneCellRef.current ||
rangeStart.colId !== rangeEnd.colId ||
rangeStart.rowId !== rangeEnd.rowId)
) {
gridElement.classList.add('rangeSelectingMultiple');
gridElement.querySelectorAll('.ag-cell-focus').forEach((el) => {
el.classList.remove('ag-cell-focus');
});
} else {
gridElement.classList.remove('rangeSelectingMultiple');
return;
}
const rangeSortedNodes = rangeSortedNodesRef.current;
if (!rangeSortedNodes) {
return;
}
redrawSelectedRanges();
},
[redrawSelectedRanges, rangeStartRef, rangeEndRef, gridDivRef, hasSelectedMoreThanOneCellRef, rangeSortedNodesRef],
);
// Handle updates after scroll / grid refresh
useInterval(
() => {
updateRangeSelectionCellClasses(true);
},
refreshIntervalEnabled ? 150 : null,
);
const clearRangeSelection = useCallback(() => {
hasSelectedMoreThanOneCellRef.current = false;
setRefreshIntervalEnabled(false);
hasSelectedMoreThanOneCellRef.current = false;
rangeStartRef.current = null;
rangeEndRef.current = null;
updateRangeSelectionCellClasses();
}, [updateRangeSelectionCellClasses, hasSelectedMoreThanOneCellRef, rangeStartRef, rangeEndRef]);
const mouseDownRef = useRef<[number, number]>([0, 0]);
const trackMouseDown = useCallback(
(e: MouseEvent) => {
mouseDownRef.current = [e.screenX, e.screenY];
},
[mouseDownRef],
);
const trackMouseUp = useCallback(
(e: MouseEvent) => {
if (rangeEndRef.current) {
rangeEndRef.current.clickLocation = [e.screenX, e.screenY];
}
},
[rangeEndRef],
);
useEffect(() => {
document.addEventListener('mousedown', trackMouseDown, { capture: true });
document.addEventListener('mouseup', trackMouseUp, { capture: true });
return () => {
document.removeEventListener('mousemove', trackMouseDown, { capture: true });
document.removeEventListener('mouseup', trackMouseUp, { capture: true });
};
}, [trackMouseDown, trackMouseUp]);
const onCellMouseOver = useCallback(
(e: CellMouseOverEvent, mouseDown?: boolean) => {
if (!enableRangeSelection) {
return;
}
const button = (e.event as { buttons?: number }).buttons;
if (button !== 1) {
// TODO fix this?
//rangeSortedNodesRef.current = null;
return;
}
rangeEndRef.current = {
rowId: e.node.data.id,
colId: e.column.getColId(),
clickLocation: [0, 0],
};
if (mouseDown) {
const sortedNodes: IRowNode<TData>[] = [];
e.api.forEachNodeAfterFilterAndSort((node: IRowNode<TData>) => sortedNodes.push(node));
rangeSortedNodesRef.current = sortedNodes;
setRefreshIntervalEnabled(true);
rangeStartRef.current = { ...rangeEndRef.current, clickLocation: mouseDownRef.current };
}
if (
rangeStartRef.current &&
rangeEndRef.current &&
(rangeStartRef.current.rowId !== rangeEndRef.current.rowId ||
rangeStartRef.current.colId !== rangeEndRef.current.colId)
) {
e.api.stopEditing();
hasSelectedMoreThanOneCellRef.current = true;
window.getSelection()?.removeAllRanges();
}
updateRangeSelectionCellClasses();
},
[
enableRangeSelection,
updateRangeSelectionCellClasses,
hasSelectedMoreThanOneCellRef,
rangeEndRef,
rangeSortedNodesRef,
rangeStartRef,
],
);
const onCellMouseDown = useCallback(
(e: CellMouseDownEvent) => {
const button = (e.event as { buttons?: number }).buttons;
if (button === 1) {
clearRangeSelection();
}
onCellMouseOver(e as unknown as CellMouseOverEvent, true);
},
[onCellMouseOver, clearRangeSelection],
);
return { clearRangeSelection, ranges, onCellMouseDown, onCellMouseOver };
};