thisbeyond / solid-dnd

A lightweight, performant, extensible drag and drop toolkit for Solid JS.
https://solid-dnd.com
MIT License
516 stars 34 forks source link

Sortable with Draghandle #84

Closed tgikf closed 1 year ago

tgikf commented 1 year ago

Hi there, Thanks for doing all the hard work on this, helps a ton!

I'm looking to combine the "Sortable" and "Fine grained/Drag handle" use cases. This is because I have a list of elements that need to be draggable but the elements do, among other things, contain buttons and other inputs that must behave normally and not interfere with (or trigger) draggability.

Having looked at the existing sortable, an idea I had was to provide a way to specify the draggable element (not to be confused with the drag handle). This could be done by parametrizing use:sortable with a DOM reference say draggableElementId, while only applying it to the drag handler.

In my case that might do the trick, however, the more I think about it the less I like the approach as it seems to misuse the draggable and as such not very robust. Nonetheless, a code sample below.

From your end, what do you think would be a sensible approach to this scenario? Implementing a drag handle feature in create-draggable.ts that accepts 2 elements to bind the EventListeners to the correct drag handler in the first place?

Thanks for your advice!

Something like:

const Sortable = (props) => {
  const sortable = createSortable(props.id);

  return (
    <div
      classList={{ "opacity-30": sortable.isActiveDraggable }}
      attr:id={props.id}
    >
      <div
        use:sortable={props.id}
      >
        ✪
      </div>
      {props.item}
    </div>
  );
};

and in create-sortable.ts:

  const sortable = Object.defineProperties(
    (
      element: HTMLElement,
      draggableElementId?: () => string | string | undefined
    ) => {
      draggable(element, () => ({ skipTransform: true }));
      droppable(element, () => ({ skipTransform: true }));

      createEffect(() => {
        const resolvedTransform = transform();

        const specificElement =
          draggableElementId && typeof draggableElementId === "function"
            ? draggableElementId()
            : draggableElementId;

        const el = specificElement
          ? document.getElementById(specificElement)!
          : element;

        if (!transformsAreEqual(resolvedTransform, noopTransform())) {
          const style = transformStyle(transform());
          el.style.setProperty("transform", style.transform ?? null);
        } else {
          el.style.removeProperty("transform");
        }
      });
    },
    {
      ref: {
        enumerable: true,
        value: setNode,
      },
      transform: {
        enumerable: true,
        get: transform,
      },
      isActiveDraggable: {
        enumerable: true,
        get: () => draggable.isActiveDraggable,
      },
      dragActivators: {
        enumerable: true,
        get: () => draggable.dragActivators,
      },
      isActiveDroppable: {
        enumerable: true,
        get: () => droppable.isActiveDroppable,
      },
    }
  ) as unknown as Sortable;
martinpengellyphillips commented 1 year ago

👋 Couple of quick thoughts:

  1. Generally clicks will be ignored. So having buttons and inputs might "just work".
  2. You can use the same fine-grained pattern with sortables today. The main thing is that you don't use the 'directive' pattern when going fine-grained. Here is a rough example:
const Sortable = (props) => {
  const sortable = createSortable(props.item);
  const [state] = useDragDropContext();

  return (
    <div
      ref={sortable.ref}
      class="sortable flex"
      classList={{
        "opacity-25": sortable.isActiveDraggable,
        "transition-transform": !!state.active.draggable,
      }}
      style={transformStyle(sortable.transform)}
    >
      <div class="flex-1">{props.item}</div>
      <div class="handle" {...sortable.dragActivators}>
        ✪
      </div>
    </div>
  );
};
tgikf commented 1 year ago

Thanks for the quick feedback!

  1. Generally clicks will be ignored. So having buttons and inputs might "just work".

Yeah I noticed that generally the behavior is okay, but if someone were to hold the button for longer than 250ms it still activates dragging...

You can use the same fine-grained pattern with sortables today. The main thing is that you don't use the 'directive' pattern when going fine-grained.

When I tried this I faced some issues:

  1. Dragging is still activated when you click and hold on other parts of the element outside the handle. I suppose that is because the ref={sortable} is on the parent element
  2. When you drag, even when using the handle, the overlay seems not to include the handle. I can't quite make sense of that at this point.

Reproduced that scenario in this Codesandbox, did I miss something?

martinpengellyphillips commented 1 year ago
  1. In your sandbox ref={sortable} should be ref={sortable.ref}.
  2. The overlay is not an automatic copy of your draggable. Instead it is whatever component you specify as the child of the overlay. In your sandbox it is currently this:
    <DragOverlay>
    <div class="sortable">{activeItem()}</div>
    </DragOverlay>

    You need to change that to whatever you want to display as the dragged overlay. Just don't use the draggable component that has the createSortable call in it as this will cause duplicate registration and issues. Instead you might want a 'non-interactive' version of your component for the overlay.

tgikf commented 1 year ago

Oh well, clearly my miss - thanks for pointing that out. Got it to work now without issues. Took me a while to figure out how the different pieces work together in the manual (i.e., non-directive) approach, but I think it finally clicked. Thanks again, a fine piece of software you wrote here!