Here it is :
timeline.component.ts : wraps javascript for horizontal timeline
import { Component, AfterViewInit, Injector, Input, Output, EventEmitter, ViewEncapsulation } from '@angular/core';
import { AppComponentBase } from '@shared/common/app-component-base';
import { TimelineItemDto } from '@shared/service-proxies/service-proxies';
import * as moment from 'moment';
@Component({
selector: 'timeline',
styles: ['./timeline.component.css'],
templateUrl: './timeline.component.html',
encapsulation: ViewEncapsulation.None
})
export class TimelineComponent extends AppComponentBase implements AfterViewInit {
@Input()
timelineItems: TimelineItemDto[];
@Input()
selectedItemId: number = 0;
selectedItemIdAfterInit: number = 0;
@Output()
timelineItemClicked = new EventEmitter<TimelineItemDto>();
constructor(
injector: Injector
) {
super(injector);
}
ngAfterViewInit(): void {
var timelines = $('.cd-horizontal-timeline');
this.selectedItemIdAfterInit = this.selectedItemId;
(timelines.length > 0 && this.timelineItems.length > 0) && initTimeline(timelines, this.timelineItems, this.selectedItemId);
}
timelineItemClick(item: TimelineItemDto) {
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');
this.selectedItemIdAfterInit = $(this).get(0).id;
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.format("DD/MM/YYYY");
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, "");
}
timeline.component.html :
<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}">
<div>
<i class="{{timelineItem.icon}}"></i>
</div>
<div>
{{timelineItem.dateDisplay | date:'dd MMM'}}
</div>
</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 btn btn-danger"> test
<i class="fa fa-chevron-left"></i>
</a>
</li>
<li>
<a href="#0" class="next btn btn-danger">
<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"><i class="{{timelineItem.icon}}"></i> {{timelineItem.title}}</h2>
</div>
<div class="mt-author">
<div class="mt-author-name">
<a class="font-blue-madison">{{timelineItem.creatorName}}</a>
</div>
<div class="mt-author-datetime font-grey-mint">{{timelineItem.dateDisplay | date:'short'}}</div>
</div>
<div class="mt-content border-grey-steel">
<p>{{timelineItem.description}}</p>
<a href="javascript:;" class="btn btn-primary" (click)="timelineItemClick(timelineItem)">{{timelineItem.buttonText}}</a>
</div>
</li>
</ol>
</div>
</div>
and timeline.css to be added to angular.cli CSS imports
/***
Horizontal Timeline
***/
/* PLUGIN CSS */
.cd-horizontal-timeline {
opacity: 0;
-webkit-transition: opacity 0.2s;
-moz-transition: opacity 0.2s;
transition: opacity 0.2s; }
.cd-horizontal-timeline::before {
/* never visible - this is used in jQuery to check the current MQ */
content: 'mobile';
display: none; }
.cd-horizontal-timeline.loaded {
/* show the timeline after events position has been set (using JavaScript) */
opacity: 1; }
.cd-horizontal-timeline .timeline {
position: relative;
height: 140px;
width: 90%;
max-width: 800px;
margin: 0 auto; }
.cd-horizontal-timeline .events-wrapper {
position: relative;
height: 100%;
margin: 0 40px;
overflow: hidden; }
.cd-horizontal-timeline .events-wrapper::after, .cd-horizontal-timeline .events-wrapper::before {
/* these are used to create a shadow effect at the sides of the timeline */
content: '';
position: absolute;
z-index: 2;
top: 0;
height: 100%;
width: 20px; }
.cd-horizontal-timeline .events-wrapper::before {
left: 0;
background-image: -webkit-linear-gradient(left, #f8f8f8, rgba(248, 248, 248, 0));
background-image: linear-gradient(to right, #f8f8f8, rgba(248, 248, 248, 0)); }
.cd-horizontal-timeline .events-wrapper::after {
right: 0;
background-image: -webkit-linear-gradient(right, #f8f8f8, rgba(248, 248, 248, 0));
background-image: linear-gradient(to left, #f8f8f8, rgba(248, 248, 248, 0)); }
.cd-horizontal-timeline .events {
/* this is the grey line/timeline */
position: absolute;
z-index: 1;
left: 0;
top: 69px;
height: 2px;
/* width will be set using JavaScript */
background: #dfdfdf;
-webkit-transition: -webkit-transform 0.4s;
-moz-transition: -moz-transform 0.4s;
transition: transform 0.4s; }
.cd-horizontal-timeline .filling-line {
/* this is used to create the green line filling the timeline */
position: absolute;
z-index: 1;
left: 0;
top: 0;
height: 100%;
width: 100%;
background-color: #f4516c;
-webkit-transform: scaleX(0);
-moz-transform: scaleX(0);
-ms-transform: scaleX(0);
-o-transform: scaleX(0);
transform: scaleX(0);
-webkit-transform-origin: left center;
-moz-transform-origin: left center;
-ms-transform-origin: left center;
-o-transform-origin: left center;
transform-origin: left center;
-webkit-transition: -webkit-transform 0.3s;
-moz-transition: -moz-transform 0.3s;
transition: transform 0.3s; }
.cd-horizontal-timeline .events a {
position: absolute;
bottom: 0;
z-index: 2;
text-align: center;
font-size: 1rem;
padding-bottom: 15px;
color: [#383838](https://support.aspnetzero.com/QA/Questions/383838);
/* fix bug on Safari - text flickering while timeline translates */
-webkit-transform: translateZ(0);
-moz-transform: translateZ(0);
-ms-transform: translateZ(0);
-o-transform: translateZ(0);
transform: translateZ(0); }
.cd-horizontal-timeline .events a::after {
/* this is used to create the event spot */
content: '';
position: absolute;
left: 50%;
right: auto;
-webkit-transform: translateX(-50%);
-moz-transform: translateX(-50%);
-ms-transform: translateX(-50%);
-o-transform: translateX(-50%);
transform: translateX(-50%);
bottom: -5px;
height: 12px;
width: 12px;
border-radius: 50%;
-webkit-transition: background-color 0.3s, border-color 0.3s;
-moz-transition: background-color 0.3s, border-color 0.3s;
transition: background-color 0.3s, border-color 0.3s; }
.no-touch .cd-horizontal-timeline .events a:hover::after {
background-color: #7b9d6f;
border-color: #7b9d6f; }
.cd-horizontal-timeline .events a.selected {
pointer-events: none; }
@media only screen and (min-width: 1100px) {
.cd-horizontal-timeline::before {
/* never visible - this is used in jQuery to check the current MQ */
content: 'desktop'; } }
.cd-timeline-navigation a {
/* these are the left/right arrows to navigate the timeline */
position: absolute;
z-index: 1;
top: 50%;
bottom: auto;
-webkit-transform: translateY(-50%);
-moz-transform: translateY(-50%);
-ms-transform: translateY(-50%);
-o-transform: translateY(-50%);
transform: translateY(-50%);
height: 34px;
width: 34px;
border-radius: 50%;
/* replace text with an icon */
overflow: hidden;
text-indent: 100%;
white-space: nowrap;
-webkit-transition: border-color 0.3s;
-moz-transition: border-color 0.3s;
transition: border-color 0.3s; }
.cd-timeline-navigation a.prev {
left: 0; }
.cd-timeline-navigation a.next {
right: 0; }
.cd-timeline-navigation a.inactive {
cursor: not-allowed; }
.cd-timeline-navigation a.inactive::after {
background-position: 0 -16px; }
.cd-horizontal-timeline .events-content {
position: relative;
width: 100%;
margin: 10px 0 0 0;
overflow: hidden;
-webkit-transition: height 0.4s;
-moz-transition: height 0.4s;
transition: height 0.4s; }
.cd-horizontal-timeline .events-content > ol > li {
position: absolute;
z-index: 1;
width: 100%;
left: 0;
top: 0;
-webkit-transform: translateX(-100%);
-moz-transform: translateX(-100%);
-ms-transform: translateX(-100%);
-o-transform: translateX(-100%);
transform: translateX(-100%);
padding: 0;
opacity: 0;
-webkit-animation-duration: 0.4s;
-moz-animation-duration: 0.4s;
animation-duration: 0.4s;
-webkit-animation-timing-function: ease-in-out;
-moz-animation-timing-function: ease-in-out;
animation-timing-function: ease-in-out; }
.cd-horizontal-timeline .events-content > ol > li.selected {
/* visible event content */
position: relative;
z-index: 2;
opacity: 1;
-webkit-transform: translateX(0);
-moz-transform: translateX(0);
-ms-transform: translateX(0);
-o-transform: translateX(0);
transform: translateX(0); }
.cd-horizontal-timeline .events-content > ol > li.enter-right, .cd-horizontal-timeline .events-content > ol > li.leave-right {
-webkit-animation-name: cd-enter-right;
-moz-animation-name: cd-enter-right;
animation-name: cd-enter-right; }
.cd-horizontal-timeline .events-content > ol > li.enter-left, .cd-horizontal-timeline .events-content > ol > li.leave-left {
-webkit-animation-name: cd-enter-left;
-moz-animation-name: cd-enter-left;
animation-name: cd-enter-left; }
.cd-horizontal-timeline .events-content > ol > li.leave-right, .cd-horizontal-timeline .events-content > ol > li.leave-left {
-webkit-animation-direction: reverse;
-moz-animation-direction: reverse;
animation-direction: reverse; }
.cd-horizontal-timeline .events-content > ol > li {
margin: 0 auto; }
.cd-horizontal-timeline .events-content em {
display: block;
font-style: italic;
margin: 10px auto; }
.cd-horizontal-timeline .events-content em::before {
content: '- '; }
@-webkit-keyframes cd-enter-right {
0% {
opacity: 0;
-webkit-transform: translateX(100%); }
100% {
opacity: 1;
-webkit-transform: translateX(0%); } }
@-moz-keyframes cd-enter-right {
0% {
opacity: 0;
-moz-transform: translateX(100%); }
100% {
opacity: 1;
-moz-transform: translateX(0%); } }
@keyframes cd-enter-right {
0% {
opacity: 0;
-webkit-transform: translateX(100%);
-moz-transform: translateX(100%);
-ms-transform: translateX(100%);
-o-transform: translateX(100%);
transform: translateX(100%); }
100% {
opacity: 1;
-webkit-transform: translateX(0%);
-moz-transform: translateX(0%);
-ms-transform: translateX(0%);
-o-transform: translateX(0%);
transform: translateX(0%); } }
@-webkit-keyframes cd-enter-left {
0% {
opacity: 0;
-webkit-transform: translateX(-100%); }
100% {
opacity: 1;
-webkit-transform: translateX(0%); } }
@-moz-keyframes cd-enter-left {
0% {
opacity: 0;
-moz-transform: translateX(-100%); }
100% {
opacity: 1;
-moz-transform: translateX(0%); } }
@keyframes cd-enter-left {
0% {
opacity: 0;
-webkit-transform: translateX(-100%);
-moz-transform: translateX(-100%);
-ms-transform: translateX(-100%);
-o-transform: translateX(-100%);
transform: translateX(-100%); }
100% {
opacity: 1;
-webkit-transform: translateX(0%);
-moz-transform: translateX(0%);
-ms-transform: translateX(0%);
-o-transform: translateX(0%);
transform: translateX(0%); } }
/* METRONIC EXTENDED CSS */
.mt-timeline-horizontal {
font-size: 14px; }
.mt-timeline-horizontal ol, .mt-timeline-horizontal ul {
list-style: none; }
.mt-timeline-horizontal blockquote, .mt-timeline-horizontal q {
quotes: none; }
.mt-timeline-horizontal blockquote:before, .mt-timeline-horizontal blockquote:after,
.mt-timeline-horizontal q:before, .mt-timeline-horizontal q:after {
content: '';
content: none; }
.mt-timeline-horizontal table {
border-collapse: collapse;
border-spacing: 0; }
.mt-timeline-horizontal .timeline {
width: 100%;
max-width: 100%; }
.mt-timeline-horizontal .timeline:before {
background: transparent; }
.mt-timeline-horizontal .timeline .events-wrapper .events a:after {
background-color: #fff;
border: 2px solid; }
.mt-timeline-horizontal .timeline .events-wrapper .events a.selected:after {
background-color: #fff !important; }
.mt-timeline-horizontal .timeline .events-wrapper .events a:hover, .mt-timeline-horizontal .timeline .events-wrapper .events a:focus {
text-decoration: none; }
.mt-timeline-horizontal .timeline .events-wrapper:before, .mt-timeline-horizontal .timeline .events-wrapper:after {
background-image: none; }
.mt-timeline-horizontal .timeline .mt-ht-nav-icon li a {
border-radius: 50% !important; }
.mt-timeline-horizontal .timeline .mt-ht-nav-icon li a i {
position: absolute;
top: 50%;
left: 8px;
transform: translateX(50%) translateY(-50%);
width: 10px; }
.mt-timeline-horizontal .timeline .mt-ht-nav-icon li:first-child a i {
left: 5px; }
.mt-timeline-horizontal .timeline.mt-timeline-square .events a:after {
border-radius: 0 !important; }
.mt-timeline-horizontal .timeline.mt-timeline-square .mt-ht-nav-icon li a {
border-radius: 0 !important; }
.mt-timeline-horizontal .events-content ol {
padding: 0; }
.mt-timeline-horizontal .events-content ol li .mt-title {
margin-top: 15px;
float: left;
width: 60%; }
.mt-timeline-horizontal .events-content ol li .mt-title h2 {
margin: 0;
opacity: 0.8 ;
filter: alpha(opacity=80) ;
font-size: 18px;
font-weight: 600; }
.mt-timeline-horizontal .events-content ol li .mt-author {
float: right;
position: relative;
text-align: right;
width: 40%; }
.mt-timeline-horizontal .events-content ol li .mt-author > .mt-avatar {
float: right;
margin-left: 15px; }
.mt-timeline-horizontal .events-content ol li .mt-author > .mt-author-name {
margin-top: 5px; }
.mt-timeline-horizontal .events-content ol li .mt-author > .mt-author-name a {
opacity: 0.9 ;
filter: alpha(opacity=90) ;
font-size: 15px;
font-weight: 600; }
.mt-timeline-horizontal .events-content ol li .mt-author > .mt-author-name a:hover, .mt-timeline-horizontal .events-content ol li .mt-author > .mt-author-name a:focus {
text-decoration: none; }
.mt-timeline-horizontal .events-content ol li .mt-author > .mt-author-datetime {
font-size: 13px; }
.mt-timeline-horizontal .events-content ol li .mt-author > .mt-avatar {
width: 50px;
height: 50px;
border-radius: 50% !important;
overflow: hidden; }
.mt-timeline-horizontal .events-content ol li .mt-author > .mt-avatar > img {
width: 100%;
height: auto; }
.mt-timeline-horizontal .events-content ol li .mt-content {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid;
clear: both;
line-height: 1.7em; }
.mt-timeline-horizontal .events-content ol li .mt-content > p {
opacity: 0.7 ;
filter: alpha(opacity=70) ; }
.mt-timeline-horizontal .events-content ol li .mt-content img.pull-left {
margin: 0 15px 15px 0; }
.mt-timeline-horizontal .events-content ol li .mt-content img.pull-right {
margin: 0 0 15px 15px; }
.mt-timeline-horizontal .events-content ol li .mt-content .btn-group .dropdown-menu {
margin-right: 5px; }
@media (max-width: 480px) {
.mt-timeline-horizontal .events-content ol li .mt-title {
width: 100%; }
.mt-timeline-horizontal .events-content ol li .mt-author {
width: 100%;
margin-top: 15px;
text-align: left; }
.mt-timeline-horizontal .events-content ol li .mt-author > .mt-avatar {
float: left;
margin-right: 15px;
margin-left: 0; }
.mt-timeline-horizontal .events-content ol li .mt-author > .mt-author-name {
margin-top: 10px; }
.mt-timeline-horizontal .btn.pull-right {
float: none !important;
margin: 0 !important; } }
Hi @ismcagdas,
I managed to make it work again. It was mainly css stuff to be reintegrated and adapted to Metronic 5.
My horizontal-timeline component was not the same as yours. I completely wrap it in an angular component. I've also added missing css file for horizontal-timeline in angular.cli
If you're interested, I can share it
Cheers
Hi,
While using Metronic 4 with angular and asp.net core, I've built a specific component for horizontal-timeline. It was working great. I've upgraded my project with latest aspnetzero version and with metronic 5. In metronic 5, it is not available in the demo templates.
I also noticed that you removed it from aspnetzero dashboard.
Do you know if this component will be available soon for metronic 5 ?
Is it possible to get the horizontal-timeline work again (with metronic 4 assets) and how will you do it ?
Any advice would be welcome :)
Github issue created : [https://github.com/aspnetzero/aspnet-zero-core/issues/614])
I've tested a lot of cases. Are you sure that it is possible to send notifications to all users of a specific tenant from host side ? Because in this case, AbpSession.TenantId is always null... and that's probably why it's not working.
I need to find a solution to overcome this issue ; this feature is a must have for my customer.
No, nothing in database. Only notifications that I've sent directly to specific user and that I've received on UI are on database.
I've tested many things from host side (and from background worker) and tenant side (and from AppService). I can report the following behavior :
Is it an issue due to data filters or authorization or other thing that I've missed ? Have you try to send a notification to a tenant and to all subscribed user ?
Ok, this information is helping.
So, going back to my goal (send notification from backgroundworker), notifications are still not sent. I don't have any exception in the stack trace.
Here is the code of my notification :
public async Task SchedulerNewEventCreatedAsync(int? tenantId, int eventCount)
{
var notificationData = new LocalizableMessageNotificationData(
new LocalizableString(
"SchedulerNewEventCreatedNotificationMessage",
LogisavConsts.LocalizationSourceName
)
);
notificationData["eventCount"] = eventCount;
await _notificationPublisher.PublishAsync(AppNotificationNames.SchedulerNewEventCreated, notificationData, severity: NotificationSeverity.Success, tenantIds: new[] { tenantId });
}
This code should send a notification to all users of selected tenant (at least users who have subscribed to this type of notification)
Here is the code on backgroundworker :
[UnitOfWork]
protected override void DoWork()
{
var tenantsForEventSchedule = _tenantRepository.GetAllList(tenant => tenant.IsActive);
foreach (var tenant in tenantsForEventSchedule)
{
try
{
if (AsyncHelper.RunSync(() => SettingManager.GetSettingValueForTenantAsync<bool>(AppSettings.TenantEvent.Scheduler_Activated, tenant.Id)) == true)
{
//define period to search contracts
var today = Clock.Now.Date;
int dayCountBeforeEventCreation = AsyncHelper.RunSync(() => SettingManager.GetSettingValueForTenantAsync<int>(AppSettings.TenantEvent.Scheduler_DayCount_BeforeEventCreation, tenant.Id));
var searchDate = today.AddDays(dayCountBeforeEventCreation);
bool includeSuspendedContracts = AsyncHelper.RunSync(() => SettingManager.GetSettingValueForTenantAsync<bool>(AppSettings.TenantEvent.Scheduler_SuspendedContractIncluded, tenant.Id));
var contractsFound = _contractScheduleRepository
.GetAll()
.Include(eq => eq.Equipments)
.Include(c => c.Contract)
.Where(cs => cs.Contract.IsActive == true)
.WhereIf(!includeSuspendedContracts, cs => cs.Contract.IsSuspended == false)
.Where(cs => cs.ScheduleDate <= searchDate).ToList();
int eventsCreatedCount = 0;
foreach (var contractSchedule in contractsFound)
{
using (_unitOfWorkManager.Current.SetTenantId(tenant.Id))
{
Event _event = new Event();
_event.AddressId = contractSchedule.Contract.AddressId;
_event.ContractId = contractSchedule.Contract.Id;
_event.ContractScheduleId = contractSchedule.Id;
_event.TenantId = tenant.Id;
//
SOME ENTITY PROPERTY FILLING
//
//SuccessiveId
AsyncHelper.RunSync(() => _eventManager.GenerateSuccessiveId(_event));
AsyncHelper.RunSync(() => _eventRepository.InsertAsync(_event));
eventsCreatedCount++;
}
//Save change after each loop
_unitOfWorkManager.Current.SaveChanges();
}
if (eventsCreatedCount > 0)
{
AsyncHelper.RunSync(() => _appNotifier.SchedulerNewEventCreatedAsync(tenant.Id, eventsCreatedCount));
}
}
}
}
catch (Exception exception)
{
Logger.Error($"Event scheduler of tenant {tenant.TenancyName} has rised an exception !");
Logger.Error(exception.Message, exception);
}
}
No notification is sent from that piece of code. Therefore, I've tried from a basic AppService like :
[AbpAuthorize(AppPermissions.Pages_Tenant_InputDatas_Create)]
protected async Task CreateAccountingCode(AccountingCodeEditDto input)
{
var accountingCodeVar = input.MapTo<AccountingCode>();
//Check duplicate code
if (_accountingCodeRepository.GetAll().Where(c => c.Code == accountingCodeVar.Code).Count() > 0)
throw new UserFriendlyException(L("DuplicatedCode_Error_Message"), L("DuplicatedCode_Error_Detail", accountingCodeVar.Code));
await _accountingCodeRepository.InsertAsync(accountingCodeVar);
_unitOfWorkManager.Current.SaveChanges();
await _appNotifier.SchedulerNewEventCreatedAsync(AbpSession.TenantId, 30);
}
But, again, nothing received.... and no exception in stack trace. Sorry for all that code, but I'm really blocked right now :(
Hi @aaron
1 - Current user is not an host user. 2 - I've added _unitOfWorkManager.Current.SaveChanges() before sending notification... and it works ! But why am I obliged to add it ? 3 - Here is the full stack trace error without _unitOfWorkManager.Current.SaveChanges():
ERROR 2017-11-24 15:12:47,152 [14 ] Mvc.ExceptionHandling.AbpExceptionFilter - Can not set TenantId to 0 for IMustHaveTenant entities!
Abp.AbpException: Can not set TenantId to 0 for IMustHaveTenant entities!
à Abp.EntityFrameworkCore.AbpDbContext.CheckAndSetMustHaveTenantIdProperty(Object entityAsObj)
à Abp.EntityFrameworkCore.AbpDbContext.ApplyAbpConcepts()
à Abp.EntityFrameworkCore.AbpDbContext.<SaveChangesAsync>d__36.MoveNext()
--- Fin de la trace de la pile à partir de l'emplacement précédent au niveau duquel l'exception a été levée ---
à System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
à System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
à Abp.EntityFrameworkCore.Uow.EfCoreUnitOfWork.<SaveChangesInDbContextAsync>d__20.MoveNext()
--- Fin de la trace de la pile à partir de l'emplacement précédent au niveau duquel l'exception a été levée ---
à System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
à System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
à Abp.EntityFrameworkCore.Uow.EfCoreUnitOfWork.<SaveChangesAsync>d__12.MoveNext()
--- Fin de la trace de la pile à partir de l'emplacement précédent au niveau duquel l'exception a été levée ---
à System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
à System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
à Abp.Notifications.NotificationStore.<InsertNotificationAsync>d__8.MoveNext()
--- Fin de la trace de la pile à partir de l'emplacement précédent au niveau duquel l'exception a été levée ---
à System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
à System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
à Abp.Threading.InternalAsyncHelper.<AwaitTaskWithPostActionAndFinally>d__1.MoveNext()
--- Fin de la trace de la pile à partir de l'emplacement précédent au niveau duquel l'exception a été levée ---
à System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
à System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
à System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)
à Abp.Notifications.NotificationPublisher.<PublishAsync>d__12.MoveNext()
--- Fin de la trace de la pile à partir de l'emplacement précédent au niveau duquel l'exception a été levée ---
à System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
à System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
à Abp.Threading.InternalAsyncHelper.<AwaitTaskWithPostActionAndFinally>d__1.MoveNext()
--- Fin de la trace de la pile à partir de l'emplacement précédent au niveau duquel l'exception a été levée ---
à System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
à System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
à System.Runtime.CompilerServices.TaskAwaiter.GetResult()
à ELEVEN_SOFT.Logisav.Notifications.AppNotifier.<SendMessageAsync>d__5.MoveNext()
--- Fin de la trace de la pile à partir de l'emplacement précédent au niveau duquel l'exception a été levée ---
à System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
à System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
à System.Runtime.CompilerServices.TaskAwaiter.GetResult()
à ELEVEN_SOFT.Logisav.InputData.AccountingCodeAppService.<CreateAccountingCode>d__10.MoveNext()
--- Fin de la trace de la pile à partir de l'emplacement précédent au niveau duquel l'exception a été levée ---
à System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
à System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
à System.Runtime.CompilerServices.TaskAwaiter.GetResult()
à ELEVEN_SOFT.Logisav.InputData.AccountingCodeAppService.<CreateOrUpdateAccountingCode>d__8.MoveNext()
--- Fin de la trace de la pile à partir de l'emplacement précédent au niveau duquel l'exception a été levée ---
à System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
à System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
à Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeActionMethodAsync>d__27.MoveNext()
--- Fin de la trace de la pile à partir de l'emplacement précédent au niveau duquel l'exception a été levée ---
à System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
à System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
à Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeNextActionFilterAsync>d__25.MoveNext()
--- Fin de la trace de la pile à partir de l'emplacement précédent au niveau duquel l'exception a été levée ---
à Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)
à Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
à Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeNextExceptionFilterAsync>d__24.MoveNext()
Hi @ismcagdas,
Here is a simple test in AppService :
[AbpAuthorize(AppPermissions.Pages_Tenant_InputDatas_Create)]
protected async Task CreateAccountingCode(AccountingCodeEditDto input)
{
var accountingCodeVar = input.MapTo<AccountingCode>();
//Check duplicate code
if (_accountingCodeRepository.GetAll().Where(c => c.Code == accountingCodeVar.Code).Count() > 0)
throw new UserFriendlyException(L("DuplicatedCode_Error_Message"), L("DuplicatedCode_Error_Detail", accountingCodeVar.Code));
await _accountingCodeRepository.InsertAsync(accountingCodeVar);
await _appNotifier.SendMessageAsync((await UserManager.GetUserByIdAsync((long)AbpSession.UserId)).ToUserIdentifier(), "test");
}
As you can see, I just try to send a message with SendMessageAsync notification.
How can I share my code for you to test ? With a ZIP file of whole project or by taking control of my PC with a teamviewer session for instance ? I would prefer by a teamviewer session.