87 lines
3.1 KiB
TypeScript
87 lines
3.1 KiB
TypeScript
/**
|
|
* Center-canvas of the form-builder. Wraps a `DndContext` +
|
|
* `SortableContext` so child `<SortableField/>` rows can be reordered
|
|
* via pointer-drag or arrow-keys (via the bundled `KeyboardSensor`).
|
|
*
|
|
* The DndContext is intentionally kept inside the builder shell — the
|
|
* parent `FormBuilder` component owns the field-array state and feeds
|
|
* it down; this component only emits move events as new arrays so the
|
|
* parent can decide whether to debounce / persist.
|
|
*/
|
|
import type * as React from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import {
|
|
DndContext,
|
|
PointerSensor,
|
|
KeyboardSensor,
|
|
useSensor,
|
|
useSensors,
|
|
closestCenter,
|
|
type DragEndEvent,
|
|
} from "@dnd-kit/core";
|
|
import { SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
|
import type { FormField } from "@toolctrl/form-widget";
|
|
import { SortableField } from "./SortableField";
|
|
|
|
export type CanvasProps = {
|
|
fields: FormField[];
|
|
selectedKey: string | null;
|
|
onSelect: (key: string) => void;
|
|
onReorder: (next: FormField[]) => void;
|
|
onUpdate: (key: string, next: FormField) => void;
|
|
onDelete: (key: string) => void;
|
|
};
|
|
|
|
export function Canvas({
|
|
fields,
|
|
selectedKey,
|
|
onSelect,
|
|
onReorder,
|
|
onUpdate,
|
|
onDelete,
|
|
}: CanvasProps): React.JSX.Element {
|
|
const { t } = useTranslation();
|
|
const sensors = useSensors(
|
|
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
|
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
|
);
|
|
|
|
function handleDragEnd(event: DragEndEvent): void {
|
|
const { active, over } = event;
|
|
if (!over || active.id === over.id) return;
|
|
const oldIndex = fields.findIndex((f) => f.key === active.id);
|
|
const newIndex = fields.findIndex((f) => f.key === over.id);
|
|
if (oldIndex < 0 || newIndex < 0) return;
|
|
const next = fields.slice();
|
|
const [removed] = next.splice(oldIndex, 1);
|
|
next.splice(newIndex, 0, removed);
|
|
onReorder(next);
|
|
}
|
|
|
|
return (
|
|
<section className="flex-1 min-w-0 border rounded bg-gray-50 p-3" aria-label={t("settings.forms.builder.canvas.ariaLabel")}>
|
|
<h3 className="text-sm font-semibold uppercase tracking-wide text-gray-600 mb-2">{t("settings.forms.builder.canvas.title")}</h3>
|
|
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
|
<SortableContext items={fields.map((f) => f.key)} strategy={verticalListSortingStrategy}>
|
|
<div className="space-y-2">
|
|
{fields.length === 0 ? (
|
|
<p className="text-sm text-gray-500 italic">{t("settings.forms.builder.canvas.empty")}</p>
|
|
) : (
|
|
fields.map((field) => (
|
|
<SortableField
|
|
key={field.key}
|
|
field={field}
|
|
isSelected={field.key === selectedKey}
|
|
onSelect={() => onSelect(field.key)}
|
|
onUpdate={(next) => onUpdate(field.key, next)}
|
|
onDelete={() => onDelete(field.key)}
|
|
/>
|
|
))
|
|
)}
|
|
</div>
|
|
</SortableContext>
|
|
</DndContext>
|
|
</section>
|
|
);
|
|
}
|