MediaWiki:JKey.js

From FloraWiki - das Wiki zur Schweizer Flora
Revision as of 13:56, 24 August 2017 by Andreas Plank (Talk | contribs) (ESlint fix: single quotes for string)

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) ***/
/**
 * @description jKey.js - Copyright (c) 2009-2012 Stephan Opitz, Andreas Plank & 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.
 * @requires: $.imglinkBuilder
 * @requires: $.linkBuilder
 * @requires: $.random
 * @requires: $.toggleAllCollapsible
 * TODO add documentation
 */
/* Settings for JSLint: */
/*jslint sloppy:false, maxerr:100, indent:2 */
/*global $, jQuery, document, console, window, alert, wgPageName, wgServer, wgScript, wgUserName, jedtInit */

// set ECMAScript 5 Strict Mode. Done globally, not within each function. Later the whole jKey should be wrapped as a gadget providing scope for this
'use strict';

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

/**
 * @description: Exception constructor, providing toString method
 * @param {string} errorCode  identifying errors
 * @param {object} variables optional for detailed information
 * @returns {jException}
 */
function jException(errorCode, variables) {
  this.errorCode = errorCode;
  this.variables = variables;
}
/**
 * @augments jException()
 * @returns {String}
 */
jException.prototype.toString = function () {
  var message = 'JKey Exception ' + this.errorCode + '\n\n',
   key;
  for (key in this.variables) {
    // only own properties, not inherited ones:
    if (this.variables.hasOwnProperty(key)) {
      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
 * 
 * @returns {HistoryHeader}
 */
function HistoryHeader() {
 /**
  * @description: creates new header for history of previous decisions
  * 
  * @requires $.resource()
  * @param {boolean} isActiveHistory boolean is history set as active
  * @param {boolean} isFirstHistory hides active-history-flag if true
  * @param {string} newContent the new description
  * @returns {unresolved}
  */
  this.create = function (isActiveHistory, isFirstHistory, newContent) {
    // create base header
    this.item = $('<tr class="histHeader"/>')
      .append($('<td class="histHeaderContent" colspan="3"/>').html((newContent ? newContent : $.resource('jKey_historyHeading')) + ' &nbsp; ')
        .append($('<span class="histHeaderActive"/>')
          .append($.imglinkBuilder('jKey_historyActive', '', 'class="histActiveOn" onclick="return jkeySwitchHistory(this);"'))
          .toggle(!isFirstHistory)
          ));
    this.setCurrBlockActive(isActiveHistory);
    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
  * isActiveHistory: boolean
  */
  this.setCurrBlockActive = function (isActiveHistory) { // do not use imglinkBuilder, replaceWith kills layout
    var histHeaderActiveCell = this.item.find('span.histHeaderActive a');
    // change class & image
    histHeaderActiveCell.attr({'class': (isActiveHistory ? 'histActiveOn' : 'histActiveOff')});
    histHeaderActiveCell.find('img')
     .attr('src', $.resource(isActiveHistory ? 'jKey_historyActive' : 'jKey_historyInactive'))
     .attr('title', $.resource(isActiveHistory ? 'jKey_historyActiveTooltip' : 'jKey_historyInactiveTooltip'));
  };
}// HistoryHeader()
/*
 * 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
 * 
 * @returns {HistoryResult}
 */
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
 * @returns {HistoryNested}
 */
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
 * @returns {HistoryStep}
 */
function HistoryStep() {
  /*
  * Description: creates new step
  * newStepNumber: step number
  * newContent: description of previous decision
  * 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(
              'jKey_historyRevise',
              '',
              '#' + confCoupletID,
              ' class="histStepActionRevise small-linkbtn" 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('jKey_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;
  };
  /*
  * set the step content (override inheritance)
  * @param 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 small-linkbtn' : 'histStepActionRevise small-linkbtn')})
      .text($.resource((setToConfirm ? 'jKey_historyConfirm' : 'jKey_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)
  */
    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('jKey_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'),
      parentThis;
    if (jAllSteps.length) { // steps exist
      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'),
      parentThis;
    if (jAllSteps.length) { // entries exists
      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('jKey_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
  * isActiveHistory: 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 (unused?)
 * jDecisionRow: first row of a couplet - must be tested prior to calling this!
 * jPlayerCouplet: position where couplet will appended
 * @todo parentlead does lead to jDecisionRow.length === 0
 */
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'),
    jNextCouplet,
    eachLeadout = function () {// this = a leadout link
      var nextCoupletID = this.hash,
        isInternalLink = ((nextCoupletID.length > 0) && (this.href.search('/' + wgPageName + '#') !== -1)),
        jKeyTable,
        jNodeID;
      // Example for test above: wgPageName="ThisPage", this.href "http://.../AlsoThisPageTwo#xxx" -> must include / and #
      if (isInternalLink) {
        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!
            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 full href in IE8 seems ok!)
      this.href = this.href + (this.hash.length ? '' : '#') + 'jkey-autostart';
      // save in data
      $.data(this, 'coupletID', {curr: currCoupletID, next: nextCoupletID});
    },
    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('jKey_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 of each().
      jThis.click(function () {return jkeyDecision(this, false); });
      // save in data
      $.data(this, 'coupletID', {curr: currCoupletID, next: this.hash.substring(1, this.hash.length)});
    },
    //prefixID = function () {return 'jK' + this.id; },
    suffixID = function () {
      // leave the mw-customcollapsible untouched
      // if (this.id.indexOf('mw-customcollapsible') === 0) {
      //   return this.id;
      // } else {
      //   return this.id + 'jK';
      // }
      return this.id + 'jK';
    };
  // 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.replace(/(:|\.)/g,'\\$1') + '_]').each(function () {
    var jClonedRow = $(this).clone(true, true),
      jNodeCell;
    // prefix id of decision row itself and all descendants
    //jClonedRow.find('[id]').andSelf().attr('id', prefixID);
    //check if andSelf neccessary
    //jClonedRow.find('[id]').andSelf().attr('id', suffixID);
    jClonedRow.find('[id]').attr('id', suffixID);
    /**
     * test try default jQuery.makeCollapsible Tue Aug 08 2017 14:25:15 GMT+0200 (CEST)
     * jClonedRow.find('.pseudolink')
     * // remove MediaWiki's collapsible click event
     * .unbind('click.mw-collapse')
     * .attr('class', function (index, attr) {
     * // check for MediaWiki class mw-customtoggle-myKey and add jK suffix
     *   return attr.replace(/(mw-customtoggle-[^ ]+)/, '$1jK');
     * });
     * // remove all MediaWiki's collapsible class added previously by $.fn.makeCollapsible()
     * // leave mw-collapsed untouched!
     * jClonedRow
     * .find('.mw-made-collapsible')
     * .removeClass('mw-made-collapsible');
     */
    // replace lead identifier with arrow (normal) or blank (horizontal side-by-side leads)
    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) ? '►' : ' '); //\u25ba is ►
    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);
    // initialize $.fn.makeCollapsible() again
    jPlayerCouplet.append(jClonedRow.show());
    // jPlayerCouplet.find('.mw-collapsible').makeCollapsible();
    var cutsomToggleOptions = {
      toggleClasses: true,
      toggleText: {
        collapseText: $.resource('CollapseBox_captionCollapse'),
        expandText: $.resource('CollapseBox_captionExpand')
      },
      $customTogglers : jPlayerCouplet.find('.mw-customtoggle')
    };
    jPlayerCouplet.find('[id^="mw-customcollapsible-"]').makeCollapsible(cutsomToggleOptions);
  }); // 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: 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('.jkeyControls'),
      finished,
      overview,
      uniquePlayerID;
    if (mode === 'firststart') { // change permanently: first-start to restart icon/text, add overview/resume controls, removes  toggleAllExtras!
      jControls.find('.jkeyPlayerStartNew').show();
      jControls.find('.jkeyPlayerStart1st, .jkeyToggleAllExtras').hide();
      window.scrollTo(0, jKeyDiv[0].offsetTop); // scroll to current key
      mode = 'newstart';
    }
    finished = (mode === 'finished');
    overview = !(mode === 'resumed' || mode === 'newstart' || finished);
    jControls.find('.jkeyPlayerOverview').toggle(!overview);
    jControls.find('.jkeyPlayerResume').toggle(overview);
    jKeyDiv.find('.dt-header,.dt-header-toggle').toggle(overview); // metadata box (description, audience, etc.) and right-floating "more" to show metadata box
    // Show finished message only in finished mode (+ set below for resumed)
    jResultDiv.toggle(finished);
    // set entire wiki content area to text color css3 rgba with transparency 0.2 (jkeyCanvas is already 1.0).
    // all separately colored text (links etc.) must be handled separately (add all, then remove inside player)
    // NOTE: css opacity not an option, opacity of children can not increase (= made more visible) !
    $('#bodyContent')
     .addClass('jkdimmed')
     .find('a,a.new,a:visited,a:hover,.pseudolink,.linbtn,h2,h3,h4,h5,h6,.commonnames').addClass('jkdimmed');
    jKeyDiv.find('.jkdimmed').removeClass('jkdimmed');
    switch (mode) {
    case ('overview'): // STOP player, hide player, show original overview player, undo low opacity content area
      jKeyDiv.removeClass('jkeyCanvas');
      jKeyTable.show();
      jPlayerDiv.hide();
      $('#bodyContent')
       .removeClass('jkdimmed')
       .find('.jkdimmed').removeClass('jkdimmed');
      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" title="'+$.resource("jKey_certaintyTooltip")+'">'
            + $.resource('jKey_certaintyLabel') + '</label>&nbsp;'
            + '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; '
            + $.linkBuilder('jKey_tryAllAlternatives','','#',' class="small-linkbtn" onclick="return jkeyTryAll(this);" title="'+$.resource('jKey_tryAllAlternativesTooltip')+"'")
            + '</div>'
        );
      }
      // generate unique playerID (multiple keys may exist on 1 page!):
      do {
        uniquePlayerID = 'jkp_' + $.random(1, 9999999);
      } while (jkeyHistory.uniquePlayerID); // until ID 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' intended!
    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.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: 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('jKey_mainResultMsg')).append(jResult.clone());
  jkeySetMode('finished', jPlayerDiv);
}

/*
 * 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')],
      firstConfirmableStep,
      nextDecisionIsUncertain;
    // 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
      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
      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, td.leadtxt'), // TODO remove th.leadtxt later (Freitag, 23. März 2012 13:42)
    jSpan,
    jSimplifiedLeadTxt;
  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)
    if (jContainer.get(0).className.search(/leadresult-hor/) !== -1) {
      // result is separated by a table line.
      jSpan = jContainer.closest('table').find('td.leadtxt:nth-child(' + (jContainer.get(0).cellIndex + 1) + ')').find('span.leadspan');
    } else {
      jSpan = jContainer.closest('table').find('td.leadtxt').find('span.leadspan');
    }
  }
  if (jSpan.length === 0) {
    throw new jException('NotFound', {
      info: 'No lead statement!',
      jContainer: jContainer,
      jContainer_closest_table: jContainer.closest('table'),
      leadLinkCaller: leadLinkCaller
    });
  }
  // Clone span; remove links, breaks, imgs
  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)
 * @param {selector} caller DOM reference of the calling element (has $.data with members: next & curr (couplet ID))
 * @param {boolean} 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, td.leadtxt'), // TODO remove th.leadtxt later (Freitag, 23. März 2012 13:42)
      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
      firstConfirmableStep,
      jParentBlock,
      jStep,
      firstConfItemActionLink,
      jResult;
    // Decision in player may be identical to current confirmable history step
    firstConfirmableStep = jCurrHistory.getfirstConfirmableStep();
    if (firstConfirmableStep) { // = null if not found
      // is it a nested block and is it first step, which has to be confirmed?
      // -> user decided to change path so that the nested block will be removed
      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 { //
        jStep = $(firstConfirmableStep);
        // Is nextCoupletID identical to hash of firstConfirmableStep action link?
        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!).
      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('jKey_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"
 * 
 * @requires $.resource()
 * @param {selector} caller DOM reference to a link
 * @returns {Boolean} False
 */
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('jKey_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
    },
    jPlayerDiv = $(caller).closest('div.jkeyPlayer'),
    jCurrHistory = jkeyHistory[jPlayerDiv.attr('id')],
    jNestingParent;
  // Only if nested steps not already present:
  if (jCurrHistory.firstNestedStep().length === 0) {
    // jkeyTryAll always implies that all later steps must be removed
    jCurrHistory.cleanupConfirmableSteps();
    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)
 * 
 * @param {type} caller DOM reference to a link
 * @returns {Boolean}
 */
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.
 * Adds event jkey:initialized
 * 
 * @requires $.imglinkBuilder()
 * @requires $.resource()
 * @returns {undefined}
 */
function jkeyInit() {
  var jKeys = $('div.decisiontree'),
    jKeysWithCtrls,
    fragmentID,
    jKeyAutostartPos,
    isKeyRef;
  // append jKey-specific resources to global jI18n, "true" = deep extension
  $.extend(true, $.jI18n, {
    en: {
      jKey_historyActive : 'http://upload.wikimedia.org/wikipedia/commons/thumb/9/94/Symbol_support_vote.svg/20px-Symbol_support_vote.svg.png',
      jKey_historyInactive  : 'http://upload.wikimedia.org/wikipedia/commons/thumb/3/37/Symbol_partial_support_vote.svg/20px-Symbol_partial_support_vote.svg.png',
      jKey_historyActiveTooltip : 'This is the currently active history; decisions made above will append to this',
      jKey_historyInactiveTooltip  : 'Click here to make this alternative the active history',
      jKey_playerStart1st  : 'Step-by-step identification',
      jKey_playerStartNew  : 'Start new identification',
      jKey_playerOverview  : 'Key overview (printable)',
      jKey_playerResume  : 'Resume',
      jKey_coupletContinue : '&nbsp;Continue&nbsp;',
      jKey_editorEdit  : 'Edit Key',// unused
      jKey_editorSave  : 'Save',// unused
      jKey_certaintyLabel  : 'Flag this decision as uncertain',
      jKey_certaintyTooltip : 'If checkbox is activated, clicking on Next or a result (above) will flag the decision in the list of previous decisions with the word ‘uncertain’.',
      jKey_tryAllAlternatives  : 'Undecided: Try all alternatives',
      jKey_tryAllAlternativesTooltip : 'If not even an uncertain decision is possible, all alternatives may be followed in parallel. After finishing the first alternative, activate the other alternatives in the history of previous decisions (below).',
      jKey_mainResultMsg : 'You identified: ',
      jKey_historyHeading  : 'Previous decisions',
      jKey_historyConfirmable  : 'Confirmable decisions:',
      jKey_historyNested : 'Alternative',
      jKey_historyResult : 'Result: ',
      jKey_historyConfirm  : 'confirm',
      jKey_historyRevise : 'revise',
      jKey_historyUncertainFlag  : '(uncertain)',
      jKey_toolTipIsActivePath : 'Currently active identification path (multiple alternatives are being followed)' // unused
    },
    fr: {
      jKey_playerStart1st  : 'détermination interactive',
      jKey_playerStartNew  : 'détermination nouvelle',
      jKey_playerOverview  : 'Vue d’ensemble (imprimable)',
      jKey_playerResume  : 'continuer la détermination',
      jKey_coupletContinue : '&nbsp;avancer&nbsp;',
      jKey_editorEdit  : 'éditer',
      jKey_editorSave  : 'enregistrer',
      jKey_certaintyLabel  : 'Cette décision a marqué comme incertain',
      jKey_certaintyTooltip : 'Si la case à cocher est activée, cliquer sur « avancer » ou un résultat (ci-dessus) marquera la décision dans la liste des décisions précédentes avec le mot « incertain ».',
      jKey_tryAllAlternatives  : 'Indécidable: Suivez toutes les alternatives',
      jKey_tryAllAlternativesTooltip : 'Si même une décision incertaine est possible, toutes les alternatives peuvent être suivies en parallèle. Après avoir terminé la première alternative, activez les autres alternatives dans l’historique des décisions précédentes (ci-dessous).',
      jKey_mainResultMsg : 'résultat: ',
      jKey_historyActiveTooltip : 'Alternatif actif. Les décisions prises ci-dessus sont enregistrés ici',
      jKey_historyInactiveTooltip  : 'Cliquez ici pour poursuivre cette alternative',
      jKey_historyHeading  : 'Décisions antérieures',
      jKey_historyConfirmable  : 'Les décisions en cours d’examen:',
      jKey_historyResult : 'résultat: ',
      jKey_historyConfirm  : 'confirmer',
      jKey_historyRevise : 'vérifier',
      jKey_historyUncertainFlag  : '(incertain)',
      jKey_toolTipIsActivePath : 'Actuellement la route de détermination actif (plusieurs alternatives sont poursuivies)'
    },
    de: {
      jKey_playerStart1st  : 'Interaktive Bestimmung',
      jKey_playerStartNew  : 'Neue Bestimmung',
      jKey_playerOverview  : 'Übersicht (druckbar)',
      jKey_playerResume  : 'Bestimmung fortsetzen',
      jKey_coupletContinue : '&nbsp;Weiter&nbsp;',
      jKey_editorEdit  : 'Bearbeiten',
      jKey_editorSave  : 'Speichern',
      jKey_certaintyLabel  : 'Diese Entscheidung als unsicher kennzeichnen',
      jKey_certaintyTooltip : 'Wenn die Checkbox aktiviert ist, wird beim Klick auf ‚Weiter‘ oder ein Ergebnis diese Entscheidung in der Liste bisheriger Entscheidungen mit dem Wort ‚unsicher‘ gekennzeichnet.',
      jKey_tryAllAlternatives  : 'Nicht entscheidbar: Verfolge alle Alternativen',
      jKey_tryAllAlternativesTooltip : 'Wenn überhaupt keine Entscheidung möglich ist, können die Alternativen parallel verfolgt werden. Nachdem die erste Alternative zu Ende geführt wurde, können die Übrigen in der Liste der bisherigen Entscheidungen (unten) aktiviert werden.',
      jKey_mainResultMsg : 'Ergebnis: ',
      jKey_historyActiveTooltip : 'Aktive Alternative. Die oben getroffenen Entscheidungen werden hier aufgezeichnet',
      jKey_historyInactiveTooltip  : 'Klicken Sie hier, um diese Alternative weiter zu verfolgen',
      jKey_historyHeading  : 'Bisherige Entscheidungen',
      jKey_historyConfirmable  : 'Entscheidungen in Überprüfung:',
      jKey_historyResult : 'Ergebnis: ',
      jKey_historyConfirm  : 'bestätigen',
      jKey_historyRevise : 'überprüfen',
      jKey_historyUncertainFlag  : '(unsicher)',
      jKey_toolTipIsActivePath : 'Derzeit aktiver Bestimmungsweg (mehrere Alternativen werden verfolgt)'
    },
    it: {
      jKey_playerStart1st  : 'Esegui passo-dopo-passo',
      jKey_playerStartNew  : 'Nuova identificazione',
      jKey_playerOverview  : 'Sintesi completa (stampabile)',
      jKey_playerResume  : 'Ricomincia l’identificazione',
      jKey_coupletContinue : '&nbsp;Continua&nbsp;',
      jKey_editorEdit  : 'Modifica',
      jKey_editorSave  : 'Salva',
      jKey_certaintyLabel  : 'Segna scelta come insicura', //REVISE
      jKey_certaintyTooltip : '(click, then make your next decision)', //TRANSLATE
      jKey_mainResultMsg : 'Il risultato dell’identificazione è: ',
      jKey_historyHeading  : 'Scelte precedenti',
      jKey_historyConfirmable  : 'Scelta confermabile:',
      jKey_historyResult : 'Risultato: ',
      jKey_historyConfirm  : 'conferma',
      jKey_historyRevise : 'correggi',
      jKey_historyUncertainFlag  : '(incerta)',
      jKey_toolTipIsActivePath : 'Percorso di identificazione attualmente attivo (vengono seguite alternative multiple)'
    }
  });
  if (jKeys.length) { // only if at least one key exists
    $('head').append(
      '<style type="text/css">'
        // Canvas: top padding needed for IE, FF/Chrome could do without
        + 'div.jkeyCanvas {background-color:#FFFFFF; border:5px solid #FFC51A; padding:0.7em 1em 1em 1em; color:rgba(0,0,0,1.0);}\n'
        // do dim background around the jkeyCanvas, applied to body, links, etc.
        + '.jkdimmed {color:rgba(0,0,0,0.2) !important;}\n'
        // Safari 4 has problems with 100% table width:
        + ( ( navigator.userAgent.toUpperCase().indexOf('SAFARI') > 0 && parseFloat(navigator.appVersion) < 5 ) ? 'table.dt-caption {width:98%}\n' : '')
        + 'div.jkeyControls a {vertical-align: middle;}\n'
        + 'div.jkeyPlayer table.dt-body {margin:0.5em}\n'
        + 'div.jkeyResultMsg {clear:right; 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:1.8em 0 1em 0; padding:0.8em 0.2em; font-size:83%; color:#555;}\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; font-weight:bold;}\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.leadtxt, div.jkeyPlayer table.dt-body td.leadresult {padding-top:1em;line-height:1.7em}\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'
    jKeysWithCtrls = jKeys.not('.jkey-nocontrols');
    // add player control + always add 'show-all-extras' checkbox (toggleAllExtras) in Overview mode (later removed)
    jKeysWithCtrls
      .find('.dt-caption')
      .prepend('<div class="jkeyControls" style="float:right;text-align:right; background-color:transparent; font-weight:bold; margin-bottom:6px"><span class="jkeyPlayerStart1st nowrap linkbtn" style="padding:4px 6px;">'
        + $.imglinkBuilder('jKey_iconStart1st', 'jKey_playerStart1st', 'onclick="return jkeySetMode(\'firststart\',this);"')
        + '</span><span class="jkeyToggleAllExtras nowrap"><br/><input type="checkbox" id="toggleAllExtras" class="toggleAllExtras" value="1" onclick="$.toggleAllCollapsible(this.checked, this)" onkeyup="$.toggleAllCollapsible(this.checked, this)" />'
        + ' <label for="toggleAllExtras" style="font-weight:normal;font-size:83%">' + $.resource('jKey_expandAll')
        + '</label><br/></span><span class="jkeyPlayerOverview nowrap">' +
        $.imglinkBuilder('jKey_iconOverview', 'jKey_playerOverview', 'onclick="return jkeySetMode(\'overview\',this);"')
        + '<br/></span><span class="jkeyPlayerResume nowrap">'
        + $.imglinkBuilder('jKey_iconResume', 'jKey_playerResume', 'onclick="return jkeySetMode(\'resumed\',this);"')
        + '<br/></span><span class="jkeyPlayerStartNew nowrap">'
        + $.imglinkBuilder('jKey_iconStartNew', 'jKey_playerStartNew', 'onclick="return jkeySetMode(\'newstart\',this);"') +
        '</span></div>')
      .find('.jkeyPlayerOverview,.jkeyPlayerResume,.jkeyPlayerStartNew').hide(); // RETEST WHETHER DIRECT HIDE/DISPLAY:NONE NOW POSSIBLE! putting display:none causes layout problems when showing them later!
    // Evaluate URL-hash-based and div-class-based autostart options:
    fragmentID = document.location.hash;
    if (fragmentID.length > 1) { // is non-empty hash (>1 to ignore '#' itself):
      jKeyAutostartPos = fragmentID.indexOf('jkey-autostart');
      if (jKeyAutostartPos > -1) { // test for presence of 'jkey-autostart' inside hash
        fragmentID = fragmentID.substring(0, jKeyAutostartPos); // remove jkey-autostart
        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);
      });
    }
    jKeys.trigger('jkey:initialized');
  }
}// jkeyInit()

$(document).ready(function () { // Called after html + all js loaded
  jkeyInit();
});