Получить положение элемента в DOM на React DnD drop?
Я использую React DnD и Redux (используя Kea) для создания формбуддера. У меня перетащить часть работает нормально, и мне удалось отправить действие, когда элемент удаляется, и я впоследствии визуализирую конструктор, используя состояние, что диспетчеризация изменилась. Тем не менее, чтобы отобразить элементы в правильном порядке, мне (кажется, мне) нужно сохранить позицию отброшенных элементов относительно их братьев и сестер, но я не могу понять, что не является абсолютно безумным. Я экспериментировал с ссылками и запросил DOM с уникальным идентификатором (я знаю, что не должен), но оба подхода кажутся довольно ужасными и даже не работают.
Вот упрощенное представление структуры моего приложения:
@DragDropContext(HTML5Backend)
@connect({ /* redux things */ })
<Builder>
<Workbench tree={this.props.tree} />
<Sidebar fields={this.props.field}/>
</Builder>
Инструментальные средства:
const boxTarget = {
drop(props, monitor, component) {
const item = monitor.getItem()
console.log(component, item.unique, component[item.unique]); // last one is undefined
window.component = component; // doing it manually works, so the element just isn't in the DOM yet
return {
key: 'workbench',
}
},
}
@DropTarget(ItemTypes.FIELD, boxTarget, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}))
export default class Workbench extends Component {
render() {
const { tree } = this.props;
const { canDrop, isOver, connectDropTarget } = this.props
return connectDropTarget(
<div className={this.props.className}>
{tree.map((field, index) => {
const { key, attributes, parent, unique } = field;
if (parent === 'workbench') { // To render only root level nodes. I know how to render the children recursively, but to keep things simple...
return (
<Field
unique={unique}
key={key}
_key={key}
parent={this} // I'm passing the parent because the refs are useless in the Field instance (?) I don't know if this is a bad idea or not
/>
);
}
return null;
}).filter(Boolean)}
</div>,
)
// ...
поле:
const boxSource = {
beginDrag(props) {
return {
key: props._key,
unique: props.unique || shortid.generate(),
attributes: props.attributes,
}
},
endDrag(props, monitor) {
const item = monitor.getItem()
const dropResult = monitor.getDropResult()
console.log(dropResult);
if (dropResult) {
props.actions.onDrop({
item,
dropResult,
});
}
},
}
@connect({ /* redux stuff */ })
@DragSource(ItemTypes.FIELD, boxSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging(),
}))
export default class Field extends Component {
render() {
const { TagName, title, attributes, parent } = this.props
const { isDragging, connectDragSource } = this.props
const opacity = isDragging ? 0.4 : 1
return connectDragSource(
<div
className={classes.frame}
style={{opacity}}
data-unique={this.props.unique || false}
ref={(x) => parent[this.props.unique || this.props.key] = x} // If I save the ref to this instance, how do I access it in the drop function that works in context to boxTarget & Workbench?
>
<header className={classes.header}>
<span className={classes.headerName}>{title}</span>
</header>
<div className={classes.wrapper}>
<TagName {...attributes} />
</div>
</div>
)
}
}
Боковая панель не очень актуальна.
Мое состояние - это плоский массив, состоящий из объектов, которые я могу использовать для визуализации полей, поэтому я переупорядочиваю его в зависимости от положения элементов в DOM.
[
{
key: 'field_type1',
parent: 'workbench',
children: ['DAWPNC'], // If there's more children, "mutate" this according to the DOM
unique: 'AWJOPD',
attributes: {},
},
{
key: 'field_type2',
parent: 'AWJOPD',
children: false,
unique: 'DAWPNC',
attributes: {},
},
]
Соответствующая часть этого вопроса вращается вокруг
const boxTarget = {
drop(props, monitor, component) {
const item = monitor.getItem()
console.log(component, item.unique, component[item.unique]); // last one is undefined
window.component = component; // doing it manually works, so the element just isn't in the DOM yet
return {
key: 'workbench',
}
},
}
Я подумал, что просто получу ссылку на элемент, но в DOM его пока нет. То же самое, если я попытаюсь взломать ReactDOM:
// still inside the drop function, "works" with the timeout, doesn't without, but this is a bad idea
setTimeout(() => {
const domNode = ReactDOM.findDOMNode(component);
const itemEl = domNode.querySelector(`[data-unique="${item.unique}"]`);
const parentEl = itemEl.parentNode;
const index = Array.from(parentEl.children).findIndex(x => x.getAttribute('data-unique') === item.unique);
console.log(domNode, itemEl, index);
});
Как мне добиться того, чего я хочу?
Извиняюсь за непоследовательное использование точек с запятой, я не знаю, чего я хочу от них. Я ненавижу их.
1 ответ
Я думаю, что ключом здесь является понимание того, что Field
компонент может быть как DragSource
и DropTarget
, Затем мы можем определить стандартный набор типов отбрасывания, которые будут влиять на то, как мутирует состояние.
const DropType = {
After: 'DROP_AFTER',
Before: 'DROP_BEFORE',
Inside: 'DROP_INSIDE'
};
After
а также Before
позволит переупорядочить поля, в то время как Inside
позволит вложенность полей (или падение на верстак).
Теперь создатель действия для обработки любой капли будет:
const drop = (source, target, dropType) => ({
type: actions.DROP,
source,
target,
dropType
});
Он просто берет исходный и целевой объекты, а также тип происходящего отбрасывания, который затем будет преобразован в мутацию состояния.
Тип перетаскивания - это на самом деле просто функция целевых границ, позиции перетаскивания и (необязательно) источника перетаскивания, все в контексте определенного DropTarget
тип:
(bounds, position, source) => dropType
Эта функция должна быть определена для каждого типа DropTarget
поддерживается. Это позволило бы каждому DropTarget
поддерживать другой набор типов отбрасывания. Например, Workbench
знает только, как поместить что-то внутрь себя, а не до или после него, поэтому реализация рабочей среды может выглядеть так:
(bounds, position) => DropType.Inside
Для Field
, вы можете использовать логику из примера простой сортировки карт, где верхняя половина DropTarget
переводит на Before
падение в то время как нижняя половина переводится в After
падение:
(bounds, position) => {
const middleY = (bounds.bottom - bounds.top) / 2;
const relativeY = position.y - bounds.top;
return relativeY < middleY ? DropType.Before : DropType.After;
};
Этот подход также означает, что каждый DropTarget
мог справиться с drop()
Спец метод таким же образом:
- получить границы DOM-элемента целевой цели
- получить позицию падения
- рассчитать тип капли из границ, положения и источника
- если произошел какой-либо тип сброса, обработайте действие сброса
С React DnD мы должны быть осторожны, чтобы правильно обрабатывать вложенные цели отбрасывания, так как мы имеем Field
в Workbench
:
const configureDrop = getDropType => (props, monitor, component) => {
// a nested element handled the drop already
if (monitor.didDrop())
return;
// requires that the component attach the ref to a node property
const { node } = component;
if (!node) return;
const bounds = node.getBoundingClientRect();
const position = monitor.getClientOffset();
const source = monitor.getItem();
const dropType = getDropType(bounds, position, source);
if (!dropType)
return;
const { onDrop, ...target } = props;
onDrop(source, target, dropType);
// won't be used, but need to declare that the drop was handled
return { dropped: true };
};
Component
класс в конечном итоге будет выглядеть примерно так:
@connect(...)
@DragSource(ItemTypes.FIELD, {
beginDrag: ({ unique, parent, attributes }) => ({ unique, parent, attributes })
}, dragCollect)
// IMPORTANT: DropTarget has to be applied first so we aren't receiving
// the wrapped DragSource component in the drop() component argument
@DropTarget(ItemTypes.FIELD, {
drop: configureDrop(getFieldDropType)
canDrop: ({ parent }) => parent // don't drop if it isn't on the Workbench
}, dropCollect)
class Field extends React.Component {
render() {
return (
// ref prop used to provide access to the underlying DOM node in drop()
<div ref={ref => this.node = ref}>
// field stuff
</div>
);
}
Пара вещей, чтобы отметить:
Будьте внимательны к порядку декоратора. DropTarget
следует обернуть компонент, затем DragSource
следует обернуть завернутый компонент. Таким образом, у нас есть доступ к правильному component
экземпляр внутри drop()
,
Корневой узел удаляемой цели должен быть узлом собственного элемента, а не узлом пользовательского компонента.
Любой компонент, который будет украшен DropTarget
использующий configureDrop()
потребует, чтобы компонент установил DOM своего корневого узла ref
к node
имущество.
Так как мы обрабатываем падение DropTarget
, DragSource
просто необходимо реализовать beginDrag()
метод, который будет просто возвращать любое состояние, которое вы хотите смешать в состояние вашего приложения.
Последнее, что нужно сделать, это обработать каждый тип капель в вашем редукторе. Важно помнить, что каждый раз, когда вы перемещаете что-либо, вам нужно удалить источник из его текущего родителя (если применимо), а затем вставить его в нового родителя. Каждое действие может изменить состояние до трех элементов, существующего родителя источника (чтобы очистить его children
), источник (назначить его parent
ссылка), а также родитель или цель цели, если Inside
падение (чтобы добавить к его children
).
Вы также можете рассмотреть возможность сделать ваше состояние объектом, а не массивом, с которым может быть проще работать при реализации редуктора.
{
AWJOPD: { ... },
DAWPNC: { ... },
workbench: {
key: 'workbench',
parent: null,
children: [ 'DAWPNC' ]
}
}