PadDND
PadDND es un componente de alto nivel que construye un “pad” de elementos arrastrables (tecladito) encima del sistema DnD.
Su objetivo es que no tengas que armar a mano 10 Draggable para números, o un montón de letras/opciones:
ya trae patrones listos para:
- Números
0–9(más.opcional). - Opciones personalizadas (
options). - Caracteres de una palabra (
word).
Siempre genera Draggable<PadObject<T>> envueltos en FactoryLiteThemedContainer con estilos coherentes para Explorax.
Tipo base: PadObject<T>
El tipo de dato que viaja en cada draggable del pad:
export interface PadObject<T = any> {
/** Identificador lógico: puede ser único o varios índices esperados */
id: number | number[];
/** Texto que se muestra en el botón del pad */
text: string;
/** Payload extra opcional para tu caso de uso */
data?: T;
/** ID único opcional (útil para tracking) */
randomUniqueId?: string;
/** Valor “default” opcional (por ejemplo, posición esperada) */
default?: number;
}
En el DNDProvider normalmente usarás:
<DNDProvider<PadObject<MyExtraData>> ...>
<PadDND<MyExtraData> ... />
{/* PadDroppable / Droppable usan también PadObject<MyExtraData> */}
</DNDProvider>
API de PadDND
interface NumberDnDProps<T extends any> {
workspaceDimensions: {
width: number;
height: number;
};
// Modo de contenido
useNumbers?: boolean;
useDot?: boolean;
word?: string;
options?: PadObject<T>[];
useOptions?: boolean;
// Comportamiento / visual
disappearAfterDrop?: boolean; // (por ahora no se usa internamente)
showOrderIndicator?: boolean;
adaptContent?: boolean;
useRandomUniqueId?: boolean;
disable?: boolean;
// Estilos
contentStyle?: StyleProp<ViewStyle>;
containerStyle?: StyleProp<ViewStyle>;
fontSize?: number;
// Imagen durante el drag
dragImage?: ImageProps | { uri: string };
styleDragImageProp?: StyleProp<ImageStyle>;
// Layout avanzado
keepPositions?: boolean;
answers?: number;
}
const PadDND = <T,>(props: NumberDnDProps<T>) => JSX.Element;
export default PadDND;
Props principales
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
workspaceDimensions | { width: number; height: number } | — (obligatorio) | Dimensiones del espacio donde vive el pad; se usan para tamaños relativos. |
useNumbers | boolean | false | Si es true, el pad muestra dígitos 0–9 (y opcionalmente .). Tiene prioridad sobre useOptions. |
useDot | boolean | false | Si es true y useNumbers es true, agrega un draggable extra con ".". |
word | string | "" | Palabra a fragmentar en caracteres. Solo se usa en modo “palabra” (cuando useNumbers y useOptions son false) y para los indicadores de orden en modo números. |
options | PadObject<T>[] | [] | Opciones personalizadas para el modo “opciones”. |
useOptions | boolean | false | Si es true (y useNumbers es false), el pad muestra options en vez de números o caracteres de word. |
showOrderIndicator | boolean | false | Activa los circulitos/indicadores de orden de FactoryLiteThemedContainer (varía por modo, ver más abajo). |
adaptContent | boolean | false | Solo en modo “opciones”: usa un tamaño de caja más “alargado” (styles.adaptContent) para textos largos. |
useRandomUniqueId | boolean | false | En modo “palabra”: rellena randomUniqueId con UUID.v4() por carácter. Útil para tracking fino. |
disable | boolean | false | Deshabilita todos los drags del pad. |
contentStyle | StyleProp<ViewStyle> | undefined | Estilo adicional para el contenido dentro de cada FactoryLiteThemedContainer. |
containerStyle | StyleProp<ViewStyle> | undefined | Estilo extra para el View que agrupa todos los draggables. |
fontSize | number | height * 0.05 | Tamaño base del texto dentro del pad. |
dragImage | ImageProps | { uri: string } | undefined | Imagen opcional que se dibuja encima del botón cuando isDragging === true. |
styleDragImageProp | StyleProp<ImageStyle> | undefined | Estilos para la imagen de drag (tamaño, posición absoluta, etc.). |
keepPositions | boolean | false | Controla si se memoiza el orden (word/opciones) o se re-barajan en cada render. |
answers | number | undefined | Ajusta tamaños/gaps del layout según cuántos “slots de respuesta” tienes (si >= 4, usa cajas un poco más pequeñas). |
disappearAfterDrop | boolean | false | Reservado para futuro: hoy no se pasa a los Draggable, la desaparición la controla más bien el mode en DNDProvider. |
Modos de operación
PadDND tiene 3 modos mutuamente excluyentes, con esta prioridad:
useNumbers === true→ modo números.useOptions === true(y!useNumbers) → modo opciones.- Caso contrario → modo palabra.
1. Modo números (useNumbers)
-
Genera 10
Draggable<PadObject>con:data={{
id: i,
text: `${i}`,
randomUniqueId: useMemo(() => UUID.v4(), [i]),
}} -
Cada uno:
-
Tiene
resetAfterDrop. -
Respeta
disable. -
Se renderiza con
FactoryLiteThemedContainer:<FactoryLiteThemedContainer
order={
showOrderIndicator
? word.split("").reduce(
(acc, char, idx) =>
char === data.text ? [...acc, idx + 1] : acc,
[] as number[]
)
: []
}
showOrderIndicator
workspaceDimensions={workspaceDimensions}
type="text"
containerStyle={{ justifyContent: "center", alignItems: "center" }}
contentStyle={[styles.content, contentStyle]}
reactive
colorCombination="answerblue"
text={data.text}
textVariant="textBig"
textStyle={{
fontSize: fontSize ?? workspaceDimensions.height * 0.05,
color: "white",
fontFamily: "lato-bold",
}}
/>
-
-
Indicadores de orden (si
showOrderIndicator):- Muestran todas las posiciones donde ese dígito aparece en
word. Ejemplo:word = "2024"→ para el dígito"2"se mostrarían{1, 3}.
- Muestran todas las posiciones donde ese dígito aparece en
-
Punto decimal (
useDot):-
Si
useDotestrue, se agrega un draggable extra:data={{ id: 10, text: ".", randomUniqueId: "10" }} -
Visualmente se comporta igual que un número más.
-
Nota: aunque el comentario habla de números “shuffled”, la implementación actual los muestra en orden
0,1,...,9. Si quieres mezclarlos, puedes barajar tu propio arreglo y usarPadDNDen modouseOptions.
2. Modo opciones (useOptions)
Se activa si:
useOptions === true && useNumbers === false
-
Primero se calcula:
const shuffledOptions =
keepPositions
? useMemo(() => shuffleArray(options), [options])
: shuffleArray(options);keepPositions = true: solo se rebarajan cuando cambia el arrayoptions.keepPositions = false: se rebarajan en cada render (comportamiento más aleatorio).
-
Luego:
shuffledOptions.map((option, i) => (
<Draggable<PadObject<T>>
disable={disable}
timeMove={0}
data={option}
resetAfterDrop
>
{({ data, isDragging }) => (
<>
<FactoryLiteThemedContainer
order={showOrderIndicator ? [i + 1] : []}
showOrderIndicator
workspaceDimensions={workspaceDimensions}
type="text"
containerStyle={{ justifyContent: "center", alignItems: "center" }}
contentStyle={[
adaptContent ? styles.adaptContent : styles.content,
contentStyle,
]}
reactive
colorCombination="answerblue"
text={data.text}
textVariant="textBig"
textStyle={{
fontSize: fontSize ?? workspaceDimensions.height * 0.05,
lineHeight: workspaceDimensions.height * 0.06,
color: "white",
fontFamily: "lato-bold",
}}
borderRadius={workspaceDimensions.height * 0.02}
/>
{isDragging && dragImage && (
<Image source={dragImage} style={styleDragImageProp} />
)}
</>
)}
</Draggable>
)); -
Indicadores de orden:
- Si
showOrderIndicator, cada opción muestra[i + 1](posición secuencial en el pad).
- Si
-
adaptContent:false→ usastyles.content: cajas más cuadradas (similar a números/letras).true→ usastyles.adaptContent: cajas más altas y estrechas, mejor para textos largos.
3. Modo palabra (por defecto)
Se activa cuando:
!useNumbers && !useOptions
-
Se construye
shuffledWorda partir deword:const shuffledWord =
keepPositions
? useMemo(() => /* algoritmo de reordenamiento memorizado */, [word])
: shuffleArray(
word.split("").map((char, i) => ({ id: i, text: char }))
);- En ambos casos, cada carácter termina como
{ id, text }. keepPositions = true→ solo se recalcula cuando cambiaword.keepPositions = false→ se rebaraja en cada render.
- En ambos casos, cada carácter termina como
-
Render:
(useNumbers
? /* no aplica */
: useOptions
? /* no aplica */
: shuffledWord.map((char) => (
<Draggable<PadObject>
timeMove={0}
data={{
id: char.id,
text: char.text,
randomUniqueId: useRandomUniqueId
? useMemo(() => UUID.v4(), [char.text])
: undefined,
}}
resetAfterDrop
disable={disable}
>
{({ data, isDragging }) => (
<>
<FactoryLiteThemedContainer
order={showOrderIndicator ? [char.id + 1] : []}
showOrderIndicator
workspaceDimensions={workspaceDimensions}
type="text"
containerStyle={{
justifyContent: "center",
alignItems: "center",
}}
contentStyle={[styles.content, contentStyle]}
reactive
colorCombination="answerblue"
text={data.text}
textVariant="textBig"
textStyle={{
fontSize: fontSize ?? workspaceDimensions.height * 0.05,
color: "white",
fontFamily: "lato-bold",
}}
/>
{isDragging && dragImage && (
<Image source={dragImage} style={styleDragImageProp} />
)}
</>
)}
</Draggable>
))); -
Indicadores de orden:
[char.id + 1]→ corresponden a la posición original del carácter en la palabra.
-
useRandomUniqueId:- Si es
true, cada carácter lleva unrandomUniqueIddistinto, generado conUUID.v4(); útil si necesitas diferenciar dos letras iguales (“A” en posición 1 vs “A” en posición 4).
- Si es
Layout y answers
El estilo se genera con:
const getStyle = ({ width, height }, answers?: number) => {
const droppableSize =
answers && answers >= 4 ? height * 0.1 : height * 0.11;
return StyleSheet.create({
container: {
flexDirection: "row",
flexWrap: "wrap",
alignItems: "center",
justifyContent: "center",
width: height * 0.6,
maxWidth: width * 0.9,
gap: answers && answers >= 4 ? height * 0.03 : height * 0.024,
},
content: {
width: droppableSize,
height: droppableSize,
justifyContent: "center",
alignItems: "center",
},
adaptContent: {
height: height * 0.13,
width: height * 0.06,
justifyContent: "center",
alignItems: "center",
},
dragImage: {
resizeMode: "contain",
},
});
};
-
Si esperas muchas respuestas (>= 4), conviene pasar
answerscon ese valor para que:- Los botones sean un poco más pequeños.
- El
gapentre ellos aumente un poco.
-
contentse usa en números y palabra (cajas cuadradas). -
adaptContentse usa en opciones cuandoadaptContent = true.
Ejemplo de uso típico con números
import { DNDProvider } from "@components/DnDRevolution/DNDProvider";
import PadDND, { PadObject } from "@components/DnDRevolution/PadDnD";
import PadDroppable from "@components/DnDRevolution/PadDroppable";
type ExtraData = { correctValue: string };
const workspaceDimensions = { width: 360, height: 640 };
export const NumericPadExample = () => {
return (
<DNDProvider<PadObject<ExtraData>>
workSpaceDimensions={workspaceDimensions}
mode="remplacement-multi"
onDrop={(dataByDroppable, historyByDroppable) => {
// Evaluar respuestas numéricas aquí
}}
>
{/* Droppables de destino (slots para los dígitos) */}
<View style={{ flexDirection: "row", justifyContent: "center", gap: 8 }}>
{Array.from({ length: 4 }, (_, i) => (
<PadDroppable key={i} index={i} workspaceDimensions={workspaceDimensions} />
))}
</View>
{/* Pad de números + punto */}
<PadDND<ExtraData>
workspaceDimensions={workspaceDimensions}
useNumbers
useDot
showOrderIndicator={false}
answers={4}
dragImage={require("@assets/drag-ghost.png")}
styleDragImageProp={{
width: 40,
height: 40,
position: "absolute",
}}
/>
</DNDProvider>
);
};
Ejemplo de uso con palabra
<PadDND
workspaceDimensions={workspaceDimensions}
word="CAÑA"
showOrderIndicator
useRandomUniqueId
/>
- Genera un pad con las letras de “CAÑA” (barajadas por defecto).
- Los indicadores de orden muestran
1, 2, 3, 4según la posición correcta. randomUniqueIdpermite identificar cada letra aunque se repitan caracteres.
Resumen
-
PadDNDte da un pad configurable de draggables para:- Números (con o sin punto).
- Opciones personalizadas.
- Letras de una palabra.
-
Se integra naturalmente con:
DNDProvider<PadObject<T>>PadDroppable/Droppable<PadObject<T>>
-
Soporta:
- Indicadores de orden (
showOrderIndicator). - Layout responsive con
workspaceDimensionsyanswers. - Tracking fino con
useRandomUniqueId. - Imagen overlay durante el drag (
dragImage).
- Indicadores de orden (
Siguiente sección recomendada:
👉 PadDroppable para ver cómo recibir estos PadObject del pad.