Overview

  • The JavaScript Scheduler component allows operations on multiple events.

  • Copy events from multiple Scheduler rows using Ctrl+drag

  • In addition to the drag and drop method, you can use “Copy” and “Paste” context menu items.

  • Includes a trial version of DayPilot Pro for JavaScript (see 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. Buy a license.

Allow Selection of Multiple Events

First, it is necessary to set the allowMultiSelect property to true to enable selection of multiple events.

allowMultiSelect: true,

Now we will set the event click action to "Select" (eventClickHandling property). This will allow event selection on click. Multiple events can be selected using Ctrl+click.

eventClickHandling: "Select",

Copy Multiple Events using Drag and Drop

javascript-scheduler-copy-multiple-events-drag-drop.png

Enable moving of multiple selected events using allowMultiMove property of the JavaScript Scheduler component.

Set the multiMoveVerticalMode to "All". That will ensure that all selected events will be moved vertically.

In the onEventMove event handler (which is fired before the event position is updated after drop) we need to detect the Ctrl/Meta key state. If the modifier key is pressed, we will copy the events:

  • We cancel the default action (update of the selected events positions) using args.preventDefault().

  • We create new events using events.add() at the target position. Selected properties of the source events can be copied here, we use the modified text.

Please note that the new events need to have different IDs (duplicate event IDs are not allowed). In this example, we simply generate a random ID on the client side. Normally you would call the API and use the IDs generated by the database.

In this case it’s not possible to use onEventMoved event handler - this event is fired after the events are already updated.

allowMultiMove: true,
multiMoveVerticalMode: "All",
onEventMove: args => {
  const copy = args.ctrl || args.meta;
  if (copy) {
    args.preventDefault();
    args.multimove.forEach(item => {
      dp.events.add({
        start: item.start,
        end: item.end,
        id: DayPilot.guid(),
        text: "Copy of " + item.event.text(),
        resource: item.resource
      });
    });
  }
},

Copy Events using Context Menu (Copy & Paste)

javascript-scheduler-copy-multiple-events-context-menu-copy-paste.png

The event context menu (defined using contextMenu property) needs to include the “Copy” item.

It stores the selected events in the global clipboard variable.

let clipboard = [];

const dp = new DayPilot.Scheduler("dp", {
  // ...
  contextMenu: new DayPilot.Menu({
    items: [
      {
        text: "Copy",
        onClick: args => {
          clipboard = dp.multiselect.events();
        }
      }
    ]
  }),
});
dp.init();

Now we need to add the “Paste” item to the time range selection context menu (contextMenuSelection):

contextMenuSelection: new DayPilot.Menu({
  onShow: args => {
    const pasteEnabled = clipboard.length > 0;
    dp.contextMenuSelection.items[0].disabled = !pasteEnabled;
    dp.contextMenuSelection.items[1].disabled = !pasteEnabled;
  },
  items: [
    {
      text: "Paste",
      onClick: args => {
        const ref = clipboard[0];
        const refRowIndex = dp.rows.find(ref.data.resource).index;
        const selection = args.source;
        const targetRowIndex = dp.rows.find(selection.resource).index;

        const events = clipboard.map(e => {

          const srcRowIndex = dp.rows.find(e.data.resource).index;
          const rowOffset = srcRowIndex - refRowIndex;
          const rowIndex = targetRowIndex + rowOffset;
          const cell = dp.cells.findXy(0, rowIndex)[0];
          if (!cell) {
            return null;
          }
          const resource = dp.cells.findXy(0, rowIndex)[0].resource;

          const duration = e.duration(); // milliseconds
          const offset = e.start().getTime() - ref.start().getTime();
          const start = selection.start.addTime(offset);
          const end = start.addTime(duration);

          return new DayPilot.Event({
            start,
            end,
            text: "Copy of " + e.text(),
            resource,
            id: DayPilot.guid()
          });

        });

        events.forEach(e => {
          dp.events.add(e);
        });

        dp.clearSelection();
      }
    },
    {
      text: "Paste into the same row",
      onClick: args => {
        const ref = clipboard[0];
        const selection = args.source;

        clipboard.forEach(e=> {
          const duration = e.duration(); // milliseconds
          const offset = e.start().getTime() - ref.start().getTime();
          const start = selection.start.addTime(offset);
          const end = start.addTime(duration);

          var newEvent = new DayPilot.Event({
            start,
            end,
            text: "Copy of " + e.text(),
            resource: selection.resource,
            id: DayPilot.guid()  // generate random id
          });
          dp.events.add(newEvent);

        });

        dp.clearSelection();
      }
    }
  ]
})

The onClick handler of the “Paste” item calculates the relative position for each of the events in the clipboard and creates a corresponding new event in the JavaScript Scheduler.

For reference, the context menu also includes “Paste into the same row” which places all copied events in the same target row.

Full Source Code

And here is the complete source code of our example that configures the JavaScript Scheduler to allow copying emultiple vents using context menu and drag and drop.

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

  <style type="text/css">
    /* ... */
  </style>

  <!-- DayPilot library -->
  <script src="js/daypilot/daypilot-all.min.js"></script>
</head>
<body>
<div class="header">
  <h1><a href='https://code.daypilot.org/72047/javascript-scheduler-copy-multiple-events'>JavaScript Scheduler: Copy Multiple Events</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">
    Select multiple events using Ctrl+click (or <a href="javascript: dp.events.all().forEach(e => dp.multiselect.add(e))">select all</a>)
    and right click to copy and paste. You can also copy events using Ctrl+drag.
  </div>
  <div id="dp"></div>
  <div class="generated">Generated using <a href="https://builder.daypilot.org/">DayPilot UI Builder</a>.</div>
</div>

<script>

  let clipboard = [];

  const dp = new DayPilot.Scheduler("dp", {
    timeHeaders: [{"groupBy":"Month"},{"groupBy":"Day","format":"d"}],
    scale: "Day",
    days: 30,
    startDate: "2021-04-01",
    timeRangeSelectedHandling: "Enabled",
    allowMultiSelect: true,
    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
      });
    },
    eventClickHandling: "Select",
    allowMultiMove: true,
    multiMoveVerticalMode: "All",
    treeEnabled: true,
    onEventMove: args => {
      const copy = args.ctrl || args.meta;
      if (copy) {
        args.preventDefault();
        args.multimove.forEach(item => {
          dp.events.add({
            start: item.start,
            end: item.end,
            id: DayPilot.guid(),
            text: "Copy of " + item.event.text(),
            resource: item.resource
          });
        });
      }
    },
    contextMenu: new DayPilot.Menu({
      items: [
        {
          text: "Copy",
          onClick: args => {
            clipboard = dp.multiselect.events();
          }
        }
      ]
    }),
    contextMenuSelection: new DayPilot.Menu({
      onShow: args => {
        const pasteEnabled = clipboard.length > 0;
        dp.contextMenuSelection.items[0].disabled = !pasteEnabled;
        dp.contextMenuSelection.items[1].disabled = !pasteEnabled;
      },
      items: [
        {
          text: "Paste",
          onClick: args => {
            const ref = clipboard[0];
            const refRowIndex = dp.rows.find(ref.data.resource).index;
            const selection = args.source;
            const targetRowIndex = dp.rows.find(selection.resource).index;

            const events = clipboard.map(e => {

              const srcRowIndex = dp.rows.find(e.data.resource).index;
              const rowOffset = srcRowIndex - refRowIndex;
              const rowIndex = targetRowIndex + rowOffset;
              const cell = dp.cells.findXy(0, rowIndex)[0];
              if (!cell) {
                return null;
              }
              const resource = dp.cells.findXy(0, rowIndex)[0].resource;

              const duration = e.duration(); // milliseconds
              const offset = e.start().getTime() - ref.start().getTime();
              const start = selection.start.addTime(offset);
              const end = start.addTime(duration);

              return new DayPilot.Event({
                start,
                end,
                text: "Copy of " + e.text(),
                resource,
                id: DayPilot.guid()
              });

            });

            events.forEach(e => {
              dp.events.add(e);
            });

            dp.clearSelection();
          }
        },
        {
          text: "Paste into the same row",
          onClick: args => {
            const ref = clipboard[0];
            const selection = args.source;

            clipboard.forEach(e=> {
              const duration = e.duration(); // milliseconds
              const offset = e.start().getTime() - ref.start().getTime();
              const start = selection.start.addTime(offset);
              const end = start.addTime(duration);

              var newEvent = new DayPilot.Event({
                start,
                end,
                text: "Copy of " + e.text(),
                resource: selection.resource,
                id: DayPilot.guid()  // generate random id
              });
              dp.events.add(newEvent);

            });

            dp.clearSelection();
          }
        }
      ]
    })
  });
  dp.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"},
  ];
  dp.events.list = [{"start":"2021-04-05T00:00:00","end":"2021-04-10T00:00:00","id":"d0d4fe15-edde-728e-d75b-d31b36a9e1eb","resource":"R3","text":"Event 1"},{"start":"2021-04-08T00:00:00","end":"2021-04-13T00:00:00","id":"60e400c6-0dc9-cf4e-25b3-1d955670fe0d","resource":"R4","text":"Event 1"},{"start":"2021-04-10T00:00:00","end":"2021-04-12T00:00:00","id":"6d96bfcb-51ce-4406-45e7-e82ff824a3b7","resource":"R5","text":"Event 1"}];
  dp.init();
</script>

</body>
</html>