timeline

A simple timeline with vue.js.

Author

chris

CSS/scss

@import 'https://fonts.googleapis.com/css?family=Libre+Franklin';

body {
    font-family: 'Libre Franklin', sans-serif;
}

#timeline-header {
    font-size: 26px;
}

.timeline {
    list-style: none;
    padding: 20px 0 20px;
    position: relative;

    &:before {
        background-color: #eee;
        bottom: 0;
        content: " ";
        left: 50px;
        margin-left: -1.5px;
        position: absolute;
        top: 0;
        width: 3px;
    }

    > li {
        margin-bottom: 20px;
        position: relative;

        &:before,
        &:after {
            content: " ";
            display: table;
        }

        &:after {
            clear: both;
        }

        > .timeline-panel {
            border-radius: 2px;
            border: 1px solid #d4d4d4;
            box-shadow: 0 1px 2px rgba(100, 100, 100, 0.2);
            margin-left: 100px;
            padding: 20px;
            position: relative;
          
          .timeline-heading {
            .timeline-panel-controls {
              position: absolute;
              right: 8px;
              top: 5px;
              
              .timestamp {
                display: inline-block;
              }
              
              .controls {
                display: inline-block;
                padding-right: 5px;
                border-right: 1px solid #aaa;
                
                a {
                  color: #999;
                  font-size: 11px;
                  padding: 0 5px;
                  
                  &:hover {
                    color: #333;
                    text-decoration: none;
                    cursor: pointer;
                  }
                }
              }
              
              .controls + .timestamp {
                padding-left: 5px;
              }
            }
          }
        }

        .timeline-badge {
            background-color: #999;
            border-radius: 50%;
            color: #fff;
            font-size: 1.4em;
            height: 50px;
            left: 50px;
            line-height: 52px;
            margin-left: -25px;
            position: absolute;
            text-align: center;
            top: 16px;
            width: 50px;
            z-index: 100;
        }

        .timeline-badge + .timeline-panel {
            &:before {
                border-bottom: 15px solid transparent;
                border-left: 0 solid #ccc;
                border-right: 15px solid #ccc;
                border-top: 15px solid transparent;
                content: " ";
                display: inline-block;
                position: absolute;
                left: -15px;
                right: auto;
                top: 26px;
            }

            &:after {
                border-bottom: 14px solid transparent;
                border-left: 0 solid #fff;
                border-right: 14px solid #fff;
                border-top: 14px solid transparent;
                content: " ";
                display: inline-block;
                position: absolute;
                left: -14px;
                right: auto;
                top: 27px;
            }
        }
    }
}

.timeline-badge {
    &.primary {
        background-color: #2e6da4 !important;
    }

    &.success {
        background-color: #3f903f !important;
    }

    &.warning {
        background-color: #f0ad4e !important;
    }

    &.danger {
        background-color: #d9534f !important;
    }

    &.info {
        background-color: #5bc0de !important;
    }
}

.timeline-title {
  margin-top: 0;
  color: inherit;
}

.timeline-body {
    > p,
    > ul {
        margin-bottom: 0;
    }

    > p + p {
        margin-top: 5px;
    }
}

.copy {
  position: absolute; 
  top: 5px;
  right: 5px;
  color: #aaa;
  font-size: 11px;
  > * { color: #444; }
}

js

Vue.component('timeline-control', {
    template: '#timeline-control-template',
    
    props: ['control'],
    
    methods: {
        handleClick: function() {
            if(this.control.method == 'delete') {
                this.$dispatch('timeline-delete');
            } else if(this.control.method == 'edit') {
                this.$dispatch('timeline-edit');
            } else {
                console.log("Unknown method "+this.control.method)
            }
        }
    },
});

Vue.component('timeline', {
    template: '#timeline-template',
    
    props: ['items'],
    
    events: {
        'delete-item': function() {
            return true; // forward to parent
        }
    }
});

Vue.component('timeline-item', {
    template: '#timeline-item-template',
    
    props: ['item'],
    
    methods: {
        delete: function() {
            this.$dispatch('timeline-delete-item', this.item.id)
        },
        
        edit: function() {
            
        }
    },
    
    events: {
        'timeline-delete': function() {
            this.delete();
        },
        
        'timeline-edit': function() {
            this.edit();
        }
    }
});

new Vue({
    el: '#app',
    
    data: {
        timeline: [
            {
                id: 5,
                icon_class: 'glyphicon glyphicon-comment',
                icon_status: '',
                title: 'Admin added a comment.',
                controls: [
                    { 
                        method: 'edit', 
                        icon_class: 'glyphicon glyphicon-pencil' 
                    },
                    { 
                        method: 'delete', 
                        icon_class: 'glyphicon glyphicon-trash' 
                    }
                ],
                created: '24. Sep 17:03',
                body: '<p><i>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Totam, maxime alias nam dignissimos natus voluptate iure deleniti. Doloremque, perspiciatis voluptas dignissimos ex, ullam et, reprehenderit similique possimus iste commodi minima fugiat non culpa, veniam temporibus laborum. Distinctio ipsam cupiditate debitis aliquid deleniti consectetur voluptates corporis officiis tempora minus veniam, accusamus cum optio nesciunt illo nulla odio? Quidem nesciunt, omnis at quo aliquam porro amet fugit mollitia minus explicabo, possimus deserunt rem ut commodi laboriosam quia. Numquam, est facilis rem iste voluptatum. Cupiditate porro fuga saepe quis nulla mollitia, magni dicta soluta distinctio tempore voluptate quo perferendis. Maiores eveniet deleniti, nemo.</i></p>'
            },
            {
                id: 4,
                icon_class: 'glyphicon glyphicon-edit',
                icon_status: 'success',
                title: 'Started editing',
                controls: [],
                created: '24. Sep 14:48',
                body: '<p>Someone has started editing.</p>'
            },
            {
                id: 3,
                icon_class: 'glyphicon glyphicon-hand-right',
                icon_status: 'warning',
                title: 'Message delegated',
                controls: [],
                created: '23. Sep 11:12',
                body: '<p>This message has been delegated.</p>'
            },
            {
                id: 2,
                icon_class: 'glyphicon glyphicon-map-marker',
                icon_status: 'danger',
                title: 'Message approved and forwarded',
                controls: [],
                created: '20. Sep 15:56',
                body: '<p>Message has been approved and forwarded to responsible.</p>'
            },
            {
                id: 1,
                icon_class: 'glyphicon glyphicon-map-marker',
                icon_status: '',
                title: 'Message forwarded for approval',
                controls: [],
                created: '19. Sep 19:49',
                body: '<p>Message has been forwarded for approval.</p>'
            },
        ]
    },
        
    events: {
        'timeline-delete-item': function(id) {
            this.timeline = _.remove(this.timeline, function(item) { 
                return item.id != id 
            });
        }
    }
})

html

<div id="app">
    <div class="container">
        <div class="page-header">
            <span id="timeline-header">Timeline</span>
        </div>
        <timeline :items="timeline"></timeline>
    </div>

    <span class="copy">
        Based on an example from <a href="https://codepen.io/betdream">betdream</a>
    </span>
    
    
    
    <template id="timeline-template">
        <ul class="timeline">
            <li 
                v-for="item in items" 
                is="timeline-item" 
                :item="item">
            </li>
        </ul>
    </template>

    <template id="timeline-item-template">
        <li class="timeline-item">
            <div class="timeline-badge {{ item.icon_status }}"><i class="{{ item.icon_class }}"></i></div>
                <div class="timeline-panel">
                    <div class="timeline-heading">
                        <h4 class="timeline-title">{{ item.title }}</h4>
                        <div class="timeline-panel-controls">
                            <div class="controls">
                                <a 
                                   v-for="control in item.controls" 
                                   is="timeline-control" 
                                   :control="control">
                                </a>
                            </div>
                            <div class="timestamp">
                                <small class="text-muted">{{ item.created }}</small>
                            </div>
                        </div>
                    </div>
                    <div class="timeline-body">{{{ item.body }}}</div>
                </div>
            </div>
        </li>
    </template>

    <template id="timeline-control-template">
        <a href="#" @click="handleClick">
            <i class="{{ control.icon_class }}"></i>
        </a>
    </template>
</div>

Demo

See the Pen A simple timeline by chris (@chrgl86) on CodePen.