Features

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.

Using Event Versions to Display the Allowed Range

JavaScript Scheduler Highlight Target Drag and Drop Date Range

The JavaScript Scheduler component can display one or more previous versions of each event. The version bars are read-only and appear outside of the main event box. You can place them above or below the event using the eventVersionPosition property. The default theme uses striped styling so the version stays visually separate from the active event.

The main purpose of this feature is to display the original plan together with the current position so users can see how the schedule has changed.

In this example, the version bar defines the horizontal range that the event is allowed to use during drag and drop moving.

The range is stored in the versions field of each event data object:

const startDate = DayPilot.Date.today().firstDayOfMonth();

const events = [
  {
    start: startDate.addDays(4),
    end: startDate.addDays(7),
    id: 1,
    resource: "R2",
    text: "Event 1",
    versions: [
      {
        start: startDate.addDays(1),
        end: startDate.addDays(11),
        barHidden: true,
      },
    ],
  },
  {
    start: startDate.addDays(5),
    end: startDate.addDays(8),
    id: 2,
    resource: "R4",
    text: "Event 2",
    versions: [
      {
        start: startDate.addDays(3),
        end: startDate.addDays(14),
        barHidden: true,
      },
    ],
  },
];

Limiting the Drag and Drop Range

JavaScript Scheduler Limit Target Drag and Drop Date Range

Now that the allowed range is visible, we can actually limit the target position during drag and drop. This is handled in the real-time onEventMoving event handler.

The pulled sample keeps both behaviors in one page so you can switch between them using toolbar buttons. For the tutorial text, it is clearer to show the two onEventMoving implementations separately so you can copy the one you prefer directly. The full source code later combines both options behind the button-driven mode switch.

Clamp the Target Range

Use this version when you want the event shadow to stay inside the allowed range while the user is dragging:

onEventMoving: args => {
  const version = args.e.data.versions?.[0];
  if (!version) {
    return;
  }
  const range = {
    start: new DayPilot.Date(version.start),
    end: new DayPilot.Date(version.end),
  };

  if (args.start < range.start) {
    const diff = range.start.getTime() - args.start.getTime();
    args.start = range.start;
    args.end = args.end.addTime(diff);
  }

  if (args.end > range.end) {
    const diff = args.end.getTime() - range.end.getTime();
    args.end = range.end;
    args.start = args.start.addTime(-diff);
  }
},

Mark the Drop as Forbidden

JavaScript Scheduler Limit Target Drag and Drop Date Range Forbidden

Use this version when you want to keep the event shadow under the pointer and only reject drops outside the allowed range:

onEventMoving: args => {
  const version = args.e.data.versions?.[0];
  if (!version) {
    return;
  }
  const range = {
    start: new DayPilot.Date(version.start),
    end: new DayPilot.Date(version.end),
  };

  if (args.start < range.start || args.end > range.end) {
    args.allowed = false;
  }
},

In this mode, the Scheduler shows the forbidden styling and won't fire onEventMove if the event is dropped outside the allowed window.

Full Source Code

The downloadable sample includes both options in one page, so its full source keeps the button-driven app.mode switch and a single conditional onEventMoving handler.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>JavaScript Scheduler: Limited Drag and Drop Range</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>
  <style>
    .toolbar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
    .toolbar button {
      padding: 6px 10px;
      border: 1px solid #c5ccd6;
      background: #f4f7fb;
      border-radius: 5px;
      cursor: pointer;
    }
    .toolbar button.active {
      background: #1155a3;
      border-color: #1155a3;
      color: #ffffff;
    }
  </style>

  <!-- DayPilot library -->
  <script src="js/daypilot/daypilot-all.min.js"></script>
</head>
<body>
<div class="header">
  <h1><a href='https://code.daypilot.org/23016/javascript-scheduler-limited-drag-and-drop-range'>JavaScript Scheduler: Limited Drag and Drop Range</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="toolbar" id="modes"></div>
  <div id="dp"></div>
  <div class="generated">Generated using <a href="https://builder.daypilot.org/">DayPilot UI Builder</a>.</div>
</div>

<script>

  const app = {
    mode: "clamp",

    loadData() {
      const startDate = DayPilot.Date.today().firstDayOfMonth();

      const resources = Array.from({ length: 9 }, (_, index) => ({
        id: `R${index + 1}`,
        name: `Resource ${index + 1}`,
      }));

      const events = [
        {
          start: startDate.addDays(4),
          end: startDate.addDays(7),
          id: 1,
          resource: "R2",
          text: "Event 1",
          versions: [
            {
              start: startDate.addDays(1),
              end: startDate.addDays(11),
              barHidden: true,
            },
          ],
        },
        {
          start: startDate.addDays(5),
          end: startDate.addDays(8),
          id: 2,
          resource: "R4",
          text: "Event 2",
          versions: [
            {
              start: startDate.addDays(3),
              end: startDate.addDays(14),
              barHidden: true,
            },
          ],
        },
      ];
      dp.update({events, resources});
    },

    init() {
      dp.init();
      this.loadData();

      this.initButtons();
    },

    initButtons() {
      const container = document.getElementById("modes");

      const modeButtons = [
        { id: "clamp", label: "Clamp Moving" },
        { id: "forbidden", label: "Forbidden Drop" },
      ];

      modeButtons.forEach((mode) => {
        const button = document.createElement("button");
        button.type = "button";
        button.textContent = mode.label;
        button.dataset.mode = mode.id;
        button.addEventListener("click", () => this.applyMode(mode.id));
        container.appendChild(button);
      });

      this.applyMode(this.mode);
    },

    applyMode(modeId) {
      this.mode = modeId;
      document.querySelectorAll("#modes button").forEach((button) => {
        button.classList.toggle("active", button.dataset.mode === modeId);
      });
    },
  };

  const dp = new DayPilot.Scheduler("dp", {
    startDate: DayPilot.Date.today().firstDayOfMonth(),
    days: DayPilot.Date.today().daysInMonth(),
    rowHeaderWidth: 100,
    treeEnabled: true,
    scale: "Day",
    timeHeaders: [{ groupBy: "Month" }, { groupBy: "Day", format: "d" }],
    eventVersionsEnabled: true,
    eventVersionsReserveSpace: true,
    eventVersionHeight: 10,
    timeRangeSelectedHandling: "Enabled",
    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,
      });
    },
    onEventMoving: (args) => {
      const version = args.e.data.versions?.[0];
      if (!version) {
        return;
      }
      const range = {
        start: new DayPilot.Date(version.start),
        end: new DayPilot.Date(version.end),
      };

      if (app.mode === "forbidden") {
        if (args.start < range.start || args.end > range.end) {
          args.allowed = false;
        }
      }
      else if (app.mode === "clamp") {
        if (args.start < range.start) {
          const diff = range.start.getTime() - args.start.getTime();
          args.start = range.start;
          args.end = args.end.addTime(diff);
        }

        if (args.end > range.end) {
          const diff = args.end.getTime() - range.end.getTime();
          args.end = range.end;
          args.start = args.start.addTime(-diff);
        }
      }
    },
  });

  app.init();
</script>

</body>
</html>

History

  • April 11, 2026: Refreshed the sample to the current JavaScript Scheduler builder template, updated the code examples to modern JavaScript, and simplified the implementation so the app object owns the mode state while a single onEventMoving handler branches between clamp and forbidden behavior.