Fix drag and drop add to map bugs with scrolled tile grid

This commit is contained in:
Mitchell McCaffrey 2021-06-08 23:46:20 +10:00
parent 2088c2ab04
commit 57cce9346d
9 changed files with 115 additions and 183 deletions

View File

@ -5,8 +5,8 @@
"dependencies": {
"@babylonjs/core": "^4.2.0",
"@babylonjs/loaders": "^4.2.0",
"@dnd-kit/core": "3.0.2",
"@dnd-kit/sortable": "^3.0.1",
"@dnd-kit/core": "^3.0.4",
"@dnd-kit/sortable": "^3.1.0",
"@mitchemmc/dexie-export-import": "^1.0.1",
"@msgpack/msgpack": "^2.4.1",
"@sentry/react": "^6.2.2",

View File

@ -1,54 +0,0 @@
import React from "react";
import { createPortal } from "react-dom";
import Droppable from "../drag/Droppable";
import { ADD_TO_MAP_ID_PREFIX } from "../../contexts/TileDragContext";
function TilesAddDroppable({ containerSize }) {
return createPortal(
<div>
<Droppable
id={`${ADD_TO_MAP_ID_PREFIX}-1`}
style={{
width: "100vw",
height: `calc(50vh - ${containerSize.height / 2}px)`,
position: "absolute",
top: 0,
}}
/>
<Droppable
id={`${ADD_TO_MAP_ID_PREFIX}-2`}
style={{
width: "100vw",
height: `calc(50vh - ${containerSize.height / 2}px)`,
position: "absolute",
bottom: 0,
}}
/>
<Droppable
id={`${ADD_TO_MAP_ID_PREFIX}-3`}
style={{
width: `calc(50vw - ${containerSize.width / 2}px)`,
height: "100vh",
position: "absolute",
top: 0,
left: 0,
}}
/>
<Droppable
id={`${ADD_TO_MAP_ID_PREFIX}-4`}
style={{
width: `calc(50vw - ${containerSize.width / 2}px)`,
height: "100vh",
position: "absolute",
top: 0,
right: 0,
}}
/>
</div>,
document.body
);
}
export default TilesAddDroppable;

View File

@ -3,9 +3,12 @@ import { Grid, useThemeUI } from "theme-ui";
import SimpleBar from "simplebar-react";
import { useGroup } from "../../contexts/GroupContext";
import { ADD_TO_MAP_ID } from "../../contexts/TileDragContext";
import useResponsiveLayout from "../../hooks/useResponsiveLayout";
import Droppable from "../drag/Droppable";
function TilesContainer({ children }) {
const { onGroupSelect } = useGroup();
@ -28,10 +31,19 @@ function TilesContainer({ children }) {
sx={{
borderRadius: "4px",
overflow: "hidden",
position: "relative",
}}
gap={2}
columns={`repeat(${layout.tileGridColumns}, 1fr)`}
>
<Droppable
id={ADD_TO_MAP_ID}
style={{
position: "absolute",
inset: 0,
zIndex: -1,
}}
/>
{children}
</Grid>
</SimpleBar>

View File

@ -5,8 +5,7 @@ import ReactResizeDetector from "react-resize-detector";
import SimpleBar from "simplebar-react";
import { useGroup } from "../../contexts/GroupContext";
import TilesUngroupDroppable from "./TilesUngroupDroppable";
import { UNGROUP_ID, ADD_TO_MAP_ID } from "../../contexts/TileDragContext";
import useResponsiveLayout from "../../hooks/useResponsiveLayout";
@ -16,7 +15,9 @@ import GroupNameModal from "../../modals/GroupNameModal";
import { renameGroup } from "../../helpers/group";
function TilesOverlay({ children }) {
import Droppable from "../drag/Droppable";
function TilesOverlay({ modalSize, children }) {
const {
groups,
openGroupId,
@ -41,11 +42,6 @@ function TilesOverlay({ children }) {
setContinerSize({ width: size, height: size });
}
const [overlaySize, setOverlaySize] = useState({ width: 0, height: 0 });
function handleOverlayResize(width, height) {
setOverlaySize({ width, height });
}
const [isGroupNameModalOpen, setIsGroupNameModalOpen] = useState(false);
function handleGroupNameChange(name) {
onGroupsChange(renameGroup(groups, openGroupId, name));
@ -57,28 +53,16 @@ function TilesOverlay({ children }) {
return (
<>
{openGroupId && (
<TilesUngroupDroppable
innerContainerSize={containerSize}
outerContainerSize={overlaySize}
<Box
sx={{
position: "absolute",
width: "100%",
height: "100%",
top: 0,
}}
bg="overlay"
/>
)}
{openGroupId && (
<ReactResizeDetector
handleWidth
handleHeight
onResize={handleOverlayResize}
>
<Box
sx={{
position: "absolute",
width: "100%",
height: "100%",
top: 0,
}}
bg="overlay"
/>
</ReactResizeDetector>
)}
<ReactResizeDetector
handleWidth
handleHeight
@ -147,11 +131,38 @@ function TilesOverlay({ children }) {
sx={{
borderRadius: "4px",
overflow: "hidden",
position: "relative",
}}
gap={2}
columns={`repeat(${layout.groupGridColumns}, 1fr)`}
p={3}
>
<Droppable
id={ADD_TO_MAP_ID}
style={{
position: "absolute",
width: modalSize.width,
// height: modalSize.height,
height: `calc(100% + ${
modalSize.height - containerSize.height + 48
}px)`,
left: `-${
(modalSize.width - containerSize.width) / 2 + 8
}px`,
top: `-${
(modalSize.height - containerSize.height) / 2 + 48
}px`,
zIndex: -1,
}}
/>
<Droppable
id={UNGROUP_ID}
style={{
position: "absolute",
inset: 0,
zIndex: -1,
}}
/>
{children}
</Grid>
</SimpleBar>

View File

@ -1,59 +0,0 @@
import React from "react";
import { createPortal } from "react-dom";
import Droppable from "../drag/Droppable";
import { UNGROUP_ID_PREFIX } from "../../contexts/TileDragContext";
function TilesUngroupDroppable({ outerContainerSize, innerContainerSize }) {
const width = (outerContainerSize.width - innerContainerSize.width) / 2;
const height = (outerContainerSize.height - innerContainerSize.height) / 2;
return createPortal(
<div>
<Droppable
id={`${UNGROUP_ID_PREFIX}-1`}
style={{
width: outerContainerSize.width,
height,
position: "absolute",
top: `calc(50% - ${innerContainerSize.height / 2 + height}px)`,
left: `calc(50% - ${outerContainerSize.width / 2}px)`,
}}
/>
<Droppable
id={`${UNGROUP_ID_PREFIX}-2`}
style={{
width: outerContainerSize.width,
height,
position: "absolute",
top: `calc(50% + ${innerContainerSize.height / 2}px)`,
left: `calc(50% - ${outerContainerSize.width / 2}px)`,
}}
/>
<Droppable
id={`${UNGROUP_ID_PREFIX}-3`}
style={{
width,
height: outerContainerSize.height,
position: "absolute",
top: `calc(50% - ${outerContainerSize.height / 2}px)`,
left: `calc(50% - ${innerContainerSize.width / 2 + width}px)`,
}}
/>
<Droppable
id={`${UNGROUP_ID_PREFIX}-4`}
style={{
width,
height: outerContainerSize.height,
position: "absolute",
top: `calc(50% - ${outerContainerSize.height / 2}px)`,
left: `calc(50% + ${innerContainerSize.width / 2}px)`,
}}
/>
</div>,
document.body
);
}
export default TilesUngroupDroppable;

View File

@ -6,7 +6,6 @@ import {
useSensor,
useSensors,
closestCenter,
rectIntersection,
} from "@dnd-kit/core";
import { useGroup } from "./GroupContext";
@ -18,8 +17,26 @@ const TileDragContext = React.createContext();
export const BASE_SORTABLE_ID = "__base__";
export const GROUP_SORTABLE_ID = "__group__";
export const GROUP_ID_PREFIX = "__group__";
export const UNGROUP_ID_PREFIX = "__ungroup__";
export const ADD_TO_MAP_ID_PREFIX = "__add__";
export const UNGROUP_ID = "__ungroup__";
export const ADD_TO_MAP_ID = "__add__";
// Custom rectIntersect that takes a point
function rectIntersection(rects, point) {
for (let rect of rects) {
const [id, bounds] = rect;
if (
id &&
bounds &&
point.x > bounds.offsetLeft &&
point.x < bounds.offsetLeft + bounds.width &&
point.y > bounds.offsetTop &&
point.y < bounds.offsetTop + bounds.height
) {
return id;
}
}
return null;
}
export function TileDragProvider({ onDragAdd, children }) {
const {
@ -59,11 +76,11 @@ export function TileDragProvider({ onDragAdd, children }) {
setOverId(over?.id);
if (over) {
if (
over.id.startsWith(UNGROUP_ID_PREFIX) ||
over.id.startsWith(UNGROUP_ID) ||
over.id.startsWith(GROUP_ID_PREFIX)
) {
setDragCursor("alias");
} else if (over.id.startsWith(ADD_TO_MAP_ID_PREFIX)) {
} else if (over.id.startsWith(ADD_TO_MAP_ID)) {
setDragCursor(onDragAdd ? "copy" : "no-drop");
} else {
setDragCursor("grabbing");
@ -100,7 +117,7 @@ export function TileDragProvider({ onDragAdd, children }) {
moveGroupsInto(activeGroups, overGroupIndex, selectedIndices),
openGroupId
);
} else if (over.id.startsWith(UNGROUP_ID_PREFIX)) {
} else if (over.id === UNGROUP_ID) {
onGroupSelect();
// Handle tile ungroup
const newGroups = ungroup(groups, openGroupId, selectedIndices);
@ -109,7 +126,7 @@ export function TileDragProvider({ onDragAdd, children }) {
onGroupClose();
}
onGroupsChange(newGroups);
} else if (over.id.startsWith(ADD_TO_MAP_ID_PREFIX)) {
} else if (over.id === ADD_TO_MAP_ID) {
onDragAdd && onDragAdd(selectedGroupIds, over.rect);
} else if (!filter) {
// Hanlde tile move only if we have no filter
@ -124,27 +141,39 @@ export function TileDragProvider({ onDragAdd, children }) {
}
function customCollisionDetection(rects, rect) {
// Handle group rects
if (openGroupId) {
const ungroupRects = rects.filter(([id]) =>
id.startsWith(UNGROUP_ID_PREFIX)
);
const intersectingGroupRect = rectIntersection(ungroupRects, rect);
if (intersectingGroupRect) {
return intersectingGroupRect;
// Calculate rect bottom taking into account any scroll offset
const rectBottom = rect.top + rect.bottom - rect.offsetTop;
const rectCenter = {
x: rect.left + rect.width / 2,
y: rectBottom - rect.height / 2,
};
// Find whether out rect center is outside our add to map rect
const addRect = rects.find(([id]) => id === ADD_TO_MAP_ID);
if (addRect) {
const intersectingAddRect = rectIntersection([addRect], rectCenter);
if (!intersectingAddRect) {
return ADD_TO_MAP_ID;
}
}
// Handle add to map rects
const addRects = rects.filter(([id]) =>
id.startsWith(ADD_TO_MAP_ID_PREFIX)
);
const intersectingAddRect = rectIntersection(addRects, rect);
if (intersectingAddRect) {
return intersectingAddRect;
// Find whether out rect center is outside our ungroup rect
if (openGroupId) {
const ungroupRect = rects.find(([id]) => id === UNGROUP_ID);
if (ungroupRect) {
const intersectingGroupRect = rectIntersection(
[ungroupRect],
rectCenter
);
if (!intersectingGroupRect) {
return UNGROUP_ID;
}
}
}
const otherRects = rects.filter(([id]) => id !== UNGROUP_ID_PREFIX);
const otherRects = rects.filter(
([id]) => id !== ADD_TO_MAP_ID && id !== UNGROUP_ID
);
return closestCenter(otherRects, rect);
}

View File

@ -17,7 +17,6 @@ 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, getItemNames } from "../helpers/group";
@ -231,7 +230,6 @@ function SelectMapModal({
<TileActionBar onAdd={openImageDialog} addTitle="Import Map(s)" />
<Box sx={{ position: "relative" }}>
<TileDragProvider onDragAdd={canAddDraggedMap && handleDragAdd}>
<TilesAddDroppable containerSize={modalSize} />
<TilesContainer>
<MapTiles
maps={maps}
@ -241,8 +239,7 @@ function SelectMapModal({
</TilesContainer>
</TileDragProvider>
<TileDragProvider onDragAdd={canAddDraggedMap && handleDragAdd}>
<TilesAddDroppable containerSize={modalSize} />
<TilesOverlay>
<TilesOverlay modalSize={modalSize}>
<MapTiles
maps={maps}
onMapEdit={setEditingMapId}

View File

@ -16,7 +16,6 @@ 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, getItemNames } from "../helpers/group";
@ -234,7 +233,6 @@ function SelectTokensModal({ isOpen, onRequestClose, onMapTokensStateCreate }) {
/>
<Box sx={{ position: "relative" }}>
<TileDragProvider onDragAdd={handleTokensAddToMap}>
<TilesAddDroppable containerSize={modalSize} />
<TilesContainer>
<TokenTiles
tokens={tokens}
@ -243,8 +241,7 @@ function SelectTokensModal({ isOpen, onRequestClose, onMapTokensStateCreate }) {
</TilesContainer>
</TileDragProvider>
<TileDragProvider onDragAdd={handleTokensAddToMap}>
<TilesAddDroppable containerSize={modalSize} />
<TilesOverlay>
<TilesOverlay modalSize={modalSize}>
<TokenTiles
tokens={tokens}
onTokenEdit={setEditingTokenId}

View File

@ -1803,21 +1803,20 @@
dependencies:
tslib "^2.0.0"
"@dnd-kit/core@3.0.2", "@dnd-kit/core@^3.0.0":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-3.0.2.tgz#e46ae11ef667aa5c31fddab21cf36ffd80d3ce5b"
integrity sha512-L+rGnDYBb4BfYKDylzIBeODRIlJ+YVvo2iL9pVXsh317Nq7c9irCvi3XK8JnWD5QBw/3WZ5FmbPmTE91EKwKeA==
"@dnd-kit/core@^3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-3.0.4.tgz#b10a0ffc9300a665108a4f7387b1c55dc6bf9c7f"
integrity sha512-EoUNyRWnRm8l0okwqG0iUwB0zrPkqBfJrIPdb3kMrTjoG/+70i4RnIuAhdKDnOxldFynWahPfJV3YBoDgeudgg==
dependencies:
"@dnd-kit/accessibility" "^3.0.0"
"@dnd-kit/utilities" "^2.0.0"
tslib "^2.0.0"
"@dnd-kit/sortable@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-3.0.1.tgz#a63c2bcecb57c48cd72abcc6364b7c35d3af351f"
integrity sha512-fRflFwkj1hXkNZTy/nA6zlgLryZCDKm0OaJnzcFWu9TNZ7hZ0Ja6EMQwhOu6aGuHyCTUGTToBho9ZyyVN671qw==
"@dnd-kit/sortable@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-3.1.0.tgz#4638ea3d9202287ffb186679b593b3407c7e737d"
integrity sha512-BwnNgqMTqwIASdu9/x5PdqnAaFakx4HrflDP/zHfZAnGm912+Y545PlztVtAdxK+U6L3pXPR1BQaZO7JQjDxmg==
dependencies:
"@dnd-kit/core" "^3.0.0"
"@dnd-kit/utilities" "^2.0.0"
tslib "^2.0.0"