6

I've been reviewing the documentation and github issues and connectDragPreview still remains a mystery to me. I would like to define a custom preview for how the item appears when it is being dragged. It should function just as the example here works with the horse image appearing on drag. Here is my code:

export const tokenCollector: DragSourceCollector = (connect, monitor) => {
  return {
    connectDragSource: connect.dragSource(),
    connectDragPreview: connect.dragPreview(),
    isDragging: monitor.isDragging()
  };
};

class TokenClass extends React.Component<TokenProps> {
  componentDidMount() {
    const { connectDragPreview } = this.props;
    const img = new Image();
    img.src = '<encoded image>';
    img.onload = () => connectDragPreview(<div>{img}</div>);
  }
  render() {
    const { connectDragSource, isDragging, children } = this.props;
    return connectDragSource(
      <div style={{ opacity: isDragging ? 0.5 : 1 }}>
        {children}
      </div>
    );
  }
}
const dragSource = DragSource(DropType.Token, tokenSpec, tokenCollector)(TokenClass);
export { dragSource as Token };

The standard preview appears with this code.

I then tried to wrap my connectDragSource in my component's render() method with connectDragPreview, but that only appears to change the drag source where it was picked up from, not how it appears as it's being dragged.

How can I specify the markup that should be used as the dragging visual?

im1dermike
  • 4,579
  • 8
  • 54
  • 104
  • Left an answer. If that doesn't work, can you share your `DropType.Token` and `tokenSpec`? – Luke Feb 19 '18 at 18:07

3 Answers3

7

Its seems you are using react-dnd-html5-backend. React-dnd-html5-backend provides connectDragSource: https://github.com/react-dnd/react-dnd-html5-backend/blob/85fc956c58a5d1a9fde2fca3c7fca9115a7c87df/src/HTML5Backend.js#L100

And react-dnd-html5-backend works only with html5 drag&drop events: https://github.com/react-dnd/react-dnd-html5-backend/blob/85fc956c58a5d1a9fde2fca3c7fca9115a7c87df/src/HTML5Backend.js#L65-L74

So, you can set up only an Image to you drag effect: https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/setDragImage

You can simply change your img.onload callback:

export const tokenCollector: DragSourceCollector = (connect, monitor) => {
  return {
    connectDragSource: connect.dragSource(),
    connectDragPreview: connect.dragPreview(),
    isDragging: monitor.isDragging()
  };
};

class TokenClass extends React.Component<TokenProps> {
  componentDidMount() {
    const { connectDragPreview } = this.props;
    const img = new Image();
    img.src = 'base64-image';
    img.onload = () => connectDragPreview(img);
  }
  render() {
    const { connectDragSource, isDragging, children } = this.props;
    return connectDragSource(
      <div style={{ opacity: isDragging ? 0.5 : 1 }}>
        {children}
      </div>
    );
  }
}
const dragSource = DragSource(DropType.Token, tokenSpec, tokenCollector)(TokenClass);
export { dragSource as Token };

In case you want to use custom markup, you can call connectDragPreview with getEmptyImage argument, and then, implement CustomDragLayer.

Your TokenClass:

import React, { Component } from "react";
import { DragSource } from "react-dnd";
import logo from "./logo.svg";
import { getEmptyImage } from "react-dnd-html5-backend";

export const tokenCollector = (connect, monitor) => {
  return {
    connectDragSource: connect.dragSource(),
    connectDragPreview: connect.dragPreview(),
    isDragging: monitor.isDragging()
  };
};

class TokenClass extends React.Component {
  componentDidMount() {
    const { connectDragPreview } = this.props;

    // Use empty image as a drag preview so browsers don't draw it
    // and we can draw whatever we want on the custom drag layer instead.
    connectDragPreview(getEmptyImage());
  }
  render() {
    const { connectDragSource, isDragging, children } = this.props;
    return connectDragSource(
      <div style={{ opacity: isDragging ? 0.5 : 1, backgroundColor: "green" }}>
        {children}
      </div>
    );
  }
}

const tokenSpec = {
  beginDrag() {
    return {};
  }
};

export default DragSource("DropType.Markup", tokenSpec, tokenCollector)(
  TokenClass
);

CustomDragLayer implementation:

import React, { Component } from "react";
import { DragLayer } from "react-dnd";

function getItemStyles(props) {
  const { initialOffset, currentOffset } = props;
  if (!initialOffset || !currentOffset) {
    return {
      display: "none"
    };
  }

  let { x, y } = currentOffset;

  const transform = `translate(${x}px, ${y}px)`;
  return {
    transform,
    WebkitTransform: transform
  };
}

class CustomDragLayer extends Component {
  render() {
    const isDragging = this.props.isDragging;
    if (!isDragging) {
      return null;
    }

    // You can specify acceptable type:
    if (this.props.itemType !== "DropType.Markup") {
      return null;
    }

    // The component will work only when dragging
    return (
      <div>
        <div style={getItemStyles(this.props)}>Custom drag layer!</div>
      </div>
    );
  }
}

function collect(monitor) {
  return {
    item: monitor.getItem(),
    itemType: monitor.getItemType(),
    initialOffset: monitor.getInitialSourceClientOffset(),
    currentOffset: monitor.getSourceClientOffset(),
    isDragging: monitor.isDragging()
  };
}

export default DragLayer(collect)(CustomDragLayer); // eslint-disable-line new-cap

Also, I uploaded to github solution which works with both an image and a markup. You can upload them and run: yarn install yarn start Solution: https://github.com/xnimorz/stackoverflow-example/tree/dragAndDrop

Nik
  • 1,601
  • 1
  • 8
  • 15
  • `connectDragPreview` is of type `ConnectDragPreview` which doesn't accept an image. Your code gives the following error: `Argument of type 'HTMLImageElement' is not assignable to parameter of type 'ReactElement'.` FWIW, the only reason I'm using an image in this example is to match React DnD's tutorial. Ultimately, I need to specify markup so my `
    ` example is apropos.
    – im1dermike Feb 26 '18 at 14:31
  • @im1dermike DragAndDrop react-dnd-html5-backend feature doesn't support custom markup by default in connectDragPreview method. To set up custom markup you can implement CustomDragLayer. I edited my answer and added links to solutions with an image and a markup ( https://github.com/xnimorz/stackoverflow-example/tree/dragAndDrop ) – Nik Feb 26 '18 at 16:22
  • Thanks a lot for your contributions to this question. I think I'm getting closer. I implemented your suggested code (https://kopy.io/HWRqn), but now when I drag, I see nothing at all. What am I missing? – im1dermike Feb 26 '18 at 20:54
  • Make sure, that you use CustomDragLayer in the your component that wrapped with DragDropContext. https://github.com/xnimorz/stackoverflow-example/blob/dragAndDrop/src/App.js#L23-L29 And avoid to use preventDefault function in onMouseEvent and onMouseLeave methods – Nik Feb 27 '18 at 08:37
  • I added `` to all my components that wrap `DragDropContext`. Now the `CustomDragLayer` class' `render()` function fires, but I still am not seeing the "Custom drag layer!" test text. Thoughts? – im1dermike Feb 27 '18 at 16:38
  • I was able to pause while dragging using this https://stackoverflow.com/questions/17931571/freeze-screen-in-chrome-debugger-devtools-panel-for-popover-inspection (awesome!) and saw that the custom drag layer was not displaying b/c of z-index. – im1dermike Feb 27 '18 at 16:54
  • So I set `position: absolute;` for the Custom Drag Layer so I can now see it. The problem is that the offset for the markup is well below the actual cursor. I'm seeing this in your example too. How can I address this? – im1dermike Feb 27 '18 at 18:18
  • ps. Your demo appears to only work in Chrome. Not Edge or FF. – im1dermike Feb 27 '18 at 20:01
  • "The problem is that the offset for the markup is well below the actual cursor." In this case you can get your CustomDargLayer or DragSource offsets (element.offsetTop, element.offsetLeft) (depends on markup around) and set that offsets in your getItemStyles function. I tested my demo on Chrome and Firefox v. 58.0.1. – Nik Feb 28 '18 at 09:24
  • What about touch screens? – Julia Mar 15 '19 at 13:51
  • 1
    @Julka it depends on drag-drop-context. React-dnd supports special middleware (called backend), which should implement specific dnd logic, e.g. react-dnd-html5-backend works with 'drag' and 'drop' events or touch backend http://react-dnd.github.io/react-dnd/docs/backends/touch-backend. More information about backends you can find here: http://react-dnd.github.io/react-dnd/docs/api/drag-drop-context – Nik Mar 16 '19 at 16:54
0

Try changing your img.onload to use the img directly instead of wrapping it in a div:

img.onload = () => connectDragPreview(img);

As far as I can tell, this is the only real difference in your code from the examples.

Luke
  • 7,719
  • 3
  • 43
  • 74
0

import React from 'react';
import { DragSource } from 'react-dnd';

/* ... */

function collect(connect, monitor) {
  return {
    connectDragSource: connect.dragSource(),
    connectDragPreview: connect.dragPreview()
  };
}

class ComponentWithCopyEffect {
  render() {
    const { connectDragSource } = this.props;

    return connectDragSource(
      <div>
        This div shows a plus icon in some browsers.
      </div>,
      { dropEffect: 'copy' }
    );
  }
});
ComponentWithCopyEffect = DragSource(/* ... */)(ComponentWithCopyEffect);

class ComponentWithHandle {
  render() {
    const { connectDragSource, connectDragPreview } = this.props;

    return connectDragPreview(
      <div>
        This div is draggable by a handle!
        {connectDragSource(
          <div>drag me</div>
        )}
      </div>
    );
  }
}
ComponentWithHandle = DragSource(/* ... */)(ComponentWithHandle);

class ComponentWithImagePreview {
  componentDidMount() {
    const { connectDragPreview } = this.props;

    const img = new Image();
    img.src = '/image.jpg';
    img.onload = () => connectDragPreview(img);
  }

  render() {
    const { connectDragSource } = this.props;

    return connectDragSource(
      <div>
        This div shows an image when dragged!
      </div>
    );
  }
}
ComponentWithImagePreview = DragSource(/* ... */)(ComponentWithImagePreview);

Source

Michael
  • 838
  • 6
  • 6