SelectedItemsPane.jsx 5.2 KB

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