Custom Draggable Element With Drag Handles

by Richard Hunter

HTML

<div id="container">
  <my-draggable id="alpha">
    <div id="foo" class="foo">
      <span class="drag-handle">
        <my-draggable id="alpha-drag-handle">
          <div>drag</div>
        </my-draggable>
      </span>
    </div>
  </my-draggable>

  <my-draggable id="beta">
    <div class="bar"></div>
  </my-draggable>
</div>

CSS

.foo {
  background: pink;
  padding: 10px;
  border: solid 3px black;
  position: relative;
}
.bar {
  background: antiquewhite;
  padding: 10px;
  width: 40px;
  height: 30px;
  border: dashed 3px dodgerblue;
}
.drag-handle {
  background: red;
  display: inline-block;
  width: 30px;
  height: 20px;
  position: absolute;
  right: 0;
  bottom: 0;
}
#container {
  position: relative;
  background: green;
  height: 100vh;
  width: 100vw;
}
body {
  margin: 0;
}

JavaScript

let alphaPosition = {
  left: 20,
  top: 20,
  width: 100,
  height: 100,
};

let betaPosition = {
  left: 50,
  top: 50,
};

class Draggable extends HTMLElement {
  constructor() {
    super();

    this.pointerDown = false;

    this.style.position = 'absolute';
    this.style.left = 0;
    this.style.top = 0;

    this.addEventListener('pointerdown', (event) => {
      event.stopPropagation();
      event.target.setPointerCapture(event.pointerId);
      this.pointerDown = true;
      this.startX = event.clientX;
      this.startY = event.clientY;
    });

    this.addEventListener('pointermove', (event) => {
      event.stopPropagation();
      if (this.pointerDown) {
        var dragEvent = new CustomEvent("drag", {
          bubbles: true,
          detail: {
            id: this.id,
            xdiff: event.clientX - this.startX,
            ydiff: event.clientY - this.startY,
          }
        });

        this.startX = event.clientX;
        this.startY = event.clientY;

        event.target.dispatchEvent(dragEvent);
      }
    });

    this.addEventListener('pointerup', (event) => {
      event.stopPropagation();
      event.target.releasePointerCapture(event.pointerId);
      this.pointerDown = false;
    });
  }
}

customElements.define('my-draggable', Draggable);

function render() {
	foo.style.width = alphaPosition.width + 'px';
  foo.style.height = alphaPosition.height + 'px';
  alpha.style.transform = `translate(${alphaPosition.left}px, ${alphaPosition.top}px)`;
  beta.style.transform = `translate(${betaPosition.left}px, ${betaPosition.top}px)`;
}

const leftLimit = 200;
const topLimit = 200;

container.addEventListener('drag', (event) => {
  if (event.detail.id === 'alpha') {
    alphaPosition = {
      ...alphaPosition,
      left: Math.min(leftLimit, Math.max(20, alphaPosition.left + event.detail.xdiff)),
      top: Math.min(topLimit, Math.max(20, alphaPosition.top + event.detail.ydiff)),
    };

    render();
  }

  if (event.detail.id ===...