Optimised re-renders with sortable tiles
This commit is contained in:
parent
6b927b4456
commit
bc97f4838a
@ -4,6 +4,7 @@ import MapTile from "./MapTile";
|
||||
import MapTileGroup from "./MapTileGroup";
|
||||
|
||||
import SortableTiles from "../tile/SortableTiles";
|
||||
import SortableTilesDragOverlay from "../tile/SortableTilesDragOverlay";
|
||||
|
||||
import { getGroupItems } from "../../helpers/group";
|
||||
|
||||
@ -57,7 +58,12 @@ function MapTiles({ mapsById, onMapEdit, onMapSelect, subgroup }) {
|
||||
}
|
||||
}
|
||||
|
||||
return <SortableTiles renderTile={renderTile} subgroup={subgroup} />;
|
||||
return (
|
||||
<>
|
||||
<SortableTiles renderTile={renderTile} subgroup={subgroup} />
|
||||
<SortableTilesDragOverlay renderTile={renderTile} subgroup={subgroup} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MapTiles;
|
||||
|
@ -1,26 +1,24 @@
|
||||
import React from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { DragOverlay } from "@dnd-kit/core";
|
||||
import { SortableContext } from "@dnd-kit/sortable";
|
||||
import { animated, useSpring, config } from "react-spring";
|
||||
import { Badge } from "theme-ui";
|
||||
|
||||
import { moveGroupsInto } from "../../helpers/group";
|
||||
import { keyBy } from "../../helpers/shared";
|
||||
import Vector2 from "../../helpers/Vector2";
|
||||
|
||||
import SortableTile from "./SortableTile";
|
||||
|
||||
import {
|
||||
useTileDrag,
|
||||
useTileDragId,
|
||||
useTileDragCursor,
|
||||
useTileOverGroupId,
|
||||
BASE_SORTABLE_ID,
|
||||
GROUP_SORTABLE_ID,
|
||||
GROUP_ID_PREFIX,
|
||||
} from "../../contexts/TileDragContext";
|
||||
import { useGroup } from "../../contexts/GroupContext";
|
||||
|
||||
function SortableTiles({ renderTile, subgroup }) {
|
||||
const { dragId, overId, dragCursor } = useTileDrag();
|
||||
const dragId = useTileDragId();
|
||||
const dragCursor = useTileDragCursor();
|
||||
const overGroupId = useTileOverGroupId();
|
||||
const {
|
||||
groups,
|
||||
selectedGroupIds: allSelectedIds,
|
||||
@ -46,15 +44,6 @@ function SortableTiles({ renderTile, subgroup }) {
|
||||
const disableSorting = (openGroupId && !subgroup) || filter;
|
||||
const disableGrouping = subgroup || disableSorting || filter;
|
||||
|
||||
const dragBounce = useSpring({
|
||||
transform: !!dragId ? "scale(0.9)" : "scale(1)",
|
||||
config: config.wobbly,
|
||||
position: "relative",
|
||||
});
|
||||
|
||||
const overGroupId =
|
||||
overId && overId.startsWith(GROUP_ID_PREFIX) && overId.slice(9);
|
||||
|
||||
function renderSortableGroup(group, selectedGroups) {
|
||||
if (overGroupId === group.id && dragId && group.id !== dragId) {
|
||||
// If dragging over a group render a preview of that group
|
||||
@ -68,57 +57,6 @@ function SortableTiles({ renderTile, subgroup }) {
|
||||
return renderTile(group);
|
||||
}
|
||||
|
||||
function renderDragOverlays() {
|
||||
let selectedIndices = selectedGroupIds.map((groupId) =>
|
||||
activeGroups.findIndex((group) => group.id === groupId)
|
||||
);
|
||||
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
|
||||
);
|
||||
|
||||
selectedIndices = selectedIndices.slice(0, 5);
|
||||
|
||||
let coords = selectedIndices.map(
|
||||
(_, index) => new Vector2(5 * index, 5 * index)
|
||||
);
|
||||
|
||||
// Reverse so the first element is rendered on top
|
||||
selectedIndices = selectedIndices.reverse();
|
||||
coords = coords.reverse();
|
||||
|
||||
const selectedGroups = selectedIndices.map((index) => activeGroups[index]);
|
||||
|
||||
return selectedGroups.map((group, index) => (
|
||||
<DragOverlay dropAnimation={null} key={group.id}>
|
||||
<div
|
||||
style={{
|
||||
transform: `translate(${coords[index].x}%, ${coords[index].y}%)`,
|
||||
cursor: dragCursor,
|
||||
}}
|
||||
>
|
||||
<animated.div style={dragBounce}>
|
||||
{renderTile(group)}
|
||||
{index === selectedIndices.length - 1 &&
|
||||
selectedGroupIds.length > 1 && (
|
||||
<Badge
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
transform: "translate(25%, -25%)",
|
||||
}}
|
||||
>
|
||||
{selectedGroupIds.length}
|
||||
</Badge>
|
||||
)}
|
||||
</animated.div>
|
||||
</div>
|
||||
</DragOverlay>
|
||||
));
|
||||
}
|
||||
|
||||
function renderTiles() {
|
||||
const groupsByIds = keyBy(activeGroups, "id");
|
||||
const selectedGroupIdsSet = new Set(selectedGroupIds);
|
||||
@ -156,7 +94,6 @@ function SortableTiles({ renderTile, subgroup }) {
|
||||
return (
|
||||
<SortableContext items={activeGroups} id={sortableId}>
|
||||
{renderTiles()}
|
||||
{createPortal(dragId && renderDragOverlays(), document.body)}
|
||||
</SortableContext>
|
||||
);
|
||||
}
|
||||
|
93
src/components/tile/SortableTilesDragOverlay.js
Normal file
93
src/components/tile/SortableTilesDragOverlay.js
Normal file
@ -0,0 +1,93 @@
|
||||
import React from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { DragOverlay } from "@dnd-kit/core";
|
||||
import { animated, useSpring, config } from "react-spring";
|
||||
import { Badge } from "theme-ui";
|
||||
|
||||
import Vector2 from "../../helpers/Vector2";
|
||||
|
||||
import { useTileDragId } from "../../contexts/TileDragContext";
|
||||
import { useGroup } from "../../contexts/GroupContext";
|
||||
|
||||
function SortableTilesDragOverlay({ renderTile, subgroup }) {
|
||||
const dragId = useTileDragId();
|
||||
const {
|
||||
groups,
|
||||
selectedGroupIds: allSelectedIds,
|
||||
filter,
|
||||
openGroupId,
|
||||
openGroupItems,
|
||||
filteredGroupItems,
|
||||
} = useGroup();
|
||||
|
||||
const activeGroups = subgroup
|
||||
? openGroupItems
|
||||
: filter
|
||||
? filteredGroupItems
|
||||
: groups;
|
||||
|
||||
// Only populate selected groups if needed
|
||||
let selectedGroupIds = [];
|
||||
if ((subgroup && openGroupId) || (!subgroup && !openGroupId)) {
|
||||
selectedGroupIds = allSelectedIds;
|
||||
}
|
||||
const dragBounce = useSpring({
|
||||
transform: !!dragId ? "scale(0.9)" : "scale(1)",
|
||||
config: config.wobbly,
|
||||
position: "relative",
|
||||
});
|
||||
|
||||
function renderDragOverlays() {
|
||||
let selectedIndices = selectedGroupIds.map((groupId) =>
|
||||
activeGroups.findIndex((group) => group.id === groupId)
|
||||
);
|
||||
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
|
||||
);
|
||||
|
||||
selectedIndices = selectedIndices.slice(0, 5);
|
||||
|
||||
let coords = selectedIndices.map(
|
||||
(_, index) => new Vector2(5 * index, 5 * index)
|
||||
);
|
||||
|
||||
// Reverse so the first element is rendered on top
|
||||
selectedIndices = selectedIndices.reverse();
|
||||
coords = coords.reverse();
|
||||
|
||||
const selectedGroups = selectedIndices.map((index) => activeGroups[index]);
|
||||
|
||||
return selectedGroups.map((group, index) => (
|
||||
<DragOverlay dropAnimation={null} key={group.id}>
|
||||
<div
|
||||
style={{
|
||||
transform: `translate(${coords[index].x}%, ${coords[index].y}%)`,
|
||||
}}
|
||||
>
|
||||
<animated.div style={dragBounce}>
|
||||
{renderTile(group)}
|
||||
{index === selectedIndices.length - 1 &&
|
||||
selectedGroupIds.length > 1 && (
|
||||
<Badge
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
transform: "translate(25%, -25%)",
|
||||
}}
|
||||
>
|
||||
{selectedGroupIds.length}
|
||||
</Badge>
|
||||
)}
|
||||
</animated.div>
|
||||
</div>
|
||||
</DragOverlay>
|
||||
));
|
||||
}
|
||||
|
||||
return createPortal(dragId && renderDragOverlays(), document.body);
|
||||
}
|
||||
|
||||
export default SortableTilesDragOverlay;
|
@ -16,17 +16,14 @@ function Tile({
|
||||
children,
|
||||
}) {
|
||||
const [ref, inView] = useInView({ triggerOnce: true });
|
||||
|
||||
return (
|
||||
<Flex
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "0",
|
||||
paddingTop: "100%",
|
||||
borderRadius: "4px",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
overflow: "hidden",
|
||||
userSelect: "none",
|
||||
}}
|
||||
@ -128,7 +125,7 @@ function Tile({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import TokenTileGroup from "./TokenTileGroup";
|
||||
import TokenHiddenBadge from "./TokenHiddenBadge";
|
||||
|
||||
import SortableTiles from "../tile/SortableTiles";
|
||||
import SortableTilesDragOverlay from "../tile/SortableTilesDragOverlay";
|
||||
|
||||
import { getGroupItems } from "../../helpers/group";
|
||||
|
||||
@ -61,7 +62,12 @@ function TokenTiles({ tokensById, onTokenEdit, subgroup }) {
|
||||
}
|
||||
}
|
||||
|
||||
return <SortableTiles renderTile={renderTile} subgroup={subgroup} />;
|
||||
return (
|
||||
<>
|
||||
<SortableTiles renderTile={renderTile} subgroup={subgroup} />
|
||||
<SortableTilesDragOverlay renderTile={renderTile} subgroup={subgroup} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default TokenTiles;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useState, useContext } from "react";
|
||||
import React, { useState, useContext, useEffect } from "react";
|
||||
import {
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
@ -16,7 +16,9 @@ import { moveGroupsInto, moveGroups, ungroup } from "../helpers/group";
|
||||
|
||||
import usePreventSelect from "../hooks/usePreventSelect";
|
||||
|
||||
const TileDragContext = React.createContext();
|
||||
const TileDragIdContext = React.createContext();
|
||||
const TileOverGroupIdContext = React.createContext();
|
||||
const TileDragCursorContext = React.createContext();
|
||||
|
||||
export const BASE_SORTABLE_ID = "__base__";
|
||||
export const GROUP_SORTABLE_ID = "__group__";
|
||||
@ -61,7 +63,7 @@ export function TileDragProvider({
|
||||
} = useGroup();
|
||||
|
||||
const mouseSensor = useSensor(MouseSensor, {
|
||||
activationConstraint: { distance: 5 },
|
||||
activationConstraint: { distance: 3 },
|
||||
});
|
||||
const touchSensor = useSensor(TouchSensor, {
|
||||
activationConstraint: { delay: 250, tolerance: 5 },
|
||||
@ -70,16 +72,23 @@ export function TileDragProvider({
|
||||
|
||||
const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor);
|
||||
|
||||
const [dragId, setDragId] = useState();
|
||||
const [overId, setOverId] = useState();
|
||||
const [dragId, setDragId] = useState(null);
|
||||
const [overId, setOverId] = useState(null);
|
||||
const [dragCursor, setDragCursor] = useState("pointer");
|
||||
|
||||
const [preventSelect, resumeSelect] = usePreventSelect();
|
||||
|
||||
const [overGroupId, setOverGroupId] = useState(null);
|
||||
useEffect(() => {
|
||||
setOverGroupId(
|
||||
(overId && overId.startsWith(GROUP_ID_PREFIX) && overId.slice(9)) || null
|
||||
);
|
||||
}, [overId]);
|
||||
|
||||
function handleDragStart(event) {
|
||||
const { active, over } = event;
|
||||
setDragId(active.id);
|
||||
setOverId(over?.id);
|
||||
setOverId(over?.id || null);
|
||||
if (!selectedGroupIds.includes(active.id)) {
|
||||
onGroupSelect(active.id);
|
||||
}
|
||||
@ -93,7 +102,7 @@ export function TileDragProvider({
|
||||
function handleDragOver(event) {
|
||||
const { over } = event;
|
||||
|
||||
setOverId(over?.id);
|
||||
setOverId(over?.id || null);
|
||||
if (over) {
|
||||
if (
|
||||
over.id.startsWith(UNGROUP_ID) ||
|
||||
@ -111,8 +120,8 @@ export function TileDragProvider({
|
||||
function handleDragEnd(event) {
|
||||
const { active, over, overlayNodeClientRect } = event;
|
||||
|
||||
setDragId();
|
||||
setOverId();
|
||||
setDragId(null);
|
||||
setOverId(null);
|
||||
setDragCursor("pointer");
|
||||
if (active && over && active.id !== over.id) {
|
||||
let selectedIndices = selectedGroupIds.map((groupId) =>
|
||||
@ -165,8 +174,8 @@ export function TileDragProvider({
|
||||
}
|
||||
|
||||
function handleDragCancel(event) {
|
||||
setDragId();
|
||||
setOverId();
|
||||
setDragId(null);
|
||||
setOverId(null);
|
||||
setDragCursor("pointer");
|
||||
|
||||
resumeSelect();
|
||||
@ -210,8 +219,6 @@ export function TileDragProvider({
|
||||
return closestCenter(otherRects, rect);
|
||||
}
|
||||
|
||||
const value = { dragId, overId, dragCursor };
|
||||
|
||||
return (
|
||||
<DragContext
|
||||
onDragStart={handleDragStart}
|
||||
@ -221,19 +228,37 @@ export function TileDragProvider({
|
||||
sensors={sensors}
|
||||
collisionDetection={customCollisionDetection}
|
||||
>
|
||||
<TileDragContext.Provider value={value}>
|
||||
{children}
|
||||
</TileDragContext.Provider>
|
||||
<TileDragIdContext.Provider value={dragId}>
|
||||
<TileOverGroupIdContext.Provider value={overGroupId}>
|
||||
<TileDragCursorContext.Provider value={dragCursor}>
|
||||
{children}
|
||||
</TileDragCursorContext.Provider>
|
||||
</TileOverGroupIdContext.Provider>
|
||||
</TileDragIdContext.Provider>
|
||||
</DragContext>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTileDrag() {
|
||||
const context = useContext(TileDragContext);
|
||||
export function useTileDragId() {
|
||||
const context = useContext(TileDragIdContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useTileDrag must be used within a TileDragProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export default TileDragContext;
|
||||
export function useTileOverGroupId() {
|
||||
const context = useContext(TileOverGroupIdContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useTileDrag must be used within a TileDragProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useTileDragCursor() {
|
||||
const context = useContext(TileDragCursorContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useTileDrag must be used within a TileDragProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user