SelectedItemsPane.jsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import React from "react";
  2. import styled from "styled-components";
  3. import { toast } from "react-toastify";
  4. import { useTranslation } from "react-i18next";
  5. import { insideClass, hasClass, smallUid } from "../../utils";
  6. import EditItemButton from "./EditItemButton";
  7. import {
  8. useAvailableActions,
  9. useSelectionBox,
  10. useSelectedItems,
  11. useBoardState,
  12. useItemActions,
  13. } from "react-sync-board";
  14. import useGameItemActions from "../../gameComponents/useGameItemActions";
  15. const ActionPane = styled.div.attrs(({ top, left, height }) => {
  16. if (top < 120) {
  17. return {
  18. style: {
  19. transform: `translate(${left}px, ${top + height + 5}px)`,
  20. },
  21. };
  22. }
  23. return {
  24. style: {
  25. transform: `translate(${left}px, ${top - 60}px)`,
  26. },
  27. };
  28. })`
  29. top: 0;
  30. left: 0;
  31. user-select: none;
  32. touch-action: none;
  33. position: absolute;
  34. display: flex;
  35. background-color: var(--color-blueGrey);
  36. justify-content: center;
  37. align-items: center;
  38. border-radius: 4px;
  39. padding: 0.1em 0.5em;
  40. transition: opacity 100ms;
  41. opacity: ${({ hide }) => (hide ? 0 : 0.9)};
  42. box-shadow: 2px 2px 10px 0.3px rgba(0, 0, 0, 0.5);
  43. &:hover{
  44. opacity: 1;
  45. }
  46. & button{
  47. margin 0 4px;
  48. padding: 0em;
  49. height: 50px
  50. }
  51. & .button.icon-only{
  52. padding: 0em;
  53. opacity: 0.5;
  54. }
  55. & button.icon-only:hover{
  56. opacity: 1;
  57. }
  58. & .count{
  59. color: var(--color-secondary);
  60. display: flex;
  61. flex-direction: column;
  62. align-items: center;
  63. line-height: 0.8em;
  64. }
  65. & .number{
  66. font-size: 1.5em;
  67. line-height: 1em;
  68. }
  69. `;
  70. const SelectedItemsPane = ({ hideMenu = false }) => {
  71. const { findElementUnderPointer } = useItemActions();
  72. const { actionMap } = useGameItemActions();
  73. const { availableActions } = useAvailableActions();
  74. const [showEdit, setShowEdit] = React.useState(false);
  75. const { t } = useTranslation();
  76. const selectedItems = useSelectedItems();
  77. const boardState = useBoardState();
  78. const selectionBox = useSelectionBox();
  79. const parsedAvailableActions = React.useMemo(
  80. () =>
  81. availableActions
  82. .map(({ name, args }) => {
  83. const action = { ...actionMap[name] };
  84. action.action = action.action(args);
  85. action.label = action.label(args);
  86. action.uid = smallUid();
  87. return action;
  88. })
  89. .filter(({ multiple }) => !multiple || selectedItems.length > 1),
  90. [actionMap, availableActions, selectedItems]
  91. );
  92. const showEditButton =
  93. !boardState.selecting &&
  94. !(boardState.zooming || boardState.panning || boardState.movingItems);
  95. React.useEffect(() => {
  96. const onKeyUp = (e) => {
  97. // Block shortcut if we are typing in a textarea or input
  98. if (["INPUT", "TEXTAREA"].includes(e.target.tagName)) return;
  99. for (let i = 0; i < parsedAvailableActions.length; i++) {
  100. const { shortcut, action, edit: whileEdit } = parsedAvailableActions[i];
  101. if (shortcut === e.key && showEdit === !!whileEdit) {
  102. action();
  103. break;
  104. }
  105. }
  106. };
  107. document.addEventListener("keyup", onKeyUp);
  108. return () => {
  109. document.removeEventListener("keyup", onKeyUp);
  110. };
  111. }, [actionMap, availableActions, parsedAvailableActions, showEdit]);
  112. const onDblClick = React.useCallback(
  113. async (e) => {
  114. const foundElement = await findElementUnderPointer(e, {
  115. returnLocked: true,
  116. });
  117. // We dblclick outside of an element
  118. if (!foundElement) return;
  119. if (hasClass(foundElement, "locked")) {
  120. toast.info(t("Long click to select locked elements"));
  121. return;
  122. }
  123. // Ignore action disabled on dblclick
  124. const filteredActions = parsedAvailableActions.filter(
  125. ({ disableDblclick }) => !disableDblclick
  126. );
  127. if (e.ctrlKey && filteredActions.length > 1) {
  128. // Use second action
  129. filteredActions[1].action();
  130. } else if (filteredActions.length > 0) {
  131. filteredActions[0].action();
  132. }
  133. },
  134. [findElementUnderPointer, parsedAvailableActions, t]
  135. );
  136. React.useEffect(() => {
  137. document.addEventListener("dblclick", onDblClick);
  138. return () => {
  139. document.removeEventListener("dblclick", onDblClick);
  140. };
  141. }, [onDblClick]);
  142. if (hideMenu || selectedItems.length === 0) {
  143. return null;
  144. }
  145. return (
  146. <ActionPane
  147. {...selectionBox}
  148. hide={boardState.zooming || boardState.panning || boardState.movingItems}
  149. >
  150. {(selectedItems.length > 1 || boardState.selecting) && (
  151. <div className="count">
  152. <span className="number">{selectedItems.length}</span>
  153. <span>{t("Items")}</span>
  154. </div>
  155. )}
  156. {!boardState.selecting &&
  157. parsedAvailableActions.map(
  158. ({ label, action, edit: onlyEdit, shortcut, icon, uid }) => {
  159. if (onlyEdit && !showEdit) return null;
  160. return (
  161. <button
  162. className="button clear icon-only"
  163. key={uid}
  164. onClick={() => action()}
  165. title={label + (shortcut ? ` (${shortcut})` : "")}
  166. >
  167. <img
  168. src={icon}
  169. style={{ width: "32px", height: "32px" }}
  170. alt={label}
  171. />
  172. </button>
  173. );
  174. }
  175. )}
  176. {showEditButton && (
  177. <EditItemButton showEdit={showEdit} setShowEdit={setShowEdit} />
  178. )}
  179. </ActionPane>
  180. );
  181. };
  182. export default SelectedItemsPane;