Add shortcut to actions
This commit is contained in:
parent
cf4e68ca34
commit
03dab656aa
3 changed files with 113 additions and 85 deletions
|
@ -29,7 +29,7 @@ const Wrapper = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledImage = styled.img`
|
const FrontImage = styled.img`
|
||||||
transition: transform 200ms;
|
transition: transform 200ms;
|
||||||
transform: rotateY(${({ visible }) => (visible ? 0 : 180)}deg);
|
transform: rotateY(${({ visible }) => (visible ? 0 : 180)}deg);
|
||||||
backface-visibility: hidden;
|
backface-visibility: hidden;
|
||||||
|
@ -37,7 +37,7 @@ const StyledImage = styled.img`
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const BackImage = styled(StyledImage)`
|
const BackImage = styled(FrontImage)`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -86,7 +86,7 @@ const Image = ({
|
||||||
return {
|
return {
|
||||||
...prevItem,
|
...prevItem,
|
||||||
unflippedFor: currentUser.id,
|
unflippedFor: currentUser.id,
|
||||||
flipped: false,
|
flipped: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -102,8 +102,7 @@ const Image = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
const flippedForMe =
|
const flippedForMe =
|
||||||
backContent &&
|
backContent && flipped && unflippedFor !== currentUser.id;
|
||||||
(flipped || (unflippedFor && unflippedFor !== currentUser.id));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper onDoubleClick={onDblClick}>
|
<Wrapper onDoubleClick={onDblClick}>
|
||||||
|
@ -118,7 +117,7 @@ const Image = ({
|
||||||
)}
|
)}
|
||||||
{flippedForMe && backText && <Label>{backText}</Label>}
|
{flippedForMe && backText && <Label>{backText}</Label>}
|
||||||
{(!flippedForMe || !backText) && text && <Label>{text}</Label>}
|
{(!flippedForMe || !backText) && text && <Label>{text}</Label>}
|
||||||
<StyledImage
|
<FrontImage
|
||||||
visible={!flippedForMe}
|
visible={!flippedForMe}
|
||||||
src={content}
|
src={content}
|
||||||
alt=""
|
alt=""
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { useRecoilValue } from "recoil";
|
||||||
import { selectedItemsAtom } from "../../Selector";
|
import { selectedItemsAtom } from "../../Selector";
|
||||||
import debounce from "lodash.debounce";
|
import debounce from "lodash.debounce";
|
||||||
|
|
||||||
|
import styled, { css } from "styled-components";
|
||||||
|
|
||||||
import Rect from "./Rect";
|
import Rect from "./Rect";
|
||||||
import Round from "./Round";
|
import Round from "./Round";
|
||||||
import Image from "./Image";
|
import Image from "./Image";
|
||||||
|
@ -30,21 +32,47 @@ const getComponent = (type) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ItemWrapper = styled.div.attrs(({ x, y, rotation }) => ({
|
||||||
|
className: "item",
|
||||||
|
style: { left: `${x}px`, top: `${y}px`, transform: `rotate(${rotation}deg)` },
|
||||||
|
}))`
|
||||||
|
position: absolute;
|
||||||
|
display: inline-block;
|
||||||
|
transition: transform 200ms;
|
||||||
|
z-index: ${({ layer }) => (layer || 0) + 3};
|
||||||
|
${({ selected }) =>
|
||||||
|
selected
|
||||||
|
? css`
|
||||||
|
border: 2px dashed #ff0000a0;
|
||||||
|
padding: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
`
|
||||||
|
: css`
|
||||||
|
padding: 4px;
|
||||||
|
`}
|
||||||
|
${({ locked }) =>
|
||||||
|
locked &&
|
||||||
|
css`
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
const Item = ({ setState, state }) => {
|
const Item = ({ setState, state }) => {
|
||||||
const selectedItems = useRecoilValue(selectedItemsAtom);
|
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);
|
||||||
|
|
||||||
// Allow to operate on locked item if ctrl is pressed
|
// Allow to operate on locked item if key is pressed
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const onKeyDown = (e) => {
|
const onKeyDown = (e) => {
|
||||||
if (e.key === "Control") {
|
if (e.key === "u" || e.key === "l") {
|
||||||
setUnlock(true);
|
setUnlock(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const onKeyUp = (e) => {
|
const onKeyUp = (e) => {
|
||||||
if (e.key === "Control") {
|
if (e.key === "u" || e.key === "l") {
|
||||||
setUnlock(false);
|
setUnlock(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -65,6 +93,7 @@ const Item = ({ setState, state }) => {
|
||||||
[setState, state.id]
|
[setState, state.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Update actual dimension. Usefull when image with own dimensions.
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const actualSizeCallback = React.useCallback(
|
const actualSizeCallback = React.useCallback(
|
||||||
debounce((entries) => {
|
debounce((entries) => {
|
||||||
|
@ -102,43 +131,18 @@ const Item = ({ setState, state }) => {
|
||||||
};
|
};
|
||||||
}, [actualSizeCallback]);
|
}, [actualSizeCallback]);
|
||||||
|
|
||||||
const extraStyle = selectedItems.includes(state.id)
|
return (
|
||||||
? { border: "2px dashed #ff0000a0", padding: "2px" }
|
<ItemWrapper
|
||||||
: { padding: "4px" };
|
x={state.x}
|
||||||
|
y={state.y}
|
||||||
const content = (
|
rotation={rotation}
|
||||||
<div
|
locked={state.locked && !unlock}
|
||||||
style={{
|
selected={selectedItems.includes(state.id)}
|
||||||
left: state.x + "px",
|
|
||||||
top: state.y + "px",
|
|
||||||
position: "absolute",
|
|
||||||
display: "inline-block",
|
|
||||||
transform: `rotate(${rotation}deg)`,
|
|
||||||
transition: "transform 200ms",
|
|
||||||
zIndex: (state.layer || 0) + 3,
|
|
||||||
...extraStyle,
|
|
||||||
}}
|
|
||||||
className="item"
|
|
||||||
ref={itemRef}
|
ref={itemRef}
|
||||||
id={state.id}
|
id={state.id}
|
||||||
>
|
>
|
||||||
<Component {...state} x={0} y={0} setState={updateState} />
|
<Component {...state} x={0} y={0} setState={updateState} />
|
||||||
</div>
|
</ItemWrapper>
|
||||||
);
|
|
||||||
|
|
||||||
if (!state.locked || unlock) {
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
pointerEvents: "none",
|
|
||||||
userSelect: "none",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,11 +5,15 @@ import { useRecoilValue } from "recoil";
|
||||||
import { useItems } from "./Board/Items";
|
import { useItems } from "./Board/Items";
|
||||||
import { selectedItemsAtom } from "../components/Board/Selector";
|
import { selectedItemsAtom } from "../components/Board/Selector";
|
||||||
|
|
||||||
|
import { insideClass } from "../utils";
|
||||||
|
|
||||||
import ItemFormFactory from "./Board/Items/Item/forms/ItemFormFactory";
|
import ItemFormFactory from "./Board/Items/Item/forms/ItemFormFactory";
|
||||||
|
|
||||||
import { confirmAlert } from "react-confirm-alert";
|
import { confirmAlert } from "react-confirm-alert";
|
||||||
import "react-confirm-alert/src/react-confirm-alert.css";
|
import "react-confirm-alert/src/react-confirm-alert.css";
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useUsers } from "./users";
|
||||||
|
|
||||||
const SelectedPane = styled.div.attrs(() => ({ className: "casrd" }))`
|
const SelectedPane = styled.div.attrs(() => ({ className: "casrd" }))`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -32,6 +36,8 @@ export const SelectedItems = ({ edit }) => {
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { currentUser } = useUsers();
|
||||||
|
|
||||||
const selectedItems = useRecoilValue(selectedItemsAtom);
|
const selectedItems = useRecoilValue(selectedItemsAtom);
|
||||||
|
|
||||||
const selectedItemList = React.useMemo(() => {
|
const selectedItemList = React.useMemo(() => {
|
||||||
|
@ -66,33 +72,21 @@ export const SelectedItems = ({ edit }) => {
|
||||||
});
|
});
|
||||||
}, [selectedItemList, selectedItems, batchUpdateItems]);
|
}, [selectedItemList, selectedItems, batchUpdateItems]);
|
||||||
|
|
||||||
const flip = React.useCallback(() => {
|
|
||||||
batchUpdateItems(selectedItems, (item) => ({
|
|
||||||
...item,
|
|
||||||
flipped: true,
|
|
||||||
}));
|
|
||||||
}, [selectedItems, batchUpdateItems]);
|
|
||||||
|
|
||||||
const tap = React.useCallback(() => {
|
|
||||||
batchUpdateItems(selectedItems, (item) => ({
|
|
||||||
...item,
|
|
||||||
rotation: 90,
|
|
||||||
}));
|
|
||||||
}, [selectedItems, batchUpdateItems]);
|
|
||||||
|
|
||||||
const untap = React.useCallback(() => {
|
|
||||||
batchUpdateItems(selectedItems, (item) => ({
|
|
||||||
...item,
|
|
||||||
rotation: 0,
|
|
||||||
}));
|
|
||||||
}, [selectedItems, batchUpdateItems]);
|
|
||||||
|
|
||||||
const toggleTap = React.useCallback(() => {
|
const toggleTap = React.useCallback(() => {
|
||||||
|
const tappedCount = selectedItemList.filter(
|
||||||
|
({ rotation }) => rotation === 90
|
||||||
|
).length;
|
||||||
|
|
||||||
|
let untap = false;
|
||||||
|
if (tappedCount > selectedItems.length / 2) {
|
||||||
|
untap = true;
|
||||||
|
}
|
||||||
|
|
||||||
batchUpdateItems(selectedItems, (item) => ({
|
batchUpdateItems(selectedItems, (item) => ({
|
||||||
...item,
|
...item,
|
||||||
rotation: item.rotation === 90 ? 0 : 90,
|
rotation: untap ? 0 : 90,
|
||||||
}));
|
}));
|
||||||
}, [selectedItems, batchUpdateItems]);
|
}, [selectedItems, batchUpdateItems, selectedItemList]);
|
||||||
|
|
||||||
const toggleLock = React.useCallback(() => {
|
const toggleLock = React.useCallback(() => {
|
||||||
batchUpdateItems(selectedItems, (item) => ({
|
batchUpdateItems(selectedItems, (item) => ({
|
||||||
|
@ -102,30 +96,63 @@ export const SelectedItems = ({ edit }) => {
|
||||||
}, [selectedItems, batchUpdateItems]);
|
}, [selectedItems, batchUpdateItems]);
|
||||||
|
|
||||||
const toggleFlip = React.useCallback(() => {
|
const toggleFlip = React.useCallback(() => {
|
||||||
batchUpdateItems(selectedItems, (item) => ({
|
const flippedCount = selectedItemList.filter(({ flipped }) => flipped)
|
||||||
...item,
|
.length;
|
||||||
flipped: !item.flipped,
|
|
||||||
}));
|
|
||||||
}, [selectedItems, batchUpdateItems]);
|
|
||||||
|
|
||||||
const unflip = React.useCallback(() => {
|
let flip = true;
|
||||||
|
if (flippedCount > selectedItems.length / 2) {
|
||||||
|
flip = false;
|
||||||
|
}
|
||||||
batchUpdateItems(selectedItems, (item) => ({
|
batchUpdateItems(selectedItems, (item) => ({
|
||||||
...item,
|
...item,
|
||||||
flipped: false,
|
flipped: flip,
|
||||||
|
unflippedFor: undefined,
|
||||||
}));
|
}));
|
||||||
}, [selectedItems, batchUpdateItems]);
|
}, [selectedItemList, selectedItems, batchUpdateItems]);
|
||||||
|
|
||||||
|
const revealForMe = React.useCallback(() => {
|
||||||
|
batchUpdateItems(selectedItems, (item) => ({
|
||||||
|
...item,
|
||||||
|
flipped: true,
|
||||||
|
unflippedFor: currentUser.id,
|
||||||
|
}));
|
||||||
|
}, [batchUpdateItems, selectedItems, currentUser.id]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const onKeyUp = (e) => {
|
||||||
|
if (e.key === "f") {
|
||||||
|
if (insideClass(e.target, "item")) return;
|
||||||
|
toggleFlip();
|
||||||
|
}
|
||||||
|
if (e.key === "t") {
|
||||||
|
if (insideClass(e.target, "item")) return;
|
||||||
|
toggleTap();
|
||||||
|
}
|
||||||
|
if (e.key === "o") {
|
||||||
|
if (insideClass(e.target, "item")) return;
|
||||||
|
revealForMe();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("keyup", onKeyUp);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keyup", onKeyUp);
|
||||||
|
};
|
||||||
|
}, [revealForMe, toggleFlip, toggleTap]);
|
||||||
|
|
||||||
|
const onSubmitHandler = React.useCallback(
|
||||||
|
(formValues) => {
|
||||||
|
updateItem(formValues.id, (item) => ({
|
||||||
|
...item,
|
||||||
|
...formValues,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[updateItem]
|
||||||
|
);
|
||||||
|
|
||||||
if (selectedItemList.length === 0) {
|
if (selectedItemList.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmitHandler = (formValues) => {
|
|
||||||
updateItem(formValues.id, (item) => ({
|
|
||||||
...item,
|
|
||||||
...formValues,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRemove = () => {
|
const onRemove = () => {
|
||||||
confirmAlert({
|
confirmAlert({
|
||||||
title: t("Confirmation"),
|
title: t("Confirmation"),
|
||||||
|
@ -195,12 +222,10 @@ export const SelectedItems = ({ edit }) => {
|
||||||
<h3>{t("items selected", { count: selectedItems.length })}</h3>
|
<h3>{t("items selected", { count: selectedItems.length })}</h3>
|
||||||
</header>
|
</header>
|
||||||
<section className="content">
|
<section className="content">
|
||||||
<button onClick={shuffleSelectedItems}>{t("Shuffle")}</button>
|
<button onClick={toggleFlip}>{t("Reveal") + "/" + t("Hide")}</button>
|
||||||
|
<button onClick={toggleTap}>{t("Tap") + "/" + t("Untap")}</button>
|
||||||
<button onClick={align}>{t("Stack")}</button>
|
<button onClick={align}>{t("Stack")}</button>
|
||||||
<button onClick={flip}>{t("Hide")}</button>
|
<button onClick={shuffleSelectedItems}>{t("Shuffle")}</button>
|
||||||
<button onClick={unflip}>{t("Reveal")}</button>
|
|
||||||
<button onClick={tap}>{t("Tap")}</button>
|
|
||||||
<button onClick={untap}>{t("Untap")}</button>
|
|
||||||
{edit && <button onClick={onRemove}>{t("Remove all")}</button>}
|
{edit && <button onClick={onRemove}>{t("Remove all")}</button>}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue