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.
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.
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