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.
- 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);
|