6

I'm trying create draggable elements that can also be resized by dragging bottom right corner with the help of resize: both CSS rule. So far I'm able drag it around screen, but I can't figure out how to detect if cursor at "resize" state, because attempting resize the element moves it instead.

Currently as a work around I'm simply checking if cursor within 20px from bottom-right corner, but that's not very accurate (and probably browser/os dependent), in some places it refuses resize and move the element at all.

Any suggestions?

!function(){
"use strict";
let x, y, drag;
document.addEventListener("mousedown", function(e) {
  if (e.target.parentNode.lastChild !== e.target && e.target.parentNode.classList.contains("main")) {
    //bring element to the front and dispatch mousedown event again otherwise resize doesn't work
    e.target.parentNode.appendChild(e.target);
    return e.target.dispatchEvent(new MouseEvent(e.type, e));
  }

  if (!e.target.classList.contains("draggable"))
    return;

  /* if cursor within 20px from bottom right corner don't move */
  const r = e.target.getBoundingClientRect();
  if (r.right < e.x + 20 && r.bottom < e.y + 20)
    return;

  drag = e.target;
  x = e.x - drag.offsetLeft;
  y = e.y - drag.offsetTop;
  document.body.classList.add("drag");
  drag.classList.add("drag");
});

document.addEventListener("mouseup", function(e) {
  document.body.classList.remove("drag");
  drag = drag && drag.classList.remove("drag");
});

document.addEventListener("mousemove", function(e) {
  if (!drag || e.x - drag.offsetLeft == x || e.y - drag.offsetTop == y)
    return;

  drag.style.left = (e.x - x) + "px";
  drag.style.top = (e.y - y) + "px";
});

/*init*/
for (let i = 0, c, d = document.getElementsByClassName("main")[0].children; i < d.length; i++) {
  c = (0x1000000 + Math.random() * 0xffffff).toString(16).substr(1, 6);
  d[i].style.backgroundColor = '#' + c;
  d[i].classList.toggle("dark", ((parseInt(c.substr(0, 2), 16) * 299) + (parseInt(c.substr(2, 2), 16) * 587) + (parseInt(c.substr(4, 2), 16) * 114)) / 1000 < 128);
  d[i].style.left = document.documentElement.scrollWidth / 8 + Math.random() * (document.documentElement.scrollWidth / 1.33 - d[i].offsetWidth) + "px";
  d[i].style.top = document.documentElement.scrollHeight / 8 + Math.random() * (document.documentElement.scrollHeight / 1.33 - d[i].offsetHeight) + "px";
}
}()
div.main>div {
  width: 5em;
  height: 5em;
  border: 1px solid black;
  position: absolute;
  resize: both;
  overflow: hidden;
  mix-blend-mode: hard-light;
  display: flex;
  border-radius: 0.3em;
}

div.main>div:hover {
  box-shadow: 0 0 5px black;
}

div.main>div:last-child {
  box-shadow: 0 0 10px black;
}

div.draggable {
  cursor: grab;
}

div.main>div:not(.draggable):before {
  content: "can't move me";
}

div.main>div.draggable:before {
  content: "move me";
}

div.main>div:before {
  color: black;
  text-shadow: 0 0 1em black;
  margin: auto;
  text-align: center;
}

div.main>div.dark:before {
  color: white;
  text-shadow: 0 0 1em white;
}

body.drag {
  user-select: none;
}

body.drag div.draggable {
  cursor: grabbing;
}
<div class="main">
  <div></div>
  <div class="draggable"></div>
  <div class="draggable"></div>
  <div class="draggable"></div>
</div>
vanowm
  • 9,466
  • 2
  • 21
  • 37
  • 1
    I don't know for sure, but I recall researching this myself about a year ago and concluding that, unfortunately, the best solution is what you already have, checking if the cursor is in a small box in the bottom-right of the element. – Quelklef May 10 '21 at 03:36
  • [related](https://stackoverflow.com/questions/49136251/detect-click-on-resizable-elements-resize-handle) – Quelklef May 10 '21 at 03:39
  • The body of your question and its title ask for different things... What do you want exactly? To find if the cursor is above the "resize zone", or detect if the element **has been** resized? – Kaiido May 10 '21 at 07:15
  • @Kaiido I want be able use resize feature together with move. Updated title – vanowm May 10 '21 at 10:33

1 Answers1

3

You could wrap the mouse-target (.draggable) inside a resizeable container element.
This way, the UI for the CSS-resize will get hit before the draggable element and you can handle only the dragging nicely:

!function(){
"use strict";
let x, y, drag;
document.addEventListener("mousedown", function(e) {
  if (e.target.parentNode.lastChild !== e.target && e.target.parentNode.classList.contains("main")) {
    //bring element to the front and dispatch mousedown event again otherwise resize doesn't work
    e.target.parentNode.appendChild(e.target);
    return e.target.dispatchEvent(new MouseEvent(e.type, e));
  }

  if (!e.target.classList.contains("draggable"))
    return;

  e.preventDefault();
  drag = e.target.parentNode;
  x = e.x - drag.offsetLeft;
  y = e.y - drag.offsetTop;
  document.body.classList.add("drag");
  drag.classList.add("drag");
});

document.addEventListener("mouseup", function(e) {
  document.body.classList.remove("drag");
  drag = drag && drag.classList.remove("drag");
});

document.addEventListener("mousemove", function(e) {
  if (!drag || e.x - drag.offsetLeft == x || e.y - drag.offsetTop == y)
    return;

  drag.style.left = (e.x - x) + "px";
  drag.style.top = (e.y - y) + "px";
});

/*init*/
for (let i = 0, c, d = document.getElementsByClassName("main")[0].children; i < d.length; i++) {
  c = (0x1000000 + Math.random() * 0xffffff).toString(16).substr(1, 6);
  d[i].style.backgroundColor = '#' + c;
  d[i].classList.toggle("dark", ((parseInt(c.substr(0, 2), 16) * 299) + (parseInt(c.substr(2, 2), 16) * 587) + (parseInt(c.substr(4, 2), 16) * 114)) / 1000 < 128);
  d[i].style.left = document.documentElement.scrollWidth / 8 + Math.random() * (document.documentElement.scrollWidth / 1.33 - d[i].offsetWidth) + "px";
  d[i].style.top = document.documentElement.scrollHeight / 8 + Math.random() * (document.documentElement.scrollHeight / 1.33 - d[i].offsetHeight) + "px";
}
}()
.resizeable {
  width: 5em;
  height: 5em;
  border: 1px solid black;
  position: absolute;
  resize: both;
  overflow: hidden;
  mix-blend-mode: hard-light;
  border-radius: 0.3em;
}

div.main>div:hover {
  box-shadow: 0 0 5px black;
}

div.main>div:last-child {
  box-shadow: 0 0 10px black;
}

div.draggable {
  cursor: grab;
  width: 100%;
  height: 100%;
  display: flex;
}
div.no-drag {
  display: flex;
}
div.main div.no-drag:before {
  content: "can't move me";
}
div.draggable:before {
  content: "move me";
}
div.main div:before {
  color: black;
  text-shadow: 0 0 1em black;
  margin: auto;
  text-align: center;
}

div.main>div.dark:before {
  color: white;
  text-shadow: 0 0 1em white;
}

body.drag {
  user-select: none;
}

body.drag div.draggable {
  cursor: grabbing;
}
<div class="main">
  <div class="resizeable no-drag"></div>
  <div class="resizeable"><div class="draggable"></div></div>
  <div class="resizeable"><div class="draggable"></div></div>
  <div class="resizeable"><div class="draggable"></div></div>
</div>
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • 2
    That is brilliant! Thank you. I have to adjust the part that brings the element forward (unfortunately I still have to dispatch the mouse event within mousedown event), but it works flawlessly otherwise. – vanowm May 11 '21 at 00:15