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 MapTileGroup from "./MapTileGroup";
|
||||||
|
|
||||||
import SortableTiles from "../tile/SortableTiles";
|
import SortableTiles from "../tile/SortableTiles";
|
||||||
|
import SortableTilesDragOverlay from "../tile/SortableTilesDragOverlay";
|
||||||
|
|
||||||
import { getGroupItems } from "../../helpers/group";
|
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;
|
export default MapTiles;
|
||||||
|
@ -1,26 +1,24 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { createPortal } from "react-dom";
|
|
||||||
import { DragOverlay } from "@dnd-kit/core";
|
|
||||||
import { SortableContext } from "@dnd-kit/sortable";
|
import { SortableContext } from "@dnd-kit/sortable";
|
||||||
import { animated, useSpring, config } from "react-spring";
|
|
||||||
import { Badge } from "theme-ui";
|
|
||||||
|
|
||||||
import { moveGroupsInto } from "../../helpers/group";
|
import { moveGroupsInto } from "../../helpers/group";
|
||||||
import { keyBy } from "../../helpers/shared";
|
import { keyBy } from "../../helpers/shared";
|
||||||
import Vector2 from "../../helpers/Vector2";
|
|
||||||
|
|
||||||
import SortableTile from "./SortableTile";
|
import SortableTile from "./SortableTile";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useTileDrag,
|
useTileDragId,
|
||||||
|
useTileDragCursor,
|
||||||
|
useTileOverGroupId,
|
||||||
BASE_SORTABLE_ID,
|
BASE_SORTABLE_ID,
|
||||||
GROUP_SORTABLE_ID,
|
GROUP_SORTABLE_ID,
|
||||||
GROUP_ID_PREFIX,
|
|
||||||
} from "../../contexts/TileDragContext";
|
} from "../../contexts/TileDragContext";
|
||||||
import { useGroup } from "../../contexts/GroupContext";
|
import { useGroup } from "../../contexts/GroupContext";
|
||||||
|
|
||||||
function SortableTiles({ renderTile, subgroup }) {
|
function SortableTiles({ renderTile, subgroup }) {
|
||||||
const { dragId, overId, dragCursor } = useTileDrag();
|
const dragId = useTileDragId();
|
||||||
|
const dragCursor = useTileDragCursor();
|
||||||
|
const overGroupId = useTileOverGroupId();
|
||||||
const {
|
const {
|
||||||
groups,
|
groups,
|
||||||
selectedGroupIds: allSelectedIds,
|
selectedGroupIds: allSelectedIds,
|
||||||
@ -46,15 +44,6 @@ function SortableTiles({ renderTile, subgroup }) {
|
|||||||
const disableSorting = (openGroupId && !subgroup) || filter;
|
const disableSorting = (openGroupId && !subgroup) || filter;
|
||||||
const disableGrouping = subgroup || disableSorting || 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) {
|
function renderSortableGroup(group, selectedGroups) {
|
||||||
if (overGroupId === group.id && dragId && group.id !== dragId) {
|
if (overGroupId === group.id && dragId && group.id !== dragId) {
|
||||||
// If dragging over a group render a preview of that group
|
// If dragging over a group render a preview of that group
|
||||||
@ -68,57 +57,6 @@ function SortableTiles({ renderTile, subgroup }) {
|
|||||||
return renderTile(group);
|
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() {
|
function renderTiles() {
|
||||||
const groupsByIds = keyBy(activeGroups, "id");
|
const groupsByIds = keyBy(activeGroups, "id");
|
||||||
const selectedGroupIdsSet = new Set(selectedGroupIds);
|
const selectedGroupIdsSet = new Set(selectedGroupIds);
|
||||||
@ -156,7 +94,6 @@ function SortableTiles({ renderTile, subgroup }) {
|
|||||||
return (
|
return (
|
||||||
<SortableContext items={activeGroups} id={sortableId}>
|
<SortableContext items={activeGroups} id={sortableId}>
|
||||||
{renderTiles()}
|
{renderTiles()}
|
||||||
{createPortal(dragId && renderDragOverlays(), document.body)}
|
|
||||||
</SortableContext>
|
</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,
|
children,
|
||||||
}) {
|
}) {
|
||||||
const [ref, inView] = useInView({ triggerOnce: true });
|
const [ref, inView] = useInView({ triggerOnce: true });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "0",
|
height: "0",
|
||||||
paddingTop: "100%",
|
paddingTop: "100%",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
}}
|
}}
|
||||||
@ -128,7 +125,7 @@ function Tile({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import TokenTileGroup from "./TokenTileGroup";
|
|||||||
import TokenHiddenBadge from "./TokenHiddenBadge";
|
import TokenHiddenBadge from "./TokenHiddenBadge";
|
||||||
|
|
||||||
import SortableTiles from "../tile/SortableTiles";
|
import SortableTiles from "../tile/SortableTiles";
|
||||||
|
import SortableTilesDragOverlay from "../tile/SortableTilesDragOverlay";
|
||||||
|
|
||||||
import { getGroupItems } from "../../helpers/group";
|
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;
|
export default TokenTiles;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useContext } from "react";
|
import React, { useState, useContext, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
MouseSensor,
|
MouseSensor,
|
||||||
TouchSensor,
|
TouchSensor,
|
||||||
@ -16,7 +16,9 @@ import { moveGroupsInto, moveGroups, ungroup } from "../helpers/group";
|
|||||||
|
|
||||||
import usePreventSelect from "../hooks/usePreventSelect";
|
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 BASE_SORTABLE_ID = "__base__";
|
||||||
export const GROUP_SORTABLE_ID = "__group__";
|
export const GROUP_SORTABLE_ID = "__group__";
|
||||||
@ -61,7 +63,7 @@ export function TileDragProvider({
|
|||||||
} = useGroup();
|
} = useGroup();
|
||||||
|
|
||||||
const mouseSensor = useSensor(MouseSensor, {
|
const mouseSensor = useSensor(MouseSensor, {
|
||||||
activationConstraint: { distance: 5 },
|
activationConstraint: { distance: 3 },
|
||||||
});
|
});
|
||||||
const touchSensor = useSensor(TouchSensor, {
|
const touchSensor = useSensor(TouchSensor, {
|
||||||
activationConstraint: { delay: 250, tolerance: 5 },
|
activationConstraint: { delay: 250, tolerance: 5 },
|
||||||
@ -70,16 +72,23 @@ export function TileDragProvider({
|
|||||||
|
|
||||||
const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor);
|
const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor);
|
||||||
|
|
||||||
const [dragId, setDragId] = useState();
|
const [dragId, setDragId] = useState(null);
|
||||||
const [overId, setOverId] = useState();
|
const [overId, setOverId] = useState(null);
|
||||||
const [dragCursor, setDragCursor] = useState("pointer");
|
const [dragCursor, setDragCursor] = useState("pointer");
|
||||||
|
|
||||||
const [preventSelect, resumeSelect] = usePreventSelect();
|
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) {
|
function handleDragStart(event) {
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
setDragId(active.id);
|
setDragId(active.id);
|
||||||
setOverId(over?.id);
|
setOverId(over?.id || null);
|
||||||
if (!selectedGroupIds.includes(active.id)) {
|
if (!selectedGroupIds.includes(active.id)) {
|
||||||
onGroupSelect(active.id);
|
onGroupSelect(active.id);
|
||||||
}
|
}
|
||||||
@ -93,7 +102,7 @@ export function TileDragProvider({
|
|||||||
function handleDragOver(event) {
|
function handleDragOver(event) {
|
||||||
const { over } = event;
|
const { over } = event;
|
||||||
|
|
||||||
setOverId(over?.id);
|
setOverId(over?.id || null);
|
||||||
if (over) {
|
if (over) {
|
||||||
if (
|
if (
|
||||||
over.id.startsWith(UNGROUP_ID) ||
|
over.id.startsWith(UNGROUP_ID) ||
|
||||||
@ -111,8 +120,8 @@ export function TileDragProvider({
|
|||||||
function handleDragEnd(event) {
|
function handleDragEnd(event) {
|
||||||
const { active, over, overlayNodeClientRect } = event;
|
const { active, over, overlayNodeClientRect } = event;
|
||||||
|
|
||||||
setDragId();
|
setDragId(null);
|
||||||
setOverId();
|
setOverId(null);
|
||||||
setDragCursor("pointer");
|
setDragCursor("pointer");
|
||||||
if (active && over && active.id !== over.id) {
|
if (active && over && active.id !== over.id) {
|
||||||
let selectedIndices = selectedGroupIds.map((groupId) =>
|
let selectedIndices = selectedGroupIds.map((groupId) =>
|
||||||
@ -165,8 +174,8 @@ export function TileDragProvider({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleDragCancel(event) {
|
function handleDragCancel(event) {
|
||||||
setDragId();
|
setDragId(null);
|
||||||
setOverId();
|
setOverId(null);
|
||||||
setDragCursor("pointer");
|
setDragCursor("pointer");
|
||||||
|
|
||||||
resumeSelect();
|
resumeSelect();
|
||||||
@ -210,8 +219,6 @@ export function TileDragProvider({
|
|||||||
return closestCenter(otherRects, rect);
|
return closestCenter(otherRects, rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = { dragId, overId, dragCursor };
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragContext
|
<DragContext
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
@ -221,19 +228,37 @@ export function TileDragProvider({
|
|||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
collisionDetection={customCollisionDetection}
|
collisionDetection={customCollisionDetection}
|
||||||
>
|
>
|
||||||
<TileDragContext.Provider value={value}>
|
<TileDragIdContext.Provider value={dragId}>
|
||||||
{children}
|
<TileOverGroupIdContext.Provider value={overGroupId}>
|
||||||
</TileDragContext.Provider>
|
<TileDragCursorContext.Provider value={dragCursor}>
|
||||||
|
{children}
|
||||||
|
</TileDragCursorContext.Provider>
|
||||||
|
</TileOverGroupIdContext.Provider>
|
||||||
|
</TileDragIdContext.Provider>
|
||||||
</DragContext>
|
</DragContext>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTileDrag() {
|
export function useTileDragId() {
|
||||||
const context = useContext(TileDragContext);
|
const context = useContext(TileDragIdContext);
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
throw new Error("useTileDrag must be used within a TileDragProvider");
|
throw new Error("useTileDrag must be used within a TileDragProvider");
|
||||||
}
|
}
|
||||||
return context;
|
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