05_drag_and_drop_system_.md - ganeshramakrishnanbr/Jumbo_NoCodeBuilder GitHub Wiki

Welcome back! We've come a long way. We now understand the basic building blocks: the different ControlTypes (like "Text Box" or "Tab"), the individual Control instances that represent each piece on your form, how the entire design is held together in a single Questionnaire object, and how the QuestionnaireContext acts as the central brain managing all this data.

But how do you actually put those building blocks together visually? How do you move that Text Box onto the canvas? How do you rearrange controls after you've added them?

This is where the Drag and Drop System comes in!

What is the Drag and Drop System?

Think about building with physical blocks. You pick up a block, carry it, and place it where you want it to go. The Drag and Drop System in our builder is the digital equivalent of that. It's the tool that lets you use your mouse to:

  1. Pick a new Control type from the "Control Palette" (the list of available controls, usually on the side).
  2. Drag it over to the "Design Canvas" (the main area where you build your form).
  3. Drop it onto the canvas or inside another container control (like a Tab or Accordion section).
  4. Pick up an existing Control on the Design Canvas and move it to a different position or into a different container.

While you're dragging, the system handles showing visual cues (like highlights) to indicate where you can drop the item and where it will end up. When you drop, the system figures out what happened and tells the QuestionnaireContext to update the underlying Questionnaire data accordingly (either by adding a new Control or moving an existing one).

Solving a Use Case: Adding a New Control from the Palette

Let's use our running example: You want to add a Text Box to your form. You see the "Text Box" item in the Control Palette and you want to drag it onto the empty canvas.

This visual action needs to translate into a change in the Questionnaire data managed by the QuestionnaireContext. Specifically, the system needs to create a new Control object with type: ControlType.TextBox and add it to the controls array within the Questionnaire object. The Drag and Drop system is the bridge that makes this happen based on your mouse actions.

Key Concepts of Drag and Drop

The Drag and Drop system involves several parts working together:

  1. Drag Source: This is the element you click and start dragging. It could be an item in the Control Palette (representing a new control) or an existing Control instance on the Design Canvas. The source needs to carry information about what is being dragged (e.g., its ControlType if new, or its id if existing).
  2. Drop Target: This is the area where you can release a dragged item. The Design Canvas is a main drop target. Container controls (like Tabs or Accordions) also have areas that act as drop targets for dropping controls inside them. Drop targets need to be able to validate if the currently dragged item is allowed to be dropped there (e.g., can you drop a Tab inside a Text Box? Probably not).
  3. Drag State: While an item is being dragged, the system keeps track of information about it – what it is, where the drag started, whether it's a new item or an existing one. This state is often managed globally so both the source and potential targets can access it.
  4. Visual Cues: As you drag an item over a drop target, the system provides visual feedback. This could be highlighting the target area, showing an insertion line to indicate where the item will be placed, or changing the cursor.
  5. Drop Logic: When you release the mouse button over a valid drop target, the drop target's code runs. It reads the information about the dragged item (from the drag state) and decides what action to take. This action then typically involves calling functions on the QuestionnaireContext to modify the actual form data.

How it Works: Adding a New Control (Simplified Flow)

Let's trace the path of adding a new Text Box from the palette to the canvas using Drag and Drop and how it interacts with the QuestionnaireContext:

sequenceDiagram
    Participant U as User
    Participant CP as ControlPalette
    Participant DDC as DragDropContext
    Participant DC as DesignCanvas
    Participant QC as QuestionnaireContext

    U->>CP: Clicks and drags "Text Box"
    CP->>DDC: Calls startDrag({ controlType: ControlType.TextBox, isNew: true, ... })
    Note over DDC: DDC stores info about the dragged Text Box (it's new)
    U->>DC: Drags over the Design Canvas area
    DC->>DDC: Calls canDropIn('canvas')?
    DDC-->>DC: Returns true (Canvas is a valid target for new controls)
    DC->>DC: Shows visual drop indicator (e.g., blue border)
    U->>DC: Releases mouse (Drops)
    DC->>DC: Handle drop event
    DC->>DDC: Gets draggedItem info (ControlType.TextBox, isNew: true)
    DC->>DC: Creates data for a new Control instance based on type
    DC->>QC: Calls addControl({ type: ControlType.TextBox, label: 'New Text Box', ... })
    Note over QC: QC generates ID, adds Control to Questionnaire.controls array
    QC-->>DC: State updates, DC re-renders
    DC->>DC: Shows the new Text Box on the canvas
    DC->>DDC: Calls endDrag()
    Note over DDC: DDC clears dragged item state
Loading

This diagram illustrates how the visual action (dragging from Control Palette, dropping on Design Canvas) is managed by the DragDropContext, and how the DesignCanvas component uses the information from the DragDropContext to ultimately call the appropriate function (addControl) on the QuestionnaireContext to update the underlying form data.

Looking at the Code

Let's look at how this is implemented using React and the Context API, specifically focusing on the interaction between ControlPalette, DragDropContext, and DesignCanvas.

First, the ControlPalette initiates the drag:

// --- File: src/components/designer/controls/ControlPalette.tsx ---
import React from 'react';
import { useDragDrop } from '../../../contexts/DragDropContext'; // Import the DragDrop hook
import { ControlType } from '../../../types';
// ... icon imports ...

const ControlPalette: React.FC = () => {
  // Use the hook to get access to drag/drop functions
  const { startDrag } = useDragDrop(); 

  // ... control definitions ...

  // This function is called when a palette item starts being dragged
  const handleDragStart = (controlType: ControlType, label: string) => {
    console.log('[ControlPalette] Starting drag for:', controlType);
    // Call startDrag from the context, providing info about the item
    startDrag({
      id: `new-${controlType}-${Date.now()}`, // Temporary ID for the drag operation
      type: 'control', // What general type of item is it? (e.g., 'control', could be 'section', etc.)
      controlType, // <-- The specific type (e.g., ControlType.TextBox)
      isNew: true, // <-- Important flag: this is a new control
      // No sourceId or sourceIndex needed as it's new from the palette
    });
  };

  // ... render functions calling handleDragStart on draggable elements ...
  return (
    <div>
      {/* ... groups and controls ... */}
      {/* Example item: */}
      <div
        draggable // This makes the div a drag source
        onDragStart={() => handleDragStart(ControlType.TextBox, 'Text Box')} // Call our handler on drag start
        className="..."
      >
        {/* ... icon and label ... */}
      </div>
      {/* ... other controls ... */}
    </div>
  );
};

export default ControlPalette;

The ControlPalette uses the useDragDrop hook to access the startDrag function provided by the DragDropContext. When a user starts dragging one of the palette items (because the div has the draggable attribute and an onDragStart handler), handleDragStart is called, which in turn calls startDrag, passing the controlType and setting isNew to true.

Next, the DragDropContext itself manages the state of the drag operation:

// --- File: src/contexts/DragDropContext.tsx ---
import React, { createContext, useContext, useState, ReactNode, useRef } from 'react';
import { ControlType } from '../types';

// Interface defining the data for an item being dragged
interface DragItem {
  id: string; // Unique ID for this drag action (or control ID if existing)
  type: string; // General type of item ('control', 'tab', 'section', etc.)
  controlType: ControlType; // Specific control type being dragged
  sourceId?: string; // ID of the container where the drag started (if existing)
  isNew?: boolean; // Is this a new item from the palette?
  sourceIndex?: number; // Index within the source container (if existing)
}

// Interface defining the context value provided to components
interface DragDropContextType {
  draggedItem: DragItem | null; // The currently dragged item info
  isDragging: boolean; // Simple flag if any drag is active
  startDrag: (item: DragItem) => void; // Function to start a drag
  endDrag: () => void; // Function to end a drag
  canDropIn: (targetType: string) => boolean; // Function to validate drop targets
  getSourceContainer: () => string | null; // Helper to get source container
  getSourceIndex: () => number | null; // Helper to get source index
}

// Create the context
const DragDropContext = createContext<DragDropContextType | undefined>(undefined);

export const DragDropProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
  // State to hold info about the dragged item
  const [draggedItem, setDraggedItem] = useState<DragItem | null>(null);
  const [isDragging, setIsDragging] = useState(false);

  // Refs to store transient info about the drag source (more complex handling in actual code)
  const sourceContainerRef = useRef<string | null>(null);
  const sourceIndexRef = useRef<number | null>(null);

  // Function called by drag sources (like ControlPalette or CanvasControl)
  const startDrag = (item: DragItem) => {
    console.log('[DragDropContext] Starting drag:', item);
    setDraggedItem(item); // Store the item being dragged
    setIsDragging(true); // Set dragging flag

    // Store source info if provided
    sourceContainerRef.current = item.sourceId || 'canvas'; // Default to canvas source if none specified
    sourceIndexRef.current = item.sourceIndex !== undefined ? item.sourceIndex : null;
  };

  // Function called by drop targets when drop is complete
  const endDrag = () => {
    console.log('[DragDropContext] Ending drag.');
    setDraggedItem(null); // Clear the dragged item state
    setIsDragging(false); // Clear dragging flag
    sourceContainerRef.current = null; // Clear source info
    sourceIndexRef.current = null;
  };

  // Function used by potential drop targets to check if the current item can be dropped there
  const canDropIn = (targetType: string): boolean => {
    if (!draggedItem) return false; // Cannot drop if nothing is dragged

    console.log(`[DragDropContext] Checking drop validity: dragged ${draggedItem.controlType} (${draggedItem.isNew ? 'new' : 'existing'}) into ${targetType}`);

    // Example validation logic:
    // Container controls (Tab, Accordion, ColumnLayout) can only be dropped directly on the canvas
    if ([ControlType.Tab, ControlType.Accordion, ControlType.ColumnLayout].includes(draggedItem.controlType)) {
      return targetType === 'canvas';
    }

    // Basic and specialized controls can be dropped on canvas or inside specific containers
    return ['tab', 'column', 'accordion', 'canvas'].includes(targetType);
  };

  // ... getSourceContainer, getSourceIndex helper functions ...

  // Provide the state and functions to wrapped components
  return (
    <DragDropContext.Provider
      value={{
        draggedItem,
        setDraggedItem, // (Sometimes needed for complex scenarios)
        isDragging,
        startDrag,
        endDrag,
        canDropIn,
        getSourceContainer,
        getSourceIndex,
      }}
    >
      {children} {/* The rest of your application */}
    </DragDropContext.Provider>
  );
};

// Hook for components to easily access the context
export const useDragDrop = () => {
  const context = useContext(DragDropContext);
  if (context === undefined) {
    throw new Error('useDragDrop must be used within a DragDropProvider');
  }
  return context;
};

The DragDropProvider wraps part of the application (like the main designer area) and holds the draggedItem and isDragging state using useState. The startDrag function updates this state when a drag begins, and endDrag clears it. The canDropIn function contains the core logic for determining if a drop is allowed based on the draggedItem type and the type of the potential drop target (targetType). Any component wanting to initiate a drag or act as a drop target uses the useDragDrop hook to access these.

Finally, the DesignCanvas component acts as a major drop target and uses the information from the DragDropContext to handle drops:

// --- File: src/components/designer/canvas/DesignCanvas.tsx ---
import React, { useRef, useState } from 'react';
import { useQuestionnaire } from '../../../contexts/QuestionnaireContext'; // Need context to update data
import { useDragDrop } from '../../../contexts/DragDropContext'; // Need context for drag state/validation
import { Control, ControlType } from '../../../types';
import CanvasControl from './CanvasControl'; // Component to render individual controls

const DesignCanvas: React.FC = () => {
  // Get functions from QuestionnaireContext to modify form data
  const { questionnaire, addControl, moveControl, updateQuestionnaireControls } = useQuestionnaire();
  // Get state and functions from DragDropContext for drag/drop logic
  const { draggedItem, canDropIn, endDrag, getSourceContainer, getSourceIndex } = useDragDrop();

  const canvasRef = useRef<HTMLDivElement>(null); // Ref for the canvas element
  const [dragOverIndex, setDragOverIndex] = useState<number | null>(null); // State to track potential drop index

  // Handler for when something is dragged over the canvas
  const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
    // Check if an item is being dragged AND if the canvas is a valid drop target for it
    if (draggedItem && canDropIn('canvas')) {
      e.preventDefault(); // Prevent default browser drag behavior (like opening the item)
      
      // Logic here determines the dragOverIndex based on mouse position
      // and updates state to show visual indicator (e.g., line between controls)
      // ... simplified for example ...
       if (!draggedItem.isNew) { // Only calculate position for existing controls
           // Find where the mouse is relative to controls on the canvas
           // This would involve iterating through rendered controls and checking mouse position
           // For simplicity, imagine this calculates and sets dragOverIndex
           // setDragOverIndex(calculatedIndex); 
       } else {
           // For new items, maybe just highlight the whole canvas or indicate append
           setDragOverIndex(questionnaire.controls.length); // Example: always suggest appending
       }
    }
  };

  // Handler for when something is dropped on the canvas
  const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
    e.preventDefault(); // Prevent default browser drop behavior
    e.stopPropagation(); // Stop event from bubbling up

    console.log('[DesignCanvas] Drop event triggered');

    // Double-check if we have a dragged item and if canvas is valid target
    if (draggedItem && canDropIn('canvas')) {
      try {
        if (draggedItem.isNew) {
          console.log('[DesignCanvas] Dropped a new control:', draggedItem.controlType);
          
          // Prepare the data for the new control
          const controlData: Partial<Control> = {
            type: draggedItem.controlType,
            label: `New ${draggedItem.controlType}`,
            // ... add other default properties based on type ...
          };

          // Call the function from QuestionnaireContext to add the control
          addControl(controlData); 

          // If dragOverIndex was determined, you might move it after adding
          // The actual DesignCanvas code does this:
          // addControl(controlData); // Adds to end
          // const newControlId = questionnaire.controls[questionnaire.controls.length - 1].id;
          // moveControl(newControlId, 'canvas', 'canvas', dragOverIndex); // Then moves it

        } else {
          console.log('[DesignCanvas] Dropped an existing control:', draggedItem.id);
          // This is for reordering existing controls on the canvas

          // Use moveControl from QuestionnaireContext to change its position
          // The actual index logic can be complex depending on nested containers
          // For canvas, it's moving within the top-level 'controls' array
          const sourceContainer = getSourceContainer(); // e.g., 'canvas'
          const sourceIndex = getSourceIndex(); // e.g., 3 (if it was the 4th item)
          const targetContainer = 'canvas'; // Dropping onto the canvas
          const targetIndex = dragOverIndex !== null ? dragOverIndex : questionnaire.controls.length; // Where to drop

          if (sourceContainer && sourceIndex !== null) {
               // Call the moveControl function from QuestionnaireContext
               // The actual implementation in QuestionnaireContext handles the array manipulation
              moveControl(draggedItem.id, sourceContainer, targetContainer, targetIndex);
              console.log(`[DesignCanvas] Requested move control ${draggedItem.id} from ${sourceContainer}[${sourceIndex}] to ${targetContainer}[${targetIndex}]`);
          } else {
             console.warn('[DesignCanvas] Cannot move existing control: Missing source info.');
          }
        }

        // Clear the drag state in the context now that the drop is handled
        setDragOverIndex(null); // Clear visual indicator state
        endDrag(); // Call endDrag in the context
        // ... also clear visual indicators on the UI ...

      } catch (error) {
        console.error('[DesignCanvas] Error in canvas drop:', error);
      }
    }
  };

  // Handler for when the dragged item leaves the canvas area
  const handleDragLeave = () => {
      // Clear the visual drop indicator
      setDragOverIndex(null);
  };

  return (
    // The canvas area is a drop target
    <div 
      ref={canvasRef}
      className={`... ${draggedItem && canDropIn('canvas') ? 'border-blue-400 bg-blue-50' : '...'}`} // Change style if valid drop target
      onDragOver={handleDragOver} // Listen for drag over events
      onDrop={handleDrop} // Listen for drop events
      onDragLeave={handleDragLeave} // Listen for drag leave events
    >
      {/* Render existing controls */}
      <div className="space-y-4">
         {/*
          The DesignCanvas renders the controls from questionnaire.controls.
          Each rendered control element (like CanvasControl) is also made draggable
          using the useDragDrop hook to allow reordering.
          Example rendering loop (simplified):
         */}
        {questionnaire.controls.map((control, index) => (
          <div 
             key={control.id} 
             className={`relative canvas-control-wrapper ${dragOverIndex === index ? 'border-t-2 border-blue-500' : dragOverIndex === index + 1 ? 'border-b-2 border-blue-500' : ''}`}
             draggable // Make existing controls draggable
             onDragStart={(e) => {
                // Start drag for existing control, pass its ID, index, etc.
                startDrag({
                    id: control.id,
                    type: 'control',
                    controlType: control.type,
                    isNew: false,
                    sourceId: 'canvas', // This drag started on the canvas
                    sourceIndex: index, // Its original index
                });
                // ... add visual cues (e.g., hide original element) ...
            }}
             onDragEnd={() => {
                 // Clean up visual cues
                 // ... ensure endDrag() is called after successful drop or cancellation ...
             }}
             // Listen for drag over here too for specific drop positions *between* controls
             onDragOver={(e) => { /* ... calculate dragOverIndex and show line indicator ... */ }}
             onDragLeave={(e) => { /* ... clear line indicator ... */ }}
             // This is the handler for dropping directly ONTO this control,
             // often used for nesting inside containers.
             // The main handleDrop on the canvas div is for dropping *between* controls or onto empty canvas.
             onDrop={(e) => { /* ... handle drops specifically on this control ... */ }}
          >
            <CanvasControl control={control} /> {/* Render the control */}
          </div>
        ))}
      </div>
      {/* ... empty state message ... */}
    </div>
  );
};

export default DesignCanvas;

The DesignCanvas component uses useQuestionnaire to get access to the form data (questionnaire.controls) and the functions to modify it (addControl, moveControl). It also uses useDragDrop to know if something is being dragged (draggedItem) and if the canvas is a valid target (canDropIn). The onDragOver handler provides visual feedback, and the onDrop handler is where the core logic lives: it checks if the draggedItem is new or existing and calls the appropriate QuestionnaireContext function (addControl or moveControl) to update the questionnaire.controls array. Finally, it calls endDrag on the DragDropContext to clean up the drag state. The individual CanvasControl elements within the canvas are also made draggable so users can rearrange them.

Drag and Drop Components in a Nutshell

Component / Concept Role How it Interacts
Drag Source The element being dragged (Palette item or Canvas control). Calls startDrag on DragDropContext, provides item info (id, type, isNew).
Drop Target The area where items can be dropped (Canvas, Container). Listens for dragover (calls canDropIn for visual cues) and drop (gets draggedItem, calls addControl/moveControl on QuestionnaireContext, calls endDrag).
DragDropContext Manages the state of the current drag operation. Stores draggedItem, isDragging. Provides startDrag, endDrag, canDropIn.
QuestionnaireContext Manages the form data (Questionnaire). Receives calls (addControl, moveControl) from drop targets to update the form structure.
Visual Cues Highlights or indicators showing drop possibility/position. Controlled by drop targets based on canDropIn and mouse position.

Conclusion

In this chapter, we explored the Drag and Drop System, the key feature that allows users to visually build and rearrange their forms. We learned how it translates physical mouse actions into meaningful updates to the application's state. Drag sources initiate the process by informing the DragDropContext what is being dragged. Drop targets listen for drag events, use the DragDropContext to validate drops and get dragged item information, and finally, call functions on the QuestionnaireContext to modify the central Questionnaire data, adding or moving Control instances as needed.

Now that you can add and arrange controls on the canvas, you'll naturally want to change their appearance and behavior. How do you change the label of a Text Box, make a Checkbox required, or add options to a Dropdown? This is handled by the Properties Panel, which we'll explore next.

Properties Panel

⚠️ **GitHub.com Fallback** ⚠️