Dragging Items
Drop items from outside the calendar - a task list, a backlog, a sidebar - onto the grid and turn them into events. This guide covers drag-in from outside only. Moving, resizing, and creating events inside the grid already work with no extra code.
How drag-in works
Three parts share the job:
- Your app owns the gesture. The calendar doesn't install native drag listeners. You mark the source items as
draggableand handledragstart,dragover, anddropyourself. - The calendar owns the preview. While the drag is running, it finds the cell under the pointer, snaps to the nearest slot, and draws a marker where the event would land.
- You own the result. On drop, you create the event with the data the calendar resolved.
The link between your handlers and the calendar is the eventProjection prop. Think of it as a shared note: you write the pointer position on it, the calendar writes back the start and end that match that position - plus the resource, in resource views.
The gesture always follows the same four steps:
| Native event | What you do |
|---|---|
dragstart on the source item | create the note with the proposed event data |
dragover on the calendar wrapper | update the pointer position, call preventDefault() |
drop on the calendar wrapper | read the resolved dates, add the event, clear the note |
dragend on the source item | clear the note (the drag was cancelled) |
One rule worth remembering: keep the same nested event object for the whole gesture. The calendar writes the resolved dates into that object, so replacing it mid-drag throws away the answer.
Making the source items draggable
Mark each item with draggable="true" and open the note on dragstart. Set id: null so the calendar generates the ID later:
<script setup>
import { ref } from "vue";
const tasks = [
{ id: "t1", text: "Design review", duration: 60 * 60000 },
{ id: "t2", text: "Quick sync", duration: 30 * 60000 },
];
const eventProjection = ref(null);
function onTaskDragStart(task) {
eventProjection.value = { htmlEvent: null, event: { ...task, id: null } };
}
function onTaskDragEnd() {
eventProjection.value = null;
}
</script>
<template>
<div
v-for="task in tasks"
:key="task.id"
draggable="true"
:ondragstart="() => onTaskDragStart(task)"
:ondragend="onTaskDragEnd"
>
{{ task.text }}
</div>
</template>
Anything you put into event survives the drag, so this is the place to copy the item's text, color, or any custom fields.
Showing the preview
Wrap the calendar in a container and feed it pointer coordinates on every dragover. Call preventDefault() - without it the browser never fires drop:
<script setup>
function onTaskDrag(ev) {
eventProjection.value = { ...eventProjection.value, htmlEvent: ev };
ev.preventDefault();
}
</script>
<template>
<div :ondragover="onTaskDrag" :ondrop="onTaskDrop">
<Calendar ref="api" :events="events" :date="date" view="week" :eventProjection="eventProjection" />
</div>
</template>
The marker follows the cursor and snaps to real slots, so what the user sees before releasing is what they get after.
Creating the event on drop
By the time drop fires, eventProjection.event already carries the resolved start and end. Pass it to the add-event action and clear the note:
<script setup>
function onTaskDrop(ev) {
if (!api.value || !eventProjection.value?.event.start || !eventProjection.value.event.end)
return;
ev.preventDefault();
api.value.exec("add-event", {
event: { ...eventProjection.value.event, duration: null },
});
eventProjection.value = null;
}
</script>
Two details in that snippet:
- The guard on
startandendcovers drops outside the grid - on the header, on empty space around the calendar, on a view that has no time slots. Nothing was resolved there, so there is nothing to add. duration: nulltells the store to keep the resolvedstart/endinstead of recalculating the span fromduration.
Controlling the length of the dropped event
The preview needs to know how long the event is before it can draw anything. Give it either:
- a positive
durationin milliseconds, or - a
start/endpair with a positive span, which the calendar shifts to the drop position.
An item with neither never resolves and never previews. Duration also decides where the event lands in views split into several sections: a 30-minute task previews inside the time grid, while a full-day task previews in the all-day row above it.
const tasks = [
{ id: "t1", text: "Quick sync", duration: 30 * 60000 }, // time grid
{ id: "t2", text: "Conference day", duration: 1440 * 60000 }, // all-day row
];
Dropping into resource views
In views built around resources or timelines, the calendar also resolves which column or row the pointer is over and writes that field into the event. A task dropped on the "Room B" lane arrives assigned to Room B - your handlers stay exactly the same.
See resourcesViewModel and timelineViewModel for how those views are configured.
Opening the editor after the drop
Dropped items usually need a title or extra fields. Render the Editor next to the calendar and it opens for the newly created event:
<template>
<div :ondragover="onTaskDrag" :ondrop="onTaskDrop">
<Calendar ref="api" :events="events" :date="date" view="week" :eventProjection="eventProjection" />
<Editor v-if="api" :api="api" />
</div>
</template>
What to read next
eventProjection- the full descriptor type and field reference.add-event- parameters of the action you dispatch on drop.- Editing Events - what happens to the event after it lands on the grid.