Interactive functionality for FullCalendar including event dragging, resizing, date clicking, and external drag-and-drop support
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
External element dragging functionality allows you to make DOM elements outside of a calendar draggable onto the calendar, creating new events when dropped. This provides a powerful way to create events by dragging items from lists, sidebars, or other interface elements.
Makes external HTML elements draggable onto FullCalendar instances. Provides built-in visual feedback, touch support, and event data generation.
/**
* Makes an element external to any calendar draggable onto calendars
* @param el - The HTML element to make draggable
* @param settings - Configuration options for drag behavior
*/
export class Draggable {
constructor(el: HTMLElement, settings?: ExternalDraggableSettings);
/** Destroys the draggable instance and removes all event listeners */
destroy(): void;
/** Internal dragging implementation (read-only) */
readonly dragging: FeaturefulElementDragging;
/** Configuration settings (read-only) */
readonly settings: ExternalDraggableSettings;
}Usage Examples:
import { Draggable } from '@fullcalendar/interaction';
// Basic draggable element
const draggableEl = document.getElementById('external-events');
const draggable = new Draggable(draggableEl, {
eventData: {
title: 'My Event',
duration: '02:00'
}
});
// Dynamic event data based on element
const listEl = document.getElementById('event-list');
const listDraggable = new Draggable(listEl, {
itemSelector: '.draggable-item',
eventData: (el) => {
return {
title: el.textContent,
backgroundColor: el.dataset.color,
duration: el.dataset.duration || '01:00'
};
}
});
// Cleanup when done
draggable.destroy();
listDraggable.destroy();Configuration interface for customizing draggable behavior.
interface ExternalDraggableSettings {
/** Event data or function to generate event data when dropped */
eventData?: DragMetaGenerator;
/** CSS selector for draggable items within the element */
itemSelector?: string;
/** Minimum distance in pixels before drag starts */
minDistance?: number;
/** Delay in milliseconds for long press on touch devices */
longPressDelay?: number;
/** Parent element for drag mirror positioning */
appendTo?: HTMLElement;
}Property Details:
Type for specifying event data that will be created when an external element is dropped.
type DragMetaGenerator = DragMetaInput | ((el: HTMLElement) => DragMetaInput);
interface DragMetaInput {
/** Duration to add to start time when dropped */
startTime?: DurationInput;
/** Duration of the event when dropped */
duration?: DurationInput;
/** Whether to create an event when dropped (default: true) */
create?: boolean;
/** Source ID for event creation */
sourceId?: string;
/** Any additional event properties (title, backgroundColor, etc.) */
[key: string]: any;
}Static Event Data:
const draggable = new Draggable(element, {
eventData: {
duration: '01:30',
title: 'Team Meeting',
backgroundColor: '#3788d8'
}
});Dynamic Event Data:
const draggable = new Draggable(element, {
eventData: (draggedEl) => {
const dataset = draggedEl.dataset;
return {
duration: dataset.duration || '01:00',
title: draggedEl.textContent || 'Untitled Event',
backgroundColor: dataset.color || '#3788d8',
category: dataset.category,
priority: dataset.priority
};
}
});Multiple Draggable Lists:
import { Draggable } from '@fullcalendar/interaction';
// Different types of events from different lists
const taskList = new Draggable(document.getElementById('tasks'), {
itemSelector: '.task-item',
eventData: (el) => ({
title: el.querySelector('.task-title').textContent,
backgroundColor: '#ff6b6b',
extendedProps: { type: 'task' }
})
});
const meetingList = new Draggable(document.getElementById('meetings'), {
itemSelector: '.meeting-item',
eventData: (el) => ({
title: el.querySelector('.meeting-title').textContent,
duration: '01:00',
backgroundColor: '#4ecdc4',
extendedProps: { type: 'meeting' }
})
});Touch-Optimized Configuration:
const mobileDraggable = new Draggable(element, {
minDistance: 10, // Small movement threshold for touch
longPressDelay: 800, // 800ms press to start drag
itemSelector: '.drag-handle' // Specific drag handles for precise control
});Custom Drag Mirror Positioning:
const constrainedDraggable = new Draggable(element, {
appendTo: document.getElementById('drag-container'), // Constrain mirror to container
eventData: { title: 'Constrained Event' }
});Cleanup and Memory Management:
import { Draggable } from '@fullcalendar/interaction';
// Always destroy draggable instances when no longer needed
const draggables = [];
function setupDraggables() {
const elements = document.querySelectorAll('.draggable-item');
elements.forEach(el => {
const draggable = new Draggable(el, {
eventData: { title: el.textContent }
});
draggables.push(draggable);
});
}
function cleanupDraggables() {
// Essential for preventing memory leaks
draggables.forEach(draggable => draggable.destroy());
draggables.length = 0;
}
// Clean up when component unmounts or page unloads
window.addEventListener('beforeunload', cleanupDraggables);Error Handling for Dynamic Event Data:
const safeDraggable = new Draggable(element, {
eventData: (el) => {
try {
// Safe parsing of element data
const data = JSON.parse(el.dataset.eventConfig || '{}');
return {
title: data.title || el.textContent || 'Untitled Event',
duration: data.duration || '01:00',
backgroundColor: data.color || '#3788d8',
// Provide fallbacks for all properties
extendedProps: {
source: 'external',
originalId: el.id,
...data.props
}
};
} catch (error) {
console.warn('Failed to parse event data:', error);
// Return safe fallback data
return {
title: el.textContent || 'Untitled Event',
duration: '01:00',
backgroundColor: '#ccc'
};
}
}
});Handling Dynamic DOM Changes:
// When DOM structure changes, recreate draggables
function refreshDraggables() {
// Clean up existing instances
cleanupDraggables();
// Check if elements still exist before creating new instances
const elements = document.querySelectorAll('.draggable-item');
if (elements.length === 0) {
console.warn('No draggable elements found');
return;
}
// Recreate draggables
setupDraggables();
}
// Use MutationObserver for automatic refresh
const observer = new MutationObserver((mutations) => {
let shouldRefresh = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
shouldRefresh = true;
}
});
if (shouldRefresh) {
refreshDraggables();
}
});
observer.observe(document.getElementById('draggable-container'), {
childList: true,
subtree: true
});External dragging works seamlessly with FullCalendar's event system:
const calendar = new Calendar(calendarEl, {
plugins: [interactionPlugin],
// Handle successful drops
drop: (info) => {
console.log('External element dropped:', info.draggedEl);
console.log('Drop location:', info.dateStr);
// Optionally remove the dragged element from its original location
info.draggedEl.parentNode?.removeChild(info.draggedEl);
},
// Handle event creation from external drag
eventReceive: (info) => {
console.log('New event created:', info.event.title);
console.log('Event data:', info.event.extendedProps);
}
});