Я хочу сделать перетаскиваемый (то есть перемещаемый мышью) компонент React, который, кажется, обязательно включает обработчики глобального состояния и разрозненных событий. Я могу сделать это грязным способом, используя глобальную переменную в моем JS-файле, и, возможно, даже мог бы обернуть его в красивый интерфейс закрытия, но я хочу знать, есть ли способ, который лучше взаимодействует с React.
Кроме того, поскольку я никогда раньше не делал этого в чистом JavaScript, я хотел бы посмотреть, как это делает эксперт, чтобы убедиться, что у меня есть все угловые случаи, особенно в том, что касается React.
Спасибо.
javascript
reactjs
Эндрю Флинор
источник
источник
Ответы:
Возможно, мне стоит превратить это в сообщение в блоге, но вот довольно убедительный пример.
Комментарии должны довольно хорошо объяснить ситуацию, но дайте мне знать, если у вас есть вопросы.
И вот скрипка, с которой можно поиграть: 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 указал, что, поскольку «перетаскиваемый» - это больше атрибут, чем вещь сама по себе, она могла бы лучше служить миксином. Мой опыт работы с миксинами ограничен, поэтому я не видел, как они могут помочь или помешать в сложных ситуациях. Это вполне может быть лучшим вариантом.
источник
Mixin
чем полный класс, поскольку «Draggable» на самом деле не объект, это способность объекта.var computedStyle = window.getComputedStyle(this.getDOMNode()); pos = { top: parseInt(computedStyle.top), left: parseInt(computedStyle.left) };
Если вы используете jquery с response, вы, вероятно, делаете что-то не так;) Если вам нужен какой-то плагин jquery, я считаю, что это обычно проще и меньше кода, чтобы переписать его в чистой реакции.this.getDOMNode().getBoundingClientRect()
надежное решение - getComputedStyle может выводить любое допустимое свойство CSS, включаяauto
в этом случае приведенный выше код приведет к созданию файлаNaN
. См. Статью MDN: developer.mozilla.org/en-US/docs/Web/API/Element/…this.getDOMNode()
, это устарело. Используйте ссылку, чтобы получить узел dom. facebook.github.io/react/docs/…Я реализовал response-dnd , гибкий миксин перетаскивания HTML5 для React с полным контролем DOM.
Существующие библиотеки перетаскивания не подходили для моего варианта использования, поэтому я написал свою собственную. Он похож на код, который мы запускаем около года на Stampsy.com, но переписан, чтобы воспользоваться преимуществами React и Flux.
Основные требования, которые у меня были:
Если они вам знакомы, читайте дальше.
Применение
Простой источник перетаскивания
Сначала объявите типы данных, которые можно перетаскивать.
Они используются для проверки «совместимости» источников и целей перетаскивания:
// 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
быть целью дляIMAGE
s. Это почти то же самое, за исключением того, что мы должны дать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 .
источник
Ответ Джареда Форсайта ужасно неверен и устарел. Он следует за целым набором антипаттернов, таких как использование
stopPropagation
, инициализация состояния из реквизита , использование jQuery, вложенные объекты в состоянии и некоторыеdragging
поля нечетного состояния. В случае переписывания решение будет следующим, но оно по-прежнему вызывает согласование виртуального DOM при каждом такте перемещения мыши и не очень эффективно.UPD. Мой ответ был ужасно неправильным и устаревшим. Теперь код устраняет проблемы, связанные с медленным жизненным циклом компонентов React, с помощью собственных обработчиков событий и обновлений стилей, использует,
transform
поскольку он не приводит к перекомпоновке, и регулирует изменения DOMrequestAnimationFrame
. Теперь у меня стабильно 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.
источник
Я обновил решение 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;
источник
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>
Вам нужны их стили, а это коротко, иначе вы получите не совсем ожидаемое поведение. Мне нравится это поведение больше, чем некоторые другие возможные варианты, но есть еще кое-что, называемое « реагировать с изменением размера и перемещением» . Я пытаюсь изменить размер, работая с перетаскиванием, но пока без радости.
источник
Вот простой современный подход к этому с
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
источник
Я хотел бы добавить третий сценарий
Положение движения никак не сохраняется. Думайте об этом как о движении мыши - ваш курсор не является компонентом 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 - изящная вещь (я никогда не думал, что скажу это)
источник
setXandY
функцию воображаемым компонентом?Вот ответ 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> ); }
источник
Я обновил класс, используя ссылки, так как все решения, которые я вижу здесь, имеют вещи, которые больше не поддерживаются или скоро будут обесцениваться, например
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;
источник