Skip to content

Instantly share code, notes, and snippets.

@otakustay
Created February 22, 2020 05:38
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save otakustay/9b59153da2e124f0637732fef5c71c6a to your computer and use it in GitHub Desktop.
Save otakustay/9b59153da2e124f0637732fef5c71c6a to your computer and use it in GitHub Desktop.
React hook to control table selection
import {useReducer, useCallback} from 'react';
import {union, without, difference, range} from 'lodash';
interface Action {
type: 'single' | 'multiple' | 'range';
payload: number;
}
export interface SelectionMethods {
selectIndex(index: number, e: ClickContext): void;
}
type SelectionHook = [number[], SelectionMethods];
interface ClickContext {
ctrlKey: boolean;
metaKey: boolean;
shiftKey: boolean;
}
interface SelectionContext {
rangeStart: number;
rangeEnd?: number;
selected: number[];
}
const getActionType = (e: ClickContext): Action['type'] => {
if (e.shiftKey) {
return 'range';
}
if (e.metaKey || e.ctrlKey) {
return 'multiple';
}
return 'single';
};
const toggleSingleSelected = (selected: number[], target: number): number[] => {
if (selected.includes(target)) {
return without(selected, target);
}
return selected.concat(target);
};
export default (): SelectionHook => {
const [{selected}, dispatch] = useReducer(
(state: SelectionContext, action: Action) => {
const {rangeStart, rangeEnd, selected} = state;
const {type, payload} = action;
if (type === 'range') {
if (rangeEnd === undefined) {
const extra = payload < rangeStart
? range(payload, rangeStart + 1)
: range(rangeStart, payload + 1);
return {
rangeStart,
rangeEnd: payload,
selected: union(selected, extra),
};
}
else if (payload === rangeEnd) {
return state;
}
else {
const currentRange = range(
Math.min(rangeStart, rangeEnd),
Math.max(rangeStart, rangeEnd) + 1
);
const nextRange = range(
Math.min(rangeStart, payload),
Math.max(rangeStart, payload) + 1
);
const nextSelected = union(difference(selected, currentRange), nextRange);
return {
rangeStart,
rangeEnd: payload,
selected: nextSelected,
};
}
}
const nextSelected = type === 'single' ? [payload] : toggleSingleSelected(selected, payload);
return {
rangeStart: payload,
rangeEnd: undefined,
selected: nextSelected,
};
},
{rangeStart: 0, rangeEnd: undefined, selected: []}
);
const selectIndex: SelectionMethods['selectIndex'] = useCallback(
(index, e) => dispatch({type: getActionType(e), payload: index}),
[]
);
return [selected, {selectIndex}];
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment