When moving one timeline, the other moves along. Use mouse scroll wheel for zoom-in and zoom-out. A click on an event in the first timeline selects an event in the second timeline (Project A or Project B).
<script>
//<![CDATA[
function onrangechange1() {
var second = PF('timelineSecondWdgt');
if (second.jq.data("rangeFired")) {
second.jq.data("rangeFired", null);
return;
}
var first = PF('timelineFirstWdgt');
var range = first.getVisibleRange();
first.jq.data("rangeFired", true);
PF('timelineSecondWdgt').setVisibleRange(range.start, range.end);
}
function onrangechange2() {
var first = PF('timelineFirstWdgt');
if (first.jq.data("rangeFired")) {
first.jq.data("rangeFired", null);
return;
}
var second = PF('timelineSecondWdgt');
var range = second.getVisibleRange();
second.jq.data("rangeFired", true);
PF('timelineFirstWdgt').setVisibleRange(range.start, range.end);
}
//]]>
</script>
<h:form id="form">
<p:growl id="growl" showSummary="true" showDetail="false">
<p:autoUpdate/>
</p:growl>
<div class="card">
<h5 style="margin-top: 0">Events</h5>
<p:timeline id="timelineFirst" value="#{linkedTimelinesView.modelFirst}" var="task"
height="250px" widgetVar="timelineFirstWdgt"
start="#{linkedTimelinesView.start}" end="#{linkedTimelinesView.end}">
<p:ajax event="rangechange" process="@none" onstart="onrangechange1(); return false;"/>
<p:ajax event="select" listener="#{linkedTimelinesView.onSelect}"/>
<h:panelGroup layout="block" rendered="#{not task.period}" style="padding-bottom:5px">
<h:outputText value="#{task.title}"/>
</h:panelGroup>
<p:graphicImage library="demo" name="#{task.imagePath}" height="26px"/>
<h:panelGroup rendered="#{task.period}" style="padding:8px">
<h:outputText value="#{task.title}"/>
</h:panelGroup>
</p:timeline>
</div>
<div class="card">
<h5>Projects</h5>
<p:timeline id="timelineSecond" value="#{linkedTimelinesView.modelSecond}"
height="150px" widgetVar="timelineSecondWdgt">
<p:ajax event="rangechange" process="@none" onstart="onrangechange2(); return false;"/>
</p:timeline>
</div>
</h:form>
@Named("linkedTimelinesView")
@ViewScoped
public class LinkedTimelinesView implements Serializable {
private TimelineModel<Task, ?> modelFirst; // model of the first timeline
private TimelineModel<String, ?> modelSecond; // model of the second timeline
private boolean aSelected; // flag if the project A is selected (for test of select() call on the 2. model)
private LocalDateTime start = LocalDate.of(2015, 8, 22).atStartOfDay();
private LocalDateTime end = LocalDate.of(2015, 9, 4).atStartOfDay();
@PostConstruct
public void init() {
createFirstTimeline();
createSecondTimeline();
}
private void createFirstTimeline() {
modelFirst = new TimelineModel<>();
modelFirst.add(TimelineEvent.<Task>builder()
.data(new Task("Mail from boss", "images/timeline/mail.png", false))
.startDate(LocalDateTime.of(2015, 8, 22, 17, 30))
.build());
modelFirst.add(TimelineEvent.<Task>builder()
.data(new Task("Call back my boss", "images/timeline/callback.png", false))
.startDate(LocalDateTime.of(2015, 8, 23, 23, 0))
.build());
modelFirst.add(TimelineEvent.<Task>builder()
.data(new Task("Travel to Spain", "images/timeline/location.png", false))
.startDate(LocalDateTime.of(2015, 8, 24, 21, 45))
.build());
modelFirst.add(TimelineEvent.<Task>builder()
.data(new Task("Do homework", "images/timeline/homework.png", true))
.startDate(LocalDate.of(2015, 8, 26))
.endDate(LocalDate.of(2015, 9, 2))
.build());
modelFirst.add(TimelineEvent.<Task>builder()
.data(new Task("Create memo", "images/timeline/memo.png", false))
.startDate(LocalDate.of(2015, 8, 28))
.build());
modelFirst.add(TimelineEvent.<Task>builder()
.data(new Task("Create report", "images/timeline/report.png", true))
.startDate(LocalDate.of(2015, 8, 31))
.endDate(LocalDate.of(2015, 9, 3))
.build());
// duplicate events with 6 month-shift to check for potential daylight-saving-issues
new ArrayList<TimelineEvent<Task>>(modelFirst.getEvents()).forEach(e -> {
modelFirst.add(TimelineEvent.<Task>builder()
.data(e.getData())
.startDate(e.getStartDate().minusMonths(6))
.endDate(e.getEndDate() == null ? null : e.getEndDate().minusMonths(6))
.build());
});
}
private void createSecondTimeline() {
modelSecond = new TimelineModel<>();
modelSecond.add(TimelineEvent.<String>builder()
.data("Project A")
.startDate(LocalDate.of(2015, 8, 23))
.endDate(LocalDate.of(2015, 8, 30))
.build());
modelSecond.add(TimelineEvent.<String>builder()
.data("Project B")
.startDate(LocalDate.of(2015, 8, 27))
.endDate(LocalDate.of(2015, 8, 31))
.build());
}
public void onSelect(TimelineSelectEvent<?> e) {
// get a thread-safe TimelineUpdater instance for the second timeline
TimelineUpdater timelineUpdater = TimelineUpdater.getCurrentInstance(":form:timelineSecond");
if (aSelected) {
// select project B visually (index in the event's list is 1)
timelineUpdater.select("projectB");
}
else {
// select project A visually (index in the event's list is 0)
timelineUpdater.select("projectA");
}
aSelected = !aSelected;
FacesMessage msg = new FacesMessage(FacesMessage.SEVERITY_INFO,
"Selected project: " + (aSelected ? "A" : "B"), null);
FacesContext.getCurrentInstance().addMessage(null, msg);
}
public TimelineModel<Task, ?> getModelFirst() {
return modelFirst;
}
public TimelineModel<String, ?> getModelSecond() {
return modelSecond;
}
public LocalDateTime getStart() {
return start;
}
public void setStart(LocalDateTime start) {
this.start = start;
}
public LocalDateTime getEnd() {
return end;
}
public void setEnd(LocalDateTime end) {
this.end = end;
}
public class Task implements Serializable {
private final String title;
private final String imagePath;
private final boolean period;
public Task(String title, String imagePath, boolean period) {
this.title = title;
this.imagePath = imagePath;
this.period = period;
}
public String getTitle() {
return title;
}
public String getImagePath() {
return imagePath;
}
public boolean isPeriod() {
return period;
}
}
}