feat: ability to drag and drop modules (#123)
This commit is contained in:
@@ -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} />;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user