From 5a074b5bb68c675c484a7b693f67a67488be9bcf Mon Sep 17 00:00:00 2001 From: Travis Lane <63308171+Tormak9970@users.noreply.github.com> Date: Tue, 21 Feb 2023 22:37:26 -0500 Subject: [PATCH] feat: added reorderable list and updated fieldProps (#57) --- src/custom-components/ReorderableList.tsx | 174 ++++++++++++++++++++++ src/custom-components/index.ts | 1 + 2 files changed, 175 insertions(+) create mode 100644 src/custom-components/ReorderableList.tsx diff --git a/src/custom-components/ReorderableList.tsx b/src/custom-components/ReorderableList.tsx new file mode 100644 index 0000000..06eb023 --- /dev/null +++ b/src/custom-components/ReorderableList.tsx @@ -0,0 +1,174 @@ +import { Fragment, JSXElementConstructor, ReactElement, useEffect, useState } from 'react'; + +import { Field, FieldProps, Focusable, GamepadButton } from '../deck-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: string; + 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); + } + } + + 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} + +
+ ); +} diff --git a/src/custom-components/index.ts b/src/custom-components/index.ts index 7035898..94d0a7b 100644 --- a/src/custom-components/index.ts +++ b/src/custom-components/index.ts @@ -1,2 +1,3 @@ export * from './SuspensefulImage'; export * from './ColorPickerModal'; +export * from './ReorderableList';