Maintenance

All wikis at Biowikifarm are in read-only mode due to the restoration after a severe cyberattack in October 2023.
After 1 year being shut down the Biowikifarm is online again.
You see the latest restored version from 18th October 2023.

MediaWiki:JKey.js

From Species-ID
Revision as of 09:36, 18 October 2011 by Andreas Plank (Talk | contribs) (update from www.offene-naturfuehrer.de)

Jump to: navigation, search

Note: After saving, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Clear the cache in Tools → Preferences
/*** TEMP NOTE: current class flags: jkey-hidekeymetadata (TEST), jkey-nocontrols (OK) jkey-autostart (FAIL nested keys) jkey-simplified (NEEDS TESTING) ***/
/* jKey.js - Copyright (c) 2009, 2010 Stephan Opitz & G. Hagedorn 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 jQuery, document, console, window, alert, wgPageName, wgServer, wgScript, wgUserName, jedtInit */ /* = settings for JSLint */
"use strict"; // set ECMAScript 5 Strict Mode
 
////////////////////////
// Exception handling //
////////////////////////
 
/*
 * Description: Exception constructor, together with toString method
 * errorCode: string identifying errors
 * variables: may used for detailed information
 */
function jException(errorCode, variables) {
  this.errorCode = errorCode;
  this.variables = variables;
}
jException.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: jException or JS internal exception
 */
function jExceptionAlert(exception) {
  if(window.console) { // IE dev.tools (=F12), or FF firebug console ENABLED
    console.log(exception); // TODO: in ie watch-console there is no debug fct?!
  } else { // perhaps in FF write to browser-javascript-console: throw new Error("text");
    alert(exception);
  }
}
 
/////////////////////////
// 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 = $('<tr class="histHeader"/>')
    .append($('<td class="histHeaderContent" colspan="3"/>').html(newContent + " &nbsp; ")
      .append($('<span class="histHeaderActive"/>')
        .append($.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", $.resource(newIsActiveHistory ? "historyActiveOn" : "historyActiveOff"));
  };
}
/*
 * Description: history confirm subheading CONSTRUCTOR
 */
function HistoryConfirmSubheading() {
  /* Description: create new subheading with newContent string */
  this.create = function(newContent) {
    this.item = $("<tr class='histConfirmSubhdg'/>")
      .append($("<td colspan='3' class='histConfirmSubhdgContent'/>").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 = $("<tr class='histResult'/>")
    .append($("<td class='histResultSymbol'/>").text("►"))
    .append($("<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 = $('<tr class="histNested"/>')
    .append($('<td class="histNestedEmpty"/>'))
    .append($('<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 = $('<tr class ="histStep"/>')
    .append($('<td class="histStepNumber"/>').text(newStepNumber + "."))
    .append('<td class="histStepContent"/>')
    // Create revise-history action (changable to confirm).
    .append($('<td class="histStepActions"/>')
      // (side-requirement: href MUST be confCoupletID:)
      .append($.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;" + $.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('<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($.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 historyResult (small)
  var jCurrHistBlock,
    historyHeader = new HistoryHeader(),
    historyConfirmSubheading = new HistoryConfirmSubheading(),
    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 $('<table cellpadding="0" cellspacing="0" class="'+(isFirstHistory ? "histTable" : "histBlock")+'"/>')
      .append('<tr class="histLayout"/><td/><td/><td/></tr>')
      .append(historyHeader.create(active, isFirstHistory, "<b>" + ((newContent) ? newContent : $.resource("historyHeading")) + "</b>"));
  };
  /*
  * Description: add an item to historyTable
  */
  this.addItem = function(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
          $(this).before(parentThis.createHistoryConfirmSubheading("<i>" + $.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($(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.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();
    historyStep.item = $(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 $ 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($('<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 jkeyisKeyRef(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 jException("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" -> must include / and #
    if (isInternalLink) {
      var jNextCouplet = $(nextCoupletID);
      // Check if valid element within a player was found
      isInternalLink = jkeyisKeyRef(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
    $(this)
      .attr("target", (isInternalLink ? "_self" : "_blank"))
      .addClass("linkbtn").removeAttr("style")
      .click(function() {return jkeyDecision(this, false);});
    // pass autostart marker on (note: this.hash = this.hash + "jkey-autostart" is ok in FF, but IE8 behaves strangely. Using href IE8 seems ok!)
    this.href = this.href + (this.hash.length ? "" :"#") + "jkey-autostart";
    // save in data
    $.data(this, "coupletID", { curr:currCoupletID, next:nextCoupletID });
  };
  var eachLeadon = function() { // this = a leadon link
    var jThis = $(this);
    if (jThis.parent().hasClass("leadon")) { // for leadon (but not leadontext) overwrite display text
      jThis.html($.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
    $.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, 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 = $(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
    $.data(jNodeCell.get(0), "couplet", { id : $.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'
  // $("#"... -> document scope, coupletID may be in other key on same page
  jkeyTransformCouplet(jPlayerDiv, $("#" + 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 = $ object containing the combined result html (commonnames, resultlink, qualifier); will be cloned
 */
function jkeyLoadResult(jPlayerDiv, jResult) {
  jPlayerDiv.find("div.jkeyResultMsg").empty().html($.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 = $(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;" +
        $.imglinkBuilder("iconOverview", "playerOverview", "onclick='return jkeySetMode(\"overview\",this);'") +
        "</span> <span class='jkeyPlayerResume nowrap'> &nbsp; &nbsp;" +
        $.imglinkBuilder("iconResume", "playerResume", "onclick='return jkeySetMode(\"resumed\",this);'")  +
        "</span> <span class='jkeyPlayerStartNew nowrap'> &nbsp; &nbsp;" +
        $.imglinkBuilder("iconStartNew", "playerStartNew", "onclick='return jkeySetMode(\"newstart\",this);'") +
        "</span>");
      // scroll to this key:
      window.scrollTo(0,jKeyDiv[0].offsetTop);
      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 = $('<table class="dt-body" cellspacing="0" cellpadding="0"/>');
      jPlayerDiv = $('<div class="jkeyPlayer"/>')
        .append(jPlayerCouplet)
        // Append a hidden result section (for finished mode)
        .append($('<div class="jkeyResultMsg"/>').hide());
      if (!jKeyDiv.hasClass("jkey-simplified")) {
        jPlayerDiv.append('<div class="certaintyDiv noprint"><input type="checkbox" id="decisionUncertain" value="1" /> <label for="decisionUncertain" style="font-weight:bold">' + $.resource("certaintyLabel") + "</label>&nbsp;"  + $.resource("certaintyHint") + '&nbsp; &nbsp;<span class="tryAllDiv noprint"/><input type="button" onclick="return jkeyTryAll(this);" value="'+$.resource("tryAllAlternatives")+'" /></span></div>');
      }
      // generate unique player id
      var uniquePlayerID;
      do {
        uniquePlayerID = "jkp_"+$.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) {
    jExceptionAlert(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 = $(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) {
    jExceptionAlert(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 = $(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 jException("NotFound", {info:"No lead statement!"});
  }
  // Clone span; remove links, breaks, imgs
  var jSimplifiedLeadTxt = jSpan.clone(true);
  jSimplifiedLeadTxt.find("a").each( function() { $(this).replaceWith($(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 $.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 = $(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 = $.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 = $(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, $.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 = $('<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($('<span/>').append($.resource("historyResult")).append(jResult));
      if (!quiet) { // load into main result area
        jkeyLoadResult(jPlayerDiv, jResult);
      }
    }
  } catch (err) {
    jExceptionAlert(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, $.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 = $(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 = $(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 = $("div.decisiontree");
  if (jKeys.length) { // only if at least one key exists
    $("head").append("<style type=\"text/css\">" +
    // Canvas: top padding needed for IE, FF could do without
    "div.jkeyCanvas {background-color:#FFFFFF; border:5px solid #FFC51A; padding:0.7em 1em 1em 1em}\n" +
    // Safari 4 has problems with 100% table width:
    ($.browser.safari && $.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" +
    "</style>");
    // flag: hide top metadata display (geoscope, creator, etc.)
    jKeys.filter(".jkey-hidekeymetadata").find("span.collapseButton:first a").click()
    // flag: show interactive key controls only for keys without class "jkey-nocontrols"
    var jKeysWithCtrls = jKeys.not(".jkey-nocontrols");
    // add player controls + always add "show-all-extras" checkbox
    jKeysWithCtrls.find("table.dt-caption tr:first").append('<td class="jkeyControls noprint"><span class="jkeyPlayerStart1st nowrap"> &nbsp; &nbsp;' + $.imglinkBuilder("iconStart1st", "playerStart1st", "onclick='return jkeySetMode(\"firststart\",this);'") + "  <br/><input type='checkbox' id='toggleAllExtras' value='1' onclick='toggleAllCollapsible(this.checked)' /> <label for='toggleAllExtras' style='font-weight:normal;'>" + $.resource("expandAll") + '</label></span></td>');
    // Evaluate URL-hash-based and div-class-based autostart options:
    var fragmentID = document.location.hash;
    if (fragmentID.length > 1) { // is non-empty hash (>1 to ignore "#" itself):      
      var jKeyAutostartPos = fragmentID.indexOf("jkey-autostart");
      if (jKeyAutostartPos > -1) { // test for presence of "jkey-autostart" inside hash
        fragmentID = fragmentID.substring(0, jKeyAutostartPos); // remove jkey-autostart
        var isKeyRef;
        if (fragmentID.length===1) { // just the "#"
          isKeyRef = false;
        } else {          
          isKeyRef = jkeyisKeyRef($(fragmentID));
        }
        if (isKeyRef) { // existing ID, start this key (multiple keys may exist)
          jkeySetMode("firststart", fragmentID);
        } else { // else start first key
          jkeySetMode("firststart", jKeys.get(0));
        }
      }
    } else { // no URL-based autostart -> check for flag: jkey-autostart -> automatically change to interactive mode
      jKeys.filter(".jkey-autostart").each(function() {
        jkeySetMode("firststart", "#"+this.id);
      });
    }
    if (wgUserName) { // create edit button if signed-in; load editor (large code!) only on demand
    // TODO: editor should be activated only for ff/opera/newest IE
    //  jKeysWithCtrls.find("div.dt-header").append("<div class='jkeyLCtrls nowrap' style='font-size:0.8em'> &nbsp; <input class='editbtn' type='button' style='font-size:0.76em;vertical-align:middle;' value='" + $.resource("editorEdit") + "' onclick='return jkeyInitEditor(this)' /></div>");
    }
  } // end at least one jKey
}
 
function jkeyInitEditor(caller) {
  $.getScript(wgServer + wgScript + "?title=MediaWiki:JKeyEditor.js&action=raw&ctype=text/javascript",
    function(){jedtInit(caller);});
  return false;
}
 
/*
 * Numbering edit tools for the Wiki templates Lead and Decision Horizontal 
 * see MediaWiki:JKey_changeKeyNumbering.js and MediaWiki:Commons.js
 */
 
/*
 * Description: Called after html document and all js-sources are loaded
 */
$(document).ready(function() {
  // append jKey-specific resources to global object, "true" = deep extension
  $.extend(true, $.jI18n, {
  en: {
    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)"
  },
  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 : "überprüfen",
    historyUncertainFlag  : "(unsicher)",
    toolTipIsActivePath : "Derzeit aktiver Bestimmungsweg (mehrere Alternativen werden verfolgt)"
  },
  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)"
  }
  });
  jkeyInit(); // init player & editor-preloader
});