ДА / НЕТ - есть ли способ улучшить перетаскивание мышью с помощью чистых инструментов SVG?

Поэтому я потратил некоторое время на игры с перетаскиванием чистых (без внешних библиотек) элементов SVG.

В целом все работает, но для быстро движущейся мыши есть одна неприятная проблема: - когда пользователь перемещает перетаскиваемый SVG-элемент близко к его краю, - затем перетаскивает (мышиный) такой перетаскиваемый объект слишком быстро - мышь "теряет" перетаскиваемый элемент.

Здесь проблема описана более подробно: http://www.svgopen.org/2005/papers/AdvancedMouseEventModelForSVG-1/index.html Также здесь автор попытался исправить UX, используя событие mouseout: http://nuclearprojects.com/blog/svg-click-and-drag-object-with-mouse-code/

Я скопировал приведенный фрагмент кода здесь: http://codepen.io/cmer41k/pen/zNGwpa

У меня есть вопрос:

Нет ли другого способа (предоставляемого чистым SVG) предотвратить такую ​​"потерю" элемента SVG, когда мышь движется слишком быстро?

Моя попытка решить эту проблему заключалась в следующем: - обнаружить (каким-то образом), что событие mouseout произошло без завершения перетаскивания. - и если это так (мы как бы обнаружили "отключение") - повторно подключите элемент SVG к текущей позиции мыши.

Есть ли причина, по которой это не сработает?

Код:

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

1 ответ

Решение

Самая важная часть вашего кода отсутствует, а именно, как или, более конкретно, на каком элементе вы регистрируете события.

Чтобы предотвратить эту проблему, вы в основном регистрируете события mousemove и mouseup на самом внешнем элементе svg, а не на элементе, который хотите перетащить.

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

При запуске перетаскивания зарегистрируйте события в элементе svg, а когда закончите, отмените их регистрацию.

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

Вы должны сохранить элемент, который вы в данный момент перетаскиваете, чтобы он был доступен в других обработчиках событий.

что я дополнительно делаю, это устанавливаю указатель-события на "перетаскиваемый" элемент "none", чтобы вы могли реагировать на события мыши под перетаскиваемым элементом (например, найти цель перетаскивания...)

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

но не забудьте вернуть его к чему-то разумному, когда перетаскивание сделано

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>

более продвинутый

есть еще две вещи, которые не очень хорошо с этим кодом.

  1. он не работает ни с SVG ViewBoxed, ни с элементами внутри преобразованных родителей.
  2. все глобалы - плохая практика кодирования.

вот как это исправить: Nr. 1 решается путем преобразования координат мыши в локальные координаты с использованием обратной функции 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())
  }

Для № 2 увидеть эту реализацию:

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>

Другие вопросы по тегам