26

I created a draggable drag and drop table with draggable rows.
I'm using react beautiful-dnd for this.
When I drag a row, the row gets out of position instead on the position of my cursor.
When I drag a row, the row gets position: fixed and some top and left styling.
I suspect thats the issue, but why does it get the wrong numbers, so that its causing to not show on the right position?
This GIF will show the problem.
enter image description here

This is my full code:

import update from "immutability-helper";
import * as React from "react";
import * as ReactDnD from "react-dnd";
import { WithNamespaces, withNamespaces } from "react-i18next";
import { toastr } from "react-redux-toastr";
import * as HttpHelper from "../../httpHelper";
import { FormState } from "../common/ValidatedForm";
import Addtagmodal from "../common/AttributeModal";
import AttributeModal from "./AttributeModal";
import PreviewModal from "./PreviewModal";
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
/* import locale from "react-json-editor-ajrm/locale/en"; */
type Props = WithNamespaces & {
  id: number;
  displayName: string;
};

interface Fields {
  columns: any;
}

type State = FormState<Fields> & {
  isLoading: boolean,
  canSave: boolean,
  isSaving: boolean,
  possibleTags: any,
  configTagModalActive: boolean,
  previewModalActive: boolean,
  activeTag: any
};
const getItemStyle = (draggableStyle: any) => ({
  ...draggableStyle
});
const Card = (props: any) => {
  const opacitys = props.isDragging ? 0.3 : 1;

  function findindex(val: any) {
    return props.tags.some((item: any) => val === item.name);
  }
  let select;
  let selectStyle = {};
  let tagInputStyle = {};
  if (props.tags.length == 0 || props.tags.length > 3) {
    selectStyle = { border: "0px", outline: "none", width: "100%", height: "20px", backgroundColor: "transparent", zIndex: 0, float: "left", position: "relative" };
    tagInputStyle = {border: "1px solid #ced4da", height: "auto", width: "400px", padding: "8px", minHeight: "38px", background: "white"};
  }
  else {
    selectStyle = { border: "0px", outline: "none", width: "100%", height: "20px", backgroundColor: "transparent", zIndex: 0, float: "left", top: "-20px", position: "relative" };
    tagInputStyle = {border: "1px solid #ced4da", height: "auto", width: "400px", padding: "8px", minHeight: "38px", background: "white", marginTop: "10px"};
  }
  if (props.tags.length < 4) {
    select =
  <select value="" className="autocomplete-select" style={selectStyle} id={props.index} onChange={props.onaddtag}>
    <option value="" disabled ></option>
    {props.possibleTags.map((i: any) =>

      <option value={i.name} disabled={i.uses == 0 || findindex(i.name) == true ? true : false}>{i.name}</option>

    )}
  </select>;
  }
  else {
    select = undefined;
  }
  return (
        <tr ref={props.provided.innerRef}
        {...props.provided.draggableProps} style={getItemStyle(props.provided.draggableProps.style)} className={(props.indexnr % 2 ? "whiterow" : "grayrow")} key={props.indexnr} data-id={props.indexnr} >
          <td {...props.provided.dragHandleProps} style={{width: "50px", textAlign: "center"}}><i className="fa fa-bars" style={{lineHeight: "40px", fontSize: "24px"}}></i></td>
          <td style={{ textAlign: "center", width: "80px" }}>
            <input
              type="checkbox"
              className="flipswitch"
              id={props.index}
              checked={props.export}
              onChange={props.oncheck}
            />
          </td>
          <td>
            <input
              type="text"
              name="caption"
              id={props.index}
              className="form-control"
              value={props.caption}
              onChange={props.ontextupdate}
            />
          </td>
          <td>
            <input
              type="text"
              name="fieldname"
              id={props.index}
              className="form-control"
              value={props.fieldname}
              onChange={props.ontextupdate}
            />
          </td>
          <td style={{width: "400px"}}>
            <div className="tags-input" style={tagInputStyle}>
            {Object.keys(props.tags).map((key, i) =>
              <div key={key} style={{backgroundColor: "#0753ad", height: "20px", borderRadius: "3px", display: "inline-block", padding: "5px", lineHeight: "12px", float: "left", color: "white", marginRight: "5px", fontSize: "10px", width: "90px", position: "relative", zIndex: 20}}>
                {props.tags[i].name} <i className="fa fa-trash" id={props.index} data-key={i} data-name={props.tags[i].name} onClick={props.ondeletetag} style={{float: "right"}} ></i><i className="fa fa-cog" data-id={i} data-parent={props.index} style={{float: "right", marginRight: "5px"}} onClick={props.onConfigButtonClicked}></i>
              </div>
            )}
            {select}
            </div>
           </td>
          <td style={{ textAlign: "center", width: "80px" }}>
          <button onClick={() => props.ondeleterow(props.index)} type="button" style={{padding : "8px 16px" }} className="btn btn-danger btn-rounded"><i className="fa fa-trash"></i></button>
          </td>
        </tr>
  );
};
const reorder = (list: any, startIndex: any, endIndex: any) => {
  const result = Array.from(list);
  const [removed] = result.splice(startIndex, 1);
  console.log(startIndex, endIndex, removed);
  result.splice(endIndex, 0, removed);

  return result;
};
interface SetColumnsResponse extends HttpHelper.ResponseData { columns: any; }

class CrmConnectorColumns extends React.Component<Props, State> {

  constructor(props: Props) {
    super(props);
    this.moveCard = this.moveCard.bind(this);
    this.oncheck = this.oncheck.bind(this);
    this.ontextupdate = this.ontextupdate.bind(this);
    this.ondeleterow = this.ondeleterow.bind(this);
    this.onaddnewrow = this.onaddnewrow.bind(this);
    this.ondeletetag = this.ondeletetag.bind(this);
    this.onaddtag = this.onaddtag.bind(this);
    this.onConfigButtonClicked = this.onConfigButtonClicked.bind(this);
    this.onPreviewButtonClicked = this.onPreviewButtonClicked.bind(this);
    this.onClosePreview = this.onClosePreview.bind(this);
    this.state = {
      isLoading: true,
      isSaving: false,
      canSave: false,
      errorColor: "danger",
      fields: { columns: {} },
      deleteModalActive: false,
      configTagModalActive: false,
      previewModalActive: false,
      activeTag: {name: "", attributes: [{name: "", value: ""}]},
      possibleTags: [
        {name: "SUBTITLE", status: "new", helptexts: [{language: "nl", helptext: "Dit is de subtitel van een record"}], attributes: [], uses: 1},
        {name: "URL", status: "new", helptexts: [{language: "nl", helptext: "De waarde wordt gezien als html link."}], attributes: [{name: "link", status: "new", helptexts: [{language: "nl", helptext: "De link is deze waarde. Voorbeeld waarde is \"http://www.google.nl?search=[naam]\". op de plaats van \"[naam]\" wordt de waarde van het veld \"naam\" ingevuld."}], uses: undefined}]},
        {name: "TITLE", status: "new", helptexts: [{language: "nl", helptext: "Dit is de hoofdtitel van een record"}], attributes: [], uses: 1},
        {name: "PHONE", status: "new", helptexts: [{language: "nl", helptext: "De waarde wordt gezien telefoonnummer"}], attributes: [], uses: undefined},
        {name: "BUTTON", status: "new", helptexts: [{language: "nl", helptext: "Uiterlijk van een knop"}], attributes: [], uses: undefined},
        {name: "EMAIL", status: "new", helptexts: [{language: "nl", helptext: "De waarde wordt gezien e-mail adres"}], attributes: [], uses: undefined},
        {name: "IMAGE", status: "new", helptexts: [{language: "nl", helptext: "De waarde wordt als afbeelding weergegeven"}], attributes: [], uses: undefined},
        {name: "HTML", status: "new", helptexts: [{language: "nl", helptext: "De waarde wordt gezien als HTML"}], attributes: [{name: "HTML code", status: "new", helptexts: [{language: "nl", helptext: "Vul hier je custom HTML code in. De waarde tussen de [] word vervangen door de data."}], uses: undefined}]}
      ]
    };
    this.onDragEnd = this.onDragEnd.bind(this);
  }
  onDragEnd(result: any) {
    // dropped outside the list
    if (!result.destination) {
      return;
    }
    let newlist = [...this.state.fields.columns];
    newlist = reorder(
      newlist,
      result.source.index,
      result.destination.index
    );
    Object.keys(newlist).forEach((nr) => {
      newlist[parseInt(nr, 10)].index = parseInt(nr, 10);
      });
    this.setState({ fields: { columns: newlist } });
    console.log(this.state.fields.columns);
    this.setState({ canSave: true });




  }
  async componentDidMount() {
    console.log("Start select columns");

    const fields = await HttpHelper.getJson<Fields>(`/connectortypes/${this.props.id}/columns`);
    this.setState(prevState => {
      return update(prevState, {
        fields: { $set: fields },
        isLoading: { $set: false },
      });
    });
    for (let i = 0; i < fields.columns.length; i++) {
      fields.columns[i].index = i;
    }
    this.setState({ fields: { columns: fields.columns } });
    const newlist = [...this.state.possibleTags];
    console.log(newlist);
    for (const column of fields.columns) {
      for (const tags of column.tags) {
        const index = newlist.findIndex(item => item.name == tags.name);
        if (newlist[index].uses > 0) {
          newlist[index].uses = 0;
        }
      }
    }
    this.setState({ possibleTags: newlist });
    console.log(this.state.possibleTags);

  }
  moveCard (index: any, indexnr: any) {
    const cards = this.state.fields.columns;
    const sourceCard = cards.find((card: any) => card.index === index);
    const sortCards = cards.filter((card: any) => card.index !== index);
    sortCards.splice(indexnr, 0, sourceCard);
     Object.keys(sortCards).forEach((nr) => {
    sortCards[nr].index = parseInt(nr, 10);
    });
    this.setState({ fields: { columns: sortCards } });
    console.log(this.state.fields.columns);
    this.setState({ canSave: true });
  }
  oncheck(e: any) {
    const cards = this.state.fields.columns;
    cards[e.target.id].export = e.target.checked;
    this.setState({ fields: { columns: cards } });
    console.log(this.state.fields.columns);
    this.setState({ canSave: true });
  }
  ondeleterow(nr: any) {
    console.log(nr);
    const array = [...this.state.fields.columns]; // make a separate copy of the array
    const arrayCopy = array.filter((row: any) => row.index !== nr);
    this.setState({ fields: { columns: arrayCopy }});
    console.log(this.state.fields.columns);
    this.setState({ canSave: true });
  }
  ontextupdate(e: any) {
    const cards = this.state.fields.columns;
    cards[e.target.id][e.target.name] = e.target.value;
    this.setState({ fields: { columns: cards } });
    this.setState({ canSave: true });
  }
  onaddnewrow() {
    const columnsCopy = this.state.fields.columns;
    columnsCopy.push({index: this.state.fields.columns.length, export: true, editable: false, fieldname: "", caption: "", tags: [] });
    this.setState({ fields: { columns: columnsCopy } });
    this.setState({ canSave: true });
  }
  onDragStart = (e: any) => {
    e.dataTransfer.effectAllowed = "move";
    e.dataTransfer.setData("text/html", e.target.parentNode);
    e.dataTransfer.setDragImage(e.target.parentNode, 20, 20);
  }
  ondragOver(e: any) {
    e.preventDefault();
    const columnsCopy = this.state.fields.columns;
    columnsCopy.pop();
    columnsCopy.push({index: e.target.dataset.id, export: true, editable: false, fieldname: "", caption: "", tags: [] });
    this.setState({ fields: { columns: columnsCopy } });
  }
  onaddtag(e: any) {
    function findindex(element: any) {
      return element.name == e.target.value;
    }
    const index = this.state.possibleTags.findIndex(findindex);
    const array = this.state.fields.columns;

    for (const column of array) {

      if (column.index == e.target.id) {
         const newArray = [ ...array[e.target.id].tags, {name: this.state.possibleTags[index].name, attributes: [] } ];
         array[e.target.id].tags = newArray;
      }
      else {
        const newArray = [...column.tags];
        column.tags = newArray;
      }
      this.setState({ fields: { columns: array } });
    }
    this.setState({ canSave: true });
    const tags = this.state.possibleTags;
    if (tags[index].uses > 0) {
      tags[index].uses = 0;
    }
    this.setState({ possibleTags: tags });
  }
  ondeletetag(e: any) {
    const array = this.state.fields.columns;
    for (const column of array) {
      if (column.index == e.target.id) {
        const newlist = [].concat(array[e.target.id].tags); // Clone array with concat or slice(0)
        newlist.splice(e.target.dataset.key, 1);
        array[e.target.id].tags = newlist;
      }
      else {
        const newArray = [...column.tags];
        column.tags = newArray;
      }
    }
    this.setState({ fields: { columns: array } });
    this.setState({ canSave: true });
    function findindex(element: any) {
      return element.name == e.target.dataset.name;
    }
    const index = this.state.possibleTags.findIndex(findindex);
    const tags = this.state.possibleTags;
    if (tags[index].uses == 0) {
      tags[index].uses = 1;
    }
    this.setState({ possibleTags: tags });
  }
  onUpdateAttribute() {
    this.setState({ configTagModalActive: false });
    this.setState({ canSave: true });
  }
  onPreviewButtonClicked() {
    this.setState({ previewModalActive: true });
  }
  onClosePreview() {
    this.setState({ previewModalActive: false });
  }
  onCancelUpdateAttribute() {
    this.setState({ configTagModalActive: false });
  }
  onConfigButtonClicked(e: any) {
    e.preventDefault();
    this.setState({ activeTag: this.state.fields.columns[e.target.dataset.parent].tags[e.target.dataset.id]});
    this.setState({ configTagModalActive: true, errorMessage: undefined });
    console.log(this.state.activeTag);
  }
  onSubmit = (e: any) => {
    e.preventDefault();
    console.log("Start saving changes");
    this.setState({ isSaving: true }, () => {
      if (this.state.fields) {
        HttpHelper.postJson<SetColumnsResponse>(`/connectortypes/${this.props.id}/columns/`, { columns: this.state.fields.columns }).then((responseData) => {
          if (responseData.responseStatus !== undefined && responseData.responseStatus !== null && responseData.responseStatus.message !== null) {
            this.setState({ isSaving: false, errorMessage: responseData.responseStatus.message });
          }
          else {
            this.setState({ canSave: false, isSaving: false, fields: { columns: responseData.columns } }, () => {
              toastr.success(this.props.displayName, this.props.t("columnsUpdated"));
            });
          }
        });
      }
    });
  }
  public render() {
    const columns = this.state.fields.columns || [] ;
    const { t } = this.props;
    return (
    <form>
      <div className="App">
        <main>
          <button onClick={this.onSubmit} className="btn btn-primary" type="submit" style={{float: "right"}} disabled={!this.state.canSave || this.state.isSaving}>{this.state.isSaving ? <i className="fa fa-spinner fa-spin"></i> : ""} {this.props.t("update")}</button><br/><br/>
          <DragDropContext onDragEnd={this.onDragEnd}>
          <Droppable droppableId="droppable">
          {(provided: any) => (
          <table ref={provided.innerRef} className="col-8 table columns" style={{border: "1px solid #dee2e6"}} >
          <thead className="thead-dark" style={{border: "1px solid #1b2847"}}>
          <tr>
          <th colSpan={2}>
            <button onClick={this.onaddnewrow} type="button" style={{padding : "8px 16px" }} className="btn btn-primary btn-rounded"><i className="fa fa-plus"></i> </button>
          </th>
           <th>{t("displayname")}</th>
           <th>Element</th>
           <th>Tags</th>
           <th>
             <button onClick={this.onPreviewButtonClicked} type="button" className="btn btn-primary"  style={{float: "right"}} >Preview</button>
          </th>
          </tr>
        </thead>
        <tbody>
            {Object.keys(columns).map((key, i) => (
              <Draggable key={i} draggableId={key} index={i}>
              {(provided) => (
             <Card
             key={columns[i].index}
             indexnr={i}
             oncheck={this.oncheck}
             ontextupdate={this.ontextupdate}
             ondeleterow={this.ondeleterow}
             ondeletetag={this.ondeletetag}
             onaddtag={this.onaddtag}
             possibleTags={this.state.possibleTags}
             onConfigButtonClicked={this.onConfigButtonClicked}
             onPreviewButtonClicked={this.onPreviewButtonClicked}
             onClosePreview={this.onClosePreview}
             provided={provided}
             {...columns[i]}
           />
           )}
           </Draggable>
           ))}
            </tbody>
          </table>
          )}
         </Droppable>
      </DragDropContext>
        </main>
      </div>
      <AttributeModal
        startAction={this.onUpdateAttribute.bind(this)}
        isOpen={this.state.configTagModalActive}
        headerText={t("header")}
        activeTag={this.state.activeTag}
        addText={t("close")}
        possibleTags={this.state.possibleTags} >
      </AttributeModal>

      <PreviewModal
        startAction={this.onClosePreview.bind(this)}
        isOpen={this.state.previewModalActive}
        headerText="Preview"
        addText={t("close")}
        columns={this.state.fields.columns} >
      </PreviewModal>
    </form>
    );
  }
}

export default withNamespaces("crmConnectorColumns")(CrmConnectorColumns);

Does anyone know why my draggable item gets out of position?
The only css I'm using is bootstrap and the ones in my code.

frogatto
  • 28,539
  • 11
  • 83
  • 129

11 Answers11

16

I had the same position problem when I try to show react-beautiful-dnd inside react-modal and I found a solution by adding these CSS to draggable items.

.draggable {
  top: auto !important;
  left: auto !important;
}
ozgrozer
  • 1,824
  • 1
  • 23
  • 35
11

I had the same issue and I figured it out! :-)

The solution can be found here: https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/patterns/using-a-portal.md

Basically when the library is using position: fixed as OP mentioned, there are are some unintended consequences sometimes - and in those cases you need to use portal.

I got it to work by looking at the portal example here: https://github.com/atlassian/react-beautiful-dnd/blob/master/stories/src/portal/portal-app.jsx

solution found thanks to this comment: https://github.com/atlassian/react-beautiful-dnd/issues/485#issuecomment-385816391

andre
  • 1,660
  • 3
  • 19
  • 31
6

A similar thing happened to me while using react-beautiful-dnd. In my case, the reason was that I had two items which had the same id.

Fl4v
  • 1,024
  • 11
  • 19
Abdo Moha
  • 89
  • 3
  • In that case, when the drag for 1 item is started, it affects the other and then both the items move in sync. The listed issue is different from what you are pointing to. – Neeraj Yadav Feb 27 '23 at 09:25
6

Fix by removing transform from a parent element

If any parent element has transform rule set to anything other than none, but also as little as having will-change: transform on a parent element will cause this issue

I found out, that Chrome Dev Tools can be a great help in this case, to find a parent element with such rule:

Go to Elements -> Styles -> Computed -> filter transform and search through all parent elements for any rules, that might be causing this

Fix by reparenting <Draggable />

If removing this rule isn't an option, the library has a solution for that as well, because you can reparent a or use a portal in React, (which they don't recommend however)

ViRPo
  • 126
  • 2
  • 11
3

In my case, the problem was that one of the Draggable's parent element had the "transform" css property, inside the "keyframes" animation. Removing it solved the issue.

Glib Kozub
  • 31
  • 4
3

Overriding the draggable element's position: 'fixed' with position: 'static' helped in my case.

falsarella
  • 12,217
  • 9
  • 69
  • 115
  • Any chance you can show how you did this? where can I override? – mike hennessy Jun 05 '20 at 20:21
  • 1
    You can find further example in the linked answer, but in general, it's something like: ` ({ ...draggableStyle, position: 'static' })} />` – falsarella Jun 05 '20 at 20:26
  • 1
    Thx for the reply...I tried it, but doesn't work for me. I have a parent div which uses a transform, and that is causing the issue. I can't figure out how to implement the portal solution...wish it was better documented. – mike hennessy Jun 05 '20 at 20:40
  • This introduces other unintended effects like the original droppable not shrinking once the draggable is outside its boundary. Also a lot of flickering comes into picture. – Neeraj Yadav Feb 27 '23 at 09:28
1

Search the hole tree for elements with transition or transform attributes as @Glib mentioned and remove them. If you updating legacy code or integrating to other libraries there might be top elements with these attributes that you are not aware of.

0

I keep bumping into this thread so here is one more (very straightforward) mistake that will cause such behavior: You put the {provided.placeholder} in the wrong place or not at all, or not enough times :).

Example (you have nested setup):

<DragDropContext ...>
    <Droppable ...>
      {(provided) => (
        <div
          ref={provided.innerRef}
        >
          {items.map((item, index) => (
            <Draggable ...>
              {(provided, snapshot) => (
                <div>
                  <div ref={provided.innerRef} ...>
                    <ComponentWithDroppableInsideWithItsOwnPlaceHolder item={item}/>
                  </div>
                  {provided.placeholder} //<--- Observe our "out of place" placeholder
                </div>
              )}
            </Draggable>
          ))}
          {provided.placeholder}
        </div>
      )}
    </Droppable>
  </DragDropContext>

So "normally" you only need 1 placeholder as the last tag of each droppable (usually right bellow the dragable items). So if you have a droppable nested in an other you only need two right? Nope. You need three, because you want each droppable to handle its own dragables AND want to drag the second level droppables as well and they need to end up somewhere during drag.

Missing this will cause a very similar issue as in the posted gif, where all the items can be dragged but they clip out of the screen when you move them somewhere where they expect a placeholder but there isn't one.

One more

In a similar nested case you want to sort out your type attribute on Droppable-s from docs:

type: A TypeId(string) that can be used to simply accept only the specified class of <Draggable />. <Draggable />s always inherit type from the <Droppable /> they are defined in. For example, if you use the type PERSON then it will only allow <Draggable />s of type PERSON to be dropped on itself. <Draggable />s of type TASK would not be able to be dropped on a <Droppable /> with type PERSON. If no type is provided, it will be set to 'DEFAULT'.

Don Kartacs
  • 396
  • 3
  • 8
0

I had a similar issue (there was a transform from a parent element being applied to my <Draggable/>). I solved it using the Cloning API to reparent my <Draggable/> into the correct DOM location while a drag is occurring. react-beautiful-dnd now recommends this method over creating your own portal.

N.Jones
  • 149
  • 1
  • 13
0

If you haven't set the key prop for your draggable component, then setting it might solve the issue. It did for me.

Mahdi Ghajary
  • 2,717
  • 15
  • 20
0

I was getting left align issue, so add left: "auto !important", Now it is working fine.

<Draggable key={index} draggableId={String(index)} index={index}>
  {(provided, snapshot) => (
    <div
      ref={provided.innerRef}
      {...provided.draggableProps}
      {...provided.dragHandleProps}
      style={{
        ...provided.draggableProps.style,
        left: "auto !important",
      }}
    >
      <Stack direction="row" alignItems="center">
        Some thing
      </Stack>
    </div>
  )}
</Draggable>;

I referred this issue https://github.com/atlassian/react-beautiful-dnd/issues/1881#issuecomment-1652549194