Re-firing pointer events onto lower layer (for irregular-shaped dragging with interact.js)
Asked Answered
C

2

26

I need to manually construct/fire a mousedown event in a way that can be handled by any relevant event handlers (either in straight JS, jQuery, or interact.js), just as a natural mousedown event would. However, the event does not seem to trigger anything the way I expect it to.

I am trying to make some irregularly shaped images draggable using the interact.js library. The images are simply rectangular img elements with transparent portions. On each element, I have defined 2 interact.js event listeners:

  1. Checking if the click was inside the image area, disabling drag if not (fires on "down" event)
  2. Handling the drag (fires on "drag" event)

However, if the img elements are overlapping, and the user clicks in the transparent area of the top element but on the filled area of the lower element, the lower element should be the target of the drag. After trying several things (see below), I have settled on the solution of: re-ordering the z-indexes of the elements at the same time that I disable drag in step 1, then re-firing the mousedown event on all lower elements. I'm using the "native" event type (not jQuery or interact.js) in hopes that it will literally just replicate the original mousedown event.

// code to re-assign "zIndex"s
function demote(element, interactible, event){

    // block dragging on element
    interactible.draggable(false);

    // get all images lower than the target 
    var z = element.css("zIndex");
    var images = $("img").filter(function() {
    return Number($(this).css("zIndex")) < z;
    });

    // push the target to the back
    element.css("zIndex",1);

    // re-process all lower events
    $(images).each( function () {
          // move element up
          $(this).css("zIndex",Number($(this).css("zIndex"))+1);

          // re-fire event if element began below current target
          elem = document.getElementById($(this).attr('id'));

          // Create the event.
          e = new MouseEvent("mousedown", {clientX: event.pageX, clientY: event.pageY});
          var cancelled = !elem.dispatchEvent(e);
    });
}

Unfortunately, this does not work, as the mousedown event does not register with any of the handlers. Why?

I have all the (relevant) code at this JSFiddle: https://jsfiddle.net/tfollo/xr27938a/10/ Note that this JSFiddle does not seem to work as smoothly as it does in a normal browser window, but I think it demonstrates the intended functionality.

Other things I have tried:

Lots of people have proposed different schemes to handle similar problems (i.e. forwarding events to lower layers, using pointer-events: none, etc), but none seem to work to trigger the interact.js handler and start a drag interaction on the right element. I also tried using interaction.start (provided by interact.js) but it seems buggy--there is at least one open issue on the topic and when I tried to start a new drag interaction on a target of my choice I got lots of errors from within the library's code.

I'm not against revisiting any of these solutions per se, but I would also really like to know why manually firing a mousedown event won't work.

Cerellia answered 15/3, 2016 at 23:13 Comment(5)
Which browser are you using? IE doesn't support constructing Event objects - you have to use static methods such as document.createEvent() or something.Kuo
I was working in Chrome. I'd like to maximize compatibility though, so I'll try to refactor to use createEvent().Cerellia
MDN says that createEvent() is deprecated and that event constructors (which you are using) are the preferred method for creating event objects.Kuo
It would be helpful for me to know whether performance is a critical aspect. I think in order to do this task you need to know if a given pixel at a given coordinate is visible or not. This operation however might be a bit expensive.Ladonnalady
Performance is not too critical. I'm already reordering the z-index of every img element on every click and checking the pixel offset of every click, so I think solving this problem in a clean way would help reduce the waste that is happening currently.Cerellia
G
1

The idea is to listen down event on parent element and to choose manually drag target. Also I didn't use z-index for choosing which image to drag, because z-index doesn't work with position:static. Instead of that I just gave priorities to images, it's all up to you. https://jsfiddle.net/yevt/6wb5oxx3/3/

var dragTarget;

function dragMoveListener (event) {
  var target = event.target,
      // keep the dragged position in the data-x/data-y attributes
      x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx,
      y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy;

  // translate the element
  target.style.webkitTransform =
    target.style.transform =
    'translate(' + x + 'px, ' + y + 'px)';

  // update the posiion attributes
  target.setAttribute('data-x', x);
  target.setAttribute('data-y', y);
}

function setDragTarget(event) {
  //choose element to drag
  dragTarget = $('#parent').find('img')
    .filter(function(i, el) {
      var clickCandicateRect = el.getBoundingClientRect();
      return insideBoundingRect(event.x, event.y, clickCandicateRect);
    }).sort(byPriority).get(0);
}

function insideBoundingRect(x, y, rect) {
  return (rect.left <= x) && (rect.top <= y) && (rect.right >= x) && (rect.bottom >= y);
}

function byPriority (a, b) {
  return $(b).data('priority') - $(a).data('priority');
}

function startDrag(event) {
  var interaction = event.interaction;
  var target = dragTarget;

  if (!interaction.interacting()) {
    interaction.start({ name: 'drag' }, event.interactable, target);
  }
}

//Give priority as you wish
$('#face1').data('priority', 2);
$('#face2').data('priority', 1);

interact('#parent').draggable({
  //use manualStart to determine which element to drag
  manualStart: true,
  onmove: dragMoveListener,
  restrict: {
    restriction: "parent",
    endOnly: true,
    elementRect: { top: 0, left: 0, bottom: 1, right: 1 }
  },
})
.on('down', setDragTarget)
.on('move', startDrag);
Germanium answered 12/5, 2016 at 19:18 Comment(1)
+1 for using priorities. +2 for defining new events/interactions. Both new to me. I was relying pretty heavily on the library (since I didn't know how to do that stuff on my own), but I think the functionality I am looking for is a bit too custom for that, so I think you have the general idea right.Cerellia
Y
1

Have you tried to set the css property pointer-events: none on the higher levels (it could also be set via javascript)?

Yarrow answered 15/4, 2016 at 18:42 Comment(1)
Yep, one of the problems is that I want all the draggable elements to be accessible initially. I check to make sure the click happened within a valid (non-transparent) part of the element area, and if it is, fine. If not, only then do I want the interaction to pass through to a lower element if it exists (and then be checked against that element's bounds, etc). Changing the z-index and adding "pointer-events: none" both fail because after I check the bounds against the first element I can't restart the event for the second.Cerellia
G
1

The idea is to listen down event on parent element and to choose manually drag target. Also I didn't use z-index for choosing which image to drag, because z-index doesn't work with position:static. Instead of that I just gave priorities to images, it's all up to you. https://jsfiddle.net/yevt/6wb5oxx3/3/

var dragTarget;

function dragMoveListener (event) {
  var target = event.target,
      // keep the dragged position in the data-x/data-y attributes
      x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx,
      y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy;

  // translate the element
  target.style.webkitTransform =
    target.style.transform =
    'translate(' + x + 'px, ' + y + 'px)';

  // update the posiion attributes
  target.setAttribute('data-x', x);
  target.setAttribute('data-y', y);
}

function setDragTarget(event) {
  //choose element to drag
  dragTarget = $('#parent').find('img')
    .filter(function(i, el) {
      var clickCandicateRect = el.getBoundingClientRect();
      return insideBoundingRect(event.x, event.y, clickCandicateRect);
    }).sort(byPriority).get(0);
}

function insideBoundingRect(x, y, rect) {
  return (rect.left <= x) && (rect.top <= y) && (rect.right >= x) && (rect.bottom >= y);
}

function byPriority (a, b) {
  return $(b).data('priority') - $(a).data('priority');
}

function startDrag(event) {
  var interaction = event.interaction;
  var target = dragTarget;

  if (!interaction.interacting()) {
    interaction.start({ name: 'drag' }, event.interactable, target);
  }
}

//Give priority as you wish
$('#face1').data('priority', 2);
$('#face2').data('priority', 1);

interact('#parent').draggable({
  //use manualStart to determine which element to drag
  manualStart: true,
  onmove: dragMoveListener,
  restrict: {
    restriction: "parent",
    endOnly: true,
    elementRect: { top: 0, left: 0, bottom: 1, right: 1 }
  },
})
.on('down', setDragTarget)
.on('move', startDrag);
Germanium answered 12/5, 2016 at 19:18 Comment(1)
+1 for using priorities. +2 for defining new events/interactions. Both new to me. I was relying pretty heavily on the library (since I didn't know how to do that stuff on my own), but I think the functionality I am looking for is a bit too custom for that, so I think you have the general idea right.Cerellia

© 2022 - 2024 — McMap. All rights reserved.