Features

The JavaScript Scheduler can skip non-business time during drag and drop. This example keeps weekends visible and uses two different policies:

  • Moving preserves the event's business duration using a built-in setting.
  • Creating and resizing extend non-business endpoints outward using one shared helper.

Events remain continuous start/end intervals. A weekend inside an event does not split it into separate events. The live demo and downloadable JavaScript project use a daily timeline and local event updates.

The sample includes a trial version of DayPilot Pro for JavaScript.

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.

Skipping Non-Business Cells during Event Moving

scheduler moving

Enable eventMoveSkipNonBusiness to let DayPilot calculate the new start and end:

const dp = new DayPilot.Scheduler("dp", {
  // ...
  eventMoveSkipNonBusiness: true,
  // ...
});

Moving preserves business duration. For example, a three-business-day event moved to Thursday can run through Monday, with an exclusive end on Tuesday. Its elapsed duration grows across the weekend; moving it back to Monday can shorten the elapsed duration again. The time between the grab point and the event start is also taken into account.

A source with zero business duration cannot be moved while business-time skipping applies. A target without enough known business time is forbidden: the shadow turns red and releasing leaves the event unchanged. Moving back to a valid target permits the move again. Ordinary boundary clamping still works when the event fits.

Times hidden from the timeline and visible cells marked non-business are related but distinct configurations. This sample shows weekends. For row-specific business rules, you can set args.cell.business in onBeforeCellRender; the row's business flags affect these calculations.

Creating and Resizing Events

scheduler creating

When creating events, the Scheduler lets the application decide how to adjust the selected range. Our policy always extends non-business endpoints outward. Connect the same helper to onTimeRangeSelecting and onEventResizing:

In src/app.js, import DayPilot and initialize the Scheduler with the daily timeline and both callbacks. The complete configuration below also includes the local creation handler:

import {DayPilot} from "daypilot-pro-javascript";

const dp = new DayPilot.Scheduler("dp", {
  timeHeaders: [{groupBy: "Month"}, {groupBy: "Day", format: "d"}],
  scale: "Day",
  startDate: "2026-10-01",
  days: 21,
  cellWidth: 40,
  eventMoveSkipNonBusiness: true,
  onTimeRangeSelecting: adjustForNonBusiness,
  onEventResizing: adjustForNonBusiness,
  onTimeRangeSelected: async args => {
    const modal = await DayPilot.Modal.prompt("Create a new event:", "Event 1");
    dp.clearSelection();
    if (!modal.result) {
      return;
    }
    dp.events.add({
      start: args.start,
      end: args.end,
      id: DayPilot.guid(),
      resource: args.resource,
      text: modal.result
    });
  }
});
dp.init();

onTimeRangeSelecting adjusts the preview while the pointer moves. Once the user releases an allowed selection, onTimeRangeSelected prompts for a name, clears the selection, and adds an event using the adjusted start and end. Canceling or submitting an empty name leaves the event list unchanged. Moving and resizing use the Scheduler's default local update behavior.

Implementing the Shared Helper

Define adjustForNonBusiness() in the same file. Both callbacks receive a row and proposed start/end values, so they can share this implementation:

function adjustForNonBusiness(args) {
  const cells = args.row.cells.forRange(args.start, args.end);
  if (!cells || !cells.length) {
    args.allowed = false;
    return;
  }

  let first = cells[0];
  let last = cells[cells.length - 1];

  while (first && !first.properties.business) {
    first = dp.cells.findXy(first.x - 1, first.y, first.grid)[0];
    if (first) {
      args.start = first.start;
    }
  }
  while (last && !last.properties.business) {
    last = dp.cells.findXy(last.x + 1, last.y, last.grid)[0];
    if (last) {
      args.end = last.end;
    }
  }

  if (!first || !last) {
    args.allowed = false;
  }
}

The helper starts with args.row.cells.forRange(args.start, args.end), which identifies the cells covered by the proposed interval on that row. If the lookup is empty, it sets args.allowed = false and returns before reading either endpoint.

The first loop checks first.properties.business. While the first cell is non-business, it asks the public cells.findXy() method for the cell immediately to its left: first.x - 1 keeps the same row coordinate and grid. Each successful lookup moves args.start to that cell's start. The loop stops as soon as it reaches a business cell.

The second loop performs the matching search to the right using last.x + 1. It updates args.end to each neighboring cell's exclusive end. Using actual cell boundaries instead of adding the previous cell's duration ensures that differently sized cells do not cause a business cell to be skipped.

Endpoints already in business cells stay unchanged. Selecting Saturday and Sunday extends the range to Friday through Monday, ending at Tuesday's exclusive boundary. If either search reaches the timeline boundary without finding a business cell, args.allowed = false rejects the operation. A failed lookup leaves the last in-range boundary as the preview; no extra visible-range assignment is needed.

scheduler resizing

Resizing uses the same endpoint policy. It may adjust the stationary endpoint too if that endpoint is non-business. Unlike moving, this helper does not preserve business duration: it expands the requested interval.

This small helper assumes ordinary resource dates, exclusive event ends, and snapped whole-cell interactions. Custom row bounds, inclusive date-only ends, or unsnapped interactions may require a different policy. The helper adds no explanation label.

Loading the Resources and Events

Complete src/app.js with the sample-data initialization below. dp.update({resources, events}) loads four rows and three events inside the displayed October timeline. Event 1 starts on Monday and ends at Thursday's exclusive boundary, giving it three business days for the moving example.

const app = {
  init() {
    const resources = [
      {name: "Resource 1", id: "R1"},
      {name: "Resource 2", id: "R2"},
      {name: "Resource 3", id: "R3"},
      {name: "Resource 4", id: "R4"}
    ];
    const events = [
      {id: 1, text: "Event 1", start: "2026-10-05", end: "2026-10-08", resource: "R1"},
      {id: 2, text: "Event 2", start: "2026-10-08", end: "2026-10-10", resource: "R2"},
      {id: 3, text: "Event 3", start: "2026-10-12", end: "2026-10-15", resource: "R3"}
    ];
    dp.update({resources, events});
  }
};
app.init();

Together, the Scheduler initialization, shared helper, and data initialization form the complete JavaScript implementation used by the sample. The events remain in browser memory; this example does not save changes to a server.

Optional Invalid-Move Feedback

DayPilot already displays a forbidden shadow and rejects an impossible move. To add an application-supplied explanation, include this optional handler in the Scheduler configuration:

onEventMoving: args => {
  if (args.invalid) {
    args.right.enabled = true;
    args.right.html = "This move cannot be completed here.";
  }
}

args.invalid is a read-only boolean snapshot in onEventMoving. It reports a calculation failure, including an invalid secondary event in a multi-move. It is not the disabled-cell or overlap flag, and it is not supplied to onTimeRangeSelecting or onEventResizing.

Do not assign to args.invalid or mirror it into args.allowed. DayPilot enforces rejection; the application's args.allowed veto remains separate. The optional label clears when the pointer returns to a valid target. The normal sample omits this handler.

History of Changes

  • October 6, 2026: Updated the example to DayPilot Pro for JavaScript sandbox build 2026.4.7084. Replaced the selection/resizing helper with cell-by-cell navigation, added optional invalid-move feedback, and expanded the implementation walkthrough.