Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"canDrop", how to know what element we are on top of? #439

Closed
Vadorequest opened this issue Apr 14, 2016 · 4 comments
Closed

"canDrop", how to know what element we are on top of? #439

Vadorequest opened this issue Apr 14, 2016 · 4 comments

Comments

@Vadorequest
Copy link

In the drop(props, monitor, component) method, we have three arguments. But in the canDrop(props, monitor) there isn't the component arg.

I want that when an item is dragged over another component I "split" that component in two parts of 50% (height). If the item is dropped in the higher part then he'll be placed above it, if it's dropped in the lower part then it'll be placed after.

I already have the code that detects whether the component is dropped in the top/bottom part. What I want now is to add a class based on whether the mouse is in the top/bottom part (but during the hover action, not the drop action).

I don't know if I can use the hover(props, monitor, component) method, because I don't see how I'm supposed to add a property in my component (which would add a CSS class in the DOM or alike) from it, since it doesn't belong to the component itself and doesn't have access to its context and so on.

I guess I'm missing something. :)

@froatsnook
Copy link
Collaborator

Hey, you're in luck! I've implemented almost exactly this before, so let me throw together some code.

@froatsnook
Copy link
Collaborator

OK, so the idea is that you want to set state in your component, and then when the state is set, you add the class.

Let's say you're dragging a Foo into a Bar:

import React, { Component } from "react";
import classNames from "classnames";

class Bar extends Component {
  constructor(props) {
    super(props);

    this.state = {
      // Where a foo is hovering over the component.  One of null, "above", or "below"
      fooHover: null,
    };
  }

  render() {
    const { fooHover } = this.state;

    const classes = classNames("bar", {
      "bar-above": fooHover === "above",
      "bar-below": fooHover === "below",
    });
  }
}

The only question is how we set that state!

import { findDOMNode } from "react-dom";

const barTarget = {
  ...
  hover(props, monitor, component) {
    if (!monitor.canDrop()) {
      return;
    }

    const rawComponent = undecorate(component); // undecorate described below

    const { y } = monitor.getClientOffset();
    const { top, height } = findDOMNode(component).getBoundingClientRect();

    if (y < top + height/2) {
      rawComponent.setFooHover("above"); // setFooHover described below
    } else {
      rawComponent.setFooHover("below");
    }
  },
  ...
}

So basically, you want to call an instance method of your component, setFooHover, which will call setState. That should look like this for performance reasons:

  // In Bar
  setFooHover(fooHover) {
    if (fooHover !== this.state.fooHover) {
      this.setState({ fooHover });
    }
  }

The reason why I am calling undecorate above is that react-dnd uses higher order components, and we want the unwrapped components. This is an ugly complication of higher order components in my opinion. Maybe you could implement this instead by dispatching an action to your flux store or something, but in case you decide to do it like me, here's my undecorate:

function undecorate(component) {
  let curr = component;
  while (typeof curr.getDecoratedComponentInstance === "function") {
    curr = curr.getDecoratedComponentInstance();
  }

  return curr;
}

Now if you implement this, everything will be looking good except for one small problem. The class never goes away! No one ever calls setHoverState(null) (there's no such thing as an endHover function in your barTarget). The proper way to handle this is to use componentWillReceiveProps like this:

  componentWillReceiveProps(nextProps) {
    if (this.props.isFooOver && !nextProps.isFooOver) {
      this.setState({ fooHover: null });
    }
  }

(Don't forget to add { isFooOver: monitor.isOver() } in your collecting function)

Let me know how it goes!

@Vadorequest
Copy link
Author

Thanks a lot for the detailled explanation! I'll let you know :)

@Vadorequest
Copy link
Author

Finally, I used the Redux approach, so I basically dispatched an event through a reducer when the drop event was fired.

    drop: (propsTargetItem, monitor, targetItem) ->
        draggedItem = monitor.getItem()# Item that has been dragged and eventually dropped.
        coordsDrop = monitor.getClientOffset()# Coords of the drop action.
        diff = monitor.getDifferenceFromInitialOffset()# Comparison of the initial click (dragging start) against the end of the click (dropping start).

        patternId = draggedItem.patternId
        patternPosition = draggedItem.position
        targetId = propsTargetItem.context.patternId
        targetPosition = propsTargetItem.context.position
        isCursorAboveHalf = GeoService.isAboveHalf(ReactDOM.findDOMNode(targetItem), coordsDrop.y)

        # Extract positions in the array of children.
        oldPosition = draggedItem.position# Current position of the item, which will be the old position as soon as the item is moved. (by the server through sockets)
        newPosition = calcDropPosition(targetId, patternId, patternPosition, targetPosition, isCursorAboveHalf)# Position where we will move the dragged item.

        # Dispatch only if the position has changed and is set.
        if oldPosition isnt newPosition and newPosition?
            draggedItem.moveDnDPattern(Object.assign({}, draggedItem, {newPosition: newPosition, oldPosition: oldPosition}))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants