import {
View,
Text,
ScrollView,
StyleSheet,
Dimensions,
TouchableOpacity,
} from "react-native";
import React, { useEffect, useMemo, useState } from "react";
import Animated, {
runOnJS,
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withSpring,
} from "react-native-reanimated";
import { PanGestureHandler } from "react-native-gesture-handler";
const { width } = Dimensions.get("window");
// Child component to respect Rules of Hooks (no hooks in loops)
const DraggableItem = ({ item, gestureHandler, draggingItem, translateX, translateY, onActivate }) => {
const style = useAnimatedStyle(() => ({
transform: [
{ translateX: draggingItem.value === item.id ? translateX.value : 0 },
{ translateY: draggingItem.value === item.id ? translateY.value : 0 },
],
zIndex: draggingItem.value === item.id ? 10 : 0,
}));
return (
onActivate(item.id)}
>
{item.label ?? String(item)}
);
};
// Props:
// - sourceItems: array of { id, label, value } or strings
// - initialSelected: array of ids to prefill right side (optional)
// - onChange: callback with array of values (or strings) in right order
const SongStructureDragDrop = ({ sourceItems, initialSelected, onChange }) => {
const normalize = (arr = []) =>
arr.map((item, idx) =>
typeof item === "string"
? { id: `${item}-${idx}`, label: item, value: item }
: item,
);
const normalizedSource = useMemo(
() => normalize(sourceItems || []),
[sourceItems],
);
const [rightItems, setRightItems] = useState([]);
// Compare arrays by item id and order to avoid unnecessary state churn
const sameById = (a = [], b = []) => {
if (a === b) return true;
if (!a || !b) return false;
if (a.length !== b.length) return false;
for (let idx = 0; idx < a.length; idx++) {
if (a[idx]?.id !== b[idx]?.id) return false;
}
return true;
};
useEffect(() => {
const selected = Array.isArray(initialSelected) ? initialSelected : [];
const selectedSet = new Set(selected);
const nextRight = normalizedSource.filter((i) => selectedSet.has(i.id));
setRightItems((prev) => (sameById(prev, nextRight) ? prev : nextRight));
}, [normalizedSource, initialSelected]);
const leftItems = useMemo(() => {
if (!rightItems || rightItems.length === 0) return normalizedSource;
const rightIds = new Set(rightItems.map((item) => item.id));
return normalizedSource.filter((item) => !rightIds.has(item.id));
}, [normalizedSource, rightItems]);
const draggingItem = useSharedValue(null);
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const onActivate = (id) => {
draggingItem.value = id;
};
const moveItem = (itemId) => {
const item = normalizedSource.find((i) => i.id === itemId);
if (!item) return;
setRightItems((current) => {
if (current.some((i) => i.id === itemId)) return current;
const next = [...current, item];
onChange?.(next.map((i) => i.value ?? i.label));
return next;
});
};
const removeItem = (itemId) => {
setRightItems((current) => {
if (!current.some((i) => i.id === itemId)) return current;
const next = current.filter((i) => i.id !== itemId);
onChange?.(next.map((i) => i.value ?? i.label));
return next;
});
};
const gestureHandler = useAnimatedGestureHandler({
onStart: (_, ctx) => {
ctx.startX = translateX.value;
ctx.startY = translateY.value;
},
onActive: (event, ctx) => {
translateX.value = ctx.startX + event.translationX;
translateY.value = ctx.startY + event.translationY;
},
onEnd: () => {
if (translateX.value > width / 2) {
runOnJS(moveItem)(draggingItem.value);
}
translateX.value = withSpring(0);
translateY.value = withSpring(0);
draggingItem.value = null;
},
});
return (
Left
{leftItems.map((item) => (
))}
Right
Appuyez pour retirer un élément
{rightItems.map((item) => (
removeItem(item.id)}
activeOpacity={0.7}
accessibilityRole="button"
accessibilityLabel={`Retirer ${item.label ?? String(item)}`}
>
{item.label ?? String(item)}
Retirer
))}
);
};
export default SongStructureDragDrop;
const styles = StyleSheet.create({
scrollWrapper: {
flexDirection: "row",
justifyContent: "space-around",
},
scroll: {
width: width / 2.2,
height: "90%",
backgroundColor: "#f0f0f0",
borderRadius: 10,
margin: 5,
padding: 10,
},
title: {
fontWeight: "bold",
fontSize: 16,
marginBottom: 10,
},
item: {
backgroundColor: "#aaf",
padding: 15,
marginVertical: 5,
borderRadius: 10,
},
selectedItem: {
backgroundColor: "#bdf",
padding: 15,
marginVertical: 5,
borderRadius: 10,
alignItems: "center",
},
helpText: {
fontSize: 12,
color: "#666",
marginBottom: 8,
textAlign: "center",
},
removeHint: {
fontSize: 12,
color: "#555",
marginTop: 4,
},
text: {
color: "#333",
textAlign: "center",
},
});