or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

external-dragging.mdindex.mdinteraction-types.mdthird-party-integration.md
tile.json

external-dragging.mddocs/

External Element Dragging

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.

Capabilities

Draggable Class (ExternalDraggable)

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();

ExternalDraggableSettings

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:

  • eventData: Defines what event will be created when dropped. Can be static data or a function that receives the dragged element
  • itemSelector: When provided, only child elements matching this selector will be draggable
  • minDistance: Prevents accidental drags by requiring minimum movement (default varies by device)
  • longPressDelay: Touch devices require press-and-hold to start dragging (default from FullCalendar settings)
  • appendTo: Specifies where the drag mirror element should be appended (defaults to document body)

DragMetaGenerator

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
    };
  }
});

Advanced Usage Patterns

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' }
});

Error Handling and Edge Cases

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
});

Integration with FullCalendar Events

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);
  }
});