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}</>; +} | 
