From 5adc5e14eda875438303a579344c818588b71d46 Mon Sep 17 00:00:00 2001 From: Tormak <63308171+Tormak9970@users.noreply.github.com> Date: Wed, 1 Feb 2023 08:18:55 -0600 Subject: [PATCH] refactor: updated to simpler reorderable list --- src/custom-components/ReorderableList.tsx | 403 +++++++--------------- 1 file changed, 130 insertions(+), 273 deletions(-) diff --git a/src/custom-components/ReorderableList.tsx b/src/custom-components/ReorderableList.tsx index d5a910b..e202355 100644 --- a/src/custom-components/ReorderableList.tsx +++ b/src/custom-components/ReorderableList.tsx @@ -1,291 +1,148 @@ -import React, { Fragment, useEffect, useRef, useState } from "react"; +import { Fragment, JSXElementConstructor, ReactElement, useState } from "react" +import { FaEllipsisH } from "react-icons/fa" +import { DialogButton, Field, Focusable, GamepadButton, gamepadDialogClasses, quickAccessControlsClasses } from "../deck-components" -import { FaEllipsisH, FaArrowsAltV } from "react-icons/fa"; -import { ButtonItem, DialogButton, Field, Focusable, GamepadButton, GamepadEvent } from "../deck-components"; - -interface Positioned { - position: number -} - -export type ReorderableEntry = { - key: string, +export type ReorderableEntry = { label: string, - data: T + data?:T, + position:number } -export type ReorderableEntryProps = { - entry: ReorderableEntry, - index: number, - action: (e: MouseEvent, entry: ReorderableEntry) => void +type ListProps = { + entries: ReorderableEntry[], + onAction: (entryReference: ReorderableEntry) => void, + onSave: (entries: ReorderableEntry[]) => void, + secondButton?: JSXElementConstructor<{entry:ReorderableEntry}> } -export type ReorderableListData = { - [key: string]: ReorderableEntry -} +/** + * A component for creating reorderable lists. + * + * Implementation example can be found {@link https://github.com/Tormak9970/Component-Testing-Plugin/blob/main/src/testing-window/ReorderableListTest.tsx here}. + */ +export function ReorderableList(props: ListProps) { + const [entryList, setEntryList] = useState[]>(props.entries.sort((a:ReorderableEntry, b:ReorderableEntry) => a.position - b.position)); + const [reorderEnabled, setReorderEnabled] = useState(false); -export type ReloadData = { - showReload: boolean, - reload: () => Promise, - reloadLabel?: string -} + function toggleReorderEnabled(): void { + let newReorderValue = !reorderEnabled; + setReorderEnabled(newReorderValue); -type ReorderableListProps = { - data: ReorderableListData, - action: (e: MouseEvent, entry: ReorderableEntry) => void, - onUpdate: (data: { [key: string]: T }) => void, - reloadData: ReloadData -} - -const ELEM_HEIGHT = 32; //height of each ReorderableEntry element - -export function ReorderableList(props: ReorderableListProps) { - let reorderEnabled = useRef(false); - const touchOrigin = useRef({ "x": -1, "y": -1 }); - const mouseOrigin = useRef({ "x": -1, "y": -1 }); - let focusedSide = useRef(false); //false = left, true = right - let focusIdx = useRef(0); - - let data = props.data; - let onUpdate = props.onUpdate; - let dataAsList: ReorderableEntry[] = Object.values(props.data).sort((a, b) => a.data.position - b.data.position);; - - const [update, setUpdate] = useState(0); - - useEffect(() => { - dataAsList = []; - dataAsList = Object.values(props.data).sort((a, b) => a.data.position - b.data.position); - data = props.data; - }); - - function enableReorder() { reorderEnabled.current = true; } - function disabledReorder() { reorderEnabled.current = false; } - function forceUpdate() { setUpdate(update === 0 ? 1 : 0); } - - function ReorderableEntry(props: ReorderableEntryProps) { - const wrapperFocusable = useRef(null); - const reorderBtn = useRef(null); - const optionsBtn = useRef(null); - - let lastEvent = false; - - useEffect(() => { - if (focusIdx.current === props.index) { - if (!focusedSide.current) { - optionsBtn.current?.blur(); - reorderBtn.current?.focus(); - } else { - reorderBtn.current?.blur(); - optionsBtn.current?.focus(); - } - } - }); - - function reorder(down: boolean) { - if ((down && props.entry.data.position != dataAsList.length) || (!down && props.entry.data.position != 1)) { - const thisData = props.entry; - const previous = dataAsList[down ? props.index + 1 : props.index - 1]; - const tmp = thisData.data.position; - thisData.data.position = previous.data.position; - previous.data.position = tmp; - - const refs = data; - refs[thisData.key] = thisData; - refs[previous.key] = previous; - - const toSave: { [key: string]: T } = {}; - Object.values(refs).map((val: ReorderableEntry) => { - toSave[val.key] = val.data; - }); - onUpdate(toSave); - - if (down) { - focusIdx.current++; - } else { - focusIdx.current--; - } - } + if (!newReorderValue){ + props.onSave(entryList); } - - return ( - - {/* @ts-ignore */} - { focusIdx.current = props.index; }} ref={wrapperFocusable} style={{ width: "100%" }}> - { - switch (e.detail.button) { - case GamepadButton.DIR_DOWN: { - if (reorderEnabled.current && props.entry.data.position === dataAsList.length) { - e.preventDefault(); - e.stopImmediatePropagation(); - } - - if (reorderEnabled.current && props.entry.data.position != dataAsList.length) reorder(true); - - if (props.entry.data.position != dataAsList.length) { - focusIdx.current++; - forceUpdate(); - } - break; - } - case GamepadButton.DIR_UP: { - if (reorderEnabled.current && props.entry.data.position === 1) { - e.preventDefault(); - e.stopImmediatePropagation(); - } - - if (reorderEnabled.current && props.entry.data.position != 1) reorder(false); - - if (props.entry.data.position != 1) { - focusIdx.current--; - forceUpdate(); - } - break; - } - case GamepadButton.DIR_LEFT: { - lastEvent = true; - if (focusedSide.current) { - focusedSide.current = false; - } - reorderEnabled.current = false; - } - case GamepadButton.DIR_RIGHT: { - if (!lastEvent) { - if (!focusedSide.current) { - focusedSide.current = true; - } - reorderEnabled.current = false; - } else { - lastEvent = false; - } - } - } - return false; - }} - onMouseMove={(e: React.MouseEvent) => { - // once user has moved height of an entry, swap - if (reorderEnabled.current) { - const dy = e.clientY - mouseOrigin.current.y; - if (Math.abs(dy) >= ELEM_HEIGHT) { - reorder(dy > 0); - mouseOrigin.current = { - "x": e.clientX, - "y": e.clientY, - } - } - } - }} - onTouchMove={(e: React.TouchEvent) => { - if (reorderEnabled.current) { - const dy = e.touches[0].clientY - touchOrigin.current.y; - if (Math.abs(dy) >= ELEM_HEIGHT) { - reorder(dy > 0); - touchOrigin.current = { - "x": e.touches[0].clientX, - "y": e.touches[0].clientY, - } - } - } - }} - > - { - switch (e.detail.button) { - case GamepadButton.OK: { - enableReorder(); - } - } - }} - onButtonUp={(e: GamepadEvent) => { - switch (e.detail.button) { - case GamepadButton.OK: { - disabledReorder(); - } - } - }} - onMouseDown={(e: MouseEvent) => { - mouseOrigin.current = { - "x": e.clientX, - "y": e.clientY, - } - enableReorder(); - }} - onTouchStart={(e: TouchEvent) => { - touchOrigin.current = { - "x": e.touches[0].clientX, - "y": e.touches[0].clientY, - } - enableReorder(); - }} - > - - - { props.action(e, props.entry); }} - ref={optionsBtn} - > - - - - - - ); } return ( -
{ - mouseOrigin.current = { - "x": -1, - "y": -1, - } - disabledReorder(); - }} - onTouchEnd={() => { - touchOrigin.current = { - "x": -1, - "y": -1, - } - disabledReorder(); - }} - > - {dataAsList.length > 0 ? - dataAsList.map((itm: ReorderableEntry, i: number) => ( - - )) : ( -
- No data to display right now. -
- ) + +
+ + { + entryList.map((entry: ReorderableEntry) => ( + + {props.secondButton ? : null} + + )) + } +
); +} + +type ListEntryProps = { + listData: ReorderableEntry[], + entryData: ReorderableEntry, + reorderEntryFunc: CallableFunction, + reorderEnabled: boolean, + onAction: (entryReference: ReorderableEntry) => void, + children:ReactElement|null +} + +function ReorderableItem(props: ListEntryProps) { + 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)); + } + + const baseCssProps = { + display: "flex", + flexDirection: "row", + justifyContent: "space-between", + width: "100%" + }; + + return( + // @ts-ignore + + + {props.children} + props.onAction(props.entryData)} onOKButton={() => props.onAction(props.entryData)}> + + + + + ); } \ No newline at end of file