timeline_setter 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +11 -2
- data/config/assets.yml +9 -0
- data/doc/doc.markdown +65 -17
- data/doc/templates.html +3 -0
- data/doc/timeline-setter.html +327 -238
- data/doc/timeline-setter.min.html +3 -0
- data/documentation/TimelineSetter/CLI.html +77 -52
- data/documentation/TimelineSetter/Parser.html +40 -39
- data/documentation/TimelineSetter/Timeline.html +132 -83
- data/documentation/TimelineSetter.html +27 -12
- data/documentation/_index.html +23 -12
- data/documentation/class_list.html +20 -9
- data/documentation/css/style.css +7 -5
- data/documentation/file.README.html +33 -23
- data/documentation/file_list.html +20 -9
- data/documentation/frames.html +1 -1
- data/documentation/index.html +33 -23
- data/documentation/js/app.js +16 -14
- data/documentation/js/full_list.js +7 -6
- data/documentation/js/jquery.js +3 -3
- data/documentation/method_list.html +42 -23
- data/documentation/top-level-namespace.html +26 -11
- data/index.html +100 -19
- data/lib/timeline_setter/cli.rb +2 -0
- data/lib/timeline_setter/timeline.rb +6 -3
- data/lib/timeline_setter/version.rb +1 -1
- data/lib/timeline_setter.rb +0 -7
- data/public/javascripts/templates/card.jst +21 -0
- data/public/javascripts/templates/notch.jst +1 -0
- data/public/javascripts/templates/series_legend.jst +3 -0
- data/public/javascripts/templates/timeline.jst +20 -0
- data/public/javascripts/templates/year_notch.jst +3 -0
- data/public/javascripts/templates.js +1 -0
- data/public/javascripts/timeline-setter.js +303 -167
- data/public/javascripts/timeline-setter.min.js +1 -0
- data/public/stylesheets/timeline-setter.css +5 -5
- data/spec/timeline_setter_spec.rb +2 -2
- data/templates/timeline-markup.erb +5 -59
- data/timeline_setter.gemspec +15 -5
- metadata +15 -5
@@ -1,9 +1,12 @@
|
|
1
|
-
(function(){
|
1
|
+
(function($, undefined){
|
2
2
|
|
3
3
|
// Expose `TimelineSetter` globally, so we can call `Timeline.Timeline.boot()`
|
4
4
|
// to kick off at any point.
|
5
5
|
var TimelineSetter = window.TimelineSetter = (window.TimelineSetter || {});
|
6
6
|
|
7
|
+
// Current version of `TimelineSetter`
|
8
|
+
TimelineSetter.VERSION = "0.3.0";
|
9
|
+
|
7
10
|
// Mixins
|
8
11
|
// ------
|
9
12
|
// Each mixin operates on an object's `prototype`.
|
@@ -12,18 +15,19 @@
|
|
12
15
|
// object. Unlike other notification systems, when an event is triggered every
|
13
16
|
// callback bound to the object is invoked.
|
14
17
|
var observable = function(obj){
|
15
|
-
|
16
18
|
// Registers a callback function for notification at a later time.
|
17
|
-
obj.bind = function(cb){
|
18
|
-
this._callbacks = this._callbacks ||
|
19
|
-
|
19
|
+
obj.bind = function(e, cb){
|
20
|
+
var callbacks = (this._callbacks = this._callbacks || {});
|
21
|
+
var list = (callbacks[e] = callbacks[e] || []);
|
22
|
+
list.push(cb);
|
20
23
|
};
|
21
24
|
|
22
25
|
// Invoke all callbacks registered to the object with `bind`.
|
23
|
-
obj.trigger = function(){
|
24
|
-
if
|
25
|
-
|
26
|
-
|
26
|
+
obj.trigger = function(e){
|
27
|
+
if(!this._callbacks) return;
|
28
|
+
var list = this._callbacks[e];
|
29
|
+
if(!list) return;
|
30
|
+
for(var i = 0; i < list.length; i++) list[i].apply(this, arguments);
|
27
31
|
};
|
28
32
|
};
|
29
33
|
|
@@ -31,14 +35,9 @@
|
|
31
35
|
// Each `transformable` contains two event listeners that handle moving associated
|
32
36
|
// DOM elements around the page.
|
33
37
|
var transformable = function(obj){
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
// message passing. So each registered callback first checks to see if the
|
38
|
-
// event fired matches the event it is listening for.
|
39
|
-
obj.move = function(e){
|
40
|
-
if (!e.type === "move" || !e.deltaX) return;
|
41
|
-
|
38
|
+
// Move the associated element a specified delta.
|
39
|
+
obj.move = function(evtName, e){
|
40
|
+
if (!e.deltaX) return;
|
42
41
|
if (_.isUndefined(this.currOffset)) this.currOffset = 0;
|
43
42
|
this.currOffset += e.deltaX;
|
44
43
|
this.el.css({"left" : this.currOffset});
|
@@ -47,12 +46,20 @@
|
|
47
46
|
// The width for the `Bar` and `CardContainer` objects is set in percentages,
|
48
47
|
// in order to zoom the Timeline all that's needed is to increase or decrease
|
49
48
|
// the percentage width.
|
50
|
-
obj.zoom = function(e){
|
51
|
-
if (!e.
|
49
|
+
obj.zoom = function(evtName, e){
|
50
|
+
if (!e.width) return;
|
52
51
|
this.el.css({ "width": e.width });
|
53
52
|
};
|
54
53
|
};
|
55
54
|
|
55
|
+
// The `queryable` mixin scopes jQuery to
|
56
|
+
// a given container.
|
57
|
+
var queryable = function(obj, container) {
|
58
|
+
obj.$ = function(query) {
|
59
|
+
return window.$(query, container);
|
60
|
+
};
|
61
|
+
};
|
62
|
+
|
56
63
|
|
57
64
|
// Plugins
|
58
65
|
// -------
|
@@ -60,7 +67,7 @@
|
|
60
67
|
|
61
68
|
// Check to see if we're on a mobile device.
|
62
69
|
var touchInit = 'ontouchstart' in document;
|
63
|
-
if (touchInit)
|
70
|
+
if (touchInit) $.event.props.push("touches");
|
64
71
|
|
65
72
|
// The `draggable` plugin tracks changes in X offsets due to mouse movement
|
66
73
|
// or finger gestures and proxies associated events on a particular element.
|
@@ -182,41 +189,56 @@
|
|
182
189
|
}
|
183
190
|
};
|
184
191
|
|
185
|
-
//
|
186
|
-
|
187
|
-
|
192
|
+
// Format dates based for AP style.
|
193
|
+
// Pass an override function in the config object to override.
|
194
|
+
Intervals.dateFormats = function(timestamp) {
|
195
|
+
var d = new Date(timestamp);
|
196
|
+
var defaults = {};
|
197
|
+
var months = ['Jan.', 'Feb.', 'March', 'April', 'May', 'June', 'July', 'Aug.', 'Sept.', 'Oct.', 'Nov.', 'Dec.'];
|
198
|
+
var bigHours = d.getHours() > 12;
|
199
|
+
var ampm = " " + (d.getHours() >= 12 ? 'p.m.' : 'a.m.');
|
200
|
+
|
201
|
+
|
202
|
+
defaults.month = months[d.getMonth()];
|
203
|
+
defaults.year = d.getFullYear();
|
204
|
+
defaults.date = defaults.month + " " + d.getDate() + ', ' + defaults.year;
|
205
|
+
|
206
|
+
var hours;
|
207
|
+
if(bigHours) {
|
208
|
+
hours = d.getHours() - 12;
|
209
|
+
} else {
|
210
|
+
hours = d.getHours() > 0 ? d.getHours() : "12";
|
211
|
+
}
|
212
|
+
|
213
|
+
hours += ":" + padNumber(d.getMinutes());
|
214
|
+
defaults.hourWithMinutes = hours + ampm;
|
215
|
+
defaults.hourWithMinutesAndSeconds = hours + ":" + padNumber(d.getSeconds()) + ampm;
|
216
|
+
// If we have user overrides, set them to defaults.
|
217
|
+
return Intervals.formatter(d, defaults) || defaults;
|
188
218
|
};
|
189
219
|
|
190
220
|
// A utility function to format dates in AP Style.
|
191
221
|
Intervals.dateStr = function(timestamp, interval) {
|
192
|
-
var d
|
193
|
-
var dYear = d.getFullYear();
|
194
|
-
var dMonth = Intervals.HUMAN_DATES.months[d.getMonth()];
|
195
|
-
var dDate = dMonth + " " + d.getDate() + ', ' + dYear;
|
196
|
-
var bigHours = d.getHours() > 12;
|
197
|
-
var isPM = d.getHours() >= 12;
|
198
|
-
var dHourWithMinutes = (bigHours ? d.getHours() - 12 : (d.getHours() > 0 ? d.getHours() : "12")) + ":" + padNumber(d.getMinutes()) + " " + (isPM ? 'p.m.' : 'a.m.');
|
199
|
-
var dHourMinuteSecond = dHourWithMinutes + ":" + padNumber(d.getSeconds());
|
200
|
-
|
222
|
+
var d = new Intervals.dateFormats(timestamp);
|
201
223
|
switch (interval) {
|
202
224
|
case "Decade":
|
203
|
-
return
|
225
|
+
return d.year;
|
204
226
|
case "Lustrum":
|
205
|
-
return
|
227
|
+
return d.year;
|
206
228
|
case "FullYear":
|
207
|
-
return
|
229
|
+
return d.year;
|
208
230
|
case "Month":
|
209
|
-
return
|
231
|
+
return d.month + ', ' + d.year;
|
210
232
|
case "Week":
|
211
|
-
return
|
233
|
+
return d.date;
|
212
234
|
case "Date":
|
213
|
-
return
|
235
|
+
return d.date;
|
214
236
|
case "Hours":
|
215
|
-
return
|
237
|
+
return d.hourWithMinutes;
|
216
238
|
case "Minutes":
|
217
|
-
return
|
239
|
+
return d.hourWithMinutes;
|
218
240
|
case "Seconds":
|
219
|
-
return
|
241
|
+
return d.hourWithMinutesAndSeconds;
|
220
242
|
}
|
221
243
|
};
|
222
244
|
|
@@ -245,8 +267,9 @@
|
|
245
267
|
|
246
268
|
// Find the maximum interval we should use based on the estimates in `INTERVALS`.
|
247
269
|
computeMaxInterval : function() {
|
248
|
-
for (var i = 0; i < this.INTERVAL_ORDER.length; i++)
|
270
|
+
for (var i = 0; i < this.INTERVAL_ORDER.length; i++) {
|
249
271
|
if (!this.isAtLeastA(this.INTERVAL_ORDER[i])) break;
|
272
|
+
}
|
250
273
|
return i - 1;
|
251
274
|
},
|
252
275
|
|
@@ -266,7 +289,7 @@
|
|
266
289
|
|
267
290
|
// Returns the first year of the five year "lustrum" a Date belongs to
|
268
291
|
// as an integer. A lustrum is a fancy Roman word for a "five-year period."
|
269
|
-
// You can read more about it [here](http://en.wikipedia.org/wiki/Lustrum).
|
292
|
+
// You can read more about it [here](http://en.wikipedia.org/wiki/Lustrum).
|
270
293
|
// This all means that if you pass in the year 2011 you'll get 2010 back.
|
271
294
|
// And if you pass in the year 1997 you'll get 1995 back.
|
272
295
|
getLustrum : function(date) {
|
@@ -297,7 +320,7 @@
|
|
297
320
|
|
298
321
|
// Zero the special extensions, and adjust as idx necessary.
|
299
322
|
switch(intvl){
|
300
|
-
case 'Decade':
|
323
|
+
case 'Decade':
|
301
324
|
date.setFullYear(this.getDecade(date));
|
302
325
|
break;
|
303
326
|
case 'Lustrum':
|
@@ -310,7 +333,7 @@
|
|
310
333
|
|
311
334
|
// Zero out the rest
|
312
335
|
while(idx--){
|
313
|
-
|
336
|
+
intvl = this.INTERVAL_ORDER[idx];
|
314
337
|
if(intvl !== 'Week') date["set" + intvl](intvl === "Date" ? 1 : 0);
|
315
338
|
}
|
316
339
|
|
@@ -331,7 +354,7 @@
|
|
331
354
|
case 'Week':
|
332
355
|
date.setTime(this.getWeekCeil(date).getTime());
|
333
356
|
break;
|
334
|
-
default:
|
357
|
+
default:
|
335
358
|
date["set" + intvl](date["get" + intvl]() + 1);
|
336
359
|
}
|
337
360
|
return date.getTime();
|
@@ -365,18 +388,10 @@
|
|
365
388
|
var sync = function(origin, listener){
|
366
389
|
var events = Array.prototype.slice.call(arguments, 2);
|
367
390
|
_.each(events, function(ev){
|
368
|
-
origin.bind(function(
|
369
|
-
if (e.type === ev && listener[ev])
|
370
|
-
listener[ev](e);
|
371
|
-
});
|
391
|
+
origin.bind(ev, function(){ listener[ev].apply(listener, arguments); });
|
372
392
|
});
|
373
393
|
};
|
374
394
|
|
375
|
-
// Get a template from the DOM and return a compiled function.
|
376
|
-
var template = function(query) {
|
377
|
-
return _.template($(query).html());
|
378
|
-
};
|
379
|
-
|
380
395
|
// Simple function to strip suffixes like `"px"` and return a clean integer for
|
381
396
|
// use.
|
382
397
|
var cleanNumber = function(str){
|
@@ -436,32 +451,80 @@
|
|
436
451
|
// Models
|
437
452
|
// ------
|
438
453
|
|
439
|
-
//
|
440
|
-
//
|
441
|
-
// intervals `sync`s the `Bar` and `CardContainer` objects and triggers the
|
442
|
-
// `render` event.
|
454
|
+
// Initialize a Timeline object in a container element specified
|
455
|
+
// in the config object.
|
443
456
|
var Timeline = TimelineSetter.Timeline = function(data, config) {
|
444
|
-
|
457
|
+
_.bindAll(this, 'render', 'setCurrentTimeline');
|
458
|
+
this.data = data.sort(function(a, b){ return a.timestamp - b.timestamp; });
|
445
459
|
this.bySid = {};
|
460
|
+
this.cards = [];
|
446
461
|
this.series = [];
|
447
|
-
this.config =
|
448
|
-
this.
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
462
|
+
this.config = config;
|
463
|
+
this.config.container = this.config.container || "#timeline";
|
464
|
+
|
465
|
+
// Override default date formats
|
466
|
+
// by writing a `formatter` function that returns
|
467
|
+
// an object containing all the formats
|
468
|
+
// you'd like to override. Pass in `d`
|
469
|
+
// which is a date object, and `defaults`, which
|
470
|
+
// are the formatters we override.
|
471
|
+
//
|
472
|
+
// formatter : function(d, defaults) {
|
473
|
+
// defaults.months = ['enero', 'febrero', 'marzo',
|
474
|
+
// 'abril', 'mayo', 'junio', 'julio',
|
475
|
+
// 'agosto', 'septiembre', 'octubre',
|
476
|
+
// 'noviembre', 'diciembre'];
|
477
|
+
// return defaults;
|
478
|
+
// }
|
479
|
+
Intervals.formatter = this.config.formatter || function(d, defaults) { return defaults; };
|
461
480
|
};
|
462
481
|
observable(Timeline.prototype);
|
463
482
|
|
464
483
|
Timeline.prototype = _.extend(Timeline.prototype, {
|
484
|
+
// The main kickoff point for rendering the timeline. The `Timeline` constructor
|
485
|
+
// takes a JSON array of card representations and then builds series, calculates
|
486
|
+
// intervals `sync`s the `Bar` and `CardContainer` objects.
|
487
|
+
render : function() {
|
488
|
+
var that = this;
|
489
|
+
|
490
|
+
// create `this.$` from queryable mixin.
|
491
|
+
queryable(this, this.config.container);
|
492
|
+
|
493
|
+
// Stick the barebones HTML structure in the dom,
|
494
|
+
// so we can play with it.
|
495
|
+
$(this.config.container).html(JST.timeline());
|
496
|
+
|
497
|
+
this.bounds = new Bounds();
|
498
|
+
this.bar = new Bar(this);
|
499
|
+
this.cardCont = new CardScroller(this);
|
500
|
+
this.createSeries(this.data);
|
501
|
+
var range = new Intervals(this.bounds, this.config.interval);
|
502
|
+
this.intervals = range.getRanges();
|
503
|
+
this.bounds.extend(this.bounds.min - range.getMaxInterval() / 2);
|
504
|
+
this.bounds.extend(this.bounds.max + range.getMaxInterval() / 2);
|
505
|
+
this.bar.render();
|
506
|
+
sync(this.bar, this.cardCont, "move", "zoom");
|
507
|
+
this.trigger('render');
|
508
|
+
|
509
|
+
new Zoom("in", this);
|
510
|
+
new Zoom("out", this);
|
511
|
+
this.chooseNext = new Chooser("next", this);
|
512
|
+
this.choosePrev = new Chooser("prev", this);
|
513
|
+
if (!this.$(".TS-card_active").is("*")) this.chooseNext.click();
|
514
|
+
|
515
|
+
// Bind a click handler to this timeline container
|
516
|
+
// that sets it as as the global current timeline
|
517
|
+
// for key presses.
|
518
|
+
$(this.config.container).bind('click', this.setCurrentTimeline);
|
519
|
+
|
520
|
+
this.trigger('load');
|
521
|
+
},
|
522
|
+
|
523
|
+
// Set a global with the current timeline, mostly for key presses.
|
524
|
+
setCurrentTimeline : function() {
|
525
|
+
TimelineSetter.currentTimeline = this;
|
526
|
+
},
|
527
|
+
|
465
528
|
// Loop through the JSON and add each element to a series.
|
466
529
|
createSeries : function(series){
|
467
530
|
for(var i = 0; i < series.length; i++)
|
@@ -481,6 +544,8 @@
|
|
481
544
|
|
482
545
|
this.bounds.extend(series.max());
|
483
546
|
this.bounds.extend(series.min());
|
547
|
+
|
548
|
+
this.trigger('cardAdd', card);
|
484
549
|
}
|
485
550
|
});
|
486
551
|
|
@@ -494,18 +559,19 @@
|
|
494
559
|
// scenes `Bar` handles the moving and zooming behaviours through the `draggable`
|
495
560
|
// and `wheel` plugins.
|
496
561
|
var Bar = function(timeline) {
|
497
|
-
|
498
|
-
this.el.css({ "left": 0 });
|
562
|
+
var that = this;
|
499
563
|
this.timeline = timeline;
|
564
|
+
|
565
|
+
this.el = this.timeline.$(".TS-notchbar");
|
566
|
+
this.el.css({ "left": 0 });
|
500
567
|
draggable(this);
|
501
568
|
wheel(this);
|
502
569
|
_.bindAll(this, "moving", "doZoom");
|
503
570
|
this.el.bind("dragging scrolled", this.moving);
|
504
571
|
this.el.bind("doZoom", this.doZoom);
|
505
|
-
this.template = template("#TS-year_notch_tmpl");
|
506
572
|
this.el.bind("dblclick doubletap", function(e){
|
507
573
|
e.preventDefault();
|
508
|
-
|
574
|
+
that.timeline.$(".TS-zoom_in").click();
|
509
575
|
});
|
510
576
|
};
|
511
577
|
observable(Bar.prototype);
|
@@ -527,9 +593,9 @@
|
|
527
593
|
if (offset + e.deltaX > pOffset)
|
528
594
|
e.deltaX = pOffset - offset;
|
529
595
|
|
530
|
-
|
531
|
-
this.trigger(e);
|
532
|
-
this.move(e);
|
596
|
+
this.trigger("move", e);
|
597
|
+
this.timeline.trigger("move", e); // for API
|
598
|
+
this.move("move", e);
|
533
599
|
},
|
534
600
|
|
535
601
|
// As the timeline zooms, the `Bar` tries to keep the current notch (i.e.
|
@@ -538,7 +604,7 @@
|
|
538
604
|
// bar to correct for this behaviour, and in future versions we'll fix this.
|
539
605
|
doZoom : function(e, width){
|
540
606
|
var that = this;
|
541
|
-
var notch =
|
607
|
+
var notch = this.timeline.$(".TS-notch_active");
|
542
608
|
var getCur = function() {
|
543
609
|
return notch.length > 0 ? notch.position().left : 0;
|
544
610
|
};
|
@@ -553,7 +619,7 @@
|
|
553
619
|
curr = getCur();
|
554
620
|
e = $.Event("zoom");
|
555
621
|
e.width = current + "%";
|
556
|
-
that.trigger(e);
|
622
|
+
that.trigger("zoom", e);
|
557
623
|
}
|
558
624
|
});
|
559
625
|
},
|
@@ -565,7 +631,7 @@
|
|
565
631
|
var bounds = this.timeline.bounds;
|
566
632
|
|
567
633
|
for (var i = 0; i < intervals.length; i++) {
|
568
|
-
var html =
|
634
|
+
var html = JST.year_notch({'timestamp' : intervals[i].timestamp, 'human' : intervals[i].human });
|
569
635
|
this.el.append($(html).css("left", bounds.project(intervals[i].timestamp, 100) + "%"));
|
570
636
|
}
|
571
637
|
}
|
@@ -575,7 +641,7 @@
|
|
575
641
|
// The `CardScroller` mirrors the moving and zooming of the `Bar` and is the
|
576
642
|
// canvas where individual cards are rendered.
|
577
643
|
var CardScroller = function(timeline){
|
578
|
-
this.el =
|
644
|
+
this.el = timeline.$(".TS-card_scroller_inner");
|
579
645
|
};
|
580
646
|
observable(CardScroller.prototype);
|
581
647
|
transformable(CardScroller.prototype);
|
@@ -588,8 +654,7 @@
|
|
588
654
|
this.color = this.name.length > 0 ? color() : "default";
|
589
655
|
this.cards = [];
|
590
656
|
_.bindAll(this, "render", "showNotches", "hideNotches");
|
591
|
-
this.
|
592
|
-
this.timeline.bind(this.render);
|
657
|
+
this.timeline.bind("render", this.render);
|
593
658
|
};
|
594
659
|
observable(Series.prototype);
|
595
660
|
|
@@ -609,23 +674,22 @@
|
|
609
674
|
hideNotches : function(e){
|
610
675
|
e.preventDefault();
|
611
676
|
this.el.addClass("TS-series_legend_item_inactive");
|
612
|
-
this.trigger(
|
677
|
+
this.trigger("hideNotch");
|
613
678
|
},
|
614
679
|
|
615
680
|
// Activate the legend item and trigger the `showNotch` event.
|
616
681
|
showNotches : function(e){
|
617
682
|
e.preventDefault();
|
618
683
|
this.el.removeClass("TS-series_legend_item_inactive");
|
619
|
-
this.trigger(
|
684
|
+
this.trigger("showNotch");
|
620
685
|
},
|
621
686
|
|
622
687
|
// Create and append the label to `.TS-series_nav_container` and bind up
|
623
688
|
// `hideNotches` and `showNotches`.
|
624
689
|
render : function(e){
|
625
|
-
if (!e.type === "render") return;
|
626
690
|
if (this.name.length === 0) return;
|
627
|
-
this.el = $(
|
628
|
-
|
691
|
+
this.el = $(JST.series_legend(this));
|
692
|
+
this.timeline.$(".TS-series_nav_container").append(this.el);
|
629
693
|
this.el.toggle(this.hideNotches, this.showNotches);
|
630
694
|
}
|
631
695
|
});
|
@@ -642,43 +706,37 @@
|
|
642
706
|
// and a `.TS-card_container` which is lazily rendered.
|
643
707
|
var Card = function(card, series) {
|
644
708
|
this.series = series;
|
645
|
-
|
709
|
+
this.timeline = this.series.timeline;
|
710
|
+
card = _.clone(card);
|
646
711
|
this.timestamp = card.timestamp;
|
647
712
|
this.attributes = card;
|
648
713
|
this.attributes.topcolor = series.color;
|
649
|
-
|
650
|
-
this.template = template("#TS-card_tmpl");
|
651
|
-
this.ntemplate = template("#TS-notch_tmpl");
|
652
714
|
_.bindAll(this, "render", "activate", "flip", "setPermalink", "toggleNotch");
|
653
|
-
this.series.bind(this.toggleNotch);
|
654
|
-
this.series.
|
655
|
-
this.
|
715
|
+
this.series.bind("hideNotch", this.toggleNotch);
|
716
|
+
this.series.bind("showNotch", this.toggleNotch);
|
717
|
+
this.timeline.bind("render", this.render);
|
718
|
+
this.timeline.bar.bind("flip", this.flip);
|
656
719
|
this.id = [
|
657
720
|
this.get('timestamp'),
|
658
721
|
this.get('description').split(/ /)[0].replace(/[^a-zA-Z\-]/g,"")
|
659
722
|
].join("-");
|
723
|
+
this.timeline.cards.push(this);
|
660
724
|
};
|
661
725
|
|
662
|
-
Card.prototype = {
|
726
|
+
Card.prototype = _.extend(Card.prototype, {
|
663
727
|
// Get a particular attribute by key.
|
664
728
|
get : function(key){
|
665
729
|
return this.attributes[key];
|
666
730
|
},
|
667
731
|
|
668
|
-
// A version of `jQuery` scoped to the `Card`'s element.
|
669
|
-
$ : function(query){
|
670
|
-
return $(query, this.el);
|
671
|
-
},
|
672
|
-
|
673
732
|
// When each `Card` is rendered via a render event, it appends a notch to the
|
674
733
|
// `Bar` and binds a click handler so it can be activated. if the `Card`'s id
|
675
734
|
// is currently selected via `window.location.hash` it's activated.
|
676
|
-
render : function(
|
677
|
-
|
678
|
-
|
679
|
-
var html = this.ntemplate(this.attributes);
|
735
|
+
render : function(){
|
736
|
+
this.offset = this.timeline.bounds.project(this.timestamp, 100);
|
737
|
+
var html = JST.notch(this.attributes);
|
680
738
|
this.notch = $(html).css({"left": this.offset + "%"});
|
681
|
-
|
739
|
+
this.timeline.$(".TS-notchbar").append(this.notch);
|
682
740
|
this.notch.click(this.activate);
|
683
741
|
if (history.get() === this.id) this.activate();
|
684
742
|
},
|
@@ -686,15 +744,15 @@
|
|
686
744
|
// As the `Bar` moves the current card checks to see if it's outside the viewport,
|
687
745
|
// if it is the card is flipped so as to be visible for the longest period
|
688
746
|
// of time. The magic number here (7) is half the width of the css arrow.
|
689
|
-
flip : function(
|
690
|
-
if (
|
747
|
+
flip : function() {
|
748
|
+
if (!this.el || !this.el.is(":visible")) return;
|
691
749
|
var rightEdge = this.$(".TS-item").offset().left + this.$(".TS-item").width();
|
692
|
-
var tRightEdge =
|
750
|
+
var tRightEdge = this.timeline.$(".timeline_setter").offset().left + this.timeline.$(".timeline_setter").width();
|
693
751
|
var margin = this.el.css("margin-left") === this.originalMargin;
|
694
|
-
var flippable = this.$(".TS-item").width() <
|
752
|
+
var flippable = this.$(".TS-item").width() < this.timeline.$(".timeline_setter").width() / 2;
|
695
753
|
var offTimeline = this.el.position().left - this.$(".TS-item").width() < 0;
|
696
754
|
|
697
|
-
// If the card's right edge is more than the timeline's right edge and
|
755
|
+
// If the card's right edge is more than the timeline's right edge and
|
698
756
|
// it's never been flipped before and it won't go off the timeline when
|
699
757
|
// flipped. We'll flip it.
|
700
758
|
if (tRightEdge - rightEdge < 0 && margin && !offTimeline) {
|
@@ -703,7 +761,7 @@
|
|
703
761
|
// Otherwise, if the card is off the left side of the timeline and we have
|
704
762
|
// flipped it before and the card's width is less than half of the width
|
705
763
|
// of the whole timeline, we'll flip it to the default position.
|
706
|
-
} else if (this.el.offset().left -
|
764
|
+
} else if (this.el.offset().left - this.timeline.$(".timeline_setter").offset().left < 0 && !margin && flippable) {
|
707
765
|
this.el.css({"margin-left": this.originalMargin});
|
708
766
|
this.$(".TS-css_arrow").css({"left": 0});
|
709
767
|
}
|
@@ -714,26 +772,33 @@
|
|
714
772
|
// and moves the `Bar` if its element outside the visible portion of the
|
715
773
|
// timeline.
|
716
774
|
activate : function(e){
|
775
|
+
var that = this;
|
717
776
|
this.hideActiveCard();
|
718
777
|
if (!this.el) {
|
719
|
-
this.el = $(
|
778
|
+
this.el = $(JST.card({card: this}));
|
779
|
+
|
780
|
+
// create a `this.$` scoped to its card.
|
781
|
+
queryable(this, this.el);
|
782
|
+
|
720
783
|
this.el.css({"left": this.offset + "%"});
|
721
|
-
|
784
|
+
this.timeline.$(".TS-card_scroller_inner").append(this.el);
|
722
785
|
this.originalMargin = this.el.css("margin-left");
|
723
786
|
this.el.delegate(".TS-permalink", "click", this.setPermalink);
|
724
787
|
// Reactivate if there are images in the html so we can recalculate
|
725
788
|
// widths and position accordingly.
|
726
|
-
this.$("img").load(this.activate);
|
789
|
+
this.timeline.$("img").load(this.activate);
|
727
790
|
}
|
791
|
+
|
728
792
|
this.el.show().addClass(("TS-card_active"));
|
729
793
|
this.notch.addClass("TS-notch_active");
|
730
794
|
this.setWidth();
|
731
795
|
|
732
|
-
// In the case that the card is outside the bounds the wrong way when
|
796
|
+
// In the case that the card is outside the bounds the wrong way when
|
733
797
|
// it's flipped, we'll take care of it here before we move the actual
|
734
798
|
// card.
|
735
|
-
this.flip(
|
799
|
+
this.flip();
|
736
800
|
this.move();
|
801
|
+
this.series.timeline.trigger("cardActivate", this.attributes);
|
737
802
|
},
|
738
803
|
|
739
804
|
// For Internet Explorer each card sets the width of` .TS-item_label` to
|
@@ -742,9 +807,10 @@
|
|
742
807
|
// width. Which is a funny way of saying, if you'd like to set the width of
|
743
808
|
// the card as a whole, fiddle with `.TS-item_year`s width.
|
744
809
|
setWidth : function(){
|
745
|
-
var
|
746
|
-
|
747
|
-
|
810
|
+
var that = this;
|
811
|
+
var max = _.max(_.toArray(this.$(".TS-item_user_html").children()), function(el){ return that.$(el).width(); });
|
812
|
+
if (this.$(max).width() > this.$(".TS-item_year").width()) {
|
813
|
+
this.$(".TS-item_label").css("width", this.$(max).width());
|
748
814
|
} else {
|
749
815
|
this.$(".TS-item_label").css("width", this.$(".TS-item_year").width());
|
750
816
|
}
|
@@ -754,13 +820,13 @@
|
|
754
820
|
move : function() {
|
755
821
|
var e = $.Event('moving');
|
756
822
|
var offset = this.$(".TS-item").offset();
|
757
|
-
var toffset =
|
823
|
+
var toffset = this.timeline.$(".timeline_setter").offset();
|
758
824
|
if (offset.left < toffset.left) {
|
759
825
|
e.deltaX = toffset.left - offset.left + cleanNumber(this.$(".TS-item").css("padding-left"));
|
760
|
-
this.
|
761
|
-
} else if (offset.left + this.$(".TS-item").outerWidth() > toffset.left +
|
762
|
-
e.deltaX = toffset.left +
|
763
|
-
this.
|
826
|
+
this.timeline.bar.moving(e);
|
827
|
+
} else if (offset.left + this.$(".TS-item").outerWidth() > toffset.left + this.timeline.$(".timeline_setter").width()) {
|
828
|
+
e.deltaX = toffset.left + this.timeline.$(".timeline_setter").width() - (offset.left + this.$(".TS-item").outerWidth());
|
829
|
+
this.timeline.bar.moving(e);
|
764
830
|
}
|
765
831
|
},
|
766
832
|
|
@@ -771,13 +837,13 @@
|
|
771
837
|
|
772
838
|
// Globally hide any cards with `TS-card_active`.
|
773
839
|
hideActiveCard : function() {
|
774
|
-
|
775
|
-
|
840
|
+
this.timeline.$(".TS-card_active").removeClass("TS-card_active").hide();
|
841
|
+
this.timeline.$(".TS-notch_active").removeClass("TS-notch_active");
|
776
842
|
},
|
777
843
|
|
778
|
-
// An event listener to toggle this
|
844
|
+
// An event listener to toggle this notch on and off via `Series`.
|
779
845
|
toggleNotch : function(e){
|
780
|
-
switch (e
|
846
|
+
switch (e) {
|
781
847
|
case "hideNotch":
|
782
848
|
this.notch.hide().removeClass("TS-notch_active").addClass("TS-series_inactive");
|
783
849
|
if (this.el) this.el.hide();
|
@@ -787,7 +853,7 @@
|
|
787
853
|
}
|
788
854
|
}
|
789
855
|
|
790
|
-
};
|
856
|
+
});
|
791
857
|
|
792
858
|
|
793
859
|
// Simple inheritance helper for `Controls`.
|
@@ -802,16 +868,17 @@
|
|
802
868
|
// --------
|
803
869
|
|
804
870
|
// Each control is basically a callback wrapper for a given DOM element.
|
805
|
-
var Control = function(direction){
|
871
|
+
var Control = function(direction, timeline){
|
872
|
+
this.timeline = timeline;
|
806
873
|
this.direction = direction;
|
807
|
-
this.el =
|
874
|
+
this.el = this.timeline.$(this.prefix + direction);
|
808
875
|
var that = this;
|
809
876
|
this.el.bind('click', function(e) { e.preventDefault(); that.click(e);});
|
810
877
|
};
|
811
878
|
|
812
879
|
// Each `Zoom` control adjusts the `curZoom` when clicked.
|
813
880
|
var curZoom = 100;
|
814
|
-
var Zoom = function(direction) {
|
881
|
+
var Zoom = function(direction, timeline) {
|
815
882
|
Control.apply(this, arguments);
|
816
883
|
};
|
817
884
|
inherits(Zoom, Control);
|
@@ -824,7 +891,7 @@
|
|
824
891
|
click : function() {
|
825
892
|
curZoom += (this.direction === "in" ? +100 : -100);
|
826
893
|
if (curZoom >= 100) {
|
827
|
-
|
894
|
+
this.timeline.$(".TS-notchbar").trigger('doZoom', [curZoom]);
|
828
895
|
} else {
|
829
896
|
curZoom = 100;
|
830
897
|
}
|
@@ -833,9 +900,9 @@
|
|
833
900
|
|
834
901
|
|
835
902
|
// Each `Chooser` activates the next or previous notch.
|
836
|
-
var Chooser = function(direction) {
|
903
|
+
var Chooser = function(direction, timeline) {
|
837
904
|
Control.apply(this, arguments);
|
838
|
-
this.notches =
|
905
|
+
this.notches = this.timeline.$(".TS-notch");
|
839
906
|
};
|
840
907
|
inherits(Chooser, Control);
|
841
908
|
|
@@ -847,7 +914,7 @@
|
|
847
914
|
click: function(e){
|
848
915
|
var el;
|
849
916
|
var notches = this.notches.not(".TS-series_inactive");
|
850
|
-
var curCardIdx = notches.index(
|
917
|
+
var curCardIdx = notches.index(this.timeline.$(".TS-notch_active"));
|
851
918
|
var numOfCards = notches.length;
|
852
919
|
if (this.direction === "next") {
|
853
920
|
el = (curCardIdx < numOfCards ? notches.eq(curCardIdx + 1) : false);
|
@@ -859,37 +926,106 @@
|
|
859
926
|
}
|
860
927
|
});
|
861
928
|
|
929
|
+
// JS API
|
930
|
+
// ------
|
931
|
+
|
932
|
+
// The TimelineSetter JS API allows you to listen to certain
|
933
|
+
// timeline events, and activate cards programmatically.
|
934
|
+
// To take advantage of it, assign the timeline boot function to a variable
|
935
|
+
// like so:
|
936
|
+
//
|
937
|
+
// var currentTimeline = TimelineSetter.Timeline.boot(
|
938
|
+
// [data], {config}
|
939
|
+
// );
|
940
|
+
//
|
941
|
+
// then call methods on the `currentTimeline.api` object
|
942
|
+
//
|
943
|
+
// currentTimeline.api.onLoad(function() {
|
944
|
+
// console.log("I'm ready");
|
945
|
+
// });
|
946
|
+
//
|
947
|
+
TimelineSetter.Api = function(timeline) {
|
948
|
+
this.timeline = timeline;
|
949
|
+
};
|
950
|
+
|
951
|
+
TimelineSetter.Api.prototype = _.extend(TimelineSetter.Api.prototype, {
|
952
|
+
// Register a callback for when the timeline is loaded
|
953
|
+
onLoad : function(cb) {
|
954
|
+
this.timeline.bind('load', cb);
|
955
|
+
},
|
956
|
+
|
957
|
+
// Register a callback for when a card is added to the timeline
|
958
|
+
// Callback has access to the event name and the card object
|
959
|
+
onCardAdd : function(cb) {
|
960
|
+
this.timeline.bind('cardAdd', cb);
|
961
|
+
},
|
962
|
+
|
963
|
+
// Register a callback for when a card is activated.
|
964
|
+
// Callback has access to the event name and the card object
|
965
|
+
onCardActivate : function(cb) {
|
966
|
+
this.timeline.bind('cardActivate', cb);
|
967
|
+
},
|
968
|
+
|
969
|
+
// Register a callback for when the bar is moved or zoomed.
|
970
|
+
// Be careful with this one: Bar move events can be fast
|
971
|
+
// and furious, especially with scroll wheels in Safari.
|
972
|
+
onBarMove : function(cb) {
|
973
|
+
// Bind a 'move' event to the timeline, because
|
974
|
+
// at this point, `timeline.bar` isn't available yet.
|
975
|
+
// To get around this, we'll trigger the bar's
|
976
|
+
// timeline's move event when the bar is moved.
|
977
|
+
this.timeline.bind('move', cb);
|
978
|
+
},
|
979
|
+
|
980
|
+
// Show the card matching a given timestamp
|
981
|
+
// Right now, timelines only support one card per timestamp
|
982
|
+
activateCard : function(timestamp) {
|
983
|
+
_(this.timeline.cards).detect(function(card) { return card.timestamp === timestamp; }).activate();
|
984
|
+
}
|
985
|
+
});
|
986
|
+
|
987
|
+
// Global TS keydown function to bind key events to the
|
988
|
+
// current global currentTimeline.
|
989
|
+
TimelineSetter.bindKeydowns = function() {
|
990
|
+
$(document).bind('keydown', function(e) {
|
991
|
+
if (e.keyCode === 39) {
|
992
|
+
TimelineSetter.currentTimeline.chooseNext.click();
|
993
|
+
} else if (e.keyCode === 37) {
|
994
|
+
TimelineSetter.currentTimeline.choosePrev.click();
|
995
|
+
} else {
|
996
|
+
return;
|
997
|
+
}
|
998
|
+
});
|
999
|
+
};
|
1000
|
+
|
862
1001
|
|
863
1002
|
// Finally, let's create the whole timeline. Boot is exposed globally via
|
864
1003
|
// `TimelineSetter.Timeline.boot()` which takes the JSON generated by
|
865
|
-
// the timeline-setter binary as
|
866
|
-
//
|
867
|
-
//
|
1004
|
+
// the timeline-setter binary as its first argument, and a config hash as its second.
|
1005
|
+
// The config hash looks for a container element, an interval for interval notches
|
1006
|
+
// and a formatter function for dates. All of these are optional.
|
868
1007
|
//
|
869
|
-
//
|
870
|
-
//
|
871
|
-
//
|
1008
|
+
// We also initialize a new API object for each timeline, accessible via the
|
1009
|
+
// timeline variable's `api` method (e.g. `currentTimeline.api`) and look for
|
1010
|
+
// how many timelines are globally on the page for keydown purposes. We'll only
|
1011
|
+
// bind keydowns globally if there's only one timeline on the page.
|
872
1012
|
Timeline.boot = function(data, config) {
|
873
|
-
|
874
|
-
|
875
|
-
TimelineSetter.timeline = new Timeline(data, config || {});
|
876
|
-
new Zoom("in");
|
877
|
-
new Zoom("out");
|
878
|
-
var chooseNext = new Chooser("next");
|
879
|
-
var choosePrev = new Chooser("prev");
|
880
|
-
if (!$(".TS-card_active").is("*")) chooseNext.click();
|
881
|
-
|
882
|
-
$(document).bind('keydown', function(e) {
|
883
|
-
if (e.keyCode === 39) {
|
884
|
-
chooseNext.click();
|
885
|
-
} else if (e.keyCode === 37) {
|
886
|
-
choosePrev.click();
|
887
|
-
} else {
|
888
|
-
return;
|
889
|
-
}
|
890
|
-
});
|
891
|
-
});
|
1013
|
+
var timeline = TimelineSetter.timeline = new Timeline(data, config || {});
|
1014
|
+
var api = new TimelineSetter.Api(timeline);
|
892
1015
|
|
1016
|
+
if (!TimelineSetter.pageTimelines) {
|
1017
|
+
TimelineSetter.currentTimeline = timeline;
|
1018
|
+
TimelineSetter.bindKeydowns();
|
1019
|
+
}
|
1020
|
+
|
1021
|
+
TimelineSetter.pageTimelines = TimelineSetter.pageTimelines ? TimelineSetter.pageTimelines += 1 : 1;
|
1022
|
+
|
1023
|
+
$(timeline.render);
|
1024
|
+
|
1025
|
+
return {
|
1026
|
+
timeline : timeline,
|
1027
|
+
api : api
|
1028
|
+
};
|
893
1029
|
};
|
894
1030
|
|
895
|
-
})();
|
1031
|
+
})(jQuery);
|