Difference between revisions of "MediaWiki:JKey.js"

From Biowikifarm Metawiki
Jump to: navigation, search
m (+Operafix toggleCollapse() — „more...“-link problem → reason: encoding &Acute;)
Line 10: Line 10:
  
 
$j.jI18n = { // resource string dictionary
 
$j.jI18n = { // resource string dictionary
en: {
+
  en: {
iconStart1st : "http://upload.wikimedia.org/wikipedia/commons/thumb/4/49/View-playback_Gion_simple.svg/20px-View-playback_Gion_simple.svg.png",
+
    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",
+
    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",
+
    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",
+
    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",
+
    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",
+
    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",
+
    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",
+
    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",
+
    playerStart1st : "Step-by-step identification",
playerStartNew : "Start new identification",
+
    playerStartNew : "Start new identification",
playerOverview : "Key overview (printable)",
+
    playerOverview : "Key overview (printable)",
playerResume : "Resume",
+
    playerResume : "Resume",
coupletContinue : " Continue ",
+
    coupletContinue : " Continue ",
editorEdit : "Edit Key",
+
    editorEdit : "Edit Key",
editorSave : "Save",
+
    editorSave : "Save",
certaintyLabel : "Flag decision above as uncertain",
+
    certaintyLabel : "Flag decision above as uncertain",
certaintyHint : "(check, then make your next decision)",
+
    certaintyHint : "(check, then make your next decision)",
tryAllAlternatives : "Undecided: Try all alternatives",
+
    tryAllAlternatives : "Undecided: Try all alternatives",
mainResultMsg : "You identified: ",
+
    mainResultMsg : "You identified: ",
historyHeading : "Previous decisions",
+
    historyHeading : "Previous decisions",
historyConfirmable : "Confirmable decisions:",
+
    historyConfirmable : "Confirmable decisions:",
historyNested : "Alternative",
+
    historyNested : "Alternative",
historyResult : "Result: ",
+
    historyResult : "Result: ",
historyConfirm : "confirm",
+
    historyConfirm : "confirm",
historyRevise : "revise",
+
    historyRevise : "revise",
historyUncertainFlag : "(uncertain)",
+
    historyUncertainFlag : "(uncertain)",
toolTipIsActivePath : "Currently active identification path (multiple alternatives are being followed)",
+
    toolTipIsActivePath : "Currently active identification path (multiple alternatives are being followed)",
imageMetadataLink : "(Information about Creator, License and Copyright)",
+
    imageMetadataLink : "(Information about Creator, License and Copyright)",
collapseCaption : "  (show less) ",
+
    collapseCaption : " (show less) ",
expandCaption : "  (more...) ",
+
    expandCaption : " (more...) ",
expandAll : "Show all extras",
+
    expandAll : "Show all extras",
toolTipClose : "Click to close",
+
    toolTipClose : "Click to close",
toolTipImageZooming : "Images can be enlarged by clicking on it",
+
    toolTipImageZooming : "Images can be enlarged by clicking on it",
zoomNotPossible : "(The enlargement function is currently not available for this image)"
+
    zoomNotPossible : "(The enlargement function is currently not available for this image)"
},
+
  },
de: {
+
  de: {
playerStart1st : "Interaktive Bestimmung",
+
    playerStart1st : "Interaktive Bestimmung",
playerStartNew : "Neue Bestimmung",
+
    playerStartNew : "Neue Bestimmung",
playerOverview : "Übersichtsdarstellung (druckbar)",
+
    playerOverview : "Übersichtsdarstellung (druckbar)",
playerResume : "Bestimmung fortsetzen",
+
    playerResume : "Bestimmung fortsetzen",
coupletContinue : " Weiter ",
+
    coupletContinue : " Weiter ",
editorEdit : "Bearbeiten",
+
    editorEdit : "Bearbeiten",
editorSave : "Speichern",
+
    editorSave : "Speichern",
certaintyLabel : "Markiere obige Entscheidung als unsicher",
+
    certaintyLabel : "Markiere obige Entscheidung als unsicher",
certaintyHint : "(markieren, dann nächste Entscheidung fällen)",
+
    certaintyHint : "(markieren, dann nächste Entscheidung fällen)",
tryAllAlternatives : "Nicht entscheidbar: Verfolge alle Alternativen",
+
    tryAllAlternatives : "Nicht entscheidbar: Verfolge alle Alternativen",
mainResultMsg : "Ergebnis: ",
+
    mainResultMsg : "Ergebnis: ",
historyHeading : "Bisherige Entscheidungen",
+
    historyHeading : "Bisherige Entscheidungen",
historyConfirmable : "Entscheidungen in Überprüfung:",
+
    historyConfirmable : "Entscheidungen in Überprüfung:",
historyResult : "Ergebnis: ",
+
    historyResult : "Ergebnis: ",
historyConfirm : "bestätigen",
+
    historyConfirm : "bestätigen",
historyRevise : "überarbeiten",
+
    historyRevise : "überarbeiten",
historyUncertainFlag : "(unsicher)",
+
    historyUncertainFlag : "(unsicher)",
toolTipIsActivePath : "Derzeit aktiver Bestimmungsweg (mehrere Alternativen werden verfolgt)",
+
    toolTipIsActivePath : "Derzeit aktiver Bestimmungsweg (mehrere Alternativen werden verfolgt)",
imageMetadataLink : "(Informationen zu Autor, Lizenz und Copyright)",
+
    imageMetadataLink : "(Informationen zu Autor, Lizenz und Copyright)",
collapseCaption : " (weniger anzeigen) ",
+
    collapseCaption : " (weniger anzeigen) ",
expandCaption : " (mehr...) ",
+
    expandCaption : " (mehr...) ",
expandAll : "Alle Zusatzinformationen zeigen",
+
    expandAll : "Alle Zusatzinformationen zeigen",
toolTipClose : "Zum Schließen klicken",
+
    toolTipClose : "Zum Schließen klicken",
toolTipImageZooming : "Bilder können durch Anklicken vergrößert betrachtet werden",
+
    toolTipImageZooming : "Bilder können durch Anklicken vergrößert betrachtet werden",
zoomNotPossible : "(Die Vergrößerungsfunktion ist zur Zeit für dieses Bild nicht verfügbar)"
+
    zoomNotPossible : "(Die Vergrößerungsfunktion ist zur Zeit für dieses Bild nicht verfügbar)"
},
+
  },
it: {
+
  it: {
playerStart1st : "Esegui passo-dopo-passo",
+
    playerStart1st : "Esegui passo-dopo-passo",
playerStartNew : "Nuova identificazione",
+
    playerStartNew : "Nuova identificazione",
playerOverview : "Sintesi completa (stampabile)",
+
    playerOverview : "Sintesi completa (stampabile)",
playerResume : "Ricomincia l’identificazione",
+
    playerResume : "Ricomincia l’identificazione",
coupletContinue : " Continua ",
+
    coupletContinue : " Continua ",
editorEdit : "Modifica",
+
    editorEdit : "Modifica",
editorSave : "Salva",
+
    editorSave : "Salva",
certaintyLabel : "Segna scelta come insicura", //REVISE
+
    certaintyLabel : "Segna scelta come insicura", //REVISE
certaintyHint : "(click, then make your next decision)", //TRANSLATE
+
    certaintyHint : "(click, then make your next decision)", //TRANSLATE
mainResultMsg : "Il risultato dell'identificazione è: ",
+
    mainResultMsg : "Il risultato dell'identificazione è: ",
historyHeading : "Scelte precedenti",
+
    historyHeading : "Scelte precedenti",
historyConfirmable : "Scelta confermabile:",
+
    historyConfirmable : "Scelta confermabile:",
historyResult : "Risultato: ",
+
    historyResult : "Risultato: ",
historyConfirm : "conferma",
+
    historyConfirm : "conferma",
historyRevise : "correggi",
+
    historyRevise : "correggi",
historyUncertainFlag : "(incerta)",
+
    historyUncertainFlag : "(incerta)",
toolTipIsActivePath : "Percorso di identificazione attualmente attivo (vengono seguite alternative multiple)",
+
    toolTipIsActivePath : "Percorso di identificazione attualmente attivo (vengono seguite alternative multiple)",
imageMetadataLink : "(Informazione sull'Autore, Licenza e Copyright)",
+
    imageMetadataLink : "(Informazione sull'Autore, Licenza e Copyright)",
collapseCaption : " (mostra di meno) ",
+
    collapseCaption : " (mostra di meno) ",
expandCaption : " (più...) ",
+
    expandCaption : " (più...) ",
expandAll : "Mostra tutti informazione", //REVISE
+
    expandAll : "Mostra tutti informazione", //REVISE
toolTipClose : "Clicca per chiudere",
+
    toolTipClose : "Clicca per chiudere",
toolTipImageZooming : "Le immagini possono essere ingrandite cliccandoci sopra",
+
    toolTipImageZooming : "Le immagini possono essere ingrandite cliccandoci sopra",
zoomNotPossible : "(Al momento non è possibilie ingrandire questa immagine)"
+
    zoomNotPossible : "(Al momento non è possibilie ingrandire questa immagine)"
}
+
  }
 
};
 
};
  
 
/*
 
/*
 
  * Description: Get resource string (text, image URLs) for a given language, based on a string-key
 
  * 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.
+
  * 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)
 
  * resourceKey: key for the resource (string)
 
  */
 
  */
 
$j.resource = function (resourceKey) {
 
$j.resource = function (resourceKey) {
var lang = wgContentLanguage.split("-")[0]; // language: "pt-BR", "de-formal", etc.
+
    var lang = wgContentLanguage.split("-")[0]; // language: "pt-BR", "de-formal", etc.
return ($j.jI18n[lang] && $j.jI18n[lang][resourceKey] ?
+
    return ($j.jI18n[lang] && $j.jI18n[lang][resourceKey] ?
$j.jI18n[lang][resourceKey] :
+
            $j.jI18n[lang][resourceKey] :
($j.jI18n.en[resourceKey]) ? $j.jI18n.en[resourceKey] : "MISSING RESOURCE");
+
            ($j.jI18n.en[resourceKey]) ? $j.jI18n.en[resourceKey] : "MISSING RESOURCE");
};
+
  };
  
 
/*
 
/*
Line 120: Line 120:
 
  */
 
  */
 
$j.linkBuilder = function (txtResourceKey, txtContent, href, attributes) {
 
$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>" : ""));
+
  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) {
 
$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);
+
  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;}
+
$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));
+
  return (min + parseInt(Math.random() * (max - min + 1), 10));
 
};
 
};
  
Line 135: Line 135:
  
 
/* Description: Highlight all targets of page-internal links; generic function but
 
/* 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)
+
  * 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=...>)
 
// Highlight a single element that is target of the link-object caller (e.g. <a href=...>)
 
function highlightTarget(caller) {
 
function highlightTarget(caller) {
var target = $j(caller.hash.replace(/\./g, "\\.")); // hash could be 'a.34', jquery needs 'a\.34'
+
  var target = $j(caller.hash.replace(/\./g, "\\.")); // hash could be 'a.34', jquery needs 'a\.34'
if (target.length) {
+
  if (target.length) {
var tStyle = target.get(0).style,
+
    var tStyle = target.get(0).style,
resetString = "resetHighlight(\"" + caller.hash + "\",\"" + tStyle.backgroundColor + "\",\"" + tStyle.textDecoration + "\")";
+
      resetString = "resetHighlight(\"" + caller.hash + "\",\"" + tStyle.backgroundColor + "\",\"" + tStyle.textDecoration + "\")";
tStyle.backgroundColor = "#EAEAEA";
+
    tStyle.backgroundColor = "#EAEAEA";
tStyle.textDecoration = "blink";
+
    tStyle.textDecoration = "blink";
window.setTimeout(resetString, 2000);
+
    window.setTimeout(resetString, 2000);
}
+
  }
 
}
 
}
  
 
// Stop highlighting
 
// Stop highlighting
 
function resetHighlight(hash, backColor, txtDeco) {
 
function resetHighlight(hash, backColor, txtDeco) {
if (hash) { // reset
+
  if (hash) { // reset
var tStyle = $j(hash.replace(/\./g, "\\.")).get(0).style;
+
    var tStyle = $j(hash.replace(/\./g, "\\.")).get(0).style;
tStyle.backgroundColor = backColor;
+
    tStyle.backgroundColor = backColor;
tStyle.textDecoration = (txtDeco === "") ? "none" : txtDeco;
+
    tStyle.textDecoration = (txtDeco === "") ? "none" : txtDeco;
}
+
  }
 
}
 
}
  
 
// Add onclick events to all page-internal links
 
// Add onclick events to all page-internal links
 
function initTargetHighlighting() {
 
function initTargetHighlighting() {
for (var i=0, max=document.links.length; i < max; i++) {
+
  for (var i=0, max=document.links.length; i < max; i++) {
var lnk = document.links[i];
+
    var lnk = document.links[i];
if ((lnk.pathname === location.pathname) && lnk.hash.length > 1) { // page internal link; exluding single "#"
+
    if ((lnk.pathname === location.pathname) && lnk.hash.length > 1) { // page internal link; exluding single "#"
lnk.onclick = function() {
+
      lnk.onclick = function() {
highlightTarget(this);
+
        highlightTarget(this);
};
+
      };
}
+
    }
}
+
  }
 
}
 
}
  
Line 204: Line 204:
  
 
function toggleAllCollapsible(shallExpand) { // all collapsible tables on wiki page
 
function toggleAllCollapsible(shallExpand) { // all collapsible tables on wiki page
$j("table span.collapseButton a").each(function() {toggleCollapse(this, shallExpand);} );
+
  $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
+
  $j("span.toggleAllExtras input[type=checkbox]").each(function() {this.checked = shallExpand;} ); // in case of multiple keys
 
}
 
}
  
 
function initCollapseButtons() {
 
function initCollapseButtons() {
var autoCollapse = 2, // CONSTANT
+
  var autoCollapse = 2, // CONSTANT
idx = 0,
+
    idx = 0,
linkstring = $j.linkBuilder("collapseCaption", "", "#", "onclick='return toggleCollapse(this,null);'");
+
    linkstring = $j.linkBuilder("collapseCaption", "", "#", "onclick='return toggleCollapse(this,null);'");
var eachTable = function() { // is closure relative to idx, linkstring
+
  var eachTable = function() { // is closure relative to idx, linkstring
var jTable = $j(this),
+
    var jTable = $j(this),
jHeader = jTable.find("tr th"); // add collapse button only if header row present
+
      jHeader = jTable.find("tr th"); // add collapse button only if header row present
if (jHeader.length) {
+
    if (jHeader.length) {
// init "more..."-links
+
      // init "more..."-links
var jButton = $j(linkstring);
+
      var jButton = $j(linkstring);
jButton.get(0).style.color = jHeader.get(0).style.color;
+
      jButton.get(0).style.color = jHeader.get(0).style.color;
jHeader.append($j('<span class="collapseButton noprint"/>').append(jButton));
+
      jHeader.append($j('<span class="collapseButton noprint"/>').append(jButton));
// do collapse
+
      // do collapse
if (jTable.hasClass("collapsed") ||
+
      if (jTable.hasClass("collapsed") ||
(idx >= autoCollapse && jTable.hasClass("autocollapse")) ||
+
        (idx >= autoCollapse && jTable.hasClass("autocollapse")) ||
// also collapse inner if innercollapse and is inside outercollapse
+
        // also collapse inner if innercollapse and is inside outercollapse
(jTable.hasClass("innercollapse") && jTable.closest(".outercollapse").length)) {
+
        (jTable.hasClass("innercollapse") && jTable.closest(".outercollapse").length)) {
toggleCollapse(jButton.get(0),null);
+
        toggleCollapse(jButton.get(0),null);
}
+
      }
idx++;
+
      idx++;
}
+
    }
};
+
  };
$j("table.collapsible").each(eachTable);
+
  $j("table.collapsible").each(eachTable);
 
}
 
}
  
Line 243: Line 243:
 
  */
 
  */
 
function JKeyException(errorCode, variables) {
 
function JKeyException(errorCode, variables) {
this.errorCode = errorCode;
+
  this.errorCode = errorCode;
this.variables = variables;
+
  this.variables = variables;
 
}
 
}
 
JKeyException.prototype.toString = function() {
 
JKeyException.prototype.toString = function() {
var message="JKey Exception " + this.errorCode + "\n\n";
+
  var message="JKey Exception " + this.errorCode + "\n\n";
for (var key in this.variables) {
+
  for (var key in this.variables) {
message += " " + key + ": " + this.variables[key] + "\n";
+
    message += " " + key + ": " + this.variables[key] + "\n";
}
+
  }
return message;
+
  return message;
 
};
 
};
 
// also override toString of default Error constructor
 
// also override toString of default Error constructor
 
Error.prototype.toString = function() {  
 
Error.prototype.toString = function() {  
return "Javascript exception: " + this.name + "\n\nMessage: " + this.message + "\nFileName: " + this.fileName + "\nLineNumber: " + this.lineNumber;
+
  return "Javascript exception: " + this.name + "\n\nMessage: " + this.message + "\nFileName: " + this.fileName + "\nLineNumber: " + this.lineNumber;
 
};
 
};
  
Line 263: Line 263:
 
  */
 
  */
 
function jkeyExceptionAlert(exception) {
 
function jkeyExceptionAlert(exception) {
if(window.console) { // IE dev. tools (F12), or FF with firebug console ENABLED
+
  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?!
+
    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");
+
  } else { // perhaps if FF write to browser-javascript-console: throw new Error("text");
alert(exception);
+
    alert(exception);
}
+
  }
 
}
 
}
  
Line 278: Line 278:
 
  */
 
  */
 
function jkeyModalLayer_Hide() {
 
function jkeyModalLayer_Hide() {
$j(document).unbind("keydown", jkeyModalLayer_KeyDown);
+
  $j(document).unbind("keydown", jkeyModalLayer_KeyDown);
$j("#jkeymodal-layer").fadeOut(function() {
+
  $j("#jkeymodal-layer").fadeOut(function() {
$j("#jkeymodal-overlay").hide();
+
    $j("#jkeymodal-overlay").hide();
$j(this).empty().hide();
+
    $j(this).empty().hide();
});
+
  });
 
}
 
}
  
Line 290: Line 290:
 
  */
 
  */
 
function jkeyModalLayer_KeyDown(e) {
 
function jkeyModalLayer_KeyDown(e) {
if ((e.keyCode == 8) || (e.keyCode == 27) || (e.keyCode == 37)) { jkeyModalLayer_Hide(); }
+
  if ((e.keyCode == 8) || (e.keyCode == 27) || (e.keyCode == 37)) { jkeyModalLayer_Hide(); }
 
}
 
}
  
Line 299: Line 299:
 
  */
 
  */
 
function jkeyModalLayer_Create(myFunction, myFunctionParams) {
 
function jkeyModalLayer_Create(myFunction, myFunctionParams) {
// find existing or create overlay & layer
+
  // find existing or create overlay & layer
var jkeyModalOverlay = $j("#jkeymodal-overlay").length ? $j("#jkeymodal-overlay") : $j("<div id='jkeymodal-overlay'/>"),
+
  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'/>");
+
    jkeyModalLayer = $j("#jkeymodal-layer").length ? $j("#jkeymodal-layer") : $j("<div id='jkeymodal-layer'/>");
if (typeof(document.body.style.maxHeight) === "undefined") { // if IE 6
+
  if (typeof(document.body.style.maxHeight) === "undefined") { // if IE 6
$j("body","html").css({height: "100%", width: "100%"});
+
    $j("body","html").css({height: "100%", width: "100%"});
}
+
  }
// append the overlay to document body
+
  // append the overlay to document body
$j("body").append(jkeyModalOverlay);
+
  $j("body").append(jkeyModalOverlay);
// CSS: use full height, background opaque; then fade in
+
  // CSS: use full height, background opaque; then fade in
jkeyModalOverlay.css("height", $j(document).height()).show();
+
  jkeyModalOverlay.css("height", $j(document).height()).show();
// activate keydown listener
+
  // activate keydown listener
$j(document).keydown(jkeyModalLayer_KeyDown);
+
  $j(document).keydown(jkeyModalLayer_KeyDown);
// generic functionality
+
  // generic functionality
jkeyModalLayer.append($j('<div style="float:right"/>')
+
  jkeyModalLayer.append($j('<div style="float:right"/>')
.append($j("<a/>")
+
    .append($j("<a/>")
.append($j("<img/>").attr("src", $j.resource("iconCloseWindow")) )
+
      .append($j("<img/>").attr("src", $j.resource("iconCloseWindow")) )
.hover(
+
      .hover(
function() { $j(this).find("img:first").attr("src", $j.resource("iconCloseWindowHover")); },
+
        function() { $j(this).find("img:first").attr("src", $j.resource("iconCloseWindowHover")); },
function() { $j(this).find("img:first").attr("src", $j.resource("iconCloseWindow")); })
+
        function() { $j(this).find("img:first").attr("src", $j.resource("iconCloseWindow")); })
.click(function() { jkeyModalLayer_Hide(); })
+
      .click(function() { jkeyModalLayer_Hide(); })
)
+
    )
);
+
  );
// Execute custom logic
+
  // Execute custom logic
myFunction(myFunctionParams, jkeyModalLayer);
+
  myFunction(myFunctionParams, jkeyModalLayer);
 
}
 
}
  
Line 336: Line 336:
 
*/
 
*/
 
function jkeyModalLayer_LoadImage(jkeyModalLayer, newImg, oriImg) {
 
function jkeyModalLayer_LoadImage(jkeyModalLayer, newImg, oriImg) {
var title = oriImg.title,
+
  var title = oriImg.title,
imgWidth = newImg.width,
+
    imgWidth = newImg.width,
imgHeight = newImg.height;
+
    imgHeight = newImg.height;
if (imgWidth===0) { // in IE cloned image has no width; occurs only if newImg = oriImg.clone
+
  if (imgWidth===0) { // in IE cloned image has no width; occurs only if newImg = oriImg.clone
imgWidth = oriImg.width;
+
    imgWidth = oriImg.width;
imgHeight = oriImg.height;
+
    imgHeight = oriImg.height;
}
+
  }
// extend height of modal layer for no-zoom msg
+
  // extend height of modal layer for no-zoom msg
var zoomIsPossible = (imgWidth != oriImg.width),
+
  var zoomIsPossible = (imgWidth != oriImg.width),
layerHeight = imgHeight + 105 + ((!zoomIsPossible) ? 60 : 0),
+
    layerHeight = imgHeight + 105 + ((!zoomIsPossible) ? 60 : 0),
layerWidth = Math.max(300, imgWidth + 70); // max to reserve min text width
+
    layerWidth = Math.max(300, imgWidth + 70); // max to reserve min text width
// delete alt text & add click function to hide modal
+
  // delete alt text & add click function to hide modal
$j(newImg).removeAttr("alt")
+
  $j(newImg).removeAttr("alt")
.attr("title", title.replace($j.resource("toolTipImageZooming"),$j.resource("toolTipClose")))
+
    .attr("title", title.replace($j.resource("toolTipImageZooming"),$j.resource("toolTipClose")))
.click( function() {jkeyModalLayer_Hide();});
+
    .click( function() {jkeyModalLayer_Hide();});
// append content container
+
  // append content container
jkeyModalLayer.css({width: layerWidth + "px", height: layerHeight + "px", "margin-left": -(layerWidth/2)})
+
  jkeyModalLayer.css({width: layerWidth + "px", height: layerHeight + "px", "margin-left": -(layerWidth/2)})
.append($j("<div/>")
+
  .append($j("<div/>")
.css({"margin-left": (layerWidth-imgWidth) / 2 + "px", "margin-top": "35px"})
+
    .css({"margin-left": (layerWidth-imgWidth) / 2 + "px", "margin-top": "35px"})
.append(newImg)
+
    .append(newImg)
)
+
  )
.append($j("<div style=\"text-align:center; margin:2px; margin-top:8px;font-size:1.15em; font-weight:bold;\"/>")
+
  .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 />")
+
  .append(title.replace("("+$j.resource("toolTipImageZooming")+")","")+"<br />")
// URL to metadata page from "a[href]" around img
+
  // URL to metadata page from "a[href]" around img
.append($j($j.linkBuilder("imageMetadataLink", "", $j(oriImg).closest("a").attr("href"), "target='_blank'")))
+
  .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>") : "")
+
  .append((!zoomIsPossible) ? ("<br/><br/><span style='color:red;'>" + $j.resource("zoomNotPossible") + "</span>") : "")
);
+
  );
if ( !($j.browser.msie && $j.browser.version < 7)) { // take away IE6 modifications
+
  if ( !($j.browser.msie && $j.browser.version < 7)) { // take away IE6 modifications
jkeyModalLayer.css({"margin-top": -((layerHeight + 8) / 2)}); // 8 from other margin-top
+
    jkeyModalLayer.css({"margin-top": -((layerHeight + 8) / 2)}); // 8 from other margin-top
}
+
  }
$j("body").append(jkeyModalLayer); // add to DOM & start
+
  $j("body").append(jkeyModalLayer); // add to DOM & start
jkeyModalLayer.fadeIn(50);
+
  jkeyModalLayer.fadeIn(50);
 
}
 
}
  
Line 377: Line 377:
 
*/
 
*/
 
function jkeyModalLayer_ZoomImage(paramsObj, jkeyModalLayer) {
 
function jkeyModalLayer_ZoomImage(paramsObj, jkeyModalLayer) {
var img = $j(paramsObj.link).find("img").get(0); // img inside a[href]
+
  var img = $j(paramsObj.link).find("img").get(0); // img inside a[href]
var urlParts = img.src.split("/");
+
  var urlParts = img.src.split("/");
if ((img.src.search(/\/thumb\//) === -1) || (urlParts[urlParts.length - 1].search(/px-/) === -1)) {
+
  if ((img.src.search(/\/thumb\//) === -1) || (urlParts[urlParts.length - 1].search(/px-/) === -1)) {
// no larger picture possible, use existing
+
    // no larger picture possible, use existing
jkeyModalLayer_LoadImage(jkeyModalLayer, $j(img).clone().get(0), img);
+
    jkeyModalLayer_LoadImage(jkeyModalLayer, $j(img).clone().get(0), img);
} else { // images including "/thumb/" in path can be enlarged using URL-based wiki resize
+
  } 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],
+
    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
+
      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
+
      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
+
    // 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);
+
    var maxScaledWidth = img.width * Math.min(maxHeight/img.height, maxWidth/img.width);
// reduce to next smaller standard thumb width (mediawiki preview settings plus additions)
+
    // reduce to next smaller standard thumb width (mediawiki preview settings plus additions)
for (var i = 0; i < 16; i++) {
+
    for (var i = 0; i < 16; i++) {
if (stdThumbWidths[i] < maxScaledWidth) {
+
      if (stdThumbWidths[i] < maxScaledWidth) {
maxScaledWidth = stdThumbWidths[i];
+
        maxScaledWidth = stdThumbWidths[i];
break;
+
        break;
}
+
      }
}
+
    }
urlParts[urlParts.length - 1] = maxScaledWidth + "px-" + urlParts[urlParts.length - 2];
+
    urlParts[urlParts.length - 1] = maxScaledWidth + "px-" + urlParts[urlParts.length - 2];
var newImg = new Image();
+
    var newImg = new Image();
// Load image. Load/error occur asynchronously, need independent calls
+
    // Load image. Load/error occur asynchronously, need independent calls
// "random()" is necessary for IE6-8, else "zoom image, close, zoom again" fails.
+
    // "random()" is necessary for IE6-8, else "zoom image, close, zoom again" fails.
$j(newImg).attr("src", urlParts.join("/")+"&rnd="+$j.random(0,10000))
+
    $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
+
      .load(function() { // load succeeded: create modal layer after loading image, else values (width, etc.) are 0
jkeyModalLayer_LoadImage(jkeyModalLayer, newImg, img);
+
        jkeyModalLayer_LoadImage(jkeyModalLayer, newImg, img);
})
+
      })
.error(function() { // failed: load original picture
+
      .error(function() { // failed: load original picture
// Main reason: thumbs can never be larger than ori size. Currently assuming this reason
+
      // 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
+
      // 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
+
      // Currently assuming and using original, unthumbed image from repository
urlParts.pop();
+
      urlParts.pop();
newImg = new Image();
+
      newImg = new Image();
$j(newImg).attr("src", urlParts.join("/").replace("/thumb", "")+"&rnd="+$j.random(0,10000)) // remove "/thumb/" from url
+
      $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
+
        .load(function() {jkeyModalLayer_LoadImage(jkeyModalLayer, newImg, img);}) // load succeeded
// 2nd level fail -> load from wikimedia.org
+
        // 2nd level fail -> load from wikimedia.org
.error(function() {
+
        .error(function() {
newImg = new Image();
+
          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))
+
          $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
+
            .load(function() {jkeyModalLayer_LoadImage(jkeyModalLayer, newImg, img);}) // load succeeded
// 3rd level fail -> load unchanged wiki page thumb
+
            // 3rd level fail -> load unchanged wiki page thumb
.error(function() {jkeyModalLayer_LoadImage(jkeyModalLayer, $j(img).clone().get(0), img);});
+
            .error(function() {jkeyModalLayer_LoadImage(jkeyModalLayer, $j(img).clone().get(0), img);});      
});
+
        });
}); // end first level error
+
      }); // end first level error
}
+
  }
 
}
 
}
  
Line 428: Line 428:
 
  */
 
  */
 
function jkeyZoomImage(caller) {
 
function jkeyZoomImage(caller) {
jkeyModalLayer_Create(jkeyModalLayer_ZoomImage, {link: caller});
+
  jkeyModalLayer_Create(jkeyModalLayer_ZoomImage, {link: caller});
return false; // cancel default event
+
  return false; // cancel default event
 
}
 
}
  
Line 436: Line 436:
 
  */
 
  */
 
function jkeyInitImageZooming() {
 
function jkeyInitImageZooming() {
$j("div.decisiontree").find("div.floatnone img, div.floatright img, a.image img").each(function() {
+
  $j("div.decisiontree").find("div.floatnone img, div.floatright img, a.image img").each(function() {
var jParent = $j(this).parent();
+
    var jParent = $j(this).parent();
if (jParent.is("a[href]")) { // = parent is link
+
    if (jParent.is("a[href]")) { // = parent is link
var metaURL = jParent.attr("href"),
+
      var metaURL = jParent.attr("href"),
urlParts = this.src.split("/"),
+
        urlParts = this.src.split("/"),
imgFileName = (this.src.search(/\/thumb\//) != -1) ? urlParts[urlParts.length - 2] : urlParts[urlParts.length - 1];
+
        imgFileName = (this.src.search(/\/thumb\//) != -1) ? urlParts[urlParts.length - 2] : urlParts[urlParts.length - 1];
// ignore links to targets other than media metadata page
+
      // ignore links to targets other than media metadata page
if (metaURL.search(imgFileName) == -1) { return; }
+
      if (metaURL.search(imgFileName) == -1) { return; }
// Else add new link before, move img there, remove old link
+
      // 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)!
+
      // 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) );
+
      jParent.replaceWith( $j("<a class='image' href=\""+metaURL+"\" onclick='return jkeyZoomImage(this);'><span/></a>").append(this) );
} else { // new link around image
+
    } else { // new link around image
jParent.prepend($j("<a class='image' href='#' onclick='return jkeyZoomImage(this);'/>").append(this));
+
      jParent.prepend($j("<a class='image' href='#' onclick='return jkeyZoomImage(this);'/>").append(this));
}
+
    }
// set or change title, set alt to title
+
    // set or change title, set alt to title
var newTitle = this.alt + ((this.alt.length === 0) ? "" : " ") + "(" + $j.resource("toolTipImageZooming") + ")";
+
    var newTitle = this.alt + ((this.alt.length === 0) ? "" : " ") + "(" + $j.resource("toolTipImageZooming") + ")";
$j(this).attr({title:newTitle, alt:newTitle});
+
    $j(this).attr({title:newTitle, alt:newTitle});
} );
+
  } );
 
}
 
}
  
Line 464: Line 464:
 
  */
 
  */
 
function HistoryHeader() {
 
function HistoryHeader() {
/*
+
  /*
* Description: creates new header
+
  * Description: creates new header
* newIsActiveHistory: boolean is history set as active
+
  * newIsActiveHistory: boolean is history set as active
* newIsFirstHistory: if set hides active-history-flag
+
  * newIsFirstHistory: if set hides active-history-flag
* newContent: the new description
+
  * newContent: the new description
*/
+
  */
this.create = function(newIsActiveHistory, newIsFirstHistory, newContent) {
+
  this.create = function(newIsActiveHistory, newIsFirstHistory, newContent) {
// create base header
+
    // create base header
this.item = $j('<tr class="histHeader"/>')
+
    this.item = $j('<tr class="histHeader"/>')
.append($j('<td class="histHeaderContent" colspan="3"/>').html(newContent + " &nbsp; ")
+
    .append($j('<td class="histHeaderContent" colspan="3"/>').html(newContent + " &nbsp; ")
.append($j('<span class="histHeaderActive"/>')
+
      .append($j('<span class="histHeaderActive"/>')
.append($j.imglinkBuilder("historyActiveOn", "", "class='histActiveOn' onclick='return jkeySwitchHistory(this);'"))
+
        .append($j.imglinkBuilder("historyActiveOn", "", "class='histActiveOn' onclick='return jkeySwitchHistory(this);'"))
.toggle(!newIsFirstHistory)
+
        .toggle(!newIsFirstHistory)
));
+
        ));
this.setCurrBlockActive(newIsActiveHistory);
+
    this.setCurrBlockActive(newIsActiveHistory);
return this.item;
+
    return this.item;
};
+
  };
// item specific fields
+
  // item specific fields
/*
+
  /*
* Description: get "active" state; return boolean
+
  * Description: get "active" state; return boolean
*/
+
  */
this.isActive = function() {
+
  this.isActive = function() {
return this.item.find("span.histHeaderActive a").hasClass("histActiveOn");
+
    return this.item.find("span.histHeaderActive a").hasClass("histActiveOn");
};
+
  };
/*
+
  /*
* Description: set the active history flag
+
  * Description: set the active history flag
* newIsActiveHistory: boolean
+
  * newIsActiveHistory: boolean
*/
+
  */
this.setCurrBlockActive = function(newIsActiveHistory) { // do not use imglinkBuilder replaceWith kills layout
+
  this.setCurrBlockActive = function(newIsActiveHistory) { // do not use imglinkBuilder replaceWith kills layout
var histHeaderActiveCell = this.item.find("span.histHeaderActive a");
+
    var histHeaderActiveCell = this.item.find("span.histHeaderActive a");
// change class & image
+
    // change class & image
histHeaderActiveCell.attr({"class": (newIsActiveHistory ? "histActiveOn" : "histActiveOff")});
+
    histHeaderActiveCell.attr({"class": (newIsActiveHistory ? "histActiveOn" : "histActiveOff")});
histHeaderActiveCell.find("img").attr("src", $j.resource(newIsActiveHistory ? "historyActiveOn" : "historyActiveOff"));
+
    histHeaderActiveCell.find("img").attr("src", $j.resource(newIsActiveHistory ? "historyActiveOn" : "historyActiveOff"));
};
+
  };
 
}
 
}
 
/*
 
/*
Line 503: Line 503:
 
  */
 
  */
 
function HistoryConfirmSubheading() {
 
function HistoryConfirmSubheading() {
/* Description: create new subheading with newContent string */
+
  /* Description: create new subheading with newContent string */
this.create = function(newContent) {
+
  this.create = function(newContent) {
this.item = $j("<tr class='histConfirmSubhdg'/>")
+
    this.item = $j("<tr class='histConfirmSubhdg'/>")
.append($j("<td colspan='3' class='histConfirmSubhdgContent'/>").html(newContent));
+
      .append($j("<td colspan='3' class='histConfirmSubhdgContent'/>").html(newContent));
return this.item;
+
    return this.item;
};
+
  };
 
}
 
}
 
/*
 
/*
Line 514: Line 514:
 
  */
 
  */
 
function HistorySubkeyHeading() {
 
function HistorySubkeyHeading() {
/* Description: create new subheading with newContent string */
+
  /* Description: create new subheading with newContent string */
this.create = function(newContent) {
+
  this.create = function(newContent) {
this.item = $j("<tr class='histSubkeyHdg'/>")
+
    this.item = $j("<tr class='histSubkeyHdg'/>")
.append($j("<td colspan='3' class='histSubkeyHdgContent'/>").html(newContent));
+
    .append($j("<td colspan='3' class='histSubkeyHdgContent'/>").html(newContent));
return this.item;
+
    return this.item;
};
+
  };
 
}
 
}
 
/*
 
/*
Line 525: Line 525:
 
  */
 
  */
 
function HistoryResult() {
 
function HistoryResult() {
/* Description: create new result item with newContent string */
+
  /* Description: create new result item with newContent string */
this.create = function(newContent) {
+
  this.create = function(newContent) {
this.item = $j("<tr class='histResult'/>")
+
    this.item = $j("<tr class='histResult'/>")
.append($j("<td class='histResultSymbol'/>").text("►"))
+
    .append($j("<td class='histResultSymbol'/>").text("►"))
.append($j("<td colspan='2' class='histResultContent'/>").html(newContent));
+
    .append($j("<td colspan='2' class='histResultContent'/>").html(newContent));
return this.item;
+
    return this.item;
};
+
  };
 
}
 
}
  
Line 538: Line 538:
 
  */
 
  */
 
function HistoryNested() {
 
function HistoryNested() {
/*
+
  /*
* newContent: blocks with alternative paths  
+
  * newContent: blocks with alternative paths  
*/
+
  */
this.create = function(newContent) {
+
  this.create = function(newContent) {
// create base step
+
    // create base step
this.item = $j('<tr class="histNested"/>')
+
    this.item = $j('<tr class="histNested"/>')
.append($j('<td class="histNestedEmpty"/>'))
+
    .append($j('<td class="histNestedEmpty"/>'))
.append($j('<td class="histNestedContent" colspan="2"/>').html(newContent));
+
    .append($j('<td class="histNestedContent" colspan="2"/>').html(newContent));
return this.item;
+
    return this.item;
};
+
  };
 
}
 
}
 
/*
 
/*
Line 553: Line 553:
 
  */
 
  */
 
function HistoryStep() {
 
function HistoryStep() {
/*
+
  /*
* Description: creates new step
+
  * Description: creates new step
* newStepNumber: step number
+
  * newStepNumber: step number
* newContent: description
+
  * newContent: description
* isUncertain: flag whether decision was uncertain
+
  * isUncertain: flag whether decision was uncertain
* confCoupletID: id of confirm-couplet
+
  * confCoupletID: id of confirm-couplet
* revCoupletID: id of revise-couplet
+
  * revCoupletID: id of revise-couplet
*/
+
  */
this.create = function(newStepNumber, newContent, isUncertain, confCoupletID, revCoupletID) {
+
  this.create = function(newStepNumber, newContent, isUncertain, confCoupletID, revCoupletID) {
// create base step
+
    // create base step
this.item = $j('<tr class ="histStep"/>')
+
    this.item = $j('<tr class ="histStep"/>')
.append($j('<td class="histStepNumber"/>').text(newStepNumber + "."))
+
    .append($j('<td class="histStepNumber"/>').text(newStepNumber + "."))
.append($j('<td class="histStepContent"/>'))
+
    .append($j('<td class="histStepContent"/>'))
// Create revise-history action (changable to confirm).  
+
    // Create revise-history action (changable to confirm).  
.append($j('<td class="histStepActions"/>')
+
    .append($j('<td class="histStepActions"/>')
// (side-requirement: href must be confCoupletID:)
+
      // (side-requirement: href must be confCoupletID:)
.append($j.linkBuilder("historyRevise", "", "#" + confCoupletID, " class='histStepActionRevise' onclick='return jkeyHistoryAction(this, \"" + revCoupletID + "\", \"" + confCoupletID + "\");'"))
+
      .append($j.linkBuilder("historyRevise", "", "#" + confCoupletID, " class='histStepActionRevise' onclick='return jkeyHistoryAction(this, \"" + revCoupletID + "\", \"" + confCoupletID + "\");'"))
);
+
    );
this.setContent(newContent);
+
    this.setContent(newContent);
this.setUncertainty(isUncertain);
+
    this.setUncertainty(isUncertain);
return this.item;
+
    return this.item;
};
+
  };
/*
+
  /*
* Description: set decision certainty of history step
+
  * Description: set decision certainty of history step
* isUncertain: true -> display marker text
+
  * isUncertain: true -> display marker text
*/
+
  */
this.setUncertainty = function(isUncertain) {
+
  this.setUncertainty = function(isUncertain) {
this.item.find("td.histStepContent span.histStepCertainty")
+
    this.item.find("td.histStepContent span.histStepCertainty")
.html(isUncertain ? ("&nbsp;" + $j.resource("historyUncertainFlag") + "&nbsp;") : "");
+
    .html(isUncertain ? ("&nbsp;" + $j.resource("historyUncertainFlag") + "&nbsp;") : "");
};
+
  };
/*
+
  /*
* Description: return previously recorded uncertainty for a history step (boolean)
+
  * Description: return previously recorded uncertainty for a history step (boolean)
*/
+
  */
this.getUncertainty = function() {
+
  this.getUncertainty = function() {
return this.item.find("td.histStepContent span.histStepCertainty").text().length > 0;
+
    return this.item.find("td.histStepContent span.histStepCertainty").text().length > 0;
};
+
  };
/*
+
  /*
* Description: set the step content (override inheritance)
+
  * Description: set the step content (override inheritance)
* newContent: the new html value
+
  * newContent: the new html value
*/
+
  */
this.setContent = function(newContent) {
+
  this.setContent = function(newContent) {
this.item.find("td.histStepContent")
+
    this.item.find("td.histStepContent")
.empty()
+
    .empty()
.append(newContent)
+
    .append(newContent)
.append($j('<span class="histStepCertainty"/>')
+
    .append($j('<span class="histStepCertainty"/>')    
);
+
    );
};
+
  };
/*
+
  /*
* Description: get number in front of step
+
  * Description: get number in front of step
*/
+
  */
this.getStepNumber = function() {
+
  this.getStepNumber = function() {
return parseInt(this.item.find("td.histStepNumber").text(),10); // parseInt example: " 8." will return 8
+
    return parseInt(this.item.find("td.histStepNumber").text(),10); // parseInt example: " 8." will return 8
};
+
  };
/*
+
  /*
* Description: set confirm/revise action
+
  * Description: set confirm/revise action
* setToConfirm: the new action state (true = confirm active, false = revise active)
+
  * setToConfirm: the new action state (true = confirm active, false = revise active)
*/
+
  */
this.setConfirmable = function(setToConfirm) {
+
  this.setConfirmable = function(setToConfirm) {
this.item.find("td.histStepActions a")
+
    this.item.find("td.histStepActions a")
.attr({"class": (setToConfirm ? "histStepActionConfirm" : "histStepActionRevise")})
+
    .attr({"class": (setToConfirm ? "histStepActionConfirm" : "histStepActionRevise")})
.text($j.resource((setToConfirm ? "historyConfirm" : "historyRevise")));
+
    .text($j.resource((setToConfirm ? "historyConfirm" : "historyRevise")));
};
+
  };
/*
+
  /*
* Description: check whether action is confirm or revise
+
  * Description: check whether action is confirm or revise
*/
+
  */
this.isConfirmable = function() {
+
  this.isConfirmable = function() {
return this.item.find("td.histStepActions a").is(".histStepActionConfirm");
+
    return this.item.find("td.histStepActions a").is(".histStepActionConfirm");
};
+
  };
 
}
 
}
  
Line 630: Line 630:
 
  */
 
  */
 
function PlayerHistory(jPlayerDiv) {
 
function PlayerHistory(jPlayerDiv) {
// local variables: item types = histHeader, histStep, histSubkeyHdg, histResult, histBlock;
+
  // local variables: item types = histHeader, histStep, histSubkeyHdg, histResult, histBlock;
// no more lazy loading for header (used immediately!), and historySubkeyHeading/historyResult (small)
+
  // no more lazy loading for header (used immediately!), and historySubkeyHeading/historyResult (small)
var jCurrHistBlock,
+
  var jCurrHistBlock,
historyHeader = new HistoryHeader(),
+
    historyHeader = new HistoryHeader(),
historyConfirmSubheading = new HistoryConfirmSubheading(),
+
    historyConfirmSubheading = new HistoryConfirmSubheading(),
historySubkeyHeading = new HistorySubkeyHeading(),
+
    historySubkeyHeading = new HistorySubkeyHeading(),
historyResult = new HistoryResult(),
+
    historyResult = new HistoryResult(),
historyNested = new HistoryNested(),
+
    historyNested = new HistoryNested(),
historyStep; // LAZY LOADING
+
    historyStep; // LAZY LOADING
/*
+
  /*
* Description: ensure that static class historyStep is loaded (lazy loading, deferring until used)
+
  * Description: ensure that static class historyStep is loaded (lazy loading, deferring until used)
*/
+
  */
var histStep_lazyLoad = function() {
+
  var histStep_lazyLoad = function() {
if (historyStep === undefined) { historyStep = new HistoryStep(); }
+
    if (historyStep === undefined) { historyStep = new HistoryStep(); }
};
+
  };
/*
+
  /*
* Description: create new history section in DOM tree, together with header row.
+
  * Description: create new history section in DOM tree, together with header row.
* active: true = set as active history
+
  * active: true = set as active history
* isFirstHistory: if set hides active-history-flag
+
  * isFirstHistory: if set hides active-history-flag
*/
+
  */
this.createBlock = function(active, isFirstHistory, newContent) {
+
  this.createBlock = function(active, isFirstHistory, newContent) {
return $j('<table cellpadding="0" cellspacing="0" class="'+(isFirstHistory ? "histTable" : "histBlock")+'"/>')
+
    return $j('<table cellpadding="0" cellspacing="0" class="'+(isFirstHistory ? "histTable" : "histBlock")+'"/>')
.append($j('<tr class="histLayout"/>').append($j("<td/><td/><td/>")))
+
      .append($j('<tr class="histLayout"/>').append($j("<td/><td/><td/>")))
.append(historyHeader.create(active, isFirstHistory, "<b>" + ((newContent) ? newContent : $j.resource("historyHeading")) + "</b>"));
+
      .append(historyHeader.create(active, isFirstHistory, "<b>" + ((newContent) ? newContent : $j.resource("historyHeading")) + "</b>"));
};
+
  };
/*
+
  /*
* Description: 3 methods to validate item: general, addable, specific type.
+
  * Description: 3 methods to validate item: general, addable, specific type.
* item: history table row
+
  * item: history table row
*/
+
  */
var isValidItem = function(item) { // maybe rename to isValidAddableItem ?!
+
  var isValidItem = function(item) { // maybe rename to isValidAddableItem ?!
return (typeof(item) == 'object') && (item.tagName) && (item.tagName.toLowerCase() == "tr");
+
    return (typeof(item) == 'object') && (item.tagName) && (item.tagName.toLowerCase() == "tr");
};
+
  };
var isAddableItem = function(item) {
+
  var isAddableItem = function(item) {
return (isValidItem(item) && !$j(item).hasClass("histHeader"));
+
    return (isValidItem(item) && !$j(item).hasClass("histHeader"));
};
+
  };
var isValidItemForType = function(item, type) {
+
  var isValidItemForType = function(item, type) {
return (isValidItem(item) && $j(item).hasClass(type));
+
    return (isValidItem(item) && $j(item).hasClass(type));
};
+
  };
/*
+
  /*
* Description: add an item to historyTable
+
  * Description: add an item to historyTable
*/
+
  */
this.addItem = function(item) {
+
  this.addItem = function(item) {
if (!isAddableItem(item)) {
+
    if (!isAddableItem(item)) {
throw new JKeyException("NotValid", {info:"item type=" + typeof(item)});
+
      throw new JKeyException("NotValid", {info:"item type=" + typeof(item)});
}
+
    }
jCurrHistBlock.append(item);
+
    jCurrHistBlock.append(item);
};
+
  };
/*
+
  /*
* Description: change active history
+
  * Description: change active history
*/
+
  */
this.changeActiveBlock = function(newActiveBlock) {
+
  this.changeActiveBlock = function(newActiveBlock) {
this.setCurrBlockActive(false); // deactivate current block
+
    this.setCurrBlockActive(false); // deactivate current block
this.setCurrBlock(newActiveBlock); // set new
+
    this.setCurrBlock(newActiveBlock); // set new
this.setCurrBlockActive(true); // activate new
+
    this.setCurrBlockActive(true); // activate new
};
+
  };
/*
+
  /*
* Description: get history block
+
  * Description: get history block
*/
+
  */
this.getCurrBlock = function() {
+
  this.getCurrBlock = function() {
return jCurrHistBlock;
+
    return jCurrHistBlock;
};
+
  };
/*
+
  /*
* Description: set history block
+
  * Description: set history block
*/
+
  */
this.setCurrBlock = function(newBlock) {
+
  this.setCurrBlock = function(newBlock) {
jCurrHistBlock = newBlock;
+
    jCurrHistBlock = newBlock;
};
+
  };
/*
+
  /*
* Description: first item step visible confirm link
+
  * Description: first item step visible confirm link
* historyTable: history ref
+
  * historyTable: history ref
* return: item object or null if not found
+
  * return: item object or null if not found
*/
+
  */
this.getfirstConfirmableStep = function() {
+
  this.getfirstConfirmableStep = function() {
var retValue = null,
+
    var retValue = null,
jAllSteps = jCurrHistBlock.find("tr:first").nextAll("tr.histStep");
+
    jAllSteps = jCurrHistBlock.find("tr:first").nextAll("tr.histStep");
if (jAllSteps.length) { // steps exist
+
    if (jAllSteps.length) { // steps exist
var parentThis = this;
+
      var parentThis = this;
jAllSteps.each(function() {
+
      jAllSteps.each(function() {
if ((retValue === null) && parentThis.withHistoryStep(this).isConfirmable()) {
+
        if ((retValue === null) && parentThis.withHistoryStep(this).isConfirmable()) {
retValue = this;
+
          retValue = this;
}
+
        }
});
+
      });
}
+
    }
return retValue;
+
    return retValue;
};
+
  };
/*
+
  /*
* Description: change all history action links in history table
+
  * Description: change all history action links in history table
* (revisable before, confirmable starting at firstConfirmableItem)
+
  * (revisable before, confirmable starting at firstConfirmableItem)
* historyTable: history ref
+
  * historyTable: history ref
* firstConfirmableItem: first confirmable item after updating
+
  * firstConfirmableItem: first confirmable item after updating
*/
+
  */
this.updateConfirmability = function(firstConfirmableItem) {
+
  this.updateConfirmability = function(firstConfirmableItem) {
var itemFound = false,
+
    var itemFound = false,
// get steps within block, not including nested steps
+
      // get steps within block, not including nested steps
jAllSteps = jCurrHistBlock.find("tr:first").nextAll("tr.histStep");
+
      jAllSteps = jCurrHistBlock.find("tr:first").nextAll("tr.histStep");
if (jAllSteps.length) { // entries exists
+
    if (jAllSteps.length) { // entries exists
var parentThis = this;
+
      var parentThis = this;
jAllSteps.each(function() {
+
      jAllSteps.each(function() {
if (this === firstConfirmableItem) {
+
        if (this === firstConfirmableItem) {
itemFound = true; // true once item was passed in loop
+
          itemFound = true; // true once item was passed in loop
// add confirm subheading in front of confirmable steps
+
          // add confirm subheading in front of confirmable steps
$j(this).before(parentThis.createHistoryConfirmSubheading("<i>" + $j.resource("historyConfirmable") + "</i>"));
+
          $j(this).before(parentThis.createHistoryConfirmSubheading("<i>" + $j.resource("historyConfirmable") + "</i>"));
}
+
        }
// update history actions
+
        // update history actions
parentThis.withHistoryStep(this).setConfirmable(itemFound);
+
        parentThis.withHistoryStep(this).setConfirmable(itemFound);
}); // TODO: parentThis and itemFound create CLOSUREs - change code?
+
      }); // TODO: parentThis and itemFound create CLOSUREs - change code?
}
+
    }
};
+
  };
/*
+
  /*
* Description: handle history path changes (cleanup of obsolete steps)
+
  * Description: handle history path changes (cleanup of obsolete steps)
*/
+
  */
this.cleanupAfter = function(jStep) {
+
  this.cleanupAfter = function(jStep) {
// rework history if decision path has changed. Remove confirm-sub-heading, item itself & following siblings
+
    // rework history if decision path has changed. Remove confirm-sub-heading, item itself & following siblings
jStep.prevAll("tr.histConfirmSubhdg").remove();
+
    jStep.prevAll("tr.histConfirmSubhdg").remove();
jStep.nextAll("tr").andSelf().remove();
+
    jStep.nextAll("tr").andSelf().remove();
};
+
  };
this.cleanupNestedBlocks = function() {
+
  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)
+
    // 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");
+
    var jStep = jCurrHistBlock.find("tr:first").nextAll("tr.histHeader, tr.histStep").filter(":last");
if (jStep.length) {
+
    if (jStep.length) {
jStep.nextAll("tr").remove();
+
      jStep.nextAll("tr").remove();
}
+
    }
};
+
  };
this.cleanupConfirmableSteps = function() {
+
  this.cleanupConfirmableSteps = function() {
var firstConfirmableStep = this.getfirstConfirmableStep();
+
    var firstConfirmableStep = this.getfirstConfirmableStep();
if (firstConfirmableStep) {
+
    if (firstConfirmableStep) {
this.cleanupAfter($j(firstConfirmableStep));
+
      this.cleanupAfter($j(firstConfirmableStep));
}
+
    }
};
+
  };
  
/*
+
  /*
* Description: create a history sub-heading, nested block, or result history table row
+
  * Description: create a history sub-heading, nested block, or result history table row
* newContent: content for item
+
  * newContent: content for item
*/
+
  */  
this.createHistoryConfirmSubheading = function(newContent) {
+
  this.createHistoryConfirmSubheading = function(newContent) {
return historyConfirmSubheading.create(newContent); // this.addItem( because will be added between block items
+
    return historyConfirmSubheading.create(newContent); // this.addItem( because will be added between block items
};
+
  };
this.createHistorySubkeyHeading = function(newContent) {
+
  this.createHistorySubkeyHeading = function(newContent) {
return historySubkeyHeading.create(newContent);
+
    return historySubkeyHeading.create(newContent);
};
+
  };
this.createHistoryNested = function(newContent) {
+
  this.createHistoryNested = function(newContent) {
this.addItem(historyNested.create(newContent).get(0));
+
    this.addItem(historyNested.create(newContent).get(0));
};
+
  };
this.createHistoryResult = function(newContent) {
+
  this.createHistoryResult = function(newContent) {
this.addItem(historyResult.create(newContent).get(0));
+
    this.addItem(historyResult.create(newContent).get(0));
};
+
  };
/*
+
  /*
* Description: creates a history step item
+
  * Description: creates a history step item
*/
+
  */
this.createHistoryStep = function(newContent, isUncertain, confCoupletID, revCoupletID) {
+
  this.createHistoryStep = function(newContent, isUncertain, confCoupletID, revCoupletID) {
histStep_lazyLoad();
+
    histStep_lazyLoad();
this.addItem(historyStep.create(this.getNewStepNumber(), newContent, isUncertain, confCoupletID, revCoupletID).get(0));
+
    this.addItem(historyStep.create(this.getNewStepNumber(), newContent, isUncertain, confCoupletID, revCoupletID).get(0));
};
+
  };
/*
+
  /*
* Description: return history step class initialized with item
+
  * Description: return history step class initialized with item
*/
+
  */
this.withHistoryStep = function(item) {
+
  this.withHistoryStep = function(item) {
histStep_lazyLoad();
+
    histStep_lazyLoad();
if (!isValidItemForType(item, "histStep")) {
+
    if (!isValidItemForType(item, "histStep")) {
throw new JKeyException("NotValid", {info:"item type=" + typeof(item)});
+
      throw new JKeyException("NotValid", {info:"item type=" + typeof(item)});
}
+
    }
historyStep.item = $j(item);
+
    historyStep.item = $j(item);
return historyStep;
+
    return historyStep;
};
+
  };
// item specific fields
+
  // item specific fields
/*
+
  /*
* Description: get "active" state; return boolean
+
  * Description: get "active" state; return boolean
*/
+
  */
this.isActive = function() {
+
  this.isActive = function() {
historyHeader.item = jCurrHistBlock.find("tr.histHeader:first");
+
    historyHeader.item = jCurrHistBlock.find("tr.histHeader:first");
return historyHeader.isActive();
+
    return historyHeader.isActive();
};
+
  };
/*
+
  /*
* Description: set the active history flag
+
  * Description: set the active history flag
* newIsActiveHistory: boolean
+
  * newIsActiveHistory: boolean
*/
+
  */
this.setCurrBlockActive = function(newIsActive) {
+
  this.setCurrBlockActive = function(newIsActive) {
historyHeader.item = jCurrHistBlock.find("tr.histHeader:first");
+
    historyHeader.item = jCurrHistBlock.find("tr.histHeader:first");
historyHeader.setCurrBlockActive(newIsActive);
+
    historyHeader.setCurrBlockActive(newIsActive);
};
+
  };
/*
+
  /*
* Description: get next available number for a history step in a history block (last + 1).
+
  * 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.
+
  * 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
+
  * historyBlock : a jquery-history-block, defaults to jCurrHistBlock if omitted
*/
+
  */
this.getNewStepNumber = function(historyBlock) {
+
  this.getNewStepNumber = function(historyBlock) {
if (!historyBlock) {
+
    if (!historyBlock) {
historyBlock = jCurrHistBlock;
+
      historyBlock = jCurrHistBlock;
}
+
    }
var stepNumber = 0,
+
    var stepNumber = 0,
jLastStep = historyBlock.find("tr:first").nextAll("tr.histStep:last");
+
      jLastStep = historyBlock.find("tr:first").nextAll("tr.histStep:last");
if (jLastStep.length) {
+
    if (jLastStep.length) {
stepNumber = this.withHistoryStep(jLastStep.get(0)).getStepNumber();
+
      stepNumber = this.withHistoryStep(jLastStep.get(0)).getStepNumber();
} else if (!historyBlock.is(".histTable")) { // unless outermost and not step (return 0): recurse to parent
+
    } else if (!historyBlock.is(".histTable")) { // unless outermost and not step (return 0): recurse to parent
historyBlock = this.getParentBlock();
+
      historyBlock = this.getParentBlock();    
return this.getNewStepNumber(historyBlock);
+
      return this.getNewStepNumber(historyBlock);
}
+
    }
return stepNumber + 1;
+
    return stepNumber + 1;
};
+
  };
/*
+
  /*
* Description: checks whether first step has to be confirmed
+
  * Description: checks whether first step has to be confirmed
*/
+
  */
this.isFirstStepInBlockConfirmableStep = function() {
+
  this.isFirstStepInBlockConfirmableStep = function() {
var firstStep = jCurrHistBlock.find("tr:first").nextAll("tr.histStep:first");
+
    var firstStep = jCurrHistBlock.find("tr:first").nextAll("tr.histStep:first");
return (firstStep.prev("tr").hasClass("histConfirmSubhdg"));
+
    return (firstStep.prev("tr").hasClass("histConfirmSubhdg"));
};
+
  };
/*
+
  /*
* Description: retrieve first nested block within current history block
+
  * Description: retrieve first nested block within current history block
*/
+
  */
this.firstNestedStep = function() {
+
  this.firstNestedStep = function() {
return jCurrHistBlock.find("tr:first").nextAll("tr.histNested:first");
+
    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)
+
  * Description: get parent block of current block (as $j object; if already outermost -> getParentBlock.length=0)
*/
+
  */
this.getParentBlock = function() {
+
  this.getParentBlock = function() {
// first closest("table.histBlock, table.histTable") will find own block, then up and find parent:
+
    // first closest("table.histBlock, table.histTable") will find own block, then up and find parent:
var jBlock = jCurrHistBlock.closest("table.histBlock, table.histTable");
+
    var jBlock = jCurrHistBlock.closest("table.histBlock, table.histTable");
if (!jBlock.is(".histTable")) { // unless already outermost
+
    if (!jBlock.is(".histTable")) { // unless already outermost
jBlock = jBlock.parent().closest("table.histBlock, table.histTable");
+
      jBlock = jBlock.parent().closest("table.histBlock, table.histTable");
}
+
    }
return jBlock;  
+
    return jBlock;  
};
+
  };
+
 
// Constructor logic - has to be at end of obj/class definition
+
  // 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
+
  jCurrHistBlock = this.createBlock(true, true); // first and (here in constructor so far) only one
jPlayerDiv.append($j('<div class="jkeyHistory dt-box"/>').hide()
+
  jPlayerDiv.append($j('<div class="jkeyHistory dt-box"/>').hide()
.append(jCurrHistBlock)
+
    .append(jCurrHistBlock)
);
+
  );
 
}
 
}
  
Line 871: Line 871:
 
  */
 
  */
 
function jkeyIsValidKeyRef(jRefTarget) {
 
function jkeyIsValidKeyRef(jRefTarget) {
return ( jRefTarget.is("td.dt-nodeid") || jRefTarget.is("tr.dt-row") || jRefTarget.is("div.decisiontree") );
+
  return ( jRefTarget.is("td.dt-nodeid") || jRefTarget.is("tr.dt-row") || jRefTarget.is("div.decisiontree") );
 
}
 
}
  
Line 881: Line 881:
 
  */
 
  */
 
function jkeyTransformCouplet(jPlayerDiv, jDecisionRow, jPlayerCouplet) {
 
function jkeyTransformCouplet(jPlayerDiv, jDecisionRow, jPlayerCouplet) {
if (jDecisionRow.length === 0) {
+
  if (jDecisionRow.length === 0) {
throw new JKeyException("NotFound", {info:"Transform w/o valid jDecisionRow"});
+
    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"
+
  // 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 currCoupletID = jDecisionRow.children("td[id]:first").attr("id");
var eachLeadout = function() { // this = a leadout link
+
  var eachLeadout = function() { // this = a leadout link
var nextCoupletID = this.hash,
+
    var nextCoupletID = this.hash,
isInternalLink = ((nextCoupletID.length > 0) && (this.href.search("/"+wgPageName+"#") != -1));
+
      isInternalLink = ((nextCoupletID.length > 0) && (this.href.search("/"+wgPageName+"#") != -1));
// Example for test above: wgPageName="ThisPage", this.href "http://.../AlsoThisPageTwo#xxx" -> include must / and #
+
    // Example for test above: wgPageName="ThisPage", this.href "http://.../AlsoThisPageTwo#xxx" -> include must / and #
if (isInternalLink) {
+
    if (isInternalLink) {
var jNextCouplet = $j(nextCoupletID);
+
      var jNextCouplet = $j(nextCoupletID);
// Check if valid element within a player was found
+
      // Check if valid element within a player was found
isInternalLink = jkeyIsValidKeyRef(jNextCouplet);
+
      isInternalLink = jkeyIsValidKeyRef(jNextCouplet);
if (isInternalLink) {
+
      if (isInternalLink) {
if (jNextCouplet.is("td.dt-nodeid")) { // already is the target node-id
+
        if (jNextCouplet.is("td.dt-nodeid")) { // already is the target node-id
nextCoupletID = jNextCouplet.attr("id");
+
          nextCoupletID = jNextCouplet.attr("id");
} else { // might be row or div; get key div (closest finds itself), then table, then dt-nodeid
+
        } 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!
+
          // jKeyTable is not loop-invariable; a wiki page may have multiple keys!
var jKeyTable = jNextCouplet.closest("div.decisiontree").find("table.dt-body:last"),
+
          var jKeyTable = jNextCouplet.closest("div.decisiontree").find("table.dt-body:last"),
jNodeID = jKeyTable.find("td.dt-nodeid:first");
+
            jNodeID = jKeyTable.find("td.dt-nodeid:first");
isInternalLink = (jNodeID.length>0);
+
          isInternalLink = (jNodeID.length>0);
if (isInternalLink) {
+
          if (isInternalLink) {
nextCoupletID = jNodeID.attr("id");
+
            nextCoupletID = jNodeID.attr("id");
} // else no leads found in div
+
          } // else no leads found in div
}
+
        }
} // else: local id NOT found or NOT valid for player
+
      } // else: local id NOT found or NOT valid for player
} // END if isInternalLink
+
    } // END if isInternalLink
// Following is NOT an else, isInternalLink may have been changed.
+
    // Following is NOT an else, isInternalLink may have been changed.
nextCoupletID = (!isInternalLink) ? "" : nextCoupletID;
+
    nextCoupletID = (!isInternalLink) ? "" : nextCoupletID;
// prepare resultlink (page or internal subkey) for player
+
    // prepare resultlink (page or internal subkey) for player
$j(this)
+
    $j(this)
.attr("target", (isInternalLink ? "_self" : "_blank"))
+
      .attr("target", (isInternalLink ? "_self" : "_blank"))
.addClass("linkbtn").removeAttr("style")
+
      .addClass("linkbtn").removeAttr("style")
.click(function() {return jkeyDecision(this, false);});
+
      .click(function() {return jkeyDecision(this, false);});
// save in data
+
    // save in data
$j.data(this, "coupletID", { curr:currCoupletID, next:nextCoupletID });
+
    $j.data(this, "coupletID", { curr:currCoupletID, next:nextCoupletID });
};
+
  };
var eachLeadon = function() { // this = a leadon link
+
  var eachLeadon = function() { // this = a leadon link
var jThis = $j(this);
+
    var jThis = $j(this);
if (jThis.parent().hasClass("leadon")) { // for leadon (but not leadontext) overwrite display text
+
    if (jThis.parent().hasClass("leadon")) { // for leadon (but not leadontext) overwrite display text
jThis.html($j.resource("coupletContinue"));
+
      jThis.html($j.resource("coupletContinue"));
}
+
    }
jThis.addClass("linkbtn").removeAttr("style");
+
    jThis.addClass("linkbtn").removeAttr("style");
// General problem: changing link onclick attribute works in FF, but is ignored by IE (known bug)
+
    // 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.
+
    // 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().
+
    // Also working is use of jquery.click(), but watch referencing "this". Here "this" is correct because each().
jThis.click(function() {return jkeyDecision(this, false);});
+
    jThis.click(function() {return jkeyDecision(this, false);});
// save in data
+
    // save in data
$j.data(this, "coupletID", { curr:currCoupletID, next:this.hash.substring(1, this.hash.length) });
+
    $j.data(this, "coupletID", { curr:currCoupletID, next:this.hash.substring(1, this.hash.length) });
};
+
  };
var prefixID = function() {return "jK" + this.id;};
+
  var prefixID = function() {return "jK" + this.id;};
// Process all leads in couplet
+
  // 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*").
+
  // 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.
+
  // 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"
+
  // 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
+
  // 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!
+
  // * 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() {
+
  jDecisionRow.parent().children("tr.dt-row[id^="+currCoupletID+"_]").each(function() {
var jClonedRow = $j(this).clone(true);
+
    var jClonedRow = $j(this).clone(true);
// prefix id of decision row itself and all descendants
+
    // prefix id of decision row itself and all descendants
jClonedRow.find("[id]").andSelf().attr("id", prefixID);
+
    jClonedRow.find("[id]").andSelf().attr("id", prefixID);
// replace lead identifier with arrow (normal) or blank (horizontal side-by-side leads)
+
    // replace lead identifier with arrow (normal) or blank (horizontal side-by-side leads)
var jNodeCell = jClonedRow.find("td.dt-nodeid:first");
+
    var jNodeCell = jClonedRow.find("td.dt-nodeid:first");
// CHECK why coupletID must be added here in addition to data stored generally for each link
+
    // 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
+
    // 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
+
    $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) ? "►" : " ");
+
    jNodeCell.html((jClonedRow.get(0).className.search(/dt-row-hor\w+/) == -1) ? "►" : " ");
jClonedRow.find("td.leadalt").empty();
+
    jClonedRow.find("td.leadalt").empty();
// Transform all relevant leadout (= result pages) and leadon/leadontext (next couplet) links
+
    // Transform all relevant leadout (= result pages) and leadon/leadontext (next couplet) links
// (depending on row mode, multiple links may exist)
+
    // (depending on row mode, multiple links may exist)
jClonedRow.find("span.leadout a").each(eachLeadout);
+
    jClonedRow.find("span.leadout a").each(eachLeadout);
jClonedRow.find("span.leadon a, div.leadontext a, span.leadontext a").each(eachLeadon);
+
    jClonedRow.find("span.leadon a, div.leadontext a, span.leadontext a").each(eachLeadon);
jPlayerCouplet.append(jClonedRow.show());
+
    jPlayerCouplet.append(jClonedRow.show());
}); // END each lead row of couplet
+
  }); // END each lead row of couplet
 
}
 
}
  
Line 964: Line 964:
 
  */
 
  */
 
function jkeyLoadCouplet(jPlayerDiv, coupletID, isUncertain) {
 
function jkeyLoadCouplet(jPlayerDiv, coupletID, isUncertain) {
var jPlayerCouplet = jPlayerDiv.find("table.dt-body");
+
  var jPlayerCouplet = jPlayerDiv.find("table.dt-body");
jPlayerCouplet.empty(); // flush
+
  jPlayerCouplet.empty(); // flush
// coupletID could be 'a.34', jquery needs 'a\.34'
+
  // coupletID could be 'a.34', jquery needs 'a\.34'
// $j("#"... -> document scope, coupletID may be in other key on same page
+
  // $j("#"... -> document scope, coupletID may be in other key on same page
jkeyTransformCouplet(jPlayerDiv, $j("#" + coupletID.replace(/\./g,"\\.")).closest("tr"), jPlayerCouplet);
+
  jkeyTransformCouplet(jPlayerDiv, $j("#" + coupletID.replace(/\./g,"\\.")).closest("tr"), jPlayerCouplet);
// after history actions: results div may have to be hidden, and certainty div re-displayed
+
  // after history actions: results div may have to be hidden, and certainty div re-displayed
jPlayerDiv.find("div.jkeyResultMsg").hide();
+
  jPlayerDiv.find("div.jkeyResultMsg").hide();
jPlayerDiv.find("div.certaintyDiv").show()
+
  jPlayerDiv.find("div.certaintyDiv").show()
.find("input#decisionUncertain").get(0).checked = isUncertain; // setting checkbox
+
    .find("input#decisionUncertain").get(0).checked = isUncertain; // setting checkbox
 
}
 
}
 
/*
 
/*
Line 980: Line 980:
 
  */
 
  */
 
function jkeyLoadResult(jPlayerDiv, jResult) {
 
function jkeyLoadResult(jPlayerDiv, jResult) {
jPlayerDiv.find("div.jkeyResultMsg").empty().html($j.resource("mainResultMsg")).append(jResult.clone());  
+
  jPlayerDiv.find("div.jkeyResultMsg").empty().html($j.resource("mainResultMsg")).append(jResult.clone());  
jkeySetMode("finished", jPlayerDiv);
+
  jkeySetMode("finished", jPlayerDiv);
 
}
 
}
  
Line 987: Line 987:
 
  * Description: toggle visibility of player controls (top right player area)
 
  * Description: toggle visibility of player controls (top right player area)
 
  * mode: string for current control mode;
 
  * mode: string for current control mode;
  * values are "resumed", "overview", "newstart", "finished"
+
  * values are "resumed", "overview", "newstart", "finished"
 
  * elementInKeyDiv: any element inside div.decisiontree or div.decisiontree itself, Either as DOM ref OR as "#id" string
 
  * 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).
 
  * (.closest() works inclusive!), usually a caller of a control (link, button).
Line 993: Line 993:
 
  */
 
  */
 
function jkeySetMode(mode, elementInKeyDiv) {
 
function jkeySetMode(mode, elementInKeyDiv) {
try {
+
  try {
// Cancel if called with undefined element; may occur for "newstart"
+
    // Cancel if called with undefined element; may occur for "newstart"
// when called based on undefined id from location.hash
+
    // when called based on undefined id from location.hash
if (elementInKeyDiv === undefined) { return; }
+
    if (elementInKeyDiv === undefined) { return; }
var jKeyDiv = $j(elementInKeyDiv).closest("div.decisiontree"),
+
    var jKeyDiv = $j(elementInKeyDiv).closest("div.decisiontree"),
jKeyTable = jKeyDiv.find("table.dt-body:last"),
+
      jKeyTable = jKeyDiv.find("table.dt-body:last"),
jPlayerDiv = jKeyDiv.find("div.jkeyPlayer"),
+
      jPlayerDiv = jKeyDiv.find("div.jkeyPlayer"),
jPlayerCouplet = jPlayerDiv.find("table.dt-body"),
+
      jPlayerCouplet = jPlayerDiv.find("table.dt-body"),
jResultDiv = jPlayerDiv.find("div.jkeyResultMsg"),
+
      jResultDiv = jPlayerDiv.find("div.jkeyResultMsg"),
jControls = jKeyDiv.find("td.jkeyControls");
+
      jControls = jKeyDiv.find("td.jkeyControls");
if (mode == "firststart") { // change start permanently to restart icon/text, add overview/resume controls
+
    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;" +
+
      jControls.find("span.jkeyPlayerStart1st").replaceWith("<span class='jkeyPlayerOverview nowrap'> &nbsp; &nbsp;" +
$j.imglinkBuilder("iconOverview", "playerOverview", "onclick='return jkeySetMode(\"overview\",this);'") +
+
        $j.imglinkBuilder("iconOverview", "playerOverview", "onclick='return jkeySetMode(\"overview\",this);'") +
"</span> <span class='jkeyPlayerResume nowrap'> &nbsp; &nbsp;" +
+
        "</span> <span class='jkeyPlayerResume nowrap'> &nbsp; &nbsp;" +
$j.imglinkBuilder("iconResume", "playerResume", "onclick='return jkeySetMode(\"resumed\",this);'") +
+
        $j.imglinkBuilder("iconResume", "playerResume", "onclick='return jkeySetMode(\"resumed\",this);'") +
"</span> <span class='jkeyPlayerStartNew nowrap'> &nbsp; &nbsp;" +
+
        "</span> <span class='jkeyPlayerStartNew nowrap'> &nbsp; &nbsp;" +
$j.imglinkBuilder("iconStartNew", "playerStartNew", "onclick='return jkeySetMode(\"newstart\",this);'") +
+
        $j.imglinkBuilder("iconStartNew", "playerStartNew", "onclick='return jkeySetMode(\"newstart\",this);'") +
"</span>");
+
        "</span>");
mode = "newstart";
+
      mode = "newstart";
}
+
    }
var finished = (mode=="finished"),
+
    var finished = (mode=="finished"),
overview = !(mode=="resumed" || mode=="newstart" || finished);
+
      overview = !(mode=="resumed" || mode=="newstart" || finished);  
jControls.find("span.jkeyPlayerOverview").toggle(!overview);
+
    jControls.find("span.jkeyPlayerOverview").toggle(!overview);
jControls.find("span.jkeyPlayerResume").toggle(overview);
+
    jControls.find("span.jkeyPlayerResume").toggle(overview);
jKeyDiv.find("div.dt-header").toggle(overview); // metadata box (description, audience, etc.)
+
    jKeyDiv.find("div.dt-header").toggle(overview); // metadata box (description, audience, etc.)
// Show finished message only in finished mode (+ set below for resumed)
+
    // Show finished message only in finished mode (+ set below for resumed)
jResultDiv.toggle(finished);
+
    jResultDiv.toggle(finished);
switch(mode) {
+
    switch(mode) {
case("overview"): // STOP player, hide player, show original overview player
+
    case("overview"): // STOP player, hide player, show original overview player
jKeyDiv.find("div.jkeyLCtrls input.editbtn").show();
+
      jKeyDiv.find("div.jkeyLCtrls input.editbtn").show();
jKeyDiv.removeClass("jkeyCanvas");
+
      jKeyDiv.removeClass("jkeyCanvas");
jKeyTable.show();
+
      jKeyTable.show();
jPlayerDiv.hide();
+
      jPlayerDiv.hide();
break;
+
      break;
case("newstart"):
+
    case("newstart"):
// Delete player-div in case of restart (also invalidates jPlayerCouplet)
+
      // Delete player-div in case of restart (also invalidates jPlayerCouplet)
jPlayerDiv.remove();
+
      jPlayerDiv.remove();
jKeyTable.hide();
+
      jKeyTable.hide();
// create new player & table, transform couplet
+
      // create new player & table, transform couplet
jPlayerCouplet = $j('<table class="dt-body" cellspacing="0" cellpadding="0"/>');
+
      jPlayerCouplet = $j('<table class="dt-body" cellspacing="0" cellpadding="0"/>');
jPlayerDiv = $j('<div class="jkeyPlayer"/>')
+
      jPlayerDiv = $j('<div class="jkeyPlayer"/>')
.append(jPlayerCouplet)
+
        .append(jPlayerCouplet)
// Append a hidden result section (for finished mode)
+
        // Append a hidden result section (for finished mode)
.append($j('<div class="jkeyResultMsg"/>').hide());
+
        .append($j('<div class="jkeyResultMsg"/>').hide());
if (!jKeyDiv.hasClass("jkey-simplified")) {
+
      if (!jKeyDiv.hasClass("jkey-simplified")) {
jPlayerDiv.append($j('<div class="certaintyDiv noprint"/>')
+
        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>'));
+
          .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
+
      // generate unique player id    
var uniquePlayerID;
+
      var uniquePlayerID;
do {
+
      do {
uniquePlayerID = "jkp_"+$j.random(1,9999999);
+
        uniquePlayerID = "jkp_"+$j.random(1,9999999);
} while (jkeyHistory.uniquePlayerID); // until not yet used
+
      } while (jkeyHistory.uniquePlayerID); // until not yet used
// add id to connect with history
+
      // add id to connect with history
jPlayerDiv.attr("id", uniquePlayerID);
+
      jPlayerDiv.attr("id", uniquePlayerID);
// initalize player history class (lazy loading)
+
      // initalize player history class (lazy loading)
jkeyHistory[uniquePlayerID] = new PlayerHistory(jPlayerDiv);
+
      jkeyHistory[uniquePlayerID] = new PlayerHistory(jPlayerDiv);
// Initialize player with first decision row (table may start with spacer row)
+
      // Initialize player with first decision row (table may start with spacer row)
jKeyTable.before(jPlayerDiv); // add to DOM
+
      jKeyTable.before(jPlayerDiv); // add to DOM
jkeyTransformCouplet(jKeyDiv, jKeyTable.find("tr.dt-row:first"), jPlayerCouplet);
+
      jkeyTransformCouplet(jKeyDiv, jKeyTable.find("tr.dt-row:first"), jPlayerCouplet);
// NOTE: NO BREAK here, fallthrough to "resumed"
+
      // NOTE: NO BREAK here, fallthrough to "resumed"
case("resumed"): // = player resumed. This may have to show couplet or finished mode
+
    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
+
      // style change for key div + hide original table & show table in player
jKeyDiv.find("div.jkeyLCtrls input.editbtn").hide();
+
      jKeyDiv.find("div.jkeyLCtrls input.editbtn").hide();
jKeyDiv.addClass("jkeyCanvas");
+
      jKeyDiv.addClass("jkeyCanvas");
jKeyTable.hide();
+
      jKeyTable.hide();
jPlayerCouplet.show();
+
      jPlayerCouplet.show();
jPlayerDiv.show();
+
      jPlayerDiv.show();
// redisplay result div if still filled
+
      // redisplay result div if still filled
jResultDiv.toggle(jResultDiv.text().length>0);
+
      jResultDiv.toggle(jResultDiv.text().length>0);
break;
+
      break;
case("finished"): // empty current couplet, hide certainty checkbox
+
    case("finished"): // empty current couplet, hide certainty checkbox
jPlayerCouplet.empty();
+
      jPlayerCouplet.empty();
jPlayerDiv.find("div.certaintyDiv").hide();
+
      jPlayerDiv.find("div.certaintyDiv").hide();
break;
+
      break;
} // end case
+
    } // end case
return false; // cancel default event
+
    return false; // cancel default event
} catch (err) {
+
  } catch (err) {
jkeyExceptionAlert(err);
+
    jkeyExceptionAlert(err);
}
+
  }
 
}
 
}
  
Line 1,081: Line 1,081:
 
  */  
 
  */  
 
function jkeyHistoryAction(caller, revCoupletID, confCoupletID) {
 
function jkeyHistoryAction(caller, revCoupletID, confCoupletID) {
try {
+
  try {
// get currently active history (find first & look in hierarchy)
+
    // get currently active history (find first & look in hierarchy)
var jCaller = $j(caller),
+
    var jCaller = $j(caller),
jStepItem = jCaller.closest("tr"), // history row around action link
+
      jStepItem = jCaller.closest("tr"), // history row around action link
jPlayerDiv = jStepItem.closest("div.jkeyPlayer"),
+
      jPlayerDiv = jStepItem.closest("div.jkeyPlayer"),
jCurrHistory = jkeyHistory[jPlayerDiv.attr("id")];
+
      jCurrHistory = jkeyHistory[jPlayerDiv.attr("id")];
// set history block to the one containing the caller (or outermost histTable itself)
+
    // set history block to the one containing the caller (or outermost histTable itself)
jCurrHistory.changeActiveBlock(jCaller.closest("table.histBlock, table.histTable"));
+
    jCurrHistory.changeActiveBlock(jCaller.closest("table.histBlock, table.histTable"));
// Handle possible history change
+
    // Handle possible history change
if (jCaller.hasClass("histStepActionConfirm")) { // confirm was clicked
+
    if (jCaller.hasClass("histStepActionConfirm")) { // confirm was clicked
revCoupletID = confCoupletID; // revCoupletID = next couplet to load
+
      revCoupletID = confCoupletID; // revCoupletID = next couplet to load
// Advance to next history step
+
      // Advance to next history step
jStepItem = jStepItem.nextAll("tr.histStep:first");
+
      jStepItem = jStepItem.nextAll("tr.histStep:first");
// Update history step certainty from player checkbox
+
      // Update history step certainty from player checkbox
var firstConfirmableStep = jCurrHistory.getfirstConfirmableStep();
+
      var firstConfirmableStep = jCurrHistory.getfirstConfirmableStep();
if (firstConfirmableStep) { // null if none found
+
      if (firstConfirmableStep) { // null if none found
// firstConfirmableStep is the couplet shown in player!
+
        // firstConfirmableStep is the couplet shown in player!
jCurrHistory.withHistoryStep(firstConfirmableStep)
+
        jCurrHistory.withHistoryStep(firstConfirmableStep)
.setUncertainty(jPlayerDiv.find("div.certaintyDiv input#decisionUncertain").is(":checked"));
+
          .setUncertainty(jPlayerDiv.find("div.certaintyDiv input#decisionUncertain").is(":checked"));
}
+
      }
}
+
    }
// Remove confirm "confirmable-decisions" sub-heading; will be recreated in updateConfirmability if necessary
+
    // Remove confirm "confirmable-decisions" sub-heading; will be recreated in updateConfirmability if necessary
jCurrHistory.getCurrBlock().find("tr.histConfirmSubhdg").remove();
+
    jCurrHistory.getCurrBlock().find("tr.histConfirmSubhdg").remove();
if (revCoupletID === "") { // RESULT
+
    if (revCoupletID === "") { // RESULT
// try-all may result in multiple alternative results. Thus confirming a result needs to refresh rather than re-display
+
      // 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"));
+
      jkeyLoadResult(jPlayerDiv, jCaller.closest("tr.histStep").next("tr.histResult").find("span.leadout"));
} else { // Refresh player with couplet corresponding to jStep
+
    } else { // Refresh player with couplet corresponding to jStep
var nextDecisionIsUncertain = false; // default
+
      var nextDecisionIsUncertain = false; // default
if (jStepItem.length) { // extract history step certainty
+
      if (jStepItem.length) { // extract history step certainty
nextDecisionIsUncertain = jCurrHistory.withHistoryStep(jStepItem.get(0)).getUncertainty();
+
        nextDecisionIsUncertain = jCurrHistory.withHistoryStep(jStepItem.get(0)).getUncertainty();
}
+
      }
jkeyLoadCouplet(jPlayerDiv, revCoupletID, nextDecisionIsUncertain);
+
      jkeyLoadCouplet(jPlayerDiv, revCoupletID, nextDecisionIsUncertain);
}
+
    }
jCurrHistory.updateConfirmability(jStepItem.get(0));
+
    jCurrHistory.updateConfirmability(jStepItem.get(0));
} catch (err) {
+
  } catch (err) {
jkeyExceptionAlert(err);
+
    jkeyExceptionAlert(err);
}
+
  }
return false; // cancel default event
+
  return false; // cancel default event
 
}
 
}
  
Line 1,130: Line 1,130:
 
  */
 
  */
 
function jkeySimplifiedLeadtxt(leadLinkCaller) {
 
function jkeySimplifiedLeadtxt(leadLinkCaller) {
var jContainer = $j(leadLinkCaller).closest("td.leadresult, th.leadtxt"),
+
  var jContainer = $j(leadLinkCaller).closest("td.leadresult, th.leadtxt"),
jSpan;
+
    jSpan;
if (jContainer.hasClass("leadtxt")) {
+
  if (jContainer.hasClass("leadtxt")) {
// occurs if leadspan itself is formatted as link (having both leadspan and leadontext class)
+
    // occurs if leadspan itself is formatted as link (having both leadspan and leadontext class)
jSpan = jContainer.find("span.leadspan");
+
    jSpan = jContainer.find("span.leadspan");
} else if (jContainer.hasClass("leadresult")) {
+
  } else if (jContainer.hasClass("leadresult")) {
// pos of span.leadspan depends on arrangement (horizontal or not)
+
    // 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");
+
    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) {
+
  if (jSpan.length === 0) {
throw new JKeyException("NotFound", {info:"No lead statement!"});
+
    throw new JKeyException("NotFound", {info:"No lead statement!"});
}
+
  }
// Clone span; remove links, breaks, imgs
+
  // Clone span; remove links, breaks, imgs
var jSimplifiedLeadTxt = jSpan.clone(true);
+
  var jSimplifiedLeadTxt = jSpan.clone(true);
jSimplifiedLeadTxt.find("a").each( function() { $j(this).replaceWith($j(this).html()); } );
+
  jSimplifiedLeadTxt.find("a").each( function() { $j(this).replaceWith($j(this).html()); } );
jSimplifiedLeadTxt.find("br, img").remove();
+
  jSimplifiedLeadTxt.find("br, img").remove();
return jSimplifiedLeadTxt;
+
  return jSimplifiedLeadTxt;
 
}
 
}
  
 
/*
 
/*
 
  * Description: Used in onclick events for both next couplet and final result, i.e. user made a decision.
 
  * 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)
+
  * 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))
 
  * 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
 
  * quiet: do not output couplets or results, only add to history
 
  */
 
  */
 
function jkeyDecision(caller, quiet) {  
 
function jkeyDecision(caller, quiet) {  
try {
+
  try {
var jCaller = $j(caller),
+
    var jCaller = $j(caller),
jContainer = jCaller.closest("td.leadresult, th.leadtxt"),
+
      jContainer = jCaller.closest("td.leadresult, th.leadtxt"),
jPlayerDiv = jContainer.closest("div.jkeyPlayer"),
+
      jPlayerDiv = jContainer.closest("div.jkeyPlayer"),
jSimplifiedLeadTxt = jkeySimplifiedLeadtxt(caller), // single time this is called
+
      jSimplifiedLeadTxt = jkeySimplifiedLeadtxt(caller), // single time this is called
jCurrHistory = jkeyHistory[jPlayerDiv.attr("id")],
+
      jCurrHistory = jkeyHistory[jPlayerDiv.attr("id")],
nextCoupletID = $j.data(caller, "coupletID").next; // ref to id attribute of next couplet when used on next couplet link
+
      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
+
    // Decision in player may be identical to current confirmable history step
var firstConfirmableStep = jCurrHistory.getfirstConfirmableStep();
+
    var firstConfirmableStep = jCurrHistory.getfirstConfirmableStep();
if (firstConfirmableStep) { // = null if not found
+
    if (firstConfirmableStep) { // = null if not found
// is it a nested block and is it first step, which has to be confirmed
+
      // 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
+
      // means user decided to change path so that the nested block will be removed
var jParentBlock = jCurrHistory.getParentBlock(); // null if not nested
+
      var jParentBlock = jCurrHistory.getParentBlock(); // null if not nested
// in inner, nested blocks, confirming first step implies removing the try-all structure
+
      // in inner, nested blocks, confirming first step implies removing the try-all structure
if (!jParentBlock.is(".histTable") && jCurrHistory.isFirstStepInBlockConfirmableStep()) {     
+
      if (!jParentBlock.is(".histTable") && jCurrHistory.isFirstStepInBlockConfirmableStep()) {     
jCurrHistory.setCurrBlock(jParentBlock);
+
        jCurrHistory.setCurrBlock(jParentBlock);
jCurrHistory.setCurrBlockActive(true);
+
        jCurrHistory.setCurrBlockActive(true);
jCurrHistory.cleanupNestedBlocks();
+
        jCurrHistory.cleanupNestedBlocks();
} else { //  
+
      } else { //  
var jStep = $j(firstConfirmableStep);
+
        var jStep = $j(firstConfirmableStep);
// Is nextCoupletID identical to hash of firstConfirmableStep action link?
+
        // Is nextCoupletID identical to hash of firstConfirmableStep action link?
var firstConfItemActionLink = jStep.find("td.histStepActions a").get(0);
+
        var firstConfItemActionLink = jStep.find("td.histStepActions a").get(0);
if (firstConfItemActionLink.hash == "#"+nextCoupletID) {  
+
        if (firstConfItemActionLink.hash == "#"+nextCoupletID) {  
// Selection in player is identical to current confirmable history action, i.e. does NOT change path.
+
          // Selection in player is identical to current confirmable history action, i.e. does NOT change path.
// Execute confirm and exit jkeyDecision immediately after
+
          // Execute confirm and exit jkeyDecision immediately after
jkeyHistoryAction(firstConfItemActionLink, nextCoupletID, nextCoupletID);
+
          jkeyHistoryAction(firstConfItemActionLink, nextCoupletID, nextCoupletID);
return false;
+
          return false;
}
+
        }
// from here, path has changed; rework history. 1. Remove confirm sub-heading, item itself & following siblings
+
        // from here, path has changed; rework history. 1. Remove confirm sub-heading, item itself & following siblings
jCurrHistory.cleanupAfter(jStep);
+
        jCurrHistory.cleanupAfter(jStep);
// empty result-div of player (no longer valid)
+
        // empty result-div of player (no longer valid)
jPlayerDiv.children("div.jkeyResultMsg").empty().hide();
+
        jPlayerDiv.children("div.jkeyResultMsg").empty().hide();
}
+
      }
} else { // enable history div (disabled at start)
+
    } else { // enable history div (disabled at start)
jPlayerDiv.find("div.jkeyHistory").show();
+
      jPlayerDiv.find("div.jkeyHistory").show();    
// remove all following if path has changed
+
      // remove all following if path has changed
jCurrHistory.cleanupNestedBlocks();
+
      jCurrHistory.cleanupNestedBlocks();
}
+
    }
// Add new step with simplified lead text to history
+
    // Add new step with simplified lead text to history
jCurrHistory.createHistoryStep(jSimplifiedLeadTxt.html(),
+
    jCurrHistory.createHistoryStep(jSimplifiedLeadTxt.html(),
jPlayerDiv.find("div.certaintyDiv input#decisionUncertain").is(":checked"),
+
      jPlayerDiv.find("div.certaintyDiv input#decisionUncertain").is(":checked"),
nextCoupletID, $j.data(caller, "coupletID").curr
+
      nextCoupletID, $j.data(caller, "coupletID").curr
);
+
    );
if (nextCoupletID.length) { // -> NEXT couplet
+
    if (nextCoupletID.length) { // -> NEXT couplet
if (!quiet) {
+
      if (!quiet) {
// Last parameter false: default (i.e. user can change this later) for NEXT decision is certain  
+
        // Last parameter false: default (i.e. user can change this later) for NEXT decision is certain  
jkeyLoadCouplet(jPlayerDiv, nextCoupletID, false);
+
        jkeyLoadCouplet(jPlayerDiv, nextCoupletID, false);
}
+
      }
} else { // -> RESULT
+
    } else { // -> RESULT
// Prepare main result link, common names and resultqualifier (latter 2 may be missing!).
+
      // Prepare main result link, common names and resultqualifier (latter 2 may be missing!).
var jResult = $j('<span class="leadout"/>')
+
      var jResult = $j('<span class="leadout"/>')
.append(jContainer.find("span.commonnames").clone().append(" "))
+
        .append(jContainer.find("span.commonnames").clone().append(" "))
.append(jCaller.clone().removeClass("linkbtn").unbind('click'))
+
        .append(jCaller.clone().removeClass("linkbtn").unbind('click'))
.append(jContainer.find("span.resultqualifier").clone().prepend(" "));
+
        .append(jContainer.find("span.resultqualifier").clone().prepend(" "));
jResult.find("br, img").remove(); // don't combine with above!
+
      jResult.find("br, img").remove(); // don't combine with above!
// Add History result row (use .clone(), result already used above)
+
      // Add History result row (use .clone(), result already used above)
// ## TODO: is it possible to avoid redundant span element? Text node?
+
      // ## TODO: is it possible to avoid redundant span element? Text node?
jCurrHistory.createHistoryResult($j('<span/>').append($j.resource("historyResult")).append(jResult));
+
      jCurrHistory.createHistoryResult($j('<span/>').append($j.resource("historyResult")).append(jResult));
if (!quiet) { // load into main result area
+
      if (!quiet) { // load into main result area
jkeyLoadResult(jPlayerDiv, jResult);
+
        jkeyLoadResult(jPlayerDiv, jResult);
}
+
      }
}
+
    }
} catch (err) {
+
  } catch (err) {
jkeyExceptionAlert(err);
+
    jkeyExceptionAlert(err);
}
+
  }
return false; // cancel default event
+
  return false; // cancel default event
 
}
 
}
  
Line 1,229: Line 1,229:
 
  */
 
  */
 
function jkeyTryAll(caller) {
 
function jkeyTryAll(caller) {
var eachDecisionLink = function (jCurrHistory, jParentBlock, caller, idx) {
+
  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
+
      // 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) + ":");
+
      var jBlock = jCurrHistory.createBlock(false, false, $j.resource("historyNested") + " " + (+idx + 1) + ":");
jCurrHistory.createHistoryNested(jBlock);
+
      jCurrHistory.createHistoryNested(jBlock);
jCurrHistory.setCurrBlock(jBlock);
+
      jCurrHistory.setCurrBlock(jBlock);
jkeyDecision(caller, true); // true = no couplet/result loading into main window, history only
+
      jkeyDecision(caller, true); // true = no couplet/result loading into main window, history only    
jCurrHistory.setCurrBlock(jParentBlock); // revert current block to parent
+
      jCurrHistory.setCurrBlock(jParentBlock); // revert current block to parent
};
+
    };
var jPlayerDiv = $j(caller).closest("div.jkeyPlayer"),
+
  var jPlayerDiv = $j(caller).closest("div.jkeyPlayer"),
jCurrHistory = jkeyHistory[jPlayerDiv.attr("id")];
+
    jCurrHistory = jkeyHistory[jPlayerDiv.attr("id")];
// Only if nested steps not already present:
+
  // Only if nested steps not already present:
if (jCurrHistory.firstNestedStep().length===0) {
+
  if (jCurrHistory.firstNestedStep().length===0) {
// jkeyTryAll always implies that all later steps must be removed
+
    // jkeyTryAll always implies that all later steps must be removed
jCurrHistory.cleanupConfirmableSteps();
+
    jCurrHistory.cleanupConfirmableSteps();
var jNestingParent = jCurrHistory.getCurrBlock(); // preserve current for loop
+
    var jNestingParent = jCurrHistory.getCurrBlock(); // preserve current for loop
// add all lead-on and lead-out links to history
+
    // 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) {
+
    jPlayerDiv.find("table.dt-body:first").find("span.leadon a, span.leadout a").each(function (idx) {
eachDecisionLink(jCurrHistory, jNestingParent, this, idx);
+
      eachDecisionLink(jCurrHistory, jNestingParent, this, idx);
});
+
    });
}
+
  }
// activate first Nested history block; mark as active in history, then load couplet or result
+
  // activate first Nested history block; mark as active in history, then load couplet or result
jCurrHistory.firstNestedStep().find("span.histHeaderActive a").click();
+
  jCurrHistory.firstNestedStep().find("span.histHeaderActive a").click();  
return false; // cancel default event
+
  return false; // cancel default event
 
}
 
}
 
/*
 
/*
Line 1,258: Line 1,258:
 
  */
 
  */
 
function jkeySwitchHistory(caller) {
 
function jkeySwitchHistory(caller) {
var jClosestHistory = $j(caller).closest("table.histBlock, table.histTable");
+
  var jClosestHistory = $j(caller).closest("table.histBlock, table.histTable");
// set new & show last entry
+
  // set new & show last entry
jkeyHistory[jClosestHistory.closest("div.jkeyPlayer").attr("id")].changeActiveBlock(jClosestHistory);
+
  jkeyHistory[jClosestHistory.closest("div.jkeyPlayer").attr("id")].changeActiveBlock(jClosestHistory);
// find direct last step (excluding nested), click TWICE on action (revise it & then confirm it)
+
  // find directly last step (excluding nested), 2x click is: revise, then confirm
jClosestHistory.find("tr:first").nextAll("tr.histStep:last").find("td.histStepActions a").click().click();
+
  jClosestHistory.find("tr:first").nextAll("tr.histStep:last").find("td.histStepActions a").click().click();
return false; // cancel default event
+
  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;
 
  * Description: Initialize interactive mode (step-by-step) for key if keys exist, init key editor delayed;
Line 1,281: Line 1,271:
 
  */
 
  */
 
function jkeyInit() {
 
function jkeyInit() {
var jKeys = $j("div.decisiontree");
+
  var jKeys = $j("div.decisiontree");
if (jKeys.length) { // only if at least one key exists
+
  if (jKeys.length) { // only if at least one key exists
$j("head").append("<style type=\"text/css\">" +
+
    $j("head").append("<style type=\"text/css\">" +
// modal layer styles
+
    // 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-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 {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" +
+
    "#jkeymodal-layer img {display:block;}\n" +
// /* IE6 hack: IE-CSS-expression is very slow; inserted only for IE < 7.
+
    // /* 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" : "") +
+
    ($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!
+
    // 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" +
+
    "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
+
    // Safari 4 has problems with 100% table width:
"div.jkeyLCtrls {margin:0.3em 0 0;padding:0;}\n" +
+
    ($j.browser.safari && $j.browser.version < 5 ? "table.dt-caption {width:98%}\n" : "") +  
"td.jkeyControls {text-align:right; background-color:transparent;}\n" +
+
    "div.jkeyLCtrls {margin:0.3em 0 0;padding:0;}\n" +
"td.jkeyControls a {vertical-align: middle;}" +
+
    "td.jkeyControls {text-align:right; background-color:transparent; font-weight:bold;}\n" +
"div.jkeyPlayer table.dt-body {margin:0.5em}\n" +
+
    "td.jkeyControls a {vertical-align: middle;}" +
"div.jkeyResultMsg {margin:0 0 1em; padding:1em; font-weight:bold; background-color:#EEF0F0; border:1px solid #444444; }\n" +
+
    "div.jkeyPlayer table.dt-body {margin:0.5em}\n" +
"div.jkeyResultMsg span.leadout, div.jkeyResultMsg span.commonnames {background-color:transparent}\n" +
+
    "div.jkeyResultMsg {margin:0 0 1em; padding:1em; font-weight:bold; background-color:#EEF0F0; border:1px solid #444444; }\n" +
"div.certaintyDiv {margin:2em 0 1em 0; padding:0.6em 0.2em; border-top:1px solid #D0D0D0;}\n" +
+
    "div.jkeyResultMsg span.leadout, div.jkeyResultMsg span.commonnames {background-color:transparent}\n" +
"input[type=checkbox] {vertical-align:middle;}\n" + // MAKE GENERIC??
+
    "div.certaintyDiv {margin:2em 0 1em 0; padding:0.6em 0.2em; border-top:1px solid #D0D0D0;}\n" +
"div.jkeyHistory {margin-top:1em;}\n" +
+
    "input[type=checkbox] {vertical-align:middle;}\n" + // MAKE GENERIC??
"table.histTable, table.histBlock {background-color:transparent;}\n" +
+
    "div.jkeyHistory {margin-top:1em;}\n" +
"table.histBlock {border-left:1px solid; width:100%;}\n" +
+
    "table.histTable, table.histBlock {background-color:transparent;}\n" +
"td.histStepNumber, td.histNestedEmpty, td.histResultSymbol {width:1em; padding:0 0.6em;}\n" +
+
    "table.histBlock {border-left:1px solid; width:100%;}\n" +
"td.histHeaderContent, td.histConfirmSubhdgContent {padding-left:0.6em;}\n" +
+
    "td.histStepNumber, td.histNestedEmpty, td.histResultSymbol {width:1em; padding:0 0.6em;}\n" +
"td.histStepActions {text-align:center; width:50px; padding-left:1em; }\n" +
+
    "td.histHeaderContent, td.histConfirmSubhdgContent {padding-left:0.6em;}\n" +
"td.histStepNumber, td.histStepActions, td.histResultSymbol {vertical-align:top;}\n" +
+
    "td.histStepActions {text-align:center; width:50px; padding-left:1em; }\n" +
"span.histStepCertainty {background-color:#FFA07A;}\n" +
+
    "td.histStepNumber, td.histStepActions, td.histResultSymbol {vertical-align:top;}\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" +
+
    "span.histStepCertainty {background-color:#FFA07A;}\n" +
// where span.leadout followed by span.leadon, the two a.linkbtn easily overlap. OK with padding 1.5px!
+
    "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" +
"a.linkbtn {background-color:#EEF0F0; border:1px solid #444444; padding:1.5px 6px; text-align:center; font-weight:bold; white-space:nowrap;}\n" +
+
    // where span.leadout followed by span.leadon, the two a.linkbtn easily overlap. OK with padding 1.5px!
"a.linkbtn:visited, a.linkbtn:hover {color:#444444; text-decoration:none;}\n" +
+
    "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:hover {background-color:#DFDFDF;}\n" +
+
    "a.linkbtn:visited, a.linkbtn:hover {color:#444444; text-decoration:none;}\n" +
"@media screen, handheld, projection {*.printonly {display:none;}} @media print {*.noprint {display:none;}}\n" +
+
    "a.linkbtn:hover {background-color:#DFDFDF;}\n" +
"</style>");
+
    "@media screen, handheld, projection {*.printonly {display:none;}} @media print {*.noprint {display:none;}}\n" +
// flag: show interactive key controls only for keys without class "jkey-nocontrols"
+
    "</style>");
var filtered = jKeys.not(".jkey-nocontrols");
+
    // flag: show interactive key controls only for keys without class "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>"));
+
    var filtered = jKeys.not(".jkey-nocontrols");
// always add "show-all-extras" checkbox (edit button will also be added later here)
+
    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>"));
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>");
+
    // always add "show-all-extras" checkbox (edit button will also be added later here)
// if current URL contains hash (>1 to ignore "#" itself): attempt to start that player (will check for undefined ids)
+
    filtered.find("div.dt-header").append("<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 (document.location.hash.length > 1) {
+
    // if current URL contains hash (>1 to ignore "#" itself): attempt to start that player (will check for undefined ids)
if (jkeyIsValidKeyRef($j(document.location.hash))) {
+
    if (document.location.hash.length > 1) {
jkeySetMode("firststart", $j(document.location.hash));
+
      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() {
+
    } else { // check for flag: jkey-autostart -> automatically change to interactive mode   
jkeySetMode("firststart", "#"+this.id);
+
      jKeys.filter(".jkey-autostart").each(function() {
});
+
        jkeySetMode("firststart", "#"+this.id);
}
+
      });
// delayed loading of editor js and creation of edit button
+
    }
window.setTimeout("jkeyInitEditor()", 500);
+
    if (wgUserName) { // creation of edit button if signed-in
}
+
    // load editor (large code!) only on demand
 +
    // TODO: editor should be activated only for ff/opera/newest IE
 +
      $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 jkeyInitEditor(this)' />");
 +
    }
 +
 
 +
  }
 +
}
 +
 
 +
function jkeyInitEditor(caller) {
 +
  $j.getScript(wgServer + wgScript + "?title=MediaWiki:JKeyEditor.js&action=raw&ctype=text/javascript",
 +
    function(){jedtInitKey(caller)});
 +
  return false;
 
}
 
}
  
Line 1,340: Line 1,341:
 
  */
 
  */
 
$j(document).ready(function() {
 
$j(document).ready(function() {
initCollapseButtons(); // execute first; this changes page layout strongly
+
  initCollapseButtons(); // strongly changes page layout: execute first
jkeyInitImageZooming();
+
  jkeyInitImageZooming();
initTargetHighlighting(); // page-internal jumps
+
  initTargetHighlighting(); // page-internal jumps
jkeyInit(); // init player & editor
+
  jkeyInit(); // init player & editor
 
});
 
});

Revision as of 22:46, 30 May 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 : "&nbsp;Continue&nbsp;",
    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 : "&nbsp;(show less)&nbsp;",
    expandCaption : "&nbsp;(more...)&nbsp;",
    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 : "&nbsp;Weiter&nbsp;",
    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 : "&nbsp;(weniger anzeigen)&nbsp;",
    expandCaption : "&nbsp;(mehr...)&nbsp;",
    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 : "&nbsp;Continua&nbsp;",
    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 : "&nbsp;(mostra di meno)&nbsp;",
    expandCaption : "&nbsp;(più...)&nbsp;",
    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 directly last step (excluding nested), 2x click is: revise, then confirm
  jClosestHistory.find("tr:first").nextAll("tr.histStep:last").find("td.histStepActions a").click().click();
  return false; // cancel default event
}

/*
 * 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" +
    // Safari 4 has problems with 100% table width:
    ($j.browser.safari && $j.browser.version < 5 ? "table.dt-caption {width:98%}\n" : "") +    
    "div.jkeyLCtrls {margin:0.3em 0 0;padding:0;}\n" +
    "td.jkeyControls {text-align:right; background-color:transparent; font-weight:bold;}\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("div.dt-header").append("<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 { // check for flag: jkey-autostart -> automatically change to interactive mode   
      jKeys.filter(".jkey-autostart").each(function() {
        jkeySetMode("firststart", "#"+this.id);
      });
    }
    if (wgUserName) { // creation of edit button if signed-in
    // load editor (large code!) only on demand
    // TODO: editor should be activated only for ff/opera/newest IE
      $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 jkeyInitEditor(this)' />");
    }

  }
}

function jkeyInitEditor(caller) {
  $j.getScript(wgServer + wgScript + "?title=MediaWiki:JKeyEditor.js&action=raw&ctype=text/javascript", 
    function(){jedtInitKey(caller)});
  return false;
}

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