661 lines
17 KiB
JavaScript
661 lines
17 KiB
JavaScript
|
/*
|
||
|
* Timeglider for Javascript / jQuery
|
||
|
* http://timeglider.com/jquery
|
||
|
*
|
||
|
* Copyright 2011, Mnemograph LLC
|
||
|
* Licensed under Timeglider Dual License
|
||
|
* http://timeglider.com/jquery/?p=license
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
/*
|
||
|
* Version 2 of TG_Org has a "global" check of
|
||
|
* event-block position, rather than checking
|
||
|
* against a tree of levels...
|
||
|
|
||
|
THIS IS THE LAST VERSION OF BOTTOM-UP LAYOUTS
|
||
|
BY DEFAULT
|
||
|
|
||
|
*/
|
||
|
|
||
|
(function(tg){
|
||
|
|
||
|
// standard "brick" height for placement grid
|
||
|
var lev_ht = tg.levelHeight = 12,
|
||
|
// number of available levels for events
|
||
|
$ = jQuery,
|
||
|
ceiling_padding = 30,
|
||
|
topdown_pad = 30,
|
||
|
bottomup_pad = -8;
|
||
|
|
||
|
|
||
|
/*
|
||
|
* @constructor
|
||
|
*/
|
||
|
tg.TG_Org = function() {
|
||
|
|
||
|
var me = this;
|
||
|
var icon_f = tg.icon_folder;
|
||
|
|
||
|
this.blocks = [];
|
||
|
this.ids = [];
|
||
|
this.vis = [];
|
||
|
this.pol = -1;
|
||
|
this.placedBlocks = [];
|
||
|
this.freshBlocks = [];
|
||
|
|
||
|
|
||
|
/*
|
||
|
* ******** PUBLIC METHODS **********
|
||
|
*/
|
||
|
|
||
|
|
||
|
/*
|
||
|
* TG_Org.addBlock
|
||
|
* Adds a 2D geometric block object, corresponding to an event
|
||
|
* into the "borg" layout.
|
||
|
*
|
||
|
* @param {object} evob Event object including position values: left, width, top, height
|
||
|
-- no need for right and bottom
|
||
|
* @param {string/number} tickScope This either "sweep" or the serial of a single tick (Number)
|
||
|
*
|
||
|
*/
|
||
|
this.addBlock = function (evob, tickScope) {
|
||
|
evob.right = evob.left + evob.width;
|
||
|
evob.bottom = evob.top + evob.height;
|
||
|
evob.tickScope = tickScope;
|
||
|
me.freshBlocks.push(evob);
|
||
|
me.blocks.push(evob);
|
||
|
};
|
||
|
|
||
|
|
||
|
/*
|
||
|
*
|
||
|
*/
|
||
|
this.clearFresh = function () {
|
||
|
me.freshBlocks = [];
|
||
|
}
|
||
|
|
||
|
|
||
|
/*
|
||
|
* TG_Org.getBorg
|
||
|
*
|
||
|
* @return {object} This particular "borg" object with its blocks, etc
|
||
|
*
|
||
|
*/
|
||
|
this.getBorg = function () {
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
|
||
|
/*
|
||
|
* TG_Org.getBlocks
|
||
|
*
|
||
|
* @return {array} An array of placement blocks (objects), each corresponding
|
||
|
* to an event on the timeline.
|
||
|
*
|
||
|
*/
|
||
|
this.getBlocks = function () {
|
||
|
return this.blocks;
|
||
|
};
|
||
|
|
||
|
|
||
|
/*
|
||
|
* TG_Org.getHTML
|
||
|
* inside of args (args.tickScope, etc)
|
||
|
* @param tickScope {string|number} This either "sweep" or the serial of a single tick (Number)
|
||
|
* @param ceiling {number} The max height of the timeline display, after which a "+" appears
|
||
|
* @param onZoom {boolean} is the timeline at its preferred/initial zoom?
|
||
|
*
|
||
|
* @return {string} HTML with events passed back to view for actual layout of timeline
|
||
|
*/
|
||
|
this.getHTML = function (args) {
|
||
|
|
||
|
var tickScope = args.tickScope;
|
||
|
var ceiling = args.ceiling;
|
||
|
var laneTops = args.laneTops;
|
||
|
this.onIZoom = args.onIZoom;
|
||
|
|
||
|
if (tickScope == "sweep") {
|
||
|
this.vis = [];
|
||
|
}
|
||
|
|
||
|
if (args.inverted) {
|
||
|
// top down
|
||
|
this.pol = 1;
|
||
|
} else {
|
||
|
// bottom up
|
||
|
this.pol = -1;
|
||
|
}
|
||
|
|
||
|
this.freshBlocks.sort(sortBlocksByImportance);
|
||
|
// cycle through events and move overlapping event up
|
||
|
|
||
|
var positioned = [],
|
||
|
blHeight,
|
||
|
lastPos,
|
||
|
span_selector_class,
|
||
|
span_div,
|
||
|
img = '',
|
||
|
icon = '',
|
||
|
html = '',
|
||
|
top_or_bottom_padding_from_title = 0,
|
||
|
b = {},
|
||
|
blength = this.freshBlocks.length,
|
||
|
b_span_color = "",
|
||
|
title_adj = 0,
|
||
|
highest = 0,
|
||
|
img_scale = 100,
|
||
|
img_style = "",
|
||
|
css_class = "",
|
||
|
p_icon = "",
|
||
|
p_overf = "",
|
||
|
image_class = "lane",
|
||
|
selected_class = "",
|
||
|
lane_class = "",
|
||
|
polarity_cond = "",
|
||
|
in_lane = false,
|
||
|
lane_sp_title = "";
|
||
|
|
||
|
|
||
|
for (var i=0; i<blength; i++) {
|
||
|
|
||
|
b = this.freshBlocks[i];
|
||
|
title_adj = 0;
|
||
|
img_scale = 100;
|
||
|
img_style = "";
|
||
|
selected_class = "";
|
||
|
lane_class = "";
|
||
|
in_lane = false;
|
||
|
|
||
|
|
||
|
// full sweep or just a tick added left or right
|
||
|
if (b.tickScope == tickScope) {
|
||
|
|
||
|
// is it not yet visible?
|
||
|
if (_.indexOf(this.vis, b.id) == -1) {
|
||
|
|
||
|
// it's not in the "visible" array, so add it
|
||
|
this.vis.push(b.id);
|
||
|
|
||
|
// if it's got static HTML in it
|
||
|
if (b.html && b.html.substr(0,4) == "<div") {
|
||
|
// only positions interior html at proper left position!
|
||
|
html += ("<div style='position:relative; left:" + b.left + "px' " +
|
||
|
"id='" + b.id + "'>" + b.html + "</div>");
|
||
|
|
||
|
} else {
|
||
|
|
||
|
// if it has an image, it's either in "layout" mode (out on timeline full size)
|
||
|
// or it's going to be thumbnailed into the "bar"
|
||
|
if (b.image) {
|
||
|
|
||
|
|
||
|
image_class = b.image.display_class;
|
||
|
|
||
|
|
||
|
if (b.shape && image_class == "inline") {
|
||
|
img_style = " style='width:" + b.shape.img_wi + "px;height:auto;top:-" + b.shape.img_ht + "px'";
|
||
|
} else {
|
||
|
img_style = "";
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
title_adj = 0; // b.shape.img_ht + 4;
|
||
|
|
||
|
|
||
|
// different image classes ("bar", "above") are positioned
|
||
|
// using a separate $.each routine in TimelineView rather than
|
||
|
// being given absolute positioning here.
|
||
|
img = "<div data-max_height='" + b.image.max_height + "' class='timeglider-event-image-" + image_class + "'><img src='" + b.image.src + "' " + img_style + "></div>";
|
||
|
|
||
|
|
||
|
} else {
|
||
|
// no image
|
||
|
img = "";
|
||
|
}
|
||
|
|
||
|
highest = ceiling - ceiling_padding;
|
||
|
|
||
|
// debug.log("laneTops:", laneTops);
|
||
|
|
||
|
|
||
|
if (this.onIZoom && b.y_position > 0) {
|
||
|
// absolute positioning
|
||
|
b.top = me.pol * b.y_position;
|
||
|
|
||
|
} else if ( typeof laneTops[b.lane] == "number" && typeof b.lane == "string") {
|
||
|
try {
|
||
|
// debug.log("b.lane in org:", b.lane, laneTops[b.lane]);
|
||
|
b.top = me.pol * laneTops[b.lane];
|
||
|
// debug.log("btop:", b.top);
|
||
|
in_lane = true;
|
||
|
lane_class = " lane-event";
|
||
|
} catch (err) {
|
||
|
debug.log("err:", err);
|
||
|
}
|
||
|
|
||
|
} else {
|
||
|
// starts out checking block against the bottom layer
|
||
|
//!RECURSIVE
|
||
|
// *** alters the `b` block object
|
||
|
b.attempts = 0;
|
||
|
checkAgainstPlaced(b, highest);
|
||
|
|
||
|
}
|
||
|
|
||
|
|
||
|
// note: divs that are higher have lower "top" values
|
||
|
// `ceiling` being set at 0 (event_overflow set to "scroll")
|
||
|
// may require/allow for event scrolling possibilities...
|
||
|
|
||
|
if (me.pol == -1) {
|
||
|
polarity_cond = (ceiling && (Math.abs(b.top) > highest));
|
||
|
} else {
|
||
|
polarity_cond = (ceiling && (Math.abs(b.top + 30) > highest));
|
||
|
}
|
||
|
|
||
|
//konvay
|
||
|
polarity_cond=false;
|
||
|
if (polarity_cond){
|
||
|
|
||
|
p_overf = (me.pol == -1) ? "top:-" + (ceiling-10) + "px": "top:" + ceiling + "px"
|
||
|
|
||
|
var white_cir = "<img src='" + icon_f + "shapes/circle_white.png'>";
|
||
|
p_icon = (b.icon) ? "<img src='" + icon_f + b.icon + "'>": white_cir;
|
||
|
|
||
|
// + + + symbols in place of events just under ceiling
|
||
|
// if things are higher than the ceiling, show plus signs instead,
|
||
|
// and we'll zoom in with these.
|
||
|
html += "<div id='" + b.id + "' class='timeglider-timeline-event tg-event-overflow' style='left:" + b.left + "px;" + p_overf + "'>" + p_icon + "</div>";
|
||
|
|
||
|
} else {
|
||
|
|
||
|
|
||
|
|
||
|
if (b.fontsize > 2) {
|
||
|
|
||
|
b_span_color = (b.span_color) ? ";background-color:" + b.span_color: "";
|
||
|
|
||
|
b.fontsize < 10 ? b.opacity = b.fontsize / 10 : b.opacity=1;
|
||
|
|
||
|
if (b.selected) {
|
||
|
selected_class = "selected";
|
||
|
}
|
||
|
|
||
|
if (b.span == true) {
|
||
|
span_selector_class = "timeglider-event-spanning";
|
||
|
// add seconds into span data in case calculations
|
||
|
// are in demand in DOM
|
||
|
// if it's a lane span...
|
||
|
lane_sp_title = in_lane ? "<span>" + b.title + "</span>": "";
|
||
|
|
||
|
span_div = "<div data-starts='" + b.startdateObj.sec + "' data-ends='" + b.enddateObj.sec + "' class='timeglider-event-spanner' style='top:" + "px;height:" + b.fontsize + "px;width:" + b.spanwidth + "px" + b_span_color + "'>" + lane_sp_title + "</div>";
|
||
|
|
||
|
} else {
|
||
|
span_selector_class = "";
|
||
|
span_div = "";
|
||
|
}
|
||
|
|
||
|
if (b.icon) {
|
||
|
icon = "<img class='timeglider-event-icon' src='" + icon_f + b.icon + "' style='height:" + b.fontsize + "px;left:-" + (b.fontsize + 2) + "px; top:" + title_adj + "px'>";
|
||
|
} else {
|
||
|
icon = '';
|
||
|
}
|
||
|
|
||
|
// pad inverted (polarity 1) events to exceed the height
|
||
|
// of the timeline title bar; pad "normal" top-up events
|
||
|
// to have some space between them and the title bar
|
||
|
if(!in_lane) {
|
||
|
top_or_bottom_padding_from_title = (me.pol === 1) ?
|
||
|
topdown_pad : bottomup_pad;
|
||
|
}
|
||
|
|
||
|
// possible customized class
|
||
|
css_class = b.css_class || '';
|
||
|
|
||
|
// TODO: function for getting "standard" event shit
|
||
|
html += "<div class='timeglider-timeline-event "
|
||
|
+ css_class + " " + span_selector_class + lane_class
|
||
|
+ " " + selected_class
|
||
|
|
||
|
+ "' starttime='" + b.startdate + "' "
|
||
|
+ "' endtime='" + b.enddate + "' "
|
||
|
+ "style='width:" + b.width + "px;"
|
||
|
+ "height:" + b.height + "px;"
|
||
|
+ "left:" + b.left + "px;"
|
||
|
+ "opacity:" + b.opacity + ";"
|
||
|
+ "top:24px;"
|
||
|
+ "font-size:" + b.fontsize + "px;'>"
|
||
|
+ icon + img + span_div;
|
||
|
|
||
|
if (!lane_sp_title) {
|
||
|
html += "<div class='timeglider-event-title' style='top:" + title_adj + "px'>" + b.title + "</div>";
|
||
|
}
|
||
|
html += "</div>";
|
||
|
|
||
|
} // endif fontsize is > 2
|
||
|
|
||
|
} // end if/else :: height > ceiling
|
||
|
|
||
|
} // end if it's got valid HTML
|
||
|
|
||
|
} // end check for visible... EXPENSIVE!!!!
|
||
|
|
||
|
} // end tickScope check
|
||
|
|
||
|
} // end for()
|
||
|
|
||
|
|
||
|
return {"html":html};
|
||
|
|
||
|
|
||
|
}; /// end getHTML
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
/// PRIVATE STUFF ///
|
||
|
|
||
|
/**
|
||
|
* sortBlocksByImportance
|
||
|
* Sorter helper for sorting events by importance
|
||
|
* @param a {Number} 1st sort number
|
||
|
* @param b {Number} 2nd sort number
|
||
|
*/
|
||
|
var sortBlocksByImportance = function (a, b) {
|
||
|
var x = b.importance,
|
||
|
y = a.importance;
|
||
|
|
||
|
if (a.image && b.image){
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
return ((x < y) ? -1 : ((x > y) ? 1 : 0));
|
||
|
};
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
/**
|
||
|
* isOverlapping
|
||
|
* Takes two objects and sees if the prospect overlaps with
|
||
|
* an existing object [part of loop in checkAgainstPlaced()]
|
||
|
*
|
||
|
* @param {object} b1 Timeline-event object IN PLACE
|
||
|
* @param {object} b2 Timeline-event object BEING ADDED
|
||
|
*/
|
||
|
var isOverlapping = function (b1, b2) {
|
||
|
|
||
|
//!TODO ******* POLARITY IS NOT WORKED INTO THIS YET
|
||
|
|
||
|
if (b2.shape) {
|
||
|
var io = isOverlapping(b1, b2.shape);
|
||
|
if (io == true) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var vPadding = -6,
|
||
|
lPadding = -16;
|
||
|
|
||
|
if ((b2.left + lPadding > b1.right) || (b2.right < b1.left + lPadding) || (b2.bottom < b1.top + vPadding)) {
|
||
|
// clear to left or right.
|
||
|
return false;
|
||
|
|
||
|
} else {
|
||
|
|
||
|
if (
|
||
|
((b2.left >= b1.left) && (b2.left <= b1.right)) ||
|
||
|
((b2.right >= b1.left) && (b2.right <= b1.right)) ||
|
||
|
((b2.right >= b1.right) && (b2.left <= b1.left)) ||
|
||
|
((b2.right <= b1.right) && (b2.left >= b1.left))
|
||
|
) {
|
||
|
|
||
|
// OK, some kind of left-right overlap is happening, but
|
||
|
// there also has to be top-bottom overlap for collision
|
||
|
if (
|
||
|
//
|
||
|
((b2.bottom <= b1.bottom) && (b2.bottom >= b1.top)) ||
|
||
|
((b2.top <= b1.bottom) && (b2.top >= b1.top)) ||
|
||
|
((b2.bottom == b1.bottom) && (b2.top == b1.top))
|
||
|
) {
|
||
|
// passes 2nd test -- it's overlapping!
|
||
|
return true;
|
||
|
|
||
|
} else {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// end first big if: fails initial test
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// return false;
|
||
|
|
||
|
};
|
||
|
|
||
|
|
||
|
// private function
|
||
|
var checkAgainstPlaced = function (block, ceil) {
|
||
|
|
||
|
var ol = false,
|
||
|
|
||
|
placed = me.placedBlocks,
|
||
|
placed_len = me.placedBlocks.length,
|
||
|
|
||
|
collision = false;
|
||
|
|
||
|
if ((placed_len == 0) || (Math.abs(block.top) > ceil)) {
|
||
|
// just place it!
|
||
|
collision = false;
|
||
|
|
||
|
} else {
|
||
|
|
||
|
// Go through all the placed blocks
|
||
|
for (var e=0; e < placed_len; e++) {
|
||
|
|
||
|
sh = false;
|
||
|
ol = isOverlapping(placed[e],block);
|
||
|
|
||
|
if (block.shape) {
|
||
|
sh = isOverlapping(placed[e],block.shape);
|
||
|
}
|
||
|
|
||
|
if (ol == true || sh == true) {
|
||
|
// BUMP UP
|
||
|
if (me.pol === -1) {
|
||
|
// DEFAULT, bottom up
|
||
|
block.top -= lev_ht;
|
||
|
block.bottom -= lev_ht;
|
||
|
if (block.shape) {
|
||
|
block.shape.top -= lev_ht;
|
||
|
block.shape.bottom -= lev_ht;
|
||
|
}
|
||
|
} else {
|
||
|
// "SOUTH" side, top town
|
||
|
block.top += lev_ht;
|
||
|
block.bottom += lev_ht;
|
||
|
if (block.shape) {
|
||
|
block.shape.top += lev_ht;
|
||
|
block.shape.bottom += lev_ht;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
// *** RECURSIVE ***
|
||
|
block.attempts++;
|
||
|
|
||
|
|
||
|
// ......aaaaaand then check again
|
||
|
checkAgainstPlaced(block, ceil);
|
||
|
|
||
|
collision = true;
|
||
|
|
||
|
// STOP LOOP -- there's a collision
|
||
|
break;
|
||
|
} // end if overlap is true
|
||
|
|
||
|
} // end for
|
||
|
}
|
||
|
|
||
|
if (collision == false) {
|
||
|
|
||
|
me.placedBlocks.push(block);
|
||
|
|
||
|
if (block.shape) {
|
||
|
me.placedBlocks.push(block.shape);
|
||
|
}
|
||
|
} // end if collision is false
|
||
|
|
||
|
}; // end checkAgainstPlaced()
|
||
|
|
||
|
|
||
|
}; ///// END TG_Org
|
||
|
|
||
|
|
||
|
|
||
|
/* TG_OrgList is a mobile-friendly list-style layout
|
||
|
to run in lieu of the typical timeline layout;
|
||
|
still in development!
|
||
|
*/
|
||
|
tg.TG_OrgList = function(events, med) {
|
||
|
|
||
|
var me = this;
|
||
|
var icon_f = tg.icon_folder;
|
||
|
|
||
|
this.blocks = events;
|
||
|
this.ids = [];
|
||
|
|
||
|
this.mediator = med;
|
||
|
|
||
|
|
||
|
/*
|
||
|
* ******** PUBLIC METHODS **********
|
||
|
*/
|
||
|
|
||
|
/*
|
||
|
* TG_OrgList.getHTML
|
||
|
* no args: just straight list of all events in a timeline...
|
||
|
*
|
||
|
* @return {string} HTML with events passed back to view for actual layout of timeline
|
||
|
*/
|
||
|
this.getHTML = function () {
|
||
|
|
||
|
var positioned = [],
|
||
|
span_selector_class,
|
||
|
span_div,
|
||
|
img = '',
|
||
|
icon = '',
|
||
|
html = '',
|
||
|
more = '',
|
||
|
desc = '',
|
||
|
datef = '',
|
||
|
elip = '',
|
||
|
b = {},
|
||
|
stripdesc = '',
|
||
|
blength = this.blocks.length,
|
||
|
|
||
|
img_scale = 100,
|
||
|
css_class = "",
|
||
|
p_icon = "";
|
||
|
|
||
|
|
||
|
for (var i=0; i<blength; i++) {
|
||
|
|
||
|
b = this.blocks[i];
|
||
|
|
||
|
img_scale = 100;
|
||
|
|
||
|
if (b.image) {
|
||
|
|
||
|
// different image classes ("bar", "above") are positioned
|
||
|
// using a separate $.each routine in TimelineView rather than
|
||
|
// being given absolute positioning here.
|
||
|
img = "<div class='tg-event-list-img'><img src='" + b.image.src + "'></div>";
|
||
|
|
||
|
|
||
|
} else {
|
||
|
// no image
|
||
|
img = "";
|
||
|
}
|
||
|
|
||
|
b_span_color = (b.span_color) ? ";background-color:" + b.span_color: "";
|
||
|
|
||
|
b.fontsize < 10 ? b.opacity = b.fontsize / 10 : b.opacity=1;
|
||
|
|
||
|
if (b.span == true) {
|
||
|
span_selector_class = "timeglider-event-spanning";
|
||
|
// add seconds into span data in case calculations
|
||
|
// are in demand in DOM
|
||
|
|
||
|
} else {
|
||
|
span_selector_class = "";
|
||
|
|
||
|
}
|
||
|
|
||
|
if (b.icon) {
|
||
|
icon_img = "<img src='" + icon_f + b.icon + "'>";
|
||
|
} else {
|
||
|
icon_img = '';
|
||
|
}
|
||
|
|
||
|
icon = "<div class='tg-event-list-icon'>" + icon_img + " </div>"
|
||
|
|
||
|
// possible customized class
|
||
|
css_class = b.css_class || '';
|
||
|
|
||
|
if (b.description || img) {
|
||
|
more = "<img src='timeglider/img/mobile_more.png'>";
|
||
|
} else {
|
||
|
more = "";
|
||
|
}
|
||
|
|
||
|
if (b.description) {
|
||
|
// strip tags to we have no half baked html in blurb
|
||
|
stripdesc = b.description.replace(/(<([^>]+)>)/ig,"");
|
||
|
elip = (stripdesc.length>110) ? "...":"";
|
||
|
desc = "<div class='tg-list-blurb'>" + stripdesc.substr(0,110) + elip + "</div>";
|
||
|
} else {
|
||
|
desc = "";
|
||
|
}
|
||
|
|
||
|
|
||
|
datef = b.startdateObj.format('', true, this.mediator.timeOffset);
|
||
|
|
||
|
// TODO: function for getting "standard" event shit
|
||
|
html += "<li class='tg-list-li timeglider-timeline-event clearfix"
|
||
|
+ css_class + " " + span_selector_class
|
||
|
+ "' id='" + b.id + "'>"
|
||
|
+ "<div class='tg-list-dateline timeglider-dateline-startdate'>" + datef + "</div>"
|
||
|
+ img + icon
|
||
|
+ "<div class='timeglider-event-title'>"
|
||
|
+ b.title
|
||
|
|
||
|
+ "</div>"
|
||
|
+ desc + "</li>";
|
||
|
|
||
|
|
||
|
|
||
|
} // end for()
|
||
|
|
||
|
|
||
|
return {"html":html};
|
||
|
|
||
|
|
||
|
}; /// end getHTML
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
}; ///// END TG_OrgList
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
})(timeglider);
|