SelectedItemsPane.jsx 5.0 KB

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