feat: ability to drag and drop modules (#123)

This commit is contained in:
Dmytro Savin
2024-05-17 21:04:06 +03:00
committed by GitHub
parent 75dd759f7c
commit 00f753843b
4 changed files with 137 additions and 4 deletions

View File

@@ -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<HTMLDivElement>) => void) => {
return (e: React.DragEvent<HTMLDivElement>) => {
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 <TreeListing level={props.level} getChildren={getChildren} />;

View File

@@ -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<HTMLDivElement>) => {
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<HTMLDivElement>) => {
e.preventDefault();
}, []);
const onDragEnd = React.useCallback(
(e: React.DragEvent<HTMLDivElement>) => {
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
<img
src={`https://images.evetech.net/types/${esiItem.charge.type_id}/icon?size=64`}
title={`${eveData?.typeIDs?.[esiItem.type_id].name}\n${eveData?.typeIDs?.[esiItem.charge.type_id].name}`}
draggable={true}
onDragStart={onDragStart}
/>
);
} else {
@@ -172,6 +222,8 @@ export const Slot = (props: { type: string; index: number; fittable: boolean; ma
<img
src={`https://images.evetech.net/types/${esiItem.type_id}/icon?size=64`}
title={eveData?.typeIDs?.[esiItem.type_id].name}
draggable={true}
onDragStart={onDragStart}
/>
);
}
@@ -206,7 +258,7 @@ export const Slot = (props: { type: string; index: number; fittable: boolean; ma
return (
<div className={styles.slotOuter} data-hasitem={esiItem !== undefined}>
<div className={styles.slot} onClick={cycleState} data-state={state}>
<div className={styles.slot} onClick={cycleState} data-state={state} onDrop={onDragEnd} onDragOver={onDragOver}>
{svg}
<div className={imageStyle}>{item}</div>
</div>

View File

@@ -53,6 +53,7 @@ export const TreeLeaf = (props: {
iconTitle?: string;
content: string;
onClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
onDragStart?: (e: React.DragEvent<HTMLDivElement>) => 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 && (
<span className={styles.leafIcon}>

View File

@@ -70,6 +70,8 @@ interface ShipSnapshot {
currentFit?: EsiFit;
currentSkills?: Record<string, number>;
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<ShipSnapshot>({
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;