diff --git a/frontend/src/lib/components/DragList/index.ts b/frontend/src/lib/components/DragList/index.ts deleted file mode 100644 index 67b3c2119..000000000 --- a/frontend/src/lib/components/DragList/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { DragList } from "./dragList"; -export { DragListItem } from "./dragListItem"; -export { DragListGroup } from "./dragListGroup"; - -export type { DragListProps } from "./dragList"; -export type { DragListItemProps } from "./dragListItem"; -export type { DragListGroupProps } from "./dragListGroup"; diff --git a/frontend/src/lib/components/SortableList/index.ts b/frontend/src/lib/components/SortableList/index.ts new file mode 100644 index 000000000..d1b7d12b3 --- /dev/null +++ b/frontend/src/lib/components/SortableList/index.ts @@ -0,0 +1,7 @@ +export { SortableList } from "./sortableList"; +export { SortableListItem } from "./sortableListItem"; +export { SortableListGroup } from "./sortableListGroup"; + +export type { SortableListProps } from "./sortableList"; +export type { SortableListItemProps } from "./sortableListItem"; +export type { SortableListGroupProps } from "./sortableListGroup"; diff --git a/frontend/src/lib/components/DragList/dragList.tsx b/frontend/src/lib/components/SortableList/sortableList.tsx similarity index 86% rename from frontend/src/lib/components/DragList/dragList.tsx rename to frontend/src/lib/components/SortableList/sortableList.tsx index b093b8a5c..993be0d85 100644 --- a/frontend/src/lib/components/DragList/dragList.tsx +++ b/frontend/src/lib/components/SortableList/sortableList.tsx @@ -7,8 +7,8 @@ import { Vec2, point2Distance } from "@lib/utils/vec2"; import { isEqual } from "lodash"; -import { DragListGroupProps } from "./dragListGroup"; -import { DragListItemProps } from "./dragListItem"; +import { SortableListGroupProps } from "./sortableListGroup"; +import { SortableListItemProps } from "./sortableListItem"; export enum HoveredArea { TOP = "top", @@ -17,27 +17,27 @@ export enum HoveredArea { CENTER = "center", } -export type DragListContextType = { +export type SortableListContextType = { draggedElementId: string | null; hoveredElementId: string | null; hoveredArea: HoveredArea | null; dragPosition: Vec2 | null; }; -export const DragListContext = React.createContext({ +export const SortableListContext = React.createContext({ draggedElementId: null, hoveredElementId: null, hoveredArea: null, dragPosition: null, }); -export type DragListProps = { +export type SortableListProps = { contentWhenEmpty?: React.ReactNode; - children: React.ReactElement[]; + children: React.ReactElement[]; onItemMove?: (itemId: string, originId: string | null, destinationId: string | null, position: number) => void; }; -function assertTargetIsDragListItemAndExtractProps( +function assertTargetIsSortableListItemAndExtractProps( target: EventTarget | null ): { element: HTMLElement; id: string; parentId: string | null } | null { if (!target) { @@ -49,37 +49,37 @@ function assertTargetIsDragListItemAndExtractProps( return null; } - const dragListItemIndicator = element.closest(".drag-list-element-indicator"); - if (!dragListItemIndicator) { + const sortableListItemIndicator = element.closest(".sortable-list-element-indicator"); + if (!sortableListItemIndicator) { return null; } - const dragListElement = element.closest(".drag-list-element"); - if (!dragListElement) { + const sortableListElement = element.closest(".sortable-list-element"); + if (!sortableListElement) { return null; } - if (!(dragListElement instanceof HTMLElement)) { + if (!(sortableListElement instanceof HTMLElement)) { return null; } - const id = dragListElement.dataset.itemId; + const id = sortableListElement.dataset.itemId; if (!id) { return null; } if ( - dragListElement.parentElement && - dragListElement.parentElement instanceof HTMLElement && - dragListElement.parentElement.classList.contains("drag-list-group") + sortableListElement.parentElement && + sortableListElement.parentElement instanceof HTMLElement && + sortableListElement.parentElement.classList.contains("sortable-list-group") ) { - const parentId = dragListElement.parentElement.dataset.itemId; + const parentId = sortableListElement.parentElement.dataset.itemId; if (parentId) { - return { element: dragListElement, id, parentId }; + return { element: sortableListElement, id, parentId }; } } - return { element: dragListElement, id, parentId: null }; + return { element: sortableListElement, id, parentId: null }; } enum ItemType { @@ -92,9 +92,9 @@ type HoveredItemIdAndArea = { area: HoveredArea; }; -const ELEMENT_TOP_AND_CENTER_AREA_SIZE_IN_PX = 10; +const ELEMENT_TOP_AND_CENTER_AREA_SIZE_IN_PERCENT = 50; -export function DragList(props: DragListProps): React.ReactNode { +export function SortableList(props: SortableListProps): React.ReactNode { const { onItemMove } = props; const [isDragging, setIsDragging] = React.useState(false); @@ -103,7 +103,7 @@ export function DragList(props: DragListProps): React.ReactNode { const [dragPosition, setDragPosition] = React.useState({ x: 0, y: 0 }); const [currentScrollPosition, setCurrentScrollPosition] = React.useState(0); const [prevChildren, setPrevChildren] = React.useState< - React.ReactElement[] + React.ReactElement[] >(props.children); const listDivRef = React.useRef(null); @@ -150,13 +150,13 @@ export function DragList(props: DragListProps): React.ReactNode { return; } - const dragListItemProps = assertTargetIsDragListItemAndExtractProps(target); - if (!dragListItemProps) { + const sortableListItemProps = assertTargetIsSortableListItemAndExtractProps(target); + if (!sortableListItemProps) { return; } - const element = dragListItemProps.element; - draggedElement = dragListItemProps; + const element = sortableListItemProps.element; + draggedElement = sortableListItemProps; pointerDownPosition = { x: e.clientX, y: e.clientY }; draggingActive = false; @@ -225,12 +225,12 @@ export function DragList(props: DragListProps): React.ReactNode { const parentElement = parent ?? currentListDivRef; for (const child of parentElement.children) { - if (child instanceof HTMLElement && child.classList.contains("drag-list-item")) { + if (child instanceof HTMLElement && child.classList.contains("sortable-list-item")) { items.push(child); } - if (child instanceof HTMLElement && child.classList.contains("drag-list-group")) { + if (child instanceof HTMLElement && child.classList.contains("sortable-list-group")) { items.push(child); - const content = child.querySelector(".drag-list-group-content"); + const content = child.querySelector(".sortable-list-group-content"); if (content && content instanceof HTMLElement) { items.push(...getDragElementsRecursively(content)); } @@ -245,7 +245,7 @@ export function DragList(props: DragListProps): React.ReactNode { if (rectContainsPoint(item.getBoundingClientRect(), { x: e.clientX, y: e.clientY })) { const type = getItemType(item); if (type === ItemType.CONTAINER) { - const content = item.querySelector(".drag-list-group-content"); + const content = item.querySelector(".sortable-list-group-content"); if ( content && rectContainsPoint(content.getBoundingClientRect(), { x: e.clientX, y: e.clientY }) @@ -262,9 +262,9 @@ export function DragList(props: DragListProps): React.ReactNode { } function getItemType(item: HTMLElement): ItemType | null { - if (item.classList.contains("drag-list-item")) { + if (item.classList.contains("sortable-list-item")) { return ItemType.ITEM; - } else if (item.classList.contains("drag-list-group")) { + } else if (item.classList.contains("sortable-list-group")) { return ItemType.CONTAINER; } return null; @@ -273,20 +273,20 @@ export function DragList(props: DragListProps): React.ReactNode { function getHoveredAreaOfItem(item: HTMLElement, e: PointerEvent): HoveredArea { const rect = item.getBoundingClientRect(); const topAreaTop = rect.top; - const topAreaBottom = rect.top + ELEMENT_TOP_AND_CENTER_AREA_SIZE_IN_PX; + const topAreaBottom = rect.top + (ELEMENT_TOP_AND_CENTER_AREA_SIZE_IN_PERCENT / 100) * rect.height; if (e.clientY >= topAreaTop && e.clientY <= topAreaBottom) { return HoveredArea.TOP; } - const bottomAreaTop = rect.bottom - ELEMENT_TOP_AND_CENTER_AREA_SIZE_IN_PX; + const bottomAreaTop = rect.bottom - (ELEMENT_TOP_AND_CENTER_AREA_SIZE_IN_PERCENT / 100) * rect.height; const bottomAreaBottom = rect.bottom; if (e.clientY >= bottomAreaTop && e.clientY <= bottomAreaBottom) { return HoveredArea.BOTTOM; } - const headerElement = item.querySelector(".drag-list-item-header"); + const headerElement = item.querySelector(".sortable-list-item-header"); if (!headerElement) { return HoveredArea.CENTER; } @@ -300,7 +300,7 @@ export function DragList(props: DragListProps): React.ReactNode { } function getItemParentGroupId(item: HTMLElement): string | null { - const group = item.parentElement?.closest(".drag-list-group"); + const group = item.parentElement?.closest(".sortable-list-group"); if (!group || !(group instanceof HTMLElement)) { return null; } @@ -308,7 +308,7 @@ export function DragList(props: DragListProps): React.ReactNode { } function getItemPositionInGroup(item: HTMLElement): number { - let group = item.parentElement?.closest(".drag-list-group-content"); + let group = item.parentElement?.closest(".sortable-list-group-content"); if (!group || !(group instanceof HTMLElement)) { group = currentListDivRef; } @@ -466,7 +466,7 @@ export function DragList(props: DragListProps): React.ReactNode { if (typeof child.type === "string") { continue; } - if (child.type.name === "DragListItem") { + if (child.type.name === "SortableListItem") { children.push( React.cloneElement(child, { key: child.props.id, @@ -485,7 +485,7 @@ export function DragList(props: DragListProps): React.ReactNode { return (
-
)} - + ); } diff --git a/frontend/src/lib/components/DragList/dragListDropIndicator.tsx b/frontend/src/lib/components/SortableList/sortableListDropIndicator.tsx similarity index 78% rename from frontend/src/lib/components/DragList/dragListDropIndicator.tsx rename to frontend/src/lib/components/SortableList/sortableListDropIndicator.tsx index 7f6f72d08..905f3118f 100644 --- a/frontend/src/lib/components/DragList/dragListDropIndicator.tsx +++ b/frontend/src/lib/components/SortableList/sortableListDropIndicator.tsx @@ -1,4 +1,4 @@ -export function DragListDropIndicator() { +export function SortableListDropIndicator() { return (
diff --git a/frontend/src/lib/components/DragList/dragListGroup.tsx b/frontend/src/lib/components/SortableList/sortableListGroup.tsx similarity index 69% rename from frontend/src/lib/components/DragList/dragListGroup.tsx rename to frontend/src/lib/components/SortableList/sortableListGroup.tsx index 67c519ed0..ce13697da 100644 --- a/frontend/src/lib/components/DragList/dragListGroup.tsx +++ b/frontend/src/lib/components/SortableList/sortableListGroup.tsx @@ -5,31 +5,31 @@ import { createPortal } from "@lib/utils/createPortal"; import { resolveClassNames } from "@lib/utils/resolveClassNames"; import { DragIndicator, ExpandLess, ExpandMore } from "@mui/icons-material"; -import { DragListContext, HoveredArea } from "./dragList"; -import { DragListDropIndicator } from "./dragListDropIndicator"; -import { DragListItemProps } from "./dragListItem"; +import { HoveredArea, SortableListContext } from "./sortableList"; +import { SortableListDropIndicator } from "./sortableListDropIndicator"; +import { SortableListItemProps } from "./sortableListItem"; -export type DragListGroupProps = { +export type SortableListGroupProps = { id: string; icon?: React.ReactNode; title: string; startAdornment?: React.ReactNode; endAdornment?: React.ReactNode; contentWhenEmpty?: React.ReactNode; - children: React.ReactElement[]; + children: React.ReactElement[]; }; -export function DragListGroup(props: DragListGroupProps): React.ReactNode { +export function SortableListGroup(props: SortableListGroupProps): React.ReactNode { const [isExpanded, setIsExpanded] = React.useState(true); const divRef = React.useRef(null); const boundingClientRect = useElementBoundingRect(divRef); - const dragListContext = React.useContext(DragListContext); + const sortableListContext = React.useContext(SortableListContext); - const isHovered = dragListContext.hoveredElementId === props.id; - const isHeaderHovered = isHovered && dragListContext.hoveredArea === HoveredArea.HEADER; - const isDragging = dragListContext.draggedElementId === props.id; - const dragPosition = dragListContext.dragPosition; + const isHovered = sortableListContext.hoveredElementId === props.id; + const isHeaderHovered = isHovered && sortableListContext.hoveredArea === HoveredArea.HEADER; + const isDragging = sortableListContext.draggedElementId === props.id; + const dragPosition = sortableListContext.dragPosition; function handleToggleExpanded() { setIsExpanded(!isExpanded); @@ -37,9 +37,9 @@ export function DragListGroup(props: DragListGroupProps): React.ReactNode { return ( <> - {isHovered && dragListContext.hoveredArea === HoveredArea.TOP && } + {isHovered && sortableListContext.hoveredArea === HoveredArea.TOP && }
- {isHovered && dragListContext.hoveredArea === HoveredArea.BOTTOM && } + {isHovered && sortableListContext.hoveredArea === HoveredArea.BOTTOM && } ); } @@ -95,8 +95,8 @@ type HeaderProps = { function Header(props: HeaderProps): React.ReactNode { return ( -
-
+
+
(null); const boundingClientRect = useElementBoundingRect(divRef); - const dragListContext = React.useContext(DragListContext); + const sortableListContext = React.useContext(SortableListContext); - const isHovered = dragListContext.hoveredElementId === props.id; - const isDragging = dragListContext.draggedElementId === props.id; - const dragPosition = dragListContext.dragPosition; + const isHovered = sortableListContext.hoveredElementId === props.id; + const isDragging = sortableListContext.draggedElementId === props.id; + const dragPosition = sortableListContext.dragPosition; return ( <> - {isHovered && dragListContext.hoveredArea === HoveredArea.TOP && } + {isHovered && sortableListContext.hoveredArea === HoveredArea.TOP && }
@@ -46,7 +46,7 @@ export function DragListItem(props: DragListItemProps): React.ReactNode { createPortal(
{props.children}
- {isHovered && dragListContext.hoveredArea === HoveredArea.BOTTOM && } + {isHovered && sortableListContext.hoveredArea === HoveredArea.BOTTOM && } ); } @@ -74,7 +74,7 @@ type HeaderProps = { function Header(props: HeaderProps): React.ReactNode { return (
-
+
diff --git a/frontend/src/modules/MyModule2/settings.tsx b/frontend/src/modules/MyModule2/settings.tsx index 2d8a01631..334728398 100644 --- a/frontend/src/modules/MyModule2/settings.tsx +++ b/frontend/src/modules/MyModule2/settings.tsx @@ -1,9 +1,9 @@ import React from "react"; -import { DragListGroup, DragListItem } from "@lib/components/DragList"; -import { DragList } from "@lib/components/DragList/dragList"; import { Input } from "@lib/components/Input"; import { Label } from "@lib/components/Label"; +import { SortableListGroup, SortableListItem } from "@lib/components/SortableList"; +import { SortableList } from "@lib/components/SortableList/sortableList"; import { useAtom } from "jotai"; @@ -50,68 +50,114 @@ export const Settings = () => { }, ], }, + { + id: "6", + type: "item", + title: "Item 5", + children: [], + }, + { + id: "7", + type: "group", + title: "Group 2", + children: [ + { + id: "8", + type: "item", + title: "Item 6", + children: [], + }, + { + id: "9", + type: "item", + title: "Item 7", + children: [], + }, + { + id: "10", + type: "group", + title: "Group 3", + children: [ + { + id: "11", + type: "item", + title: "Item 8", + children: [], + }, + ], + }, + ], + }, ]); function handleAtomTextChange(event: React.ChangeEvent) { setAtomText(event.target.value); } - function handleItemMove(itemId: string, originId: string | null, destinationid: string | null, position: number) { - const newItems = [...items]; - - const item = findItemById(itemId, newItems); - if (!item) { - return; - } + const handleItemMove = React.useCallback( + function handleItemMove( + itemId: string, + originId: string | null, + destinationid: string | null, + position: number + ) { + const newItems = [...items]; + + const item = findItemById(itemId, newItems); + if (!item) { + return; + } - const origin = originId ? findItemById(originId, newItems) : null; - let originArr: Item[] = []; - if (origin) { - originArr = origin.children; - } else { - originArr = newItems; - } + const origin = originId ? findItemById(originId, newItems) : null; + let originArr: Item[] = []; + if (origin) { + originArr = origin.children; + } else { + originArr = newItems; + } - const destination = findItemById(destinationid!, newItems); - let destinationArr: Item[] = []; - if (destination) { - destinationArr = destination.children; - } else { - destinationArr = newItems; - } + const destination = findItemById(destinationid!, newItems); + let destinationArr: Item[] = []; + if (destination) { + destinationArr = destination.children; + } else { + destinationArr = newItems; + } - originArr.splice( - originArr.findIndex((i) => i.id === itemId), - 1 - ); + originArr.splice( + originArr.findIndex((i) => i.id === itemId), + 1 + ); - if (position === -1) { - destinationArr.unshift(item); - } else { - destinationArr.splice(position, 0, item); - } + if (position === -1) { + destinationArr.unshift(item); + } else { + destinationArr.splice(position, 0, item); + } - setItems(newItems); - } + setItems(newItems); + }, + [items] + ); function makeChildren(items: Item[]): React.ReactElement[] { return items.map((item) => { if (item.type === "item") { return ( - + {item.title} - + ); } else { return ( - No items
} > {makeChildren(item.children)} - + ); } }); @@ -122,9 +168,11 @@ export const Settings = () => { - - {makeChildren(items)} - +
+ + {makeChildren(items)} + +
); };