Shopify / draggable

The JavaScript Drag & Drop library your grandparents warned you about.
https://shopify.github.io/draggable
MIT License
18k stars 1.09k forks source link

Two draggable instances? #483

Open AnteroRamos opened 3 years ago

AnteroRamos commented 3 years ago

Hello,

I have this drag and drop form builder where I want only one container swappable. Is it possible to have an instance of Draggable.Draggable and another of Draggable.sortable? I've made this code, but I'm not being able to sort the items on the right container. This is the code I have:

https://jsfiddle.net/gx32et8b/4/

Thank you in advance for the explanation.

zjffun commented 3 years ago

Change

let sortable = new Draggable.Sortable(document.querySelectorAll('.drop-field'), {
    draggable: '.sortable'
});

to

let sortable = new Draggable.Sortable(document.querySelectorAll('.form-zone'), {
    draggable: '.sortable'
});

will work fine.

https://jsfiddle.net/gmfqow01/

AnteroRamos commented 3 years ago

I tried that class because in the documentation said that sortable only worked with direct children, but couldn't make to work... must have done something wrong. Thank you.

AnteroRamos commented 3 years ago

Just so I don't need to open another issue... Is it possible to have the sortable feature while dragging from the container on the left to the one on the right in the way I have the code? Basically, when dragged from the left to the right container I would be able to drag it anywhere I want.

zjffun commented 3 years ago

drag:over event can do this. For example https://jsfiddle.net/9q2mu6kd/1/

AnteroRamos commented 3 years ago

That way it let's me change when dragging but it adds to the last position because of the way I add the html, with += innerHTML. I didn't see it, but you guys have any function to replace the item without a huge amount of if statements?

The way I was going to do it was:

droppable.on('drag:over', function (evt) {
    if (overContainer !== dropField) {
        return;
    }
    if (mirror2.id === "campoTexto") {
         evt.over.parentNode.insertBefore(texto(), evt.over);
    } else if (mirror2.id === "campoEmail") {
         evt.over.parentNode.insertBefore(email(), evt.over);
    }
});

But, in the future, I'll have a lot more fields. Would there be a better way to do this with the library?

zjffun commented 3 years ago

How about in left container

<li class="field-item">
  <div class="field-item__name">E-mail</div>
  <div class="field-item__content hidden">
    <label>Email</label>
    <textarea type="text" name="teste" placeholder="mail"/>
  </div>
</li>

after drop to right container

<li class="field-item">
  <div class="field-item__name hidden">E-mail</div>
  <div class="field-item__content">
    <label>Email</label>
    <textarea type="text" name="teste" placeholder="mail"/>
  </div>
</li>
AnteroRamos commented 3 years ago

Thanks for your input! That would actually work, but I would have a LOT of HTML that is very similar (I will use a fair amount of code for each field, not only the one I sent, that's a example). Since most of the HTML is similar, and only changes some things I was thinking of doing an array with the things that change for each and making a template that receives those things as parameter... That way I would have almost no HTML code in comparation.

That said, would there be another alternative with the way I have the code?

zjffun commented 3 years ago

Did you mean this method?

<draggable 
  v-model="myArray" --- an array of each field
  group="people" 
  @start="drag=true" 
  @end="drag=false" 
  item-key="id">
  <template #item="{element}">  --- create each field according to the myArray
    <div>{{element.name}}</div>
  </template>
</draggable>
AnteroRamos commented 3 years ago

No, I actually didn't know that method... And I also don't know what it does ahah

What I mean is this:

let htmlToAdd = {
    campoTexto: ['campoTexto'],
    campoEmail: ['campoEmail']
}

droppable.on('drag:stopped', function (evt) {

    draggedElementId = evt.originalSource.id;

    label = htmlToAdd[draggedElementId];

    makeFields(label)

    dropField.querySelector(".field-item:not(.sortable)").remove();
});

function makeFields(label) {
    dropField.querySelector('.form-zone').innerHTML += `<div class="sortable" draggable=true><label>${label}</label> <input type="text"/><br></div>`;
}

This way I only have one template and an object with all the different information. This may not make sense in the way I have right now, only 1 item per array, but in the final version there will be around 5 items for each.

That said, is there any way to add the field to the position I want when dragging, in the way I have the code right now? I'll leave the JSfiddle link, maybe it's easier:

https://jsfiddle.net/ug1vxoeq/5/

zjffun commented 3 years ago

I may not understand “add the field to the position I want when dragging”.

My understanding is that if you drag the Email element on the Campo element, the Email element will be inserted before the Campo element. This can be done by using drag:over manipulate DOM.

image

AnteroRamos commented 3 years ago

Yes, that's what I meant. But I've changed my mind and I'm using your approach to create the fields.

But until now, I hand't noticed I wasn't adding the fields to the "form-zone", it was only being added to the container. I changed from '.drop-field' to '.form-zone' (where the form is created) and not being able to change the order.

https://jsfiddle.net/dngo247e/

zjffun commented 3 years ago

Change line 59 if (overContainer !== dropField) { to if (!overContainer.classList.contains('form-zone')) { can make the mirror2 insert before the over element.

AnteroRamos commented 3 years ago

Thank you! Just one last question, when sorting in the form, it always creates a another instance of the element I'm dragging below, but it doesn't delete... How would I be able to do that?

https://jsfiddle.net/e3m0ph8u/2/

zjffun commented 3 years ago

Sure. Just need to change the mirror:created listener to:

  droppable.on("mirror:created", function (evt) {
    if (evt.source.classList.contains("sortable-mirror")) {
      mirror2 = evt.source;
      return;
    }
    mirror2 = evt.mirror.cloneNode(true);
    mirror2.classList.add("sortable-mirror");
  });

https://jsfiddle.net/e3m0ph8u/2/

AnteroRamos commented 3 years ago

That worked very well! Thank you!

Still on sortable, I added a 'form-row' to make the fields side by side. But I want to give the option to add below too (also in the middle of 2 fields, not only at the end of the form). For that, with bootstrap I would have to add another 'from-group', but I'm not being able to figure out a way to trigger the creation of a 'form-group'. This is what I did:

https://jsfiddle.net/cuf6tn51/3/

Is there a way to do that with this framework?

zjffun commented 3 years ago

Suppose we have two rows of elements.

item1
item2

Do you mean you want to have an option if you select it, you will be able to drag an element to ph, ph' and ph''. And if you unselect it, you will be able to drag an element to ph2 and ph2''.

ph
item1 ph2
ph'
item2 ph2'
ph'‘

This effect can be achieved by changing the css flex property.

AnteroRamos commented 3 years ago

Essentially, yes, that's it! But I wanted to be able to do that with the drag motion. I could either drag to the side or below, I don't know if that's possible.

Also, I'm using bootstrap. What flex property should I use for that use?

zjffun commented 3 years ago

How about try https://getbootstrap.com/docs/5.0/layout/columns/ . Using the col-12 class to drag to the below, and the col class to drag to the side. Like https://jsfiddle.net/acshwvr5/

AnteroRamos commented 3 years ago

I've tried that, but then I wouldn't be able to add fields to the side if I wanted with the col-12.

I've managed to make it work it another way, where I have two divs and if I drag to the second one, it will add the another like (a new form-row) in the div above. It's a workaround that I think will do just fine.

In this new way, the problem is: I'm not being able to add a event listener to a "hr" element inside the form.

document.querySelectorAll('.hr-form-seperator').forEach(item => {
    console.log("justThis");
    console.log(item.innerHTML);
    item.addEventListener('click', event => {
        console.log("this123");
    })
});

I click, and nothing happens, but if I do this code inside the browser console, it works. I think it might have something to do with the draggable instance. Is there any way to "turn it off" when I stop dragging? I already remove the form container when I stop dragging, but didn't seem to make any difference. Or if there any other solution to force the add event listener on click to work..

    formDivs.forEach(e => droppable.removeContainer(document.querySelector(e)));
zjffun commented 3 years ago

It seems that the element may add after the event was listened to. Or it was under a transparent element, so it was not clicked. Like: https://jsfiddle.net/agq6mkjL/

AnteroRamos commented 3 years ago

That makes a lot of sense! Didn't think abou that one.

I think I've managed to fix that issue. Can you just give me an idea on why I can't add a field above the 'div' with the "hr" element? I do not mean inside, I want to be able to add the field above that line. I'm gonna have multiple of them across the form and I wanted to be able to add fields between them.

https://jsfiddle.net/djt1bkya/13/

zjffun commented 3 years ago

Change <div class="div-for-hr-seperator"> to <div class="div-for-hr-seperator field-item"> can do this.

https://jsfiddle.net/ksx2dLv1/

AnteroRamos commented 3 years ago

Correct, that would work! But when I drag the item it will be added inside the div instead of below. Is there any way to make it only sortable, for example, I drag and it gets added below or above the line. I could manage to do it with vanilla JS, just asking if there is any way with the library.

zjffun commented 3 years ago

Seems no way with the library. As far as I know, unless the official Sortable, Swappable or Droppable libraries fully meet the requirements, Draggable can only be used, which can only help you create mirrors and monitoring items moving in and out of items or containers. Others include creating new elements, moving elements to specified positions, etc. can only be achieved by vanilla JS.