DragSelect icon indicating copy to clipboard operation
DragSelect copied to clipboard

Feature Request: add "DropZones"

Open moi90 opened this issue 2 years ago • 17 comments

I would like to use DragSelect only for selecting elements (no free movement). Then, I want to drag and drop them to another place (think of files being dragged into folders).

I tried this, but it does not work reliably (sometimes I can drag, but most of the time, nothing happens):

ds.subscribe("dragstart", ({ isDragging }) => {
      console.log("drag_select.dragstart");
      if (isDragging) {
        ds.stop(false, false);
       ds.start();
      }
    });

I will try to create a minimal jsfiddle to illustrate the problem.

moi90 avatar Dec 15 '21 17:12 moi90

@moi90 I believe you can use:

draggability: false,
keyboardDrag: false,

When creating the DragSelect object.

That should disable the dragging functionality altogether.

UPDATE:

Sorry I missed the fact that you still want drag to work. I am doing something similar actually, and I kept this library just for selecting items, and went with https://interactjs.io/ to achieve the draggability you're looking for - it offers more features. I'm only saying this here because previous versions of this library didn't support dragging at all.

owenfar avatar Dec 22 '21 09:12 owenfar

Thanks for looking into this!

Yes, if I use the above settings, even native dragging does not work anymore.

I want to achive the typical file explorer behavior: Files can be dragged into folders, multiple files can be selected by drawing a rectangle, etc.

Thanks for the pointer to interactjs, I'll give it a try. Can you show me the code where you used the two libraries together?

moi90 avatar Dec 22 '21 10:12 moi90

That is exactly what we had to build, a complete file system online. So it is possible with both these libraries, but unfortunately I cannot share the code base, it's not mine. But that part of the code is not very complicated, you just have one class on the item as selectable for DragSelect and another as draggable for interactJS. It should just work.. :)

owenfar avatar Dec 22 '21 10:12 owenfar

Hi there and thanks for using the tool!

Yes please create a JS fiddle. What's the issue with the library's native drag? Seems to me as if it could do exactly what you want out of the box? Selecting items, then dragging them from one place to another 🤔

Please share, I'd love to understand your use case and why it's not feasible with the current out-of-the-box version of the library.

Otherwise yes, to disable the dragging part of the library you can turn it off with the properties passed to the constructor, see the docs

ThibaultJanBeyer avatar Dec 22 '21 12:12 ThibaultJanBeyer

Here is a fiddle: https://jsfiddle.net/oauzLn6j/

This is not working as I want it:

  • The rectangle can be drawn in the whole window instead of just inside div.right.
  • The objects can be moved freely. Instead, I want them to be droppable only on the nodes in the tree on the left side. In all other cases, they should return to their initial position. (Therefore, I wanted to use native dragging, but then, it is also hard to implement drag and drop for multiple objects.)
  • The native dragstart event handler is not executed.

moi90 avatar Dec 22 '21 14:12 moi90

@moi90 The key part regarding the second bullet point is to have a "cloned" item that is actually being dragged - not the original element, like what happens on a desktop. The other option is to save the x, y positions of the item before you start dragging, and if when they are released it is not a dropzone - you set them back to the original x, y position.

This is something you'll have to write custom as it is not offered in any draggable library by default.. as far as I know.

owenfar avatar Dec 22 '21 14:12 owenfar

Your JSFiddle is broken. But I believe @owenfar understood your request and gave a great answer already. This is an interesting feature request.

ThibaultJanBeyer avatar Dec 24 '21 11:12 ThibaultJanBeyer

To answer your initial question: how to use it with native drag and drop. I think you'd probably have to disable the draggability as mentioned here https://github.com/ThibaultJanBeyer/DragSelect/issues/115#issuecomment-999439276 and then figure out how to add the native drag and drop on multiple elements. Probably you could just place the selected elements into a draggable div and then the user would drag that div which contains the elements. Or recreate the drop like owenfar said here: https://github.com/ThibaultJanBeyer/DragSelect/issues/115#issuecomment-999633569. There is a whole section in the docs on how to use a custom drag and drop: https://github.com/ThibaultJanBeyer/DragSelect#use-your-own-drag-and-drop. The native is also considered custom. I'll rename this to the feature request

ThibaultJanBeyer avatar Dec 24 '21 11:12 ThibaultJanBeyer

You're right, it would be great to have dropzones in DragSelect! How hard would this be to implement?

Your JSFiddle is broken.

I'm sorry, I forgot the revision in the link.

This is the current version: https://jsfiddle.net/63oh0men/4/

For the DropZones, it is important to note that they can be outside of the selectable area. (In my example Node1-3 inside #left, while #right is the selectable area.)

How would you design the API? I think it would be nice if it could be done alone the following (although my JS fu is pretty weak):

var ds = new DragSelect({
    selectables: document.getElementsByClassName("draggable"),
    area: document.getElementById('area'),
    // NEW: Revert unless dropped:
    revert: "unless_dropped",
    // NEW: Constrain movement to area (default: true):
    // (But the dropzone may lie outside of area.)
    constrain: false,
    // NEW: When dropped here, a drop event is sent
    dropzones: document.getElementsByClassName("dropzone")
});

ds.subscribe("drop", ({ items, dropzone }) => {
    // Check if the dropzone accepts the items
    if (acceptsAll(dropzone, items)) {
        // Process drop
        // ...
    } else {
        // Return the items to their initial position
        ds.revert();
    }
    // OR: One could even process the items individually and only revert those that are not accepted:
    ds.revert(items.filter(item => !acceptsSingle(dropzone, item)));
});

moi90 avatar Dec 30 '21 09:12 moi90

Thank you for providing input to this and also an API design 🌟 That the dropzones can lie outside of the draggable area sounds very complicated and that would take a lot of time. The dropzones only are also a lot of work but I'd assume 2-3 weekends of dedicated work would do it. However, I don't get much free time lately so yeah would be great if you could look into this, or consider sponsoring me :) Thank you for your patience :)

ThibaultJanBeyer avatar Apr 02 '22 19:04 ThibaultJanBeyer

I am trying to do exactly the same. @moi90 how did you move forward? Did you keep using this lib, switch to another, go full custom or partially?

I want to achive the typical file explorer behavior: Files can be dragged into folders, multiple files can be selected by drawing a rectangle, etc.

Deitsch avatar Apr 29 '22 12:04 Deitsch

@ThibaultJanBeyer Ok.

@Deitsch I did not yet implement this behavior in my application. If you find a solution, please let me know :)

moi90 avatar Apr 29 '22 14:04 moi90

Hello again everyone @moi90, @Deitsch. I know it has been a long time but I’m picking this up now!

Here was my idea for the design:

The Settings:

new DragSelect({
  selectables: document.getElementsByClassName("draggable"),
  dropZone: [{ zoneId: 'some-id', zoneEl: document.getElementById("dropzone1"), items: document.getElementsByClassName("draggable") }]),
})
  • zoneId: is any string to identify the zone, this can be used to add specific CSS classes to the elements belonging to the zone
  • zoneEl: would be the element that represents a zone
  • items: would be the selectable items that can be dropped into that zone

Because we might have use-cases where one wants to have multiple zones where different things can be dropped into.

The callback:

I was thinking to re-use the generic callback for this like so:

ds.subscribe('callback', ({ ..., 
 dropped
... }) => console.log(dropped) // => [{ zoneId: 'some-id', zoneEl: <zone />, items: [<node1 />, <node2 />, …] }]
  • dropped: would be undefined if no element was not dropped into a corresponding dropZone. But if an element is dropped into a zone it would look like this: [{ zoneId: 'some-id', zoneEl: node, items: [node] }] = array of dropZones with corresponding items/elements that were dropped into the zone [node].

The classes:

We can add classes to ellements according to the state like so:

name trigger
.ds-dropped on successful element drop into container
.ds-droppable on element that can be dropped into at least one container
.ds-droppable-${zoneId} on element that can be dropped into a zone with specific identifier, ${zoneId} will be replaced by the corresponding zoneId
.ds-dropzone on each dropZone
.ds-dropzone-ready on corresponding dropZone when element is dragged
.ds-dropzone-dropped on dropZone when any element is successfully dropped inside

Tricky parts

DropZones outside of the area

I don’t think this would be possible, since if you select an area the elements are bound into that area (can not leave the area). So the dropzone should be within the area or there should be no area, then they can be moved freely.

When are they actually dropped?

One thing I am struggling with deciding is WHEN?! is the elements are dropped?

  • Does the element itself need to be inside of the dropzone?
  • Or would it be the mouse position on release that counts?

Like imagine this scenario:

  1. I am selecting multiple elements
  2. I want to drop them all together in a container
  3. I move the mouse to the container (see video)

https://user-images.githubusercontent.com/12467511/195585321-dc75b3bd-0f7a-4fb6-975a-ced7322ad584.mov

Should in this case all elements be marked as dropped into the container because they were all selected on drop? Or only the first one because that is the only one that is actually visibly inside of the container?

Or let’s think about this use-case:

https://user-images.githubusercontent.com/12467511/195589902-d6c6232d-a9fb-4a2e-8eda-f2f5ca3b1c6c.mov

Where would the elements then be dropped? All were selected and the mouse went to the green zone to release. But by that action, some elements ended up being in the red zone, so would they be marked as dropped in the red or on the green? All of them or only the ones that actually hover the zone?

What do you think?

Your thoughts?

I would love to hear your thoughts, is this kinda how you imagined it?

ThibaultJanBeyer avatar Oct 13 '22 11:10 ThibaultJanBeyer

Maybe we should differentiate between "inside of the zone" and "mouse released inside of the zone". Like maybe in the callback have a mouseDropped and an elementsDropped key? Where elementsDropped would be holding all elements that were actually visually inside of a zone and mouseDropped just indicate in which zone the mouse ended? 🤔

If mouse position is what you are looking for, you could use elementsFromPoint in order to unblock you do something like this:

const cb = ({ event, items, isDragging }: any) => {
      if (!isDragging || !items) return
      if (
        document
          .elementsFromPoint(event.clientX, event.clientY)
          .includes(approveElement)
      )
        console.log('approved')
      if (
        document
          .elementsFromPoint(event.clientX, event.clientY)
          .includes(rejectElement)
      )
        console.log('rejected')
    }

https://user-images.githubusercontent.com/12467511/195595397-b74bf967-6bee-44fe-901e-6fa070aa6f8b.mov

This would be a similar solution I’d implement in order to know which zone was activated by the mouse.

ThibaultJanBeyer avatar Oct 13 '22 12:10 ThibaultJanBeyer

Hi @ThibaultJanBeyer, thanks for picking this up!

Should in this case all elements be marked as dropped into the container because they were all selected on drop? Or only the first one because that is the only one that is actually visibly inside of the container?

I would expect the elements to be dropped where the mouse cursor is released, independent of their actual own location on the screen. This is the behavior that we know from the usual file explorer, isn't it? I can't really imagine a use-case, where the second behavior would make sense...

(These Windows-3.1-style icons are awesome! ;))

moi90 avatar Oct 14 '22 07:10 moi90

Awesome @ThibaultJanBeyer! I would definitely go with the cursor position. I implemented a google photos like behaviour myself (less fancy animated tho). I implemented a multi select drag by redrawing the dragUI gathering all the images. You could do the same on hover, to make it clear that all a dropped in there.

Deitsch avatar Oct 14 '22 08:10 Deitsch

Haha, thank you @moi90 yes I’m building something fun as an example page for DragSelect =) @Deitsch thanks for sharing your use case! I always love to hear the use cases. I did not really understand the on-hover part, would you mind sharing some visual examples for this?

Cool, then that’s a decision! So basically what I made in this comment using the elementsFromPoint method within the library itself: giving a dropped callback key that then just holds the zone where the mouse has released something like:

ds.subscribe('callback', ({ ..., 
 dropped,
 items,
... }) => console.log(dropped)
// => [{ zoneId: 'some-id', zoneEl: <zone />, items: [<item1 />, <item2 />, …] }]

Which is then just the zone, where the mouse was released. The items are the items that are within the selection and assigned to the zone (you might have selected elements that are not droppable into that specific zone and thus would not be in the "items" list from the dropped key. I’d still let it be an array because there might be overlapping dropzones, and then the dropped[0] would always be the topmost one. Feels like a good callback design.

Thanks for your inputs! On it 💪

ThibaultJanBeyer avatar Oct 14 '22 08:10 ThibaultJanBeyer

A PR is out 🎉 The callback is a bit different, please check the PR and give me your thoughts

ThibaultJanBeyer avatar Nov 02 '22 17:11 ThibaultJanBeyer

Thanks, I will try it out as soon as I find the time!

moi90 avatar Nov 10 '22 09:11 moi90

Great, find the most up to date docs in the readme 🧀

ThibaultJanBeyer avatar Nov 10 '22 15:11 ThibaultJanBeyer