import { Fragment, JSXElementConstructor, ReactElement, ReactNode, useEffect, useState } from 'react'; import { Field, FieldProps, Focusable, GamepadButton } from '../components'; /** * A ReorderableList entry of type . * @param label The name of this entry in the list. * @param data Optional data to connect to this entry. * @param position The position of this entry in the list. */ export type ReorderableEntry = { label: ReactNode; data?: T; position: number; }; /** * Properties for a ReorderableList component of type . * * @param animate If the list should animate. @default true */ export type ReorderableListProps = { entries: ReorderableEntry[]; onSave: (entries: ReorderableEntry[]) => void; interactables?: JSXElementConstructor<{ entry: ReorderableEntry }>; fieldProps?: FieldProps; animate?: boolean; }; /** * A component for creating reorderable lists. * * See an example implementation {@linkplain https://github.com/Tormak9970/Component-Testing-Plugin/blob/main/src/testing-window/ReorderableListTest.tsx here}. */ export function ReorderableList(props: ReorderableListProps) { if (props.animate === undefined) props.animate = true; const [entryList, setEntryList] = useState[]>( [...props.entries].sort((a: ReorderableEntry, b: ReorderableEntry) => a.position - b.position), ); const [reorderEnabled, setReorderEnabled] = useState(false); useEffect(() => { setEntryList([...props.entries].sort((a: ReorderableEntry, b: ReorderableEntry) => a.position - b.position)); }, [props.entries]); function toggleReorderEnabled(): void { let newReorderValue = !reorderEnabled; setReorderEnabled(newReorderValue); if (!newReorderValue) { props.onSave(entryList); } } function saveOnBackout(e: Event) { const event = e as CustomEvent; if (event.detail.button == GamepadButton.CANCEL && reorderEnabled) { setReorderEnabled(!reorderEnabled); props.onSave(entryList); } } return (
{entryList.map((entry: ReorderableEntry) => ( {props.interactables ? : null} ))}
); } /** * Properties for a ReorderableItem component of type */ export type ReorderableListEntryProps = { fieldProps?: FieldProps; listData: ReorderableEntry[]; entryData: ReorderableEntry; reorderEntryFunc: CallableFunction; reorderEnabled: boolean; animate: boolean; children: ReactElement | null; }; function ReorderableItem(props: ReorderableListEntryProps) { const [isSelected, _setIsSelected] = useState(false); const [isSelectedLastFrame, setIsSelectedLastFrame] = useState(false); const listEntries = props.listData; function onReorder(e: Event): void { if (!props.reorderEnabled) return; const event = e as CustomEvent; const currentIdx = listEntries.findIndex((entryData: ReorderableEntry) => entryData === props.entryData); const currentIdxValue = listEntries[currentIdx]; if (currentIdx < 0) return; let targetPosition: number = -1; if (event.detail.button == GamepadButton.DIR_DOWN) { targetPosition = currentIdxValue.position + 1; } else if (event.detail.button == GamepadButton.DIR_UP) { targetPosition = currentIdxValue.position - 1; } if (targetPosition >= listEntries.length || targetPosition < 0) return; let otherToUpdate = listEntries.find((entryData: ReorderableEntry) => entryData.position === targetPosition); if (!otherToUpdate) return; let currentPosition = currentIdxValue.position; currentIdxValue.position = otherToUpdate.position; otherToUpdate.position = currentPosition; props.reorderEntryFunc( [...listEntries].sort((a: ReorderableEntry, b: ReorderableEntry) => a.position - b.position), ); } async function setIsSelected(val: boolean) { _setIsSelected(val); // Wait 3 frames, then set. I have no idea why, but if you dont wait long enough it doesn't work. for (let i = 0; i < 3; i++) await new Promise((res) => requestAnimationFrame(res)); setIsSelectedLastFrame(val); } return (
setIsSelected(false)} onGamepadFocus={() => setIsSelected(true)} > {props.children}
); }