Overview

This tutorial shows how to customize the JavaScript Scheduler drag and drop behavior to swap events:

  • If an event is dropped on an existing event the events will be swapped

  • A hint is displayed to the user during dragging

  • If there are multiple events at the target location, the drop will be forbidden

  • Includes a trial version of DayPilot Pro for JavaScript (see also License below)

License

Licensed for testing and evaluation purposes. Please see the license agreement included in the sample project. You can use the source code of the tutorial if you are a licensed user of DayPilot Pro for JavaScript.

Swapping Events on Drop

Moving a Scheduler event:

javascript scheduler swap before

After drop:

javascript scheduler swap after

The JavaScript Scheduler component allows customization of the drop action using the onEventMove event handler. We will use it to implement event swapping.

First, we detect events that overlap the proposed destination on the target row. The range ends are exclusive: events that only touch at an edge do not overlap. We filter out the dragged event itself so that moving within its original row does not count it as a swap target. If no other event overlaps, the Scheduler performs a normal move:

onEventMove: (args) => {
  const targetRow = dp.rows.find(args.newResource);
  const existing = targetRow.events.forRange(args.newStart, args.newEnd)
    .filter(e => e.id() !== args.e.id());
  if (existing.length === 0) {
    return;
  }

  // ...
}

If there is more than one other event at the target position, the operation will not be allowed. Calling args.preventDefault() cancels the drop:

onEventMove: (args) => {
  // ...
  if (existing.length > 1) {
    DayPilot.Modal.alert("More than one event at the target position");
    args.preventDefault();
    return;
  }
  // ...
}

If there is exactly one other event, move it to the dragged event’s original start time and resource. Save the target event’s duration before changing its start, then calculate its new end so its duration stays the same:

onEventMove: (args) => {
  // ...
  const sourceEvent = args.e;
  const targetEvent = existing[0];

  const targetDuration = targetEvent.duration();

  targetEvent.data.start = sourceEvent.data.start;
  targetEvent.data.end = targetEvent.start().addTime(targetDuration);
  targetEvent.data.resource = sourceEvent.data.resource;
  // ...
}

The full onEventMove handler below updates the target event and lets the Scheduler complete the dragged event’s normal move to the drop position. The dragged event keeps its own duration too; it is not forced to match the target’s interval. The optional Delayed effect checkbox removes the target briefly and adds it back after 200 milliseconds, making its relocation easier to see:

onEventMove: (args) => {
  const targetRow = dp.rows.find(args.newResource);
  const existing = targetRow.events.forRange(args.newStart, args.newEnd)
    .filter(e => e.id() !== args.e.id());
  if (existing.length === 0) {
    return;
  }

  if (existing.length > 1) {
    DayPilot.Modal.alert("More than one event at the target position");
    args.preventDefault();
    return;
  }

  const sourceEvent = args.e;
  const targetEvent = existing[0];

  const targetDuration = targetEvent.duration();

  targetEvent.data.start = sourceEvent.data.start;
  targetEvent.data.end = targetEvent.start().addTime(targetDuration);
  targetEvent.data.resource = sourceEvent.data.resource;

  const delayed = app.elements.delayed.checked;

  if (delayed) {
    dp.events.remove(targetEvent);
    setTimeout(() => {
      dp.events.add(targetEvent);
    }, 200);
  }
  else {
    dp.events.update(targetEvent);
  }

},

Drag Feedback

javascript scheduler swap feedback

To show what will happen on drop, we add an onEventMoving handler. It uses the same overlap search as the drop handler. One other event shows “Events will be swapped”; multiple targets show a warning and set args.allowed = false, displaying a forbidden drag shadow:

onEventMoving: (args) => {
  const targetRow = dp.rows.find(args.resource);
  const existing = targetRow.events.forRange(args.start, args.end)
    .filter(e => e.id() !== args.e.id());
  if (existing.length === 0) {
    return;
  }

  if (existing.length > 1) {
    args.right.enabled = true;
    args.right.html = "More than one event at the target position, unable to swap";
    args.allowed = false;
    return;
  }

  args.right.enabled = true;
  args.right.html = "Events will be swapped";
},

This example checks the proposed destination only. It does not enforce a global no-overlap rule at the original position, and it does not save changes to a server. Moves, resizing, and newly created events stay in memory until the page is reloaded.

Full Source Code

The sample displays the current month. Select an empty time range to create an event, or drag an event edge to resize it. You can create two overlapping events on one row to try the forbidden-drop case. Start from index.html, or use the DayPilot UI Builder to configure your own Scheduler.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>JavaScript Scheduler: Swapping Events (Drag and Drop)</title>

  <style type="text/css">
    p, body, td, input, select, button { font-family: -apple-system,system-ui,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif; font-size: 14px; }
    body { padding: 0px; margin: 0px; background-color: #ffffff; }
    a { color: #1155a3; }
    .space { margin: 10px 0px 10px 0px; }
    .header { background: #003267; background: linear-gradient(to right, #011329 0%,#00639e 44%,#011329 100%); padding:20px 10px; color: white; box-shadow: 0px 0px 10px 5px rgba(0,0,0,0.75); }
    .header a { color: white; }
    .header h1 a { text-decoration: none; }
    .header h1 { padding: 0px; margin: 0px; }
    .main { padding: 10px; margin-top: 10px; }
    .generated { color: #999; }
    .generated a { color: #999; }
  </style>

  <!-- DayPilot library -->
  <script src="js/daypilot/daypilot-all.min.js"></script>
</head>
<body>
<div class="header">
  <h1><a href='https://code.daypilot.org/42265/javascript-scheduler-swapping-events-drag-and-drop'>JavaScript Scheduler: Swapping Events (Drag and Drop)</a></h1>
  <div><a href="https://javascript.daypilot.org/">DayPilot for JavaScript</a> - HTML5 Calendar/Scheduling Components for JavaScript/Angular/React/Vue</div>
</div>

<div class="main">
  <div class="space"><label><input type="checkbox" id="delayed"> Delayed effect</label></div>
  <div id="dp"></div>
  <div class="generated">Generated using <a href="https://builder.daypilot.org/">DayPilot UI Builder</a>.</div>
</div>

<script>
  const dp = new DayPilot.Scheduler("dp", {
    days: DayPilot.Date.today().daysInMonth(),
    eventClickHandling: "Disabled",
    onTimeRangeSelected: (async (args) => {
      const scheduler = args.control;
      const modal = await DayPilot.Modal.prompt("Create a new event:", "Event 1");
      scheduler.clearSelection();
      if (modal.canceled) {
          return;
      }
      scheduler.events.add({
          start: args.start,
          end: args.end,
          id: DayPilot.guid(),
          resource: args.resource,
          text: modal.result
      });
    }),
    onEventMove: (args) => {
      const targetRow = dp.rows.find(args.newResource);
      const existing = targetRow.events.forRange(args.newStart, args.newEnd)
        .filter(e => e.id() !== args.e.id());
      if (existing.length === 0) {
        return;
      }

      if (existing.length > 1) {
        DayPilot.Modal.alert("More than one event at the target position");
        args.preventDefault();
        return;
      }

      const sourceEvent = args.e;
      const targetEvent = existing[0];

      const targetDuration = targetEvent.duration();

      targetEvent.data.start = sourceEvent.data.start;
      targetEvent.data.end = targetEvent.start().addTime(targetDuration);
      targetEvent.data.resource = sourceEvent.data.resource;

      const delayed = app.elements.delayed.checked;

      if (delayed) {
        dp.events.remove(targetEvent);
        setTimeout(() => {
          dp.events.add(targetEvent);
        }, 200);
      }
      else {
        dp.events.update(targetEvent);
      }

    },
    onEventMoving: (args) => {
      const targetRow = dp.rows.find(args.resource);
      const existing = targetRow.events.forRange(args.start, args.end)
        .filter(e => e.id() !== args.e.id());
      if (existing.length === 0) {
        return;
      }

      if (existing.length > 1) {
        args.right.enabled = true;
        args.right.html = "More than one event at the target position, unable to swap";
        args.allowed = false;
        return;
      }

      args.right.enabled = true;
      args.right.html = "Events will be swapped";
    },
    scale: "Day",
    startDate: (DayPilot.Date.today().firstDayOfMonth()),
    timeHeaders: [{"groupBy":"Month"},{"format":"d","groupBy":"Day"}],
    timeRangeSelectedHandling: "Enabled",
  });
  dp.init();

  const app = {
    elements: {
      delayed: document.getElementById("delayed")
    },
    init() {
      const resources = [
        {name: "Resource 1", id: "R1"},
        {name: "Resource 2", id: "R2"},
        {name: "Resource 3", id: "R3"},
        {name: "Resource 4", id: "R4"},
        {name: "Resource 5", id: "R5"},
      ];
      const month = DayPilot.Date.today().firstDayOfMonth();
      const events = [
        {id: 1, start: month.addDays(1), end: month.addDays(7), resource: "R2", text: "Event 1", barColor: "#cc0000"},
        {id: 2, start: month.addDays(3), end: month.addDays(9), resource: "R5", text: "Event 2"}
      ];
      dp.update({resources, events});
    }
  };
  app.init();
</script>

</body>
</html>