วิธีที่แนะนำในการทำให้ React component / div สามารถลากได้


100

ฉันต้องการสร้างองค์ประกอบการตอบสนองที่ลากได้ (นั่นคือเปลี่ยนตำแหน่งได้ด้วยเมาส์) ซึ่งดูเหมือนว่าจำเป็นต้องเกี่ยวข้องกับสถานะทั่วโลกและตัวจัดการเหตุการณ์ที่กระจัดกระจาย ฉันทำได้แบบสกปรกโดยมีตัวแปรส่วนกลางในไฟล์ JS ของฉันและอาจรวมไว้ในอินเทอร์เฟซการปิดที่ดี แต่ฉันต้องการทราบว่ามีวิธีที่ทำให้ React ดีขึ้นหรือไม่

นอกจากนี้เนื่องจากฉันไม่เคยทำสิ่งนี้ใน JavaScript ดิบมาก่อนฉันจึงต้องการดูว่าผู้เชี่ยวชาญทำอย่างไรเพื่อให้แน่ใจว่าฉันได้จัดการกับกรณีต่างๆทั้งหมดโดยเฉพาะอย่างยิ่งเมื่อเกี่ยวข้องกับ React

ขอบคุณ.


ที่จริงอย่างน้อยฉันก็พอใจกับคำอธิบายร้อยแก้วเป็นรหัสหรือแม้แต่แค่ "คุณทำได้ดี" แต่นี่คือ JSFiddle ของงานของฉันจนถึงตอนนี้: jsfiddle.net/Z2JtM
Andrew Fleenor

ฉันยอมรับว่านี่เป็นคำถามที่ถูกต้องเนื่องจากมีตัวอย่างโค้ดตอบกลับให้ดูน้อยมากในปัจจุบัน
Jared Forsyth

1
พบวิธี HTML5 ง่ายๆสำหรับกรณีการใช้งานของฉัน - youtu.be/z2nHLfiiKBA อาจมีคนช่วย !!
เปรม

ลองทำตามนี้ HOC ง่าย ๆ ในการเปลี่ยนองค์ประกอบที่ห่อหุ้มให้ลากได้ npmjs.com/package/just-drag
Shan

คำตอบ:


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)เมื่อใดก็ตามที่เกิดเหตุการณ์มูสมูฟขึ้น

สถานการณ์จำลอง 2

ผู้ปกครองจะต้องเป็นเจ้าของ "ตำแหน่งที่ไม่เคลื่อนที่" เท่านั้นดังนั้นผู้ลากจะเป็นเจ้าของ "ตำแหน่งลาก" แต่เมื่อใช้เมาส์จะเรียก this.props.onChange(x, y)และเลื่อนการตัดสินใจขั้นสุดท้ายไปยังผู้ปกครอง หากผู้ปกครองไม่ชอบจุดที่การลากเลื่อนลงเอยด้วยการลากจะไม่อัปเดตสถานะและการลากจะ "สแนปกลับ" ไปที่ตำแหน่งเริ่มต้นก่อนที่จะลาก

Mixin หรือส่วนประกอบ?

@ssorallen ชี้ให้เห็นว่าเนื่องจาก "ลากได้" เป็นแอตทริบิวต์มากกว่าสิ่งของในตัวมันเองจึงอาจให้บริการได้ดีกว่าในฐานะมิกซ์อิน ประสบการณ์ของฉันเกี่ยวกับมิกซ์อินมี จำกัด ดังนั้นฉันจึงไม่เห็นว่าพวกเขาจะช่วยหรือหลีกเลี่ยงในสถานการณ์ที่ซับซ้อนได้อย่างไร นี่อาจเป็นตัวเลือกที่ดีที่สุด


4
ตัวอย่างที่ดี สิ่งนี้ดูเหมาะสมMixinกว่าในระดับเต็มเนื่องจาก "Draggable" ไม่ใช่วัตถุ แต่เป็นความสามารถของวัตถุ
Ross Allen

2
ฉันเล่นกับมันเล็กน้อยดูเหมือนว่าการลากออกไปข้างนอกแม่ของมันไม่ได้ทำอะไรเลย แต่มีสิ่งแปลก ๆ เกิดขึ้นเมื่อมันถูกลากไปยังส่วนประกอบปฏิกิริยาอื่น
Gorkem Yurtseven

11
คุณสามารถลบการพึ่งพา jquery ได้โดยทำ: var computedStyle = window.getComputedStyle(this.getDOMNode()); pos = { top: parseInt(computedStyle.top), left: parseInt(computedStyle.left) }; หากคุณใช้ jquery กับการตอบสนองคุณอาจทำอะไรผิดพลาด;) หากคุณต้องการปลั๊กอิน jquery ฉันพบว่ามันมักจะง่ายกว่าและเขียนโค้ดน้อยลงในการตอบสนองที่บริสุทธิ์
Matt Crinklaw-Vogt

7
เพียงแค่ต้องการติดตามความคิดเห็นข้างต้นโดย @ MattCrinklaw-Vogt เพื่อบอกว่าต้องใช้วิธีการกันกระสุนมากกว่าthis.getDOMNode().getBoundingClientRect()- getComputedStyle สามารถส่งออกคุณสมบัติ CSS ที่ถูกต้องautoซึ่งในกรณีนี้รหัสด้านบนจะส่งผลให้เกิดไฟล์NaN. ดูบทความของ MDN: developer.mozilla.org/en-US/docs/Web/API/Element/…
Andru

2
และเพื่อติดตามอีกครั้งthis.getDOMNode()ที่เลิกใช้งานแล้ว ใช้ ref เพื่อรับโหนด dom facebook.github.io/react/docs/…
Chris Sattinger

65

ฉันใช้react-dndซึ่งเป็นมิกซ์อินแบบลากแล้ววาง HTML5 ที่ยืดหยุ่นสำหรับการตอบสนองด้วยการควบคุม DOM แบบเต็ม

ไลบรารีลากแล้ววางที่มีอยู่ไม่เหมาะกับกรณีการใช้งานของฉันดังนั้นฉันจึงเขียนของตัวเอง คล้ายกับรหัสที่เราใช้งานมาประมาณหนึ่งปีบน Stampsy.com แต่เขียนใหม่เพื่อใช้ประโยชน์จาก React และ Flux

ข้อกำหนดสำคัญที่ฉันมี:

  • ปล่อย DOM หรือ CSS ของตัวเองเป็นศูนย์ปล่อยทิ้งไว้ที่ส่วนประกอบที่ใช้
  • กำหนดโครงสร้างให้น้อยที่สุดสำหรับส่วนประกอบที่บริโภค
  • ใช้การลากและวาง HTML5 เป็นแบ็กเอนด์หลัก แต่ทำให้สามารถเพิ่มแบ็กเอนด์ต่างๆได้ในอนาคต
  • เช่นเดียวกับ HTML5 API ดั้งเดิมให้เน้นการลากข้อมูลไม่ใช่แค่ "มุมมองที่ลากได้"
  • ซ่อน HTML5 API quirks จากโค้ดที่ใช้;
  • ส่วนประกอบที่แตกต่างกันอาจเป็น "แหล่งที่มาของการลาก" หรือ "เป้าหมายที่วาง" สำหรับข้อมูลประเภทต่างๆ
  • อนุญาตให้ส่วนประกอบหนึ่งมีแหล่งข้อมูลการลากหลายแหล่งและวางเป้าหมายเมื่อจำเป็น
  • ทำให้เป้าหมายการดร็อปเปลี่ยนรูปลักษณ์ได้ง่ายหากมีการลากหรือวางข้อมูลที่เข้ากันได้
  • ทำให้ง่ายต่อการใช้ภาพเพื่อลากภาพขนาดย่อแทนภาพหน้าจอขององค์ประกอบหลีกเลี่ยงความแปลกประหลาดของเบราว์เซอร์

หากเสียงเหล่านี้คุ้นเคยสำหรับคุณอ่านต่อ

การใช้งาน

แหล่งลากอย่างง่าย

ขั้นแรกประกาศประเภทข้อมูลที่สามารถลากได้

สิ่งเหล่านี้ใช้เพื่อตรวจสอบ "ความเข้ากันได้" ของแหล่งที่มาของการลากและวางเป้าหมาย:

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

(หากคุณไม่มีข้อมูลหลายประเภท Libary นี้อาจไม่เหมาะกับคุณ)

จากนั้นมาสร้างส่วนประกอบที่ลากได้ง่ายมากซึ่งเมื่อลากจะแสดงถึง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เพื่อส่งคืนรายการที่แสดงถึงข้อมูลที่คุณกำลังลากและตัวเลือกบางตัวเลือกที่จะปรับ UI การลาก คุณสามารถเลือกที่canDragจะใช้เพื่อห้ามการลากหรือendDrag(didDrop)เรียกใช้ตรรกะบางอย่างเมื่อเกิดการดร็อป (หรือไม่มี) และคุณสามารถแชร์ตรรกะนี้ระหว่างคอมโพเนนต์โดยปล่อยให้มิกซ์อินที่แชร์สร้างdragSourceให้

สุดท้ายคุณต้องใช้{...this.dragSourceFor(itemType)}กับองค์ประกอบบางอย่าง (หนึ่งหรือมากกว่า) renderเพื่อแนบตัวจัดการการลาก ซึ่งหมายความว่าคุณสามารถมี "ที่จับสำหรับลาก" ได้หลายแบบในองค์ประกอบเดียวและอาจเกี่ยวข้องกับรายการประเภทต่างๆ (หากคุณไม่คุ้นเคยกับJSX Spread Attributesไวยากรณ์โปรดตรวจสอบ)

วางเป้าหมายง่ายๆ

สมมติว่าเราต้องการImageBlockเป็นเป้าหมายตกสำหรับIMAGEs มันสวยมากเหมือนกันยกเว้นว่าเราต้องให้การดำเนินงาน:registerTypedropTarget

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>
    );
  }
);

ลาก 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และสำหรับdragSourceItemTypes.BLOCK
  • สมมติว่าเราเพิ่มบล็อกประเภทอื่น ๆ เราสามารถนำตรรกะการจัดลำดับใหม่มาใช้ใหม่ได้โดยวางไว้ในมิกซ์อิน
  • dropTargetFor(...types) อนุญาตให้ระบุหลายประเภทพร้อมกันดังนั้นดร็อปโซนหนึ่งสามารถจับได้หลายประเภท
  • เมื่อคุณต้องการการควบคุมแบบละเอียดมากขึ้นเมธอดส่วนใหญ่จะส่งผ่านเหตุการณ์ลากที่ทำให้เกิดเป็นพารามิเตอร์สุดท้าย

สำหรับการขึ้นไปวันที่เอกสารคำแนะนำและการติดตั้งหัวเพื่อตอบสนอง-DND repo บน Github


6
การลากและวางและการลากเมาส์มีอะไรที่เหมือนกันนอกเหนือจากการใช้เมาส์? คำตอบของคุณไม่เกี่ยวข้องกับคำถามเลยและชัดเจนว่าเป็นโฆษณาของห้องสมุด
polkovnikov.ph

5
ดูเหมือนว่า 29 คนพบว่าเกี่ยวข้องกับคำถามนี้ React DnD ยังช่วยให้คุณใช้การลากเมาส์ได้ดี ฉันจะคิดว่าดีกว่าที่จะแบ่งปันงานของฉันฟรีและทำงานกับตัวอย่างและเอกสารที่กว้างขวางในครั้งต่อไปดังนั้นฉันจึงไม่ต้องเสียเวลาอ่านความคิดเห็นที่น่ารังเกียจ
Dan Abramov

7
ใช่ฉันรู้ดี การที่คุณมีเอกสารประกอบที่อื่นไม่ได้หมายความว่านี่เป็นคำตอบสำหรับคำถามที่ระบุ คุณสามารถเขียน "ใช้ Google" เพื่อให้ได้ผลลัพธ์เดียวกัน 29 upvotes เกิดจากการโพสต์ที่ยาวนานโดยบุคคลที่รู้จักไม่ใช่เพราะคุณภาพของคำตอบ
polkovnikov.ph

อัปเดตลิงก์ไปยังตัวอย่างอย่างเป็นทางการของสิ่งที่ลากได้อย่างอิสระด้วย react-dnd: basic , advanced
uryga

@DanAbramov ฉันขอขอบคุณในความพยายามของคุณในการโพสต์หากไม่มีฉันอาจไม่ได้รับการตอบสนองอย่างรวดเร็ว เช่นเดียวกับผลิตภัณฑ์จำนวนมากของคุณในตอนแรกดูเหมือนว่าจะซับซ้อนโดยไม่จำเป็นแต่รวมเข้าด้วยกันอย่างมืออาชีพ (ในเอกสารของคุณฉันมักจะเรียนรู้เกี่ยวกับวิศวกรรมซอฟต์แวร์ประมาณหนึ่งหรือสองอย่าง ... ) ฉันได้อ่าน "คนที่ไม่ทำ รู้ว่ายูนิกซ์ถึงวาระที่จะนำไปใช้งานใหม่ ... แย่มาก "และฉันรู้สึกว่าสิ่งที่คล้ายกันนี้อาจเป็นจริงสำหรับการสร้างสรรค์ของคุณหลาย ๆ อย่าง
Nathan Chappell

23

คำตอบของ Jared Forsyth นั้นผิดและล้าสมัยอย่างมาก มันเป็นไปตามรูปแบบทั้งหมดเช่นการใช้งานของstopPropagation , รัฐเริ่มต้นจากอุปกรณ์ประกอบฉากการใช้งานของ jQuery วัตถุที่ซ้อนกันในรัฐและมีบางส่วนที่แปลกdraggingฟิลด์รัฐ หากมีการเขียนใหม่โซลูชันจะเป็นดังต่อไปนี้ แต่ยังคงบังคับให้มีการกระทบยอด DOM เสมือนในทุกครั้งที่เลื่อนเมาส์และไม่มีประสิทธิภาพมากนัก

UPD. คำตอบของฉันผิดและล้าสมัยอย่างมาก ตอนนี้โค้ดช่วยบรรเทาปัญหาของวงจรการใช้งานคอมโพเนนต์การตอบสนองที่ช้าโดยใช้ตัวจัดการเหตุการณ์ดั้งเดิมและการอัปเดตสไตล์ใช้transformที่มันไม่ได้นำไปสู่การจัดเรียงใหม่และ throttles เปลี่ยนแปลง 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 ไม่ค่าเริ่มต้นเป็นสิ่งที่ชั่วร้ายนั่นคือปัญหา อะไรคือการกระทำที่เหมาะสมเมื่ออุปกรณ์ประกอบฉากเปลี่ยนไป? เราควรรีเซ็ตสถานะเป็นค่าเริ่มต้นใหม่หรือไม่? เราควรเปรียบเทียบค่าเริ่มต้นใหม่กับค่าเริ่มต้นเก่าเพื่อรีเซ็ตสถานะเป็นค่าเริ่มต้นเฉพาะเมื่อค่าเริ่มต้นเปลี่ยนแปลงหรือไม่ ไม่มีทางที่จะ จำกัด ผู้ใช้ให้ใช้เฉพาะค่าคงที่และไม่มีอะไรอื่น นั่นเป็นเหตุผลว่าทำไมถึงเป็นแอนตี้แพตเทิร์น ควรสร้างค่าดีฟอลต์อย่างชัดเจนผ่านส่วนประกอบลำดับสูง (เช่นสำหรับทั้งคลาสไม่ใช่สำหรับอ็อบเจ็กต์) และไม่ควรตั้งค่าผ่านอุปกรณ์ประกอบฉาก
polkovnikov.ph

1
ฉันไม่เห็นด้วยอย่างเคารพ - สถานะองค์ประกอบเป็นสถานที่ที่ยอดเยี่ยมในการจัดเก็บข้อมูลที่เฉพาะเจาะจงกับ UI ของส่วนประกอบซึ่งไม่มีความเกี่ยวข้องกับแอปโดยรวมเช่น หากไม่สามารถส่งผ่านค่าเริ่มต้นเป็นอุปกรณ์ประกอบฉากได้ในบางกรณีตัวเลือกสำหรับการดึงข้อมูลนี้หลังการเมาท์มี จำกัด และในหลาย ๆ สถานการณ์ (ส่วนใหญ่?) ที่ไม่พึงประสงค์น้อยกว่าความหลากหลายรอบ ๆ ส่วนประกอบที่อาจส่งผ่าน someDefaultValue prop ที่แตกต่างกันที่ a ในภายหลัง ฉันไม่ได้สนับสนุนให้มันเป็นแนวทางปฏิบัติที่ดีที่สุดหรืออะไรก็ตามมันไม่เป็นอันตรายอย่างที่คุณแนะนำ imo
ryan j

2
วิธีแก้ปัญหาที่ง่ายและสวยงามมาก ฉันมีความสุขที่เห็นว่าสิ่งที่ฉันทำมันคล้าย ๆ กัน ฉันมีคำถามหนึ่งข้อ: คุณพูดถึงประสิทธิภาพที่ไม่ดีคุณจะเสนออะไรเพื่อให้ได้คุณสมบัติที่คล้ายกันโดยคำนึงถึงประสิทธิภาพการทำงาน
Guillaume M

1
อย่างไรก็ตามตอนนี้เรามีตะขอและฉันต้องอัปเดตคำตอบอีกครั้งในไม่ช้า
polkovnikov.ph

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 เป็นขนาดของกริด (snap-to) ในทิศทาง x ขอแนะนำให้มี gridX และ gridY> 1 เพื่อปรับปรุงประสิทธิภาพ
anyhotcountry

สิ่งนี้ได้ผลดีสำหรับฉัน เกี่ยวกับการเปลี่ยนแปลงที่ฉันทำในฟังก์ชัน onStart (): การคำนวณ relX และ relY ฉันใช้ e.clienX-this.props.x สิ่งนี้ทำให้ฉันสามารถวางองค์ประกอบที่ลากได้ภายในคอนเทนเนอร์หลักแทนที่จะขึ้นอยู่กับทั้งหน้าเป็นพื้นที่ลาก ฉันรู้ว่ามันเป็นความคิดเห็นที่ล่าช้า แต่แค่อยากจะขอบคุณ
Geoff

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>

คุณต้องการสไตล์ที่สั้นหรือคุณไม่ได้รับพฤติกรรมที่คาดหวัง ผมชอบพฤติกรรมมากกว่าบางส่วนของตัวเลือกที่เป็นไปได้อื่น ๆ แต่ยังมีสิ่งที่เรียกว่าตอบสนอง-ปรับขนาดได้และสามารถเคลื่อนย้าย ฉันกำลังพยายามปรับขนาดการทำงานด้วยการลากได้ แต่ก็ยังไม่มีความสุข


9

นี่คือวิธีการที่ทันสมัยง่ายต่อการนี้กับ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

ฉันต้องการเพิ่มไฟล์ สถานการณ์ที่ 3

ตำแหน่งที่เคลื่อนที่จะไม่ถูกบันทึกไว้ แต่อย่างใด คิดว่ามันเป็นการเคลื่อนไหวของเมาส์ - เคอร์เซอร์ของคุณไม่ใช่ส่วนประกอบของปฏิกิริยาใช่ไหม?

สิ่งที่คุณทำคือเพิ่มเสาเช่น "ลากได้" ไปยังส่วนประกอบของคุณและสตรีมของเหตุการณ์การลากที่จะจัดการกับโดม

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 พร้อม Hook:

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

ผมได้ปรับปรุงระดับโดยใช้ refs เป็นการแก้ปัญหาทั้งหมดที่ฉันเห็นในที่นี่มีสิ่งที่ไม่ได้รับการสนับสนุนหรือเร็ว ๆ 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;

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.