ItemLibrary.jsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. import React, { memo } from "react";
  2. import { useTranslation } from "react-i18next";
  3. import { nanoid } from "nanoid";
  4. import styled from "styled-components";
  5. import { useRecoilCallback } from "recoil";
  6. import { debounce } from "lodash";
  7. import { useItems } from "../components/board/Items";
  8. import useToggle from "./hooks/useToggle";
  9. import { search } from "./utils";
  10. import Chevron from "./ui/Chevron";
  11. import { PanZoomRotateAtom } from "./board";
  12. const StyledItemList = styled.ul`
  13. display: flex;
  14. flex-flow: row wrap;
  15. list-style: none;
  16. margin: 0;
  17. padding: 0;
  18. & li.group {
  19. background-color: rgba(0, 0, 0, 0.1);
  20. padding: 0 0.5em;
  21. flex-basis: 100%;
  22. }
  23. overflow: visible;
  24. `;
  25. const StyledItem = styled.li`
  26. display: block;
  27. padding: 0.5em;
  28. margin: 0.2em;
  29. cursor: pointer;
  30. opacity: 0.7;
  31. &:hover {
  32. opacity: 1;
  33. }
  34. & > div {
  35. display: flex;
  36. flex-direction: column;
  37. align-items: center;
  38. pointer-events: none;
  39. max-width: 80px;
  40. & > span {
  41. margin-top: 0.2em;
  42. text-align: center;
  43. max-width: 80px;
  44. white-space: nowrap;
  45. overflow: hidden;
  46. text-overflow: ellipsis;
  47. padding: 0.2em 0.5em;
  48. }
  49. }
  50. &:hover > div > span {
  51. z-index: 2;
  52. max-width: none;
  53. overflow: visible;
  54. background-color: #222;
  55. box-shadow: 0px 3px 6px #00000029;
  56. }
  57. `;
  58. const size = 60;
  59. const NewItem = memo(({ type, template, component: Component, name }) => {
  60. const { pushItem } = useItems();
  61. const addItem = useRecoilCallback(
  62. ({ snapshot }) => async () => {
  63. const { centerX, centerY } = await snapshot.getPromise(PanZoomRotateAtom);
  64. pushItem({
  65. ...template,
  66. x: centerX,
  67. y: centerY,
  68. id: nanoid(),
  69. type,
  70. });
  71. },
  72. [pushItem, template, type]
  73. );
  74. return (
  75. <>
  76. <StyledItem onClick={addItem}>
  77. <div>
  78. <Component {...template} width={size} height={size} size={size} />
  79. <span>{name}</span>
  80. </div>
  81. </StyledItem>
  82. </>
  83. );
  84. });
  85. NewItem.displayName = "NewItem";
  86. const SubItemList = ({ name, items }) => {
  87. const { t } = useTranslation();
  88. const [open, toggleOpen] = useToggle(false);
  89. const { pushItem } = useItems();
  90. const addItems = useRecoilCallback(
  91. ({ snapshot }) => async (items) => {
  92. const { centerX, centerY } = await snapshot.getPromise(PanZoomRotateAtom);
  93. items.forEach(({ template }, index) => {
  94. pushItem({
  95. ...template,
  96. x: centerX + 2 * index,
  97. y: centerY + 2 * index,
  98. id: nanoid(),
  99. });
  100. });
  101. },
  102. [pushItem]
  103. );
  104. return (
  105. <>
  106. <h3 onClick={toggleOpen} style={{ cursor: "pointer" }}>
  107. {open ? (
  108. <Chevron orientation="bottom" color="#8c8c8c" />
  109. ) : (
  110. <Chevron color="#8c8c8c" />
  111. )}{" "}
  112. {name}{" "}
  113. <span
  114. style={{ fontSize: "0.6em" }}
  115. onClick={(e) => {
  116. e.preventDefault();
  117. e.stopPropagation();
  118. addItems(items);
  119. }}
  120. >
  121. [{t("Add all")}]
  122. </span>
  123. </h3>
  124. {open && <ItemList items={items} />}
  125. </>
  126. );
  127. };
  128. const ItemList = ({ items }) => {
  129. return (
  130. <StyledItemList>
  131. {items.map((node) => {
  132. if (node.type) {
  133. return <NewItem {...node} key={node.uid} />;
  134. } else {
  135. // it's a group
  136. return (
  137. <li key={`group_${node.name}`} className="group">
  138. <SubItemList {...node} />
  139. </li>
  140. );
  141. }
  142. })}
  143. </StyledItemList>
  144. );
  145. };
  146. const MemoizedItemList = memo(ItemList);
  147. const filterItems = (filter, nodes) => {
  148. return nodes.reduce((acc, node) => {
  149. if (node.type) {
  150. if (search(filter, node.name)) {
  151. acc.push(node);
  152. }
  153. return acc;
  154. } else {
  155. const filteredItems = filterItems(filter, node.items);
  156. if (filteredItems.length) {
  157. acc.push({ ...node, items: filteredItems });
  158. }
  159. return acc;
  160. }
  161. }, []);
  162. };
  163. const ItemLibrary = ({ items }) => {
  164. const { t } = useTranslation();
  165. const [filter, setFilter] = React.useState("");
  166. const [filteredItems, setFilteredItems] = React.useState(items);
  167. // eslint-disable-next-line react-hooks/exhaustive-deps
  168. const debouncedFilterItems = React.useCallback(
  169. debounce((filter, items) => {
  170. setFilteredItems(filterItems(filter, items));
  171. }, 500),
  172. []
  173. );
  174. React.useEffect(() => {
  175. debouncedFilterItems(filter, items);
  176. }, [debouncedFilterItems, filter, items]);
  177. return (
  178. <>
  179. <input
  180. onChange={(e) => setFilter(e.target.value)}
  181. style={{ marginBottom: "1em" }}
  182. placeholder={t("Search...")}
  183. />
  184. <MemoizedItemList items={filteredItems} />
  185. </>
  186. );
  187. };
  188. export default memo(ItemLibrary);