Search code examples
javascriptjquerydrag-and-droprotationinteract.js

Drag and drop with interact.js


I am trying to build an interface that allows both resize/drag and rotate on some element and to achieve this I am using interact.js javascript library.

I have my interact functions working:

    interact('.resize-drag-ratio')
      .draggable({
        onmove: window.dragMoveListener
      })
      .resizable({
        preserveAspectRatio: true,
        edges: { left: true, right: true, bottom: true, top: true }
      })
      .on('resizemove', function (event) {
        var target = event.target,
            x = (parseFloat(target.getAttribute('data-x')) || 0),
            y = (parseFloat(target.getAttribute('data-y')) || 0);

        var min_size = 35;

        if(event.rect.width>min_size){
            // update the element's style
            target.style.width  = event.rect.width + 'px';
            target.style.height = event.rect.height + 'px';


            // translate when resizing from top or left edges
            x += event.deltaRect.left;
            y += event.deltaRect.top;

            target.style.webkitTransform = target.style.transform =
                'translate(' + x + 'px,' + y + 'px)';

            target.setAttribute('data-x', x);
            target.setAttribute('data-y', y);

            }
      });

and drag-rotate that allows for rotation

    interact('.drag-rotate')
      .draggable({
      onstart: function (event) {
        const element = event.target;
        const rect = element.getBoundingClientRect();
        // store the center as the element has css `transform-origin: center center`
        element.dataset.centerX = rect.left + rect.width / 2;
        element.dataset.centerY = rect.top + rect.height / 2;

        console.log("element.dataset.centerX: "+element.dataset.centerX);
        console.log("element.dataset.centerY: "+element.dataset.centerY);

        // get the angle of the element when the drag starts
        element.dataset.angle = getDragAngle(event);
      },
      onmove: function (event) {
        var element = event.target;
        var center = {
          x: 300,
          y: 300,
        };

        console.log("element.dataset.centerX: "+element.dataset.centerX);
        console.log("element.dataset.centerY: "+element.dataset.centerY);

        var angle = getDragAngle(event);

        // update transform style on dragmove
      element.style.transform = 'rotate(' + angle + 'rad' + ')';
      },
      onend: function (event) {
        const element = event.target;

        // save the angle on dragend
        element.dataset.angle = getDragAngle(event);
      },
    })

The two classes get switched using jQuery thus turning drag into rotation and vice versa.

My problem is that object location and rotation angle do not stay as placed and I am not sure how to fix that.

After i drag an element to some position and press the rotate button, one I start rotating the element it moves to top:0px left:0px and does not stay at its dragged position.

you can see the full working code right here: https://codepen.io/yaary-vidanpeled/pen/ZZwGmE


Solution

  • The Issue:

    This is happening because each time you apply css, you are overwriting your previous styles.

    Here is an example, let's say you have a text ( #text element ) with color of red, now you want to change it with JavaScript.

    document.getElementById('text').style.color = 'green';
    

    what actually happened here ? whatever was assigned as the color property of the style object is now overwritten.

    The same thing is happening when you are writing (in your interact initialisation of .resize-drag-ratio):

    target.style.transform = 'translate(' + x + 'px,' + y + 'px)';
    

    And overwriting the translate again by writing (in your interact initialisation of .drag-rotate)

    element.style.transform = 'rotate(' + angle + 'rad' + ')';
    

    Remember that the rotate() and translate() both are values of the css translate property.

    The Solution:

    You should somehow preserve the all these rotation angle, and translate values. (Looks like you already have data-attribtues for them so it wont be hard)

    And apply the value of the element.style.transform as following:

    target.style.transform = 'translate(' + x + 'px,' + y + 'px) rotate(' + angle + 'rad)';
    

    Note: Your snippet has the function dragMoveListener(event) { declared twice.

    Working Snippet:

    console.log('start');
    
    //function isEven
    function isEven(n) {
      return n == parseFloat(n) ? !(n % 2) : void 0;
    }
    
    
    interact('.resize-drag-ratio')
      .draggable({
        onmove: window.dragMoveListener
      })
      .resizable({
        preserveAspectRatio: true,
        edges: {
          left: true,
          right: true,
          bottom: true,
          top: true
        }
      })
      .on('resizemove', function(event) {
        var target = event.target,
          x = (parseFloat(target.getAttribute('data-x')) || 0),
          y = (parseFloat(target.getAttribute('data-y')) || 0);
    
        rotation = (parseFloat(target.getAttribute('data-angle')) || 0)
    
        var min_size = 35;
    
        if (event.rect.width > min_size) {
          // update the element's style
          target.style.width = event.rect.width + 'px';
          target.style.height = event.rect.height + 'px';
    
    
          // translate when resizing from top or left edges
          x += event.deltaRect.left;
          y += event.deltaRect.top;
    
          target.style.webkitTransform = target.style.transform =
            'translate(' + x + 'px,' + y + 'px) rotate(' + rotation + 'rad)';
    
          target.setAttribute('data-x', x);
          target.setAttribute('data-y', y);
    
        }
      });
    
    
    interact('.resize-drag')
      .draggable({
        onmove: window.dragMoveListener
      })
      .resizable({
        preserveAspectRatio: false,
        edges: {
          left: true,
          right: true,
          bottom: true,
          top: true
        }
      })
      .on('resizemove', function(event) {
        var target = event.target,
          x = (parseFloat(target.getAttribute('data-x')) || 0),
          y = (parseFloat(target.getAttribute('data-y')) || 0),
    
          rotation = (parseFloat(target.getAttribute('data-angle')) || 0)
    
        //console.log("event.rect.width: "+event.rect.width);
    
        //prevents resizing to units smaller then 35px
        var min_size = 35;
    
        if (event.rect.width > min_size) {
          // update the element's style
          target.style.width = event.rect.width + 'px';
          target.style.height = event.rect.height + 'px';
    
          //$("#form_bubble_width").val(event.rect.width);
          //$("#form_bubble_width").val(event.rect.height);
    
          // translate when resizing from top or left edges
          x += event.deltaRect.left;
          y += event.deltaRect.top;
    
          target.style.webkitTransform = target.style.transform =
            'translate(' + x + 'px,' + y + 'px) rotate(' + rotation + 'rad)';
    
          target.setAttribute('data-x', x);
          target.setAttribute('data-y', y);
          //target.textContent = event.rect.width + '×' + event.rect.height;
        }
      });
    
    
    
    
    // target elements with the "draggable" class
    interact('.draggable')
      .draggable({
        // enable inertial throwing
        inertia: true,
        // keep the element within the area of it's parent
        restrict: {
          restriction: "parent",
          endOnly: true,
          elementRect: {
            top: 0,
            left: 0,
            bottom: 1,
            right: 1
          }
        },
        // enable autoScroll
        autoScroll: true,
    
        // call this function on every dragmove event
        onmove: dragMoveListener,
        // call this function on every dragend event
        onend: function(event) {
    
          // var textEl = event.target.querySelector('p');
    
          console.log(event.target.id)
    
          var distance = (Math.sqrt(Math.pow(event.pageX - event.x0, 2) +
              Math.pow(event.pageY - event.y0, 2) | 0))
            .toFixed(2) + 'px';
    
        }
    
      });
    
    
    interact('.resize-drag , .resize-drag-ratio').on('tap', function(event) {
      event.preventDefault();
      var target = event.target
      console.log("tap resize-drag class element");
    
      var uuid = target.id;
      //console.log("uuid: "+uuid);
      console.log("click");
    });
    
    
    function dragMoveListener(event) {
      var target = event.target,
        // keep the dragged position in the data-x/data-y attributes
        x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx,
        y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy,
    
        rotation = (parseFloat(target.getAttribute('data-angle')) || 0);
    
      // translate the element
      target.style.webkitTransform =
        target.style.transform =
        'translate(' + x + 'px, ' + y + 'px) rotate(' + rotation + 'rad)';
    
      // update the posiion attributes
      target.setAttribute('data-x', x);
      target.setAttribute('data-y', y);
      target.setAttribute('data-angle', rotation);
    }
    
    // this is used later in the resizing and gesture demos
    window.dragMoveListener = dragMoveListener;
    
    
    var mouseX = 0,
      mouseY = 0
    
    
    //function onMousemove(e)
    function onMousemove(e) {
      var m_posx = 0,
        m_posy = 0,
        e_posx = 0,
        e_posy = 0,
        obj = this;
      //get mouse position on document crossbrowser
      if (!e) {
        e = window.event;
      }
      if (e.pageX || e.pageY) {
        m_posx = e.pageX;
        m_posy = e.pageY;
      } else if (e.clientX || e.clientY) {
        m_posx = e.clientX + document.body.scrollLeft +
          document.documentElement.scrollLeft;
        m_posy = e.clientY + document.body.scrollTop +
          document.documentElement.scrollTop;
      }
      //get parent element position in document
      if (obj.offsetParent) {
        do {
          e_posx += obj.offsetLeft;
          e_posy += obj.offsetTop;
        } while (obj = obj.offsetParent);
      }
      // mouse position minus elm position is mouseposition relative to element:
      dbg.innerHTML = ' X Position: ' + (m_posx - e_posx) +
        ' Y Position: ' + (m_posy - e_posy);
    
      mouseX = (m_posx - e_posx);
      mouseY = (m_posy - e_posy);
    
    }
    
    var elem = document.getElementById('container');
    //elem.addEventListener('mousemove', onMousemove, false);
    
    var dbg = document.getElementById('dbg'); //just for debug div instead of console
    
    $(document).ready(function() {
      var is_rotate = true;
    
      $("#btn_rotate").click(function() {
        // console.log('ddd');
        if (is_rotate) {
          $(this).text('drag-resize');
          $(".element").removeClass("drag-rotate");
          $(".element").addClass("resize-drag-ratio");
          is_rotate = false;
        } else {
          $(this).text('rotate');
          $(".element").removeClass("resize-drag-ratio");
          $(".element").addClass("drag-rotate");
          is_rotate = true;
        }
        //console.log('click: '+is_rotate);
    
      });
    
    
      var saved_mouseX = 0;
      var saved_mouseY = 0;
    
      //interact("#container").on('tap', function (event) {
      interact("#container").on('tap', function(event) {
        event.preventDefault();
        var target = event.target
        if (target.id == "tp_image") {
          console.log(target.id);
          console.log(mouseX + "-" + mouseY);
    
          saved_mouseX = mouseX;
          saved_mouseY = mouseY;
    
          //$('#modal_stickers').modal('show');
        }
      });
    
    
      //interact('.drag-rotate')
      interact('.drag-rotate')
        .draggable({
          onstart: function(event) {
            const element = event.target;
            const rect = element.getBoundingClientRect();
            // store the center as the element has css `transform-origin: center center`
            element.dataset.centerX = rect.left + rect.width / 2;
            element.dataset.centerY = rect.top + rect.height / 2;
    
            // console.log("element.dataset.centerX: " + element.dataset.centerX);
            // console.log("element.dataset.centerY: " + element.dataset.centerY);
    
            // get the angle of the element when the drag starts
            element.dataset.angle = getDragAngle(event);
          },
          onmove: function(event) {
            var element = event.target;
    
            var center = {
              x: 300,
              y: 300,
            };
    
            // console.log("element.dataset.centerX: " + element.dataset.centerX);
            // console.log("element.dataset.centerY: " + element.dataset.centerY);
    
            var angle = getDragAngle(event);
            var x = element.dataset.x;
            var y = element.dataset.y;
    
            // update transform style on dragmove
            // this is where the bug was; at initial point, there was no x, or y position set on the dataset of the element. thus your style value would be undefined, so here we check the values of x and y first and set the style accordingly;
            if (typeof x != 'undefined' && typeof y != 'undefined') {
              element.style.transform = 'translate(' + x + 'px, ' + y + 'px) rotate(' + angle + 'rad' + ')';
            } else {
              element.style.transform = 'rotate(' + angle + 'rad' + ')';
            }
    
          },
          onend: function(event) {
            const element = event.target;
    
            // save the angle on dragend
            element.dataset.angle = getDragAngle(event);
          },
        })
    
    
      //function getDragAngle(event)
      function getDragAngle(event) {
        var element = event.target;
        var startAngle = parseFloat(element.dataset.angle) || 0;
        var center = {
          x: parseFloat(element.dataset.centerX) || 0,
          y: parseFloat(element.dataset.centerY) || 0,
        };
        var angle = Math.atan2(center.y - event.clientY,
          center.x - event.clientX);
    
        return angle - startAngle;
      }
    
    });
    #btn_rotate {
      position: absolute;
      top: 0;
      left: 0;
      cursor: pointer;
      background: #ccc;
      padding: 30px;
    }
    
    .element {
      width: 25%;
      min-height: 6.5em;
      margin: 10%;
      background-color: #29e;
      color: white;
      /* added later */
      touch-action: none;
      box-sizing: border-box;
    }
    <script type="text/javascript" src="https://code.jquery.com/jquery-3.4.0.js"></script>
    <script type="text/javascript" src="https://unpkg.com/interactjs@next/dist/interact.js"></script>
    
    
    
    
    <div class="element drag-rotate">
      <p> drag to rotate</p>
    </div>
    
    <div id="btn_rotate">rotate
    </div>

    Check line 288 on the script part. there is a comment explaining about the if block and why this was happening.