\
69 | <% if (data.read_more_url) { %> \
70 | Read more<\/a> \
71 | <% } %> \
72 | <\/div> \
73 | <\/div> \
74 | \
75 | <\/div> \
76 | <\/div> \
77 | ',
78 | groupMarkerTemplate: '
\
79 |
\
80 |
\
81 |
<%= data.groupDisplay %><\/div> \
82 | <\/div> \
83 | <\/div> \
84 | <\/div> \
85 | ',
86 | buttonTemplate: '
\
87 |
\
88 | active<% } %>" href="#">Expand all<\/span><\/a> \
89 | active<% } %>" href="#">Collapse all<\/span><\/a> \
90 | <\/div> \
91 |
\
92 | active<% } %>" href="#">Newest first<\/span><\/a> \
93 | active<% } %>" href="#">Oldest first<\/span><\/a> \
94 | <\/div> \
95 | <\/div> \
96 | ',
97 | timelineTemplate: '
\
98 |
\
99 |
<\/div> \
100 | <\/div> \
101 | <%= data.posts %> \
102 | <%= data.groups %> \
103 | <\/div> \
104 | ',
105 | loadingTemplate: '
\
106 | Loading... \
107 | <\/div> \
108 | '
109 | };
110 |
111 | var groupingFunctions = {};
112 | /**
113 | * Grouping function by Decade.
114 | */
115 | groupingFunctions.groupSegmentByDecade = function(row, groups, direction) {
116 | var year = row.date.year();
117 | var yearStr = year.toString();
118 | var id = yearStr.slice(0, -1);
119 | var start = moment(id + '0-01-01T00:00:00');
120 | var end = moment(id + '9-12-31T12:59:99');
121 |
122 | if (_.isUndefined(groups[id])) {
123 | groups[id] = {
124 | id: id,
125 | groupDisplay: id + '0s',
126 | timestamp: (direction == 'newest') ? end.unix() : start.unix(),
127 | timestampStart: start.unix(),
128 | timestampEnd: end.unix()
129 | };
130 | }
131 |
132 | return groups;
133 | };
134 |
135 | /**
136 | * Grouping function by year.
137 | */
138 | groupingFunctions.groupSegmentByYear = function(row, groups, direction) {
139 | var year = row.date.year();
140 | var start = moment(year + '-01-01T00:00:00');
141 | var end = moment(year + '-12-31T12:59:99');
142 |
143 | if (_.isUndefined(groups[year.toString()])) {
144 | groups[year.toString()] = {
145 | id: year,
146 | groupDisplay: year,
147 | timestamp: (direction == 'newest') ? end.unix() : start.unix(),
148 | timestampStart: start.unix(),
149 | timestampEnd: end.unix()
150 | };
151 | }
152 |
153 | return groups;
154 | };
155 |
156 | /**
157 | * Grouping function by day.
158 | */
159 | groupingFunctions.groupSegmentByDay = function(row, groups, direction) {
160 | var month = new Date(row.timestamp).getMonth();
161 | var year = new Date(row.timestamp).getFullYear();
162 | var day = new Date(row.timestamp).getDate();
163 | var _month_str = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'June', 'July', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec'];
164 | var _time_start = Date.parse(_month_str[month] + ' ' + day + ', ' + year);
165 | var _time_end = Date.parse(_month_str[month] + ' ' + (day+1) + ', ' + year);
166 | var _id = day + (month + year * 100) * 100;
167 |
168 | groups[_id] = {
169 | id: _id,
170 | groupDisplay: _month_str[month] + ' ' + day + ', ' + year,
171 | timestamp: (direction == 'newest') ? _time_end: _time_start,
172 | timestampStart: _time_start,
173 | timestampEnd: _time_end
174 | };
175 |
176 | return groups;
177 | };
178 |
179 |
180 |
181 | /**
182 | * Base class for timeline
183 | */
184 | var VerticalTimeline = function(el, options) {
185 | this.options = $.extend(true, {}, defaultsOptions, options);
186 |
187 | // Check to see if grouping function is an option
188 | this.options.groupFunction = (!_.isFunction(this.options.groupFunction) && _.isFunction(groupingFunctions[this.options.groupFunction])) ? groupingFunctions[this.options.groupFunction] : this.options.groupFunction;
189 |
190 | // Consistent reference to jquery object
191 | this.$el = $(el);
192 |
193 | // Build templates for performance
194 | this.templates = {};
195 | this.templates.post = _.template(this.options.postTemplate);
196 | this.templates.group = _.template(this.options.groupMarkerTemplate);
197 | this.templates.buttons = _.template(this.options.buttonTemplate);
198 | this.templates.timeline = _.template(this.options.timelineTemplate);
199 | this.templates.loading = _.template(this.options.loadingTemplate);
200 |
201 | // Use custom events to make things happens
202 | this.loadEvents();
203 | this.$el.trigger('vt.build');
204 | };
205 |
206 | /**
207 | * Methods and properties of class
208 | */
209 | _.extend(VerticalTimeline.prototype, {
210 | // Events
211 | events: {
212 | 'vt.build': ['buildLayout'],
213 | 'vt.layoutBuilt': ['getData'],
214 | 'vt.gotData': ['parseData'],
215 | 'vt.parsedData': ['buildTimeline'],
216 | 'vt.builtTimeline': ['loadImages', 'adjustWidth'],
217 | 'vt.loadedImages': ['isotopeIt'],
218 | 'vt.isotopized': ['adjustWidth', 'adjustSpine', 'domEvents']
219 | },
220 |
221 | // Event delegation
222 | loadEvents: function() {
223 | _.each(this.events, function(ea, ename) {
224 | _.each(ea, function(ehandler) {
225 | if (_.isFunction(this[ehandler])) {
226 | this.$el.on(ename, _.bind(this[ehandler], this));
227 | }
228 | }, this);
229 | }, this);
230 | },
231 |
232 | // Initial building
233 | buildLayout: function() {
234 | // Add base class for styling
235 | this.$el.addClass('vertical-timeline-container');
236 |
237 | // Add template layout
238 | this.$el.html(this.templates.buttons({
239 | data: this.options
240 | }) + this.templates.loading({}));
241 |
242 | // Get data
243 | this.$el.trigger('vt.layoutBuilt');
244 | },
245 |
246 | // Get data. Data can be from from Google Spreadsheet or JSON
247 | getData: function() {
248 | var thisVT = this;
249 |
250 | // Check if data is set and has data
251 | if (_.isArray(this.options.data) && this.options.data.length > 0) {
252 | this.data = this.options.data;
253 | this.$el.trigger('vt.gotData');
254 | }
255 | else {
256 | Tabletop.init(_.extend({}, this.options.tabletopOptions, {
257 | key: this.options.key,
258 | wanted: [this.options.sheetName],
259 | callback: function(data, tabletop) {
260 | thisVT.data = data[thisVT.options.sheetName].elements;
261 | thisVT.tabletop = tabletop;
262 | thisVT.$el.trigger('vt.gotData');
263 | }
264 | }));
265 | }
266 | },
267 |
268 | // Process data
269 | parseData: function() {
270 | // Placeholder for groups
271 | this.groups = this.groups || {};
272 |
273 | // Go through each row
274 | this.data = _.map(this.data, function(row) {
275 | // Column mapping.
276 | _.each(this.options.columnMapping, function(column, key) {
277 | if (!_.isUndefined(row[column])) {
278 | row[key] = row[column];
279 | }
280 | });
281 |
282 | // Parse date with moment
283 | row.date = moment(row.date, this.options.dateParse);
284 | row.timestamp = row.date.unix();
285 |
286 | // Process into group
287 | this.groups = this.options.groupFunction(row, this.groups, this.options.defaultDirection);
288 |
289 | return row;
290 | }, this);
291 |
292 | // Trigger done
293 | this.$el.trigger('vt.parsedData');
294 | },
295 |
296 | // Build timline
297 | buildTimeline: function() {
298 | this.$el.append(this.templates.timeline({
299 | data: {
300 | posts: _.map(this.data, function(d, di) {
301 | return this.templates.post({
302 | data: d,
303 | options: this.options
304 | });
305 | }, this).join(' '),
306 | groups: _.map(this.groups, function(g, gi) {
307 | return this.templates.group({
308 | data: g
309 | });
310 | }, this).join(' ')
311 | }
312 | }));
313 |
314 | this.$timeline = this.$el.find('.vertical-timeline-timeline');
315 | this.$el.trigger('vt.builtTimeline');
316 | },
317 |
318 | // Wait for images to be loaded
319 | loadImages: function() {
320 | this.$el.imagesLoaded(_.bind(function() {
321 | this.$el.find('.loading').slideUp();
322 | this.$timeline.fadeIn('fast', _.bind(function() {
323 | this.$el.trigger('vt.loadedImages');
324 | }, this));
325 | }, this));
326 | },
327 |
328 | // Make isotope layout
329 | isotopeIt: function() {
330 | this.$el.find('.vertical-timeline-timeline').isotope({
331 | itemSelector: '.item',
332 | transformsEnabled: true,
333 | layoutMode: 'spineAlign',
334 | spineAlign:{
335 | gutterWidth: this.options.gutterWidth
336 | },
337 | getSortData: {
338 | timestamp: function($el) {
339 | return parseFloat($el.data('timestamp'));
340 | }
341 | },
342 | sortBy: 'timestamp',
343 | sortAscending: (this.options.defaultDirection === 'newest') ? false : true,
344 | itemPositionDataEnabled: true,
345 | onLayout: _.bind(function($els, instance) {
346 | this.$el.trigger('vt.isotopized');
347 | }, this),
348 | containerStyle: {
349 | position: 'relative'
350 | }
351 | });
352 | },
353 |
354 | // Adjust width of timeline
355 | adjustWidth: function() {
356 | var w = this.options.width;
357 | var containerW = this.$el.width();
358 | var timelineW;
359 | var postW;
360 |
361 | if (w === 'auto') {
362 | w = containerW + 'px';
363 | }
364 |
365 | // Set timeline width
366 | this.$timeline.css('width', w);
367 | timelineW = this.$timeline.width();
368 |
369 | // Set width on posts
370 | postW = (timelineW / 2) - (this.options.gutterWidth / 2) - 3;
371 | this.$timeline.find('.post').width(postW);
372 | },
373 |
374 | // Adjust the middle line
375 | adjustSpine: function() {
376 | var $lastItem = this.$el.find('.item.last');
377 | var itemPosition = $lastItem.data('isotope-item-position');
378 | var dateHeight = $lastItem.find('.date').height();
379 | var dateOffset = $lastItem.find('.date').position();
380 | var innerMargin = parseInt($lastItem.find('.inner').css('marginTop'), 10);
381 | var top = (dateOffset === undefined) ? 0 : parseInt(dateOffset.top, 10);
382 | var y = (itemPosition && itemPosition.y) ?
383 | parseInt(itemPosition.y, 10) : 0;
384 | var lineHeight = y + innerMargin + top + (dateHeight / 2);
385 | var $line = this.$el.find('.line');
386 | var xOffset = (this.$timeline.width() / 2) - ($line.width() / 2);
387 |
388 | $line.height(lineHeight)
389 | .css('left', xOffset + 'px');
390 | },
391 |
392 | // DOM event
393 | domEvents: function() {
394 | if (this.domEventsAdded) {
395 | this.$el.trigger('vt.domEventsAdded');
396 | return;
397 | }
398 |
399 | // Handle click of open close buttons on post
400 | this.$el.find('.item a.open-close').on('click', _.bind(function(e) {
401 | e.preventDefault();
402 | var $thisButton = $(e.currentTarget);
403 | var $post = $thisButton.parents('.post');
404 | var direction = ($post.hasClass('collapsed')) ? 'slideDown' : 'slideUp';
405 |
406 | // Slide body
407 | $thisButton.siblings('.body')[direction](_.bind(function() {
408 | // Mark post and poke isotope
409 | $post.toggleClass('collapsed').toggleClass('expanded');
410 | this.$timeline.isotope('reLayout');
411 | }, this));
412 | // Change top buttons
413 | this.$el.find('.expand-collapse-buttons a').removeClass('active');
414 | }, this));
415 |
416 | // Handle expand/collapse buttons
417 | this.$el.find('.vertical-timeline-buttons a.expand-all').on('click', _.bind(function(e) {
418 | e.preventDefault();
419 | var $this = $(e.currentTarget);
420 | var thisVT = this;
421 |
422 | this.$el.find('.post .body').slideDown(function() {
423 | thisVT.$timeline.isotope('reLayout');
424 | });
425 | this.$el.find('.post').removeClass('collapsed').addClass('expanded');
426 | this.$el.find('.expand-collapse-buttons a').removeClass('active');
427 | $this.addClass('active');
428 | }, this));
429 | this.$el.find('.vertical-timeline-buttons a.collapse-all').on('click', _.bind(function(e) {
430 | e.preventDefault();
431 | var $this = $(e.currentTarget);
432 | var thisVT = this;
433 |
434 | this.$el.find('.post .body').slideUp(function() {
435 | thisVT.$timeline.isotope('reLayout');
436 | });
437 | this.$el.find('.post').addClass('collapsed').removeClass('expanded');
438 | this.$el.find('.expand-collapse-buttons a').removeClass('active');
439 | $this.addClass('active');
440 | }, this));
441 |
442 | // Sorting buttons
443 | this.$el.find('.sort-buttons a').on('click', _.bind(function(e) {
444 | e.preventDefault();
445 | var $this = $(e.currentTarget);
446 |
447 | // Don't proceed if already selected
448 | if ($this.hasClass('active')) {
449 | return false;
450 | }
451 |
452 | // Mark buttons
453 | this.$el.find('.sort-buttons a').removeClass('active');
454 | $this.addClass('active');
455 |
456 | // Change sorting
457 | if ($this.hasClass('sort-newest')) {
458 | this.updateGroups('newest');
459 | }
460 | else {
461 | this.updateGroups('oldest');
462 | }
463 | }, this));
464 |
465 | // If jQuery resize plugin is enabled and the option is
466 | // enabled then handle resize
467 | if (this.options.handleResize === true && _.isFunction(this.$el.resize)) {
468 | this.$el.resize(_.throttle(_.bind(function() {
469 | this.$el.trigger('vt.isotopized');
470 | }, this), 200));
471 | }
472 |
473 | // Only need to add these once
474 | this.domEventsAdded = true;
475 | this.$el.trigger('vt.domEventsAdded');
476 | },
477 |
478 | // Updates group markers with the appropriate timestamp
479 | // for isotope layout
480 | updateGroups: function(direction) {
481 | var thisVT = this;
482 | direction = direction || this.options.defaultDirection;
483 |
484 | this.$el.find('.group-marker').each(function() {
485 | var $this = $(this);
486 | var timestamp = (direction !== 'newest') ?
487 | thisVT.groups[$this.data('id')].timestampStart :
488 | thisVT.groups[$this.data('id')].timestampEnd;
489 | $this.data('timestamp', timestamp);
490 | });
491 |
492 | // Poke isotope
493 | this.$timeline.isotope('reloadItems')
494 | .isotope({ sortAscending: (direction !== 'newest') });
495 | }
496 | });
497 |
498 |
499 | /**
500 | * Extend Isotope for custom layout: spineAlign
501 | */
502 | _.extend($.Isotope.prototype, {
503 | _spineAlignReset: function() {
504 | this.spineAlign = {
505 | colA: 0,
506 | colB: 0,
507 | lastY: -60
508 | };
509 | },
510 |
511 | _spineAlignLayout: function( $elems ) {
512 | var instance = this,
513 | props = this.spineAlign,
514 | gutterWidth = Math.round( this.options.spineAlign && this.options.spineAlign.gutterWidth ) || 0,
515 | centerX = Math.round(this.element.width() / 2);
516 |
517 | $elems.each(function(i, val) {
518 | var $this = $(this);
519 | var x, y;
520 |
521 | $this.removeClass('last').removeClass('top');
522 | if (i == $elems.length - 1) {
523 | $this.addClass('last');
524 | }
525 | if ($this.hasClass('group-marker')) {
526 | var width = $this.width();
527 | x = centerX - (width / 2);
528 | if (props.colA >= props.colB) {
529 | y = props.colA;
530 | if (y === 0) {
531 | $this.addClass('top');
532 | }
533 | props.colA += $this.outerHeight(true);
534 | props.colB = props.colA;
535 | }
536 | else {
537 | y = props.colB;
538 | if (y === 0) {
539 | $this.addClass('top');
540 | }
541 | props.colB += $this.outerHeight(true);
542 | props.colA = props.colB;
543 | }
544 | }
545 | else {
546 | $this.removeClass('left').removeClass('right');
547 | var isColA = props.colB >= props.colA;
548 | if (isColA) {
549 | $this.addClass('left');
550 | }
551 | else {
552 | $this.addClass('right');
553 | }
554 |
555 | x = isColA ?
556 | centerX - ( $this.outerWidth(true) + gutterWidth / 2 ) : // left side
557 | centerX + (gutterWidth / 2); // right side
558 | y = isColA ? props.colA : props.colB;
559 | if (y - props.lastY <= 60) {
560 | var extraSpacing = 60 - Math.abs(y - props.lastY);
561 | $this.find('.inner').css('marginTop', extraSpacing);
562 | props.lastY = y + extraSpacing;
563 | }
564 | else {
565 | $this.find('.inner').css('marginTop', 0);
566 | props.lastY = y;
567 | }
568 | props[( isColA ? 'colA' : 'colB' )] += $this.outerHeight(true);
569 | }
570 | instance._pushPosition( $this, x, y );
571 | });
572 | },
573 |
574 | _spineAlignGetContainerSize: function() {
575 | var size = {};
576 | size.height = this.spineAlign[( this.spineAlign.colB > this.spineAlign.colA ? 'colB' : 'colA' )];
577 | return size;
578 | },
579 |
580 | _spineAlignResizeChanged: function() {
581 | return true;
582 | }
583 | });
584 |
585 | /**
586 | * Turn verticalTimeline into jQuery plugin
587 | */
588 | $.fn.verticalTimeline = function(options) {
589 | return this.each(function() {
590 | if (!$.data(this, 'verticalTimeline')) {
591 | $.data(this, 'verticalTimeline', new VerticalTimeline(this, options));
592 | }
593 | });
594 | };
595 |
596 | // Incase someone wants to use the base class, return it
597 | return VerticalTimeline;
598 |
599 | });
600 |
--------------------------------------------------------------------------------
/dist/jquery-vertical-timeline.min.css:
--------------------------------------------------------------------------------
1 | /*! jquery-vertical-timeline - v0.3.0 - 2016-08-24
2 | * https://github.com/MinnPost/jquery-vertical-timeline
3 | * Copyright (c) 2016 MinnPost; Licensed MIT */
4 |
5 |
6 | .vertical-timeline-container{*zoom:1}.vertical-timeline-container:after,.vertical-timeline-container:before{content:" ";display:table}.vertical-timeline-container:after{clear:both}.vertical-timeline-container .vertical-timeline-timeline{width:600px;margin:20px auto 30px;display:none}.vertical-timeline-container .clearfix{*zoom:1}.vertical-timeline-container .clearfix:after,.vertical-timeline-container .clearfix:before{content:" ";display:table}.vertical-timeline-container .clearfix:after{clear:both}.vertical-timeline-container .loading{text-align:center;font-size:2em;margin:1em;color:#565656}.vertical-timeline-container .post{width:271px;margin:0 0 10px;float:left}.vertical-timeline-container .post.last{margin-bottom:0}.vertical-timeline-container .post .inner{position:relative;padding:11px;border:1px #adadad solid;background-color:#fff;min-height:37px;word-wrap:break-word;-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;-moz-box-shadow:0 1px 1px 0 #ADADAD;-webkit-box-shadow:0 1px 1px 0 #ADADAD;box-shadow:0 1px 1px 0 #ADADAD}.vertical-timeline-container .post.right{float:right}.vertical-timeline-container .post .timestamp{display:none}.vertical-timeline-container .post h3{margin:0;font-family:Georgia,sans-serif;font-size:20px;font-weight:400;color:#010101;padding-right:30px;line-height:1.3em}.vertical-timeline-container .post .caption{color:#9d9d9d;font-family:Georgia,sans-serif;font-size:12px;margin-top:4px}.vertical-timeline-container .post .body{margin-top:10px}.vertical-timeline-container .post.collapsed .body{display:none}.vertical-timeline-container .post.expanded .body{display:block}.vertical-timeline-container .post .body img{max-width:100%}.vertical-timeline-container .post .text{color:#393939;font-family:Georgia,sans-serif;font-size:15px;margin:10px 0;line-height:1.5}.vertical-timeline-container .post a.open-close{background:transparent url(./images/button-up-down-arrow.png?1471988818) no-repeat;height:17px;width:16px;position:absolute;right:11px;top:11px}.vertical-timeline-container .post.collapsed a.open-close{background-position:left bottom}.vertical-timeline-container .post .title .title-icon{height:20px;width:20px;margin-right:4px;vertical-align:top}.vertical-timeline-container .post.closed .title{display:table;min-height:40px}.vertical-timeline-container .post.closed .title h3{display:table-cell;vertical-align:middle}.vertical-timeline-container .post a.more{color:#676767;text-decoration:none;text-transform:uppercase;font-size:12px;font-weight:700;text-align:center;padding:4px 15px;margin:5px auto 0;border:1px #BDBDBD solid;display:block;line-height:28px;-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;-moz-box-shadow:0 1px 1px 0 #BDBDBD;-webkit-box-shadow:0 1px 1px 0 #BDBDBD;box-shadow:0 1px 1px 0 #BDBDBD}.vertical-timeline-container .post a.more:hover{background-color:#FAFAFA}.vertical-timeline-container .post .date{color:#6a6a6a;font-size:11px;text-align:center;position:absolute;top:19px;display:block;width:54px;height:21px;line-height:20px}.vertical-timeline-container .post.left .date{background:transparent url(./images/tab-left.png?1471988818) no-repeat right;right:-54px}.vertical-timeline-container .post.right .date{background:transparent url(./images/tab-right.png?1471988818) no-repeat left;left:-54px}.vertical-timeline-container .group-marker{float:left;width:80px;margin:50px 0 15px}.vertical-timeline-container .group-marker .timestamp{display:none}.vertical-timeline-container .group-marker.top{margin-top:0}.vertical-timeline-container .group-marker .inner{width:76px;height:26px;padding:2px;background-color:#FFF}.vertical-timeline-container .group-marker .inner2{background-color:#434a50;-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px}.vertical-timeline-container .group{text-align:center;color:#fff;font-size:14px;font-weight:700;height:26px;line-height:26px}.vertical-timeline-container .vertical-timeline-buttons{text-align:center;margin:20px 0 10px}.vertical-timeline-container .vertical-timeline-buttons div{display:-moz-inline-stack;display:inline-block;zoom:1;*display:inline}.vertical-timeline-container .vertical-timeline-buttons .expand-collapse-buttons{margin-right:20px}.vertical-timeline-container .vertical-timeline-buttons a{display:-moz-inline-stack;display:inline-block;zoom:1;*display:inline;width:126px;border:1px #adadad solid;-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;font-size:12px;font-weight:700;text-decoration:none;padding:10px 12px;background-color:#FFF;color:#3b3b3b;text-transform:uppercase}.vertical-timeline-container .vertical-timeline-buttons a.active,.vertical-timeline-container .vertical-timeline-buttons a.active:hover{background-color:#f6f6f6;color:#a9a9a9;cursor:default;-moz-box-shadow:none;-webkit-box-shadow:none;box-shadow:none}.vertical-timeline-container .vertical-timeline-buttons a:hover{-moz-box-shadow:0 1px 1px 0 #adadad;-webkit-box-shadow:0 1px 1px 0 #adadad;box-shadow:0 1px 1px 0 #adadad}.vertical-timeline-container .vertical-timeline-buttons a span{padding-right:15px;background-position:right -20px}.vertical-timeline-container .vertical-timeline-buttons a.active span{background-position:right 4px}.vertical-timeline-container .vertical-timeline-buttons a.expand-all span,.vertical-timeline-container .vertical-timeline-buttons a.sort-newest span{background-image:url(./images/down-arrows.png?1471988818);background-repeat:no-repeat}.vertical-timeline-container .vertical-timeline-buttons a.collapse-all span,.vertical-timeline-container .vertical-timeline-buttons a.sort-oldest span{background-image:url(./images/up-arrows.png?1471988818);background-repeat:no-repeat}@media (max-width:675px){.vertical-timeline-container .vertical-timeline-buttons .expand-collapse-buttons,.vertical-timeline-container .vertical-timeline-buttons .sort-buttons{margin:0 0 10px}}.vertical-timeline-container .line-container{width:4px;text-align:center;margin:0 auto;display:block}.vertical-timeline-container .isotope .line{margin:0 auto;background-color:#b3b6b8;display:block;float:left;height:100%;left:298px;width:4px;position:absolute}.vertical-timeline-container .isotope-item{z-index:2}.vertical-timeline-container .isotope-hidden.isotope-item{pointer-events:none;z-index:1}.vertical-timeline-container .vertical-timeline-container .isotope,.vertical-timeline-container .vertical-timeline-container .isotope .isotope-item{-moz-transition-duration:.8s;-o-transition-duration:.8s;-webkit-transition-duration:.8s;transition-duration:.8s}.vertical-timeline-container .vertical-timeline-container .isotope{-moz-transition-property:height,width;-o-transition-property:height,width;-webkit-transition-property:height,width;transition-property:height,width}.vertical-timeline-container .vertical-timeline-container .isotope .isotope-item{-webkit-transition-property:-webkit-transform,opacity;-moz-transition-property:-moz-transform,opacity;-ms-transition-property:-ms-transform,opacity;-o-transition-property:top,left,opacity;transition-property:transform,opacity}.vertical-timeline-container .isotope .isotope-item.no-transition,.vertical-timeline-container .isotope.no-transition,.vertical-timeline-container .isotope.no-transition .isotope-item{-moz-transition-duration:0s;-o-transition-duration:0s;-webkit-transition-duration:0s;transition-duration:0s}
--------------------------------------------------------------------------------
/dist/jquery-vertical-timeline.min.js:
--------------------------------------------------------------------------------
1 | /*! jquery-vertical-timeline - v0.3.0 - 2016-08-24
2 | * https://github.com/MinnPost/jquery-vertical-timeline
3 | * Copyright (c) 2016 MinnPost; Licensed MIT */
4 |
5 | !function(a,b){if("undefined"!=typeof module&&module.exports&&"function"==typeof require)b(require("jquery"),require("underscore"),require("tabletop"),require("isotope"),require("imagesloaded"),require("moment"));else if("function"==typeof define&&define.amd)define("jquery-vertical-timeline",["jquery","underscore","tabletop","moment","isotope","imagesloaded"],b);else{if(!(a.jQuery&&a._&&a.Tabletop&&a.moment&&a.jQuery.fn.isotope&&a.jQuery.fn.imagesLoaded))throw new Error("Could not find dependencies for jQuery Vertical Timeline.");b(a.jQuery,a._,a.Tabletop,a.moment,a.jQuery.fn.isotope,a.jQuery.fn.resize,a.jQuery.fn.imagesLoaded)}}("undefined"!=typeof window?window:this,function(a,b,c,d){var e={key:"https://docs.google.com/spreadsheet/pub?key=0AsmHVq28GtVJdG1fX3dsQlZrY18zTVA2ZG8wTXdtNHc&output=html",sheetName:"Posts",dateParse:["MMM DD, YYYY","MM/DD/YYYY","M/D/YYYY","DD MMM YYYY"],defaultDirection:"newest",defaultExpansion:"expanded",groupFunction:"groupSegmentByYear",sharing:!1,gutterWidth:57,width:"auto",handleResize:!1,tabletopOptions:{},columnMapping:{title:"title",title_icon:"title icon",date:"date",display_date:"display date",photo_url:"photo url",caption:"caption",body:"body",read_more_url:"read more url"},postTemplate:' <% if (data.title_icon) { %>
',groupMarkerTemplate:' ',buttonTemplate:' ',timelineTemplate:' <%= data.posts %> <%= data.groups %> ',loadingTemplate:'jQuery Vertical Example
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
<% if (data.title_icon) { %>
<% } %> <%= data.title %>
<%= data.display_date %>
<% if (data.photo_url) { %>
<% } %> <% if (data.caption) { %>
<%= data.caption %>
<% } %> <% if (data.body) { %> <%= data.body %>
<% } %> <% if (data.read_more_url) { %> Read more <% } %>
<%= data.groupDisplay %>
Loading...
'},f={};f.groupSegmentByDecade=function(a,c,e){var f=a.date.year(),g=f.toString(),h=g.slice(0,-1),i=d(h+"0-01-01T00:00:00"),j=d(h+"9-12-31T12:59:99");return b.isUndefined(c[h])&&(c[h]={id:h,groupDisplay:h+"0s",timestamp:"newest"==e?j.unix():i.unix(),timestampStart:i.unix(),timestampEnd:j.unix()}),c},f.groupSegmentByYear=function(a,c,e){var f=a.date.year(),g=d(f+"-01-01T00:00:00"),h=d(f+"-12-31T12:59:99");return b.isUndefined(c[f.toString()])&&(c[f.toString()]={id:f,groupDisplay:f,timestamp:"newest"==e?h.unix():g.unix(),timestampStart:g.unix(),timestampEnd:h.unix()}),c},f.groupSegmentByDay=function(a,b,c){var d=new Date(a.timestamp).getMonth(),e=new Date(a.timestamp).getFullYear(),f=new Date(a.timestamp).getDate(),g=["Jan","Feb","Mar","Apr","May","June","July","Aug","Sept","Oct","Nov","Dec"],h=Date.parse(g[d]+" "+f+", "+e),i=Date.parse(g[d]+" "+(f+1)+", "+e),j=f+100*(d+100*e);return b[j]={id:j,groupDisplay:g[d]+" "+f+", "+e,timestamp:"newest"==c?i:h,timestampStart:h,timestampEnd:i},b};var g=function(c,d){this.options=a.extend(!0,{},e,d),this.options.groupFunction=!b.isFunction(this.options.groupFunction)&&b.isFunction(f[this.options.groupFunction])?f[this.options.groupFunction]:this.options.groupFunction,this.$el=a(c),this.templates={},this.templates.post=b.template(this.options.postTemplate),this.templates.group=b.template(this.options.groupMarkerTemplate),this.templates.buttons=b.template(this.options.buttonTemplate),this.templates.timeline=b.template(this.options.timelineTemplate),this.templates.loading=b.template(this.options.loadingTemplate),this.loadEvents(),this.$el.trigger("vt.build")};return b.extend(g.prototype,{events:{"vt.build":["buildLayout"],"vt.layoutBuilt":["getData"],"vt.gotData":["parseData"],"vt.parsedData":["buildTimeline"],"vt.builtTimeline":["loadImages","adjustWidth"],"vt.loadedImages":["isotopeIt"],"vt.isotopized":["adjustWidth","adjustSpine","domEvents"]},loadEvents:function(){b.each(this.events,function(a,c){b.each(a,function(a){b.isFunction(this[a])&&this.$el.on(c,b.bind(this[a],this))},this)},this)},buildLayout:function(){this.$el.addClass("vertical-timeline-container"),this.$el.html(this.templates.buttons({data:this.options})+this.templates.loading({})),this.$el.trigger("vt.layoutBuilt")},getData:function(){var a=this;b.isArray(this.options.data)&&this.options.data.length>0?(this.data=this.options.data,this.$el.trigger("vt.gotData")):c.init(b.extend({},this.options.tabletopOptions,{key:this.options.key,wanted:[this.options.sheetName],callback:function(b,c){a.data=b[a.options.sheetName].elements,a.tabletop=c,a.$el.trigger("vt.gotData")}}))},parseData:function(){this.groups=this.groups||{},this.data=b.map(this.data,function(a){return b.each(this.options.columnMapping,function(c,d){b.isUndefined(a[c])||(a[d]=a[c])}),a.date=d(a.date,this.options.dateParse),a.timestamp=a.date.unix(),this.groups=this.options.groupFunction(a,this.groups,this.options.defaultDirection),a},this),this.$el.trigger("vt.parsedData")},buildTimeline:function(){this.$el.append(this.templates.timeline({data:{posts:b.map(this.data,function(a,b){return this.templates.post({data:a,options:this.options})},this).join(" "),groups:b.map(this.groups,function(a,b){return this.templates.group({data:a})},this).join(" ")}})),this.$timeline=this.$el.find(".vertical-timeline-timeline"),this.$el.trigger("vt.builtTimeline")},loadImages:function(){this.$el.imagesLoaded(b.bind(function(){this.$el.find(".loading").slideUp(),this.$timeline.fadeIn("fast",b.bind(function(){this.$el.trigger("vt.loadedImages")},this))},this))},isotopeIt:function(){this.$el.find(".vertical-timeline-timeline").isotope({itemSelector:".item",transformsEnabled:!0,layoutMode:"spineAlign",spineAlign:{gutterWidth:this.options.gutterWidth},getSortData:{timestamp:function(a){return parseFloat(a.data("timestamp"))}},sortBy:"timestamp",sortAscending:"newest"===this.options.defaultDirection?!1:!0,itemPositionDataEnabled:!0,onLayout:b.bind(function(a,b){this.$el.trigger("vt.isotopized")},this),containerStyle:{position:"relative"}})},adjustWidth:function(){var a,b,c=this.options.width,d=this.$el.width();"auto"===c&&(c=d+"px"),this.$timeline.css("width",c),a=this.$timeline.width(),b=a/2-this.options.gutterWidth/2-3,this.$timeline.find(".post").width(b)},adjustSpine:function(){var a=this.$el.find(".item.last"),b=a.data("isotope-item-position"),c=a.find(".date").height(),d=a.find(".date").position(),e=parseInt(a.find(".inner").css("marginTop"),10),f=void 0===d?0:parseInt(d.top,10),g=b&&b.y?parseInt(b.y,10):0,h=g+e+f+c/2,i=this.$el.find(".line"),j=this.$timeline.width()/2-i.width()/2;i.height(h).css("left",j+"px")},domEvents:function(){return this.domEventsAdded?void this.$el.trigger("vt.domEventsAdded"):(this.$el.find(".item a.open-close").on("click",b.bind(function(c){c.preventDefault();var d=a(c.currentTarget),e=d.parents(".post"),f=e.hasClass("collapsed")?"slideDown":"slideUp";d.siblings(".body")[f](b.bind(function(){e.toggleClass("collapsed").toggleClass("expanded"),this.$timeline.isotope("reLayout")},this)),this.$el.find(".expand-collapse-buttons a").removeClass("active")},this)),this.$el.find(".vertical-timeline-buttons a.expand-all").on("click",b.bind(function(b){b.preventDefault();var c=a(b.currentTarget),d=this;this.$el.find(".post .body").slideDown(function(){d.$timeline.isotope("reLayout")}),this.$el.find(".post").removeClass("collapsed").addClass("expanded"),this.$el.find(".expand-collapse-buttons a").removeClass("active"),c.addClass("active")},this)),this.$el.find(".vertical-timeline-buttons a.collapse-all").on("click",b.bind(function(b){b.preventDefault();var c=a(b.currentTarget),d=this;this.$el.find(".post .body").slideUp(function(){d.$timeline.isotope("reLayout")}),this.$el.find(".post").addClass("collapsed").removeClass("expanded"),this.$el.find(".expand-collapse-buttons a").removeClass("active"),c.addClass("active")},this)),this.$el.find(".sort-buttons a").on("click",b.bind(function(b){b.preventDefault();var c=a(b.currentTarget);return c.hasClass("active")?!1:(this.$el.find(".sort-buttons a").removeClass("active"),c.addClass("active"),void(c.hasClass("sort-newest")?this.updateGroups("newest"):this.updateGroups("oldest")))},this)),this.options.handleResize===!0&&b.isFunction(this.$el.resize)&&this.$el.resize(b.throttle(b.bind(function(){this.$el.trigger("vt.isotopized")},this),200)),this.domEventsAdded=!0,void this.$el.trigger("vt.domEventsAdded"))},updateGroups:function(b){var c=this;b=b||this.options.defaultDirection,this.$el.find(".group-marker").each(function(){var d=a(this),e="newest"!==b?c.groups[d.data("id")].timestampStart:c.groups[d.data("id")].timestampEnd;d.data("timestamp",e)}),this.$timeline.isotope("reloadItems").isotope({sortAscending:"newest"!==b})}}),b.extend(a.Isotope.prototype,{_spineAlignReset:function(){this.spineAlign={colA:0,colB:0,lastY:-60}},_spineAlignLayout:function(b){var c=this,d=this.spineAlign,e=Math.round(this.options.spineAlign&&this.options.spineAlign.gutterWidth)||0,f=Math.round(this.element.width()/2);b.each(function(g,h){var i,j,k=a(this);if(k.removeClass("last").removeClass("top"),g==b.length-1&&k.addClass("last"),k.hasClass("group-marker")){var l=k.width();i=f-l/2,d.colA>=d.colB?(j=d.colA,0===j&&k.addClass("top"),d.colA+=k.outerHeight(!0),d.colB=d.colA):(j=d.colB,0===j&&k.addClass("top"),d.colB+=k.outerHeight(!0),d.colA=d.colB)}else{k.removeClass("left").removeClass("right");var m=d.colB>=d.colA;if(m?k.addClass("left"):k.addClass("right"),i=m?f-(k.outerWidth(!0)+e/2):f+e/2,j=m?d.colA:d.colB,j-d.lastY<=60){var n=60-Math.abs(j-d.lastY);k.find(".inner").css("marginTop",n),d.lastY=j+n}else k.find(".inner").css("marginTop",0),d.lastY=j;d[m?"colA":"colB"]+=k.outerHeight(!0)}c._pushPosition(k,i,j)})},_spineAlignGetContainerSize:function(){var a={};return a.height=this.spineAlign[this.spineAlign.colB>this.spineAlign.colA?"colB":"colA"],a},_spineAlignResizeChanged:function(){return!0}}),a.fn.verticalTimeline=function(b){return this.each(function(){a.data(this,"verticalTimeline")||a.data(this,"verticalTimeline",new g(this,b))})},g});
--------------------------------------------------------------------------------
/example/example.js:
--------------------------------------------------------------------------------
1 | /**
2 | * JS for example using RequireJS
3 | */
4 |
5 | require.config({
6 | shim: {
7 | 'underscore': {
8 | exports: '_'
9 | },
10 | 'handlebars': {
11 | exports: 'Handlebars'
12 | },
13 | 'tabletop': {
14 | exports: 'Tabletop'
15 | },
16 | 'isotope': {
17 | deps: ['jquery']
18 | },
19 | 'jquery-resize': {
20 | deps: ['jquery']
21 | }
22 | },
23 | paths: {
24 | 'jquery': '../bower_components/jquery/jquery.min',
25 | 'underscore': '../bower_components/underscore/underscore-min',
26 | 'tabletop': '../bower_components/tabletop/src/tabletop',
27 | 'moment': '../bower_components/momentjs/min/moment.min',
28 | 'isotope': '../bower_components/isotope/jquery.isotope.min',
29 | 'jquery-resize': '../bower_components/jquery-resize/jquery.ba-resize.min',
30 | 'eventEmitter/EventEmitter': '../bower_components/eventEmitter/EventEmitter.min',
31 | 'eventie/eventie': '../bower_components/eventie/eventie',
32 | 'imagesloaded': '../bower_components/imagesloaded/imagesloaded',
33 | 'jquery-vertical-timeline': '../dist/jquery-vertical-timeline.min'
34 | }
35 | });
36 |
37 | require(['jquery', 'underscore', 'jquery-resize', 'jquery-vertical-timeline'], function($, _) {
38 | $(document).ready(function() {
39 | $('.timeline-jquery-example-mn-gambling').verticalTimeline({
40 | key: 'https://docs.google.com/spreadsheet/pub?key=0AtX8MXQ89fOKdDZadGcyNE9CZmFZV29tQjI5RFU3X3c&output=html',
41 | sheetName: 'timeline-data',
42 | dateParse: 'MMM DD, YYYY',
43 | defaultDirection: 'oldest',
44 | defaultExpansion: 'collapsed',
45 | groupFunction: 'groupSegmentByDecade',
46 | width: '50%',
47 | tabletopOptions: {
48 | parameterize: 'http://gs-proxy.herokuapp.com/proxy?url='
49 | }
50 | });
51 |
52 | $.getJSON('./example/example.json', function(data) {
53 | $('.timeline-jquery-example-kitten').verticalTimeline({
54 | data: data,
55 | dateParse: 'DD MMM YYYY',
56 | width: '75%'
57 | });
58 | });
59 |
60 | $('.timeline-jquery-example-mn-example').verticalTimeline({
61 | key: '0AjYft7IGrHzNdHVTTi1xRGtpdHV5SzJDZW4yRnlRN2c',
62 | sheetName: 'Entries',
63 | dateParse: 'M/D/YYYY',
64 | tabletopOptions: {
65 | parameterize: 'http://gs-proxy.herokuapp.com/proxy?url='
66 | }
67 | });
68 | });
69 | });
70 |
--------------------------------------------------------------------------------
/example/example.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "title": "This timeline uses JSON data directly",
4 | "date": "27 Mar 2008",
5 | "display date": "Mar 27",
6 | "photourl": "http://placekitten.com/600/400",
7 | "caption": "Mike Baird",
8 | "body": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec qu",
9 | "read more url": "http://www.flickr.com/photos/mikebaird/2529507825/"
10 | },
11 | {
12 | "title": "Peregrine Falcon Adult, Morro Bay, CA",
13 | "date": "27 May 2008",
14 | "display date": "May 27",
15 | "photo url": "http://placekitten.com/400/500",
16 | "caption": "Mike Baird",
17 | "body": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec qu",
18 | "read more url": "http://www.flickr.com/photos/mikebaird/2529507825/"
19 | },
20 | {
21 | "title": "Peregrine Falcon Adult, Morro Bay, CA",
22 | "date": "27 Oct 2008",
23 | "display date": "Oct 27",
24 | "photo url": "http://placekitten.com/500/220",
25 | "caption": "Mike Baird",
26 | "body": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec qu",
27 | "read more url": "http://www.flickr.com/photos/mikebaird/2529507825/"
28 | }
29 | ]
30 |
--------------------------------------------------------------------------------
/example/style.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Example CSS
3 | */
4 |
5 | body {
6 | margin: 0;
7 | padding: 0 1em;
8 | }
9 |
10 | .example {
11 | padding-bottom: 4em;
12 | margin-bottom: 4em;
13 | border-bottom: 1px solid #BABABA;
14 | }
15 |
16 | .example h1 {
17 | max-width: 960px;
18 | margin: 0 auto;
19 | text-align: center;
20 | text-transform: uppercase;
21 | margin-top: 1em;
22 | }
23 |
24 | .example a.spreadsheet-link,
25 | .example a.spreadsheet-link:visited {
26 | color: blue;
27 | display: block;
28 | max-width: 960px;
29 | margin: 0 auto;
30 | text-align: center;
31 | font-size: 1.25em;
32 | }
33 |
34 | code {
35 | max-width: 960px;
36 | margin: 0 auto;
37 | display: block;
38 | padding: .85em;
39 | margin-bottom: 2em;
40 | background-color: #EFEFEF;
41 | border-radius: .25em;
42 | }
43 |
44 | /**
45 | * http://nicolasgallagher.com/micro-clearfix-hack/
46 | * For modern browsers
47 | * 1. The space content is one way to avoid an Opera bug when the
48 | * contenteditable attribute is included anywhere else in the document.
49 | * Otherwise it causes space to appear at the top and bottom of elements
50 | * that are clearfixed.
51 | * 2. The use of `table` rather than `block` is only necessary if using
52 | * `:before` to contain the top-margins of child elements.
53 | */
54 | .cf:before,
55 | .cf:after {
56 | content: " "; /* 1 */
57 | display: table; /* 2 */
58 | }
59 |
60 | .cf:after {
61 | clear: both;
62 | }
63 |
64 | /**
65 | * For IE 6/7 only
66 | * Include this rule to trigger hasLayout and contain floats.
67 | */
68 | .cf {
69 | *zoom: 1;
70 | }
--------------------------------------------------------------------------------
/images/button-up-down-arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MinnPost/jquery-vertical-timeline/609c704f4077d097fcc0e400135c644cf049b5c4/images/button-up-down-arrow.png
--------------------------------------------------------------------------------
/images/down-arrows.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MinnPost/jquery-vertical-timeline/609c704f4077d097fcc0e400135c644cf049b5c4/images/down-arrows.png
--------------------------------------------------------------------------------
/images/icon-link.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MinnPost/jquery-vertical-timeline/609c704f4077d097fcc0e400135c644cf049b5c4/images/icon-link.png
--------------------------------------------------------------------------------
/images/icon-share.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MinnPost/jquery-vertical-timeline/609c704f4077d097fcc0e400135c644cf049b5c4/images/icon-share.png
--------------------------------------------------------------------------------
/images/popup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MinnPost/jquery-vertical-timeline/609c704f4077d097fcc0e400135c644cf049b5c4/images/popup.png
--------------------------------------------------------------------------------
/images/tab-left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MinnPost/jquery-vertical-timeline/609c704f4077d097fcc0e400135c644cf049b5c4/images/tab-left.png
--------------------------------------------------------------------------------
/images/tab-right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MinnPost/jquery-vertical-timeline/609c704f4077d097fcc0e400135c644cf049b5c4/images/tab-right.png
--------------------------------------------------------------------------------
/images/up-arrows.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MinnPost/jquery-vertical-timeline/609c704f4077d097fcc0e400135c644cf049b5c4/images/up-arrows.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
18 |
27 |
28 |
29 |
30 | Kitten JSON Example
19 | JSON 20 |// Uses example/example.json data soure
21 | {
22 | data: data,
23 | width: '75%'
24 | }
25 |
26 |
31 |
46 |
47 |
48 |
49 | Minnesota Gambling Example
32 | Spreadsheet 33 |{
34 | key: 'https://docs.google.com/spreadsheet/pub?key=0AtX8MXQ89fOKdDZadGcyNE9CZmFZV29tQjI5RFU3X3c&output=html',
35 | sheetName: 'timeline-data',
36 | defaultDirection: 'oldest',
37 | defaultExpansion: 'collapsed',
38 | groupFunction: 'groupSegmentByDecade',
39 | width: '50%',
40 | tabletopOptions: {
41 | parameterize: 'http://gs-proxy.herokuapp.com/proxy?url='
42 | }
43 | }
44 |
45 |
50 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/js/jquery-vertical-timeline.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Vertical timeline plugin for jQuery.
3 | */
4 |
5 | // Wrapper to handle using with RequireJS or Browserify or as a global
6 | (function(global, factory) {
7 | // Common JS (i.e. browserify) environment
8 | if (typeof module !== 'undefined' && module.exports && typeof require === 'function') {
9 | factory(require('jquery'), require('underscore'), require('tabletop'), require('isotope'), require('imagesloaded'), require('moment'));
10 | }
11 | // AMD?
12 | else if (typeof define === 'function' && define.amd) {
13 | define('jquery-vertical-timeline', ['jquery', 'underscore', 'tabletop', 'moment', 'isotope', 'imagesloaded'], factory);
14 | }
15 | // Browser global
16 | else if (global.jQuery && global._ && global.Tabletop && global.moment && global.jQuery.fn.isotope && global.jQuery.fn.imagesLoaded) {
17 | factory(global.jQuery, global._, global.Tabletop, global.moment, global.jQuery.fn.isotope, global.jQuery.fn.resize, global.jQuery.fn.imagesLoaded);
18 | }
19 | else {
20 | throw new Error('Could not find dependencies for jQuery Vertical Timeline.' );
21 | }
22 | })(typeof window !== 'undefined' ? window : this, function($, _, Tabletop, moment) {
23 |
24 | /**
25 | * Default options
26 | */
27 | var defaultsOptions = {
28 | key: 'https://docs.google.com/spreadsheet/pub?key=0AsmHVq28GtVJdG1fX3dsQlZrY18zTVA2ZG8wTXdtNHc&output=html',
29 | sheetName: 'Posts',
30 | dateParse: ['MMM DD, YYYY', 'MM/DD/YYYY', 'M/D/YYYY', 'DD MMM YYYY'],
31 | defaultDirection: 'newest',
32 | defaultExpansion: 'expanded',
33 | groupFunction: 'groupSegmentByYear',
34 | sharing: false,
35 | gutterWidth: 57,
36 | width: 'auto',
37 | handleResize: false,
38 | tabletopOptions: {},
39 | columnMapping: {
40 | 'title': 'title',
41 | 'title_icon': 'title icon',
42 | 'date': 'date',
43 | 'display_date': 'display date',
44 | 'photo_url': 'photo url',
45 | 'caption': 'caption',
46 | 'body': 'body',
47 | 'read_more_url': 'read more url'
48 | },
49 | postTemplate: 'Minnesota Marriage Example
51 | Spreadsheet 52 |{
53 | key: '0AjYft7IGrHzNdHVTTi1xRGtpdHV5SzJDZW4yRnlRN2c',
54 | sheetName: 'Entries',
55 | tabletopOptions: {
56 | parameterize: 'http://gs-proxy.herokuapp.com/proxy?url='
57 | }
58 | }
59 |
60 | \
50 |
\
51 |
\
52 | \
53 | <% if (data.title_icon) { %>
\
53 | <% if (data.title_icon) { %>
<% } %> \
54 | <%= data.title %> \
55 | <\/h3> \
56 | <\/div> \
57 | <%= data.display_date %><\/div> \
58 | \
59 | <% if (data.photo_url) { %> \
60 |
\
61 | <% } %> \
62 | <% if (data.caption) { %> \
63 | <%= data.caption %><\/div> \
64 | <% } %> \
65 | <% if (data.body) { %> \
66 | <%= data.body %><\/div> \
67 | <% } %> \
68 | \
69 | <% if (data.read_more_url) { %> \
70 | Read more<\/a> \
71 | <% } %> \
72 | <\/div> \
73 | <\/div> \
74 | \
75 | <\/div> \
76 | <\/div> \
77 | ',
78 | groupMarkerTemplate: ' \
79 | \
80 | \
81 | <%= data.groupDisplay %><\/div> \
82 | <\/div> \
83 | <\/div> \
84 | <\/div> \
85 | ',
86 | buttonTemplate: ' \
87 | \
88 | active<% } %>" href="#">Expand all<\/span><\/a> \
89 | active<% } %>" href="#">Collapse all<\/span><\/a> \
90 | <\/div> \
91 | \
92 | active<% } %>" href="#">Newest first<\/span><\/a> \
93 | active<% } %>" href="#">Oldest first<\/span><\/a> \
94 | <\/div> \
95 | <\/div> \
96 | ',
97 | timelineTemplate: ' \
98 | \
99 | <\/div> \
100 | <\/div> \
101 | <%= data.posts %> \
102 | <%= data.groups %> \
103 | <\/div> \
104 | ',
105 | loadingTemplate: ' \
106 | Loading... \
107 | <\/div> \
108 | '
109 | };
110 |
111 | var groupingFunctions = {};
112 | /**
113 | * Grouping function by Decade.
114 | */
115 | groupingFunctions.groupSegmentByDecade = function(row, groups, direction) {
116 | var year = row.date.year();
117 | var yearStr = year.toString();
118 | var id = yearStr.slice(0, -1);
119 | var start = moment(id + '0-01-01T00:00:00');
120 | var end = moment(id + '9-12-31T12:59:99');
121 |
122 | if (_.isUndefined(groups[id])) {
123 | groups[id] = {
124 | id: id,
125 | groupDisplay: id + '0s',
126 | timestamp: (direction == 'newest') ? end.unix() : start.unix(),
127 | timestampStart: start.unix(),
128 | timestampEnd: end.unix()
129 | };
130 | }
131 |
132 | return groups;
133 | };
134 |
135 | /**
136 | * Grouping function by year.
137 | */
138 | groupingFunctions.groupSegmentByYear = function(row, groups, direction) {
139 | var year = row.date.year();
140 | var start = moment(year + '-01-01T00:00:00');
141 | var end = moment(year + '-12-31T12:59:99');
142 |
143 | if (_.isUndefined(groups[year.toString()])) {
144 | groups[year.toString()] = {
145 | id: year,
146 | groupDisplay: year,
147 | timestamp: (direction == 'newest') ? end.unix() : start.unix(),
148 | timestampStart: start.unix(),
149 | timestampEnd: end.unix()
150 | };
151 | }
152 |
153 | return groups;
154 | };
155 |
156 | /**
157 | * Grouping function by day.
158 | */
159 | groupingFunctions.groupSegmentByDay = function(row, groups, direction) {
160 | var month = new Date(row.timestamp).getMonth();
161 | var year = new Date(row.timestamp).getFullYear();
162 | var day = new Date(row.timestamp).getDate();
163 | var _month_str = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'June', 'July', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec'];
164 | var _time_start = Date.parse(_month_str[month] + ' ' + day + ', ' + year);
165 | var _time_end = Date.parse(_month_str[month] + ' ' + (day+1) + ', ' + year);
166 | var _id = day + (month + year * 100) * 100;
167 |
168 | groups[_id] = {
169 | id: _id,
170 | groupDisplay: _month_str[month] + ' ' + day + ', ' + year,
171 | timestamp: (direction == 'newest') ? _time_end: _time_start,
172 | timestampStart: _time_start,
173 | timestampEnd: _time_end
174 | };
175 |
176 | return groups;
177 | };
178 |
179 |
180 |
181 | /**
182 | * Base class for timeline
183 | */
184 | var VerticalTimeline = function(el, options) {
185 | this.options = $.extend(true, {}, defaultsOptions, options);
186 |
187 | // Check to see if grouping function is an option
188 | this.options.groupFunction = (!_.isFunction(this.options.groupFunction) && _.isFunction(groupingFunctions[this.options.groupFunction])) ? groupingFunctions[this.options.groupFunction] : this.options.groupFunction;
189 |
190 | // Consistent reference to jquery object
191 | this.$el = $(el);
192 |
193 | // Build templates for performance
194 | this.templates = {};
195 | this.templates.post = _.template(this.options.postTemplate);
196 | this.templates.group = _.template(this.options.groupMarkerTemplate);
197 | this.templates.buttons = _.template(this.options.buttonTemplate);
198 | this.templates.timeline = _.template(this.options.timelineTemplate);
199 | this.templates.loading = _.template(this.options.loadingTemplate);
200 |
201 | // Use custom events to make things happens
202 | this.loadEvents();
203 | this.$el.trigger('vt.build');
204 | };
205 |
206 | /**
207 | * Methods and properties of class
208 | */
209 | _.extend(VerticalTimeline.prototype, {
210 | // Events
211 | events: {
212 | 'vt.build': ['buildLayout'],
213 | 'vt.layoutBuilt': ['getData'],
214 | 'vt.gotData': ['parseData'],
215 | 'vt.parsedData': ['buildTimeline'],
216 | 'vt.builtTimeline': ['loadImages', 'adjustWidth'],
217 | 'vt.loadedImages': ['isotopeIt'],
218 | 'vt.isotopized': ['adjustWidth', 'adjustSpine', 'domEvents']
219 | },
220 |
221 | // Event delegation
222 | loadEvents: function() {
223 | _.each(this.events, function(ea, ename) {
224 | _.each(ea, function(ehandler) {
225 | if (_.isFunction(this[ehandler])) {
226 | this.$el.on(ename, _.bind(this[ehandler], this));
227 | }
228 | }, this);
229 | }, this);
230 | },
231 |
232 | // Initial building
233 | buildLayout: function() {
234 | // Add base class for styling
235 | this.$el.addClass('vertical-timeline-container');
236 |
237 | // Add template layout
238 | this.$el.html(this.templates.buttons({
239 | data: this.options
240 | }) + this.templates.loading({}));
241 |
242 | // Get data
243 | this.$el.trigger('vt.layoutBuilt');
244 | },
245 |
246 | // Get data. Data can be from from Google Spreadsheet or JSON
247 | getData: function() {
248 | var thisVT = this;
249 |
250 | // Check if data is set and has data
251 | if (_.isArray(this.options.data) && this.options.data.length > 0) {
252 | this.data = this.options.data;
253 | this.$el.trigger('vt.gotData');
254 | }
255 | else {
256 | Tabletop.init(_.extend({}, this.options.tabletopOptions, {
257 | key: this.options.key,
258 | wanted: [this.options.sheetName],
259 | callback: function(data, tabletop) {
260 | thisVT.data = data[thisVT.options.sheetName].elements;
261 | thisVT.tabletop = tabletop;
262 | thisVT.$el.trigger('vt.gotData');
263 | }
264 | }));
265 | }
266 | },
267 |
268 | // Process data
269 | parseData: function() {
270 | // Placeholder for groups
271 | this.groups = this.groups || {};
272 |
273 | // Go through each row
274 | this.data = _.map(this.data, function(row) {
275 | // Column mapping.
276 | _.each(this.options.columnMapping, function(column, key) {
277 | if (!_.isUndefined(row[column])) {
278 | row[key] = row[column];
279 | }
280 | });
281 |
282 | // Parse date with moment
283 | row.date = moment(row.date, this.options.dateParse);
284 | row.timestamp = row.date.unix();
285 |
286 | // Process into group
287 | this.groups = this.options.groupFunction(row, this.groups, this.options.defaultDirection);
288 |
289 | return row;
290 | }, this);
291 |
292 | // Trigger done
293 | this.$el.trigger('vt.parsedData');
294 | },
295 |
296 | // Build timline
297 | buildTimeline: function() {
298 | this.$el.append(this.templates.timeline({
299 | data: {
300 | posts: _.map(this.data, function(d, di) {
301 | return this.templates.post({
302 | data: d,
303 | options: this.options
304 | });
305 | }, this).join(' '),
306 | groups: _.map(this.groups, function(g, gi) {
307 | return this.templates.group({
308 | data: g
309 | });
310 | }, this).join(' ')
311 | }
312 | }));
313 |
314 | this.$timeline = this.$el.find('.vertical-timeline-timeline');
315 | this.$el.trigger('vt.builtTimeline');
316 | },
317 |
318 | // Wait for images to be loaded
319 | loadImages: function() {
320 | this.$el.imagesLoaded(_.bind(function() {
321 | this.$el.find('.loading').slideUp();
322 | this.$timeline.fadeIn('fast', _.bind(function() {
323 | this.$el.trigger('vt.loadedImages');
324 | }, this));
325 | }, this));
326 | },
327 |
328 | // Make isotope layout
329 | isotopeIt: function() {
330 | this.$el.find('.vertical-timeline-timeline').isotope({
331 | itemSelector: '.item',
332 | transformsEnabled: true,
333 | layoutMode: 'spineAlign',
334 | spineAlign:{
335 | gutterWidth: this.options.gutterWidth
336 | },
337 | getSortData: {
338 | timestamp: function($el) {
339 | return parseFloat($el.data('timestamp'));
340 | }
341 | },
342 | sortBy: 'timestamp',
343 | sortAscending: (this.options.defaultDirection === 'newest') ? false : true,
344 | itemPositionDataEnabled: true,
345 | onLayout: _.bind(function($els, instance) {
346 | this.$el.trigger('vt.isotopized');
347 | }, this),
348 | containerStyle: {
349 | position: 'relative'
350 | }
351 | });
352 | },
353 |
354 | // Adjust width of timeline
355 | adjustWidth: function() {
356 | var w = this.options.width;
357 | var containerW = this.$el.width();
358 | var timelineW;
359 | var postW;
360 |
361 | if (w === 'auto') {
362 | w = containerW + 'px';
363 | }
364 |
365 | // Set timeline width
366 | this.$timeline.css('width', w);
367 | timelineW = this.$timeline.width();
368 |
369 | // Set width on posts
370 | postW = (timelineW / 2) - (this.options.gutterWidth / 2) - 3;
371 | this.$timeline.find('.post').width(postW);
372 | },
373 |
374 | // Adjust the middle line
375 | adjustSpine: function() {
376 | var $lastItem = this.$el.find('.item.last');
377 | var itemPosition = $lastItem.data('isotope-item-position');
378 | var dateHeight = $lastItem.find('.date').height();
379 | var dateOffset = $lastItem.find('.date').position();
380 | var innerMargin = parseInt($lastItem.find('.inner').css('marginTop'), 10);
381 | var top = (dateOffset === undefined) ? 0 : parseInt(dateOffset.top, 10);
382 | var y = (itemPosition && itemPosition.y) ?
383 | parseInt(itemPosition.y, 10) : 0;
384 | var lineHeight = y + innerMargin + top + (dateHeight / 2);
385 | var $line = this.$el.find('.line');
386 | var xOffset = (this.$timeline.width() / 2) - ($line.width() / 2);
387 |
388 | $line.height(lineHeight)
389 | .css('left', xOffset + 'px');
390 | },
391 |
392 | // DOM event
393 | domEvents: function() {
394 | if (this.domEventsAdded) {
395 | this.$el.trigger('vt.domEventsAdded');
396 | return;
397 | }
398 |
399 | // Handle click of open close buttons on post
400 | this.$el.find('.item a.open-close').on('click', _.bind(function(e) {
401 | e.preventDefault();
402 | var $thisButton = $(e.currentTarget);
403 | var $post = $thisButton.parents('.post');
404 | var direction = ($post.hasClass('collapsed')) ? 'slideDown' : 'slideUp';
405 |
406 | // Slide body
407 | $thisButton.siblings('.body')[direction](_.bind(function() {
408 | // Mark post and poke isotope
409 | $post.toggleClass('collapsed').toggleClass('expanded');
410 | this.$timeline.isotope('reLayout');
411 | }, this));
412 | // Change top buttons
413 | this.$el.find('.expand-collapse-buttons a').removeClass('active');
414 | }, this));
415 |
416 | // Handle expand/collapse buttons
417 | this.$el.find('.vertical-timeline-buttons a.expand-all').on('click', _.bind(function(e) {
418 | e.preventDefault();
419 | var $this = $(e.currentTarget);
420 | var thisVT = this;
421 |
422 | this.$el.find('.post .body').slideDown(function() {
423 | thisVT.$timeline.isotope('reLayout');
424 | });
425 | this.$el.find('.post').removeClass('collapsed').addClass('expanded');
426 | this.$el.find('.expand-collapse-buttons a').removeClass('active');
427 | $this.addClass('active');
428 | }, this));
429 | this.$el.find('.vertical-timeline-buttons a.collapse-all').on('click', _.bind(function(e) {
430 | e.preventDefault();
431 | var $this = $(e.currentTarget);
432 | var thisVT = this;
433 |
434 | this.$el.find('.post .body').slideUp(function() {
435 | thisVT.$timeline.isotope('reLayout');
436 | });
437 | this.$el.find('.post').addClass('collapsed').removeClass('expanded');
438 | this.$el.find('.expand-collapse-buttons a').removeClass('active');
439 | $this.addClass('active');
440 | }, this));
441 |
442 | // Sorting buttons
443 | this.$el.find('.sort-buttons a').on('click', _.bind(function(e) {
444 | e.preventDefault();
445 | var $this = $(e.currentTarget);
446 |
447 | // Don't proceed if already selected
448 | if ($this.hasClass('active')) {
449 | return false;
450 | }
451 |
452 | // Mark buttons
453 | this.$el.find('.sort-buttons a').removeClass('active');
454 | $this.addClass('active');
455 |
456 | // Change sorting
457 | if ($this.hasClass('sort-newest')) {
458 | this.updateGroups('newest');
459 | }
460 | else {
461 | this.updateGroups('oldest');
462 | }
463 | }, this));
464 |
465 | // If jQuery resize plugin is enabled and the option is
466 | // enabled then handle resize
467 | if (this.options.handleResize === true && _.isFunction(this.$el.resize)) {
468 | this.$el.resize(_.throttle(_.bind(function() {
469 | this.$el.trigger('vt.isotopized');
470 | }, this), 200));
471 | }
472 |
473 | // Only need to add these once
474 | this.domEventsAdded = true;
475 | this.$el.trigger('vt.domEventsAdded');
476 | },
477 |
478 | // Updates group markers with the appropriate timestamp
479 | // for isotope layout
480 | updateGroups: function(direction) {
481 | var thisVT = this;
482 | direction = direction || this.options.defaultDirection;
483 |
484 | this.$el.find('.group-marker').each(function() {
485 | var $this = $(this);
486 | var timestamp = (direction !== 'newest') ?
487 | thisVT.groups[$this.data('id')].timestampStart :
488 | thisVT.groups[$this.data('id')].timestampEnd;
489 | $this.data('timestamp', timestamp);
490 | });
491 |
492 | // Poke isotope
493 | this.$timeline.isotope('reloadItems')
494 | .isotope({ sortAscending: (direction !== 'newest') });
495 | }
496 | });
497 |
498 |
499 | /**
500 | * Extend Isotope for custom layout: spineAlign
501 | */
502 | _.extend($.Isotope.prototype, {
503 | _spineAlignReset: function() {
504 | this.spineAlign = {
505 | colA: 0,
506 | colB: 0,
507 | lastY: -60
508 | };
509 | },
510 |
511 | _spineAlignLayout: function( $elems ) {
512 | var instance = this,
513 | props = this.spineAlign,
514 | gutterWidth = Math.round( this.options.spineAlign && this.options.spineAlign.gutterWidth ) || 0,
515 | centerX = Math.round(this.element.width() / 2);
516 |
517 | $elems.each(function(i, val) {
518 | var $this = $(this);
519 | var x, y;
520 |
521 | $this.removeClass('last').removeClass('top');
522 | if (i == $elems.length - 1) {
523 | $this.addClass('last');
524 | }
525 | if ($this.hasClass('group-marker')) {
526 | var width = $this.width();
527 | x = centerX - (width / 2);
528 | if (props.colA >= props.colB) {
529 | y = props.colA;
530 | if (y === 0) {
531 | $this.addClass('top');
532 | }
533 | props.colA += $this.outerHeight(true);
534 | props.colB = props.colA;
535 | }
536 | else {
537 | y = props.colB;
538 | if (y === 0) {
539 | $this.addClass('top');
540 | }
541 | props.colB += $this.outerHeight(true);
542 | props.colA = props.colB;
543 | }
544 | }
545 | else {
546 | $this.removeClass('left').removeClass('right');
547 | var isColA = props.colB >= props.colA;
548 | if (isColA) {
549 | $this.addClass('left');
550 | }
551 | else {
552 | $this.addClass('right');
553 | }
554 |
555 | x = isColA ?
556 | centerX - ( $this.outerWidth(true) + gutterWidth / 2 ) : // left side
557 | centerX + (gutterWidth / 2); // right side
558 | y = isColA ? props.colA : props.colB;
559 | if (y - props.lastY <= 60) {
560 | var extraSpacing = 60 - Math.abs(y - props.lastY);
561 | $this.find('.inner').css('marginTop', extraSpacing);
562 | props.lastY = y + extraSpacing;
563 | }
564 | else {
565 | $this.find('.inner').css('marginTop', 0);
566 | props.lastY = y;
567 | }
568 | props[( isColA ? 'colA' : 'colB' )] += $this.outerHeight(true);
569 | }
570 | instance._pushPosition( $this, x, y );
571 | });
572 | },
573 |
574 | _spineAlignGetContainerSize: function() {
575 | var size = {};
576 | size.height = this.spineAlign[( this.spineAlign.colB > this.spineAlign.colA ? 'colB' : 'colA' )];
577 | return size;
578 | },
579 |
580 | _spineAlignResizeChanged: function() {
581 | return true;
582 | }
583 | });
584 |
585 | /**
586 | * Turn verticalTimeline into jQuery plugin
587 | */
588 | $.fn.verticalTimeline = function(options) {
589 | return this.each(function() {
590 | if (!$.data(this, 'verticalTimeline')) {
591 | $.data(this, 'verticalTimeline', new VerticalTimeline(this, options));
592 | }
593 | });
594 | };
595 |
596 | // Incase someone wants to use the base class, return it
597 | return VerticalTimeline;
598 |
599 | });
600 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jquery-vertical-timeline",
3 | "version": "0.3.0",
4 | "homepage": "https://github.com/MinnPost/jquery-vertical-timeline",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/minnpost/jquery-vertical-timeline"
8 | },
9 | "bugs": "https://github.com/minnpost/jquery-vertical-timeline/issues",
10 | "license": "MIT",
11 | "author": {
12 | "name": "MinnPost",
13 | "email": "data@minnpost.com"
14 | },
15 | "dependencies": {},
16 | "devDependencies": {
17 | "grunt": "~0.4.1",
18 | "grunt-contrib-concat": "~0.3.0",
19 | "grunt-contrib-jshint": "~0.8.0",
20 | "grunt-contrib-uglify": "~0.3.1",
21 | "grunt-contrib-copy": "~0.5.0",
22 | "grunt-contrib-clean": "~0.5.0",
23 | "grunt-contrib-watch": "~0.5.3",
24 | "grunt-contrib-connect": "~0.6.0",
25 | "grunt-contrib-requirejs": "~0.4.0",
26 | "grunt-contrib-cssmin": "~0.7.0",
27 | "grunt-contrib-compass": "~0.7.0",
28 | "grunt-text-replace": "~0.3.10"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/styles/styles.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * CSS for timeline-jquery.
3 | */
4 |
5 | @import "compass";
6 |
7 | // Variables
8 | $font-sans-serif: Georgia, sans-serif;
9 |
10 |
11 | // Mixins
12 | @mixin inline-block() {
13 | display: -moz-inline-stack;
14 | display: inline-block;
15 | zoom: 1;
16 | *display: inline;
17 | }
18 |
19 | @mixin clearfix() {
20 | *zoom: 1;
21 |
22 | &:before,
23 | &:after {
24 | content: " ";
25 | display: table;
26 | }
27 |
28 | &:after {
29 | clear: both;
30 | }
31 | }
32 |
33 | // Styles
34 | .vertical-timeline-container {
35 | @include clearfix();
36 |
37 | .vertical-timeline-timeline {
38 | width: 600px;
39 | margin: 20px auto 30px auto;
40 |
41 | // Will show when images are loaded
42 | display: none;
43 | }
44 |
45 | .clearfix {
46 | @include clearfix();
47 | }
48 |
49 | .loading {
50 | text-align: center;
51 | font-size: 2em;
52 | margin: 1em;
53 | color: #565656;
54 | }
55 |
56 |
57 | /**
58 | * Posts
59 | */
60 | .post {
61 | width: 271px;
62 | margin: 0 0 10px 0;
63 | float: left;
64 |
65 | &.last {
66 | margin-bottom: 0;
67 | }
68 |
69 | .inner {
70 | position: relative;
71 | padding: 11px;
72 | border: 1px #adadad solid;
73 | background-color: #fff;
74 | min-height: 37px;
75 | word-wrap: break-word;
76 | @include border-radius(2px);
77 | @include box-shadow(0px 1px 1px 0px #ADADAD);
78 | }
79 |
80 | &.right {
81 | float: right;
82 | }
83 |
84 | .timestamp {
85 | display: none;
86 | }
87 |
88 | h3 {
89 | margin: 0;
90 | font-family: $font-sans-serif;
91 | font-size: 20px;
92 | font-weight: normal;
93 | color: #010101;
94 | padding-right: 30px;
95 | line-height: 1.3em;
96 | }
97 |
98 | .caption {
99 | color: #9d9d9d;
100 | font-family: $font-sans-serif;
101 | font-size: 12px;
102 | margin-top: 4px;
103 | }
104 |
105 | .body {
106 | margin-top: 10px;
107 | }
108 |
109 | &.collapsed .body {
110 | display: none;
111 | }
112 | &.expanded .body {
113 | display: block;
114 | }
115 |
116 | .body img {
117 | max-width: 100%;
118 | }
119 |
120 | .text {
121 | color: #393939;
122 | font-family: Georgia, sans-serif;
123 | font-size: 15px;
124 | margin: 10px 0;
125 | line-height: 1.5;
126 | }
127 |
128 | a.open-close {
129 | background: transparent image-url("button-up-down-arrow.png") no-repeat;
130 | height: 17px;
131 | width: 16px;
132 | position: absolute;
133 | right: 11px;
134 | top: 11px;
135 | }
136 |
137 | &.collapsed a.open-close {
138 | background-position: left bottom;
139 | }
140 |
141 | .title .title-icon {
142 | height: 20px;
143 | width: 20px;
144 | margin-right: 4px;
145 | vertical-align: top;
146 | }
147 |
148 | &.closed .title {
149 | display: table;
150 | min-height: 40px;
151 |
152 | h3 {
153 | display: table-cell;
154 | vertical-align: middle;
155 | }
156 | }
157 |
158 | a.more {
159 | color: #676767;
160 | text-decoration: none;
161 | text-transform: uppercase;
162 | font-size: 12px;
163 | font-weight: bold;
164 | text-align: center;
165 | padding: 4px 15px;
166 | margin: 5px auto 0 auto;
167 | border: 1px #BDBDBD solid;
168 | display: block;
169 | line-height: 28px;
170 | @include border-radius(2px);
171 | @include box-shadow(0px 1px 1px 0px #BDBDBD);
172 | }
173 |
174 | a.more:hover {
175 | background-color: #FAFAFA;
176 | }
177 |
178 | .date {
179 | color: #6a6a6a;
180 | font-size: 11px;
181 | text-align: center;
182 | position: absolute;
183 | top: 19px;
184 | display: block;
185 | width: 54px;
186 | height: 21px;
187 | line-height: 20px;
188 | }
189 |
190 | &.left .date {
191 | background: transparent image-url("tab-left.png") no-repeat right;
192 | right: -54px;
193 | }
194 |
195 | &.right .date {
196 | background: transparent image-url("tab-right.png") no-repeat left;
197 | left: -54px;
198 | }
199 | }
200 |
201 |
202 | /**
203 | * Group marker
204 | */
205 | .group-marker {
206 | float: left;
207 | width: 80px;
208 | margin: 50px 0 15px 0;
209 |
210 | .timestamp {
211 | display: none;
212 | }
213 |
214 | &.top {
215 | margin-top: 0;
216 | }
217 |
218 | .inner {
219 | width: 76px;
220 | height: 26px;
221 | padding: 2px;
222 | background-color: #FFF;
223 | }
224 |
225 | .inner2 {
226 | background-color: #434a50;
227 | @include border-radius(2px);
228 | }
229 | }
230 |
231 |
232 | /**
233 | * Group
234 | */
235 | .group {
236 | text-align: center;
237 | color: #fff;
238 | font-size: 14px;
239 | font-weight: bold;
240 | height: 26px;
241 | line-height: 26px;
242 | }
243 |
244 | /**
245 | * Top buttons
246 | */
247 | .vertical-timeline-buttons {
248 | text-align: center;
249 | margin: 20px 0 10px 0;
250 |
251 | div {
252 | @include inline-block();
253 | }
254 |
255 | .expand-collapse-buttons {
256 | margin-right: 20px;
257 | }
258 |
259 | a {
260 | @include inline-block();
261 | width: 126px;
262 | border: 1px #adadad solid;
263 | @include border-radius(2px);
264 | font-size: 12px;
265 | font-weight: bold;
266 | text-decoration: none;
267 | padding: 10px 12px;
268 | background-color: #FFF;
269 | color: #3b3b3b;
270 | text-transform: uppercase;
271 | }
272 |
273 | a.active:hover,
274 | a.active {
275 | background-color: #f6f6f6;
276 | color: #a9a9a9;
277 | cursor: default;
278 | @include box-shadow(none);
279 | }
280 |
281 | a:hover {
282 | @include box-shadow(0px 1px 1px 0px #adadad);
283 | }
284 |
285 | a span {
286 | padding-right: 15px;
287 | background-position: right -20px;
288 | }
289 |
290 | a.active span {
291 | background-position: right 4px;
292 | }
293 |
294 | a.expand-all span,
295 | a.sort-newest span {
296 | background-image: image-url("down-arrows.png");
297 | background-repeat: no-repeat;
298 | }
299 |
300 | a.collapse-all span,
301 | a.sort-oldest span {
302 | background-image: image-url("up-arrows.png");
303 | background-repeat: no-repeat;
304 | }
305 |
306 |
307 | @media (max-width: 675px) {
308 |
309 | .expand-collapse-buttons,
310 | .sort-buttons {
311 | margin: 0 0 10px 0;
312 | }
313 | }
314 | }
315 |
316 |
317 | /**
318 | * Layout
319 | */
320 | .line-container {
321 | width: 4px;
322 | text-align: center;
323 | margin: 0 auto;
324 | display: block;
325 | }
326 |
327 | .isotope .line {
328 | margin: 0 auto;
329 | background-color: #b3b6b8;
330 | display: block;
331 | float: left;
332 | height: 100%;
333 | left: 298px;
334 | width: 4px;
335 | position: absolute;
336 | }
337 |
338 | /**
339 | * Isotope Filtering
340 | */
341 | .isotope-item {
342 | z-index: 2;
343 | }
344 |
345 | .isotope-hidden.isotope-item {
346 | pointer-events: none;
347 | z-index: 1;
348 | }
349 |
350 | /**
351 | * Isotope CSS3 transitions
352 | */
353 | .vertical-timeline-container .isotope,
354 | .vertical-timeline-container .isotope .isotope-item {
355 | @include transition-duration(0.8s);
356 | }
357 |
358 | .vertical-timeline-container .isotope {
359 | @include transition-property(height, width);
360 | }
361 |
362 | .vertical-timeline-container .isotope .isotope-item {
363 | -webkit-transition-property: -webkit-transform, opacity;
364 | -moz-transition-property: -moz-transform, opacity;
365 | -ms-transition-property: -ms-transform, opacity;
366 | -o-transition-property: top, left, opacity;
367 | transition-property: transform, opacity;
368 | }
369 |
370 | /**
371 | * disabling Isotope CSS3 transitions
372 | */
373 | .isotope.no-transition,
374 | .isotope.no-transition .isotope-item,
375 | .isotope .isotope-item.no-transition {
376 | @include transition-duration(0s);
377 | }
378 | }
379 |
--------------------------------------------------------------------------------
\
59 | <% if (data.photo_url) { %> \
60 |
\
61 | <% } %> \
62 | <% if (data.caption) { %> \
63 |
<%= data.caption %><\/div> \
64 | <% } %> \
65 | <% if (data.body) { %> \
66 |
<%= data.body %><\/div> \
67 | <% } %> \
68 |
\
69 | <% if (data.read_more_url) { %> \
70 | Read more<\/a> \
71 | <% } %> \
72 | <\/div> \
73 | <\/div> \
74 | \
75 | <\/div> \
76 | <\/div> \
77 | ',
78 | groupMarkerTemplate: '
\
79 |
\
80 |
\
81 |
<%= data.groupDisplay %><\/div> \
82 | <\/div> \
83 | <\/div> \
84 | <\/div> \
85 | ',
86 | buttonTemplate: '
\
87 |
\
88 | active<% } %>" href="#">Expand all<\/span><\/a> \
89 | active<% } %>" href="#">Collapse all<\/span><\/a> \
90 | <\/div> \
91 |
\
92 | active<% } %>" href="#">Newest first<\/span><\/a> \
93 | active<% } %>" href="#">Oldest first<\/span><\/a> \
94 | <\/div> \
95 | <\/div> \
96 | ',
97 | timelineTemplate: '
\
98 |
\
99 |
<\/div> \
100 | <\/div> \
101 | <%= data.posts %> \
102 | <%= data.groups %> \
103 | <\/div> \
104 | ',
105 | loadingTemplate: '
\
106 | Loading... \
107 | <\/div> \
108 | '
109 | };
110 |
111 | var groupingFunctions = {};
112 | /**
113 | * Grouping function by Decade.
114 | */
115 | groupingFunctions.groupSegmentByDecade = function(row, groups, direction) {
116 | var year = row.date.year();
117 | var yearStr = year.toString();
118 | var id = yearStr.slice(0, -1);
119 | var start = moment(id + '0-01-01T00:00:00');
120 | var end = moment(id + '9-12-31T12:59:99');
121 |
122 | if (_.isUndefined(groups[id])) {
123 | groups[id] = {
124 | id: id,
125 | groupDisplay: id + '0s',
126 | timestamp: (direction == 'newest') ? end.unix() : start.unix(),
127 | timestampStart: start.unix(),
128 | timestampEnd: end.unix()
129 | };
130 | }
131 |
132 | return groups;
133 | };
134 |
135 | /**
136 | * Grouping function by year.
137 | */
138 | groupingFunctions.groupSegmentByYear = function(row, groups, direction) {
139 | var year = row.date.year();
140 | var start = moment(year + '-01-01T00:00:00');
141 | var end = moment(year + '-12-31T12:59:99');
142 |
143 | if (_.isUndefined(groups[year.toString()])) {
144 | groups[year.toString()] = {
145 | id: year,
146 | groupDisplay: year,
147 | timestamp: (direction == 'newest') ? end.unix() : start.unix(),
148 | timestampStart: start.unix(),
149 | timestampEnd: end.unix()
150 | };
151 | }
152 |
153 | return groups;
154 | };
155 |
156 | /**
157 | * Grouping function by day.
158 | */
159 | groupingFunctions.groupSegmentByDay = function(row, groups, direction) {
160 | var month = new Date(row.timestamp).getMonth();
161 | var year = new Date(row.timestamp).getFullYear();
162 | var day = new Date(row.timestamp).getDate();
163 | var _month_str = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'June', 'July', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec'];
164 | var _time_start = Date.parse(_month_str[month] + ' ' + day + ', ' + year);
165 | var _time_end = Date.parse(_month_str[month] + ' ' + (day+1) + ', ' + year);
166 | var _id = day + (month + year * 100) * 100;
167 |
168 | groups[_id] = {
169 | id: _id,
170 | groupDisplay: _month_str[month] + ' ' + day + ', ' + year,
171 | timestamp: (direction == 'newest') ? _time_end: _time_start,
172 | timestampStart: _time_start,
173 | timestampEnd: _time_end
174 | };
175 |
176 | return groups;
177 | };
178 |
179 |
180 |
181 | /**
182 | * Base class for timeline
183 | */
184 | var VerticalTimeline = function(el, options) {
185 | this.options = $.extend(true, {}, defaultsOptions, options);
186 |
187 | // Check to see if grouping function is an option
188 | this.options.groupFunction = (!_.isFunction(this.options.groupFunction) && _.isFunction(groupingFunctions[this.options.groupFunction])) ? groupingFunctions[this.options.groupFunction] : this.options.groupFunction;
189 |
190 | // Consistent reference to jquery object
191 | this.$el = $(el);
192 |
193 | // Build templates for performance
194 | this.templates = {};
195 | this.templates.post = _.template(this.options.postTemplate);
196 | this.templates.group = _.template(this.options.groupMarkerTemplate);
197 | this.templates.buttons = _.template(this.options.buttonTemplate);
198 | this.templates.timeline = _.template(this.options.timelineTemplate);
199 | this.templates.loading = _.template(this.options.loadingTemplate);
200 |
201 | // Use custom events to make things happens
202 | this.loadEvents();
203 | this.$el.trigger('vt.build');
204 | };
205 |
206 | /**
207 | * Methods and properties of class
208 | */
209 | _.extend(VerticalTimeline.prototype, {
210 | // Events
211 | events: {
212 | 'vt.build': ['buildLayout'],
213 | 'vt.layoutBuilt': ['getData'],
214 | 'vt.gotData': ['parseData'],
215 | 'vt.parsedData': ['buildTimeline'],
216 | 'vt.builtTimeline': ['loadImages', 'adjustWidth'],
217 | 'vt.loadedImages': ['isotopeIt'],
218 | 'vt.isotopized': ['adjustWidth', 'adjustSpine', 'domEvents']
219 | },
220 |
221 | // Event delegation
222 | loadEvents: function() {
223 | _.each(this.events, function(ea, ename) {
224 | _.each(ea, function(ehandler) {
225 | if (_.isFunction(this[ehandler])) {
226 | this.$el.on(ename, _.bind(this[ehandler], this));
227 | }
228 | }, this);
229 | }, this);
230 | },
231 |
232 | // Initial building
233 | buildLayout: function() {
234 | // Add base class for styling
235 | this.$el.addClass('vertical-timeline-container');
236 |
237 | // Add template layout
238 | this.$el.html(this.templates.buttons({
239 | data: this.options
240 | }) + this.templates.loading({}));
241 |
242 | // Get data
243 | this.$el.trigger('vt.layoutBuilt');
244 | },
245 |
246 | // Get data. Data can be from from Google Spreadsheet or JSON
247 | getData: function() {
248 | var thisVT = this;
249 |
250 | // Check if data is set and has data
251 | if (_.isArray(this.options.data) && this.options.data.length > 0) {
252 | this.data = this.options.data;
253 | this.$el.trigger('vt.gotData');
254 | }
255 | else {
256 | Tabletop.init(_.extend({}, this.options.tabletopOptions, {
257 | key: this.options.key,
258 | wanted: [this.options.sheetName],
259 | callback: function(data, tabletop) {
260 | thisVT.data = data[thisVT.options.sheetName].elements;
261 | thisVT.tabletop = tabletop;
262 | thisVT.$el.trigger('vt.gotData');
263 | }
264 | }));
265 | }
266 | },
267 |
268 | // Process data
269 | parseData: function() {
270 | // Placeholder for groups
271 | this.groups = this.groups || {};
272 |
273 | // Go through each row
274 | this.data = _.map(this.data, function(row) {
275 | // Column mapping.
276 | _.each(this.options.columnMapping, function(column, key) {
277 | if (!_.isUndefined(row[column])) {
278 | row[key] = row[column];
279 | }
280 | });
281 |
282 | // Parse date with moment
283 | row.date = moment(row.date, this.options.dateParse);
284 | row.timestamp = row.date.unix();
285 |
286 | // Process into group
287 | this.groups = this.options.groupFunction(row, this.groups, this.options.defaultDirection);
288 |
289 | return row;
290 | }, this);
291 |
292 | // Trigger done
293 | this.$el.trigger('vt.parsedData');
294 | },
295 |
296 | // Build timline
297 | buildTimeline: function() {
298 | this.$el.append(this.templates.timeline({
299 | data: {
300 | posts: _.map(this.data, function(d, di) {
301 | return this.templates.post({
302 | data: d,
303 | options: this.options
304 | });
305 | }, this).join(' '),
306 | groups: _.map(this.groups, function(g, gi) {
307 | return this.templates.group({
308 | data: g
309 | });
310 | }, this).join(' ')
311 | }
312 | }));
313 |
314 | this.$timeline = this.$el.find('.vertical-timeline-timeline');
315 | this.$el.trigger('vt.builtTimeline');
316 | },
317 |
318 | // Wait for images to be loaded
319 | loadImages: function() {
320 | this.$el.imagesLoaded(_.bind(function() {
321 | this.$el.find('.loading').slideUp();
322 | this.$timeline.fadeIn('fast', _.bind(function() {
323 | this.$el.trigger('vt.loadedImages');
324 | }, this));
325 | }, this));
326 | },
327 |
328 | // Make isotope layout
329 | isotopeIt: function() {
330 | this.$el.find('.vertical-timeline-timeline').isotope({
331 | itemSelector: '.item',
332 | transformsEnabled: true,
333 | layoutMode: 'spineAlign',
334 | spineAlign:{
335 | gutterWidth: this.options.gutterWidth
336 | },
337 | getSortData: {
338 | timestamp: function($el) {
339 | return parseFloat($el.data('timestamp'));
340 | }
341 | },
342 | sortBy: 'timestamp',
343 | sortAscending: (this.options.defaultDirection === 'newest') ? false : true,
344 | itemPositionDataEnabled: true,
345 | onLayout: _.bind(function($els, instance) {
346 | this.$el.trigger('vt.isotopized');
347 | }, this),
348 | containerStyle: {
349 | position: 'relative'
350 | }
351 | });
352 | },
353 |
354 | // Adjust width of timeline
355 | adjustWidth: function() {
356 | var w = this.options.width;
357 | var containerW = this.$el.width();
358 | var timelineW;
359 | var postW;
360 |
361 | if (w === 'auto') {
362 | w = containerW + 'px';
363 | }
364 |
365 | // Set timeline width
366 | this.$timeline.css('width', w);
367 | timelineW = this.$timeline.width();
368 |
369 | // Set width on posts
370 | postW = (timelineW / 2) - (this.options.gutterWidth / 2) - 3;
371 | this.$timeline.find('.post').width(postW);
372 | },
373 |
374 | // Adjust the middle line
375 | adjustSpine: function() {
376 | var $lastItem = this.$el.find('.item.last');
377 | var itemPosition = $lastItem.data('isotope-item-position');
378 | var dateHeight = $lastItem.find('.date').height();
379 | var dateOffset = $lastItem.find('.date').position();
380 | var innerMargin = parseInt($lastItem.find('.inner').css('marginTop'), 10);
381 | var top = (dateOffset === undefined) ? 0 : parseInt(dateOffset.top, 10);
382 | var y = (itemPosition && itemPosition.y) ?
383 | parseInt(itemPosition.y, 10) : 0;
384 | var lineHeight = y + innerMargin + top + (dateHeight / 2);
385 | var $line = this.$el.find('.line');
386 | var xOffset = (this.$timeline.width() / 2) - ($line.width() / 2);
387 |
388 | $line.height(lineHeight)
389 | .css('left', xOffset + 'px');
390 | },
391 |
392 | // DOM event
393 | domEvents: function() {
394 | if (this.domEventsAdded) {
395 | this.$el.trigger('vt.domEventsAdded');
396 | return;
397 | }
398 |
399 | // Handle click of open close buttons on post
400 | this.$el.find('.item a.open-close').on('click', _.bind(function(e) {
401 | e.preventDefault();
402 | var $thisButton = $(e.currentTarget);
403 | var $post = $thisButton.parents('.post');
404 | var direction = ($post.hasClass('collapsed')) ? 'slideDown' : 'slideUp';
405 |
406 | // Slide body
407 | $thisButton.siblings('.body')[direction](_.bind(function() {
408 | // Mark post and poke isotope
409 | $post.toggleClass('collapsed').toggleClass('expanded');
410 | this.$timeline.isotope('reLayout');
411 | }, this));
412 | // Change top buttons
413 | this.$el.find('.expand-collapse-buttons a').removeClass('active');
414 | }, this));
415 |
416 | // Handle expand/collapse buttons
417 | this.$el.find('.vertical-timeline-buttons a.expand-all').on('click', _.bind(function(e) {
418 | e.preventDefault();
419 | var $this = $(e.currentTarget);
420 | var thisVT = this;
421 |
422 | this.$el.find('.post .body').slideDown(function() {
423 | thisVT.$timeline.isotope('reLayout');
424 | });
425 | this.$el.find('.post').removeClass('collapsed').addClass('expanded');
426 | this.$el.find('.expand-collapse-buttons a').removeClass('active');
427 | $this.addClass('active');
428 | }, this));
429 | this.$el.find('.vertical-timeline-buttons a.collapse-all').on('click', _.bind(function(e) {
430 | e.preventDefault();
431 | var $this = $(e.currentTarget);
432 | var thisVT = this;
433 |
434 | this.$el.find('.post .body').slideUp(function() {
435 | thisVT.$timeline.isotope('reLayout');
436 | });
437 | this.$el.find('.post').addClass('collapsed').removeClass('expanded');
438 | this.$el.find('.expand-collapse-buttons a').removeClass('active');
439 | $this.addClass('active');
440 | }, this));
441 |
442 | // Sorting buttons
443 | this.$el.find('.sort-buttons a').on('click', _.bind(function(e) {
444 | e.preventDefault();
445 | var $this = $(e.currentTarget);
446 |
447 | // Don't proceed if already selected
448 | if ($this.hasClass('active')) {
449 | return false;
450 | }
451 |
452 | // Mark buttons
453 | this.$el.find('.sort-buttons a').removeClass('active');
454 | $this.addClass('active');
455 |
456 | // Change sorting
457 | if ($this.hasClass('sort-newest')) {
458 | this.updateGroups('newest');
459 | }
460 | else {
461 | this.updateGroups('oldest');
462 | }
463 | }, this));
464 |
465 | // If jQuery resize plugin is enabled and the option is
466 | // enabled then handle resize
467 | if (this.options.handleResize === true && _.isFunction(this.$el.resize)) {
468 | this.$el.resize(_.throttle(_.bind(function() {
469 | this.$el.trigger('vt.isotopized');
470 | }, this), 200));
471 | }
472 |
473 | // Only need to add these once
474 | this.domEventsAdded = true;
475 | this.$el.trigger('vt.domEventsAdded');
476 | },
477 |
478 | // Updates group markers with the appropriate timestamp
479 | // for isotope layout
480 | updateGroups: function(direction) {
481 | var thisVT = this;
482 | direction = direction || this.options.defaultDirection;
483 |
484 | this.$el.find('.group-marker').each(function() {
485 | var $this = $(this);
486 | var timestamp = (direction !== 'newest') ?
487 | thisVT.groups[$this.data('id')].timestampStart :
488 | thisVT.groups[$this.data('id')].timestampEnd;
489 | $this.data('timestamp', timestamp);
490 | });
491 |
492 | // Poke isotope
493 | this.$timeline.isotope('reloadItems')
494 | .isotope({ sortAscending: (direction !== 'newest') });
495 | }
496 | });
497 |
498 |
499 | /**
500 | * Extend Isotope for custom layout: spineAlign
501 | */
502 | _.extend($.Isotope.prototype, {
503 | _spineAlignReset: function() {
504 | this.spineAlign = {
505 | colA: 0,
506 | colB: 0,
507 | lastY: -60
508 | };
509 | },
510 |
511 | _spineAlignLayout: function( $elems ) {
512 | var instance = this,
513 | props = this.spineAlign,
514 | gutterWidth = Math.round( this.options.spineAlign && this.options.spineAlign.gutterWidth ) || 0,
515 | centerX = Math.round(this.element.width() / 2);
516 |
517 | $elems.each(function(i, val) {
518 | var $this = $(this);
519 | var x, y;
520 |
521 | $this.removeClass('last').removeClass('top');
522 | if (i == $elems.length - 1) {
523 | $this.addClass('last');
524 | }
525 | if ($this.hasClass('group-marker')) {
526 | var width = $this.width();
527 | x = centerX - (width / 2);
528 | if (props.colA >= props.colB) {
529 | y = props.colA;
530 | if (y === 0) {
531 | $this.addClass('top');
532 | }
533 | props.colA += $this.outerHeight(true);
534 | props.colB = props.colA;
535 | }
536 | else {
537 | y = props.colB;
538 | if (y === 0) {
539 | $this.addClass('top');
540 | }
541 | props.colB += $this.outerHeight(true);
542 | props.colA = props.colB;
543 | }
544 | }
545 | else {
546 | $this.removeClass('left').removeClass('right');
547 | var isColA = props.colB >= props.colA;
548 | if (isColA) {
549 | $this.addClass('left');
550 | }
551 | else {
552 | $this.addClass('right');
553 | }
554 |
555 | x = isColA ?
556 | centerX - ( $this.outerWidth(true) + gutterWidth / 2 ) : // left side
557 | centerX + (gutterWidth / 2); // right side
558 | y = isColA ? props.colA : props.colB;
559 | if (y - props.lastY <= 60) {
560 | var extraSpacing = 60 - Math.abs(y - props.lastY);
561 | $this.find('.inner').css('marginTop', extraSpacing);
562 | props.lastY = y + extraSpacing;
563 | }
564 | else {
565 | $this.find('.inner').css('marginTop', 0);
566 | props.lastY = y;
567 | }
568 | props[( isColA ? 'colA' : 'colB' )] += $this.outerHeight(true);
569 | }
570 | instance._pushPosition( $this, x, y );
571 | });
572 | },
573 |
574 | _spineAlignGetContainerSize: function() {
575 | var size = {};
576 | size.height = this.spineAlign[( this.spineAlign.colB > this.spineAlign.colA ? 'colB' : 'colA' )];
577 | return size;
578 | },
579 |
580 | _spineAlignResizeChanged: function() {
581 | return true;
582 | }
583 | });
584 |
585 | /**
586 | * Turn verticalTimeline into jQuery plugin
587 | */
588 | $.fn.verticalTimeline = function(options) {
589 | return this.each(function() {
590 | if (!$.data(this, 'verticalTimeline')) {
591 | $.data(this, 'verticalTimeline', new VerticalTimeline(this, options));
592 | }
593 | });
594 | };
595 |
596 | // Incase someone wants to use the base class, return it
597 | return VerticalTimeline;
598 |
599 | });
600 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jquery-vertical-timeline",
3 | "version": "0.3.0",
4 | "homepage": "https://github.com/MinnPost/jquery-vertical-timeline",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/minnpost/jquery-vertical-timeline"
8 | },
9 | "bugs": "https://github.com/minnpost/jquery-vertical-timeline/issues",
10 | "license": "MIT",
11 | "author": {
12 | "name": "MinnPost",
13 | "email": "data@minnpost.com"
14 | },
15 | "dependencies": {},
16 | "devDependencies": {
17 | "grunt": "~0.4.1",
18 | "grunt-contrib-concat": "~0.3.0",
19 | "grunt-contrib-jshint": "~0.8.0",
20 | "grunt-contrib-uglify": "~0.3.1",
21 | "grunt-contrib-copy": "~0.5.0",
22 | "grunt-contrib-clean": "~0.5.0",
23 | "grunt-contrib-watch": "~0.5.3",
24 | "grunt-contrib-connect": "~0.6.0",
25 | "grunt-contrib-requirejs": "~0.4.0",
26 | "grunt-contrib-cssmin": "~0.7.0",
27 | "grunt-contrib-compass": "~0.7.0",
28 | "grunt-text-replace": "~0.3.10"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/styles/styles.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * CSS for timeline-jquery.
3 | */
4 |
5 | @import "compass";
6 |
7 | // Variables
8 | $font-sans-serif: Georgia, sans-serif;
9 |
10 |
11 | // Mixins
12 | @mixin inline-block() {
13 | display: -moz-inline-stack;
14 | display: inline-block;
15 | zoom: 1;
16 | *display: inline;
17 | }
18 |
19 | @mixin clearfix() {
20 | *zoom: 1;
21 |
22 | &:before,
23 | &:after {
24 | content: " ";
25 | display: table;
26 | }
27 |
28 | &:after {
29 | clear: both;
30 | }
31 | }
32 |
33 | // Styles
34 | .vertical-timeline-container {
35 | @include clearfix();
36 |
37 | .vertical-timeline-timeline {
38 | width: 600px;
39 | margin: 20px auto 30px auto;
40 |
41 | // Will show when images are loaded
42 | display: none;
43 | }
44 |
45 | .clearfix {
46 | @include clearfix();
47 | }
48 |
49 | .loading {
50 | text-align: center;
51 | font-size: 2em;
52 | margin: 1em;
53 | color: #565656;
54 | }
55 |
56 |
57 | /**
58 | * Posts
59 | */
60 | .post {
61 | width: 271px;
62 | margin: 0 0 10px 0;
63 | float: left;
64 |
65 | &.last {
66 | margin-bottom: 0;
67 | }
68 |
69 | .inner {
70 | position: relative;
71 | padding: 11px;
72 | border: 1px #adadad solid;
73 | background-color: #fff;
74 | min-height: 37px;
75 | word-wrap: break-word;
76 | @include border-radius(2px);
77 | @include box-shadow(0px 1px 1px 0px #ADADAD);
78 | }
79 |
80 | &.right {
81 | float: right;
82 | }
83 |
84 | .timestamp {
85 | display: none;
86 | }
87 |
88 | h3 {
89 | margin: 0;
90 | font-family: $font-sans-serif;
91 | font-size: 20px;
92 | font-weight: normal;
93 | color: #010101;
94 | padding-right: 30px;
95 | line-height: 1.3em;
96 | }
97 |
98 | .caption {
99 | color: #9d9d9d;
100 | font-family: $font-sans-serif;
101 | font-size: 12px;
102 | margin-top: 4px;
103 | }
104 |
105 | .body {
106 | margin-top: 10px;
107 | }
108 |
109 | &.collapsed .body {
110 | display: none;
111 | }
112 | &.expanded .body {
113 | display: block;
114 | }
115 |
116 | .body img {
117 | max-width: 100%;
118 | }
119 |
120 | .text {
121 | color: #393939;
122 | font-family: Georgia, sans-serif;
123 | font-size: 15px;
124 | margin: 10px 0;
125 | line-height: 1.5;
126 | }
127 |
128 | a.open-close {
129 | background: transparent image-url("button-up-down-arrow.png") no-repeat;
130 | height: 17px;
131 | width: 16px;
132 | position: absolute;
133 | right: 11px;
134 | top: 11px;
135 | }
136 |
137 | &.collapsed a.open-close {
138 | background-position: left bottom;
139 | }
140 |
141 | .title .title-icon {
142 | height: 20px;
143 | width: 20px;
144 | margin-right: 4px;
145 | vertical-align: top;
146 | }
147 |
148 | &.closed .title {
149 | display: table;
150 | min-height: 40px;
151 |
152 | h3 {
153 | display: table-cell;
154 | vertical-align: middle;
155 | }
156 | }
157 |
158 | a.more {
159 | color: #676767;
160 | text-decoration: none;
161 | text-transform: uppercase;
162 | font-size: 12px;
163 | font-weight: bold;
164 | text-align: center;
165 | padding: 4px 15px;
166 | margin: 5px auto 0 auto;
167 | border: 1px #BDBDBD solid;
168 | display: block;
169 | line-height: 28px;
170 | @include border-radius(2px);
171 | @include box-shadow(0px 1px 1px 0px #BDBDBD);
172 | }
173 |
174 | a.more:hover {
175 | background-color: #FAFAFA;
176 | }
177 |
178 | .date {
179 | color: #6a6a6a;
180 | font-size: 11px;
181 | text-align: center;
182 | position: absolute;
183 | top: 19px;
184 | display: block;
185 | width: 54px;
186 | height: 21px;
187 | line-height: 20px;
188 | }
189 |
190 | &.left .date {
191 | background: transparent image-url("tab-left.png") no-repeat right;
192 | right: -54px;
193 | }
194 |
195 | &.right .date {
196 | background: transparent image-url("tab-right.png") no-repeat left;
197 | left: -54px;
198 | }
199 | }
200 |
201 |
202 | /**
203 | * Group marker
204 | */
205 | .group-marker {
206 | float: left;
207 | width: 80px;
208 | margin: 50px 0 15px 0;
209 |
210 | .timestamp {
211 | display: none;
212 | }
213 |
214 | &.top {
215 | margin-top: 0;
216 | }
217 |
218 | .inner {
219 | width: 76px;
220 | height: 26px;
221 | padding: 2px;
222 | background-color: #FFF;
223 | }
224 |
225 | .inner2 {
226 | background-color: #434a50;
227 | @include border-radius(2px);
228 | }
229 | }
230 |
231 |
232 | /**
233 | * Group
234 | */
235 | .group {
236 | text-align: center;
237 | color: #fff;
238 | font-size: 14px;
239 | font-weight: bold;
240 | height: 26px;
241 | line-height: 26px;
242 | }
243 |
244 | /**
245 | * Top buttons
246 | */
247 | .vertical-timeline-buttons {
248 | text-align: center;
249 | margin: 20px 0 10px 0;
250 |
251 | div {
252 | @include inline-block();
253 | }
254 |
255 | .expand-collapse-buttons {
256 | margin-right: 20px;
257 | }
258 |
259 | a {
260 | @include inline-block();
261 | width: 126px;
262 | border: 1px #adadad solid;
263 | @include border-radius(2px);
264 | font-size: 12px;
265 | font-weight: bold;
266 | text-decoration: none;
267 | padding: 10px 12px;
268 | background-color: #FFF;
269 | color: #3b3b3b;
270 | text-transform: uppercase;
271 | }
272 |
273 | a.active:hover,
274 | a.active {
275 | background-color: #f6f6f6;
276 | color: #a9a9a9;
277 | cursor: default;
278 | @include box-shadow(none);
279 | }
280 |
281 | a:hover {
282 | @include box-shadow(0px 1px 1px 0px #adadad);
283 | }
284 |
285 | a span {
286 | padding-right: 15px;
287 | background-position: right -20px;
288 | }
289 |
290 | a.active span {
291 | background-position: right 4px;
292 | }
293 |
294 | a.expand-all span,
295 | a.sort-newest span {
296 | background-image: image-url("down-arrows.png");
297 | background-repeat: no-repeat;
298 | }
299 |
300 | a.collapse-all span,
301 | a.sort-oldest span {
302 | background-image: image-url("up-arrows.png");
303 | background-repeat: no-repeat;
304 | }
305 |
306 |
307 | @media (max-width: 675px) {
308 |
309 | .expand-collapse-buttons,
310 | .sort-buttons {
311 | margin: 0 0 10px 0;
312 | }
313 | }
314 | }
315 |
316 |
317 | /**
318 | * Layout
319 | */
320 | .line-container {
321 | width: 4px;
322 | text-align: center;
323 | margin: 0 auto;
324 | display: block;
325 | }
326 |
327 | .isotope .line {
328 | margin: 0 auto;
329 | background-color: #b3b6b8;
330 | display: block;
331 | float: left;
332 | height: 100%;
333 | left: 298px;
334 | width: 4px;
335 | position: absolute;
336 | }
337 |
338 | /**
339 | * Isotope Filtering
340 | */
341 | .isotope-item {
342 | z-index: 2;
343 | }
344 |
345 | .isotope-hidden.isotope-item {
346 | pointer-events: none;
347 | z-index: 1;
348 | }
349 |
350 | /**
351 | * Isotope CSS3 transitions
352 | */
353 | .vertical-timeline-container .isotope,
354 | .vertical-timeline-container .isotope .isotope-item {
355 | @include transition-duration(0.8s);
356 | }
357 |
358 | .vertical-timeline-container .isotope {
359 | @include transition-property(height, width);
360 | }
361 |
362 | .vertical-timeline-container .isotope .isotope-item {
363 | -webkit-transition-property: -webkit-transform, opacity;
364 | -moz-transition-property: -moz-transform, opacity;
365 | -ms-transition-property: -ms-transform, opacity;
366 | -o-transition-property: top, left, opacity;
367 | transition-property: transform, opacity;
368 | }
369 |
370 | /**
371 | * disabling Isotope CSS3 transitions
372 | */
373 | .isotope.no-transition,
374 | .isotope.no-transition .isotope-item,
375 | .isotope .isotope-item.no-transition {
376 | @include transition-duration(0s);
377 | }
378 | }
379 |
--------------------------------------------------------------------------------