Overview

Let users scroll quickly to the next free time slot in the JavaScript Scheduler row.

This example uses DayPilot Lite for JavaScript, an open-source scheduling library available under the Apache License 2.0.

JavaScript Scheduler: Find Next Free Slot (Demo)

Find the Next Free Slot in the Scheduler Row

We will add an icon to the row headers of the JavaScript Scheduler. The icon will be displayed on hover and it will search the row on click. On touch devices, the icon stays visible.

First, we need to create the active area that displays the icon. Define it in the onBeforeRowHeaderRender event handler in src/app.js:

onBeforeRowHeaderRender: args => {
  args.row.areas = [
    {
      right: 6,
      top: "calc(50% - 8px)",
      height: 16,
      width: 16,
      visibility: "TouchVisible",
      symbol: "icons/daypilot.svg#minichevron-right-2",
      backColor: "#4caf50",
      fontColor: "#ffffff",
      borderRadius: 3,
      style: "cursor: pointer;",
      toolTip: "Find next free slot",
      // ...
    }
  ];
}

The active area onClick handler searches the row events and finds the next free slot. The search starts at the left edge of the visible timeline, obtained using getViewport(). This is a gap search from the current scroll position; it does not use the current clock time.

It gets the events in the row using args.row.events.all() and keeps those that end after the viewport start. This includes events that started earlier but still occupy that position. Sorting by start time lets the search process events in chronological order, even after events have been moved or added.

If the viewport starts in free space, the search skips that gap and begins at the next event. It then advances pos to the end of each overlapping or adjacent event. An event wholly inside a longer event cannot move the position backwards. The first event that starts after pos marks a gap, so the loop stops. If there is no later event, the free space starts at the end of the final occupied block.

When a free slot is found, the handler scrolls there using the scrollTo() method:

onClick: () => {
  const start = dp.getViewport().start;

  // Include events that started earlier but still occupy the viewport edge.
  const events = args.row.events.all()
    .filter(e => e.end() > start)
    .sort((a, b) => a.start().getTime() - b.start().getTime());
  if (events.length === 0) {
    return;
  }

  // Skip the current free space so repeated clicks advance to a new gap.
  let pos = start;
  if (events[0].start() > pos) {
    pos = events[0].start();
  }

  for (const e of events) {
    if (e.start() > pos) {
      break;
    }
    // Merge overlapping and adjacent events without moving backwards.
    if (e.end() > pos) {
      pos = e.end();
    }
  }

  dp.scrollTo(pos);
}

Click the green chevron in Resource 2 repeatedly to jump between gaps. If the row has no events ending after the current position, the view stays unchanged. This example finds the start of any gap; it does not require a minimum free duration. Near the end of the timeline, the Scheduler limits scrolling to the available range.

Full Source Code

The download contains an npm project. Run npm ci and npm run dev, then open the local address printed by Vite. The index.html file provides the sample page and loads src/app.js as a module. The project shell can also be generated using DayPilot UI Builder.

index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>JavaScript Scheduler: Find Next Free Slot</title>

  <style>
    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; }
    body .scheduler_default_rowheader_inner { padding-right: 30px; }
    body .scheduler_default_event_inner {
      background: #fcb71199;
      border-radius: 5px;
      border: 1px solid #fcb711;
      color: #333333;
    }
  </style>

</head>
<body>
<div class="header">
  <h1><a href='https://code.daypilot.org/94263/javascript-scheduler-find-next-free-slot'>JavaScript Scheduler: Find Next Free Slot</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 id="dp"></div>
  <div class="generated">Generated using <a href="https://builder.daypilot.org/">DayPilot UI Builder</a>.</div>
</div>

<script type="module" src="/src/app.js"></script>

</body>
</html>

src/app.js

Here is the full JavaScript source. It adds the active areas to the row headers and generates one-to-three-day events with repeatable gaps in the “R2” row. The timeline starts on the first day of the current month. You can select a time range to create an event; the search reads the current row data each time you click the icon.

import {DayPilot} from "@daypilot/daypilot-lite-javascript";

const dp = new DayPilot.Scheduler("dp", {
  timeHeaders: [{groupBy: "Month"}, {groupBy: "Day", format: "d"}],
  scale: "Day",
  days: 150,
  cellWidth: 60,
  eventHeight: 60,
  durationBarVisible: false,
  rowMarginBottom: 3,
  rowMarginTop: 3,
  rowHeaderWidth: 120,
  startDate: DayPilot.Date.today().firstDayOfMonth(),
  onTimeRangeSelected: async args => {
    const modal = await DayPilot.Modal.prompt("Create a new event:", "Event 1");
    dp.clearSelection();
    if (modal.canceled) {
      return;
    }
    dp.events.add({
      start: args.start,
      end: args.end,
      id: DayPilot.guid(),
      resource: args.resource,
      text: modal.result
    });
  },
  onBeforeRowHeaderRender: args => {
    args.row.areas = [
      {
        right: 6,
        top: "calc(50% - 8px)",
        height: 16,
        width: 16,
        visibility: "TouchVisible",
        symbol: "icons/daypilot.svg#minichevron-right-2",
        backColor: "#4caf50",
        fontColor: "#ffffff",
        borderRadius: 3,
        style: "cursor: pointer;",
        toolTip: "Find next free slot",
        onClick: () => {
          const start = dp.getViewport().start;

          // Include events that started earlier but still occupy the viewport edge.
          const events = args.row.events.all()
            .filter(e => e.end() > start)
            .sort((a, b) => a.start().getTime() - b.start().getTime());
          if (events.length === 0) {
            return;
          }

          // Skip the current free space so repeated clicks advance to a new gap.
          let pos = start;
          if (events[0].start() > pos) {
            pos = events[0].start();
          }

          for (const e of events) {
            if (e.start() > pos) {
              break;
            }
            // Merge overlapping and adjacent events without moving backwards.
            if (e.end() > pos) {
              pos = e.end();
            }
          }

          dp.scrollTo(pos);
        }
      }
    ];
  }
});
dp.init();

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"},
      {name: "Resource 5", id: "R5"},
      {name: "Resource 6", id: "R6"},
      {name: "Resource 7", id: "R7"},
      {name: "Resource 8", id: "R8"},
      {name: "Resource 9", id: "R9"}
    ];

    // A repeatable sequence of one-to-three-day events with a gap every fifth event.
    const events = [];
    let start = dp.startDate;
    for (let i = 1; i < 80; i++) {
      const durationDays = (i % 3) + 1;
      const end = start.addDays(durationDays);
      if (i % 5 !== 0) {
        events.push({
          start,
          end,
          id: i,
          resource: "R2",
          text: "Event " + i
        });
      }
      start = end;
    }

    dp.update({resources, events});
  }
};
app.init();

The events are stored in memory. Scrolling keeps your changes, and reloading the page restores the generated data. For use with saved bookings, load your events before searching the row.

History

October 25, 2026: Switched to DayPilot Lite for JavaScript. The search handles overlapping events and events that span the viewport start.