Jump to content
Search Community

draggable within a draggable

mallanaga test
Moderator Tag

Recommended Posts

The example is contrived... trying to imitate a pub/sub system where there are several clients all receiving instructions to move a card to a new position, and potentially append that card to a new parent. I think the loop captured that appropriately.

 

This is the real code. Event Listener receives the event:

// subscription.js

eventSource.addEventListener("positionCard", (e) => {
  const message = JSON.parse(e.data);
  const target = document.getElementById(message.id);
  const oldParent = document.getElementById(message.oldParentId);
  const dropArea = document.getElementById(message.dropAreaId);

  const a = dropArea.getBoundingClientRect();
  const b = oldParent.getBoundingClientRect();

  cardAnimations.move({
    target,
    dropArea,
    position: {
      x: parseFloat(message.x) + (b.left - a.left),
      y: parseFloat(message.y) + (b.top - a.top),
      r: parseFloat(message.r)
    }
  });
});

 

// cardAnimations.js

const move = ({ target, dropArea, position }) => {
  dropArea.append(target);

  let x, y, rotation;
  if(dropArea.id !== 'table') {
    x = `random(-10, 10, 1)`;
    y = `random(-10, 10, 1)`;
    rotation = `random(-20, 20, 1)_short`;
  } else {
    x = position.x || 0;
    y = position.y || 0;
    rotation = `${(position.r || 0)}_short`;
  }

  gsap.to(target, {x, y, duration: 0.3});
  gsap.to(target.querySelector('.Card'), {rotation, duration: 0.3});
  Draggable.get(target).update(true, true);
};

I'll keep poking at it. Sorry to bother. And thanks for the tip about the border! I'll see if I can pull that up.

Link to comment
Share on other sites

Thanks for the knowledge, Blake! That's super helpful. I'm not an animator, lol, but I'm slowly becoming one.

 

Regarding the update. The clients won't have the benefit of an onPress event. Won't they need a "manual" call to update? or would the tween be sufficient?

Link to comment
Share on other sites

59 minutes ago, mallanaga said:

Regarding the update. The clients won't have the benefit of an onPress event. Won't they need a "manual" call to update? or would the tween be sufficient?

 

No. A client only needs to call update when they start dragging. It doesn't need to happen on every client.

 

I had a real-time dragging demo up on CodePen, but it looks like my Firebase account is all screwed up.

 

See the Pen gPeeJN by osublake (@osublake) on CodePen

 

But you can see how I handle the dragging and animation in the MagnetController class. Every draggable was an instance of the MagnetController class. The onChange event would fire when something changed, like dragging the magnet.

 

class MagnetController {
    
    active  = false;
    time    = 0;
    board   = document.querySelector(".board");
    data    = this.firebaseData.magnet(this.magnetId);      
    unwatch = this.data.$watch(this.onChange.bind(this));

    draggable = null;

    constructor(
      private $element, 
      private $scope, 
      private $timeout, 
      private firebaseData) {
                       
      this.data.$loaded(event => {
        
        if (!this.data.content || !this.data.zIndex) {
          TweenLite.set($element, { autoAlpha: 0 });
          this.remove();
          return;
        }
        
        this.init();
      });
      
      this.data.$ref().onDisconnect().update({ locked: false });
    }

    init() {
      
      this.draggable = new Draggable(this.$element, {
        onDrag: this.onDrag,
        onPress: this.onPress,
        onRelease: this.onRelease,
        zIndexBoost: false,
        callbackScope: this,
        liveSnap: {
          x: n => this.active ? n : this.data.x,
          y: n => this.active ? n : this.data.y
        }      
      });
    }

    onChange(event) {
           
      var config = {
        rotation : this.data.rotation,
        zIndex   : this.data.zIndex || Draggable.zIndex
      };

      if (!this.active) {
        config.x = this.data.x;
        config.y = this.data.y;
      }
      
      TweenLite.to(this.$element, this.time, config);
      
      this.time = 0.07;
    }

    onPress(event) {
      
      if (this.data.locked) return;
      
      this.draggable.update();      
      this.updateDraggable();
      this.active = true;
      this.data.locked = true;
      this.data.zIndex = Draggable.zIndex++;
      this.data.$save(); 
    }

    onRelease(event) {
            
      if (!this.draggable.hitTest(this.board)) {
        this.remove();
        return;
      }
      
      if (!this.active) return;
            
      this.active = false;
      this.data.locked = false;
      this.data.$save(); 
    }

    onDrag(event) {
      
      if (!this.active) return;
      
      this.data.x = this.draggable.x;
      this.data.y = this.draggable.y;
      this.data.$save();
    }

    remove() {
      
      this.draggable && this.draggable.kill();
      this.unwatch();
      this.data.$remove().then(ref => {        
        this.$scope.$destroy();
        this.updateBoard();
      }, error => {
        console.log(error);
      });
    }
  }

 

 

  • Like 2
Link to comment
Share on other sites

const move = ({ target, dropArea, position }) => {
  // First - record start position
  let first = { x: 0, y: 0 };
  let rect = target.getBoundingClientRect();
  first.x = rect.left;
  first.y = rect.top;

  // State change, render new DOM
  dropArea.append(target);

  // Last - record new position
  let last = { x: 0, y: 0 };
  rect = target.getBoundingClientRect();
  last.x = rect.left;
  last.y = rect.top;

  // Invert - animate from first position
  let x, y, rotation;
  if(dropArea.id !== 'table') {
    x = `random(-10, 10, 1)`;
    y = `random(-10, 10, 1)`;
    rotation = `random(-20, 20, 1)_short`;
  } else {
    x = position.x + first.x - last.x;
    y = position.y + first.y - last.y ;
    rotation = `${position.r}_short`;
  }

  gsap.to(target, {x, y, duration: 0.3});
  gsap.to(target.querySelector('.Card'), {rotation, duration: 0.3});
};

So that's pretty clean, and I was able to move more of the code into that last function. This is great. No need to send oldParent now. Nice. So, this still jumps, unfortunately - BUT - I feel like I'm honing in on the culprit. I move the "Draw" area to the middle of the table on page load. The "jump" seems to be relative to the position of the Draw area on the table. When I don't move the Draw area, and keep it at 0,0, the animation looks perfect. As soon as I move it, it jumps again. Is there a proper way to move that element? I'm using an inline style, that's calculated on the fly. I'm not positive, but this feels like it might be part of the issue. I tried calling .update() on the Draw area when it was in the middle, and it actually moved to 0,0. 

Link to comment
Share on other sites

4 hours ago, mallanaga said:

I feel like I'm honing in on the culprit. I move the "Draw" area to the middle of the table on page load.

 

Does that need to be draggable? If every client can move that around, then I think you're going to have to add in some more logic as it's going offset stuff. If it's stationary, then it will be much easier.

 

 

Link to comment
Share on other sites

4 hours ago, mallanaga said:

As soon as I move it, it jumps again.

 

When you move the area and then drag the card to the table, sending just the position of the card would be incorrect. The position on the table would be the area x/y + the card x/y. 

 

If needed, you can grab the x and y values of any element using gsap.getProperty().

https://greensock.com/docs/v3/GSAP/gsap.getProperty()

 

If you're moving the card from the table to another part of the table, then sending just card's x/y would be ok. There would be no need to do any sort of calculation. Just tween to the new position.

 

  • Like 1
Link to comment
Share on other sites

Blake! Your command of javascript is incredible. I can't thank you enough. That works a treat, and I was able to massage it into place within my app. Great example using window.dispatchEvent to more closely match my use case! 

 

Can you confirm the expected behavior or gsap.getProperty, though? It behaves as expected when the Area starts at 0,0, and I drag it around. If I position the element manually, though, it goes back to jumping. It looks like it's grabbing the offset that the element has moved, and not necessarily the element's absolute position within another element. Seems like I'll want to use getBoundingClientRect here, unless there's another gsap convenience method available?

 

Again... you're a life saver. This one problem stalled the development on my project for weeks. I owe you!

  • Like 1
Link to comment
Share on other sites

2 hours ago, mallanaga said:

If I position the element manually, though, it goes back to jumping

 

How are you positioning it?

 

gsap.getProperty() returns the transform: translate values, which would be different if you were setting the position with left/top. 

 

The transform can be set in your CSS, inline, or with gsap. If you want to use to left/top, then this would need to change.

 

const dx = cardX + (parentX - gsap.getProperty(newParent, "x"));
const dy = cardY + (parentY - gsap.getProperty(newParent, "y"));

 

To something like this.

const a = newParent.getBoundingClientRect();
const b = oldParent.getBoundingClientRect();

const dx = cardX + (b.left - a.left);
const dy = cardY + (b.top - a.top);

 

 

  • Like 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...