Grouping events can be useful for example when showing availability of multiple people, rooms, or other resources next to each other.
Documentation<style type="text/css"> 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> <h:form id="form"> <p:growl id="growl" showSummary="true" showDetail="false"> <p:autoUpdate /> </p:growl> <p:timeline id="timeline" value="#{groupingTimelineView.model}" var="order" varGroup="truck" editable="true" eventMargin="0" eventMarginAxis="0" stackEvents="false" orientationAxis="top" widgetVar="timelineWdgt"> <p:ajax event="changed" update="@none" listener="#{groupingTimelineView.onChange}"/> <p:ajax event="delete" update="@none" listener="#{groupingTimelineView.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 #{truck.code}" 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 #{groupingTimelineView.selectedOrder} </strong> <p/> <p:selectManyMenu id="overlappedOrders" value="#{groupingTimelineView.ordersToMerge}" showCheckbox="true"> <f:selectItems value="#{groupingTimelineView.overlappedOrders}" var="order" itemLabel=" Order #{order.data.number}" itemValue="#{order}"/> <sc:convertOrder events="#{groupingTimelineView.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="#{groupingTimelineView.merge}" oncomplete="PF('overlapEventsWdgt').hide()"/> <p:commandButton type="button" value="Close" onclick="PF('overlapEventsWdgt').hide()"/> </h:panelGroup> </f:facet> </p:dialog> </h:form>
@Named("groupingTimelineView") @ViewScoped public class GroupingTimelineView implements Serializable { private TimelineModel<Order, Truck> 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() { // create timeline model model = new TimelineModel<>(); // create groups TimelineGroup<Truck> group1 = new TimelineGroup<>("id1", new Truck("10")); TimelineGroup<Truck> group2 = new TimelineGroup<>("id2", new Truck("11")); TimelineGroup<Truck> group3 = new TimelineGroup<>("id3", new Truck("12")); TimelineGroup<Truck> group4 = new TimelineGroup<>("id4", new Truck("13")); TimelineGroup<Truck> group5 = new TimelineGroup<>("id5", new Truck("14")); TimelineGroup<Truck> group6 = new TimelineGroup<>("id6", new Truck("15")); // add groups to the model model.addGroup(group1); model.addGroup(group2); model.addGroup(group3); model.addGroup(group4); model.addGroup(group5); model.addGroup(group6); int orderNumber = 1; // iterate over groups for (int j = 1; j <= 6; 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() * 4)); String imagePath = null; if (Math.random() < 0.25) { imagePath = "images/timeline/box.png"; } Order order = new Order(orderNumber, imagePath); model.add(new TimelineEvent<>(order, startDate, endDate, true, "id" + j)); orderNumber++; referenceDate = endDate; } } } public TimelineModel<Order, Truck> 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; } public static class Truck implements java.io.Serializable { private final String code; public Truck(String code) { this.code = code; } public String getCode() { return code; } } }
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; } }
@FacesConverter("org.primefaces.showcase.converter.OrderConverter") public class OrderConverter implements Converter<TimelineEvent<Order>>, Serializable { 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; } }