This example demonstrate nested groups that are 3 levels deep.
<style>
div.vis-item-content {
padding: 4px;
border-radius: 2px;
-moz-border-radius: 2px;
}
div.vis-item.vis-item-range {
border-width: 0;
}
#overlappedOrders {
margin-top: 20px;
width: 100%;
}
#overlappedOrders .ui-chkbox {
vertical-align: middle;
margin: 3px 5px;
}
</style>
<div class="card">
<h:form id="form">
<p:growl id="growl" showSummary="true" showDetail="false">
<p:autoUpdate/>
</p:growl>
<p:timeline id="timeline" value="#{nestedGroupingTimelineView.model}" var="order" varGroup="truck"
editable="true" eventMargin="0" eventMarginAxis="0" stackEvents="false"
orientationAxis="top" widgetVar="timelineWdgt">
<p:ajax event="changed" update="@none" listener="#{nestedGroupingTimelineView.onChange}"/>
<p:ajax event="delete" update="@none" listener="#{nestedGroupingTimelineView.onDelete}"/>
<p:ajax event="add" update="@none" onstart="PF('timelineWdgt').cancelAdd()"/>
<f:facet name="group">
<h:graphicImage library="demo" name="images/timeline/truck.png" style="vertical-align:middle;"
alt="Truck"/>
<h:outputText value="#{truck}" style="font-weight:bold;"/>
</f:facet>
<h:graphicImage library="demo" name="#{order.imagePath}" rendered="#{not empty order.imagePath}"
style="display:inline; vertical-align:middle;" alt="Order"/>
<h:outputText value="Order #{order.number}"/>
</p:timeline>
<!-- Dialog with overlapped timeline events -->
<p:dialog id="overlapEventsDlg" header="Overlapped Orders" widgetVar="overlapEventsWdgt"
showEffect="clip" hideEffect="clip">
<h:panelGroup id="overlappedOrdersInner" layout="block" style="padding:10px;">
<strong>
Please choose Orders you want to merge with the Order #{nestedGroupingTimelineView.selectedOrder}
</strong>
<p/>
<p:selectManyMenu id="overlappedOrders" value="#{nestedGroupingTimelineView.ordersToMerge}"
showCheckbox="true">
<f:selectItems value="#{nestedGroupingTimelineView.overlappedOrders}" var="order"
itemLabel=" Order #{order.data.number}" itemValue="#{order}"/>
<sc:convertOrder events="#{nestedGroupingTimelineView.model.events}"/>
</p:selectManyMenu>
</h:panelGroup>
<f:facet name="footer">
<h:panelGroup layout="block" style="text-align:right; padding:2px; white-space:nowrap;">
<p:commandButton value="Merge" process="overlapEventsDlg" update="@none"
action="#{nestedGroupingTimelineView.merge}"
oncomplete="PF('overlapEventsWdgt').hide()"/>
<p:commandButton type="button" value="Close" onclick="PF('overlapEventsWdgt').hide()"/>
</h:panelGroup>
</f:facet>
</p:dialog>
</h:form>
</div>
package org.primefaces.showcase.view.data.timeline;
import jakarta.annotation.PostConstruct;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.Month;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import io.quarkus.runtime.annotations.RegisterForReflection;
import org.primefaces.PrimeFaces;
import org.primefaces.component.timeline.TimelineUpdater;
import org.primefaces.event.timeline.TimelineModificationEvent;
import org.primefaces.model.timeline.TimelineEvent;
import org.primefaces.model.timeline.TimelineGroup;
import org.primefaces.model.timeline.TimelineModel;
import org.primefaces.showcase.domain.Order;
@Named("nestedGroupingTimelineView")
@ViewScoped
@RegisterForReflection(serialization = true)
public class NestedGroupingTimelineView implements Serializable {
private TimelineModel<Order, String> model;
private TimelineEvent<Order> event; // current changed event
private List<TimelineEvent<Order>> overlappedOrders; // all overlapped orders (events) to the changed order (event)
private List<TimelineEvent<Order>> ordersToMerge; // selected orders (events) in the dialog which should be merged
@PostConstruct
protected void initialize() {
model = new TimelineModel<>();
// create nested groups
TimelineGroup<String> group1 = new TimelineGroup<>("groupId1", "Truck Group Level 1", "groupId1", 1,
Arrays.asList("groupId2", "id1", "id2", "id5", "id6"));
TimelineGroup<String> group2 = new TimelineGroup<>("groupId2", "Truck Group Level 2", "groupId2", 2,
Arrays.asList("id3", "id4"));
TimelineGroup<String> group3 = new TimelineGroup<>("id1", "Truck 1", 2);
TimelineGroup<String> group4 = new TimelineGroup<>("id2", "Truck 2", 2);
TimelineGroup<String> group5 = new TimelineGroup<>("id3", "Truck 3", 3);
TimelineGroup<String> group6 = new TimelineGroup<>("id4", "Truck 4", 3);
TimelineGroup<String> group7 = new TimelineGroup<>("id5", "Truck 5", 2);
TimelineGroup<String> group8 = new TimelineGroup<>("id6", "Truck 6", 2);
TimelineGroup<String> group9 = new TimelineGroup<>("groupId3", "Truck Group Level 1", "groupId3", 1,
Arrays.asList("id7", "id8", "id9"));
TimelineGroup<String> group10 = new TimelineGroup<>("id7", "Truck 7", 2);
TimelineGroup<String> group11 = new TimelineGroup<>("id8", "Truck 8", 2);
TimelineGroup<String> group12 = new TimelineGroup<>("id9", "Truck 9", 2);
// add nested groups to the model
model.addGroup(group1);
model.addGroup(group2);
model.addGroup(group3);
model.addGroup(group4);
model.addGroup(group5);
model.addGroup(group6);
model.addGroup(group7);
model.addGroup(group8);
model.addGroup(group9);
model.addGroup(group10);
model.addGroup(group11);
model.addGroup(group12);
int orderNumber = 1;
// iterate over groups
for (int j = 1; j <= 12; j++) {
LocalDateTime referenceDate = LocalDateTime.of(2015, Month.DECEMBER, 14, 8, 0);
// iterate over events in the same group
for (int i = 0; i < 6; i++) {
LocalDateTime startDate = referenceDate.plusHours(3 * (Math.random() < 0.2 ? 1 : 0));
LocalDateTime endDate = startDate.plusHours(2 + (int) Math.floor(Math.random() * 3));
String imagePath = null;
if (Math.random() < 0.25) {
imagePath = "images/timeline/box.png";
}
Order order = new Order(orderNumber, imagePath);
model.add(TimelineEvent.<Order>builder()
.data(order)
.startDate(startDate)
.endDate(endDate)
.editable(true)
.group("id" + j)
.build());
orderNumber++;
referenceDate = endDate;
}
}
}
public TimelineModel<Order, String> getModel() {
return model;
}
public void onChange(TimelineModificationEvent<Order> e) {
// get changed event and update the model
event = e.getTimelineEvent();
model.update(event);
// get overlapped events of the same group as for the changed event
Set<TimelineEvent<Order>> overlappedEvents = model.getOverlappedEvents(event);
if (overlappedEvents == null) {
// nothing to merge
return;
}
// list of orders which can be merged in the dialog
overlappedOrders = new ArrayList<>(overlappedEvents);
// no pre-selection
ordersToMerge = null;
// update the dialog's content and show the dialog
PrimeFaces primefaces = PrimeFaces.current();
primefaces.ajax().update("form:overlappedOrdersInner");
primefaces.executeScript("PF('overlapEventsWdgt').show()");
}
public void onDelete(TimelineModificationEvent<Order> e) {
// keep the model up-to-date
model.delete(e.getTimelineEvent());
}
public void merge() {
// merge orders and update UI if the user selected some orders to be merged
if (ordersToMerge != null && !ordersToMerge.isEmpty()) {
model.merge(event, ordersToMerge, TimelineUpdater.getCurrentInstance(":form:timeline"));
} else {
FacesMessage msg
= new FacesMessage(FacesMessage.SEVERITY_INFO, "Nothing to merge, please choose orders to be merged", null);
FacesContext.getCurrentInstance().addMessage(null, msg);
}
overlappedOrders = null;
ordersToMerge = null;
}
public int getSelectedOrder() {
if (event == null) {
return 0;
}
return event.getData().getNumber();
}
public List<TimelineEvent<Order>> getOverlappedOrders() {
return overlappedOrders;
}
public List<TimelineEvent<Order>> getOrdersToMerge() {
return ordersToMerge;
}
public void setOrdersToMerge(List<TimelineEvent<Order>> ordersToMerge) {
this.ordersToMerge = ordersToMerge;
}
}
package org.primefaces.showcase.domain;
import io.quarkus.runtime.annotations.RegisterForReflection;
@RegisterForReflection
public class Order implements java.io.Serializable {
private final int number;
private final String imagePath;
public Order(int number, String imagePath) {
this.number = number;
this.imagePath = imagePath;
}
public int getNumber() {
return number;
}
public String getImagePath() {
return imagePath;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Order order = (Order) o;
return number == order.number;
}
@Override
public int hashCode() {
return number;
}
}
package org.primefaces.showcase.convert;
import java.io.Serializable;
import java.util.List;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.convert.Converter;
import jakarta.faces.convert.FacesConverter;
import jakarta.inject.Named;
import org.primefaces.model.timeline.TimelineEvent;
import org.primefaces.showcase.domain.Order;
@Named
@ApplicationScoped
@FacesConverter("org.primefaces.showcase.converter.OrderConverter")
public class OrderConverter implements Converter<TimelineEvent<Order>>, Serializable {
private static final long serialVersionUID = 1L;
private List<TimelineEvent<Order>> events;
public OrderConverter() {
}
@Override
public TimelineEvent<Order> getAsObject(FacesContext context, UIComponent component, String value) {
if (value == null || value.isEmpty() || events == null || events.isEmpty()) {
return null;
}
for (TimelineEvent<Order> event : events) {
if (event.getData().getNumber() == Integer.valueOf(value)) {
return event;
}
}
return null;
}
@Override
public String getAsString(FacesContext context, UIComponent component, TimelineEvent<Order> value) {
if (value == null) {
return null;
}
return String.valueOf(value.getData().getNumber());
}
public List<TimelineEvent<Order>> getEvents() {
return events;
}
public void setEvents(List<TimelineEvent<Order>> events) {
this.events = events;
}
}