132 lines
3.5 KiB
TypeScript
132 lines
3.5 KiB
TypeScript
import * as React from "react";
|
|
import { FieldValues, Path, UseFormReturn, useFieldArray } from "react-hook-form";
|
|
|
|
interface UseItemsTableNavigationOptions<TFieldValues extends FieldValues> {
|
|
/** Nombre del array de líneas en el formulario (tipo-safe) */
|
|
name: Path<TFieldValues>;
|
|
/** Creador de una línea vacía */
|
|
createEmpty: () => unknown; // ajusta el tipo del item si lo conoces
|
|
/** Primer campo editable de la fila */
|
|
firstEditableField?: string;
|
|
}
|
|
|
|
export function useItemsTableNavigation<TFieldValues extends FieldValues = FieldValues>(
|
|
form: UseFormReturn<TFieldValues>,
|
|
{
|
|
name,
|
|
createEmpty,
|
|
firstEditableField = "description",
|
|
}: UseItemsTableNavigationOptions<TFieldValues>
|
|
) {
|
|
const { control, getValues, setFocus } = form;
|
|
const fa = useFieldArray<TFieldValues>({ control, name });
|
|
|
|
// Desestructurar para evitar recreaciones
|
|
const { append, insert, remove: faRemove, move } = fa;
|
|
|
|
// Ref estable para getValues
|
|
const getValuesRef = React.useRef(getValues);
|
|
getValuesRef.current = getValues;
|
|
|
|
const length = React.useCallback(() => {
|
|
const arr = getValuesRef.current(name) as unknown[];
|
|
return Array.isArray(arr) ? arr.length : 0;
|
|
}, [name]);
|
|
|
|
const focusRowFirstField = React.useCallback(
|
|
(rowIndex: number) => {
|
|
queueMicrotask(() => {
|
|
try {
|
|
setFocus(`${name}.${rowIndex}.${firstEditableField}` as any, {
|
|
shouldSelect: true,
|
|
});
|
|
} catch {
|
|
// el campo aún no está montado
|
|
}
|
|
});
|
|
},
|
|
[name, firstEditableField, setFocus]
|
|
);
|
|
|
|
const addEmpty = React.useCallback(
|
|
(atEnd = true, index?: number, initial?: Record<string, unknown>) => {
|
|
const row = { ...createEmpty(), ...(initial ?? {}) };
|
|
if (!atEnd && typeof index === "number") insert(index, row);
|
|
else append(row);
|
|
},
|
|
[append, insert, createEmpty]
|
|
);
|
|
|
|
const duplicate = React.useCallback(
|
|
(i: number) => {
|
|
const curr = getValuesRef.current(`${name}.${i}`) as Record<string, unknown> | undefined;
|
|
if (!curr) return;
|
|
const clone =
|
|
typeof structuredClone === "function"
|
|
? structuredClone(curr)
|
|
: JSON.parse(JSON.stringify(curr));
|
|
const { id: _id, ...sanitized } = clone;
|
|
insert(i + 1, sanitized);
|
|
},
|
|
[insert, name]
|
|
);
|
|
|
|
const remove = React.useCallback(
|
|
(i: number) => {
|
|
if (i < 0 || i >= length()) return;
|
|
faRemove(i);
|
|
},
|
|
[faRemove, length]
|
|
);
|
|
|
|
const moveUp = React.useCallback(
|
|
(i: number) => {
|
|
if (i <= 0) return;
|
|
move(i, i - 1);
|
|
},
|
|
[move]
|
|
);
|
|
|
|
const moveDown = React.useCallback(
|
|
(i: number) => {
|
|
const len = length();
|
|
if (i < 0 || i >= len - 1) return;
|
|
move(i, i + 1);
|
|
},
|
|
[move, length]
|
|
);
|
|
|
|
const onTabFromLastCell = React.useCallback(
|
|
(rowIndex: number) => {
|
|
const len = length();
|
|
if (rowIndex === len - 1) {
|
|
addEmpty(true);
|
|
focusRowFirstField(len);
|
|
} else {
|
|
focusRowFirstField(rowIndex + 1);
|
|
}
|
|
},
|
|
[length, addEmpty, focusRowFirstField]
|
|
);
|
|
|
|
const onShiftTabFromFirstCell = React.useCallback(
|
|
(rowIndex: number) => {
|
|
if (rowIndex <= 0) return;
|
|
focusRowFirstField(rowIndex - 1);
|
|
},
|
|
[focusRowFirstField]
|
|
);
|
|
|
|
return {
|
|
fieldArray: fa, // { fields, append, remove, insert, move, ... }
|
|
addEmpty,
|
|
duplicate,
|
|
remove,
|
|
moveUp,
|
|
moveDown,
|
|
onTabFromLastCell,
|
|
onShiftTabFromFirstCell,
|
|
focusRowFirstField,
|
|
};
|
|
}
|