Jump to content
Search Community

startDrag won't trigger

Lark test
Moderator Tag

Go to solution Solved by GreenSock,

Recommended Posts

Hello friends.

I'm making an annotation tool as a personal projcet. I made it once before and you guys were great help, but now I'm rewriting the code I have and one thing doesn't work: when I trigger startDrag on a draggable it doesn't catch.

I expect that, when the mouse is still pressed, the same mouse event would start dragging . The logic is there and it worked before and I must have broken it so I now ask for you help.

 

I couldn't possibly reproduce this in a sandbox but I'm happily sharing my repo (just clone, yarn and yarn dev).

https://github.com/LarkRanger/beyond-annotations

The lines in question are in the file

src/components/annotation/pieces/resize/Resize.tsx

lines 171-174.

The code definitely reaches there, but startDrag just won't trigger (or perhaps, it does but then immediately breaks).

 

You can reproduce the issue by uploading any image, then hitting "Annotate" and clicking and dragging. What should happen is that the box appears and drags in the same event, but it doesn't drag and you end up clicking it again.

 

I would greatly appreciate it if anyone reads this, it's quite hefty but I'm truely stuck. ❤️

Link to comment
Share on other sites

Hi,

 

I see that you're using React 18.x, since that version React started calling the useEffect/useLayoutEffect hooks twice on development, which was reported here:

For using GSAP in React and it's environment we strongly recommend using GSAP Context even though you're properly doing manual cleanup in your useEffect hook:

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

 

Beyond that the issue stems from the fact that a Draggable instance is actually an array, so you have to access the first element in such array:

const d = Draggable.create(myElement);

console.log(d.startDrag); // -> undefined
console.log(d[0].startDrag); // -> f(e,t){...}

https://greensock.com/docs/v3/Plugins/Draggable/static.create()

 

Hopefully this clear things up.

Happy Tweening!

  • Like 1
Link to comment
Share on other sites

Thank you for checking this out!

I actually did not change this part of code from the version I'm trying to recreate, which means this definitely worked in the past. Was this changed to an array recently (i.e. in the past year)?

Also, if what you're saying is true, how come calling startDrag doesn't throw a "not a function" error?

Link to comment
Share on other sites

Hi,

 

Actually I’m getting this error in the console:

TypeError: d.startDrag is not a function. (In 'd.startDrag(e)', 'd.startDrag' is undefined)

I don’t know what to tell you, this shows that the draggable instance does not has a startDrag method. On top of that the docs clearly states that the create method returns an array, which makes total sense for the error the console displays.

 

Happy Tweening!

Link to comment
Share on other sites

13 hours ago, Lark said:

Was this changed to an array recently (i.e. in the past year)?

No, it has been that way for many years. As far as I can remember, there was never a time that Draggable.create() returned a Draggable instance instead of an Array. That's because someone might do something like Draggable.create(".box") when they've got 20 elements with the "box" class applied, so the API must be able to handle that properly and create a Draggable instance for each one. 

 

I bet your code in the past was doing new Draggable(...) instead of Draggable.create(...)

 

11 hours ago, Lark said:

I'm very confused, in this codepen d is also not an array. How does this align with your first comment?

I didn't understand your comment. In Rodrigo's CodePen, d is definitely an Array. Can you explain why you think it's not? Maybe I'm missing something obvious. 

 

I would strongly recommend following Rodrigo's advice about doing proper cleanup in React. gsap.context() is your new best friend because it makes it so easy.  This is a must-read for anyone using React: 

 

 

I wonder if you're inadvertently creating multiple conflicting instances due to the lack of cleanup. Maybe try putting a console.log() in your useEffect()/useLayoutEffect() to see if it's firing more than once. Well, that won't matter if you're doing proper cleanup, but it may just help clarify the issue. 

Link to comment
Share on other sites

Well, in the codepen, the lines are:

const d = Draggable.create(box); --> presumably an array

container.addEventListener("click", (e) => {
  d.startDrag(e); --> calling d instead of d[0]
});

It's clear that something isn't adding up, if d was an array it should have been called as d[0], no?

I am also not using Draggable.create, I'm using new Draggable. Could this have something to do with it?

Link to comment
Share on other sites

I'm sad to say that even with the context this issue does not go away in development.

  useLayoutEffect(() => {
    const ctx = gsap.context(() => {
      const gId = `#${elementIds.getGroupId(annotation.id)}`;
      const getDisplay = () =>
        document.getElementsByClassName(elementIds.transformWrapper)[0]!;
      const getDisplayBounds = () => getDisplay().getBoundingClientRect();
      const getBox = () =>
        document.getElementById(elementIds.getRectId(annotation.id))!;
      const getBoxBounds = () => getBox().getBoundingClientRect();
      const getWrapper = () =>
        document.getElementById(elementIds.imageWrapper)!;
      const getWrapperBounds = () => getWrapper().getBoundingClientRect();
      const updateBox = () =>
        gsap.set(`${gId}`, {
          x: annotation.box.x,
          y: annotation.box.y,
        });

      const $right = document.createElement('div');
      const $top = document.createElement('div');
      const $bottom = document.createElement('div');
      const $left = document.createElement('div');

      const topDraggable = new Draggable($top, {
        trigger: `${gId} .top, ${gId} .topRight, ${gId} .topLeft`,
        onDrag(event) {
          const scale = annotation.scale;
          const { top, bottom, height } = getBoxBounds();
          const { top: wTop } = getWrapperBounds();
          const { top: dTop } = getDisplayBounds();
          const mouse = Math.max(event.clientY, dTop, wTop);

          if (mouse >= bottom) {
            const diff = (mouse - bottom) / scale;
            const relativeBottom = annotation.box.y + annotation.box.height;
            annotation.box = { y: relativeBottom, height: diff };
            topDraggable.endDrag(event);
            bottomDraggable.startDrag(event);
          } else {
            const diff = (top - mouse) / scale;
            const relativeTop = annotation.box.y;
            annotation.box = {
              height: height / scale + diff,
              y: relativeTop - diff,
            };
          }
        },
        onPress() {
          annotation.draggable.disable();
        },
        onRelease() {
          annotation.draggable.enable();
          updateBox();
          annotation.draggable.update();
        },
      });

      const bottomDraggable = new Draggable($bottom, {
        trigger: `${gId} .bottom, ${gId} .bottomRight, ${gId} .bottomLeft`,
        onDrag(event) {
          const scale = annotation.scale;
          const { top, bottom, height } = getBoxBounds();
          const { top: wTop, height: wHeight } = getWrapperBounds();
          const { top: dTop, height: dHeight } = getDisplayBounds();
          const mouse = Math.min(event.clientY, dTop + dHeight, wTop + wHeight);

          if (mouse <= top) {
            const diff = (top - mouse) / scale;
            const relativeTop = annotation.box.y;
            annotation.box = { y: relativeTop - diff, height: diff };
            bottomDraggable.endDrag(event);
            topDraggable.startDrag(event);
          } else {
            const diff = (mouse - bottom) / scale;
            annotation.box = { height: height / scale + diff };
          }
        },
        onPress() {
          annotation.draggable.disable();
        },
        onRelease() {
          annotation.draggable.enable();
          updateBox();
          annotation.draggable.update();
        },
      });

      const rightDraggable = new Draggable($right, {
        trigger: `${gId} .right, ${gId} .topRight, ${gId} .bottomRight`,
        onDrag(event: PointerEvent) {
          const scale = annotation.scale;
          const { left, right, width } = getBoxBounds();
          const { left: wLeft, width: wWidth } = getWrapperBounds();
          const { left: dLeft, width: dWidth } = getDisplayBounds();
          const mouse = Math.min(event.clientX, dWidth + dLeft, wWidth + wLeft);

          if (mouse <= left) {
            const diff = (left - mouse) / scale;
            const relativeLeft = annotation.box.x;
            annotation.box = { x: relativeLeft - diff, width: diff };
            rightDraggable.endDrag(event);
            leftDraggable.startDrag(event);
          } else {
            const diff = (mouse - right) / scale;
            annotation.box = { width: width / scale + diff };
          }
        },
        onPress() {
          annotation.draggable.disable();
        },
        onRelease() {
          annotation.draggable.enable();
          updateBox();
          annotation.draggable.update();
        },
      });

      const leftDraggable = new Draggable($left, {
        trigger: `${gId} .left, ${gId} .bottomLeft, ${gId} .topLeft`,
        onDrag(event: PointerEvent) {
          const scale = annotation.scale;
          const { left, right, width } = getBoxBounds();
          const { left: wLeft } = getWrapperBounds();
          const { left: dLeft } = getDisplayBounds();
          const mouse = Math.max(event.clientX, dLeft, wLeft);

          if (mouse >= right) {
            const diff = (mouse - right) / scale;
            const relativeRight = annotation.box.x + annotation.box.width;
            annotation.box = { x: relativeRight, width: diff };
            leftDraggable.endDrag(event);
            rightDraggable.startDrag(event);
          } else {
            const diff = (mouse - left) / scale;
            const relativeLeft = annotation.box.x;
            annotation.box = {
              width: width / scale - diff,
              x: relativeLeft + diff,
            };
          }
        },
        onPress() {
          annotation.draggable.disable();
        },
        onRelease() {
          annotation.draggable.enable();
          updateBox();
          annotation.draggable.update();
        },
      });

      reaction(
        () => annotation.creationEvent,
        creationEvent => {
          if (creationEvent) {
            annotation.select();
            topDraggable.startDrag(creationEvent);
            leftDraggable.startDrag(creationEvent);
            annotation.setCreationEvent();
          }
        },
      );
    });

    return () => ctx.revert();
  }, []);

In production (after build) everything works fine (startDrag work as expected) but something must be breaking here in development. While this isn't a huge issue, I would still like to understand how to remedy this.

Link to comment
Share on other sites

Are you saying it only breaks when you deploy it and use a different build process or something? 

 

I should have clarified that the current version of Draggable isn't context-aware (meaning it doesn't automatically get killed when you ctx.revert()), so you can just call kill() on your Draggable instances inside your cleanup function. The upcoming release of Draggable IS context-aware, so that'll happen automatically. 

 

Maybe try calling kill() on each Draggable instance in your cleanup function and let us know if that resolves things. It's very difficult to troubleshoot by just looking at an excerpt of the code, especially if you're saying it works fine in production. There must be something in the build process that's messing things up, but it's hard to tell without being able to add console.log() calls in there and really see what's going on. 

Link to comment
Share on other sites

  • Solution
49 minutes ago, Lark said:

Sorry for the misunderstanding, the bug only happens in development.

In production it does not:

I assume that's because by default in React 18 runs in strict mode in development and that causes useLayoutEffect()/useEffect() to get called TWICE. Annoying, I know. It's a React thing. So that points even more toward improper cleanup. Your code snippet above definitely is not doing proper cleanup of the Draggable instances. You only called ctx.revert(). 

 

If you'd still like some help, can you please create a simple demo in Stackblitz that illustrates the issue? Make sure it runs in strict mode. Here's a starter you can fork: 

https://stackblitz.com/edit/gsap-react-basic-f48716?file=src%2FApp.js

Link to comment
Share on other sites

In actuality I do call kill, this was an example of how I tried using the context, but I see what I did wrong.

Either way you did explain the problem by blaming React 18 and it does not exist in production .

In my current codebase, I am calling kill on all four draggables in cleanup, with no context.

 

Either way I get it now, thank you.

  • Like 2
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...