# dnd-拖拽改变文件的位置

# 1. 介绍

原理:

  1. 侦听元素的 mousedown 事件

  2. 一旦 mousedown,则注册 mousemove 和 mouseup,并记录元素初始的位置和鼠标按下的位置

    • 在 mousemove 里改变元素的位置
    • 在 mouseup 里注销 mousemove 和 mouseup 事件

缺点:

  1. 需要在模板元素上注册事件,如果光标移动过快,则元素不会随光标移动 且 事件不能注销,造成操作错乱

# 2. 快速开始

<style>
  #box {
    position: absolute;
    top: 100px;
    left: 200px;
    width: 100px;
    height: 100px;
    background: red;
  }
</style>

<div id="box"></div>

<script>
const boxElement = document.querySelector('#box');

let position = {
  origin: { x: 0, y: 0 },
  mousedown: { x: 0, y: 0 },
  mousemove: { x: 0, y: 0 },
};

const update = () => {
  const { origin: { x, y }, mousedown, mousemove } = position; 

  const deltaX = mousemove.x - mousedown.x;
  const deltaY = mousemove.y - mousedown.y;

  boxElement.style.left = `${x + deltaX}px`;
  boxElement.style.top = `${y + deltaY}px`;
};

const mousemoveHandler = (event) => {
  const { x, y } = event;

  position.mousemove.x = x;
  position.mousemove.y = y;

  update();
};

const mouseupHandler = () => {
  boxElement.removeEventListener('mousemove', mousemoveHandler, false);
  boxElement.removeEventListener('mouseup', mouseupHandler, false);
};

const mousedownHandler = (event) => {
  position.mousedown.x = event.x;
  position.mousedown.y = event.y;

  const { x, y } = boxElement.getBoundingClientRect();
  position.origin.x = x;
  position.origin.y = y;

  boxElement.addEventListener('mousemove', mousemoveHandler, false);
  boxElement.addEventListener('mouseup', mouseupHandler, false);
}

boxElement.addEventListener('mousedown', mousedownHandler, false);
</script>

# 3. 封装为类

class Draggable {
  /** @type {HTMLElement} */
  target = null;

  /** @type {HTMLElement} */
  handle = null;

  /** @type {HTMLElement} */
  container = null;

  options = {
    /**
     * default target
     * @type {HTMLElement | string}
     */
    handle: null,

    /**
     * @type {HTMLElement | string}
     */
    container: document.body,
  };

  /**
   * @param target {HTMLElement | string}
   * @param options {{ handle?: HTMLElement | string, container?: HTMLElement | string }}
   */
  constructor(target, options = {}) {
    this.setTarget(target);
    this.setOptions(options);

    this.setHandle();
    this.setContainer();

    this.setDraggable();
  }

  setTarget(target) {
    this.target = this.getElement(target);
  }

  setOptions(options) {
    this.options = {
      ...this.options,
      ...options,
    };
  }

  setHandle() {
    const { handle } = this.options;

    if (!handle) {
      this.handle = this.target;
      return;
    }

    this.handle = this.getElement(handle);
  }

  setContainer() {
    const { container } = this.options;

    this.container = this.getElement(container);
  }

  getElement(selectorOrElement) {
    if (typeof selectorOrElement === 'string') {
      return document.querySelector(selectorOrElement);
    }

    return selectorOrElement;
  }

  setDraggable() {
    this.handle.addEventListener('mousedown', this.handleMousedown);
  }

  /** @param event {MouseEvent} */
  handleMousedown = (event) => {
    this.container.addEventListener('mousemove', this.handleMousemove);
    this.container.addEventListener('mouseup', this.handleMouseup);

    event.preventDefault();
  }

  handleMouseup = () => {
    this.container.removeEventListener('mousemove', this.handleMousemove);
    this.container.removeEventListener('mouseup', this.handleMouseup);
  }

  /** @param event {MouseEvent} */
  handleMousemove = (event) => {
    const { movementX: offsetX, movementY: offsetY } = event;

    const { x: left, y: top } = this.getTargetPosition();

    this.setTargetPosition(left + offsetX, top + offsetY);

    event.preventDefault();
  }

  setTargetPosition(x, y) {
    this.target.style.left = `${x}px`;
    this.target.style.top = `${y}px`;
  }

  getTargetPosition() {
    const { x, y } = this.target.getBoundingClientRect();
    return { x, y };
  }

  destroy() {
    this.handle.removeEventListener('mousedown', this.handleMousedown);

    this.target = null;
    this.handle = null;
    this.container = null;

    this.options = null;
  }
}

export default Draggable;

# 4. 参考

本章目录