Base solution for your next web application
Starts in:
01 DAYS
01 HRS
01 MIN
01 SEC

Activities of "Ricavir"

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.

  1. Yes I'm in a single database
  2. PublishAsync works for a specific user BUT only when fired from an AppService linked to the current tenant. It doesn't work when notification is fired from a background worker (then from host side) > in this case, getting a user from userrepository doesn't work.
  3. I injected INotificationSubscriptionManager and tenant users have subscription (like it is the case also for existing NewUser notification)

I've tested many things from host side (and from background worker) and tenant side (and from AppService). I can report the following behavior :

  • From host side (background worker)
    • Sending notification to specific tenant doesn't work. Even if setting current unit of work to target tenant
    • Sending notification to specific user doesn't work neither
  • From tenant side (AppService)
    • Sending notification to tenant (and therfore to any subscribed user) doesn't work. I even try to set tenantId to null when publishing notification...
    • Sending notification directly to a specific user works !

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.

Showing 251 to 260 of 325 entries