Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/modules/formWidget/builder/Canvas.tsx
T

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>
);
}