External Resource Drag & Drop
Usage
theme: 'ios' You can turn auto theming on and let it decide based on the platform.
Usage
themeVariant: 'light' You can turn auto dark mode on based on system settings.
Usage
clickToCreate: true,
// click to create is on dragToCreate: true,
// drag to create is on dragToMove: true,
// reordering is on dragToResize: true,
// resizing is on eventDelete: true
// deleting is on Fine-tune drag & drop with these options, which are false by default.
Usage
locale: localeEn Selecting the locale loads the localized settings and the translated UI text.
External Resource Drag & Drop
By combining the resourceReorder feature with external resource drag-and-drop, you can easily organize, group, and adjust resources by dragging them into or out of the timeline view.
Resources can be created by dragging and dropping an external element onto the timeline. In order for that to work you will need to have two things set up:
- Enable the timeline to receive external resources by setting
externalResourceDroptotrue. - Initialize the external resources (containers) as
draggablecomponents.
Use the MbscDraggable component to specify a skeleton event through its dragData option and reference the draggable container in the element option. The dragData accepts a full event definition that will be added to the event calendar on drop. If omitted, a default event will be created.
Use the resource-create for triggering a custom logic on drop like showing a toast.
Additionally resource can be removed by dragging them out from the timeline and dropping them onto an external drop container. To activate this, you will need to:
- Enable the timeline to allow dragging events out by setting
externalResourceDragtotrue. - Initialize the
dropcontainer
Resources can also be dropped on another timeline with externalResourceDrop enabled.
When a resource is dropped into an external drop container or another timeline, it will be deleted from the original calendar and the resource-delete will be fired.
You can also use the resource-drag-leave and resource-drag-enter, and the drop container's item-drag-enter and item-drag-leave events to provide visual feedback or running custom logic during drag.
Demo
The live demo didn't load.
Try it on demo.mobiscroll.com
Code
View code on GitHub
<script setup>
import {
MbscButton,
MbscDraggable,
MbscDropcontainer,
MbscEventcalendar,
MbscPage,
MbscToast,
setOptions
} from '@mobiscroll/vue'
import { nextTick, ref } from 'vue'
setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
})
const dragElements = ref([])
const dropCont = ref(null)
const toastMessage = ref('')
const isToastOpen = ref(false)
const timelineRef = ref()
const installers = ref([
{
id: 'it-1',
name: 'Installer team 1',
eventCreation: false,
reorder: false,
children: [
{
id: 1,
name: 'Emily Carter',
color: '#007acc',
title: 'Electrician'
},
{
id: 2,
name: 'Michael Lawson',
color: '#008000',
title: 'Plumber'
}
]
},
// Showing partial data. Download full source.
])
const availableInstallers = ref([
{
id: 9,
name: 'Adam Miller',
color: '#C70039',
title: 'Mason'
},
{
id: 10,
name: 'Isabella Martinez',
color: '#2ECC71',
title: 'Surveyor'
},
{
id: 11,
name: 'Mark White',
color: '#34495E',
title: 'Glazier'
},
// Showing partial data. Download full source.
])
const tasks = ref([
{
id: 1,
start: '2026-11-01T07:00',
end: '2026-11-01T13:00',
title: 'Electrical wiring installation',
resource: 1
},
{
id: 2,
start: '2026-11-01T14:00',
end: '2026-11-01T18:00',
title: 'Power panel connection',
resource: 1
},
{
id: 3,
start: '2026-11-01T08:00',
end: '2026-11-01T12:00',
title: 'Water pipe fitting',
resource: 2
},
// Showing partial data. Download full source.
])
const myView = {
timeline: { type: 'day', resourceReorder: true, startTime: '07:00', endTime: '18:00' }
}
function handleResourceCreate(args) {
availableInstallers.value = availableInstallers.value.filter(
(installer) => installer.id !== args.resource.id
)
toastMessage.value = args.resource.name + ' added to ' + args.parent.name
isToastOpen.value = true
}
function handleResourceDelete(args) {
toastMessage.value = args.resource.name + ' removed from ' + args.parent.name
isToastOpen.value = true
}
function handleResourceOrderUpdate(args) {
const parent = args.parent
const oldParent = args.oldParent
if (parent && oldParent) {
toastMessage.value = args.resource.name + ' moved to ' + args.parent.name
isToastOpen.value = true
}
if (parent && parent.children) {
// Remove placeholder resource
parent.children = parent.children.filter((child) => !child.placeholder)
}
if (oldParent && !oldParent.children.length) {
// Add placeholder resource
oldParent.children.push({
id: 'ph-' + oldParent.id,
name: 'Drag Technicians here',
placeholder: true,
reorder: false
})
}
}
function handleItemDrop(args) {
if (args.data && args.dataType === 'resource') {
availableInstallers.value = [...availableInstallers.value, args.data]
nextTick(() => {
const dragElms = dragElements.value
if (dragElms && dragElms[dragElms.length - 1]) {
dragElms[dragElms.length - 1].scrollIntoView({
behavior: 'smooth',
block: 'nearest'
})
}
})
}
}
function handleToastClose() {
isToastOpen.value = false
}
function addNewTeam() {
const teamLength = installers.value.length + 1
const resourceId = 'it-' + teamLength
installers.value = [
...installers.value,
{
id: resourceId,
eventCreation: false,
reorder: false,
name: 'Installer team ' + teamLength,
children: [
{
id: 'ph-' + resourceId,
name: 'Drag Technicians here',
placeholder: true,
reorder: false
}
]
}
]
timelineRef.value.instance.navigateToEvent({
start: new Date(),
resource: resourceId
})
}
</script>
<template>
<MbscPage cssClass="mds-ext-res-drop">
<div class="mbsc-grid mbsc-no-padding">
<div class="mbsc-row">
<div ref="dropCont" class="mbsc-col-sm-3 mbsc-flex-col mds-ext-res-drop-cont">
<MbscDropcontainer :element="dropCont" @item-drop="handleItemDrop">
<div class="mds-ext-res-header">Available technicians</div>
<div class="mds-ext-res-list">
<template v-for="(resource, i) in availableInstallers" :key="resource.id">
<div ref="dragElements" class="mds-ext-res-item mbsc-font">
<div class="mbsc-flex">
<div class="mds-ext-res-avatar" :style="{ background: resource.color }">
{{ resource.name[0] }}
</div>
<div class="mds-ext-res-cont">
<div class="mds-ext-res-name">{{ resource.name }}</div>
<div class="mds-ext-res-title">{{ resource.title }}</div>
</div>
</div>
<MbscDraggable :element="dragElements[i]" :dragData="resource" type="resource" />
</div>
</template>
</div>
</MbscDropcontainer>
</div>
<div class="mbsc-col-sm-9 mds-ext-res-drop-calendar">
<MbscEventcalendar
ref="timelineRef"
:data="tasks"
:dragBetweenResources="false"
:externalResourceDrop="true"
:externalResourceDrag="true"
:resources="installers"
:view="myView"
@resource-create="handleResourceCreate"
@resource-delete="handleResourceDelete"
@resource-order-update="handleResourceOrderUpdate"
>
<template #resourceHeader>
<div class="mbsc-flex mbsc-align-items-center">
<div class="mds-ext-res-header mbsc-flex-1-1">Set up teams</div>
<MbscButton variant="outline" cssClass="mds-ext-res-add" :onClick="addNewTeam">
Add team
</MbscButton>
</div>
</template>
<template #resource="res">
<div
v-if="res.isParent || res.placeholder"
:class="{ 'mds-ext-res-name': true, 'mds-ext-res-name-ph': res.placeholder }"
>
{{ res.name }}
</div>
<div v-else class="mbsc-flex">
<div class="mds-ext-res-avatar" :style="{ background: res.color }">
{{ res.name[0] }}
</div>
<div class="mds-ext-res-cont">
<div class="mds-ext-res-name">{{ res.name }}</div>
<div class="mds-ext-res-title">{{ res.title }}</div>
</div>
</div>
</template>
</MbscEventcalendar>
</div>
</div>
</div>
<MbscToast :message="toastMessage" :isOpen="isToastOpen" @close="handleToastClose" />
</MbscPage>
</template>
<style>
.mds-ext-res-drop-calendar {
border-left: 1px solid #ccc;
}
.mds-ext-res-drop-calendar .mbsc-timeline-row {
height: 50px;
}
.mds-ext-res-drop-calendar .mbsc-timeline-row-gutter {
height: 8px;
}
.mds-ext-res-drop-calendar .mbsc-timeline-resource-col {
width: 250px;
}
.mds-ext-res-drop-calendar .mbsc-timeline-parent {
height: 32px;
}
.mds-ext-res-drop-calendar .mbsc-timeline-resource {
align-items: center;
align-content: center;
}
.mds-ext-res-drop-calendar .mds-ext-res-add {
margin: 0;
}
.mds-ext-res-header {
font-weight: 600;
font-size: 0.875em;
line-height: 38px;
text-transform: uppercase;
opacity: 0.6;
}
.mds-ext-res-name-ph {
font-style: italic;
opacity: 0.4;
}
.mds-ext-res-drop-cont .mds-ext-res-header {
padding: 12px 16px;
line-height: 1.5em;
}
.mds-ext-res-list {
overflow: auto;
}
.mds-ext-res-item {
background: rgba(128, 128, 128, 0.6);
padding: 10px;
margin: 16px;
border-radius: 8px;
}
.mds-ext-res-item:first-child {
margin-top: 0;
}
.mds-ext-res-cont {
padding: 0 7px;
}
.mds-ext-res-name {
font-size: 14px;
font-weight: 600;
}
.mds-ext-res-title {
padding-top: 2px;
font-size: 12px;
font-weight: 400;
line-height: 16px;
}
.mds-ext-res-avatar {
width: 36px;
min-width: 36px;
height: 36px;
line-height: 36px;
border-radius: 18px;
text-align: center;
color: #fff;
font-size: 22px;
font-weight: 400;
}
.mds-ext-res-drop,
.mds-ext-res-drop .mbsc-grid,
.mds-ext-res-drop .mbsc-row,
.mds-ext-res-drop-cont,
.mds-ext-res-drop-calendar {
height: 100%;
}
</style> <script setup lang="ts">
import {
MbscButton,
MbscDraggable,
MbscDropcontainer,
MbscEventcalendar,
MbscPage,
MbscToast,
setOptions
} from '@mobiscroll/vue'
import type {
MbscCalendarEvent,
MbscEventcalendarView,
MbscItemDragEvent,
MbscResource,
MbscResourceCreateEvent,
MbscResourceDeleteEvent,
MbscResourceOrderEvent
} from '@mobiscroll/vue'
import { nextTick, ref } from 'vue'
setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
})
const dragElements = ref<HTMLDivElement[]>([])
const dropCont = ref<HTMLDivElement | null>(null)
const toastMessage = ref('')
const isToastOpen = ref(false)
const timelineRef = ref<typeof MbscEventcalendar>()
const installers = ref<MbscResource[]>([
{
id: 'it-1',
name: 'Installer team 1',
eventCreation: false,
reorder: false,
children: [
{
id: 1,
name: 'Emily Carter',
color: '#007acc',
title: 'Electrician'
},
{
id: 2,
name: 'Michael Lawson',
color: '#008000',
title: 'Plumber'
}
]
},
// Showing partial data. Download full source.
])
const availableInstallers = ref<MbscResource[]>([
{
id: 9,
name: 'Adam Miller',
color: '#C70039',
title: 'Mason'
},
{
id: 10,
name: 'Isabella Martinez',
color: '#2ECC71',
title: 'Surveyor'
},
{
id: 11,
name: 'Mark White',
color: '#34495E',
title: 'Glazier'
},
// Showing partial data. Download full source.
])
const tasks = ref<MbscCalendarEvent[]>([
{
id: 1,
start: '2026-11-01T07:00',
end: '2026-11-01T13:00',
title: 'Electrical wiring installation',
resource: 1
},
{
id: 2,
start: '2026-11-01T14:00',
end: '2026-11-01T18:00',
title: 'Power panel connection',
resource: 1
},
{
id: 3,
start: '2026-11-01T08:00',
end: '2026-11-01T12:00',
title: 'Water pipe fitting',
resource: 2
},
// Showing partial data. Download full source.
])
const myView: MbscEventcalendarView = {
timeline: { type: 'day', resourceReorder: true, startTime: '07:00', endTime: '18:00' }
}
function handleResourceCreate(args: MbscResourceCreateEvent) {
availableInstallers.value = availableInstallers.value.filter(
(installer) => installer.id !== args.resource.id
)
toastMessage.value = args.resource.name + ' added to ' + args.parent?.name
isToastOpen.value = true
}
function handleResourceDelete(args: MbscResourceDeleteEvent) {
toastMessage.value = args.resource.name + ' removed from ' + args.parent?.name
isToastOpen.value = true
}
function handleResourceOrderUpdate(args: MbscResourceOrderEvent) {
const parent = args.parent
const oldParent = args.oldParent
if (parent && oldParent) {
toastMessage.value = args.resource.name + ' moved to ' + args.parent?.name
isToastOpen.value = true
}
if (parent && parent.children) {
// Remove placeholder resource
parent.children = parent.children.filter((child) => !child.placeholder)
}
if (oldParent && oldParent.children && !oldParent.children.length) {
// Add placeholder resource
oldParent.children.push({
id: 'ph-' + oldParent.id,
name: 'Drag Technicians here',
placeholder: true,
reorder: false
})
}
}
function handleItemDrop(args: MbscItemDragEvent<MbscResource>) {
if (args.data && args.dataType === 'resource') {
availableInstallers.value = [...availableInstallers.value, args.data]
nextTick(() => {
const dragElms = dragElements.value
if (dragElms && dragElms[dragElms.length - 1]) {
dragElms[dragElms.length - 1].scrollIntoView({
behavior: 'smooth',
block: 'nearest'
})
}
})
}
}
function handleToastClose() {
isToastOpen.value = false
}
function addNewTeam() {
const teamLength = installers.value.length + 1
const resourceId = 'it-' + teamLength
installers.value = [
...installers.value,
{
id: resourceId,
eventCreation: false,
reorder: false,
name: 'Installer team ' + teamLength,
children: [
{
id: 'ph-' + resourceId,
name: 'Drag Technicians here',
placeholder: true,
reorder: false
}
]
}
]
timelineRef.value?.instance.navigateToEvent({
start: new Date(),
resource: resourceId
})
}
</script>
<template>
<MbscPage cssClass="mds-ext-res-drop">
<div class="mbsc-grid mbsc-no-padding">
<div class="mbsc-row">
<div ref="dropCont" class="mbsc-col-sm-3 mbsc-flex-col mds-ext-res-drop-cont">
<MbscDropcontainer :element="dropCont" @item-drop="handleItemDrop">
<div class="mds-ext-res-header">Available technicians</div>
<div class="mds-ext-res-list">
<template v-for="(resource, i) in availableInstallers" :key="resource.id">
<div ref="dragElements" class="mds-ext-res-item mbsc-font">
<div class="mbsc-flex">
<div class="mds-ext-res-avatar" :style="{ background: resource.color }">
{{ resource.name[0] }}
</div>
<div class="mds-ext-res-cont">
<div class="mds-ext-res-name">{{ resource.name }}</div>
<div class="mds-ext-res-title">{{ resource.title }}</div>
</div>
</div>
<MbscDraggable :element="dragElements[i]" :dragData="resource" type="resource" />
</div>
</template>
</div>
</MbscDropcontainer>
</div>
<div class="mbsc-col-sm-9 mds-ext-res-drop-calendar">
<MbscEventcalendar
ref="timelineRef"
:data="tasks"
:dragBetweenResources="false"
:externalResourceDrop="true"
:externalResourceDrag="true"
:resources="installers"
:view="myView"
@resource-create="handleResourceCreate"
@resource-delete="handleResourceDelete"
@resource-order-update="handleResourceOrderUpdate"
>
<template #resourceHeader>
<div class="mbsc-flex mbsc-align-items-center">
<div class="mds-ext-res-header mbsc-flex-1-1">Set up teams</div>
<MbscButton variant="outline" cssClass="mds-ext-res-add" :onClick="addNewTeam">
Add team
</MbscButton>
</div>
</template>
<template #resource="res">
<div
v-if="res.isParent || res.placeholder"
:class="{ 'mds-ext-res-name': true, 'mds-ext-res-name-ph': res.placeholder }"
>
{{ res.name }}
</div>
<div v-else class="mbsc-flex">
<div class="mds-ext-res-avatar" :style="{ background: res.color }">
{{ res.name[0] }}
</div>
<div class="mds-ext-res-cont">
<div class="mds-ext-res-name">{{ res.name }}</div>
<div class="mds-ext-res-title">{{ res.title }}</div>
</div>
</div>
</template>
</MbscEventcalendar>
</div>
</div>
</div>
<MbscToast :message="toastMessage" :isOpen="isToastOpen" @close="handleToastClose" />
</MbscPage>
</template>
<style>
.mds-ext-res-drop-calendar {
border-left: 1px solid #ccc;
}
.mds-ext-res-drop-calendar .mbsc-timeline-row {
height: 50px;
}
.mds-ext-res-drop-calendar .mbsc-timeline-row-gutter {
height: 8px;
}
.mds-ext-res-drop-calendar .mbsc-timeline-resource-col {
width: 250px;
}
.mds-ext-res-drop-calendar .mbsc-timeline-parent {
height: 32px;
}
.mds-ext-res-drop-calendar .mbsc-timeline-resource {
align-items: center;
align-content: center;
}
.mds-ext-res-drop-calendar .mds-ext-res-add {
margin: 0;
}
.mds-ext-res-header {
font-weight: 600;
font-size: 0.875em;
line-height: 38px;
text-transform: uppercase;
opacity: 0.6;
}
.mds-ext-res-name-ph {
font-style: italic;
opacity: 0.4;
}
.mds-ext-res-drop-cont .mds-ext-res-header {
padding: 12px 16px;
line-height: 1.5em;
}
.mds-ext-res-list {
overflow: auto;
}
.mds-ext-res-item {
background: rgba(128, 128, 128, 0.6);
padding: 10px;
margin: 16px;
border-radius: 8px;
}
.mds-ext-res-item:first-child {
margin-top: 0;
}
.mds-ext-res-cont {
padding: 0 7px;
}
.mds-ext-res-name {
font-size: 14px;
font-weight: 600;
}
.mds-ext-res-title {
padding-top: 2px;
font-size: 12px;
font-weight: 400;
line-height: 16px;
}
.mds-ext-res-avatar {
width: 36px;
min-width: 36px;
height: 36px;
line-height: 36px;
border-radius: 18px;
text-align: center;
color: #fff;
font-size: 22px;
font-weight: 400;
}
.mds-ext-res-drop,
.mds-ext-res-drop .mbsc-grid,
.mds-ext-res-drop .mbsc-row,
.mds-ext-res-drop-cont,
.mds-ext-res-drop-calendar {
height: 100%;
}
</style>