Рекомендований спосіб зробити компонент React / div перетягуванням


97

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

Крім того, оскільки я ніколи раніше не робив цього в необробленому JavaScript, я хотів би побачити, як це робить експерт, щоб переконатися, що я обробив усі кутові справи, особливо, що стосується React.

Дякую.


Насправді, я був би принаймні настільки ж радий поясненню прози, як код, або навіть просто: "Ви робите це добре". Але ось JSFiddle моєї роботи на даний момент: jsfiddle.net/Z2JtM
Ендрю

Я погоджуюсь, що це слушне запитання, враховуючи те, що в даний час існує дуже мало прикладів коду реакції, який слід переглянути зараз
Джаред Форсайт

1
Знайшов просте рішення HTML5 для мого випадку використання - youtu.be/z2nHLfiiKBA . Може допомогти комусь !!
Прем

Спробуйте це. Це просто HOC, щоб перетворити загорнуті елементи на перетягувані npmjs.com/package/just-drag
Шань

Відповіді:


111

Напевно, я мав би перетворити це на допис у блозі, але ось досить вагомий приклад.

Коментарі повинні пояснювати речі досить добре, але дайте мені знати, якщо у вас є запитання.

І ось скрипка, з якою можна пограти: 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)коли відбувається подія переміщення миші.

Сценарій 2

батькові потрібно лише володіти "нерухливою позицією", і тому перетягуваний би володів її "позицією перетягування", але при наведенні миші він викликав би this.props.onChange(x, y)і відклав остаточне рішення батькові. Якщо батькові не подобається, куди потрапив перетягуваний елемент, він просто не оновлює свій стан, а перетягуваний "повернеться" у початкове положення перед перетягуванням.

Міксин або компонент?

@ssorallen зазначив, що, оскільки "перетягування" є більше атрибутом, ніж річчю само по собі, воно може слугувати кращим змішанням. Мій досвід роботи з міксинами обмежений, тому я не бачив, як вони можуть допомогти або завадити в складних ситуаціях. Це цілком може бути найкращим варіантом.


4
Чудовий приклад. Це здається більш доречним, Mixinніж повний клас, оскільки "Перетягування" насправді не є об'єктом, це здатність об'єкта.
Росс Аллен

2
Я трохи погрався з ним, здається, перетягування за межі батька нічого не робить, але всякі дивні речі трапляються, коли його затягують в інший компонент реагування
Горкем Юрцевен,

11
Ви можете видалити залежність jquery, виконавши: var computedStyle = window.getComputedStyle(this.getDOMNode()); pos = { top: parseInt(computedStyle.top), left: parseInt(computedStyle.left) }; Якщо ви використовуєте jquery з response, ви, мабуть, робите щось не так;) Якщо вам потрібен якийсь плагін jquery, я вважаю, що він, як правило, простіший і менше коду, щоб переписати його в чистій реакції.
Matt Crinklaw-Vogt,

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/…
Кріс Саттінгер,

63

Я реалізував response-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визначаємо поведінку перетягування цього компонента. Як компоненти, що перетягуються, так і елементи, що випадають, використовують один і той же мікс.

Усередині configureDragDropнам потрібно зателефонувати registerTypeкожному нашому користувачеві, ItemTypesякий підтримує компонент. Наприклад, у вашому додатку може бути кілька зображень зображень, і кожна з них надає файлdragSource для ItemTypes.IMAGE.

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

Нарешті, ви повинні використовувати {...this.dragSourceFor(itemType)}деякі (один або кілька) елементів, renderщоб приєднати обробники перетягування. Це означає, що ви можете мати кілька «ручок перетягування» в одному елементі, і вони можуть навіть відповідати різним типам предметів. (Якщо ви не знайомі з синтаксисом розповсюдження атрибутів JSX , перевірте його).

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

Скажімо, ми хочемо 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) дозволяє вказати кілька типів одночасно, тому одна зона падіння може вловлювати багато різних типів.
  • Коли вам потрібен більш детальний контроль, більшість методів передаються подією перетягування, яка спричинила їх як останній параметр.

Щоб отримати сучасну документацію та інструкції з встановлення, перейдіть до реакції та репо на Github .


5
Що спільного у перетягуванні та перетягуванні миші, крім використання миші? Ваша відповідь взагалі не пов’язана з питанням, і, очевидно, це реклама в бібліотеці.
polkovnikov.ph

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

7
Так, я це прекрасно знаю. Той факт, що у вас є документація в іншому місці, не означає, що це відповідь на задане питання. Ви могли б написати "використовувати Google" для того самого результату. 29 голосів «за» - за довгий допис відомої особи, а не за якість відповіді.
polkovnikov.ph

оновлені посилання на офіційні приклади вільно перетягуваних речей за допомогою реакції-dnd: basic , advanced
uryga

23

Відповідь Джареда Форсайта жахливо помилкова і застаріла. Це слідує за цілим набором антишаблонів, таких як використанняstopPropagation , ініціалізація стану з реквізитів , використання jQuery, вкладених об'єктів у стан і має якесь непарне draggingполе стану. Якщо його переписати, рішення буде наступним, але воно все одно змушує віртуальне узгодження DOM при кожному тиску переміщення миші і не дуже ефективно.

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

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
Дякую за це, це, безумовно, не найефективніше рішення, але воно відповідає найкращим практикам побудови програм сьогодні.
Спец

1
@ryanj Ні, значення за замовчуванням - це зло, в цьому проблема. Яка правильна дія при зміні реквізиту? Чи слід скинути стан до нового за замовчуванням? Чи слід порівнювати нове значення за замовчуванням зі старим значенням за замовчуванням, щоб скинути стан до значення за замовчуванням лише тоді, коли значення за замовчуванням змінилося? Немає можливості обмежити користувача користуватися лише постійним значенням, і нічим іншим. Ось чому це антишаблон. Значення за замовчуванням слід створювати явно за допомогою компонентів високого порядку (тобто для всього класу, а не для об'єкта), і ніколи не слід встановлювати за допомогою props.
polkovnikov.ph

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

2
Дійсно дуже просте та елегантне рішення. Я радий бачити, що мій погляд на це був схожий. У мене є одне запитання: ви згадуєте низьку продуктивність, що б ви запропонували для досягнення подібної функції з урахуванням продуктивності?
Гійом М

1
У будь-якому разі зараз у нас є гачки, і я повинен ще раз оновити відповідь ще раз.
polkovnikov.ph

13

Я оновив рішення 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 це розмір сітки (до прив'язки) у напрямку х. Для покращення продуктивності рекомендується мати gridX та gridY> 1.
anyhotcountry

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

11

також легко використовувати. 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>

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


8

Ось простий сучасний підхід до цього з 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

Здається, це найсвіжіша відповідь тут.
codyThompson

2

Я хотів би додати 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 - це елегантна річ (я ніколи не думав, що скажу це)


1
чи можете ви заповнити setXandYфункцію уявним компонентом?
Thellimist

0

Я оновив клас, використовуючи посилання, оскільки всі рішення, які я бачу тут, містять речі, які більше не підтримуються або незабаром амортизуються 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.