grungnet/src/contexts/GroupContext.tsx

294 lines
8.1 KiB
TypeScript
Raw Normal View History

import React, { useState, useContext, useEffect } from "react";
import cloneDeep from "lodash.clonedeep";
2021-06-05 02:38:01 -04:00
import Fuse from "fuse.js";
import { useKeyboard, useBlur } from "./KeyboardContext";
2021-05-24 01:17:23 -04:00
import { getGroupItems, groupsFromIds } from "../helpers/group";
import shortcuts from "../shortcuts";
2021-07-09 02:22:35 -04:00
import { Group, GroupContainer, GroupItem } from "../types/Group";
2021-07-16 00:55:33 -04:00
export type GroupSelectMode = "single" | "multiple" | "range";
export type GroupSelectModeChangeEventHandler = (
selectMode: GroupSelectMode
) => void;
export type GroupOpenEventHandler = (groupId: string) => void;
export type GroupCloseEventHandler = () => void;
export type GroupsChangeEventHandler = (newGroups: Group[]) => void;
export type SubgroupsChangeEventHandler = (
items: GroupItem[],
groupId: string
) => void;
export type GroupSelectEventHandler = (groupId: string) => void;
export type GroupsSelectEventHandler = (groupIds: string[]) => void;
export type GroupClearSelectionEventHandler = () => void;
export type GroupFilterChangeEventHandler = (filter: string) => void;
export type GroupClearFilterEventHandler = () => void;
2021-07-09 02:22:35 -04:00
type GroupContext = {
groups: Group[];
2021-07-16 00:55:33 -04:00
activeGroups: Group[] | GroupItem[];
2021-07-09 02:22:35 -04:00
openGroupId: string | undefined;
2021-07-16 00:55:33 -04:00
openGroupItems: GroupItem[];
2021-07-09 02:22:35 -04:00
filter: string | undefined;
filteredGroupItems: GroupItem[];
selectedGroupIds: string[];
2021-07-16 00:55:33 -04:00
selectMode: GroupSelectMode;
onSelectModeChange: GroupSelectModeChangeEventHandler;
onGroupOpen: GroupOpenEventHandler;
onGroupClose: GroupCloseEventHandler;
onGroupsChange: GroupsChangeEventHandler;
onSubgroupChange: SubgroupsChangeEventHandler;
onGroupSelect: GroupSelectEventHandler;
onClearSelection: GroupClearSelectionEventHandler;
onFilterChange: GroupFilterChangeEventHandler;
onFilterClear: GroupClearFilterEventHandler;
2021-07-09 02:22:35 -04:00
};
const GroupContext = React.createContext<GroupContext | undefined>(undefined);
type GroupProviderProps = {
groups: Group[];
itemNames: Record<string, string>;
2021-07-16 00:55:33 -04:00
onGroupsChange: GroupsChangeEventHandler;
onGroupsSelect: GroupsSelectEventHandler;
2021-07-09 02:22:35 -04:00
disabled: boolean;
children: React.ReactNode;
};
export function GroupProvider({
groups,
2021-06-05 02:38:01 -04:00
itemNames,
onGroupsChange,
onGroupsSelect,
disabled,
children,
2021-07-09 02:22:35 -04:00
}: GroupProviderProps) {
const [selectedGroupIds, setSelectedGroupIds] = useState<string[]>([]);
2021-07-16 00:55:33 -04:00
const [selectMode, setSelectMode] = useState<GroupSelectMode>("single");
2021-06-05 03:16:39 -04:00
/**
* Group Open
*/
2021-07-09 02:22:35 -04:00
const [openGroupId, setOpenGroupId] = useState<string>();
2021-07-16 00:55:33 -04:00
const [openGroupItems, setOpenGroupItems] = useState<GroupItem[]>([]);
useEffect(() => {
if (openGroupId) {
const openGroups = groupsFromIds([openGroupId], groups);
if (openGroups.length === 1) {
const openGroup = openGroups[0];
setOpenGroupItems(getGroupItems(openGroup));
} else {
// Close group if we can't find it
// This can happen if it was deleted or all it's items were deleted
setOpenGroupItems([]);
2021-07-09 02:22:35 -04:00
setOpenGroupId(undefined);
}
} else {
setOpenGroupItems([]);
}
}, [openGroupId, groups]);
2021-07-09 02:22:35 -04:00
function handleGroupOpen(groupId: string) {
setSelectedGroupIds([]);
setOpenGroupId(groupId);
}
function handleGroupClose() {
setSelectedGroupIds([]);
2021-07-09 02:22:35 -04:00
setOpenGroupId(undefined);
}
2021-06-05 03:16:39 -04:00
/**
* Search
*/
2021-07-09 02:22:35 -04:00
const [filter, setFilter] = useState<string>();
const [filteredGroupItems, setFilteredGroupItems] = useState<GroupItem[]>([]);
const [fuse, setFuse] = useState<Fuse<GroupItem & { name: string }>>();
2021-06-05 03:16:39 -04:00
// Update search index when items change
useEffect(() => {
let items = [];
for (let group of groups) {
const itemsToAdd = getGroupItems(group);
const namedItems = itemsToAdd.map((item) => ({
...item,
name: itemNames[item.id],
}));
items.push(...namedItems);
}
setFuse(new Fuse(items, { keys: ["name"] }));
}, [groups, itemNames]);
// Perform search when search changes
useEffect(() => {
2021-07-09 02:22:35 -04:00
if (filter && fuse) {
2021-06-05 03:16:39 -04:00
const query = fuse.search(filter);
setFilteredGroupItems(query.map((result) => result.item));
2021-07-09 02:22:35 -04:00
setOpenGroupId(undefined);
2021-06-05 03:16:39 -04:00
} else {
setFilteredGroupItems([]);
}
}, [filter, fuse]);
/**
* Handlers
*/
const activeGroups = openGroupId
? openGroupItems
: filter
? filteredGroupItems
: groups;
2021-07-16 00:55:33 -04:00
function handleGroupsChange(newGroups: Group[]) {
onGroupsChange(newGroups);
}
function handleSubgroupChange(items: GroupItem[], groupId: string) {
const groupIndex = groups.findIndex((group) => group.id === groupId);
let updatedGroups = cloneDeep(groups);
const group = updatedGroups[groupIndex];
if (group.type === "group") {
2021-07-09 02:22:35 -04:00
updatedGroups[groupIndex] = {
...group,
2021-07-16 00:55:33 -04:00
items,
};
onGroupsChange(updatedGroups);
} else {
2021-07-16 00:55:33 -04:00
throw new Error(`Group ${group} not a subgroup`);
}
}
2021-07-16 00:55:33 -04:00
function handleGroupSelect(groupId: string) {
2021-07-09 02:22:35 -04:00
let groupIds: string[] = [];
2021-07-16 00:55:33 -04:00
switch (selectMode) {
case "single":
groupIds = [groupId];
break;
case "multiple":
if (selectedGroupIds.includes(groupId)) {
groupIds = selectedGroupIds.filter((id) => id !== groupId);
} else {
groupIds = [...selectedGroupIds, groupId];
}
break;
case "range":
if (selectedGroupIds.length > 0) {
const currentIndex = activeGroups.findIndex((g) => g.id === groupId);
const lastIndex = activeGroups.findIndex(
(g) => g.id === selectedGroupIds[selectedGroupIds.length - 1]
);
let idsToAdd: string[] = [];
let idsToRemove: string[] = [];
const direction = currentIndex > lastIndex ? 1 : -1;
for (
let i = lastIndex + direction;
direction < 0 ? i >= currentIndex : i <= currentIndex;
i += direction
) {
const id = activeGroups[i].id;
if (selectedGroupIds.includes(id)) {
idsToRemove.push(id);
} else {
idsToAdd.push(id);
2021-06-05 03:16:39 -04:00
}
}
2021-07-16 00:55:33 -04:00
groupIds = [...selectedGroupIds, ...idsToAdd].filter(
(id) => !idsToRemove.includes(id)
);
} else {
groupIds = [groupId];
}
break;
default:
groupIds = [];
}
setSelectedGroupIds(groupIds);
onGroupsSelect(groupIds);
}
2021-07-16 00:55:33 -04:00
function handleClearSelection() {
setSelectedGroupIds([]);
onGroupsSelect([]);
}
/**
* Shortcuts
*/
function handleKeyDown(event: KeyboardEvent) {
if (disabled) {
return;
}
if (shortcuts.selectRange(event)) {
setSelectMode("range");
}
if (shortcuts.selectMultiple(event)) {
setSelectMode("multiple");
}
}
function handleKeyUp(event: KeyboardEvent) {
if (disabled) {
return;
}
if (shortcuts.selectRange(event) && selectMode === "range") {
setSelectMode("single");
}
if (shortcuts.selectMultiple(event) && selectMode === "multiple") {
setSelectMode("single");
}
}
useKeyboard(handleKeyDown, handleKeyUp);
// Set select mode to single when cmd+tabing
function handleBlur() {
setSelectMode("single");
}
useBlur(handleBlur);
2021-07-16 00:55:33 -04:00
const value: GroupContext = {
groups,
2021-06-05 02:38:01 -04:00
activeGroups,
openGroupId,
openGroupItems,
2021-06-05 02:38:01 -04:00
filter,
filteredGroupItems,
selectedGroupIds,
selectMode,
2021-06-05 02:38:01 -04:00
onSelectModeChange: setSelectMode,
onGroupOpen: handleGroupOpen,
onGroupClose: handleGroupClose,
onGroupsChange: handleGroupsChange,
2021-07-16 00:55:33 -04:00
onSubgroupChange: handleSubgroupChange,
onGroupSelect: handleGroupSelect,
2021-07-16 00:55:33 -04:00
onClearSelection: handleClearSelection,
2021-06-05 02:38:01 -04:00
onFilterChange: setFilter,
2021-07-16 00:55:33 -04:00
onFilterClear: () => setFilter(undefined),
};
return (
<GroupContext.Provider value={value}>{children}</GroupContext.Provider>
);
}
2021-06-04 23:04:26 -04:00
GroupProvider.defaultProps = {
groups: [],
2021-06-05 02:38:01 -04:00
itemNames: {},
2021-06-04 23:04:26 -04:00
onGroupsChange: () => {},
onGroupsSelect: () => {},
disabled: false,
};
export function useGroup() {
const context = useContext(GroupContext);
if (context === undefined) {
throw new Error("useGroup must be used within a GroupProvider");
}
return context;
}
export default GroupContext;