From 3c171cfb8ff18ed02eeb569a183c9d43fd0b4f57 Mon Sep 17 00:00:00 2001 From: Tormak <63308171+Tormak9970@users.noreply.github.com> Date: Fri, 25 Nov 2022 18:32:55 -0500 Subject: [PATCH 01/15] feat: added reorderable list and updated fieldProps --- package.json | 3 + pnpm-lock.yaml | 15 +- src/custom-components/ReorderableList.tsx | 301 ++++++++++++++++++++++ src/deck-components/Field.tsx | 3 +- 4 files changed, 320 insertions(+), 2 deletions(-) create mode 100644 src/custom-components/ReorderableList.tsx diff --git a/package.json b/package.json index 2f0a6bb..c3837da 100644 --- a/package.json +++ b/package.json @@ -78,5 +78,8 @@ "style": "module", "parser": "typescript" } + }, + "dependencies": { + "react-icons": "^4.6.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7401639..85d30e3 100755 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,6 +16,7 @@ specifiers: minimist: ^1.2.6 prettier: ^2.7.1 prettier-plugin-import-sort: ^0.0.7 + react-icons: ^4.6.0 semantic-release: ^19.0.3 shx: ^0.3.4 ts-jest: ^27.1.4 @@ -25,6 +26,9 @@ specifiers: typedoc-plugin-missing-exports: ^1.0.0 typescript: ^4.6.3 +dependencies: + react-icons: 4.6.0 + devDependencies: '@commitlint/cli': 17.0.2 '@commitlint/config-conventional': 17.0.2 @@ -1793,8 +1797,8 @@ packages: engines: {node: '>=10'} hasBin: true dependencies: - is-text-path: 1.0.1 JSONStream: 1.3.5 + is-text-path: 1.0.1 lodash: 4.17.21 meow: 8.1.2 split2: 3.2.2 @@ -4210,6 +4214,15 @@ packages: strip-json-comments: 2.0.1 dev: true + /react-icons/4.6.0: + resolution: {integrity: sha512-rR/L9m9340yO8yv1QT1QurxWQvWpbNHqVX0fzMln2HEb9TEIrQRGsqiNFQfiv9/JEUbyHmHPlNTB2LWm2Ttz0g==} + peerDependencies: + react: '*' + peerDependenciesMeta: + react: + optional: true + dev: false + /react-is/17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} dev: true diff --git a/src/custom-components/ReorderableList.tsx b/src/custom-components/ReorderableList.tsx new file mode 100644 index 0000000..625d38a --- /dev/null +++ b/src/custom-components/ReorderableList.tsx @@ -0,0 +1,301 @@ +import React, { Fragment, useEffect, useRef, useState } from "react"; + +import { FaEllipsisH, FaArrowsAltV } from "react-icons/fa"; +import { ButtonItem, DialogButton, Field, Focusable } from "../deck-components"; +import { GamepadButton, GamepadEvent } from "../deck-components/FooterLegend" + +interface Positioned { + position: number +} + +export type ReorderableEntry = { + key: string, + label: string, + data: T +} + +export type ReorderableEntryProps = { + entry: ReorderableEntry, + index: number, + action: (e:MouseEvent, entry:ReorderableEntry) => void +} + +export type ReorderableListData = { + [key:string]: ReorderableEntry +} + +export type ReloadData = { + showReload: boolean, + reload: () => Promise, + reloadLabel?: string +} + +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--; + } + } + } + + return ( + +
+ { 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. +
+ ) + } + {props.reloadData.showReload ? ( + + Reload {props.reloadData.reloadLabel} + + ) : ""} +
+
+ ); +} \ No newline at end of file diff --git a/src/deck-components/Field.tsx b/src/deck-components/Field.tsx index efe4703..b2aedef 100644 --- a/src/deck-components/Field.tsx +++ b/src/deck-components/Field.tsx @@ -1,9 +1,10 @@ -import { FC, ReactNode, RefAttributes } from 'react'; +import { CSSProperties, FC, ReactNode, RefAttributes } from 'react'; import { findModuleChild } from '../webpack'; import { FooterLegendProps } from './FooterLegend'; export interface FieldProps extends FooterLegendProps { + style?: CSSProperties, label?: ReactNode; bottomSeparator?: 'standard' | 'thick' | 'none'; description?: ReactNode; From 5b166d6db879200b049e872cba327957ba5fb705 Mon Sep 17 00:00:00 2001 From: Tormak <63308171+Tormak9970@users.noreply.github.com> Date: Sat, 28 Jan 2023 16:53:22 -0600 Subject: [PATCH 02/15] feat: changed ReorderableList to working version --- src/custom-components/ReorderableList.tsx | 517 +++++++++++----------- 1 file changed, 258 insertions(+), 259 deletions(-) diff --git a/src/custom-components/ReorderableList.tsx b/src/custom-components/ReorderableList.tsx index 625d38a..9a74d52 100644 --- a/src/custom-components/ReorderableList.tsx +++ b/src/custom-components/ReorderableList.tsx @@ -1,301 +1,300 @@ import React, { Fragment, useEffect, useRef, useState } from "react"; import { FaEllipsisH, FaArrowsAltV } from "react-icons/fa"; -import { ButtonItem, DialogButton, Field, Focusable } from "../deck-components"; -import { GamepadButton, GamepadEvent } from "../deck-components/FooterLegend" +import { ButtonItem, DialogButton, Field, Focusable, GamepadButton, GamepadEvent } from "../deck-components"; interface Positioned { - position: number + position: number } export type ReorderableEntry = { - key: string, - label: string, - data: T + key: string, + label: string, + data: T } export type ReorderableEntryProps = { - entry: ReorderableEntry, - index: number, - action: (e:MouseEvent, entry:ReorderableEntry) => void + entry: ReorderableEntry, + index: number, + action: (e: MouseEvent, entry: ReorderableEntry) => void } export type ReorderableListData = { - [key:string]: ReorderableEntry + [key: string]: ReorderableEntry } export type ReloadData = { - showReload: boolean, - reload: () => Promise, - reloadLabel?: string + showReload: boolean, + reload: () => Promise, + reloadLabel?: string } type ReorderableListProps = { - data: ReorderableListData, - action: (e:MouseEvent, entry:ReorderableEntry) => void, - onUpdate: (data: {[key:string]:T}) => void, - reloadData: ReloadData + 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 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);; + 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; - const [update, setUpdate] = useState(0); - useEffect(() => { - dataAsList = []; - dataAsList = Object.values(props.data).sort((a, b) => a.data.position - b.data.position); - data = props.data; + if (focusIdx.current === props.index) { + if (!focusedSide.current) { + optionsBtn.current?.blur(); + reorderBtn.current?.focus(); + } else { + reorderBtn.current?.blur(); + optionsBtn.current?.focus(); + } + } }); - 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); + 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; - let lastEvent = false; + const refs = data; + refs[thisData.key] = thisData; + refs[previous.key] = previous; - useEffect(() => { - if (focusIdx.current === props.index) { - if (!focusedSide.current) { - optionsBtn.current?.blur(); - reorderBtn.current?.focus(); - } else { - reorderBtn.current?.blur(); - optionsBtn.current?.focus(); - } - } - }); + const toSave: { [key: string]: T } = {}; + Object.values(refs).map((val: ReorderableEntry) => { + toSave[val.key] = val.data; + }) + onUpdate(toSave); - 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 (down) { + focusIdx.current++; + } else { + focusIdx.current--; } - - return ( - -
- { 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, + 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, + } + } } - disabledReorder(); - }} - onTouchEnd={() => { - touchOrigin.current = { - "x": -1, - "y": -1, + }} + 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, + } + } } - disabledReorder(); - }} + }} > - {dataAsList.length > 0 ? - dataAsList.map((itm: ReorderableEntry, i:number) => ( - - )) : ( -
- No data to display right now. -
- ) - } - {props.reloadData.showReload ? ( - - Reload {props.reloadData.reloadLabel} - - ) : ""} -
-
+ { + 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. +
+ ) + } + {props.reloadData.showReload ? ( + + Reload {props.reloadData.reloadLabel} + + ) : ""} +
+
+ ); } \ No newline at end of file From cea315a52c285b31ad4e5d0a03104c674ae4f7f5 Mon Sep 17 00:00:00 2001 From: Tormak <63308171+Tormak9970@users.noreply.github.com> Date: Sat, 28 Jan 2023 17:19:24 -0600 Subject: [PATCH 03/15] feat: refactoring mostly complete --- src/custom-components/ReorderableList.tsx | 263 +++++++++++----------- 1 file changed, 127 insertions(+), 136 deletions(-) diff --git a/src/custom-components/ReorderableList.tsx b/src/custom-components/ReorderableList.tsx index 9a74d52..d5a910b 100644 --- a/src/custom-components/ReorderableList.tsx +++ b/src/custom-components/ReorderableList.tsx @@ -95,7 +95,7 @@ export function ReorderableList(props: ReorderableListProp const toSave: { [key: string]: T } = {}; Object.values(refs).map((val: ReorderableEntry) => { toSave[val.key] = val.data; - }) + }); onUpdate(toSave); if (down) { @@ -108,163 +108,154 @@ export function ReorderableList(props: ReorderableListProp 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; + {/* @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(); } - 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 (reorderEnabled.current && props.entry.data.position != dataAsList.length) reorder(true); - if (props.entry.data.position != 1) { - focusIdx.current--; - forceUpdate(); - } - break; + if (props.entry.data.position != dataAsList.length) { + focusIdx.current++; + forceUpdate(); } - case GamepadButton.DIR_LEFT: { - lastEvent = true; - if (focusedSide.current) { - focusedSide.current = false; + 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; - } - case GamepadButton.DIR_RIGHT: { - if (!lastEvent) { - if (!focusedSide.current) { - focusedSide.current = true; - } - reorderEnabled.current = false; - } else { - lastEvent = 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) => { + } + 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, } - enableReorder(); - }} - onTouchStart={(e: TouchEvent) => { + } + } + }} + 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, } - enableReorder(); - }} - > - - - { props.action(e, props.entry); }} - ref={optionsBtn} - > - - - - -
+ } + } + }} + > + { + 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, 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 04/15] 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 From c57e0eed3406ec9022f189c923502facbec3aed7 Mon Sep 17 00:00:00 2001 From: Tormak <63308171+Tormak9970@users.noreply.github.com> Date: Wed, 1 Feb 2023 15:25:23 -0600 Subject: [PATCH 05/15] refactor: changes to improve deck cohesion --- src/custom-components/ReorderableList.tsx | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/src/custom-components/ReorderableList.tsx b/src/custom-components/ReorderableList.tsx index e202355..21ab60f 100644 --- a/src/custom-components/ReorderableList.tsx +++ b/src/custom-components/ReorderableList.tsx @@ -1,6 +1,6 @@ import { Fragment, JSXElementConstructor, ReactElement, useState } from "react" import { FaEllipsisH } from "react-icons/fa" -import { DialogButton, Field, Focusable, GamepadButton, gamepadDialogClasses, quickAccessControlsClasses } from "../deck-components" +import { DialogButton, Field, Focusable, GamepadButton } from "../deck-components" export type ReorderableEntry = { label: string, @@ -47,27 +47,12 @@ export function ReorderableList(props: ListProps) { justify-content: flex-start; align-content: stretch; } - .reorderable-list .${quickAccessControlsClasses.PanelSection} { - padding: 0px; - } - - .reorderable-list .${gamepadDialogClasses.FieldChildren} { - margin: 0px 16px; - } - - .reorderable-list .${gamepadDialogClasses.FieldLabel} { - margin-left: 16px; - } .reorderable-list .custom-buttons { width: inherit; height: inherit; display: inherit; } - - .reorderable-list .custom-buttons .${gamepadDialogClasses.FieldChildren} { - margin: 0px 16px; - } `}
(props: ListEntryProps) { {props.children} - props.onAction(props.entryData)} onOKButton={() => props.onAction(props.entryData)}> + props.onAction(props.entryData)} onOKButton={() => props.onAction(props.entryData)}> From 47a6fddc89b8f9110252c5e19a6e95152c367dbf Mon Sep 17 00:00:00 2001 From: Tormak <63308171+Tormak9970@users.noreply.github.com> Date: Wed, 1 Feb 2023 15:33:27 -0600 Subject: [PATCH 06/15] feat: support for user specified icon --- src/custom-components/ReorderableList.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/custom-components/ReorderableList.tsx b/src/custom-components/ReorderableList.tsx index 21ab60f..93e3b97 100644 --- a/src/custom-components/ReorderableList.tsx +++ b/src/custom-components/ReorderableList.tsx @@ -1,6 +1,7 @@ import { Fragment, JSXElementConstructor, ReactElement, useState } from "react" import { FaEllipsisH } from "react-icons/fa" import { DialogButton, Field, Focusable, GamepadButton } from "../deck-components" +import { IconType } from "react-icons" export type ReorderableEntry = { label: string, @@ -12,7 +13,8 @@ type ListProps = { entries: ReorderableEntry[], onAction: (entryReference: ReorderableEntry) => void, onSave: (entries: ReorderableEntry[]) => void, - secondButton?: JSXElementConstructor<{entry:ReorderableEntry}> + secondButton?: JSXElementConstructor<{entry:ReorderableEntry}>, + primaryIcon?:IconType } /** @@ -61,7 +63,7 @@ export function ReorderableList(props: ListProps) { onClick={toggleReorderEnabled}> { entryList.map((entry: ReorderableEntry) => ( - + {props.secondButton ? : null} )) @@ -78,7 +80,8 @@ type ListEntryProps = { reorderEntryFunc: CallableFunction, reorderEnabled: boolean, onAction: (entryReference: ReorderableEntry) => void, - children:ReactElement|null + children:ReactElement|null, + primaryIcon: IconType } function ReorderableItem(props: ListEntryProps) { @@ -125,7 +128,7 @@ function ReorderableItem(props: ListEntryProps) { {props.children} props.onAction(props.entryData)} onOKButton={() => props.onAction(props.entryData)}> - + From dbd01b11cafe9b102cc371b9812f99aec718d106 Mon Sep 17 00:00:00 2001 From: Tormak <63308171+Tormak9970@users.noreply.github.com> Date: Wed, 1 Feb 2023 17:24:17 -0600 Subject: [PATCH 07/15] feat: support for non-interactable reordering --- src/custom-components/ReorderableList.tsx | 27 +++++++++-------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/custom-components/ReorderableList.tsx b/src/custom-components/ReorderableList.tsx index 93e3b97..e1a1667 100644 --- a/src/custom-components/ReorderableList.tsx +++ b/src/custom-components/ReorderableList.tsx @@ -1,7 +1,5 @@ -import { Fragment, JSXElementConstructor, ReactElement, useState } from "react" -import { FaEllipsisH } from "react-icons/fa" -import { DialogButton, Field, Focusable, GamepadButton } from "../deck-components" -import { IconType } from "react-icons" +import { Fragment, JSXElementConstructor, ReactElement, useState } from "react"; +import { Field, FieldProps, Focusable, GamepadButton } from "../deck-components"; export type ReorderableEntry = { label: string, @@ -11,10 +9,9 @@ export type ReorderableEntry = { type ListProps = { entries: ReorderableEntry[], - onAction: (entryReference: ReorderableEntry) => void, onSave: (entries: ReorderableEntry[]) => void, - secondButton?: JSXElementConstructor<{entry:ReorderableEntry}>, - primaryIcon?:IconType + interactables?: JSXElementConstructor<{entry:ReorderableEntry}>, + fieldProps?: FieldProps } /** @@ -63,8 +60,8 @@ export function ReorderableList(props: ListProps) { onClick={toggleReorderEnabled}> { entryList.map((entry: ReorderableEntry) => ( - - {props.secondButton ? : null} + + {props.interactables ? : null} )) } @@ -75,13 +72,12 @@ export function ReorderableList(props: ListProps) { } type ListEntryProps = { + fieldProps?: FieldProps, listData: ReorderableEntry[], entryData: ReorderableEntry, reorderEntryFunc: CallableFunction, reorderEnabled: boolean, - onAction: (entryReference: ReorderableEntry) => void, - children:ReactElement|null, - primaryIcon: IconType + children:ReactElement|null } function ReorderableItem(props: ListEntryProps) { @@ -124,12 +120,9 @@ function ReorderableItem(props: ListEntryProps) { return( // @ts-ignore - - + + {props.children} - props.onAction(props.entryData)} onOKButton={() => props.onAction(props.entryData)}> - - ); From 667933bd7c6c819675af16c3a4efb6f9a095b81f Mon Sep 17 00:00:00 2001 From: Tormak <63308171+Tormak9970@users.noreply.github.com> Date: Wed, 1 Feb 2023 17:42:03 -0600 Subject: [PATCH 08/15] refactor: addressed change reqs --- package.json | 3 --- pnpm-lock.yaml | 13 ------------- src/custom-components/ReorderableList.tsx | 8 +------- 3 files changed, 1 insertion(+), 23 deletions(-) diff --git a/package.json b/package.json index ae8253e..d4ac054 100644 --- a/package.json +++ b/package.json @@ -78,8 +78,5 @@ "style": "module", "parser": "typescript" } - }, - "dependencies": { - "react-icons": "^4.6.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85d30e3..368b7a4 100755 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,6 @@ specifiers: minimist: ^1.2.6 prettier: ^2.7.1 prettier-plugin-import-sort: ^0.0.7 - react-icons: ^4.6.0 semantic-release: ^19.0.3 shx: ^0.3.4 ts-jest: ^27.1.4 @@ -26,9 +25,6 @@ specifiers: typedoc-plugin-missing-exports: ^1.0.0 typescript: ^4.6.3 -dependencies: - react-icons: 4.6.0 - devDependencies: '@commitlint/cli': 17.0.2 '@commitlint/config-conventional': 17.0.2 @@ -4214,15 +4210,6 @@ packages: strip-json-comments: 2.0.1 dev: true - /react-icons/4.6.0: - resolution: {integrity: sha512-rR/L9m9340yO8yv1QT1QurxWQvWpbNHqVX0fzMln2HEb9TEIrQRGsqiNFQfiv9/JEUbyHmHPlNTB2LWm2Ttz0g==} - peerDependencies: - react: '*' - peerDependenciesMeta: - react: - optional: true - dev: false - /react-is/17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} dev: true diff --git a/src/custom-components/ReorderableList.tsx b/src/custom-components/ReorderableList.tsx index e1a1667..d4e0556 100644 --- a/src/custom-components/ReorderableList.tsx +++ b/src/custom-components/ReorderableList.tsx @@ -46,12 +46,6 @@ export function ReorderableList(props: ListProps) { justify-content: flex-start; align-content: stretch; } - - .reorderable-list .custom-buttons { - width: inherit; - height: inherit; - display: inherit; - } `}
= { entryData: ReorderableEntry, reorderEntryFunc: CallableFunction, reorderEnabled: boolean, - children:ReactElement|null + children: ReactElement | null } function ReorderableItem(props: ListEntryProps) { From b146eab8d7e1338afa54a168a2fd895e716c2bb2 Mon Sep 17 00:00:00 2001 From: Tormak <63308171+Tormak9970@users.noreply.github.com> Date: Fri, 3 Feb 2023 15:33:03 -0600 Subject: [PATCH 09/15] fix: list didn't update on prop change --- src/custom-components/ReorderableList.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/custom-components/ReorderableList.tsx b/src/custom-components/ReorderableList.tsx index d4e0556..4e8cc5a 100644 --- a/src/custom-components/ReorderableList.tsx +++ b/src/custom-components/ReorderableList.tsx @@ -1,4 +1,4 @@ -import { Fragment, JSXElementConstructor, ReactElement, useState } from "react"; +import { Fragment, JSXElementConstructor, ReactElement, useEffect, useState } from "react"; import { Field, FieldProps, Focusable, GamepadButton } from "../deck-components"; export type ReorderableEntry = { @@ -23,6 +23,10 @@ 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); + useEffect(() => { + setEntryList(props.entries.sort((a: ReorderableEntry, b: ReorderableEntry) => a.position - b.position)); + }, [props.entries]); + function toggleReorderEnabled(): void { let newReorderValue = !reorderEnabled; setReorderEnabled(newReorderValue); From b1b2f4fa2da755efd65b82b15b52196f89fb09c0 Mon Sep 17 00:00:00 2001 From: Tormak <63308171+Tormak9970@users.noreply.github.com> Date: Sat, 4 Feb 2023 11:39:08 -0600 Subject: [PATCH 10/15] feat: made requested changes and ran prettier --- src/custom-components/ReorderableList.tsx | 142 ++++++++++++---------- 1 file changed, 81 insertions(+), 61 deletions(-) diff --git a/src/custom-components/ReorderableList.tsx b/src/custom-components/ReorderableList.tsx index 4e8cc5a..0c210f7 100644 --- a/src/custom-components/ReorderableList.tsx +++ b/src/custom-components/ReorderableList.tsx @@ -1,26 +1,35 @@ -import { Fragment, JSXElementConstructor, ReactElement, useEffect, useState } from "react"; -import { Field, FieldProps, Focusable, GamepadButton } from "../deck-components"; +import { CSSProperties, Fragment, JSXElementConstructor, ReactElement, useEffect, useState } from 'react'; +import { Field, FieldProps, Focusable, GamepadButton } from '../deck-components'; + +/** + * A ReorderableList entry of type . + */ export type ReorderableEntry = { - label: string, - data?:T, - position:number -} + label: string; + data?: T; + position: number; +}; +/** + * Properties for a ReorderableList component of type . + */ type ListProps = { - entries: ReorderableEntry[], - onSave: (entries: ReorderableEntry[]) => void, - interactables?: JSXElementConstructor<{entry:ReorderableEntry}>, - fieldProps?: FieldProps -} + entries: ReorderableEntry[]; + onSave: (entries: ReorderableEntry[]) => void; + interactables?: JSXElementConstructor<{ entry: ReorderableEntry }>; + fieldProps?: FieldProps; +}; /** * 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}. + * + * See an example implementation {@linkplain 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 [entryList, setEntryList] = useState[]>( + props.entries.sort((a: ReorderableEntry, b: ReorderableEntry) => a.position - b.position), + ); const [reorderEnabled, setReorderEnabled] = useState(false); useEffect(() => { @@ -31,52 +40,58 @@ export function ReorderableList(props: ListProps) { let newReorderValue = !reorderEnabled; setReorderEnabled(newReorderValue); - if (!newReorderValue){ + if (!newReorderValue) { props.onSave(entryList); } } return ( - -
+
- { - entryList.map((entry: ReorderableEntry) => ( - - {props.interactables ? : null} - - )) - } + onSecondaryActionDescription={reorderEnabled ? 'Save Order' : 'Reorder'} + onClick={toggleReorderEnabled} + > + {entryList.map((entry: ReorderableEntry) => ( + + {props.interactables ? : null} + + ))}
); } +/** + * Properties for a ReorderableItem component of type + */ type ListEntryProps = { - fieldProps?: FieldProps, - listData: ReorderableEntry[], - entryData: ReorderableEntry, - reorderEntryFunc: CallableFunction, - reorderEnabled: boolean, - children: ReactElement | null -} + fieldProps?: FieldProps; + listData: ReorderableEntry[]; + entryData: ReorderableEntry; + reorderEntryFunc: CallableFunction; + reorderEnabled: boolean; + children: ReactElement | null; +}; function ReorderableItem(props: ListEntryProps) { const listEntries = props.listData; @@ -91,10 +106,10 @@ function ReorderableItem(props: ListEntryProps) { let targetPosition: number = -1; if (event.detail.button == GamepadButton.DIR_DOWN) { - targetPosition = currentIdxValue.position+1; + targetPosition = currentIdxValue.position + 1; } else if (event.detail.button == GamepadButton.DIR_UP) { - targetPosition = currentIdxValue.position-1; - } + targetPosition = currentIdxValue.position - 1; + } if (targetPosition >= listEntries.length || targetPosition < 0) return; @@ -106,22 +121,27 @@ function ReorderableItem(props: ListEntryProps) { currentIdxValue.position = otherToUpdate.position; otherToUpdate.position = currentPosition; - props.reorderEntryFunc([...listEntries].sort((a:ReorderableEntry, b:ReorderableEntry) => a.position - b.position)); + props.reorderEntryFunc( + [...listEntries].sort((a: ReorderableEntry, b: ReorderableEntry) => a.position - b.position), + ); } - const baseCssProps = { - display: "flex", - flexDirection: "row", - justifyContent: "space-between", - width: "100%" + const baseCssProps: CSSProperties = { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + width: '100%', }; - return( - // @ts-ignore - - - {props.children} - + return ( + + {props.children} ); -} \ No newline at end of file +} From dfcb3bec19900e099ae3766771e120dbd4f229f5 Mon Sep 17 00:00:00 2001 From: Tormak <63308171+Tormak9970@users.noreply.github.com> Date: Tue, 21 Feb 2023 12:22:21 -0600 Subject: [PATCH 11/15] feat: added doc comments --- src/custom-components/ReorderableList.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/custom-components/ReorderableList.tsx b/src/custom-components/ReorderableList.tsx index 0c210f7..8c9445f 100644 --- a/src/custom-components/ReorderableList.tsx +++ b/src/custom-components/ReorderableList.tsx @@ -4,6 +4,9 @@ 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; From b480d397c4251f42c1f24ff5e74322d22e313f05 Mon Sep 17 00:00:00 2001 From: Tormak <63308171+Tormak9970@users.noreply.github.com> Date: Tue, 21 Feb 2023 19:06:08 -0600 Subject: [PATCH 12/15] fix: fixed missing export --- src/custom-components/index.ts | 1 + 1 file changed, 1 insertion(+) 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'; From 53faf55df1484204e276cd21a32703c2d7809332 Mon Sep 17 00:00:00 2001 From: AAGaming Date: Tue, 21 Feb 2023 22:36:35 -0500 Subject: [PATCH 13/15] fix(Field): remove style --- src/deck-components/Field.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/deck-components/Field.tsx b/src/deck-components/Field.tsx index b2aedef..efe4703 100644 --- a/src/deck-components/Field.tsx +++ b/src/deck-components/Field.tsx @@ -1,10 +1,9 @@ -import { CSSProperties, FC, ReactNode, RefAttributes } from 'react'; +import { FC, ReactNode, RefAttributes } from 'react'; import { findModuleChild } from '../webpack'; import { FooterLegendProps } from './FooterLegend'; export interface FieldProps extends FooterLegendProps { - style?: CSSProperties, label?: ReactNode; bottomSeparator?: 'standard' | 'thick' | 'none'; description?: ReactNode; From 26fae13c8ebd3f11f134c3bc0edfc971afd42fff Mon Sep 17 00:00:00 2001 From: AAGaming Date: Tue, 21 Feb 2023 22:36:56 -0500 Subject: [PATCH 14/15] feat(ReorderableList): add animations, clean up --- src/custom-components/ReorderableList.tsx | 62 ++++++++++++++++------- 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/src/custom-components/ReorderableList.tsx b/src/custom-components/ReorderableList.tsx index 8c9445f..06eb023 100644 --- a/src/custom-components/ReorderableList.tsx +++ b/src/custom-components/ReorderableList.tsx @@ -1,4 +1,4 @@ -import { CSSProperties, Fragment, JSXElementConstructor, ReactElement, useEffect, useState } from 'react'; +import { Fragment, JSXElementConstructor, ReactElement, useEffect, useState } from 'react'; import { Field, FieldProps, Focusable, GamepadButton } from '../deck-components'; @@ -16,12 +16,15 @@ export type ReorderableEntry = { /** * Properties for a ReorderableList component of type . + * + * @param animate If the list should animate. @default true */ -type ListProps = { +export type ReorderableListProps = { entries: ReorderableEntry[]; onSave: (entries: ReorderableEntry[]) => void; interactables?: JSXElementConstructor<{ entry: ReorderableEntry }>; fieldProps?: FieldProps; + animate?: boolean; }; /** @@ -29,7 +32,8 @@ type ListProps = { * * See an example implementation {@linkplain https://github.com/Tormak9970/Component-Testing-Plugin/blob/main/src/testing-window/ReorderableListTest.tsx here}. */ -export function ReorderableList(props: ListProps) { +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), ); @@ -69,6 +73,7 @@ export function ReorderableList(props: ListProps) { > {entryList.map((entry: ReorderableEntry) => ( (props: ListProps) { /** * Properties for a ReorderableItem component of type */ -type ListEntryProps = { +export type ReorderableListEntryProps = { fieldProps?: FieldProps; listData: ReorderableEntry[]; entryData: ReorderableEntry; reorderEntryFunc: CallableFunction; reorderEnabled: boolean; + animate: boolean; children: ReactElement | null; }; -function ReorderableItem(props: ListEntryProps) { +function ReorderableItem(props: ReorderableListEntryProps) { + const [isSelected, _setIsSelected] = useState(false); + const [isSelectedLastFrame, setIsSelectedLastFrame] = useState(false); const listEntries = props.listData; function onReorder(e: Event): void { @@ -129,22 +137,38 @@ function ReorderableItem(props: ListEntryProps) { ); } - const baseCssProps: CSSProperties = { - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - width: '100%', - }; + 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 ( - - {props.children} - + setIsSelected(false)} + onGamepadFocus={() => setIsSelected(true)} + > + {props.children} + +
); } From b1591f86bbd36e160818626760e0717ee50878e0 Mon Sep 17 00:00:00 2001 From: Tormak <63308171+Tormak9970@users.noreply.github.com> Date: Wed, 29 Mar 2023 07:10:55 -0500 Subject: [PATCH 15/15] feat: reorderable list now saves on backout --- src/custom-components/ReorderableList.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/custom-components/ReorderableList.tsx b/src/custom-components/ReorderableList.tsx index 06eb023..9e89a4f 100644 --- a/src/custom-components/ReorderableList.tsx +++ b/src/custom-components/ReorderableList.tsx @@ -52,6 +52,13 @@ export function ReorderableList(props: ReorderableListProps) { } } + function saveOnBackout(e: Event) { + const event = e as CustomEvent; + if (event.detail.button == GamepadButton.CANCEL) { + toggleReorderEnabled(); + } + } + return (
(props: ReorderableListProps) { onSecondaryButton={toggleReorderEnabled} onSecondaryActionDescription={reorderEnabled ? 'Save Order' : 'Reorder'} onClick={toggleReorderEnabled} + onButtonDown={saveOnBackout} > {entryList.map((entry: ReorderableEntry) => (