grungnet/src/contexts/GroupContext.tsx

281 lines
7.5 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";
type GroupContext = {
groups: Group[];
activeGroups: Group[];
openGroupId: string | undefined;
openGroupItems: Group[];
filter: string | undefined;
filteredGroupItems: GroupItem[];
selectedGroupIds: string[];
selectMode: any;
onSelectModeChange: React.Dispatch<
React.SetStateAction<"single" | "multiple" | "range">
>;
onGroupOpen: (groupId: string) => void;
onGroupClose: () => void;
onGroupsChange: (
newGroups: Group[] | GroupItem[],
groupId: string | undefined
) => void;
onGroupSelect: (groupId: string | undefined) => void;
onFilterChange: React.Dispatch<React.SetStateAction<string | undefined>>;
};
const GroupContext = React.createContext<GroupContext | undefined>(undefined);
type GroupProviderProps = {
groups: Group[];
itemNames: Record<string, string>;
onGroupsChange: (groups: Group[]) => void;
onGroupsSelect: (groupIds: string[]) => void;
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[]>([]);
// Either single, multiple or range
2021-07-09 02:22:35 -04:00
const [selectMode, setSelectMode] =
useState<"single" | "multiple" | "range">("single");
2021-06-05 03:16:39 -04:00
/**
* Group Open
*/
2021-07-09 02:22:35 -04:00
const [openGroupId, setOpenGroupId] = useState<string>();
const [openGroupItems, setOpenGroupItems] = useState<Group[]>([]);
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-09 02:22:35 -04:00
* @param {Group[] | GroupItem[]} newGroups
* @param {string|undefined} groupId The group to apply changes to, leave undefined to replace the full group object
*/
2021-07-09 02:22:35 -04:00
function handleGroupsChange(
newGroups: Group[] | GroupItem[],
groupId: string | undefined
) {
if (groupId) {
// If a group is specidifed then update that group with the new items
const groupIndex = groups.findIndex((group) => group.id === groupId);
let updatedGroups = cloneDeep(groups);
const group = updatedGroups[groupIndex];
2021-07-09 02:22:35 -04:00
updatedGroups[groupIndex] = {
...group,
items: newGroups,
} as GroupContainer;
onGroupsChange(updatedGroups);
} else {
onGroupsChange(newGroups);
}
}
2021-07-09 02:22:35 -04:00
function handleGroupSelect(groupId: string | undefined) {
let groupIds: string[] = [];
if (groupId) {
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":
2021-06-05 03:16:39 -04:00
if (selectedGroupIds.length > 0) {
const currentIndex = activeGroups.findIndex(
(g) => g.id === groupId
);
const lastIndex = activeGroups.findIndex(
(g) => g.id === selectedGroupIds[selectedGroupIds.length - 1]
);
2021-07-09 02:22:35 -04:00
let idsToAdd: string[] = [];
let idsToRemove: string[] = [];
2021-06-05 03:16:39 -04:00
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);
}
}
groupIds = [...selectedGroupIds, ...idsToAdd].filter(
(id) => !idsToRemove.includes(id)
);
} else {
groupIds = [groupId];
}
break;
default:
groupIds = [];
}
}
setSelectedGroupIds(groupIds);
onGroupsSelect(groupIds);
}
/**
* Shortcuts
*/
2021-07-09 02:22:35 -04:00
function handleKeyDown(event: React.KeyboardEvent) {
if (disabled) {
return;
}
if (shortcuts.selectRange(event)) {
setSelectMode("range");
}
if (shortcuts.selectMultiple(event)) {
setSelectMode("multiple");
}
}
2021-07-09 02:22:35 -04:00
function handleKeyUp(event: React.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);
const value = {
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,
onGroupSelect: handleGroupSelect,
2021-06-05 02:38:01 -04:00
onFilterChange: setFilter,
};
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;