Рекомендуемый способ сделать React компонент / div перетаскиваемым


101

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

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

Спасибо.


На самом деле, я был бы по крайней мере так же доволен объяснением в прозе, как код, или даже просто «у вас все хорошо». Но вот JSFiddle моей работы на данный момент: jsfiddle.net/Z2JtM
Эндрю Флинор,

Я согласен с тем, что это правильный вопрос, учитывая, что в настоящее время существует очень мало примеров кода реакции, на которые следует обратить внимание
Джаред Форсайт

1
Нашел простое решение HTML5 для моего варианта использования - youtu.be/z2nHLfiiKBA . Может кому-нибудь помочь !!
Prem

Попробуй это. Это простой HOC для превращения элементов в перетаскиваемые npmjs.com/package/just-drag
Шан,

Ответы:


114

Возможно, мне стоит превратить это в сообщение в блоге, но вот довольно убедительный пример.

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

И вот скрипка, с которой можно поиграть: 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

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

Сценарий 2

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

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

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


4
Отличный пример. Это кажется более подходящим, Mixinчем полный класс, поскольку «Draggable» на самом деле не объект, это способность объекта.
Росс Аллен

2
Я немного поигрался с ним, похоже, что перетаскивание за пределы его родительского элемента ничего не делает, но всякие странные вещи происходят, когда его перетаскивают в другой компонент реакции
Gorkem Yurtseven

11
Вы можете удалить зависимость jquery, выполнив следующие действия: var computedStyle = window.getComputedStyle(this.getDOMNode()); pos = { top: parseInt(computedStyle.top), left: parseInt(computedStyle.left) }; Если вы используете jquery с response, вы, вероятно, делаете что-то не так;) Если вам нужен какой-то плагин jquery, я считаю, что это обычно проще и меньше кода, чтобы переписать его в чистой реакции.
Мэтт Кринкло-Фогт,

7
Просто хотел продолжить комментарий @ MattCrinklaw-Vogt выше, чтобы сказать, что следует использовать более this.getDOMNode().getBoundingClientRect()надежное решение - getComputedStyle может выводить любое допустимое свойство CSS, включая autoв этом случае приведенный выше код приведет к созданию файла NaN. См. Статью MDN: developer.mozilla.org/en-US/docs/Web/API/Element/…
Андру,

2
И чтобы продолжить this.getDOMNode(), это устарело. Используйте ссылку, чтобы получить узел dom. facebook.github.io/react/docs/…
Крис Саттингер,

65

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

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

Основные требования, которые у меня были:

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

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

Применение

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

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

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

// 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поведение этого компонента при перетаскивании. И перетаскиваемые, и перетаскиваемые компоненты используют один и тот же миксин.

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

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

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

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

Допустим, мы хотим ImageBlockбыть целью для IMAGEs. Это почти то же самое, за исключением того, что мы должны дать registerTypeв 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>
    );
  }
);

Перетащить источник + перетащить цель в один компонент

Скажем, теперь мы хотим, чтобы пользователь мог вытащить изображение из 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.
  • Предположим, мы добавляем другие типы блоков. Мы можем повторно использовать их логику переупорядочения, поместив ее в миксин.
  • dropTargetFor(...types) позволяет указать несколько типов одновременно, поэтому в одной зоне перетаскивания можно поймать много разных типов.
  • Когда вам нужен более детальный контроль, большинству методов передается событие перетаскивания, вызвавшее их, в качестве последнего параметра.

Актуальную документацию и инструкции по установке можно найти в репозитории response-dnd на Github .


6
Что общего между перетаскиванием и перетаскиванием мышью, кроме использования мыши? Ваш ответ не имеет отношения к вопросу и явно является рекламой библиотеки.
polkovnikov.ph

5
Похоже, 29 человек сочли это связанным с вопросом. React DnD также позволяет вам легко реализовать перетаскивание мышью . Я буду думать лучше, чем делиться своей работой бесплатно, и работать над примерами и обширной документацией в следующий раз, чтобы мне не приходилось тратить время на чтение язвительных комментариев.
Дэн Абрамов

7
Да, я это прекрасно знаю. Тот факт, что у вас есть документация в другом месте, не означает, что это ответ на заданный вопрос. Вы могли бы написать «использовать Google» для того же результата. 29 голосов «за» были получены из-за длинного поста известного человека, а не из-за качества ответа.
polkovnikov.ph

обновлены ссылки на официальные примеры свободно перетаскиваемого материала с помощью
react

@DanAbramov Я ценю ваше усилие по написанию поста, без него я бы, наверное, не стал бы так быстро реагировать . Как и многие ваши продукты, он сначала кажется излишне сложным , но собран исключительно профессионально (просматривая вашу документацию, я обычно узнаю кое-что о разработке программного обеспечения ...) Я читал «тех, кто не знаю, что unix обречены на его переопределение ... плохо, "и я чувствую, что нечто подобное может быть справедливо для многих ваших творений (включая это).
Натан Чаппелл,

23

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

UPD. Мой ответ был ужасно неправильным и устаревшим. Теперь код устраняет проблемы, связанные с медленным жизненным циклом компонентов React, с помощью собственных обработчиков событий и обновлений стилей, использует, transformпоскольку он не приводит к перекомпоновке, и регулирует изменения DOM requestAnimationFrame. Теперь у меня стабильно 60 FPS в каждом браузере, который я пробовал.

const throttle = (f) => {
    let token = null, lastArgs = null;
    const invoke = () => {
        f(...lastArgs);
        token = null;
    };
    const result = (...args) => {
        lastArgs = args;
        if (!token) {
            token = requestAnimationFrame(invoke);
        }
    };
    result.cancel = () => token && cancelAnimationFrame(token);
    return result;
};

class Draggable extends React.PureComponent {
    _relX = 0;
    _relY = 0;
    _ref = React.createRef();

    _onMouseDown = (event) => {
        if (event.button !== 0) {
            return;
        }
        const {scrollLeft, scrollTop, clientLeft, clientTop} = document.body;
        // Try to avoid calling `getBoundingClientRect` if you know the size
        // of the moving element from the beginning. It forces reflow and is
        // the laggiest part of the code right now. Luckily it's called only
        // once per click.
        const {left, top} = this._ref.current.getBoundingClientRect();
        this._relX = event.pageX - (left + scrollLeft - clientLeft);
        this._relY = event.pageY - (top + scrollTop - clientTop);
        document.addEventListener('mousemove', this._onMouseMove);
        document.addEventListener('mouseup', this._onMouseUp);
        event.preventDefault();
    };

    _onMouseUp = (event) => {
        document.removeEventListener('mousemove', this._onMouseMove);
        document.removeEventListener('mouseup', this._onMouseUp);
        event.preventDefault();
    };

    _onMouseMove = (event) => {
        this.props.onMove(
            event.pageX - this._relX,
            event.pageY - this._relY,
        );
        event.preventDefault();
    };

    _update = throttle(() => {
        const {x, y} = this.props;
        this._ref.current.style.transform = `translate(${x}px, ${y}px)`;
    });

    componentDidMount() {
        this._ref.current.addEventListener('mousedown', this._onMouseDown);
        this._update();
    }

    componentDidUpdate() {
        this._update();
    }

    componentWillUnmount() {
        this._ref.current.removeEventListener('mousedown', this._onMouseDown);
        this._update.cancel();
    }

    render() {
        return (
            <div className="draggable" ref={this._ref}>
                {this.props.children}
            </div>
        );
    }
}

class Test extends React.PureComponent {
    state = {
        x: 100,
        y: 200,
    };

    _move = (x, y) => this.setState({x, y});

    // you can implement grid snapping logic or whatever here
    /*
    _move = (x, y) => this.setState({
        x: ~~((x - 5) / 10) * 10 + 5,
        y: ~~((y - 5) / 10) * 10 + 5,
    });
    */

    render() {
        const {x, y} = this.state;
        return (
            <Draggable x={x} y={y} onMove={this._move}>
                Drag me
            </Draggable>
        );
    }
}

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

и немного CSS

.draggable {
    /* just to size it to content */
    display: inline-block;
    /* opaque background is important for performance */
    background: white;
    /* avoid selecting text while dragging */
    user-select: none;
}

Пример на JSFiddle.


2
Спасибо за это, это определенно не самое производительное решение, но оно соответствует лучшим сегодняшним практикам создания приложений.
Spets

1
@ryanj Нет, значения по умолчанию злые, вот в чем проблема. Что делать при смене реквизита? Должны ли мы сбросить состояние до нового значения по умолчанию? Следует ли сравнивать новое значение по умолчанию со старым значением по умолчанию, чтобы сбросить состояние до значения по умолчанию только тогда, когда значение по умолчанию изменилось? Невозможно ограничить пользователя использованием только постоянного значения и ничего больше. Вот почему это антипаттерн. Значения по умолчанию должны создаваться явно через компоненты высокого порядка (то есть для всего класса, а не для объекта), и никогда не должны устанавливаться через props.
polkovnikov.ph 08

1
Я с уважением не согласен - состояние компонента - отличное место для хранения данных, специфичных для пользовательского интерфейса компонента, которые, например, не имеют отношения к приложению в целом. Не имея возможности передавать значения по умолчанию в качестве реквизита в некоторых случаях, варианты получения этих данных после монтирования ограничены и во многих (большинстве?) Обстоятельств менее желательны, чем капризы вокруг компонента, который потенциально может быть передан другому реквизиту someDefaultValue в Поздняя дата. Я не защищаю это как лучшую практику или что-то в этом роде, это просто не так вредно, как вы предлагаете имо
райан

2
Действительно, очень простое и элегантное решение. Я рад видеть, что мои взгляды на это были похожи. У меня есть один вопрос: вы упомянули о низкой производительности, что бы вы предложили для достижения аналогичной функции с учетом производительности?
Guillaume M

1
В любом случае, у нас есть крючки, и я должен скоро обновить ответ еще раз.
polkovnikov.ph 09

14

Я обновил решение polkovnikov.ph для React 16 / ES6, добавив такие улучшения, как сенсорная обработка и привязка к сетке, что мне нужно для игры. Привязка к сетке снижает проблемы с производительностью.

import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';

class Draggable extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            relX: 0,
            relY: 0,
            x: props.x,
            y: props.y
        };
        this.gridX = props.gridX || 1;
        this.gridY = props.gridY || 1;
        this.onMouseDown = this.onMouseDown.bind(this);
        this.onMouseMove = this.onMouseMove.bind(this);
        this.onMouseUp = this.onMouseUp.bind(this);
        this.onTouchStart = this.onTouchStart.bind(this);
        this.onTouchMove = this.onTouchMove.bind(this);
        this.onTouchEnd = this.onTouchEnd.bind(this);
    }

    static propTypes = {
        onMove: PropTypes.func,
        onStop: PropTypes.func,
        x: PropTypes.number.isRequired,
        y: PropTypes.number.isRequired,
        gridX: PropTypes.number,
        gridY: PropTypes.number
    }; 

    onStart(e) {
        const ref = ReactDOM.findDOMNode(this.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)
        });
    }

    onMove(e) {
        const x = Math.trunc((e.pageX - this.state.relX) / this.gridX) * this.gridX;
        const y = Math.trunc((e.pageY - this.state.relY) / this.gridY) * this.gridY;
        if (x !== this.state.x || y !== this.state.y) {
            this.setState({
                x,
                y
            });
            this.props.onMove && this.props.onMove(this.state.x, this.state.y);
        }        
    }

    onMouseDown(e) {
        if (e.button !== 0) return;
        this.onStart(e);
        document.addEventListener('mousemove', this.onMouseMove);
        document.addEventListener('mouseup', this.onMouseUp);
        e.preventDefault();
    }

    onMouseUp(e) {
        document.removeEventListener('mousemove', this.onMouseMove);
        document.removeEventListener('mouseup', this.onMouseUp);
        this.props.onStop && this.props.onStop(this.state.x, this.state.y);
        e.preventDefault();
    }

    onMouseMove(e) {
        this.onMove(e);
        e.preventDefault();
    }

    onTouchStart(e) {
        this.onStart(e.touches[0]);
        document.addEventListener('touchmove', this.onTouchMove, {passive: false});
        document.addEventListener('touchend', this.onTouchEnd, {passive: false});
        e.preventDefault();
    }

    onTouchMove(e) {
        this.onMove(e.touches[0]);
        e.preventDefault();
    }

    onTouchEnd(e) {
        document.removeEventListener('touchmove', this.onTouchMove);
        document.removeEventListener('touchend', this.onTouchEnd);
        this.props.onStop && this.props.onStop(this.state.x, this.state.y);
        e.preventDefault();
    }

    render() {
        return <div
            onMouseDown={this.onMouseDown}
            onTouchStart={this.onTouchStart}
            style={{
                position: 'absolute',
                left: this.state.x,
                top: this.state.y,
                touchAction: 'none'
            }}
            ref={(div) => { this.handle = div; }}
        >
            {this.props.children}
        </div>;
    }
}

export default Draggable;

привет @anyhotcountry для чего вы используете коэффициент gridX ?
AlexNikonov

1
@AlexNikonov - это размер сетки (привязки) по оси x. Для повышения производительности рекомендуется иметь gridX и gridY> 1.
anyhotcountry

У меня это сработало. При изменении, которое я сделал в функции onStart (): вычисляя relX и relY, я использовал e.clienX-this.props.x. Это позволило мне разместить перетаскиваемый компонент внутри родительского контейнера, а не в зависимости от того, является ли вся страница областью перетаскивания. Я знаю, что это запоздалый комментарий, но просто хотел поблагодарить.
Джефф

11

react-draggable также прост в использовании. 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>

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


10

Вот простой современный подход к этому с useState, useEffectи useRefв ES6.

import React, { useRef, useState, useEffect } from 'react'

const quickAndDirtyStyle = {
  width: "200px",
  height: "200px",
  background: "#FF9900",
  color: "#FFFFFF",
  display: "flex",
  justifyContent: "center",
  alignItems: "center"
}

const DraggableComponent = () => {
  const [pressed, setPressed] = useState(false)
  const [position, setPosition] = useState({x: 0, y: 0})
  const ref = useRef()

  // Monitor changes to position state and update DOM
  useEffect(() => {
    if (ref.current) {
      ref.current.style.transform = `translate(${position.x}px, ${position.y}px)`
    }
  }, [position])

  // Update the current position if mouse is down
  const onMouseMove = (event) => {
    if (pressed) {
      setPosition({
        x: position.x + event.movementX,
        y: position.y + event.movementY
      })
    }
  }

  return (
    <div
      ref={ ref }
      style={ quickAndDirtyStyle }
      onMouseMove={ onMouseMove }
      onMouseDown={ () => setPressed(true) }
      onMouseUp={ () => setPressed(false) }>
      <p>{ pressed ? "Dragging..." : "Press to drag" }</p>
    </div>
  )
}

export default DraggableComponent

1
Похоже, это самый актуальный ответ здесь.
codyThompson

2

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

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

Все, что вы делаете, это добавляете к вашему компоненту такую ​​опору, как "draggable", и поток событий перетаскивания, которые будут управлять 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 - изящная вещь (я никогда не думал, что скажу это)


1
не могли бы вы заполнить setXandYфункцию воображаемым компонентом?
Thellimist

2

Вот ответ 2020 года с крючком:

function useDragging() {
  const [isDragging, setIsDragging] = useState(false);
  const [pos, setPos] = useState({ x: 0, y: 0 });
  const ref = useRef(null);

  function onMouseMove(e) {
    if (!isDragging) return;
    setPos({
      x: e.x - ref.current.offsetWidth / 2,
      y: e.y - ref.current.offsetHeight / 2,
    });
    e.stopPropagation();
    e.preventDefault();
  }

  function onMouseUp(e) {
    setIsDragging(false);
    e.stopPropagation();
    e.preventDefault();
  }

  function onMouseDown(e) {
    if (e.button !== 0) return;
    setIsDragging(true);

    setPos({
      x: e.x - ref.current.offsetWidth / 2,
      y: e.y - ref.current.offsetHeight / 2,
    });

    e.stopPropagation();
    e.preventDefault();
  }

  // When the element mounts, attach an mousedown listener
  useEffect(() => {
    ref.current.addEventListener("mousedown", onMouseDown);

    return () => {
      ref.current.removeEventListener("mousedown", onMouseDown);
    };
  }, [ref.current]);

  // Everytime the isDragging state changes, assign or remove
  // the corresponding mousemove and mouseup handlers
  useEffect(() => {
    if (isDragging) {
      document.addEventListener("mouseup", onMouseUp);
      document.addEventListener("mousemove", onMouseMove);
    } else {
      document.removeEventListener("mouseup", onMouseUp);
      document.removeEventListener("mousemove", onMouseMove);
    }
    return () => {
      document.removeEventListener("mouseup", onMouseUp);
      document.removeEventListener("mousemove", onMouseMove);
    };
  }, [isDragging]);

  return [ref, pos.x, pos.y, isDragging];
}

Затем компонент, использующий перехватчик:


function Draggable() {
  const [ref, x, y, isDragging] = useDragging();

  return (
    <div
      ref={ref}
      style={{
        position: "absolute",
        width: 50,
        height: 50,
        background: isDragging ? "blue" : "gray",
        left: x,
        top: y,
      }}
    ></div>
  );
}

1

Я обновил класс, используя ссылки, так как все решения, которые я вижу здесь, имеют вещи, которые больше не поддерживаются или скоро будут обесцениваться, например ReactDOM.findDOMNode. Может быть родительским для дочернего компонента или группы детей :)

import React, { Component } from 'react';

class Draggable extends Component {

    constructor(props) {
        super(props);
        this.myRef = React.createRef();
        this.state = {
            counter: this.props.counter,
            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(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 = (e) => {
        if (e.button !== 0) return;
        let pos = { left: this.myRef.current.offsetLeft, top: this.myRef.current.offsetTop }
        this.setState({
            dragging: true,
            rel: {
                x: e.pageX - pos.left,
                y: e.pageY - pos.top
            }
        });
        e.stopPropagation();
        e.preventDefault();
    }

    onMouseUp = (e) => {
        this.setState({ dragging: false });
        e.stopPropagation();
        e.preventDefault();
    }

    onMouseMove = (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() {
        return (
            <span ref={this.myRef} onMouseDown={this.onMouseDown} style={{ position: 'absolute', left: this.state.pos.x + 'px', top: this.state.pos.y + 'px' }}>
                {this.props.children}
            </span>
        )
    }
}

export default Draggable;

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.