Add control for moving items up and down a list.
This commit is contained in:
parent
05595fba94
commit
3c9c94957f
4 changed files with 113 additions and 29 deletions
36
frontend/src/components/basics/MoveButtonControl.tsx
Normal file
36
frontend/src/components/basics/MoveButtonControl.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import Button from "./Button.tsx";
|
||||||
|
import {ArrowDown, ArrowUp} from "lucide-react";
|
||||||
|
import {ButtonType} from "./BasicButtonDefinitions.ts";
|
||||||
|
|
||||||
|
type MoveButtonControlProps = {
|
||||||
|
isUpDisabled: boolean;
|
||||||
|
isDownDisabled: boolean;
|
||||||
|
onMoveUp(): void;
|
||||||
|
onMoveDown(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Up and down buttons for reordering list items when not using drag & drop, e.g., on mobile devices.
|
||||||
|
* @param isUpDisabled Indicates whether the up button is enabled
|
||||||
|
* @param isDownDisabled Indicates whether the down button is enabled
|
||||||
|
* @param onMoveUp Method to call when move up is clicked
|
||||||
|
* @param onMoveDown Method to call when move down is clicked
|
||||||
|
*/
|
||||||
|
export function MoveButtonControl({isUpDisabled, isDownDisabled, onMoveUp, onMoveDown}: MoveButtonControlProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col mt-2">
|
||||||
|
<Button
|
||||||
|
icon={ArrowUp}
|
||||||
|
onClick={() => onMoveUp()}
|
||||||
|
buttonType={ButtonType.TransparentButton}
|
||||||
|
disabled={isUpDisabled} // disable if first item
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon={ArrowDown}
|
||||||
|
onClick={() => onMoveDown()}
|
||||||
|
buttonType={ButtonType.TransparentButton}
|
||||||
|
disabled={isDownDisabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,3 @@
|
||||||
/**
|
|
||||||
* Desktop editor using drag-and-drop via @dnd-kit
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {closestCenter, DndContext, PointerSensor, useSensor, useSensors,} from "@dnd-kit/core";
|
import {closestCenter, DndContext, PointerSensor, useSensor, useSensors,} from "@dnd-kit/core";
|
||||||
import {arrayMove, SortableContext, verticalListSortingStrategy,} from "@dnd-kit/sortable";
|
import {arrayMove, SortableContext, verticalListSortingStrategy,} from "@dnd-kit/sortable";
|
||||||
import type {InstructionStepModel} from "../../models/InstructionStepModel";
|
import type {InstructionStepModel} from "../../models/InstructionStepModel";
|
||||||
|
|
@ -16,10 +12,19 @@ type InstructionStepListDesktopEditorProps = {
|
||||||
onChange: (steps: InstructionStepModel[]) => void;
|
onChange: (steps: InstructionStepModel[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Desktop editor using drag-and-drop via @dnd-kit
|
||||||
|
* @param instructionStepList List instruction step models
|
||||||
|
* @param onChange Method to call on any change to the list
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
export function InstructionStepListDesktopEditor({
|
export function InstructionStepListDesktopEditor({
|
||||||
instructionStepList,
|
instructionStepList,
|
||||||
onChange,
|
onChange,
|
||||||
}: InstructionStepListDesktopEditorProps) {
|
}: InstructionStepListDesktopEditorProps) {
|
||||||
|
/* Import methods for handling update of instruction steps as well as adding and removing steps.
|
||||||
|
* Those methods are shared by all versions of the instruction step editor, e.g., mobile and desktop.
|
||||||
|
*/
|
||||||
const {handleUpdate, handleAdd, handleRemove} = instructionStepListEditorMethods(
|
const {handleUpdate, handleAdd, handleRemove} = instructionStepListEditorMethods(
|
||||||
instructionStepList,
|
instructionStepList,
|
||||||
onChange
|
onChange
|
||||||
|
|
@ -27,6 +32,9 @@ export function InstructionStepListDesktopEditor({
|
||||||
|
|
||||||
const sensors = useSensors(useSensor(PointerSensor, {activationConstraint: {distance: 8}}));
|
const sensors = useSensors(useSensor(PointerSensor, {activationConstraint: {distance: 8}}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the drag end event and reorder list
|
||||||
|
*/
|
||||||
const handleDragEnd = (event: any) => {
|
const handleDragEnd = (event: any) => {
|
||||||
const {active, over} = event;
|
const {active, over} = event;
|
||||||
if (active.id !== over.id) {
|
if (active.id !== over.id) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,3 @@
|
||||||
/**
|
|
||||||
* Mobile editor using Up/Down buttons for reordering
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {Plus} from "lucide-react";
|
import {Plus} from "lucide-react";
|
||||||
import type {InstructionStepModel} from "../../models/InstructionStepModel";
|
import type {InstructionStepModel} from "../../models/InstructionStepModel";
|
||||||
import Button from "../basics/Button";
|
import Button from "../basics/Button";
|
||||||
|
|
@ -14,15 +10,28 @@ type InstructionStepListEditorMobileProps = {
|
||||||
onChange: (steps: InstructionStepModel[]) => void;
|
onChange: (steps: InstructionStepModel[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mobile editor using Up/Down buttons for reordering
|
||||||
|
* @param instructionStepList List instruction step models
|
||||||
|
* @param onChange Method to call on any change to the list
|
||||||
|
*/
|
||||||
export function InstructionStepListMobileEditor({
|
export function InstructionStepListMobileEditor({
|
||||||
instructionStepList,
|
instructionStepList,
|
||||||
onChange,
|
onChange,
|
||||||
}: InstructionStepListEditorMobileProps) {
|
}: InstructionStepListEditorMobileProps) {
|
||||||
|
/* Import methods for handling update of instruction steps as well as adding and removing steps.
|
||||||
|
* Those methods are shared by all versions of the instruction step editor, e.g., mobile and desktop.
|
||||||
|
*/
|
||||||
const {handleUpdate, handleAdd, handleRemove} = instructionStepListEditorMethods(
|
const {handleUpdate, handleAdd, handleRemove} = instructionStepListEditorMethods(
|
||||||
instructionStepList,
|
instructionStepList,
|
||||||
onChange
|
onChange
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move an instruction step either up or down in list.
|
||||||
|
* @param index Current index of the step to move
|
||||||
|
* @param direction Direction to move the step in. Either "up" or "down".
|
||||||
|
*/
|
||||||
const moveStep = (index: number, direction: "up" | "down") => {
|
const moveStep = (index: number, direction: "up" | "down") => {
|
||||||
const newIndex = direction === "up" ? index - 1 : index + 1;
|
const newIndex = direction === "up" ? index - 1 : index + 1;
|
||||||
if (newIndex < 0 || newIndex >= instructionStepList.length) return;
|
if (newIndex < 0 || newIndex >= instructionStepList.length) return;
|
||||||
|
|
@ -35,11 +44,13 @@ export function InstructionStepListMobileEditor({
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="section-heading mb-2">Zubereitung</h2>
|
<h2 className="section-heading mb-2">Zubereitung</h2>
|
||||||
|
|
||||||
|
{/* Instruction list */}
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{instructionStepList.map((step, index) => (
|
{instructionStepList.map((step, index) => (
|
||||||
<InstructionStepMobileListItem
|
<InstructionStepMobileListItem
|
||||||
index={index}
|
index={index}
|
||||||
step={step}
|
stepModel={step}
|
||||||
onMove={moveStep}
|
onMove={moveStep}
|
||||||
onUpdate={handleUpdate}
|
onUpdate={handleUpdate}
|
||||||
onRemove={handleRemove}
|
onRemove={handleRemove}
|
||||||
|
|
@ -47,6 +58,8 @@ export function InstructionStepListMobileEditor({
|
||||||
isLast={index === instructionStepList.length - 1}
|
isLast={index === instructionStepList.length - 1}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* Button for adding an additional instruction step */}
|
||||||
<Button
|
<Button
|
||||||
onClick={handleAdd}
|
onClick={handleAdd}
|
||||||
icon={Plus}
|
icon={Plus}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,53 @@
|
||||||
import Button from "../basics/Button.tsx";
|
import Button from "../basics/Button.tsx";
|
||||||
import {ArrowDown, ArrowUp, X} from "lucide-react";
|
import {X} from "lucide-react";
|
||||||
import {ButtonType} from "../basics/BasicButtonDefinitions.ts";
|
import {ButtonType} from "../basics/BasicButtonDefinitions.ts";
|
||||||
import type {InstructionStepModel} from "../../models/InstructionStepModel.ts";
|
import type {InstructionStepModel} from "../../models/InstructionStepModel.ts";
|
||||||
|
import {MoveButtonControl} from "../basics/MoveButtonControl.tsx";
|
||||||
|
|
||||||
type InstructionStepMobileListItemProps = {
|
type InstructionStepMobileListItemProps = {
|
||||||
|
/** Index of the instruction step */
|
||||||
index: number;
|
index: number;
|
||||||
step: InstructionStepModel;
|
/** Model holding the instruction step data, e.g., instruction text */
|
||||||
|
stepModel: InstructionStepModel;
|
||||||
|
/**
|
||||||
|
* Method to call on moving the instruction step.
|
||||||
|
* @param index Current index of the step to move
|
||||||
|
* @param direction Direction to move the step in (either "up" or "down")
|
||||||
|
*/
|
||||||
onMove(index: number, direction: "up" | "down"): void;
|
onMove(index: number, direction: "up" | "down"): void;
|
||||||
onUpdate: (index: number, value: string) => void;
|
/**
|
||||||
|
* Method to call on updating the instruction step.
|
||||||
|
* @param index Index of the step to be updated.
|
||||||
|
* @param instructionText new instruction text
|
||||||
|
*/
|
||||||
|
onUpdate: (index: number, instructionText: string) => void;
|
||||||
|
/**
|
||||||
|
* Method to call on removing the instruction step.
|
||||||
|
* @param index Index of the instruction step to be removed.
|
||||||
|
*/
|
||||||
onRemove: (index: number) => void;
|
onRemove: (index: number) => void;
|
||||||
|
/** Indicates whether this is the first instruction step. Used to restrict movement. */
|
||||||
isFirst: boolean;
|
isFirst: boolean;
|
||||||
|
/** Indicates whether this is the last instruction step. Used to restrict movement. */
|
||||||
isLast: boolean;
|
isLast: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Numbered list item for instructions step list editor for mobile devices.
|
||||||
|
*
|
||||||
|
* Describes a single instruction step of a recipe. As drag & drop usually is a bit clumsy on
|
||||||
|
* mobile devices, the steps can be reordered with small up and down buttons below the step number.
|
||||||
|
* @param index Index of this instruction step
|
||||||
|
* @param stepModel Model containing the data for the current instruction step, e.g., instruction text.
|
||||||
|
* @param onMove Method to call on moving the instruction step.
|
||||||
|
* @param onUpdate Method to call on updating the instruction step.
|
||||||
|
* @param onRemove Method to call on removing the instruction step.
|
||||||
|
* @param isFirst Indicates whether this is the first instruction step. In this case, the step cannot be moved up.
|
||||||
|
* @param isLast Indicates whether this is the last instruction step. In this case, the step cannot be moved down.
|
||||||
|
*/
|
||||||
export function InstructionStepMobileListItem({
|
export function InstructionStepMobileListItem({
|
||||||
index,
|
index,
|
||||||
step,
|
stepModel,
|
||||||
onMove,
|
onMove,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onRemove,
|
onRemove,
|
||||||
|
|
@ -24,33 +56,27 @@ export function InstructionStepMobileListItem({
|
||||||
}: InstructionStepMobileListItemProps) {
|
}: InstructionStepMobileListItemProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={step.id}
|
key={stepModel.id}
|
||||||
className="flex items-start gap-3 bg-gray-50 rounded-xl p-3 shadow-sm"
|
className="flex items-start gap-3 bg-gray-50 rounded-xl p-3 shadow-sm"
|
||||||
>
|
>
|
||||||
|
{/* Left column: Number of the instruction step and move controls */}
|
||||||
<div className="flex flex-col items-center pt-1">
|
<div className="flex flex-col items-center pt-1">
|
||||||
<div className="circular-container">
|
<div className="circular-container">
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col mt-2">
|
<MoveButtonControl
|
||||||
<Button
|
isUpDisabled={isFirst}
|
||||||
icon={ArrowUp}
|
isDownDisabled={isLast}
|
||||||
onClick={() => onMove(index, "up")}
|
onMoveUp={() => onMove(index, "up")}
|
||||||
buttonType={ButtonType.TransparentButton}
|
onMoveDown={() => onMove(index, "down")}
|
||||||
disabled={isFirst} // disable if first item
|
|
||||||
/>
|
/>
|
||||||
<Button
|
|
||||||
icon={ArrowDown}
|
|
||||||
onClick={() => onMove(index, "down")}
|
|
||||||
buttonType={ButtonType.TransparentButton}
|
|
||||||
disabled={isLast} // disable if last item
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Center column: Instruction step */}
|
||||||
<textarea
|
<textarea
|
||||||
className="input-field w-full min-h-[120px] resize-none overflow-hidden"
|
className="input-field w-full min-h-[120px] resize-none overflow-hidden"
|
||||||
placeholder={`Schritt ${index + 1}`}
|
placeholder={`Schritt ${index + 1}`}
|
||||||
value={step.text}
|
value={stepModel.text}
|
||||||
onChange={(e) => onUpdate(index, e.target.value)}
|
onChange={(e) => onUpdate(index, e.target.value)}
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
const el = e.target as HTMLTextAreaElement;
|
const el = e.target as HTMLTextAreaElement;
|
||||||
|
|
@ -59,6 +85,7 @@ export function InstructionStepMobileListItem({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Right column: Remove step button */}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => onRemove(index)}
|
onClick={() => onRemove(index)}
|
||||||
icon={X}
|
icon={X}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue