Added map search UI and initial multi-select
This commit is contained in:
parent
78c86e6194
commit
670f047049
39
src/components/Search.js
Normal file
39
src/components/Search.js
Normal file
@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
import { Box, Input } from "theme-ui";
|
||||
|
||||
import SearchIcon from "../icons/SearchIcon";
|
||||
|
||||
function Search(props) {
|
||||
return (
|
||||
<Box sx={{ position: "relative", flexGrow: 1 }}>
|
||||
<Input
|
||||
sx={{
|
||||
borderRadius: "0",
|
||||
border: "none",
|
||||
borderRight: "1px solid",
|
||||
":focus": {
|
||||
outline: "none",
|
||||
},
|
||||
paddingRight: "36px",
|
||||
}}
|
||||
placeholder="Search"
|
||||
{...props}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
right: "8px",
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
height: "24px",
|
||||
width: "24px",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
<SearchIcon />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Search;
|
@ -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 && (
|
||||
<Box sx={{ position: "absolute", top: 0, right: 0 }}>
|
||||
<IconButton
|
||||
aria-label="Edit Map"
|
||||
|
@ -6,34 +6,88 @@ import { useMedia } from "react-media";
|
||||
import AddIcon from "../../icons/AddIcon";
|
||||
import RemoveMapIcon from "../../icons/RemoveMapIcon";
|
||||
import ResetMapIcon from "../../icons/ResetMapIcon";
|
||||
import SelectMultipleIcon from "../../icons/SelectMultipleIcon";
|
||||
import SelectSingleIcon from "../../icons/SelectSingleIcon";
|
||||
|
||||
import MapTile from "./MapTile";
|
||||
import Link from "../Link";
|
||||
import Search from "../Search";
|
||||
|
||||
import DatabaseContext from "../../contexts/DatabaseContext";
|
||||
|
||||
function MapTiles({
|
||||
maps,
|
||||
selectedMap,
|
||||
selectedMapState,
|
||||
selectedMaps,
|
||||
selectedMapStates,
|
||||
onMapSelect,
|
||||
onMapRemove,
|
||||
onMapReset,
|
||||
onMapsRemove,
|
||||
onMapsReset,
|
||||
onMapAdd,
|
||||
onMapEdit,
|
||||
onDone,
|
||||
selectMode,
|
||||
onSelectModeChange,
|
||||
}) {
|
||||
const { databaseStatus } = useContext(DatabaseContext);
|
||||
const isSmallScreen = useMedia({ query: "(max-width: 500px)" });
|
||||
|
||||
const hasMapState =
|
||||
selectedMapState &&
|
||||
(Object.values(selectedMapState.tokens).length > 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 (
|
||||
<Box sx={{ position: "relative" }}>
|
||||
<Flex
|
||||
bg="muted"
|
||||
sx={{
|
||||
border: "1px solid",
|
||||
borderColor: "text",
|
||||
borderRadius: "4px",
|
||||
alignItems: "center",
|
||||
":focus-within": {
|
||||
outline: "1px auto",
|
||||
outlineColor: "primary",
|
||||
outlineOffset: "0px",
|
||||
},
|
||||
}}
|
||||
onFocus={() => onMapSelect()}
|
||||
>
|
||||
<Search />
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
onSelectModeChange(selectMode === "single" ? "multiple" : "single")
|
||||
}
|
||||
aria-label={
|
||||
selectMode === "single" ? "Select Multiple" : "Select Single"
|
||||
}
|
||||
title={selectMode === "single" ? "Select Multiple" : "Select Single"}
|
||||
ml={1}
|
||||
>
|
||||
{selectMode === "single" ? (
|
||||
<SelectMultipleIcon />
|
||||
) : (
|
||||
<SelectSingleIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={onMapAdd}
|
||||
aria-label="Add Map"
|
||||
title="Add Map"
|
||||
mr={1}
|
||||
>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
<SimpleBar style={{ maxHeight: "400px" }}>
|
||||
<Flex
|
||||
p={2}
|
||||
@ -43,49 +97,10 @@ function MapTiles({
|
||||
flexWrap: "wrap",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
onClick={() => onMapSelect(null)}
|
||||
onClick={() => onMapSelect()}
|
||||
>
|
||||
<Flex
|
||||
onClick={onMapAdd}
|
||||
sx={{
|
||||
":hover": {
|
||||
color: "primary",
|
||||
},
|
||||
":focus": {
|
||||
outline: "none",
|
||||
},
|
||||
":active": {
|
||||
color: "secondary",
|
||||
},
|
||||
width: isSmallScreen ? "48%" : "32%",
|
||||
height: "0",
|
||||
paddingTop: isSmallScreen ? "48%" : "32%",
|
||||
borderRadius: "4px",
|
||||
position: "relative",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
my={1}
|
||||
mx={`${isSmallScreen ? 1 : 2 / 3}%`}
|
||||
bg="muted"
|
||||
aria-label="Add Map"
|
||||
title="Add Map"
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<AddIcon large />
|
||||
</Flex>
|
||||
</Flex>
|
||||
{maps.map((map) => {
|
||||
const isSelected = selectedMap && map.id === selectedMap.id;
|
||||
const isSelected = selectedMaps.includes(map);
|
||||
return (
|
||||
<MapTile
|
||||
key={map.id}
|
||||
@ -95,6 +110,7 @@ function MapTiles({
|
||||
onMapEdit={onMapEdit}
|
||||
onDone={onDone}
|
||||
large={isSmallScreen}
|
||||
canEdit={isSelected && selectMode === "single"}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@ -118,7 +134,7 @@ function MapTiles({
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{selectedMap && (
|
||||
{selectedMaps.length > 0 && (
|
||||
<Flex
|
||||
sx={{
|
||||
position: "absolute",
|
||||
@ -132,13 +148,13 @@ function MapTiles({
|
||||
<Close
|
||||
title="Clear Selection"
|
||||
aria-label="Clear Selection"
|
||||
onClick={() => onMapSelect(null)}
|
||||
onClick={() => onMapSelect()}
|
||||
/>
|
||||
<Flex>
|
||||
<IconButton
|
||||
aria-label="Reset Map"
|
||||
title="Reset Map"
|
||||
onClick={() => onMapReset(selectedMap.id)}
|
||||
onClick={() => onMapsReset()}
|
||||
disabled={!hasMapState}
|
||||
>
|
||||
<ResetMapIcon />
|
||||
@ -146,7 +162,7 @@ function MapTiles({
|
||||
<IconButton
|
||||
aria-label="Remove Map"
|
||||
title="Remove Map"
|
||||
onClick={() => onMapRemove(selectedMap.id)}
|
||||
onClick={() => onMapsRemove()}
|
||||
>
|
||||
<RemoveMapIcon />
|
||||
</IconButton>
|
||||
|
18
src/icons/SearchIcon.js
Normal file
18
src/icons/SearchIcon.js
Normal file
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
function SearchIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
fill="currentcolor"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M15.5 14h-.79l-.28-.27c1.2-1.4 1.82-3.31 1.48-5.34-.47-2.78-2.79-5-5.59-5.34-4.23-.52-7.79 3.04-7.27 7.27.34 2.8 2.56 5.12 5.34 5.59 2.03.34 3.94-.28 5.34-1.48l.27.28v.79l4.25 4.25c.41.41 1.08.41 1.49 0 .41-.41.41-1.08 0-1.49L15.5 14zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchIcon;
|
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
function SelectMapIcon() {
|
||||
function SelectDiceIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -15,4 +15,4 @@ function SelectMapIcon() {
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectMapIcon;
|
||||
export default SelectDiceIcon;
|
||||
|
18
src/icons/SelectMultipleIcon.js
Normal file
18
src/icons/SelectMultipleIcon.js
Normal file
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
function SelectMultipleIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
fill="currentcolor"
|
||||
>
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M3 5h2V3c-1.1 0-2 .9-2 2zm0 8h2v-2H3v2zm4 8h2v-2H7v2zM3 9h2V7H3v2zm10-6h-2v2h2V3zm6 0v2h2c0-1.1-.9-2-2-2zM5 21v-2H3c0 1.1.9 2 2 2zm-2-4h2v-2H3v2zM9 3H7v2h2V3zm2 18h2v-2h-2v2zm8-8h2v-2h-2v2zm0 8c1.1 0 2-.9 2-2h-2v2zm0-12h2V7h-2v2zm0 8h2v-2h-2v2zm-4 4h2v-2h-2v2zm0-16h2V3h-2v2zM8 17h8c.55 0 1-.45 1-1V8c0-.55-.45-1-1-1H8c-.55 0-1 .45-1 1v8c0 .55.45 1 1 1zm1-8h6v6H9V9z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectMultipleIcon;
|
18
src/icons/SelectSingleIcon.js
Normal file
18
src/icons/SelectSingleIcon.js
Normal file
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
function SelectSingleIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
fill="currentcolor"
|
||||
>
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M18 4H6c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-1 14H7c-.55 0-1-.45-1-1V7c0-.55.45-1 1-1h10c.55 0 1 .45 1 1v10c0 .55-.45 1-1 1z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectSingleIcon;
|
@ -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}
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={imageLoading || !selectedMapId}
|
||||
disabled={imageLoading || selectedMapIds.length !== 1}
|
||||
onClick={handleDone}
|
||||
mt={2}
|
||||
>
|
||||
@ -266,8 +322,8 @@ function SelectMapModal({
|
||||
<EditMapModal
|
||||
isOpen={isEditModalOpen}
|
||||
onDone={() => setIsEditModalOpen(false)}
|
||||
map={selectedMap}
|
||||
mapState={selectedMapState}
|
||||
map={selectedMaps.length === 1 && selectedMaps[0]}
|
||||
mapState={selectedMapStates.length === 1 && selectedMapStates[0]}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user