diff options
author | dan <[email protected]> | 2023-08-30 14:41:37 -0400 |
---|---|---|
committer | dan <[email protected]> | 2023-08-30 14:41:37 -0400 |
commit | 7fdb76bca64e2e72768f6de7bfa3fea98d2d5bbb (patch) | |
tree | ad1d59388758eeecce29567342c870b6af8019ea | |
parent | 4c4f90af55fa2e560f3417efd9bd7a106f80b084 (diff) | |
download | draggable-form-demo-7fdb76bca64e2e72768f6de7bfa3fea98d2d5bbb.tar.gz draggable-form-demo-7fdb76bca64e2e72768f6de7bfa3fea98d2d5bbb.tar.bz2 draggable-form-demo-7fdb76bca64e2e72768f6de7bfa3fea98d2d5bbb.zip |
feat: add form builder component
-rw-r--r-- | src/components/FormBuilder/index.js | 315 |
1 files changed, 315 insertions, 0 deletions
diff --git a/src/components/FormBuilder/index.js b/src/components/FormBuilder/index.js new file mode 100644 index 0000000..b9f0389 --- /dev/null +++ b/src/components/FormBuilder/index.js @@ -0,0 +1,315 @@ +import { DragIndicator, Clear } from "@mui/icons-material"; +import { + Card, + CardContent, + CardHeader, + IconButton, + Stack, + TextField, +} from "@mui/material"; +import React, { useRef, useState } from "react"; +export default function FormBuilder() { + return ( + <Stack direction={"column"}> + <h3>FormBuilder</h3> + <Stack + direction={"row"} + justifyContent={"space-between"} + alignItems={"flex-start"} + spacing={2} + > + <Form initialForm={false} editable /> + <WidgetsLibrary /> + </Stack> + </Stack> + ); +} + +const availableWidgets = { + label: { + name: "Label", + element: (props) => ( + <TextField + id="outlined-basic" + variant="standard" + value={props?.name} + onChange={(e) => + props?.setFormField({ ...props, name: e.target.value }) + } + fullWidth + /> + ), + finalElement: (props) => <div>{props?.id}</div>, + }, + number: { + name: "Number", + element: (props) => ( + <TextField + id="outlined-basic" + label={props?.id ? `example field ${props.id}` : "Outlined"} + variant="outlined" + type={"number"} + fullWidth + /> + ), + }, + text: { + name: "Text", + element: (props) => ( + <TextField + id="outlined-basic" + label={props?.id ? `example field ${props.id}` : "Outlined"} + variant="outlined" + fullWidth + /> + ), + }, + multiline: { + name: "Multiline", + element: (props) => ( + <TextField + id="outlined-textarea" + label={props?.id ? `example field ${props.id}` : "Multiline"} + placeholder="Placeholder" + multiline + minRows={4} + fullWidth + /> + ), + }, +}; + +function WidgetsLibrary() { + return ( + <Stack + direction={"column"} + spacing={0.5} + alignItems={"flex-start"} + sx={{ maxHeight: "100vh", height: "100%", overflow: "scroll" }} + > + {Object.keys(availableWidgets).map((k) => { + const props = availableWidgets[k]; + return <WidgetCard key={k} type={k} {...props} />; + })} + </Stack> + ); +} + +function WidgetCard({ name, element, type, finalElement }) { + function onDragStart(e) { + e.dataTransfer.setData("application/formwidgettype", type); + e.dataTransfer.effectAllowed = "copy"; + console.log(e); + } + + const disabledFieldOverlay = { + pointerEvents: "none", + background: "#7773", + zIndex: 10, + }; + if (!element) { + return; + } else { + return ( + <Card + draggable + onDragStart={onDragStart} + sx={{ cursor: "grab", width: "100%" }} + > + <Stack + direction={"row"} + justifyContent={"start"} + alignItems={"center"} + alignContent={"center"} + > + <DragIndicator /> + <div> + <CardHeader title={name} /> + <CardContent> + <div style={disabledFieldOverlay}> + {finalElement ? finalElement() : element()} + </div> + </CardContent> + </div> + </Stack> + </Card> + ); + } +} + +function FormField(props) { + if (!props?.type) { + console.log("formfield render failed", props); + return; + } + const { element: Element } = availableWidgets[props?.type]; + const dragHandleRef = useRef(); + const [target, setTarget] = useState(); + return ( + <Stack + direction={"row"} + sx={{ width: "100%", cursor: "grab" }} + alignItems={"center"} + justifyContent={"space-between"} + draggable + onMouseDown={(e) => setTarget(e.target)} + onDragStart={(e) => { + if (dragHandleRef.current.contains(target)) { + e.dataTransfer.setData("application/formwidgetid", props?.id); + } else { + e.preventDefault(); + } + }} + > + <span ref={dragHandleRef}> + <DragIndicator /> + </span> + <Element {...props} /> + <IconButton onClick={() => props?.deleteFormField()}> + <Clear /> + </IconButton> + </Stack> + ); +} + +function capture(e) { + e.stopPropagation(); + e.preventDefault(); +} + +function DropZone({ insertField, index, moveField }) { + const [dragOver, setDragOver] = useState(false); + return ( + <span + style={{ + marginTop: "2px", + marginBottom: "2px", + width: "100%", + color: "transparent", + background: dragOver ? "#22a9" : "#2221", + height: dragOver ? "2em" : "4px", + borderRadius: 8, + }} + onDragOver={(e) => capture(e) || setDragOver(true)} + onDragExit={() => setDragOver(false)} + onDrop={(e) => { + e.stopPropagation(); + e.preventDefault(); + setDragOver(false); + console.log(e); + if ([...e.dataTransfer.types].includes("application/formwidgettype")) { + const type = e.dataTransfer.getData("application/formwidgettype"); + insertField({ index, type }); + } else if ( + [...e.dataTransfer.types].includes("application/formwidgetid") + ) { + const oldId = e.dataTransfer.getData("application/formwidgetid"); + moveField(oldId, index); + } + }} + /> + ); +} + +function Form({ initialForm, editable }) { + const [form, setForm] = useState({ fields: [], ...initialForm }); + + function updateFormField(newField) { + setForm({ + ...form, + fields: form.fields.map((field) => + field?.id === newField.id ? newField : field, + ), + }); + } + + function deleteFormField(id) { + const newForm = { + ...form, + fields: form.fields.filter((field) => id !== field?.id), + }; + console.log("deleteFormField", { id, form, newForm }); + setForm({ ...newForm }); + } + console.log("render", form.fields); + + function insertField({ index, type }) { + const id = crypto.randomUUID(); + const newField = { + id, + type, + }; + form?.fields?.splice(index + 1, 0, newField); + setForm({ ...form }); + } + + function moveField(oldId, newIndex) { + console.log({ oldId, newIndex }); + const oldIndex = form.fields.findIndex(({ id }) => id === oldId); + const [oldFormField] = form.fields.splice(oldIndex, 1); + if (oldIndex > newIndex) { + newIndex++; + } + form.fields.splice(newIndex, 0, oldFormField); + console.log("newform", form); + setForm({ ...form }); + } + + if (!editable && !initialForm) { + return; + // else if (!initialForm) { + // return <FormInitialPrompt />; + // } + } else { + return ( + <Stack sx={{ width: "100%" }}> + <FormHeader form={form} /> + {editable && ( + <DropZone + moveField={moveField} + insertField={insertField} + index={-1} + /> + )} + {form?.fields?.map((props, i) => ( + <Grouping key={props?.id}> + <FormField + setFormField={(field) => updateFormField(field)} + deleteFormField={() => deleteFormField(props.id)} + {...props} + /> + {editable && ( + <DropZone + moveField={moveField} + insertField={insertField} + index={i} + /> + )} + </Grouping> + ))} + </Stack> + ); + } +} + +function FormHeader() { + return; +} + +//function FormInitialPrompt() { +// return ( +// <div +// style={{ +// minWidth: "50vh", +// minHeight: "50vh", +// background: "#33333333", +// }} +// > +// Drag and drop and field here to get started +// </div> +// ); +//} +// +function Grouping({ children }) { + return <>{children}</>; +} |