From 00f753843bd194f55edfcfe53dfab878e525aad3 Mon Sep 17 00:00:00 2001 From: Dmytro Savin Date: Fri, 17 May 2024 21:04:06 +0300 Subject: [PATCH] feat: ability to drag and drop modules (#123) --- .../HardwareListing/HardwareListing.tsx | 19 +++++- src/components/ShipFit/Slot.tsx | 56 ++++++++++++++++- src/components/TreeListing/TreeListing.tsx | 3 + .../ShipSnapshotProvider.tsx | 63 ++++++++++++++++++- 4 files changed, 137 insertions(+), 4 deletions(-) diff --git a/src/components/HardwareListing/HardwareListing.tsx b/src/components/HardwareListing/HardwareListing.tsx index 4af12b4..81918b7 100644 --- a/src/components/HardwareListing/HardwareListing.tsx +++ b/src/components/HardwareListing/HardwareListing.tsx @@ -43,6 +43,22 @@ interface Filter { const ModuleGroup = (props: { level: number; group: ListingGroup; hideGroup?: boolean }) => { const shipSnapShot = React.useContext(ShipSnapshotContext); + const onItemDragStart = React.useCallback( + ( + typeId: ListingItem["typeId"], + slotType: ListingItem["slotType"], + ): ((e: React.DragEvent) => void) => { + return (e: React.DragEvent) => { + const img = new Image(); + img.src = `https://images.evetech.net/types/${typeId}/icon?size=64`; + e.dataTransfer.setDragImage(img, 32, 32); + e.dataTransfer.setData("application/type_id", typeId.toString()); + e.dataTransfer.setData("application/slot_type", slotType); + }; + }, + [], + ); + const getChildren = React.useCallback(() => { return ( <> @@ -66,6 +82,7 @@ const ModuleGroup = (props: { level: number; group: ListingGroup; hideGroup?: bo level={2} content={item.name} onClick={() => shipSnapShot.addModule(item.typeId, slotType)} + onDragStart={onItemDragStart(item.typeId, slotType)} /> ); } @@ -81,7 +98,7 @@ const ModuleGroup = (props: { level: number; group: ListingGroup; hideGroup?: bo })} ); - }, [props, shipSnapShot]); + }, [props, shipSnapShot, onItemDragStart]); if (props.hideGroup) { return ; diff --git a/src/components/ShipFit/Slot.tsx b/src/components/ShipFit/Slot.tsx index 3e9912a..79d89f5 100644 --- a/src/components/ShipFit/Slot.tsx +++ b/src/components/ShipFit/Slot.tsx @@ -25,7 +25,8 @@ export const Slot = (props: { type: string; index: number; fittable: boolean; ma const eveData = React.useContext(EveDataContext); const shipSnapshot = React.useContext(ShipSnapshotContext); - const esiFlag = esiFlagMapping[props.type][props.index - 1]; + const esiFlagType = props.type; + const esiFlag = esiFlagMapping[esiFlagType][props.index - 1]; const esiItem = shipSnapshot?.items?.find((item) => item.flag == esiFlag); const active = esiItem?.max_state !== "Passive" && esiItem?.max_state !== "Online"; @@ -148,6 +149,53 @@ export const Slot = (props: { type: string; index: number; fittable: boolean; ma [shipSnapshot, esiItem], ); + const onDragStart = React.useCallback( + (e: React.DragEvent) => { + if (esiItem === undefined) return; + + e.dataTransfer.setData("application/type_id", esiItem.type_id.toString()); + e.dataTransfer.setData("application/slot_id", esiFlag.toString()); + e.dataTransfer.setData("application/slot_type", esiFlagType); + }, + [esiItem, esiFlag, esiFlagType], + ); + + const onDragOver = React.useCallback((e: React.DragEvent) => { + e.preventDefault(); + }, []); + + const onDragEnd = React.useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + + const parseNumber = (maybeNumber: string): number | undefined => { + const num = parseInt(maybeNumber); + return Number.isInteger(num) ? num : undefined; + }; + + const draggedTypeId: number | undefined = parseNumber(e.dataTransfer.getData("application/type_id")); + const draggedSlotId: number | undefined = parseNumber(e.dataTransfer.getData("application/slot_id")); + const draggedSlotType: string = e.dataTransfer.getData("application/slot_type"); + + if (draggedTypeId === undefined) { + return; + } + + const isValidSlotGroup = draggedSlotType === esiFlagType; + if (!isValidSlotGroup) { + return; + } + + const isDraggedFromAnotherSlot = draggedSlotId !== undefined; + if (isDraggedFromAnotherSlot) { + shipSnapshot.moveModule(draggedSlotId, esiFlag); + } else { + shipSnapshot.setModule(draggedTypeId, esiFlag); + } + }, + [shipSnapshot, esiFlag, esiFlagType], + ); + /* Not fittable and nothing fitted; no need to render the slot. */ if (esiItem === undefined && !props.fittable) { return ( @@ -165,6 +213,8 @@ export const Slot = (props: { type: string; index: number; fittable: boolean; ma ); } else { @@ -172,6 +222,8 @@ export const Slot = (props: { type: string; index: number; fittable: boolean; ma ); } @@ -206,7 +258,7 @@ export const Slot = (props: { type: string; index: number; fittable: boolean; ma return (
-
+
{svg}
{item}
diff --git a/src/components/TreeListing/TreeListing.tsx b/src/components/TreeListing/TreeListing.tsx index 9ed3a4d..ea0a265 100644 --- a/src/components/TreeListing/TreeListing.tsx +++ b/src/components/TreeListing/TreeListing.tsx @@ -53,6 +53,7 @@ export const TreeLeaf = (props: { iconTitle?: string; content: string; onClick?: (e: React.MouseEvent) => void; + onDragStart?: (e: React.DragEvent) => void; }) => { const stylesHeader = styles[`header${props.level}`]; @@ -69,6 +70,8 @@ export const TreeLeaf = (props: { [styles.leaf]: props.onClick !== undefined, })} onClick={props.onClick} + draggable={!!props.onDragStart} + onDragStart={props.onDragStart} > {props.icon !== undefined && ( diff --git a/src/providers/ShipSnapshotProvider/ShipSnapshotProvider.tsx b/src/providers/ShipSnapshotProvider/ShipSnapshotProvider.tsx index 1055636..1dbc8f5 100644 --- a/src/providers/ShipSnapshotProvider/ShipSnapshotProvider.tsx +++ b/src/providers/ShipSnapshotProvider/ShipSnapshotProvider.tsx @@ -70,6 +70,8 @@ interface ShipSnapshot { currentFit?: EsiFit; currentSkills?: Record; + moveModule: (fromFlag: number, toFlag: number) => void; + setModule: (typeId: number, flag: number) => void; addModule: (typeId: number, slot: ShipSnapshotSlotsType) => void; removeModule: (flag: number) => void; addCharge: (chargeTypeId: number) => void; @@ -94,6 +96,8 @@ export const ShipSnapshotContext = React.createContext({ launcher: 0, turret: 0, }, + moveModule: () => {}, + setModule: () => {}, addModule: () => {}, removeModule: () => {}, addCharge: () => {}, @@ -145,6 +149,8 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => { launcher: 0, turret: 0, }, + moveModule: () => {}, + setModule: () => {}, addModule: () => {}, removeModule: () => {}, addCharge: () => {}, @@ -189,6 +195,47 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => { }); }, []); + const moveModule = React.useCallback((fromFlag: number, toFlag: number) => { + setCurrentFit((oldFit: EsiFit | undefined) => { + if (oldFit === undefined) return undefined; + + const newItems = [...oldFit.items]; + + const fromItemIndex = newItems.findIndex((item) => item.flag === fromFlag); + const fromItem = newItems[fromItemIndex]; + + const toItemIndex = newItems.findIndex((item) => item.flag === toFlag); + const toItem = newItems[toItemIndex]; + + fromItem.flag = toFlag; + + if (toItem !== undefined) { + /* Target slot is non-empty, swap items. */ + toItem.flag = fromFlag; + } + + return { + ...oldFit, + items: newItems, + }; + }); + }, []); + + const setModule = React.useCallback((typeId: number, flag: number) => { + setCurrentFit((oldFit: EsiFit | undefined) => { + if (oldFit === undefined) return undefined; + + const newItems = oldFit.items + .filter((item) => item.flag !== flag) + .concat({ flag: flag, type_id: typeId, quantity: 1 }); + + return { + ...oldFit, + items: newItems, + }; + }); + }, []); + const addModule = React.useCallback( (typeId: number, slot: ShipSnapshotSlotsType) => { setCurrentFit((oldFit: EsiFit | undefined) => { @@ -392,6 +439,8 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => { React.useEffect(() => { setShipSnapshot((oldSnapshot) => ({ ...oldSnapshot, + moveModule, + setModule, addModule, removeModule, addCharge, @@ -402,7 +451,19 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => { setItemState, setName, })); - }, [addModule, removeModule, addCharge, removeCharge, toggleDrones, removeDrones, changeHull, setItemState, setName]); + }, [ + moveModule, + setModule, + addModule, + removeModule, + addCharge, + removeCharge, + toggleDrones, + removeDrones, + changeHull, + setItemState, + setName, + ]); React.useEffect(() => { if (!dogmaEngine.loaded || !eveData.loaded) return;