From 670f04704998daa15c69f576d4f44bbc0b341cd6 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Wed, 30 Sep 2020 12:30:33 +1000 Subject: [PATCH] Added map search UI and initial multi-select --- src/components/Search.js | 39 ++++++++++ src/components/map/MapTile.js | 17 +++-- src/components/map/MapTiles.js | 124 ++++++++++++++++++-------------- src/icons/SearchIcon.js | 18 +++++ src/icons/SelectDiceIcon.js | 4 +- src/icons/SelectMultipleIcon.js | 18 +++++ src/icons/SelectSingleIcon.js | 18 +++++ src/modals/SelectMapModal.js | 108 +++++++++++++++++++++------- 8 files changed, 258 insertions(+), 88 deletions(-) create mode 100644 src/components/Search.js create mode 100644 src/icons/SearchIcon.js create mode 100644 src/icons/SelectMultipleIcon.js create mode 100644 src/icons/SelectSingleIcon.js diff --git a/src/components/Search.js b/src/components/Search.js new file mode 100644 index 0000000..df00aa1 --- /dev/null +++ b/src/components/Search.js @@ -0,0 +1,39 @@ +import React from "react"; +import { Box, Input } from "theme-ui"; + +import SearchIcon from "../icons/SearchIcon"; + +function Search(props) { + return ( + + + + + + + ); +} + +export default Search; diff --git a/src/components/map/MapTile.js b/src/components/map/MapTile.js index 25fb480..9437ba5 100644 --- a/src/components/map/MapTile.js +++ b/src/components/map/MapTile.js @@ -6,7 +6,15 @@ import EditMapIcon from "../../icons/EditMapIcon"; import useDataSource from "../../helpers/useDataSource"; import { mapSources as defaultMapSources, unknownSource } from "../../maps"; -function MapTile({ map, isSelected, onMapSelect, onMapEdit, onDone, large }) { +function MapTile({ + map, + isSelected, + onMapSelect, + onMapEdit, + onDone, + large, + canEdit, +}) { const [isMapTileMenuOpen, setIsTileMenuOpen] = useState(false); const isDefault = map.type === "default"; const mapSource = useDataSource( @@ -39,9 +47,7 @@ function MapTile({ map, isSelected, onMapSelect, onMapEdit, onDone, large }) { onClick={(e) => { e.stopPropagation(); setIsTileMenuOpen(false); - if (!isSelected) { - onMapSelect(map); - } + onMapSelect(map); }} onDoubleClick={(e) => { if (!isMapTileMenuOpen) { @@ -97,8 +103,7 @@ function MapTile({ map, isSelected, onMapSelect, onMapEdit, onDone, large }) { borderRadius: "4px", }} /> - {/* Show expand button only if both reset and remove is available */} - {isSelected && ( + {canEdit && ( 0 || - selectedMapState.mapDrawActions.length > 0 || - selectedMapState.fogDrawActions.length > 0); + let hasMapState = false; + if (selectedMapStates.length > 0) { + for (let state of selectedMapStates) { + if ( + Object.values(state.tokens).length > 0 || + state.mapDrawActions.length > 0 || + state.fogDrawActions.length > 0 + ) { + hasMapState = true; + break; + } + } + } return ( + onMapSelect()} + > + + + onSelectModeChange(selectMode === "single" ? "multiple" : "single") + } + aria-label={ + selectMode === "single" ? "Select Multiple" : "Select Single" + } + title={selectMode === "single" ? "Select Multiple" : "Select Single"} + ml={1} + > + {selectMode === "single" ? ( + + ) : ( + + )} + + + + + onMapSelect(null)} + onClick={() => onMapSelect()} > - - - - - {maps.map((map) => { - const isSelected = selectedMap && map.id === selectedMap.id; + const isSelected = selectedMaps.includes(map); return ( ); })} @@ -118,7 +134,7 @@ function MapTiles({ )} - {selectedMap && ( + {selectedMaps.length > 0 && ( onMapSelect(null)} + onClick={() => onMapSelect()} /> onMapReset(selectedMap.id)} + onClick={() => onMapsReset()} disabled={!hasMapState} > @@ -146,7 +162,7 @@ function MapTiles({ onMapRemove(selectedMap.id)} + onClick={() => onMapsRemove()} > diff --git a/src/icons/SearchIcon.js b/src/icons/SearchIcon.js new file mode 100644 index 0000000..095987b --- /dev/null +++ b/src/icons/SearchIcon.js @@ -0,0 +1,18 @@ +import React from "react"; + +function SearchIcon() { + return ( + + + + + ); +} + +export default SearchIcon; diff --git a/src/icons/SelectDiceIcon.js b/src/icons/SelectDiceIcon.js index c9433ef..c51c1a2 100644 --- a/src/icons/SelectDiceIcon.js +++ b/src/icons/SelectDiceIcon.js @@ -1,6 +1,6 @@ import React from "react"; -function SelectMapIcon() { +function SelectDiceIcon() { return ( + + + + ); +} + +export default SelectMultipleIcon; diff --git a/src/icons/SelectSingleIcon.js b/src/icons/SelectSingleIcon.js new file mode 100644 index 0000000..47b0bd6 --- /dev/null +++ b/src/icons/SelectSingleIcon.js @@ -0,0 +1,18 @@ +import React from "react"; + +function SelectSingleIcon() { + return ( + + + + + ); +} + +export default SelectSingleIcon; diff --git a/src/modals/SelectMapModal.js b/src/modals/SelectMapModal.js index 9169a91..eca6825 100644 --- a/src/modals/SelectMapModal.js +++ b/src/modals/SelectMapModal.js @@ -54,11 +54,13 @@ function SelectMapModal({ const [imageLoading, setImageLoading] = useState(false); // The map selected in the modal - const [selectedMapId, setSelectedMapId] = useState(null); + const [selectedMapIds, setSelectedMapIds] = useState([]); - const selectedMap = ownedMaps.find((map) => map.id === selectedMapId); - const selectedMapState = mapStates.find( - (state) => state.mapId === selectedMapId + const selectedMaps = ownedMaps.filter((map) => + selectedMapIds.includes(map.id) + ); + const selectedMapStates = mapStates.filter((state) => + selectedMapIds.includes(state.mapId) ); const [isEditModalOpen, setIsEditModalOpen] = useState(false); @@ -171,31 +173,83 @@ function SelectMapModal({ async function handleMapAdd(map) { await addMap(map); - setSelectedMapId(map.id); + setSelectedMapIds([map.id]); } - async function handleMapRemove(id) { - await removeMap(id); - setSelectedMapId(null); + async function handleMapsRemove() { + for (let id of selectedMapIds) { + await removeMap(id); + } + setSelectedMapIds([]); // Removed the map from the map screen if needed - if (currentMap && currentMap.id === selectedMapId) { + if (currentMap && selectedMapIds.includes(currentMap.id)) { onMapChange(null, null); } } + // Either single, multiple or range + const [selectMode, setSelectMode] = useState("single"); + async function handleMapSelect(map) { if (map) { - setSelectedMapId(map.id); + switch (selectMode) { + case "single": + setSelectedMapIds([map.id]); + break; + case "multiple": + setSelectedMapIds((prev) => { + if (prev.includes(map.id)) { + return prev.filter((id) => id !== map.id); + } else { + return [...prev, map.id]; + } + }); + break; + case "range": + // Add all items inbetween the previous selected map and the current selected + if (selectedMapIds.length > 0) { + const mapIndex = ownedMaps.findIndex((m) => m.id === map.id); + const lastIndex = ownedMaps.findIndex( + (m) => m.id === selectedMapIds[selectedMapIds.length - 1] + ); + let idsToAdd = []; + let idsToRemove = []; + const direction = mapIndex > lastIndex ? 1 : -1; + for ( + let i = mapIndex; + direction > 0 ? i >= lastIndex : i <= lastIndex; + i += direction + ) { + const mapId = ownedMaps[i].id; + if (selectedMapIds.includes(mapId)) { + idsToRemove.push(mapId); + } else { + idsToAdd.push(mapId); + } + } + setSelectedMapIds((prev) => { + let ids = [...prev, ...idsToAdd]; + return ids.filter((id) => idsToRemove.includes(id)); + }); + } else { + setSelectedMapIds([map.id]); + } + break; + default: + setSelectedMapIds([]); + } } else { - setSelectedMapId(null); + setSelectedMapIds([]); } } - async function handleMapReset(id) { - const newState = await resetMap(id); - // Reset the state of the current map if needed - if (currentMap && currentMap.id === selectedMapId) { - onMapStateChange(newState); + async function handleMapsReset() { + for (let id of selectedMapIds) { + const newState = await resetMap(id); + // Reset the state of the current map if needed + if (currentMap && currentMap.id === id) { + onMapStateChange(newState); + } } } @@ -207,11 +261,11 @@ function SelectMapModal({ if (imageLoading) { return; } - if (selectedMapId) { + if (selectedMapIds.length === 1) { // Update last used for cache invalidation const lastUsed = Date.now(); - await updateMap(selectedMapId, { lastUsed }); - onMapChange({ ...selectedMap, lastUsed }, selectedMapState); + await updateMap(selectedMapIds[0], { lastUsed }); + onMapChange({ ...selectedMaps[0], lastUsed }, selectedMapStates[0]); } else { onMapChange(null, null); } @@ -245,16 +299,18 @@ function SelectMapModal({ maps={ownedMaps} onMapAdd={openImageDialog} onMapEdit={() => setIsEditModalOpen(true)} - onMapReset={handleMapReset} - onMapRemove={handleMapRemove} - selectedMap={selectedMap} - selectedMapState={selectedMapState} + onMapsReset={handleMapsReset} + onMapsRemove={handleMapsRemove} + selectedMaps={selectedMaps} + selectedMapStates={selectedMapStates} onMapSelect={handleMapSelect} onDone={handleDone} + selectMode={selectMode} + onSelectModeChange={setSelectMode} />