Difference between revisions of "MediaWiki:JKey.js"

From Biowikifarm Metawiki
Jump to: navigation, search
m (+Operafix toggleCollapse() — „more...“-link problem → reason: encoding &Acute;)
Line 184: Line 184:
 
  */
 
  */
 
function toggleCollapse(caller, shallExpand) {
 
function toggleCollapse(caller, shallExpand) {
var jButton = $j(caller),
+
  var jButton = $j(caller),
jTable = jButton.closest("table");
+
      jTable = jButton.closest("table");
if (jTable.length && jButton.length) {
+
  if (jTable.length && jButton.length) {
if (shallExpand===null) {
+
    if (shallExpand===null) {
shallExpand = (jButton.html()==$j.resource("expandCaption"));
+
    /* If action not passed as parameter, then test: which text currently on button?
}
+
      Note: Opera 10 has problems with encodings: an "&Acute;" character gets somewhere into
jButton.html($j.resource( shallExpand ? "collapseCaption" : "expandCaption" ));
+
      and comparison is always false: replace white spaces/  + non-words (\W)
jTable.find("tr:not(tr:first)").toggle(shallExpand); // show/hide all in set, tr of nested tables are included, but no problem.
+
      */
}
+
      var tempCurrent = jButton.html().replace(/( | )/gi,'').replace(/\W+/gi,'');
return false;
+
      var tempExpand  = $j.resource("expandCaption").replace(/( | )/gi,'').replace(/\W+/gi,'');
 +
      shallExpand = (tempCurrent==tempExpand);
 +
    }
 +
    jButton.html($j.resource( shallExpand ? "collapseCaption" : "expandCaption" ));
 +
    jTable.find("tr:not(tr:first)").toggle(shallExpand);
 +
    // show/hide all in set, tr of nested tables are included
 +
  }
 +
  return false;
 
}
 
}
  

Revision as of 14:26, 16 April 2010

/*** TEMP NOTE: HistorySubkeyHeading not yet implemented ***/
/*** TEMP NOTE: current class flags: jkey-nocontrols (OK) jkey-autostart (FAIL nested keys) jkey-simplified (NEEDS TESTING) ***/

/* Copyright (c) 2009, Stephan Opitz, JKI Berlin Dahlem
This program is free software; you can redistribute it and/or modify it under the terms of the EUPL v.1.1 or (at your option) the GNU General Public License as published by the Free Software Foundation; either GPL v.3 or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License (http://www.gnu.org/licenses/) for more details. */

/*global $j, wgContentLanguage, wgPageName, wgUserName, importScript, window, document, location, console, alert, Image */ /* = settings for JSLint */

"use strict"; // set ECMAScript 5 Strict Mode

$j.jI18n = { // resource string dictionary
	en: {
		iconStart1st	: "http://upload.wikimedia.org/wikipedia/commons/thumb/4/49/View-playback_Gion_simple.svg/20px-View-playback_Gion_simple.svg.png",
		iconStartNew	: "http://upload.wikimedia.org/wikipedia/commons/thumb/0/05/View-refresh_Gion_simple.svg/20px-View-refresh_Gion_simple.svg.png",
		iconOverview	: "http://upload.wikimedia.org/wikipedia/commons/thumb/2/22/View-pause_Gion_simple.svg/20px-View-pause_Gion_simple.svg.png",
		iconResume	: "http://upload.wikimedia.org/wikipedia/commons/thumb/4/49/View-playback_Gion_simple.svg/20px-View-playback_Gion_simple.svg.png",
		iconCloseWindow	: "http://upload.wikimedia.org/wikipedia/commons/8/87/Close_icon_default.jpg",
		iconCloseWindowHover	: "http://upload.wikimedia.org/wikipedia/commons/d/d0/Close_icon_hover.jpg",
		historyActiveOn	: "http://upload.wikimedia.org/wikipedia/commons/thumb/9/94/Symbol_support_vote.svg/20px-Symbol_support_vote.svg.png",
		historyActiveOff	: "http://upload.wikimedia.org/wikipedia/commons/thumb/3/37/Symbol_partial_support_vote.svg/20px-Symbol_partial_support_vote.svg.png",
		playerStart1st	: "Step-by-step identification",
		playerStartNew	: "Start new identification",
		playerOverview	: "Key overview (printable)",
		playerResume	: "Resume",
		coupletContinue	: " Continue ",
		editorEdit	: "Edit Key",
		editorSave	: "Save",
		certaintyLabel	: "Flag decision above as uncertain",
		certaintyHint	: "(check, then make your next decision)",
		tryAllAlternatives	: "Undecided: Try all alternatives",
		mainResultMsg	: "You identified: ",
		historyHeading	: "Previous decisions",
		historyConfirmable	: "Confirmable decisions:",
		historyNested	: "Alternative",
		historyResult	: "Result: ",
		historyConfirm	: "confirm",
		historyRevise	: "revise",
		historyUncertainFlag	: "(uncertain)",
		toolTipIsActivePath	: "Currently active identification path (multiple alternatives are being followed)",
		imageMetadataLink	: "(Information about Creator, License and Copyright)",
		collapseCaption	: "  (show less) ",
		expandCaption	: "  (more...) ",
		expandAll	: "Show all extras",
		toolTipClose	: "Click to close",
		toolTipImageZooming	: "Images can be enlarged by clicking on it",
		zoomNotPossible	: "(The enlargement function is currently not available for this image)"
	},
	de: {
		playerStart1st	: "Interaktive Bestimmung",
		playerStartNew	: "Neue Bestimmung",
		playerOverview	: "Übersichtsdarstellung (druckbar)",
		playerResume	: "Bestimmung fortsetzen",
		coupletContinue	: " Weiter ",
		editorEdit	: "Bearbeiten",
		editorSave	: "Speichern",
		certaintyLabel	: "Markiere obige Entscheidung als unsicher",
		certaintyHint	: "(markieren, dann nächste Entscheidung fällen)",
		tryAllAlternatives	: "Nicht entscheidbar: Verfolge alle Alternativen",
		mainResultMsg	: "Ergebnis: ",
		historyHeading	: "Bisherige Entscheidungen",
		historyConfirmable	: "Entscheidungen in Überprüfung:",
		historyResult	: "Ergebnis: ",
		historyConfirm	: "bestätigen",
		historyRevise	: "überarbeiten",
		historyUncertainFlag	: "(unsicher)",
		toolTipIsActivePath	: "Derzeit aktiver Bestimmungsweg (mehrere Alternativen werden verfolgt)",
		imageMetadataLink	: "(Informationen zu Autor, Lizenz und Copyright)",
		collapseCaption	: " (weniger anzeigen) ",
		expandCaption	: " (mehr...) ",
		expandAll	: "Alle Zusatzinformationen zeigen",
		toolTipClose	: "Zum Schließen klicken",
		toolTipImageZooming	: "Bilder können durch Anklicken vergrößert betrachtet werden",
		zoomNotPossible	: "(Die Vergrößerungsfunktion ist zur Zeit für dieses Bild nicht verfügbar)"
	},
	it: {
		playerStart1st	: "Esegui passo-dopo-passo",
		playerStartNew	: "Nuova identificazione",
		playerOverview	: "Sintesi completa (stampabile)",
		playerResume	: "Ricomincia l’identificazione",
		coupletContinue	: " Continua ",
		editorEdit	: "Modifica",
		editorSave	: "Salva",
		certaintyLabel	: "Segna scelta come insicura", //REVISE
		certaintyHint	: "(click, then make your next decision)", //TRANSLATE
		mainResultMsg	: "Il risultato dell'identificazione è: ",
		historyHeading	: "Scelte precedenti",
		historyConfirmable	: "Scelta confermabile:",
		historyResult	: "Risultato: ",
		historyConfirm	: "conferma",
		historyRevise	: "correggi",
		historyUncertainFlag	: "(incerta)",
		toolTipIsActivePath	: "Percorso di identificazione attualmente attivo (vengono seguite alternative multiple)",
		imageMetadataLink	: "(Informazione sull'Autore, Licenza e Copyright)",
		collapseCaption	: " (mostra di meno) ",
		expandCaption	: " (più...) ",
		expandAll	: "Mostra tutti informazione", //REVISE
		toolTipClose	: "Clicca per chiudere",
		toolTipImageZooming	: "Le immagini possono essere ingrandite cliccandoci sopra",
		zoomNotPossible	: "(Al momento non è possibilie ingrandire questa immagine)"
	}
};

/*
 * Description: Get resource string (text, image URLs) for a given language, based on a string-key
 *	If no resource is defined in a given language for a resource key, the resource for "en" will be returned, if this is missing as well an error message.
 * resourceKey: key for the resource (string)
 */
$j.resource = function (resourceKey) {
		var lang = wgContentLanguage.split("-")[0]; // language: "pt-BR", "de-formal", etc.
		return ($j.jI18n[lang] && $j.jI18n[lang][resourceKey] ?
						$j.jI18n[lang][resourceKey] :
						($j.jI18n.en[resourceKey]) ? $j.jI18n.en[resourceKey] : "MISSING RESOURCE");
	};

/*
 * Descriptions: Create html string for link with image and/or text content
 * attributes: string of combined other attributes of link element; must use ' as inner quotes, and \" inside event functions
 * imgResourceKey, txtResourceKey: resource keys; txtContent: direct string
 * href: URI (for linkBuilder only)
 */
$j.linkBuilder = function (txtResourceKey, txtContent, href, attributes) {
	return (txtResourceKey.length ? "<a href='" + href + "' " + (attributes.length ? attributes : "") + ">" + $j.resource(txtResourceKey) + "</a>" : (txtContent.length ? "<a href='" + href + "' " + (attributes.length ? attributes : "") + ">" + txtContent + "</a>" : ""));
};
$j.imglinkBuilder = function (imgResourceKey, txtResourceKey, attributes) {
	return (imgResourceKey.length ? "<a href='#'" + (attributes.length ? " " + attributes : "") + "><img src='" + $j.resource(imgResourceKey) + "' /></a>&nbsp;" : "") + $j.linkBuilder(txtResourceKey, "", "#", attributes);
};

$j.random = function (min, max) { // NO CHECKS: if(min>max) {return -1;}	if(min==max) {return min;}
	return (min + parseInt(Math.random() * (max - min + 1), 10));
};

///////////////////////
// Highlight targets //
///////////////////////

/* Description: Highlight all targets of page-internal links; generic function but
 *	especially useful in long internally linked tables like identification keys (see Template:Key_Start)
 */

// Highlight a single element that is target of the link-object caller (e.g. <a href=...>)
function highlightTarget(caller) {
	var target = $j(caller.hash.replace(/\./g, "\\.")); // hash could be 'a.34', jquery needs 'a\.34'
	if (target.length) {
		var tStyle = target.get(0).style,
			resetString = "resetHighlight(\"" + caller.hash + "\",\"" + tStyle.backgroundColor + "\",\"" + tStyle.textDecoration + "\")";
		tStyle.backgroundColor = "#EAEAEA";
		tStyle.textDecoration = "blink";
		window.setTimeout(resetString, 2000);
	}
}

// Stop highlighting
function resetHighlight(hash, backColor, txtDeco) {
	if (hash) { // reset
		var tStyle = $j(hash.replace(/\./g, "\\.")).get(0).style;
		tStyle.backgroundColor = backColor;
		tStyle.textDecoration = (txtDeco === "") ? "none" : txtDeco;
	}
}

// Add onclick events to all page-internal links
function initTargetHighlighting() {
	for (var i=0, max=document.links.length; i < max; i++) {
		var lnk = document.links[i];
		if ((lnk.pathname === location.pathname) && lnk.hash.length > 1) { // page internal link; exluding single "#"
			lnk.onclick = function() {
				highlightTarget(this);
			};
		}
	}
}

/////////////////////
// Collapse Tables //
/////////////////////

/* Description: Allows tables to be collapsed, showing only the header row.
 * Original from Wikipedia; rewritten for jquery
 */

/* Description expand or collapse table
 * caller: collapse/expand link inside a table.
 * shallExpand: optional boolean; if absent visibility will be toggled
 */
function toggleCollapse(caller, shallExpand) {
  var jButton = $j(caller),
      jTable  = jButton.closest("table");
  if (jTable.length && jButton.length) {
    if (shallExpand===null) {
    /* If action not passed as parameter, then test: which text currently on button?
       Note: Opera 10 has problems with encodings: an "&Acute;" character gets somewhere into
       and comparison is always false: replace white spaces/&nbsp; + non-words (\W)
      */
      var tempCurrent = jButton.html().replace(/(&nbsp;| )/gi,'').replace(/\W+/gi,'');
      var tempExpand  = $j.resource("expandCaption").replace(/(&nbsp;| )/gi,'').replace(/\W+/gi,'');
      shallExpand = (tempCurrent==tempExpand);
    }
    jButton.html($j.resource( shallExpand ? "collapseCaption" : "expandCaption" ));
    jTable.find("tr:not(tr:first)").toggle(shallExpand);
    // show/hide all in set, tr of nested tables are included
  }
  return false;
}

function toggleAllCollapsible(shallExpand) { // all collapsible tables on wiki page
	$j("table span.collapseButton a").each(function() {toggleCollapse(this, shallExpand);} );
	$j("span.toggleAllExtras input[type=checkbox]").each(function() {this.checked = shallExpand;} ); // in case of multiple keys
}

function initCollapseButtons() {
	var autoCollapse = 2, // CONSTANT
		idx = 0,
		linkstring = $j.linkBuilder("collapseCaption", "", "#", "onclick='return toggleCollapse(this,null);'");
	var eachTable = function() { // is closure relative to idx, linkstring
		var jTable = $j(this),
			jHeader = jTable.find("tr th"); // add collapse button only if header row present
		if (jHeader.length) {
			// init "more..."-links
			var jButton = $j(linkstring);
			jButton.get(0).style.color = jHeader.get(0).style.color;
			jHeader.append($j('<span class="collapseButton noprint"/>').append(jButton));
			// do collapse
			if (jTable.hasClass("collapsed") ||
				(idx >= autoCollapse && jTable.hasClass("autocollapse")) ||
				// also collapse inner if innercollapse and is inside outercollapse
				(jTable.hasClass("innercollapse") && jTable.closest(".outercollapse").length)) {
				toggleCollapse(jButton.get(0),null);
			}
			idx++;
		}
	};
	$j("table.collapsible").each(eachTable);
}

////////////////////////
// Exception handling //
////////////////////////

/*
 * Description: Exception constructor, together with toString method
 * errorCode: string identifying errors
 * variables: may used for detailed information
 */
function JKeyException(errorCode, variables) {
	this.errorCode = errorCode;
	this.variables = variables;
}
JKeyException.prototype.toString = function() {
	var message="JKey Exception " + this.errorCode + "\n\n";
	for (var key in this.variables) {
		message += " " + key + ": " + this.variables[key] + "\n";
	}
	return message;
};
// also override toString of default Error constructor
Error.prototype.toString = function() { 
	return "Javascript exception: " + this.name + "\n\nMessage: " + this.message + "\nFileName: " + this.fileName + "\nLineNumber: " + this.lineNumber;
};

/*
 * Description: report exception to console or alert box.
 * exception: JKeyException or JS internal exception
 */
function jkeyExceptionAlert(exception) {
	if(window.console) { // IE dev. tools (F12), or FF with firebug console ENABLED
		console.log(exception); // TODO: in ie watch: console there is no debug fct?!
	} else { // perhaps if FF write to browser-javascript-console: throw new Error("text");
		alert(exception);
	}
}

////////////////////////////////////
// Modal Layer Base functionality //
////////////////////////////////////

/*
 * Description: Hide the modal layer (cyclical dependency with next method)
 */
function jkeyModalLayer_Hide() {
	$j(document).unbind("keydown", jkeyModalLayer_KeyDown);
	$j("#jkeymodal-layer").fadeOut(function() {
		$j("#jkeymodal-overlay").hide();
		$j(this).empty().hide();
	});
}

/*
 * Description: Close (hide) modal layer on escape, backspace and arrow left key
 * e: the keyboard event object
 */
function jkeyModalLayer_KeyDown(e) {
	if ((e.keyCode == 8) || (e.keyCode == 27) || (e.keyCode == 37)) { jkeyModalLayer_Hide(); }
}

/*
 * Description: Create modal layer and execute myFunction
 * myFunction: logic executed after modal layer was created
 * myFunctionParams: generic parameters which are used by "myFunction"
 */
function jkeyModalLayer_Create(myFunction, myFunctionParams) {
	// find existing or create overlay & layer
	var jkeyModalOverlay = $j("#jkeymodal-overlay").length ? $j("#jkeymodal-overlay") : $j("<div id='jkeymodal-overlay'/>"),
		jkeyModalLayer = $j("#jkeymodal-layer").length ? $j("#jkeymodal-layer") : $j("<div id='jkeymodal-layer'/>");
	if (typeof(document.body.style.maxHeight) === "undefined") { // if IE 6
		$j("body","html").css({height: "100%", width: "100%"});
	}
	// append the overlay to document body
	$j("body").append(jkeyModalOverlay);
	// CSS: use full height, background opaque; then fade in
	jkeyModalOverlay.css("height", $j(document).height()).show();
	// activate keydown listener
	$j(document).keydown(jkeyModalLayer_KeyDown);
	// generic functionality
	jkeyModalLayer.append($j('<div style="float:right"/>')
		.append($j("<a/>")
			.append($j("<img/>").attr("src", $j.resource("iconCloseWindow")) )
			.hover(
				function() { $j(this).find("img:first").attr("src", $j.resource("iconCloseWindowHover")); },
				function() { $j(this).find("img:first").attr("src", $j.resource("iconCloseWindow")); })
			.click(function() { jkeyModalLayer_Hide(); })
		)
	);
	// Execute custom logic
	myFunction(myFunctionParams, jkeyModalLayer);
}

////////////////
// Image zoom //
////////////////

/*
* Description: load image over jkeyModalLayer
* jkeyModalLayer: access to DOM element
* newImg: image, enlarged if possible, which will be shown
* oriImg: original image currently being zoomed
*/
function jkeyModalLayer_LoadImage(jkeyModalLayer, newImg, oriImg) {
	var title = oriImg.title,
		imgWidth = newImg.width,
		imgHeight = newImg.height;
	if (imgWidth===0) { // in IE cloned image has no width; occurs only if newImg = oriImg.clone
		imgWidth = oriImg.width;
		imgHeight = oriImg.height;
	}
	// extend height of modal layer for no-zoom msg
	var zoomIsPossible = (imgWidth != oriImg.width),
		layerHeight = imgHeight + 105 + ((!zoomIsPossible) ? 60 : 0),
		layerWidth = Math.max(300, imgWidth + 70); // max to reserve min text width
	// delete alt text & add click function to hide modal
	$j(newImg).removeAttr("alt")
		.attr("title", title.replace($j.resource("toolTipImageZooming"),$j.resource("toolTipClose")))
		.click( function() {jkeyModalLayer_Hide();});
	// append content container
	jkeyModalLayer.css({width: layerWidth + "px", height: layerHeight + "px", "margin-left": -(layerWidth/2)})
	.append($j("<div/>")
		.css({"margin-left": (layerWidth-imgWidth) / 2 + "px", "margin-top": "35px"})
		.append(newImg)
	)
	.append($j("<div style=\"text-align:center; margin:2px; margin-top:8px;font-size:1.15em; font-weight:bold;\"/>")
	.append(title.replace("("+$j.resource("toolTipImageZooming")+")","")+"<br />")
	// URL to metadata page from "a[href]" around img
	.append($j($j.linkBuilder("imageMetadataLink", "", $j(oriImg).closest("a").attr("href"), "target='_blank'")))
	.append((!zoomIsPossible) ? ("<br/><br/><span style='color:red;'>" + $j.resource("zoomNotPossible") + "</span>") : "")
	);
	if ( !($j.browser.msie && $j.browser.version < 7)) { // take away IE6 modifications
		jkeyModalLayer.css({"margin-top": -((layerHeight + 8) / 2)}); // 8 from other margin-top
	}
	$j("body").append(jkeyModalLayer); // add to DOM & start
	jkeyModalLayer.fadeIn(50);
}

/*
* Description: add div container with image to modal layer
* paramsObj: needed parms for the current function
* parameter 1 = "link": reference to a image link including an img (dependent properties evaluated here)
* jkeyModalLayer: access to DOM element
*/
function jkeyModalLayer_ZoomImage(paramsObj, jkeyModalLayer) {
	var img = $j(paramsObj.link).find("img").get(0); // img inside a[href]
	var urlParts = img.src.split("/");
	if ((img.src.search(/\/thumb\//) === -1) || (urlParts[urlParts.length - 1].search(/px-/) === -1)) {
		// no larger picture possible, use existing
		jkeyModalLayer_LoadImage(jkeyModalLayer, $j(img).clone().get(0), img);
	} else { // images including "/thumb/" in path can be enlarged using URL-based wiki resize
		var stdThumbWidths = [1600,1280,1024,800,640,480,400,320,300,250,200,180,150,120,100,80],
			maxHeight = $j(window).height() - 105, // 70 are additional text; 35 are for space at top and bottom
			maxWidth = $j(window).width() - 50; // 50 are for space at left & right
		// calculate min of upscaling factors for height+width, multiply to get max possible img width
		var maxScaledWidth = img.width * Math.min(maxHeight/img.height, maxWidth/img.width);
		// reduce to next smaller standard thumb width (mediawiki preview settings plus additions)
		for (var i = 0; i < 16; i++) {
			if (stdThumbWidths[i] < maxScaledWidth) {
				maxScaledWidth = stdThumbWidths[i];
				break;
			}
		}
		urlParts[urlParts.length - 1] = maxScaledWidth + "px-" + urlParts[urlParts.length - 2];
		var newImg = new Image();
		// Load image. Load/error occur asynchronously, need independent calls
		// "random()" is necessary for IE6-8, else "zoom image, close, zoom again" fails.
		$j(newImg).attr("src", urlParts.join("/")+"&rnd="+$j.random(0,10000))
			.load(function() { // load succeeded: create modal layer after loading image, else values (width, etc.) are 0
				jkeyModalLayer_LoadImage(jkeyModalLayer, newImg, img);
			})
			.error(function() { // failed: load original picture
			// Main reason: thumbs can never be larger than ori size. Currently assuming this reason
			// Could test using API: http://commons.wikimedia.org/w/api.php?action=query&titles=Image:Lamium_purpureum_scan.jpg&prop=imageinfo&iiprop=size
			// Currently assuming and using original, unthumbed image from repository
			urlParts.pop();
			newImg = new Image();
			$j(newImg).attr("src", urlParts.join("/").replace("/thumb", "")+"&rnd="+$j.random(0,10000)) // remove "/thumb/" from url
				.load(function() {jkeyModalLayer_LoadImage(jkeyModalLayer, newImg, img);}) // load succeeded
				 // 2nd level fail -> load from wikimedia.org
				.error(function() {
					newImg = new Image();
					$j(newImg).attr("src", "http://commons.wikimedia.org/w/thumb.php?f="+urlParts[urlParts.length-1]+"&width="+maxScaledWidth+"px"+"&rnd="+$j.random(0,10000))
						.load(function() {jkeyModalLayer_LoadImage(jkeyModalLayer, newImg, img);}) // load succeeded
						 // 3rd level fail -> load unchanged wiki page thumb
						.error(function() {jkeyModalLayer_LoadImage(jkeyModalLayer, $j(img).clone().get(0), img);});				
				});
			}); // end first level error
	}
}

/*
 * Description: Show image in modal layer
 * caller: reference to a link around image
 */
function jkeyZoomImage(caller) {
	jkeyModalLayer_Create(jkeyModalLayer_ZoomImage, {link: caller});
	return false; // cancel default event
}

/*
 * Description: Initialize all images in div.decisiontree with a modal zoom/preview functionality
 */
function jkeyInitImageZooming() {
	$j("div.decisiontree").find("div.floatnone img, div.floatright img, a.image img").each(function() {
		var jParent = $j(this).parent();
		if (jParent.is("a[href]")) { // = parent is link
			var metaURL = jParent.attr("href"),
				urlParts = this.src.split("/"),
				imgFileName = (this.src.search(/\/thumb\//) != -1) ? urlParts[urlParts.length - 2] : urlParts[urlParts.length - 1];
			// ignore links to targets other than media metadata page
			if (metaURL.search(imgFileName) == -1) { return; }
			// Else add new link before, move img there, remove old link
			// TODO replace with jParent.click( function()...); should work now that we use .clone(true)!
			jParent.replaceWith( $j("<a class='image' href=\""+metaURL+"\" onclick='return jkeyZoomImage(this);'><span/></a>").append(this) );
		} else { // new link around image
			jParent.prepend($j("<a class='image' href='#' onclick='return jkeyZoomImage(this);'/>").append(this));
		}
		// set or change title, set alt to title
		var newTitle = this.alt + ((this.alt.length === 0) ? "" : " ") + "(" + $j.resource("toolTipImageZooming") + ")";
		$j(this).attr({title:newTitle, alt:newTitle});
	} );
}

/////////////////////////
// JKey player history //
/////////////////////////

/*
 * Description: history header CONSTRUCTOR
 */
function HistoryHeader() {
	/*
	 * Description: creates new header
	 * newIsActiveHistory: boolean is history set as active
	 * newIsFirstHistory: if set hides active-history-flag
	 * newContent: the new description
	 */
	this.create = function(newIsActiveHistory, newIsFirstHistory, newContent) {
		// create base header
		this.item = $j('<tr class="histHeader"/>')
		.append($j('<td class="histHeaderContent" colspan="3"/>').html(newContent + " &nbsp; ")
			.append($j('<span class="histHeaderActive"/>')
				.append($j.imglinkBuilder("historyActiveOn", "", "class='histActiveOn' onclick='return jkeySwitchHistory(this);'"))
				.toggle(!newIsFirstHistory)
				));
		this.setCurrBlockActive(newIsActiveHistory);
		return this.item;
	};
	// item specific fields
	/*
	 * Description: get "active" state; return boolean
	 */
	this.isActive = function() {
		return this.item.find("span.histHeaderActive a").hasClass("histActiveOn");
	};
	/*
	 * Description: set the active history flag
	 * newIsActiveHistory: boolean
	 */
	this.setCurrBlockActive = function(newIsActiveHistory) { // do not use imglinkBuilder replaceWith kills layout
		var histHeaderActiveCell = this.item.find("span.histHeaderActive a");
		// change class & image
		histHeaderActiveCell.attr({"class": (newIsActiveHistory ? "histActiveOn" : "histActiveOff")});
		histHeaderActiveCell.find("img").attr("src", $j.resource(newIsActiveHistory ? "historyActiveOn" : "historyActiveOff"));
	};
}
/*
 * Description: history confirm subheading CONSTRUCTOR
 */
function HistoryConfirmSubheading() {
	/* Description: create new subheading with newContent string */
	this.create = function(newContent) {
		this.item = $j("<tr class='histConfirmSubhdg'/>")
			.append($j("<td colspan='3' class='histConfirmSubhdgContent'/>").html(newContent));
		return this.item;
	};
}
/*
 * Description: history subheading CONSTRUCTOR
 */
function HistorySubkeyHeading() {
	/* Description: create new subheading with newContent string */
	this.create = function(newContent) {
		this.item = $j("<tr class='histSubkeyHdg'/>")
		.append($j("<td colspan='3' class='histSubkeyHdgContent'/>").html(newContent));
		return this.item;
	};
}
/*
 * Description: history result CONSTRUCTOR
 */
function HistoryResult() {
	/* Description: create new result item with newContent string */
	this.create = function(newContent) {
		this.item = $j("<tr class='histResult'/>")
		.append($j("<td class='histResultSymbol'/>").text("►"))
		.append($j("<td colspan='2' class='histResultContent'/>").html(newContent));
		return this.item;
	};
}

/*
 * Description: history nested CONSTRUCTOR
 */
function HistoryNested() {
	/*
	 * newContent: blocks with alternative paths 
	 */
	this.create = function(newContent) {
		// create base step
		this.item = $j('<tr class="histNested"/>')
		.append($j('<td class="histNestedEmpty"/>'))
		.append($j('<td class="histNestedContent" colspan="2"/>').html(newContent));
		return this.item;
	};
}
/*
 * Description: history step CONSTRUCTOR
 */
function HistoryStep() {
	/*
	 * Description: creates new step
	 * newStepNumber: step number
	 * newContent: description
	 * isUncertain: flag whether decision was uncertain
	 * confCoupletID: id of confirm-couplet
	 * revCoupletID: id of revise-couplet
	 */
	this.create = function(newStepNumber, newContent, isUncertain, confCoupletID, revCoupletID) {
		// create base step
		this.item = $j('<tr class ="histStep"/>')
		.append($j('<td class="histStepNumber"/>').text(newStepNumber + "."))
		.append($j('<td class="histStepContent"/>'))
		// Create revise-history action (changable to confirm). 
		.append($j('<td class="histStepActions"/>')
			// (side-requirement: href must be confCoupletID:)
			.append($j.linkBuilder("historyRevise", "", "#" + confCoupletID, " class='histStepActionRevise' onclick='return jkeyHistoryAction(this, \"" + revCoupletID + "\", \"" + confCoupletID + "\");'"))
		);
		this.setContent(newContent);
		this.setUncertainty(isUncertain);
		return this.item;
	};
	/*
	 * Description: set decision certainty of history step
	 * isUncertain: true -> display marker text
	 */
	this.setUncertainty = function(isUncertain) {
		this.item.find("td.histStepContent span.histStepCertainty")
		.html(isUncertain ? ("&nbsp;" + $j.resource("historyUncertainFlag") + "&nbsp;") : "");
	};
	/*
	 * Description: return previously recorded uncertainty for a history step (boolean)
	 */
	this.getUncertainty = function() {
		return this.item.find("td.histStepContent span.histStepCertainty").text().length > 0;
	};
	/*
	 * Description: set the step content (override inheritance)
	 * newContent: the new html value
	 */
	this.setContent = function(newContent) {
		this.item.find("td.histStepContent")
		.empty()
		.append(newContent)
		.append($j('<span class="histStepCertainty"/>')			
		);
	};
	/*
	 * Description: get number in front of step
	 */
	this.getStepNumber = function() {
		return parseInt(this.item.find("td.histStepNumber").text(),10); // parseInt example: " 8." will return 8
	};
	/*
	 * Description: set confirm/revise action
	 * setToConfirm: the new action state (true = confirm active, false = revise active)
	 */
	this.setConfirmable = function(setToConfirm) {
		this.item.find("td.histStepActions a")
		.attr({"class": (setToConfirm ? "histStepActionConfirm" : "histStepActionRevise")})
		.text($j.resource((setToConfirm ? "historyConfirm" : "historyRevise")));
	};
	/*
	 * Description: check whether action is confirm or revise
	 */
	this.isConfirmable = function() {
		return this.item.find("td.histStepActions a").is(".histStepActionConfirm");
	};
}


// global player history object, one key/history pair for each identification key on the html page
var jkeyHistory = {};
/*
 * Description: player history CONSTRUCTOR
 */
function PlayerHistory(jPlayerDiv) {
	// local variables: item types = histHeader, histStep, histSubkeyHdg, histResult, histBlock;
	// no more lazy loading for header (used immediately!), and historySubkeyHeading/historyResult (small)
	var jCurrHistBlock,
		historyHeader = new HistoryHeader(),
		historyConfirmSubheading = new HistoryConfirmSubheading(),
		historySubkeyHeading = new HistorySubkeyHeading(),
		historyResult = new HistoryResult(),
		historyNested = new HistoryNested(),
		historyStep; // LAZY LOADING
	/*
	 * Description: ensure that static class historyStep is loaded (lazy loading, deferring until used)
	 */
	var histStep_lazyLoad = function() {
		if (historyStep === undefined) { historyStep = new HistoryStep(); }
	};
	/*
	 * Description: create new history section in DOM tree, together with header row.
	 * active: true = set as active history
	 * isFirstHistory: if set hides active-history-flag
	 */
	this.createBlock = function(active, isFirstHistory, newContent) {
		return $j('<table cellpadding="0" cellspacing="0" class="'+(isFirstHistory ? "histTable" : "histBlock")+'"/>')
			.append($j('<tr class="histLayout"/>').append($j("<td/><td/><td/>")))
			.append(historyHeader.create(active, isFirstHistory, "<b>" + ((newContent) ? newContent : $j.resource("historyHeading")) + "</b>"));
	};
	/*
	 * Description: 3 methods to validate item: general, addable, specific type.
	 * item: history table row
	 */
	var isValidItem = function(item) { // maybe rename to isValidAddableItem ?!
		return (typeof(item) == 'object') && (item.tagName) && (item.tagName.toLowerCase() == "tr");
	};
	var isAddableItem = function(item) {
		return (isValidItem(item) && !$j(item).hasClass("histHeader"));
	};
	var isValidItemForType = function(item, type) {
		return (isValidItem(item) && $j(item).hasClass(type));
	};
	/*
	 * Description: add an item to historyTable
	 */
	this.addItem = function(item) {
		if (!isAddableItem(item)) {
			throw new JKeyException("NotValid", {info:"item type=" + typeof(item)});
		}
		jCurrHistBlock.append(item);
	};
	/*
	 * Description: change active history
	 */
	this.changeActiveBlock = function(newActiveBlock) {
		this.setCurrBlockActive(false); // deactivate current block
		this.setCurrBlock(newActiveBlock); // set new
		this.setCurrBlockActive(true); // activate new
	};
	/*
	 * Description: get history block
	 */
	this.getCurrBlock = function() {
		return jCurrHistBlock;
	};
	/*
	 * Description: set history block
	 */
	this.setCurrBlock = function(newBlock) {
		jCurrHistBlock = newBlock;
	};
	/*
	 * Description: first item step visible confirm link
	 * historyTable: history ref
	 * return: item object or null if not found
	 */
	this.getfirstConfirmableStep = function() {
		var retValue = null,
		jAllSteps = jCurrHistBlock.find("tr:first").nextAll("tr.histStep");
		if (jAllSteps.length) { // steps exist
			var parentThis = this;
			jAllSteps.each(function() {
				if ((retValue === null) && parentThis.withHistoryStep(this).isConfirmable()) {
					retValue = this;
				}
			});
		}
		return retValue;
	};
	/*
	 * Description: change all history action links in history table
	 * (revisable before, confirmable starting at firstConfirmableItem)
	 * historyTable: history ref
	 * firstConfirmableItem: first confirmable item after updating
	 */
	this.updateConfirmability = function(firstConfirmableItem) {
		var itemFound = false,
			// get steps within block, not including nested steps
			jAllSteps = jCurrHistBlock.find("tr:first").nextAll("tr.histStep");
		if (jAllSteps.length) { // entries exists
			var parentThis = this;
			jAllSteps.each(function() {
				if (this === firstConfirmableItem) {
					itemFound = true; // true once item was passed in loop
					// add confirm subheading in front of confirmable steps
					$j(this).before(parentThis.createHistoryConfirmSubheading("<i>" + $j.resource("historyConfirmable") + "</i>"));
				}
				// update history actions
				parentThis.withHistoryStep(this).setConfirmable(itemFound);
			});	// TODO: parentThis and itemFound create CLOSUREs - change code?
		}
	};
	/*
	 * Description: handle history path changes (cleanup of obsolete steps)
	 */
	this.cleanupAfter = function(jStep) {
		// rework history if decision path has changed. Remove confirm-sub-heading, item itself & following siblings
		jStep.prevAll("tr.histConfirmSubhdg").remove();
		jStep.nextAll("tr").andSelf().remove();
	};
	this.cleanupNestedBlocks = function() {
		// find last normal history step (or history header; fallback if no steps exists, e.g. when using try-all as first decision)
		var jStep = jCurrHistBlock.find("tr:first").nextAll("tr.histHeader, tr.histStep").filter(":last");
		if (jStep.length) {
			jStep.nextAll("tr").remove();
		}
	};
	this.cleanupConfirmableSteps = function() {
		var firstConfirmableStep = this.getfirstConfirmableStep();
		if (firstConfirmableStep) {
			this.cleanupAfter($j(firstConfirmableStep));
		}
	};

	/*
	 * Description: create a history sub-heading, nested block, or result history table row
	 * newContent: content for item
	 */	
	this.createHistoryConfirmSubheading = function(newContent) {
		return historyConfirmSubheading.create(newContent); // this.addItem( because will be added between block items
	};
	this.createHistorySubkeyHeading = function(newContent) {
		return historySubkeyHeading.create(newContent);
	};
	this.createHistoryNested = function(newContent) {
		this.addItem(historyNested.create(newContent).get(0));
	};
	this.createHistoryResult = function(newContent) {
		this.addItem(historyResult.create(newContent).get(0));
	};
	/*
	 * Description: creates a history step item
	 */
	this.createHistoryStep = function(newContent, isUncertain, confCoupletID, revCoupletID) {
		histStep_lazyLoad();
		this.addItem(historyStep.create(this.getNewStepNumber(), newContent, isUncertain, confCoupletID, revCoupletID).get(0));
	};
	/*
	 * Description: return history step class initialized with item
	 */
	this.withHistoryStep = function(item) {
		histStep_lazyLoad();
		if (!isValidItemForType(item, "histStep")) {
			throw new JKeyException("NotValid", {info:"item type=" + typeof(item)});
		}
		historyStep.item = $j(item);
		return historyStep;
	};
	// item specific fields
	/*
	 * Description: get "active" state; return boolean
	 */
	this.isActive = function() {
		historyHeader.item = jCurrHistBlock.find("tr.histHeader:first");
		return historyHeader.isActive();
	};
	/*
	 * Description: set the active history flag
	 * newIsActiveHistory: boolean
	 */
	this.setCurrBlockActive = function(newIsActive) {
		historyHeader.item = jCurrHistBlock.find("tr.histHeader:first");
		historyHeader.setCurrBlockActive(newIsActive);
	};
	/*
	 * Description: get next available number for a history step in a history block (last + 1).
	 * If the block is nested and has no steps yet, outer blocks are taken into account.
	 * historyBlock : a jquery-history-block, defaults to jCurrHistBlock if omitted
	 */
	this.getNewStepNumber = function(historyBlock) {
		if (!historyBlock) {
			historyBlock = jCurrHistBlock;
		}
		var stepNumber = 0,
			jLastStep = historyBlock.find("tr:first").nextAll("tr.histStep:last");
		if (jLastStep.length) {
			stepNumber = this.withHistoryStep(jLastStep.get(0)).getStepNumber();
		} else if (!historyBlock.is(".histTable")) { // unless outermost and not step (return 0): recurse to parent
			historyBlock = this.getParentBlock();			
			return this.getNewStepNumber(historyBlock);
		}
		return stepNumber + 1;
	};
	/*
	 * Description: checks whether first step has to be confirmed
	 */
	this.isFirstStepInBlockConfirmableStep = function() {
		var firstStep = jCurrHistBlock.find("tr:first").nextAll("tr.histStep:first");
		return (firstStep.prev("tr").hasClass("histConfirmSubhdg"));
	};
	/*
	 * Description: retrieve first nested block within current history block
	 */
	this.firstNestedStep = function() {
		return jCurrHistBlock.find("tr:first").nextAll("tr.histNested:first");
	};
	/*
	 * Description: get parent block of current block (as $j object; if already outermost -> getParentBlock.length=0)
	 */
	this.getParentBlock = function() {
		// first closest("table.histBlock, table.histTable") will find own block, then up and find parent:
		var jBlock = jCurrHistBlock.closest("table.histBlock, table.histTable");
		if (!jBlock.is(".histTable")) { // unless already outermost
			jBlock = jBlock.parent().closest("table.histBlock, table.histTable");
		}
		return jBlock; 
	};
	
	// Constructor logic - has to be at end of obj/class definition
	jCurrHistBlock = this.createBlock(true, true); // first and (here in constructor so far) only one
	jPlayerDiv.append($j('<div class="jkeyHistory dt-box"/>').hide()
		.append(jCurrHistBlock)
	);
}

/////////////////
// JKey player //
/////////////////

/*
 * Description: is jRefTarget a valid location inside a key?
 * jRefTarget: an idref as jquery object
 * return: boolean
 */
function jkeyIsValidKeyRef(jRefTarget) {
	return ( jRefTarget.is("td.dt-nodeid") || jRefTarget.is("tr.dt-row") || jRefTarget.is("div.decisiontree") );
}

/*
 * Description: transform all rows of couplet
 * jPlayerDiv: main div around player
 * jDecisionRow: first row of a couplet - must be tested prior to calling this!
 * jPlayerCouplet: position where couplet will appended
 */
function jkeyTransformCouplet(jPlayerDiv, jDecisionRow, jPlayerCouplet) {
	if (jDecisionRow.length === 0) {
		throw new JKeyException("NotFound", {info:"Transform w/o valid jDecisionRow"});
	}
	// row id is like id="Lz_1_row"; we need the id of first td inside: id="Lz_1"
	var currCoupletID = jDecisionRow.children("td[id]:first").attr("id");
	var eachLeadout = function() { // this = a leadout link
		var nextCoupletID = this.hash,
			isInternalLink = ((nextCoupletID.length > 0) && (this.href.search("/"+wgPageName+"#") != -1));
		// Example for test above: wgPageName="ThisPage", this.href "http://.../AlsoThisPageTwo#xxx" -> include must / and #
		if (isInternalLink) {
			var jNextCouplet = $j(nextCoupletID);
			// Check if valid element within a player was found
			isInternalLink = jkeyIsValidKeyRef(jNextCouplet);
			if (isInternalLink) {
				if (jNextCouplet.is("td.dt-nodeid")) { // already is the target node-id
					nextCoupletID = jNextCouplet.attr("id");
				} else { // might be row or div; get key div (closest finds itself), then table, then dt-nodeid
					// jKeyTable is not loop-invariable; a wiki page may have multiple keys!
					var jKeyTable = jNextCouplet.closest("div.decisiontree").find("table.dt-body:last"),
						jNodeID = jKeyTable.find("td.dt-nodeid:first");
					isInternalLink = (jNodeID.length>0);
					if (isInternalLink) {
						nextCoupletID = jNodeID.attr("id");
					} // else no leads found in div
				}
			} // else: local id NOT found or NOT valid for player
		} // END if isInternalLink
		// Following is NOT an else, isInternalLink may have been changed.
		nextCoupletID = (!isInternalLink) ? "" : nextCoupletID;
		// prepare resultlink (page or internal subkey) for player
		$j(this)
			.attr("target", (isInternalLink ? "_self" : "_blank"))
			.addClass("linkbtn").removeAttr("style")
			.click(function() {return jkeyDecision(this, false);});
		// save in data
		$j.data(this, "coupletID", { curr:currCoupletID, next:nextCoupletID });
	};
	var eachLeadon = function() { // this = a leadon link
		var jThis = $j(this);
		if (jThis.parent().hasClass("leadon")) { // for leadon (but not leadontext) overwrite display text
			jThis.html($j.resource("coupletContinue"));
		}
		jThis.addClass("linkbtn").removeAttr("style");
		// General problem: changing link onclick attribute works in FF, but is ignored by IE (known bug)
		// One general solution is to build complete new link and delete previous one.
		// Also working is use of jquery.click(), but watch referencing "this". Here "this" is correct because each().
		jThis.click(function() {return jkeyDecision(this, false);});
		// save in data
		$j.data(this, "coupletID", { curr:currCoupletID, next:this.hash.substring(1, this.hash.length) });
	};
	var prefixID = function() {return "jK" + this.id;};
	// Process all leads in couplet
	// Leads may be non-consecutive (general nested order = "1 2 2* 1* 3 3*" or nested subkeys = "1 alpha beta 1*->2 2->3 2* gamma epsilon 3 3*").
	// jquery [attribute^=value] Matches elements that have specified attribute and it starting with value.
	// Trailing "_" after currCoupletID because non-consecutive IDs possible ("Lz_1_row", "Lz_1000_row"); Example: 3 leads within couplet = "Lz_1_row"/"Lz_1_2_row"/"Lz_1_3_row"
	// Notes: * Using nextAll().andSelf() would add first lead (jDecisionRow) at the end
	// * Using .prev().nextAll() fails for first couplet of horizontal style, which has no row before!
	jDecisionRow.parent().children("tr.dt-row[id^="+currCoupletID+"_]").each(function() {
		var jClonedRow = $j(this).clone(true);
		// prefix id of decision row itself and all descendants
		jClonedRow.find("[id]").andSelf().attr("id", prefixID);
		// replace lead identifier with arrow (normal) or blank (horizontal side-by-side leads)
		var jNodeCell = jClonedRow.find("td.dt-nodeid:first");
		// CHECK why coupletID must be added here in addition to data stored generally for each link
		// couplet ID has to be extracted and stored as data
		$j.data(jNodeCell.get(0), "couplet", { id : $j.trim(jNodeCell.text()) }); // needed for editor & multipleStep startup
		jNodeCell.html((jClonedRow.get(0).className.search(/dt-row-hor\w+/) == -1) ? "►" : " ");
		jClonedRow.find("td.leadalt").empty();
		// Transform all relevant leadout (= result pages) and leadon/leadontext (next couplet) links
		// (depending on row mode, multiple links may exist)
		jClonedRow.find("span.leadout a").each(eachLeadout);
		jClonedRow.find("span.leadon a, div.leadontext a, span.leadontext a").each(eachLeadon);
		jPlayerCouplet.append(jClonedRow.show());
	}); // END each lead row of couplet
}

/*
 * Description: Load couplet in  player
 * jPlayerDiv: main div around player
 * coupletID: id of the couplet to be loaded
 * isCertain: preset for certainty checkbox (normally true, but may be false when restoring from history)
 */
function jkeyLoadCouplet(jPlayerDiv, coupletID, isUncertain) {
	var jPlayerCouplet = jPlayerDiv.find("table.dt-body");
	jPlayerCouplet.empty(); // flush
	// coupletID could be 'a.34', jquery needs 'a\.34'
	// $j("#"... -> document scope, coupletID may be in other key on same page
	jkeyTransformCouplet(jPlayerDiv, $j("#" + coupletID.replace(/\./g,"\\.")).closest("tr"), jPlayerCouplet);
	// after history actions: results div may have to be hidden, and certainty div re-displayed
	jPlayerDiv.find("div.jkeyResultMsg").hide();
	jPlayerDiv.find("div.certaintyDiv").show()
		.find("input#decisionUncertain").get(0).checked = isUncertain; // setting checkbox
}
/*
 * Description: Load result in  player
 * jPlayerDiv: main div around player
 * jResult = $j object containing the combined result html (commonnames, resultlink, qualifier); will be cloned
 */
function jkeyLoadResult(jPlayerDiv, jResult) {
	jPlayerDiv.find("div.jkeyResultMsg").empty().html($j.resource("mainResultMsg")).append(jResult.clone()); 
	jkeySetMode("finished", jPlayerDiv);
}

/*
 * Description: toggle visibility of player controls (top right player area)
 * mode: string for current control mode;
 *	values are "resumed", "overview", "newstart", "finished"
 * elementInKeyDiv: any element inside div.decisiontree or div.decisiontree itself, Either as DOM ref OR as "#id" string
 * (.closest() works inclusive!), usually a caller of a control (link, button).
 * returns: jKeyDiv to be further used elsewhere
 */
function jkeySetMode(mode, elementInKeyDiv) {
	try {
		// Cancel if called with undefined element; may occur for "newstart"
		// when called based on undefined id from location.hash
		if (elementInKeyDiv === undefined) { return; }
		var jKeyDiv = $j(elementInKeyDiv).closest("div.decisiontree"),
			jKeyTable = jKeyDiv.find("table.dt-body:last"),
			jPlayerDiv = jKeyDiv.find("div.jkeyPlayer"),
			jPlayerCouplet = jPlayerDiv.find("table.dt-body"),
			jResultDiv = jPlayerDiv.find("div.jkeyResultMsg"),
			jControls = jKeyDiv.find("td.jkeyControls");
		if (mode == "firststart") { // change start permanently to restart icon/text, add overview/resume controls
			jControls.find("span.jkeyPlayerStart1st").replaceWith("<span class='jkeyPlayerOverview nowrap'> &nbsp; &nbsp;" +
				$j.imglinkBuilder("iconOverview", "playerOverview", "onclick='return jkeySetMode(\"overview\",this);'") +
				"</span> <span class='jkeyPlayerResume nowrap'> &nbsp; &nbsp;" +
				$j.imglinkBuilder("iconResume", "playerResume", "onclick='return jkeySetMode(\"resumed\",this);'")	+
				"</span> <span class='jkeyPlayerStartNew nowrap'> &nbsp; &nbsp;" +
				$j.imglinkBuilder("iconStartNew", "playerStartNew", "onclick='return jkeySetMode(\"newstart\",this);'") +
				"</span>");
			mode = "newstart";
		}
		var finished = (mode=="finished"),
			overview = !(mode=="resumed" || mode=="newstart" || finished);		
		jControls.find("span.jkeyPlayerOverview").toggle(!overview);
		jControls.find("span.jkeyPlayerResume").toggle(overview);
		jKeyDiv.find("div.dt-header").toggle(overview); // metadata box (description, audience, etc.)
		// Show finished message only in finished mode (+ set below for resumed)
		jResultDiv.toggle(finished);
		switch(mode) {
		case("overview"): // STOP player, hide player, show original overview player
			jKeyDiv.find("div.jkeyLCtrls input.editbtn").show();
			jKeyDiv.removeClass("jkeyCanvas");
			jKeyTable.show();
			jPlayerDiv.hide();
			break;
		case("newstart"):
			// Delete player-div in case of restart (also invalidates jPlayerCouplet)
			jPlayerDiv.remove();
			jKeyTable.hide();
			// create new player & table, transform couplet
			jPlayerCouplet = $j('<table class="dt-body" cellspacing="0" cellpadding="0"/>');
			jPlayerDiv = $j('<div class="jkeyPlayer"/>')
				.append(jPlayerCouplet)
				// Append a hidden result section (for finished mode)
				.append($j('<div class="jkeyResultMsg"/>').hide());
			if (!jKeyDiv.hasClass("jkey-simplified")) {
				jPlayerDiv.append($j('<div class="certaintyDiv noprint"/>')
					.html('<input type="checkbox" id="decisionUncertain" value="1" /> <label for="decisionUncertain" style="font-weight:bold">' + $j.resource("certaintyLabel") + "</label>&nbsp;"	+ $j.resource("certaintyHint") + '&nbsp; &nbsp;<span class="tryAllDiv noprint"/><input type="button" onclick="return jkeyTryAll(this);" value="'+$j.resource("tryAllAlternatives")+'" /></span>'));
			}
			// generate unique player id			
			var uniquePlayerID;
			do {
				uniquePlayerID = "jkp_"+$j.random(1,9999999);
			} while (jkeyHistory.uniquePlayerID); // until not yet used
			// add id to connect with history
			jPlayerDiv.attr("id", uniquePlayerID);
			// initalize player history class (lazy loading)
			jkeyHistory[uniquePlayerID] = new PlayerHistory(jPlayerDiv);
			// Initialize player with first decision row (table may start with spacer row)
			jKeyTable.before(jPlayerDiv); // add to DOM
			jkeyTransformCouplet(jKeyDiv, jKeyTable.find("tr.dt-row:first"), jPlayerCouplet);
			// NOTE: NO BREAK here, fallthrough to "resumed"
		case("resumed"): // = player resumed. This may have to show couplet or finished mode
			// style change for key div + hide original table & show table in player
			jKeyDiv.find("div.jkeyLCtrls input.editbtn").hide();
			jKeyDiv.addClass("jkeyCanvas");
			jKeyTable.hide();
			jPlayerCouplet.show();
			jPlayerDiv.show();
			// redisplay result div if still filled
			jResultDiv.toggle(jResultDiv.text().length>0);
			break;
		case("finished"):	// empty current couplet, hide certainty checkbox
			jPlayerCouplet.empty();
			jPlayerDiv.find("div.certaintyDiv").hide();
			break;
		} // end case
		return false; // cancel default event
	}	catch (err) {
		jkeyExceptionAlert(err);
	}
}

/*
 * Description: perform "confirm" or "revise" action from history
 * caller: DOM link; confirm if class=histStepActionConfirm, revise if histStepActionRevise
 * revCoupletID, confCoupletID: id of couplet to be revised or confirmed
 */ 
function jkeyHistoryAction(caller, revCoupletID, confCoupletID) {
	try {
		// get currently active history (find first & look in hierarchy)
		var jCaller = $j(caller),
			jStepItem = jCaller.closest("tr"), // history row around action link
			jPlayerDiv = jStepItem.closest("div.jkeyPlayer"),
			jCurrHistory = jkeyHistory[jPlayerDiv.attr("id")];
		// set history block to the one containing the caller (or outermost histTable itself)
		jCurrHistory.changeActiveBlock(jCaller.closest("table.histBlock, table.histTable"));
		// Handle possible history change
		if (jCaller.hasClass("histStepActionConfirm")) { // confirm was clicked
			revCoupletID = confCoupletID; // revCoupletID = next couplet to load
			// Advance to next history step
			jStepItem = jStepItem.nextAll("tr.histStep:first");
			// Update history step certainty from player checkbox
			var firstConfirmableStep = jCurrHistory.getfirstConfirmableStep();
			if (firstConfirmableStep) { // null if none found
				// firstConfirmableStep is the couplet shown in player!
				jCurrHistory.withHistoryStep(firstConfirmableStep)
					.setUncertainty(jPlayerDiv.find("div.certaintyDiv input#decisionUncertain").is(":checked"));
			}
		}
		// Remove confirm "confirmable-decisions" sub-heading; will be recreated in updateConfirmability if necessary
		jCurrHistory.getCurrBlock().find("tr.histConfirmSubhdg").remove();
		if (revCoupletID === "") { // RESULT
			// try-all may result in multiple alternative results. Thus confirming a result needs to refresh rather than re-display			
			jkeyLoadResult(jPlayerDiv, jCaller.closest("tr.histStep").next("tr.histResult").find("span.leadout"));
		} else { // Refresh player with couplet corresponding to jStep
			var nextDecisionIsUncertain = false; // default
			if (jStepItem.length) { // extract history step certainty
				nextDecisionIsUncertain = jCurrHistory.withHistoryStep(jStepItem.get(0)).getUncertainty();
			}
			jkeyLoadCouplet(jPlayerDiv, revCoupletID, nextDecisionIsUncertain);
		}
		jCurrHistory.updateConfirmability(jStepItem.get(0));
	}	catch (err) {
		jkeyExceptionAlert(err);
	}
	return false; // cancel default event
}

/*
 * Description: Get and simplify corresponding leadtxt (without links etc., used for history items)
 * Notes: Templates Lead and Decision Horizontal use class:leadon and caller-link (class leadon) is in td.leadresult
 * > (Decision Horizontal uses both leadresult and leadresult-hor1 as class names)
 * > Template Lead Link (cross-refs) uses class:leadontext,
 * > Decision S2 uses class:leadspan; here caller is in th.leadtxt!
 * leadLinkCaller: DOM reference of the calling element, which has to be a link (e.g: under leadon span)
 */
function jkeySimplifiedLeadtxt(leadLinkCaller) {
	var jContainer = $j(leadLinkCaller).closest("td.leadresult, th.leadtxt"),
		jSpan;
	if (jContainer.hasClass("leadtxt")) {
		// occurs if leadspan itself is formatted as link (having both leadspan and leadontext class)
		jSpan = jContainer.find("span.leadspan");
	} else if (jContainer.hasClass("leadresult")) {
		// pos of span.leadspan depends on arrangement (horizontal or not)
		jSpan = (jContainer.get(0).className.search(/leadresult-hor\w+/) != -1) ? jContainer.closest("table").find("td.leadtxt:nth-child(" + (jContainer.get(0).cellIndex + 1) + ")").find("span.leadspan") : jContainer.prev("th.leadtxt").find("span.leadspan");
	}
	if (jSpan.length === 0) {
		throw new JKeyException("NotFound", {info:"No lead statement!"});
	}
	// Clone span; remove links, breaks, imgs
	var jSimplifiedLeadTxt = jSpan.clone(true);
	jSimplifiedLeadTxt.find("a").each( function() { $j(this).replaceWith($j(this).html()); } );
	jSimplifiedLeadTxt.find("br, img").remove();	
	return jSimplifiedLeadTxt;
}

/*
 * Description: Used in onclick events for both next couplet and final result, i.e. user made a decision.
 *	Add current lead to history, show next couplet in player or finish (result)
 * caller: DOM reference of the calling element (has $j.data with members: next & curr (couplet ID))
 * quiet: do not output couplets or results, only add to history
 */
function jkeyDecision(caller, quiet) { 
	try {
		var jCaller = $j(caller),
			jContainer = jCaller.closest("td.leadresult, th.leadtxt"),
			jPlayerDiv = jContainer.closest("div.jkeyPlayer"),
			jSimplifiedLeadTxt = jkeySimplifiedLeadtxt(caller), // single time this is called
			jCurrHistory = jkeyHistory[jPlayerDiv.attr("id")],
			nextCoupletID = $j.data(caller, "coupletID").next; // ref to id attribute of next couplet when used on next couplet link
		// Decision in player may be identical to current confirmable history step
		var firstConfirmableStep = jCurrHistory.getfirstConfirmableStep();
		if (firstConfirmableStep) { // = null if not found
			// is it a nested block and is it first step, which has to be confirmed
			// means user decided to change path so that the nested block will be removed
			var jParentBlock = jCurrHistory.getParentBlock(); // null if not nested
			// in inner, nested blocks, confirming first step implies removing the try-all structure
			if (!jParentBlock.is(".histTable") && jCurrHistory.isFirstStepInBlockConfirmableStep()) {    
				jCurrHistory.setCurrBlock(jParentBlock);
				jCurrHistory.setCurrBlockActive(true);
				jCurrHistory.cleanupNestedBlocks();
			} else { // 
				var jStep = $j(firstConfirmableStep);
				// Is nextCoupletID identical to hash of firstConfirmableStep action link?
				var firstConfItemActionLink = jStep.find("td.histStepActions a").get(0);
				if (firstConfItemActionLink.hash == "#"+nextCoupletID) { 
					// Selection in player is identical to current confirmable history action, i.e. does NOT change path.
					// Execute confirm and exit jkeyDecision immediately after
					jkeyHistoryAction(firstConfItemActionLink, nextCoupletID, nextCoupletID);
					return false;
				}
				// from here, path has changed; rework history. 1. Remove confirm sub-heading, item itself & following siblings
				jCurrHistory.cleanupAfter(jStep);
				// empty result-div of player (no longer valid)
				jPlayerDiv.children("div.jkeyResultMsg").empty().hide();
			}
		} else { // enable history div (disabled at start)
			jPlayerDiv.find("div.jkeyHistory").show();			
			// remove all following if path has changed
			jCurrHistory.cleanupNestedBlocks();
		}
		// Add new step with simplified lead text to history
		jCurrHistory.createHistoryStep(jSimplifiedLeadTxt.html(),
			jPlayerDiv.find("div.certaintyDiv input#decisionUncertain").is(":checked"),
			nextCoupletID, $j.data(caller, "coupletID").curr
		);
		if (nextCoupletID.length) { // -> NEXT couplet
			if (!quiet) {
				// Last parameter false: default (i.e. user can change this later) for NEXT decision is certain 
				jkeyLoadCouplet(jPlayerDiv, nextCoupletID, false);
			}
		} else { // -> RESULT
			// Prepare main result link, common names and resultqualifier (latter 2 may be missing!).
			var jResult = $j('<span class="leadout"/>')
				.append(jContainer.find("span.commonnames").clone().append(" "))
				.append(jCaller.clone().removeClass("linkbtn").unbind('click'))
				.append(jContainer.find("span.resultqualifier").clone().prepend(" "));
			jResult.find("br, img").remove(); // don't combine with above!
			// Add History result row (use .clone(), result already used above)
			// ## TODO: is it possible to avoid redundant span element? Text node?
			jCurrHistory.createHistoryResult($j('<span/>').append($j.resource("historyResult")).append(jResult));
			if (!quiet) {	// load into main result area
				jkeyLoadResult(jPlayerDiv, jResult);
			}
		}
	}	catch (err) {
		jkeyExceptionAlert(err);
	}
	return false; // cancel default event
}

/*
 * Description: Used in onclick event of button "try all decisions"
 * caller: DOM reference to a link
 */
function jkeyTryAll(caller) {
	var eachDecisionLink = function (jCurrHistory, jParentBlock, caller, idx) {
			// create new block for current link & add it to parent block. +idx = unary operator to cast to numeric
			var jBlock = jCurrHistory.createBlock(false, false, $j.resource("historyNested") + " " + (+idx + 1) + ":");
			jCurrHistory.createHistoryNested(jBlock);
			jCurrHistory.setCurrBlock(jBlock);
			jkeyDecision(caller, true); // true = no couplet/result loading into main window, history only			
			jCurrHistory.setCurrBlock(jParentBlock); // revert current block to parent
		};
	var jPlayerDiv = $j(caller).closest("div.jkeyPlayer"),
		jCurrHistory = jkeyHistory[jPlayerDiv.attr("id")];
	// Only if nested steps not already present:
	if (jCurrHistory.firstNestedStep().length===0) {
		// jkeyTryAll always implies that all later steps must be removed
		jCurrHistory.cleanupConfirmableSteps();
		var jNestingParent = jCurrHistory.getCurrBlock(); // preserve current for loop
		// add all lead-on and lead-out links to history
		jPlayerDiv.find("table.dt-body:first").find("span.leadon a, span.leadout a").each(function (idx) {
			eachDecisionLink(jCurrHistory, jNestingParent, this, idx);
		});
	}
	// activate first Nested history block; mark as active in history, then load couplet or result
	jCurrHistory.firstNestedStep().find("span.histHeaderActive a").click();	
	return false; // cancel default event
}
/*
 * Description: switch between history blocks (multiple alternatives if couplet could not be decided)
 * caller: DOM reference to a link
 */
function jkeySwitchHistory(caller) {
	var jClosestHistory = $j(caller).closest("table.histBlock, table.histTable");
	// set new & show last entry
	jkeyHistory[jClosestHistory.closest("div.jkeyPlayer").attr("id")].changeActiveBlock(jClosestHistory);
	// find direct last step (excluding nested), click TWICE on action (revise it & then confirm it)
	jClosestHistory.find("tr:first").nextAll("tr.histStep:last").find("td.histStepActions a").click().click();
	return false; // cancel default event
}
/*
 * Description: Load editor javascript source and add button (called through window.setTimeout!)
 */
function jkeyInitEditor() {
	if (wgUserName) {
		if (document.URL.toString().search("LocalTestFile.htm") === -1) { // DEBUG, remove later
			importScript("MediaWiki:JKeyEditor.js");
		}
		$j("div.decisiontree").find("div.jkeyLCtrls:first").append(" &nbsp; <input class='editbtn' type='button' style='font-size:0.76em;vertical-align:middle;' value='" + $j.resource("editorEdit") + "' onclick='return jedtInitKey(this);' />"); // editor only for ff activated
	}
}
/*
 * Description: Initialize interactive mode (step-by-step) for key if keys exist, init key editor delayed;
 * Also: start player automatically if URL has hash pointing into a valid key
 */
function jkeyInit() {
	var jKeys = $j("div.decisiontree");
	if (jKeys.length) { // only if at least one key exists
		$j("head").append("<style type=\"text/css\">" +
		// modal layer styles
		"#jkeymodal-overlay {position:fixed; z-index:100; top:0px; left:0px; height:100%; width:100%; background:#000; opacity:0.8; display:none;}\n" +
		"#jkeymodal-layer {position:fixed; z-index:101; top:50%; left:50%; padding:3px; border:3px solid; background-color:#FFFFFF; display:none;}\n" +
		"#jkeymodal-layer img {display:block;}\n" +
		// /* IE6 hack: IE-CSS-expression is very slow; inserted only for IE < 7.
		($j.browser.msie && $j.browser.version < 7 ? "* html #jkeymodal-overlay {position: absolute; height:expression(document.body.scrollHeight > document.body.offsetHeight ? document.body.scrollHeight : document.body.offsetHeight + 'px');}\n* html #jkeymodal-layer {position: absolute; margin-top: expression(0 - parseInt(this.offsetHeight / 2) + (TBWindowMargin = document.documentElement && document.documentElement.scrollTop || document.body.scrollTop) + 'px');}\n" : "") +
		// player styles. Note on Canvas: FF does not need top 1em, but IE does!
		"div.jkeyCanvas {background-color:#FFFFFF; border:5px solid #FFC51A; padding:0.7em 1em 1em 1em}\n" +
		"table.dt-caption {width:98%}\n" + // necessary for Safari 4
		"div.jkeyLCtrls {margin:0.3em 0 0;padding:0;}\n" +
		"td.jkeyControls {text-align:right; background-color:transparent;}\n" +
		"td.jkeyControls a {vertical-align: middle;}" +
		"div.jkeyPlayer table.dt-body {margin:0.5em}\n" +
		"div.jkeyResultMsg {margin:0 0 1em; padding:1em; font-weight:bold; background-color:#EEF0F0; border:1px solid #444444; }\n" +
		"div.jkeyResultMsg span.leadout, div.jkeyResultMsg span.commonnames {background-color:transparent}\n" +
		"div.certaintyDiv {margin:2em 0 1em 0; padding:0.6em 0.2em; border-top:1px solid #D0D0D0;}\n" +
		"input[type=checkbox] {vertical-align:middle;}\n" + // MAKE GENERIC??
		"div.jkeyHistory {margin-top:1em;}\n" +
		"table.histTable, table.histBlock {background-color:transparent;}\n" +
		"table.histBlock {border-left:1px solid; width:100%;}\n" +
		"td.histStepNumber, td.histNestedEmpty, td.histResultSymbol {width:1em; padding:0 0.6em;}\n" +
		"td.histHeaderContent, td.histConfirmSubhdgContent {padding-left:0.6em;}\n" +
		"td.histStepActions {text-align:center; width:50px; padding-left:1em; }\n" +
		"td.histStepNumber, td.histStepActions, td.histResultSymbol {vertical-align:top;}\n" +
		"span.histStepCertainty {background-color:#FFA07A;}\n" +
		"div.jkeyPlayer table.dt-body td.dt-nodeid, div.jkeyPlayer table.dt-body th.leadtxt, div.jkeyPlayer table.dt-body td.leadresult {padding-top:1em;line-height:1.7em}\n" +
		// where span.leadout followed by span.leadon, the two a.linkbtn easily overlap. OK with padding 1.5px!
		"a.linkbtn {background-color:#EEF0F0; border:1px solid #444444; padding:1.5px 6px; text-align:center; font-weight:bold; white-space:nowrap;}\n" +
		"a.linkbtn:visited, a.linkbtn:hover {color:#444444; text-decoration:none;}\n" +
		"a.linkbtn:hover {background-color:#DFDFDF;}\n" +
		"@media screen, handheld, projection {*.printonly {display:none;}} @media print {*.noprint {display:none;}}\n" +
		"</style>");
		// flag: show interactive key controls only for keys without class "jkey-nocontrols"
		var filtered = jKeys.not(".jkey-nocontrols");
		filtered.find("table.dt-caption tr:first").append($j('<td class="jkeyControls noprint"/>').html("<span class='jkeyPlayerStart1st nowrap'> &nbsp; &nbsp;" + $j.imglinkBuilder("iconStart1st", "playerStart1st", "onclick='return jkeySetMode(\"firststart\",this);'") + "</span>"));
		// always add "show-all-extras" checkbox (edit button will also be added later here)
		filtered.find("table.dt-body").before("<div class='jkeyLCtrls nowrap' style='font-size:0.8em'> &nbsp; &nbsp;<input type='checkbox' id='toggleAllExtras' value='1' onclick='toggleAllCollapsible(this.checked)' /> <label for='toggleAllExtras'>" + $j.resource("expandAll") + "</label></span>");
		// if current URL contains hash (>1 to ignore "#" itself): attempt to start that player (will check for undefined ids)
		if (document.location.hash.length > 1) {
			if (jkeyIsValidKeyRef($j(document.location.hash))) {
				jkeySetMode("firststart", $j(document.location.hash));
			}
		} else { // test flag: jkey-autostart = automatically change to interactive mode   
			jKeys.filter(".jkey-autostart").each(function() {
				jkeySetMode("firststart", "#"+this.id);
			});
		}
		// delayed loading of editor js and creation of edit button
		window.setTimeout("jkeyInitEditor()", 500);
	}
}

/*
 * Description: Called after html document and all js-sources are loaded
 */
$j(document).ready(function() {
	initCollapseButtons(); // execute first; this changes page layout strongly
	jkeyInitImageZooming();
	initTargetHighlighting(); // page-internal jumps
	jkeyInit(); // init player & editor
});