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 Draggable 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 onResourceCreate 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
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 onResourceDelete will be fired.
You can also use the onResourceDragLeave and onResourceDragEnter, and the drop container's onItemDragEnter and onItemDragLeave 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
import { Button, Draggable, Dropcontainer, Eventcalendar, Page, setOptions, Toast } from '@mobiscroll/react';
import PropTypes from 'prop-types';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
});
function Installer(props) {
const [draggable, setDraggable] = useState();
const setDragElm = useCallback((elm) => {
setDraggable(elm);
}, []);
const resource = props.data;
return (
<div className="mds-ext-res-item mbsc-font" ref={setDragElm}>
<div className="mbsc-flex">
<div className="mds-ext-res-avatar" style={{ background: resource.color }}>
{resource.name[0]}
</div>
<div className="mds-ext-res-cont">
<div className="mds-ext-res-name">{resource.name}</div>
<div className="mds-ext-res-title">{resource.title}</div>
</div>
<Draggable dragData={resource} element={draggable} type="resource" />
</div>
</div>
);
}
Installer.propTypes = {
data: PropTypes.object.isRequired,
};
function App() {
const [dropCont, setDropCont] = useState();
const [shouldScroll, setShouldScroll] = useState(0);
const [toastMessage, setToastMessage] = useState('');
const [isToastOpen, setToastOpen] = useState(false);
const timelineRef = useRef();
const installerRef = useRef();
const [installers, setInstallers] = useState([
{
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, setAvailableInstallers] = useState([
{
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 = useMemo(
() => [
{
id: 1,
start: '2026-10-27T07:00',
end: '2026-10-27T13:00',
title: 'Electrical wiring installation',
resource: 1,
},
{
id: 2,
start: '2026-10-27T14:00',
end: '2026-10-27T18:00',
title: 'Power panel connection',
resource: 1,
},
{
id: 3,
start: '2026-10-27T08:00',
end: '2026-10-27T12:00',
title: 'Water pipe fitting',
resource: 2,
},
// Showing partial data. Download full source.
],
[],
);
const myView = useMemo(
() => ({
timeline: { type: 'day', resourceReorder: true, startTime: '07:00', endTime: '18:00' },
}),
[],
);
const setDropElm = useCallback((elm) => {
setDropCont(elm);
}, []);
const handleResourceCreate = useCallback(
(args) => {
setAvailableInstallers(availableInstallers.filter((installer) => installer.id !== args.resource.id));
setToastMessage(args.resource.name + ' added to ' + args.parent.name);
setToastOpen(true);
},
[availableInstallers],
);
const handleResourceDelete = useCallback((args) => {
setToastMessage(args.resource.name + ' removed from ' + args.parent.name);
setToastOpen(true);
}, []);
const handleResourceOrderUpdate = useCallback((args) => {
const parent = args.parent;
const oldParent = args.oldParent;
if (parent && oldParent) {
setToastMessage(args.resource.name + ' moved to ' + args.parent.name);
setToastOpen(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,
});
}
}, []);
const handleItemDrop = useCallback(
(args) => {
if (args.data && args.dataType === 'resource') {
setAvailableInstallers([...availableInstallers, args.data]);
setShouldScroll((prevScroll) => prevScroll + 1);
}
},
[availableInstallers],
);
const handleToastClose = useCallback(() => {
setToastOpen(false);
}, []);
const addNewTeam = useCallback(() => {
const teamLength = installers.length + 1;
const resourceId = 'it-' + teamLength;
setInstallers([
...installers,
{
id: resourceId,
eventCreation: false,
reorder: false,
name: 'Installer team ' + teamLength,
children: [
{
id: 'ph-' + resourceId,
name: 'Drag Technicians here',
placeholder: true,
reorder: false,
},
],
},
]);
if (timelineRef && timelineRef.current) {
timelineRef.current.navigateToEvent({
start: new Date(),
resource: resourceId,
});
}
}, [installers]);
const customResourceHeader = useCallback(
() => (
<div className="mbsc-flex mbsc-align-items-center">
<div className="mds-ext-res-header mbsc-flex-1-1">Set up teams</div>
<Button variant="outline" className="mds-ext-res-add" onClick={addNewTeam}>
Add team
</Button>
</div>
),
[addNewTeam],
);
const customResource = useCallback(
(resource) =>
resource.isParent || resource.placeholder ? (
<div className={'mds-ext-res-name' + (resource.placeholder ? ' mds-ext-res-name-ph' : '')}>{resource.name}</div>
) : (
<div className="mbsc-flex">
<div className="mds-ext-res-avatar" style={{ background: resource.color }}>
{resource.name[0]}
</div>
<div className="mds-ext-res-cont">
<div className="mds-ext-res-name">{resource.name}</div>
<div className="mds-ext-res-title">{resource.title}</div>
</div>
</div>
),
[],
);
useEffect(() => {
if (shouldScroll && installerRef.current) {
installerRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}, [installerRef, shouldScroll]);
return (
<Page className="mds-ext-res-drop">
<div className="mbsc-grid mbsc-no-padding">
<div className="mbsc-row">
<div className="mbsc-col-sm-3 mbsc-flex-col mds-ext-res-drop-cont" ref={setDropElm}>
<Dropcontainer onItemDrop={handleItemDrop} element={dropCont}>
<div className="mds-ext-res-header">Available technicians</div>
<div className="mds-ext-res-list">
{availableInstallers.map((installer) => (
<div key={installer.id} ref={installerRef}>
<Installer data={installer} />
</div>
))}
</div>
</Dropcontainer>
</div>
<div className="mbsc-col-sm-9 mds-ext-res-drop-calendar">
<Eventcalendar
ref={timelineRef}
data={tasks}
dragBetweenResources={false}
externalResourceDrop={true}
externalResourceDrag={true}
resources={installers}
view={myView}
renderResourceHeader={customResourceHeader}
renderResource={customResource}
onResourceCreate={handleResourceCreate}
onResourceDelete={handleResourceDelete}
onResourceOrderUpdate={handleResourceOrderUpdate}
/>
<Toast message={toastMessage} isOpen={isToastOpen} onClose={handleToastClose} />
</div>
</div>
</div>
</Page>
);
}
export default App;
import {
Button,
Draggable,
Dropcontainer,
Eventcalendar,
MbscCalendarEvent,
MbscEventcalendarView,
MbscItemDragEvent,
MbscResource,
MbscResourceCreateEvent,
MbscResourceDeleteEvent,
MbscResourceOrderEvent,
Page,
setOptions,
Toast ,
} from '@mobiscroll/react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
});
function Installer(props: { data: MbscResource }) {
const [draggable, setDraggable] = useState<HTMLDivElement>();
const setDragElm = useCallback((elm: HTMLDivElement) => {
setDraggable(elm);
}, []);
const resource = props.data;
return (
<div className="mds-ext-res-item mbsc-font" ref={setDragElm}>
<div className="mbsc-flex">
<div className="mds-ext-res-avatar" style={{ background: resource.color }}>
{resource.name && resource.name[0]}
</div>
<div className="mds-ext-res-cont">
<div className="mds-ext-res-name">{resource.name}</div>
<div className="mds-ext-res-title">{resource.title}</div>
</div>
<Draggable dragData={resource} element={draggable} type="resource" />
</div>
</div>
);
}
function App() {
const [dropCont, setDropCont] = useState<HTMLDivElement>();
const [shouldScroll, setShouldScroll] = useState(0);
const [toastMessage, setToastMessage] = useState('');
const [isToastOpen, setToastOpen] = useState(false);
const timelineRef = useRef<Eventcalendar>(null);
const installerRef = useRef<HTMLDivElement>(null);
const [installers, setInstallers] = useState<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, setAvailableInstallers] = useState<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 = useMemo<MbscCalendarEvent[]>(
() => [
{
id: 1,
start: '2026-10-27T07:00',
end: '2026-10-27T13:00',
title: 'Electrical wiring installation',
resource: 1,
},
{
id: 2,
start: '2026-10-27T14:00',
end: '2026-10-27T18:00',
title: 'Power panel connection',
resource: 1,
},
{
id: 3,
start: '2026-10-27T08:00',
end: '2026-10-27T12:00',
title: 'Water pipe fitting',
resource: 2,
},
// Showing partial data. Download full source.
],
[],
);
const myView = useMemo<MbscEventcalendarView>(
() => ({
timeline: { type: 'day', resourceReorder: true, startTime: '07:00', endTime: '18:00' },
}),
[],
);
const setDropElm = useCallback((elm: HTMLDivElement) => {
setDropCont(elm);
}, []);
const handleResourceCreate = useCallback(
(args: MbscResourceCreateEvent) => {
setAvailableInstallers(availableInstallers.filter((installer) => installer.id !== args.resource.id));
setToastMessage(args.resource.name + ' added to ' + args.parent?.name);
setToastOpen(true);
},
[availableInstallers],
);
const handleResourceDelete = useCallback((args: MbscResourceDeleteEvent) => {
setToastMessage(args.resource.name + ' removed from ' + args.parent?.name);
setToastOpen(true);
}, []);
const handleResourceOrderUpdate = useCallback((args: MbscResourceOrderEvent) => {
const parent = args.parent;
const oldParent = args.oldParent;
if (parent && oldParent) {
setToastMessage(args.resource.name + ' moved to ' + args.parent?.name);
setToastOpen(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,
});
}
}, []);
const handleItemDrop = useCallback(
(args: MbscItemDragEvent<MbscResource>) => {
if (args.data && args.dataType === 'resource') {
setAvailableInstallers([...availableInstallers, args.data]);
setShouldScroll((prevScroll) => prevScroll + 1);
}
},
[availableInstallers],
);
const handleToastClose = useCallback(() => {
setToastOpen(false);
}, []);
const addNewTeam = useCallback(() => {
const teamLength = installers.length + 1;
const resourceId = 'it-' + teamLength;
setInstallers([
...installers,
{
id: resourceId,
eventCreation: false,
reorder: false,
name: 'Installer team ' + teamLength,
children: [
{
id: 'ph-' + resourceId,
name: 'Drag Technicians here',
placeholder: true,
reorder: false,
},
],
},
]);
if (timelineRef && timelineRef.current) {
timelineRef.current.navigateToEvent({
start: new Date(),
resource: resourceId,
});
}
}, [installers]);
const customResourceHeader = useCallback(
() => (
<div className="mbsc-flex mbsc-align-items-center">
<div className="mds-ext-res-header mbsc-flex-1-1">Set up teams</div>
<Button variant="outline" className="mds-ext-res-add" onClick={addNewTeam}>
Add team
</Button>
</div>
),
[addNewTeam],
);
const customResource = useCallback(
(resource: MbscResource) =>
resource.isParent || resource.placeholder ? (
<div className={'mds-ext-res-name' + (resource.placeholder ? ' mds-ext-res-name-ph' : '')}>{resource.name}</div>
) : (
<div className="mbsc-flex">
<div className="mds-ext-res-avatar" style={{ background: resource.color }}>
{resource.name && resource.name[0]}
</div>
<div className="mds-ext-res-cont">
<div className="mds-ext-res-name">{resource.name}</div>
<div className="mds-ext-res-title">{resource.title}</div>
</div>
</div>
),
[],
);
useEffect(() => {
if (shouldScroll && installerRef.current) {
installerRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}, [installerRef, shouldScroll]);
return (
<Page className="mds-ext-res-drop">
<div className="mbsc-grid mbsc-no-padding">
<div className="mbsc-row">
<div className="mbsc-col-sm-3 mbsc-flex-col mds-ext-res-drop-cont" ref={setDropElm}>
<Dropcontainer onItemDrop={handleItemDrop} element={dropCont}>
<div className="mds-ext-res-header">Available technicians</div>
<div className="mds-ext-res-list">
{availableInstallers.map((installer) => (
<div key={installer.id} ref={installerRef}>
<Installer data={installer} />
</div>
))}
</div>
</Dropcontainer>
</div>
<div className="mbsc-col-sm-9 mds-ext-res-drop-calendar">
<Eventcalendar
ref={timelineRef}
data={tasks}
dragBetweenResources={false}
externalResourceDrop={true}
externalResourceDrag={true}
resources={installers}
view={myView}
renderResourceHeader={customResourceHeader}
renderResource={customResource}
onResourceCreate={handleResourceCreate}
onResourceDelete={handleResourceDelete}
onResourceOrderUpdate={handleResourceOrderUpdate}
/>
<Toast message={toastMessage} isOpen={isToastOpen} onClose={handleToastClose} />
</div>
</div>
</div>
</Page>
);
}
export default App;
.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%;
}