Base solution for your next web application
Open Closed

Metronic #1819


User avatar
0
joshyates created

I'm creating a prototype and want to use some of the Metronic placeholders on my dashboard.

<a class="postlink" href="http://keenthemes.com/preview/metronic/theme/admin_4/dashboard_3.html">http://keenthemes.com/preview/metronic/ ... ard_3.html</a>

After loading this page, I view source and copy the html tags I need. Most work, but the Timeline does not load correctly. It is the 4 row down, Activities and Events. When I inspect using Google Chrome, I don't get any error messages.

Any advice would be appreciated. Thanks.


15 Answer(s)
  • User Avatar
    0
    joshyates created

    Disregard. I forgot the ../assets/global/plugins/horizontal-timeline/horizontal-timeline.js

    Any advice on how to convert this to a .min.js?

  • User Avatar
    0
    hikalkan created
    Support Team

    Hi,

    You can use bundler & minifier visual studio extension <a class="postlink" href="https://visualstudiogallery.msdn.microsoft.com/9ec27da7-e24b-4d56-8064-fd7e88ac1c40(">https://visualstudiogallery.msdn.micros ... e88ac1c40(</a>) to minify css files.

  • User Avatar
    0
    Ricavir created

    Hello, I'm facing the same problem... as I am very new with javascript, can you please precise where to add ../assets/global/plugins/horizontal-timeline/horizontal-timeline.js ?

    Is it on angular-cli.json within "scripts" list ?

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi @Ricavir,

    Yes, you should put it in angular-cli.json's scripts list. Also be sure that this file is already in metronic folders. As I remember it is not included by default. You should copy it from metronic to your angular project's either "external_libs" folder or "/src/assets/metronic/" folder.

  • User Avatar
    0
    Ricavir created

    Hi,

    I try and tried again. no way, I don't manage to display this horizontal timeline :? I added "../src/assets/metronic/global/plugins/horizontal-timeline/horizontal-timeline.js" on the scripts list of angular-cli.json and grabbed the javascript file on metronic site.

    Do I have to add something else ? I really need to display this timeline for my customer :cry:

    Any idea ?

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi,

    Do you have any javascript error or it just does not work ? Does your timeline html element has "cd-horizontal-timeline" css class ?

  • User Avatar
    0
    Ricavir created

    I just have one warning on tooltip.directive.js:65 but not related with timeline: "tooltipPlacement was deprecated, please use placement instead" I have the "cd-horizontal-timeline" css class in my html element (since I get HTML source code from metronic web site) And, of course, I added the line "../src/assets/metronic/global/plugins/horizontal-timeline/horizontal-timeline.js" to scripts list in angular-cli.json ; and put the javascript file "horizontal-timeline.js" in the right folder. But still nothing displayed : I just have the portlet and title displayed in the page, nothing inside.

  • User Avatar
    0
    Ricavir created

    An other clue : I've tested something directly in css file > I changed initial opacity from 0 to 1. Timeline is displayed but items are not placed and buttons not working. I can see in the browser debugger that the javascript file "horizontal-timeline.js" is loaded.

    But the javascript is still not "linked" to the page...I don't understand why.

  • User Avatar
    0
    Ricavir created

    Still working on this issue : the javascript initialisation is done before HTML file is loaded therefore, timeline component is not wired to javascript. This is why nothing is displayed because opacity is set to 0 by default.

    Do you know a workarround/tip to make this component work ? I didn't test all metronic components, but it might the case with other ones.

    <cite>JoshYates: </cite> I forgot the ../assets/global/plugins/horizontal-timeline/horizontal-timeline.js

    @JoshYates : can you please precise if your timeline is working with an angular2 project ?

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi,

    I have checked the code for horozontal-timeline.js and it is not a jqueyr plugin. So it works before your html is ready.

    1- To fix this problem, you can convert it to an angular directive 2- Or you can wrap it in a function of your component and call it in ngAfterViewInit.

  • User Avatar
    0
    Ricavir created

    Ok, tks for the hint ! I will do this and keep informed.

  • User Avatar
    0
    Ricavir created

    Hi,

    I choose the first solution : convert timeline with angular directive. I started from here : <a class="postlink" href="https://github.com/BlackBoys/angular-horizontal-timeline">https://github.com/BlackBoys/angular-ho ... l-timeline</a>

    I added the file timeline-directive.js (and added a reference to it on angular.cli) :

    (function () {
        appModule
        .directive('timeLine', function ($timeout) {
            return {
                restrict: 'A',
                link: function (scope, element, attrs) {
    
                    //angular.element(element).css('border', '5px solid red');
                    scope.$on('LastElem', function (event) {
    
                        var timelines = element;
                        eventsMinDistance = timelines.data('spacing');
                        var timeline = timelines,
                            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(scope.nodes);
                        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');
                        scope.$watch(
                            // This function returns the value being watched. It is called for each turn of the $digest loop
                            function () {
                                return scope.nodes.length;
                            },
                            // This is the change listener, called when the value returned from the above function changes
                            function (newValue, oldValue) {
                                if (newValue !== oldValue) {
                                    console.log('change');
                                    $timeout(function () {
                                        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');
                                        setDatePosition(timelineComponents, eventsMinDistance);
                                        timelineTotWidth = setTimelineWidth(timelineComponents, eventsMinDistance);
                                    });
    
                                    // Only increment the counter if the value changed
                                    //scope.foodCounter = scope.foodCounter + 1;
                                }
                            }
                        );
    
                        //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');
                            }
                        });
    
    
                    });
                }
    
            }
        })
        .directive('timeLineLi', function ($timeout) {
            return {
                restrict: 'A',
                link: function (scope, element, attrs) {
                    console.dir(scope);
                    if (scope.$last) {
                        $timeout(function () {
                            scope.$emit('LastElem');
                        });
                    }
                    //scope.$watch('thing', function () {
                    //    var r = (Math.random() * 255).toFixed(0);
                    //    var g = (Math.random() * 255).toFixed(0);
                    //    var b = (Math.random() * 255).toFixed(0);
                    //    angular.element(element).css('color', 'rgb(' + r + ',' + g + ',' + b + ')');
                    //});
                }
    
            }
        });
    })();
    

    I also changed horizontal-timeline.js like that :

    function initTimeline(timelines) {
    	    timelines.each(function () {
    	        console.info("passage timeline");
    			eventsMinDistance = $(this).data('spacing');
    			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, "");
    	}
    

    But I'm having a javascript reference error coming from timeline-directive.js : "appModule is not defined"

    Do you see something wrong in this approach ?

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi,

    It seems like you have created a directive for angular1. Sorry, it is my fault that I didn't direct you very well to solution.

    You need to create a component because you are using angular2 version. You can create a similar one to <a class="postlink" href="https://github.com/aspnetzero/aspnet-zero-core/blob/dev/angular/src/app/shared/common/timing/date-range-picker.component.ts">https://github.com/aspnetzero/aspnet-ze ... mponent.ts</a>

    Please let me know if you face any other problems.

  • User Avatar
    0
    Ricavir created

    Hello,

    Just wanted to share the horizontal-timeline integration with Angular2. I added input/output decorators to the component to make it more generic and re-usable. Also created a specific object for timeline items.

    Here is the HTML component :

    <div class="cd-horizontal-timeline mt-timeline-horizontal" data-spacing="60">
        <div class="timeline">
            <div class="events-wrapper">
                <div class="events">
                    <ol>
                        <li *ngFor="let timelineItem of timelineItems">
                            <a href="#0" id="{{timelineItem.id}}" class="border-after-red bg-after-red " [ngClass]="{'selected' : timelineItem.id===selectedItemId}">{{timelineItem.title}}</a>
                        </li>
                    </ol>
                    <span class="filling-line bg-red" aria-hidden="true"></span>
                </div>
                
            </div>
            
            <ul class="cd-timeline-navigation mt-ht-nav-icon">
                <li>
                    <a href="#0" class="prev inactive btn btn-outline red md-skip">
                        <i class="fa fa-chevron-left"></i>
                    </a>
                </li>
                <li>
                    <a href="#0" class="next btn btn-outline red md-skip">
                        <i class="fa fa-chevron-right"></i>
                    </a>
                </li>
            </ul>
            
        </div>
        
        <div class="events-content">
            <ol>
                <li *ngFor="let timelineItem of timelineItems" [ngClass]="{'selected' : timelineItem.id===selectedItemId}" id="{{timelineItem.id}}">
                    <div class="mt-title">
                        <h2 class="mt-content-title">{{timelineItem.title}}</h2>
                    </div>
                    <div class="mt-author">
                        <p class="btn btn-circle btn-icon-only blue">
                            <i class="{{timelineItem.icon}}"></i>
                        </p>
                        <div class="mt-author-name">
                            <a href="javascript:;" class="font-blue-madison">{{timelineItem.title}}</a>
                        </div>
                        <div class="mt-author-datetime font-grey-mint">{{timelineItem.date}}</div>
                    </div>
                    <div class="clearfix"></div>
                    <div class="mt-content border-grey-steel">
                        <p>
                            {{timelineItem.description}}
                        </p>                    
                        <a href="javascript:;" class="btn btn-circle btn-icon-only blue" (click)="timelineItemClick(timelineItem)">
                            <i class="fa fa-plus"></i>
                        </a>
                    </div>
                </li>                       
            </ol>
        </div>
        
    </div>
    

    The typescript file for horizontal timeline :

    import { Component, AfterViewInit, Injector, Input, Output, EventEmitter, ViewEncapsulation } from '@angular/core';
    import { AppComponentBase } from '@shared/common/app-component-base';
    import { TimelineItem } from './timeline-item';
    import * as moment from 'moment';
    
    @Component({
        selector: 'timeline',
        templateUrl: './timeline.component.html',
        encapsulation: ViewEncapsulation.None
    })
    export class TimelineComponent extends AppComponentBase implements AfterViewInit {
         
        @Input()
        timelineItems: TimelineItem[];
        @Input()
        selectedItemId: number = 0;
    
        @Output()
        timelineItemClicked = new EventEmitter<TimelineItem>();
    
        constructor(
            injector: Injector
        ) {
            super(injector);
        }
    
        ngAfterViewInit(): void {        
            var timelines = $('.cd-horizontal-timeline');
            (timelines.length > 0 && this.timelineItems.length > 0) && initTimeline(timelines, this.timelineItems, this.selectedItemId);
        }
    
        timelineItemClick(item: TimelineItem) {
            this.timelineItemClicked.emit(item);
        }
    }
    
    
    function initTimeline(timelines, items, selectedItemId) {
        timelines.each(function () {
            var eventsMinDistance = $(this).data('spacing');
            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(items);
            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, selectedItemId);
            //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', eventsMinDistance);
            });
            //detect click on the prev arrow
            timelineComponents['timelineNavigation'].on('click', '.prev', function (event) {
                event.preventDefault();
                updateSlide(timelineComponents, timelineTotWidth, 'prev', eventsMinDistance);
            });
            //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, eventsMinDistance) {
        //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, undefined);
    }
    
    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).toString();
        var scaleValue = Number(eventLeft) / totWidth;
        setTransformValue(filling.get(0), 'scaleX', scaleValue);
    }
    
    function setDatePosition(timelineComponents, min) {
        for (var 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, selectedItemId) {
        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#' + selectedItemId), timelineComponents['fillingLine'], totalWidth);
        updateTimelinePosition('next', timelineComponents['eventsWrapper'].find('a#' + selectedItemId), timelineComponents);
    
        return totalWidth;
    }
    
    function updateVisibleContent(event, eventsContent) {
        var eventId = event[0].id,
            visibleContent = eventsContent.find('.selected'),
            selectedContent = eventsContent.find('li#' + eventId),
            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];
            var translateValue = timelineTranslate.split(',')[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 = [];
        for (var i = 0; i < events.length; i++) {       
            var dateComp = events[i].date;
            var dayComp = dateComp.split('/');
            var 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 (var 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, "");
    }
    

    The timeline item object :

    export class TimelineItem {
    
        id: number;
        date: string;
        title: string;
        description: string;
        icon: string;
    }
    

    Then, just add it to your HTML template like this example :

    <div class="portlet light margin-bottom-0">
                <div class="portlet-body">
                    <timeline [timelineItems]="items" [selectedItemId]="selectedItemId" (timelineItemClicked)="timelineItemClicked($event)"></timeline>
                </div>
            </div>
    

    Hope this can help others ;)

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi,

    Thank you for your great effort and share :)