Подтвердить что ты не робот

Рекомендуемый способ создания компонента React/div draggable

Я хочу сделать перетаскиваемый (т.е. переставляемый с помощью мыши) компонент React, который, как представляется, обязательно включает в себя глобальные состояния и разбросанные обработчики событий. Я могу сделать это грязным способом, с глобальной переменной в моем JS файле и, возможно, даже обернуть его в приятный интерфейс закрытия, но я хочу знать, есть ли способ, который сетки с React лучше.

Кроме того, поскольку я никогда раньше не делал этого в необработанном JavaScript, я хотел бы посмотреть, как это делает эксперт, чтобы убедиться, что у меня есть все обработанные угловые случаи, особенно если они относятся к React.

Спасибо.

4b9b3361

Ответ 1

Я должен, вероятно, включить это в сообщение в блоге, но здесь довольно солидный пример.

Комментарии должны хорошо объяснять, но дайте мне знать, если у вас есть вопросы.

И вот скрипка для игры: http://jsfiddle.net/Af9Jt/2/

var Draggable = React.createClass({
  getDefaultProps: function () {
    return {
      // allow the initial position to be passed in as a prop
      initialPos: {x: 0, y: 0}
    }
  },
  getInitialState: function () {
    return {
      pos: this.props.initialPos,
      dragging: false,
      rel: null // position relative to the cursor
    }
  },
  // we could get away with not having this (and just having the listeners on
  // our div), but then the experience would be possibly be janky. If there's
  // anything w/ a higher z-index that gets in the way, then you're toast,
  // etc.
  componentDidUpdate: function (props, state) {
    if (this.state.dragging && !state.dragging) {
      document.addEventListener('mousemove', this.onMouseMove)
      document.addEventListener('mouseup', this.onMouseUp)
    } else if (!this.state.dragging && state.dragging) {
      document.removeEventListener('mousemove', this.onMouseMove)
      document.removeEventListener('mouseup', this.onMouseUp)
    }
  },

  // calculate relative position to the mouse and set dragging=true
  onMouseDown: function (e) {
    // only left mouse button
    if (e.button !== 0) return
    var pos = $(this.getDOMNode()).offset()
    this.setState({
      dragging: true,
      rel: {
        x: e.pageX - pos.left,
        y: e.pageY - pos.top
      }
    })
    e.stopPropagation()
    e.preventDefault()
  },
  onMouseUp: function (e) {
    this.setState({dragging: false})
    e.stopPropagation()
    e.preventDefault()
  },
  onMouseMove: function (e) {
    if (!this.state.dragging) return
    this.setState({
      pos: {
        x: e.pageX - this.state.rel.x,
        y: e.pageY - this.state.rel.y
      }
    })
    e.stopPropagation()
    e.preventDefault()
  },
  render: function () {
    // transferPropsTo will merge style & other props passed into our
    // component to also be on the child DIV.
    return this.transferPropsTo(React.DOM.div({
      onMouseDown: this.onMouseDown,
      style: {
        left: this.state.pos.x + 'px',
        top: this.state.pos.y + 'px'
      }
    }, this.props.children))
  }
})

Мысли о государственной собственности и т.д.

"Кто должен владеть, какое состояние" является важным вопросом для ответа, с самого начала. В случае "перетаскиваемого" компонента я мог видеть несколько разных сценариев.

Сценарий 1

родитель должен владеть текущей позицией перетаскиваемого. В этом случае draggable по-прежнему будет владеть состоянием "Я перетаскиваю", но будет называть this.props.onChange(x, y) всякий раз, когда происходит событие mousemove.

Сценарий 2

родительскому нужно только владеть "не движущейся позицией", и поэтому перетаскиваемый будет владеть им "перетаскиванием позиции", но onmouseup он будет вызывать this.props.onChange(x, y) и отложить окончательное решение родительскому. Если родительу не нравится, где закончилось перетаскиваемое, оно просто не обновляло бы его состояние, а draggable "привязывался" к исходной позиции перед перетаскиванием.

Миксин или компонент?

@ssorallen указал, что, поскольку "draggable" является скорее атрибутом, чем чем-то само по себе, он может служить лучше как mixin. Мой опыт работы с mixins ограничен, поэтому я не видел, как они могут помочь или мешать в сложных ситуациях. Это может быть лучшим вариантом.

Ответ 2

Я реализовал react-dnd, гибкую комбинацию перетаскивания и перетаскивания HTML5 для React с полным контролем DOM.

Существующие библиотеки перетаскивания не подходят для моего использования, поэтому я написал свой собственный. Это похоже на код, который мы запускаем около года на Stampsy.com, но переписываем, чтобы воспользоваться React и Flux.

Основные требования:

  • Извлечь нулевые DOM или CSS самостоятельно, оставив их потребляющим компонентам;
  • Настройте как можно меньшую структуру на потребляющие компоненты;
  • Использовать перетаскивание HTML5 в качестве основного бэкэнда, но в будущем можно добавить разные бэкэнды;
  • Как оригинальный API HTML5, подчеркивайте перетаскивание данных, а не только "перетаскиваемые представления";
  • Скрыть API-интерфейсы HTML5 от кода пользователя;
  • Различные компоненты могут быть "источниками перетаскивания" или "снижать цели" для разных видов данных;
  • Разрешить одному компоненту содержать несколько источников перетаскивания и понижать целевые показатели при необходимости;
  • Упростите, чтобы падающие объекты изменяли свой внешний вид, если перетаскиваемые или зависающие совместимые данные;
  • Упростите использование изображений для перетаскивания эскизов вместо скриншотов элементов, обойдя причуды браузера.

Если вам это знакомо, читайте дальше.

Использование

Простой источник перетаскивания

Сначала объявите типы данных, которые можно перетащить.

Они используются для проверки "совместимости" источников перетаскивания и снижения целей:

// ItemTypes.js
module.exports = {
  BLOCK: 'block',
  IMAGE: 'image'
};

(Если у вас нет нескольких типов данных, эта библиотека может быть не для вас.)

Затем сделаем очень простой перетаскиваемый компонент, который при перетаскивании представляет IMAGE:

var { DragDropMixin } = require('react-dnd'),
    ItemTypes = require('./ItemTypes');

var Image = React.createClass({
  mixins: [DragDropMixin],

  configureDragDrop(registerType) {

    // Specify all supported types by calling registerType(type, { dragSource?, dropTarget? })
    registerType(ItemTypes.IMAGE, {

      // dragSource, when specified, is { beginDrag(), canDrag()?, endDrag(didDrop)? }
      dragSource: {

        // beginDrag should return { item, dragOrigin?, dragPreview?, dragEffect? }
        beginDrag() {
          return {
            item: this.props.image
          };
        }
      }
    });
  },

  render() {

    // {...this.dragSourceFor(ItemTypes.IMAGE)} will expand into
    // { draggable: true, onDragStart: (handled by mixin), onDragEnd: (handled by mixin) }.

    return (
      <img src={this.props.image.url}
           {...this.dragSourceFor(ItemTypes.IMAGE)} />
    );
  }
);

Указав configureDragDrop, мы скажем DragDropMixin поведение перетаскивания этого компонента. Оба перетаскиваемых и отбрасываемых компонента используют один и тот же mixin.

Внутри configureDragDrop нам нужно вызвать registerType для каждого из наших пользовательских ItemTypes, которые поддерживает этот компонент. Например, может быть несколько изображений изображений в вашем приложении, и каждый из них предоставит dragSource для ItemTypes.IMAGE.

A dragSource - это просто объект, указывающий, как работает источник перетаскивания. Вы должны реализовать beginDrag, чтобы вернуть элемент, представляющий данные, которые вы перетаскиваете, и, при необходимости, несколько параметров, которые настраивают интерфейс перетаскивания. Вы можете опционально реализовать canDrag, чтобы запретить перетаскивание, или endDrag(didDrop), чтобы выполнить некоторую логику, когда произошло падение (или не произошло). И вы можете поделиться этой логикой между компонентами, разрешив для них общий mixin dragSource.

Наконец, вы должны использовать {...this.dragSourceFor(itemType)} для некоторых (одного или нескольких) элементов в render для прикрепления обработчиков перетаскивания. Это означает, что вы можете иметь несколько "ручек перетаскивания" в одном элементе, и они могут даже соответствовать различным типам элементов. (Если вы не знакомы с синтаксисом JSX Spread Attributes, проверьте его).

Простая цель удаления

Предположим, что мы хотим, чтобы ImageBlock была целью капли для IMAGE s. Это почти то же самое, за исключением того, что нам нужно дать реализацию registerType a dropTarget:

var { DragDropMixin } = require('react-dnd'),
    ItemTypes = require('./ItemTypes');

var ImageBlock = React.createClass({
  mixins: [DragDropMixin],

  configureDragDrop(registerType) {

    registerType(ItemTypes.IMAGE, {

      // dropTarget, when specified, is { acceptDrop(item)?, enter(item)?, over(item)?, leave(item)? }
      dropTarget: {
        acceptDrop(image) {
          // Do something with image! for example,
          DocumentActionCreators.setImage(this.props.blockId, image);
        }
      }
    });
  },

  render() {

    // {...this.dropTargetFor(ItemTypes.IMAGE)} will expand into
    // { onDragEnter: (handled by mixin), onDragOver: (handled by mixin), onDragLeave: (handled by mixin), onDrop: (handled by mixin) }.

    return (
      <div {...this.dropTargetFor(ItemTypes.IMAGE)}>
        {this.props.image &&
          <img src={this.props.image.url} />
        }
      </div>
    );
  }
);

Drag Source + Drop Target в одном компоненте

Скажем, теперь мы хотим, чтобы пользователь мог вытащить изображение из ImageBlock. Нам просто нужно добавить к нему подходящий dragSource и несколько обработчиков:

var { DragDropMixin } = require('react-dnd'),
    ItemTypes = require('./ItemTypes');

var ImageBlock = React.createClass({
  mixins: [DragDropMixin],

  configureDragDrop(registerType) {

    registerType(ItemTypes.IMAGE, {

      // Add a drag source that only works when ImageBlock has an image:
      dragSource: {
        canDrag() {
          return !!this.props.image;
        },

        beginDrag() {
          return {
            item: this.props.image
          };
        }
      }

      dropTarget: {
        acceptDrop(image) {
          DocumentActionCreators.setImage(this.props.blockId, image);
        }
      }
    });
  },

  render() {

    return (
      <div {...this.dropTargetFor(ItemTypes.IMAGE)}>

        {/* Add {...this.dragSourceFor} handlers to a nested node */}
        {this.props.image &&
          <img src={this.props.image.url}
               {...this.dragSourceFor(ItemTypes.IMAGE)} />
        }
      </div>
    );
  }
);

Что еще возможно?

Я не рассматривал все, но можно использовать этот API еще несколькими способами:

  • Используйте getDragState(type) и getDropState(type), чтобы узнать, активно ли перетаскивание и использовать его для переключения классов или атрибутов CSS;
  • Укажите dragPreview как IMAGE для использования изображений в качестве заполнителей перетаскивания (используйте ImagePreloaderMixin для их загрузки);
  • Скажем, мы хотим сделать ImageBlocks перезаписываемым. Нам нужны только они для реализации dropTarget и dragSource для ItemTypes.BLOCK.
  • Предположим, мы добавляем другие типы блоков. Мы можем повторно использовать их логику переупорядочения, помещая ее в mixin.
  • dropTargetFor(...types) позволяет сразу указывать несколько типов, поэтому одна зона выпадения может захватывать множество разных типов.
  • Если вам требуется более мелкомасштабное управление, большинству методов передается событие перетаскивания, которое вызвало их как последний параметр.

Для получения последней документации и инструкций по установке, перейдите в реакцию-dnd repo на Github.

Ответ 3

Ответ Джареда Форсайт ужасно ошибочен и устарел. Он следует за множеством антипаттернов, таких как использование stopPropagation, инициализация состояние из реквизита, использование jQuery, вложенных объектов в состоянии и имеет некоторое нечетное поле состояния dragging. При перезаписи решение будет следующим, но оно по-прежнему вынуждает виртуальную согласованность DOM на каждом тике перемещения мыши и не очень эффективна.

const Draggable = React.createClass({
  getInitialState() {
    return {
      relX: 0,
      relY: 0
    };
  },
  onMouseDown(e) {
    if (e.button !== 0) return;
    const ref = ReactDOM.findDOMNode(this.refs.handle);
    const body = document.body;
    const box = ref.getBoundingClientRect();
    this.setState({
      relX: e.pageX - (box.left + body.scrollLeft - body.clientLeft),
      relY: e.pageY - (box.top + body.scrollTop - body.clientTop)
    });
    document.addEventListener('mousemove', this.onMouseMove);
    document.addEventListener('mouseup', this.onMouseUp);
    e.preventDefault();
  },
  onMouseUp(e) {
    document.removeEventListener('mousemove', this.onMouseMove);
    document.removeEventListener('mouseup', this.onMouseUp);
    e.preventDefault();
  },
  onMouseMove(e) {
    this.props.onMove({
      x: e.pageX - this.state.relX,
      y: e.pageY - this.state.relY
    });
    e.preventDefault();
  },
  render() {
    return <div
      onMouseDown={this.onMouseDown}
      style={{
        position: 'absolute',
        left: this.props.x,
        top: this.props.y
      }}
      ref="handle"
    >{this.props.children}</div>;
  }
})

const Test = React.createClass({
  getInitialState() {
    return {x: 100, y: 200};
  },
  render() {
    const {x, y} = this.state;
    return <Draggable x={x} y={y} onMove={this.move}>
        Drag me
    </Draggable>;
  },
  move(e) {
    this.setState(e);
  }
});

ReactDOM.render(<Test />, document.getElementById('container'));

Пример JSFiddle.

Ответ 4

реагировать-перетаскиваемый также прост в использовании. Github:

https://github.com/mzabriskie/react-draggable

import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import Draggable from 'react-draggable';

var App = React.createClass({
    render() {
        return (
            <div>
                <h1>Testing Draggable Windows!</h1>
                <Draggable handle="strong">
                    <div className="box no-cursor">
                        <strong className="cursor">Drag Here</strong>
                        <div>You must click my handle to drag me</div>
                    </div>
                </Draggable>
            </div>
        );
    }
});

ReactDOM.render(
    <App />, document.getElementById('content')
);

И мой index.html:

<html>
    <head>
        <title>Testing Draggable Windows</title>
        <link rel="stylesheet" type="text/css" href="style.css" />
    </head>
    <body>
        <div id="content"></div>
        <script type="text/javascript" src="bundle.js" charset="utf-8"></script>    
    <script src="http://localhost:8080/webpack-dev-server.js"></script>
    </body>
</html>

Вам нужны их стили, короткие, или вы не получите ожидаемого поведения. Мне нравится поведение больше, чем некоторые другие возможные варианты, но также есть что-то, называемое реакционно-зависимым и подвижным. Я пытаюсь изменить размер с помощью draggable, но пока не доставляет радости.

Ответ 5

Я хотел бы добавить 3-й сценарий

Положение перемещения не сохраняется. Подумайте об этом как о движении мыши - ваш курсор не является компонентом React, не так ли?

Все, что вы делаете, это добавить пропед, как "перетаскиваемый" к вашему компоненту, и поток событий перетаскивания, которые будут манипулировать dom.

setXandY: function(event) {
    // DOM Manipulation of x and y on your node
},

componentDidMount: function() {
    if(this.props.draggable) {
        var node = this.getDOMNode();
        dragStream(node).onValue(this.setXandY);  //baconjs stream
    };
},

В этом случае манипуляция с DOM - это элегантная вещь (я никогда не думал, что я это скажу)