<!--{{{-->
<link rel='alternate' type='application/rss+xml' title='RSS' href='index.xml' />
<!--}}}-->
Background: #fff
Foreground: #000
PrimaryPale: #8cf
PrimaryLight: #18f
PrimaryMid: #04b
PrimaryDark: #014
SecondaryPale: #ffc
SecondaryLight: #fe8
SecondaryMid: #db4
SecondaryDark: #841
TertiaryPale: #eee
TertiaryLight: #ccc
TertiaryMid: #999
TertiaryDark: #666
Error: #f88
/*{{{*/
body {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}

a {color:[[ColorPalette::PrimaryMid]];}
a:hover {background-color:[[ColorPalette::PrimaryMid]]; color:[[ColorPalette::Background]];}
a img {border:0;}

h1,h2,h3,h4,h5,h6 {color:[[ColorPalette::SecondaryDark]]; background:transparent;}
h1 {border-bottom:2px solid [[ColorPalette::TertiaryLight]];}
h2,h3 {border-bottom:1px solid [[ColorPalette::TertiaryLight]];}

.button {color:[[ColorPalette::PrimaryDark]]; border:1px solid [[ColorPalette::Background]];}
.button:hover {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::SecondaryLight]]; border-color:[[ColorPalette::SecondaryMid]];}
.button:active {color:[[ColorPalette::Background]]; background:[[ColorPalette::SecondaryMid]]; border:1px solid [[ColorPalette::SecondaryDark]];}

.header {background:[[ColorPalette::PrimaryMid]];}
.headerShadow {color:[[ColorPalette::Foreground]];}
.headerShadow a {font-weight:normal; color:[[ColorPalette::Foreground]];}
.headerForeground {color:[[ColorPalette::Background]];}
.headerForeground a {font-weight:normal; color:[[ColorPalette::PrimaryPale]];}

.tabSelected {color:[[ColorPalette::PrimaryDark]];
	background:[[ColorPalette::TertiaryPale]];
	border-left:1px solid [[ColorPalette::TertiaryLight]];
	border-top:1px solid [[ColorPalette::TertiaryLight]];
	border-right:1px solid [[ColorPalette::TertiaryLight]];
}
.tabUnselected {color:[[ColorPalette::Background]]; background:[[ColorPalette::TertiaryMid]];}
.tabContents {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::TertiaryPale]]; border:1px solid [[ColorPalette::TertiaryLight]];}
.tabContents .button {border:0;}

#sidebar {}
#sidebarOptions input {border:1px solid [[ColorPalette::PrimaryMid]];}
#sidebarOptions .sliderPanel {background:[[ColorPalette::PrimaryPale]];}
#sidebarOptions .sliderPanel a {border:none;color:[[ColorPalette::PrimaryMid]];}
#sidebarOptions .sliderPanel a:hover {color:[[ColorPalette::Background]]; background:[[ColorPalette::PrimaryMid]];}
#sidebarOptions .sliderPanel a:active {color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::Background]];}

.wizard {background:[[ColorPalette::PrimaryPale]]; border:1px solid [[ColorPalette::PrimaryMid]];}
.wizard h1 {color:[[ColorPalette::PrimaryDark]]; border:none;}
.wizard h2 {color:[[ColorPalette::Foreground]]; border:none;}
.wizardStep {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];
	border:1px solid [[ColorPalette::PrimaryMid]];}
.wizardStep.wizardStepDone {background:[[ColorPalette::TertiaryLight]];}
.wizardFooter {background:[[ColorPalette::PrimaryPale]];}
.wizardFooter .status {background:[[ColorPalette::PrimaryDark]]; color:[[ColorPalette::Background]];}
.wizard .button {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryLight]]; border: 1px solid;
	border-color:[[ColorPalette::SecondaryPale]] [[ColorPalette::SecondaryDark]] [[ColorPalette::SecondaryDark]] [[ColorPalette::SecondaryPale]];}
.wizard .button:hover {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Background]];}
.wizard .button:active {color:[[ColorPalette::Background]]; background:[[ColorPalette::Foreground]]; border: 1px solid;
	border-color:[[ColorPalette::PrimaryDark]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryDark]];}

.wizard .notChanged {background:transparent;}
.wizard .changedLocally {background:#80ff80;}
.wizard .changedServer {background:#8080ff;}
.wizard .changedBoth {background:#ff8080;}
.wizard .notFound {background:#ffff80;}
.wizard .putToServer {background:#ff80ff;}
.wizard .gotFromServer {background:#80ffff;}

#messageArea {border:1px solid [[ColorPalette::SecondaryMid]]; background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]];}
#messageArea .button {color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::SecondaryPale]]; border:none;}

.popupTiddler {background:[[ColorPalette::TertiaryPale]]; border:2px solid [[ColorPalette::TertiaryMid]];}

.popup {background:[[ColorPalette::TertiaryPale]]; color:[[ColorPalette::TertiaryDark]]; border-left:1px solid [[ColorPalette::TertiaryMid]]; border-top:1px solid [[ColorPalette::TertiaryMid]]; border-right:2px solid [[ColorPalette::TertiaryDark]]; border-bottom:2px solid [[ColorPalette::TertiaryDark]];}
.popup hr {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::PrimaryDark]]; border-bottom:1px;}
.popup li.disabled {color:[[ColorPalette::TertiaryMid]];}
.popup li a, .popup li a:visited {color:[[ColorPalette::Foreground]]; border: none;}
.popup li a:hover {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; border: none;}
.popup li a:active {background:[[ColorPalette::SecondaryPale]]; color:[[ColorPalette::Foreground]]; border: none;}
.popupHighlight {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
.listBreak div {border-bottom:1px solid [[ColorPalette::TertiaryDark]];}

.tiddler .defaultCommand {font-weight:bold;}

.shadow .title {color:[[ColorPalette::TertiaryDark]];}

.title {color:[[ColorPalette::SecondaryDark]];}
.subtitle {color:[[ColorPalette::TertiaryDark]];}

.toolbar {color:[[ColorPalette::PrimaryMid]];}
.toolbar a {color:[[ColorPalette::TertiaryLight]];}
.selected .toolbar a {color:[[ColorPalette::TertiaryMid]];}
.selected .toolbar a:hover {color:[[ColorPalette::Foreground]];}

.tagging, .tagged {border:1px solid [[ColorPalette::TertiaryPale]]; background-color:[[ColorPalette::TertiaryPale]];}
.selected .tagging, .selected .tagged {background-color:[[ColorPalette::TertiaryLight]]; border:1px solid [[ColorPalette::TertiaryMid]];}
.tagging .listTitle, .tagged .listTitle {color:[[ColorPalette::PrimaryDark]];}
.tagging .button, .tagged .button {border:none;}

.footer {color:[[ColorPalette::TertiaryLight]];}
.selected .footer {color:[[ColorPalette::TertiaryMid]];}

.error, .errorButton {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Error]];}
.warning {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryPale]];}
.lowlight {background:[[ColorPalette::TertiaryLight]];}

.zoomer {background:none; color:[[ColorPalette::TertiaryMid]]; border:3px solid [[ColorPalette::TertiaryMid]];}

.imageLink, #displayArea .imageLink {background:transparent;}

.annotation {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; border:2px solid [[ColorPalette::SecondaryMid]];}

.viewer .listTitle {list-style-type:none; margin-left:-2em;}
.viewer .button {border:1px solid [[ColorPalette::SecondaryMid]];}
.viewer blockquote {border-left:3px solid [[ColorPalette::TertiaryDark]];}

.viewer table, table.twtable {border:2px solid [[ColorPalette::TertiaryDark]];}
.viewer th, .viewer thead td, .twtable th, .twtable thead td {background:[[ColorPalette::SecondaryMid]]; border:1px solid [[ColorPalette::TertiaryDark]]; color:[[ColorPalette::Background]];}
.viewer td, .viewer tr, .twtable td, .twtable tr {border:1px solid [[ColorPalette::TertiaryDark]];}

.viewer pre {border:1px solid [[ColorPalette::SecondaryLight]]; background:[[ColorPalette::SecondaryPale]];}
.viewer code {color:[[ColorPalette::SecondaryDark]];}
.viewer hr {border:0; border-top:dashed 1px [[ColorPalette::TertiaryDark]]; color:[[ColorPalette::TertiaryDark]];}

.highlight, .marked {background:[[ColorPalette::SecondaryLight]];}

.editor input {border:1px solid [[ColorPalette::PrimaryMid]];}
.editor textarea {border:1px solid [[ColorPalette::PrimaryMid]]; width:100%;}
.editorFooter {color:[[ColorPalette::TertiaryMid]];}
.readOnly {background:[[ColorPalette::TertiaryPale]];}

#backstageArea {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::TertiaryMid]];}
#backstageArea a {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
#backstageArea a:hover {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; }
#backstageArea a.backstageSelTab {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
#backstageButton a {background:none; color:[[ColorPalette::Background]]; border:none;}
#backstageButton a:hover {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
#backstagePanel {background:[[ColorPalette::Background]]; border-color: [[ColorPalette::Background]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]];}
.backstagePanelFooter .button {border:none; color:[[ColorPalette::Background]];}
.backstagePanelFooter .button:hover {color:[[ColorPalette::Foreground]];}
#backstageCloak {background:[[ColorPalette::Foreground]]; opacity:0.6; filter:alpha(opacity=60);}
/*}}}*/
/*{{{*/
* html .tiddler {height:1%;}

body {font-size:.75em; font-family:arial,helvetica; margin:0; padding:0;}

h1,h2,h3,h4,h5,h6 {font-weight:bold; text-decoration:none;}
h1,h2,h3 {padding-bottom:1px; margin-top:1.2em;margin-bottom:0.3em;}
h4,h5,h6 {margin-top:1em;}
h1 {font-size:1.35em;}
h2 {font-size:1.25em;}
h3 {font-size:1.1em;}
h4 {font-size:1em;}
h5 {font-size:.9em;}

hr {height:1px;}

a {text-decoration:none;}

dt {font-weight:bold;}

ol {list-style-type:decimal;}
ol ol {list-style-type:lower-alpha;}
ol ol ol {list-style-type:lower-roman;}
ol ol ol ol {list-style-type:decimal;}
ol ol ol ol ol {list-style-type:lower-alpha;}
ol ol ol ol ol ol {list-style-type:lower-roman;}
ol ol ol ol ol ol ol {list-style-type:decimal;}

.txtOptionInput {width:11em;}

#contentWrapper .chkOptionInput {border:0;}

.externalLink {text-decoration:underline;}

.indent {margin-left:3em;}
.outdent {margin-left:3em; text-indent:-3em;}
code.escaped {white-space:nowrap;}

.tiddlyLinkExisting {font-weight:bold;}
.tiddlyLinkNonExisting {font-style:italic;}

/* the 'a' is required for IE, otherwise it renders the whole tiddler in bold */
a.tiddlyLinkNonExisting.shadow {font-weight:bold;}

#mainMenu .tiddlyLinkExisting,
	#mainMenu .tiddlyLinkNonExisting,
	#sidebarTabs .tiddlyLinkNonExisting {font-weight:normal; font-style:normal;}
#sidebarTabs .tiddlyLinkExisting {font-weight:bold; font-style:normal;}

.header {position:relative;}
.header a:hover {background:transparent;}
.headerShadow {position:relative; padding:4.5em 0 1em 1em; left:-1px; top:-1px;}
.headerForeground {position:absolute; padding:4.5em 0 1em 1em; left:0; top:0;}

.siteTitle {font-size:3em;}
.siteSubtitle {font-size:1.2em;}

#mainMenu {position:absolute; left:0; width:10em; text-align:right; line-height:1.6em; padding:1.5em 0.5em 0.5em 0.5em; font-size:1.1em;}

#sidebar {position:absolute; right:3px; width:16em; font-size:.9em;}
#sidebarOptions {padding-top:0.3em;}
#sidebarOptions a {margin:0 0.2em; padding:0.2em 0.3em; display:block;}
#sidebarOptions input {margin:0.4em 0.5em;}
#sidebarOptions .sliderPanel {margin-left:1em; padding:0.5em; font-size:.85em;}
#sidebarOptions .sliderPanel a {font-weight:bold; display:inline; padding:0;}
#sidebarOptions .sliderPanel input {margin:0 0 0.3em 0;}
#sidebarTabs .tabContents {width:15em; overflow:hidden;}

.wizard {padding:0.1em 1em 0 2em;}
.wizard h1 {font-size:2em; font-weight:bold; background:none; padding:0; margin:0.4em 0 0.2em;}
.wizard h2 {font-size:1.2em; font-weight:bold; background:none; padding:0; margin:0.4em 0 0.2em;}
.wizardStep {padding:1em 1em 1em 1em;}
.wizard .button {margin:0.5em 0 0; font-size:1.2em;}
.wizardFooter {padding:0.8em 0.4em 0.8em 0;}
.wizardFooter .status {padding:0 0.4em; margin-left:1em;}
.wizard .button {padding:0.1em 0.2em;}

#messageArea {position:fixed; top:2em; right:0; margin:0.5em; padding:0.5em; z-index:2000; _position:absolute;}
.messageToolbar {display:block; text-align:right; padding:0.2em;}
#messageArea a {text-decoration:underline;}

.tiddlerPopupButton {padding:0.2em;}
.popupTiddler {position: absolute; z-index:300; padding:1em; margin:0;}

.popup {position:absolute; z-index:300; font-size:.9em; padding:0; list-style:none; margin:0;}
.popup .popupMessage {padding:0.4em;}
.popup hr {display:block; height:1px; width:auto; padding:0; margin:0.2em 0;}
.popup li.disabled {padding:0.4em;}
.popup li a {display:block; padding:0.4em; font-weight:normal; cursor:pointer;}
.listBreak {font-size:1px; line-height:1px;}
.listBreak div {margin:2px 0;}

.tabset {padding:1em 0 0 0.5em;}
.tab {margin:0 0 0 0.25em; padding:2px;}
.tabContents {padding:0.5em;}
.tabContents ul, .tabContents ol {margin:0; padding:0;}
.txtMainTab .tabContents li {list-style:none;}
.tabContents li.listLink { margin-left:.75em;}

#contentWrapper {display:block;}
#splashScreen {display:none;}

#displayArea {margin:1em 17em 0 14em;}

.toolbar {text-align:right; font-size:.9em;}

.tiddler {padding:1em 1em 0;}

.missing .viewer,.missing .title {font-style:italic;}

.title {font-size:1.6em; font-weight:bold;}

.missing .subtitle {display:none;}
.subtitle {font-size:1.1em;}

.tiddler .button {padding:0.2em 0.4em;}

.tagging {margin:0.5em 0.5em 0.5em 0; float:left; display:none;}
.isTag .tagging {display:block;}
.tagged {margin:0.5em; float:right;}
.tagging, .tagged {font-size:0.9em; padding:0.25em;}
.tagging ul, .tagged ul {list-style:none; margin:0.25em; padding:0;}
.tagClear {clear:both;}

.footer {font-size:.9em;}
.footer li {display:inline;}

.annotation {padding:0.5em; margin:0.5em;}

* html .viewer pre {width:99%; padding:0 0 1em 0;}
.viewer {line-height:1.4em; padding-top:0.5em;}
.viewer .button {margin:0 0.25em; padding:0 0.25em;}
.viewer blockquote {line-height:1.5em; padding-left:0.8em;margin-left:2.5em;}
.viewer ul, .viewer ol {margin-left:0.5em; padding-left:1.5em;}

.viewer table, table.twtable {border-collapse:collapse; margin:0.8em 1.0em;}
.viewer th, .viewer td, .viewer tr,.viewer caption,.twtable th, .twtable td, .twtable tr,.twtable caption {padding:3px;}
table.listView {font-size:0.85em; margin:0.8em 1.0em;}
table.listView th, table.listView td, table.listView tr {padding:0 3px 0 3px;}

.viewer pre {padding:0.5em; margin-left:0.5em; font-size:1.2em; line-height:1.4em; overflow:auto;}
.viewer code {font-size:1.2em; line-height:1.4em;}

.editor {font-size:1.1em;}
.editor input, .editor textarea {display:block; width:100%; font:inherit;}
.editorFooter {padding:0.25em 0; font-size:.9em;}
.editorFooter .button {padding-top:0; padding-bottom:0;}

.fieldsetFix {border:0; padding:0; margin:1px 0px;}

.zoomer {font-size:1.1em; position:absolute; overflow:hidden;}
.zoomer div {padding:1em;}

* html #backstage {width:99%;}
* html #backstageArea {width:99%;}
#backstageArea {display:none; position:relative; overflow: hidden; z-index:150; padding:0.3em 0.5em;}
#backstageToolbar {position:relative;}
#backstageArea a {font-weight:bold; margin-left:0.5em; padding:0.3em 0.5em;}
#backstageButton {display:none; position:absolute; z-index:175; top:0; right:0;}
#backstageButton a {padding:0.1em 0.4em; margin:0.1em;}
#backstage {position:relative; width:100%; z-index:50;}
#backstagePanel {display:none; z-index:100; position:absolute; width:90%; margin-left:3em; padding:1em;}
.backstagePanelFooter {padding-top:0.2em; float:right;}
.backstagePanelFooter a {padding:0.2em 0.4em;}
#backstageCloak {display:none; z-index:20; position:absolute; width:100%; height:100px;}

.whenBackstage {display:none;}
.backstageVisible .whenBackstage {display:block;}
/*}}}*/
/***
StyleSheet for use when a translation requires any css style changes.
This StyleSheet can be used directly by languages such as Chinese, Japanese and Korean which need larger font sizes.
***/
/*{{{*/
body {font-size:0.8em;}
#sidebarOptions {font-size:1.05em;}
#sidebarOptions a {font-style:normal;}
#sidebarOptions .sliderPanel {font-size:0.95em;}
.subtitle {font-size:0.8em;}
.viewer table.listView {font-size:0.95em;}
/*}}}*/
/*{{{*/
@media print {
#mainMenu, #sidebar, #messageArea, .toolbar, #backstageButton, #backstageArea {display: none !important;}
#displayArea {margin: 1em 1em 0em;}
noscript {display:none;} /* Fixes a feature in Firefox 1.5.0.2 where print preview displays the noscript content */
}
/*}}}*/
<!--{{{-->
<div class='header' macro='gradient vert [[ColorPalette::PrimaryLight]] [[ColorPalette::PrimaryMid]]'>
<div class='headerShadow'>
<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>&nbsp;
<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
</div>
<div class='headerForeground'>
<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>&nbsp;
<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
</div>
</div>
<div id='mainMenu' refresh='content' tiddler='MainMenu'></div>
<div id='sidebar'>
<div id='sidebarOptions' refresh='content' tiddler='SideBarOptions'></div>
<div id='sidebarTabs' refresh='content' force='true' tiddler='SideBarTabs'></div>
</div>
<div id='displayArea'>
<div id='messageArea'></div>
<div id='tiddlerDisplay'></div>
</div>
<!--}}}-->
<!--{{{-->
<div class='toolbar' macro='toolbar [[ToolbarCommands::ViewToolbar]]'></div>
<div class='title' macro='view title'></div>
<div class='subtitle'><span macro='view modifier link'></span>, <span macro='view modified date'></span> (<span macro='message views.wikified.createdPrompt'></span> <span macro='view created date'></span>)</div>
<div class='tagging' macro='tagging'></div>
<div class='tagged' macro='tags'></div>
<div class='viewer' macro='view text wikified'></div>
<div class='tagClear'></div>
<!--}}}-->
<!--{{{-->
<div class='toolbar' macro='toolbar [[ToolbarCommands::EditToolbar]]'></div>
<div class='title' macro='view title'></div>
<div class='editor' macro='edit title'></div>
<div macro='annotations'></div>
<div class='editor' macro='edit text'></div>
<div class='editor' macro='edit tags'></div><div class='editorFooter'><span macro='message views.editor.tagPrompt'></span><span macro='tagChooser excludeLists'></span></div>
<!--}}}-->
To get started with this blank [[TiddlyWiki]], you'll need to modify the following tiddlers:
* [[SiteTitle]] & [[SiteSubtitle]]: The title and subtitle of the site, as shown above (after saving, they will also appear in the browser title bar)
* [[MainMenu]]: The menu (usually on the left)
* [[DefaultTiddlers]]: Contains the names of the tiddlers that you want to appear when the TiddlyWiki is opened
You'll also need to enter your username for signing your edits: <<option txtUserName>>
These [[InterfaceOptions]] for customising [[TiddlyWiki]] are saved in your browser

Your username for signing your edits. Write it as a [[WikiWord]] (eg [[JoeBloggs]])

<<option txtUserName>>
<<option chkSaveBackups>> [[SaveBackups]]
<<option chkAutoSave>> [[AutoSave]]
<<option chkRegExpSearch>> [[RegExpSearch]]
<<option chkCaseSensitiveSearch>> [[CaseSensitiveSearch]]
<<option chkAnimate>> [[EnableAnimations]]

----
Also see [[AdvancedOptions]]
<<importTiddlers>>
/***
!!!608/609/610 toolbars - toggles, separators and transclusion
***/
// // {{groupbox small{
/***
http://trac.tiddlywiki.org/ticket/608 - OPEN (more/less toggle)
http://trac.tiddlywiki.org/ticket/609 - OPEN (separators)
http://trac.tiddlywiki.org/ticket/610 - OPEN (wikify tiddler/slice/section content)

This combination tweak extends the """<<toolbar>>""" macro to add use of '<' to insert a 'less' menu command (the opposite of '>' == 'more'), as well as use of '*' to insert linebreaks and "!" to insert a vertical line separator between toolbar items.  In addition, this tweak add the ability to use references to tiddlernames, slices, or sections and render their content inline within the toolbar, allowing easy creation of new toolbar commands using TW content (such as macros, links, inline scripts, etc.)

To produce a one-line style, with "less" at the end, use
| ViewToolbar| foo bar baz > yabba dabba doo < |
or to use a two-line style with more/less toggle:
| ViewToolbar| foo bar baz > < * yabba dabba doo |
***/
//{{{
merge(config.macros.toolbar,{
	moreLabel: ' more\u25BC',
	morePrompt: 'Show extra commands',
	lessLabel: '\u25C4less',
	lessPrompt: 'Hide extra commands',
	separator: '|'
});
config.macros.toolbar.onClickMore = function(ev) {
	var e = this.nextSibling;
	e.style.display = 'inline'; // show menu
	this.style.display = 'none'; // hide button
	return false;
};
config.macros.toolbar.onClickLess = function(ev) {
	var e = this.parentNode;
	var m = e.previousSibling;
	e.style.display = 'none'; // hide menu
	m.style.display = 'inline'; // show button
	return false;
};
config.macros.toolbar.handler = function(place,macroName,params,wikifier,paramString,tiddler) {
	for(var t=0; t<params.length; t++) {
		var c = params[t];
		switch(c) {
			case '!':  // ELS - SEPARATOR (added)
				createTiddlyText(place,this.separator);
				break;
			case '*':  // ELS - LINEBREAK (added)
				createTiddlyElement(place,'BR');
				break;
			case '<': // ELS - LESS COMMAND (added)
				var btn = createTiddlyButton(place,
					this.lessLabel,this.lessPrompt,config.macros.toolbar.onClickLess,'moreCommand');
				break;
			case '>':
				var btn = createTiddlyButton(place,
					this.moreLabel,this.morePrompt,config.macros.toolbar.onClickMore,'moreCommand');
				var e = createTiddlyElement(place,'span',null,'moreCommand');
				e.style.display = 'none';
				place = e;
				break;
			default:
				var theClass = '';
				switch(c.substr(0,1)) {
					case '+':
						theClass = 'defaultCommand';
						c = c.substr(1);
						break;
					case '-':
						theClass = 'cancelCommand';
						c = c.substr(1);
						break;
				}
				if(c in config.commands)

					this.createCommand(place,c,tiddler,theClass);
				else { // ELS - WIKIFY TIDDLER/SLICE/SECTION (added)
					if (c.substr(0,1)=='~') c=c.substr(1); // ignore leading ~
					var txt=store.getTiddlerText(c);
					if (txt) {
						// trim any leading/trailing newlines
						txt=txt.replace(/^\n*/,'').replace(/\n*$/,'');
						// trim PRE format wrapper if any
						txt=txt.replace(/^\{\{\{\n/,'').replace(/\n\}\}\}$/,'');
						// render content into toolbar
						wikify(txt,createTiddlyElement(place,'span'),null,tiddler);
					}
				} // ELS - end WIKIFY CONTENT
				break;
		}
	}
};
//}}}
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: 0px;  
  border-radius: 0px;  
  -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: 0px;
  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('collateral/timeline/img/16/new.png') no-repeat center;
}

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

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

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

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

div.timeline-navigation-move-right {
  background: url(''collateral/timeline/img/16/moveright.png') no-repeat center;
}
/***
|''Name:''|CHAPTimelinePlugin|
|''Version:''|0.2 (2012-04-17)|
|''Author:''|AntonJ, based on code from Almende at http://chap.almende.com/timeline.  All rights are theirs where appropriate.  My bit is licenced under the GPL|
|''Adapted By:''||
|''Type:''|Plugin|
|''Requires:''|DataTiddlerPlugin|
!Description
This Plugin implements the CHAP Links Timeline
!Usage
Just install the plugin and tag with systemConfig. Put the following in the tiddler you wish to contain the timline.
{{{
<<drawVisualization DataTag>>
<html><div id='mytimeline'></div></html>
}}}
where "DataTag" is the tag allocated to tiddlers which contain the data for the timline events.

Create tiddlers to contain your events using the tag you selected above (eg DataTag).  Put the following in the body of the tiddlers:

{{{
<<showData [[Tiddler Name]]>>
<data>{
"startYear":2012,
"startMonth":4,
"startDay":23,
"endYear":2012,
"endMonth":6,
"endDay":30,
"color":"Orchid",
"text":"My task",
"group":"Group 1"}</data>
}}}

Obviously, [[Tiddler Name]] should be changed to match the name of the tiddler.  This is necessary because the Tiddler Data plugin doesn't like tiddler names with spaces when the tiddler name is left out of the tiddler parameters.

Change other values as appropriate.

!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, <jos@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){
        // specify options
        var options = {
          'width':  '100%',
          'height': '300px',
          'editable': false,   // enable dragging and editing events
          'style': 'box',
          'showNavigation': true
        };

        // Instantiate our timeline object.
        timeline = new links.Timeline(place);

        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(data1, options);
//	timeline.addItem({'start': new Date(2012,01,24), 'end': new Date(2012,2,24), 'content': '<div style="background-color:#00FFFF; border:0px solid green;padding:0px;">Possible visibile</div>',             'group': 'T-12-03' });
	params = paramString.parseParams("anon",null,true,false,false);
	var title = getParam(params,"anon","");
	if(title == "" && tiddler instanceof Tiddler)
		title = tiddler.title;
	var sortby = getParam(params,"sortBy",false);
	var tagged = store.getTaggedTiddlers(title,sortby);

	var t;
	var startYear;;
	var startMonth;
	var startDay;
	var startDate;
	var endYear;
	var endMonth;
	var endDay;
	var endDate;
	var CTcolor;
	var CTtext;
	var CTcontent;
	var CTgroup;

	for(t=0; t<tagged.length; t++) {
		startYear = tagged[t].data("startYear");
		startMonth = tagged[t].data("startMonth");
		startDay = tagged[t].data("startDay");
		startDate = new Date();
		startDate.setYear(startYear);
		startDate.setMonth(parseInt(startMonth)-1,parseInt(startDay));
		endYear = tagged[t].data("endYear");
		endMonth = tagged[t].data("endMonth");
		endDay = tagged[t].data("endDay");
		endDate = new Date();
		endDate.setYear(endYear);
		endDate.setMonth(parseInt(endMonth)-1,parseInt(endDay));
		CTcolor = tagged[t].data("color");
		CTtext = tagged[t].data("text");
		CTcontent = '<div style="background-color:' + CTcolor + '; border:0px solid ' + CTcolor + '; padding:0px;">' + CTtext + '</div>';
		CTgroup = tagged[t].data("group");
		timeline.addItem({'start': startDate, 'end': endDate , 'content': CTcontent, 'group': CTgroup});
		endMonth = 1;
		endDate = null;
	}
	timeline.setVisibleChartRangeAuto();

}

//}}}
/***
|''Name:''|DataTiddlerPlugin|
|''Version:''|1.0.7 (2012-04-19)|
|''Summary:''|Enhance your tiddlers with structured data (such as strings, booleans, numbers, or even arrays and compound objects) that can be easily accessed and modified through named fields (in JavaScript code).|
|''Source:''|http://tiddlywiki.abego-software.de/#DataTiddlerPlugin|
|''Twitter:''|[[@abego|https://twitter.com/#!/abego]]|
|''Author:''|UdoBorkowski (ub [at] abego-software [dot] de)|
|''License:''|[[BSD open source license|http://www.abego-software.de/legal/apl-v10.html]]|
!Description
Enhance your tiddlers with structured data (such as strings, booleans, numbers, or even arrays and compound objects) that can be easily accessed and modified through named fields (in JavaScript code).

Such tiddler data can be used in various applications. E.g. you may create tables that collect data from various tiddlers. 

''//Example: "Table with all December Expenses"//''
{{{
<<forEachTiddler
    where
        'tiddler.tags.contains("expense") && tiddler.data("month") == "Dec"'
    write
        '"|[["+tiddler.title+"]]|"+tiddler.data("descr")+"| "+tiddler.data("amount")+"|\n"'
>>
}}}
//(This assumes that expenses are stored in tiddlers tagged with "expense".)//
<<forEachTiddler
    where
        'tiddler.tags.contains("expense") && tiddler.data("month") == "Dec"'
    write
        '"|[["+tiddler.title+"]]|"+tiddler.data("descr")+"| "+tiddler.data("amount")+"|\n"'
>>
For other examples see DataTiddlerExamples.




''Access and Modify Tiddler Data''

You can "attach" data to every tiddler by assigning a JavaScript value (such as a string, boolean, number, or even arrays and compound objects) to named fields. 

These values can be accessed and modified through the following Tiddler methods:
|!Method|!Example|!Description|
|{{{data(field)}}}|{{{t.data("age")}}}|Returns the value of the given data field of the tiddler. When no such field is defined or its value is undefined {{{undefined}}} is returned.|
|{{{data(field,defaultValue)}}}|{{{t.data("isVIP",false)}}}|Returns the value of the given data field of the tiddler. When no such field is defined or its value is undefined the defaultValue is returned.|
|{{{data()}}}|{{{t.data()}}}|Returns the data object of the tiddler, with a property for every field. The properties of the returned data object may only be read and not be modified. To modify the data use DataTiddler.setData(...) or the corresponding Tiddler method.|
|{{{setData(field,value)}}}|{{{t.setData("age",42)}}}|Sets the value of the given data field of the tiddler to the value. When the value is {{{undefined}}} the field is removed.|
|{{{setData(field,value,defaultValue)}}}|{{{t.setData("isVIP",flag,false)}}}|Sets the value of the given data field of the tiddler to the value. When the value is equal to the defaultValue no value is set (and the field is removed).|

Alternatively you may use the following functions to access and modify the data. In this case the tiddler argument is either a tiddler or the name of a tiddler.
|!Method|!Description|
|{{{DataTiddler.getData(tiddler,field)}}}|Returns the value of the given data field of the tiddler. When no such field is defined or its value is undefined {{{undefined}}} is returned.|
|{{{DataTiddler.getData(tiddler,field,defaultValue)}}}|Returns the value of the given data field of the tiddler. When no such field is defined or its value is undefined the defaultValue is returned.|
|{{{DataTiddler.getDataObject(tiddler)}}}|Returns the data object of the tiddler, with a property for every field. The properties of the returned data object may only be read and not be modified. To modify the data use DataTiddler.setData(...) or the corresponding Tiddler method.|
|{{{DataTiddler.setData(tiddler,field,value)}}}|Sets the value of the given data field of the tiddler to the value. When the value is {{{undefined}}} the field is removed.|
|{{{DataTiddler.setData(tiddler,field,value,defaultValue)}}}|Sets the value of the given data field of the tiddler to the value. When the value is equal to the defaultValue no value is set (and the field is removed).|
//(For details on the various functions see the detailed comments in the source code.)//


''Data Representation in a Tiddler''

The data of a tiddler is stored as plain text in the tiddler's content/text, inside a "data" section that is framed by a {{{<data>...</data>}}} block. Inside the data section the information is stored in the [[JSON format|http://www.crockford.com/JSON/index.html]]. 

//''Data Section Example:''//
{{{
<data>{"isVIP":true,"user":"John Brown","age":34}</data>
}}}

The data section is not displayed when viewing the tiddler (see also "The showData Macro").

Beside the data section a tiddler may have all kind of other content.

Typically you will not access the data section text directly but use the methods given above. Nevertheless you may retrieve the text of the data section's content through the {{{DataTiddler.getDataText(tiddler)}}} function.


''Saving Changes''

The "setData" methods respect the "ForceMinorUpdate" and "AutoSave" configuration values. I.e. when "ForceMinorUpdate" is true changing a value using setData will not affect the "modifier" and "modified" attributes. With "AutoSave" set to true every setData will directly save the changes after a setData.


''Notifications''

No notifications are sent when a tiddler's data value is changed through the "setData" methods. 

''Escape Data Section''
In case that you want to use the text {{{<data>}}} or {{{</data>}}} in a tiddler text you must prefix the text with a tilde ('~'). Otherwise it may be wrongly considered as the data section. The tiddler text {{{~<data>}}} is displayed as {{{<data>}}}.


''The showData Macro''

By default the data of a tiddler (that is stored in the {{{<data>...</data>}}} section of the tiddler) is not displayed. If you want to display this data you may used the {{{<<showData ...>>}}} macro:

''Syntax:'' 
|>|{{{<<}}}''showData '' [''JSON''] [//tiddlerName//] {{{>>}}}|
|''JSON''|By default the data is rendered as a table with a "Name" and "Value" column. When defining ''JSON'' the data is rendered in JSON format|
|//tiddlerName//|Defines the tiddler holding the data to be displayed. When no tiddler is given the tiddler containing the showData macro is used. When the tiddler name contains spaces you must quote the name (or use the {{{[[...]]}}} syntax.)|
|>|~~Syntax formatting: Keywords in ''bold'', optional parts in [...]. 'or' means that exactly one of the two alternatives must exist.~~|
!Source Code
***/
/***
This plugin's source code is compressed (and hidden). 
Use this [[link|http://tiddlywiki.abego-software.de/archive/DataTiddlerPlugin/1.0.7/DataTiddlerPlugin-1.0.7-src.js]] to get the readable source code.
***/
///%
if(!version.extensions.DataTiddlerPlugin){version.extensions.DataTiddlerPlugin={major:1,minor:0,revision:7,date:new Date(2012,3,19),type:"plugin",source:"http://tiddlywiki.abego-software.de/#DataTiddlerPlugin"};if(!window.story){window.story=window}if(!TiddlyWiki.prototype.getTiddler){TiddlyWiki.prototype.getTiddler=function(b){var a=this.tiddlers[b];return(a!==undefined&&a instanceof Tiddler)?a:null}}function DataTiddler(){}DataTiddler={stringify:null,parse:null};window.DataTiddler=DataTiddler;DataTiddler.getData=function(c,d,a){var b=(typeof c=="string")?store.getTiddler(c):c;if(!(b instanceof Tiddler)){throw"Tiddler expected. Got "+c}return DataTiddler.getTiddlerDataValue(b,d,a)};DataTiddler.setData=function(c,e,d,a){var b=(typeof c=="string")?store.getTiddler(c):c;if(!(b instanceof Tiddler)){throw"Tiddler expected. Got "+c+"("+b+")"}DataTiddler.setTiddlerDataValue(b,e,d,a)};DataTiddler.getDataObject=function(b){var a=(typeof b=="string")?store.getTiddler(b):b;if(!(a instanceof Tiddler)){throw"Tiddler expected. Got "+b}return DataTiddler.getTiddlerDataObject(a)};DataTiddler.getDataText=function(b){var a=(typeof b=="string")?store.getTiddler(b):b;if(!(a instanceof Tiddler)){throw"Tiddler expected. Got "+b}return DataTiddler.readDataSectionText(a)};DataTiddler.extendJSONError=function(a){if(a.name=="JSONError"){a.toString=function(){return a.name+": "+a.message+" ("+a.text+")"}}return a};DataTiddler.getTiddlerDataObject=function(a){if(a.dataObject===undefined){var b=DataTiddler.readData(a);a.dataObject=(b)?b:{}}return a.dataObject};DataTiddler.getTiddlerDataValue=function(b,d,a){var c=DataTiddler.getTiddlerDataObject(b)[d];return(c===undefined)?a:c};DataTiddler.setTiddlerDataValue=function(c,f,e,a){var d=DataTiddler.getTiddlerDataObject(c);var b=d[f];if(e==a){if(b!==undefined){delete d[f];DataTiddler.save(c)}return}d[f]=e;DataTiddler.save(c)};DataTiddler.readDataSectionText=function(a){var b=DataTiddler.getDataTiddlerMatches(a);if(b===null||!b[2]){return null}return b[2]};DataTiddler.readData=function(b){var c=DataTiddler.readDataSectionText(b);try{return c?DataTiddler.parse(c):null}catch(a){throw DataTiddler.extendJSONError(a)}};DataTiddler.getDataTextOfTiddler=function(a){var b=DataTiddler.getTiddlerDataObject(a);return DataTiddler.stringify(b)};DataTiddler.indexOfNonEscapedText=function(c,a,d){var b=c.indexOf(a,d);while((b>0)&&(c[b-1]=="~")){b=c.indexOf(a,b+1)}return b};DataTiddler.getDataSectionInfo=function(e){var a="<data>{";var f="}</data>";var d=DataTiddler.indexOfNonEscapedText(e,a,0);if(d<0){return null}var c=e.indexOf(f,d);if(c<0){return null}var b;while((b=e.indexOf(f,c+1))>=0){c=b}return{prefixEnd:d,dataStart:d+(a.length)-1,dataEnd:c,suffixStart:c+(f.length)}};DataTiddler.getDataTiddlerMatches=function(a){var f=a.text;var e=DataTiddler.getDataSectionInfo(f);if(!e){return null}var c=f.substr(0,e.prefixEnd);var b=f.substr(e.dataStart,e.dataEnd-e.dataStart+1);var d=f.substr(e.suffixStart);return[f,c,b,d]};DataTiddler.save=function(a){var e=DataTiddler.getDataTiddlerMatches(a);var d;var f;if(e===null){d=a.text;f=""}else{d=e[1];f=e[3]}var b=DataTiddler.getDataTextOfTiddler(a);var c=(b!==null)?d+"<data>"+b+"</data>"+f:d+f;if(c!=a.text){a.isDataTiddlerChange=true;a.set(a.title,c,config.options.txtUserName,config.options.chkForceMinorUpdate?undefined:new Date(),a.tags);delete a.isDataTiddlerChange;store.dirty=true;if(config.options.chkAutoSave){saveChanges()}}};DataTiddler.MyTiddlerChangedFunction=function(){if(this.dataObject&&!this.isDataTiddlerChange){delete this.dataObject}DataTiddler.originalTiddlerChangedFunction.apply(this,arguments)};config.formatters.push({name:"data-escape",match:"~<\\/?data>",handler:function(a){a.outputText(a.output,a.matchStart+1,a.nextMatch)}});config.formatters.push({name:"data",match:"<data>",handler:function(a){var b=DataTiddler.getDataSectionInfo(a.source);if(b&&b.prefixEnd==a.matchStart){a.nextMatch=b.suffixStart}else{a.outputText(a.output,a.matchStart,a.nextMatch)}}});DataTiddler.originalTiddlerChangedFunction=Tiddler.prototype.changed;Tiddler.prototype.changed=DataTiddler.MyTiddlerChangedFunction;Tiddler.prototype.data=function(b,a){return(b)?DataTiddler.getTiddlerDataValue(this,b,a):DataTiddler.getTiddlerDataObject(this)};Tiddler.prototype.setData=function(c,b,a){DataTiddler.setTiddlerDataValue(this,c,b,a)};config.macros.showData={label:"showData",prompt:"Display the values stored in the data section of the tiddler"};config.macros.showData.handler=function(a,g,h){var c=0;var d=false;if((c<h.length)&&h[c]=="JSON"){c++;d=true}var b=story.findContainingTiddler(a).getAttribute("tiddler");if(c<h.length){b=h[c];c++}try{if(d){this.renderDataInJSONFormat(a,b)}else{this.renderDataAsTable(a,b)}}catch(f){this.createErrorElement(a,f)}};config.macros.showData.renderDataInJSONFormat=function(a,b){var c=DataTiddler.getDataText(b);if(c){createTiddlyElement(a,"pre",null,null,c)}};config.macros.showData.renderDataAsTable=function(a,b){var f="|!Name|!Value|\n";var e=DataTiddler.getDataObject(b);if(e){for(var c in e){var d=e[c];f+="|"+c+"|"+DataTiddler.stringify(d)+"|\n"}}wikify(f,a)};config.macros.showData.createErrorElement=function(a,b){var c=(b.description)?b.description:b.toString();return createTiddlyElement(a,"span",null,"showDataError","<<showData ...>>: "+c)};setStylesheet(".showDataError{color: #ffffff;background-color: #880000;}","showData")}var JSON={copyright:"(c)2005 JSON.org",license:"http://www.crockford.com/JSON/license.html",stringify:function(c){var b=[];function f(a){b[b.length]=a}function d(a){var j,h=undefined,e,g;switch(typeof a){case"object":if(a){if(a instanceof Array){f("[");e=b.length;for(h=0;h<a.length;h+=1){g=a[h];if(typeof g!="undefined"&&typeof g!="function"){if(e<b.length){f(",")}d(g)}}f("]");return}else{if(typeof a.toString!="undefined"){f("{");e=b.length;for(h in a){g=a[h];if(a.hasOwnProperty(h)&&typeof g!="undefined"&&typeof g!="function"){if(e<b.length){f(",")}d(h);f(":");d(g)}}return f("}")}}}f("null");return;case"number":f(isFinite(a)?+a:"null");return;case"string":e=a.length;f('"');for(h=0;h<e;h+=1){j=a.charAt(h);if(j>=" "){if(j=="\\"||j=='"'){f("\\")}f(j)}else{switch(j){case"\b":f("\\b");break;case"\f":f("\\f");break;case"\n":f("\\n");break;case"\r":f("\\r");break;case"\t":f("\\t");break;default:j=j.charCodeAt();f("\\u00"+Math.floor(j/16).toString(16)+(j%16).toString(16))}}}f('"');return;case"boolean":f(String(a));return;default:f("null");return}}d(c);return b.join("")},parse:function(text){var p=/^\s*(([,:{}\[\]])|"(\\.|[^\x00-\x1f"\\])*"|-?\d+(\.\d*)?([eE][+-]?\d+)?|true|false|null)\s*/,token=undefined,operator=undefined;function error(m,t){throw {name:"JSONError",message:m,text:t||operator||token}}function next(b){if(b&&b!=operator){error("Expected '"+b+"'")}if(text){var t=p.exec(text);if(t){if(t[2]){token=null;operator=t[2]}else{operator=null;try{token=eval(t[1])}catch(e){error("Bad token",t[1])}}text=text.substring(t[0].length)}else{error("Unrecognized token",text)}}else{token=operator=undefined}}function val(){var k,o;switch(operator){case"{":next("{");o={};if(operator!="}"){for(;;){if(operator||typeof token!="string"){error("Missing key")}k=token;next();next(":");o[k]=val();if(operator!=","){break}next(",")}}next("}");return o;case"[":next("[");o=[];if(operator!="]"){for(;;){o.push(val());if(operator!=","){break}next(",")}}next("]");return o;default:if(operator!==null){error("Missing value")}k=token;next();return k}}next();return val()}};DataTiddler.format="JSON";DataTiddler.stringify=JSON.stringify;DataTiddler.parse=JSON.parse;
//%/
[tag[TimeLine]]
<<showData [[Event 1]]>>
<data>{
"startYear":2012,
"startMonth":4,
"startDay":23,
"endYear":2012,
"endMonth":6,
"endDay":30,
"color":"Orchid",
"text":"Text for Event 1",
"group":"Group A"}</data>
<<showData [[Event 1]]>>
<data>{
"startYear":2012,
"startMonth":6,
"startDay":30,
"endYear":2012,
"endMonth":9,
"endDay":15,
"color":"Aqua",
"text":"Text for Event 2",
"group":"Group A"}</data>
<<showData [[Event 3]]>>
<data>{
"startYear":2012,
"startMonth":5,
"startDay":23,
"endYear":2012,
"endMonth":8,
"endDay":30,
"color":"PaleGreen",
"text":"Text for Event 3",
"group":"Group B"}</data>
<data>{"startYear":"1973","startMonth":"10","startDay":2,"endYear":"2011","endMonth":9,"endDay":30,"color":"Yellow","text":"<br><p style=\"font-size:20px\"><img src='https://groups.google.com/group/tiddlywiki/attach/9940d58af095b121/Sample1.jpg?hl=da&part=2&thumb=1'> <a href=\"javascript:;\" onclick=\"story.displayTiddler(null,'Event 7'); jQuery('#tiddlerDisplay').show(); return false;\">Est-ce possible?</a>&nbsp;<a href=\"javascript:;\" onclick=\"story.displayTiddler(null,'Event 7'); jQuery('#tiddlerDisplay').show(); return false;\"><img src='http://xn--mns-ula.dk/sky/apps/files_sharing/get.php?token=0d5f29bc3eb2f973bd63559135352c5548f5967b&path=/MK%2C%20Abstract%20Me%20i%20-III%2C%201993.jpg'width=\"auto\" height=\"100\"vspace=\"0\" hspace=\"15\" border=\"1\" title=\"Event 7\" alt=\"Event 7\"></a>Oui! :-)</p><br>","group":"Group B"}</data> 
<data>{"startYear":"1970","endYear":"2013","endMonth":"4","startDay":"2","endDay":"3","group":"Group A","startMonth":"2","text":"<a href=\"javascript:;\" onclick=\"story.displayTiddler(null,'Event 8'); jQuery('#tiddlerDisplay').show(); return false;\"><p style=\"font-size:20px\" align=\"center\">Texte</p></a>"}</data>
/***
|''Name:''|ForEachTiddlerPlugin|
|''Version:''|1.0.8 (2007-04-12)|
|''Source:''|http://tiddlywiki.abego-software.de/#ForEachTiddlerPlugin|
|''Author:''|UdoBorkowski (ub [at] abego-software [dot] de)|
|''Licence:''|[[BSD open source license (abego Software)|http://www.abego-software.de/legal/apl-v10.html]]|
|''Copyright:''|&copy; 2005-2007 [[abego Software|http://www.abego-software.de]]|
|''TiddlyWiki:''|1.2.38+, 2.0|
|''Browser:''|Firefox 1.0.4+; Firefox 1.5; InternetExplorer 6.0|
!Description

Create customizable lists, tables etc. for your selections of tiddlers. Specify the tiddlers to include and their order through a powerful language.

''Syntax:''
|>|{{{<<}}}''forEachTiddler'' [''in'' //tiddlyWikiPath//] [''where'' //whereCondition//] [''sortBy'' //sortExpression// [''ascending'' //or// ''descending'']] [''script'' //scriptText//] [//action// [//actionParameters//]]{{{>>}}}|
|//tiddlyWikiPath//|The filepath to the TiddlyWiki the macro should work on. When missing the current TiddlyWiki is used.|
|//whereCondition//|(quoted) JavaScript boolean expression. May refer to the build-in variables {{{tiddler}}} and  {{{context}}}.|
|//sortExpression//|(quoted) JavaScript expression returning "comparable" objects (using '{{{<}}}','{{{>}}}','{{{==}}}'. May refer to the build-in variables {{{tiddler}}} and  {{{context}}}.|
|//scriptText//|(quoted) JavaScript text. Typically defines JavaScript functions that are called by the various JavaScript expressions (whereClause, sortClause, action arguments,...)|
|//action//|The action that should be performed on every selected tiddler, in the given order. By default the actions [[addToList|AddToListAction]] and [[write|WriteAction]] are supported. When no action is specified [[addToList|AddToListAction]]  is used.|
|//actionParameters//|(action specific) parameters the action may refer while processing the tiddlers (see action descriptions for details). <<tiddler [[JavaScript in actionParameters]]>>|
|>|~~Syntax formatting: Keywords in ''bold'', optional parts in [...]. 'or' means that exactly one of the two alternatives must exist.~~|

See details see [[ForEachTiddlerMacro]] and [[ForEachTiddlerExamples]].

!Revision history
* v1.0.8 (2007-04-12)
** Adapted to latest TiddlyWiki 2.2 Beta importTiddlyWiki API (introduced with changeset 2004). TiddlyWiki 2.2 Beta builds prior to changeset 2004 are no longer supported (but TiddlyWiki 2.1 and earlier, of cause)
* v1.0.7 (2007-03-28)
** Also support "pre" formatted TiddlyWikis (introduced with TW 2.2) (when using "in" clause to work on external tiddlers)
* v1.0.6 (2006-09-16)
** Context provides "viewerTiddler", i.e. the tiddler used to view the macro. Most times this is equal to the "inTiddler", but when using the "tiddler" macro both may be different.
** Support "begin", "end" and "none" expressions in "write" action
* v1.0.5 (2006-02-05)
** Pass tiddler containing the macro with wikify, context object also holds reference to tiddler containing the macro ("inTiddler"). Thanks to SimonBaird.
** Support Firefox 1.5.0.1
** Internal
*** Make "JSLint" conform
*** "Only install once"
* v1.0.4 (2006-01-06)
** Support TiddlyWiki 2.0
* v1.0.3 (2005-12-22)
** Features:
*** Write output to a file supports multi-byte environments (Thanks to Bram Chen)
*** Provide API to access the forEachTiddler functionality directly through JavaScript (see getTiddlers and performMacro)
** Enhancements:
*** Improved error messages on InternetExplorer.
* v1.0.2 (2005-12-10)
** Features:
*** context object also holds reference to store (TiddlyWiki)
** Fixed Bugs:
*** ForEachTiddler 1.0.1 has broken support on win32 Opera 8.51 (Thanks to BrunoSabin for reporting)
* v1.0.1 (2005-12-08)
** Features:
*** Access tiddlers stored in separated TiddlyWikis through the "in" option. I.e. you are no longer limited to only work on the "current TiddlyWiki".
*** Write output to an external file using the "toFile" option of the "write" action. With this option you may write your customized tiddler exports.
*** Use the "script" section to define "helper" JavaScript functions etc. to be used in the various JavaScript expressions (whereClause, sortClause, action arguments,...).
*** Access and store context information for the current forEachTiddler invocation (through the build-in "context" object) .
*** Improved script evaluation (for where/sort clause and write scripts).
* v1.0.0 (2005-11-20)
** initial version

!Code
***/
//{{{


//============================================================================
//============================================================================
//		   ForEachTiddlerPlugin
//============================================================================
//============================================================================

// Only install once
if (!version.extensions.ForEachTiddlerPlugin) {

if (!window.abego) window.abego = {};

version.extensions.ForEachTiddlerPlugin = {
	major: 1, minor: 0, revision: 8,
	date: new Date(2007,3,12),
	source: "http://tiddlywiki.abego-software.de/#ForEachTiddlerPlugin",
	licence: "[[BSD open source license (abego Software)|http://www.abego-software.de/legal/apl-v10.html]]",
	copyright: "Copyright (c) abego Software GmbH, 2005-2007 (www.abego-software.de)"
};

// For backward compatibility with TW 1.2.x
//
if (!TiddlyWiki.prototype.forEachTiddler) {
	TiddlyWiki.prototype.forEachTiddler = function(callback) {
		for(var t in this.tiddlers) {
			callback.call(this,t,this.tiddlers[t]);
		}
	};
}

//============================================================================
// forEachTiddler Macro
//============================================================================

version.extensions.forEachTiddler = {
	major: 1, minor: 0, revision: 8, date: new Date(2007,3,12), provider: "http://tiddlywiki.abego-software.de"};

// ---------------------------------------------------------------------------
// Configurations and constants
// ---------------------------------------------------------------------------

config.macros.forEachTiddler = {
	 // Standard Properties
	 label: "forEachTiddler",
	 prompt: "Perform actions on a (sorted) selection of tiddlers",

	 // actions
	 actions: {
		 addToList: {},
		 write: {}
	 }
};

// ---------------------------------------------------------------------------
//  The forEachTiddler Macro Handler
// ---------------------------------------------------------------------------

config.macros.forEachTiddler.getContainingTiddler = function(e) {
	while(e && !hasClass(e,"tiddler"))
		e = e.parentNode;
	var title = e ? e.getAttribute("tiddler") : null;
	return title ? store.getTiddler(title) : null;
};

config.macros.forEachTiddler.handler = function(place,macroName,params,wikifier,paramString,tiddler) {
	// config.macros.forEachTiddler.traceMacroCall(place,macroName,params,wikifier,paramString,tiddler);

	if (!tiddler) tiddler = config.macros.forEachTiddler.getContainingTiddler(place);
	// --- Parsing ------------------------------------------

	var i = 0; // index running over the params
	// Parse the "in" clause
	var tiddlyWikiPath = undefined;
	if ((i < params.length) && params[i] == "in") {
		i++;
		if (i >= params.length) {
			this.handleError(place, "TiddlyWiki path expected behind 'in'.");
			return;
		}
		tiddlyWikiPath = this.paramEncode((i < params.length) ? params[i] : "");
		i++;
	}

	// Parse the where clause
	var whereClause ="true";
	if ((i < params.length) && params[i] == "where") {
		i++;
		whereClause = this.paramEncode((i < params.length) ? params[i] : "");
		i++;
	}

	// Parse the sort stuff
	var sortClause = null;
	var sortAscending = true;
	if ((i < params.length) && params[i] == "sortBy") {
		i++;
		if (i >= params.length) {
			this.handleError(place, "sortClause missing behind 'sortBy'.");
			return;
		}
		sortClause = this.paramEncode(params[i]);
		i++;

		if ((i < params.length) && (params[i] == "ascending" || params[i] == "descending")) {
			 sortAscending = params[i] == "ascending";
			 i++;
		}
	}

	// Parse the script
	var scriptText = null;
	if ((i < params.length) && params[i] == "script") {
		i++;
		scriptText = this.paramEncode((i < params.length) ? params[i] : "");
		i++;
	}

	// Parse the action.
	// When we are already at the end use the default action
	var actionName = "addToList";
	if (i < params.length) {
	   if (!config.macros.forEachTiddler.actions[params[i]]) {
			this.handleError(place, "Unknown action '"+params[i]+"'.");
			return;
		} else {
			actionName = params[i];
			i++;
		}
	}

	// Get the action parameter
	// (the parsing is done inside the individual action implementation.)
	var actionParameter = params.slice(i);


	// --- Processing ------------------------------------------
	try {
		this.performMacro({
				place: place,
				inTiddler: tiddler,
				whereClause: whereClause,
				sortClause: sortClause,
				sortAscending: sortAscending,
				actionName: actionName,
				actionParameter: actionParameter,
				scriptText: scriptText,
				tiddlyWikiPath: tiddlyWikiPath});

	} catch (e) {
		this.handleError(place, e);
	}
};

// Returns an object with properties "tiddlers" and "context".
// tiddlers holds the (sorted) tiddlers selected by the parameter,
// context the context of the execution of the macro.
//
// The action is not yet performed.
//
// @parameter see performMacro
//
config.macros.forEachTiddler.getTiddlersAndContext = function(parameter) {

	var context = config.macros.forEachTiddler.createContext(parameter.place, parameter.whereClause, parameter.sortClause, parameter.sortAscending, parameter.actionName, parameter.actionParameter, parameter.scriptText, parameter.tiddlyWikiPath, parameter.inTiddler);

	var tiddlyWiki = parameter.tiddlyWikiPath ? this.loadTiddlyWiki(parameter.tiddlyWikiPath) : store;
	context["tiddlyWiki"] = tiddlyWiki;

	// Get the tiddlers, as defined by the whereClause
	var tiddlers = this.findTiddlers(parameter.whereClause, context, tiddlyWiki);
	context["tiddlers"] = tiddlers;

	// Sort the tiddlers, when sorting is required.
	if (parameter.sortClause) {
		this.sortTiddlers(tiddlers, parameter.sortClause, parameter.sortAscending, context);
	}

	return {tiddlers: tiddlers, context: context};
};

// Returns the (sorted) tiddlers selected by the parameter.
//
// The action is not yet performed.
//
// @parameter see performMacro
//
config.macros.forEachTiddler.getTiddlers = function(parameter) {
	return this.getTiddlersAndContext(parameter).tiddlers;
};

// Performs the macros with the given parameter.
//
// @param parameter holds the parameter of the macro as separate properties.
//				  The following properties are supported:
//
//						place
//						whereClause
//						sortClause
//						sortAscending
//						actionName
//						actionParameter
//						scriptText
//						tiddlyWikiPath
//
//					All properties are optional.
//					For most actions the place property must be defined.
//
config.macros.forEachTiddler.performMacro = function(parameter) {
	var tiddlersAndContext = this.getTiddlersAndContext(parameter);

	// Perform the action
	var actionName = parameter.actionName ? parameter.actionName : "addToList";
	var action = config.macros.forEachTiddler.actions[actionName];
	if (!action) {
		this.handleError(parameter.place, "Unknown action '"+actionName+"'.");
		return;
	}

	var actionHandler = action.handler;
	actionHandler(parameter.place, tiddlersAndContext.tiddlers, parameter.actionParameter, tiddlersAndContext.context);
};

// ---------------------------------------------------------------------------
//  The actions
// ---------------------------------------------------------------------------

// Internal.
//
// --- The addToList Action -----------------------------------------------
//
config.macros.forEachTiddler.actions.addToList.handler = function(place, tiddlers, parameter, context) {
	// Parse the parameter
	var p = 0;

	// Check for extra parameters
	if (parameter.length > p) {
		config.macros.forEachTiddler.createExtraParameterErrorElement(place, "addToList", parameter, p);
		return;
	}

	// Perform the action.
	var list = document.createElement("ul");
	place.appendChild(list);
	for (var i = 0; i < tiddlers.length; i++) {
		var tiddler = tiddlers[i];
		var listItem = document.createElement("li");
		list.appendChild(listItem);
		createTiddlyLink(listItem, tiddler.title, true);
	}
};

abego.parseNamedParameter = function(name, parameter, i) {
	var beginExpression = null;
	if ((i < parameter.length) && parameter[i] == name) {
		i++;
		if (i >= parameter.length) {
			throw "Missing text behind '%0'".format([name]);
		}

		return config.macros.forEachTiddler.paramEncode(parameter[i]);
	}
	return null;
}

// Internal.
//
// --- The write Action ---------------------------------------------------
//
config.macros.forEachTiddler.actions.write.handler = function(place, tiddlers, parameter, context) {
	// Parse the parameter
	var p = 0;
	if (p >= parameter.length) {
		this.handleError(place, "Missing expression behind 'write'.");
		return;
	}

	var textExpression = config.macros.forEachTiddler.paramEncode(parameter[p]);
	p++;

	// Parse the "begin" option
	var beginExpression = abego.parseNamedParameter("begin", parameter, p);
	if (beginExpression !== null)
		p += 2;
	var endExpression = abego.parseNamedParameter("end", parameter, p);
	if (endExpression !== null)
		p += 2;
	var noneExpression = abego.parseNamedParameter("none", parameter, p);
	if (noneExpression !== null)
		p += 2;

	// Parse the "toFile" option
	var filename = null;
	var lineSeparator = undefined;
	if ((p < parameter.length) && parameter[p] == "toFile") {
		p++;
		if (p >= parameter.length) {
			this.handleError(place, "Filename expected behind 'toFile' of 'write' action.");
			return;
		}

		filename = config.macros.forEachTiddler.getLocalPath(config.macros.forEachTiddler.paramEncode(parameter[p]));
		p++;
		if ((p < parameter.length) && parameter[p] == "withLineSeparator") {
			p++;
			if (p >= parameter.length) {
				this.handleError(place, "Line separator text expected behind 'withLineSeparator' of 'write' action.");
				return;
			}
			lineSeparator = config.macros.forEachTiddler.paramEncode(parameter[p]);
			p++;
		}
	}

	// Check for extra parameters
	if (parameter.length > p) {
		config.macros.forEachTiddler.createExtraParameterErrorElement(place, "write", parameter, p);
		return;
	}

	// Perform the action.
	var func = config.macros.forEachTiddler.getEvalTiddlerFunction(textExpression, context);
	var count = tiddlers.length;
	var text = "";
	if (count > 0 && beginExpression)
		text += config.macros.forEachTiddler.getEvalTiddlerFunction(beginExpression, context)(undefined, context, count, undefined);

	for (var i = 0; i < count; i++) {
		var tiddler = tiddlers[i];
		text += func(tiddler, context, count, i);
	}

	if (count > 0 && endExpression)
		text += config.macros.forEachTiddler.getEvalTiddlerFunction(endExpression, context)(undefined, context, count, undefined);

	if (count == 0 && noneExpression)
		text += config.macros.forEachTiddler.getEvalTiddlerFunction(noneExpression, context)(undefined, context, count, undefined);


	if (filename) {
		if (lineSeparator !== undefined) {
			lineSeparator = lineSeparator.replace(/\\n/mg, "\n").replace(/\\r/mg, "\r");
			text = text.replace(/\n/mg,lineSeparator);
		}
		saveFile(filename, convertUnicodeToUTF8(text));
	} else {
		var wrapper = createTiddlyElement(place, "span");
		wikify(text, wrapper, null/* highlightRegExp */, context.inTiddler);
	}
};


// ---------------------------------------------------------------------------
//  Helpers
// ---------------------------------------------------------------------------

// Internal.
//
config.macros.forEachTiddler.createContext = function(placeParam, whereClauseParam, sortClauseParam, sortAscendingParam, actionNameParam, actionParameterParam, scriptText, tiddlyWikiPathParam, inTiddlerParam) {
	return {
		place : placeParam,
		whereClause : whereClauseParam,
		sortClause : sortClauseParam,
		sortAscending : sortAscendingParam,
		script : scriptText,
		actionName : actionNameParam,
		actionParameter : actionParameterParam,
		tiddlyWikiPath : tiddlyWikiPathParam,
		inTiddler : inTiddlerParam, // the tiddler containing the <<forEachTiddler ...>> macro call.
		viewerTiddler : config.macros.forEachTiddler.getContainingTiddler(placeParam) // the tiddler showing the forEachTiddler result
	};
};

// Internal.
//
// Returns a TiddlyWiki with the tiddlers loaded from the TiddlyWiki of
// the given path.
//
config.macros.forEachTiddler.loadTiddlyWiki = function(path, idPrefix) {
	if (!idPrefix) {
		idPrefix = "store";
	}
	var lenPrefix = idPrefix.length;

	// Read the content of the given file
	var content = loadFile(this.getLocalPath(path));
	if(content === null) {
		throw "TiddlyWiki '"+path+"' not found.";
	}

	var tiddlyWiki = new TiddlyWiki();

	// Starting with TW 2.2 there is a helper function to import the tiddlers
	if (tiddlyWiki.importTiddlyWiki) {
		if (!tiddlyWiki.importTiddlyWiki(content))
			throw "File '"+path+"' is not a TiddlyWiki.";
		tiddlyWiki.dirty = false;
		return tiddlyWiki;
	}

	// The legacy code, for TW < 2.2

	// Locate the storeArea div's
	var posOpeningDiv = content.indexOf(startSaveArea);
	var posClosingDiv = content.lastIndexOf(endSaveArea);
	if((posOpeningDiv == -1) || (posClosingDiv == -1)) {
		throw "File '"+path+"' is not a TiddlyWiki.";
	}
	var storageText = content.substr(posOpeningDiv + startSaveArea.length, posClosingDiv);

	// Create a "div" element that contains the storage text
	var myStorageDiv = document.createElement("div");
	myStorageDiv.innerHTML = storageText;
	myStorageDiv.normalize();

	// Create all tiddlers in a new TiddlyWiki
	// (following code is modified copy of TiddlyWiki.prototype.loadFromDiv)
	var store = myStorageDiv.childNodes;
	for(var t = 0; t < store.length; t++) {
		var e = store[t];
		var title = null;
		if(e.getAttribute)
			title = e.getAttribute("tiddler");
		if(!title && e.id && e.id.substr(0,lenPrefix) == idPrefix)
			title = e.id.substr(lenPrefix);
		if(title && title !== "") {
			var tiddler = tiddlyWiki.createTiddler(title);
			tiddler.loadFromDiv(e,title);
		}
	}
	tiddlyWiki.dirty = false;

	return tiddlyWiki;
};



// Internal.
//
// Returns a function that has a function body returning the given javaScriptExpression.
// The function has the parameters:
//
//	 (tiddler, context, count, index)
//
config.macros.forEachTiddler.getEvalTiddlerFunction = function (javaScriptExpression, context) {
	var script = context["script"];
	var functionText = "var theFunction = function(tiddler, context, count, index) { return "+javaScriptExpression+"}";
	var fullText = (script ? script+";" : "")+functionText+";theFunction;";
	return eval(fullText);
};

// Internal.
//
config.macros.forEachTiddler.findTiddlers = function(whereClause, context, tiddlyWiki) {
	var result = [];
	var func = config.macros.forEachTiddler.getEvalTiddlerFunction(whereClause, context);
	tiddlyWiki.forEachTiddler(function(title,tiddler) {
		if (func(tiddler, context, undefined, undefined)) {
			result.push(tiddler);
		}
	});
	return result;
};

// Internal.
//
config.macros.forEachTiddler.createExtraParameterErrorElement = function(place, actionName, parameter, firstUnusedIndex) {
	var message = "Extra parameter behind '"+actionName+"':";
	for (var i = firstUnusedIndex; i < parameter.length; i++) {
		message += " "+parameter[i];
	}
	this.handleError(place, message);
};

// Internal.
//
config.macros.forEachTiddler.sortAscending = function(tiddlerA, tiddlerB) {
	var result =
		(tiddlerA.forEachTiddlerSortValue == tiddlerB.forEachTiddlerSortValue)
			? 0
			: (tiddlerA.forEachTiddlerSortValue < tiddlerB.forEachTiddlerSortValue)
			   ? -1
			   : +1;
	return result;
};

// Internal.
//
config.macros.forEachTiddler.sortDescending = function(tiddlerA, tiddlerB) {
	var result =
		(tiddlerA.forEachTiddlerSortValue == tiddlerB.forEachTiddlerSortValue)
			? 0
			: (tiddlerA.forEachTiddlerSortValue < tiddlerB.forEachTiddlerSortValue)
			   ? +1
			   : -1;
	return result;
};

// Internal.
//
config.macros.forEachTiddler.sortTiddlers = function(tiddlers, sortClause, ascending, context) {
	// To avoid evaluating the sortClause whenever two items are compared
	// we pre-calculate the sortValue for every item in the array and store it in a
	// temporary property ("forEachTiddlerSortValue") of the tiddlers.
	var func = config.macros.forEachTiddler.getEvalTiddlerFunction(sortClause, context);
	var count = tiddlers.length;
	var i;
	for (i = 0; i < count; i++) {
		var tiddler = tiddlers[i];
		tiddler.forEachTiddlerSortValue = func(tiddler,context, undefined, undefined);
	}

	// Do the sorting
	tiddlers.sort(ascending ? this.sortAscending : this.sortDescending);

	// Delete the temporary property that holds the sortValue.
	for (i = 0; i < tiddlers.length; i++) {
		delete tiddlers[i].forEachTiddlerSortValue;
	}
};


// Internal.
//
config.macros.forEachTiddler.trace = function(message) {
	displayMessage(message);
};

// Internal.
//
config.macros.forEachTiddler.traceMacroCall = function(place,macroName,params) {
	var message ="<<"+macroName;
	for (var i = 0; i < params.length; i++) {
		message += " "+params[i];
	}
	message += ">>";
	displayMessage(message);
};


// Internal.
//
// Creates an element that holds an error message
//
config.macros.forEachTiddler.createErrorElement = function(place, exception) {
	var message = (exception.description) ? exception.description : exception.toString();
	return createTiddlyElement(place,"span",null,"forEachTiddlerError","<<forEachTiddler ...>>: "+message);
};

// Internal.
//
// @param place [may be null]
//
config.macros.forEachTiddler.handleError = function(place, exception) {
	if (place) {
		this.createErrorElement(place, exception);
	} else {
		throw exception;
	}
};

// Internal.
//
// Encodes the given string.
//
// Replaces
//	 "$))" to ">>"
//	 "$)" to ">"
//
config.macros.forEachTiddler.paramEncode = function(s) {
	var reGTGT = new RegExp("\\$\\)\\)","mg");
	var reGT = new RegExp("\\$\\)","mg");
	return s.replace(reGTGT, ">>").replace(reGT, ">");
};

// Internal.
//
// Returns the given original path (that is a file path, starting with "file:")
// as a path to a local file, in the systems native file format.
//
// Location information in the originalPath (i.e. the "#" and stuff following)
// is stripped.
//
config.macros.forEachTiddler.getLocalPath = function(originalPath) {
	// Remove any location part of the URL
	var hashPos = originalPath.indexOf("#");
	if(hashPos != -1)
		originalPath = originalPath.substr(0,hashPos);
	// Convert to a native file format assuming
	// "file:///x:/path/path/path..." - pc local file --> "x:\path\path\path..."
	// "file://///server/share/path/path/path..." - FireFox pc network file --> "\\server\share\path\path\path..."
	// "file:///path/path/path..." - mac/unix local file --> "/path/path/path..."
	// "file://server/share/path/path/path..." - pc network file --> "\\server\share\path\path\path..."
	var localPath;
	if(originalPath.charAt(9) == ":") // pc local file
		localPath = unescape(originalPath.substr(8)).replace(new RegExp("/","g"),"\\");
	else if(originalPath.indexOf("file://///") === 0) // FireFox pc network file
		localPath = "\\\\" + unescape(originalPath.substr(10)).replace(new RegExp("/","g"),"\\");
	else if(originalPath.indexOf("file:///") === 0) // mac/unix local file
		localPath = unescape(originalPath.substr(7));
	else if(originalPath.indexOf("file:/") === 0) // mac/unix local file
		localPath = unescape(originalPath.substr(5));
	else // pc network file
		localPath = "\\\\" + unescape(originalPath.substr(7)).replace(new RegExp("/","g"),"\\");
	return localPath;
};

// ---------------------------------------------------------------------------
// Stylesheet Extensions (may be overridden by local StyleSheet)
// ---------------------------------------------------------------------------
//
setStylesheet(
	".forEachTiddlerError{color: #ffffff;background-color: #880000;}",
	"forEachTiddler");

//============================================================================
// End of forEachTiddler Macro
//============================================================================


//============================================================================
// String.startsWith Function
//============================================================================
//
// Returns true if the string starts with the given prefix, false otherwise.
//
version.extensions["String.startsWith"] = {major: 1, minor: 0, revision: 0, date: new Date(2005,11,20), provider: "http://tiddlywiki.abego-software.de"};
//
String.prototype.startsWith = function(prefix) {
	var n =  prefix.length;
	return (this.length >= n) && (this.slice(0, n) == prefix);
};



//============================================================================
// String.endsWith Function
//============================================================================
//
// Returns true if the string ends with the given suffix, false otherwise.
//
version.extensions["String.endsWith"] = {major: 1, minor: 0, revision: 0, date: new Date(2005,11,20), provider: "http://tiddlywiki.abego-software.de"};
//
String.prototype.endsWith = function(suffix) {
	var n = suffix.length;
	return (this.length >= n) && (this.right(n) == suffix);
};


//============================================================================
// String.contains Function
//============================================================================
//
// Returns true when the string contains the given substring, false otherwise.
//
version.extensions["String.contains"] = {major: 1, minor: 0, revision: 0, date: new Date(2005,11,20), provider: "http://tiddlywiki.abego-software.de"};
//
String.prototype.contains = function(substring) {
	return this.indexOf(substring) >= 0;
};

//============================================================================
// Array.indexOf Function
//============================================================================
//
// Returns the index of the first occurance of the given item in the array or
// -1 when no such item exists.
//
// @param item [may be null]
//
version.extensions["Array.indexOf"] = {major: 1, minor: 0, revision: 0, date: new Date(2005,11,20), provider: "http://tiddlywiki.abego-software.de"};
//
Array.prototype.indexOf = function(item) {
	for (var i = 0; i < this.length; i++) {
		if (this[i] == item) {
			return i;
		}
	}
	return -1;
};

//============================================================================
// Array.contains Function
//============================================================================
//
// Returns true when the array contains the given item, otherwise false.
//
// @param item [may be null]
//
version.extensions["Array.contains"] = {major: 1, minor: 0, revision: 0, date: new Date(2005,11,20), provider: "http://tiddlywiki.abego-software.de"};
//
Array.prototype.contains = function(item) {
	return (this.indexOf(item) >= 0);
};

//============================================================================
// Array.containsAny Function
//============================================================================
//
// Returns true when the array contains at least one of the elements
// of the item. Otherwise (or when items contains no elements) false is returned.
//
version.extensions["Array.containsAny"] = {major: 1, minor: 0, revision: 0, date: new Date(2005,11,20), provider: "http://tiddlywiki.abego-software.de"};
//
Array.prototype.containsAny = function(items) {
	for(var i = 0; i < items.length; i++) {
		if (this.contains(items[i])) {
			return true;
		}
	}
	return false;
};


//============================================================================
// Array.containsAll Function
//============================================================================
//
// Returns true when the array contains all the items, otherwise false.
//
// When items is null false is returned (even if the array contains a null).
//
// @param items [may be null]
//
version.extensions["Array.containsAll"] = {major: 1, minor: 0, revision: 0, date: new Date(2005,11,20), provider: "http://tiddlywiki.abego-software.de"};
//
Array.prototype.containsAll = function(items) {
	for(var i = 0; i < items.length; i++) {
		if (!this.contains(items[i])) {
			return false;
		}
	}
	return true;
};


} // of "install only once"

// Used Globals (for JSLint) ==============
// ... DOM
/*global 	document */
// ... TiddlyWiki Core
/*global 	convertUnicodeToUTF8, createTiddlyElement, createTiddlyLink,
			displayMessage, endSaveArea, hasClass, loadFile, saveFile,
			startSaveArea, store, wikify */
//}}}


/***
!Licence and Copyright
Copyright (c) abego Software ~GmbH, 2005 ([[www.abego-software.de|http://www.abego-software.de]])

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or other
materials provided with the distribution.

Neither the name of abego Software nor the names of its contributors may be
used to endorse or promote products derived from this software without specific
prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
***/
/***
<<checkForDataTiddlerPlugin>>
|''Name:''|FormTiddlerPlugin|
|''Version:''|1.0.7 (2012-04-19)|
|''Summary:''|Use form-based tiddlers to enter your tiddler data using text fields, listboxes, checkboxes etc. (All standard HTML Form input elements supported).|
|''Documentation:''|[[Introduction|FormTiddler Introduction]], [[Examples|FormTiddler Examples]]|
|''Source:''|http://tiddlywiki.abego-software.de/#FormTiddlerPlugin|
|''Twitter:''|[[@abego|https://twitter.com/#!/abego]]|
|''Author:''|UdoBorkowski (ub [at] abego-software [dot] de)|
|''License:''|[[BSD open source license|http://www.abego-software.de/legal/apl-v10.html]]|
|''Requires:''|DataTiddlerPlugin|
!Description
Use form-based tiddlers to enter your tiddler data using text fields, listboxes, checkboxes etc. (All standard HTML Form input elements supported).

''Syntax:'' 
|>|{{{<<}}}''formTiddler'' //tiddlerName//{{{>>}}}|
|//tiddlerName//|The name of the FormTemplate tiddler to be used to edit the data of the tiddler containing the macro.|

|>|{{{<<}}}''newTiddlerWithForm'' //formTemplateName// //buttonLabel// [//titleExpression// [''askUser'']] {{{>>}}}|
|//formTemplateName//|The name of the tiddler that defines the form the new tiddler should use.|
|//buttonLabel//|The label of the button|
|//titleExpression//|A (quoted) JavaScript String expression that defines the title (/name) of the new tiddler.|
|''askUser''|Typically the user is not asked for the title when a title is specified (and not yet used). When ''askUser'' is given the user will be asked in any case. This may be used when the calculated title is just a suggestion that must be confirmed by the user|
|>|~~Syntax formatting: Keywords in ''bold'', optional parts in [...]. 'or' means that exactly one of the two alternatives must exist.~~|

For details and how to use the macros see the [[introduction|FormTiddler Introduction]] and the [[examples|FormTiddler Examples]].

!Source Code
***/
/***
This plugin's source code is compressed (and hidden). 
Use this [[link|http://tiddlywiki.abego-software.de/archive/FormTiddlerPlugin/1.0.7/FormTiddlerPlugin-1.0.7-src.js]] to get the readable source code.
***/
///%
if(!window.abego){window.abego={}}abego.getOptionsValue=function(c,b){var a=c.options[b].value;if(!a&&c.options[b].text){a=c.options[b].text}return a};version.extensions.FormTiddlerPlugin={major:1,minor:0,revision:7,date:new Date(2012,3,19),type:"plugin",source:"http://tiddlywiki.abego-software.de/#FormTiddlerPlugin"};if(!window.story){window.story=window}if(!TiddlyWiki.prototype.getTiddler){TiddlyWiki.prototype.getTiddler=function(a){return t=this.tiddlers[a];return(t!=undefined&&t instanceof Tiddler)?t:null}}config.macros.formTiddler={label:"formTiddler",prompt:"Edit tiddler data using forms",setter:{button:function(b,a){},checkbox:function(b,a){b.checked=a},file:function(b,a){try{b.value=a}catch(b){}},hidden:function(b,a){b.value=a},password:function(b,a){b.value=a},radio:function(b,a){b.checked=(b.value==a)},reset:function(b,a){},"select-one":function(b,a){config.macros.formTiddler.setSelectOneValue(b,a)},"select-multiple":function(b,a){config.macros.formTiddler.setSelectMultipleValue(b,a)},submit:function(b,a){},text:function(b,a){b.value=a},textarea:function(b,a){b.value=a}},getter:{button:function(b,a){return undefined},checkbox:function(b,a){return b.checked},file:function(b,a){return b.value},hidden:function(b,a){return b.value},password:function(b,a){return b.value},radio:function(b,a){return b.checked?b.value:undefined},reset:function(b,a){return undefined},"select-one":function(b,a){return config.macros.formTiddler.getSelectOneValue(b)},"select-multiple":function(b,a){return config.macros.formTiddler.getSelectMultipleValue(b)},submit:function(b,a){return undefined},text:function(b,a){return b.value},textarea:function(b,a){return b.value}}};config.macros.formTiddler.handler=function(g,c,f,h,d,n){if(!config.macros.formTiddler.checkForExtensions(g,c)){return}var j=0;var l=undefined;if(j<f.length){l=f[j];j++}if(!l){config.macros.formTiddler.createErrorElement(g,"No form template specified in <<"+c+">>.");return}var a=store.getTiddler(l);if(!a){config.macros.formTiddler.createErrorElement(g,"Form template '"+l+"' not found.");return}var m=a.text;if(!m){return}var b=config.macros.formTiddler.getContainingTiddlerName(g);var o="form"+l+"__"+b;var k=document.createElement("form");k.setAttribute("name",o);g.appendChild(k);wikify(m,k);config.macros.formTiddler.initValuesAndHandlersInFormElements(o,DataTiddler.getDataObject(b))};config.macros.formTiddler.initValuesAndHandlersInFormElements=function(l,f){var b=config.macros.formTiddler.findForm(l);if(!b){return}try{var a=b.elements;for(var g=0;g<a.length;g++){var j=a[g];var d=config.macros.formTiddler.setter[j.type];if(d){var k=f[j.name];if(k!=null){d(j,k)}j.onchange=onFormTiddlerChange}else{config.macros.formTiddler.displayFormTiddlerError("No setter defined for INPUT element of type '"+j.type+"'. (Element '"+j.name+"' in form '"+l+"')")}}}catch(h){config.macros.formTiddler.displayFormTiddlerError("Error when updating elements with new formData. "+h)}};config.macros.formTiddler.findForm=function(c){var a=window.document.forms;for(var b=0;b<a.length;b++){var d=a[b];if(d.name==c){return d}}return null};config.macros.formTiddler.setSelectOneValue=function(b,c){var d=b.options.length;for(var a=0;a<d;a++){b.options[a].selected=abego.getOptionsValue(b,a)==c}};config.macros.formTiddler.setSelectMultipleValue=function(c,d){var a={};for(var b=0;b<d.length;b++){a[d[b]]=true}var e=c.length;for(var b=0;b<e;b++){c.options[b].selected=!(!a[abego.getOptionsValue(c,b)])}};config.macros.formTiddler.getSelectOneValue=function(b){var a=b.selectedIndex;return(a>=0)?abego.getOptionsValue(b,a):null};config.macros.formTiddler.getSelectMultipleValue=function(c){var a=[];var d=c.length;for(var b=0;b<d;b++){if(c.options[b].selected){a.push(abego.getOptionsValue(c,b))}}return a};config.macros.formTiddler.checkForExtensions=function(a,b){if(!version.extensions.DataTiddlerPlugin){config.macros.formTiddler.createErrorElement(a,"<<"+b+">> requires the DataTiddlerPlugin. (You can get it from http://tiddlywiki.abego-software.de/#DataTiddlerPlugin)");return false}return true};config.macros.formTiddler.trace=function(a){displayMessage("Trace: "+a)};config.macros.formTiddler.displayFormTiddlerError=function(a){alert("FormTiddlerPlugin Error: "+a)};config.macros.formTiddler.createErrorElement=function(a,b){return createTiddlyElement(a,"span",null,"formTiddlerError",b)};config.macros.formTiddler.getContainingTiddlerName=function(a){return story.findContainingTiddler(a).getAttribute("tiddler")};function onFormTiddlerChange(f){if(!f){f=window.event}var d=resolveTarget(f);var b=config.macros.formTiddler.getContainingTiddlerName(d);var a=config.macros.formTiddler.getter[d.type];if(a){var c=a(d);DataTiddler.setData(b,d.name,c)}else{config.macros.formTiddler.displayFormTiddlerError("No getter defined for INPUT element of type '"+d.type+"'. (Element '"+d.name+"' used in tiddler '"+b+"')")}}window.onFormTiddlerChange=onFormTiddlerChange;setStylesheet(".formTiddlerError{color: #ffffff;background-color: #880000;}","formTiddler");config.macros.checkForDataTiddlerPlugin={label:"checkForDataTiddlerPlugin",version:{major:1,minor:0,revision:0,date:new Date(2005,12,14)},prompt:"Check if the DataTiddlerPlugin exists"};config.macros.checkForDataTiddlerPlugin.handler=function(a,b,c){config.macros.formTiddler.checkForExtensions(a,config.macros.formTiddler.label)};config.macros.newTiddlerWithForm={label:"newTiddlerWithForm",version:{major:1,minor:0,revision:1,date:new Date(2006,1,6)},prompt:"Creates a new Tiddler with a <<formTiddler ...>> macro"};config.macros.newTiddlerWithForm.handler=function(place,macroName,params){var i=0;var formTemplateName=undefined;if(i<params.length){formTemplateName=params[i];i++}if(!formTemplateName){config.macros.formTiddler.createErrorElement(place,"No form template specified in <<"+macroName+">>.");return}var buttonLabel=undefined;if(i<params.length){buttonLabel=params[i];i++}if(!buttonLabel){config.macros.formTiddler.createErrorElement(place,"No button label specified in <<"+macroName+">>.");return}var tiddlerNameScript=undefined;var askUser=false;if(i<params.length){tiddlerNameScript=params[i];i++;if(i<params.length&&params[i]=="askUser"){askUser=true;i++}}if(!readOnly){var onClick=function(){var tiddlerName=undefined;if(tiddlerNameScript){try{tiddlerName=eval(tiddlerNameScript)}catch(ex){}}if(!tiddlerName||askUser){tiddlerName=prompt("Please specify a tiddler name.",askUser?tiddlerName:"")}while(tiddlerName&&store.getTiddler(tiddlerName)){tiddlerName=prompt("A tiddler named '"+tiddlerName+"' already exists.\n\nPlease specify a tiddler name.",tiddlerName)}if(tiddlerName){var body="<<formTiddler [["+formTemplateName+"]]>>";var tags=[];store.saveTiddler(tiddlerName,tiddlerName,body,config.options.txtUserName,new Date(),tags);story.displayTiddler(null,tiddlerName,1)}};createTiddlyButton(place,buttonLabel,buttonLabel,onClick)}};
//%/
/***
|Name:|HideWhenPlugin|
|Description:|Allows conditional inclusion/exclusion in templates|
|Version:|3.2a|
|Date:|27-Jun-2011|
|Source:|http://mptw.tiddlyspot.com/#HideWhenPlugin|
|Author:|Simon Baird <simon.baird@gmail.com>|
|License:|http://mptw.tiddlyspot.com/#TheBSDLicense|
For use in ViewTemplate and EditTemplate. Example usage:
{{{<div macro="showWhenTagged Task">[[TaskToolbar]]</div>}}}
{{{<div macro="showWhen tiddler.modifier == 'BartSimpson'"><img src="bart.gif"/></div>}}}

Warning: the showWhen and hideWhen macros will blindly eval paramString.
This could be used to execute harmful javascript from a tiddler.

(TODO: Make some effort to sanitize paramString. Perhaps disallow the equals sign?)
***/
//{{{

window.hideWhenLastTest = false;

window.removeElementWhen = function(test,place) {
  window.hideWhenLastTest = test;
  if (test) {
    jQuery(place).empty()
    place.parentNode.removeChild(place);
  }
};

merge(config.macros,{

  hideWhen: { handler: function(place,macroName,params,wikifier,paramString,tiddler) {
    removeElementWhen( eval(paramString), place );
  }},

  showWhen: { handler: function(place,macroName,params,wikifier,paramString,tiddler) {
    removeElementWhen( !eval(paramString), place );
  }},

  hideWhenTagged: { handler: function (place,macroName,params,wikifier,paramString,tiddler) {
    removeElementWhen( tiddler.tags.containsAll(params), place );
  }},

  showWhenTagged: { handler: function (place,macroName,params,wikifier,paramString,tiddler) {
    removeElementWhen( !tiddler.tags.containsAll(params), place );
  }},

  hideWhenTaggedAny: { handler: function (place,macroName,params,wikifier,paramString,tiddler) {
    removeElementWhen( tiddler.tags.containsAny(params), place );
  }},

  showWhenTaggedAny: { handler: function (place,macroName,params,wikifier,paramString,tiddler) {
    removeElementWhen( !tiddler.tags.containsAny(params), place );
  }},

  hideWhenTaggedAll: { handler: function (place,macroName,params,wikifier,paramString,tiddler) {
    removeElementWhen( tiddler.tags.containsAll(params), place );
  }},

  showWhenTaggedAll: { handler: function (place,macroName,params,wikifier,paramString,tiddler) {
    removeElementWhen( !tiddler.tags.containsAll(params), place );
  }},

  hideWhenExists: { handler: function(place,macroName,params,wikifier,paramString,tiddler) {
    removeElementWhen( store.tiddlerExists(params[0]) || store.isShadowTiddler(params[0]), place );
  }},

  showWhenExists: { handler: function(place,macroName,params,wikifier,paramString,tiddler) {
    removeElementWhen( !(store.tiddlerExists(params[0]) || store.isShadowTiddler(params[0])), place );
  }},

  hideWhenTitleIs: { handler: function(place,macroName,params,wikifier,paramString,tiddler) {
    removeElementWhen( tiddler.title == params[0], place );
  }},

  showWhenTitleIs: { handler: function(place,macroName,params,wikifier,paramString,tiddler) {
    removeElementWhen( tiddler.title != params[0], place );
  }},

  'else': { handler: function(place,macroName,params,wikifier,paramString,tiddler) {
    removeElementWhen( !window.hideWhenLastTest, place );
  }}

});

//}}}
/***
|''Name:''|LoadRemoteFileThroughProxy (previous LoadRemoteFileHijack)|
|''Description:''|When the TiddlyWiki file is located on the web (view over http) the content of [[SiteProxy]] tiddler is added in front of the file url. If [[SiteProxy]] does not exist "/proxy/" is added. |
|''Version:''|1.1.0|
|''Date:''|mar 17, 2007|
|''Source:''|http://tiddlywiki.bidix.info/#LoadRemoteFileHijack|
|''Author:''|BidiX (BidiX (at) bidix (dot) info)|
|''License:''|[[BSD open source license|http://tiddlywiki.bidix.info/#%5B%5BBSD%20open%20source%20license%5D%5D ]]|
|''~CoreVersion:''|2.2.0|
***/
//{{{
version.extensions.LoadRemoteFileThroughProxy = {
 major: 1, minor: 1, revision: 0, 
 date: new Date("mar 17, 2007"), 
 source: "http://tiddlywiki.bidix.info/#LoadRemoteFileThroughProxy"};

if (!window.bidix) window.bidix = {}; // bidix namespace
if (!bidix.core) bidix.core = {};

bidix.core.loadRemoteFile = loadRemoteFile;
loadRemoteFile = function(url,callback,params)
{
 if ((document.location.toString().substr(0,4) == "http") && (url.substr(0,4) == "http")){ 
 url = store.getTiddlerText("SiteProxy", "/proxy/") + url;
 }
 return bidix.core.loadRemoteFile(url,callback,params);
}
//}}}
/***
|Name|[[MoveablePanelPlugin]]|
|Source|http://www.TiddlyTools.com/#MoveablePanelPlugin|
|Documentation|http://www.TiddlyTools.com/#MoveablePanelPluginInfo|
|Version|3.0.4|
|Author|Eric Shulman|
|License|http://www.TiddlyTools.com/#LegalStatements|
|~CoreVersion|2.1|
|Type|plugin|
|Description|move/size any tiddler or page element|
Use the mouse to move/resize any specific tiddler content, page element, or [[floating slider panel|NestedSlidersPlugin]].
!!!!!Documentation
>see [[MoveablePanelPluginInfo]]
!!!!!Configuration
<<<
<<option chkMoveablePanelShowStatus>> show position/size while moving/resizing a panel
<<option chkMoveablePanelShowManager>> automatically add Panel Manager button to undocked panels (see [[PanelManagerPlugin]])
<<<
!!!!!Revisions
<<<
2010.12.24 3.0.4 fixed findMouseX/findMouseY for webkit browsers
|please see [[MoveablePanelPluginInfo]] for additional revision details|
2006.03.04 1.0.0 Initial public release
<<<
!!!!!Code
***/
//{{{
version.extensions.MoveablePanelPlugin= {major: 3, minor: 0, revision: 4, date: new Date(2010,12,24)};
if (config.macros.moveablePanel===undefined) config.macros.moveablePanel={};
//}}}
// // translate
//{{{
// TRANSLATORS: copy this section to MoveablePanelPluginLingoXX (where 'XX' is a language/country code)
if (config.macros.moveablePanel===undefined) config.macros.moveablePanel={};
merge(config.macros.moveablePanel,{

	foldLabel:	'\u2212', // minus
	foldTip:	'FOLD=reduce panel size',
	unfoldLabel:	'+',
	unfoldTip:	'UNFOLD=restore panel size',
	hoverLabel:	'^',
	hoverTip:	'HOVER=keep panel in view when scrolling',
	scrollLabel:	'\u2248', // asymp
	scrollTip:	'SCROLL=allow panel to move with page',
	closeLabel:	'X',
	closeTip:	'CLOSE=hide this panel',
	dockLabel:	'\u221A', // radic
	dockTip:	'DOCK=reset size/position',

	noPid:		'unnamed panel',

	statusMsg:	'%0: pos=(%1,%2)%3 size=(%4,%5) z=%6',
	hoveredMsg:	'[hovering]',
	dockedTip:	'%0: docked',
	scrollMsg:	'%0: pos=(%1,%2)',
	msgDuration:	3000,

	moveTip:	'%0DRAG EDGE=move',
	sizeTip:	'(SHIFT=resize)',
	sizeWidthTip:	'(SHIFT=resize width)',
	sizeHeightTip:	'(SHIFT=resize height)',
	clickTip:	  'CLICK=bring to front, SHIFT-CLICK=send to back',
	dblclickdockTip:  'DOUBLE-CLICK=dock',
	dblclickunfoldTip:'DOUBLE-CLICK=unfold',

	foldParam:	'fold',
	hoverParam:	'hover',
	nocloseParam:	'noclose',
	nodockParam:	'nodock',
	undockedParam:	'undocked',

	jumpParam:	'jump',
	dockParam:	'dock',
	moveParam:	'move',
	labelParam:	'label',
	promptParam:	'prompt',

	allParam:	'all',
	nameParam:	'name',
	topParam:	'top',
	leftParam:	'left',
	widthParam:	'width',
	heightParam:	'height',

	managerParam:	'manager'
});
//}}}
// // global functions (general utilities)
//{{{
// if removeCookie() function is not defined by TW core, define it here (for <TW2.5)
if (window.removeCookie===undefined) {
	window.removeCookie=function(name) {
		document.cookie = name+'=; expires=Thu, 01-Jan-1970 00:00:01 UTC; path=/;'; 
	}
}
if (window.copyObject===undefined) {
	window.copyObject=function(src)	{
		for (var i in src) this[i]=typeof src[i]!='object'?src[i]:new copyObject(src[i]);
	}
}
if (window.compareObjects===undefined) {
	window.compareObjects=function(a,b) {
		if (a===b) return true;
		if (a==undefined||b==undefined) return false;
		for (var i in a) if (typeof a[i]!='object'?a[i]!==b[i]:!compareObjects(a[i],b[i])) return false;
		return true;
	}
}
if (window.isEmptyObject===undefined) {
	window.isEmptyObject=function(src) { for (var i in src) return false; return true; }
}

// cross-browser metrics
window.findMouseX=function(ev) { if (!ev) return 0; var x=0;
	if (config.browser.isIE)	return ev.clientX+findScrollX();// IE
	if (config.browser.isSafari) 	return ev.pageX+findScrollX(); 	// Webkit
	else				return ev.pageX;		// Firefox/other
}
window.findMouseY=function(ev){ if (!ev) return 0; var y=0;
	if (config.browser.isIE)	return ev.clientY+findScrollY();// IE
	if (config.browser.isSafari) 	return ev.pageY+findScrollY();	// Webkit
	else				return ev.pageY;		// Firefox/other
}
//}}}
// // macro
//{{{
merge(config.macros.moveablePanel,{
	handler: function(place,macroName,params,wikifier,paramString,tiddler) {

		// ALTERNATIVE OUTPUT: Panel Manager macro extensions...
		if (this.manager && this.manager.handler(place,macroName,params,wikifier,paramString,tiddler))
			return; // processed by PanelManager

		// UNPACK KEYWORD PARAMS
		var showfold	 =params.contains(this.foldParam);
		var showhover	 =params.contains(this.hoverParam);
		var showclose	 =!params.contains(this.nocloseParam);
		var showdock	 =!params.contains(this.nodockParam);
		var showmanager  =params.contains(this.managerParam);
		var startundocked=params.contains(this.undockedParam);
		var jump	 =params.contains(this.jumpParam);
		var dock	 =params.contains(this.dockParam);
		var move	 =params.contains(this.moveParam);
		var all		 =params.contains(this.allParam);

		// UNPACK VALUE PARAMS
		params=paramString.parseParams('anon',null,true,false,false);
		var label =getParam(params,this.labelParam,null);
		var prompt=getParam(params,this.promptParam,null);
		var name  =getParam(params,this.nameParam,null);
		var top   =getParam(params,this.topParam,null);
		var left  =getParam(params,this.leftParam,null);
		var width =getParam(params,this.widthParam,null);
		var height=getParam(params,this.heightParam,null);

		// COMMANDS: JUMP, MOVE, DOCK
		if (jump||move||dock) {
			if (!label||!label.length) {
				var p=this.findPanel(name);
				if (jump) { if (p) this.scrollToPanel(p,true); else window.scrollTo(left,top); }
				if (move) { this.movePanel(p,left,top,true,true); }
				if (dock) { if (all) this.forAllPanels(this.dockPanel); else if (p) this.dockPanel(p); }
				return;
			}
			var tip=(jump?this.jumpParam:move?this.moveParam:dock?this.dockParam:'')+': '+name;
			var b=createTiddlyButton(place,label,prompt||tip, function(ev) {
				var cmm=config.macros.moveablePanel; var p=cmm.findPanel(this.name);
				if (this.jump) { if (p) cmm.scrollToPanel(p,true); else window.scrollTo(this.left,this.top); }
				if (this.move) { if (p) cmm.movePanel(p,this.left,this.top,true,true); }
				if (this.dock) { if (p) cmm.dockPanel(p); else if (this.all) cmm.forAllPanels(cmm.dockPanel); }
				return cmm.processed(ev)
			},'button');
			b.jump=jump; b.move=move; b.dock=dock;
			b.name=name; b.all=all;   b.left=left; b.top=top;
			return;
		}

		// PANEL SETUP
		var p=this.getPanel(place);
		this.cachePanel(p);
		addClass(p,'moveablePanel');
		p.pid=name;
		p.showmanager=showmanager;
		p.fixedheight=height||undefined;
		p.fixedwidth=width||undefined;
		this.addPanelButtons(p,showfold,showhover,showclose,showdock,showmanager);
		this.addMouseHandlers(p);
		if (startundocked) {
			this.undockPanel(p);
			if (!startingUp) { this.bringPanelToFront(p); this.scrollToPanel(p); }
		}
		if (this.manager) { this.manager.applyMap(p); this.manager.trackMap(p); }
		this.notify(p);
	},
//}}}
// // notifications
//{{{
	quiet: 0, // flag to suspend/resume notifications
	notify: function(p) { // notify others of panel changes
		if (this.quiet) return;
		if (this.manager) this.manager.notify(p); // pass notices to manager (updates viewers)
	},
//}}}
// // general panel utilities
//{{{
	getPanel: function(place) { // find containing panel or floating slider (use current element as fallback)
		var p=place;
		while (p && !(hasClass(p,'moveablePanel')||hasClass(p,'floatingPanel'))) p=p.parentNode;
		return p||place;
	},
	getAllPanels: function(zSort) { // find 'moveablePanel' elements (optionally sort by zIndex)
		var panels=[];
		var sortByZindex=function(a,b){
			var v1=parseInt(a.style.zIndex); if (isNaN(v1)) v1=0;
			var v2=parseInt(b.style.zIndex); if (isNaN(v2)) v2=0;
			return(v1==v2)?0:(v1>v2?1:-1);
		}
		// if native browser fn is defined, use it (*much* more efficient!)
		if (document.getElementsByClassName) { 
			var elems=document.getElementsByClassName('moveablePanel');
			for (var i=0; i<elems.length; i++) panels.push(elems[i]);
			return zSort?panels.sort(sortByZindex):panels;
		}
		// otherwise, find all DIVs and SPANs with the right class
		// NOTE: IE requires use of Enumerator() to iterate over elements, or it FREEZES UP COMPLETELY!!
		var isIE=config.browser.isIE;
		var elems=document.getElementsByTagName('DIV');
		for (var i=isIE?new Enumerator(elems):0; isIE?!i.atEnd():(i<elems.length); isIE?i.moveNext():i++) {
			var panel=isIE?i.item():elems[i];
			if (hasClass(panel,'moveablePanel')) panels.push(panel);
		}
		var elems=document.getElementsByTagName('SPAN');
		for (var i=isIE?new Enumerator(elems):0; isIE?!i.atEnd():(i<elems.length); isIE?i.moveNext():i++) {
			var panel=isIE?i.item():elems[i];
			if (hasClass(panel,'moveablePanel')) panels.push(panel);
		}
		return zSort?panels.sort(sortByZindex):panels;
	},
	findPanel: function(pid) { // find a named panel
		var p=this.getAllPanels();
		for (var i=0; i<p.length; i++) { if (pid && p[i].pid==pid) return p[i]; }
		return undefined;
	},
	forAllPanels: function(callback) { // invoke a function on each panel
		var panels=this.getAllPanels();
		this.quiet++;
		for (var i=0; i<panels.length; i++) callback.apply(this,[panels[i]]);
		this.quiet--;
		this.notify('all');
	},
	cachePanel: function(p) { // save original styles and handlers
		if (!p.saved) p.saved={ 
			x:p.style.left||'', y:p.style.top||'', w:p.style.width||'', h:p.style.height||'',
			z:p.style.zIndex||'', pos:p.style.position||'', title: p.title,
			mouseover:p.onmouseover, mouseout:p.onmouseout,
			mousedown:p.onmousedown, mousemove:p.onmousemove, dblclick:p.ondblclick
		};
	},
	restorePanel: function(p) { // restore original styles
		if (!p.saved) return;
		p.style.left=p.saved.x; p.style.top=p.saved.y; p.style.width=p.saved.w; p.style.height=p.saved.h;
		p.style.zIndex=p.saved.z; p.style.position=p.saved.pos; p.title=p.saved.title;
		removeClass(p,'folded'); removeClass(p,'hover'); removeClass(p,'undocked');
	},
//}}}
// // panel metrics
//{{{
	getPanelOffset: function(p) { // adjustment for child elements inside relative/floatingPanel containers
		var r=new Object(); r.x=0; r.y=0; if (!p) return r;
		var pp=p.parentNode; while (pp && !(pp.style&&pp.style.position=='relative')) pp=pp.parentNode;
		if (pp) { r.x+=findPosX(pp); r.y+=findPosY(pp); }
		var pp=p.parentNode; while (pp && !hasClass(pp,'floatingPanel')) pp=pp.parentNode;
		if (pp) { r.x+=findPosX(pp); r.y+=findPosY(pp); }
		return r;
	},
	// PROBLEM: the offsetWidth/offsetHeight do not seem to account for padding or borders
	// WORKAROUND: subtract padding and border (in px) from width and height
	// ISSUE: I still don't understand why this is needed...
	// TBD: get padding/border values from p.style and convert to px
	// NOTE: 10.6667 seems to be about 1em...
	getPanelEdgeWidth:
	  	function(p) { return 10.6667; },
	getPanelEdgeHeight:
		function(p) { return 10.6667; },
	getPanelHeight:
		function(p) { var pad=10.6667; var border=1; return p.offsetHeight-(pad*2+border*2); },
	getPanelWidth:
		function(p) { var pad=10.6667; var border=1; return p.offsetWidth -(pad*2+border*2); },
//}}}
// // panel stacking (zIndex)
//{{{
	isStackable: function(p) { // zIndex is only effective with absolute or fixed elements
		return (['absolute','fixed'].contains(p.style.position)&&!hasClass(p,'popup'));
	},
	normalizeStack: function(panels) { // set zIndex to correspond to stack order
		for (var i=0; i<panels.length; i++) {var z=panels[i].style.zIndex;
			if (z==0||z=='auto') continue; // if not stacking (e.g., 'auto', '', or null)
			if (z<10000 || z>10000) continue; // use large values for "always in front/back"
			if (z!=i+2) panels[i].style.zIndex=i+2;
			if (this.manager) this.manager.trackMap(panels[i]);
		}
		return panels;
	},
	bringPanelToFront: function(p) { if (!p) return;
		if (!this.isStackable(p)) return; // can't be stacked
		var panels=this.getAllPanels(true);
// WFFL - normalizing every time works, but takes too long
//		if (p.style.zIndex>panels.length+2) return; // stay in front
//		this.normalizeStack(panels);
//		p.style.zIndex=panels.length+2;
// WFFL - for now, just bump up the max (ignore z>10000) and normalize much less often
		if (p.style.zIndex>1000) this.normalizeStack(panels);
		var zMax=0; if (panels.length) {
			var i=panels.length-1; zMax=parseInt(panels[i].style.zIndex);
			while (zMax>10000 && i>0) zMax=parseInt(panels[--i].style.zIndex);
			if (p==panels[i]) return; // already in front
			if (isNaN(zMax)) zMax=0;
		}
		p.style.zIndex=zMax+1;
		this.notify(p);
	},
	sendPanelToBack: function(p) { if (!p) return;
		if (!this.isStackable(p)) return; // can't be stacked
		var panels=this.getAllPanels(true);
// WFFL - normalizing every time works, but takes too long
//		if (p.style.zIndex<2) return; // stay in back
//		this.normalizeStack(panels);
//		p.style.zIndex=1;
// WFFL - for now, just bump down the min (ignore z<10000) and normalize much less often
		if (p.style.zIndex<1000) this.normalizeStack(panels);
		var zMin=0; if (panels.length) {
			var i=0; zMin=parseInt(panels[i].style.zIndex);
			while (zMin<10000 && i<panels.length-1) zMin=parseInt(panels[++i].style.zIndex);
			if (p==panels[i]) return; // already in back
			if (isNaN(zMin)) zMin=0;
		}
		p.style.zIndex=zMin-1;
		this.notify(p);
	},
	returnPanelToStack: function(p) { if (!p) return;
		p.style.zIndex=p.saved?p.saved.zIndex:'';
		this.notify(p);
	},
//}}}
// // panel scrolling 
//{{{
	noScrollX: 0, // flags to disable TW built-in scrolling behavior
	noScrollY: 0, // set by hijacks, cleared by ensurePanelVisible(), below
	// scroll view to show panel along nearest edge of window or centered (optional)
	scrollToPanel: function(p,center) { if (!p) return;
		if (hasClass(p,'popup')) return; // popup=let core scrolling handle it
		if (hasClass(p,'hover')) return; // hover=always in view=don't scroll
		var scrollSize=findWindowWidth()-document.body.offsetWidth; // width of scrollbar
		var sx=findScrollX();	var ww=findWindowWidth()-scrollSize;
		var sy=findScrollY();	var wh=findWindowHeight()-scrollSize;
		var px=findPosX(p);	var pw=p.offsetWidth;
		var py=findPosY(p);	var ph=p.offsetHeight;
		var nx=sx; var ny=sy; // assume no scrolling is needed
		// if BR is not in view, scroll to show BR
		if (px+pw>sx+ww) nx=px+pw-ww;
		if (py+ph>sy+wh) ny=py+ph-wh;
		// if TL not in view or too big... scroll to show TL
		if (px<nx || px>nx+ww || px+pw>nx+ww) nx=px;
		if (py<ny || py>ny+wh || py+ph>ny+wh) ny=py;
		// optionally, center in view (if panel fits)
		if (center && pw<ww) nx-=(ww-pw)/2;
		if (center && ph<wh) ny-=(wh-ph)/2;
		if (nx!=sx||ny!=sy) { // if we need to scroll...
			window.scrollTo(nx,ny);
			if (config.options.chkMoveablePanelShowStatus && !startingUp) {
				var id=hasClass(p,'tiddler')?p.getAttribute('tiddler'):p.pid;
				this.timedMessage(this.scrollMsg.format([id||this.noPid,px,py]),this.msgDuration);
			}
			this.notify(p);
		}
	},
	// bring to front and scroll into view (with optional ASYNC)
	ensurePanelVisible: function(p,delay) { if (!p) return;
		if (delay && !startingUp) { // wait for core animation to complete...
			if (hasClass(p,'tiddler'))
				p=config.macros.moveablePanel.findPanel(p.getAttribute('tiddler'))||p;
			if (!p.id) p.id=new Date().getTime()+Math.random(); // unique ID
			var code='config.macros.moveablePanel.ensurePanelVisible(document.getElementById("%0"));';
			setTimeout(code.format([p.id]),delay);
			return;
		}
		// unblock scrolling and bring the panel into view
		if (this.noScrollX>0) this.noScrollX--; if (this.noScrollY>0) this.noScrollY--;
		if (hasClass(p,'popup')) return; // leave popups alone!
		this.bringPanelToFront(p);
		if (this.noScrollX+this.noScrollY==0 && !startingUp) // no scroll during document startup
			this.scrollToPanel(p);
	},
//}}}
// // panel status
//{{{
	formatPanelStatus: function(p) {
		var s=p.style; var msg=this.statusMsg.format([p.pid||this.noPid,
			s.left,s.top,hasClass(p,'hover')?this.hoveredMsg:'',s.width,s.height,s.zIndex]);
		return msg.replace(/(\.[0-9]+)|px/g,''); // remove decimals and 'px'
	},
	showPanelStatus: function(p,show) { // display panel info in titlebar while moving/sizing
		if (!config.options.chkMoveablePanelShowStatus) return;
		if (show) document.title=this.formatPanelStatus(p)
		else refreshPageTitle();
	},
	timedMessage: function(msg,duration) {
		document.title=msg; setTimeout('refreshPageTitle()',duration);
	},
	getPanelTooltip: function(p) {
		return hasClass(p,'undocked')?this.formatPanelStatus(p):this.dockedTip.format([p.pid||this.noPid]);
	},
//}}}
// // panel actions
//{{{
	undockPanel: function(p,front) { // undocked with default pos/size
		if (hasClass(p,'undocked')) return; // already undocked
		// get size BEFORE undocking
		p.style.width=p.fixedwidth  ||(this.getPanelWidth(p)+'px');
		p.style.height=p.fixedheight||(this.getPanelHeight(p)+'px');
		addClass(p,'undocked');	if (!this.isStackable(p)) p.style.position='absolute'; // UNDOCK it
		// set position AFTER undocking
		var offset=this.getPanelOffset(p);
		p.style.left=findPosX(p)-offset.x+'px'; p.style.top=findPosY(p)-offset.y+'px';
		if (front) this.bringPanelToFront(p);
		this.notify(p);
	},
	dockPanel: function(p) { // reset to docked pos/size
		if (!hasClass(p,'undocked')) return; // already docked
		this.restorePanel(p); // reset panel
		// FOR FLOATING SLIDERS: trigger slider adjustment handler (if any)
		if (hasClass(p,'floatingPanel') && window.adjustSliderPos)
			window.adjustSliderPos(p.parentNode,p.button,p);
		this.quiet++; if (this.manager) this.manager.trackMap(p); this.quiet--;
		this.notify(p)
	},
	closePanel: function(p) { // dock panel, then close (for tiddlers and floating sliders)
		var t=story.findContainingTiddler(p);
		var isTiddler=t&&this.findPanel(t.getAttribute('tiddler'));
		var isFloating=hasClass(p,'floatingPanel');
		if (!isTiddler) // when closing TIDDLERS, leave them undocked (keeps size/pos)
			this.dockPanel(p);
		// FOR FLOATING SLIDERS: set focus and do a fake click on slider button
		if (isFloating) { p.button.focus(); onClickNestedSlider({target:p.button}); }
		// FOR TIDDLERS: call story.closeTiddler()
		if (isTiddler) { story.closeTiddler(t.getAttribute('tiddler')); }
	},
	movePanel: function(p,x,y,show,centered) { if (!p) return;
		this.quiet++;
		this.undockPanel(p);
		// adjust for child elements inside relative/floatingPanel containers
		var offset=this.getPanelOffset(p);
		p.style.left=x-offset.x+'px'; p.style.top=y-offset.y+'px';
		if (show) { this.bringPanelToFront(p); this.scrollToPanel(p,centered); }
		this.quiet--;
		this.showPanelStatus(p,true);
		if (this.manager) this.manager.trackMap(p);
	},
	foldPanel: function(p) { // toggle panel height
		if (hasClass(p,'folded')) removeClass(p,'folded'); else addClass(p,'folded');
		if (this.manager) this.manager.trackMap(p);
		this.notify(p);
	},
	hoverPanel: function(p) { // toggle fixed position
		if (hasClass(p,'hover')) {
			removeClass(p,'hover');
			var offset=this.getPanelOffset(p);
			p.style.left=p.offsetLeft+findScrollX()-offset.x+'px';
			p.style.top=p.offsetTop+findScrollY()-offset.y+'px';
		} else {
			var offset=this.getPanelOffset(p);
			var ww=findWindowWidth(); var wh=findWindowHeight();
			p.style.left=(p.offsetLeft-findScrollX()+offset.x)%ww+'px';
			p.style.top =(p.offsetTop -findScrollY()+offset.y)%wh+'px';
			addClass(p,'hover'); 
		}
		if (this.manager) this.manager.trackMap(p);
		this.notify(p);
	},
	resetPanel: function(p) { // reset to session starting pos/size
		if (this.manager) this.manager.resetPanel(p); else this.dockPanel(p);
	},
//}}}
// // menu buttons
//{{{
	processed: function(ev) { var ev=ev||window.event; // use to end event handling for menus and mouse actions
		if (ev) { ev.cancelBubble=true; if (ev.stopPropagation) ev.stopPropagation(); } return false;
	},
	addPanelButtons: function(p,showfold,showhover,showclose,showdock,showmanager) {
		if (p.menu) return; // only once per panel
		function cmd(menu,label,tip,callback,show,arg) {
			var fn=function(ev){return this.callback.apply(config.macros.moveablePanel,[this,ev,this.arg]);}
			var b=createTiddlyButton(menu,label,tip,fn,'moveablePanelButton');
			b.style.display=show?'inline':'none'; b.callback=callback; b.arg=arg;
			return b;
		}
		var m=createTiddlyElement(p,'div',null,'moveablePanelMenu');
		p.showfold=showfold;
		p.foldbutton= cmd(m,this.foldLabel,this.foldTip,this.foldHandler,showfold);
		p.unfoldbutton= cmd(m,this.unfoldLabel,this.unfoldTip,this.foldHandler,false);
		p.showhover=showhover;
		p.hoverbutton=cmd(m,this.hoverLabel,this.hoverTip,this.hoverHandler,showhover);
		p.scrollbutton=cmd(m,this.scrollLabel,this.scrollTip,this.hoverHandler,false);
		p.showdock=showdock;
		p.dockbutton= cmd(m,this.dockLabel,this.dockTip,this.dockHandler,showdock);
		p.showclose=showclose;
		p.closebutton=cmd(m,this.closeLabel,this.closeTip,this.closeHandler,showclose);
		p.showmanager=showmanager;
		if (this.manager) p.managerbutton=cmd(m,this.manager.buttonLabel,this.manager.buttonTip,
			this.manager.popup,showmanager,p.pid);
		p.menu=m;
	},
	togglePanelButtons: function(p,show) { if (!p||!p.menu) return;
		var undocked=hasClass(p,'undocked');
		var floating=hasClass(p,'floatingPanel');
		var hover=hasClass(p,'hover');
		var folded=hasClass(p,'folded');
		var t=story.findContainingTiddler(p);
		var tiddler=t&&this.findPanel(t.getAttribute('tiddler'));
		var show=show&&(undocked||floating);
		p.menu.style.display=show?'inline':'none';
		if (p.showfold)  p.foldbutton.style.display  =!folded?'inline':'none';
		if (p.showfold)  p.unfoldbutton.style.display= folded?'inline':'none';
		if (p.showhover) p.hoverbutton.style.display =!hover?'inline':'none';
		if (p.showhover) p.scrollbutton.style.display= hover?'inline':'none';
		if (p.showdock)  p.dockbutton.style.display =undocked?'inline':'none';
		if (p.showclose) p.closebutton.style.display=floating||(tiddler&&undocked)?'inline':'none';
		if (p.managerbutton) { // see [[PanelManagerPlugin]]
			var show=p.showmanager||config.options.chkMoveablePanelShowManager;
			p.managerbutton.style.display=show?'inline':'none';
		}
	},
	foldHandler: function(place,ev){ var p=this.getPanel(place);
		this.foldPanel(p); this.togglePanelButtons(p,true); return this.processed(ev); },
	hoverHandler: function(place,ev){ var p=this.getPanel(place);
		this.hoverPanel(p); this.togglePanelButtons(p,true); return this.processed(ev); },
	dockHandler: function(place,ev){ var p=this.getPanel(place);
		this.dockPanel(p); this.togglePanelButtons(p,true); return this.processed(ev); },
	closeHandler: function(place,ev){ var p=this.getPanel(place);
		this.closePanel(p); this.togglePanelButtons(p,true); return this.processed(ev); },
//}}}
// // mouse handlers
//{{{
	addMouseHandlers: function(p) {
		if (p.handlers) return true; // only add handlers ONCE
		p.onmouseover=function(ev) { var ev=ev||window.event;
			var r=config.macros.moveablePanel.mouseover(this,ev);
			return r&&this.saved.mouseover?this.saved.mouseover.apply(this,arguments):true;
		};
		p.onmouseout=function(ev) { var ev=ev||window.event;
			var r=config.macros.moveablePanel.mouseout(this,ev);
			return r&&this.saved.mouseout?this.saved.mouseout.apply(this,arguments):true;
		};
		p.onmousemove=function(ev) { var ev=ev||window.event;
			var r=config.macros.moveablePanel.mousemove(this,ev);
			return r&&this.saved.mousemove?this.saved.mousemove.apply(this,arguments):true;
		};
		p.ondblclick=function(ev) { var ev=ev||window.event;
			var r=config.macros.moveablePanel.dblclick(this,ev);
			return r&&this.saved.dblclick?this.saved.dblclick.apply(this,arguments):r;
		};
		p.onmousedown=function(ev) { var ev=ev||window.event;
			var r=config.macros.moveablePanel.mousedown(this,ev);
			return r&&this.saved.mousedown?this.saved.mousedown.apply(this,arguments):r;
		};
		p.handlers=true;
	},
	isEdge: function(p,ev) { // near 'edge' of panel (or child element)?
		var ev=ev||window.event; var target=resolveTarget(ev);
		if (!p) return false;
		// ignore form input fields
		if (['input','select','option','textarea'].contains(target.nodeName.toLowerCase())) return false;
		var left=findPosX(p); var top=findPosY(p);
		var width=p.offsetWidth; var height=p.offsetHeight;
		var x=findMouseX(ev); var y=findMouseY(ev);
		if (hasClass(p,'hover')) { x-=findScrollX(); y-=findScrollY(); } // window-relative panel
		if (x<left||y<top||x>=left+width||y>=top+height) { // outside of panel
			if (p==target || p!=this.getPanel(target)) return false;
			return this.isEdge(target,ev); // check target child element
		}
		var edgeW=this.getPanelEdgeWidth(p); var edgeH=this.getPanelEdgeHeight(p);
		var isT=(y-top<edgeH); var isL=(x-left<edgeW);
		var isB=(top+height-y<edgeH); var isR=(left+width-x<edgeW);
		return isT||isL||isB||isR;
	},
	// temporary element during move/size keeps document from shrinking 
	addGhost: function(p) {
		var g=document.getElementById('moveablePanelGhost');
		if (!g) g=createTiddlyElement(document.body,'div','moveablePanelGhost','moveablePanelGhost');
		var border=1; // note: must match CSS for 'moveablePanelGhost' WFFL-HACK
		g.style.left=findPosX(p)+'px';
		g.style.top=findPosY(p)+'px';
		g.style.width=((p.offsetWidth-border*2)||0)+'px';
		g.style.height=((p.offsetHeight-border*2)||0)+'px';
	},
	clearGhost: function() {
		var e=document.getElementById('moveablePanelGhost');
		if (e) e.parentNode.removeChild(e);
	},
	// MOUSEOVER=SHOW MENU
	mouseover: function(place,ev) { var ev=ev||window.event;
		var p=this.getPanel(place);
		addClass(p,'selected'); // shows toolbar-classed items
		this.togglePanelButtons(p,true);
		return true;
	},
	// MOUSEOUT=HIDE MENU
	mouseout: function(place,ev) { var ev=ev||window.event;
		var p=this.getPanel(place);
		removeClass(p,'selected'); // hides toolbar-classed items
		this.togglePanelButtons(p,false);
		return true;
	},
	// MOUSEMOVE=SHOW MENU AND SET CURSOR/TIP
	mousemove: function(place,ev) { var ev=ev||window.event;
		var p=this.getPanel(place);
		p.style.cursor='auto'; p.title=p.saved?p.saved.title:'';
		if (!this.isEdge(p,ev)) return true;
		var fw=p.fixedwidth;  if (fw==null) fw=undefined;
		var fh=p.fixedheight; if (fh==null) fh=undefined;

		p.title=this.moveTip.format([p.pid?p.pid+': ':'']);
		if (fw===undefined&&fh===undefined) p.title+=' '+this.sizeTip;
		else if  (fw===undefined) p.title+=' '+this.sizeWidthTip;
		else if  (fh===undefined) p.title+=' '+this.sizeHeightTip;
		if (hasClass(p,'undocked')) {
			p.title+=', '+this.clickTip+', ';
			p.title+=hasClass(p,'folded')?this.dblclickunfoldTip:this.dblclickdockTip;
		}
		p.style.cursor='move';
		if (ev.shiftKey&&!(fw&&fh)) { // set resizing cursor (if not fixed width/height)
			var left=findPosX(p); var top=findPosY(p);
			var width=p.offsetWidth; var height=p.offsetHeight;
			var x=findMouseX(ev); var y=findMouseY(ev);
			if (hasClass(p,'hover')) { x-=findScrollX(); y-=findScrollY(); } // window-relative panel
			var edgeW=this.getPanelEdgeWidth(p); var edgeH=this.getPanelEdgeHeight(p);
			var isT=(y-top<edgeH); var isL=(x-left<edgeW);
			var isB=(top+height-y<edgeH); var isR=(left+width-x<edgeW);
			p.style.cursor=(fh===undefined?(isT?'n':(isB?'s':'')):'')
				+(fw===undefined?(isL?'w':(isR?'e':'')):'')+'-resize';
		}
		return true;
	},
	// DOUBLE-CLICK=DOCK OR UNFOLD
	dblclick: function(place,ev) { var ev=ev||window.event;
		var p=this.getPanel(place);
		if (!this.isEdge(p,ev)) return true;
		// if folded... unfold, otherwise... undock
		if (hasClass(p,'folded')) this.foldPanel(p); else this.dockPanel(p);
		this.togglePanelButtons(p,false);
		return this.processed(ev);
	},
	// MOUSEDOWN=START MOVE/SIZE, CLICK=BRING TO FRONT, SHIFT-CLICK=SEND TO BACK
	mousedown: function(place,ev) { var ev=ev||window.event;
		var p=this.getPanel(place);

		// CLICK ALWAYS BRINGS TO FRONT
		this.quiet++;
		this.bringPanelToFront(p);
		if (this.manager) this.manager.trackMap(p);
		this.quiet--;
		if (!this.isEdge(p,ev)) return true;

		// start capturing mouse events and set mouse/key handlers
		var target=p; // if 'capture' not supported, track in panel only
		if (document.body.setCapture) // IE
			{ document.body.setCapture(); var target=document.body; }
		if (window.captureEvents) // moz
			{ window.captureEvents(Event.MouseMove|Event.MouseUp,true); var target=window; }
 		if (target.onmousemove!=undefined) target.saved_mousemove=target.onmousemove;
		target.onmousemove=this.dragmove;
		if (target.onmouseup!=undefined) target.saved_mouseup=target.onmouseup;
		target.onmouseup=this.dragstop;
 		if (target.onkeydown!=undefined) target.saved_keydown=target.onkeydown;
		target.onkeydown=this.dragkey;

		// calculate and save drag data in target element
		var x=findMouseX(ev); var left=findPosX(p); var width =p.offsetWidth;
		var y=findMouseY(ev); var top =findPosY(p); var height=p.offsetHeight;
		var sizing=ev.shiftKey;
		var edgeW=this.getPanelEdgeWidth(p); var edgeH=this.getPanelEdgeHeight(p);
		var isT=(y-top<edgeH); var isL=(x-left<edgeW);
		var isB=(top+height-y<edgeH); var isR=(left+width-x<edgeW);
		var d=new Object();
		d.panel=p; d.left=left; d.top=top;
		d.width=this.getPanelWidth(p); d.height=this.getPanelHeight(p);
		d.sizing=sizing; d.edgeW=edgeW; d.edgeH=edgeH;
		d.isT=isT; d.isL=isL; d.isB=isB; d.isR=isR; d.offset=this.getPanelOffset(p);
		d.saved={ x:p.style.left, y:p.style.top, w:p.style.width, h:p.style.height,
			z:p.style.zIndex, pos:p.style.position, classname:p.className };
		target.data=d;
		this.addGhost(p); // keep document from shrinking during move/size
		return this.processed(ev);
	},
	// MOUSEMOVE (during drag)=move/size panel
	dragmove: function(ev){ var ev=ev||window.event; var cmm=config.macros.moveablePanel;
		var d=this.data; var p=d.panel;
		if (!p) { this.onmousemove=this.saved_mousemove?this.saved_mousemove:null; return; }

		cmm.quiet++; // save all notifications until the end...

		// ensure panel is undocked and scrolled into view, THEN get starting mouse and scroll positions
		if (!hasClass(p,'undocked'))
			{ cmm.undockPanel(p,true); if (this.manager) this.manager.trackMap(p); }
		if (d.x===undefined) // first move event only
			{ cmm.scrollToPanel(p); d.x=findMouseX(ev); d.y=findMouseY(ev); }

		// get current mouse pos
		var newX=findMouseX(ev); var newY=findMouseY(ev);

		// calculate new TLWH (start with current panel pos/size)
		var startX=d.x; var startY=d.y; var offsetX=d.offset.x; var offsetY=d.offset.y;
		var L=d.left; var T=d.top; var W=d.width; var H=d.height;
		var newL=L; var newT=T; var newW=p.fixedwidth||W; var newH=p.fixedheight||H;
		if (d.sizing) { // resize panel
			var minW=d.edgeW*2; var minH=d.edgeH*2; // stay bigger than edge areas
			if (hasClass(p,'folded')) this.fold(p.foldButton,ev); // un-fold first!
			if (d.isT) newH=H-newY+startY+1;
			if (d.isB) newH=H+newY-startY+1;
			if (d.isL) newW=W-newX+startX+1;
			if (d.isR) newW=W+newX-startX+1;
			if (d.isT) newT=T-offsetY+newY-startY+1; else newT=T-offsetY; 
			if (d.isL) newL=L-offsetX+newX-startX+1; else newL=L-offsetX; 
			if ((d.isL||d.isR)&&!p.fixedwidth)  newW=(newW>minW?newW:minW);
			if ((d.isT||d.isB)&&!p.fixedheight) newH=(newH>minH?newH:minH);
		} else { // move panel
			newL=L-offsetX+newX-startX+1;
			newT=T-offsetY+newY-startY+1;
		}
		if (hasClass(p,'hover')) { // hover=stay on first screen
			var ww=findWindowWidth(); var wh=findWindowHeight();
			newL+=offsetX; newT+=offsetY; // hover=no relative offset (window-relative)
			// WFFL lower right is off... a bit too far (perhaps scrollwidth?)
			if (newL+newW>ww) newL=ww-newW; if (newT+newH>wh) newT=wh-newH; // limit lower right
			if (newL<0) newL=0; if (newT<0) newT=0; // limit upper left
		} else { // normal floating panel=limit upper left (stay on page)
			if (newL+offsetX<0) newL=0-offsetX; if (newT+offsetY<0) newT=0-offsetY;
		}

		// move the panel and scroll into view as needed
		p.style.left=newL.toString()+'px';
		p.style.top=newT.toString()+'px';
		if (d.sizing) p.style.width=newW.toString()+'px';
		if (d.sizing) p.style.height=newH.toString()+'px';
		cmm.scrollToPanel(p);

		// report new position and notify panel manager... done!
		cmm.quiet--; cmm.showPanelStatus(p,true); cmm.notify(p);
		return cmm.processed(ev);
	},
	dragkey: function(ev){ var ev=ev||window.event;
		var d=this.data; var p=d.panel;
		if (ev.keyCode==27) { // ESC=CANCEL... restore panel to previous pos/size
			p.style.left =d.saved.x; p.style.top   =d.saved.y;
			p.style.width=d.saved.w; p.style.height=d.saved.h;
			p.style.zIndex=d.saved.z;
			p.style.position=d.saved.pos;
			p.className=d.saved.classname;
			return this.onmouseup(ev);
		}
		if (this.saved_keydown) return this.saved_keydown(ev);
	},
	// MOUSEUP: END MOVE/SIZE, SHIFT-CLICK=SEND TO BACK
	dragstop: function(ev){ var ev=ev||window.event; var cmm=config.macros.moveablePanel;
		var newX=findMouseX(ev); var newY=findMouseY(ev);
		if (this.releaseCapture) this.releaseCapture(); // IE
		if (this.releaseEvents) this.releaseEvents(Event.MouseMove|Event.MouseUp); // moz
		this.onmousemove=this.saved_mousemove?this.saved_mousemove:null;
		this.onmouseup=this.saved_mouseup?this.saved_mouseup:null;
		this.onkeydown=this.saved_keydown?this.saved_keydown:null;
		var d=this.data; var p=d.panel;
		if (ev.shiftKey && d.x==newX && d.y==newY && cmm.isEdge(p,ev))
			cmm.sendPanelToBack(p); // SHIFT-CLICK *EDGE*
		cmm.togglePanelButtons(p,true);
		cmm.quiet++; if (cmm.manager) cmm.manager.trackMap(p); cmm.quiet--;
		cmm.clearGhost(); // allow document to adjust extents (if needed)
		cmm.showPanelStatus(p,false); cmm.timedMessage(cmm.formatPanelStatus(p),cmm.msgDuration);
		return cmm.processed(ev);
	},
//}}}
// // CSS definitions
//{{{
	css: '/*{{{*/\n'
		+'.moveablePanelMenu\n'
			+'\t{ display:none; position:absolute; right:.5em; top:-1em; }\n'
		+'.undocked .selected.moveablePanelMenu\n'
			+'\t{ display:inline; }\n'
		+'.floatingPanel .selected .moveablePanelMenu\n'
			+'\t{ display:inline; }\n'
		+'.hover\n'
			+'\t{ position:fixed !important; }\n'
		+'.folded\n'
			+'\t{ height:1.5em !important; overflow:hidden !important; }\n'
		+'.tiddler .folded\n'
			+'\t{ height:2em !important; }\n'
		+'.folded  .moveablePanelMenu\n'
			+'\t{ top:.5em; }	/* buttons fit in folded panel */\n'
		+'.tiddler .moveablePanelMenu\n'
			+'\t{ top:.2em; }	/* buttons fit in tiddler title */\n'
		+'.undocked .toolbar\n'
			+'\t{ padding-right:7.5em; }	/* make room for buttons next to toolbar */\n'
		+'.floatingPanel .moveablePanelMenu\n'
			+'\t{ right:1em;top:1em; } /* buttons fit in floating panel */\n'
		+'.moveablePanelButton\n {'
			+'\tbackground:#ccc !important; color:#000 !important;\n'
			+'\tborder:1px solid #666; padding:0 .25em; margin:0px 1px;\n'
			+'}\n'
		+'.moveablePanelButton:hover\n'
			+'\t{ background:#fff !important; color:#000 !important; }\n'
		+'.popup\n'
			+'\t{ z-index:9999999 !important; } /* popups MUST always be on top!  */\n'
		+'.moveablePanelGhost\n'
			+'\t{ position:absolute; border:1px dotted #999; }\n'
		+'/*}}}*/'
});
//}}}
// // load time initialization
//{{{
// defaults for options
if (config.options.txtMoveablePanelMapName===undefined)
	config.options.txtMoveablePanelMapName='DefaultMap';
if (config.options.chkMoveablePanelShowStatus===undefined)
	config.options.chkMoveablePanelShowStatus=true;
if (config.options.chkMoveablePanelShowManager===undefined)
	config.options.chkMoveablePanelShowManager=true;

// set up shadow stylesheet, then load styles (might be customized)
config.shadowTiddlers.MoveablePanelStyles=config.macros.moveablePanel.css;
var css=store.getRecursiveTiddlerText('MoveablePanelStyles',config.macros.moveablePanel.css,10);
setStylesheet(css,'moveablePanelStyles');
//}}}
// // hijacks
//{{{
// adjust popup placement to account for the current horizontal scrollbar
// offset (if any), so that popups will appear in the correct location, even
// when their 'root' element is scrolled far from the page origin.
var fn=Popup.place; fn=fn.toString(); if (fn.indexOf('findScrollX')==-1) { // only once
	fn=fn.replace(/winWidth\s*-\s*scrollWidth\s*-\s*1/,
		'findScrollX() + winWidth - scrollWidth - 1');
	fn=fn.replace(/winWidth\s*-\s*popupWidth\s*-\s*scrollWidth\s*-\s*1/,
		'findScrollX() + winWidth - popupWidth - scrollWidth - 1');
	eval('Popup.place='+fn);
}
//}}}
//{{{
// window.scrollTo() is used throughout the core (and plugins) to scroll to a *vertical*
// position as computed by ensureVisible(), in order to bring a tiddler into view.
// Unfortunately, the *horizontal* scroll position is almost always hard-coded to 0
// (i.e. a return to the left edge of the page).  Normally, this is not a problem,
// since a page is rarely scrolled horizontally.  However, when there are moveable
// panels, they can appear far from the left edge, so always scrolling to the left
// edge is very disruptive.  In addition, unwanted scrolling can occur as a side
// effect when displaying or refreshing a tiddler (e.g., switching between
// view/edit templates).  These hijacks adds control flags ('noScrollX' and 'noscrollY')
// that can be used to temporarily disable scrolling within the document.
if (window.scrollTo_moveablePanel==undefined) { // only once
window.scrollTo_moveablePanel=window.scrollTo;
	window.scrollTo=function(x,y) {
		var cmm=config.macros.moveablePanel;
		if (cmm.noScrollX&&cmm.noScrollY) return;
		x=cmm.noScrollX?findScrollX():x;
		y=cmm.noScrollY?findScrollY():y;
		window.scrollTo_moveablePanel(x,y);
	}
}
// ensureVisible() is used to calculate the y-offset of a tiddler, just before scrolling
// This tweak sets up an ASYNC timer to invoke the 'bring to front/scroll into view'
// function for 'tiddler' and 'popup' classes.  This allows the function
// to be triggered *after* core scrolling occurs, so the fixups are not
// stomped on by the core's normal scroll handling
if (window.ensureVisible_moveablePanel==undefined) { // only once
	window.ensureVisible_moveablePanel=window.ensureVisible;
	window.ensureVisible=function(e) {
		var ny=ensureVisible_moveablePanel.apply(this,arguments); // get core value

		// fixup height to account for horizontal scrollbar (if present)
		var atBottom=findPosY(e)+e.offsetHeight>=findScrollY()+findWindowHeight();
		var hasHScroll=document.documentElement.scrollWidth>findWindowWidth();
		var hScrollSize=findWindowWidth()-document.body.offsetWidth;
		if (atBottom && hasHScroll) ny+=hScrollSize;

		// defer scrolling for tiddlers and popups (except during startup)
		if (!startingUp && (hasClass(e,'tiddler')||hasClass(e,'popup'))) {
			var cmm=config.macros.moveablePanel;
			cmm.noScrollX++; if (hasClass(e,'tiddler')) cmm.noScrollY++;
			var delay=config.options.chkAnimate?config.animDuration+50:50;
			cmm.ensurePanelVisible(e,delay); // ASYNC SCROLL
		}
		return ny;
	}
}

// story.refreshTiddler()
if (Story.prototype.refreshTiddler_moveablePanel==undefined) { // only once
	Story.prototype.refreshTiddler_moveablePanel=Story.prototype.refreshTiddler;
	Story.prototype.refreshTiddler=function() {
		var cmm=config.macros.moveablePanel;
		cmm.noScrollX++; cmm.noScrollY++; // DON'T SCROLL AT ALL
		var r=this.refreshTiddler_moveablePanel.apply(this,arguments);
		cmm.noScrollX--; cmm.noScrollY--;
		return r;

	}
}
// story.displayTiddler()
if (Story.prototype.displayTiddler_moveablePanel==undefined) { // only once
	Story.prototype.displayTiddler_moveablePanel=Story.prototype.displayTiddler;
	Story.prototype.displayTiddler=function(srcElement,tiddler) {
		var cmm=config.macros.moveablePanel;
//WFFL		cmm.noScrollX++; cmm.noScrollY++;
		var r=this.displayTiddler_moveablePanel.apply(this,arguments);
		var title=(tiddler instanceof Tiddler)?tiddler.title:tiddler;
		var panel=cmm.findPanel(title); // if moveable... unfold panel (but not during startup)
		if (panel&&hasClass(panel,'folded')&&!startingUp) cmm.foldPanel(panel);
		var delay=config.options.chkAnimate?config.animDuration+50:50;
		cmm.ensurePanelVisible(this.getTiddler(title),delay); // ASYNC SCROLL
		return r;

	}
}
//}}}
//{{{
// Zoomer() displays an animated bounding box when showing a tiddler.  But this box 'zooms' to the tiddler's 'anchor point', not the current panel position, which can cause a 'scroll blink'.  This hijack redirects the zoomer's target directly to the undocked panel (if any)
if (window.Zoomer_moveablePanel==undefined) { // only once
	window.Zoomer_moveablePanel=window.Zoomer;
	window.Zoomer=function(text,startElement,targetElement,unused) {
		if (hasClass(targetElement,'tiddler')) {
			var tid=targetElement.getAttribute('tiddler');
			arguments[2]=config.macros.moveablePanel.findPanel(tid)||targetElement;			
		}
		return window.Zoomer_moveablePanel.apply(this,arguments);
	}
}
//}}}
/***
|Name|NestedSlidersPlugin|
|Source|http://www.TiddlyTools.com/#NestedSlidersPlugin|
|Documentation|http://www.TiddlyTools.com/#NestedSlidersPluginInfo|
|Version|2.4.9|
|Author|Eric Shulman|
|License|http://www.TiddlyTools.com/#LegalStatements|
|~CoreVersion|2.1|
|Type|plugin|
|Description|show content in nest-able sliding/floating panels, without creating separate tiddlers for each panel's content|
!!!!!Documentation
>see [[NestedSlidersPluginInfo]]
!!!!!Configuration
<<<
<<option chkFloatingSlidersAnimate>> allow floating sliders to animate when opening/closing
>Note: This setting can cause 'clipping' problems in some versions of InternetExplorer.
>In addition, for floating slider animation to occur you must also allow animation in general (see [[AdvancedOptions]]).
<<<
!!!!!Revisions
<<<
2008.11.15 - 2.4.9 in adjustNestedSlider(), don't make adjustments if panel is marked as 'undocked' (CSS class).  In onClickNestedSlider(), SHIFT-CLICK docks panel (see [[MoveablePanelPlugin]])
|please see [[NestedSlidersPluginInfo]] for additional revision details|
2005.11.03 - 1.0.0 initial public release.  Thanks to RodneyGomes, GeoffSlocock, and PaulPetterson for suggestions and experiments.
<<<
!!!!!Code
***/
//{{{
version.extensions.NestedSlidersPlugin= {major: 2, minor: 4, revision: 9, date: new Date(2008,11,15)};

// options for deferred rendering of sliders that are not initially displayed
if (config.options.chkFloatingSlidersAnimate===undefined)
	config.options.chkFloatingSlidersAnimate=false; // avoid clipping problems in IE

// default styles for 'floating' class
setStylesheet(".floatingPanel { position:absolute; z-index:10; padding:0.5em; margin:0em; \
	background-color:#eee; color:#000; border:1px solid #000; text-align:left; }","floatingPanelStylesheet");

// if removeCookie() function is not defined by TW core, define it here.
if (window.removeCookie===undefined) {
	window.removeCookie=function(name) {
		document.cookie = name+'=; expires=Thu, 01-Jan-1970 00:00:01 UTC; path=/;'; 
	}
}

config.formatters.push( {
	name: "nestedSliders",
	match: "\\n?\\+{3}",
	terminator: "\\s*\\={3}\\n?",
	lookahead: "\\n?\\+{3}(\\+)?(\\([^\\)]*\\))?(\\!*)?(\\^(?:[^\\^\\*\\@\\[\\>]*\\^)?)?(\\*)?(\\@)?(?:\\{\\{([\\w]+[\\s\\w]*)\\{)?(\\[[^\\]]*\\])?(\\[[^\\]]*\\])?(?:\\}{3})?(\\#[^:]*\\:)?(\\>)?(\\.\\.\\.)?\\s*",
	handler: function(w)
		{
			lookaheadRegExp = new RegExp(this.lookahead,"mg");
			lookaheadRegExp.lastIndex = w.matchStart;
			var lookaheadMatch = lookaheadRegExp.exec(w.source)
			if(lookaheadMatch && lookaheadMatch.index == w.matchStart)
			{
				var defopen=lookaheadMatch[1];
				var cookiename=lookaheadMatch[2];
				var header=lookaheadMatch[3];
				var panelwidth=lookaheadMatch[4];
				var transient=lookaheadMatch[5];
				var hover=lookaheadMatch[6];
				var buttonClass=lookaheadMatch[7];
				var label=lookaheadMatch[8];
				var openlabel=lookaheadMatch[9];
				var panelID=lookaheadMatch[10];
				var blockquote=lookaheadMatch[11];
				var deferred=lookaheadMatch[12];

				// location for rendering button and panel
				var place=w.output;

				// default to closed, no cookie, no accesskey, no alternate text/tip
				var show="none"; var cookie=""; var key="";
				var closedtext=">"; var closedtip="";
				var openedtext="<"; var openedtip="";

				// extra "+", default to open
				if (defopen) show="block";

				// cookie, use saved open/closed state
				if (cookiename) {
					cookie=cookiename.trim().slice(1,-1);
					cookie="chkSlider"+cookie;
					if (config.options[cookie]==undefined)
						{ config.options[cookie] = (show=="block") }
					show=config.options[cookie]?"block":"none";
				}

				// parse label/tooltip/accesskey: [label=X|tooltip]
				if (label) {
					var parts=label.trim().slice(1,-1).split("|");
					closedtext=parts.shift();
					if (closedtext.substr(closedtext.length-2,1)=="=")	
						{ key=closedtext.substr(closedtext.length-1,1); closedtext=closedtext.slice(0,-2); }
					openedtext=closedtext;
					if (parts.length) closedtip=openedtip=parts.join("|");
					else { closedtip="show "+closedtext; openedtip="hide "+closedtext; }
				}

				// parse alternate label/tooltip: [label|tooltip]
				if (openlabel) {
					var parts=openlabel.trim().slice(1,-1).split("|");
					openedtext=parts.shift();
					if (parts.length) openedtip=parts.join("|");
					else openedtip="hide "+openedtext;
				}

				var title=show=='block'?openedtext:closedtext;
				var tooltip=show=='block'?openedtip:closedtip;

				// create the button
				if (header) { // use "Hn" header format instead of button/link
					var lvl=(header.length>5)?5:header.length;
					var btn = createTiddlyElement(createTiddlyElement(place,"h"+lvl,null,null,null),"a",null,buttonClass,title);
					btn.onclick=onClickNestedSlider;
					btn.setAttribute("href","javascript:;");
					btn.setAttribute("title",tooltip);
				}
				else
					var btn = createTiddlyButton(place,title,tooltip,onClickNestedSlider,buttonClass);
				btn.innerHTML=title; // enables use of HTML entities in label

				// set extra button attributes
				btn.setAttribute("closedtext",closedtext);
				btn.setAttribute("closedtip",closedtip);
				btn.setAttribute("openedtext",openedtext);
				btn.setAttribute("openedtip",openedtip);
				btn.sliderCookie = cookie; // save the cookiename (if any) in the button object
				btn.defOpen=defopen!=null; // save default open/closed state (boolean)
				btn.keyparam=key; // save the access key letter ("" if none)
				if (key.length) {
					btn.setAttribute("accessKey",key); // init access key
					btn.onfocus=function(){this.setAttribute("accessKey",this.keyparam);}; // **reclaim** access key on focus
				}
				btn.setAttribute("hover",hover?"true":"false");
				btn.onmouseover=function(ev) {
					// optional 'open on hover' handling
					if (this.getAttribute("hover")=="true" && this.sliderPanel.style.display=='none') {
						document.onclick.call(document,ev); // close transients
						onClickNestedSlider(ev); // open this slider
					}
					// mouseover on button aligns floater position with button
					if (window.adjustSliderPos) window.adjustSliderPos(this.parentNode,this,this.sliderPanel);
				}

				// create slider panel
				var panelClass=panelwidth?"floatingPanel":"sliderPanel";
				if (panelID) panelID=panelID.slice(1,-1); // trim off delimiters
				var panel=createTiddlyElement(place,"div",panelID,panelClass,null);
				panel.button = btn; // so the slider panel know which button it belongs to
				btn.sliderPanel=panel; // so the button knows which slider panel it belongs to
				panel.defaultPanelWidth=(panelwidth && panelwidth.length>2)?panelwidth.slice(1,-1):"";
				panel.setAttribute("transient",transient=="*"?"true":"false");
				panel.style.display = show;
				panel.style.width=panel.defaultPanelWidth;
				panel.onmouseover=function(event) // mouseover on panel aligns floater position with button
					{ if (window.adjustSliderPos) window.adjustSliderPos(this.parentNode,this.button,this); }

				// render slider (or defer until shown) 
				w.nextMatch = lookaheadMatch.index + lookaheadMatch[0].length;
				if ((show=="block")||!deferred) {
					// render now if panel is supposed to be shown or NOT deferred rendering
					w.subWikify(blockquote?createTiddlyElement(panel,"blockquote"):panel,this.terminator);
					// align floater position with button
					if (window.adjustSliderPos) window.adjustSliderPos(place,btn,panel);
				}
				else {
					var src = w.source.substr(w.nextMatch);
					var endpos=findMatchingDelimiter(src,"+++","===");
					panel.setAttribute("raw",src.substr(0,endpos));
					panel.setAttribute("blockquote",blockquote?"true":"false");
					panel.setAttribute("rendered","false");
					w.nextMatch += endpos+3;
					if (w.source.substr(w.nextMatch,1)=="\n") w.nextMatch++;
				}
			}
		}
	}
)

function findMatchingDelimiter(src,starttext,endtext) {
	var startpos = 0;
	var endpos = src.indexOf(endtext);
	// check for nested delimiters
	while (src.substring(startpos,endpos-1).indexOf(starttext)!=-1) {
		// count number of nested 'starts'
		var startcount=0;
		var temp = src.substring(startpos,endpos-1);
		var pos=temp.indexOf(starttext);
		while (pos!=-1)  { startcount++; pos=temp.indexOf(starttext,pos+starttext.length); }
		// set up to check for additional 'starts' after adjusting endpos
		startpos=endpos+endtext.length;
		// find endpos for corresponding number of matching 'ends'
		while (startcount && endpos!=-1) {
			endpos = src.indexOf(endtext,endpos+endtext.length);
			startcount--;
		}
	}
	return (endpos==-1)?src.length:endpos;
}
//}}}
//{{{
window.onClickNestedSlider=function(e)
{
	if (!e) var e = window.event;
	var theTarget = resolveTarget(e);
	while (theTarget && theTarget.sliderPanel==undefined) theTarget=theTarget.parentNode;
	if (!theTarget) return false;
	var theSlider = theTarget.sliderPanel;
	var isOpen = theSlider.style.display!="none";

	// if SHIFT-CLICK, dock panel first (see [[MoveablePanelPlugin]])
	if (e.shiftKey && config.macros.moveablePanel) config.macros.moveablePanel.dock(theSlider,e);

	// toggle label
	theTarget.innerHTML=isOpen?theTarget.getAttribute("closedText"):theTarget.getAttribute("openedText");
	// toggle tooltip
	theTarget.setAttribute("title",isOpen?theTarget.getAttribute("closedTip"):theTarget.getAttribute("openedTip"));

	// deferred rendering (if needed)
	if (theSlider.getAttribute("rendered")=="false") {
		var place=theSlider;
		if (theSlider.getAttribute("blockquote")=="true")
			place=createTiddlyElement(place,"blockquote");
		wikify(theSlider.getAttribute("raw"),place);
		theSlider.setAttribute("rendered","true");
	}

	// show/hide the slider
	if(config.options.chkAnimate && (!hasClass(theSlider,'floatingPanel') || config.options.chkFloatingSlidersAnimate))
		anim.startAnimating(new Slider(theSlider,!isOpen,e.shiftKey || e.altKey,"none"));
	else
		theSlider.style.display = isOpen ? "none" : "block";

	// reset to default width (might have been changed via plugin code)
	theSlider.style.width=theSlider.defaultPanelWidth;

	// align floater panel position with target button
	if (!isOpen && window.adjustSliderPos) window.adjustSliderPos(theSlider.parentNode,theTarget,theSlider);

	// if showing panel, set focus to first 'focus-able' element in panel
	if (theSlider.style.display!="none") {
		var ctrls=theSlider.getElementsByTagName("*");
		for (var c=0; c<ctrls.length; c++) {
			var t=ctrls[c].tagName.toLowerCase();
			if ((t=="input" && ctrls[c].type!="hidden") || t=="textarea" || t=="select")
				{ try{ ctrls[c].focus(); } catch(err){;} break; }
		}
	}
	var cookie=theTarget.sliderCookie;
	if (cookie && cookie.length) {
		config.options[cookie]=!isOpen;
		if (config.options[cookie]!=theTarget.defOpen) window.saveOptionCookie(cookie);
		else window.removeCookie(cookie); // remove cookie if slider is in default display state
	}

	// prevent SHIFT-CLICK from being processed by browser (opens blank window... yuck!)
	// prevent clicks *within* a slider button from being processed by browser
	// but allow plain click to bubble up to page background (to close transients, if any)
	if (e.shiftKey || theTarget!=resolveTarget(e))
		{ e.cancelBubble=true; if (e.stopPropagation) e.stopPropagation(); }
	Popup.remove(); // close open popup (if any)
	return false;
}
//}}}
//{{{
// click in document background closes transient panels 
document.nestedSliders_savedOnClick=document.onclick;
document.onclick=function(ev) { if (!ev) var ev=window.event; var target=resolveTarget(ev);

	if (document.nestedSliders_savedOnClick)
		var retval=document.nestedSliders_savedOnClick.apply(this,arguments);
	// if click was inside a popup... leave transient panels alone
	var p=target; while (p) if (hasClass(p,"popup")) break; else p=p.parentNode;
	if (p) return retval;
	// if click was inside transient panel (or something contained by a transient panel), leave it alone
	var p=target; while (p) {
		if ((hasClass(p,"floatingPanel")||hasClass(p,"sliderPanel"))&&p.getAttribute("transient")=="true") break;
		p=p.parentNode;
	}
	if (p) return retval;
	// otherwise, find and close all transient panels...
	var all=document.all?document.all:document.getElementsByTagName("DIV");
	for (var i=0; i<all.length; i++) {
		 // if it is not a transient panel, or the click was on the button that opened this panel, don't close it.
		if (all[i].getAttribute("transient")!="true" || all[i].button==target) continue;
		// otherwise, if the panel is currently visible, close it by clicking it's button
		if (all[i].style.display!="none") window.onClickNestedSlider({target:all[i].button})
		if (!hasClass(all[i],"floatingPanel")&&!hasClass(all[i],"sliderPanel")) all[i].style.display="none";
	}
	return retval;
};
//}}}
//{{{
// adjust floating panel position based on button position
if (window.adjustSliderPos==undefined) window.adjustSliderPos=function(place,btn,panel) {
	if (hasClass(panel,"floatingPanel") && !hasClass(panel,"undocked")) {
		// see [[MoveablePanelPlugin]] for use of 'undocked'
		var rightEdge=document.body.offsetWidth-1;
		var panelWidth=panel.offsetWidth;
		var left=0;
		var top=btn.offsetHeight; 
		if (place.style.position=="relative" && findPosX(btn)+panelWidth>rightEdge) {
			left-=findPosX(btn)+panelWidth-rightEdge; // shift panel relative to button
			if (findPosX(btn)+left<0) left=-findPosX(btn); // stay within left edge
		}
		if (place.style.position!="relative") {
			var left=findPosX(btn);
			var top=findPosY(btn)+btn.offsetHeight;
			var p=place; while (p && !hasClass(p,'floatingPanel')) p=p.parentNode;
			if (p) { left-=findPosX(p); top-=findPosY(p); }
			if (left+panelWidth>rightEdge) left=rightEdge-panelWidth;
			if (left<0) left=0;
		}
		panel.style.left=left+"px"; panel.style.top=top+"px";
	}
}
//}}}
//{{{
// TW2.1 and earlier:
// hijack Slider stop handler so overflow is visible after animation has completed
Slider.prototype.coreStop = Slider.prototype.stop;
Slider.prototype.stop = function()
	{ this.coreStop.apply(this,arguments); this.element.style.overflow = "visible"; }

// TW2.2+
// hijack Morpher stop handler so sliderPanel/floatingPanel overflow is visible after animation has completed
if (version.major+.1*version.minor+.01*version.revision>=2.2) {
	Morpher.prototype.coreStop = Morpher.prototype.stop;
	Morpher.prototype.stop = function() {
		this.coreStop.apply(this,arguments);
		var e=this.element;
		if (hasClass(e,"sliderPanel")||hasClass(e,"floatingPanel")) {
			// adjust panel overflow and position after animation
			e.style.overflow = "visible";
			if (window.adjustSliderPos) window.adjustSliderPos(e.parentNode,e.button,e);
		}
	};
}
//}}}
<<tiddler NewTimeline##macro with:{{(new Date()).formatString("DDD, DD. MMM YYYY")}}>>/%
!macro
<<newTiddler label:newTimeline prompt:"Create a new timeline for $1" title:"$1" tag:"TimeLine" text:{{"<<drawVisualization '$1'\>\><html><div id='$1'></div></html>"}} focus:title>>
!formTemplate


! %/
/***
|''Name:''|PasswordOptionPlugin|
|''Description:''|Extends TiddlyWiki options with non encrypted password option.|
|''Version:''|1.0.2|
|''Date:''|Apr 19, 2007|
|''Source:''|http://tiddlywiki.bidix.info/#PasswordOptionPlugin|
|''Author:''|BidiX (BidiX (at) bidix (dot) info)|
|''License:''|[[BSD open source license|http://tiddlywiki.bidix.info/#%5B%5BBSD%20open%20source%20license%5D%5D ]]|
|''~CoreVersion:''|2.2.0 (Beta 5)|
***/
//{{{
version.extensions.PasswordOptionPlugin = {
	major: 1, minor: 0, revision: 2, 
	date: new Date("Apr 19, 2007"),
	source: 'http://tiddlywiki.bidix.info/#PasswordOptionPlugin',
	author: 'BidiX (BidiX (at) bidix (dot) info',
	license: '[[BSD open source license|http://tiddlywiki.bidix.info/#%5B%5BBSD%20open%20source%20license%5D%5D]]',
	coreVersion: '2.2.0 (Beta 5)'
};

config.macros.option.passwordCheckboxLabel = "Save this password on this computer";
config.macros.option.passwordInputType = "password"; // password | text
setStylesheet(".pasOptionInput {width: 11em;}\n","passwordInputTypeStyle");

merge(config.macros.option.types, {
	'pas': {
		elementType: "input",
		valueField: "value",
		eventName: "onkeyup",
		className: "pasOptionInput",
		typeValue: config.macros.option.passwordInputType,
		create: function(place,type,opt,className,desc) {
			// password field
			config.macros.option.genericCreate(place,'pas',opt,className,desc);
			// checkbox linked with this password "save this password on this computer"
			config.macros.option.genericCreate(place,'chk','chk'+opt,className,desc);			
			// text savePasswordCheckboxLabel
			place.appendChild(document.createTextNode(config.macros.option.passwordCheckboxLabel));
		},
		onChange: config.macros.option.genericOnChange
	}
});

merge(config.optionHandlers['chk'], {
	get: function(name) {
		// is there an option linked with this chk ?
		var opt = name.substr(3);
		if (config.options[opt]) 
			saveOptionCookie(opt);
		return config.options[name] ? "true" : "false";
	}
});

merge(config.optionHandlers, {
	'pas': {
 		get: function(name) {
			if (config.options["chk"+name]) {
				return encodeCookie(config.options[name].toString());
			} else {
				return "";
			}
		},
		set: function(name,value) {config.options[name] = decodeCookie(value);}
	}
});

// need to reload options to load passwordOptions
loadOptionsCookie();

/*
if (!config.options['pasPassword'])
	config.options['pasPassword'] = '';

merge(config.optionsDesc,{
		pasPassword: "Test password"
	});
*/
//}}}
/%
!info
|Name|RefreshTiddler|
|Source|http://www.TiddlyTools.com/#RefreshTiddler|
|Version|2.0.0|
|Author|Eric Shulman|
|License|http://www.TiddlyTools.com/#LegalStatements|
|~CoreVersion|2.1|
|Type|transclusion|
|Description|create a link to force an immediate refresh of the current tiddler|
Usage
<<<
{{{
<<tiddler RefreshTiddler>>
<<tiddler RefreshTiddler with: label tip>>
}}}
<<<
Example
<<<
{{{<<tiddler RefreshTiddler with: "click me">>}}}
<<tiddler RefreshTiddler##show with: "click me">>
content displayed at <<today 0hh:0mm:0ss>>
<<<
!end
!show
<html><nowiki><a href="javascript:;" title="$2"
onclick="
	var here=story.findContainingTiddler(this);
	if (here) story.refreshTiddler(here.getAttribute('tiddler'),null,true);
	return false;
">$1</a></html>
!end
%/<<tiddler {{var src='RefreshTiddler'; src+(tiddler&&tiddler.title==src?'##info':'##show')}}
	with:	{{'$1'!='$'+'1'?'$1':'refresh'}}
		{{'$2'!='$'+'2'?'$2':'redisplay current tiddler content'}}
>>
<<drawVisualization TLData>>
<html><div id='mytimeline'></div></html>

<<search>><<closeAll>><<permaview>><<newTiddler>><<tiddler NewTimeline>><<newJournal "DD MMM YYYY" "journal">><<saveChanges>><<tiddler TspotSidebar>><<slider chkSliderOptionsPanel OptionsPanel "options »" "Change TiddlyWiki advanced options">>
[[CHAPTimelineCSS]]
[[StyleSheetShortcuts]]
/***
!NestedSlidersPlugin 
***/
/*{{{*/
.floatingPanel
	{ z-index:700; padding:1em; margin:0em; border:1px solid; -moz-border-radius:1em;-webkit-border-radius:1em; font-size:8pt; text-align:left; }
.floatingPanel hr
	{ margin:2px 0 1px 0; padding:0; }
#sidebarOptions .sliderPanel
	{ margin:0; padding:0; font-size:1em; background:transparent; }
#sidebarOptions .sliderPanel a
	{ font-weight:normal; }
#sidebarOptions .sliderPanel blockquote
	{ margin:0;padding:0;margin-left:1em; border-left:1px dotted; padding-left:1em }

.selected .floatingPanel .button,
.selected .floatingPanel a:link,
.selected .floatingPanel a:hover,
.selected .floatingPanel a:visited,
.floatingPanel .button,
.floatingPanel a:link,
.floatingPanel a:hover,
.floatingPanel a:visited
	{ color:[[ColorPalette::PrimaryDark]] !important; }

/*}}}*/
/***
|Name|StyleSheetShortcuts|
|Source|http://www.TiddlyTools.com/#StyleSheetShortcuts|
|Version||
|Author|Eric Shulman|
|License|http://www.TiddlyTools.com/#LegalStatements|
|~CoreVersion|2.1|
|Type|CSS|
|Description|'convenience' classes for common formatting, alignment, boxes, tables, etc.|

These 'style tweaks' can be easily included in other stylesheet tiddler so they can share a baseline look-and-feel that can then be customized to create a wide variety of 'flavors'.
***/
/*{{{*/

/* text alignments */
.left
	{ display:block;text-align:left; }
.center
	{ display:block;text-align:center; }
.center table
	{ margin:auto !important; }
.right	
	{ display:block;text-align:right; }
.justify
	{ display:block;text-align:justify; }
.indent
	{ display:block;margin:0;padding:0;border:0;margin-left:2em; }
.floatleft
	{ float:left; }
.floatright
	{ float:right; }
.valignTop, .valignTop table, .valignTop tbody, .valignTop th, .valignTop tr, .valignTop td
	{ vertical-align:top; }
.valignBottom, .valignBottom table, .valignBottom tbody, .valignBottom th, .valignBottom tr, .valignBottom td
	{ vertical-align:bottom; }
.clear
	{ clear:both; }
.wrap
	{ white-space:normal; }
.nowrap
	{ white-space:nowrap; }
.hidden
	{ display:none; }
.show
	{ display:inline !important; }
.span
	{ display:span; }
.block
	{ display:block; }
.relative
	{ position:relative; }
.absolute
	{ position:absolute; }

/* font sizes */
.big
	{ font-size:14pt;line-height:120% }
.medium
	{ font-size:12pt;line-height:120% }
.normal
	{ font-size:9pt;line-height:120% }
.small
	{ font-size:8pt;line-height:120% }
.fine
	{ font-size:7pt;line-height:120% }
.tiny
	{ font-size:6pt;line-height:120% }
.larger
	{ font-size:120%; }
.smaller
	{ font-size:80%; }

/* font styles */
.bold
	{ font-weight:bold; }
.italic
	{ font-style:italic; }
.underline
	{ text-decoration:underline; }

/* plain list items (no bullets or indent) */
.nobullets li { list-style-type: none; margin-left:-2em; }

/* vertical tabsets - courtesy of Tobias Beer */
.vTabs .tabset {float:left;display:block;padding:0px;margin-top:.5em;min-width:20%;}
.vTabs .tabset .tab {display:block;text-align:right;padding:2px 3px 2px 7px; margin:0 1px 1px 0;}
.vTabs .tabContents {margin-left:20%;max-width:80%;padding:5px;}
.vTabs .tabContents .tabContents {border:none; background:transparent;}

/* multi-column tiddler content (not supported in Internet Explorer) */
.twocolumns { display:block;
	-moz-column-count:2; -moz-column-gap:1em; -moz-column-width:50%; /* FireFox */
	-webkit-column-count:2; -webkit-column-gap:1em; -webkit-column-width:50%; /* Safari */
	column-count:2; column-gap:1em; column-width:50%; /* Opera */
}
.threecolumns { display:block;
	-moz-column-count:3; -moz-column-gap:1em; -moz-column-width:33%; /* FireFox */
	-webkit-column-count:3; -webkit-column-gap:1em; -webkit-column-width:33%; /* Safari */
	column-count:3; column-gap:1em; column-width:33%; /* Opera */
}
.fourcolumns { display:block;
	-moz-column-count:4; -moz-column-gap:1em; -moz-column-width:25%; /* FireFox */
	-webkit-column-count:4; -webkit-column-gap:1em; -webkit-column-width:25%; /* Safari */
	column-count:4; column-gap:1em; column-width:25%; /* Opera */
}

/* page breaks */
.breakbefore { page-break-before:always; }
.breakafter { page-break-before:always; } 

/* show/hide browser-specific content for InternetExplorer vs. non-IE ("moz") browsers */
*[class="ieOnly"]
	{ display:none; } /* hide in moz (uses CSS selector) */
* html .mozOnly, *:first-child+html .mozOnly
	{ display: none; } /* hide in IE (uses IE6/IE7 CSS hacks) */

/* borderless tables */
.borderless, .borderless table, .borderless td, .borderless tr, .borderless th, .borderless tbody
	{ border:0 !important; margin:0 !important; padding:0 !important; }
.widetable, .widetable table
	{ width:100%; }

/* thumbnail images (fixed-sized scaled images) */
.thumbnail img { height:5em !important; }

/* stretchable images (auto-size to fit tiddler) */
.stretch img { width:95%; }

/* grouped content */
.outline
	{ display:block; padding:1em; -moz-border-radius:1em;-webkit-border-radius:1em; border:1px solid; }
.menubox
	{ display:block; padding:1em; -moz-border-radius:1em;-webkit-border-radius:1em; border:1px solid; background:#fff; color:#000; }
.menubox .button, .menubox .tiddlyLinkExisting, .menubox .tiddlyLinkNonExisting
	{ color:#009 !important; }
.groupbox
	{ display:block; padding:1em; -moz-border-radius:1em;-webkit-border-radius:1em; border:1px solid; background:#ffe; color:#000; }
.groupbox a, .groupbox .button, .groupbox .tiddlyLinkExisting, .groupbox .tiddlyLinkNonExisting
	{ color:#009 !important; }
.groupbox code
	{ color:#333 !important; }
.borderleft
	{ margin:0;padding:0;border:0;margin-left:1em; border-left:1px dotted; padding-left:.5em; }
.borderright
	{ margin:0;padding:0;border:0;margin-right:1em; border-right:1px dotted; padding-right:.5em; }
.borderbottom
	{ margin:0;padding:1px 0;border:0;border-bottom:1px dotted; margin-bottom:1px; padding-bottom:1px; }
.bordertop
	{ margin:0;padding:0;border:0;border-top:1px dotted; margin-top:1px; padding-top:1px; }

/* scrolled content */
.scrollbars { overflow:auto; }
.height10em { height:10em; }
.height15em { height:15em; }
.height20em { height:20em; }
.height25em { height:25em; }
.height30em { height:30em; }
.height35em { height:35em; }
.height40em { height:40em; }

/* compact form */
.smallform
	{ white-space:nowrap; }
.smallform input, .smallform textarea, .smallform button, .smallform checkbox, .smallform radio, .smallform select
	{ font-size:8pt; }

/* stretchable edit fields and textareas (auto-size to fit tiddler) */
.stretch input { width:99%; }
.stretch textarea { width:99%; }

/* compact input fields (limited to a few characters for entering percentages and other small values) */
.onechar input   { width:1em; }
.twochar input   { width:2em; }
.threechar input { width:3em; }
.fourchar input  { width:4em; }
.fivechar input  { width:5em; }

/* text colors */
.white { color:#fff !important }
.gray  { color:#999 !important }
.black { color:#000 !important }
.red   { color:#f66 !important }
.green { color:#0c0 !important }
.blue  { color:#99f !important }

/* rollover highlighting */
.mouseover 
	{color:[[ColorPalette::TertiaryLight]] !important;}
.mouseover a
	{color:[[ColorPalette::TertiaryLight]] !important;}
.selected .mouseover
	{color:[[ColorPalette::Foreground]] !important;}
.selected .mouseover .button, .selected .mouseover a
	{color:[[ColorPalette::PrimaryDark]] !important;}

/* rollover zoom text */
.zoomover
	{ font-size:80% !important; }
.selected .zoomover
	{ font-size:100% !important; }

/* [[ColorPalette]] text colors */
.Background	{ color:[[ColorPalette::Background]];	 }
.Foreground	{ color:[[ColorPalette::Foreground]];	 }
.PrimaryPale	{ color:[[ColorPalette::PrimaryPale]];	 }
.PrimaryLight	{ color:[[ColorPalette::PrimaryLight]];	 }
.PrimaryMid	{ color:[[ColorPalette::PrimaryMid]];	 }
.PrimaryDark	{ color:[[ColorPalette::PrimaryDark]];	 }
.SecondaryPale	{ color:[[ColorPalette::SecondaryPale]]; }
.SecondaryLight	{ color:[[ColorPalette::SecondaryLight]];}
.SecondaryMid	{ color:[[ColorPalette::SecondaryMid]];	 }
.SecondaryDark	{ color:[[ColorPalette::SecondaryDark]]; }
.TertiaryPale	{ color:[[ColorPalette::TertiaryPale]];	 }
.TertiaryLight	{ color:[[ColorPalette::TertiaryLight]]; }
.TertiaryMid	{ color:[[ColorPalette::TertiaryMid]];	 }
.TertiaryDark	{ color:[[ColorPalette::TertiaryDark]];	 }
.Error		{ color:[[ColorPalette::Error]];	 }

/* [[ColorPalette]] background colors */
.BGBackground	  { background-color:[[ColorPalette::Background]];	}
.BGForeground	  { background-color:[[ColorPalette::Foreground]];	}
.BGPrimaryPale	  { background-color:[[ColorPalette::PrimaryPale]];	}
.BGPrimaryLight	  { background-color:[[ColorPalette::PrimaryLight]];	}
.BGPrimaryMid	  { background-color:[[ColorPalette::PrimaryMid]];	}
.BGPrimaryDark	  { background-color:[[ColorPalette::PrimaryDark]];	}
.BGSecondaryPale  { background-color:[[ColorPalette::SecondaryPale]]; 	}
.BGSecondaryLight { background-color:[[ColorPalette::SecondaryLight]];	}
.BGSecondaryMid	  { background-color:[[ColorPalette::SecondaryMid]];	}
.BGSecondaryDark  { background-color:[[ColorPalette::SecondaryDark]]; 	}
.BGTertiaryPale	  { background-color:[[ColorPalette::TertiaryPale]];	}
.BGTertiaryLight  { background-color:[[ColorPalette::TertiaryLight]]; 	}
.BGTertiaryMid	  { background-color:[[ColorPalette::TertiaryMid]];	}
.BGTertiaryDark	  { background-color:[[ColorPalette::TertiaryDark]];	}
.BGError	  { background-color:[[ColorPalette::Error]];	 	}
/*}}}*/
&nbsp;+++^61em^*[data]
<<moveablePanel name:{{tiddler?tiddler.title:""}} docked width:61em height:auto fold hover noclose>>
|borderless widetable|k
| <<tiddler TimelineFormTemplate1920-2020##Year>> | <<tiddler TimelineFormTemplate1920-2020##Month>> | <<tiddler TimelineFormTemplate1920-2020##Day>> | <<tiddler TimelineFormTemplate1920-2020##Color>> |
|>|>|>| <<tiddler TimelineFormTemplate1920-2020##Text>> |
===
/%
!Year
<html>
<sub><b>startYear:</b></sub><br/>
<select name=startYear >
<option>1920
<option>1921
<option>1922
<option>1923
<option>1924
<option>1925
<option>1926
<option>1927
<option>1928
<option>1929
<option>1930
<option>1931
<option>1932
<option>1933
<option>1934
<option>1935
<option>1936
<option>1937
<option>1938
<option>1939
<option>1940
<option>1941
<option>1942
<option>1943
<option>1944
<option>1945
<option>1946
<option>1947
<option>1948
<option>1949
<option>1950
<option>1951
<option>1952
<option>1953
<option>1954
<option>1955
<option>1956
<option>1957
<option>1958
<option>1959
<option>1960
<option>1961
<option>1962
<option>1963
<option>1964
<option>1965
<option>1966
<option>1967
<option>1968
<option>1969
<option>1970
<option>1971
<option>1972
<option>1973
<option>1974
<option>1975
<option>1976
<option>1977
<option>1978
<option>1979
<option>1980
<option>1981
<option>1982
<option>1983
<option>1984
<option>1985
<option>1986
<option>1987
<option>1988
<option>1989
<option>1990
<option>1991
<option>1992
<option>1993
<option>1994
<option>1995
<option>1996
<option>1997
<option>1998
<option>1999
<option>2000
<option>2001
<option>2002
<option>2003
<option>2004
<option>2005
<option>2006
<option>2007
<option>2008
<option>2009
<option>2010
<option>2011
<option>2012
<option>2013
<option>2014
<option>2015
<option>2016
<option>2017
<option>2018
<option>2019
<option>2020

</select ><br/>
<sub><b>endYear:</b></sub><br/>
<select name=endYear >
<option>1920
<option>1921
<option>1922
<option>1923
<option>1924
<option>1925
<option>1926
<option>1927
<option>1928
<option>1929
<option>1930
<option>1931
<option>1932
<option>1933
<option>1934
<option>1935
<option>1936
<option>1937
<option>1938
<option>1939
<option>1940
<option>1941
<option>1942
<option>1943
<option>1944
<option>1945
<option>1946
<option>1947
<option>1948
<option>1949
<option>1950
<option>1951
<option>1952
<option>1953
<option>1954
<option>1955
<option>1956
<option>1957
<option>1958
<option>1959
<option>1960
<option>1961
<option>1962
<option>1963
<option>1964
<option>1965
<option>1966
<option>1967
<option>1968
<option>1969
<option>1970
<option>1971
<option>1972
<option>1973
<option>1974
<option>1975
<option>1976
<option>1977
<option>1978
<option>1979
<option>1980
<option>1981
<option>1982
<option>1983
<option>1984
<option>1985
<option>1986
<option>1987
<option>1988
<option>1989
<option>1990
<option>1991
<option>1992
<option>1993
<option>1994
<option>1995
<option>1996
<option>1997
<option>1998
<option>1999
<option>2000
<option>2001
<option>2002
<option>2003
<option>2004
<option>2005
<option>2006
<option>2007
<option>2008
<option>2009
<option>2010
<option>2011
<option>2012
<option>2013
<option>2014
<option>2015
<option>2016
<option>2017
<option>2018
<option>2019
<option>2020

</select ><br/></html>
!Month
<html>
<sub><b>startMonth:</b></sub><br/>
<select name=startMonth >
 <option>1
 <option>2
 <option>3
 <option>4
 <option>5
 <option>6
 <option>7
 <option>8
 <option>9
 <option>10
 <option>11
 <option>12
</select ><br/>
<sub><b>endMonth:</b></sub><br/>
<select name=endMonth >
<option>1
 <option>2
 <option>3
 <option>4
 <option>5
 <option>6
 <option>7
 <option>8
 <option>9
 <option>10
 <option>11
 <option>12
</select >
<br/></html>
!Day
<html>
<sub><b>startDay:</b></sub><br/>
<select name=startDay >
 <option>1
 <option>2
 <option>3
 <option>4
 <option>5
 <option>6
 <option>7
 <option>8
 <option>9
 <option>10
 <option>11
 <option>12
 <option>13
 <option>14
 <option>15
 <option>16
 <option>17
 <option>18
 <option>19
 <option>20
 <option>21
 <option>22
 <option>23
 <option>24
 <option>25
 <option>26
 <option>27
 <option>28
 <option>29
 <option>30
 <option>31
</select ><br/>
<sub><b>endDay:</b></sub><br/>
<select name=endDay >
 <option>1
 <option>2
 <option>3
 <option>4
 <option>5
 <option>6
 <option>7
 <option>8
 <option>9
 <option>10
 <option>11
 <option>12
 <option>13
 <option>14
 <option>15
 <option>16
 <option>17
 <option>18
 <option>19
 <option>20
 <option>21
 <option>22
 <option>23
 <option>24
 <option>25
 <option>26
 <option>27
 <option>28
 <option>29
 <option>30
 <option>31
</select ><br/></html>
!Color
<html>
<sub><b>color:</b></sub><br/>
<select name=color >
 <option>Orange
 <option>Yellow
 <option>Blue
 <option>Red
</select ><br/>
<sub><b>group:</b></sub><br/>
<select name=group >
 <option>Group A
 <option>Group B
 <option>Group C
 <option>Group D
</select ><br/></html>
!Text
<html>
<sub><b>Text (no linebreaks!):</b></sub><br/>
<TEXTAREA name=text rows=4 cols=80 ></TEXTAREA>
<p/>
</html>
! %/
{{span{<<showWhenTagged Event>><<formTiddler TimelineFormTemplate1920-2020>>}}}
|~ViewToolbar|closeTiddler closeOthers +editTiddler newTaggedEvent > fields syncing permalink references jump < refreshTiddler TimelineFormTemplate1920-2020Toolbar |
|~EditToolbar|+saveTiddler -cancelTiddler deleteTiddler|
/***
Description: Contains the stuff you need to use Tiddlyspot
Note, you also need UploadPlugin, PasswordOptionPlugin and LoadRemoteFileThroughProxy
from http://tiddlywiki.bidix.info for a complete working Tiddlyspot site.
***/
//{{{

// edit this if you are migrating sites or retrofitting an existing TW
config.tiddlyspotSiteId = 'mamachap';

// make it so you can by default see edit controls via http
config.options.chkHttpReadOnly = false;
window.readOnly = false; // make sure of it (for tw 2.2)
window.showBackstage = true; // show backstage too

// disable autosave in d3
if (window.location.protocol != "file:")
	config.options.chkGTDLazyAutoSave = false;

// tweak shadow tiddlers to add upload button, password entry box etc
with (config.shadowTiddlers) {
	SiteUrl = 'http://'+config.tiddlyspotSiteId+'.tiddlyspot.com';
	SideBarOptions = SideBarOptions.replace(/(<<saveChanges>>)/,"$1<<tiddler TspotSidebar>>");
	OptionsPanel = OptionsPanel.replace(/^/,"<<tiddler TspotOptions>>");
	DefaultTiddlers = DefaultTiddlers.replace(/^/,"[[WelcomeToTiddlyspot]] ");
	MainMenu = MainMenu.replace(/^/,"[[WelcomeToTiddlyspot]] ");
}

// create some shadow tiddler content
merge(config.shadowTiddlers,{

'TspotOptions':[
 "tiddlyspot password:",
 "<<option pasUploadPassword>>",
 ""
].join("\n"),

'TspotControls':[
 "| tiddlyspot password:|<<option pasUploadPassword>>|",
 "| site management:|<<upload http://" + config.tiddlyspotSiteId + ".tiddlyspot.com/store.cgi index.html . .  " + config.tiddlyspotSiteId + ">>//(requires tiddlyspot password)//<br>[[control panel|http://" + config.tiddlyspotSiteId + ".tiddlyspot.com/controlpanel]], [[download (go offline)|http://" + config.tiddlyspotSiteId + ".tiddlyspot.com/download]]|",
 "| links:|[[tiddlyspot.com|http://tiddlyspot.com/]], [[FAQs|http://faq.tiddlyspot.com/]], [[blog|http://tiddlyspot.blogspot.com/]], email [[support|mailto:support@tiddlyspot.com]] & [[feedback|mailto:feedback@tiddlyspot.com]], [[donate|http://tiddlyspot.com/?page=donate]]|"
].join("\n"),

'WelcomeToTiddlyspot':[
 "This document is a ~TiddlyWiki from tiddlyspot.com.  A ~TiddlyWiki is an electronic notebook that is great for managing todo lists, personal information, and all sorts of things.",
 "",
 "@@font-weight:bold;font-size:1.3em;color:#444; //What now?// &nbsp;&nbsp;@@ Before you can save any changes, you need to enter your password in the form below.  Then configure privacy and other site settings at your [[control panel|http://" + config.tiddlyspotSiteId + ".tiddlyspot.com/controlpanel]] (your control panel username is //" + config.tiddlyspotSiteId + "//).",
 "<<tiddler TspotControls>>",
 "See also GettingStarted.",
 "",
 "@@font-weight:bold;font-size:1.3em;color:#444; //Working online// &nbsp;&nbsp;@@ You can edit this ~TiddlyWiki right now, and save your changes using the \"save to web\" button in the column on the right.",
 "",
 "@@font-weight:bold;font-size:1.3em;color:#444; //Working offline// &nbsp;&nbsp;@@ A fully functioning copy of this ~TiddlyWiki can be saved onto your hard drive or USB stick.  You can make changes and save them locally without being connected to the Internet.  When you're ready to sync up again, just click \"upload\" and your ~TiddlyWiki will be saved back to tiddlyspot.com.",
 "",
 "@@font-weight:bold;font-size:1.3em;color:#444; //Help!// &nbsp;&nbsp;@@ Find out more about ~TiddlyWiki at [[TiddlyWiki.com|http://tiddlywiki.com]].  Also visit [[TiddlyWiki.org|http://tiddlywiki.org]] for documentation on learning and using ~TiddlyWiki. New users are especially welcome on the [[TiddlyWiki mailing list|http://groups.google.com/group/TiddlyWiki]], which is an excellent place to ask questions and get help.  If you have a tiddlyspot related problem email [[tiddlyspot support|mailto:support@tiddlyspot.com]].",
 "",
 "@@font-weight:bold;font-size:1.3em;color:#444; //Enjoy :)// &nbsp;&nbsp;@@ We hope you like using your tiddlyspot.com site.  Please email [[feedback@tiddlyspot.com|mailto:feedback@tiddlyspot.com]] with any comments or suggestions."
].join("\n"),

'TspotSidebar':[
 "<<upload http://" + config.tiddlyspotSiteId + ".tiddlyspot.com/store.cgi index.html . .  " + config.tiddlyspotSiteId + ">><html><a href='http://" + config.tiddlyspotSiteId + ".tiddlyspot.com/download' class='button'>download</a></html>"
].join("\n")

});
//}}}
<<drawVisualization 'Tuesday, 24. April 2012'>><html><div id='Tuesday, 24. April 2012'></div></html>
| !date | !user | !location | !storeUrl | !uploadDir | !toFilename | !backupdir | !origin |
| 24/04/2012 17:21:32 | YourName | [[/|http://mamachap.tiddlyspot.com/]] | [[store.cgi|http://mamachap.tiddlyspot.com/store.cgi]] | . | [[index.html | http://mamachap.tiddlyspot.com/index.html]] | . |
| 24/04/2012 17:24:45 | YourName | [[/|http://mamachap.tiddlyspot.com/]] | [[store.cgi|http://mamachap.tiddlyspot.com/store.cgi]] | . | [[index.html | http://mamachap.tiddlyspot.com/index.html]] | . |
| 24/04/2012 17:32:19 | YourName | [[/|http://mamachap.tiddlyspot.com/]] | [[store.cgi|http://mamachap.tiddlyspot.com/store.cgi]] | . | [[index.html | http://mamachap.tiddlyspot.com/index.html]] | . |
| 24/04/2012 17:40:39 | YourName | [[/|http://mamachap.tiddlyspot.com/]] | [[store.cgi|http://mamachap.tiddlyspot.com/store.cgi]] | . | [[index.html | http://mamachap.tiddlyspot.com/index.html]] | . | ok |
| 24/04/2012 17:44:42 | YourName | [[/|http://mamachap.tiddlyspot.com/]] | [[store.cgi|http://mamachap.tiddlyspot.com/store.cgi]] | . | [[index.html | http://mamachap.tiddlyspot.com/index.html]] | . |
| 24/04/2012 17:48:54 | YourName | [[/|http://mamachap.tiddlyspot.com/]] | [[store.cgi|http://mamachap.tiddlyspot.com/store.cgi]] | . | [[index.html | http://mamachap.tiddlyspot.com/index.html]] | . |
| 24/04/2012 17:53:19 | YourName | [[/|http://mamachap.tiddlyspot.com/]] | [[store.cgi|http://mamachap.tiddlyspot.com/store.cgi]] | . | [[index.html | http://mamachap.tiddlyspot.com/index.html]] | . |
| 24/04/2012 18:00:48 | YourName | [[/|http://mamachap.tiddlyspot.com/]] | [[store.cgi|http://mamachap.tiddlyspot.com/store.cgi]] | . | [[index.html | http://mamachap.tiddlyspot.com/index.html]] | . |
| 25/04/2012 00:43:56 | YourName | [[/|http://mamachap.tiddlyspot.com/]] | [[store.cgi|http://mamachap.tiddlyspot.com/store.cgi]] | . | [[index.html | http://mamachap.tiddlyspot.com/index.html]] | . | ok |
| 25/04/2012 00:46:05 | YourName | [[/|http://mamachap.tiddlyspot.com/]] | [[store.cgi|http://mamachap.tiddlyspot.com/store.cgi]] | . | [[index.html | http://mamachap.tiddlyspot.com/index.html]] | . |
/***
|''Name:''|UploadPlugin|
|''Description:''|Save to web a TiddlyWiki|
|''Version:''|4.1.3|
|''Date:''|Feb 24, 2008|
|''Source:''|http://tiddlywiki.bidix.info/#UploadPlugin|
|''Documentation:''|http://tiddlywiki.bidix.info/#UploadPluginDoc|
|''Author:''|BidiX (BidiX (at) bidix (dot) info)|
|''License:''|[[BSD open source license|http://tiddlywiki.bidix.info/#%5B%5BBSD%20open%20source%20license%5D%5D ]]|
|''~CoreVersion:''|2.2.0|
|''Requires:''|PasswordOptionPlugin|
***/
//{{{
version.extensions.UploadPlugin = {
	major: 4, minor: 1, revision: 3,
	date: new Date("Feb 24, 2008"),
	source: 'http://tiddlywiki.bidix.info/#UploadPlugin',
	author: 'BidiX (BidiX (at) bidix (dot) info',
	coreVersion: '2.2.0'
};

//
// Environment
//

if (!window.bidix) window.bidix = {}; // bidix namespace
bidix.debugMode = false;	// true to activate both in Plugin and UploadService
	
//
// Upload Macro
//

config.macros.upload = {
// default values
	defaultBackupDir: '',	//no backup
	defaultStoreScript: "store.php",
	defaultToFilename: "index.html",
	defaultUploadDir: ".",
	authenticateUser: true	// UploadService Authenticate User
};
	
config.macros.upload.label = {
	promptOption: "Save and Upload this TiddlyWiki with UploadOptions",
	promptParamMacro: "Save and Upload this TiddlyWiki in %0",
	saveLabel: "save to web", 
	saveToDisk: "save to disk",
	uploadLabel: "upload"	
};

config.macros.upload.messages = {
	noStoreUrl: "No store URL in parmeters or options",
	usernameOrPasswordMissing: "Username or password missing"
};

config.macros.upload.handler = function(place,macroName,params) {
	if (readOnly)
		return;
	var label;
	if (document.location.toString().substr(0,4) == "http") 
		label = this.label.saveLabel;
	else
		label = this.label.uploadLabel;
	var prompt;
	if (params[0]) {
		prompt = this.label.promptParamMacro.toString().format([this.destFile(params[0], 
			(params[1] ? params[1]:bidix.basename(window.location.toString())), params[3])]);
	} else {
		prompt = this.label.promptOption;
	}
	createTiddlyButton(place, label, prompt, function() {config.macros.upload.action(params);}, null, null, this.accessKey);
};

config.macros.upload.action = function(params)
{
		// for missing macro parameter set value from options
		if (!params) params = {};
		var storeUrl = params[0] ? params[0] : config.options.txtUploadStoreUrl;
		var toFilename = params[1] ? params[1] : config.options.txtUploadFilename;
		var backupDir = params[2] ? params[2] : config.options.txtUploadBackupDir;
		var uploadDir = params[3] ? params[3] : config.options.txtUploadDir;
		var username = params[4] ? params[4] : config.options.txtUploadUserName;
		var password = config.options.pasUploadPassword; // for security reason no password as macro parameter	
		// for still missing parameter set default value
		if ((!storeUrl) && (document.location.toString().substr(0,4) == "http")) 
			storeUrl = bidix.dirname(document.location.toString())+'/'+config.macros.upload.defaultStoreScript;
		if (storeUrl.substr(0,4) != "http")
			storeUrl = bidix.dirname(document.location.toString()) +'/'+ storeUrl;
		if (!toFilename)
			toFilename = bidix.basename(window.location.toString());
		if (!toFilename)
			toFilename = config.macros.upload.defaultToFilename;
		if (!uploadDir)
			uploadDir = config.macros.upload.defaultUploadDir;
		if (!backupDir)
			backupDir = config.macros.upload.defaultBackupDir;
		// report error if still missing
		if (!storeUrl) {
			alert(config.macros.upload.messages.noStoreUrl);
			clearMessage();
			return false;
		}
		if (config.macros.upload.authenticateUser && (!username || !password)) {
			alert(config.macros.upload.messages.usernameOrPasswordMissing);
			clearMessage();
			return false;
		}
		bidix.upload.uploadChanges(false,null,storeUrl, toFilename, uploadDir, backupDir, username, password); 
		return false; 
};

config.macros.upload.destFile = function(storeUrl, toFilename, uploadDir) 
{
	if (!storeUrl)
		return null;
		var dest = bidix.dirname(storeUrl);
		if (uploadDir && uploadDir != '.')
			dest = dest + '/' + uploadDir;
		dest = dest + '/' + toFilename;
	return dest;
};

//
// uploadOptions Macro
//

config.macros.uploadOptions = {
	handler: function(place,macroName,params) {
		var wizard = new Wizard();
		wizard.createWizard(place,this.wizardTitle);
		wizard.addStep(this.step1Title,this.step1Html);
		var markList = wizard.getElement("markList");
		var listWrapper = document.createElement("div");
		markList.parentNode.insertBefore(listWrapper,markList);
		wizard.setValue("listWrapper",listWrapper);
		this.refreshOptions(listWrapper,false);
		var uploadCaption;
		if (document.location.toString().substr(0,4) == "http") 
			uploadCaption = config.macros.upload.label.saveLabel;
		else
			uploadCaption = config.macros.upload.label.uploadLabel;
		
		wizard.setButtons([
				{caption: uploadCaption, tooltip: config.macros.upload.label.promptOption, 
					onClick: config.macros.upload.action},
				{caption: this.cancelButton, tooltip: this.cancelButtonPrompt, onClick: this.onCancel}
				
			]);
	},
	options: [
		"txtUploadUserName",
		"pasUploadPassword",
		"txtUploadStoreUrl",
		"txtUploadDir",
		"txtUploadFilename",
		"txtUploadBackupDir",
		"chkUploadLog",
		"txtUploadLogMaxLine"		
	],
	refreshOptions: function(listWrapper) {
		var opts = [];
		for(i=0; i<this.options.length; i++) {
			var opt = {};
			opts.push();
			opt.option = "";
			n = this.options[i];
			opt.name = n;
			opt.lowlight = !config.optionsDesc[n];
			opt.description = opt.lowlight ? this.unknownDescription : config.optionsDesc[n];
			opts.push(opt);
		}
		var listview = ListView.create(listWrapper,opts,this.listViewTemplate);
		for(n=0; n<opts.length; n++) {
			var type = opts[n].name.substr(0,3);
			var h = config.macros.option.types[type];
			if (h && h.create) {
				h.create(opts[n].colElements['option'],type,opts[n].name,opts[n].name,"no");
			}
		}
		
	},
	onCancel: function(e)
	{
		backstage.switchTab(null);
		return false;
	},
	
	wizardTitle: "Upload with options",
	step1Title: "These options are saved in cookies in your browser",
	step1Html: "<input type='hidden' name='markList'></input><br>",
	cancelButton: "Cancel",
	cancelButtonPrompt: "Cancel prompt",
	listViewTemplate: {
		columns: [
			{name: 'Description', field: 'description', title: "Description", type: 'WikiText'},
			{name: 'Option', field: 'option', title: "Option", type: 'String'},
			{name: 'Name', field: 'name', title: "Name", type: 'String'}
			],
		rowClasses: [
			{className: 'lowlight', field: 'lowlight'} 
			]}
};

//
// upload functions
//

if (!bidix.upload) bidix.upload = {};

if (!bidix.upload.messages) bidix.upload.messages = {
	//from saving
	invalidFileError: "The original file '%0' does not appear to be a valid TiddlyWiki",
	backupSaved: "Backup saved",
	backupFailed: "Failed to upload backup file",
	rssSaved: "RSS feed uploaded",
	rssFailed: "Failed to upload RSS feed file",
	emptySaved: "Empty template uploaded",
	emptyFailed: "Failed to upload empty template file",
	mainSaved: "Main TiddlyWiki file uploaded",
	mainFailed: "Failed to upload main TiddlyWiki file. Your changes have not been saved",
	//specific upload
	loadOriginalHttpPostError: "Can't get original file",
	aboutToSaveOnHttpPost: 'About to upload on %0 ...',
	storePhpNotFound: "The store script '%0' was not found."
};

bidix.upload.uploadChanges = function(onlyIfDirty,tiddlers,storeUrl,toFilename,uploadDir,backupDir,username,password)
{
	var callback = function(status,uploadParams,original,url,xhr) {
		if (!status) {
			displayMessage(bidix.upload.messages.loadOriginalHttpPostError);
			return;
		}
		if (bidix.debugMode) 
			alert(original.substr(0,500)+"\n...");
		// Locate the storeArea div's 
		var posDiv = locateStoreArea(original);
		if((posDiv[0] == -1) || (posDiv[1] == -1)) {
			alert(config.messages.invalidFileError.format([localPath]));
			return;
		}
		bidix.upload.uploadRss(uploadParams,original,posDiv);
	};
	
	if(onlyIfDirty && !store.isDirty())
		return;
	clearMessage();
	// save on localdisk ?
	if (document.location.toString().substr(0,4) == "file") {
		var path = document.location.toString();
		var localPath = getLocalPath(path);
		saveChanges();
	}
	// get original
	var uploadParams = new Array(storeUrl,toFilename,uploadDir,backupDir,username,password);
	var originalPath = document.location.toString();
	// If url is a directory : add index.html
	if (originalPath.charAt(originalPath.length-1) == "/")
		originalPath = originalPath + "index.html";
	var dest = config.macros.upload.destFile(storeUrl,toFilename,uploadDir);
	var log = new bidix.UploadLog();
	log.startUpload(storeUrl, dest, uploadDir,  backupDir);
	displayMessage(bidix.upload.messages.aboutToSaveOnHttpPost.format([dest]));
	if (bidix.debugMode) 
		alert("about to execute Http - GET on "+originalPath);
	var r = doHttp("GET",originalPath,null,null,username,password,callback,uploadParams,null);
	if (typeof r == "string")
		displayMessage(r);
	return r;
};

bidix.upload.uploadRss = function(uploadParams,original,posDiv) 
{
	var callback = function(status,params,responseText,url,xhr) {
		if(status) {
			var destfile = responseText.substring(responseText.indexOf("destfile:")+9,responseText.indexOf("\n", responseText.indexOf("destfile:")));
			displayMessage(bidix.upload.messages.rssSaved,bidix.dirname(url)+'/'+destfile);
			bidix.upload.uploadMain(params[0],params[1],params[2]);
		} else {
			displayMessage(bidix.upload.messages.rssFailed);			
		}
	};
	// do uploadRss
	if(config.options.chkGenerateAnRssFeed) {
		var rssPath = uploadParams[1].substr(0,uploadParams[1].lastIndexOf(".")) + ".xml";
		var rssUploadParams = new Array(uploadParams[0],rssPath,uploadParams[2],'',uploadParams[4],uploadParams[5]);
		var rssString = generateRss();
		// no UnicodeToUTF8 conversion needed when location is "file" !!!
		if (document.location.toString().substr(0,4) != "file")
			rssString = convertUnicodeToUTF8(rssString);	
		bidix.upload.httpUpload(rssUploadParams,rssString,callback,Array(uploadParams,original,posDiv));
	} else {
		bidix.upload.uploadMain(uploadParams,original,posDiv);
	}
};

bidix.upload.uploadMain = function(uploadParams,original,posDiv) 
{
	var callback = function(status,params,responseText,url,xhr) {
		var log = new bidix.UploadLog();
		if(status) {
			// if backupDir specified
			if ((params[3]) && (responseText.indexOf("backupfile:") > -1))  {
				var backupfile = responseText.substring(responseText.indexOf("backupfile:")+11,responseText.indexOf("\n", responseText.indexOf("backupfile:")));
				displayMessage(bidix.upload.messages.backupSaved,bidix.dirname(url)+'/'+backupfile);
			}
			var destfile = responseText.substring(responseText.indexOf("destfile:")+9,responseText.indexOf("\n", responseText.indexOf("destfile:")));
			displayMessage(bidix.upload.messages.mainSaved,bidix.dirname(url)+'/'+destfile);
			store.setDirty(false);
			log.endUpload("ok");
		} else {
			alert(bidix.upload.messages.mainFailed);
			displayMessage(bidix.upload.messages.mainFailed);
			log.endUpload("failed");			
		}
	};
	// do uploadMain
	var revised = bidix.upload.updateOriginal(original,posDiv);
	bidix.upload.httpUpload(uploadParams,revised,callback,uploadParams);
};

bidix.upload.httpUpload = function(uploadParams,data,callback,params)
{
	var localCallback = function(status,params,responseText,url,xhr) {
		url = (url.indexOf("nocache=") < 0 ? url : url.substring(0,url.indexOf("nocache=")-1));
		if (xhr.status == 404)
			alert(bidix.upload.messages.storePhpNotFound.format([url]));
		if ((bidix.debugMode) || (responseText.indexOf("Debug mode") >= 0 )) {
			alert(responseText);
			if (responseText.indexOf("Debug mode") >= 0 )
				responseText = responseText.substring(responseText.indexOf("\n\n")+2);
		} else if (responseText.charAt(0) != '0') 
			alert(responseText);
		if (responseText.charAt(0) != '0')
			status = null;
		callback(status,params,responseText,url,xhr);
	};
	// do httpUpload
	var boundary = "---------------------------"+"AaB03x";	
	var uploadFormName = "UploadPlugin";
	// compose headers data
	var sheader = "";
	sheader += "--" + boundary + "\r\nContent-disposition: form-data; name=\"";
	sheader += uploadFormName +"\"\r\n\r\n";
	sheader += "backupDir="+uploadParams[3] +
				";user=" + uploadParams[4] +
				";password=" + uploadParams[5] +
				";uploaddir=" + uploadParams[2];
	if (bidix.debugMode)
		sheader += ";debug=1";
	sheader += ";;\r\n"; 
	sheader += "\r\n" + "--" + boundary + "\r\n";
	sheader += "Content-disposition: form-data; name=\"userfile\"; filename=\""+uploadParams[1]+"\"\r\n";
	sheader += "Content-Type: text/html;charset=UTF-8" + "\r\n";
	sheader += "Content-Length: " + data.length + "\r\n\r\n";
	// compose trailer data
	var strailer = new String();
	strailer = "\r\n--" + boundary + "--\r\n";
	data = sheader + data + strailer;
	if (bidix.debugMode) alert("about to execute Http - POST on "+uploadParams[0]+"\n with \n"+data.substr(0,500)+ " ... ");
	var r = doHttp("POST",uploadParams[0],data,"multipart/form-data; ;charset=UTF-8; boundary="+boundary,uploadParams[4],uploadParams[5],localCallback,params,null);
	if (typeof r == "string")
		displayMessage(r);
	return r;
};

// same as Saving's updateOriginal but without convertUnicodeToUTF8 calls
bidix.upload.updateOriginal = function(original, posDiv)
{
	if (!posDiv)
		posDiv = locateStoreArea(original);
	if((posDiv[0] == -1) || (posDiv[1] == -1)) {
		alert(config.messages.invalidFileError.format([localPath]));
		return;
	}
	var revised = original.substr(0,posDiv[0] + startSaveArea.length) + "\n" +
				store.allTiddlersAsHtml() + "\n" +
				original.substr(posDiv[1]);
	var newSiteTitle = getPageTitle().htmlEncode();
	revised = revised.replaceChunk("<title"+">","</title"+">"," " + newSiteTitle + " ");
	revised = updateMarkupBlock(revised,"PRE-HEAD","MarkupPreHead");
	revised = updateMarkupBlock(revised,"POST-HEAD","MarkupPostHead");
	revised = updateMarkupBlock(revised,"PRE-BODY","MarkupPreBody");
	revised = updateMarkupBlock(revised,"POST-SCRIPT","MarkupPostBody");
	return revised;
};

//
// UploadLog
// 
// config.options.chkUploadLog :
//		false : no logging
//		true : logging
// config.options.txtUploadLogMaxLine :
//		-1 : no limit
//      0 :  no Log lines but UploadLog is still in place
//		n :  the last n lines are only kept
//		NaN : no limit (-1)

bidix.UploadLog = function() {
	if (!config.options.chkUploadLog) 
		return; // this.tiddler = null
	this.tiddler = store.getTiddler("UploadLog");
	if (!this.tiddler) {
		this.tiddler = new Tiddler();
		this.tiddler.title = "UploadLog";
		this.tiddler.text = "| !date | !user | !location | !storeUrl | !uploadDir | !toFilename | !backupdir | !origin |";
		this.tiddler.created = new Date();
		this.tiddler.modifier = config.options.txtUserName;
		this.tiddler.modified = new Date();
		store.addTiddler(this.tiddler);
	}
	return this;
};

bidix.UploadLog.prototype.addText = function(text) {
	if (!this.tiddler)
		return;
	// retrieve maxLine when we need it
	var maxLine = parseInt(config.options.txtUploadLogMaxLine,10);
	if (isNaN(maxLine))
		maxLine = -1;
	// add text
	if (maxLine != 0) 
		this.tiddler.text = this.tiddler.text + text;
	// Trunck to maxLine
	if (maxLine >= 0) {
		var textArray = this.tiddler.text.split('\n');
		if (textArray.length > maxLine + 1)
			textArray.splice(1,textArray.length-1-maxLine);
			this.tiddler.text = textArray.join('\n');		
	}
	// update tiddler fields
	this.tiddler.modifier = config.options.txtUserName;
	this.tiddler.modified = new Date();
	store.addTiddler(this.tiddler);
	// refresh and notifiy for immediate update
	story.refreshTiddler(this.tiddler.title);
	store.notify(this.tiddler.title, true);
};

bidix.UploadLog.prototype.startUpload = function(storeUrl, toFilename, uploadDir,  backupDir) {
	if (!this.tiddler)
		return;
	var now = new Date();
	var text = "\n| ";
	var filename = bidix.basename(document.location.toString());
	if (!filename) filename = '/';
	text += now.formatString("0DD/0MM/YYYY 0hh:0mm:0ss") +" | ";
	text += config.options.txtUserName + " | ";
	text += "[["+filename+"|"+location + "]] |";
	text += " [[" + bidix.basename(storeUrl) + "|" + storeUrl + "]] | ";
	text += uploadDir + " | ";
	text += "[[" + bidix.basename(toFilename) + " | " +toFilename + "]] | ";
	text += backupDir + " |";
	this.addText(text);
};

bidix.UploadLog.prototype.endUpload = function(status) {
	if (!this.tiddler)
		return;
	this.addText(" "+status+" |");
};

//
// Utilities
// 

bidix.checkPlugin = function(plugin, major, minor, revision) {
	var ext = version.extensions[plugin];
	if (!
		(ext  && 
			((ext.major > major) || 
			((ext.major == major) && (ext.minor > minor))  ||
			((ext.major == major) && (ext.minor == minor) && (ext.revision >= revision))))) {
			// write error in PluginManager
			if (pluginInfo)
				pluginInfo.log.push("Requires " + plugin + " " + major + "." + minor + "." + revision);
			eval(plugin); // generate an error : "Error: ReferenceError: xxxx is not defined"
	}
};

bidix.dirname = function(filePath) {
	if (!filePath) 
		return;
	var lastpos;
	if ((lastpos = filePath.lastIndexOf("/")) != -1) {
		return filePath.substring(0, lastpos);
	} else {
		return filePath.substring(0, filePath.lastIndexOf("\\"));
	}
};

bidix.basename = function(filePath) {
	if (!filePath) 
		return;
	var lastpos;
	if ((lastpos = filePath.lastIndexOf("#")) != -1) 
		filePath = filePath.substring(0, lastpos);
	if ((lastpos = filePath.lastIndexOf("/")) != -1) {
		return filePath.substring(lastpos + 1);
	} else
		return filePath.substring(filePath.lastIndexOf("\\")+1);
};

bidix.initOption = function(name,value) {
	if (!config.options[name])
		config.options[name] = value;
};

//
// Initializations
//

// require PasswordOptionPlugin 1.0.1 or better
bidix.checkPlugin("PasswordOptionPlugin", 1, 0, 1);

// styleSheet
setStylesheet('.txtUploadStoreUrl, .txtUploadBackupDir, .txtUploadDir {width: 22em;}',"uploadPluginStyles");

//optionsDesc
merge(config.optionsDesc,{
	txtUploadStoreUrl: "Url of the UploadService script (default: store.php)",
	txtUploadFilename: "Filename of the uploaded file (default: in index.html)",
	txtUploadDir: "Relative Directory where to store the file (default: . (downloadService directory))",
	txtUploadBackupDir: "Relative Directory where to backup the file. If empty no backup. (default: ''(empty))",
	txtUploadUserName: "Upload Username",
	pasUploadPassword: "Upload Password",
	chkUploadLog: "do Logging in UploadLog (default: true)",
	txtUploadLogMaxLine: "Maximum of lines in UploadLog (default: 10)"
});

// Options Initializations
bidix.initOption('txtUploadStoreUrl','');
bidix.initOption('txtUploadFilename','');
bidix.initOption('txtUploadDir','');
bidix.initOption('txtUploadBackupDir','');
bidix.initOption('txtUploadUserName','');
bidix.initOption('pasUploadPassword','');
bidix.initOption('chkUploadLog',true);
bidix.initOption('txtUploadLogMaxLine','10');


// Backstage
merge(config.tasks,{
	uploadOptions: {text: "upload", tooltip: "Change UploadOptions and Upload", content: '<<uploadOptions>>'}
});
config.backstageTasks.push("uploadOptions");


//}}}

<!--{{{-->
<div class='toolbar' macro='toolbar [[ToolbarCommands::ViewToolbar]]'></div>
<div class='title' macro='view title'></div>
<div class='subtitle'><span macro='view modifier link'></span>, <span macro='view modified date'></span> (<span macro='message views.wikified.createdPrompt'></span> <span macro='view created date'></span>)</div>
<div class='tagging' macro='tagging'></div>
<div class='tagged' macro='tags'></div>
<div class='viewer' macro='view text wikified'></div>
<div class='tagClear'></div>
<!--}}}-->
!!This Tiddlyspot was set up to demonstrate the ~CHAPTimelinePlugin.
The following tiddlers are of relevance:
*Sample setup
**SampleTimeline
**[[Event 1]]
**[[Event 2]]
**[[Event 3]]
*Backend/Plugin/Configuration
**[[CHAPTimelinePlugin]]
**CHAPTimelineCSS
**StyleSheet
**DataTiddlerPlugin
**ForEachTiddlerPlugin


This document is a ~TiddlyWiki from tiddlyspot.com.  A ~TiddlyWiki is an electronic notebook that is great for managing todo lists, personal information, and all sorts of things.

@@font-weight:bold;font-size:1.3em;color:#444; //What now?// &nbsp;&nbsp;@@ Before you can save any changes, you need to enter your password in the form below.  Then configure privacy and other site settings at your [[control panel|http://chaptimeline.tiddlyspot.com/controlpanel]] (your control panel username is //chaptimeline//).
<<tiddler TspotControls>>
See also GettingStarted.

@@font-weight:bold;font-size:1.3em;color:#444; //Working online// &nbsp;&nbsp;@@ You can edit this ~TiddlyWiki right now, and save your changes using the "save to web" button in the column on the right.

@@font-weight:bold;font-size:1.3em;color:#444; //Working offline// &nbsp;&nbsp;@@ A fully functioning copy of this ~TiddlyWiki can be saved onto your hard drive or USB stick.  You can make changes and save them locally without being connected to the Internet.  When you're ready to sync up again, just click "upload" and your ~TiddlyWiki will be saved back to tiddlyspot.com.

@@font-weight:bold;font-size:1.3em;color:#444; //Help!// &nbsp;&nbsp;@@ Find out more about ~TiddlyWiki at [[TiddlyWiki.com|http://tiddlywiki.com]].  Also visit [[TiddlyWiki.org|http://tiddlywiki.org]] for documentation on learning and using ~TiddlyWiki. New users are especially welcome on the [[TiddlyWiki mailing list|http://groups.google.com/group/TiddlyWiki]], which is an excellent place to ask questions and get help.  If you have a tiddlyspot related problem email [[tiddlyspot support|mailto:support@tiddlyspot.com]].

@@font-weight:bold;font-size:1.3em;color:#444; //Enjoy :)// &nbsp;&nbsp;@@ We hope you like using your tiddlyspot.com site.  Please email [[feedback@tiddlyspot.com|mailto:feedback@tiddlyspot.com]] with any comments or suggestions.
{{span{<<showWhenTagged TimeLine>><<tiddler newTaggedEvent##macro with:{{tiddler.title}}>>}}}/%
!macro
<<newTiddler label:newEvent prompt:"Create a new timeline event tagged with this tiddlers title" title:"Event for $1 Group X" tag:"$1" tag:"Event" text:{{store.getTiddlerText('newTaggedEvent##formTemplate')}} focus:title>>
!formTemplate
{{hidden{
To input data - click on the toolbar "more" and "data"
To create a tiddlylink write this in the text area:
<a href="javascript:;" onclick="story.displayTiddler(null,'TiddlerTitle'); jQuery('#tiddlerDisplay').show(); return false;">Label</a>
To create a clickable image:
<a href="javascript:;" onclick="story.displayTiddler(null,'TiddlerTitle'); jQuery('#tiddlerDisplay').show(); return false;"><img src='http://SomeImageURL.jpg'width="auto" height="100"vspace="0" hspace="15" border="1" title="PrompText"></a>
}}}
! %/
&nbsp; <<tiddler RefreshTiddler>>
config.options.chkHTMLHideLinebreaks=true;