timeline_setter 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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);