2021-05-23 23:34:21 -04:00
|
|
|
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";
|
2021-05-23 23:34:21 -04:00
|
|
|
|
|
|
|
import { useKeyboard, useBlur } from "./KeyboardContext";
|
|
|
|
|
2021-05-24 01:17:23 -04:00
|
|
|
import { getGroupItems, groupsFromIds } from "../helpers/group";
|
2021-05-23 23:34:21 -04:00
|
|
|
|
|
|
|
import shortcuts from "../shortcuts";
|
|
|
|
|
|
|
|
const GroupContext = React.createContext();
|
|
|
|
|
|
|
|
export function GroupProvider({
|
|
|
|
groups,
|
2021-06-05 02:38:01 -04:00
|
|
|
itemNames,
|
2021-05-23 23:34:21 -04:00
|
|
|
onGroupsChange,
|
|
|
|
onGroupsSelect,
|
|
|
|
disabled,
|
|
|
|
children,
|
|
|
|
}) {
|
|
|
|
const [selectedGroupIds, setSelectedGroupIds] = useState([]);
|
|
|
|
// Either single, multiple or range
|
|
|
|
const [selectMode, setSelectMode] = useState("single");
|
|
|
|
|
2021-06-05 03:16:39 -04:00
|
|
|
/**
|
|
|
|
* Group Open
|
|
|
|
*/
|
2021-06-05 02:38:01 -04:00
|
|
|
const [openGroupId, setOpenGroupId] = useState();
|
|
|
|
const [openGroupItems, setOpenGroupItems] = useState([]);
|
2021-05-23 23:34:21 -04:00
|
|
|
useEffect(() => {
|
|
|
|
if (openGroupId) {
|
2021-06-30 21:24:55 -04:00
|
|
|
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([]);
|
|
|
|
setOpenGroupId();
|
|
|
|
}
|
2021-05-23 23:34:21 -04:00
|
|
|
} else {
|
|
|
|
setOpenGroupItems([]);
|
|
|
|
}
|
|
|
|
}, [openGroupId, groups]);
|
|
|
|
|
|
|
|
function handleGroupOpen(groupId) {
|
|
|
|
setSelectedGroupIds([]);
|
|
|
|
setOpenGroupId(groupId);
|
|
|
|
}
|
|
|
|
|
|
|
|
function handleGroupClose() {
|
|
|
|
setSelectedGroupIds([]);
|
|
|
|
setOpenGroupId();
|
|
|
|
}
|
|
|
|
|
2021-06-05 03:16:39 -04:00
|
|
|
/**
|
|
|
|
* Search
|
|
|
|
*/
|
|
|
|
const [filter, setFilter] = useState();
|
|
|
|
const [filteredGroupItems, setFilteredGroupItems] = useState([]);
|
|
|
|
const [fuse, setFuse] = useState();
|
|
|
|
// 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(() => {
|
|
|
|
if (filter) {
|
|
|
|
const query = fuse.search(filter);
|
|
|
|
setFilteredGroupItems(query.map((result) => result.item));
|
|
|
|
setOpenGroupId();
|
|
|
|
} else {
|
|
|
|
setFilteredGroupItems([]);
|
|
|
|
}
|
|
|
|
}, [filter, fuse]);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handlers
|
|
|
|
*/
|
|
|
|
|
|
|
|
const activeGroups = openGroupId
|
|
|
|
? openGroupItems
|
|
|
|
: filter
|
|
|
|
? filteredGroupItems
|
|
|
|
: groups;
|
|
|
|
|
2021-05-27 23:13:21 -04:00
|
|
|
/**
|
|
|
|
* @param {string|undefined} groupId The group to apply changes to, leave undefined to replace the full group object
|
|
|
|
*/
|
|
|
|
function handleGroupsChange(newGroups, groupId) {
|
|
|
|
if (groupId) {
|
|
|
|
// If a group is specidifed then update that group with the new items
|
|
|
|
const groupIndex = groups.findIndex((group) => group.id === groupId);
|
2021-05-23 23:34:21 -04:00
|
|
|
let updatedGroups = cloneDeep(groups);
|
|
|
|
const group = updatedGroups[groupIndex];
|
|
|
|
updatedGroups[groupIndex] = { ...group, items: newGroups };
|
|
|
|
onGroupsChange(updatedGroups);
|
|
|
|
} else {
|
|
|
|
onGroupsChange(newGroups);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function handleGroupSelect(groupId) {
|
|
|
|
let groupIds = [];
|
|
|
|
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]
|
|
|
|
);
|
|
|
|
let idsToAdd = [];
|
|
|
|
let idsToRemove = [];
|
|
|
|
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;
|
2021-05-23 23:34:21 -04:00
|
|
|
default:
|
|
|
|
groupIds = [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setSelectedGroupIds(groupIds);
|
|
|
|
onGroupsSelect(groupIds);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Shortcuts
|
|
|
|
*/
|
|
|
|
function handleKeyDown(event) {
|
|
|
|
if (disabled) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (shortcuts.selectRange(event)) {
|
|
|
|
setSelectMode("range");
|
|
|
|
}
|
|
|
|
if (shortcuts.selectMultiple(event)) {
|
|
|
|
setSelectMode("multiple");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function handleKeyUp(event) {
|
|
|
|
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,
|
2021-05-23 23:34:21 -04:00
|
|
|
openGroupId,
|
|
|
|
openGroupItems,
|
2021-06-05 02:38:01 -04:00
|
|
|
filter,
|
|
|
|
filteredGroupItems,
|
2021-05-23 23:34:21 -04:00
|
|
|
selectedGroupIds,
|
|
|
|
selectMode,
|
2021-06-05 02:38:01 -04:00
|
|
|
onSelectModeChange: setSelectMode,
|
2021-05-23 23:34:21 -04:00
|
|
|
onGroupOpen: handleGroupOpen,
|
|
|
|
onGroupClose: handleGroupClose,
|
|
|
|
onGroupsChange: handleGroupsChange,
|
|
|
|
onGroupSelect: handleGroupSelect,
|
2021-06-05 02:38:01 -04:00
|
|
|
onFilterChange: setFilter,
|
2021-05-23 23:34:21 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
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,
|
|
|
|
};
|
|
|
|
|
2021-05-23 23:34:21 -04:00
|
|
|
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;
|