Review My eLearning Blog

Randomizing Drag and Drop Objects in Storyline Using JavaScript

Written by James Kingsley | Oct 24, 2024 9:30:14 PM

One common challenge in Storyline development is randomizing the position of drag and drop objects. While Storyline has built-in randomization for question banks and quiz questions, there isn't a native way to randomize the position of drag and drop objects. This can be particularly challenging when dealing with many drag items - imagine manually repositioning 95 objects every time you want a different arrangement!

In this post, I'll share a JavaScript solution that automatically randomizes the position of drag objects while maintaining their drag and drop functionality.

 

The Problem

A community member recently described this challenge:

"I have a drag and drop quiz with 95 drag options that are then added to a chart. This is all working beautifully and tiling onto the chart very nicely, but if I want to randomize the order that the 95 options appear in (on the drag sequence menu) then I have to manually drag them around to make them appear randomized."

The Solution

We can solve this using JavaScript to:

  1. Identify all draggable objects on the slide
  2. Extract their current positions
  3. Randomly shuffle these positions
  4. Apply the new positions while maintaining all other properties

Implementation Steps

  1. Add the Initial JavaScript
    • In Storyline, go to your slide with drag objects
    • Add an "Execute JavaScript" trigger that runs when the timeline starts
    • Copy and paste this code into the trigger:
 

  window.randomizeDraggablePositions = function() {
    // Get all draggable elements
    const draggables = document.querySelectorAll('.draggable');
    
    // Extract current positions and store elements
    const positions = Array.from(draggables).map(el => {
        console.log(el);
        // Extract transform values using regex
        const transform = el.style.transform;
        const translateMatch = transform.match(/translate\((\d+)px,\s*(\d+)px\)/);
        
        if (!translateMatch) return null;
        
        return {
            element: el,
            x: parseInt(translateMatch[1]),
            y: parseInt(translateMatch[2]),
            // Preserve other transform values
            rotation: (transform.match(/rotate\(([-\d.]+)deg\)/) || ['', '0'])[1],
            scale: (transform.match(/scale\(([-\d.]+),\s*([-\d.]+)\)/) || ['', '1'])[1]
        };
    }).filter(pos => pos !== null);

    // Create array of positions
    const availablePositions = positions.map(pos => ({
        x: pos.x,
        y: pos.y
    }));

    // Shuffle positions array
    for (let i = availablePositions.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [availablePositions[i], availablePositions[j]] = 
        [availablePositions[j], availablePositions[i]];
    }

    // Apply new positions while maintaining other transform properties
    positions.forEach((pos, index) => {
        const newPos = availablePositions[index];
        console.log("newPos", newPos);
        const newTransform = `translate(${newPos.x}px, ${newPos.y}px) rotate(${pos.rotation}deg) scale(${pos.scale}, ${pos.scale})`;
        pos.element.style.transform = newTransform;
    });
}

function initializeDraggable() {
    const draggables = document.querySelectorAll('.draggable');
    
    draggables.forEach(draggable => {
        let offsetX, offsetY;
        let currentX, currentY;
        let rotation, scale;

        function handleDragStart(e) {
            e.preventDefault();
            
            // Extract current transform values
            const transform = draggable.style.transform;
            const translateMatch = transform.match(/translate\((\d+)px,\s*(\d+)px\)/);
            rotation = (transform.match(/rotate\(([-\d.]+)deg\)/) || ['', '0'])[1];
            scale = (transform.match(/scale\(([-\d.]+),\s*([-\d.]+)\)/) || ['', '1'])[1];
            
            if (translateMatch) {
                currentX = parseInt(translateMatch[1]);
                currentY = parseInt(translateMatch[2]);
            }

            // Calculate offset between mouse and element position
            offsetX = e.clientX - currentX;
            offsetY = e.clientY - currentY;

            // Add move and up listeners
            document.addEventListener('mousemove', handleDragMove);
            document.addEventListener('mouseup', handleDragEnd);
            
            // Add grabbing cursor
            draggable.style.cursor = 'grabbing';
        }

        function handleDragMove(e) {
            e.preventDefault();
            
            // Calculate new position based on mouse position minus offset
            const newX = e.clientX - offsetX;
            const newY = e.clientY - offsetY;
            
            // Update element position while maintaining rotation and scale
            draggable.style.transform = `translate(${newX}px, ${newY}px) rotate(${rotation}deg) scale(${scale}, ${scale})`;
        }

        function handleDragEnd() {
            // Remove move and up listeners
            document.removeEventListener('mousemove', handleDragMove);
            document.removeEventListener('mouseup', handleDragEnd);
            
            // Reset cursor
            draggable.style.cursor = 'grab';
        }

        // Add mousedown listener
        draggable.addEventListener('mousedown', handleDragStart);
        
        // Set initial grab cursor
        draggable.style.cursor = 'grab';
    });
}

// Initialize everything
initializeDraggable();
window.randomizeDraggablePositions();
  
  1. Add a Randomize Button (Optional)
    • Add a button to your slide
    • Add an "Execute JavaScript" trigger to the button
    • Put this code in the button's trigger: window.randomizeDraggablePositions()

How It Works

The solution has two main components:

  1. Position Randomization
    • Finds all elements with the 'draggable' class
    • Extracts their current positions from the transform style
    • Creates a shuffled array of these positions
    • Applies the new positions while maintaining rotation and scale
  2. Improved Drag Behavior
    • Ensures objects stay under the cursor while dragging
    • Maintains smooth drag movement
    • Preserves rotation and scale during dragging
    • Adds visual feedback with cursor changes

Key Features

  • Works with any number of drag objects (tested with 95+)
  • Maintains all original properties of drag items
  • Compatible with Storyline's native drag and drop functionality
  • Option to re-randomize positions during runtime
  • Smooth, accurate dragging behavior

Technical Notes

  • The code uses modern JavaScript features but is compatible with Storyline's JavaScript engine
  • All original transform properties (rotation, scale) are preserved during randomization
  • The solution handles edge cases like missing transform values
  • Mouse position is properly tracked for accurate dragging

Limitations and Considerations

  • Applies to objects with the 'draggable' class only
  • Objects should have their initial positions set in Storyline
  • Works best when drag objects are similar in size

Conclusion

This solution provides an efficient way to randomize drag object positions in Storyline, saving significant development time and enabling more dynamic interactions. Whether you're creating assessments, interactive charts, or sorting activities, this code can help create more engaging and varied experiences for your learners.

Remember to test thoroughly with your specific use case, as drag and drop behaviors can be complex and may need adjustments based on your particular requirements.

Questions?

Feel free to reach out if you need help implementing this solution or run into any issues!