Overview

  • Select two events and then swap them using a context menu

  • The selection can be made using a special checkbox displayed in the upper-right corner of events.

  • The context menu item that allows swapping is activated/deactivated depending on whether the conditions are met.

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

There is also another tutorial that uses a different approach for swapping events (simply dragging the event over the other one):

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. Buy a license.

JavaScript Scheduler Component Setup

This tutorial assumes that you have already added the JavaScript Scheduler component to your application and configured it according to your requirements.

You can generate a blank project with a pre-configured JavaScript Scheduler using the online Scheduler UI Builder app, or you can start with one of the tutorials (ASP.NET Core, Spring Boot, PHP).

In this tutorial, we will focus on adding a “Swap events” feature to an configured Scheduler.

Selecting the Events to Be Swapped

JavaScript Scheduler Selecting Events to Be Swapped

First, it is necessary to select two events that will be swapped.

We will use the built-in event multi-select feature, which allows users of your scheduling application to select multiple events using both Ctrl+Shift and/or rectangle selection (drag and drop).

There is a built-in API for handling the event selection. This API can be used to add events to the selection, retrieve the list of selected events, clear the selection, among other functions.

Checkbox for Selecting Events

To make it obvious that events can be selected, we will add a checkbox to the upper-right corner of each event.

To display the checkbox, we will not use the <input type="checkbox"> element, but rather an SVG icon from the daypilot.svg icon bundle (see event icons).

To add the icon, we will create an active area using the onBeforeEventRender event handler:

onBeforeEventRender: args => {

  // ...

  args.data.areas = [
    {
      right: 3,
      top: 6,
      height: 14,
      width: 14,
      symbol: "icons/daypilot.svg#x-4",
      style: "border: 1px solid #0079b7; background-color: white; cursor: pointer; border-radius: 4px;",
      onClick: args => {
        const e = args.source;
        const isSelected = dp.multiselect.isSelected(e);

        if (isSelected) {
          dp.multiselect.remove(e);
        }
        else {
          dp.multiselect.add(e);
        }
      },
    }
  ];
},

Clicking on the icon will toggle the event's selection status. Selected events are marked with .scheduler_event_selected CSS class. We will add the following CSS to show/hide the “X” icon depending on the selection status:

.scheduler_default_event svg {
  display: none;
}
.scheduler_default_event.scheduler_default_selected svg {
  display: unset;
}

Context Menu with “Swap selected events” Item

JavaScript Scheduler Swap Selected Events Context Menu Item

We are now ready to implement the action to swap events.

This action will be accessible through the event context menu, which appears upon right-clicking an event box.

contextMenu: new DayPilot.Menu({
  // ...
  items: [
    {
      text: "Swap selected events",
    },

    // ...

  ]
})

Since the “Swap selected events” action can only be executed when exactly two events are selected, we will dynamically adjust the menu item each time the context menu is activated.

This can be done using the onShow event handler. Within this handler, we verify whether exactly two events are selected and if the context menu is invoked for one of these selected events.

contextMenu: new DayPilot.Menu({
  onShow: args => {
    const e = args.source;
    const isSelected = dp.multiselect.isSelected(e);
    const swapAllowed = dp.multiselect.get().length === 2 && isSelected;
    args.menu.items[0].disabled = !swapAllowed;
  },
  items: [
    // ...
  ]
})

Swapping the Events

The swap action will be performed in the onClick event handler of the “Swap Selected Events” context menu item.

There may be different approaches to the swapping logic. One option is to allow swapping only between events with the same duration. Alternatively, you can maintain the duration of the existing time slots, meaning the duration may change during the swap.

In this example, the duration of the original event will be preserved, with only the start time and resource (row) being changed.

contextMenu: new DayPilot.Menu({
  items: [
    {
      text: "Swap selected events",
      onClick: args => {
        const selected = dp.multiselect.get();
        const a = selected[0];
        const b = selected[1];

        const aUpdated = {
          start: b.data.start,
          end: b.data.start.addTime(a.duration()),
          resource: b.data.resource
        };

        const bUpdated = {
          start: a.data.start,
          end: a.data.start.addTime(b.duration()),
          resource: a.data.resource
        };

        a.data.start = aUpdated.start;
        a.data.end = aUpdated.end;
        a.data.resource = aUpdated.resource;

        b.data.start = bUpdated.start;
        b.data.end = bUpdated.end;
        b.data.resource = bUpdated.resource;

        dp.multiselect.clear();
        dp.events.update(a);
        dp.events.update(b);
      }
    },
  ]
})

Full Source Code

Here is the full JavaScript source code of a Scheduler component with “Event swapping” feature implemented:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>JavaScript Scheduler: Swap Events using Context Menu</title>

  <style>
    .scheduler_default_event_inner {
      border-radius: 5px;
    }
    .scheduler_default_event svg {
      display: none;
    }
    .scheduler_default_event.scheduler_default_selected svg {
      display: unset;
    }
  </style>

  <!-- DayPilot library -->
  <script src="js/daypilot/daypilot-all.min.js"></script>
</head>
<body>
<div class="header">
  <h1><a href='https://code.daypilot.org/20006/javascript-scheduler-swap-events-using-context-menu'>JavaScript Scheduler: Swap Events using Context Menu</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>
  const dp = new DayPilot.Scheduler("dp", {
    timeHeaders: [{"groupBy":"Month"},{"groupBy":"Day","format":"d"}],
    scale: "Day",
    days: 366,
    startDate: "2024-01-01",
    timeRangeSelectedHandling: "Enabled",
    durationBarVisible: false,
    eventHeight: 50,
    onTimeRangeSelected: async (args) => {
      const dp = args.control;
      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
      });
    },
    onBeforeEventRender: args => {
      args.data.backColor = "#0089d0";
      args.data.fontColor = "#ffffff";
      args.data.borderColor = "#0079b7";

      args.data.areas = [
        {
          right: 3,
          top: 6,
          height: 14,
          width: 14,
          symbol: "icons/daypilot.svg#x-4",
          style: "border: 1px solid #0079b7; background-color: white; cursor: pointer; border-radius: 4px;",
          onClick: args => {
            const e = args.source;
            const isSelected = dp.multiselect.isSelected(e);

            if (isSelected) {
              dp.multiselect.remove(e);
            }
            else {
              dp.multiselect.add(e);
            }
          },
        }
      ];
    },
    contextMenu: new DayPilot.Menu({
      onShow: args => {
        const e = args.source;
        const isSelected = dp.multiselect.isSelected(e);
        const swapAllowed = dp.multiselect.get().length === 2 && isSelected;
        args.menu.items[0].disabled = !swapAllowed;

        const somethingSelected = dp.multiselect.get().length > 0;
        args.menu.items[2].disabled = !somethingSelected;
      },
      items: [
        {
          text: "Swap selected events",
          onClick: args => {
            const selected = dp.multiselect.get();
            const a = selected[0];
            const b = selected[1];

            const aUpdated = {
              start: b.data.start,
              end: b.data.start.addTime(a.duration()),
              resource: b.data.resource
            };

            const bUpdated = {
              start: a.data.start,
              end: a.data.start.addTime(b.duration()),
              resource: a.data.resource
            };

            a.data.start = aUpdated.start;
            a.data.end = aUpdated.end;
            a.data.resource = aUpdated.resource;

            b.data.start = bUpdated.start;
            b.data.end = bUpdated.end;
            b.data.resource = bUpdated.resource;

            dp.multiselect.clear();
            dp.events.update(a);
            dp.events.update(b);
          }
        },
        {
          text: "-"
        },
        {
          text: "Clear selection",
          onClick: args => {
            dp.multiselect.clear();
          }
        }
      ]
    })
  });
  dp.init();

  const app = {
    loadData: function() {

      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"},
      ];

      const events = [
        {
          id: 1,
          text: "Event 1",
          start: "2024-01-05T00:00:00",
          end: "2024-01-08T00:00:00",
          resource: "R2"
        },
        {
          id: 2,
          text: "Event 2",
          start: "2024-01-10T00:00:00",
          end: "2024-01-18T00:00:00",
          resource: "R4"
        },
      ];

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

</script>

</body>
</html>