
| Demo | |
|---|---|
| Downloads | |
| Repository | |
| Languages | JavaScriptHTML5 |
| Code License | Apache License 2.0 (Open-Source) |
Features
-
Drag custom items from an external list to JavaScript Scheduler events (each event is an active HTML5 drop target)
-
Built using standard HTML5 drag and drop API
-
Each event can hold multiple external items
-
Items are removed from the source list after drop
Try the live demo or download the JavaScript project attached to this tutorial. The sample uses the npm-based Scheduler Lite project from DayPilot UI Builder. Run npm install and npm run dev to start it locally.
License
The tutorial source and DayPilot Lite for JavaScript are available under Apache License 2.0. See the licensing information included in the download. Learn more about DayPilot for JavaScript.
Dragging Items to Scheduler Events
The JavaScript Scheduler includes built-in support for dragging external items to the Scheduler. These items will be transformed into events and you can use this feature to schedule tasks from a list using drag and drop.
It is also possible to drag items from an external list to existing Scheduler events. This tutorial will show how to implement it using the standard HTML drag-and-drop API. Each accepted item is stored in the target event's items array. Updating the event redraws its contents without losing previously assigned items.
This example uses mouse-based native HTML dragging. Assignments are stored in memory and reset when the page reloads. Event creation, moving, and resizing are disabled to keep the example focused on assigning items.
Making External Items Draggable
Let's have a list of items that we want to drag to the Scheduler events. We will use simple <div> elements with additional data saved using data-id and data-name attributes:
<div class="draggable item" data-id="1" data-name="Item 1">Item 1</div>
<div class="draggable item" data-id="2" data-name="Item 2">Item 2</div>
<div class="draggable item" data-id="3" data-name="Item 3">Item 3</div>
The source pills and assigned items use coordinated green colors. Padding separates each label from its rounded border, and the source hover style makes the draggable item easy to identify.
.item { display: inline-block; padding: 9px 14px; border-radius: 18px; background: #237763; color: white; line-height: 18px; }
.draggable { cursor: grab; border: 1px solid #19604f; }
.draggable:hover { background: #185b4b; box-shadow: 0 0 0 3px #b9e0d5; }
.draggable:active { cursor: grabbing; }
The app.activateItems() method adds the draggable attribute and installs dragstart and dragend handlers. On drag start, it remembers the source element and writes a JSON payload using the custom daypilot/external-item type. The custom type distinguishes these items from unrelated text or file drags.
activateItems() {
for (const item of document.querySelectorAll("#items .draggable")) {
item.setAttribute("draggable", "true");
item.addEventListener("dragstart", ev => {
this.activeSource = item;
const data = {id: item.dataset.id, name: item.dataset.name};
ev.dataTransfer.setData(this.type, JSON.stringify(data));
ev.dataTransfer.effectAllowed = "move";
});
item.addEventListener("dragend", () => {
this.activeSource = null;
document.querySelectorAll(".dragging-over").forEach(target => target.classList.remove("dragging-over"));
});
}
},
The dragend handler only clears temporary state and highlighting. Source removal happens in the successful drop handler below. Do not rely on dragend.dataTransfer.dropEffect to decide whether to remove the source: WebKit can report a value that does not reflect the completed move.
Scheduler Events as Drop Target
In this step, we will activate the Scheduler events as drop targets. We set native dragover, dragleave, and drop handlers using onAfterEventRender, which provides access to the event's <div> element. This hook is available in DayPilot Lite starting with build 2026.4.886 (npm package 6.1.0).
During dragover, we check the custom type and active source, then call preventDefault() to allow a drop. The payload is read only on drop, because browser drag data is protected during dragover. Leaving a nested child of the same event should not remove the highlight, so dragleave checks relatedTarget.
onAfterEventRender: args => {
args.div.dataset.eventId = args.e.id();
args.div.addEventListener("dragover", ev => {
if (!app.accepts(ev.dataTransfer)) {
return;
}
ev.preventDefault();
ev.dataTransfer.dropEffect = "move";
args.div.classList.add("dragging-over");
});
args.div.addEventListener("dragleave", ev => {
if (!args.div.contains(ev.relatedTarget)) {
args.div.classList.remove("dragging-over");
}
});
args.div.addEventListener("drop", ev => {
ev.preventDefault();
args.div.classList.remove("dragging-over");
const accepted = app.readItem(ev.dataTransfer);
if (!accepted) {
return;
}
const {data, source} = accepted;
const items = args.e.data.items || [];
if (dp.events.list.some(event => (event.items || []).some(item => item.id === data.id))) {
return;
}
args.e.data.items = [...items, data];
dp.events.update(args.e);
source.remove();
app.activeSource = null;
document.querySelector("#status").textContent = `${data.name} assigned. Remaining items: ${document.querySelectorAll("#items .draggable").length}.`;
});
}
After validation, we append the item to the event data and call events.update(). Then we explicitly remove the remembered source element. The duplicate-ID check also prevents an item from being assigned twice. Because handlers are attached by onAfterEventRender, newly rendered event elements remain usable as drop targets.
Validating the Drag Data
The custom MIME type is a filter, not validation by itself. The readItem() method catches invalid JSON, checks the ID and name types, and matches both values against the active source element. It also verifies that this source still belongs to the source list. An unrelated drag, stale source, or changed payload leaves both lists untouched.
type: "daypilot/external-item",
activeSource: null,
accepts(transfer) {
return transfer && Array.from(transfer.types).includes(this.type) &&
this.activeSource?.isConnected;
},
readItem(transfer) {
if (!this.accepts(transfer)) {
return null;
}
try {
const data = JSON.parse(transfer.getData(this.type));
const source = this.activeSource;
if (!data || typeof data.id !== "string" || typeof data.name !== "string" ||
data.id !== source.dataset.id || data.name !== source.dataset.name ||
!document.querySelector("#items").contains(source)) {
return null;
}
return {data: {id: data.id, name: data.name}, source};
} catch {
return null;
}
},
Displaying Assigned Items
To display items inside events, onBeforeEventRender reads the items array from args.data. Each name is escaped using DayPilot.Util.escapeHtml() before it is inserted into custom HTML. If the array is empty, the event displays “Drop items here”.
onBeforeEventRender: args => {
args.data.backColor = "#e0f2ed";
args.data.borderColor = "#87b9aa";
args.data.fontColor = "#174d40";
const items = args.data.items || [];
args.data.html = items.length
? items.map(item => `<span class="item assigned">${DayPilot.Util.escapeHtml(item.name)}</span>`).join("")
: '<span class="empty">Drop items here</span>';
},
The items live in event data rather than only in the DOM, so both an individual event update and a full Scheduler update retain the assignments. The CSS below rounds the event container and adds a contrasting inner border while an accepted item is dragged over it:
.scheduler_default_event { border-radius: 10px; }
.scheduler_default_event_inner { border-radius: 10px; padding: 8px 12px; display: flex; align-items: center; gap: 6px; }
.assigned { padding: 6px 12px; }
.empty { color: #426359; }
.dragging-over .scheduler_default_event_inner { background: #c2e7da !important; box-shadow: inset 0 0 0 2px #237763; }
DayPilot




