React 漂亮的 DnD 拖出位置问题
React beautiful DnD drag out of position problem
我创建了一个带有可拖动行的可拖动拖放 table。
为此,我正在使用 react beautiful-dnd
。
当我拖动一行时,该行偏离了我光标所在的位置。
当我拖动一行时,该行得到 position: fixed
和一些 top
和 left
样式。
我怀疑是这个问题,但为什么它得到错误的数字,导致它没有显示在正确的位置?
这个GIF会显示问题。
这是我的完整代码:
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);
有谁知道我的可拖动项为什么会错位?
我使用的唯一 css 是 bootstrap
和我代码中的那些。
我在使用 react-beautiful-dnd
时发生了类似的事情。就我而言,原因是我有两个具有相同 ID 的项目。
我遇到了同样的问题,我解决了! :-)
可以在这里找到解决方案:https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/patterns/using-a-portal.md
基本上,如 OP 所述,当图书馆使用 position: fixed
时,有时会出现一些意想不到的后果 - 在这些情况下,您需要使用门户网站。
我通过查看此处的门户示例使其工作:https://github.com/atlassian/react-beautiful-dnd/blob/master/stories/src/portal/portal-app.jsx
由于这条评论找到了解决方案:https://github.com/atlassian/react-beautiful-dnd/issues/485#issuecomment-385816391
在我的例子中,问题是 Draggable 的父元素之一在“关键帧”动画中有“变换”css 属性。删除它解决了问题。
用 覆盖可拖动元素的 position: 'fixed'
对我的情况有所帮助。
如@Glib 所述,在孔树中搜索具有 transition
或 transform
属性的元素并将其删除。如果您更新遗留代码或集成到其他库,可能会有您不知道的具有这些属性的顶级元素。
我一直碰到这个线程,所以这是另一个(非常简单的)错误,会导致这种行为:
您将 {provided.placeholder}
放在错误的位置或根本没有,或者次数不够 :).
示例(您有嵌套设置):
<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>
所以“通常”您只需要 1 个占位符作为每个可放置项的最后一个标签(通常在可拖动项的正下方)。所以如果你有一个嵌套在另一个的 droppable 你只需要两个对吗?没有。您需要三个,因为您希望每个 droppable 处理它自己的 dragables 并且还想拖动第二级 droppables 并且它们需要在拖动过程中结束在某个地方。
缺少它会导致与发布的 gif 中非常相似的问题,其中可以拖动所有项目,但是当您将它们移动到他们期望占位符但没有占位符的地方时,它们会从屏幕上剪下来。
多一个
在类似的嵌套情况下,您想在 Droppable-s 上整理您的类型属性
来自 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'.
我遇到了类似的问题(父元素的转换应用于我的 <Draggable/>
)。我使用 Cloning API 将我的 <Draggable/>
重新定位到正确的 DOM 位置,同时发生拖动。 react-beautiful-dnd
现在推荐使用此方法创建您自己的门户。
通过从父元素中删除 transform
进行修复
如果任何父元素将 transform
规则设置为 none
以外的任何内容,即使在父元素上设置 will-change: transform
也会导致此问题
我发现,Chrome 开发工具在这种情况下可以提供很大的帮助,可以找到具有以下规则的父元素:
转到元素 -> 样式 -> 计算 -> 过滤器 transform
并在所有父元素中搜索可能导致此问题的任何规则
通过重新设置父级来修复<Draggable />
如果删除此规则不是一个选项,图书馆也有解决方案,因为您可以 reparent a or use a portal in React,(但他们不推荐)
当我尝试在 react-modal
中显示 react-beautiful-dnd
时,我遇到了同样的位置问题,我通过将这些 CSS 添加到可拖动项目找到了解决方案。
.draggable {
top: auto !important;
left: auto !important;
}
如果您还没有为可拖动组件设置 key 属性,那么设置它可能会解决问题。它对我有用。
我创建了一个带有可拖动行的可拖动拖放 table。
为此,我正在使用 react beautiful-dnd
。
当我拖动一行时,该行偏离了我光标所在的位置。
当我拖动一行时,该行得到 position: fixed
和一些 top
和 left
样式。
我怀疑是这个问题,但为什么它得到错误的数字,导致它没有显示在正确的位置?
这个GIF会显示问题。
这是我的完整代码:
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);
有谁知道我的可拖动项为什么会错位?
我使用的唯一 css 是 bootstrap
和我代码中的那些。
我在使用 react-beautiful-dnd
时发生了类似的事情。就我而言,原因是我有两个具有相同 ID 的项目。
我遇到了同样的问题,我解决了! :-)
可以在这里找到解决方案:https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/patterns/using-a-portal.md
基本上,如 OP 所述,当图书馆使用 position: fixed
时,有时会出现一些意想不到的后果 - 在这些情况下,您需要使用门户网站。
我通过查看此处的门户示例使其工作:https://github.com/atlassian/react-beautiful-dnd/blob/master/stories/src/portal/portal-app.jsx
由于这条评论找到了解决方案:https://github.com/atlassian/react-beautiful-dnd/issues/485#issuecomment-385816391
在我的例子中,问题是 Draggable 的父元素之一在“关键帧”动画中有“变换”css 属性。删除它解决了问题。
用 position: 'fixed'
对我的情况有所帮助。
如@Glib 所述,在孔树中搜索具有 transition
或 transform
属性的元素并将其删除。如果您更新遗留代码或集成到其他库,可能会有您不知道的具有这些属性的顶级元素。
我一直碰到这个线程,所以这是另一个(非常简单的)错误,会导致这种行为:
您将 {provided.placeholder}
放在错误的位置或根本没有,或者次数不够 :).
示例(您有嵌套设置):
<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>
所以“通常”您只需要 1 个占位符作为每个可放置项的最后一个标签(通常在可拖动项的正下方)。所以如果你有一个嵌套在另一个的 droppable 你只需要两个对吗?没有。您需要三个,因为您希望每个 droppable 处理它自己的 dragables 并且还想拖动第二级 droppables 并且它们需要在拖动过程中结束在某个地方。
缺少它会导致与发布的 gif 中非常相似的问题,其中可以拖动所有项目,但是当您将它们移动到他们期望占位符但没有占位符的地方时,它们会从屏幕上剪下来。
多一个在类似的嵌套情况下,您想在 Droppable-s 上整理您的类型属性 来自 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'.
我遇到了类似的问题(父元素的转换应用于我的 <Draggable/>
)。我使用 Cloning API 将我的 <Draggable/>
重新定位到正确的 DOM 位置,同时发生拖动。 react-beautiful-dnd
现在推荐使用此方法创建您自己的门户。
通过从父元素中删除 transform
进行修复
如果任何父元素将 transform
规则设置为 none
以外的任何内容,即使在父元素上设置 will-change: transform
也会导致此问题
我发现,Chrome 开发工具在这种情况下可以提供很大的帮助,可以找到具有以下规则的父元素:
转到元素 -> 样式 -> 计算 -> 过滤器 transform
并在所有父元素中搜索可能导致此问题的任何规则
通过重新设置父级来修复<Draggable />
如果删除此规则不是一个选项,图书馆也有解决方案,因为您可以 reparent a or use a portal in React,(但他们不推荐)
当我尝试在 react-modal
中显示 react-beautiful-dnd
时,我遇到了同样的位置问题,我通过将这些 CSS 添加到可拖动项目找到了解决方案。
.draggable {
top: auto !important;
left: auto !important;
}
如果您还没有为可拖动组件设置 key 属性,那么设置它可能会解决问题。它对我有用。