8
votes

Réagissez au magnifique problème de traînée DnD hors de position

J'ai créé une table glisser-déposer avec des lignes déplaçables.
J'utilise react beautiful-dnd pour cela.
Lorsque je fais glisser une ligne, la ligne sort de sa position à la place de la position de mon curseur.
Lorsque je fais glisser une ligne, la ligne obtient la position: fixed et un style en top et à left .
Je soupçonne que c'est le problème, mais pourquoi a-t-il les mauvais numéros, de sorte que cela cause de ne pas apparaître sur la bonne position?
Ce GIF montrera le problème.
entrez la description de l'image ici

Voici mon code complet:

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

Est-ce que quelqu'un sait pourquoi mon objet déplaçable est déplacé?
Le seul css que j'utilise est le bootstrap et ceux de mon code.


0 commentaires

8 Réponses :


6
votes

Une chose similaire m'est arrivée lors de l'utilisation de react-beautiful-dnd . Dans mon cas, la raison était que j'avais deux éléments qui avaient le même identifiant.


0 commentaires

5
votes

J'ai eu le même problème et je l'ai compris! :-)

La solution peut être trouvée ici: https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/patterns/using-a-portal.md

Fondamentalement, lorsque la bibliothèque utilise position: fixed comme OP mentionné, il y a parfois des conséquences inattendues - et dans ces cas, vous devez utiliser le portail.

Je l'ai fait fonctionner en regardant l'exemple de portail ici: https://github.com/atlassian/react-beautiful-dnd/blob/master/stories/src/portal/portal-app.jsx

solution trouvée grâce à ce commentaire: https://github.com/atlassian/react-beautiful-dnd/issues/485#issuecomment-385816391


2 commentaires

Veuillez mettre à jour les liens github.com/atlassian/react-beautiful-dnd/blob/master/docs/… , ils mènent à un 404.


La solution doit être mise à jour, son lien ne fonctionne pas.



3
votes

Dans mon cas, le problème était que l'un des éléments parents du Draggable avait la propriété css "transform", à l'intérieur de l'animation "keyframes". Le supprimer a résolu le problème.


0 commentaires

4
votes

Remplacer la position: 'fixed' l'élément déplaçable position: 'fixed' avec position: 'static' aidé dans mon cas.


3 commentaires

Avez-vous une chance de montrer comment vous avez fait cela? où puis-je remplacer?


Vous pouvez trouver un autre exemple dans la réponse liée, mais en général, c'est quelque chose comme: <Draggable ... style={(_isDragging, draggableStyle) => ({ ...draggableStyle, position: 'static' })} />


Thx pour la réponse ... Je l'ai essayé, mais ne fonctionne pas pour moi. J'ai un div parent qui utilise une transformation, ce qui cause le problème. Je ne peux pas comprendre comment mettre en œuvre la solution de portail ... j'aurais aimé qu'elle soit mieux documentée.



0
votes

Recherchez dans l'arborescence des trous des éléments avec transition attributs de transition ou de transform comme @Glib mentionné et supprimez-les. Si vous mettez à jour du code hérité ou si vous intégrez à d'autres bibliothèques, il se peut que vous ne connaissiez pas les principaux éléments avec ces attributs.


0 commentaires

0
votes

Je continue à me heurter à ce fil, alors voici une autre erreur (très simple) qui provoquera un tel comportement: vous mettez le {provided.placeholder} au mauvais endroit ou pas du tout, ou pas assez de fois :).

Exemple (vous avez une configuration imbriquée):

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

Donc, "normalement", vous n'avez besoin que d'un espace réservé comme dernière balise de chaque droppable (généralement juste en dessous des éléments déplaçables). Donc, si vous avez un droppable imbriqué dans un autre, vous n'avez besoin que de deux non? Nan. Vous en avez besoin de trois, car vous voulez que chaque droppable gère ses propres dragables ET que vous voulez également faire glisser les droppables de deuxième niveau et qu'ils doivent se retrouver quelque part pendant le glissement.

Si vous ne le faites pas, cela causera un problème très similaire à celui du gif publié, où tous les éléments peuvent être glissés mais ils se détachent de l'écran lorsque vous les déplacez quelque part où ils attendent un espace réservé, mais il n'y en a pas.

Un de plus

Dans un cas imbriqué similaire, vous souhaitez trier votre attribut de type sur Droppable-s à partir de documents :

type: Un TypeId (chaîne) qui peut être utilisé pour accepter simplement uniquement la classe spécifiée de <Draggable /> . <Draggable /> héritent toujours du type du <Droppable /> ils sont définis. Par exemple, si vous utilisez le type PERSON, il autorisera uniquement les <Draggable /> de type PERSON à être déposés sur lui-même. <Draggable /> de type TASK ne pourraient pas être déposés sur un <Droppable /> de type PERSON. Si aucun type n'est fourni, il sera défini sur «DEFAULT».


0 commentaires

0
votes

J'ai eu un problème similaire (il y avait une transformation d'un élément parent appliqué à mon <Draggable/> ). Je l'ai résolu en utilisant l' API de clonage pour reparent mon <Draggable/> dans l'emplacement DOM correct pendant qu'un glissement se produit. react-beautiful-dnd recommande maintenant cette méthode plutôt que de créer votre propre portail.


0 commentaires

0
votes

Correction en supprimant la transform d'un élément parent

Si un élément parent a une règle de transform définie sur autre chose que none , mais aussi peu que will-change: transform sur un élément parent provoquera ce problème

J'ai découvert que Chrome Dev Tools peut être d'une grande aide dans ce cas, pour trouver un élément parent avec une telle règle:

Allez dans Éléments -> Styles -> Calculé -> filtrez la transform et recherchez dans tous les éléments parents toutes les règles qui pourraient en être la cause

Correction en reparentant <Draggable />

Si la suppression de cette règle n'est pas une option, la bibliothèque a également une solution pour cela, car vous pouvez réparer un ou utiliser un portail dans React , (ce qu'ils ne recommandent cependant pas)


0 commentaires