<body>
  <div id="app">
    <div class="row">
      <div class="panel">
        <h3 class="title">
          panel1
        </h3>
        <div class="body" draggable="false">
          各 panel をドラッグ操作で動かすことができるサンプルです。<br>
          任意の panel をドラッグ、要素の境界にドロップすると、その位置に panel を移動できます。
        </div>
      </div>
      <div class="panel">
        <h3 class="title">
          panel2
        </h3>
        <div class="body">
          This is a sample what the panels can be moved by dragging.
        </div>
      </div>
    </div>
    <div class="row">
      <div class="panel">
        <h3 class="title">
          panel3
        </h3>
        <div class="body">
          <span style="font-size:xx-large">🍣</span>
        </div>
      </div>
    </div>
    <div class="row">
      <div class="panel">
        <h3 class="title">
          panel4
        </h3>
        <div class="body">
          The implementation is not efficient. Paddings are deleted and re-created each time the panel is moved.
        </div>
      </div>
    </div>
  </div>
</body>
body {
  background-color: #111;
  font-family: sans-serif;
}

#app {
  border: 2px solid #aaa;
  background-color: #333;
}

.panel {
  background-color: #38c;
  flex: 1;
  display: flex;
  flex-direction: column;
  border-radius: 6px;
}

.panel .title {
  background-color: #ddd;
  padding: 15px;
  margin: 0px 0px 3px;
  flex: 0;
}

.panel .body {
  background-color: #8cc;
  padding: 15px;
  flex: 1;
}

.row {
  display: flex;
  flex-direction: row;
  gap: 0;
}

.padding {
  padding: 5px;
  flex: 0;
}
.padding.active {
  background-color: #f33;
}
let g_dragging_element = null;

function createRow() {
  const elem = document.createElement("div");
  elem.className = "row";
  return elem;
}

function createPadding() {
  const elem = document.createElement("div");
  elem.className = "padding";
  elem.addEventListener("dragenter", (e) => {
    e.target.classList.add("active");
  });
  elem.addEventListener("dragleave", (e) => {
    e.target.classList.remove("active");
  });
  elem.addEventListener("dragover", (e) => {
    e.dataTransfer.dropEffect = "move";
    e.preventDefault();
  });
  elem.addEventListener("drop", (e) => {
    e.preventDefault();
    if (g_dragging_element === null) return;
    const panel = g_dragging_element;
    g_dragging_element = null;
    movePanel(panel, event.target);
  });
  return elem;
}

function movePanel(panel, destination) {
  const destParent = destination.parentElement;
  if (destParent.classList.contains("row")) {
    destParent.insertBefore(panel, destination);
  } else {
    const newRow = createRow();
    destParent.insertBefore(newRow, destination);
    newRow.appendChild(panel);
  }

  resetAll();
  setupAll();
}

function resetAll() {
  document.querySelectorAll(".padding").forEach((e) => e.remove());
  document
    .querySelectorAll(".row")
    .forEach((e) => e.children.length === 0 && e.remove());
}

function setupPanel(elem) {
  if (elem.draggable === true || elem.draggable === "true") {
    return;
  }
  elem.addEventListener("drag", undefined);
  elem.addEventListener("dragstart", (e) => {
    g_dragging_element = e.target;
  });
  elem.addEventListener("dragend", () => {
    g_dragging_element = null;
  });
  elem.draggable = "true";
}

function setupRow(elem) {
  const panels = elem.querySelectorAll(":scope > .panel");
  for (const panel of panels) {
    elem.insertBefore(createPadding(), panel);
    setupPanel(panel);
  }
  elem.appendChild(createPadding());
}

function setupRoot(elem) {
  const rows = elem.querySelectorAll(":scope > .row");
  for (const row of rows) {
    elem.insertBefore(createPadding(), row);
    setupRow(row);
  }
  elem.appendChild(createPadding());
}

function setupAll() {
  const root = document.querySelector("#app");
  setupRoot(root);
}

window.addEventListener("DOMContentLoaded", () => {
  setupAll();
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.