YES/NO - is there a way to improve mouse dragging

2019-06-09 00:32发布

So I was spending some time playing around with pure (no external libraries) SVG elements dragging.

In general all works, but there is this nasty issue for fast moving mouse: - when user mousedowns a draggable SVG element close to its edge - then drags (mousemove) such draggable too fast - the mouse "loses" the draggable

Here the issue is described in more details: http://www.svgopen.org/2005/papers/AdvancedMouseEventModelForSVG-1/index.html#S3.2 Also here the author tried to fix UX by leveraging mouseout event: http://nuclearprojects.com/blog/svg-click-and-drag-object-with-mouse-code/

I copied the above code snippet here: http://codepen.io/cmer41k/pen/zNGwpa

The question I have is:

Is there no other way (provided by pure SVG) to prevent such "loss" of SVG element while mouse moves too fast?

My attempt to solve this was: - detect (somehow) that mouseout event happened without finishing the dragging. - and if so (we sort of detected "disconnect") - reconnect the SVG element with current mouse position.

Is there a reason why this wouldn't work?

Code:

    var click=false; // flag to indicate when shape has been clicked
    var clickX, clickY; // stores cursor location upon first click
    var moveX=0, moveY=0; // keeps track of overall transformation
    var lastMoveX=0, lastMoveY=0; // stores previous transformation (move)
    function mouseDown(evt){
        evt.preventDefault(); // Needed for Firefox to allow dragging correctly
        click=true;
        clickX = evt.clientX; 
        clickY = evt.clientY;
        evt.target.setAttribute("fill","green");
    }

    function move(evt){
        evt.preventDefault();
        if(click){
            moveX = lastMoveX + ( evt.clientX – clickX );
            moveY = lastMoveY + ( evt.clientY – clickY );

            evt.target.setAttribute("transform", "translate(" + moveX + "," + moveY + ")");
        }
    }

    function endMove(evt){
        click=false;
        lastMoveX = moveX;
        lastMoveY = moveY;
        evt.target.setAttribute("fill","gray");
    }

标签: svg mouse drag
1条回答
爷、活的狠高调
2楼-- · 2019-06-09 01:14

The most important part of your code is missing, namely how or more specifically on which element you register the events.

What you basically do to prevent this problem is to register the mousemove and mouseup events on the outermost svg element, and not on the element you want to drag.

svg.addEventListener("mousemove", move)
svg.addEventListener("mouseup", endMove)

When starting the drag, register the events on the svg element, and when done unregister them.

svg.removeEventListener("mousemove", move)
svg.removeListener("mouseup", endMove)

you have to store the element you are currently dragging, so it is available in the other event handlers.

what i additionally do is to set pointer-events to "none" on the dragged element so that you can react to mouse events underneath the dragged element (f.e. finding the drop target...)

evt.target.setAttribute("pointer-events", "none")

but don't forget to set it back to something sensible when dragging is done

evt.target.setAttribute("pointer-events", "all")

var click = false; // flag to indicate when shape has been clicked
var clickX, clickY; // stores cursor location upon first click
var moveX = 0,
  moveY = 0; // keeps track of overall transformation
var lastMoveX = 0,
  lastMoveY = 0; // stores previous transformation (move)
var currentTarget = null

function mouseDown(evt) {
  evt.preventDefault(); // Needed for Firefox to allow dragging correctly
  click = true;
  clickX = evt.clientX;
  clickY = evt.clientY;
  evt.target.setAttribute("fill", "green");
  // register move events on outermost SVG Element
  currentTarget = evt.target
  svg.addEventListener("mousemove", move)
  svg.addEventListener("mouseup", endMove)
  evt.target.setAttribute("pointer-events", "none")
}

function move(evt) {
  evt.preventDefault();
  if (click) {
    moveX = lastMoveX + (evt.clientX - clickX);
    moveY = lastMoveY + (evt.clientY - clickY);
    currentTarget.setAttribute("transform", "translate(" + moveX + "," + moveY + ")");
  }
}

function endMove(evt) {
  click = false;
  lastMoveX = moveX;
  lastMoveY = moveY;
  currentTarget.setAttribute("fill", "gray");
  svg.removeEventListener("mousemove", move)
  svg.removeEventListener("mouseup", endMove)
  currentTarget.setAttribute("pointer-events", "all")
}
<svg id="svg" width="800" height="600" style="border: 1px solid black; background: #E0FFFF;">
  <rect x="0" y="0" width="800" height="600" fill="none" pointer-events="all" />
  <circle id="mycirc" cx="60" cy="60" r="22" onmousedown="mouseDown(evt)" />
</svg>

more advanced

there are still two things not so well with this code.

  1. it does not work for viewBoxed SVGs nor for elements inside transformed parents.
  2. all the globals are bad coding practice.

here is how to fix those: Nr. 1 is solved by converting mouse coordinates into local coordinates using the inverse of getScreenCTM (CTM = Current Transformation Matrix).

function globalToLocalCoords(x, y) {
    var p = elem.ownerSVGElement.createSVGPoint()
    var m = elem.parentNode.getScreenCTM()
    p.x = x
    p.y = y
    return p.matrixTransform(m.inverse())
  }

For nr. 2 see this implementation:

var dre = document.querySelectorAll(".draggable")
for (var i = 0; i < dre.length; i++) {
  var o = new Draggable(dre[i])
}

function Draggable(elem) {
  this.target = elem
  this.clickPoint = this.target.ownerSVGElement.createSVGPoint()
  this.lastMove = this.target.ownerSVGElement.createSVGPoint()
  this.currentMove = this.target.ownerSVGElement.createSVGPoint()
  this.target.addEventListener("mousedown", this)
  this.handleEvent = function(evt) {
    evt.preventDefault()
    this.clickPoint = globalToLocalCoords(evt.clientX, evt.clientY)
    this.target.classList.add("dragged")
    this.target.setAttribute("pointer-events", "none")
    this.target.ownerSVGElement.addEventListener("mousemove", this.move)
    this.target.ownerSVGElement.addEventListener("mouseup", this.endMove)
  }
  this.move = function(evt) {
    var p = globalToLocalCoords(evt.clientX, evt.clientY)
    this.currentMove.x = this.lastMove.x + (p.x - this.clickPoint.x)
    this.currentMove.y = this.lastMove.y + (p.y - this.clickPoint.y)
    this.target.setAttribute("transform", "translate(" + this.currentMove.x + "," + this.currentMove.y + ")")
  }.bind(this)

  this.endMove = function(evt) {
    this.lastMove.x = this.currentMove.x
    this.lastMove.y = this.currentMove.y
    this.target.classList.remove("dragged")
    this.target.setAttribute("pointer-events", "all")
    this.target.ownerSVGElement.removeEventListener("mousemove", this.move)
    this.target.ownerSVGElement.removeEventListener("mouseup", this.endMove)
  }.bind(this)

  function globalToLocalCoords(x, y) {
    var p = elem.ownerSVGElement.createSVGPoint()
    var m = elem.parentNode.getScreenCTM()
    p.x = x
    p.y = y
    return p.matrixTransform(m.inverse())
  }
}
.dragged {
  fill-opacity: 0.5;
  stroke-width: 0.5px;
  stroke: black;
  stroke-dasharray: 1 1;
}
.draggable{cursor:move}
<svg id="svg" viewBox="0 0 800 600" style="border: 1px solid black; background: #E0FFFF;">
  <rect x="0" y="0" width="800" height="600" fill="none" pointer-events="all" />
  <circle class="draggable" id="mycirc" cx="60" cy="60" r="22" fill="blue" />
  <g transform="rotate(45,175,75)">
    <rect class="draggable" id="mycirc" x="160" y="60" width="30" height="30" fill="green" />
  </g>
  <g transform="translate(200 200) scale(2 2)">
    <g class="draggable">
      <circle cx="0" cy="0" r="30" fill="yellow"/>
      <text text-anchor="middle" x="0" y="0" fill="red">I'm draggable</text>
    </g>
  </g>
</svg>
<div id="out"></div>

查看更多
登录 后发表回答