Friday, October 4, 2013

PrimeFaces Extensions: drag-and-drop feature in Timeline

Since the last release of the PrimeFaces Extensions, the Timeline component allows dragging items from the outside (an external list, etc.) and dropping them onto the Timeline. The picture shows the dragging / dropping process.


When an item is dragged and dropped, an event with an end date will be created. The end date is the start date + 10% of the timeline width. This default algorithm fits the most use cases. Tip: if events are editable, you can adjust the start / end date later by double clicking on an event.

To activate the built-in drag-and-drop feature, simple add p:ajax with event="drop" to the timeline tag.
<div style="float:left;">
    <strong>Drag and drop events</strong>
    <p/>
    <p:dataList id="eventsList" value="#{dndTimelineController.events}"
                var="event" itemType="circle">
        <h:panelGroup id="eventBox" layout="box" style="z-index:9999; cursor:move;">
            #{event.name}
        </h:panelGroup>
        
        <p:draggable for="eventBox" revert="true" helper="clone" cursor="move"/>
    </p:dataList>
</div>
    
<pe:timeline id="timeline" value="#{dndTimelineController.model}" var="event"
             editable="true" eventMargin="10" eventMarginAxis="0" minHeight="250"
             start="#{dndTimelineController.start}" end="#{dndTimelineController.end}"
             timeZone="#{dndTimelineController.localTimeZone}"
             style="margin-left:135px;" snapEvents="false" showNavigation="true"
             dropActiveStyleClass="ui-state-highlight" dropHoverStyleClass="ui-state-hover">
    <p:ajax event="drop" listener="#{dndTimelineController.onDrop}" global="false" update="eventsList"/>
    
    <h:outputText value="#{event.name}"/>
    ...  other properties can be displayed too
</pe:timeline>
In this example, the objects in the list (on the left side) have properties name, start date, end date.
public class Event implements Serializable {

    private String name;
    private Date start;
    private Date end;

    // constructors, getter / setter
    ...

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }

        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        Event event = (Event) o;

        if (name != null ? !name.equals(event.name) : event.name != null) {
     return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        return name != null ? name.hashCode() : 0;
    }
}
The bean class defines the AJAX listener onDrop. The listener gets an instance of the class TimelineDragDropEvent. Besides start / end date and group, this event object also contains a client ID of the dragged component and dragged model object if draggable item is within a data iteration component.
public class DndTimelineController implements Serializable {

    private TimelineModel model;
    private TimeZone localTimeZone = TimeZone.getTimeZone("Europe/Berlin");
    private List<Event> events = new ArrayList<Event>();

    @PostConstruct
    protected void initialize() {
        // create timeline model
        model = new TimelineModel();

        // create available events for drag-&-drop
        for (int i = 1; i <= 13; i++) {
            events.add(new Event("Event " + i));
        }
    }

    public void onDrop(TimelineDragDropEvent e) {
        // get dragged model object (event class) if draggable item is within a data iteration component,
        // update event's start / end dates.
        Event dndEvent = (Event) e.getData();
        dndEvent.setStart(e.getStartDate());
        dndEvent.setEnd(e.getEndDate());

        // create a timeline event (not editable)
        TimelineEvent event = new TimelineEvent(dndEvent, e.getStartDate(), e.getEndDate(), false, e.getGroup());

        // add a new event
        TimelineUpdater timelineUpdater = TimelineUpdater.getCurrentInstance(":mainForm:timeline");
        model.add(event, timelineUpdater);

        // remove from the list of all events
        events.remove(dndEvent);

        FacesMessage msg = new FacesMessage(FacesMessage.SEVERITY_INFO,
                                            "The " + dndEvent.getName() + " was added", null);
        FacesContext.getCurrentInstance().addMessage(null, msg);
    }

    public TimelineModel getModel() {
        return model;
    }

    public TimeZone getLocalTimeZone() {
        return localTimeZone;
    }

    public List<Event> getEvents() {
        return events;
    }

    public Date getStart() {
        return start;
    }

    public Date getEnd() {
        return end;
    }
}
Please also consider the drag-and-drop related attributes dropHoverStyleClass, dropActiveStyleClass, dropAccept, dropScope. They are similar to corresponding attributes in p:droppable. There is only one difference - the datasource attribute is not needed. p:droppable in PrimeFaces has this attribute which represents an ID of an UIData component to connect with. I have never understood why we need this. It is possible to get the UIData component with a simple trick. The reading below is for advanced developers.

The drop callback from the jQuery Droppable gets an ui object with a reference to the draggable element. So we can look for a closest DOM element which is a container for an UIData component.
var params = [];

// Check if draggable is within a data iteration component.
// Note for PrimeFaces team: an additional unified style class ".ui-data" for all UIData components would be welcome here!
var uiData = ui.draggable.closest(".ui-datatable, .ui-datagrid, .ui-datalist, .ui-carousel");
if (uiData.length > 0) {
    params.push({
        name: this.id + '_uiDataId',
        value: uiData.attr('id')
    });
}

params.push({
    name: this.id + '_dragId',
    value: ui.draggable.attr('id')
});

// call the drop listener
this.getBehavior("drop").call(this, evt, {params: params, ...});
In the component itself, the current dragged object is extracted by this snippet
FacesContext context = FacesContext.getCurrentInstance();
Map<String, String> params = context.getExternalContext().getRequestParameterMap();
String clientId = this.getClientId(context);

Object data = null;
String dragId = params.get(clientId + "_dragId");
String uiDataId = params.get(clientId + "_uiDataId");

if (dragId != null && uiDataId != null) {
    // draggable is within a data iteration component
    UIDataContextCallback contextCallback = new UIDataContextCallback(dragId);
    context.getViewRoot().invokeOnComponent(context, uiDataId, contextCallback);
    data = contextCallback.getData();
}

TimelineDragDropEvent te = new TimelineDragDropEvent(this, behaviorEvent.getBehavior(), ... dragId, data);
The JSF standard method invokeOnComponent does the job. Now the data object is available in TimelineDragDropEvent. The class UIDataContextCallback is a simple implementation of the JSF ContextCallback interface.
public class UIDataContextCallback implements ContextCallback {

    private String dragId;
    private Object data;

    public UIDataContextCallback(String dragId) {
        this.dragId = dragId;
    }

    public void invokeContextCallback(FacesContext fc, UIComponent component) {
        UIData uiData = (UIData) component;
        String[] idTokens = dragId.split(String.valueOf(UINamingContainer.getSeparatorChar(fc)));
        int rowIndex = Integer.parseInt(idTokens[idTokens.length - 2]);
        uiData.setRowIndex(rowIndex);
        data = uiData.getRowData();
        uiData.setRowIndex(-1);
    }

    public Object getData() {
        return data;
    }
}

1 comment:

  1. The information written in the article is descriptive and well written.It is also simple to read and understand.Good Read.
    Prestashop Theme Developer

    ReplyDelete