SelectedItemsPane.jsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  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 } from "../utils";
  6. import SidePanel from "../../ui/SidePanel";
  7. import ItemFormFactory from "./ItemFormFactory";
  8. import {
  9. useAvailableActions,
  10. useSelectionBox,
  11. useSelectedItems,
  12. useBoardState,
  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 CardContent = styled.div.attrs(() => ({ className: "content" }))`
  71. display: flex;
  72. flex-direction: column;
  73. padding: 0.5em;
  74. `;
  75. const SelectedItemsPane = ({ hideMenu = false, ItemFormComponent }) => {
  76. const { actionMap } = useGameItemActions();
  77. const { availableActions } = useAvailableActions();
  78. const [showEdit, setShowEdit] = React.useState(false);
  79. const { t } = useTranslation();
  80. const selectedItems = useSelectedItems();
  81. const boardState = useBoardState();
  82. const selectionBox = useSelectionBox();
  83. React.useEffect(() => {
  84. const onKeyUp = (e) => {
  85. // Block shortcut if we are typing in a textarea or input
  86. if (["INPUT", "TEXTAREA"].includes(e.target.tagName)) return;
  87. Object.keys(actionMap).forEach((key) => {
  88. const { shortcut, action, edit: whileEdit } = actionMap[key];
  89. if (
  90. availableActions.includes(key) &&
  91. e.key === shortcut &&
  92. showEdit === !!whileEdit
  93. ) {
  94. // here
  95. action();
  96. }
  97. });
  98. };
  99. document.addEventListener("keyup", onKeyUp);
  100. return () => {
  101. document.removeEventListener("keyup", onKeyUp);
  102. };
  103. }, [actionMap, availableActions, showEdit]);
  104. const onDblClick = React.useCallback(
  105. (e) => {
  106. const foundElement = insideClass(e.target, "item");
  107. // We dblclick outside of an element
  108. if (!foundElement) return;
  109. if (hasClass(foundElement, "locked")) {
  110. toast.info(t("Long click to select locked elements"));
  111. return;
  112. }
  113. const filteredActions = availableActions.filter(
  114. (action) => !actionMap[action].disableDblclick
  115. );
  116. if (e.ctrlKey && filteredActions.length > 1) {
  117. // Use second action
  118. // here
  119. actionMap[filteredActions[1]].action();
  120. } else if (filteredActions.length > 0) {
  121. // here
  122. actionMap[filteredActions[0]].action();
  123. }
  124. },
  125. [actionMap, availableActions, t]
  126. );
  127. React.useEffect(() => {
  128. document.addEventListener("dblclick", onDblClick);
  129. return () => {
  130. document.removeEventListener("dblclick", onDblClick);
  131. };
  132. }, [onDblClick]);
  133. if (hideMenu || selectedItems.length === 0) {
  134. return null;
  135. }
  136. let title = "";
  137. if (selectedItems.length === 1) {
  138. title = t("Edit item");
  139. }
  140. if (selectedItems.length > 1) {
  141. title = t("Edit all items");
  142. }
  143. return (
  144. <>
  145. <SidePanel
  146. key={selectedItems[0]}
  147. open={showEdit && !boardState.selecting}
  148. onClose={() => {
  149. setShowEdit(false);
  150. }}
  151. title={title}
  152. width="25%"
  153. >
  154. <CardContent>
  155. <ItemFormFactory ItemFormComponent={ItemFormComponent} />
  156. </CardContent>
  157. </SidePanel>
  158. {selectedItems.length && !hideMenu && (
  159. <ActionPane
  160. {...selectionBox}
  161. hide={
  162. boardState.zooming || boardState.panning || boardState.movingItems
  163. }
  164. >
  165. {(selectedItems.length > 1 || boardState.selecting) && (
  166. <div className="count">
  167. <span className="number">{selectedItems.length}</span>
  168. <span>{t("Items")}</span>
  169. </div>
  170. )}
  171. {!boardState.selecting &&
  172. availableActions.map((action) => {
  173. const {
  174. label,
  175. action: handler,
  176. multiple,
  177. edit: onlyEdit,
  178. shortcut,
  179. icon,
  180. } = actionMap[action];
  181. if (multiple && selectedItems.length < 2) return null;
  182. if (onlyEdit && !showEdit) return null;
  183. return (
  184. <button
  185. className="button clear icon-only"
  186. key={action}
  187. // here
  188. onClick={() => handler()}
  189. title={label + (shortcut ? ` (${shortcut})` : "")}
  190. >
  191. <img
  192. src={icon}
  193. style={{ width: "32px", height: "32px" }}
  194. alt={label}
  195. />
  196. </button>
  197. );
  198. })}
  199. {!boardState.selecting && (
  200. <button
  201. className="button clear icon-only"
  202. onClick={() => setShowEdit((prev) => !prev)}
  203. title={t("Edit")}
  204. >
  205. {!showEdit && (
  206. <img
  207. src="https://icongr.am/feather/edit.svg?size=32&color=ffffff"
  208. alt={t("Edit")}
  209. />
  210. )}
  211. {showEdit && (
  212. <img
  213. src="https://icongr.am/feather/edit.svg?size=32&color=db5034"
  214. alt={t("Edit")}
  215. />
  216. )}
  217. </button>
  218. )}
  219. </ActionPane>
  220. )}
  221. </>
  222. );
  223. };
  224. export default SelectedItemsPane;