timeline-setter.js | |
---|---|
(function($, undefined){ | |
Expose | var TimelineSetter = window.TimelineSetter = (window.TimelineSetter || {}); |
Current version of | TimelineSetter.VERSION = "0.3.1"; |
MixinsEach mixin operates on an object's | |
The | var observable = function(obj){ |
Registers a callback function for notification at a later time. | obj.bind = function(e, cb){
var callbacks = (this._callbacks = this._callbacks || {});
var list = (callbacks[e] = callbacks[e] || []);
list.push(cb);
}; |
Invoke all callbacks registered to the object with | obj.trigger = function(e){
if(!this._callbacks) return;
var list = this._callbacks[e];
if(!list) return;
for(var i = 0; i < list.length; i++) list[i].apply(this, arguments);
};
}; |
Each | var transformable = function(obj){ |
Move the associated element a specified delta. | obj.move = function(evtName, e){
if (!e.deltaX) return;
if (_.isUndefined(this.currOffset)) this.currOffset = 0;
this.currOffset += e.deltaX;
this.el.css({"left" : this.currOffset});
}; |
The width for the | obj.zoom = function(evtName, e){
if (!e.width) return;
this.el.css({ "width": e.width });
};
}; |
The | var queryable = function(obj, container) {
obj.$ = function(query) {
return window.$(query, container);
};
}; |
PluginsEach plugin operates on an instance of an object. | |
Check to see if we're on a mobile device. | var touchInit = 'ontouchstart' in document;
if (touchInit) $.event.props.push("touches"); |
The | var draggable = function(obj){
var drag; |
Start tracking deltas due to a tap or single click. | function mousedown(e){
e.preventDefault();
drag = {x: e.pageX};
e.type = "dragstart";
obj.el.trigger(e);
} |
The user is interacting; capture the offset and trigger a | function mousemove(e){
if (!drag) return;
e.preventDefault();
e.type = "dragging";
e = _.extend(e, {
deltaX: (e.pageX || e.touches[0].pageX) - drag.x
});
drag = { x: (e.pageX || e.touches[0].pageX) };
obj.el.trigger(e);
} |
We're done tracking the movement set drag back to | function mouseup(e){
if (!drag) return;
drag = null;
e.type = "dragend";
obj.el.trigger(e);
}
if (!touchInit) { |
Bind on mouse events if we have a mouse... | obj.el.bind("mousedown", mousedown);
$(document).bind("mousemove", mousemove);
$(document).bind("mouseup", mouseup);
} else { |
otherwise capture | var last;
obj.el.bind("touchstart", function(e) {
var now = Date.now();
var delta = now - (last || now);
var type = delta > 0 && delta <= 250 ? "doubletap" : "tap";
drag = {x: e.touches[0].pageX};
last = now;
obj.el.trigger($.Event(type));
});
obj.el.bind("touchmove", mousemove);
obj.el.bind("touchend", mouseup);
}
}; |
Older versions of safari fire incredibly huge mousewheel deltas. We'll need to dampen the effects. | var safari = /WebKit\/533/.test(navigator.userAgent); |
The | var wheel = function(obj){
function mousewheel(e){
e.preventDefault();
var delta = (e.wheelDelta || -e.detail);
if (safari){
var negative = delta < 0 ? -1 : 1;
delta = Math.log(Math.abs(delta)) * negative * 2;
}
e.type = "scrolled";
e.deltaX = delta;
obj.el.trigger(e);
}
obj.el.bind("mousewheel DOMMouseScroll", mousewheel);
}; |
Utilities | |
A utility class for storing the extent of the timeline. | var Bounds = function(){
this.min = +Infinity;
this.max = -Infinity;
};
Bounds.prototype.extend = function(num){
this.min = Math.min(num, this.min);
this.max = Math.max(num, this.max);
};
Bounds.prototype.width = function(){
return this.max - this.min;
}; |
Translate a particular number from the current bounds to a given range. | Bounds.prototype.project = function(num, max){
return (num - this.min) / this.width() * max;
}; |
| var Intervals = function(bounds, interval) {
this.max = bounds.max;
this.min = bounds.min;
if(!interval || !this.INTERVALS[interval]) {
var i = this.computeMaxInterval();
this.maxInterval = this.INTERVAL_ORDER[i];
this.idx = i;
} else {
this.maxInterval = interval;
this.idx = _.indexOf(this.INTERVAL_ORDER, interval);
}
}; |
Format dates based for AP style. Pass an override function in the config object to override. | Intervals.dateFormats = function(timestamp) {
var d = new Date(timestamp);
var defaults = {};
var months = ['Jan.', 'Feb.', 'March', 'April', 'May', 'June', 'July', 'Aug.', 'Sept.', 'Oct.', 'Nov.', 'Dec.'];
var bigHours = d.getHours() > 12;
var ampm = " " + (d.getHours() >= 12 ? 'p.m.' : 'a.m.');
defaults.month = months[d.getMonth()];
defaults.year = d.getFullYear();
defaults.date = defaults.month + " " + d.getDate() + ', ' + defaults.year;
var hours;
if(bigHours) {
hours = d.getHours() - 12;
} else {
hours = d.getHours() > 0 ? d.getHours() : "12";
}
hours += ":" + padNumber(d.getMinutes());
defaults.hourWithMinutes = hours + ampm;
defaults.hourWithMinutesAndSeconds = hours + ":" + padNumber(d.getSeconds()) + ampm; |
If we have user overrides, set them to defaults. | return Intervals.formatter(d, defaults) || defaults;
}; |
A utility function to format dates in AP Style. | Intervals.dateStr = function(timestamp, interval) {
var d = new Intervals.dateFormats(timestamp);
switch (interval) {
case "Millennium":
return d.year;
case "Quincentenary":
return d.year;
case "Century":
return d.year;
case "HalfCentury":
return d.year;
case "Decade":
return d.year;
case "Lustrum":
return d.year;
case "FullYear":
return d.year;
case "Month":
return d.month + ', ' + d.year;
case "Week":
return d.date;
case "Date":
return d.date;
case "Hours":
return d.hourWithMinutes;
case "HalfHour":
return d.hourWithMinutes;
case "QuarterHour":
return d.hourWithMinutes;
case "Minutes":
return d.hourWithMinutes;
case "Seconds":
return d.hourWithMinutesAndSeconds;
}
};
Intervals.prototype = { |
Sane estimates of date ranges for the | INTERVALS : {
Millennium : 69379200000000, // 2200 years is the trigger
Quincentenary : 34689600000000, // 1100 years is the trigger
Century : 9460800000000, // 300 years is the trigger
HalfCentury : 3153600000000, // 100 years is the trigger
Decade : 315360000000,
Lustrum : 157680000000,
FullYear : 31536000000,
Month : 2592000000,
Week : 604800000,
Date : 86400000,
Hours : 3600000,
HalfHour : 1800000,
QuarterHour : 900000,
Minutes : 60000,
Seconds : 1000 // 1,000 millliseconds equals on second
}, |
The order used when testing where exactly a timespan falls. | INTERVAL_ORDER : [
'Seconds',
'Minutes',
'QuarterHour',
'HalfHour',
'Hours',
'Date',
'Week',
'Month',
'FullYear',
'Lustrum',
'Decade',
'HalfCentury',
'Century',
'Quincentenary',
'Millennium'
], |
The year adjustment used for supra-year intervals. | YEAR_FRACTIONS : {
Millenium : 1000,
Quincentenary : 500,
Century : 100,
HalfCentury : 50,
Decade : 10,
Lustrum : 5
}, |
A test to find the appropriate range of intervals, for example if a range of
timestamps only spans hours this will return true when called with | isAtLeastA : function(interval) {
return ((this.max - this.min) > this.INTERVALS[interval]);
}, |
Find the maximum interval we should use based on the estimates in | computeMaxInterval : function() {
for (var i = 0; i < this.INTERVAL_ORDER.length; i++) {
if (!this.isAtLeastA(this.INTERVAL_ORDER[i])) break;
}
return i - 1;
}, |
Return the calculated | getMaxInterval : function() {
return this.INTERVALS[this.INTERVAL_ORDER[this.idx]];
}, |
Floor the year to a given epoch. | getYearFloor : function(date, intvl){
var fudge = this.YEAR_FRACTIONS[intvl] || 1;
return (date.getFullYear() / fudge | 0) * fudge;
}, |
Return a date with the year set to the next interval in a given epoch. | getYearCeil : function(date, intvl){
if(this.YEAR_FRACTIONS[intvl]) return this.getYearFloor(date, intvl) + this.YEAR_FRACTIONS[intvl];
return date.getFullYear();
}, |
Return a Date object rounded down to the previous Sunday, a.k.a. the first day of the week. | getWeekFloor: function(date) {
thisDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
thisDate.setDate(date.getDate() - date.getDay());
return thisDate;
}, |
Return a Date object rounded up to the next Sunday, a.k.a. the start of the next week. | getWeekCeil: function(date) {
thisDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
thisDate.setDate(thisDate.getDate() + (7 - date.getDay()));
return thisDate;
}, |
Return the half of the hour this date belongs to. Anything before 30 min. past the hour comes back as zero. Anything after comes back as 30. | getHalfHour: function(date) {
return date.getMinutes() > 30 ? 30 : 0;
}, |
Return the quarter of the hour this date belongs to. Anything before 15 min. past the hour comes back as zero; 15-30 comes back as 15; 30-45 as 30; 45-60 as 45. | getQuarterHour: function(date) {
var minutes = date.getMinutes();
if (minutes < 15) return 0;
if (minutes < 30) return 15;
if (minutes < 45) return 30;
return 45;
}, |
Zero out a date from the current interval down to seconds. | floor : function(ts){
var date = new Date(ts);
var intvl = this.INTERVAL_ORDER[this.idx];
var idx = this.idx > _.indexOf(this.INTERVAL_ORDER,'FullYear') ?
_.indexOf(this.INTERVAL_ORDER,'FullYear') :
idx; |
Zero the special extensions, and adjust as idx necessary. | date.setFullYear(this.getYearFloor(date, intvl));
switch(intvl){
case 'Week':
date.setDate(this.getWeekFloor(date).getDate());
idx = _.indexOf(this.INTERVAL_ORDER, 'Week');
case 'HalfHour':
date.setMinutes(this.getHalfHour(date));
case 'QuarterHour':
date.setMinutes(this.getQuarterHour(date));
} |
Zero out the rest | while(idx--){
intvl = this.INTERVAL_ORDER[idx];
if (!(_.include(['Week', 'HalfHour', 'QuarterHour'].concat(_.keys(this.YEAR_FRACTIONS)), intvl)))
date["set" + intvl](intvl === "Date" ? 1 : 0);
}
return date.getTime();
}, |
Find the next date based on the past in timestamp. | ceil : function(ts){
var date = new Date(this.floor(ts));
var intvl = this.INTERVAL_ORDER[this.idx];
date.setFullYear(this.getYearCeil(date, intvl));
switch(intvl){
case 'Week':
date.setTime(this.getWeekCeil(date).getTime());
break;
case 'HalfHour':
date.setMinutes(this.getHalfHour(date) + 30);
break;
case 'QuarterHour':
date.setMinutes(this.getQuarterHour(date) + 15);
break;
default:
if (!(_.include(['Week', 'HalfHour', 'QuarterHour'].concat(_.keys(this.YEAR_FRACTIONS)), intvl)))
date["set" + intvl](date["get" + intvl]() + 1);
}
return date.getTime();
}, |
The actual difference in timespans accounting for time oddities like different length months and leap years. | span : function(ts){
return this.ceil(ts) - this.floor(ts);
}, |
Calculate and return a list of human formatted strings and raw timestamps. | getRanges : function() {
if (this.intervals) return this.intervals;
this.intervals = [];
for (var i = this.floor(this.min); i <= this.ceil(this.max); i += this.span(i)) {
this.intervals.push({
human : Intervals.dateStr(i, this.maxInterval),
timestamp : i
});
}
return this.intervals;
}
}; |
Handy dandy function to bind a listener on multiple events. For example,
| var sync = function(origin, listener){
var events = Array.prototype.slice.call(arguments, 2);
_.each(events, function(ev){
origin.bind(ev, function(){ listener[ev].apply(listener, arguments); });
});
}; |
Simple function to strip suffixes like | var cleanNumber = function(str){
return parseInt(str.replace(/^[^+\-\d]?([+\-]?\d+)?.*$/, "$1"), 10);
}; |
Zero pad a number less than 10 and return a 2 digit value. | var padNumber = function(number) {
return (number < 10 ? '0' : '') + number;
}; |
A quick and dirty hash manager for setting and getting values from
| var hashStrip = /^#*/;
var history = {
get : function(){
return window.location.hash.replace(hashStrip, "");
},
set : function(url){
window.location.hash = url;
}
}; |
Every new | |
These colors can be styled like such in timeline-setter.css, where the numbers 1-9 cycle through in that order:
The default color will fall through to what is styled with
| var curColor = 1;
var color = function(){
var chosen;
if (curColor < 10) {
chosen = curColor;
curColor += 1;
} else {
chosen = "default";
}
return chosen;
}; |
Models | |
Initialize a Timeline object in a container element specified in the config object. | var Timeline = TimelineSetter.Timeline = function(data, config) {
_.bindAll(this, 'render', 'setCurrentTimeline');
this.data = data.sort(function(a, b){ return a.timestamp - b.timestamp; });
this.bySid = {};
this.cards = [];
this.series = [];
this.config = config;
this.config.container = this.config.container || "#timeline"; |
Override default date formats
by writing a | Intervals.formatter = this.config.formatter || function(d, defaults) { return defaults; };
};
observable(Timeline.prototype);
Timeline.prototype = _.extend(Timeline.prototype, { |
The main kickoff point for rendering the timeline. The | render : function() {
var that = this; |
create | queryable(this, this.config.container); |
Stick the barebones HTML structure in the dom, so we can play with it. | $(this.config.container).html(JST.timeline());
this.bounds = new Bounds();
this.bar = new Bar(this);
this.cardCont = new CardScroller(this);
this.createSeries(this.data);
var range = new Intervals(this.bounds, this.config.interval);
this.intervals = range.getRanges();
this.bounds.extend(this.bounds.min - range.getMaxInterval() / 2);
this.bounds.extend(this.bounds.max + range.getMaxInterval() / 2);
this.bar.render();
sync(this.bar, this.cardCont, "move", "zoom");
this.trigger('render');
new Zoom("in", this);
new Zoom("out", this);
this.chooseNext = new Chooser("next", this);
this.choosePrev = new Chooser("prev", this);
if (!this.$(".TS-card_active").is("*")) this.chooseNext.click(); |
Bind a click handler to this timeline container that sets it as as the global current timeline for key presses. | $(this.config.container).bind('click', this.setCurrentTimeline);
this.trigger('load');
}, |
Set a global with the current timeline, mostly for key presses. | setCurrentTimeline : function() {
TimelineSetter.currentTimeline = this;
}, |
Loop through the JSON and add each element to a series. | createSeries : function(series){
for(var i = 0; i < series.length; i++)
this.add(series[i]);
}, |
If a particular element in the JSON array mentions a series that's not
in the | add : function(card){
if (!(card.series in this.bySid)){
this.bySid[card.series] = new Series(card, this);
this.series.push(this.bySid[card.series]);
}
var series = this.bySid[card.series];
var crd = series.add(card);
this.bounds.extend(series.max());
this.bounds.extend(series.min());
this.trigger('cardAdd', card, crd);
}
}); |
Views | |
The main interactive element in the timeline is | var Bar = function(timeline) {
var that = this;
this.timeline = timeline;
this.el = this.timeline.$(".TS-notchbar");
this.el.css({ "left": 0 });
draggable(this);
wheel(this);
_.bindAll(this, "moving", "doZoom");
this.el.bind("dragging scrolled", this.moving);
this.el.bind("doZoom", this.doZoom);
this.el.bind("dblclick doubletap", function(e){
e.preventDefault();
that.timeline.$(".TS-zoom_in").click();
});
};
observable(Bar.prototype);
transformable(Bar.prototype);
Bar.prototype = _.extend(Bar.prototype, { |
Every time the | moving : function(e){
var parent = this.el.parent();
var pOffset = parent.offset().left;
var offset = this.el.offset().left;
var width = this.el.width();
if (_.isUndefined(e.deltaX)) e.deltaX = 0;
if (offset + width + e.deltaX < pOffset + parent.width())
e.deltaX = (pOffset + parent.width()) - (offset + width);
if (offset + e.deltaX > pOffset)
e.deltaX = pOffset - offset;
this.trigger("move", e);
this.timeline.trigger("move", e); // for API
this.move("move", e);
}, |
As the timeline zooms, the | doZoom : function(e, width){
var that = this;
var notch = this.timeline.$(".TS-notch_active");
var getCur = function() {
return notch.length > 0 ? notch.position().left : 0;
};
var curr = getCur();
this.el.animate({"width": width + "%"}, {
step: function(current, fx) {
var e = $.Event("dragging");
var delta = curr - getCur();
e.deltaX = delta;
that.moving(e);
curr = getCur();
e = $.Event("zoom");
e.width = current + "%";
that.trigger("zoom", e);
}
});
}, |
When asked to render the bar places the appropriate timestamp notches
inside | render : function(){
var intervals = this.timeline.intervals;
var bounds = this.timeline.bounds;
for (var i = 0; i < intervals.length; i++) {
var html = JST.year_notch({'timestamp' : intervals[i].timestamp, 'human' : intervals[i].human });
this.el.append($(html).css("left", bounds.project(intervals[i].timestamp, 100) + "%"));
}
}
}); |
The | var CardScroller = function(timeline){
this.el = timeline.$(".TS-card_scroller_inner");
};
observable(CardScroller.prototype);
transformable(CardScroller.prototype); |
Each | var Series = function(series, timeline) {
this.timeline = timeline;
this.name = series.series;
this.color = this.name.length > 0 ? color() : "default";
this.cards = [];
_.bindAll(this, "render", "showNotches", "hideNotches");
this.timeline.bind("render", this.render);
};
observable(Series.prototype);
Series.prototype = _.extend(Series.prototype, { |
Create and add a particular card to the cards array. | add : function(card){
var crd = new Card(card, this);
this.cards.push(crd);
return crd;
}, |
The comparing function for | _comparator : function(crd){
return crd.timestamp;
}, |
Inactivate this series legend item and trigger a | hideNotches : function(e){
e.preventDefault();
this.el.addClass("TS-series_legend_item_inactive");
this.trigger("hideNotch");
}, |
Activate the legend item and trigger the | showNotches : function(e){
e.preventDefault();
this.el.removeClass("TS-series_legend_item_inactive");
this.trigger("showNotch");
}, |
Create and append the label to | render : function(e){
if (this.name.length === 0) return;
this.el = $(JST.series_legend(this));
this.timeline.$(".TS-series_nav_container").append(this.el);
this.el.toggle(this.hideNotches, this.showNotches);
}
}); |
Proxy to underscore for | _(["min", "max"]).each(function(key){
Series.prototype[key] = function() {
return _[key].call(_, this.cards, this._comparator).get("timestamp");
};
}); |
Every | var Card = function(card, series) {
this.series = series;
this.timeline = this.series.timeline;
card = _.clone(card);
this.timestamp = card.timestamp;
this.attributes = card;
this.attributes.topcolor = series.color;
_.bindAll(this, "render", "activate", "flip", "setPermalink", "toggleNotch");
this.series.bind("hideNotch", this.toggleNotch);
this.series.bind("showNotch", this.toggleNotch);
this.timeline.bind("render", this.render);
this.timeline.bar.bind("move", this.flip);
this.id = [
this.get('timestamp'),
this.get('description').split(/ /)[0].replace(/[^a-zA-Z\-]/g,"")
].join("-");
this.timeline.cards.push(this);
this.template = window.JST.card;
};
Card.prototype = _.extend(Card.prototype, { |
Get a particular attribute by key. | get : function(key){
return this.attributes[key];
}, |
When each | render : function(){
this.offset = this.timeline.bounds.project(this.timestamp, 100);
var html = JST.notch(this.attributes);
this.notch = $(html).css({"left": this.offset + "%"});
this.timeline.$(".TS-notchbar").append(this.notch);
this.notch.click(this.activate);
if (history.get() === this.id) this.activate();
}, |
As the | flip : function() {
if (!this.el || !this.el.is(":visible")) return;
var rightEdge = this.$(".TS-item").offset().left + this.$(".TS-item").width();
var tRightEdge = this.timeline.$(".timeline_setter").offset().left + this.timeline.$(".timeline_setter").width();
var margin = this.el.css("margin-left") === this.originalMargin;
var flippable = this.$(".TS-item").width() < this.timeline.$(".timeline_setter").width() / 2;
var offTimeline = (this.el.offset().left - this.el.parent().offset().left) - this.$(".TS-item").width() < 0; |
If the card's right edge is more than the timeline's right edge and it's never been flipped before and it won't go off the timeline when flipped. We'll flip it. | if (tRightEdge - rightEdge < 0 && margin && !offTimeline) {
this.el.css({"margin-left": -(this.$(".TS-item").width() + 7)});
this.$(".TS-css_arrow").css({"left" : this.$(".TS-item").width()}); |
Otherwise, if the card is off the left side of the timeline and we have flipped it before and the card's width is less than half of the width of the whole timeline, we'll flip it to the default position. | } else if (this.el.offset().left - this.timeline.$(".timeline_setter").offset().left < 0 && !margin && flippable) {
this.el.css({"margin-left": this.originalMargin});
this.$(".TS-css_arrow").css({"left": 0});
}
}, |
The first time a card is activated it renders its | activate : function(e){
var that = this;
this.hideActiveCard();
if (!this.el) {
this.el = $(this.template({card: this})); |
create a | queryable(this, this.el);
this.el.css({"left": this.offset + "%"});
this.timeline.$(".TS-card_scroller_inner").append(this.el);
this.originalMargin = this.el.css("margin-left");
this.el.delegate(".TS-permalink", "click", this.setPermalink); |
Reactivate if there are images in the html so we can recalculate widths and position accordingly. | this.timeline.$("img").load(this.activate);
}
this.el.show().addClass(("TS-card_active"));
this.notch.addClass("TS-notch_active");
this.setWidth(); |
In the case that the card is outside the bounds the wrong way when it's flipped, we'll take care of it here before we move the actual card. | this.flip();
this.move();
this.series.timeline.trigger("cardActivate", this.attributes);
}, |
For Internet Explorer each card sets the width of | setWidth : function(){
var that = this;
var max = _.max(_.toArray(this.$(".TS-item_user_html").children()), function(el){ return that.$(el).width(); });
if (this.$(max).width() > this.$(".TS-item_year").width()) {
this.$(".TS-item_label").css("width", this.$(max).width());
} else {
this.$(".TS-item_label").css("width", this.$(".TS-item_year").width());
}
}, |
Move the | move : function() {
var e = $.Event('moving');
var offset = this.$(".TS-item").offset();
var toffset = this.timeline.$(".timeline_setter").offset();
if (offset.left < toffset.left) {
e.deltaX = toffset.left - offset.left + cleanNumber(this.$(".TS-item").css("padding-left"));
this.timeline.bar.moving(e);
} else if (offset.left + this.$(".TS-item").outerWidth() > toffset.left + this.timeline.$(".timeline_setter").width()) {
e.deltaX = toffset.left + this.timeline.$(".timeline_setter").width() - (offset.left + this.$(".TS-item").outerWidth());
this.timeline.bar.moving(e);
}
}, |
The click handler to set the current hash to the | setPermalink : function() {
history.set(this.id);
}, |
Globally hide any cards with | hideActiveCard : function() {
this.timeline.$(".TS-card_active").removeClass("TS-card_active").hide();
this.timeline.$(".TS-notch_active").removeClass("TS-notch_active");
}, |
An event listener to toggle this notch on and off via | toggleNotch : function(e){
switch (e) {
case "hideNotch":
this.notch.hide().removeClass("TS-notch_active").addClass("TS-series_inactive");
if (this.el) this.el.hide();
return;
case "showNotch":
this.notch.removeClass("TS-series_inactive").show();
}
}
}); |
Simple inheritance helper for | var ctor = function(){};
var inherits = function(child, parent){
ctor.prototype = parent.prototype;
child.prototype = new ctor();
child.prototype.constructor = child;
}; |
Controls | |
Each control is basically a callback wrapper for a given DOM element. | var Control = function(direction, timeline){
this.timeline = timeline;
this.direction = direction;
this.el = this.timeline.$(this.prefix + direction);
var that = this;
this.el.bind('click', function(e) { e.preventDefault(); that.click(e);});
}; |
Each | var curZoom = 100;
var Zoom = function(direction, timeline) {
Control.apply(this, arguments);
};
inherits(Zoom, Control);
Zoom.prototype = _.extend(Zoom.prototype, {
prefix : ".TS-zoom_", |
Adjust the | click : function() {
curZoom += (this.direction === "in" ? +100 : -100);
if (curZoom >= 100) {
this.timeline.$(".TS-notchbar").trigger('doZoom', [curZoom]);
} else {
curZoom = 100;
}
}
}); |
Each | var Chooser = function(direction, timeline) {
Control.apply(this, arguments);
this.notches = this.timeline.$(".TS-notch");
};
inherits(Chooser, Control);
Chooser.prototype = _.extend(Control.prototype, {
prefix: ".TS-choose_", |
Figure out which notch to activate and do so by triggering a click on that notch. | click: function(e){
var el;
var notches = this.notches.not(".TS-series_inactive");
var curCardIdx = notches.index(this.timeline.$(".TS-notch_active"));
var numOfCards = notches.length;
if (this.direction === "next") {
el = (curCardIdx < numOfCards ? notches.eq(curCardIdx + 1) : false);
} else {
el = (curCardIdx > 0 ? notches.eq(curCardIdx - 1) : false);
}
if (!el) return;
el.trigger("click");
}
}); |
JS API | |
The TimelineSetter JS API allows you to listen to certain timeline events, and activate cards programmatically. To take advantage of it, assign the timeline boot function to a variable like so:
then call methods on the | TimelineSetter.Api = function(timeline) {
this.timeline = timeline;
};
TimelineSetter.Api.prototype = _.extend(TimelineSetter.Api.prototype, { |
Register a callback for when the timeline is loaded | onLoad : function(cb) {
this.timeline.bind('load', cb);
}, |
Register a callback for when a card is added to the timeline Callback has access to the event name and the card object | onCardAdd : function(cb) {
this.timeline.bind('cardAdd', cb);
}, |
Register a callback for when a card is activated. Callback has access to the event name and the card object | onCardActivate : function(cb) {
this.timeline.bind('cardActivate', cb);
}, |
Register a callback for when the bar is moved or zoomed. Be careful with this one: Bar move events can be fast and furious, especially with scroll wheels in Safari. | onBarMove : function(cb) { |
Bind a 'move' event to the timeline, because
at this point, | this.timeline.bind('move', cb);
}, |
Show the card matching a given timestamp Right now, timelines only support one card per timestamp | activateCard : function(timestamp) {
_(this.timeline.cards).detect(function(card) { return card.timestamp === timestamp; }).activate();
}
}); |
Global TS keydown function to bind key events to the current global currentTimeline. | TimelineSetter.bindKeydowns = function() {
$(document).bind('keydown', function(e) {
if (e.keyCode === 39) {
TimelineSetter.currentTimeline.chooseNext.click();
} else if (e.keyCode === 37) {
TimelineSetter.currentTimeline.choosePrev.click();
} else {
return;
}
});
}; |
Finally, let's create the whole timeline. Boot is exposed globally via
We also initialize a new API object for each timeline, accessible via the
timeline variable's | Timeline.boot = function(data, config) {
var timeline = TimelineSetter.timeline = new Timeline(data, config || {});
var api = new TimelineSetter.Api(timeline);
if (!TimelineSetter.pageTimelines) {
TimelineSetter.currentTimeline = timeline;
TimelineSetter.bindKeydowns();
}
TimelineSetter.pageTimelines = TimelineSetter.pageTimelines ? TimelineSetter.pageTimelines += 1 : 1;
$(timeline.render);
return {
timeline : timeline,
api : api
};
};
})(jQuery);
|