javascriptclickevent-propagation

Add click event after mouse up


I have a draggable element. After dragging, I want to add the click-funtion again. The click-event should not fire when dragging, but I want to add it when dragging is over.

If I add my click-event again after dragging (line 46) it get's fired immediately

clickElemtent(document.getElementById("mydiv")); 

I don't understand the logic.. Thank you so much!

dragElement(document.getElementById("mydiv"));
clickElemtent(document.getElementById("mydiv"));

function clickElemtent(elmnt) {
    elmnt.onclick = function() {
        alert("click")
    }
}

function dragElement(elmnt) {
  var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
  if (document.getElementById(elmnt.id + "header")) {
    document.getElementById(elmnt.id + "header").onmousedown = dragMouseDown;
  } else {
    elmnt.onmousedown = dragMouseDown;
  }

  function dragMouseDown(e) {  
    e = e || window.event;
    e.preventDefault();
    pos3 = e.clientX;
    pos4 = e.clientY;
    document.onmouseup = closeDragElement;
    document.onmousemove = elementDrag;
  }

  function elementDrag(e) {
    elmnt.onclick = null;

    e = e || window.event;
    e.preventDefault();
    pos1 = pos3 - e.clientX;
    pos2 = pos4 - e.clientY;
    pos3 = e.clientX;
    pos4 = e.clientY;
    elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
    elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
  }

  function closeDragElement(e) {
    document.onmouseup = null;
    document.onmousemove = null;

    e.stopImmediatePropagation();
    e.stopPropagation();
        
    clickElemtent(document.getElementById("mydiv"));       ///// ?????
  }
}
#mydiv {
  position: absolute;
  z-index: 9;
  background-color: #f1f1f1;
  text-align: center;
  border: 1px solid #d3d3d3;
}

#mydivheader {
  padding: 10px;
  cursor: move;
  z-index: 10;
  background-color: #2196F3;
  color: #fff;
}
<div id="mydiv">
  <div id="mydivheader">click OR drag</div>
</div>


Solution

  • mouseup and click are different events (even though both are caused by the same interaction!), so stopping one doesn't affect the other.

    Event order

    (An explanation to Bujaq's answer:)

    An event's propagation path is determined before propagation. That means adding/removing click listeners in another click listener won't have any effect on the current event's path.

    However, different event types can affect each other, even those from the same event source, e.g. a mouse click. Events are run in a fixed order; for mouse events, mouseup runs before click.

    Taking the event loop into consideration: Calls to the event handlers are queued, so tasks added via setTimeout() will be run later. That is also the reason why 0 ms is enough, too.

    That means, adding the onclick like that will only have an effect after the pending events have been handled.

    Conditional call

    You want to only accept click events if no dragging occured. That means we need a way to tell if dragging occured.

    Dragging has always occured if a mousemove event happened after a mousedown event.

    Since a click event is fired even when dragging, we can call the "actual" listener only when no dragging occured:

    var clickHandler = function(e) {
      alert("click");
    };
    dragElement(document.getElementById("mydiv"), clickHandler);
    
    function dragElement(elmnt, clickCallback /*New*/) {
      var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
      var isDragged = false; // New
      
      if (document.getElementById(elmnt.id + "header")) {
        document.getElementById(elmnt.id + "header").onmousedown = dragMouseDown;
      } else {
        elmnt.onmousedown = dragMouseDown;
      }
      
      if (clickCallback) {
        elmnt.onclick = function(e) {
          // Only call if no dragging occured
          if (!isDragged) clickCallback(e);
        };
      }
    
      function dragMouseDown(e) {
        isDragged = false; // No dragging since now
        
        e = e || window.event;
        e.preventDefault();
        pos3 = e.clientX;
        pos4 = e.clientY;
        document.onmouseup = closeDragElement;
        document.onmousemove = elementDrag;
      }
    
      function elementDrag(e) {
        isDragged = true; // Dragging occured
    
        pos1 = pos3 - e.clientX;
        pos2 = pos4 - e.clientY;
        pos3 = e.clientX;
        pos4 = e.clientY;
        elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
        elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
      }
    
      function closeDragElement(e) {
        document.onmouseup = null;
        document.onmousemove = null;
      }
    }
    #mydiv {
      position: absolute;
      z-index: 9;
      background-color: #f1f1f1;
      text-align: center;
      border: 1px solid #d3d3d3;
    }
    
    #mydivheader {
      padding: 10px;
      cursor: move;
      z-index: 10;
      background-color: #2196F3;
      color: #fff;
    }
    <div id="mydiv">
      <div id="mydivheader">click OR drag</div>
    </div>

    Capture and stop

    Alternatively to conditionally calling the click listener, we can just stop the event from propagating.

    We need to stop the event before it reaches the expected listener. This can be done in two ways:

    Both ways require the use of addEventListener() to attach the listeners, which is the preferred way. The onevent properties and especially inline event attributes are generally discouraged.

    In the end we want to make use of the event flow. Example:

    const select = document.querySelector("select");
    const button = document.querySelector("button");
    
    // Notice order of attachment
    
    button.addEventListener("click", function stopEarly(evt) {
      if (select.value === "early") evt.stopImmediatePropagation();
    });
    
    button.addEventListener("click", () => console.log("Button reached!"));
    
    button.addEventListener("click", function stopInCapture(evt) {
      if (select.value === "capture") evt.stopPropagation();
    }, { capture: true });
    
    button.addEventListener("click", function stopEarly(evt) {
      if (select.value === "late") evt.stopImmediatePropagation();
    }, { capture: false /*default*/ });
    <select>
      <option value=none>No stopping</option>
      <option value=capture>Stop in capture</option>
      <option value=early>Stop early</option>
      <option value=late>Stop late</option>
    </select>
    <button>Log text</button>

    If we ensure that the propagation-stopping listener is called before the "intended" listener, we don't have to remove and reattach any listeners at all:

    var clickHandler = function(e) {
      alert("click");
    };
    dragElement(document.getElementById("mydiv"), clickHandler);
    
    function dragElement(elmnt, clickCallback /*New*/) {
      var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
      var isDragged = false; // New
      
      if (document.getElementById(elmnt.id + "header")) {
        document.getElementById(elmnt.id + "header").onmousedown = dragMouseDown;
      } else {
        elmnt.onmousedown = dragMouseDown;
      }
      
      if (clickCallback) {
        elmnt.addEventListener("click", function(e) {
          // Stop here if dragging occured
          if (isDragged) e.stopImmediatePropagation();
        }, { capture: true });
        elmnt.addEventListener("click", clickCallback);
      }
    
      function dragMouseDown(e) {
        isDragged = false; // No dragging since now
        
        e = e || window.event;
        e.preventDefault();
        pos3 = e.clientX;
        pos4 = e.clientY;
        document.onmouseup = closeDragElement;
        document.onmousemove = elementDrag;
      }
    
      function elementDrag(e) {
        isDragged = true; // Dragging occured
    
        pos1 = pos3 - e.clientX;
        pos2 = pos4 - e.clientY;
        pos3 = e.clientX;
        pos4 = e.clientY;
        elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
        elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
      }
    
      function closeDragElement(e) {
        document.onmouseup = null;
        document.onmousemove = null;
      }
    }
    #mydiv {
      position: absolute;
      z-index: 9;
      background-color: #f1f1f1;
      text-align: center;
      border: 1px solid #d3d3d3;
    }
    
    #mydivheader {
      padding: 10px;
      cursor: move;
      z-index: 10;
      background-color: #2196F3;
      color: #fff;
    }
    <div id="mydiv">
      <div id="mydivheader">click OR drag</div>
    </div>


    Apart from the three sections above, I want to demonstrate one more thing:

    Event delegation

    As seen in the event flow, an event goes through multiple phases:

    1. The capture phase.
    2. The target phase.
    3. The bubbling phase.

    Events propagate down to and up from their target. That means we can use a single listener on an ancestor for multiple targets. This is called event delegation.

    Assuming that only one element can be dragged at once, we can attach a single listener on an ancestor, where we'll stop propagation when applicable.

    Under these conditions, we can make elements automatically behave as wished simply by adding a class:

    const button = document.querySelector("button");
    // Add *click* listener to "click or drag" button
    button.addEventListener("click", () => {
      console.log("Initial button");
    });
    
    // Adding new draggable buttons is simple:
    const button2 = document.createElement("button");
    button2.classList.add("draggable"); // For dragging functionality
    button2.classList.add("blue-button"); // For styling
    button2.addEventListener("click", () => console.log("Button 2!")); // The *click* listener
    button2.textContent = "Click or Drag 2";
    
    document.body.append(button2);
    
    (function pageWideDraggable() {
      let target = null;
      let isDragged = false;
      document.addEventListener("mousedown", evt => {
        // Get draggable target and reset "dragging memory"
        target = event.target.closest(".draggable");
        isDragged = false;
      });
      document.addEventListener("mousemove", evt => {
        // If draggable target exists, remember that dragging occured and update target's position
        if (!target) return;
    
        isDragged = true;
        target.style.top = `${target.offsetTop + evt.movementY}px`;
        target.style.left = `${target.offsetLeft + evt.movementX}px`;
      });
      document.addEventListener("click", evt => {
        // Capture click and stop event when dragging occured
        if (isDragged) evt.stopPropagation();
        target = null;
      }, { capture: true });
    })();
    .draggable {
      position: absolute;
      padding: 0;
      cursor: pointer;
      user-select: none;
    }
    .blue-button {
      padding: 10px;
      display: inline-block;
      color: white;
      background-color: #2196F3;
    }
    <button class="draggable blue-button">
      click OR drag
    </button>

    Sidenote: Notice how the deeper elements don't need to be aware of the capturing. This allows for easy component-based development.