CHAP timeline

1,579 views
Skip to first unread message

ant...@bellingen.nsw.gov.au

unread,
Apr 18, 2012, 9:10:27 PM4/18/12
to TiddlyWiki
Hi All,

I'm new to the group, but I've been fiddling with TW for a little
while (a few months).

I tried the Simile Timeline but found it to be too unreliable and not
sufficiently robust, so I tried to port the CHAP timeline from
http://chap.almende.com/timeline to TW. I chse the JSON data version
as I wanted my wiki to work in stand-alone mode.

I've had a little bit of success, but my javascript and TW backend
skills aren't quite up to the job of getting it polished.

So far I've only managed to get the timeline to show if I put the
following in the PageTemplate tiddler, not by referencing the macro
<<drawVisualization>> in a tiddler.

I also need to work out how to store the data in a different tiddler,
presumably using the DataTiddlerPlugin.

Any help or suggestions greatfully appreciated.

Hopefully I'm not breaking any of the groups rules of etiquette by
posting the full plugin & CSS here. Apologies if so.

**************************Start CHAPTimelineCSS
Tiddler*****************************
div.timeline-frame {
border: 1px solid #BEBEBE;
overflow: hidden;
}

div.timeline-axis {
border-color: #BEBEBE;
border-width: 1px;
border-top-style: solid;
}
div.timeline-axis-grid {
border-left-style: solid;
border-width: 1px;
}
div.timeline-axis-grid-minor {
border-color: #e5e5e5;
}
div.timeline-axis-grid-major {
border-color: #bfbfbf;
}
div.timeline-axis-text {
color: #4D4D4D;
padding: 3px;
white-space: nowrap;
}

div.timeline-axis-text-minor {
}

div.timeline-axis-text-major {
}

div.timeline-event {
color: #1A1A1A;
border-color: #97B0F8;
background-color: #D5DDF6;


display: inline-block;

}

div.timeline-event-selected {
border-color: #FFC200;
background-color: #FFF785;
}


div.timeline-event-box {
text-align: center;
border-style: solid;
border-width: 1px;
border-radius: 5px;
-moz-border-radius: 5px; /* For Firefox 3.6 and older */
}

div.timeline-event-dot {
border-style: solid;
border-width: 5px;
border-radius: 5px;
-moz-border-radius: 5px; /* For Firefox 3.6 and older */
}

div.timeline-event-range {
border-style: solid;
border-width: 1px;
border-radius: 2px;
-moz-border-radius: 2px; /* For Firefox 3.6 and older */
}

div.timeline-event-line {
border-left-width: 1px;
border-left-style: solid;
}

div.timeline-event-content {
margin: 5px;
white-space: nowrap;
overflow: hidden;
}

div.timeline-groups-axis {
border-color: #BEBEBE;
border-width: 1px;
}
div.timeline-groups-text {
color: #4D4D4D;
padding-left: 10px;
padding-right: 10px;
}

div.timeline-currenttime {
background-color: #FF7F6E;
width: 2px;
}

div.timeline-customtime {
background-color: #6E94FF;
width: 2px;
cursor: move;
}

div.timeline-navigation {
font-family: arial;
font-size: 20px;
font-weight: bold;
color: gray;

border: 1px solid #BEBEBE;
background-color: #F5F5F5;
border-radius: 2px;
-moz-border-radius: 2px; /* For Firefox 3.6 and older */
}

div.timeline-navigation-new, div.timeline-navigation-delete,
div.timeline-navigation-zoom-in, div.timeline-navigation-zoom-
out,
div.timeline-navigation-move-left, div.timeline-navigation-move-
right {
cursor: pointer;
padding: 10px 10px;
float: left;
text-decoration: none;
border-color: #BEBEBE; /* border is used for the separator between
new and navigation buttons */

width: 16px;
height: 16px;
}

div.timeline-navigation-new {
background: url('img/16/new.png') no-repeat center;
}

div.timeline-navigation-delete {
padding: 0px;
padding-left: 5px;
background: url('img/16/delete.png') no-repeat center;
}

div.timeline-navigation-zoom-in {
background: url('img/16/zoomin.png') no-repeat center;
}

div.timeline-navigation-zoom-out {
background: url('img/16/zoomout.png') no-repeat center;
}

div.timeline-navigation-move-left {
background: url('img/16/moveleft.png') no-repeat center;
}

div.timeline-navigation-move-right {
background: url('img/16/moveright.png') no-repeat center;
}
**************End CHAPTimelineCSS
Tiddler********************************************

**************************************START
CHAPTimelinePlugin*******************************
/***
|''Name:''|CHAPTimelinePlugin|
|''Version:''|0.1 (2012-04-17)|
|''Author:''|Ajay|
|''Adapted By:''||
|''Type:''|Plugin|
!Description
This Plugin implements the CHAP Links Timeline
!Usage
Just install the plugin and tag with systemConfig.
Create the tiddler CHAPTimelineCSS and reference in your StyleSheet
tiddler.
Optionally position the following div in your PageTemplate to control
the positioning of the breadcrumbs menu:
{{{
<div id='mytimeline'></div>
}}}
!Revision History
* Just started
!Code
***/

// // Blah
//{{{
/**
* @file timeline.js
*
* @brief
* The Timeline is an interactive visualization chart to visualize
events in
* time, having a start and end date.
* You can freely move and zoom in the timeline by dragging
* and scrolling in the Timeline. Items are optionally dragable. The
time
* scale on the axis is adjusted automatically, and supports scales
ranging
* from milliseconds to years.
*
* Timeline is part of the CHAP Links library.
*
* Timeline is tested on Firefox 3.6, Safari 5.0, Chrome 6.0, Opera
10.6, and
* Internet Explorer 6+.
*
* @license
* Licensed under the Apache License, Version 2.0 (the "License"); you
may not
* use this file except in compliance with the License. You may obtain
a copy
* of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the
* License for the specific language governing permissions and
limitations under
* the License.
*
* Copyright (c) 2011-2012 Almende B.V.
*
* @author Jos de Jong, <j...@almende.org>
* @date 2012-03-22
*/


/*
* TODO
*
* Add methods deleteItem, addItem, changeItem to the GWT wrapper
* Add moving items from one group to another
* Add options for a minimum and maximum zoom level
* Add zooming with pinching on Android
*
* Bug: neglect items when they have no valid start/end, instead of
throwing an error
* Bug: Pinching on ipad does not work very well, sometimes the page
will zoom when pinching vertically
* Bug: cannot set max width for an item, like div.timeline-event-
content {white-space: normal; max-width: 100px;}
* Bug on IE in Quirks mode. When you have groups, and delete an item,
the groups become invisible
*
*/

/**
* Declare a unique namespace for CHAP's Common Hybrid Visualisation
Library,
* "links"
*/
if (typeof links === 'undefined') {
links = {};
// important: do not use var, as "var links = {};" will overwrite
// the existing links variable value with undefined in
IE8, IE7.
}


/**
* Ensure the variable google exists
*/
if (typeof google === 'undefined') {
google = undefined;
// important: do not use var, as "var google = undefined;" will
overwrite
// the existing google variable value with undefined in
IE8, IE7.
}


/**
* @class Timeline
* The timeline is a visualization chart to visualize events in time.
*
* The timeline is developed in javascript as a Google Visualization
Chart.
*
* @param {dom_element} container The DOM element in which the
Timeline will
* be created. Normally a div
element.
*/
links.Timeline = function(container) {
// create variables and set default values
this.dom = {};
this.conversion = {};
this.eventParams = {}; // stores parameters for mouse events
this.groups = [];
this.groupIndexes = {};
this.items = [];
this.selection = undefined; // stores index and item which is
currently selected

this.listeners = {}; // event listener callbacks

// Initialize sizes.
// Needed for IE (which gives an error when you try to set an
undefined
// value in a style)
this.size = {
'actualHeight': 0,
'axis': {
'characterMajorHeight': 0,
'characterMajorWidth': 0,
'characterMinorHeight': 0,
'characterMinorWidth': 0,
'height': 0,
'labelMajorTop': 0,
'labelMinorTop': 0,
'line': 0,
'lineMajorWidth': 0,
'lineMinorHeight': 0,
'lineMinorTop': 0,
'lineMinorWidth': 0,
'top': 0
},
'contentHeight': 0,
'contentLeft': 0,
'contentWidth': 0,
'dataChanged': false,
'frameHeight': 0,
'frameWidth': 0,
'groupsLeft': 0,
'groupsWidth': 0,
'items': {
'top': 0
}
};

this.dom.container = container;

this.options = {
'width': "100%",
'height': "auto",
'minHeight': 0, // minimal height in pixels
'autoHeight': true,

'eventMargin': 10, // minimal margin between events
'eventMarginAxis': 20, // minimal margin beteen events and the
axis
'dragAreaWidth': 10, // pixels

'moveable': true,
'zoomable': true,
'selectable': true,
'editable': false,
'snapEvents': true,

'showCurrentTime': true, // show a red bar displaying the current
time
'showCustomTime': false, // show a blue, draggable bar displaying
a custom time
'showMajorLabels': true,
'showNavigation': false,
'showButtonAdd': true,
'groupsOnRight': false,
'axisOnTop': false,
'stackEvents': true,
'animate': true,
'animateZoom': true,
'style': 'box'
};

this.clientTimeOffset = 0; // difference between client time and
the time
// set via Timeline.setCurrentTime()
var dom = this.dom;

// remove all elements from the container element.
while (dom.container.hasChildNodes()) {
dom.container.removeChild(dom.container.firstChild);
}

// create a step for drawing the axis
this.step = new links.Timeline.StepDate();

// initialize data
this.data = [];
this.firstDraw = true;

// date interval must be initialized
this.setVisibleChartRange(undefined, undefined, false);

// create all DOM elements
this.redrawFrame();

// Internet Explorer does not support Array.indexof,
// so we define it here in that case
// http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/
if(!Array.prototype.indexOf) {
Array.prototype.indexOf = function(obj){
for(var i = 0; i < this.length; i++){
if(this[i] == obj){
return i;
}
}
return -1;
}
}

// fire the ready event
this.trigger('ready');
}


/**
* Main drawing logic. This is the function that needs to be called
* in the html page, to draw the timeline.
*
* A data table with the events must be provided, and an options
table.
*
* @param {DataTable} data The data containing the events for
the timeline.
* Object DataTable is defined in
* google.visualization.DataTable
* @param {name/value map} options A name/value map containing
settings for the
* timeline. Optional.
*/
links.Timeline.prototype.draw = function(data, options) {
if (options) {
// retrieve parameter values
for (var i in options) {
if (options.hasOwnProperty(i)) {
this.options[i] = options[i];
}
}
}
this.options.autoHeight = (this.options.height === "auto");

// read the data
this.setData(data);

// set timer range. this will also redraw the timeline
if (options && options.start && options.end) {
this.setVisibleChartRange(options.start, options.end);
}
else if (this.firstDraw) {
this.setVisibleChartRangeAuto();
}

this.firstDraw = false;
}

/**
* Set data for the timeline
* @param {DataTable or JSON array} data
*/
links.Timeline.prototype.setData = function(data) {
// unselect any previously selected item
this.unselectItem();

if (!data) {
data = [];
}

this.items = [];
this.data = data;
var items = this.items;
var options = this.options;

// create groups from the data
this.setGroups(data);

if (google && google.visualization &&
data instanceof google.visualization.DataTable) {
// read DataTable
var hasGroups = (data.getNumberOfColumns() > 3);
for (var row = 0, rows = data.getNumberOfRows(); row < rows; row+
+) {
items.push(this.createItem({
'start': data.getValue(row, 0),
'end': data.getValue(row, 1),
'content': data.getValue(row, 2),
'group': (hasGroups ? data.getValue(row, 3) : undefined)
}));
}
}
else if (links.Timeline.isArray(data)) {
// read JSON array
for (var row = 0, rows = data.length; row < rows; row++) {
var itemData = data[row]
var item = this.createItem(itemData);
items.push(item);
}
}
else {
throw "Unknown data type. DataTable or Array expected.";
}

// set a flag to force the recalcSize method to recalculate the
// heights and widths of the events
this.size.dataChanged = true;
this.redrawFrame(); // create the items for the new data
this.recalcSize(); // position the items
this.stackEvents(false);
this.redrawFrame(); // redraw the items on the final positions
this.size.dataChanged = false;
}

/**
* Set the groups available in the given dataset
* @param {DataTable or JSON array} data
*/
links.Timeline.prototype.setGroups = function (data) {
this.deleteGroups();
var groups = this.groups;
var groupIndexes = this.groupIndexes;

if (google && google.visualization &&
data instanceof google.visualization.DataTable) {
// get groups from DataTable
var hasGroups = (data.getNumberOfColumns() > 3);
if (hasGroups) {
var groupNames = data.getDistinctValues(3);
for (var i = 0, iMax = groupNames.length; i < iMax; i++) {
this.addGroup(groupNames[i]);
}
}
}
else if (links.Timeline.isArray(data)){
// get groups from JSON Array
for (var i = 0, iMax = data.length; i < iMax; i++) {
var row = data[i],
group = row.group;
if (group) {
this.addGroup(group);
}
}
}
else {
throw 'Unknown data type. DataTable or Array expected.';
}
}


/**
* Return the original data table.
* @param {Google DataTable or Array} data
*/
links.Timeline.prototype.getData = function () {
return this.data;
}


/**
* Update the original data with changed start, end or group.
*
* @param {Number} index
* @param {Object} values An object containing some of the following
parameters:
* {Date} start,
* {Date} end,
* {String} content,
* {String} group
*/
links.Timeline.prototype.updateData = function (index, values) {
var data = this.data;

if (google && google.visualization &&
data instanceof google.visualization.DataTable) {
// update the original google DataTable
var missingRows = (index + 1) - data.getNumberOfRows();
if (missingRows > 0) {
data.addRows(missingRows);
}

if (values.start) {
data.setValue(index, 0, values.start);
}
if (values.end) {
data.setValue(index, 1, values.end);
}
if (values.content) {
data.setValue(index, 2, values.content);
}
if (values.group && data.getNumberOfColumns() > 3) {
// TODO: append a column when needed?
data.setValue(index, 3, values.group);
}
}
else if (links.Timeline.isArray(data)) {
// update the original JSON table
var row = data[index];
if (row == undefined) {
row = {};
data[index] = row;
}

if (values.start) {
row.start = values.start;
}
if (values.end) {
row.end = values.end;
}
if (values.content) {
row.content = values.content;
}
if (values.group) {
row.group = values.group;
}
}
else {
throw "Cannot update data, unknown type of data";
}
}

/**
* Find the item index from a given HTML element
* If no item index is found, undefined is returned
* @param {HTML DOM element} element
* @return {Number} index
*/
links.Timeline.prototype.getItemIndex = function(element) {
var e = element,
dom = this.dom,
items = this.items,
index = undefined;

// try to find the frame where the items are located in
while (e.parentNode && e.parentNode !== dom.items.frame) {
e = e.parentNode;
}

if (e.parentNode === dom.items.frame) {
// yes! we have found the parent element of all items
// retrieve its id from the array with items
for (var i = 0, iMax = items.length; i < iMax; i++) {
if (items[i].dom === e) {
index = i;
break;
}
}
}

return index;
}

/**
* Set a new size for the timeline
* @param {string} width Width in pixels or percentage (for example
"800px"
* or "50%")
* @param {string} height Height in pixels or percentage (for
example "400px"
* or "30%")
*/
links.Timeline.prototype.setSize = function(width, height) {
if (width) {
this.options.width = width;
this.dom.frame.style.width = width;
}
if (height) {
this.options.height = height;
this.options.autoHeight = (this.options.height === "auto");
if (height !== "auto" ) {
this.dom.frame.style.height = height;
}
}

this.recalcSize();
this.stackEvents(false);
this.redrawFrame();
}


/**
* Set a new value for the visible range int the timeline.
* Set start to null to include everything from the earliest date to
end.
* Set end to null to include everything from start to the last date.
* Example usage:
* myTimeline.setVisibleChartRange(new Date("2010-08-22"),
* new Date("2010-09-13"));
* @param {Date} start The start date for the timeline. optional
* @param {Date} end The end date for the timeline. optional
* @param {boolean} redraw Optional. If true (default) the Timeline
is
* directly redrawn
*/
links.Timeline.prototype.setVisibleChartRange = function(start, end,
redraw) {
if (start != null) {
this.start = new Date(start);
} else {
// default of 3 days ago
this.start = new Date();
this.start.setDate(this.start.getDate() - 3);
}

if (end != null) {
this.end = new Date(end);
} else {
// default of 4 days ahead
this.end = new Date();
this.end.setDate(this.end.getDate() + 4);
}

// prevent start Date <= end Date
if (this.end.valueOf() <= this.start.valueOf()) {
this.end = new Date(this.start);
this.end.setDate(this.end.getDate() + 7);
}

if (redraw == undefined || redraw == true) {
this.recalcSize();
this.stackEvents(false);
this.redrawFrame();
}
else {
this.recalcConversion();
}
}


/**
* Change the visible chart range such that all items become visible
*/
links.Timeline.prototype.setVisibleChartRangeAuto = function() {
var items = this.items;
startMin = undefined, // long value of a data
endMax = undefined; // long value of a data

// find earliest start date from the data
for (var i = 0, iMax = items.length; i < iMax; i++) {
var item = items[i],
start = item.start ? item.start.valueOf() : undefined,
end = item.end ? item.end.valueOf() : start;

if (startMin !== undefined && start !== undefined) {
startMin = Math.min(startMin, start);
}
else {
startMin = start;
}
if (endMax !== undefined && end !== undefined) {
endMax = Math.max(endMax, end);
}
else {
endMax = end;
}
}

if (startMin !== undefined && endMax !== undefined) {
// zoom out 5% such that you have a little white space on the left
and right
var center = (endMax + startMin) / 2,
diff = (endMax - startMin);
startMin = startMin - diff * 0.05;
endMax = endMax + diff * 0.05;

// adjust the start and end date
this.setVisibleChartRange(new Date(startMin), new Date(endMax));
}
else {
this.setVisibleChartRange(undefined, undefined);
}
}

/**
* Adjust the visible range such that the current time is located in
the center
* of the timeline
*/
links.Timeline.prototype.setVisibleChartRangeNow = function() {
var now = new Date();

var diff = (this.end.getTime() - this.start.getTime());

var startNew = new Date(now.getTime() - diff/2);
var endNew = new Date(startNew.getTime() + diff);
this.setVisibleChartRange(startNew, endNew);
}


/**
* Retrieve the current visible range in the timeline.
* @return {Object} An object with start and end properties
*/
links.Timeline.prototype.getVisibleChartRange = function() {
var range = {
'start': new Date(this.start),
'end': new Date(this.end)
};
return range;
}


/**
* Redraw the timeline. This needs to be executed after the start and/
or
* end time are changed, or when data is added or removed dynamically.
*/
links.Timeline.prototype.redrawFrame = function() {
var dom = this.dom,
options = this.options,
size = this.size;

if (!dom.frame) {
// the surrounding main frame
dom.frame = document.createElement("DIV");
dom.frame.className = "timeline-frame";
dom.frame.style.position = "relative";
dom.frame.style.overflow = "hidden";
dom.container.appendChild(dom.frame);
}

if (options.autoHeight) {
dom.frame.style.height = size.frameHeight + "px";
}
else {
dom.frame.style.height = options.height || "100%";
}
dom.frame.style.width = options.width || "100%";

this.redrawContent();
this.redrawGroups();
this.redrawCurrentTime();
this.redrawCustomTime();
this.redrawNavigation();
}


/**
* Redraw the content of the timeline: the axis and the items
*/
links.Timeline.prototype.redrawContent = function() {
var dom = this.dom,
size = this.size;

if (!dom.content) {
// create content box where the axis and canvas will
dom.content = document.createElement("DIV");
//this.frame.className = "timeline-frame";
dom.content.style.position = "relative";
dom.content.style.overflow = "hidden";
dom.frame.appendChild(dom.content);

var timelines = document.createElement("DIV");
timelines.style.position = "absolute";
timelines.style.left = "0px";
timelines.style.top = "0px";
timelines.style.height = "100%";
timelines.style.width = "0px";
dom.content.appendChild(timelines);
dom.contentTimelines = timelines;

var params = this.eventParams,
me = this;
if (!params.onMouseDown) {
params.onMouseDown = function (event) {me.onMouseDown(event);};
links.Timeline.addEventListener(dom.content, "mousedown",
params.onMouseDown);
}
if (!params.onTouchStart) {
params.onTouchStart = function (event)
{me.onTouchStart(event);};
links.Timeline.addEventListener(dom.content, "touchstart",
params.onTouchStart);
}
if (!params.onMouseWheel) {
params.onMouseWheel = function (event)
{me.onMouseWheel(event);};
links.Timeline.addEventListener(dom.content, "mousewheel",
params.onMouseWheel);
}
if (!params.onDblClick) {
params.onDblClick = function (event) {me.onDblClick(event);};
links.Timeline.addEventListener(dom.content, "dblclick",
params.onDblClick);
}
}
dom.content.style.left = size.contentLeft + "px";
dom.content.style.top = "0px";
dom.content.style.width = size.contentWidth + "px";
dom.content.style.height = size.frameHeight + "px";

this.redrawAxis();
this.redrawItems();
this.redrawDeleteButton();
this.redrawDragAreas();
}

/**
* Redraw the timeline axis with minor and major labels
*/
links.Timeline.prototype.redrawAxis = function() {
var dom = this.dom,
options = this.options,
size = this.size,
step = this.step;

var axis = dom.axis;
if (!axis) {
axis = {};
dom.axis = axis;
}
if (size.axis.properties === undefined) {
size.axis.properties = {};
}
if (axis.minorTexts === undefined) {
axis.minorTexts = [];
}
if (axis.minorLines === undefined) {
axis.minorLines = [];
}
if (axis.majorTexts === undefined) {
axis.majorTexts = [];
}
if (axis.majorLines === undefined) {
axis.majorLines = [];
}

if (!axis.frame) {
axis.frame = document.createElement("DIV");
axis.frame.style.position = "absolute";
axis.frame.style.left = "0px";
axis.frame.style.top = "0px";
dom.content.appendChild(axis.frame);
}

// take axis offline
dom.content.removeChild(axis.frame);

axis.frame.style.width = (size.contentWidth) + "px";
axis.frame.style.height = (size.axis.height) + "px";

// the drawn axis is more wide than the actual visual part, such
that
// the axis can be dragged without having to redraw it each time
again.
var start = this.screenToTime(0);
var end = this.screenToTime(size.contentWidth);
var width = size.contentWidth;

// calculate minimum step (in milliseconds) based on character size
this.minimumStep = this.screenToTime(size.axis.characterMinorWidth *
6).valueOf() -
this.screenToTime(0).valueOf();

step.setRange(start, end, this.minimumStep);

this.redrawAxisCharacters();

this.redrawAxisStartOverwriting();

step.start();
var xFirstMajorLabel = undefined;
while (!step.end()) {
var cur = step.getCurrent(),
x = this.timeToScreen(cur),
isMajor = step.isMajor();

this.redrawAxisMinorText(x, step.getLabelMinor());

if (isMajor && options.showMajorLabels) {
if (x > 0) {
if (xFirstMajorLabel === undefined) {
xFirstMajorLabel = x;
}
this.redrawAxisMajorText(x, step.getLabelMajor());
}
this.redrawAxisMajorLine(x);
}
else {
this.redrawAxisMinorLine(x);
}

step.next();
}

// create a major label on the left when needed
if (options.showMajorLabels) {
var leftTime = this.screenToTime(0),
leftText = this.step.getLabelMajor(leftTime),
width = leftText.length * size.axis.characterMajorWidth + 10;//
estimation

if (xFirstMajorLabel === undefined || width < xFirstMajorLabel) {
this.redrawAxisMajorText(0, leftText, leftTime);
}
}

this.redrawAxisHorizontal();

// cleanup left over labels
this.redrawAxisEndOverwriting();

// put axis online
dom.content.insertBefore(axis.frame, dom.content.firstChild);

}

/**
* Create characters used to determine the size of text on the axis
*/
links.Timeline.prototype.redrawAxisCharacters = function () {
// calculate the width and height of a single character
// this is used to calculate the step size, and also the positioning
of the
// axis
var dom = this.dom,
axis = dom.axis;

if (!axis.characterMinor) {
var text = document.createTextNode("0");
var characterMinor = document.createElement("DIV");
characterMinor.className = "timeline-axis-text timeline-axis-text-
minor";
characterMinor.appendChild(text);
characterMinor.style.position = "absolute";
characterMinor.style.visibility = "hidden";
characterMinor.style.paddingLeft = "0px";
characterMinor.style.paddingRight = "0px";
axis.frame.appendChild(characterMinor);

axis.characterMinor = characterMinor;
}

if (!axis.characterMajor) {
var text = document.createTextNode("0");
var characterMajor = document.createElement("DIV");
characterMajor.className = "timeline-axis-text timeline-axis-text-
major";
characterMajor.appendChild(text);
characterMajor.style.position = "absolute";
characterMajor.style.visibility = "hidden";
characterMajor.style.paddingLeft = "0px";
characterMajor.style.paddingRight = "0px";
axis.frame.appendChild(characterMajor);

axis.characterMajor = characterMajor;
}
}

/**
* Initialize redraw of the axis. All existing labels and lines will
be
* overwritten and reused.
*/
links.Timeline.prototype.redrawAxisStartOverwriting = function () {
var properties = this.size.axis.properties;

properties.minorTextNum = 0;
properties.minorLineNum = 0;
properties.majorTextNum = 0;
properties.majorLineNum = 0;
}

/**
* End of overwriting HTML DOM elements of the axis.
* remaining elements will be removed
*/
links.Timeline.prototype.redrawAxisEndOverwriting = function () {
var dom = this.dom,
props = this.size.axis.properties,
frame = this.dom.axis.frame;

// remove leftovers
var minorTexts = dom.axis.minorTexts,
num = props.minorTextNum;
while (minorTexts.length > num) {
var minorText = minorTexts[num];
frame.removeChild(minorText);
minorTexts.splice(num, 1);
}

var minorLines = dom.axis.minorLines,
num = props.minorLineNum;
while (minorLines.length > num) {
var minorLine = minorLines[num];
frame.removeChild(minorLine);
minorLines.splice(num, 1);
}

var majorTexts = dom.axis.majorTexts,
num = props.majorTextNum;
while (majorTexts.length > num) {
var majorText = majorTexts[num];
frame.removeChild(majorText);
majorTexts.splice(num, 1);
}

var majorLines = dom.axis.majorLines,
num = props.majorLineNum;
while (majorLines.length > num) {
var majorLine = majorLines[num];
frame.removeChild(majorLine);
majorLines.splice(num, 1);
}
}

/**
* Redraw the horizontal line and background of the axis
*/
links.Timeline.prototype.redrawAxisHorizontal = function() {
var axis = this.dom.axis,
size = this.size;

if (!axis.backgroundLine) {
// create the axis line background (for a background color or so)
var backgroundLine = document.createElement("DIV");
backgroundLine.className = "timeline-axis";
backgroundLine.style.position = "absolute";
backgroundLine.style.left = "0px";
backgroundLine.style.width = "100%";
backgroundLine.style.border = "none";
axis.frame.insertBefore(backgroundLine, axis.frame.firstChild);

axis.backgroundLine = backgroundLine;
}
axis.backgroundLine.style.top = size.axis.top + "px";
axis.backgroundLine.style.height = size.axis.height + "px";

if (axis.line) {
// put this line at the end of all childs
var line = axis.frame.removeChild(axis.line);
axis.frame.appendChild(line);
}
else {
// make the axis line
var line = document.createElement("DIV");
line.className = "timeline-axis";
line.style.position = "absolute";
line.style.left = "0px";
line.style.width = "100%";
line.style.height = "0px";
axis.frame.appendChild(line);

axis.line = line;
}
axis.line.style.top = size.axis.line + "px";

}

/**
* Create a minor label for the axis at position x
* @param {Number} x
* @param {String} text
*/
links.Timeline.prototype.redrawAxisMinorText = function (x, text) {
var size = this.size,
dom = this.dom,
props = size.axis.properties,
frame = dom.axis.frame,
minorTexts = dom.axis.minorTexts,
index = props.minorTextNum,
label;

if (index < minorTexts.length) {
label = minorTexts[index]
}
else {
// create new label
var content = document.createTextNode(""),
label = document.createElement("DIV");
label.appendChild(content);
label.className = "timeline-axis-text timeline-axis-text-minor";
label.style.position = "absolute";

frame.appendChild(label);

minorTexts.push(label);
}

label.childNodes[0].nodeValue = text;
label.style.left = x + "px";
label.style.top = size.axis.labelMinorTop + "px";
//label.title = title; // TODO: this is a heavy operation

props.minorTextNum++;
}

/**
* Create a minor line for the axis at position x
* @param {Number} x
*/
links.Timeline.prototype.redrawAxisMinorLine = function (x) {
var axis = this.size.axis,
dom = this.dom,
props = axis.properties,
frame = dom.axis.frame,
minorLines = dom.axis.minorLines,
index = props.minorLineNum,
line;

if (index < minorLines.length) {
line = minorLines[index];
}
else {
// create vertical line
line = document.createElement("DIV");
line.className = "timeline-axis-grid timeline-axis-grid-minor";
line.style.position = "absolute";
line.style.width = "0px";

frame.appendChild(line);
minorLines.push(line);
}

line.style.top = axis.lineMinorTop + "px";
line.style.height = axis.lineMinorHeight + "px";
line.style.left = (x - axis.lineMinorWidth/2) + "px";

props.minorLineNum++;
}

/**
* Create a Major label for the axis at position x
* @param {Number} x
* @param {String} text
*/
links.Timeline.prototype.redrawAxisMajorText = function (x, text) {
var size = this.size,
props = size.axis.properties,
frame = this.dom.axis.frame,
majorTexts = this.dom.axis.majorTexts,
index = props.majorTextNum,
label;

if (index < majorTexts.length) {
label = majorTexts[index];
}
else {
// create label
var content = document.createTextNode(text);
label = document.createElement("DIV");
label.className = "timeline-axis-text timeline-axis-text-major";
label.appendChild(content);
label.style.position = "absolute";
label.style.top = "0px";

frame.appendChild(label);
majorTexts.push(label);
}

label.childNodes[0].nodeValue = text;
label.style.top = size.axis.labelMajorTop + "px";
label.style.left = x + "px";
//label.title = title; // TODO: this is a heavy operation

props.majorTextNum ++;
}

/**
* Create a Major line for the axis at position x
* @param {Number} x
*/
links.Timeline.prototype.redrawAxisMajorLine = function (x) {
var size = this.size,
props = size.axis.properties,
axis = this.size.axis,
frame = this.dom.axis.frame,
majorLines = this.dom.axis.majorLines,
index = props.majorLineNum,
line;

if (index < majorLines.length) {
var line = majorLines[index];
}
else {
// create vertical line
line = document.createElement("DIV");
line.className = "timeline-axis-grid timeline-axis-grid-major";
line.style.position = "absolute";
line.style.top = "0px";
line.style.width = "0px";

frame.appendChild(line);
majorLines.push(line);
}

line.style.left = (x - axis.lineMajorWidth/2) + "px";
line.style.height = size.frameHeight + "px";

props.majorLineNum ++;
}

/**
* Redraw all items
*/
links.Timeline.prototype.redrawItems = function() {
var dom = this.dom,
options = this.options,
boxAlign = (options.box && options.box.align) ?
options.box.align : undefined;
size = this.size,
contentWidth = size.contentWidth,
items = this.items;

if (!dom.items) {
dom.items = {};
}

// draw the frame containing the items
var frame = dom.items.frame;
if (!frame) {
frame = document.createElement("DIV");
frame.style.position = "relative";
dom.content.appendChild(frame);
dom.items.frame = frame;
}

frame.style.left = "0px";
//frame.style.width = "0px";
frame.style.top = size.items.top + "px";
frame.style.height = (size.frameHeight - size.axis.height) + "px";

// initialize arrarys for storing the items
var ranges = dom.items.ranges;
if (!ranges) {
ranges = [];
dom.items.ranges = ranges;
}
var boxes = dom.items.boxes;
if (!boxes) {
boxes = [];
dom.items.boxes = boxes;
}
var dots = dom.items.dots;
if (!dots) {
dots = [];
dom.items.dots = dots;
}

// Take frame offline
dom.content.removeChild(frame);

if (size.dataChanged) {
// create the items
var rangesCreated = ranges.length,
boxesCreated = boxes.length,
dotsCreated = dots.length,
rangesUsed = 0,
boxesUsed = 0,
dotsUsed = 0,
itemsLength = items.length;

for (var i = 0, iMax = items.length; i < iMax; i++) {
var item = items[i];
switch (item.type) {
case 'range':
if (rangesUsed < rangesCreated) {
// reuse existing range
var domItem = ranges[rangesUsed];
domItem.firstChild.innerHTML = item.content;
domItem.style.display = '';
item.dom = domItem;
rangesUsed++;
}
else {
// create a new range
var domItem = this.createEventRange(item.content);
ranges[rangesUsed] = domItem;
frame.appendChild(domItem);
item.dom = domItem;
rangesUsed++;
rangesCreated++;
}
break;

case 'box':
if (boxesUsed < boxesCreated) {
// reuse existing box
var domItem = boxes[boxesUsed];
domItem.firstChild.innerHTML = item.content;
domItem.style.display = '';
item.dom = domItem;
boxesUsed++;
}
else {
// create a new box
var domItem = this.createEventBox(item.content);
boxes[boxesUsed] = domItem;
frame.appendChild(domItem);
frame.insertBefore(domItem.line, frame.firstChild);
// Note: line must be added in front of the items,
// such that it stays below all items
frame.appendChild(domItem.dot);
item.dom = domItem;
boxesUsed++;
boxesCreated++;
}
break;

case 'dot':
if (dotsUsed < dotsCreated) {
// reuse existing box
var domItem = dots[dotsUsed];
domItem.firstChild.innerHTML = item.content;
domItem.style.display = '';
item.dom = domItem;
dotsUsed++;
}
else {
// create a new box
var domItem = this.createEventDot(item.content);
dots[dotsUsed] = domItem;
frame.appendChild(domItem);
item.dom = domItem;
dotsUsed++;
dotsCreated++;
}
break;

default:
// do nothing
break;
}
}

// remove redundant items when needed
for (var i = rangesUsed; i < rangesCreated; i++) {
frame.removeChild(ranges[i]);
}
ranges.splice(rangesUsed, rangesCreated - rangesUsed);
for (var i = boxesUsed; i < boxesCreated; i++) {
var box = boxes[i];
frame.removeChild(box.line);
frame.removeChild(box.dot);
frame.removeChild(box);
}
boxes.splice(boxesUsed, boxesCreated - boxesUsed);
for (var i = dotsUsed; i < dotsCreated; i++) {
frame.removeChild(dots[i]);
}
dots.splice(dotsUsed, dotsCreated - dotsUsed);
}

// reposition all items
for (var i = 0, iMax = items.length; i < iMax; i++) {
var item = items[i],
domItem = item.dom;

switch (item.type) {
case 'range':
var left = this.timeToScreen(item.start),
right = this.timeToScreen(item.end);

// limit the width of the item, as browsers cannot draw very
wide divs
if (left < -contentWidth) {
left = -contentWidth;
}
if (right > 2 * contentWidth) {
right = 2 * contentWidth;
}

var visible = right > -contentWidth && left < 2 *
contentWidth;
if (visible || size.dataChanged) {
// when data is changed, all items must be kept visible, as
their heights must be measured
if (item.hidden) {
item.hidden = false;
domItem.style.display = '';
}
domItem.style.top = item.top + "px";
domItem.style.left = left + "px";
//domItem.style.width = Math.max(right - left - 2 *
item.borderWidth, 1) + "px"; // TODO: borderWidth
domItem.style.width = Math.max(right - left, 1) + "px";
}
else {
// hide when outside of the current window
if (!item.hidden) {
domItem.style.display = 'none';
item.hidden = true;
}
}

break;

case 'box':
var left = this.timeToScreen(item.start);

var axisOnTop = options.axisOnTop,
axisHeight = size.axis.height,
axisTop = size.axis.top;
var visible = ((left + item.width/2 > -contentWidth) &&
(left - item.width/2 < 2 * contentWidth));
if (visible || size.dataChanged) {
// when data is changed, all items must be kept visible, as
their heights must be measured
if (item.hidden) {
item.hidden = false;
domItem.style.display = '';
domItem.line.style.display = '';
domItem.dot.style.display = '';
}
domItem.style.top = item.top + "px";
if (boxAlign == 'right') {
domItem.style.left = (left - item.width) + "px";
}
else if (boxAlign == 'left') {
domItem.style.left = (left) + "px";
}
else { // default or 'center'
domItem.style.left = (left - item.width/2) + "px";
}

var line = domItem.line;
line.style.left = (left - item.lineWidth/2) + "px";
if (axisOnTop) {
//line.style.top = axisHeight + "px"; // TODO: cleanup
//line.style.height = (item.top - axisHeight) + "px";
line.style.top = "0px";
line.style.height = Math.max(item.top, 0) + "px";
}
else {
line.style.top = (item.top + item.height) + "px";
line.style.height = Math.max(axisTop - item.top -
item.height, 0) + "px";
}

var dot = domItem.dot;
dot.style.left = (left - item.dotWidth/2) + "px";
dot.style.top = (axisTop - item.dotHeight/2) + "px";
}
else {
// hide when outside of the current window
if (!item.hidden) {
domItem.style.display = 'none';
domItem.line.style.display = 'none';
domItem.dot.style.display = 'none';
item.hidden = true;
}
}
break;

case 'dot':
var left = this.timeToScreen(item.start);

var axisOnTop = options.axisOnTop,
axisHeight = size.axis.height,
axisTop = size.axis.top;
var visible = (left + item.width > -contentWidth) && (left < 2
* contentWidth);
if (visible || size.dataChanged) {
// when data is changed, all items must be kept visible, as
their heights must be measured
if (item.hidden) {
item.hidden = false;
domItem.style.display = '';
}
domItem.style.top = item.top + "px";
domItem.style.left = (left - item.dotWidth / 2) + "px";

domItem.content.style.marginLeft = (1.5 * item.dotWidth) +
"px";
//domItem.content.style.marginRight = (0.5 * item.dotWidth)
+ "px"; // TODO
domItem.dot.style.top = ((item.height - item.dotHeight) / 2)
+ "px";
}
else {
// hide when outside of the current window
if (!item.hidden) {
domItem.style.display = 'none';
item.hidden = true;
}
}
break;

default:
// do nothing
break;
}
}

// move selected item to the end, to ensure that it is always on top
if (this.selection) {
var item = this.selection.item;
frame.removeChild(item);
frame.appendChild(item);
}

// put frame online again
dom.content.appendChild(frame);

/* TODO
// retrieve all image sources from the items, and set a callback
once
// all images are retrieved
var urls = [];
var timeline = this;
links.Timeline.filterImageUrls(frame, urls);
if (urls.length) {
for (var i = 0; i < urls.length; i++) {
var url = urls[i];
var callback = function (url) {
timeline.redraw();
};
var sendCallbackWhenAlreadyLoaded = false;
links.imageloader.load(url, callback,
sendCallbackWhenAlreadyLoaded);
}
}
*/
}


/**
* Create an event in the timeline, with (optional) formatting: inside
a box
* with rounded corners, and a vertical line+dot to the axis.
* @param {string} content The content for the event. This can be
plain text
* or HTML code.
*/
links.Timeline.prototype.createEventBox = function(content) {
// background box
var divBox = document.createElement("DIV");
divBox.style.position = "absolute";
divBox.style.left = "0px";
divBox.style.top = "0px";
divBox.className = "timeline-event timeline-event-box";

// contents box (inside the background box). used for making margins
var divContent = document.createElement("DIV");
divContent.className = "timeline-event-content";
divContent.innerHTML = content;
divBox.appendChild(divContent);

// line to axis
var divLine = document.createElement("DIV");
divLine.style.position = "absolute";
divLine.style.width = "0px";
divLine.className = "timeline-event timeline-event-line";
// important: the vertical line is added at the front of the list of
elements,
// so it will be drawn behind all boxes and ranges
divBox.line = divLine;

// dot on axis
var divDot = document.createElement("DIV");
divDot.style.position = "absolute";
divDot.style.width = "0px";
divDot.style.height = "0px";
divDot.className = "timeline-event timeline-event-dot";
divBox.dot = divDot;

return divBox;
}


/**
* Create an event in the timeline: a dot, followed by the content.
* @param {string} content The content for the event. This can be
plain text
* or HTML code.
*/
links.Timeline.prototype.createEventDot = function(content) {
// background box
var divBox = document.createElement("DIV");
divBox.style.position = "absolute";

// contents box, right from the dot
var divContent = document.createElement("DIV");
divContent.className = "timeline-event-content";
divContent.innerHTML = content;
divBox.appendChild(divContent);

// dot at start
var divDot = document.createElement("DIV");
divDot.style.position = "absolute";
divDot.className = "timeline-event timeline-event-dot";
divDot.style.width = "0px";
divDot.style.height = "0px";
divBox.appendChild(divDot);

divBox.content = divContent;
divBox.dot = divDot;

return divBox;
}


/**
* Create an event range as a beam in the timeline.
* @param {string} content The content for the event. This can be
plain text
* or HTML code.
*/
links.Timeline.prototype.createEventRange = function(content) {
// background box
var divBox = document.createElement("DIV");
divBox.style.position = "absolute";
divBox.className = "timeline-event timeline-event-range";

// contents box
var divContent = document.createElement("DIV");
divContent.className = "timeline-event-content";
divContent.innerHTML = content;
divBox.appendChild(divContent);

return divBox;
}

/**
* Redraw the group labels
*/
links.Timeline.prototype.redrawGroups = function() {
var dom = this.dom,
options = this.options,
size = this.size,
groups = this.groups;

if (dom.groups === undefined) {
dom.groups = {};
}

var labels = dom.groups.labels;
if (!labels) {
labels = [];
dom.groups.labels = labels;
}
var labelLines = dom.groups.labelLines;
if (!labelLines) {
labelLines = [];
dom.groups.labelLines = labelLines;
}
var itemLines = dom.groups.itemLines;
if (!itemLines) {
itemLines = [];
dom.groups.itemLines = itemLines;
}

// create the frame for holding the groups
var frame = dom.groups.frame;
if (!frame) {
var frame = document.createElement("DIV");
frame.className = "timeline-groups-axis";
frame.style.position = "absolute";
frame.style.overflow = "hidden";
frame.style.top = "0px";
frame.style.height = "100%";

dom.frame.appendChild(frame);
dom.groups.frame = frame;
}

frame.style.left = size.groupsLeft + "px";
frame.style.width = (options.groupsWidth !== undefined) ?
options.groupsWidth :
size.groupsWidth + "px";

// hide groups axis when there are no groups
if (groups.length == 0) {
frame.style.display = 'none';
}
else {
frame.style.display = '';
}

if (size.dataChanged) {
// create the items
var current = labels.length,
needed = groups.length;

// overwrite existing items
for (var i = 0, iMax = Math.min(current, needed); i < iMax; i++) {
var group = groups[i];
var label = labels[i];
label.innerHTML = group.content;
label.style.display = '';
}

// append new items when needed
for (var i = current; i < needed; i++) {
var group = groups[i];

// create text label
var label = document.createElement("DIV");
label.className = "timeline-groups-text";
label.style.position = "absolute";
if (options.groupsWidth === undefined) {
label.style.whiteSpace = "nowrap";
}
label.innerHTML = group.content;
frame.appendChild(label);
labels[i] = label;

// create the grid line between the group labels
var labelLine = document.createElement("DIV");
labelLine.className = "timeline-axis-grid timeline-axis-grid-
minor";
labelLine.style.position = "absolute";
labelLine.style.left = "0px";
labelLine.style.width = "100%";
labelLine.style.height = "0px";
labelLine.style.borderTopStyle = "solid";
frame.appendChild(labelLine);
labelLines[i] = labelLine;

// create the grid line between the items
var itemLine = document.createElement("DIV");
itemLine.className = "timeline-axis-grid timeline-axis-grid-
minor";
itemLine.style.position = "absolute";
itemLine.style.left = "0px";
itemLine.style.width = "100%";
itemLine.style.height = "0px";
itemLine.style.borderTopStyle = "solid";
dom.content.insertBefore(itemLine, dom.content.firstChild);
itemLines[i] = itemLine;
}

// remove redundant items from the DOM when needed
for (var i = needed; i < current; i++) {
var label = labels[i],
labelLine = labelLines[i],
itemLine = itemLines[i];

frame.removeChild(label);
frame.removeChild(labelLine);
dom.content.removeChild(itemLine);
}
labels.splice(needed, current - needed);
labelLines.splice(needed, current - needed);
itemLines.splice(needed, current - needed);

frame.style.borderStyle = options.groupsOnRight ?
"none none none solid" :
"none solid none none";
}

// position the groups
for (var i = 0, iMax = groups.length; i < iMax; i++) {
var group = groups[i],
label = labels[i],
labelLine = labelLines[i],
itemLine = itemLines[i];

label.style.top = group.labelTop + "px";
labelLine.style.top = group.lineTop + "px";
itemLine.style.top = group.lineTop + "px";
itemLine.style.width = size.contentWidth + "px";
}

if (!dom.groups.background) {
// create the axis grid line background
var background = document.createElement("DIV");
background.className = "timeline-axis";
background.style.position = "absolute";
background.style.left = "0px";
background.style.width = "100%";
background.style.border = "none";

frame.appendChild(background);
dom.groups.background = background;
}
dom.groups.background.style.top = size.axis.top + 'px';
dom.groups.background.style.height = size.axis.height + 'px';

if (!dom.groups.line) {
// create the axis grid line
var line = document.createElement("DIV");
line.className = "timeline-axis";
line.style.position = "absolute";
line.style.left = "0px";
line.style.width = "100%";
line.style.height = "0px";

frame.appendChild(line);
dom.groups.line = line;
}
dom.groups.line.style.top = size.axis.line + 'px';
}


/**
* Redraw the current time bar
*/
links.Timeline.prototype.redrawCurrentTime = function() {
var options = this.options,
dom = this.dom,
size = this.size;

if (!options.showCurrentTime) {
if (dom.currentTime) {
dom.contentTimelines.removeChild(dom.currentTime);
delete dom.currentTime;
}

return;
}

if (!dom.currentTime) {
// create the current time bar
var currentTime = document.createElement("DIV");
currentTime.className = "timeline-currenttime";
currentTime.style.position = "absolute";
currentTime.style.top = "0px";
currentTime.style.height = "100%";

dom.contentTimelines.appendChild(currentTime);
dom.currentTime = currentTime;
}

var now = new Date();
var nowOffset = new Date(now.getTime() + this.clientTimeOffset);
var x = this.timeToScreen(nowOffset);

var visible = (x > -size.contentWidth && x < 2 * size.contentWidth);
dom.currentTime.style.display = visible ? '' : 'none';
dom.currentTime.style.left = x + "px";
dom.currentTime.title = "Current time: " + nowOffset;

// start a timer to adjust for the new time
if (this.currentTimeTimer != undefined) {
clearTimeout(this.currentTimeTimer);
delete this.currentTimeTimer;
}
var timeline = this;
var onTimeout = function() {
timeline.redrawCurrentTime();
}
// the time equal to the width of one pixel, divided by 2 for more
smoothness
var interval = 1 / this.conversion.factor / 2;
if (interval < 30) interval = 30;
this.currentTimeTimer = setTimeout(onTimeout, interval);
}

/**
* Redraw the custom time bar
*/
links.Timeline.prototype.redrawCustomTime = function() {
var options = this.options,
dom = this.dom,
size = this.size;

if (!options.showCustomTime) {
if (dom.customTime) {
dom.contentTimelines.removeChild(dom.customTime);
delete dom.customTime;
}

return;
}

if (!dom.customTime) {
var customTime = document.createElement("DIV");
customTime.className = "timeline-customtime";
customTime.style.position = "absolute";
customTime.style.top = "0px";
customTime.style.height = "100%";

var drag = document.createElement("DIV");
drag.style.position = "relative";
drag.style.top = "0px";
drag.style.left = "-10px";
drag.style.height = "100%";
drag.style.width = "20px";
customTime.appendChild(drag);

dom.contentTimelines.appendChild(customTime);
dom.customTime = customTime;

// initialize parameter
this.customTime = new Date();
}

var x = this.timeToScreen(this.customTime),
visible = (x > -size.contentWidth && x < 2 * size.contentWidth);
dom.customTime.style.display = visible ? '' : 'none';
dom.customTime.style.left = x + "px";
dom.customTime.title = "Time: " + this.customTime;
}


/**
* Redraw the delete button, on the top right of the currently
selected item
* if there is no item selected, the button is hidden.
*/
links.Timeline.prototype.redrawDeleteButton = function () {
var timeline = this,
options = this.options,
dom = this.dom,
size = this.size,
frame = dom.items.frame;

if (!options.editable) {
return;
}

var deleteButton = dom.items.deleteButton;
if (!deleteButton) {
// create a delete button
deleteButton = document.createElement("DIV");
deleteButton.className = "timeline-navigation-delete";
deleteButton.style.position = "absolute";

frame.appendChild(deleteButton);
dom.items.deleteButton = deleteButton;
}

if (this.selection) {
var index = this.selection.index,
item = this.items[index],
domItem = this.selection.item,
right,
top = item.top;

switch (item.type) {
case 'range':
right = this.timeToScreen(item.end);
break;

case 'box':
//right = this.timeToScreen(item.start) + item.width / 2 +
item.borderWidth; // TODO: borderWidth
right = this.timeToScreen(item.start) + item.width / 2;
break;

case 'dot':
right = this.timeToScreen(item.start) + item.width;
break;
}

// limit the position
if (right < -size.contentWidth) {
right = -size.contentWidth;
}
if (right > 2 * size.contentWidth) {
right = 2 * size.contentWidth;
}

deleteButton.style.left = right + 'px';
deleteButton.style.top = top + 'px';
deleteButton.style.display = '';
frame.removeChild(deleteButton);
frame.appendChild(deleteButton);
}
else {
deleteButton.style.display = 'none';
}
}


/**
* Redraw the drag areas. When an item (ranges only) is selected,
* it gets a drag area on the left and right side, to change its width
*/
links.Timeline.prototype.redrawDragAreas = function () {
var timeline = this,
options = this.options,
dom = this.dom,
size = this.size,
frame = this.dom.items.frame;

if (!options.editable) {
return;
}

// create left drag area
var dragLeft = dom.items.dragLeft;
if (!dragLeft) {
dragLeft = document.createElement("DIV");
dragLeft.style.width = options.dragAreaWidth + "px";
dragLeft.style.position = "absolute";
dragLeft.style.cursor = "w-resize";

frame.appendChild(dragLeft);
dom.items.dragLeft = dragLeft;
}

// create right drag area
var dragRight = dom.items.dragRight;
if (!dragRight) {
dragRight = document.createElement("DIV");
dragRight.style.width = options.dragAreaWidth + "px";
dragRight.style.position = "absolute";
dragRight.style.cursor = "e-resize";

frame.appendChild(dragRight);
dom.items.dragRight = dragRight;
}

// reposition left and right drag area
if (this.selection) {
var index = this.selection.index,
item = this.items[index];

if (item.type == 'range') {
var domItem = item.dom,
left = this.timeToScreen(item.start),
right = this.timeToScreen(item.end),
top = item.top,
height = item.height;

dragLeft.style.left = left + 'px';
dragLeft.style.top = top + 'px';
dragLeft.style.height = height + 'px';
dragLeft.style.display = '';
frame.removeChild(dragLeft);
frame.appendChild(dragLeft);

dragRight.style.left = (right - options.dragAreaWidth) + 'px';
dragRight.style.top = top + 'px';
dragRight.style.height = height + 'px';
dragRight.style.display = '';
frame.removeChild(dragRight);
frame.appendChild(dragRight);
}
}
else {
dragLeft.style.display = 'none';
dragRight.style.display = 'none';
}
}



/**
* Create the navigation buttons for zooming and moving
*/
links.Timeline.prototype.redrawNavigation = function () {
var timeline = this,
options = this.options,
dom = this.dom,
frame = dom.frame,
navBar = dom.navBar;

if (!navBar) {
if (options.editable || options.showNavigation) {
// create a navigation bar containing the navigation buttons
navBar = document.createElement("DIV");
navBar.style.position = "absolute";
navBar.className = "timeline-navigation";
if (options.groupsOnRight) {
navBar.style.left = '10px';
}
else {
navBar.style.right = '10px';
}
if (options.axisOnTop) {
navBar.style.bottom = '10px';
}
else {
navBar.style.top = '10px';
}
dom.navBar = navBar;
frame.appendChild(navBar);
}

if (options.editable && options.showButtonAdd) {
// create a new in button
navBar.addButton = document.createElement("DIV");
navBar.addButton.className = "timeline-navigation-new";

navBar.addButton.title = "Create new event";
var onAdd = function(event) {
links.Timeline.preventDefault(event);
links.Timeline.stopPropagation(event);

// create a new event at the center of the frame
var w = timeline.size.contentWidth;
var x = w / 2;
var xstart = timeline.screenToTime(x - w / 10); // subtract
10% of timeline width
var xend = timeline.screenToTime(x + w / 10); // add 10% of
timeline width
if (options.snapEvents) {
timeline.step.snap(xstart);
timeline.step.snap(xend);
}

var content = "New";
var group = timeline.groups.length ?
timeline.groups[0].content : undefined;

timeline.addItem({
'start': xstart,
'end': xend,
'content': content,
'group': group
});
var index = (timeline.items.length - 1);
timeline.selectItem(index);

timeline.applyAdd = true;

// fire an add event.
// Note that the change can be canceled from within an event
listener if
// this listener calls the method cancelAdd().
timeline.trigger('add');

if (!timeline.applyAdd) {
// undo an add
timeline.deleteItem(index);
}
timeline.redrawDeleteButton();
timeline.redrawDragAreas();
};
links.Timeline.addEventListener(navBar.addButton, "mousedown",
onAdd);
navBar.appendChild(navBar.addButton);
}

if (options.editable && options.showButtonAdd &&
options.showNavigation) {
// create a separator line
navBar.addButton.style.borderRightWidth = "1px";
navBar.addButton.style.borderRightStyle = "solid";
}

if (options.showNavigation) {
// create a zoom in button
navBar.zoomInButton = document.createElement("DIV");
navBar.zoomInButton.className = "timeline-navigation-zoom-in";
navBar.zoomInButton.title = "Zoom in";
var onZoomIn = function(event) {
links.Timeline.preventDefault(event);
links.Timeline.stopPropagation(event);
timeline.zoom(0.4);
timeline.trigger("rangechange");
timeline.trigger("rangechanged");
};
links.Timeline.addEventListener(navBar.zoomInButton,
"mousedown", onZoomIn);
navBar.appendChild(navBar.zoomInButton);

// create a zoom out button
navBar.zoomOutButton = document.createElement("DIV");
navBar.zoomOutButton.className = "timeline-navigation-zoom-out";
navBar.zoomOutButton.title = "Zoom out";
var onZoomOut = function(event) {
links.Timeline.preventDefault(event);
links.Timeline.stopPropagation(event);
timeline.zoom(-0.4);
timeline.trigger("rangechange");
timeline.trigger("rangechanged");
};
links.Timeline.addEventListener(navBar.zoomOutButton,
"mousedown", onZoomOut);
navBar.appendChild(navBar.zoomOutButton);

// create a move left button
navBar.moveLeftButton = document.createElement("DIV");
navBar.moveLeftButton.className = "timeline-navigation-move-
left";
navBar.moveLeftButton.title = "Move left";
var onMoveLeft = function(event) {
links.Timeline.preventDefault(event);
links.Timeline.stopPropagation(event);
timeline.move(-0.2);
timeline.trigger("rangechange");
timeline.trigger("rangechanged");
};
links.Timeline.addEventListener(navBar.moveLeftButton,
"mousedown", onMoveLeft);
navBar.appendChild(navBar.moveLeftButton);

// create a move right button
navBar.moveRightButton = document.createElement("DIV");
navBar.moveRightButton.className = "timeline-navigation-move-
right";
navBar.moveRightButton.title = "Move right";
var onMoveRight = function(event) {
links.Timeline.preventDefault(event);
links.Timeline.stopPropagation(event);
timeline.move(0.2);
timeline.trigger("rangechange");
timeline.trigger("rangechanged");
};
links.Timeline.addEventListener(navBar.moveRightButton,
"mousedown", onMoveRight);
navBar.appendChild(navBar.moveRightButton);
}
}
}


/**
* Set current time. This function can be used to set the time in the
client
* timeline equal with the time on a server.
* @param {Date} time
*/
links.Timeline.prototype.setCurrentTime = function(time) {
var now = new Date();
this.clientTimeOffset = time.getTime() - now.getTime();

this.redrawCurrentTime();
}

/**
* Get current time. The time can have an offset from the real time,
when
* the current time has been changed via the method setCurrentTime.
* @return {Date} time
*/
links.Timeline.prototype.getCurrentTime = function() {
var now = new Date();
return new Date(now.getTime() + this.clientTimeOffset);
}


/**
* Set custom time.
* The custom time bar can be used to display events in past or
future.
* @param {Date} time
*/
links.Timeline.prototype.setCustomTime = function(time) {
this.customTime = new Date(time);
this.redrawCustomTime();
}

/**
* Retrieve the current custom time.
* @return {Date} customTime
*/
links.Timeline.prototype.getCustomTime = function() {
return new Date(this.customTime);
}

/**
* Set a custom scale. Autoscaling will be disabled.
* For example setScale(SCALE.MINUTES, 5) will result
* in minor steps of 5 minutes, and major steps of an hour.
*
* @param {Step.SCALE} newScale A scale. Choose from
SCALE.MILLISECOND,
* SCALE.SECOND, SCALE.MINUTE,
SCALE.HOUR,
* SCALE.DAY, SCALE.MONTH, SCALE.YEAR.
* @param {int} newStep A step size, by default 1. Choose for
* example 1, 2, 5, or 10.
*/
links.Timeline.prototype.setScale = function(scale, step) {
this.step.setScale(scale, step);
this.redrawFrame();
}

/**
* Enable or disable autoscaling
* @param {boolean} enable If true or not defined, autoscaling is
enabled.
* If false, autoscaling is disabled.
*/
links.Timeline.prototype.setAutoScale = function(enable) {
this.step.setAutoScale(enable);
this.redrawFrame();
}

/**
* Redraw the timeline
* Reloads the (linked) data table and redraws the timeline when
resized.
* See also the method checkResize
*/
links.Timeline.prototype.redraw = function() {
this.setData(this.data);
}


/**
* Check if the timeline is resized, and if so, redraw the timeline.
* Useful when the webpage is resized.
*/
links.Timeline.prototype.checkResize = function() {
var resized = this.recalcSize();
if (resized) {
this.redrawFrame();
}
}

/**
* Recursively retrieve all image urls from the images located inside
a given
* HTML element
* @param {HTMLElement} elem
* @param {Array with String} urls Urls will be added here (no
duplicates)
*/
links.Timeline.filterImageUrls = function(elem, urls) {
var child = elem.firstChild;
while (child) {
if (child.tagName == 'IMG') {
var url = child.src;
if (urls.indexOf(url) == -1) {
urls.push(url);
}
}

links.Timeline.filterImageUrls(child, urls);

child = child.nextSibling;
}
}

/**
* Recalculate the sizes of all frames, groups, items, axis
* After recalcSize() is executed, the Timeline should be redrawn
normally
*
* @return {boolean} resized Returns true when the timeline has been
resized
*/
links.Timeline.prototype.recalcSize = function() {
var resized = false;

var timeline = this;
size = this.size,
options = this.options,
axisOnTop = options.axisOnTop,
dom = this.dom,
axis = dom.axis,
groups = this.groups,
labels = dom.groups.labels,
items = this.items

groupsWidth = size.groupsWidth,
characterMinorWidth = axis.characterMinor ?
axis.characterMinor.clientWidth : 0,
characterMinorHeight = axis.characterMinor ?
axis.characterMinor.clientHeight : 0,
characterMajorWidth = axis.characterMajor ?
axis.characterMajor.clientWidth : 0,
characterMajorHeight = axis.characterMajor ?
axis.characterMajor.clientHeight : 0,
axisHeight = characterMinorHeight + (options.showMajorLabels ?
characterMajorHeight : 0),
actualHeight = size.actualHeight || axisHeight;

// TODO: move checking for loaded items when creating the dom
if (size.dataChanged) {
// retrieve all image sources from the items, and set a callback
once
// all images are retrieved
var urls = [];
for (var i = 0, iMax = items.length; i < iMax; i++) {
var item = items[i],
domItem = item.dom;

if (domItem) {
links.Timeline.filterImageUrls(domItem, urls);
}
}
if (urls.length) {
for (var i = 0; i < urls.length; i++) {
var url = urls[i];
var callback = function (url) {
timeline.redraw();
};
var sendCallbackWhenAlreadyLoaded = false;
links.imageloader.load(url, callback,
sendCallbackWhenAlreadyLoaded);
}
}
}

// check sizes of the items and groups (width and height) when the
data is changed
if (size.dataChanged) { // TODO: always calculate the size of an
item?
//if (true) {
groupsWidth = 0;

// loop through all groups to get the maximum width and the
heights
for (var i = 0, iMax = labels.length; i < iMax; i++) {
var group = groups[i];
group.width = labels[i].clientWidth;
group.height = labels[i].clientHeight;
group.labelHeight = group.height;

groupsWidth = Math.max(groupsWidth, group.width);
}

// loop through the width and height of all items
for (var i = 0, iMax = items.length; i < iMax; i++) {
var item = items[i],
domItem = item.dom,
group = item.group;

var width = domItem ? domItem.clientWidth : 0;
var height = domItem ? domItem.clientHeight : 0;
resized = resized || (item.width != width);
resized = resized || (item.height != height);
item.width = width;
item.height = height;
//item.borderWidth = (domItem.offsetWidth - domItem.clientWidth
- 2) / 2; // TODO: borderWidth

switch (item.type) {
case 'range':
break;

case 'box':
item.dotHeight = domItem.dot.offsetHeight;
item.dotWidth = domItem.dot.offsetWidth;
item.lineWidth = domItem.line.offsetWidth;
break;

case 'dot':
item.dotHeight = domItem.dot.offsetHeight;
item.dotWidth = domItem.dot.offsetWidth;
item.contentHeight = domItem.content.offsetHeight;
break;
}

if (group) {
group.height = group.height ? Math.max(group.height,
item.height) : item.height;
}
}

// calculate the actual height of the timeline (needed for auto
sizing
// the timeline)
actualHeight = axisHeight + 2 * options.eventMarginAxis;
for (var i = 0, iMax = groups.length; i < iMax; i++) {
actualHeight += groups[i].height + options.eventMargin;
}
}

// calculate actual height of the timeline when there are no groups
// but stacked items
if (groups.length == 0 && options.autoHeight) {
var min = 0,
max = 0;

if (this.animation && this.animation.finalItems) {
// adjust the offset of all finalItems when the actualHeight has
been changed
var finalItems = this.animation.finalItems,
finalItem = finalItems[0];
if (finalItem && finalItem.top) {
min = finalItem.top,
max = finalItem.top + finalItem.height;
}
for (var i = 1, iMax = finalItems.length; i < iMax; i++) {
finalItem = finalItems[i];
min = Math.min(min, finalItem.top);
max = Math.max(max, finalItem.top + finalItem.height);
}
}
else {
var item = items[0];
if (item && item.top) {
min = item.top,
max = item.top + item.height;
}
for (var i = 1, iMax = items.length; i < iMax; i++) {
var item = items[i];
if (item.top) {
min = Math.min(min, item.top);
max = Math.max(max, (item.top + item.height));
}
}
}

actualHeight = (max - min) + 2 * options.eventMarginAxis +
axisHeight;

if (size.actualHeight != actualHeight && options.autoHeight && !
options.axisOnTop) {
// adjust the offset of all items when the actualHeight has been
changed
var diff = actualHeight - size.actualHeight;
if (this.animation && this.animation.finalItems) {
var finalItems = this.animation.finalItems;
for (var i = 0, iMax = finalItems.length; i < iMax; i++) {
finalItems[i].top += diff;
finalItems[i].item.top += diff; // TODO
}
}
else {
for (var i = 0, iMax = items.length; i < iMax; i++) {
items[i].top += diff;
}
}
}
}

// now the heights of the elements are known, we can calculate the
the
// width and height of frame and axis and content
// Note: IE7 has issues with giving frame.clientWidth, therefore I
use offsetWidth instead
var frameWidth = dom.frame ? dom.frame.offsetWidth : 0,
frameHeight = Math.max(options.autoHeight ?
actualHeight : (dom.frame ? dom.frame.clientHeight : 0),
options.minHeight),
axisTop = axisOnTop ? 0 : frameHeight - axisHeight,
axisLine = axisOnTop ? axisHeight : axisTop,
itemsTop = axisOnTop ? axisHeight : 0,
contentHeight = Math.max(frameHeight - axisHeight, 0);

if (options.groupsWidth !== undefined) {
groupsWidth = dom.groups.frame ? dom.groups.frame.clientWidth : 0;
}
var groupsLeft = options.groupsOnRight ? frameWidth - groupsWidth :
0;

if (size.dataChanged) {
// calculate top positions of the group labels and lines
var eventMargin = options.eventMargin,
top = axisOnTop ?
options.eventMarginAxis + eventMargin/2 :
contentHeight - options.eventMarginAxis + eventMargin/2;

for (var i = 0, iMax = groups.length; i < iMax; i++) {
var group = groups[i];
if (axisOnTop) {
group.top = top;
group.labelTop = top + axisHeight + (group.height -
group.labelHeight) / 2;
group.lineTop = top + axisHeight + group.height + eventMargin/
2;
top += group.height + eventMargin;
}
else {
top -= group.height + eventMargin;
group.top = top;
group.labelTop = top + (group.height - group.labelHeight) / 2;
group.lineTop = top - eventMargin/2;
}
}

// calculate top position of the items
for (var i = 0, iMax = items.length; i < iMax; i++) {
var item = items[i],
group = item.group;

if (group) {
item.top = group.top;
}
}

resized = true;
}

resized = resized || (size.groupsWidth !== groupsWidth);
resized = resized || (size.groupsLeft !== groupsLeft);
resized = resized || (size.actualHeight !== actualHeight);
size.groupsWidth = groupsWidth;
size.groupsLeft = groupsLeft;
size.actualHeight = actualHeight;

resized = resized || (size.frameWidth !== frameWidth);
resized = resized || (size.frameHeight !== frameHeight);
size.frameWidth = frameWidth;
size.frameHeight = frameHeight;

resized = resized || (size.groupsWidth !== groupsWidth);
size.groupsWidth = groupsWidth;
size.contentLeft = options.groupsOnRight ? 0 : groupsWidth;
size.contentWidth = Math.max(frameWidth - groupsWidth, 0);
size.contentHeight = contentHeight;

resized = resized || (size.axis.top !== axisTop);
resized = resized || (size.axis.line !== axisLine);
resized = resized || (size.axis.height !== axisHeight);
resized = resized || (size.items.top !== itemsTop);
size.axis.top = axisTop;
size.axis.line = axisLine;
size.axis.height = axisHeight;
size.axis.labelMajorTop = options.axisOnTop ? 0 : axisLine +
characterMinorHeight;
size.axis.labelMinorTop = options.axisOnTop ?
(options.showMajorLabels ? characterMajorHeight : 0) :
axisLine;
size.axis.lineMinorTop = options.axisOnTop ?
size.axis.labelMinorTop : 0;
size.axis.lineMinorHeight = options.showMajorLabels ?
frameHeight - characterMajorHeight:
frameHeight;
size.axis.lineMinorWidth = dom.axis.minorLines.length ?
dom.axis.minorLines[0].offsetWidth : 1;
size.axis.lineMajorWidth = dom.axis.majorLines.length ?
dom.axis.majorLines[0].offsetWidth : 1;

size.items.top = itemsTop;

resized = resized || (size.axis.characterMinorWidth !==
characterMinorWidth);
resized = resized || (size.axis.characterMinorHeight !==
characterMinorHeight);
resized = resized || (size.axis.characterMajorWidth !==
characterMajorWidth);
resized = resized || (size.axis.characterMajorHeight !==
characterMajorHeight);
size.axis.characterMinorWidth = characterMinorWidth;
size.axis.characterMinorHeight = characterMinorHeight;
size.axis.characterMajorWidth = characterMajorWidth;
size.axis.characterMajorHeight = characterMajorHeight;

// conversion factors can be changed when width of the Timeline is
changed,
// and when start or end are changed
this.recalcConversion();

return resized;
}



/**
* Calculate the factor and offset to convert a position on screen to
the
* corresponding date and vice versa.
* After the method calcConversionFactor is executed once, the methods
screenToTime and
* timeToScreen can be used.
*/
links.Timeline.prototype.recalcConversion = function() {
this.conversion.offset = parseFloat(this.start.valueOf());
this.conversion.factor = parseFloat(this.size.contentWidth) /
parseFloat(this.end.valueOf() - this.start.valueOf());
}


/**
* Convert a position on screen (pixels) to a datetime
* Before this method can be used, the method calcConversionFactor
must be
* executed once.
* @param {int} x Position on the screen in pixels
* @return {Date} time The datetime the corresponds with given
position x
*/
links.Timeline.prototype.screenToTime = function(x) {
var conversion = this.conversion,
time = new Date(parseFloat(x) / conversion.factor +
conversion.offset);
return time;
}

/**
* Convert a datetime (Date object) into a position on the screen
* Before this method can be used, the method calcConversionFactor
must be
* executed once.
* @param {Date} time A date
* @return {int} x The position on the screen in pixels which
corresponds
* with the given date.
*/
links.Timeline.prototype.timeToScreen = function(time) {
var conversion = this.conversion;
var x = (time.valueOf() - conversion.offset) * conversion.factor;
return x;
}



/**
* Event handler for touchstart event on mobile devices
*/
links.Timeline.prototype.onTouchStart = function(event) {
var params = this.eventParams,
dom = this.dom,
me = this;

if (params.touchDown) {
// if already moving, return
return;
}

params.touchDown = true;
params.zoomed = false;

this.onMouseDown(event);

if (!params.onTouchMove) {
params.onTouchMove = function (event) {me.onTouchMove(event);};
links.Timeline.addEventListener(document, "touchmove",
params.onTouchMove);
}
if (!params.onTouchEnd) {
params.onTouchEnd = function (event) {me.onTouchEnd(event);};
links.Timeline.addEventListener(document, "touchend",
params.onTouchEnd);
}
};

/**
* Event handler for touchmove event on mobile devices
*/
links.Timeline.prototype.onTouchMove = function(event) {
var params = this.eventParams;

if (event.scale && event.scale !== 1) {
params.zoomed = true;
}

if (!params.zoomed) {
// move
this.onMouseMove(event);
}
else {
if (this.options.zoomable) {
// pinch
// TODO: pinch only supported on iPhone/iPad. Create something
manually for Android?
params.zoomed = true;

var scale = event.scale,
oldWidth = (params.end.valueOf() - params.start.valueOf()),
newWidth = oldWidth / scale,
diff = newWidth - oldWidth,
start = new Date(parseInt(params.start.valueOf() - diff/2)),
end = new Date(parseInt(params.end.valueOf() + diff/2));

// TODO: determine zoom-around-date from touch positions?

this.setVisibleChartRange(start, end);
timeline.trigger("rangechange");

links.Timeline.preventDefault(event);
}
}
};

/**
* Event handler for touchend event on mobile devices
*/
links.Timeline.prototype.onTouchEnd = function(event) {
var params = this.eventParams;
params.touchDown = false;

/* TODO: cleanup
document.getElementById("info").innerHTML = "touchEnd";
*/

if (params.zoomed) {
timeline.trigger("rangechanged");
}

if (params.onTouchMove) {
links.Timeline.removeEventListener(document, "touchmove",
params.onTouchMove);
delete params.onTouchMove;

}
if (params.onTouchEnd) {
links.Timeline.removeEventListener(document, "touchend",
params.onTouchEnd);
delete params.onTouchEnd;
}

this.onMouseUp(event);
};


/**
* Start a moving operation inside the provided parent element
* @param {event} event The event that occurred (required for
* retrieving the mouse position)
*/
links.Timeline.prototype.onMouseDown = function(event) {
event = event || window.event;

var params = this.eventParams,
options = this.options,
dom = this.dom;

// only react on left mouse button down
var leftButtonDown = event.which ? (event.which == 1) :
(event.button == 1);
if (!leftButtonDown && !params.touchDown) {
return;
}

// check if frame is not resized (causing a mismatch with the end
Date)
this.recalcSize();

// get mouse position
if (!params.touchDown) {
params.mouseX = event.clientX;
params.mouseY = event.clientY;
}
else {
params.mouseX = event.targetTouches[0].clientX;
params.mouseY = event.targetTouches[0].clientY;
}
if (params.mouseX === undefined) {params.mouseX = 0;}
if (params.mouseY === undefined) {params.mouseY = 0;}
params.frameLeft = links.Timeline.getAbsoluteLeft(this.dom.content);
params.frameTop = links.Timeline.getAbsoluteTop(this.dom.content);
params.previousLeft = 0;
params.previousOffset = 0;

params.moved = false;
params.start = new Date(this.start);
params.end = new Date(this.end);

params.target = links.Timeline.getTarget(event);
params.itemDragLeft = (params.target === this.dom.items.dragLeft);
params.itemDragRight = (params.target === this.dom.items.dragRight);

if (params.itemDragLeft || params.itemDragRight) {
params.itemIndex = this.selection ? this.selection.index :
undefined;
}
else {
params.itemIndex = this.getItemIndex(params.target);
}

params.customTime = (params.target === dom.customTime ||
params.target.parentNode === dom.customTime) ?
this.customTime :
undefined;

params.addItem = (options.editable && event.ctrlKey);
if (params.addItem) {
// create a new event at the current mouse position
var x = params.mouseX - params.frameLeft;
var y = params.mouseY - params.frameTop;

var xstart = this.screenToTime(x);
if (options.snapEvents) {
this.step.snap(xstart);
}
var xend = new Date(xstart);
var content = "New";
var group = this.getGroupFromHeight(y);
this.addItem({
'start': xstart,
'end': xend,
'content': content,
'group': group.content
});
params.itemIndex = (this.items.length - 1);
this.selectItem(params.itemIndex);
params.itemDragRight = true;
}

params.editItem = options.editable ?
this.isSelected(params.itemIndex) : undefined;
if (params.editItem) {
var item = this.items[params.itemIndex];
params.itemStart = item.start;
params.itemEnd = item.end;
params.itemType = item.type;
if (params.itemType == 'range') {
params.itemLeft = this.timeToScreen(item.start);
params.itemRight = this.timeToScreen(item.end);
}
else {
params.itemLeft = this.timeToScreen(item.start);
}
}
else {
this.dom.frame.style.cursor = 'move';
}
if (!params.touchDown) {
// add event listeners to handle moving the contents
// we store the function onmousemove and onmouseup in the
timeline, so we can
// remove the eventlisteners lateron in the function mouseUp()
var me = this;
if (!params.onMouseMove) {
params.onMouseMove = function (event) {me.onMouseMove(event);};
links.Timeline.addEventListener(document, "mousemove",
params.onMouseMove);
}
if (!params.onMouseUp) {
params.onMouseUp = function (event) {me.onMouseUp(event);};
links.Timeline.addEventListener(document, "mouseup",
params.onMouseUp);
}

links.Timeline.preventDefault(event);
}
}


/**
* Perform moving operating.
* This function activated from within the funcion
links.Timeline.onMouseDown().
* @param {event} event Well, eehh, the event
*/
links.Timeline.prototype.onMouseMove = function (event) {
event = event || window.event;

var params = this.eventParams,
size = this.size,
dom = this.dom,
options = this.options;

// calculate change in mouse position
if (!params.touchDown) {
var mouseX = event.clientX;
var mouseY = event.clientY;
}
else {
var mouseX = event.targetTouches[0].clientX;
var mouseY = event.targetTouches[0].clientY;
}
if (mouseX === undefined) {mouseX = 0;}
if (mouseY === undefined) {mouseY = 0;}

if (params.mouseX === undefined) {
params.mouseX = mouseX;
}
if (params.mouseY === undefined) {
params.mouseY = mouseY;
}

var diffX = parseFloat(mouseX) - params.mouseX;
var diffY = parseFloat(mouseY) - params.mouseY;

params.moved = true;

if (params.customTime) {
var x = this.timeToScreen(params.customTime);
var xnew = x + diffX;
this.customTime = this.screenToTime(xnew);
this.redrawCustomTime();

// fire a timechange event
this.trigger('timechange');
}
else if (params.editItem) {
var item = this.items[params.itemIndex],
domItem = item.dom,
left,
right;

if (params.itemDragLeft) {
// move the start of the item
left = params.itemLeft + diffX;
right = params.itemRight;

item.start = this.screenToTime(left);
if (options.snapEvents) {
this.step.snap(item.start);
left = this.timeToScreen(item.start);
}

if (left > right) {
left = right;
item.start = this.screenToTime(left);
}
}
else if (params.itemDragRight) {
// move the end of the item
left = params.itemLeft;
right = params.itemRight + diffX;

item.end = this.screenToTime(right);
if (options.snapEvents) {
this.step.snap(item.end);
right = this.timeToScreen(item.end);
}

if (right < left) {
right = left;
item.end = this.screenToTime(right);
}
}
else {
// move the item
left = params.itemLeft + diffX;
item.start = this.screenToTime(left);
if (options.snapEvents) {
this.step.snap(item.start);
left = this.timeToScreen(item.start);
}

if (item.end) {
right = left + (params.itemRight - params.itemLeft);
item.end = this.screenToTime(right);
}
}

switch(item.type) {
case 'range':
domItem.style.left = left + "px";
//domItem.style.width = Math.max(right - left - 2 *
item.borderWidth, 1) + "px"; // TODO
domItem.style.width = Math.max(right - left, 1) + "px";
break;

case 'box':
domItem.style.left = (left - item.width / 2) + "px";
domItem.line.style.left = (left - item.lineWidth / 2) + "px";
domItem.dot.style.left = (left - item.dotWidth / 2) + "px";
break;

case 'dot':
domItem.style.left = (left - item.dotWidth / 2) + "px";
break;
}

if (this.groups.length == 0) {
// TODO: does not work well in FF, forces redraw with every
mouse move it seems
this.stackEvents(options.animate);
if (!options.animate) {
this.redrawFrame();
}
// Note: when animate==true, no redraw is needed here, its done
by stackEvents animation
}
else {
/* TODO: move item from one group to another when needed
var y = mouseY - params.frameTop;
var group = this.getGroupFromHeight(y);
if (item.group !== group) {
// ... move item to the other group
}
*/
}

this.redrawDeleteButton();
this.redrawDragAreas();
}
else if (options.moveable) {
var interval = (params.end.valueOf() - params.start.valueOf());
var diffMillisecs = parseFloat(-diffX) / size.contentWidth *
interval;
this.start = new Date(params.start.valueOf() +
Math.round(diffMillisecs));
this.end = new Date(params.end.valueOf() +
Math.round(diffMillisecs));

this.recalcConversion();

// move the items by changing the left position of their frame.
// this is much faster than repositioning all elements
individually via the
// redrawFrame() function (which is done once at mouseup)
// note that we round diffX to prevent wrong positioning on
millisecond scale
var previousLeft = params.previousLeft || 0;
var currentLeft = parseFloat(dom.items.frame.style.left) || 0;
var previousOffset = params.previousOffset || 0;
var frameOffset = previousOffset + (currentLeft - previousLeft);
var frameLeft = -Math.round(diffMillisecs) / interval *
size.contentWidth + frameOffset;
params.previousOffset = frameOffset;
params.previousLeft = frameLeft;

dom.items.frame.style.left = (frameLeft) + "px";

this.redrawCurrentTime();
this.redrawCustomTime();
this.redrawAxis();

// fire a rangechange event
this.trigger('rangechange');
}

links.Timeline.preventDefault(event);
}


/**
* Stop moving operating.
* This function activated from within the funcion
links.Timeline.onMouseDown().
* @param {event} event The event
*/
links.Timeline.prototype.onMouseUp = function (event) {
var params = this.eventParams,
options = this.options;

event = event || window.event;

this.dom.frame.style.cursor = 'auto';

// remove event listeners here, important for Safari
if (params.onMouseMove) {
links.Timeline.removeEventListener(document, "mousemove",
params.onMouseMove);
delete params.onMouseMove;
}
if (params.onMouseUp) {
links.Timeline.removeEventListener(document, "mouseup",
params.onMouseUp);
delete params.onMouseUp;
}
//links.Timeline.preventDefault(event);

if (params.customTime) {
// fire a timechanged event
this.trigger('timechanged');
}
else if (params.editItem) {
var item = this.items[params.itemIndex];

if (params.moved || params.addItem) {
this.applyChange = true;
this.applyAdd = true;

this.updateData(params.itemIndex, {
'start': item.start,
'end': item.end
});

// fire an add or change event.
// Note that the change can be canceled from within an event
listener if
// this listener calls the method cancelChange().
this.trigger(params.addItem ? 'add' : 'change');

if (params.addItem) {
if (this.applyAdd) {
this.updateData(params.itemIndex, {
'start': item.start,
'end': item.end,
'content': item.content,
'group': item.group ? item.group.content : undefined
});
}
else {
// undo an add
this.deleteItem(params.itemIndex);
}
}
else {
if (this.applyChange) {
this.updateData(params.itemIndex, {
'start': item.start,
'end': item.end
});
}
else {
// undo a change
delete this.applyChange;
delete this.applyAdd;

var item = this.items[params.itemIndex],
domItem = item.dom;

item.start = params.itemStart;
item.end = params.itemEnd;
domItem.style.left = params.itemLeft + "px";
domItem.style.width = (params.itemRight - params.itemLeft) +
"px";
}
}

this.recalcSize();
this.stackEvents(options.animate);
if (!options.animate) {
this.redrawFrame();
}
this.redrawDeleteButton();
this.redrawDragAreas();
}
}
else {
if (!params.moved && !params.zoomed) {
// mouse did not move -> user has selected an item

if (options.editable && (params.target ===
this.dom.items.deleteButton)) {
// delete item
if (this.selection) {
this.confirmDeleteItem(this.selection.index);
}
this.redrawFrame();
}
else if (options.selectable) {
// select/unselect item
if (params.itemIndex !== undefined) {
if (!this.isSelected(params.itemIndex)) {
this.selectItem(params.itemIndex);
this.trigger('select');
}
}
else {
this.unselectItem();
}
this.redrawDeleteButton();
}
}
else {
// timeline is moved
this.redrawFrame();

if ((params.moved && options.moveable) || (params.zoomed &&
options.zoomable) ) {
// fire a rangechanged event
this.trigger('rangechanged');
}
}
}
}

/**
* Double click event occurred for an item
* @param {event} event
*/
links.Timeline.prototype.onDblClick = function (event) {
var params = this.eventParams,
options = this.options,
dom = this.dom,
size = this.size;
event = event || window.event;

if (!options.editable) {
return;
}

if (params.itemIndex !== undefined) {
// fire the edit event
this.trigger('edit');
}
else {
// create a new item
var x = event.clientX -
links.Timeline.getAbsoluteLeft(dom.content);
var y = event.clientY -
links.Timeline.getAbsoluteTop(dom.content);

// create a new event at the current mouse position
var xstart = this.screenToTime(x);
var xend = this.screenToTime(x + size.frameWidth / 10); // add
10% of timeline width
if (options.snapEvents) {
this.step.snap(xstart);
this.step.snap(xend);
}

var content = "New";
var group = this.getGroupFromHeight(y); // (group may be
undefined)
this.addItem({
'start': xstart,
'end': xend,
'content': content,
'group': group.content
});
params.itemIndex = (this.items.length - 1);
this.selectItem(params.itemIndex);

this.applyAdd = true;

// fire an add event.
// Note that the change can be canceled from within an event
listener if
// this listener calls the method cancelAdd().
this.trigger('add');

if (!this.applyAdd) {
// undo an add
this.deleteItem(params.itemIndex);
}

this.redrawDeleteButton();
this.redrawDragAreas();
}

links.Timeline.preventDefault(event);
}


/**
* Event handler for mouse wheel event, used to zoom the timeline
* Code from http://adomas.org/javascript-mouse-wheel/
* @param {event} event The event
*/
links.Timeline.prototype.onMouseWheel = function(event) {
if (!this.options.zoomable)
return;

if (!event) { /* For IE. */
event = window.event;
}

// retrieve delta
var delta = 0;
if (event.wheelDelta) { /* IE/Opera. */
delta = event.wheelDelta/120;
} else if (event.detail) { /* Mozilla case. */
// In Mozilla, sign of delta is different than in IE.
// Also, delta is multiple of 3.
delta = -event.detail/3;
}

// If delta is nonzero, handle it.
// Basically, delta is now positive if wheel was scrolled up,
// and negative, if wheel was scrolled down.
if (delta) {
// TODO: on FireFox, the window is not redrawn within repeated
scroll-events
// -> use a delayed redraw? Make a zoom queue?

var timeline = this;
var zoom = function () {
// check if frame is not resized (causing a mismatch with the
end date)
timeline.recalcSize();

// perform the zoom action. Delta is normally 1 or -1
var zoomFactor = delta / 5.0;
var frameLeft =
links.Timeline.getAbsoluteLeft(timeline.dom.content);
var zoomAroundDate =
(event.clientX != undefined && frameLeft != undefined) ?
timeline.screenToTime(event.clientX - frameLeft) :
undefined;

timeline.zoom(zoomFactor, zoomAroundDate);

// fire a rangechange and a rangechanged event
timeline.trigger("rangechange");
timeline.trigger("rangechanged");

/* TODO: smooth scrolling on FF
timeline.zooming = false;

if (timeline.zoomingQueue) {
setTimeout(timeline.zoomingQueue, 100);
timeline.zoomingQueue = undefined;
}

timeline.zoomCount = (timeline.zoomCount || 0) + 1;
console.log('zoomCount', timeline.zoomCount)
*/
};

zoom();

/* TODO: smooth scrolling on FF
if (!timeline.zooming || true) {

timeline.zooming = true;
setTimeout(zoom, 100);
}
else {
timeline.zoomingQueue = zoom;
}
//*/
}

// Prevent default actions caused by mouse wheel.
// That might be ugly, but we handle scrolls somehow
// anyway, so don't bother here...
links.Timeline.preventDefault(event);
}


/**
* Zoom the timeline the given zoomfactor in or out. Start and end
date will
* be adjusted, and the timeline will be redrawn. You can optionally
give a
* date around which to zoom.
* For example, try zoomfactor = 0.1 or -0.1
* @param {float} zoomFactor Zooming amount. Positive value will
zoom in,
* negative value will zoom out
* @param {Date} zoomAroundDate Date around which will be zoomed.
Optional
*/
links.Timeline.prototype.zoom = function(zoomFactor, zoomAroundDate) {
// if zoomAroundDate is not provided, take it half between start
Date and end Date
if (zoomAroundDate == undefined) {
zoomAroundDate = new Date((this.start.valueOf() +
this.end.valueOf()) / 2);
}

// prevent zoom factor larger than 1 or smaller than -1 (larger than
1 will
// result in a start>=end )
if (zoomFactor >= 1) {
zoomFactor = 0.9;
}
if (zoomFactor <= -1) {
zoomFactor = -0.9;
}

// adjust a negative factor such that zooming in with 0.1 equals
zooming
// out with a factor -0.1
if (zoomFactor < 0) {
zoomFactor = zoomFactor / (1 + zoomFactor);
}

// zoom start Date and end Date relative to the zoomAroundDate
var startDiff = parseFloat(this.start.valueOf() -
zoomAroundDate.valueOf());
var endDiff = parseFloat(this.end.valueOf() -
zoomAroundDate.valueOf());

// calculate new dates
var newStart = new Date(this.start.valueOf() - startDiff *
zoomFactor);
var newEnd = new Date(this.end.valueOf() - endDiff * zoomFactor);

// prevent scale of less than 10 milliseconds
// TODO: IE has problems with milliseconds
if (zoomFactor > 0 && (newEnd.valueOf() - newStart.valueOf()) < 10)
{
return;
}

// prevent scale of more than than 10 thousand years
if (zoomFactor < 0 && (newEnd.getFullYear() -
newStart.getFullYear()) > 10000) {
return;
}

// apply new dates
this.start = newStart;
this.end = newEnd;

this.recalcSize();
var animate = this.options.animate ? this.options.animateZoom :
false;
this.stackEvents(animate);
if (!animate || this.groups.length > 0) {
this.redrawFrame();
}
/* TODO
else {
this.redrawFrame();
this.recalcSize();
this.stackEvents(animate);
this.redrawFrame();
}*/
}


/**
* Move the timeline the given movefactor to the left or right. Start
and end
* date will be adjusted, and the timeline will be redrawn.
* For example, try moveFactor = 0.1 or -0.1
* @param {float} moveFactor Moving amount. Positive value will
move right,
* negative value will move left
*/
links.Timeline.prototype.move = function(moveFactor) {
// zoom start Date and end Date relative to the zoomAroundDate
var diff = parseFloat(this.end.valueOf() - this.start.valueOf());

// apply new dates
this.start = new Date(this.start.valueOf() + diff * moveFactor);
this.end = new Date(this.end.valueOf() + diff * moveFactor);

this.recalcConversion();
this.redrawFrame();
}

/**
* Delete an item after a confirmation.
* The deletion can be cancelled by executing .cancelDelete() during
the
* triggered event 'delete'.
* @param {int} index Index of the item to be deleted
*/
links.Timeline.prototype.confirmDeleteItem = function(index) {
this.applyDelete = true;

// select the event to be deleted
if (!this.isSelected(index)) {
this.selectItem(index);
}

// fire a delete event trigger.
// Note that the delete event can be canceled from within an event
listener if
// this listener calls the method cancelChange().
this.trigger('delete');

if (this.applyDelete) {
this.deleteItem(index);
}

delete this.applyDelete;
}

/**
* Delete an item
* @param {int} index Index of the item to be deleted
*/
links.Timeline.prototype.deleteItem = function(index) {
if (index >= this.items.length) {
throw "Cannot delete row, index out of range";
}

this.unselectItem();

// actually delete the item
this.items.splice(index, 1);

// delete the row in the original data table
if (this.data) {
if (google && google.visualization &&
this.data instanceof google.visualization.DataTable) {
this.data.removeRow(index);
}
else if (links.Timeline.isArray(this.data)) {
this.data.splice(index, 1);
}
else {
throw "Cannot delete row from data, unknown data type";
}
}

this.size.dataChanged = true;
this.redrawFrame();
this.recalcSize();
this.stackEvents(this.options.animate);
if (!this.options.animate) {
this.redrawFrame();
}
this.size.dataChanged = false;
}


/**
* Delete all items
*/
links.Timeline.prototype.deleteAllItems = function() {
this.unselectItem();

// delete the loaded data
this.items = [];

// delete the groups
this.deleteGroups();

// empty original data table
if (this.data) {
if (google && google.visualization &&
this.data instanceof google.visualization.DataTable) {
this.data.removeRows(0, this.data.getNumberOfRows());
}
else if (links.Timeline.isArray(this.data)) {
this.data.splice(0, this.data.length);
}
else {
throw "Cannot delete row from data, unknown data type";
}
}

this.size.dataChanged = true;
this.redrawFrame();
this.recalcSize();
this.stackEvents(this.options.animate);
if (!this.options.animate) {
this.redrawFrame();
}
this.size.dataChanged = false;

}


/**
* Find the group from a given height in the timeline
* @param {Number} height Height in the timeline
* @return {Object} group The group object, or undefined if out of
range
*/
links.Timeline.prototype.getGroupFromHeight = function(height) {
var groups = this.groups,
options = this.options,
size = this.size,
y = height - (options.axisOnTop ? size.axis.height : 0);

if (groups) {
var group;
for (var i = 0, iMax = groups.length; i < iMax; i++) {
group = groups[i];
if (y > group.top && y < group.top + group.height) {
return group;
}
}

return group; // return the last group
}

return undefined;
}

/**
* Retrieve the properties of an item.
* @param {Number} index
* @return {Object} properties Object containing item
properties:<br>
* {Date} start (required),
* {Date} end (optional),
* {String} content (required),
* {String} group (optional)
*/
links.Timeline.prototype.getItem = function (index) {
if (index >= this.items.length) {
throw "Cannot get item, index out of range";
}

var item = this.items[index];

var properties = {};
properties.start = new Date(item.start);
if (item.end) {
properties.end = new Date(item.end);
}
properties.content = item.content;
if (item.group) {
properties.group = item.group.content;
}

return properties;
}

/**
* Add a new item.
* @param {Object} itemData Object containing item properties:<br>
* {Date} start (required),
* {Date} end (optional),
* {String} content (required),
* {String} group (optional)
*/
links.Timeline.prototype.addItem = function (itemData) {
var items = [
itemData
];

this.addItems(items);
}

/**
* Add new items.
* @param {Array} items An array containing Objects.
* The objects must have the following
parameters:
* {Date} start,
* {Date} end,
* {String} content with text or HTML code,
* {String} group
*/
links.Timeline.prototype.addItems = function (items) {
var newItems = items,
curItems = this.items,
groups = this.groups,
groupIndexes = this.groupIndexes;

// append the items
for (var i = 0, iMax = newItems.length; i < iMax; i++) {
var itemData = items[i];

this.addGroup(itemData.group);

curItems.push(this.createItem(itemData));

var index = curItems.length - 1;
this.updateData(index, itemData);
}

// redraw timeline
this.size.dataChanged = true;
this.redrawFrame();
this.recalcSize();
this.stackEvents(false);
this.redrawFrame();
this.size.dataChanged = false;
}

/**
* Create an item object, containing all needed parameters
* @param {Object} itemData Object containing parameters start, end
* content, group.
* @return {Object} item
*/
links.Timeline.prototype.createItem = function(itemData) {
var item = {
'start': itemData.start,
'end': itemData.end,
'content': itemData.content,
'type': itemData.end ? 'range' : this.options.style,
'group': this.findGroup(itemData.group),
'top': 0,
'left': 0,
'width': 0,
'height': 0,
'lineWidth' : 0,
'dotWidth': 0,
'dotHeight': 0
};
return item;
}

/**
* Edit an item
* @param {Number} index
* @param {Object} itemData Object containing item properties:<br>
* {Date} start (required),
* {Date} end (optional),
* {String} content (required),
* {String} group (optional)
*/
links.Timeline.prototype.changeItem = function (index, itemData) {
if (index >= this.items.length) {
throw "Cannot change item, index out of range";
}

var style = this.options.style;
var item = this.items[index];

// edit the item
if (itemData.start) {
item.start = itemData.start;
}
if (itemData.end) {
item.end = itemData.end;
}
if (itemData.content) {
item.content = itemData.content;
}
if (itemData.group) {
item.group = this.addGroup(itemData.group);
}

// update the original data table
this.updateData(index, itemData);

// redraw timeline
this.size.dataChanged = true;
this.redrawFrame();
this.recalcSize();
this.stackEvents(false);
this.redrawFrame();
this.size.dataChanged = false;
}


/**
* Find a group by its name.
* @param {String} group
* @return {Object} a group object or undefined when group is not
found
*/
links.Timeline.prototype.findGroup = function (group) {
var index = this.groupIndexes[group];
return (index != undefined) ? this.groups[index] : undefined;
}

/**
* Delete all groups
*/
links.Timeline.prototype.deleteGroups = function () {
this.groups = [];
this.groupIndexes = {};
}


/**
* Add a group. When the group already exists, no new group is created
* but the existing group is returned.
* @param {String} groupName the name of the group
* @return {Object} groupObject
*/
links.Timeline.prototype.addGroup = function (groupName) {
var groups = this.groups,
groupIndexes = this.groupIndexes;

var groupObj = groupIndexes[groupName];
if (groupObj === undefined && groupName !== undefined) {
var groupObj = {
'content': groupName,
'labelTop': 0,
'lineTop': 0
// note: this object will lateron get addition information,
// such as height and width of the group
};
groups.push(groupObj);

// sort the groups
if (this.options.axisOnTop) {
groups.sort(function (a, b) {
return a.content > b.content;
});
}
else {
groups.sort(function (a, b) {
return a.content < b.content;
});
}

// rebuilt the groupIndexes
for (var i = 0, iMax = groups.length; i < iMax; i++) {
groupIndexes[groups[i].content] = i;
}
}

return groupObj;
}

/**
* Cancel a change item
* This method can be called insed an event listener which catches the
"change"
* event. The changed event position will be undone.
*/
links.Timeline.prototype.cancelChange = function () {
this.applyChange = false;
}

/**
* Cancel deletion of an item
* This method can be called insed an event listener which catches the
"delete"
* event. Deletion of the event will be undone.
*/
links.Timeline.prototype.cancelDelete = function () {
this.applyDelete = false;
}


/**
* Cancel creation of a new item
* This method can be called insed an event listener which catches the
"new"
* event. Creation of the new the event will be undone.
*/
links.Timeline.prototype.cancelAdd = function () {
this.applyAdd = false;
}


/**
* Select an event. The visible chart range will be moved such that
the selected
* event is placed in the middle.
* For example selection = [{row: 5}];
* @param {array} sel An array with a column row, containing the row
number
* (the id) of the event to be selected.
* @return {boolean} true if selection is succesfully set, else
false.
*/
links.Timeline.prototype.setSelection = function(selection) {
if (selection != undefined && selection.length > 0) {
if (selection[0].row != undefined) {
var index = selection[0].row;
if (this.items[index]) {
var item = this.items[index];
this.selectItem(index);

// move the visible chart range to the selected event.
var start = item.start;
var end = item.end;
if (end != undefined) {
var middle = new Date((end.valueOf() + start.valueOf()) /
2);
} else {
var middle = new Date(start);
}
var diff = (this.end.valueOf() - this.start.valueOf()),
newStart = new Date(middle.valueOf() - diff/2),
newEnd = new Date(middle.valueOf() + diff/2);

this.setVisibleChartRange(newStart, newEnd);

return true;
}
}
}
return false;
}

/**
* Retrieve the currently selected event
* @return {array} sel An array with a column row, containing the row
number
* of the selected event. If there is no
selection, an
* empty array is returned.
*/
links.Timeline.prototype.getSelection = function() {
var sel = [];
if (this.selection) {
sel.push({"row": this.selection.index});
}
return sel;
}


/**
* Select an item by its index
* @param {Number} index
*/
links.Timeline.prototype.selectItem = function(index) {
this.unselectItem();

this.selection = undefined;

if (this.items[index] !== undefined) {
var item = this.items[index],
domItem = item.dom;

this.selection = {
'index': index,
'item': domItem
};

if (this.options.editable) {
domItem.style.cursor = 'move';
}
switch (item.type) {
case 'range':
domItem.className = "timeline-event timeline-event-selected
timeline-event-range";
break;
case 'box':
domItem.className = "timeline-event timeline-event-selected
timeline-event-box";
domItem.line.className = "timeline-event timeline-event-
selected timeline-event-line";
domItem.dot.className = "timeline-event timeline-event-
selected timeline-event-dot";
break;
case 'dot':
domItem.className = "timeline-event timeline-event-selected";
domItem.dot.className = "timeline-event timeline-event-
selected timeline-event-dot";
break;
}
}
}

/**
* Check if an item is currently selected
* @param {Number} index
* @return {boolean} true if row is selected, else false
*/
links.Timeline.prototype.isSelected = function (index) {
return (this.selection && this.selection.index === index);
}

/**
* Unselect the currently selected event (if any)
*/
links.Timeline.prototype.unselectItem = function() {
if (this.selection) {
var item = this.items[this.selection.index];

if (item && item.dom) {
var domItem = item.dom;
domItem.style.cursor = '';
switch (item.type) {
case 'range':
domItem.className = "timeline-event timeline-event-range";
break;
case 'box':
domItem.className = "timeline-event timeline-event-box";
domItem.line.className = "timeline-event timeline-event-
line";
domItem.dot.className = "timeline-event timeline-event-dot";
break;
case 'dot':
domItem.className = "";
domItem.dot.className = "timeline-event timeline-event-dot";
break;
}
}
}

this.selection = undefined;
}


/**
* Stack the items such that they don't overlap. The items will have a
minimal
* distance equal to options.eventMargin.
* @param {boolean} animate if animate is true, the items are
moved to
* their new position animated
*/
links.Timeline.prototype.stackEvents = function(animate) {
if (this.options.stackEvents == false || this.groups.length > 0) {
// under this conditions we refuse to stack the events
return;
}

if (animate == undefined) {
animate = false;
}

var sortedItems = this.stackOrder(this.items);
var finalItems = this.stackCalculateFinal(sortedItems, animate);

if (animate) {
// move animated to the final positions
var animation = this.animation;
if (!animation) {
animation = {};
this.animation = animation;
}
animation.finalItems = finalItems;

var timeline = this;
var step = function () {
var arrived = timeline.stackMoveOneStep(sortedItems,
animation.finalItems);

timeline.recalcSize();
timeline.redrawFrame();

if (!arrived) {
animation.timer = setTimeout(step, 30);
}
else {
delete animation.finalItems;
delete animation.timer;
}
}

if (!animation.timer) {
animation.timer = setTimeout(step, 30);
}
}
else {
this.stackMoveToFinal(sortedItems, finalItems);
this.recalcSize();
//this.redraw(); // TODO: cleanup
}
}


/**
* Order the items in the array this.items. The order is determined
via:
* - Ranges go before boxes and dots.
* - The item with the left most location goes first
* @param {Array} items Array with items
* @return {Array} sortedItems Array with sorted items
*/
links.Timeline.prototype.stackOrder = function(items) {
// TODO: store the sorted items, to have less work later on
var sortedItems = items.concat([]);

var f = function (a, b) {
if (a.type == 'range' && b.type != 'range') {
return -1;
}

if (a.type != 'range' && b.type == 'range') {
return 1;
}

return (a.left - b.left);
};

sortedItems.sort(f);

return sortedItems;
}

/**
* Adjust vertical positions of the events such that they don't
overlap each
* other.
*/
links.Timeline.prototype.stackCalculateFinal = function(items) {
var size = this.size,
axisTop = size.axis.top,
options = this.options,
axisOnTop = options.axisOnTop,
eventMargin = options.eventMargin,
eventMarginAxis = options.eventMarginAxis,
finalItems = [];

// initialize final positions
for (var i = 0, iMax = items.length; i < iMax; i++) {
var item = items[i],
top,
left,
right,
bottom,
height = item.height,
width = item.width;

if (axisOnTop) {
top = axisTop + eventMarginAxis + eventMargin / 2;
}
else {
top = axisTop - height - eventMarginAxis - eventMargin / 2;
}
bottom = top + height;

switch (item.type) {
case 'range':
case 'dot':
left = this.timeToScreen(item.start);
right = item.end ? this.timeToScreen(item.end) : left + width;
break;

case 'box':
left = this.timeToScreen(item.start) - width / 2;
right = left + width;
break;
}

finalItems[i] = {
'left': left,
'top': top,
'right': right,
'bottom': bottom,
'height': height,
'item': item
};
}

// calculate new, non-overlapping positions
//var items = sortedItems;
for (var i = 0, iMax = finalItems.length; i < iMax; i++) {
//for (var i = finalItems.length - 1; i >= 0; i--) {
var finalItem = finalItems[i];
var collidingItem = null;
do {
// TODO: optimize checking for overlap. when there is a gap
without items,
// you only need to check for items from the next item on, not
from zero
collidingItem = this.stackEventsCheckOverlap(finalItems, i, 0,
i-1);
if (collidingItem != null) {
// There is a collision. Reposition the event above the
colliding element
if (axisOnTop) {
finalItem.top = collidingItem.top + collidingItem.height +
eventMargin;
}
else {
finalItem.top = collidingItem.top - finalItem.height -
eventMargin;
}
finalItem.bottom = finalItem.top + finalItem.height;
}
} while (collidingItem);
}

return finalItems;
}


/**
* Move the events one step in the direction of their final positions
* @param {Array} currentItems Array with the real items and their
current
* positions
* @param {Array} finalItems Array with objects containing the
final
* positions of the items
* @return {boolean} arrived True if all items have reached their
final
* location, else false
*/
links.Timeline.prototype.stackMoveOneStep = function(currentItems,
finalItems) {
// TODO: check this method
var arrived = true;

// apply new positions animated
for (i = 0, iMax = currentItems.length; i < iMax; i++) {
var finalItem = finalItems[i],
item = finalItem.item;

var topNow = parseInt(item.top);
var topFinal = parseInt(finalItem.top);
var diff = (topFinal - topNow);
if (diff) {
var step = (topFinal == topNow) ? 0 : ((topFinal > topNow) ? 1 :
-1);
if (Math.abs(diff) > 4) step = diff / 4;
var topNew = parseInt(topNow + step);

if (topNew != topFinal) {
arrived = false;
}

item.top = topNew;
item.bottom = item.top + item.height;
}
else {
item.top = finalItem.top;
item.bottom = finalItem.bottom;
}

item.left = finalItem.left;
item.right = finalItem.right;
}

return arrived;
}



/**
* Move the events from their current position to the final position
* @param {Array} currentItems Array with the real items and their
current
* positions
* @param {Array} finalItems Array with objects containing the
final
* positions of the items
*/
links.Timeline.prototype.stackMoveToFinal = function(currentItems,
finalItems) {
// Put the events directly at there final position
for (i = 0, iMax = currentItems.length; i < iMax; i++) {
var current = currentItems[i],
finalItem = finalItems[i];

current.left = finalItem.left;
current.top = finalItem.top;
current.right = finalItem.right;
current.bottom = finalItem.bottom;
}
}



/**
* Check if the destiny position of given item overlaps with any
* of the other items from index itemStart to itemEnd.
* @param {Array} items Array with items
* @param {int} itemIndex Number of the item to be checked for
overlap
* @param {int} itemStart First item to be checked.
* @param {int} itemEnd Last item to be checked.
* @return {Object} colliding item, or undefined when no
collisions
*/
links.Timeline.prototype.stackEventsCheckOverlap = function(items,
itemIndex,
itemStart, itemEnd) {
eventMargin = this.options.eventMargin,
collision = this.collision;

/* TODO: cleanup
var item1 = items[itemIndex];
for (var i = itemStart; i <= itemEnd; i++) {
var item2 = items[i];
if (collision(item1, item2, eventMargin)) {
if (i != itemIndex) {
return item2;
}
}
}
return;
//*/

// we loop from end to start, as we suppose that the chance of a
// collision is larger for items at the end, so check these first.
var item1 = items[itemIndex];
for (var i = itemEnd; i >= itemStart; i--) {
var item2 = items[i];
if (collision(item1, item2, eventMargin)) {
if (i != itemIndex) {
return item2;
}
}
}
}

/**
* Test if the two provided items collide
* The items must have parameters left, right, top, and bottom.
* @param {htmlelement} item1 The first item
* @param {htmlelement} item2 The second item
* @param {int} margin A minimum required margin. Optional.
* If margin is provided, the two items
will be
* marked colliding when they overlap or
* when the margin between the two is
smaller than
* the requested margin.
* @return {boolean} true if item1 and item2 collide, else
false
*/
links.Timeline.prototype.collision = function(item1, item2, margin) {
// set margin if not specified
if (margin == undefined) {
margin = 0;
}

// calculate if there is overlap (collision)
return (item1.left - margin < item2.right &&
item1.right + margin > item2.left &&
item1.top - margin < item2.bottom &&
item1.bottom + margin > item2.top);
}


/**
* fire an event
* @param {String} event The name of an event, for example
"rangechange" or "edit"
*/
links.Timeline.prototype.trigger = function (event) {
// built up properties
var properties = null;
switch (event) {
case 'rangechange':
case 'rangechanged':
properties = {
'start': new Date(this.start),
'end': new Date(this.end)
};
break;

case 'timechange':
case 'timechanged':
properties = {
'time': new Date(this.customTime)
};
break;
}

// trigger the links event bus
links.events.trigger(this, event, properties);

// trigger the google event bus
if (google && google.visualization) {
google.visualization.events.trigger(this, event, properties);
}
}



/**
------------------------------------------------------------------------
**/


/**
* Event listener (singleton)
*/
links.events = links.events || {
'listeners': [],

/**
* Find a single listener by its object
* @param {Object} object
* @return {Number} index -1 when not found
*/
'indexOf': function (object) {
var listeners = this.listeners;
for (var i = 0, iMax = this.listeners.length; i < iMax; i++) {
var listener = listeners[i];
if (listener && listener.object == object) {
return i;
}
}
return -1;
},

/**
* Add an event listener
* @param {Object} object
* @param {String} event The name of an event, for example
'select'
* @param {function} callback The callback method, called when the
* event takes place
*/
'addListener': function (object, event, callback) {
var index = this.indexOf(object);
var listener = this.listeners[index];
if (!listener) {
listener = {
'object': object,
'events': {}
};
this.listeners.push(listener);
}

var callbacks = listener.events[event];
if (!callbacks) {
callbacks = [];
listener.events[event] = callbacks;
}

// add the callback if it does not yet exist
if (callbacks.indexOf(callback) == -1) {
callbacks.push(callback);
}
},

/**
* Remove an event listener
* @param {Object} object
* @param {String} event The name of an event, for example
'select'
* @param {function} callback The registered callback method
*/
'removeListener': function (object, event, callback) {
var index = this.indexOf(object);
var listener = this.listeners[index];
if (listener) {
var callbacks = listener.events[event];
if (callbacks) {
var index = callbacks.indexOf(callback);
if (index != -1) {
callbacks.splice(index, 1);
}

// remove the array when empty
if (callbacks.length == 0) {
delete listener.events[event];
}
}

// count the number of registered events. remove listener when
empty
var count = 0;
var events = listener.events;
for (var event in events) {
if (events.hasOwnProperty(event)) {
count++;
}
}
if (count == 0) {
delete this.listeners[index];
}
}
},

/**
* Remove all registered event listeners
*/
'removeAllListeners': function () {
this.listeners = [];
},

/**
* Trigger an event. All registered event handlers will be called
* @param {Object} object
* @param {String} event
* @param {Object} properties (optional)
*/
'trigger': function (object, event, properties) {
var index = this.indexOf(object);
var listener = this.listeners[index];
if (listener) {
var callbacks = listener.events[event];
if (callbacks) {
for (var i = 0, iMax = callbacks.length; i < iMax; i++) {
callbacks[i](properties);
}
}
}
}
};


/**
------------------------------------------------------------------------
**/

/**
* @class StepDate
* The class StepDate is an iterator for dates. You provide a start
date and an
* end date. The class itself determines the best scale (step size)
based on the
* provided start Date, end Date, and minimumStep.
*
* If minimumStep is provided, the step size is chosen as close as
possible
* to the minimumStep but larger than minimumStep. If minimumStep is
not
* provided, the scale is set to 1 DAY.
* The minimumStep should correspond with the onscreen size of about 6
characters
*
* Alternatively, you can set a scale by hand.
* After creation, you can initialize the class by executing start().
Then you
* can iterate from the start date to the end date via next(). You can
check if
* the end date is reached with the function end(). After each step,
you can
* retrieve the current date via get().
* The class step has scales ranging from milliseconds, seconds,
minutes, hours,
* days, to years.
*
* Version: 0.9
*
* @param {Date} start The start date, for example new
Date(2010, 9, 21)
* or new Date(2010, 9,21,23,45,00)
* @param {Date} end The end date
* @param {int} minimumStep Optional. Minimum step size in
milliseconds
*/
links.Timeline.StepDate = function(start, end, minimumStep) {

// variables
this.current = new Date();
this._start = new Date();
this._end = new Date();

this.autoScale = true;
this.scale = links.Timeline.StepDate.SCALE.DAY;
this.step = 1;

// initialize the range
this.setRange(start, end, minimumStep);
}

/// enum scale
links.Timeline.StepDate.SCALE = { MILLISECOND : 1,
SECOND : 2,
MINUTE : 3,
HOUR : 4,
DAY : 5,
MONTH : 6,
YEAR : 7};


/**
* Set a new range
* If minimumStep is provided, the step size is chosen as close as
possible
* to the minimumStep but larger than minimumStep. If minimumStep is
not
* provided, the scale is set to 1 DAY.
* The minimumStep should correspond with the onscreen size of about 6
characters
* @param {Date} start The start date and time.
* @param {Date} end The end date and time.
* @param {int} minimumStep Optional. Minimum step size in
milliseconds
*/
links.Timeline.StepDate.prototype.setRange = function(start, end,
minimumStep) {
if (isNaN(start) || isNaN(end)) {
//throw "No legal start or end date in method setRange";
return;
}

this._start = (start != undefined) ? new Date(start) : new
Date();
this._end = (end != undefined) ? new Date(end) : new
Date();

if (this.autoScale) {
this.setMinimumStep(minimumStep);
}
}

/**
* Set the step iterator to the start date.
*/
links.Timeline.StepDate.prototype.start = function() {
this.current = new Date(this._start);
this.roundToMinor();
}

/**
* Round the current date to the first minor date value
* This must be executed once when the current date is set to start
Date
*/
links.Timeline.StepDate.prototype.roundToMinor = function() {
// round to floor
// IMPORTANT: we have no breaks in this switch! (this is no bug)
switch (this.scale) {
case links.Timeline.StepDate.SCALE.YEAR:
this.current.setFullYear(this.step *
Math.floor(this.current.getFullYear() / this.step));
this.current.setMonth(0);
case links.Timeline.StepDate.SCALE.MONTH:
this.current.setDate(1);
case links.Timeline.StepDate.SCALE.DAY:
this.current.setHours(0);
case links.Timeline.StepDate.SCALE.HOUR:
this.current.setMinutes(0);
case links.Timeline.StepDate.SCALE.MINUTE:
this.current.setSeconds(0);
case links.Timeline.StepDate.SCALE.SECOND:
this.current.setMilliseconds(0);
//case links.Timeline.StepDate.SCALE.MILLISECOND: // nothing to do
for milliseconds
}

if (this.step != 1) {
// round down to the first minor value that is a multiple of the
current step size
switch (this.scale) {
case links.Timeline.StepDate.SCALE.MILLISECOND:
this.current.setMilliseconds(this.current.getMilliseconds() -
this.current.getMilliseconds() % this.step); break;
case links.Timeline.StepDate.SCALE.SECOND:
this.current.setSeconds(this.current.getSeconds() -
this.current.getSeconds() % this.step); break;
case links.Timeline.StepDate.SCALE.MINUTE:
this.current.setMinutes(this.current.getMinutes() -
this.current.getMinutes() % this.step); break;
case links.Timeline.StepDate.SCALE.HOUR:
this.current.setHours(this.current.getHours() -
this.current.getHours() % this.step); break;
case links.Timeline.StepDate.SCALE.DAY:
this.current.setDate((this.current.getDate()-1) -
(this.current.getDate()-1) % this.step + 1); break;
case links.Timeline.StepDate.SCALE.MONTH:
this.current.setMonth(this.current.getMonth() -
this.current.getMonth() % this.step); break;
case links.Timeline.StepDate.SCALE.YEAR:
this.current.setFullYear(this.current.getFullYear() -
this.current.getFullYear() % this.step); break;
default: break;
}
}
}

/**
* Check if the end date is reached
* @return {boolean} true if the current date has passed the end date
*/
links.Timeline.StepDate.prototype.end = function () {
return (this.current.getTime() > this._end.getTime());
}

/**
* Do the next step
*/
links.Timeline.StepDate.prototype.next = function() {
var prev = this.current.getTime();

// Two cases, needed to prevent issues with switching daylight
savings
// (end of March and end of October)
if (this.current.getMonth() < 6) {
switch (this.scale)
{
case links.Timeline.StepDate.SCALE.MILLISECOND:

this.current = new Date(this.current.getTime() + this.step);
break;
case links.Timeline.StepDate.SCALE.SECOND: this.current =
new Date(this.current.getTime() + this.step * 1000); break;
case links.Timeline.StepDate.SCALE.MINUTE: this.current =
new Date(this.current.getTime() + this.step * 1000 * 60); break;
case links.Timeline.StepDate.SCALE.HOUR:
this.current = new Date(this.current.getTime() + this.step *
1000 * 60 * 60);
// in case of skipping an hour for daylight savings, adjust
the hour again (else you get: 0h 5h 9h ... instead of 0h 4h 8h ...)
var h = this.current.getHours();
this.current.setHours(h - (h % this.step));
break;
case links.Timeline.StepDate.SCALE.DAY:
this.current.setDate(this.current.getDate() + this.step); break;
case links.Timeline.StepDate.SCALE.MONTH:
this.current.setMonth(this.current.getMonth() + this.step); break;
case links.Timeline.StepDate.SCALE.YEAR:
this.current.setFullYear(this.current.getFullYear() + this.step);
break;
default: break;
}
}
else {
switch (this.scale)
{
case links.Timeline.StepDate.SCALE.MILLISECOND:

this.current = new Date(this.current.getTime() + this.step);
break;
case links.Timeline.StepDate.SCALE.SECOND:
this.current.setSeconds(this.current.getSeconds() + this.step); break;
case links.Timeline.StepDate.SCALE.MINUTE:
this.current.setMinutes(this.current.getMinutes() + this.step); break;
case links.Timeline.StepDate.SCALE.HOUR:
this.current.setHours(this.current.getHours() + this.step); break;
case links.Timeline.StepDate.SCALE.DAY:
this.current.setDate(this.current.getDate() + this.step); break;
case links.Timeline.StepDate.SCALE.MONTH:
this.current.setMonth(this.current.getMonth() + this.step); break;
case links.Timeline.StepDate.SCALE.YEAR:
this.current.setFullYear(this.current.getFullYear() + this.step);
break;
default: break;
}
}

if (this.step != 1) {
// round down to the correct major value
switch (this.scale) {
case links.Timeline.StepDate.SCALE.MILLISECOND:
if(this.current.getMilliseconds() < this.step)
this.current.setMilliseconds(0); break;
case links.Timeline.StepDate.SCALE.SECOND:
if(this.current.getSeconds() < this.step) this.current.setSeconds(0);
break;
case links.Timeline.StepDate.SCALE.MINUTE:
if(this.current.getMinutes() < this.step) this.current.setMinutes(0);
break;
case links.Timeline.StepDate.SCALE.HOUR:
if(this.current.getHours() < this.step) this.current.setHours(0);
break;
case links.Timeline.StepDate.SCALE.DAY:
if(this.current.getDate() < this.step+1) this.current.setDate(1);
break;
case links.Timeline.StepDate.SCALE.MONTH:
if(this.current.getMonth() < this.step) this.current.setMonth(0);
break;
case links.Timeline.StepDate.SCALE.YEAR: break; //
nothing to do for year
default: break;
}
}

// safety mechanism: if current time is still unchanged, move to the
end
if (this.current.getTime() == prev) {
this.current = new Date(this._end);
}
}


/**
* Get the current datetime
* @return {Date} current The current date
*/
links.Timeline.StepDate.prototype.getCurrent = function() {
return this.current;
}

/**
* Set a custom scale. Autoscaling will be disabled.
* For example setScale(SCALE.MINUTES, 5) will result
* in minor steps of 5 minutes, and major steps of an hour.
*
* @param {Step.SCALE} newScale A scale. Choose from
SCALE.MILLISECOND,
* SCALE.SECOND, SCALE.MINUTE,
SCALE.HOUR,
* SCALE.DAY, SCALE.MONTH, SCALE.YEAR.
* @param {int} newStep A step size, by default 1. Choose for
* example 1, 2, 5, or 10.
*/
links.Timeline.StepDate.prototype.setScale = function(newScale,
newStep) {
this.scale = newScale;

if (newStep > 0)
this.step = newStep;

this.autoScale = false;
}

/**
* Enable or disable autoscaling
* @param {boolean} enable If true, autoascaling is set true
*/
links.Timeline.StepDate.prototype.setAutoScale = function (enable) {
this.autoScale = enable;
}


/**
* Automatically determine the scale that bests fits the provided
minimum step
* @param {int} minimumStep The minimum step size in milliseconds
*/
links.Timeline.StepDate.prototype.setMinimumStep =
function(minimumStep) {
if (minimumStep == undefined)
return;

var stepYear = (1000 * 60 * 60 * 24 * 30 * 12);
var stepMonth = (1000 * 60 * 60 * 24 * 30);
var stepDay = (1000 * 60 * 60 * 24);
var stepHour = (1000 * 60 * 60);
var stepMinute = (1000 * 60);
var stepSecond = (1000);
var stepMillisecond= (1);

// find the smallest step that is larger than the provided
minimumStep
if (stepYear*1000 > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.YEAR; this.step = 1000;}
if (stepYear*500 > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.YEAR; this.step = 500;}
if (stepYear*100 > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.YEAR; this.step = 100;}
if (stepYear*50 > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.YEAR; this.step = 50;}
if (stepYear*10 > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.YEAR; this.step = 10;}
if (stepYear*5 > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.YEAR; this.step = 5;}
if (stepYear > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.YEAR; this.step = 1;}
if (stepMonth*3 > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.MONTH; this.step = 3;}
if (stepMonth > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.MONTH; this.step = 1;}
if (stepDay*5 > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.DAY; this.step = 5;}
if (stepDay*2 > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.DAY; this.step = 2;}
if (stepDay > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.DAY; this.step = 1;}
if (stepHour*4 > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.HOUR; this.step = 4;}
if (stepHour > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.HOUR; this.step = 1;}
if (stepMinute*15 > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.MINUTE; this.step = 15;}
if (stepMinute*10 > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.MINUTE; this.step = 10;}
if (stepMinute*5 > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.MINUTE; this.step = 5;}
if (stepMinute > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.MINUTE; this.step = 1;}
if (stepSecond*15 > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.SECOND; this.step = 15;}
if (stepSecond*10 > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.SECOND; this.step = 10;}
if (stepSecond*5 > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.SECOND; this.step = 5;}
if (stepSecond > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.SECOND; this.step = 1;}
if (stepMillisecond*200 > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 200;}
if (stepMillisecond*100 > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 100;}
if (stepMillisecond*50 > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 50;}
if (stepMillisecond*10 > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 10;}
if (stepMillisecond*5 > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 5;}
if (stepMillisecond > minimumStep) {this.scale =
links.Timeline.StepDate.SCALE.MILLISECOND; this.step = 1;}
}

/**
* Snap a date to a rounded value. The snap intervals are dependent on
the
* current scale and step.
* @param {Date} date the date to be snapped
*/
links.Timeline.StepDate.prototype.snap = function(date) {
if (this.scale == links.Timeline.StepDate.SCALE.YEAR) {
var year = date.getFullYear() + Math.round(date.getMonth() / 12);
date.setFullYear(Math.round(year / this.step) * this.step);
date.setMonth(0);
date.setDate(0);
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
}
else if (this.scale == links.Timeline.StepDate.SCALE.MONTH) {
if (date.getDate() > 15) {
date.setDate(1);
date.setMonth(date.getMonth() + 1);
// important: first set Date to 1, after that change the month.
}
else {
date.setDate(1);
}

date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
}
else if (this.scale == links.Timeline.StepDate.SCALE.DAY) {
switch (this.step) {
case 5:
case 2:
date.setHours(Math.round(date.getHours() / 24) * 24); break;
default:
date.setHours(Math.round(date.getHours() / 12) * 12); break;
}
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
}
else if (this.scale == links.Timeline.StepDate.SCALE.HOUR) {
switch (this.step) {
case 4:
date.setMinutes(Math.round(date.getMinutes() / 60) * 60);
break;
default:
date.setMinutes(Math.round(date.getMinutes() / 30) * 30);
break;
}
date.setSeconds(0);
date.setMilliseconds(0);
} else if (this.scale == links.Timeline.StepDate.SCALE.MINUTE) {
switch (this.step) {
case 15:
case 10:
date.setMinutes(Math.round(date.getMinutes() / 5) * 5);
date.setSeconds(0);
break;
case 5:
date.setSeconds(Math.round(date.getSeconds() / 60) * 60);
break;
default:
date.setSeconds(Math.round(date.getSeconds() / 30) * 30);
break;
}
date.setMilliseconds(0);
}
else if (this.scale == links.Timeline.StepDate.SCALE.SECOND) {
switch (this.step) {
case 15:
case 10:
date.setSeconds(Math.round(date.getSeconds() / 5) * 5);
date.setMilliseconds(0);
break;
case 5:
date.setMilliseconds(Math.round(date.getMilliseconds() / 1000)
* 1000); break;
default:
date.setMilliseconds(Math.round(date.getMilliseconds() / 500)
* 500); break;
}
}
else if (this.scale == links.Timeline.StepDate.SCALE.MILLISECOND) {
var step = this.step > 5 ? this.step / 2 : 1;
date.setMilliseconds(Math.round(date.getMilliseconds() / step) *
step);
}
}

/**
* Check if the current step is a major step (for example when the
step
* is DAY, a major step is each first day of the MONTH)
* @return true if current date is major, else false.
*/
links.Timeline.StepDate.prototype.isMajor = function() {
switch (this.scale)
{
case links.Timeline.StepDate.SCALE.MILLISECOND:
return (this.current.getMilliseconds() == 0);
case links.Timeline.StepDate.SCALE.SECOND:
return (this.current.getSeconds() == 0);
case links.Timeline.StepDate.SCALE.MINUTE:
return (this.current.getHours() == 0) &&
(this.current.getMinutes() == 0);
// Note: this is no bug. Major label is equal for both minute
and hour scale
case links.Timeline.StepDate.SCALE.HOUR:
return (this.current.getHours() == 0);
case links.Timeline.StepDate.SCALE.DAY:
return (this.current.getDate() == 1);
case links.Timeline.StepDate.SCALE.MONTH:
return (this.current.getMonth() == 0);
case links.Timeline.StepDate.SCALE.YEAR:
return false
default:
return false;
}
}


/**
* Returns formatted text for the minor axislabel, depending on the
current
* date and the scale. For example when scale is MINUTE, the current
time is
* formatted as "hh:mm".
* @param {Date} optional custom date. if not provided, current
date is taken
* @return {string} minor axislabel
*/
links.Timeline.StepDate.prototype.getLabelMinor = function(date) {
var MONTHS_SHORT = new Array("Jan", "Feb", "Mar",
"Apr", "May", "Jun",
"Jul", "Aug", "Sep",
"Oct", "Nov", "Dec");

if (date == undefined) {
date = this.current;
}

switch (this.scale)
{
case links.Timeline.StepDate.SCALE.MILLISECOND: return
String(date.getMilliseconds());
case links.Timeline.StepDate.SCALE.SECOND: return
String(date.getSeconds());
case links.Timeline.StepDate.SCALE.MINUTE: return
this.addZeros(date.getHours(), 2) + ":" +

this.addZeros(date.getMinutes(), 2);
case links.Timeline.StepDate.SCALE.HOUR: return
this.addZeros(date.getHours(), 2) + ":" +

this.addZeros(date.getMinutes(), 2);
case links.Timeline.StepDate.SCALE.DAY: return
String(date.getDate());
case links.Timeline.StepDate.SCALE.MONTH: return
MONTHS_SHORT[date.getMonth()]; // month is zero based
case links.Timeline.StepDate.SCALE.YEAR: return
String(date.getFullYear());
default: return "";
}
}


/**
* Returns formatted text for the major axislabel, depending on the
current
* date and the scale. For example when scale is MINUTE, the major
scale is
* hours, and the hour will be formatted as "hh".
* @param {Date} optional custom date. if not provided, current
date is taken
* @return {string} major axislabel
*/
links.Timeline.StepDate.prototype.getLabelMajor = function(date) {
var MONTHS = new Array("January", "February", "March",
"April", "May", "June",
"July", "August", "September",
"October", "November", "December");
var DAYS = new Array("Sunday", "Monday", "Tuesday",
"Wednesday", "Thursday", "Friday", "Saturday");

if (date == undefined) {
date = this.current;
}

switch (this.scale) {
case links.Timeline.StepDate.SCALE.MILLISECOND:
return this.addZeros(date.getHours(), 2) + ":" +
this.addZeros(date.getMinutes(), 2) + ":" +
this.addZeros(date.getSeconds(), 2);
case links.Timeline.StepDate.SCALE.SECOND:
return date.getDate() + " " +
MONTHS[date.getMonth()] + " " +
this.addZeros(date.getHours(), 2) + ":" +
this.addZeros(date.getMinutes(), 2);
case links.Timeline.StepDate.SCALE.MINUTE:
return DAYS[date.getDay()] + " " +
date.getDate() + " " +
MONTHS[date.getMonth()] + " " +
date.getFullYear();
case links.Timeline.StepDate.SCALE.HOUR:
return DAYS[date.getDay()] + " " +
date.getDate() + " " +
MONTHS[date.getMonth()] + " " +
date.getFullYear();
case links.Timeline.StepDate.SCALE.DAY:
return MONTHS[date.getMonth()] + " " +
date.getFullYear();
case links.Timeline.StepDate.SCALE.MONTH:
return String(date.getFullYear());
default:
return "";
}
}

/**
* Add leading zeros to the given value to match the desired length.
* For example addZeros(123, 5) returns "00123"
* @param {int} value A value
* @param {int} len Desired final length
* @return {string} value with leading zeros
*/
links.Timeline.StepDate.prototype.addZeros = function(value, len) {
var str = "" + value;
while (str.length < len) {
str = "0" + str;
}
return str;
}



/**
------------------------------------------------------------------------
**/

/**
* Image Loader service.
* can be used to get a callback when a certain image is loaded
*
*/
links.imageloader = (function () {
var urls = {}; // the loaded urls
var callbacks = {}; // the urls currently being loaded. Each key
contains
// an array with callbacks

/**
* Check if an image url is loaded
* @param {String} url
* @return {boolean} loaded True when loaded, false when not
loaded
* or when being loaded
*/
function isLoaded (url) {
if (urls[url] == true) {
return true;
}

var image = new Image();
image.src = url;
if (image.complete) {
return true;
}

return false;
};


/**
* Check if an image url is being loaded
* @param {String} url
* @return {boolean} loading True when being loaded, false when
not loading
* or when already loaded
*/
function isLoading (url) {
return (callbacks[url] != undefined);
}

/**
* Load given image url
* @param {String} url
* @param {function} callback
* @param {boolean} sendCallbackWhenAlreadyLoaded optional
*/
function load (url, callback, sendCallbackWhenAlreadyLoaded) {
if (sendCallbackWhenAlreadyLoaded == undefined) {
sendCallbackWhenAlreadyLoaded = true;
}

if (isLoaded(url)) {
if (sendCallbackWhenAlreadyLoaded) {
callback(url);
}
return;
}

if (isLoading(url) && !sendCallbackWhenAlreadyLoaded) {
return;
}

var c = callbacks[url];
if (!c) {
var image = new Image();
image.src = url;

c = [];
callbacks[url] = c;

image.onload = function (event) {
urls[url] = true;
delete callbacks[url];

for (var i = 0; i < c.length; i++) {
c[i](url);
}
}
}

if (c.indexOf(callback) == -1) {
c.push(callback);
}
};

return {
'isLoaded': isLoaded,
'isLoading': isLoading,
'load': load
};
})();


/**
------------------------------------------------------------------------
**/


/**
* Add and event listener. Works for all browsers
* @param {DOM Element} element An html element
* @param {string} action The action, for example "click",
* without the prefix "on"
* @param {function} listener The callback function to be
executed
* @param {boolean} useCapture
*/
links.Timeline.addEventListener = function (element, action, listener,
useCapture) {
if (element.addEventListener) {
if (useCapture === undefined)
useCapture = false;

if (action === "mousewheel" &&
navigator.userAgent.indexOf("Firefox") >= 0) {
action = "DOMMouseScroll"; // For Firefox
}

element.addEventListener(action, listener, useCapture);
} else {
element.attachEvent("on" + action, listener); // IE browsers
}
};

/**
* Remove an event listener from an element
* @param {DOM element} element An html dom element
* @param {string} action The name of the event, for example
"mousedown"
* @param {function} listener The listener function
* @param {boolean} useCapture
*/
links.Timeline.removeEventListener = function(element, action,
listener, useCapture) {
if (element.removeEventListener) {
// non-IE browsers
if (useCapture === undefined)
useCapture = false;

if (action === "mousewheel" &&
navigator.userAgent.indexOf("Firefox") >= 0) {
action = "DOMMouseScroll"; // For Firefox
}

element.removeEventListener(action, listener, useCapture);
} else {
// IE browsers
element.detachEvent("on" + action, listener);
}
};


/**
* Get HTML element which is the target of the event
* @param {MouseEvent} event
* @return {HTML DOM} target element
*/
links.Timeline.getTarget = function (event) {
// code from http://www.quirksmode.org/js/events_properties.html
if (!event) {
var event = window.event;
}

var target;

if (event.target) {
target = event.target;
}
else if (event.srcElement) {
target = event.srcElement;
}

if (target.nodeType !== undefined && target.nodeType == 3) {
// defeat Safari bug
target = target.parentNode;
}

return target;
}

/**
* Stop event propagation
*/
links.Timeline.stopPropagation = function (event) {
if (!event)
var event = window.event;

if (event.stopPropagation) {
event.stopPropagation(); // non-IE browsers
}
else {
event.cancelBubble = true; // IE browsers
}
}


/**
* Cancels the event if it is cancelable, without stopping further
propagation of the event.
*/
links.Timeline.preventDefault = function (event) {
if (!event)
var event = window.event;

if (event.preventDefault) {
event.preventDefault(); // non-IE browsers
}
else {
event.returnValue = false; // IE browsers
}
}


/**
* Retrieve the absolute left value of a DOM element
* @param {DOM element} elem A dom element, for example a div
* @return {number} left The absolute left position of this
element
* in the browser page.
*/
links.Timeline.getAbsoluteLeft = function(elem)
{
var left = 0;
while( elem != null ) {
left += elem.offsetLeft;
left -= elem.scrollLeft;
elem = elem.offsetParent;
}
if (!document.body.scrollLeft && window.pageXOffset) {
// FF
left -= window.pageXOffset;
}
return left;
}

/**
* Retrieve the absolute top value of a DOM element
* @param {DOM element} elem A dom element, for example a div
* @return {number} top The absolute top position of this
element
* in the browser page.
*/
links.Timeline.getAbsoluteTop = function(elem)
{
var top = 0;
while( elem != null ) {
top += elem.offsetTop;
top -= elem.scrollTop;
elem = elem.offsetParent;
}
if (!document.body.scrollTop && window.pageYOffset) {
// FF
top -= window.pageYOffset;
}
return top;
}

/**
* Check if given object is a Javascript Array
* @param {any type} obj
* @return {Boolean} isArray true if the given object is an array
*/
// See http://stackoverflow.com/questions/2943805/javascript-instanceof-typeof-in-gwt-jsni
links.Timeline.isArray = function (obj) {
if (obj instanceof Array) {
return true;
}
return (Object.prototype.toString.call(obj) === '[object Array]');
}

var timeline;
var data;

// Called when the Visualization API is loaded.
config.macros.drawVisualization = {};
config.macros.drawVisualization.handler = function
(place,macroName,params,wikifier,paramString,tiddler){
// function drawVisualization() {
// Create a JSON data table
data = [
/* {
'start': new Date(2010,7,23),
'content': 'Conversation<br><img src="collateral/timeline/
img/comments-icon.png" style="width:32px; height:32px;">'
},
{
'start': new Date(2010,7,23,23,0,0),
'content': 'Mail from boss<br><img src="collateral/
timeline/img/mail-icon.png" style="width:32px; height:32px;">'
},
{
'start': new Date(2010,7,24,16,0,0),
'content': 'Report'
},
{
'start': new Date(2010,7,26),
'end': new Date(2010,8,2),
'content': 'Traject A'
},
{
'start': new Date(2010,7,28),
'content': 'Memo<br><img src="collateral/timeline/img/
notes-edit-icon.png" style="width:48px; height:48px;">'
},
{
'start': new Date(2010,7,29),
'content': 'Phone call<br><img src="collateral/timeline/
img/Hardware-Mobile-Phone-icon.png" style="width:32px; height:32px;">'
},
{
'start': new Date(2010,7,31),
'end': new Date(2010,8,3),
'content': 'Traject B'
},
{
'start': new Date(2010,8,4,12,0,0),
'content': 'Report<br><img src="collateral/timeline/img/
attachment-icon.png" style="width:32px; height:32px;">'
}
*/
{
'start': new Date(2010,7,31),
'end': new Date(2010,8,3),
'content': 'Traject B'
},

];

// specify options
var options = {
'width': '100%',
'height': '300px',
'editable': true, // enable dragging and editing events
'style': 'box'
};

// Instantiate our timeline object.
timeline = new
links.Timeline(document.getElementById('mytimeline'));

function onRangeChanged(properties) {
document.getElementById('info').innerHTML += 'rangechanged '
+
properties.start + ' - ' + properties.end + '<br>';
}

// attach an event listener using the links events handler
links.events.addListener(timeline, 'rangechanged',
onRangeChanged);

// Draw our timeline with the created data and options
timeline.draw(data, options);
}


//}}}

**********************END CHAPTimelinePlugin***************************

Eric Shulman

unread,
Apr 18, 2012, 9:36:12 PM4/18/12
to TiddlyWiki


On Apr 18, 6:10 pm, "ant...@bellingen.nsw.gov.au"
<ant...@bellingen.nsw.gov.au> wrote:
> Hopefully I'm not breaking any of the groups rules of etiquette by
> posting the full plugin & CSS here.  Apologies if so.

You haven't brokent any "rule", or even group etiquette, so no
worries!

However... posting code in GoogleGroups is not really all that useful,
because GoogleGroups word-wraps the message text... which often
results in mangled code caused when quoted text values are split
across lines. In addition, it much harder to provide any significant
debugging help, since it means having to copy/paste the code from the
message into several different tiddlers (i.e, CSS and Plugin), and
then hand-edit the PageTemplate to add the needed syntax there.

The best way to get feedback on your development work is to post your
complete TW document online (minus any personal information it may
contain, of course). In addition, if you are looking for help with a
specific problem, you should try to create a "minimal test case" (MTC)
document and post that. Typically, an MTC would include several
examples of the problem, as well as related notes and other
documentation. This let's others see the problem in a live setting so
they can more easily understand the issues involved and hopefully find
a solution, or at least offer clues that may help you solve the
problem.

Note: if you don't have your own hosting space, http://www.tiddlyspot.com
is a good place for free, instant TW hosting.

enjoy,
-e
Eric Shulman
TiddlyTools / ELS Design Studios

----
WAS THIS ANSWER HELPFUL? IF SO, PLEASE MAKE A DONATION
http://www.TiddlyTools.com/#Donations
note: donations are directly used to pay for food, rent,
gas, net connection, etc., so please give generously and often!

Professional TiddlyWiki Consulting Services...
Analysis, Design, and Custom Solutions:
http://www.TiddlyTools.com/#Contact

ant...@bellingen.nsw.gov.au

unread,
Apr 19, 2012, 9:33:07 PM4/19/12
to TiddlyWiki
Thanks Eric,

I've set up a tiddlyspot at http://chaptimeline.tiddlyspot.com.

I'm now getting data out of tiddlers.

The placement of the timeline seems to be always at the top of the
tiddler though.


Regards
Anton.

Måns

unread,
Apr 22, 2012, 12:02:25 PM4/22/12
to TiddlyWiki
Hi Anton

> I've set up a tiddlyspot athttp://chaptimeline.tiddlyspot.com.

Great - thanks.

> I'm now getting data out of tiddlers.
> The placement of the timeline seems to be always at the top of the tiddler though.

And no other text from the same tiddler is getting wikified - which is
a pity..

The chap timeline seems to be a great addition to the bag of tools
available for TiddlyWiki.
I don't like the dependency on DataTiddlerPlugin though - would it be
possible to refactor it to use simple slice value pairs - (or a
simple twocolumn wikitable) just like references to the ColorPalette -
from a StyleSheet?
On the other hand it would be very useful if it could reference
tiddlers directly on the timeline - in a interactive way. Click on the
timeline and open a tiddler...
Tiddlers tagged with a predefined tag shows up on the timeline -
slices in tiddlers defines where on the timeline it should show up -
another slice of text is rendered inside the timeline...

Thanks for sharing - and well done!
I'd love to have a simple timeline available for TiddlyWiki - please
continue making chaptimeline for TW even better :-)
I'm a teacher and I would use it for sheduling, litterature and art
history - so the ability of rendering images would also be very
welcome ...
I see a lot of potential uses for it in education..

Cheers Måns Mårtenssin

Tobias Beer

unread,
Apr 25, 2012, 2:23:47 AM4/25/12
to tiddl...@googlegroups.com
Is there a way to use the mousewheel with chaptimeline where the page wouldn't scroll as well? Or would that be a bug of my browser?

- tb

ant...@bellingen.nsw.gov.au

unread,
Apr 25, 2012, 8:49:28 PM4/25/12
to TiddlyWiki
> And no other text from the same tiddler is getting wikified - which is
> a pity..
>

Try placing your text after the macro. Seems to work OK for me. The
main issue seems to be that the timeline sits over the top of any
other text.

> I don't like the dependency on DataTiddlerPlugin though - would it be
> possible to refactor it to use simple slice value pairs -  (or a
> simple twocolumn wikitable) just like references to the ColorPalette -
> from a StyleSheet?

If someone can give me the javascript to get the data out of the
tiddler, I could probably try to incorporate it in. It's a bit beyond
my skills at the moment though.

> On the other hand it would be very useful if it could reference
> tiddlers directly on the timeline - in a interactive way. Click on the
> timeline and open a tiddler...
> Tiddlers tagged with a predefined tag shows up on the timeline -
> slices in tiddlers defines where on the timeline it should show up -
> another slice of text is rendered inside the timeline...
>
> Thanks for sharing - and well done!

Thanks

> I'd love to have a simple timeline available for TiddlyWiki - please
> continue making chaptimeline for TW even better :-)
> I'm a teacher and I would use it for sheduling, litterature and art
> history - so the ability of rendering images would also be very
> welcome ...
> I see a lot of potential uses for it in education..
>
> Cheers Måns Mårtenssin

Unfortunately my main problem is that I'm very time poor, and
javascript is a new language for me. Feel free to modify and improve
though.

ant...@bellingen.nsw.gov.au

unread,
Apr 25, 2012, 8:50:06 PM4/25/12
to TiddlyWiki
I think it may be something to do with the CSS. That doesn't happen
on the demo pages for the CHAP Timeline (non TW). Anyone got any
ideas?

ant...@bellingen.nsw.gov.au

unread,
May 29, 2012, 11:26:58 PM5/29/12
to TiddlyWiki
I've updated the CHAP Timeline plugin so that it is no longer
dependant on the DataTiddler plugin (it now uses slices), and also put
a demonstration on the site of using ForEachTiddler Plugin to
dynamically create bars. It allows you to use your own custom slices,
create multiple bars from a single tiddler etc... A little bit of
javascripting is necessary to achieve this.

http://chaptimeline.tiddlyspot.com/

Måns

unread,
May 30, 2012, 9:30:49 AM5/30/12
to TiddlyWiki
Hi Anton
VERY nice - Good job!!

I also like your fET-example very much :-D
GREAT to have a nice timeline for TiddlyWiki which uses native
syntax!!

Thank you very much!!

Cheers Måns Mårtensson

Alex Hough

unread,
May 31, 2012, 6:05:29 AM5/31/12
to tiddl...@googlegroups.com
A suggestion for a feature: to be able open tiddlers by clicking on
tiddlylinks in the timeline...

Alex
> --
> You received this message because you are subscribed to the Google Groups "TiddlyWiki" group.
> To post to this group, send email to tiddl...@googlegroups.com.
> To unsubscribe from this group, send email to tiddlywiki+...@googlegroups.com.
> For more options, visit this group at http://groups.google.com/group/tiddlywiki?hl=en.
>

Anton

unread,
May 31, 2012, 11:53:25 PM5/31/12
to TiddlyWiki
All,

I've looked at this and the CHAP Links library has an edit event that
fires if the user double clicks on a bar. The problems I need help
with are:

1) The double click event seems to be getting grabbed by either the
edit Tiddler function or in my case the back function of breadcrumbs.
Anyone know how to raise the priority of my double click event so that
if the user double clicks on the timeline box it get's handled by my
code, but if they double click outside of the timeline box the usual
behaviour occurs?

2) Ditto for mouse wheel. It's kind of annoying having the page going
up and down when you zoom on the timeline.

3) Assuming I can solve 1), I still need to work out which tiddler to
open. I think I can retrieve the content of the bar which would look
something like:
"<div style=\"background-color:LightBlue; border:1px solid
Black;padding:0px;\">Extension z<br><img src=\"collateral/pictures/
question.png\"></div>"
Is there some html'y way to hide the tiddler name in this string
without it being visible to the user?

Thanks

On May 31, 8:05 pm, Alex Hough <r.a.ho...@gmail.com> wrote:
> A suggestion for a feature: to be able open tiddlers by clicking on
> tiddlylinks in the timeline...
>
> Alex
>

Anton

unread,
May 31, 2012, 11:57:59 PM5/31/12
to TiddlyWiki
PS, It now supports single date events and pictures.

Anton

unread,
Jun 4, 2012, 3:59:45 AM6/4/12
to TiddlyWiki
Now supports clicking on a bar to open a tiddler.

I can't seem to get it to focus on the opened tiddler though.

amir

unread,
Oct 5, 2012, 12:28:05 PM10/5/12
to tiddl...@googlegroups.com
Hey Anton,

That is really great, it allows for great data visualization. How do you think I can get it to focus on a tiddler after clicking on the corresponding box in the timeline ?

Amir

Chris

unread,
Feb 4, 2014, 11:17:59 PM2/4/14
to tiddl...@googlegroups.com
I like the simplicity, but it does not render properly in IE. Does anybody have suggestions for making this compatible with IE browsers. Right now it just defaults to today's date.


On Monday, June 4, 2012 3:59:45 AM UTC-4, Anton wrote:

Danielo Rodríguez

unread,
Feb 5, 2014, 4:57:50 AM2/5/14
to tiddl...@googlegroups.com
Is this for TWC or TW5? Wich is the current status? Is this conversation the only place where I can grab it from?
Thanks in advance

Eric Shulman

unread,
Feb 5, 2014, 12:37:33 PM2/5/14
to tiddl...@googlegroups.com
On Wednesday, February 5, 2014 1:57:50 AM UTC-8, Danielo Rodríguez wrote:
Is this for TWC or TW5? 

Posts going back to 2012 and earlier are referring to TWC.  TW5 didn't really appear on the scene until last year.

-e


 
Reply all
Reply to author
Forward
0 new messages