Bootstrap Visual Timeline Component

Trabajando en proyectos y aplicaciones, comúnmente me encuentro buscando código antiguo para encontrar piezas para copiar y reutilizar – especialmente cuando se trata de UI / UX -. Hace aproximadamente unos descubrí Visualstrap (VS) por Avinava Maiti. Él fue capaz de construir un paquete con la mayoría de los componentes Bootstrap 2 amigable con Visualforce lo que hace la construcción de una página con bootstrap simplificada y fácil desde la perspectiva de codificación, he aquí un ejemplo de implementación de un panel con header (después de la implementación de los 2 componentes VS requeridos):

 

Bootstrap:



<div class="panel panel-default">

<div class="panel-heading">

<h3 class="panel-title">Panel title</h3>

    </div>


<div class="panel-body">
        Panel content
    </div>

</div>


Visualstrap:


<vs:panel title="Panel Title" type="default">
    Panel Content
</vs:panel>

Los dos nos da el mismo resultado, pero con menos de codificación en el componente de Visualstrap.

Screen-Shot-2015-05-04-at-8.51.45-AM

El componente que aquí vamos a ver es un Timeline que se puede utilizar en cualquier objeto con una fecha, y dos campos de cadena (título y descripción). El CSS usado para esto es de bootdey.com, escrito por Deyson Bejarano. Se utiliza una clase como el controlador, un componente y un recurso estático para el CSS. En una página de Accounts con un controlador estándar, mostrando las tareas cerradas para esa cuenta. El Timeline se vería así:

 

Screen-Shot-2015-05-05-at-7.01.13-AM

 

Class:


global with sharing class bootTimeline {
 
global string queryString{get;set;}
global string dateString{get;set;}
global string titleString{get;set;}
global string descString{get;set;}
 
global map<string,list<timeline>> getLineMap(){
    map<string,list<timeline>> lineMap = new map<string,list<timeline>>();
 
    for(sObject s:database.query(queryString)){
 
        date thisDate = (date)s.get(dateString);
 
        timeline line = new timeline();
        line.lineMonth = monthYear(thisDate);
        line.lineDate = thisDate;
        line.dayName = dayOfWeek(thisDate);
        line.lineId = (string)s.get('id');
        line.lineTitle = (string)s.get(titleString);
        line.lineDesc = (string)s.get(descString);
 
        if(line.lineDesc.length() > 100)line.lineDesc = line.lineDesc.substring(0,100)+'...';
 
        list<timeline> lines = new list<timeline>();
 
        if(lineMap.get(line.lineMonth) != null)lines.addAll(lineMap.get(line.lineMonth));
 
        lines.add(line);
 
        lineMap.put(line.lineMonth,lines);
 
    }
 
    for(string s:lineMap.keySet()){
 
        list<timeline> lines = lineMap.get(s);
        lines.sort();
        lineMap.put(s,lines);
 
    }
 
    return lineMap;
 
}
 
global string monthYear(date thisDate){
 
    return monthMap.get(thisDate.month())+' '+thisDate.year();
 
}
 
global string dayOfWeek(date thisDate){
 
    date startOfWeek = thisDate.toStartOfWeek();
 
    integer daysApart = startOfWeek.daysBetween(thisDate);
 
    return dayShortMap.get(daysApart);
 
}
 
global final map<integer,string> monthMap = new map<integer,string>{
    1 => 'January',
    2 => 'February',
    3 => 'March',
    4 => 'April',
    5 => 'May',
    6 => 'June',
    7 => 'July',
    8 => 'August',
    9 => 'September',
    10 => 'October',
    11 => 'November',
    12 => 'December'
};
 
global final map<integer,string> dayShortMap = new map<integer,string>{
    0=>'Sun',
    1=>'Mon',
    2=>'Tue',
    3=>'Wed',
    4=>'Thu',
    5=>'Fri',
    6=>'Sat'
};
 
global class timeline implements Comparable{
    global string lineMonth{get;set;}
    global date lineDate{get;set;}
    global string dayName{get;set;}
    global string lineId{get;set;}
    global string lineTitle{get;set;}
    global string lineDesc{get;set;}
 
    global Integer compareTo(Object compareTo) {
        timeline compareToEmp = (timeline)compareTo;
        if (lineDate == compareToEmp.lineDate) return 0;
        if (lineDate > compareToEmp.lineDate) return -1;
        return 1;
    }
}
 
}

Component:


<apex:component controller="bootTimeline">
  
    <apex:attribute name="requireBS" type="boolean" default="true" description="Is bootstrap loaded already"></apex:attribute>
    <apex:attribute name="requireFA" type="boolean" default="true" description="Is font awesome loaded already"></apex:attribute>
    <apex:attribute name="query" type="string" required="true" assignTo="{!queryString}" description="Query for the timeline"></apex:attribute>
    <apex:attribute name="dateField" type="string" required="true" assignTo="{!dateString}" description="Date or DateTime field to base the timeline on"></apex:attribute>
    <apex:attribute name="titleField" type="string" required="true" assignTo="{!titleString}" description="Title field"></apex:attribute>
    <apex:attribute name="descField" type="string" required="true" assignTo="{!descString}" description="Description field"></apex:attribute>
  
    <apex:outputPanel rendered="{!requireBS}">
        <link href="https://netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet"></link>
    </apex:outputPanel>
    <apex:outputPanel rendered="{!requireFA}">
        <link rel="stylesheet" type="text/css" href="https://netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css"/>
    </apex:outputPanel>
  
    <link href="{!URLFOR($Resource.bootTimeline_css)}" rel="stylesheet"></link>
  
	<div class="container">
		<section id="news" class="white-bg padding-top-bottom">
		<div class="container">
			<div class="timeline">
				<apex:repeat value="{!lineMap}" var="monthGroup">
				<div class="date-title">
	        		<span>{!monthGroup}</span>
	        	</div>
				<div class="row">
					<apex:variable value="{!0}" var="counter"></apex:variable>
                	<apex:repeat value="{!lineMap[monthGroup]}" var="line">
						<div class="col-sm-6 news-item {!IF(MOD(counter,2) == 0,'','right')}">
							<div class="news-content">
								<div class="date">
									<p>{!DAY(line.lineDate)}</p>
									<small>{!line.dayName}</small>
	                            </div>
								<h2 class="news-title">{!line.lineTitle}</h2><br/>
								{!line.lineDesc}<br/>
								<a class="read-more" href="/{!line.lineId}" target="_blank">
	                               	Read More
	                            </a>
	                        </div>
						</div>
						<apex:variable value="{!counter+1}" var="counter"></apex:variable>
                    </apex:repeat>
				</div>
                </apex:repeat>
            </div>
        </div>
        </section>
    </div>
</apex:component>

El componente tiene 6 atributos, se requieren 4 que controlan la presentación de los datos y los otros 2 se encargan de cargar el CSS Bootstrap.

 

Query: La consulta del Timeline que se ejecutará, deberá incluir los campos de fecha, título y descripción. La mejor opción es traer estos datos de forma descendente por el campo fecha.

dateField : Es el nombre de la API del campo de fecha

titleField : Este es el nombre de la API del campo de título, puede ser cualquier campo de texto pero debe ser normalmente menos de 80 caracteres como un campo de Name estándar.

descField : Es el nombre de la API del campo de descripción, puede ser cualquier campo de texto. Los primeros 100 caracteres se mostrarán en la pantalla.

Una vez que se construye lo anterior, (en este caso yo muestro el status y no la descripción) la línea de tiempo se puede implementar en una página VF por la línea siguiente:


<apex:page standardController="Account" standardStylesheets="true" sidebar="false" docType="html-5.0">

	<c:bootTimeline query="SELECT id, Subject, Status, ActivityDate FROM task WHERE WhatId = '{!Account.Id}' ORDER BY ActivityDate desc" dateField="ActivityDate" titleField="Subject" descField="Status">
	</c:bootTimeline>

</apex:page>

Por último un poco largo pero aquí esta el CSS.




<style>

	.timeline{
	    position:relative;
	    margin-bottom:100px;
	    z-index:1;
	}

	.timeline:before{
	    display:block;
	    content:"";
	    position:absolute;
	    width:50%;
	    height:100%;
	    left:1px;
	    top:0;
	    border-right:1px solid #5CC9DF;
	    z-index:-1;
	} 

	.timeline:after{
	    display:block;
	    content:"";
	    position:absolute;
	    width:50%;
	    height:100px;
	    left:1px;
	    bottom:-105px;
	    border-right:1px dashed #5CC9DF;
	    z-index:-1;
	} 

	.timeline .date-title{
	    text-align:center;
	    margin:70px 0 50px;
	}

	.timeline .date-title span{
	    padding:15px 30px;
	    font-size:21px;
	    font-weight:400;
	    color:#fff;
	    background:#5CC9DF;
	    border-radius:5px;
	}

	.news-item {
	    padding-bottom:45px;
	}

	.news-item.right {
	    float:right;
	    margin-top:40px;
	}

	.news-item .news-content {
	    margin:20px 30px 0 0;
	    position:relative;
	    padding:30px;
	    padding-left:100px;
	    background:#f5f5f5;
	    border-radius:10px;
	    box-shadow:-5px 5px 0 rgba(0,0,0,0.08);
	    -webkit-transition:all .3s ease-out;
	    transition:all .3s ease-out;
	}

	.news-item:hover .news-content {
	    background:#5CC9DF;
	    color:#fff;
	}

	.news-item.right .news-content {
	    margin:20px 0 0 30px;
	    box-shadow:5px 5px 0 rgba(0,0,0,0.08);
	}

	.news-item .news-content:after {
	    display:block;
	    content:"";
	    position:absolute;
	    top:50px;
	    right:-40px;
	    width:0px;
	    height:0px;
	    background:transparent;
	    border:20px solid transparent;
	    border-left:20px solid #f5f5f5;
	    -webkit-transition:border-left-color .3s ease-out;
	    transition:border-left-color .3s ease-out;
	}

	.news-item.right .news-content:after {
	    position:absolute;
	    left:-40px;
	    right:auto;
	    border-left:20px solid transparent;
	    border-right:20px solid #f5f5f5;
	    -webkit-transition:border-right-color .3s ease-out;
	    transition:border-right-color .3s ease-out;
	}

	.news-item:hover .news-content:after {
	    border-left-color:#5CC9DF;
	}

	.news-item.right:hover .news-content:after {
	    border-left-color:transparent;
	    border-right-color:#5CC9DF;
	}

	.news-item .news-content:before {
	    display:block;
	    content:"";
	    position:absolute;
	    width:20px;
	    height:20px;
	    right:-55px;
	    top:60px;
	    background:#5CC9DF;
	    border:3px solid #fff;
	    border-radius:50%;
	    -webkit-transition:background .3s ease-out;
	    transition:background .3s ease-out;
	}

	.news-item.right .news-content:before {
	    left:-55px;
	    right:auto;
	}

	.news-content .date {
	    position:absolute;
	    width:80px;
	    height:80px;
	    left:10px;
	    text-align:center;
	    color:#5CC9DF;
	    -webkit-transition:color .3s ease-out;
	    transition:color .3s ease-out;
	}

	.news-item:hover .news-content .date {
	    color:#fff;
	}

	.news-content .date p{
	    margin:0;
	    font-size:48px;
	    font-weight:600;
	    line-height:48px;
	}

	.news-content .date small{
	    margin:0;
	    font-size:26px;
	    font-weight:300;
	    line-height:24px;
	}

	.news-content .news-title{
	    font-size:24px;
	    font-weight:300;
	}

	.news-content p{
	    font-size:16px;
	    line-height:24px;
	    font-weight:300;
	    letter-spacing:0.02em;
	    margin-bottom:10px;
	}

	.news-content .read-more,
	.news-content .read-more:hover,
	.news-content .read-more:active,
	.news-content .read-more:focus{
	    padding:10px 0;
	    text-decoration:none;
	    font-size:16px;
	    color:#7A7C7F;
	    line-height:24px;
	}

	.news-item:hover .news-content .read-more,
	.news-item:hover .news-content .read-more:hover,
	.news-item:hover .news-content .read-more:active,
	.news-item:hover .news-content .read-more:focus{
	    color:#fff;
	}

	.news-content .read-more{
	    -webkit-transition:padding .3s ease-out;
	    transition:padding .3s ease-out;
	}

	.news-content .read-more:hover {
	    padding-left:7px;
	}

	.news-content .read-more:after{
	    content:'\f054';
	    padding-left:15px;
	    font-family:'FontAwesome';
	    font-size:21px;
	    line-height:21px;
	    color:#5CC9DF;
	    vertical-align:middle;
	    -webkit-transition:padding .3s ease-out;
	    transition:padding .3s ease-out;
	}

	.news-content .read-more:hover:after{
	    padding-left:20px;
	}

	.news-item:hover .news-content .read-more:after{
	    color:#fff;
	}

	.news-content .news-media{
	    position:absolute;
	    width:80px;
	    bottom:-45px;
	    right:40px;
	    border-radius:8px;
	}

	.news-content .news-media img{
	    border-radius:8px;
	    transform:scale(1);
	    -webkit-transition:-webkit-transform .3s ease-out;
	    transition:transform .3s ease-out;
	}

	.news-content .news-media a{
	    display:block;
		text-decoration:none;
	    background:#fff;
	    border-radius:8px;
	    overflow:hidden;
	    -webkit-mask-image: -webkit-radial-gradient(circle, white, black);
	}

	.news-content .news-media a:hover img{
	    -webkit-transform:scale(1.3);
	    transform:scale(1.3);
	}

	.news-content .news-media a:after{
	    content:'\f065';
	    position:absolute;
	    width:100%;
	    top:0;
	    left:0;
	    font-family:FontAwesome;
	    font-size:32px;
	    line-height:80px;
	    text-align:center;
	    color:#5CC9DF;
	    -webkit-transform:scale(0);
	    transform:scale(0);
	    opacity:0;
	    -webkit-transition:all .2s ease-out .1s;
	    transition:all .2s ease-out .1s;
	}

	.news-content .news-media.video a:after{
	    content:'\f04b';
	}

	.news-content .news-media a:hover:after{
	    -webkit-transform:scale(1);
	    transform:scale(1);
	    opacity:1;
	}

	.news-content .news-media.gallery{
	    box-shadow:4px 4px 0 #bbb,8px 8px 0 #ddd;
	}
                                    
</style>