357 lines
11 KiB
JavaScript
357 lines
11 KiB
JavaScript
import { BlurView } from "expo-blur";
|
|
import React, { useEffect, useMemo, useState } from "react";
|
|
import { FlatList, Platform, SectionList, Text, View } from "react-native";
|
|
import useLayoutType from "../../hooks/useLayoutType";
|
|
import CreateLyricsHeader from "../../screens/Writing/components/CreateLyricsHeader";
|
|
import { Palette } from "../../styles";
|
|
import { FONT_FAMILY } from "../../styles/Fonts";
|
|
import { LinearGradient } from "../LinearGradient/LinearGradient";
|
|
|
|
const BLUE_SELECTION_GRADIENT = ["#4673F9", "#7023F7"];
|
|
|
|
const buildItemKey = (value, category) =>
|
|
category ? `${category}::${value}` : `${value}`;
|
|
|
|
const ListSelection = ({
|
|
options = [],
|
|
variant = "simple",
|
|
selected: selectedProp,
|
|
setSelected: setSelectedProp,
|
|
multiple = false,
|
|
maxSelection,
|
|
getItemValue,
|
|
selectedValueExtractor,
|
|
formatSelectedValue,
|
|
// styles
|
|
contentContainerStyle,
|
|
itemContainerStyle,
|
|
itemOuterStyle,
|
|
itemTextStyle,
|
|
highlightColor = "#F94697",
|
|
disableHover = false,
|
|
}) => {
|
|
// état interne si non contrôlé
|
|
const [internalSelected, setInternalSelected] = useState(
|
|
variant === "sectioned" ? {} : multiple ? [] : null
|
|
);
|
|
const selected = selectedProp !== undefined ? selectedProp : internalSelected;
|
|
const setSelected =
|
|
setSelectedProp !== undefined ? setSelectedProp : setInternalSelected;
|
|
|
|
const { isWeb } = useLayoutType();
|
|
const [hoveredKey, setHoveredKey] = useState(null);
|
|
|
|
useEffect(() => {
|
|
if (disableHover && hoveredKey !== null) setHoveredKey(null);
|
|
}, [disableHover, hoveredKey]);
|
|
|
|
// helpers
|
|
const valueOf = useMemo(() => {
|
|
if (typeof getItemValue === "function") return getItemValue;
|
|
if (variant === "simple") return (item) => item;
|
|
return (item) => item?.title ?? item;
|
|
}, [getItemValue, variant]);
|
|
|
|
const formatValue = (item) =>
|
|
typeof formatSelectedValue === "function"
|
|
? formatSelectedValue(item)
|
|
: valueOf(item);
|
|
|
|
const extractSelectedComparable = (s) => {
|
|
if (typeof selectedValueExtractor === "function")
|
|
return selectedValueExtractor(s);
|
|
if (s && typeof s === "object" && "title" in s) return s.title;
|
|
return s;
|
|
};
|
|
|
|
const isSelected = (val, category) => {
|
|
if (variant === "sectioned") return selected?.[category] === val;
|
|
if (multiple) {
|
|
const list = Array.isArray(selected) ? selected : [];
|
|
return list.some((v) => v === val);
|
|
}
|
|
return extractSelectedComparable(selected) === val;
|
|
};
|
|
|
|
const toggleSelect = (item, category) => {
|
|
const val = valueOf(item);
|
|
|
|
if (variant === "sectioned") {
|
|
const current = selected && typeof selected === "object" ? selected : {};
|
|
const next = { ...current };
|
|
if (current?.[category] === val) next[category] = null;
|
|
else next[category] = formatValue(item);
|
|
setSelected(next);
|
|
return;
|
|
}
|
|
|
|
if (multiple) {
|
|
const list = Array.isArray(selected) ? selected : [];
|
|
const exists = list.some((v) => v === val);
|
|
if (exists) setSelected(list.filter((v) => v !== val));
|
|
else if (!maxSelection || list.length < maxSelection)
|
|
setSelected([...list, formatValue(item)]);
|
|
} else {
|
|
if (extractSelectedComparable(selected) === val) setSelected(null);
|
|
else setSelected(formatValue(item));
|
|
}
|
|
};
|
|
|
|
// contenus élémentaires : toujours encapsuler le texte dans <Text>
|
|
const renderSimpleContent = (label) => (
|
|
<View style={{ minHeight: 40, justifyContent: "center" }}>
|
|
<Text
|
|
style={{
|
|
fontSize: 16,
|
|
color: Palette.white,
|
|
fontFamily: FONT_FAMILY.InterRegular,
|
|
textAlign: "left",
|
|
...itemTextStyle,
|
|
}}
|
|
>
|
|
{typeof label === "string" ? label : String(label)}
|
|
</Text>
|
|
</View>
|
|
);
|
|
|
|
const renderTitleDescriptionContent = (item) => (
|
|
<View style={{ paddingVertical: 6 }}>
|
|
<Text
|
|
style={{
|
|
fontSize: 16,
|
|
color: Palette.white,
|
|
fontFamily: FONT_FAMILY.InterRegular,
|
|
...itemTextStyle,
|
|
}}
|
|
>
|
|
<Text style={{ fontFamily: FONT_FAMILY.InterBold }}>{item?.title}</Text>
|
|
<Text>{` : ${item?.description ?? ""}`}</Text>
|
|
</Text>
|
|
</View>
|
|
);
|
|
|
|
// applique le gradient bleu UNIQUEMENT sur web et quand sélectionné
|
|
const withWebBlueGradientIfSelected = (content, sel) => {
|
|
if (!isWeb || !sel) return content;
|
|
// wrap dans un View pour éviter texte brut direct sous le gradient (compat RN Web)
|
|
return (
|
|
<LinearGradient
|
|
colors={BLUE_SELECTION_GRADIENT}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 0 }}
|
|
locations={[0, 1]}
|
|
style={{
|
|
borderRadius: 14,
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 8,
|
|
overflow: "hidden",
|
|
}}
|
|
>
|
|
<View>{content}</View>
|
|
</LinearGradient>
|
|
);
|
|
};
|
|
|
|
// ============ SECTIONED ============
|
|
if (variant === "sectioned") {
|
|
return (
|
|
<SectionList
|
|
sections={options}
|
|
showsVerticalScrollIndicator={false}
|
|
contentContainerStyle={contentContainerStyle}
|
|
renderItem={({ item, section }) => {
|
|
const cat = section?.title;
|
|
const val = valueOf(item);
|
|
const sel = isSelected(val, cat);
|
|
const itemKey = buildItemKey(String(val ?? ""), cat);
|
|
const isHovered = isWeb && !disableHover && hoveredKey === itemKey;
|
|
const showHoverOutline = isHovered && !sel;
|
|
|
|
const baseContent = renderSimpleContent(item);
|
|
const headerContainerStyle = {
|
|
borderWidth: 0,
|
|
borderColor: "transparent",
|
|
borderRadius: 14,
|
|
...itemContainerStyle,
|
|
};
|
|
if (sel && isWeb)
|
|
headerContainerStyle.backgroundColor = "transparent";
|
|
|
|
return (
|
|
<View
|
|
style={[{ position: "relative" }, itemOuterStyle]}
|
|
onMouseEnter={
|
|
isWeb && !disableHover
|
|
? () => setHoveredKey(itemKey)
|
|
: undefined
|
|
}
|
|
onMouseLeave={
|
|
isWeb && !disableHover ? () => setHoveredKey(null) : undefined
|
|
}
|
|
>
|
|
<CreateLyricsHeader
|
|
onPress={() => toggleSelect(item, cat)}
|
|
tint={sel ? "default" : "dark"}
|
|
colors={[Palette.tran, Palette.tran]}
|
|
showBorder={!sel}
|
|
disableBlur={sel && isWeb}
|
|
blurViewStyle={
|
|
sel && isWeb
|
|
? { paddingHorizontal: 0, paddingVertical: 0 }
|
|
: undefined
|
|
}
|
|
containerStyle={headerContainerStyle}
|
|
>
|
|
{withWebBlueGradientIfSelected(baseContent, sel)}
|
|
</CreateLyricsHeader>
|
|
|
|
{showHoverOutline && (
|
|
<View
|
|
pointerEvents="none"
|
|
style={{
|
|
position: "absolute",
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
borderWidth: 2,
|
|
borderRadius: 14,
|
|
borderColor: highlightColor,
|
|
zIndex: 2,
|
|
}}
|
|
/>
|
|
)}
|
|
</View>
|
|
);
|
|
}}
|
|
renderSectionHeader={({ section: { title } }) => (
|
|
<View
|
|
style={{ alignSelf: "flex-start", marginLeft: 10, marginBottom: 6 }}
|
|
>
|
|
<BlurView
|
|
intensity={40}
|
|
tint="dark"
|
|
experimentalBlurMethod={
|
|
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
|
}
|
|
style={{
|
|
borderRadius: 18,
|
|
overflow: "hidden",
|
|
paddingHorizontal: 8,
|
|
paddingVertical: 4,
|
|
}}
|
|
>
|
|
<Text
|
|
style={{
|
|
fontSize: 16,
|
|
color: Palette.white,
|
|
fontFamily: FONT_FAMILY.InterBold,
|
|
}}
|
|
>
|
|
{title}
|
|
</Text>
|
|
</BlurView>
|
|
</View>
|
|
)}
|
|
keyExtractor={(item, idx) => `${valueOf(item)}-${idx}`}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// ============ FLAT (simple/titleDescription/emotion) ============
|
|
return (
|
|
<FlatList
|
|
data={options}
|
|
showsVerticalScrollIndicator={false}
|
|
contentContainerStyle={contentContainerStyle}
|
|
renderItem={({ item }) => {
|
|
const val = valueOf(item);
|
|
const sel = isSelected(val);
|
|
const useEmotionColors = variant === "emotion";
|
|
|
|
// pour "emotion", on masque le bord gauche si sélectionné
|
|
const borderColors = useEmotionColors
|
|
? sel
|
|
? [Palette.tran, Palette.tran]
|
|
: item?.color
|
|
: [Palette.tran, Palette.tran];
|
|
const tint = useEmotionColors ? "dark" : sel ? "default" : "dark";
|
|
|
|
const itemKey = buildItemKey(String(val ?? ""));
|
|
const isHovered = isWeb && !disableHover && hoveredKey === itemKey;
|
|
const showHoverOutline = isHovered && !sel;
|
|
const showSelectionOutline =
|
|
!isWeb &&
|
|
(variant === "simple" || variant === "emotion" || variant === "titleDescription") &&
|
|
sel;
|
|
|
|
const baseContent =
|
|
variant === "simple"
|
|
? renderSimpleContent(item)
|
|
: renderTitleDescriptionContent(item);
|
|
|
|
const headerContainerStyle = {
|
|
borderWidth: 0,
|
|
borderColor: "transparent",
|
|
borderRadius: 14,
|
|
...itemContainerStyle,
|
|
};
|
|
if (sel && isWeb) headerContainerStyle.backgroundColor = "transparent";
|
|
|
|
const shouldShowBorder = !sel;
|
|
|
|
return (
|
|
<View
|
|
style={[{ position: "relative" }, itemOuterStyle]}
|
|
onMouseEnter={
|
|
isWeb && !disableHover ? () => setHoveredKey(itemKey) : undefined
|
|
}
|
|
onMouseLeave={
|
|
isWeb && !disableHover ? () => setHoveredKey(null) : undefined
|
|
}
|
|
>
|
|
<CreateLyricsHeader
|
|
colors={borderColors}
|
|
tint={tint}
|
|
onPress={() => toggleSelect(item)}
|
|
gradientProps={
|
|
useEmotionColors && !sel ? { locations: [0.24, 1] } : undefined
|
|
}
|
|
showBorder={shouldShowBorder}
|
|
disableBlur={sel && isWeb} // web: pas de blur si gradient
|
|
blurViewStyle={
|
|
sel && isWeb
|
|
? { paddingHorizontal: 0, paddingVertical: 0 }
|
|
: undefined
|
|
}
|
|
containerStyle={headerContainerStyle}
|
|
>
|
|
{withWebBlueGradientIfSelected(baseContent, sel)}
|
|
</CreateLyricsHeader>
|
|
|
|
{(showHoverOutline || showSelectionOutline) && (
|
|
<View
|
|
pointerEvents="none"
|
|
style={{
|
|
position: "absolute",
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
borderWidth: 2,
|
|
borderRadius: 14,
|
|
borderColor: highlightColor,
|
|
zIndex: 2,
|
|
}}
|
|
/>
|
|
)}
|
|
</View>
|
|
);
|
|
}}
|
|
keyExtractor={(item, idx) => `${valueOf(item)}-${idx}`}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export default ListSelection;
|