Show selected items while selecting
This commit is contained in:
parent
7eda777b73
commit
2653008a67
5 changed files with 153 additions and 84 deletions
|
@ -21,7 +21,12 @@
|
||||||
"parser": "babel-eslint",
|
"parser": "babel-eslint",
|
||||||
"rules": {
|
"rules": {
|
||||||
"semi": "error",
|
"semi": "error",
|
||||||
"react/prop-types": "off"
|
"react/prop-types": "off",
|
||||||
|
"react-hooks/exhaustive-deps": [
|
||||||
|
"warn", {
|
||||||
|
"additionalHooks": "useRecoilCallback"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"plugins": ["react-hooks"],
|
"plugins": ["react-hooks"],
|
||||||
"extends": [
|
"extends": [
|
||||||
|
|
|
@ -1,74 +1,89 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useRecoilValue } from "recoil";
|
|
||||||
import { PanZoomRotateAtom } from "./PanZoomRotate";
|
import { PanZoomRotateAtom } from "./PanZoomRotate";
|
||||||
import { selectedItemsAtom } from "./Selector";
|
import { selectedItemsAtom } from "./Selector";
|
||||||
import { useItems } from "./Items";
|
import { useItems } from "./Items";
|
||||||
import { useRecoilState } from "recoil";
|
import { useSetRecoilState, useRecoilCallback } from "recoil";
|
||||||
import { insideClass, hasClass } from "../../utils";
|
import { insideClass, hasClass } from "../../utils";
|
||||||
|
|
||||||
const ActionPane = ({ children }) => {
|
const ActionPane = ({ children }) => {
|
||||||
const panZoomRotate = useRecoilValue(PanZoomRotateAtom);
|
|
||||||
const { putItemsOnTop, moveItems } = useItems();
|
const { putItemsOnTop, moveItems } = useItems();
|
||||||
const [selectedItems, setSelectedItems] = useRecoilState(selectedItemsAtom);
|
const setSelectedItems = useSetRecoilState(selectedItemsAtom);
|
||||||
const wrapperRef = React.useRef(null);
|
const wrapperRef = React.useRef(null);
|
||||||
const actionRef = React.useRef({});
|
const actionRef = React.useRef({});
|
||||||
|
|
||||||
const onMouseDown = (e) => {
|
const onMouseDown = useRecoilCallback(
|
||||||
if (e.button === 0 && !e.altKey) {
|
async (snapshot, e) => {
|
||||||
// Allow text selection instead of moving
|
if (e.button === 0 && !e.altKey) {
|
||||||
if (["INPUT", "TEXTAREA"].includes(e.target.tagName)) return;
|
// Allow text selection instead of moving
|
||||||
|
if (["INPUT", "TEXTAREA"].includes(e.target.tagName)) return;
|
||||||
|
|
||||||
const { top, left } = e.currentTarget.getBoundingClientRect();
|
const { top, left } = e.currentTarget.getBoundingClientRect();
|
||||||
const point = {
|
const { clientX, clientY, ctrlKey, metaKey } = e;
|
||||||
x: (e.clientX - left) / panZoomRotate.scale,
|
|
||||||
y: (e.clientY - top) / panZoomRotate.scale,
|
|
||||||
};
|
|
||||||
|
|
||||||
const foundElement = insideClass(e.target, "item");
|
const foundElement = insideClass(e.target, "item");
|
||||||
|
|
||||||
if (foundElement && !hasClass(foundElement, "locked")) {
|
const panZoomRotate = await snapshot.getPromise(PanZoomRotateAtom);
|
||||||
let selectedItemsToMove = selectedItems;
|
const selectedItems = await snapshot.getPromise(selectedItemsAtom);
|
||||||
|
|
||||||
if (!selectedItems.includes(foundElement.id)) {
|
const point = {
|
||||||
if (e.ctrlKey || e.metaKey) {
|
x: (clientX - left) / panZoomRotate.scale,
|
||||||
setSelectedItems((prev) => [...prev, foundElement.id]);
|
y: (clientY - top) / panZoomRotate.scale,
|
||||||
selectedItemsToMove = [...selectedItems, foundElement.id];
|
};
|
||||||
} else {
|
|
||||||
setSelectedItems([foundElement.id]);
|
if (foundElement && !hasClass(foundElement, "locked")) {
|
||||||
selectedItemsToMove = [foundElement.id];
|
let selectedItemsToMove = selectedItems;
|
||||||
|
|
||||||
|
if (!selectedItems.includes(foundElement.id)) {
|
||||||
|
if (ctrlKey || metaKey) {
|
||||||
|
setSelectedItems((prev) => [...prev, foundElement.id]);
|
||||||
|
selectedItemsToMove = [...selectedItems, foundElement.id];
|
||||||
|
} else {
|
||||||
|
setSelectedItems([foundElement.id]);
|
||||||
|
selectedItemsToMove = [foundElement.id];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
putItemsOnTop(selectedItemsToMove);
|
||||||
|
|
||||||
|
actionRef.current.moving = true;
|
||||||
|
actionRef.current.startX = point.x;
|
||||||
|
actionRef.current.startY = point.y;
|
||||||
|
actionRef.current.prevX = point.x;
|
||||||
|
actionRef.current.prevY = point.y;
|
||||||
|
actionRef.current.moving = true;
|
||||||
|
actionRef.current.itemId = foundElement.id;
|
||||||
|
|
||||||
|
wrapperRef.current.style.cursor = "move";
|
||||||
}
|
}
|
||||||
|
|
||||||
putItemsOnTop(selectedItemsToMove);
|
|
||||||
|
|
||||||
actionRef.current.moving = true;
|
|
||||||
actionRef.current.startX = point.x;
|
|
||||||
actionRef.current.startY = point.y;
|
|
||||||
actionRef.current.prevX = point.x;
|
|
||||||
actionRef.current.prevY = point.y;
|
|
||||||
actionRef.current.moving = true;
|
|
||||||
actionRef.current.itemId = foundElement.id;
|
|
||||||
|
|
||||||
wrapperRef.current.style.cursor = "move";
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
[putItemsOnTop, setSelectedItems]
|
||||||
|
);
|
||||||
|
|
||||||
const onMouseMouve = (e) => {
|
const onMouseMouve = useRecoilCallback(
|
||||||
if (actionRef.current.moving) {
|
async (snapshot, e) => {
|
||||||
const { top, left } = e.currentTarget.getBoundingClientRect();
|
if (actionRef.current.moving) {
|
||||||
const currentX = (e.clientX - left) / panZoomRotate.scale;
|
const { top, left } = e.currentTarget.getBoundingClientRect();
|
||||||
const currentY = (e.clientY - top) / panZoomRotate.scale;
|
const { clientX, clientY } = e;
|
||||||
moveItems(selectedItems, {
|
|
||||||
x: currentX - actionRef.current.prevX,
|
const panZoomRotate = await snapshot.getPromise(PanZoomRotateAtom);
|
||||||
y: currentY - actionRef.current.prevY,
|
const selectedItems = await snapshot.getPromise(selectedItemsAtom);
|
||||||
});
|
|
||||||
actionRef.current.prevX = currentX;
|
const currentX = (clientX - left) / panZoomRotate.scale;
|
||||||
actionRef.current.prevY = currentY;
|
const currentY = (clientY - top) / panZoomRotate.scale;
|
||||||
e.preventDefault();
|
|
||||||
}
|
moveItems(selectedItems, {
|
||||||
};
|
x: currentX - actionRef.current.prevX,
|
||||||
|
y: currentY - actionRef.current.prevY,
|
||||||
|
});
|
||||||
|
|
||||||
|
actionRef.current.prevX = currentX;
|
||||||
|
actionRef.current.prevY = currentY;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[moveItems]
|
||||||
|
);
|
||||||
|
|
||||||
const onMouseUp = React.useCallback(() => {
|
const onMouseUp = React.useCallback(() => {
|
||||||
if (actionRef.current.moving) {
|
if (actionRef.current.moving) {
|
||||||
|
|
|
@ -24,9 +24,6 @@ const ItemWrapper = styled.div.attrs(({ rotation, loaded, locked }) => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
})`
|
})`
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
transition: transform 200ms;
|
transition: transform 200ms;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
@ -70,8 +67,7 @@ const ItemWrapper = styled.div.attrs(({ rotation, loaded, locked }) => {
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Item = ({ setState, state }) => {
|
const Item = ({ setState, state, isSelected }) => {
|
||||||
const selectedItems = useRecoilValue(selectedItemsAtom);
|
|
||||||
const itemRef = React.useRef(null);
|
const itemRef = React.useRef(null);
|
||||||
const sizeRef = React.useRef({});
|
const sizeRef = React.useRef({});
|
||||||
const [unlock, setUnlock] = React.useState(false);
|
const [unlock, setUnlock] = React.useState(false);
|
||||||
|
@ -160,12 +156,15 @@ const Item = ({ setState, state }) => {
|
||||||
style={{
|
style={{
|
||||||
transform: `translate(${state.x}px, ${state.y}px)`,
|
transform: `translate(${state.x}px, ${state.y}px)`,
|
||||||
display: "inline-block",
|
display: "inline-block",
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ItemWrapper
|
<ItemWrapper
|
||||||
rotation={rotation}
|
rotation={rotation}
|
||||||
locked={state.locked && !unlock}
|
locked={state.locked && !unlock}
|
||||||
selected={selectedItems.includes(state.id)}
|
selected={isSelected}
|
||||||
ref={itemRef}
|
ref={itemRef}
|
||||||
layer={state.layer}
|
layer={state.layer}
|
||||||
loaded={loaded}
|
loaded={loaded}
|
||||||
|
@ -177,15 +176,29 @@ const Item = ({ setState, state }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(
|
const MemoizedItem = memo(
|
||||||
Item,
|
Item,
|
||||||
(
|
(
|
||||||
{ state: prevState, setState: prevSetState },
|
{ state: prevState, setState: prevSetState, isSelected: prevIsSelected },
|
||||||
{ state: nextState, setState: nextSetState }
|
{ state: nextState, setState: nextSetState, isSelected: nextIsSelected }
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
JSON.stringify(prevState) === JSON.stringify(nextState) &&
|
JSON.stringify(prevState) === JSON.stringify(nextState) &&
|
||||||
prevSetState === nextSetState
|
prevSetState === nextSetState &&
|
||||||
|
prevIsSelected === nextIsSelected
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const BaseItem = ({ setState, state }) => {
|
||||||
|
const selectedItems = useRecoilValue(selectedItemsAtom);
|
||||||
|
return (
|
||||||
|
<MemoizedItem
|
||||||
|
state={state}
|
||||||
|
setState={setState}
|
||||||
|
isSelected={selectedItems.includes(state.id)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BaseItem;
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React from "react";
|
||||||
import {
|
import {
|
||||||
atom,
|
atom,
|
||||||
useRecoilValue,
|
useRecoilValue,
|
||||||
useRecoilState,
|
useSetRecoilState,
|
||||||
useRecoilCallback,
|
useRecoilCallback,
|
||||||
} from "recoil";
|
} from "recoil";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
@ -31,21 +31,19 @@ const findSelected = (items, rect) => {
|
||||||
|
|
||||||
const SelectorZone = styled.div.attrs(({ top, left, height, width }) => ({
|
const SelectorZone = styled.div.attrs(({ top, left, height, width }) => ({
|
||||||
style: {
|
style: {
|
||||||
top: `${top}px`,
|
transform: `translate(${left}px, ${top}px)`,
|
||||||
left: `${left}px`,
|
|
||||||
height: `${height}px`,
|
height: `${height}px`,
|
||||||
width: `${width}px`,
|
width: `${width}px`,
|
||||||
},
|
},
|
||||||
}))`
|
}))`
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: #ff000050;
|
background-color: hsla(0, 40%, 50%, 10%);
|
||||||
|
border: 2px solid hsl(0, 55%, 40%);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Selector = ({ children }) => {
|
const Selector = ({ children }) => {
|
||||||
const panZoomRotate = useRecoilValue(PanZoomRotateAtom);
|
const setSelected = useSetRecoilState(selectedItemsAtom);
|
||||||
|
|
||||||
const [selected, setSelected] = useRecoilState(selectedItemsAtom);
|
|
||||||
const [selector, setSelector] = React.useState({});
|
const [selector, setSelector] = React.useState({});
|
||||||
|
|
||||||
const wrapperRef = React.useRef(null);
|
const wrapperRef = React.useRef(null);
|
||||||
|
@ -60,15 +58,18 @@ const Selector = ({ children }) => {
|
||||||
setSelected([]);
|
setSelected([]);
|
||||||
}, [config, setSelected]);
|
}, [config, setSelected]);
|
||||||
|
|
||||||
const onMouseDown = (e) => {
|
const onMouseDown = useRecoilCallback(async (snapshot, e) => {
|
||||||
if (
|
if (
|
||||||
e.button === 0 &&
|
e.button === 0 &&
|
||||||
!e.altKey &&
|
!e.altKey &&
|
||||||
(!insideClass(e.target, "item") || insideClass(e.target, "locked"))
|
(!insideClass(e.target, "item") || insideClass(e.target, "locked"))
|
||||||
) {
|
) {
|
||||||
const { top, left } = e.currentTarget.getBoundingClientRect();
|
const { top, left } = e.currentTarget.getBoundingClientRect();
|
||||||
const displayX = (e.clientX - left) / panZoomRotate.scale;
|
const { clientX, clientY } = e;
|
||||||
const displayY = (e.clientY - top) / panZoomRotate.scale;
|
|
||||||
|
const panZoomRotate = await snapshot.getPromise(PanZoomRotateAtom);
|
||||||
|
const displayX = (clientX - left) / panZoomRotate.scale;
|
||||||
|
const displayY = (clientY - top) / panZoomRotate.scale;
|
||||||
|
|
||||||
stateRef.current.moving = true;
|
stateRef.current.moving = true;
|
||||||
stateRef.current.startX = displayX;
|
stateRef.current.startX = displayX;
|
||||||
|
@ -77,15 +78,40 @@ const Selector = ({ children }) => {
|
||||||
setSelector({ ...stateRef.current });
|
setSelector({ ...stateRef.current });
|
||||||
wrapperRef.current.style.cursor = "crosshair";
|
wrapperRef.current.style.cursor = "crosshair";
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const onMouseMouve = (e) => {
|
const throttledSetSelected = useRecoilCallback(
|
||||||
|
async (snapshot, selector) => {
|
||||||
|
if (stateRef.current.moving) {
|
||||||
|
const itemList = await snapshot.getPromise(ItemListAtom);
|
||||||
|
|
||||||
|
const selected = findSelected(itemList, selector).map(({ id }) => id);
|
||||||
|
setSelected((prevSelected) => {
|
||||||
|
if (JSON.stringify(prevSelected) !== JSON.stringify(selected)) {
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
return prevSelected;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setSelected]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset selection on game loading
|
||||||
|
React.useEffect(() => {
|
||||||
|
throttledSetSelected(selector);
|
||||||
|
}, [selector, throttledSetSelected]);
|
||||||
|
|
||||||
|
const onMouseMove = useRecoilCallback(async (snapshot, e) => {
|
||||||
if (stateRef.current.moving) {
|
if (stateRef.current.moving) {
|
||||||
if (selected.length) setSelected([]);
|
|
||||||
|
|
||||||
const { top, left } = e.currentTarget.getBoundingClientRect();
|
const { top, left } = e.currentTarget.getBoundingClientRect();
|
||||||
const currentX = (e.clientX - left) / panZoomRotate.scale;
|
const { clientX, clientY } = e;
|
||||||
const currentY = (e.clientY - top) / panZoomRotate.scale;
|
e.preventDefault();
|
||||||
|
|
||||||
|
const panZoomRotate = await snapshot.getPromise(PanZoomRotateAtom);
|
||||||
|
|
||||||
|
const currentX = (clientX - left) / panZoomRotate.scale;
|
||||||
|
const currentY = (clientY - top) / panZoomRotate.scale;
|
||||||
|
|
||||||
if (currentX > stateRef.current.startX) {
|
if (currentX > stateRef.current.startX) {
|
||||||
stateRef.current.left = stateRef.current.startX;
|
stateRef.current.left = stateRef.current.startX;
|
||||||
|
@ -103,9 +129,8 @@ const Selector = ({ children }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelector({ ...stateRef.current });
|
setSelector({ ...stateRef.current });
|
||||||
e.preventDefault();
|
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const onMouseUp = useRecoilCallback(
|
const onMouseUp = useRecoilCallback(
|
||||||
async (snapshot) => {
|
async (snapshot) => {
|
||||||
|
@ -133,9 +158,9 @@ const Selector = ({ children }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
onMouseMove={onMouseMouve}
|
onMouseMove={onMouseMove}
|
||||||
onMouseEnter={onMouseMouve}
|
onMouseEnter={onMouseMove}
|
||||||
onMouseOut={onMouseMouve}
|
onMouseOut={onMouseMove}
|
||||||
ref={wrapperRef}
|
ref={wrapperRef}
|
||||||
>
|
>
|
||||||
{selector.moving && (
|
{selector.moving && (
|
||||||
|
|
|
@ -40,9 +40,19 @@ const genGame = () => {
|
||||||
x: 420,
|
x: 420,
|
||||||
y: 400,
|
y: 400,
|
||||||
});
|
});
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
type: "rect",
|
type: "rect",
|
||||||
color: "#00D022",
|
color: "#00D022",
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
type: "rect",
|
||||||
|
color: "#22D022",
|
||||||
width: 80,
|
width: 80,
|
||||||
height: 80,
|
height: 80,
|
||||||
x: 10,
|
x: 10,
|
||||||
|
@ -87,6 +97,7 @@ const genGame = () => {
|
||||||
layer: -1,
|
layer: -1,
|
||||||
width: 500,
|
width: 500,
|
||||||
height: 300,
|
height: 300,
|
||||||
|
locked: true,
|
||||||
x: 200,
|
x: 200,
|
||||||
y: 600,
|
y: 600,
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue