Initial implementation

This commit is contained in:
Nathan Bierema 2023-08-07 11:19:42 -04:00
parent 29c1d4ba45
commit 968e6c9e35

View File

@ -1,7 +1,32 @@
import React, { useCallback, useLayoutEffect, useRef } from 'react'; import React, {
forwardRef,
ReactNode,
useCallback,
useLayoutEffect,
useRef,
useState,
} from 'react';
import { Action } from 'redux'; import { Action } from 'redux';
import { PerformAction } from '@redux-devtools/core'; import { PerformAction } from '@redux-devtools/core';
import { StylingFunction } from 'react-base16-styling'; import { StylingFunction } from 'react-base16-styling';
import {
closestCenter,
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
SortableContext,
sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import ActionListRow from './ActionListRow'; import ActionListRow from './ActionListRow';
import ActionListHeader from './ActionListHeader'; import ActionListHeader from './ActionListHeader';
@ -65,9 +90,13 @@ export default function ActionListFunction<A extends Action<unknown>>({
onSweep, onSweep,
onJumpToState, onJumpToState,
lastActionId, lastActionId,
onReorderAction,
}: Props<A>) { }: Props<A>) {
const nodeRef = useRef<HTMLDivElement | null>(null); const nodeRef = useRef<HTMLDivElement | null>(null);
const prevLastActionId = useRef<number | undefined>(); const prevLastActionId = useRef<number | undefined>();
const [activeDragActionId, setActiveDragActionId] = useState<number | null>(
null,
);
useLayoutEffect(() => { useLayoutEffect(() => {
if (nodeRef.current && prevLastActionId.current !== lastActionId) { if (nodeRef.current && prevLastActionId.current !== lastActionId) {
@ -88,6 +117,28 @@ export default function ActionListFunction<A extends Action<unknown>>({
nodeRef.current = node; nodeRef.current = node;
}, []); }, []);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
const handleDragStart = useCallback(({ active }: DragStartEvent) => {
setActiveDragActionId(active.id as number);
}, []);
const handleDragEnd = useCallback(
({ active, over }: DragEndEvent) => {
if (over && active.id !== over.id) {
onReorderAction(active.id as number, over.id as number);
}
setActiveDragActionId(null);
},
[onReorderAction],
);
const lowerSearchValue = searchValue && searchValue.toLowerCase(); const lowerSearchValue = searchValue && searchValue.toLowerCase();
const filteredActionIds = searchValue const filteredActionIds = searchValue
? actionIds.filter( ? actionIds.filter(
@ -98,6 +149,35 @@ export default function ActionListFunction<A extends Action<unknown>>({
) )
: actionIds; : actionIds;
function renderActionListRow(actionId: number) {
return (
<ActionListRow
styling={styling}
actionId={actionId}
isInitAction={!actionId}
isSelected={
(startActionId !== null &&
actionId >= startActionId &&
actionId <= (selectedActionId as number)) ||
actionId === selectedActionId
}
isInFuture={
actionIds.indexOf(actionId) > actionIds.indexOf(currentActionId)
}
onSelect={(e: React.MouseEvent<HTMLDivElement>) =>
onSelect(e, actionId)
}
timestamps={getTimestamps(actions, actionIds, actionId)}
action={actions[actionId].action}
onToggleClick={() => onToggleAction(actionId)}
onJumpClick={() => onJumpToState(actionId)}
onCommitClick={() => onCommit()}
hideActionButtons={hideActionButtons}
isSkipped={skippedActionIds.indexOf(actionId) !== -1}
/>
);
}
return ( return (
<div <div
key="actionList" key="actionList"
@ -121,35 +201,58 @@ export default function ActionListFunction<A extends Action<unknown>>({
data-testid="actionListRows" data-testid="actionListRows"
{...styling('actionListRows')} {...styling('actionListRows')}
ref={setNodeRef} ref={setNodeRef}
>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<SortableContext
items={filteredActionIds}
strategy={verticalListSortingStrategy}
> >
{filteredActionIds.map((actionId) => ( {filteredActionIds.map((actionId) => (
<ActionListRow <SortableItem key={actionId} actionId={actionId}>
key={actionId} {renderActionListRow(actionId)}
styling={styling} </SortableItem>
actionId={actionId}
isInitAction={!actionId}
isSelected={
(startActionId !== null &&
actionId >= startActionId &&
actionId <= (selectedActionId as number)) ||
actionId === selectedActionId
}
isInFuture={
actionIds.indexOf(actionId) > actionIds.indexOf(currentActionId)
}
onSelect={(e: React.MouseEvent<HTMLDivElement>) =>
onSelect(e, actionId)
}
timestamps={getTimestamps(actions, actionIds, actionId)}
action={actions[actionId].action}
onToggleClick={() => onToggleAction(actionId)}
onJumpClick={() => onJumpToState(actionId)}
onCommitClick={() => onCommit()}
hideActionButtons={hideActionButtons}
isSkipped={skippedActionIds.indexOf(actionId) !== -1}
/>
))} ))}
</SortableContext>
<DragOverlay>
{activeDragActionId ? (
<Item>{renderActionListRow(activeDragActionId)}</Item>
) : null}
</DragOverlay>
</DndContext>
</div> </div>
</div> </div>
); );
} }
interface SortableItemProps {
readonly children: ReactNode;
readonly actionId: number;
}
function SortableItem({ children, actionId }: SortableItemProps) {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: actionId });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<Item ref={setNodeRef} style={style} {...attributes} {...listeners}>
{children}
</Item>
);
}
const Item = forwardRef<
HTMLDivElement,
React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
>(function Item({ ...props }, ref) {
return <div {...props} ref={ref} />;
});