SortableJS / Vue.Draggable

Vue drag-and-drop component based on Sortable.js
https://sortablejs.github.io/Vue.Draggable/
MIT License
20.12k stars 2.9k forks source link

Getting details of the dropped element and the array the element is added to #1029

Closed amthekkel closed 3 years ago

amthekkel commented 3 years ago

I have the following array for a Kanban columns. the drag and drop is working fine, however, I would like to get information on the drop element so it can be sent to the API

e.g. if card with id 3 was moved from 'Backlog' column to 'Review' column then I would like to get the following info

1) id of the card //3 2) name of the array the card is being added to // Review 3) name of the array the card is being taken from // Backlog

columns: [
             {
                title: 'Backlog',
                tasks: [
                    {
                        id: 1,
                        title: 'Add discount code to checkout page',
                        date: 'Sep 14',
                        type: "Feature Request",
                    },
                    {
                        id: 2,
                        title: 'Provide documentation on integrations',
                        date: "Sep 12",
                    },
                    {
                        id: 3,
                        title: 'Design shopping cart dropdown',
                        date: 'Sep 9',
                        type: "Design",
                    },
                    {
                        id: 4,
                        title: 'Add discount code to checkout page',
                        date: 'Sep 14',
                        type: "Feature Request",
                    }                   
                ],
             },
             {
                title: 'In Progress',
                tasks: [
                    {
                        id: 6,
                        title: 'Design shopping cart dropdown',
                        date: 'Sep 9',
                        type: "Design",
                    },                  
                ],
             },
             {
                title: 'Review',
                tasks: [

                ],
             },
             {
                title: 'Done',
                tasks: [
                    {
                        id: 14,
                        title: 'Add discount code to checkout page',
                        date: 'Sep 14',
                        type: "Feature Request",
                    }
                ],
             },
        ];

Here is the vue section of the code

<div  v-for="column in columns" :key="column.title">
     <p class="text-gray-700 font-bold font-sans tracking-wide text-sm">
    {{ column.title }}
    </p>
    <draggable :list="column.tasks" :animation="200" ghost-class="ghost-card" group="tasks" :empty-insert-threshold="100"   
         @add="add_to_list" :move="card_moved" @end="drop_end" @change="log">
           //Element showing the cards
        </draggable>
</div>

1) The change event to get the id of the item being dragged but it doesn't mention the from or to array 2) The end event provides a from and to property but can't seem to figure out how to get the titles of the from and to array 3) is it possible to get the three info from the end event rather than trying to work out info based on two separate events or having to upload the whole dataset to the API?

Any help will be much appreciated. thanks in advance.

gregveres commented 3 years ago

I have a similar situation. I have N + 1 lists. The first list like your Backlog list and the rest of them are like different stages that a story can be in. But in my case, there can be any number of stages. (to be clear, I have a completely different, sports related, data model, but I will work within your context.)

The bottom line is that Sortable.JS and then by extension vue-draggable is not very good at providing information in this context. Instead, it expects to manage the lists for you and it assumes that if you are using multiple lists, that an element that can be dragged can exist on any of the lists as is. For me, my backlog item is completely different from my other N list items. But I was able to get it to work, until I realized today there is a bug when the last backlog item is dragged to one of the other columns - the callbacks that work when there are multiple backlog items stop working when the last item is dragged.

Ok, back to how I did it.

First, you have to use a data attribute on your list to store some sort of identifier for your kanban colums. Each of my lists have an Id and I use Id 0 to indicate my backlog column. So I use a :data-box="b.Id" attribute on my columns.

I also use a :data-id="m.id" for each of the stories on my backlog and my other columns.

Then I use this for my backlog vue code:

          <draggable
            v-model="waitList"
            animation="200"
            group="players"
            ghost="ghost"
            @end="moveWaitListCheck"
          >
            <transition-group type="transition" name="flip-list">
              <div v-for="m in waitList" :key="m.Id" :data-id="m.Id" class="waitlist full-width"> {{ m.Name }}</div>
            </transition-group>
          </draggable>

And this is the relevant portion of my other lists:

<draggable
   v-model="b.nextPlayers"
   tag="tbody"
   group="players"
   @end="moveNextSessionPlayer"
   style="min-height: 32px"
    :data-box="b.ordinal"
>     
   <tr v-for="p in b.nextPlayers" :key="p.compId" :data-id="p.compId"></tr>
</draggable>

The @end="moveNextSessionPlayer" gets called when a story is moved from one of these non-backlog columns. It gets called when the drop ends.

The @end="moveWaitListCheck" gets called when a story on the backlog gets moved to one of the other columns.

Now, this was working great at one point, even for the last element. But I think I upgraded to the latest version and it broke. I think I will move back and check if it starts working again. Or, maybe I didn't test it well enough and it never worked when the last item in the list was dragged out of the list.

Now that we have the right functions being called (with the cavet of the last element in the list), we have to decode what is being sent to the end.

I wanted to take care of one thing that Sortable.js doesn't seem to think is ever possible and that is, if a user releases the item when it is not on a drop site, I want to cancel the drop and have the item return to it's original list.

So this is the code I have for each of the functions:

  // This gets called when a player is dropped within the next session tables. It gets called
  // for two situations:
  // 1) where the player is being dropped within a box
  // 2) when a player is being dragged from one box to another.
  moveNextSessionPlayer(e: SortableEvent): void {
    const ogEvent: DragEvent = (e as any).originalEvent;
    const fromBoxOrdinal = StringToNumber(e.from.getAttribute('data-box'), 0);
    const toBoxOrdinal = StringToNumber(e.to.getAttribute('data-box'), 0);
    if (ogEvent && ogEvent.type !== 'drop') {
      // The drop was cancelled and we need to put the player back where she came from
      if (e.newIndex !== undefined && e.oldIndex !== undefined && fromBoxOrdinal !== 0 && toBoxOrdinal !== 0) {
        const player = this.boxes.value[toBoxOrdinal - 1].nextPlayers.splice(e.newIndex, 1)[0];
        this.boxes.value[fromBoxOrdinal - 1].nextPlayers.splice(e.oldIndex, 0, player);
      }
    } else {
      // a player was moved within a box so let's record the adjustment
      const compId = StringToNumber(e.item.getAttribute('data-id'), 0);
      if (compId && fromBoxOrdinal && toBoxOrdinal && e.newIndex !== undefined && e.oldIndex !== undefined) {
        const fromIndex = this.boxes.value[fromBoxOrdinal - 1].startIndex + e.oldIndex;
        const toIndex = this.boxes.value[toBoxOrdinal - 1].startIndex + e.newIndex;
        this.addAdjustment(
          AdjustmentType.AbsolutePosition,
          compId,
          // the + 1 is needed when a player down within a single box to accommodate for the player being
          // removed from the toIndex by sortablejs, but still being in our count in the backend.
          toIndex + (fromBoxOrdinal === toBoxOrdinal && toIndex > fromIndex ? 1 : 0),
          toBoxOrdinal
        );
      }
    }
  }

That was for an item in one of the non-backlog items being moved. Note, I don't handle moving the item back to the backlog list - I just realized that today.

This is the function that handles the item being dragged "from" the backlog list

  // This gets called when a wait list player is moved. There are three situations:
  // the wait list player is being dropped somewhere else in the wait list
  //    We don't have to do anything there, the library takes care of that
  // the wait list player is dropped off of any drop target
  //    We consider this a cancel and we have to undo any move that might have happened
  // the wait list player is dropped on a next session box
  //    We have to add the player to the box
  moveWaitListCheck(e: SortableEvent): void {
    const ogEvent: DragEvent = (e as any).originalEvent;
    const toBoxOrdinal = StringToNumber(e.to.getAttribute('data-box'), 0);
    if (ogEvent && ogEvent.type !== 'drop' && toBoxOrdinal === 0) {
      // The drop was cancelled and we need to rearrange the waitList
      if (e.newIndex !== undefined && e.oldIndex !== undefined) {
        this.waitList.value.splice(e.oldIndex, 0, this.waitList.value.splice(e.newIndex, 1)[0]);
      }
    } else if (toBoxOrdinal > 0 && e.newIndex !== undefined) {
      // the player was dropped on the next session box
      const compId = StringToNumber(e.item.getAttribute('data-id'), 0);
      this.addAdjustment(
        AdjustmentType.AbsolutePosition,
        compId,
        this.boxes.value[toBoxOrdinal - 1].startIndex + e.newIndex,
        toBoxOrdinal
      );
    }
  }

I think you could try to do something using the @changed event, but then you are going to get and process two events per move and they will not be connected in any sort of way so you are going to have to determine if you need to synchronize them at all, in some way.

As I said, this was working great for me until I realized that events are sent when the last item is dragged out of a list. Even the removed change event is NOT sent for that last item. This seems like a big bug to me. I am about to start investigating it now and will be filing one soon if I can get a reproduction.

gregveres commented 3 years ago

Ok, it turns out I had posted about the last item in the list not getting the @end message earlier. It is issue #999 And some kind soul figured out the issue is that if you wrap your list in a v-if, you won't get those last events because vue will remove the component before the event is delivered. So forget everything I said about about the last event not getting delivered.

My solution now works great for me as illustarted above. I hope it helps.

amthekkel commented 3 years ago

Apologies for the delay in getting back and thank you for your detailed write up.

As per your suggestion I used the data attribute on the columns and card and was able to get the required values in the @end event handler.

Thanks a lot for your help. Much appreciated.

fabioselau077 commented 1 year ago

Two years later and I need the same thing, all I got was the HTML code. Is there any simpler way to do this today? Basically I want to know the data of the card dragged, as well as the id of the new column and the old one

aeruggiero commented 1 year ago

Up. Same issue. I just found out that in the CustomEvent (from @update, @remove, or whatever), in the item property, you can find a Symbol(cloneElement) that contains the item being dragged. But I can't access it! Then, if I use the clone on the VueDraggable, it has the item being dragged (and not the HTML!)

Did someone find a solution for this?