diff --git a/src/components/map/MapEditBar.js b/src/components/map/MapEditBar.js index 2c57f7a..2e6046c 100644 --- a/src/components/map/MapEditBar.js +++ b/src/components/map/MapEditBar.js @@ -20,18 +20,10 @@ function MapEditBar({ currentMap, disabled, onMapChange, onMapReset, onLoad }) { const { maps, mapStates, removeMaps, resetMap } = useMapData(); - const { - groups: allGroups, - selectedGroupIds, - onGroupSelect, - openGroupId, - openGroupItems, - } = useGroup(); - - const groups = openGroupId ? openGroupItems : allGroups; + const { activeGroups, selectedGroupIds, onGroupSelect } = useGroup(); useEffect(() => { - const selectedGroups = groupsFromIds(selectedGroupIds, groups); + const selectedGroups = groupsFromIds(selectedGroupIds, activeGroups); const selectedMaps = itemsFromGroups(selectedGroups, maps); const selectedMapStates = itemsFromGroups( selectedGroups, @@ -57,10 +49,10 @@ function MapEditBar({ currentMap, disabled, onMapChange, onMapReset, onLoad }) { } setHasMapState(_hasMapState); - }, [selectedGroupIds, maps, mapStates, groups]); + }, [selectedGroupIds, maps, mapStates, activeGroups]); function getSelectedMaps() { - const selectedGroups = groupsFromIds(selectedGroupIds, groups); + const selectedGroups = groupsFromIds(selectedGroupIds, activeGroups); return itemsFromGroups(selectedGroups, maps); } diff --git a/src/components/map/SelectMapSelectButton.js b/src/components/map/SelectMapSelectButton.js index 80823e5..4d7ae35 100644 --- a/src/components/map/SelectMapSelectButton.js +++ b/src/components/map/SelectMapSelectButton.js @@ -6,18 +6,11 @@ import { useGroup } from "../../contexts/GroupContext"; import { findGroup } from "../../helpers/group"; function SelectMapSelectButton({ onMapSelect, disabled }) { - const { - groups: allGroups, - selectedGroupIds, - openGroupId, - openGroupItems, - } = useGroup(); - - const groups = openGroupId ? openGroupItems : allGroups; + const { activeGroups, selectedGroupIds } = useGroup(); function handleSelectClick() { if (selectedGroupIds.length === 1) { - const group = findGroup(groups, selectedGroupIds[0]); + const group = findGroup(activeGroups, selectedGroupIds[0]); if (group && group.type === "item") { onMapSelect(group.id); } diff --git a/src/components/tile/SortableTile.js b/src/components/tile/SortableTile.js index 42e5c09..cc9c8a0 100644 --- a/src/components/tile/SortableTile.js +++ b/src/components/tile/SortableTile.js @@ -22,7 +22,7 @@ function SortableTile({ setDraggableNodeRef, over, active, - } = useSortable({ id, disabled: disableSorting }); + } = useSortable({ id }); const { setNodeRef: setGroupNodeRef } = useDroppable({ id: `${GROUP_ID_PREFIX}${id}`, @@ -42,7 +42,7 @@ function SortableTile({ width: "2px", height: "100%", borderRadius: "2px", - visibility: over?.id === id ? "visible" : "hidden", + visibility: over?.id === id && !disableSorting ? "visible" : "hidden", }; // Group div center aligned diff --git a/src/components/tile/SortableTiles.js b/src/components/tile/SortableTiles.js index d8ff37f..257cc6e 100644 --- a/src/components/tile/SortableTiles.js +++ b/src/components/tile/SortableTiles.js @@ -22,22 +22,29 @@ import { useGroup } from "../../contexts/GroupContext"; function SortableTiles({ renderTile, subgroup }) { const { dragId, overId, dragCursor } = useTileDrag(); const { - groups: allGroups, + groups, selectedGroupIds: allSelectedIds, + filter, openGroupId, openGroupItems, + filteredGroupItems, } = useGroup(); + const activeGroups = subgroup + ? openGroupItems + : filter + ? filteredGroupItems + : groups; + const sortableId = subgroup ? GROUP_SORTABLE_ID : BASE_SORTABLE_ID; - const groups = subgroup ? openGroupItems : allGroups; // Only populate selected groups if needed let selectedGroupIds = []; if ((subgroup && openGroupId) || (!subgroup && !openGroupId)) { selectedGroupIds = allSelectedIds; } - const disableSorting = openGroupId && !subgroup; - const disableGrouping = subgroup || disableSorting; + const disableSorting = (openGroupId && !subgroup) || filter; + const disableGrouping = subgroup || disableSorting || filter; const dragBounce = useSpring({ transform: !!dragId ? "scale(0.9)" : "scale(1)", @@ -63,9 +70,9 @@ function SortableTiles({ renderTile, subgroup }) { function renderDragOverlays() { let selectedIndices = selectedGroupIds.map((groupId) => - groups.findIndex((group) => group.id === groupId) + activeGroups.findIndex((group) => group.id === groupId) ); - const activeIndex = groups.findIndex((group) => group.id === dragId); + const activeIndex = activeGroups.findIndex((group) => group.id === dragId); // Sort so the draging tile is the first element selectedIndices = selectedIndices.sort((a, b) => a === activeIndex ? -1 : b === activeIndex ? 1 : 0 @@ -81,7 +88,7 @@ function SortableTiles({ renderTile, subgroup }) { selectedIndices = selectedIndices.reverse(); coords = coords.reverse(); - const selectedGroups = selectedIndices.map((index) => groups[index]); + const selectedGroups = selectedIndices.map((index) => activeGroups[index]); return selectedGroups.map((group, index) => ( @@ -113,7 +120,7 @@ function SortableTiles({ renderTile, subgroup }) { } function renderTiles() { - const groupsByIds = keyBy(groups, "id"); + const groupsByIds = keyBy(activeGroups, "id"); const selectedGroupIdsSet = new Set(selectedGroupIds); let selectedGroups = []; let hasSelectedContainerGroup = false; @@ -126,7 +133,7 @@ function SortableTiles({ renderTile, subgroup }) { } } } - return groups.map((group) => { + return activeGroups.map((group) => { const isDragging = dragId && selectedGroupIdsSet.has(group.id); const disableTileGrouping = disableGrouping || isDragging || hasSelectedContainerGroup; @@ -147,7 +154,7 @@ function SortableTiles({ renderTile, subgroup }) { } return ( - + {renderTiles()} {createPortal(dragId && renderDragOverlays(), document.body)} diff --git a/src/components/FilterBar.js b/src/components/tile/TileActionBar.js similarity index 66% rename from src/components/FilterBar.js rename to src/components/tile/TileActionBar.js index cb6aa1d..5865a61 100644 --- a/src/components/FilterBar.js +++ b/src/components/tile/TileActionBar.js @@ -1,22 +1,24 @@ import React from "react"; import { Flex, IconButton } from "theme-ui"; -import AddIcon from "../icons/AddIcon"; -import SelectMultipleIcon from "../icons/SelectMultipleIcon"; -import SelectSingleIcon from "../icons/SelectSingleIcon"; +import AddIcon from "../../icons/AddIcon"; +import SelectMultipleIcon from "../../icons/SelectMultipleIcon"; +import SelectSingleIcon from "../../icons/SelectSingleIcon"; -import Search from "./Search"; -import RadioIconButton from "./RadioIconButton"; +import Search from "../Search"; +import RadioIconButton from "../RadioIconButton"; + +import { useGroup } from "../../contexts/GroupContext"; + +function TileActionBar({ onAdd, addTitle }) { + const { + selectMode, + onSelectModeChange, + onGroupSelect, + filter, + onFilterChange, + } = useGroup(); -function FilterBar({ - onFocus, - search, - onSearchChange, - selectMode, - onSelectModeChange, - onAdd, - addTitle, -}) { return ( onGroupSelect()} > - + onFilterChange(e.target.value)} /> { - const selectedGroups = groupsFromIds(selectedGroupIds, groups); + const selectedGroups = groupsFromIds(selectedGroupIds, activeGroups); const selectedTokens = itemsFromGroups(selectedGroups, tokens); setHasSelectedDefaultToken( selectedTokens.some((token) => token.type === "default") ); setAllTokensVisisble(selectedTokens.every((token) => !token.hideInSidebar)); - }, [selectedGroupIds, tokens, groups]); + }, [selectedGroupIds, tokens, activeGroups]); function getSelectedTokens() { - const selectedGroups = groupsFromIds(selectedGroupIds, groups); + const selectedGroups = groupsFromIds(selectedGroupIds, activeGroups); return itemsFromGroups(selectedGroups, tokens); } diff --git a/src/contexts/GroupContext.js b/src/contexts/GroupContext.js index e414090..a784d03 100644 --- a/src/contexts/GroupContext.js +++ b/src/contexts/GroupContext.js @@ -1,5 +1,6 @@ import React, { useState, useContext, useEffect } from "react"; import cloneDeep from "lodash.clonedeep"; +import Fuse from "fuse.js"; import { useKeyboard, useBlur } from "./KeyboardContext"; @@ -11,17 +12,18 @@ const GroupContext = React.createContext(); export function GroupProvider({ groups, + itemNames, onGroupsChange, onGroupsSelect, disabled, children, }) { const [selectedGroupIds, setSelectedGroupIds] = useState([]); - const [openGroupId, setOpenGroupId] = useState(); - const [openGroupItems, setOpenGroupItems] = useState([]); // Either single, multiple or range const [selectMode, setSelectMode] = useState("single"); + const [openGroupId, setOpenGroupId] = useState(); + const [openGroupItems, setOpenGroupItems] = useState([]); useEffect(() => { if (openGroupId) { setOpenGroupItems(getGroupItems(groupsFromIds([openGroupId], groups)[0])); @@ -117,16 +119,58 @@ export function GroupProvider({ useBlur(handleBlur); + /** + * 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]); + + const activeGroups = openGroupId + ? openGroupItems + : filter + ? filteredGroupItems + : groups; + const value = { groups, + activeGroups, openGroupId, openGroupItems, + filter, + filteredGroupItems, selectedGroupIds, selectMode, + onSelectModeChange: setSelectMode, onGroupOpen: handleGroupOpen, onGroupClose: handleGroupClose, onGroupsChange: handleGroupsChange, onGroupSelect: handleGroupSelect, + onFilterChange: setFilter, }; return ( @@ -136,6 +180,7 @@ export function GroupProvider({ GroupProvider.defaultProps = { groups: [], + itemNames: {}, onGroupsChange: () => {}, onGroupsSelect: () => {}, disabled: false, diff --git a/src/contexts/TileDragContext.js b/src/contexts/TileDragContext.js index 6cf110e..95aa1a8 100644 --- a/src/contexts/TileDragContext.js +++ b/src/contexts/TileDragContext.js @@ -23,19 +23,16 @@ export const ADD_TO_MAP_ID_PREFIX = "__add__"; export function TileDragProvider({ onDragAdd, children }) { const { - groups: allGroups, + groups, + activeGroups, openGroupId, - openGroupItems, selectedGroupIds, onGroupsChange, onGroupSelect, onGroupClose, + filter, } = useGroup(); - const groupOpen = !!openGroupId; - - const groups = groupOpen ? openGroupItems : allGroups; - const mouseSensor = useSensor(MouseSensor, { activationConstraint: { delay: 250, tolerance: 5 }, }); @@ -83,7 +80,7 @@ export function TileDragProvider({ onDragAdd, children }) { } let selectedIndices = selectedGroupIds.map((groupId) => - groups.findIndex((group) => group.id === groupId) + activeGroups.findIndex((group) => group.id === groupId) ); // Maintain current group sorting selectedIndices = selectedIndices.sort((a, b) => a - b); @@ -96,15 +93,17 @@ export function TileDragProvider({ onDragAdd, children }) { return; } - const overGroupIndex = groups.findIndex((group) => group.id === overId); + const overGroupIndex = activeGroups.findIndex( + (group) => group.id === overId + ); onGroupsChange( - moveGroupsInto(groups, overGroupIndex, selectedIndices), + moveGroupsInto(activeGroups, overGroupIndex, selectedIndices), openGroupId ); } else if (over.id.startsWith(UNGROUP_ID_PREFIX)) { onGroupSelect(); // Handle tile ungroup - const newGroups = ungroup(allGroups, openGroupId, selectedIndices); + const newGroups = ungroup(groups, openGroupId, selectedIndices); // Close group if it was removed if (!newGroups.find((group) => group.id === openGroupId)) { onGroupClose(); @@ -112,11 +111,13 @@ export function TileDragProvider({ onDragAdd, children }) { onGroupsChange(newGroups); } else if (over.id.startsWith(ADD_TO_MAP_ID_PREFIX)) { onDragAdd && onDragAdd(selectedGroupIds, over.rect); - } else { - // Hanlde tile move - const overGroupIndex = groups.findIndex((group) => group.id === over.id); + } else if (!filter) { + // Hanlde tile move only if we have no filter + const overGroupIndex = activeGroups.findIndex( + (group) => group.id === over.id + ); onGroupsChange( - moveGroups(groups, overGroupIndex, selectedIndices), + moveGroups(activeGroups, overGroupIndex, selectedIndices), openGroupId ); } @@ -124,7 +125,7 @@ export function TileDragProvider({ onDragAdd, children }) { function customCollisionDetection(rects, rect) { // Handle group rects - if (groupOpen) { + if (openGroupId) { const ungroupRects = rects.filter(([id]) => id.startsWith(UNGROUP_ID_PREFIX) ); diff --git a/src/helpers/group.js b/src/helpers/group.js index f474d9d..9b81aea 100644 --- a/src/helpers/group.js +++ b/src/helpers/group.js @@ -208,3 +208,16 @@ export function findGroup(groups, groupId) { } } } + +/** + * Transform and item array to a record of item ids to item names + * @param {any[]} items + * @param {string=} itemKey + */ +export function getItemNames(items, itemKey = "id") { + let names = {}; + for (let item of items) { + names[item[itemKey]] = item.name; + } + return names; +} diff --git a/src/modals/SelectMapModal.js b/src/modals/SelectMapModal.js index 2c78b98..0fa9931 100644 --- a/src/modals/SelectMapModal.js +++ b/src/modals/SelectMapModal.js @@ -1,4 +1,4 @@ -import React, { useRef, useState } from "react"; +import React, { useRef, useState, useEffect } from "react"; import { Flex, Label, Box } from "theme-ui"; import { useToasts } from "react-toast-notifications"; import ReactResizeDetector from "react-resize-detector"; @@ -17,8 +17,9 @@ import SelectMapSelectButton from "../components/map/SelectMapSelectButton"; import TilesOverlay from "../components/tile/TilesOverlay"; import TilesContainer from "../components/tile/TilesContainer"; import TilesAddDroppable from "../components/tile/TilesAddDroppable"; +import TileActionBar from "../components/tile/TileActionBar"; -import { findGroup } from "../helpers/group"; +import { findGroup, getItemNames } from "../helpers/group"; import { createMapFromFile } from "../helpers/map"; import useResponsiveLayout from "../hooks/useResponsiveLayout"; @@ -54,6 +55,12 @@ function SelectMapModal({ } = useMapData(); const { addAssets } = useAssets(); + // Get map names for group filtering + const [mapNames, setMapNames] = useState(getItemNames(maps)); + useEffect(() => { + setMapNames(getItemNames(maps)); + }, [maps]); + /** * Image Upload */ @@ -102,6 +109,12 @@ function SelectMapModal({ } } + function openImageDialog() { + if (fileInputRef.current) { + fileInputRef.current.click(); + } + } + function handleLargeImageWarningCancel() { largeImageWarningFiles.current = undefined; setShowLargeImageWarning(false); @@ -201,6 +214,7 @@ function SelectMapModal({ > Select or import a map + diff --git a/src/modals/SelectTokensModal.js b/src/modals/SelectTokensModal.js index ed7fd81..b379f6f 100644 --- a/src/modals/SelectTokensModal.js +++ b/src/modals/SelectTokensModal.js @@ -1,4 +1,4 @@ -import React, { useRef, useState } from "react"; +import React, { useRef, useState, useEffect } from "react"; import { Flex, Label, Button, Box } from "theme-ui"; import { useToasts } from "react-toast-notifications"; import ReactResizeDetector from "react-resize-detector"; @@ -16,8 +16,9 @@ import TokenEditBar from "../components/token/TokenEditBar"; import TilesOverlay from "../components/tile/TilesOverlay"; import TilesContainer from "../components/tile/TilesContainer"; import TilesAddDroppable from "../components/tile/TilesAddDroppable"; +import TileActionBar from "../components/tile/TileActionBar"; -import { getGroupItems } from "../helpers/group"; +import { getGroupItems, getItemNames } from "../helpers/group"; import { createTokenFromFile, createTokenState, @@ -49,6 +50,12 @@ function SelectTokensModal({ isOpen, onRequestClose, onMapTokensStateCreate }) { } = useTokenData(); const { addAssets } = useAssets(); + // Get token names for group filtering + const [tokenNames, setTokenNames] = useState(getItemNames(tokens)); + useEffect(() => { + setTokenNames(getItemNames(tokens)); + }, [tokens]); + /** * Image Upload */ @@ -97,6 +104,12 @@ function SelectTokensModal({ isOpen, onRequestClose, onMapTokensStateCreate }) { } } + function openImageDialog() { + if (fileInputRef.current) { + fileInputRef.current.click(); + } + } + function handleLargeImageWarningCancel() { largeImageWarningFiles.current = undefined; setShowLargeImageWarning(false); @@ -202,6 +215,7 @@ function SelectTokensModal({ isOpen, onRequestClose, onMapTokensStateCreate }) { > @@ -213,6 +227,10 @@ function SelectTokensModal({ isOpen, onRequestClose, onMapTokensStateCreate }) { +