Search code examples
sveltedraggableinteractjssvelte-5

How to drag a clone with Svelte5 and interact.js


I'm using interact.js and Svelte 5 (version 5.0.0-next.200) to drag an element which initially sits inside a scrollable div and must be dropped outside of that div. To make this work I need to clone the element and then drag the clone (as far as I know, there is no way around).

Now, in a former Vanilla-js project I successfully adapted the code from this stackoverflow-answer and everything worked fine. However, I can't make it work in Svelte.

Below is my code. I assume the error is in interaction.start({ name: 'drag' }, ev.interactable, clone);, which fails to pass the drag-action on to the clone.

Any help is warmly welcome. I'm struggling with this since days.

<script>
    import interact from 'interactjs';

    const dragElement = (ev) => {
        const el = ev.target;
        let x = (parseFloat(el.getAttribute('data-x')) || 0) + ev.dx;
        let y = (parseFloat(el.getAttribute('data-y')) || 0) + ev.dy;
        el.style.webkitTransform = el.style.transform = `translate(${x}px,${y}px)`;
        el.setAttribute('data-x', x);
        el.setAttribute('data-y', y);
    }

    const makeDraggable = (DRAG) => {
        const draggable = document.querySelector('.draggable');
        interact(draggable)
            .draggable({
                allowFrom: '*',
                inertia: false,
                autoScroll: false,
                modifiers: [
                    interact.modifiers.restrict({
                        restriction: '#outer',
                        endOnly: true
                    })
                ],
                onstart: (ev) => {
                    console.log('DRAG START of ', ev.target);
                },

                onmove: (ev) => {
                    console.log('DRAG MOVE of ', ev.target);
                    dragElement(ev);
                },

                onend: (ev) => {
                    console.log('DRAG END of ', ev.target);
                    DRAG.node.orig.style.opacity = 100; // show original again after drag
                }
            })
            .on('move', (ev) => {
                const el = ev.currentTarget;
                const interaction = ev.interaction;
                console.log('ON.MOVE TRY of ', el);
                if (
                    interaction.pointerIsDown && // Only on active interaction
                    interaction.interacting() // Prevent activation by swiping through element
                ) {
                    // If ORIG then make a clone
                    if (
                        DRAG.active == false && // Create only one clone
                        el.getAttribute('isClone') == 'orig' // Prevent re-cloning clones
                    ) {
                        DRAG.active = true;
                        console.log('CLONING START');

                        // Clone node and set its position
                        let clone = el.cloneNode(true);
                        clone.style.left = el.offsetLeft + 'px';
                        clone.style.top = el.offsetTop + 'px';
                        clone.style.position = 'absolute';

                        clone.setAttribute('isClone', 'clone');
                        clone.style.backgroundColor = 'grey';

                        // Append Clone and start drag interaction
                        document.querySelector('#outer').appendChild(clone);
                        console.log('CLONE IS READY: ', clone);

                        DRAG.node.orig = el;
                        DRAG.node.clone = clone;
                        DRAG.node.orig.style.opacity = 0; // hide original while dragging clone
                        interaction.start({ name: 'drag' }, ev.interactable, clone);

                        // if CLONE then drag
                        // } else if (el.getAttribute('isClone') == 'clone') {
                        //  // console.log('MOVING THE CLONE START');
                        //  // dragElement(ev);
                        // }
                    }
                }
            });
    };

    ///////////////////////////////////////////////////////////////
    // DRAG State
    let DRAG = $state({
        active: false,
        node: {
            orig: undefined,
            clone: undefined
        }
    });

    ///////////////////////////////////////////////////////////////
    // Handle drag
    const handleDrag = () => {
        makeDraggable(DRAG);
    };
</script>

<div id="outer">
    <h1>outer</h1>
    <div id="inner">
        <h1>inner (scrollable)</h1>
        <div class="draggable" isClone="orig" use:handleDrag>Drag me</div>
    </div>
</div>

<style>
    #outer {
        position: absolute;
        background-color: yellow;
        height: 95vh;
        width: 90vw;
    }

    #inner {
        position: absolute;
        height: 400px;
        width: 100%;
        background: lightblue;
        overflow-y: scroll;
    }

    .draggable {
        position: absolute;
        background-color: black;
        color: white;
        height: 500px;
        width: 150px;
    }
</style>

Solution

  • Adding interaction.stop(); before interaction.start(...); allows you to move the clone.

    Note that Svelte 5 is sensitive to DOM structure manipulations - it is already incompatible with some drag-n-drop libs.