jQuery(document).ready(function ($) { var timelines = $(".cd-horizontal-timeline"), eventsMinDistance = 60; timelines.length > 0 && initTimeline(timelines); function initTimeline(timelines) { timelines.each(function () { var timeline = $(this), timelineComponents = {}; //cache timeline components timelineComponents["timelineWrapper"] = timeline.find(".events-wrapper"); timelineComponents["eventsWrapper"] = timelineComponents["timelineWrapper"].children(".events"); timelineComponents["fillingLine"] = timelineComponents["eventsWrapper"].children(".filling-line"); timelineComponents["timelineEvents"] = timelineComponents["eventsWrapper"].find("a"); timelineComponents["timelineDates"] = parseDate( timelineComponents["timelineEvents"] ); timelineComponents["eventsMinLapse"] = minLapse( timelineComponents["timelineDates"] ); timelineComponents["timelineNavigation"] = timeline.find( ".cd-timeline-navigation" ); timelineComponents["eventsContent"] = timeline.children(".events-content"); //assign a left postion to the single events along the timeline setDatePosition(timelineComponents, eventsMinDistance); //assign a width to the timeline var timelineTotWidth = setTimelineWidth( timelineComponents, eventsMinDistance ); //the timeline has been initialize - show it timeline.addClass("loaded"); //detect click on the next arrow timelineComponents["timelineNavigation"].on( "click", ".next", function (event) { event.preventDefault(); updateSlide(timelineComponents, timelineTotWidth, "next"); } ); //detect click on the prev arrow timelineComponents["timelineNavigation"].on( "click", ".prev", function (event) { event.preventDefault(); updateSlide(timelineComponents, timelineTotWidth, "prev"); } ); //detect click on the a single event - show new event content timelineComponents["eventsWrapper"].on("click", "a", function (event) { event.preventDefault(); timelineComponents["timelineEvents"].removeClass("selected"); $(this).addClass("selected"); updateOlderEvents($(this)); updateFilling( $(this), timelineComponents["fillingLine"], timelineTotWidth ); updateVisibleContent($(this), timelineComponents["eventsContent"]); }); //on swipe, show next/prev event content timelineComponents["eventsContent"].on("swipeleft", function () { var mq = checkMQ(); mq == "mobile" && showNewContent(timelineComponents, timelineTotWidth, "next"); }); timelineComponents["eventsContent"].on("swiperight", function () { var mq = checkMQ(); mq == "mobile" && showNewContent(timelineComponents, timelineTotWidth, "prev"); }); //keyboard navigation $(document).keyup(function (event) { if (event.which == "37" && elementInViewport(timeline.get(0))) { showNewContent(timelineComponents, timelineTotWidth, "prev"); } else if (event.which == "39" && elementInViewport(timeline.get(0))) { showNewContent(timelineComponents, timelineTotWidth, "next"); } }); }); } function updateSlide(timelineComponents, timelineTotWidth, string) { //retrieve translateX value of timelineComponents['eventsWrapper'] var translateValue = getTranslateValue(timelineComponents["eventsWrapper"]), wrapperWidth = Number( timelineComponents["timelineWrapper"].css("width").replace("px", "") ); //translate the timeline to the left('next')/right('prev') string == "next" ? translateTimeline( timelineComponents, translateValue - wrapperWidth + eventsMinDistance, wrapperWidth - timelineTotWidth ) : translateTimeline( timelineComponents, translateValue + wrapperWidth - eventsMinDistance ); } function showNewContent(timelineComponents, timelineTotWidth, string) { //go from one event to the next/previous one var visibleContent = timelineComponents["eventsContent"].find(".selected"), newContent = string == "next" ? visibleContent.next() : visibleContent.prev(); if (newContent.length > 0) { //if there's a next/prev event - show it var selectedDate = timelineComponents["eventsWrapper"].find(".selected"), newEvent = string == "next" ? selectedDate.parent("li").next("li").children("a") : selectedDate.parent("li").prev("li").children("a"); updateFilling( newEvent, timelineComponents["fillingLine"], timelineTotWidth ); updateVisibleContent(newEvent, timelineComponents["eventsContent"]); newEvent.addClass("selected"); selectedDate.removeClass("selected"); updateOlderEvents(newEvent); updateTimelinePosition(string, newEvent, timelineComponents); } } function updateTimelinePosition(string, event, timelineComponents) { //translate timeline to the left/right according to the position of the selected event var eventStyle = window.getComputedStyle(event.get(0), null), eventLeft = Number(eventStyle.getPropertyValue("left").replace("px", "")), timelineWidth = Number( timelineComponents["timelineWrapper"].css("width").replace("px", "") ), timelineTotWidth = Number( timelineComponents["eventsWrapper"].css("width").replace("px", "") ); var timelineTranslate = getTranslateValue( timelineComponents["eventsWrapper"] ); if ( (string == "next" && eventLeft > timelineWidth - timelineTranslate) || (string == "prev" && eventLeft < -timelineTranslate) ) { translateTimeline( timelineComponents, -eventLeft + timelineWidth / 2, timelineWidth - timelineTotWidth ); } } function translateTimeline(timelineComponents, value, totWidth) { var eventsWrapper = timelineComponents["eventsWrapper"].get(0); value = value > 0 ? 0 : value; //only negative translate value value = !(typeof totWidth === "undefined") && value < totWidth ? totWidth : value; //do not translate more than timeline width setTransformValue(eventsWrapper, "translateX", value + "px"); //update navigation arrows visibility value == 0 ? timelineComponents["timelineNavigation"] .find(".prev") .addClass("inactive") : timelineComponents["timelineNavigation"] .find(".prev") .removeClass("inactive"); value == totWidth ? timelineComponents["timelineNavigation"] .find(".next") .addClass("inactive") : timelineComponents["timelineNavigation"] .find(".next") .removeClass("inactive"); } function updateFilling(selectedEvent, filling, totWidth) { //change .filling-line length according to the selected event var eventStyle = window.getComputedStyle(selectedEvent.get(0), null), eventLeft = eventStyle.getPropertyValue("left"), eventWidth = eventStyle.getPropertyValue("width"); eventLeft = Number(eventLeft.replace("px", "")) + Number(eventWidth.replace("px", "")) / 2; var scaleValue = eventLeft / totWidth; setTransformValue(filling.get(0), "scaleX", scaleValue); } function setDatePosition(timelineComponents, min) { for (i = 0; i < timelineComponents["timelineDates"].length; i++) { var distance = daydiff( timelineComponents["timelineDates"][0], timelineComponents["timelineDates"][i] ), distanceNorm = Math.round(distance / timelineComponents["eventsMinLapse"]) + 2; timelineComponents["timelineEvents"] .eq(i) .css("left", distanceNorm * min + "px"); } } function setTimelineWidth(timelineComponents, width) { var timeSpan = daydiff( timelineComponents["timelineDates"][0], timelineComponents["timelineDates"][ timelineComponents["timelineDates"].length - 1 ] ), timeSpanNorm = timeSpan / timelineComponents["eventsMinLapse"], timeSpanNorm = Math.round(timeSpanNorm) + 4, totalWidth = timeSpanNorm * width; timelineComponents["eventsWrapper"].css("width", totalWidth + "px"); updateFilling( timelineComponents["eventsWrapper"].find("a.selected"), timelineComponents["fillingLine"], totalWidth ); updateTimelinePosition( "next", timelineComponents["eventsWrapper"].find("a.selected"), timelineComponents ); return totalWidth; } function updateVisibleContent(event, eventsContent) { var eventDate = event.data("date"), visibleContent = eventsContent.find(".selected"), selectedContent = eventsContent.find('[data-date="' + eventDate + '"]'), selectedContentHeight = selectedContent.height(); if (selectedContent.index() > visibleContent.index()) { var classEnetering = "selected enter-right", classLeaving = "leave-left"; } else { var classEnetering = "selected enter-left", classLeaving = "leave-right"; } selectedContent.attr("class", classEnetering); visibleContent .attr("class", classLeaving) .one( "webkitAnimationEnd oanimationend msAnimationEnd animationend", function () { visibleContent.removeClass("leave-right leave-left"); selectedContent.removeClass("enter-left enter-right"); } ); eventsContent.css("height", selectedContentHeight + "px"); } function updateOlderEvents(event) { event .parent("li") .prevAll("li") .children("a") .addClass("older-event") .end() .end() .nextAll("li") .children("a") .removeClass("older-event"); } function getTranslateValue(timeline) { var timelineStyle = window.getComputedStyle(timeline.get(0), null), timelineTranslate = timelineStyle.getPropertyValue("-webkit-transform") || timelineStyle.getPropertyValue("-moz-transform") || timelineStyle.getPropertyValue("-ms-transform") || timelineStyle.getPropertyValue("-o-transform") || timelineStyle.getPropertyValue("transform"); if (timelineTranslate.indexOf("(") >= 0) { var timelineTranslate = timelineTranslate.split("(")[1]; timelineTranslate = timelineTranslate.split(")")[0]; timelineTranslate = timelineTranslate.split(","); var translateValue = timelineTranslate[4]; } else { var translateValue = 0; } return Number(translateValue); } function setTransformValue(element, property, value) { element.style["-webkit-transform"] = property + "(" + value + ")"; element.style["-moz-transform"] = property + "(" + value + ")"; element.style["-ms-transform"] = property + "(" + value + ")"; element.style["-o-transform"] = property + "(" + value + ")"; element.style["transform"] = property + "(" + value + ")"; } //based on http://stackoverflow.com/questions/542938/how-do-i-get-the-number-of-days-between-two-dates-in-javascript function parseDate(events) { var dateArrays = []; events.each(function () { var singleDate = $(this), dateComp = singleDate.data("date").split("T"); if (dateComp.length > 1) { //both DD/MM/YEAR and time are provided var dayComp = dateComp[0].split("/"), timeComp = dateComp[1].split(":"); } else if (dateComp[0].indexOf(":") >= 0) { //only time is provide var dayComp = ["2000", "0", "0"], timeComp = dateComp[0].split(":"); } else { //only DD/MM/YEAR var dayComp = dateComp[0].split("/"), timeComp = ["0", "0"]; } var newDate = new Date( dayComp[2], dayComp[1] - 1, dayComp[0], timeComp[0], timeComp[1] ); dateArrays.push(newDate); }); return dateArrays; } function daydiff(first, second) { return Math.round(second - first); } function minLapse(dates) { //determine the minimum distance among events var dateDistances = []; for (i = 1; i < dates.length; i++) { var distance = daydiff(dates[i - 1], dates[i]); dateDistances.push(distance); } return Math.min.apply(null, dateDistances); } /* How to tell if a DOM element is visible in the current viewport? http://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport */ function elementInViewport(el) { var top = el.offsetTop; var left = el.offsetLeft; var width = el.offsetWidth; var height = el.offsetHeight; while (el.offsetParent) { el = el.offsetParent; top += el.offsetTop; left += el.offsetLeft; } return ( top < window.pageYOffset + window.innerHeight && left < window.pageXOffset + window.innerWidth && top + height > window.pageYOffset && left + width > window.pageXOffset ); } function checkMQ() { //check if mobile or desktop device return window .getComputedStyle( document.querySelector(".cd-horizontal-timeline"), "::before" ) .getPropertyValue("content") .replace(/'/g, "") .replace(/"/g, ""); } });