14

I'm trying to implement a drag-like functionality using the next pattern:

  • Subscribe to marker Pointer Down event.
  • When Down event fires subscribe to Window Pointer Move and Up events and remove marker.
  • Perform some actions while Move.
  • When Up event fires unsubscribe from Move and Up.

This works for Mouse events, but doesn't work for Touch events. They don't fire after Touch Start target element is removed. I tried to use Pointer Events Polyfill but it doesn't work either.

I'm using Chrome Dev Tools to emulate touch events. See the sample:

initTestBlock('mouse', {
  start: 'mousedown',
  move: 'mousemove',
  end: 'mouseup'
});
initTestBlock('touch', {
  start: 'touchstart',
  move: 'touchmove',
  end: 'touchend'
});
initTestBlock('touch-no-remove', {
  start: 'touchstart',
  move: 'touchmove',
  end: 'touchend'
}, true);

function initTestBlock(id, events, noRemove) {
  var block = document.getElementById(id);
  var parent = block.querySelector('.parent');
  var target = block.querySelector('.target');
  target.addEventListener(events.start, function(e) {
    console.log(e.type);
    if (!noRemove) {
      setTimeout(function() {
        // Remove target
        target.parentElement.removeChild(target);
      }, 1000);
    }

    function onMove(e) {
      console.log(e.type);
      var pt = getCoords(e);
      parent.style.left = pt.x + 'px';
      parent.style.top = pt.y + 'px';
    }

    function onEnd(e) {
      console.log(e.type);
      window.removeEventListener(events.move, onMove);
      window.removeEventListener(events.end, onEnd);
    }

    window.addEventListener(events.move, onMove);
    window.addEventListener(events.end, onEnd);

  });
}

// Returns pointer coordinates
function getCoords(e) {
  if (e instanceof TouchEvent) {
    return {
      x: e.touches[0].pageX,
      y: e.touches[0].pageY
    };
  }
  return {
    x: e.pageX,
    y: e.pageY
  };
}

window.addEventListener('selectstart', function() {
  return false;
}, true);
.parent {
  background: darkred;
  color: white;
  width: 10em;
  height: 10em;
  position: absolute;
}
.target {
  background: orange;
  width: 4em;
  height: 4em;
}
#mouse .parent {
  left: 0em;
}
#touch .parent {
  left: 11em;
}
#touch-no-remove .parent {
  left: 22em;
}
<div id="mouse">
  <div class="parent">Mouse events
    <div class="target">Drag here</div>
  </div>
</div>
<div id="touch">
  <div class="parent">Touch events
    <div class="target">Drag here</div>
  </div>
</div>
<div id="touch-no-remove">
  <div class="parent">Touch (no remove)
    <div class="target">Drag here</div>
  </div>
</div>
Alexander Shutau
  • 2,660
  • 22
  • 32

2 Answers2

24

Indeed, according to the docs,

If the target element is removed from the document, events will still be targeted at it, and hence won't necessarily bubble up to the window or document anymore. If there is any risk of an element being removed while it is being touched, the best practice is to attach the touch listeners directly to the target.

It turns out that the solution is to attach touchmove and touchend listeners to the event.target itself, for example:

element.addEventListener("touchstart", (event) => {
    const onTouchMove = () => {
        // handle touchmove here
    }
    const onTouchEnd = () => {
        event.target.removeEventListener("touchmove", onTouchMove);
        event.target.removeEventListener("touchend", onTouchEnd);
        // handle touchend here
    }
    event.target.addEventListener("touchmove", onTouchMove);
    event.target.addEventListener("touchend", onTouchEnd);
    // handle touchstart here
});

Even if the event.target element is removed from the DOM, events will continue to fire normally and give correct coordinates.

ZitRo
  • 1,163
  • 15
  • 24
  • 1
    This saved my day! :D – raeffs Jan 25 '18 at 09:49
  • 3
    Why does this work? The `event.target` is located inside the element in the DOM, so if the element is removed from the DOM, so is the target. How can events on the target bubble up if the target is also removed from the DOM? – poshest Aug 18 '20 at 08:59
  • i recently ran into -- what im assuming is -- a bug in my own script whereby i added a `touchmove` event with the `passive: false` option to `body` in order to `preventDefault` and then removed it. once i removed it, it prevented CSS classes with `active`. i spent hours trying to fix it and what worked was adding a new, ***"empty"*** or ***"blank"*** function to the `touchmove` event immediately after removing the first function. super bizarre. id really love to understand why this is happening. is it happening for the same reason?! – oldboy Dec 30 '20 at 10:13
  • 1
    how is this not marked as an answer , this saved my day ! thanks so much . – سعيد Mar 28 '22 at 11:53
6

The trick is to hide element until touch move finishes, but not to remove it. Here is some example (enable Touch Mode in Chrome Dev Tools and select some device or use real device): https://jsfiddle.net/alexanderby/na3rumjg/

var marker = document.querySelector('circle');
var onStart = function(startEvt) {
  startEvt.preventDefault(); // Prevent scroll
  marker.style.visibility = 'hidden'; // Hide target element
  var rect = document.querySelector('rect');
  var initial = {
    x: +rect.getAttribute('x'),
    y: +rect.getAttribute('y')
  };
  var onMove = function(moveEvt) {
    rect.setAttribute('x', initial.x + moveEvt.touches[0].clientX - startEvt.touches[0].clientX);
    rect.setAttribute('y', initial.y + moveEvt.touches[0].clientY - startEvt.touches[0].clientY);
  };
  var onEnd = function(endEvt) {
    window.removeEventListener('touchmove', onMove);
    window.removeEventListener('touchend', onEnd);
    marker.removeEventListener('touchstart', onStart);
    marker.parentElement.removeChild(marker); // Remove target element
  };
  window.addEventListener('touchmove', onMove);
  window.addEventListener('touchend', onEnd);
};
marker.addEventListener('touchstart', onStart);
<svg>
  <circle r="20" cx="50" cy="20" cursor="move"/>
  <rect x="10" y="50" width="80" height="80" />
</svg>
Alexander Shutau
  • 2,660
  • 22
  • 32
  • i recently ran into -- what im assuming is -- a bug in my own script whereby i added a `touchmove` event with the `passive: false` option to `body` in order to `preventDefault` and then removed it. once i removed it, it prevented CSS classes with `active`. i spent hours trying to fix it and what worked was adding a new, ***"empty"*** or ***"blank"*** function to the `touchmove` event immediately after removing the first function. super bizarre. id really love to understand why this is happening – oldboy Dec 30 '20 at 10:13