aepheus
aepheus

Reputation: 8197

How do I prevent drag on a child, but allow drag on the parent?

I have a div which the user can drag, inside that div is a span with some text which I want to allow the user to select (thus they cannot drag it). How do I allow the div to drag, but not the span?

The dragstart event is on the div.

I'm probably overlooking something simple. I tried draggable=true on the div, and draggable=false on the span. That didn't work. Tried returning false on dragstart, that didn't work either.

dragstart (roughly):

var jTarget = $(e.target);
if ((jTarget.is('div.header') || (jTarget.parents('div.header')) 
       && !jTarget.is('a, input, span'))) 
{
   e.originalEvent.dataTransfer.setData("Text", "test");
}
else
{
   if(e.preventBubble)
      e.preventBubble();
   if(e.stopPropagation)
      e.stopPropagation();
   return false;//???
}

The if else portion works as I expect, but I cannot get anything to stop the drag and allow the select.

Upvotes: 43

Views: 34030

Answers (9)

Roko C. Buljan
Roko C. Buljan

Reputation: 206565

To start with, you should avoid using Event.stopPropagation() unless for debugging - or if you really, really know what you're doing. The application or third party components should be always, at all times, notified about events happening in the document during their lifecycle.

Solution

Since dragstart's Event.target will (unusually) always be the actual Element that has such draggable="true" attribute (and not a deeper child like i.e. <i>), you need to add a draggable="true" to the child as well, that way you'll be able to differentiate if a child was the one starting the event by using event.target.closest(`[draggable="true"] [draggable="true"]`)

Example

addEventListener("dragstart", (ev) => {
  if (ev.target.closest(`[draggable="true"] [draggable="true"]`)) {
    // Child is being dragged!
    ev.preventDefault(); // Prevent child dragging
    return; // Exit function
  };

  // Parent drag logic goes here
});
.parent {background: #ddd;padding: 0.5em;margin: 0.5em; & > span {background: #f00;color: #fff;}}
<div class="parent" draggable="true">
  <b>Drag works</b> on parent
  <span draggable="true">not on <b>this child</b></span>
  <b>Drag works</b> on parent
</div>

Tip:
If you want to replicate the above by only using discouraged inline JavaScript on* handlers you could do that as well:

.parent {background: #ddd;padding: 0.5em;margin: 0.5em; & > span {background: #f00;color: #fff;}}
<div class="parent" draggable="true">
  <b>Drag works</b> on parent
  <span ondragstart="event.preventDefault();" draggable="true">not on <b>this child</b></span>
  <b>Drag works</b> on parent
</div>

Upvotes: 0

Moritz
Moritz

Reputation: 1638

I found that storing a flag as data attribute is a reasonably acceptable workaround:

export function Draggable(props: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      {...props}
      draggable={true}
      onMouseDown={(event) => {
        props.onMouseDown?.(event);
        const handles = event.currentTarget.querySelectorAll('.drag-handle');
        const target = event.target as HTMLElement;
        const allow = [...handles].map(handle => handle.contains(target)).some(i => i);
        if (allow) {
          event.currentTarget.setAttribute('data-draggable', 'yes');
        } else {
          event.currentTarget.removeAttribute('data-draggable');
        }
      }}
      onDragStart={(event) => {
        props.onDragStart?.(event);
        const allow = event.currentTarget.hasAttribute('data-draggable');
        if (!allow) {
          event.preventDefault();
          event.stopPropagation();
          return;
        }
      }}
    >
      {props.children}
    </div>
  );
}

Upvotes: -1

mohammad feiz
mohammad feiz

Reputation: 315

It looks like you need to set a flag that indicates whether the mouse is on the child or not. this works for me very well. if you set e.preventDefault() without condition,You will lose draggable parents.

html:

<div draggable="true" ondragstart="dragStart()">
    <span onmouseenter="childMouseEnter()" onmouseleave="childMouseLeave()">Dont 
        Drag
    </span>        
</div>

javascript:

let mouseOnChild = false
function childMouseEnter(){
    mouseOnChild = true;
}
function childMouseLeave(){
    mouseOnChild = false;
}
function dragStart(e){
    if(!mouseOnChild){
        e.preventDefault();
        e.stopPropagation();
    }
    //other operations....
}

Upvotes: -1

CyberNerd
CyberNerd

Reputation: 1

I was inspired by a few of the answers here and I think I came up with the cleanest and most reusable solution. I use a mouse down event to check if the element should be draggable or not. I do this by verifying if it has a specific class name that I only assign to draggable elements. On the mouse up event, I go through all of the elements with that class tag and set their draggable values back to true.

This can break if the input node is a deep child of the draggable div, but then you can use the same algorithm as in the mouse up event and set all of the values to false.

document.addEventListener('mousedown', function (event) {
	var clickedElem = event.target;
    if(!clickedElem.classList.contains("drop_content")){
        clickedElem.parentElement.setAttribute("draggable",false);
    }
}, false);

document.addEventListener('mouseup', function () {
    var boxes = document.querySelectorAll(".drop_content");
    for(var i = 0; i < boxes.length; i++){
        boxes[i].setAttribute("draggable",true);
    }
}, false);
div{
  display:block;
  background: black;
  padding: 5px;
  margin: 2px;
}
<html>
  <body>
    <div class="drop_content" draggable="true">
      <input value="Can't drag me">
    </div>
    <div class="drop_content" draggable="true">
      <input value="Can't drag me either">
    </div>
  </body>
</html>

Upvotes: -1

Blixt
Blixt

Reputation: 50187

If you want to truly cancel out the drag and make it unnoticeable to parent drag handlers, you need to both preventDefault and stopPropagation:

<div draggable="true" ondragstart="console.log('dragging')">
    <span>Drag me! :)</span>
    <input draggable="true"
           ondragstart="event.preventDefault();
                        event.stopPropagation();"
           value="Don't drag me :(">
</div>

Without the stopPropagation, the parent ondragstart will still be called even if the default behavior will be prevented (event.defaultPrevented == true). If you have code in that handler that doesn't handle this case, you may see subtle bugs.

You can of course put the JavaScript above inside element.addEventListener('dragstart', ...) too.

Upvotes: 45

Todd Sjolander
Todd Sjolander

Reputation: 1567

In my application, I was unable to get any of the other solutions listed here to work. This is what worked for me:

ondragstart="event.preventDefault();" draggable="false"

In my specific case, it was a text input inside a div.

Upvotes: 0

shuji
shuji

Reputation: 7545

You can change the child element to draggable and prevent default when ondragstart, this will leave the regular behavior on the child element and the parent will still be draggable

function drag(e){ if(e.target.type=="range")e.preventDefault();/*rest of your code*/  }

html:

<input type="range" draggable="true" ondragstart="drag(event)">

Upvotes: 6

mech
mech

Reputation: 685

just posting in case someone searches for it with a similar Problem

element.addEventListener('mousedown', function() { this.parentNode.setAttribute("draggable", false); });
element.addEventListener('mouseup', function() { this.parentNode.setAttribute("draggable", true); });

worked for me

Upvotes: 18

Rob Middleton
Rob Middleton

Reputation: 109

Put a mousedown handler on the span and stop the event propagation there.

Upvotes: 2

Related Questions