feat: added reorderable list and updated fieldProps

This commit is contained in:
Tormak
2022-11-25 18:32:55 -05:00
parent 47fd13692f
commit 3c171cfb8f
4 changed files with 320 additions and 2 deletions

View File

@@ -78,5 +78,8 @@
"style": "module",
"parser": "typescript"
}
},
"dependencies": {
"react-icons": "^4.6.0"
}
}

15
pnpm-lock.yaml generated
View File

@@ -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

View File

@@ -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<T extends Positioned> = {
key: string,
label: string,
data: T
}
export type ReorderableEntryProps<T extends Positioned> = {
entry: ReorderableEntry<T>,
index: number,
action: (e:MouseEvent, entry:ReorderableEntry<T>) => void
}
export type ReorderableListData<T extends Positioned> = {
[key:string]: ReorderableEntry<T>
}
export type ReloadData = {
showReload: boolean,
reload: () => Promise<void>,
reloadLabel?: string
}
type ReorderableListProps<T extends Positioned> = {
data: ReorderableListData<T>,
action: (e:MouseEvent, entry:ReorderableEntry<T>) => void,
onUpdate: (data: {[key:string]:T}) => void,
reloadData: ReloadData
}
const ELEM_HEIGHT = 32; //height of each ReorderableEntry element
export function ReorderableList<T extends Positioned>(props: ReorderableListProps<T>) {
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<T>[] = 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<T>) {
const wrapperFocusable = useRef<HTMLDivElement>(null);
const reorderBtn = useRef<HTMLDivElement>(null);
const optionsBtn = useRef<HTMLDivElement>(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<T>) => {
toSave[val.key] = val.data;
})
onUpdate(toSave);
if (down) {
focusIdx.current++;
} else {
focusIdx.current--;
}
}
}
return (
<Fragment>
<div className="custom-buttons">
<Field label={props.entry.label} onActivate={() => { focusIdx.current = props.index; }} ref={wrapperFocusable} style={{ width: "100%" }}>
<Focusable
style={{
display: "flex",
width: "100%"
}}
onGamepadDirection={(e:GamepadEvent) => {
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<HTMLDivElement>) => {
// 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<HTMLDivElement>) => {
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,
}
}
}
}}
>
<DialogButton
style={{
marginRight: "14px",
minWidth: "30px",
maxWidth: "60px",
display: "flex",
justifyContent: "center",
alignItems: "center"
}}
ref={reorderBtn}
// @ts-ignore
onOKActionDescription={"Hold to reorder items"}
onButtonDown={(e:GamepadEvent) => {
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();
}}
>
<FaArrowsAltV />
</DialogButton>
<DialogButton
style={{
minWidth: "30px",
maxWidth: "60px",
display: "flex",
justifyContent: "center",
alignItems: "center"
}}
onClick={(e:MouseEvent) => {props.action(e, props.entry);}}
ref={optionsBtn}
>
<FaEllipsisH />
</DialogButton>
</Focusable>
</Field>
</div>
</Fragment>
);
}
return (
<Fragment>
<style>{`
.scoper {
width: 100%;
display: flex;
flex-direction: column;
}
`}</style>
<div className="scoper"
onMouseUp={() => {
mouseOrigin.current = {
"x": -1,
"y": -1,
}
disabledReorder();
}}
onTouchEnd={() => {
touchOrigin.current = {
"x": -1,
"y": -1,
}
disabledReorder();
}}
>
{dataAsList.length > 0 ?
dataAsList.map((itm: ReorderableEntry<T>, i:number) => (
<ReorderableEntry entry={itm} index={i} action={props.action} />
)) : (
<div style={{width: "100%", display: "flex", justifyContent: "center", alignItems: "center", padding: "5px"}}>
No data to display right now.
</div>
)
}
{props.reloadData.showReload ? (
<ButtonItem layout="below" onClick={props.reloadData.reload} bottomSeparator='none'>
Reload {props.reloadData.reloadLabel}
</ButtonItem>
) : ""}
</div>
</Fragment>
);
}

View File

@@ -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;