Add back tile search and action bar

This commit is contained in:
Mitchell McCaffrey 2021-06-05 16:38:01 +10:00
parent d6b6d6a1eb
commit e3353c1c44
11 changed files with 161 additions and 83 deletions

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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

View File

@ -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) => (
<DragOverlay dropAnimation={null} key={group.id}>
@ -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 (
<SortableContext items={groups} id={sortableId}>
<SortableContext items={activeGroups} id={sortableId}>
{renderTiles()}
{createPortal(dragId && renderDragOverlays(), document.body)}
</SortableContext>

View File

@ -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 (
<Flex
bg="muted"
@ -31,9 +33,9 @@ function FilterBar({
outlineOffset: "0px",
},
}}
onFocus={onFocus}
onFocus={() => onGroupSelect()}
>
<Search value={search} onChange={onSearchChange} />
<Search value={filter} onChange={(e) => onFilterChange(e.target.value)} />
<Flex
mr={1}
px={1}
@ -66,4 +68,4 @@ function FilterBar({
);
}
export default FilterBar;
export default TileActionBar;

View File

@ -18,31 +18,23 @@ import shortcuts from "../../shortcuts";
function TokenEditBar({ disabled, onLoad }) {
const { tokens, removeTokens, updateTokensHidden } = useTokenData();
const {
groups: allGroups,
selectedGroupIds,
onGroupSelect,
openGroupId,
openGroupItems,
} = useGroup();
const groups = openGroupId ? openGroupItems : allGroups;
const { activeGroups, selectedGroupIds, onGroupSelect } = useGroup();
const [hasSelectedDefaultToken, setHasSelectedDefaultToken] = useState(false);
const [allTokensVisible, setAllTokensVisisble] = useState(false);
useEffect(() => {
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);
}

View File

@ -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,

View File

@ -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)
);

View File

@ -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;
}

View File

@ -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({
>
<GroupProvider
groups={mapGroups}
itemNames={mapNames}
onGroupsChange={updateMapGroups}
onGroupsSelect={handleGroupsSelect}
disabled={!isOpen}
@ -213,6 +227,7 @@ function SelectMapModal({
<Label pt={2} pb={1}>
Select or import a map
</Label>
<TileActionBar onAdd={openImageDialog} addTitle="Import Map(s)" />
<Box sx={{ position: "relative" }}>
<TileDragProvider onDragAdd={canAddDraggedMap && handleDragAdd}>
<TilesAddDroppable containerSize={modalSize} />

View File

@ -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 }) {
>
<GroupProvider
groups={tokenGroups}
itemNames={tokenNames}
onGroupsChange={updateTokenGroups}
disabled={!isOpen}
>
@ -213,6 +227,10 @@ function SelectTokensModal({ isOpen, onRequestClose, onMapTokensStateCreate }) {
<Label pt={2} pb={1}>
Edit or import a token
</Label>
<TileActionBar
onAdd={openImageDialog}
addTitle="Import Token(s)"
/>
<Box sx={{ position: "relative" }}>
<TileDragProvider onDragAdd={handleTokensAddToMap}>
<TilesAddDroppable containerSize={modalSize} />