eXXcellent solutions tech blog logoabout

UI5: drag and drop on the wild side

Cover Image for UI5: drag and drop on the wild side
Posted
by Andreas Pflugrad

Every once in a while you come across a project requirement that seems straightforward to implement but turns out to be surprisingly interesting to actually do. For us, such a situation occurred when developing an SAPUI5 application which required drag and drop functionality from a table into a planning calendar. In our case, the table would include tasks that needed a certain amount of time to complete and were to be assigned to people represented in the planning calendar. Seems like a common thing to do, right?

In this post, we describe the issues with this use case and how we came to a solution.

Disclaimer

This post presents two workarounds that, to some extent, rely on internal implementations, which is, of course, bad practice. If used, one must expect them to break with any next update. That said, so far, they worked for UI5 version 1.56 just as they now work with version 1.76.

The issue

So off we go into our sample project, setting up drag and drop the for the calendar. The sap.m.PlanningCalendar's drag and drop functionality is a bit special in that we do not have to set up a DragDropInfo ourselves. Instead, the calendar provides some functions to enable dnd and to register event handler functions:

<PlanningCalendar rows="{/people}" > <rows> <PlanningCalendarRow title="{name}" text="{role}" appointments="{/appointments}" enableAppointmentsDragAndDrop="true" appointmentDrop=".onAppointmentDrop" appointmentDragEnter=".onAppointmentDragEnter" > <!-- [...] --> </PlanningCalendarRow> </rows> </PlanningCalendar>

Defining a handler function for appointmentDragEnter allows us to not only drag appointments within the same calendar row but also between different rows and even between different calendars.

A calendar appointment being dragged into a different calendar row;

With that working, we then set up the sap.ui.core.dnd.DragInfo for the table as we would do it for a standard drag and drop task in UI5.

<Table items="{/tasks}"> <!-- [...] --> <dragDropConfig> <dnd:DragInfo sourceAggregation="items" dragStart=".onDragStart" /> </dragDropConfig> </Table>

(Note that we chose a sap.m.Table for our sample project, but this issue and its solutions are just as applicable for other sap.m.ListBase or sap.ui.table.Table derivates.)

In theory, we whould now be able to move an item from the table over to the planning calendar. So we start the application, drag an item into the calendar and we see:

A table item being dragged over the planning calender and being blocked.;

The quest for a solution

Why it won't work in the first place

Of course, we go and look at the configuration again, make sure everything is set up correctly, try different settings, try our own sap.ui.core.dnd.DropInfo for the PlanningCalendarRow with targetAggregation="appointments" instead of the built-in functions but all without success. What further complicates things is the lack of any error report that might give us a hint on where the issue lies. Looking for other people to have encountered this problem, there are very few posts that feature this issue. Unfortunately, they are also unsolved.

Having only been left with the option to find an answer ourselves, we examine the implementation of the sap.m.PlanningCalendar itself.

Here, we can see that the calendar defines its own dragDropConfig elements and handler functions. This also explains why there is the need for the separate appointmentDrop, appointmentDragEnter etc. functions instead of a plain user-configured DragDropInfo. These internal functions perform some additional work before firing the respective api-methods; This will soon become important!

Opening up the calendar's blockade

So since our own DropInfo in the calendar has no effect, can we instead use the "internal" dragDropConfig of the PlanningCalendar to drop something from the outside? Yes, we can. The first thing to note is, that the calendar's config defines a group name: "DragDropConfig". Since only configs with the same groupName can "talk" to each other, we assign the same name to our DragInfo.

<Table items="{/tasks}"> <!-- [...] --> <dragDropConfig> <dnd:DragInfo sourceAggregation="items" dragStart=".onDragStart" groupName="DragDropConfig" /> </dragDropConfig> </Table>

That alone won't have any effect but it's still necessary to get this working at all.

So what is it that will remove the cursed blocked cursor and open up the calendar for dropping?

It turns out that it is the addition of a CSS class - sapUiCalendarRowAppsOverlay - to a certain UI-element that lifts this barrier. This addition is performed in the dragStart handler function of the PlanningCalendar's self-configured DragInfo. If we now mirror this behaviour in our source list i.e. we copy the code into our own onDragStart function...

onDragStart: function (oEvent) { // overlay magic from sap.m.PlanningCalendar in order to enable the target PlanningCalendar to recognize the dragcontrol as a valid drag source var fnHandleAppsOverlay = function () { var $CalendarRowAppsOverlay = jQuery(".sapUiCalendarRowAppsOverlay"); setTimeout(function () { $CalendarRowAppsOverlay.addClass("sapUiCalendarRowAppsOverlayDragging"); }, 0); jQuery(document).one("dragend", function () { $CalendarRowAppsOverlay.removeClass("sapUiCalendarRowAppsOverlayDragging"); }); }; fnHandleAppsOverlay(); }

...we can finally drag a list entry over the calendar rows without being blocked!

Table items can now be dragged over the planning calendar;
Success — almost.

At this point we've veered off quite a bit into tampering with internal implementations so now it should become clear why there is a disclaimer at the top. But needs must when the devil drives — or when you really need to get something working.

Getting the final pieces together

We can now drag an item over the calendar, but we cannot drop it, the drop event is not being fired. There is also no outline shown to indicate, where the item would be dropped. So the last bit of tinkering we have to do is some further work in our dragStart handler.

The console now shows a couple of errors whenever we hover a table item over the calendar:

Errors shown in the browser console indicating an undefined function;
The calendar's drop implementation is expecting a sap.ui.unified.CalendarAppointment to be dropped and is trying to use the appointment's getStartDate and getEndDate functions.

So we either have to pass a CalendarAppointment or we need to provide these same functions that the appointment would provide. In our case we decided to do the latter by enhancing the dragControl with the respective functions. We do this, again, in our onDragStart function.

onDragStart: function (oEvent) { // overlay magic from sap.m.PlanningCalendar in order to enable the target PlanningCalendar to recognize the dragcontrol as a valid drag source var fnHandleAppsOverlay = function () { var $CalendarRowAppsOverlay = jQuery(".sapUiCalendarRowAppsOverlay"); setTimeout(function () { $CalendarRowAppsOverlay.addClass("sapUiCalendarRowAppsOverlayDragging"); }, 0); jQuery(document).one("dragend", function () { $CalendarRowAppsOverlay.removeClass("sapUiCalendarRowAppsOverlayDragging"); }); }; fnHandleAppsOverlay(); // provide the dragged control with start and end data along with getter functions var oDragSession = oEvent.getParameter("dragSession"); var oPseudoAppointment = oDragSession.getDragControl(); var oStartDate = new Date(); var oEndDate = new Date(oStartDate); oEndDate.setHours(oStartDate.getHours() + oPseudoAppointment.getBindingContext().getProperty("duration")); oPseudoAppointment.startDate = oStartDate; oPseudoAppointment.endDate = oEndDate; oPseudoAppointment.title= oPseudoAppointment.getBindingContext().getProperty("name"); oPseudoAppointment.getStartDate = function() { return oPseudoAppointment.startDate; }; oPseudoAppointment.getEndDate = function() { return oPseudoAppointment.endDate; }; oPseudoAppointment.getTitle = function() { return oPseudoAppointment.title; }; }

And that will gift us the sight we've been yearning for:

A table item being dragged over the planning calender. The calendar indicates a possible drop location.;

At this point, we can do our necessary work to transfer and extract our payload from the dragged item and actually add an appointment at the position where we have dropped the item.

Success — finally

The other solution

So far, we have seen the slightly more complex solution which enables us to drop something at a specific point in time in the planning calendar and create an appointment from there.

In some cases, it might be enough to just drop something onto the entire calendar row i.e. the person or resource that is represented by the row. For this case, there is a second solution — and it includes no overlay magic or pseudo-appointments, yet still, some internal details will have to be touched on.

Behind the scenes, the PlanningCalendar renders a table, with each calendar row being a list item of this table. We can make use of this by defining this internal table and its items as our drop target.

We can deactivate the dnd-methods provided by the calendar and add our own DragDropInfo to the PlanningCalendarRow template:

<PlanningCalendar id="idPC" rows="{/people}" > <rows> <PlanningCalendarRow title="{name}" text="{role}" appointments="{/appointments}" enableAppointmentsDragAndDrop="false" > <dragDropConfig> <dnd:DragDropInfo sourceAggregation="appointments" targetElement="idPC-Table" targetAggregation="items" dragEnter=".onDragEnter" drop=".onAppointmentDrop" /> </dragDropConfig> <!-- <[...] --> </PlanningCalendarRow> </rows> </PlanningCalendar>

Note that we have added a stable ID to the calendar. We will use this for reference now and later.

We have set enableAppointmentsDragAndDrop="false" so our own dragDropConfig will come into play. We add a DragDropInfo to set the calendar as both a drag source for appointments as well as the drag target for them. Note that targetElement="idPC-Table" combines our assigned stable ID with the added internal table's ID. This is the first point of this solution at which we rely on some internal knowledge — remember the disclaimer? ;-)

We use that internal table's items aggregation as our targetAggregation and now, we can drag calendar appointments onto entire calendar rows:

A calendar appointment being dragged over a calendar row. The entire row is marked as the droppable area;
Defining the DragDropInfo for the table is somewhat straightforward:

<Table items="{/tasks}"> <!-- [...] --> <dragDropConfig> <dnd:DragDropInfo sourceAggregation="items" targetElement="idPC-Table" targetAggregation="items" dragEnter=".onDragEnter" drop=".onExternalItemDrop" /> </dragDropConfig> </Table>

We drag from the table's items and, again, assign the calendar's table and its items as targetElement and targetAggregation. Note that the ID of the planning calendar's internal table must be known here so some extra work is required when the drag source is contained in a different view. It might also be necessary to define a separate drop handler function in order to deal with the different drag controls.

Also, the droppedControl in both cases (dragging from the calendar and dragging from the table) is now the behind-the-scenes ListItem of the PlanningCalendar and not the PlanningCalendarRow. In order to get the actual PlanningCalendarRow, we might have to work our way forward from the ListItem's ID which is the calendar row ID with an additional "-CLI" suffix. Earlier versions (e.g. 1.54) had a getter function that allowed us to directly access the PlanningCalendarRow. With newer UI5 versions, we might have to make use of our knowledge about the ListItem's ID — the second point where internals are needed and may not be ported to the next version...

We do see, however, that the item from our source table can be dropped onto the row without further issues.

A table item being dragged over a calendar row. The entire row is marked as the droppable area;

So whenever you need to drag external resources into an sap.m.PlanningCalendar, feel free to use whichever of these methods to get that job done.

Maybe future updates will eliminate the need for these workarounds. Or maybe you already know a more elegant way to do this in a more stable fashion? Let us know! ;-)

Image sources

The cover image used in this post was created by Rod Dion under the following license. All other images on this page were created by eXXcellent solutions under the terms of the Creative Commons Attribution 4.0 International License