Получить положение элемента в 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' ]
  }
}
Другие вопросы по тегам