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.
Files changed (40) hide show
  1. data/Rakefile +11 -2
  2. data/config/assets.yml +9 -0
  3. data/doc/doc.markdown +65 -17
  4. data/doc/templates.html +3 -0
  5. data/doc/timeline-setter.html +327 -238
  6. data/doc/timeline-setter.min.html +3 -0
  7. data/documentation/TimelineSetter/CLI.html +77 -52
  8. data/documentation/TimelineSetter/Parser.html +40 -39
  9. data/documentation/TimelineSetter/Timeline.html +132 -83
  10. data/documentation/TimelineSetter.html +27 -12
  11. data/documentation/_index.html +23 -12
  12. data/documentation/class_list.html +20 -9
  13. data/documentation/css/style.css +7 -5
  14. data/documentation/file.README.html +33 -23
  15. data/documentation/file_list.html +20 -9
  16. data/documentation/frames.html +1 -1
  17. data/documentation/index.html +33 -23
  18. data/documentation/js/app.js +16 -14
  19. data/documentation/js/full_list.js +7 -6
  20. data/documentation/js/jquery.js +3 -3
  21. data/documentation/method_list.html +42 -23
  22. data/documentation/top-level-namespace.html +26 -11
  23. data/index.html +100 -19
  24. data/lib/timeline_setter/cli.rb +2 -0
  25. data/lib/timeline_setter/timeline.rb +6 -3
  26. data/lib/timeline_setter/version.rb +1 -1
  27. data/lib/timeline_setter.rb +0 -7
  28. data/public/javascripts/templates/card.jst +21 -0
  29. data/public/javascripts/templates/notch.jst +1 -0
  30. data/public/javascripts/templates/series_legend.jst +3 -0
  31. data/public/javascripts/templates/timeline.jst +20 -0
  32. data/public/javascripts/templates/year_notch.jst +3 -0
  33. data/public/javascripts/templates.js +1 -0
  34. data/public/javascripts/timeline-setter.js +303 -167
  35. data/public/javascripts/timeline-setter.min.js +1 -0
  36. data/public/stylesheets/timeline-setter.css +5 -5
  37. data/spec/timeline_setter_spec.rb +2 -2
  38. data/templates/timeline-markup.erb +5 -59
  39. data/timeline_setter.gemspec +15 -5
  40. 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
- this._callbacks.push(cb);
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 (!this._callbacks) return;
25
- for(var i = 0; callback = this._callbacks[i]; i++)
26
- callback.apply(this, arguments);
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
- // Move the associated element a specified delta. Of note: because events
36
- // aren't scoped by key, TimelineSetter uses plain old jQuery events for
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.type === "zoom") return;
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) jQuery.event.props.push("touches");
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
- // An object containing human translations for date indexes.
186
- Intervals.HUMAN_DATES = {
187
- months : ['Jan.', 'Feb.', 'March', 'April', 'May', 'June', 'July', 'Aug.', 'Sept.', 'Oct.', 'Nov.', 'Dec.']
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 = new Date(timestamp);
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 dYear;
225
+ return d.year;
204
226
  case "Lustrum":
205
- return dYear;
227
+ return d.year;
206
228
  case "FullYear":
207
- return dYear;
229
+ return d.year;
208
230
  case "Month":
209
- return dMonth + ', ' + dYear;
231
+ return d.month + ', ' + d.year;
210
232
  case "Week":
211
- return dDate;
233
+ return d.date;
212
234
  case "Date":
213
- return dDate;
235
+ return d.date;
214
236
  case "Hours":
215
- return dHourWithMinutes;
237
+ return d.hourWithMinutes;
216
238
  case "Minutes":
217
- return dHourWithMinutes;
239
+ return d.hourWithMinutes;
218
240
  case "Seconds":
219
- return dHourMinuteSecond;
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
- var intvl = this.INTERVAL_ORDER[idx];
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(e){
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
- // The main kickoff point for rendering the timeline. The `Timeline` constructor
440
- // takes a json array of card representations and then builds series, calculates
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
- data = data.sort(function(a, b){ return a.timestamp - b.timestamp; });
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 = (config || {});
448
- this.bounds = new Bounds();
449
- this.bar = new Bar(this);
450
- this.cardCont = new CardScroller(this);
451
- this.createSeries(data);
452
- var range = new Intervals(this.bounds, config.interval);
453
- this.intervals = range.getRanges();
454
- this.bounds.extend(this.bounds.min - range.getMaxInterval() / 2);
455
- this.bounds.extend(this.bounds.max + range.getMaxInterval() / 2);
456
- this.bar.render();
457
-
458
- sync(this.bar, this.cardCont, "move", "zoom");
459
- var e = $.Event("render");
460
- this.trigger(e);
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
- this.el = $(".TS-notchbar");
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
- $(".TS-zoom_in").click();
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
- e.type = "move";
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 = $(".TS-notch_active");
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 = this.template({'timestamp' : intervals[i].timestamp, 'human' : intervals[i].human });
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 = $("#TS-card_scroller_inner");
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.template = template("#TS-series_legend_tmpl");
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($.Event("hideNotch"));
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($.Event("showNotch"));
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 = $(this.template(this));
628
- $(".TS-series_nav_container").append(this.el);
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
- var card = _.clone(card);
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.timeline.bind(this.render);
655
- this.series.timeline.bar.bind(this.flip);
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(e){
677
- if (!e.type === "render") return;
678
- this.offset = this.series.timeline.bounds.project(this.timestamp, 100);
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
- $(".TS-notchbar").append(this.notch);
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(e) {
690
- if (e.type !== "move" || !this.el || !this.el.is(":visible")) return;
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 = $("#timeline_setter").offset().left + $("#timeline_setter").width();
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() < $("#timeline_setter").width() / 2;
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 - $("#timeline_setter").offset().left < 0 && !margin && flippable) {
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 = $(this.template({card: this}));
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
- $("#TS-card_scroller_inner").append(this.el);
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($.Event("move"));
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 max = _.max(_.toArray(this.$(".TS-item_user_html").children()), function(el){ return $(el).width(); });
746
- if ($(max).width() > this.$(".TS-item_year").width()) {
747
- this.$(".TS-item_label").css("width", $(max).width());
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 = $("#timeline_setter").offset();
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.series.timeline.bar.moving(e);
761
- } else if (offset.left + this.$(".TS-item").outerWidth() > toffset.left + $("#timeline_setter").width()) {
762
- e.deltaX = toffset.left + $("#timeline_setter").width() - (offset.left + this.$(".TS-item").outerWidth());
763
- this.series.timeline.bar.moving(e);
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
- $(".TS-card_active").removeClass("TS-card_active").hide();
775
- $(".TS-notch_active").removeClass("TS-notch_active");
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 notche on and off via `Series`.
844
+ // An event listener to toggle this notch on and off via `Series`.
779
845
  toggleNotch : function(e){
780
- switch (e.type) {
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 = $(this.prefix + direction);
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
- $(".TS-notchbar").trigger('doZoom', [curZoom]);
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 = $(".TS-notch");
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($(".TS-notch_active"));
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 an argument. This is handy if you want
866
- // to be able to generate timelines at arbitrary times (say, for example,
867
- // in an ajax callback).
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
- // In the default install of TimelineSetter, Boot is called in the generated
870
- // HTML. We'll kick everything off by creating a `Timeline`, some `Controls`
871
- // and binding to `"keydown"`.
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
- $(function(){
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);