var H5P = H5P || {};
/**
 * Transition contains helper function relevant for transitioning
 */
H5P.Transition = (function ($) {

  /**
   * @class
   * @namespace H5P
   */
  Transition = {};

  /**
   * @private
   */
  Transition.transitionEndEventNames = {
    'WebkitTransition': 'webkitTransitionEnd',
    'transition':       'transitionend',
    'MozTransition':    'transitionend',
    'OTransition':      'oTransitionEnd',
    'msTransition':     'MSTransitionEnd'
  };

  /**
   * @private
   */
  Transition.cache = [];

  /**
   * Get the vendor property name for an event
   *
   * @function H5P.Transition.getVendorPropertyName
   * @static
   * @private
   * @param  {string} prop Generic property name
   * @return {string}      Vendor specific property name
   */
  Transition.getVendorPropertyName = function (prop) {

    if (Transition.cache[prop] !== undefined) {
      return Transition.cache[prop];
    }

    var div = document.createElement('div');

    // Handle unprefixed versions (FF16+, for example)
    if (prop in div.style) {
      Transition.cache[prop] = prop;
    }
    else {
      var prefixes = ['Moz', 'Webkit', 'O', 'ms'];
      var prop_ = prop.charAt(0).toUpperCase() + prop.substr(1);

      if (prop in div.style) {
        Transition.cache[prop] = prop;
      }
      else {
        for (var i = 0; i < prefixes.length; ++i) {
          var vendorProp = prefixes[i] + prop_;
          if (vendorProp in div.style) {
            Transition.cache[prop] = vendorProp;
            break;
          }
        }
      }
    }

    return Transition.cache[prop];
  };

  /**
   * Get the name of the transition end event
   *
   * @static
   * @private
   * @return {string}  description
   */
  Transition.getTransitionEndEventName = function () {
    return Transition.transitionEndEventNames[Transition.getVendorPropertyName('transition')] || undefined;
  };

  /**
   * Helper function for listening on transition end events
   *
   * @function H5P.Transition.onTransitionEnd
   * @static
   * @param  {domElement} $element The element which is transitioned
   * @param  {function} callback The callback to be invoked when transition is finished
   * @param  {number} timeout  Timeout in milliseconds. Fallback if transition event is never fired
   */
  Transition.onTransitionEnd = function ($element, callback, timeout) {
    // Fallback on 1 second if transition event is not supported/triggered
    timeout = timeout || 1000;
    Transition.transitionEndEventName = Transition.transitionEndEventName || Transition.getTransitionEndEventName();
    var callbackCalled = false;

    var doCallback = function () {
      if (callbackCalled) {
        return;
      }
      $element.off(Transition.transitionEndEventName, callback);
      callbackCalled = true;
      clearTimeout(timer);
      callback();
    };

    var timer = setTimeout(function () {
      doCallback();
    }, timeout);

    $element.on(Transition.transitionEndEventName, function () {
      doCallback();
    });
  };

  /**
   * Wait for a transition - when finished, invokes next in line
   *
   * @private
   *
   * @param {Object[]}    transitions             Array of transitions
   * @param {H5P.jQuery}  transitions[].$element  Dom element transition is performed on
   * @param {number=}     transitions[].timeout   Timeout fallback if transition end never is triggered
   * @param {bool=}       transitions[].break     If true, sequence breaks after this transition
   * @param {number}      index                   The index for current transition
   */
  var runSequence = function (transitions, index) {
    if (index >= transitions.length) {
      return;
    }

    var transition = transitions[index];
    H5P.Transition.onTransitionEnd(transition.$element, function () {
      if (transition.end) {
        transition.end();
      }
      if (transition.break !== true) {
        runSequence(transitions, index+1);
      }
    }, transition.timeout || undefined);
  };

  /**
   * Run a sequence of transitions
   *
   * @function H5P.Transition.sequence
   * @static
   * @param {Object[]}    transitions             Array of transitions
   * @param {H5P.jQuery}  transitions[].$element  Dom element transition is performed on
   * @param {number=}     transitions[].timeout   Timeout fallback if transition end never is triggered
   * @param {bool=}       transitions[].break     If true, sequence breaks after this transition
   */
  Transition.sequence = function (transitions) {
    runSequence(transitions, 0);
  };

  return Transition;
})(H5P.jQuery);
;var H5P = H5P || {};

/**
 * H5P audio module
 *
 * @external {jQuery} $ H5P.jQuery
 */
H5P.Audio = (function ($) {
  /**
  * @param {Object} params Options for this library.
  * @param {Number} id Content identifier.
  * @param {Object} extras Extras.
  * @returns {undefined}
  */
  function C(params, id, extras) {
    H5P.EventDispatcher.call(this);

    this.contentId = id;
    this.params = params;
    this.extras = extras;
    this.toggleButtonEnabled = true;

    // Retrieve previous state
    if (extras && extras.previousState !== undefined) {
      this.oldTime = extras.previousState.currentTime;
    }

    this.params = $.extend({}, {
      playerMode: 'minimalistic',
      fitToWrapper: false,
      controls: true,
      autoplay: false,
      audioNotSupported: "Your browser does not support this audio",
      playAudio: "Play audio",
      pauseAudio: "Pause audio",
      propagateButtonClickEvents: true
    }, params);

    // Required if e.g. used in CoursePresentation as area to click on
    if (this.params.playerMode === 'transparent') {
      this.params.fitToWrapper = true;
    }

    this.on('resize', this.resize, this);
  }

  C.prototype = Object.create(H5P.EventDispatcher.prototype);
  C.prototype.constructor = C;

  /**
   * Adds a minimalistic audio player with only "play" and "pause" functionality.
   *
   * @param {jQuery} $container Container for the player.
   * @param {boolean} transparentMode true: the player is only visible when hovering over it; false: player's UI always visible
   */
  C.prototype.addMinimalAudioPlayer = function ($container, transparentMode) {
    var INNER_CONTAINER = 'h5p-audio-inner';
    var AUDIO_BUTTON = 'h5p-audio-minimal-button';
    var PLAY_BUTTON = 'h5p-audio-minimal-play';
    var PLAY_BUTTON_PAUSED = 'h5p-audio-minimal-play-paused';
    var PAUSE_BUTTON = 'h5p-audio-minimal-pause';

    var self = this;
    this.$container = $container;

    self.$inner = $('<div/>', {
      'class': INNER_CONTAINER + (transparentMode ? ' h5p-audio-transparent' : '')
    }).appendTo($container);

    var audioButton = $('<button/>', {
      'class': AUDIO_BUTTON + " " + PLAY_BUTTON,
      'aria-label': this.params.playAudio
    }).appendTo(self.$inner)
      .click( function (event) {
        if (!self.params.propagateButtonClickEvents){
          event.stopPropagation();
        }

        if (!self.isEnabledToggleButton()) {
          return;
        }

        // Prevent ARIA from playing over audio on click
        this.setAttribute('aria-hidden', 'true');

        if (self.audio.paused) {
          self.play();
        }
        else {
          self.pause();
        }
      })
      .on('focusout', function () {
        // Restore ARIA, required when playing longer audio and tabbing out and back in
        this.setAttribute('aria-hidden', 'false');
      });

    // Fit to wrapper
    if (this.params.fitToWrapper) {
      audioButton.css({
        'width': '100%',
        'height': '100%'
      });
    }

    //Event listeners that change the look of the player depending on events.
    self.audio.addEventListener('ended', function () {
      audioButton
        .attr('aria-hidden', false)
        .attr('aria-label', self.params.playAudio)
        .removeClass(PAUSE_BUTTON)
        .removeClass(PLAY_BUTTON_PAUSED)
        .addClass(PLAY_BUTTON);
    });

    self.audio.addEventListener('play', function () {
      audioButton
        .attr('aria-label', self.params.pauseAudio)
        .removeClass(PLAY_BUTTON)
        .removeClass(PLAY_BUTTON_PAUSED)
        .addClass(PAUSE_BUTTON);
    });

    self.audio.addEventListener('pause', function () {
      // Don't override if initial look is set
      if (!audioButton.hasClass(PLAY_BUTTON)) {
        audioButton
          .attr('aria-hidden', false)
          .attr('aria-label', self.params.playAudio)
          .removeClass(PAUSE_BUTTON)
          .addClass(PLAY_BUTTON_PAUSED);
      }
    });

    H5P.Audio.MINIMAL_BUTTON = AUDIO_BUTTON + " " + PLAY_BUTTON;
    H5P.Audio.MINIMAL_BUTTON_PAUSED = AUDIO_BUTTON + " " + PLAY_BUTTON_PAUSED;

    this.$audioButton = audioButton;
    // Scale icon to container
    self.resize();
  };

  /**
   * Resizes the audio player icon when the wrapper is resized.
   */
  C.prototype.resize = function () {
    // Find the smallest value of height and width, and use it to choose the font size.
    if (this.params.fitToWrapper && this.$container && this.$container.width()) {
      var w = this.$container.width();
      var h = this.$container.height();
      if (w < h) {
        this.$audioButton.css({'font-size': w / 2 + 'px'});
      }
      else {
        this.$audioButton.css({'font-size': h / 2 + 'px'});
      }
    }
  };

  return C;
})(H5P.jQuery);

/**
 * Wipe out the content of the wrapper and put our HTML in it.
 *
 * @param {jQuery} $wrapper Our poor container.
 */
H5P.Audio.prototype.attach = function ($wrapper) {
  const self = this;
  $wrapper.addClass('h5p-audio-wrapper');

  // Check if browser supports audio.
  var audio = document.createElement('audio');
  if (audio.canPlayType === undefined) {
    this.attachNotSupportedMessage($wrapper);
    return;
  }

  // Add supported source files.
  if (this.params.files !== undefined && this.params.files instanceof Object) {
    for (var i = 0; i < this.params.files.length; i++) {
      var file = this.params.files[i];

      if (audio.canPlayType(file.mime)) {
        var source = document.createElement('source');
        source.src = H5P.getPath(file.path, this.contentId);
        source.type = file.mime;
        audio.appendChild(source);
      }
    }
  }

  if (!audio.children.length) {
    this.attachNotSupportedMessage($wrapper);
    return;
  }

  if (this.endedCallback !== undefined) {
    audio.addEventListener('ended', this.endedCallback, false);
  }

  audio.className = 'h5p-audio';
  audio.controls = this.params.controls === undefined ? true : this.params.controls;

  // Menu removed, because it's cut off if audio is used as H5P.Question intro
  const controlsList = 'nodownload noplaybackrate';
  audio.setAttribute('controlsList', controlsList);

  audio.preload = 'auto';
  audio.style.display = 'block';

  if (this.params.fitToWrapper === undefined || this.params.fitToWrapper) {
    audio.style.width = '100%';
    if (!this.isRoot()) {
      // Only set height if this isn't a root
      audio.style.height = '100%';
    }
  }

  this.audio = audio;

  if (this.params.playerMode === 'minimalistic') {
    audio.controls = false;
    this.addMinimalAudioPlayer($wrapper, false);
  }
  else if (this.params.playerMode === 'transparent') {
    audio.controls = false;
    this.addMinimalAudioPlayer($wrapper, true);
  }
  else {
    $wrapper.html(audio);
  }

  if (audio.controls) {
    $wrapper.addClass('h5p-audio-controls');
  }

  // Set time to saved time from previous run
  if (this.oldTime) {
    if (this.$audioButton) {
      this.$audioButton.attr('class', H5P.Audio.MINIMAL_BUTTON_PAUSED);
    }
    this.seekTo(this.oldTime);
  }

  // Avoid autoplaying in authoring tool
  if (window.H5PEditor === undefined) {
    // Keep record of autopauses.
    // I.e: we don't wanna autoplay if the user has excplicitly paused.
    self.autoPaused = true;

    // Set up intersection observer
    new IntersectionObserver(function (entries) {
      const entry = entries[0];

      if (entry.intersectionRatio == 0) {
        if (!self.audio.paused) {
          // Audio element is hidden, pause it
          self.autoPaused = true;
          self.audio.pause();
        }
      }
      else if (self.params.autoplay && self.autoPaused) {
        // Audio element is visible. Autoplay if autoplay is enabled and it was
        // not explicitly paused by a user
        self.autoPaused = false;
        self.play();
      }
    }, {
      root: document.documentElement,
      threshold: [0, 1] // Get events when it is shown and hidden
    }).observe($wrapper.get(0));
  }
};

/**
 * Attaches not supported message.
 *
 * @param {jQuery} $wrapper Our dear container.
 */
H5P.Audio.prototype.attachNotSupportedMessage = function ($wrapper) {
  $wrapper.addClass('h5p-audio-not-supported');
  $wrapper.html(
    '<div class="h5p-audio-inner">' +
      '<div class="h5p-audio-not-supported-icon"><span/></div>' +
      '<span>' + this.params.audioNotSupported + '</span>' +
    '</div>'
  );

  if (this.endedCallback !== undefined) {
    this.endedCallback();
  }
}

/**
 * Stop & reset playback.
 *
 * @returns {undefined}
 */
H5P.Audio.prototype.resetTask = function () {
  this.stop();
  this.seekTo(0);
  if (this.$audioButton) {
    this.$audioButton.attr('class', H5P.Audio.MINIMAL_BUTTON);
  }
};

/**
 * Stop the audio. TODO: Rename to pause?
 *
 * @returns {undefined}
 */
H5P.Audio.prototype.stop = function () {
  if (this.audio !== undefined) {
    this.audio.pause();
  }
};

/**
 * Play
 */
H5P.Audio.prototype.play = function () {
  if (this.audio !== undefined) {
    // play() returns a Promise that can fail, e.g. while autoplaying
    this.audio.play().catch((error) => {
      console.warn(error);
    });
  }
};

/**
 * @public
 * Pauses the audio.
 */
H5P.Audio.prototype.pause = function () {
  if (this.audio !== undefined) {
    this.audio.pause();
  }
};

/**
 * @public
 * Seek to audio position.
 *
 * @param {number} seekTo Time to seek to in seconds.
 */
H5P.Audio.prototype.seekTo = function (seekTo) {
  if (this.audio !== undefined) {
    this.audio.currentTime = seekTo;
  }
};

/**
 * @public
 * Get current state for resetting it later.
 *
 * @returns {object} Current state.
 */
H5P.Audio.prototype.getCurrentState = function () {
  if (this.audio !== undefined && this.audio.currentTime > 0) {
    const currentTime = this.audio.ended ? 0 : this.audio.currentTime;
    return {
      currentTime: currentTime
    };
  }
};

/**
 * @public
 * Disable button.
 * Not using disabled attribute to block button activation, because it will
 * implicitly set tabindex = -1 and confuse ChromeVox navigation. Clicks handled
 * using "pointer-events: none" in CSS.
 */
H5P.Audio.prototype.disableToggleButton = function () {
  this.toggleButtonEnabled = false;
  if (this.$audioButton) {
    this.$audioButton.addClass(H5P.Audio.BUTTON_DISABLED);
  }
};

/**
 * @public
 * Enable button.
 */
H5P.Audio.prototype.enableToggleButton = function () {
  this.toggleButtonEnabled = true;
  if (this.$audioButton) {
    this.$audioButton.removeClass(H5P.Audio.BUTTON_DISABLED);
  }
};

/**
 * @public
 * Check if button is enabled.
 * @return {boolean} True, if button is enabled. Else false.
 */
H5P.Audio.prototype.isEnabledToggleButton = function () {
  return this.toggleButtonEnabled;
};

/** @constant {string} */
H5P.Audio.BUTTON_DISABLED = 'h5p-audio-disabled';
;var H5P = H5P || {};

/**
 * H5P-Text Utilities
 *
 * Some functions that can be useful when dealing with texts in H5P.
 *
 * @param {H5P.jQuery} $
 */
H5P.TextUtilities = function () {
  'use strict';
  /**
   * Create Text Utilities.
   *
   * Might be needed later.
   *
   * @constructor
   */
  function TextUtilities () {
  }

  // Inheritance
  TextUtilities.prototype = Object.create(H5P.EventDispatcher.prototype);
  TextUtilities.prototype.constructor = TextUtilities;

  /** @constant {object} */
  TextUtilities.WORD_DELIMITER = /[\s.?!,\';\"]/g;

  /**
   * Check if a candidate string is considered isolated (in a larger string) by
   * checking the symbol before and after the candidate.
   *
   * @param {string} candidate - String to be looked for.
   * @param {string} text - (Larger) string that should contain candidate.
   * @param {object} params - Parameters.
   * @param {object} params.delimiter - Regular expression containing symbols used to isolate the candidate.
   * @return {boolean} True if string is isolated.
   */
  TextUtilities.isIsolated = function (candidate, text, params) {
    // Sanitization
    if (!candidate || !text) {
      return;
    }
    var delimiter = (!!params && !!params.delimiter) ? params.delimiter : TextUtilities.WORD_DELIMITER;

    var pos = (!!params && !!params.index && typeof params.index === 'number') ? params.index : text.indexOf(candidate);
    if (pos < 0 || pos > text.length-1) {
      return false;
    }

    var pred = (pos === 0 ? '' : text[pos - 1].replace(delimiter, ''));
    var succ = (pos + candidate.length === text.length ? '' : text[pos + candidate.length].replace(delimiter, ''));

    if (pred !== '' || succ !== '') {
      return false;
    }
    return true;
  };

  /**
   * Check whether two strings are considered to be similar.
   * The similarity is temporarily computed by word length and number of number of operations
   * required to change one word into the other (Damerau-Levenshtein). It's subject to
   * change, cmp. https://github.com/otacke/udacity-machine-learning-engineer/blob/master/submissions/capstone_proposals/h5p_fuzzy_blanks.md
   *
   * @param {String} string1 - String #1.
   * @param {String} string2 - String #2.
   * @param {object} params - Parameters.
   * @return {boolean} True, if strings are considered to be similar.
   */
  TextUtilities.areSimilar = function (string1, string2) {
    // Sanitization
    if (!string1 || typeof string1 !== 'string') {
      return;
    }
    if (!string2 || typeof string2 !== 'string') {
      return;
    }

    // Just temporariliy this unflexible. Will be configurable via params.
    var length = Math.min(string1.length, string2.length);
    var levenshtein = H5P.TextUtilities.computeLevenshteinDistance(string1, string2, true);
    if (levenshtein === 0) {
      return true;
    }
    if ((length > 9) && (levenshtein <= 2)) {
      return true;
    }
    if ((length > 3) && (levenshtein <= 1)) {
      return true;
    }
    return false;
  };

  /**
   * Compute the (Damerau-)Levenshtein distance for two strings.
   *
   * The (Damerau-)Levenshtein distance that is returned is equivalent to the
   * number of operations that are necessary to transform one string into the
   * other. Consequently, lower numbers indicate higher similarity between the
   * two strings.
   *
   * While the Levenshtein distance counts deletions, insertions and mismatches,
   * the Damerau-Levenshtein distance also counts swapping two characters as
   * only one operation (instead of two mismatches), because this seems to
   * happen quite often.
   *
   * See http://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance for details
   *
   * @public
   * @param {string} str1 - String no. 1.
   * @param {string} str2 - String no. 2.
   * @param {boolean} [countSwapping=false] - If true, swapping chars will count as operation.
   * @returns {number} Distance.
   */
  TextUtilities.computeLevenshteinDistance = function(str1, str2, countSwapping) {
    // sanity checks
    if (typeof str1 !== 'string' || typeof str2 !== 'string') {
      return undefined;
    }
    if (countSwapping && typeof countSwapping !== 'boolean') {
      countSwapping = false;
    }

    // degenerate cases
    if (str1 === str2) {
      return 0;
    }
    if (str1.length === 0) {
      return str2.length;
    }
    if (str2.length === 0) {
      return str1.length;
    }

    // counter variables
    var i, j;

    // indicates characters that don't match
    var cost;

    // matrix for storing distances
    var distance = [];

    // initialization
    for (i = 0; i <= str1.length; i++) {
      distance[i] = [i];
    }
    for (j = 0; j <= str2.length; j++) {
      distance[0][j] = j;
    }

    // computation
    for (i = 1; i <= str1.length; i++) {
      for (j = 1; j <= str2.length; j++) {
        cost = (str1[i-1] === str2[j-1]) ? 0 : 1;
        distance[i][j] = Math.min(
          distance[i-1][j] + 1,     // deletion
          distance[i][j-1] + 1,     // insertion
          distance[i-1][j-1] + cost // mismatch
        );
        // in Damerau-Levenshtein distance, transpositions are operations
        if (countSwapping) {
          if (i > 1 && j > 1 && str1[i-1] === str2[j-2] && str1[i-2] === str2[j-1]) {
            distance[i][j] = Math.min(distance[i][j], distance[i-2][j-2] + cost);
          }
        }
      }
    }
    return distance[str1.length][str2.length];
  };

  /**
   * Compute the Jaro(-Winkler) distance for two strings.
   *
   * The Jaro(-Winkler) distance will return a value between 0 and 1 indicating
   * the similarity of two strings. The higher the value, the more similar the
   * strings are.
   *
   * See https://en.wikipedia.org/wiki/Jaro%E2%80%93Winkler_distance for details
   *
   * It seems that a more generalized implementation of Winkler's modification
   * can improve the results. This might be implemented later.
   * http://disi.unitn.it/~p2p/RelatedWork/Matching/Hermans_bnaic-2012.pdf
   *
   * @public
   * @param {string} str1 - String no. 1.
   * @param {string} str2 - String no. 2.
   * @param {boolean} [favorSameStart=false] - If true, strings with same start get higher distance value.
   * @param {boolean} [longTolerance=false] - If true, Winkler's tolerance for long words will be used.
   * @returns {number} Distance.
   */
  TextUtilities.computeJaroDistance = function(str1, str2, favorSameStart, longTolerance) {
    // sanity checks
    if (typeof str1 !== 'string' || typeof str2 !== 'string') {
      return undefined;
    }
    if (favorSameStart && typeof favorSameStart !== 'boolean') {
      favorSameStart = false;
    }
    if (longTolerance && typeof longTolerance !== 'boolean') {
      longTolerance = false;
    }

    // degenerate cases
    if (str1.length === 0 || str2.length === 0) {
      return 0;
    }
    if (str1 === str2) {
      return 1;
    }

    // counter variables
    var i, j, k;

    // number of matches between both strings
    var matches = 0;

    // number of transpositions between both strings
    var transpositions = 0;

    // The Jaro-Winkler distance
    var distance = 0;

    // length of common prefix up to 4 chars
    var l = 0;

    // scaling factor, should not exceed 0.25 (Winkler default = 0.1)
    var p = 0.1;

    // will be used often
    var str1Len = str1.length;
    var str2Len = str2.length;

    // determines the distance that still counts as a match
    var matchWindow = Math.floor(Math.max(str1Len, str2Len) / 2)- 1;

    // will store matches
    var str1Flags = new Array(str1Len);
    var str2Flags = new Array(str2Len);

    // count matches
    for (i = 0; i < str1Len; i++) {
      var start  = (i >= matchWindow) ? i - matchWindow : 0;
      var end = (i + matchWindow <= (str2Len - 1)) ? (i + matchWindow) : (str2Len - 1);

      for (j = start; j <= end; j++) {
        if (str1Flags[i] !== true && str2Flags[j] !== true && str1[i] === str2[j]) {
          str1Flags[i] = str2Flags[j] = true;
          matches += 1;
          break;
        }
      }
    }
    if (matches === 0) {
      return 0;
    }

    // count transpositions
    k = 0;
    for (i = 0; i < str1Len; i++) {
      if (!str1Flags[i]) {
        continue;
      }
      while (!str2Flags[k]) {
        k += 1;
      }
      if (str1[i] !== str2[k]) {
        transpositions += 1;
      }
      k += 1;
    }
    transpositions = transpositions / 2;

    // compute Jaro distance
    distance = (matches/str1Len + matches/str2Len + (matches - transpositions) / matches) / 3;

    // modification used by Winkler
    if (favorSameStart) {
      if (distance > 0.7 && str1Len > 3 && str2Len > 3) {
        while (str1[l] === str2[l] && l < 4) {
          l += 1;
        }
        distance = distance + l * p * (1 - distance);

        // modification for long words
        if (longTolerance) {
          if (Math.max(str1Len, str2Len) > 4 && matches > l + 1 && 2 * matches >= Math.max(str1Len, str2Len) + l) {
            distance += ((1.0 - distance) * ((matches - l - 1) / (str1Len + str2Len - 2 * l + 2)));
          }
        }
      }
    }

    return distance;
  };


  /**
   * Check whether a text contains a string, but fuzzy.
   *
   * This function is naive. It moves a window of needle's length (+2)
   * over the haystack's text and each move compares for similarity using
   * a given string metric. This will be slow for long texts!!!
   *
   * TODO: You might want to look into the bitap algorithm or experiment
   *       with regexps
   *
   * @param {String} needle - String to look for.
   * @param {String} haystack - Text to look in.
   */
  TextUtilities.fuzzyContains = function (needle, haystack) {
    return this.fuzzyFind(needle, haystack).contains;
  };

  /**
   * Find the first position of a fuzzy string within a text
   * @param {String} needle - String to look for.
   * @param {String} haystack - Text to look in.
   */
  TextUtilities.fuzzyIndexOf = function (needle, haystack) {
    return this.fuzzyFind(needle, haystack).indexOf;
  };

  /**
   * Find the first fuzzy match of a string within a text
   * @param {String} needle - String to look for.
   * @param {String} haystack - Text to look in.
   */
  TextUtilities.fuzzyMatch = function (needle, haystack) {
    return this.fuzzyFind(needle, haystack).match;
  };

  /**
   * Find a fuzzy string with in a text.
   * TODO: This could be cleaned ...
   * @param {String} needle - String to look for.
   * @param {String} haystack - Text to look in.
   * @param {object} params - Parameters.
   */
  TextUtilities.fuzzyFind = function (needle, haystack, params) {
    // Sanitization
    if (!needle || typeof needle !== 'string') {
      return false;
    }
    if (!haystack || typeof haystack !== 'string') {
      return false;
    }
    if (params === undefined || params.windowSize === undefined || typeof params.windowSize !== 'number') {
      params = {'windowSize': 3};
    }

    var match;

    var found = haystack.split(' ').some(function(hay) {
      match = hay;
      return H5P.TextUtilities.areSimilar(needle, hay);
    });
    if (found) {
      return {'contains' : found, 'match': match, 'index': haystack.indexOf(match)};
    }

    // This is not used for single words but for phrases
    for (var i = 0; i < haystack.length - needle.length + 1; i++) {
      var hay = [];
      for (var j = 0; j < params.windowSize; j++) {
        hay[j] = haystack.substr(i, needle.length + j);
      }

      // Checking isIsolated will e.g. prevent finding beginnings of words
      for (var j = 0; j < hay.length; j++) {
        if (TextUtilities.isIsolated(hay[j], haystack) && TextUtilities.areSimilar(hay[j], needle)) {
          match = hay[j];
          found = true;
          break;
        }
      }
      if (found) {
        break;
      }
    }
    if (!found) {
      match = undefined;
    }
    return {'contains' : found, 'match': match, 'index': haystack.indexOf(match)};
  };

  return TextUtilities;
}();
;var H5P = H5P || {};

/**
 * Class responsible for creating a help text dialog
 */
H5P.JoubelHelpTextDialog = (function ($) {

  var numInstances = 0;
  /**
   * Display a pop-up containing a message.
   *
   * @param {H5P.jQuery}  $container  The container which message dialog will be appended to
   * @param {string}      message     The message
   * @param {string}      closeButtonTitle The title for the close button
   * @return {H5P.jQuery}
   */
  function JoubelHelpTextDialog(header, message, closeButtonTitle) {
    H5P.EventDispatcher.call(this);

    var self = this;

    numInstances++;
    var headerId = 'joubel-help-text-header-' + numInstances;
    var helpTextId = 'joubel-help-text-body-' + numInstances;

    var $helpTextDialogBox = $('<div>', {
      'class': 'joubel-help-text-dialog-box',
      'role': 'dialog',
      'aria-labelledby': headerId,
      'aria-describedby': helpTextId
    });

    $('<div>', {
      'class': 'joubel-help-text-dialog-background'
    }).appendTo($helpTextDialogBox);

    var $helpTextDialogContainer = $('<div>', {
      'class': 'joubel-help-text-dialog-container'
    }).appendTo($helpTextDialogBox);

    $('<div>', {
      'class': 'joubel-help-text-header',
      'id': headerId,
      'role': 'header',
      'html': header
    }).appendTo($helpTextDialogContainer);

    $('<div>', {
      'class': 'joubel-help-text-body',
      'id': helpTextId,
      'html': message,
      'role': 'document',
      'tabindex': 0
    }).appendTo($helpTextDialogContainer);

    var handleClose = function () {
      $helpTextDialogBox.remove();
      self.trigger('closed');
    };

    var $closeButton = $('<div>', {
      'class': 'joubel-help-text-remove',
      'role': 'button',
      'title': closeButtonTitle,
      'tabindex': 1,
      'click': handleClose,
      'keydown': function (event) {
        // 32 - space, 13 - enter
        if ([32, 13].indexOf(event.which) !== -1) {
          event.preventDefault();
          handleClose();
        }
      }
    }).appendTo($helpTextDialogContainer);

    /**
     * Get the DOM element
     * @return {HTMLElement}
     */
    self.getElement = function () {
      return $helpTextDialogBox;
    };

    self.focus = function () {
      $closeButton.focus();
    };
  }

  JoubelHelpTextDialog.prototype = Object.create(H5P.EventDispatcher.prototype);
  JoubelHelpTextDialog.prototype.constructor = JoubelHelpTextDialog;

  return JoubelHelpTextDialog;
}(H5P.jQuery));
;var H5P = H5P || {};

/**
 * Class responsible for creating auto-disappearing dialogs
 */
H5P.JoubelMessageDialog = (function ($) {

  /**
   * Display a pop-up containing a message.
   *
   * @param {H5P.jQuery} $container The container which message dialog will be appended to
   * @param {string} message The message
   * @return {H5P.jQuery}
   */
  function JoubelMessageDialog ($container, message) {
    var timeout;

    var removeDialog = function () {
      $warning.remove();
      clearTimeout(timeout);
      $container.off('click.messageDialog');
    };

    // Create warning popup:
    var $warning = $('<div/>', {
      'class': 'joubel-message-dialog',
      text: message
    }).appendTo($container);

    // Remove after 3 seconds or if user clicks anywhere in $container:
    timeout = setTimeout(removeDialog, 3000);
    $container.on('click.messageDialog', removeDialog);

    return $warning;
  }

  return JoubelMessageDialog;
})(H5P.jQuery);
;var H5P = H5P || {};

/**
 * Class responsible for creating a circular progress bar
 */

H5P.JoubelProgressCircle = (function ($) {

  /**
   * Constructor for the Progress Circle
   *
   * @param {Number} number The amount of progress to display
   * @param {string} progressColor Color for the progress meter
   * @param {string} backgroundColor Color behind the progress meter
   */
  function ProgressCircle(number, progressColor, fillColor, backgroundColor) {
    progressColor = progressColor || '#1a73d9';
    fillColor = fillColor || '#f0f0f0';
    backgroundColor = backgroundColor || '#ffffff';
    var progressColorRGB = this.hexToRgb(progressColor);

    //Verify number
    try {
      number = Number(number);
      if (number === '') {
        throw 'is empty';
      }
      if (isNaN(number)) {
        throw 'is not a number';
      }
    } catch (e) {
      number = 'err';
    }

    //Draw circle
    if (number > 100) {
      number = 100;
    }

    // We can not use rgba, since they will stack on top of each other.
    // Instead we create the equivalent of the rgba color
    // and applies this to the activeborder and background color.
    var progressColorString = 'rgb(' + parseInt(progressColorRGB.r, 10) +
      ',' + parseInt(progressColorRGB.g, 10) +
      ',' + parseInt(progressColorRGB.b, 10) + ')';

    // Circle wrapper
    var $wrapper = $('<div/>', {
      'class': "joubel-progress-circle-wrapper"
    });

    //Active border indicates progress
    var $activeBorder = $('<div/>', {
      'class': "joubel-progress-circle-active-border"
    }).appendTo($wrapper);

    //Background circle
    var $backgroundCircle = $('<div/>', {
      'class': "joubel-progress-circle-circle"
    }).appendTo($activeBorder);

    //Progress text/number
    $('<span/>', {
      'text': number + '%',
      'class': "joubel-progress-circle-percentage"
    }).appendTo($backgroundCircle);

    var deg = number * 3.6;
    if (deg <= 180) {
      $activeBorder.css('background-image',
        'linear-gradient(' + (90 + deg) + 'deg, transparent 50%, ' + fillColor + ' 50%),' +
        'linear-gradient(90deg, ' + fillColor + ' 50%, transparent 50%)')
        .css('border', '2px solid' + backgroundColor)
        .css('background-color', progressColorString);
    } else {
      $activeBorder.css('background-image',
        'linear-gradient(' + (deg - 90) + 'deg, transparent 50%, ' + progressColorString + ' 50%),' +
        'linear-gradient(90deg, ' + fillColor + ' 50%, transparent 50%)')
        .css('border', '2px solid' + backgroundColor)
        .css('background-color', progressColorString);
    }

    this.$activeBorder = $activeBorder;
    this.$backgroundCircle = $backgroundCircle;
    this.$wrapper = $wrapper;

    this.initResizeFunctionality();

    return $wrapper;
  }

  /**
   * Initializes resize functionality for the progress circle
   */
  ProgressCircle.prototype.initResizeFunctionality = function () {
    var self = this;

    $(window).resize(function () {
      // Queue resize
      setTimeout(function () {
        self.resize();
      });
    });

    // First resize
    setTimeout(function () {
      self.resize();
    }, 0);
  };

  /**
   * Resize function makes progress circle grow or shrink relative to parent container
   */
  ProgressCircle.prototype.resize = function () {
    var $parent = this.$wrapper.parent();

    if ($parent !== undefined && $parent) {

      // Measurements
      var fontSize = parseInt($parent.css('font-size'), 10);

      // Static sizes
      var fontSizeMultiplum = 3.75;
      var progressCircleWidthPx = parseInt((fontSize / 4.5), 10) % 2 === 0 ? parseInt((fontSize / 4.5), 10) + 4 : parseInt((fontSize / 4.5), 10) + 5;
      var progressCircleOffset = progressCircleWidthPx / 2;

      var width = fontSize * fontSizeMultiplum;
      var height = fontSize * fontSizeMultiplum;
      this.$activeBorder.css({
        'width': width,
        'height': height
      });

      this.$backgroundCircle.css({
        'width': width - progressCircleWidthPx,
        'height': height - progressCircleWidthPx,
        'top': progressCircleOffset,
        'left': progressCircleOffset
      });
    }
  };

  /**
   * Hex to RGB conversion
   * @param hex
   * @returns {{r: Number, g: Number, b: Number}}
   */
  ProgressCircle.prototype.hexToRgb = function (hex) {
    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result ? {
      r: parseInt(result[1], 16),
      g: parseInt(result[2], 16),
      b: parseInt(result[3], 16)
    } : null;
  };

  return ProgressCircle;

}(H5P.jQuery));
;var H5P = H5P || {};

H5P.SimpleRoundedButton = (function ($) {

  /**
   * Creates a new tip
   */
  function SimpleRoundedButton(text) {

    var $simpleRoundedButton = $('<div>', {
      'class': 'joubel-simple-rounded-button',
      'title': text,
      'role': 'button',
      'tabindex': '0'
    }).keydown(function (e) {
      // 32 - space, 13 - enter
      if ([32, 13].indexOf(e.which) !== -1) {
        $(this).click();
        e.preventDefault();
      }
    });

    $('<span>', {
      'class': 'joubel-simple-rounded-button-text',
      'html': text
    }).appendTo($simpleRoundedButton);

    return $simpleRoundedButton;
  }

  return SimpleRoundedButton;
}(H5P.jQuery));
;var H5P = H5P || {};

/**
 * Class responsible for creating speech bubbles
 */
H5P.JoubelSpeechBubble = (function ($) {

  var $currentSpeechBubble;
  var $currentContainer;  
  var $tail;
  var $innerTail;
  var removeSpeechBubbleTimeout;
  var currentMaxWidth;

  var DEFAULT_MAX_WIDTH = 400;

  var iDevice = navigator.userAgent.match(/iPod|iPhone|iPad/g) ? true : false;

  /**
   * Creates a new speech bubble
   *
   * @param {H5P.jQuery} $container The speaking object
   * @param {string} text The text to display
   * @param {number} maxWidth The maximum width of the bubble
   * @return {H5P.JoubelSpeechBubble}
   */
  function JoubelSpeechBubble($container, text, maxWidth) {
    maxWidth = maxWidth || DEFAULT_MAX_WIDTH;
    currentMaxWidth = maxWidth;
    $currentContainer = $container;

    this.isCurrent = function ($tip) {
      return $tip.is($currentContainer);
    };

    this.remove = function () {
      remove();
    };

    var fadeOutSpeechBubble = function ($speechBubble) {
      if (!$speechBubble) {
        return;
      }

      // Stop removing bubble
      clearTimeout(removeSpeechBubbleTimeout);

      $speechBubble.removeClass('show');
      setTimeout(function () {
        if ($speechBubble) {
          $speechBubble.remove();
          $speechBubble = undefined;
        }
      }, 500);
    };

    if ($currentSpeechBubble !== undefined) {
      remove();
    }

    var $h5pContainer = getH5PContainer($container);

    // Make sure we fade out old speech bubble
    fadeOutSpeechBubble($currentSpeechBubble);

    // Create bubble
    $tail = $('<div class="joubel-speech-bubble-tail"></div>');
    $innerTail = $('<div class="joubel-speech-bubble-inner-tail"></div>');
    var $innerBubble = $(
      '<div class="joubel-speech-bubble-inner">' +
      '<div class="joubel-speech-bubble-text">' + text + '</div>' +
      '</div>'
    ).prepend($innerTail);

    $currentSpeechBubble = $(
      '<div class="joubel-speech-bubble" aria-live="assertive">'
    ).append([$tail, $innerBubble])
      .appendTo($h5pContainer);

    // Show speech bubble with transition
    setTimeout(function () {
      $currentSpeechBubble.addClass('show');
    }, 0);

    position($currentSpeechBubble, $currentContainer, maxWidth, $tail, $innerTail);

    // Handle click to close
    H5P.$body.on('mousedown.speechBubble', handleOutsideClick);

    // Handle window resizing
    H5P.$window.on('resize', '', handleResize);

    // Handle clicks when inside IV which blocks bubbling.
    $container.parents('.h5p-dialog')
      .on('mousedown.speechBubble', handleOutsideClick);

    if (iDevice) {
      H5P.$body.css('cursor', 'pointer');
    }

    return this;
  }

  // Remove speechbubble if it belongs to a dom element that is about to be hidden
  H5P.externalDispatcher.on('domHidden', function (event) {
    if ($currentSpeechBubble !== undefined && event.data.$dom.find($currentContainer).length !== 0) {
      remove();
    }
  });

  /**
   * Returns the closest h5p container for the given DOM element.
   * 
   * @param {object} $container jquery element
   * @return {object} the h5p container (jquery element)
   */
  function getH5PContainer($container) {
    var $h5pContainer = $container.closest('.h5p-frame');

    // Check closest h5p frame first, then check for container in case there is no frame.
    if (!$h5pContainer.length) {
      $h5pContainer = $container.closest('.h5p-container');
    }

    return $h5pContainer;
  }

  /**
   * Event handler that is called when the window is resized.
   */
  function handleResize() {
    position($currentSpeechBubble, $currentContainer, currentMaxWidth, $tail, $innerTail);
  }

  /**
   * Repositions the speech bubble according to the position of the container.
   * 
   * @param {object} $currentSpeechbubble the speech bubble that should be positioned   
   * @param {object} $container the container to which the speech bubble should point 
   * @param {number} maxWidth the maximum width of the speech bubble
   * @param {object} $tail the tail (the triangle that points to the referenced container)
   * @param {object} $innerTail the inner tail (the triangle that points to the referenced container)
   */
  function position($currentSpeechBubble, $container, maxWidth, $tail, $innerTail) {
    var $h5pContainer = getH5PContainer($container);

    // Calculate offset between the button and the h5p frame
    var offset = getOffsetBetween($h5pContainer, $container);

    var direction = (offset.bottom > offset.top ? 'bottom' : 'top');
    var tipWidth = offset.outerWidth * 0.9; // Var needs to be renamed to make sense
    var bubbleWidth = tipWidth > maxWidth ? maxWidth : tipWidth;

    var bubblePosition = getBubblePosition(bubbleWidth, offset);
    var tailPosition = getTailPosition(bubbleWidth, bubblePosition, offset, $container.width());
    // Need to set font-size, since element is appended to body.
    // Using same font-size as parent. In that way it will grow accordingly
    // when resizing
    var fontSize = 16;//parseFloat($parent.css('font-size'));

    // Set width and position of speech bubble
    $currentSpeechBubble.css(bubbleCSS(
      direction,
      bubbleWidth,
      bubblePosition,
      fontSize
    ));

    var preparedTailCSS = tailCSS(direction, tailPosition);
    $tail.css(preparedTailCSS);
    $innerTail.css(preparedTailCSS);
  }

  /**
   * Static function for removing the speechbubble
   */
  var remove = function () {
    H5P.$body.off('mousedown.speechBubble');
    H5P.$window.off('resize', '', handleResize);
    $currentContainer.parents('.h5p-dialog').off('mousedown.speechBubble');
    if (iDevice) {
      H5P.$body.css('cursor', '');
    }
    if ($currentSpeechBubble !== undefined) {
      // Apply transition, then remove speech bubble
      $currentSpeechBubble.removeClass('show');

      // Make sure we remove any old timeout before reassignment
      clearTimeout(removeSpeechBubbleTimeout);
      removeSpeechBubbleTimeout = setTimeout(function () {
        $currentSpeechBubble.remove();
        $currentSpeechBubble = undefined;
      }, 500);
    }
    // Don't return false here. If the user e.g. clicks a button when the bubble is visible,
    // we want the bubble to disapear AND the button to receive the event
  };

  /**
   * Remove the speech bubble and container reference
   */
  function handleOutsideClick(event) {
    if (event.target === $currentContainer[0]) {
      return; // Button clicks are not outside clicks
    }

    remove();
    // There is no current container when a container isn't clicked
    $currentContainer = undefined;
  }

  /**
   * Calculate position for speech bubble
   *
   * @param {number} bubbleWidth The width of the speech bubble
   * @param {object} offset
   * @return {object} Return position for the speech bubble
   */
  function getBubblePosition(bubbleWidth, offset) {
    var bubblePosition = {};

    var tailOffset = 9;
    var widthOffset = bubbleWidth / 2;

    // Calculate top position
    bubblePosition.top = offset.top + offset.innerHeight;

    // Calculate bottom position
    bubblePosition.bottom = offset.bottom + offset.innerHeight + tailOffset;

    // Calculate left position
    if (offset.left < widthOffset) {
      bubblePosition.left = 3;
    }
    else if ((offset.left + widthOffset) > offset.outerWidth) {
      bubblePosition.left = offset.outerWidth - bubbleWidth - 3;
    }
    else {
      bubblePosition.left = offset.left - widthOffset + (offset.innerWidth / 2);
    }

    return bubblePosition;
  }

  /**
   * Calculate position for speech bubble tail
   *
   * @param {number} bubbleWidth The width of the speech bubble
   * @param {object} bubblePosition Speech bubble position
   * @param {object} offset
   * @param {number} iconWidth The width of the tip icon
   * @return {object} Return position for the tail
   */
  function getTailPosition(bubbleWidth, bubblePosition, offset, iconWidth) {
    var tailPosition = {};
    // Magic numbers. Tuned by hand so that the tail fits visually within
    // the bounds of the speech bubble.
    var leftBoundary = 9;
    var rightBoundary = bubbleWidth - 20;

    tailPosition.left = offset.left - bubblePosition.left + (iconWidth / 2) - 6;
    if (tailPosition.left < leftBoundary) {
      tailPosition.left = leftBoundary;
    }
    if (tailPosition.left > rightBoundary) {
      tailPosition.left = rightBoundary;
    }

    tailPosition.top = -6;
    tailPosition.bottom = -6;

    return tailPosition;
  }

  /**
   * Return bubble CSS for the desired growth direction
   *
   * @param {string} direction The direction the speech bubble will grow
   * @param {number} width The width of the speech bubble
   * @param {object} position Speech bubble position
   * @param {number} fontSize The size of the bubbles font
   * @return {object} Return CSS
   */
  function bubbleCSS(direction, width, position, fontSize) {
    if (direction === 'top') {
      return {
        width: width + 'px',
        bottom: position.bottom + 'px',
        left: position.left + 'px',
        fontSize: fontSize + 'px',
        top: ''
      };
    }
    else {
      return {
        width: width + 'px',
        top: position.top + 'px',
        left: position.left + 'px',
        fontSize: fontSize + 'px',
        bottom: ''
      };
    }
  }

  /**
   * Return tail CSS for the desired growth direction
   *
   * @param {string} direction The direction the speech bubble will grow
   * @param {object} position Tail position
   * @return {object} Return CSS
   */
  function tailCSS(direction, position) {
    if (direction === 'top') {
      return {
        bottom: position.bottom + 'px',
        left: position.left + 'px',
        top: ''
      };
    }
    else {
      return {
        top: position.top + 'px',
        left: position.left + 'px',
        bottom: ''
      };
    }
  }

  /**
   * Calculates the offset between an element inside a container and the
   * container. Only works if all the edges of the inner element are inside the
   * outer element.
   * Width/height of the elements is included as a convenience.
   *
   * @param {H5P.jQuery} $outer
   * @param {H5P.jQuery} $inner
   * @return {object} Position offset
   */
  function getOffsetBetween($outer, $inner) {
    var outer = $outer[0].getBoundingClientRect();
    var inner = $inner[0].getBoundingClientRect();

    return {
      top: inner.top - outer.top,
      right: outer.right - inner.right,
      bottom: outer.bottom - inner.bottom,
      left: inner.left - outer.left,
      innerWidth: inner.width,
      innerHeight: inner.height,
      outerWidth: outer.width,
      outerHeight: outer.height
    };
  }

  return JoubelSpeechBubble;
})(H5P.jQuery);
;var H5P = H5P || {};

H5P.JoubelThrobber = (function ($) {

  /**
   * Creates a new tip
   */
  function JoubelThrobber() {

    // h5p-throbber css is described in core
    var $throbber = $('<div/>', {
      'class': 'h5p-throbber'
    });

    return $throbber;
  }

  return JoubelThrobber;
}(H5P.jQuery));
;H5P.JoubelTip = (function ($) {
  var $conv = $('<div/>');

  /**
   * Creates a new tip element.
   *
   * NOTE that this may look like a class but it doesn't behave like one.
   * It returns a jQuery object.
   *
   * @param {string} tipHtml The text to display in the popup
   * @param {Object} [behaviour] Options
   * @param {string} [behaviour.tipLabel] Set to use a custom label for the tip button (you want this for good A11Y)
   * @param {boolean} [behaviour.helpIcon] Set to 'true' to Add help-icon classname to Tip button (changes the icon)
   * @param {boolean} [behaviour.showSpeechBubble] Set to 'false' to disable functionality (you may this in the editor)
   * @param {boolean} [behaviour.tabcontrol] Set to 'true' if you plan on controlling the tabindex in the parent (tabindex="-1")
   * @return {H5P.jQuery|undefined} Tip button jQuery element or 'undefined' if invalid tip
   */
  function JoubelTip(tipHtml, behaviour) {

    // Keep track of the popup that appears when you click the Tip button
    var speechBubble;

    // Parse tip html to determine text
    var tipText = $conv.html(tipHtml).text().trim();
    if (tipText === '') {
      return; // The tip has no textual content, i.e. it's invalid.
    }

    // Set default behaviour
    behaviour = $.extend({
      tipLabel: tipText,
      helpIcon: false,
      showSpeechBubble: true,
      tabcontrol: false
    }, behaviour);

    // Create Tip button
    var $tipButton = $('<div/>', {
      class: 'joubel-tip-container' + (behaviour.showSpeechBubble ? '' : ' be-quiet'),
      'aria-label': behaviour.tipLabel,
      'aria-expanded': false,
      role: 'button',
      tabindex: (behaviour.tabcontrol ? -1 : 0),
      click: function (event) {
        // Toggle show/hide popup
        toggleSpeechBubble();
        event.preventDefault();
      },
      keydown: function (event) {
        if (event.which === 32 || event.which === 13) { // Space & enter key
          // Toggle show/hide popup
          toggleSpeechBubble();
          event.stopPropagation();
          event.preventDefault();
        }
        else { // Any other key
          // Toggle hide popup
          toggleSpeechBubble(false);
        }
      },
      // Add markup to render icon
      html: '<span class="joubel-icon-tip-normal ' + (behaviour.helpIcon ? ' help-icon': '') + '">' +
              '<span class="h5p-icon-shadow"></span>' +
              '<span class="h5p-icon-speech-bubble"></span>' +
              '<span class="h5p-icon-info"></span>' +
            '</span>'
      // IMPORTANT: All of the markup elements must have 'pointer-events: none;'
    });

    const $tipAnnouncer = $('<div>', {
      'class': 'hidden-but-read',
      'aria-live': 'polite',
      appendTo: $tipButton,
    });

    /**
     * Tip button interaction handler.
     * Toggle show or hide the speech bubble popup when interacting with the
     * Tip button.
     *
     * @private
     * @param {boolean} [force] 'true' shows and 'false' hides.
     */
    var toggleSpeechBubble = function (force) {
      if (speechBubble !== undefined && speechBubble.isCurrent($tipButton)) {
        // Hide current popup
        speechBubble.remove();
        speechBubble = undefined;

        $tipButton.attr('aria-expanded', false);
        $tipAnnouncer.html('');
      }
      else if (force !== false && behaviour.showSpeechBubble) {
        // Create and show new popup
        speechBubble = H5P.JoubelSpeechBubble($tipButton, tipHtml);
        $tipButton.attr('aria-expanded', true);
        $tipAnnouncer.html(tipHtml);
      }
    };

    return $tipButton;
  }

  return JoubelTip;
})(H5P.jQuery);
;var H5P = H5P || {};

H5P.JoubelSlider = (function ($) {

  /**
   * Creates a new Slider
   *
   * @param {object} [params] Additional parameters
   */
  function JoubelSlider(params) {
    H5P.EventDispatcher.call(this);

    this.$slider = $('<div>', $.extend({
      'class': 'h5p-joubel-ui-slider'
    }, params));

    this.$slides = [];
    this.currentIndex = 0;
    this.numSlides = 0;
  }
  JoubelSlider.prototype = Object.create(H5P.EventDispatcher.prototype);
  JoubelSlider.prototype.constructor = JoubelSlider;

  JoubelSlider.prototype.addSlide = function ($content) {
    $content.addClass('h5p-joubel-ui-slide').css({
      'left': (this.numSlides*100) + '%'
    });
    this.$slider.append($content);
    this.$slides.push($content);

    this.numSlides++;

    if(this.numSlides === 1) {
      $content.addClass('current');
    }
  };

  JoubelSlider.prototype.attach = function ($container) {
    $container.append(this.$slider);
  };

  JoubelSlider.prototype.move = function (index) {
    var self = this;

    if(index === 0) {
      self.trigger('first-slide');
    }
    if(index+1 === self.numSlides) {
      self.trigger('last-slide');
    }
    self.trigger('move');

    var $previousSlide = self.$slides[this.currentIndex];
    H5P.Transition.onTransitionEnd(this.$slider, function () {
      $previousSlide.removeClass('current');
      self.trigger('moved');
    });
    this.$slides[index].addClass('current');

    var translateX = 'translateX(' + (-index*100) + '%)';
    this.$slider.css({
      '-webkit-transform': translateX,
      '-moz-transform': translateX,
      '-ms-transform': translateX,
      'transform': translateX
    });

    this.currentIndex = index;
  };

  JoubelSlider.prototype.remove = function () {
    this.$slider.remove();
  };

  JoubelSlider.prototype.next = function () {
    if(this.currentIndex+1 >= this.numSlides) {
      return;
    }

    this.move(this.currentIndex+1);
  };

  JoubelSlider.prototype.previous = function () {
    this.move(this.currentIndex-1);
  };

  JoubelSlider.prototype.first = function () {
    this.move(0);
  };

  JoubelSlider.prototype.last = function () {
    this.move(this.numSlides-1);
  };

  return JoubelSlider;
})(H5P.jQuery);
;var H5P = H5P || {};

/**
 * @module
 */
H5P.JoubelScoreBar = (function ($) {

  /* Need to use an id for the star SVG since that is the only way to reference
     SVG filters  */
  var idCounter = 0;

  /**
   * Creates a score bar
   * @class H5P.JoubelScoreBar
   * @param {number} maxScore  Maximum score
   * @param {string} [label] Makes it easier for readspeakers to identify the scorebar
   * @param {string} [helpText] Score explanation
   * @param {string} [scoreExplanationButtonLabel] Label for score explanation button
   */
  function JoubelScoreBar(maxScore, label, helpText, scoreExplanationButtonLabel) {
    var self = this;

    self.maxScore = maxScore;
    self.score = 0;
    idCounter++;

    /**
     * @const {string}
     */
    self.STAR_MARKUP = '<svg tabindex="-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 63.77 53.87" aria-hidden="true" focusable="false">' +
        '<title>star</title>' +
        '<filter tabindex="-1" id="h5p-joubelui-score-bar-star-inner-shadow-' + idCounter + '" x0="-50%" y0="-50%" width="200%" height="200%">' +
          '<feGaussianBlur in="SourceAlpha" stdDeviation="3" result="blur"></feGaussianBlur>' +
          '<feOffset dy="2" dx="4"></feOffset>' +
          '<feComposite in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="shadowDiff"></feComposite>' +
          '<feFlood flood-color="#ffe95c" flood-opacity="1"></feFlood>' +
          '<feComposite in2="shadowDiff" operator="in"></feComposite>' +
          '<feComposite in2="SourceGraphic" operator="over" result="firstfilter"></feComposite>' +
          '<feGaussianBlur in="firstfilter" stdDeviation="3" result="blur2"></feGaussianBlur>' +
          '<feOffset dy="-2" dx="-4"></feOffset>' +
          '<feComposite in2="firstfilter" operator="arithmetic" k2="-1" k3="1" result="shadowDiff"></feComposite>' +
          '<feFlood flood-color="#ffe95c" flood-opacity="1"></feFlood>' +
          '<feComposite in2="shadowDiff" operator="in"></feComposite>' +
          '<feComposite in2="firstfilter" operator="over"></feComposite>' +
        '</filter>' +
        '<path tabindex="-1" class="h5p-joubelui-score-bar-star-shadow" d="M35.08,43.41V9.16H20.91v0L9.51,10.85,9,10.93C2.8,12.18,0,17,0,21.25a11.22,11.22,0,0,0,3,7.48l8.73,8.53-1.07,6.16Z"/>' +
        '<g tabindex="-1">' +
          '<path tabindex="-1" class="h5p-joubelui-score-bar-star-border" d="M61.36,22.8,49.72,34.11l2.78,16a2.6,2.6,0,0,1,.05.64c0,.85-.37,1.6-1.33,1.6A2.74,2.74,0,0,1,49.94,52L35.58,44.41,21.22,52a2.93,2.93,0,0,1-1.28.37c-.91,0-1.33-.75-1.33-1.6,0-.21.05-.43.05-.64l2.78-16L9.8,22.8A2.57,2.57,0,0,1,9,21.25c0-1,1-1.33,1.81-1.49l16.07-2.35L34.09,2.83c.27-.59.85-1.33,1.55-1.33s1.28.69,1.55,1.33l7.21,14.57,16.07,2.35c.75.11,1.81.53,1.81,1.49A3.07,3.07,0,0,1,61.36,22.8Z"/>' +
          '<path tabindex="-1" class="h5p-joubelui-score-bar-star-fill" d="M61.36,22.8,49.72,34.11l2.78,16a2.6,2.6,0,0,1,.05.64c0,.85-.37,1.6-1.33,1.6A2.74,2.74,0,0,1,49.94,52L35.58,44.41,21.22,52a2.93,2.93,0,0,1-1.28.37c-.91,0-1.33-.75-1.33-1.6,0-.21.05-.43.05-.64l2.78-16L9.8,22.8A2.57,2.57,0,0,1,9,21.25c0-1,1-1.33,1.81-1.49l16.07-2.35L34.09,2.83c.27-.59.85-1.33,1.55-1.33s1.28.69,1.55,1.33l7.21,14.57,16.07,2.35c.75.11,1.81.53,1.81,1.49A3.07,3.07,0,0,1,61.36,22.8Z"/>' +
          '<path tabindex="-1" filter="url(#h5p-joubelui-score-bar-star-inner-shadow-' + idCounter + ')" class="h5p-joubelui-score-bar-star-fill-full-score" d="M61.36,22.8,49.72,34.11l2.78,16a2.6,2.6,0,0,1,.05.64c0,.85-.37,1.6-1.33,1.6A2.74,2.74,0,0,1,49.94,52L35.58,44.41,21.22,52a2.93,2.93,0,0,1-1.28.37c-.91,0-1.33-.75-1.33-1.6,0-.21.05-.43.05-.64l2.78-16L9.8,22.8A2.57,2.57,0,0,1,9,21.25c0-1,1-1.33,1.81-1.49l16.07-2.35L34.09,2.83c.27-.59.85-1.33,1.55-1.33s1.28.69,1.55,1.33l7.21,14.57,16.07,2.35c.75.11,1.81.53,1.81,1.49A3.07,3.07,0,0,1,61.36,22.8Z"/>' +
        '</g>' +
      '</svg>';

    /**
     * @function appendTo
     * @memberOf H5P.JoubelScoreBar#
     * @param {H5P.jQuery}  $wrapper  Dom container
     */
    self.appendTo = function ($wrapper) {
      self.$scoreBar.appendTo($wrapper);
    };

    /**
     * Create the text representation of the scorebar .
     *
     * @private
     * @return {string}
     */
    var createLabel = function (score) {
      if (!label) {
        return '';
      }

      return label.replace(':num', score).replace(':total', self.maxScore);
    };

    /**
     * Creates the html for this widget
     *
     * @method createHtml
     * @private
     */
    var createHtml = function () {
      // Container div
      self.$scoreBar = $('<div>', {
        'class': 'h5p-joubelui-score-bar',
      });

      var $visuals = $('<div>', {
        'class': 'h5p-joubelui-score-bar-visuals',
        appendTo: self.$scoreBar
      });

      // The progress bar wrapper
      self.$progressWrapper = $('<div>', {
        'class': 'h5p-joubelui-score-bar-progress-wrapper',
        appendTo: $visuals
      });

      self.$progress = $('<div>', {
        'class': 'h5p-joubelui-score-bar-progress',
        'html': createLabel(self.score),
        appendTo: self.$progressWrapper
      });

      // The star
      $('<div>', {
        'class': 'h5p-joubelui-score-bar-star',
        html: self.STAR_MARKUP
      }).appendTo($visuals);

      // The score container
      var $numerics = $('<div>', {
        'class': 'h5p-joubelui-score-numeric',
        appendTo: self.$scoreBar,
        'aria-hidden': true
      });

      // The current score
      self.$scoreCounter = $('<span>', {
        'class': 'h5p-joubelui-score-number h5p-joubelui-score-number-counter',
        text: 0,
        appendTo: $numerics
      });

      // The separator
      $('<span>', {
        'class': 'h5p-joubelui-score-number-separator',
        text: '/',
        appendTo: $numerics
      });

      // Max score
      self.$maxScore = $('<span>', {
        'class': 'h5p-joubelui-score-number h5p-joubelui-score-max',
        text: self.maxScore,
        appendTo: $numerics
      });

      if (helpText) {
        H5P.JoubelUI.createTip(helpText, {
          tipLabel: scoreExplanationButtonLabel ? scoreExplanationButtonLabel : helpText,
          helpIcon: true
        }).appendTo(self.$scoreBar);
        self.$scoreBar.addClass('h5p-score-bar-has-help');
      }
    };

    /**
     * Set the current score
     * @method setScore
     * @memberOf H5P.JoubelScoreBar#
     * @param  {number} score
     */
    self.setScore = function (score) {
      // Do nothing if score hasn't changed
      if (score === self.score) {
        return;
      }
      self.score = score > self.maxScore ? self.maxScore : score;
      self.updateVisuals();
    };

    /**
     * Increment score
     * @method incrementScore
     * @memberOf H5P.JoubelScoreBar#
     * @param  {number=}        incrementBy Optional parameter, defaults to 1
     */
    self.incrementScore = function (incrementBy) {
      self.setScore(self.score + (incrementBy || 1));
    };

    /**
     * Set the max score
     * @method setMaxScore
     * @memberOf H5P.JoubelScoreBar#
     * @param  {number}    maxScore The max score
     */
    self.setMaxScore = function (maxScore) {
      self.maxScore = maxScore;
    };

    /**
     * Updates the progressbar visuals
     * @memberOf H5P.JoubelScoreBar#
     * @method updateVisuals
     */
    self.updateVisuals = function () {
      self.$progress.html(createLabel(self.score));
      self.$scoreCounter.text(self.score);
      self.$maxScore.text(self.maxScore);

      setTimeout(function () {
        // Start the progressbar animation
        self.$progress.css({
          width: ((self.score / self.maxScore) * 100) + '%'
        });

        H5P.Transition.onTransitionEnd(self.$progress, function () {
          // If fullscore fill the star and start the animation
          self.$scoreBar.toggleClass('h5p-joubelui-score-bar-full-score', self.score === self.maxScore);
          self.$scoreBar.toggleClass('h5p-joubelui-score-bar-animation-active', self.score === self.maxScore);

          // Only allow the star animation to run once
          self.$scoreBar.one("animationend", function() {
            self.$scoreBar.removeClass("h5p-joubelui-score-bar-animation-active");
          });
        }, 600);
      }, 300);
    };

    /**
     * Removes all classes
     * @method reset
     */
    self.reset = function () {
      self.$scoreBar.removeClass('h5p-joubelui-score-bar-full-score');
    };

    createHtml();
  }

  return JoubelScoreBar;
})(H5P.jQuery);
;var H5P = H5P || {};

H5P.JoubelProgressbar = (function ($) {

  /**
   * Joubel progressbar class
   * @method JoubelProgressbar
   * @constructor
   * @param  {number}          steps Number of steps
   * @param {Object} [options] Additional options
   * @param {boolean} [options.disableAria] Disable readspeaker assistance
   * @param {string} [options.progressText] A progress text for describing
   *  current progress out of total progress for readspeakers.
   *  e.g. "Slide :num of :total"
   */
  function JoubelProgressbar(steps, options) {
    H5P.EventDispatcher.call(this);
    var self = this;
    this.options = $.extend({
      progressText: 'Slide :num of :total'
    }, options);
    this.currentStep = 0;
    this.steps = steps;

    this.$progressbar = $('<div>', {
      'class': 'h5p-joubelui-progressbar'
    });
    this.$background = $('<div>', {
      'class': 'h5p-joubelui-progressbar-background'
    }).appendTo(this.$progressbar);
  }

  JoubelProgressbar.prototype = Object.create(H5P.EventDispatcher.prototype);
  JoubelProgressbar.prototype.constructor = JoubelProgressbar;

  JoubelProgressbar.prototype.updateAria = function () {
    var self = this;
    if (this.options.disableAria) {
      return;
    }

    if (!this.$currentStatus) {
      this.$currentStatus = $('<div>', {
        'class': 'h5p-joubelui-progressbar-slide-status-text',
        'aria-live': 'assertive'
      }).appendTo(this.$progressbar);
    }
    var interpolatedProgressText = self.options.progressText
      .replace(':num', self.currentStep)
      .replace(':total', self.steps);
    this.$currentStatus.html(interpolatedProgressText);
  };

  /**
   * Appends to a container
   * @method appendTo
   * @param  {H5P.jquery} $container
   */
  JoubelProgressbar.prototype.appendTo = function ($container) {
    this.$progressbar.appendTo($container);
  };

  /**
   * Update progress
   * @method setProgress
   * @param  {number}    step
   */
  JoubelProgressbar.prototype.setProgress = function (step) {
    // Check for valid value:
    if (step > this.steps || step < 0) {
      return;
    }
    this.currentStep = step;
    this.$background.css({
      width: ((this.currentStep/this.steps)*100) + '%'
    });

    this.updateAria();
  };

  /**
   * Increment progress with 1
   * @method next
   */
  JoubelProgressbar.prototype.next = function () {
    this.setProgress(this.currentStep+1);
  };

  /**
   * Reset progressbar
   * @method reset
   */
  JoubelProgressbar.prototype.reset = function () {
    this.setProgress(0);
  };

  /**
   * Check if last step is reached
   * @method isLastStep
   * @return {Boolean}
   */
  JoubelProgressbar.prototype.isLastStep = function () {
    return this.steps === this.currentStep;
  };

  return JoubelProgressbar;
})(H5P.jQuery);
;var H5P = H5P || {};

/**
 * H5P Joubel UI library.
 *
 * This is a utility library, which does not implement attach. I.e, it has to bee actively used by
 * other libraries
 * @module
 */
H5P.JoubelUI = (function ($) {

  /**
   * The internal object to return
   * @class H5P.JoubelUI
   * @static
   */
  function JoubelUI() {}

  /* Public static functions */

  /**
   * Create a tip icon
   * @method H5P.JoubelUI.createTip
   * @param  {string}  text   The textual tip
   * @param  {Object}  params Parameters
   * @return {H5P.JoubelTip}
   */
  JoubelUI.createTip = function (text, params) {
    return new H5P.JoubelTip(text, params);
  };

  /**
   * Create message dialog
   * @method H5P.JoubelUI.createMessageDialog
   * @param  {H5P.jQuery}               $container The dom container
   * @param  {string}                   message    The message
   * @return {H5P.JoubelMessageDialog}
   */
  JoubelUI.createMessageDialog = function ($container, message) {
    return new H5P.JoubelMessageDialog($container, message);
  };

  /**
   * Create help text dialog
   * @method H5P.JoubelUI.createHelpTextDialog
   * @param  {string}             header  The textual header
   * @param  {string}             message The textual message
   * @param  {string}             closeButtonTitle The title for the close button
   * @return {H5P.JoubelHelpTextDialog}
   */
  JoubelUI.createHelpTextDialog = function (header, message, closeButtonTitle) {
    return new H5P.JoubelHelpTextDialog(header, message, closeButtonTitle);
  };

  /**
   * Create progress circle
   * @method H5P.JoubelUI.createProgressCircle
   * @param  {number}             number          The progress (0 to 100)
   * @param  {string}             progressColor   The progress color in hex value
   * @param  {string}             fillColor       The fill color in hex value
   * @param  {string}             backgroundColor The background color in hex value
   * @return {H5P.JoubelProgressCircle}
   */
  JoubelUI.createProgressCircle = function (number, progressColor, fillColor, backgroundColor) {
    return new H5P.JoubelProgressCircle(number, progressColor, fillColor, backgroundColor);
  };

  /**
   * Create throbber for loading
   * @method H5P.JoubelUI.createThrobber
   * @return {H5P.JoubelThrobber}
   */
  JoubelUI.createThrobber = function () {
    return new H5P.JoubelThrobber();
  };

  /**
   * Create simple rounded button
   * @method H5P.JoubelUI.createSimpleRoundedButton
   * @param  {string}                  text The button label
   * @return {H5P.SimpleRoundedButton}
   */
  JoubelUI.createSimpleRoundedButton = function (text) {
    return new H5P.SimpleRoundedButton(text);
  };

  /**
   * Create Slider
   * @method H5P.JoubelUI.createSlider
   * @param  {Object} [params] Parameters
   * @return {H5P.JoubelSlider}
   */
  JoubelUI.createSlider = function (params) {
    return new H5P.JoubelSlider(params);
  };

  /**
   * Create Score Bar
   * @method H5P.JoubelUI.createScoreBar
   * @param  {number=}       maxScore The maximum score
   * @param {string} [label] Makes it easier for readspeakers to identify the scorebar
   * @return {H5P.JoubelScoreBar}
   */
  JoubelUI.createScoreBar = function (maxScore, label, helpText, scoreExplanationButtonLabel) {
    return new H5P.JoubelScoreBar(maxScore, label, helpText, scoreExplanationButtonLabel);
  };

  /**
   * Create Progressbar
   * @method H5P.JoubelUI.createProgressbar
   * @param  {number=}       numSteps The total numer of steps
   * @param {Object} [options] Additional options
   * @param {boolean} [options.disableAria] Disable readspeaker assistance
   * @param {string} [options.progressText] A progress text for describing
   *  current progress out of total progress for readspeakers.
   *  e.g. "Slide :num of :total"
   * @return {H5P.JoubelProgressbar}
   */
  JoubelUI.createProgressbar = function (numSteps, options) {
    return new H5P.JoubelProgressbar(numSteps, options);
  };

  /**
   * Create standard Joubel button
   *
   * @method H5P.JoubelUI.createButton
   * @param {object} params
   *  May hold any properties allowed by jQuery. If href is set, an A tag
   *  is used, if not a button tag is used.
   * @return {H5P.jQuery} The jquery element created
   */
  JoubelUI.createButton = function(params) {
    var type = 'button';
    if (params.href) {
      type = 'a';
    }
    else {
      params.type = 'button';
    }
    if (params.class) {
      params.class += ' h5p-joubelui-button';
    }
    else {
      params.class = 'h5p-joubelui-button';
    }
    return $('<' + type + '/>', params);
  };

  /**
   * Fix for iframe scoll bug in IOS. When focusing an element that doesn't have
   * focus support by default the iframe will scroll the parent frame so that
   * the focused element is out of view. This varies dependening on the elements
   * of the parent frame.
   */
  if (H5P.isFramed && !H5P.hasiOSiframeScrollFix &&
      /iPad|iPhone|iPod/.test(navigator.userAgent)) {
    H5P.hasiOSiframeScrollFix = true;

    // Keep track of original focus function
    var focus = HTMLElement.prototype.focus;

    // Override the original focus
    HTMLElement.prototype.focus = function () {
      // Only focus the element if it supports it natively
      if ( (this instanceof HTMLAnchorElement ||
            this instanceof HTMLInputElement ||
            this instanceof HTMLSelectElement ||
            this instanceof HTMLTextAreaElement ||
            this instanceof HTMLButtonElement ||
            this instanceof HTMLIFrameElement ||
            this instanceof HTMLAreaElement) && // HTMLAreaElement isn't supported by Safari yet.
          !this.getAttribute('role')) { // Focus breaks if a different role has been set
          // In theory this.isContentEditable should be able to recieve focus,
          // but it didn't work when tested.

        // Trigger the original focus with the proper context
        focus.call(this);
      }
    };
  }

  return JoubelUI;
})(H5P.jQuery);
;H5P.Tooltip = H5P.Tooltip || function() {};

H5P.Question = (function ($, EventDispatcher, JoubelUI) {

  /**
   * Extending this class make it alot easier to create tasks for other
   * content types.
   *
   * @class H5P.Question
   * @extends H5P.EventDispatcher
   * @param {string} type
   */
  function Question(type) {
    var self = this;

    // Inheritance
    EventDispatcher.call(self);

    // Register default section order
    self.order = ['video', 'image', 'audio', 'introduction', 'content', 'explanation', 'feedback', 'scorebar', 'buttons', 'read'];

    // Keep track of registered sections
    var sections = {};

    // Buttons
    var buttons = {};
    var buttonOrder = [];

    // Wrapper when attached
    var $wrapper;

    // Click element
    var clickElement;

    // ScoreBar
    var scoreBar;

    // Keep track of the feedback's visual status.
    var showFeedback;

    // Keep track of which buttons are scheduled for hiding.
    var buttonsToHide = [];

    // Keep track of which buttons are scheduled for showing.
    var buttonsToShow = [];

    // Keep track of the hiding and showing of buttons.
    var toggleButtonsTimer;
    var toggleButtonsTransitionTimer;
    var buttonTruncationTimer;

    // Keeps track of initialization of question
    var initialized = false;

    /**
     * @type {Object} behaviour Behaviour of Question
     * @property {Boolean} behaviour.disableFeedback Set to true to disable feedback section
     */
    var behaviour = {
      disableFeedback: false,
      disableReadSpeaker: false
    };

    // Keeps track of thumb state
    var imageThumb = true;

    // Keeps track of image transitions
    var imageTransitionTimer;

    // Keep track of whether sections is transitioning.
    var sectionsIsTransitioning = false;

    // Keep track of auto play state
    var disableAutoPlay = false;

    // Feedback transition timer
    var feedbackTransitionTimer;

    // Used when reading messages to the user
    var $read, readText;

    /**
     * Register section with given content.
     *
     * @private
     * @param {string} section ID of the section
     * @param {(string|H5P.jQuery)} [content]
     */
    var register = function (section, content) {
      sections[section] = {};
      var $e = sections[section].$element = $('<div/>', {
        'class': 'h5p-question-' + section,
      });
      if (content) {
        $e[content instanceof $ ? 'append' : 'html'](content);
      }
    };

    /**
     * Update registered section with content.
     *
     * @private
     * @param {string} section ID of the section
     * @param {(string|H5P.jQuery)} content
     */
    var update = function (section, content) {
      if (content instanceof $) {
        sections[section].$element.html('').append(content);
      }
      else {
        sections[section].$element.html(content);
      }
    };

    /**
     * Insert element with given ID into the DOM.
     *
     * @private
     * @param {array|Array|string[]} order
     * List with ordered element IDs
     * @param {string} id
     * ID of the element to be inserted
     * @param {Object} elements
     * Maps ID to the elements
     * @param {H5P.jQuery} $container
     * Parent container of the elements
     */
    var insert = function (order, id, elements, $container) {
      // Try to find an element id should be after
      for (var i = 0; i < order.length; i++) {
        if (order[i] === id) {
          // Found our pos
          while (i > 0 &&
          (elements[order[i - 1]] === undefined ||
          !elements[order[i - 1]].isVisible)) {
            i--;
          }
          if (i === 0) {
            // We are on top.
            elements[id].$element.prependTo($container);
          }
          else {
            // Add after element
            elements[id].$element.insertAfter(elements[order[i - 1]].$element);
          }
          elements[id].isVisible = true;
          break;
        }
      }
    };

    /**
     * Make feedback into a popup and position relative to click.
     *
     * @private
     * @param {string} [closeText] Text for the close button
     */
    var makeFeedbackPopup = function (closeText) {
      var $element = sections.feedback.$element;
      var $parent = sections.content.$element;
      var $click = (clickElement != null ? clickElement.$element : null);

      $element.appendTo($parent).addClass('h5p-question-popup');

      if (sections.scorebar) {
        sections.scorebar.$element.appendTo($element);
      }

      $parent.addClass('h5p-has-question-popup');

      // Draw the tail
      var $tail = $('<div/>', {
        'class': 'h5p-question-feedback-tail'
      }).hide()
        .appendTo($parent);

      // Draw the close button
      var $close = $('<div/>', {
        'class': 'h5p-question-feedback-close',
        'tabindex': 0,
        'title': closeText,
        on: {
          click: function (event) {
            $element.remove();
            $tail.remove();
            event.preventDefault();
          },
          keydown: function (event) {
            switch (event.which) {
              case 13: // Enter
              case 32: // Space
                $element.remove();
                $tail.remove();
                event.preventDefault();
            }
          }
        }
      }).hide().appendTo($element);

      if ($click != null) {
        if ($click.hasClass('correct')) {
          $element.addClass('h5p-question-feedback-correct');
          $close.show();
          sections.buttons.$element.hide();
        }
        else {
          sections.buttons.$element.appendTo(sections.feedback.$element);
        }
      }

      positionFeedbackPopup($element, $click);
    };

    /**
     * Position the feedback popup.
     *
     * @private
     * @param {H5P.jQuery} $element Feedback div
     * @param {H5P.jQuery} $click Visual click div
     */
    var positionFeedbackPopup = function ($element, $click) {
      var $container = $element.parent();
      var $tail = $element.siblings('.h5p-question-feedback-tail');
      var popupWidth = $element.outerWidth();
      var popupHeight = setElementHeight($element);
      var space = 15;
      var disableTail = false;
      var positionY = $container.height() / 2 - popupHeight / 2;
      var positionX = $container.width() / 2 - popupWidth / 2;
      var tailX = 0;
      var tailY = 0;
      var tailRotation = 0;

      if ($click != null) {
        // Edge detection for click, takes space into account
        var clickNearTop = ($click[0].offsetTop < space);
        var clickNearBottom = ($click[0].offsetTop + $click.height() > $container.height() - space);
        var clickNearLeft = ($click[0].offsetLeft < space);
        var clickNearRight = ($click[0].offsetLeft + $click.width() > $container.width() - space);

        // Click is not in a corner or close to edge, calculate position normally
        positionX = $click[0].offsetLeft - popupWidth / 2  + $click.width() / 2;
        positionY = $click[0].offsetTop - popupHeight - space;
        tailX = positionX + popupWidth / 2 - $tail.width() / 2;
        tailY = positionY + popupHeight - ($tail.height() / 2);
        tailRotation = 225;

        // If popup is outside top edge, position under click instead
        if (popupHeight + space > $click[0].offsetTop) {
          positionY = $click[0].offsetTop + $click.height() + space;
          tailY = positionY - $tail.height() / 2 ;
          tailRotation = 45;
        }

        // If popup is outside left edge, position left
        if (positionX < 0) {
          positionX = 0;
        }

        // If popup is outside right edge, position right
        if (positionX + popupWidth > $container.width()) {
          positionX = $container.width() - popupWidth;
        }

        // Special cases such as corner clicks, or close to an edge, they override X and Y positions if met
        if (clickNearTop && (clickNearLeft || clickNearRight)) {
          positionX = $click[0].offsetLeft + (clickNearLeft ? $click.width() : -popupWidth);
          positionY = $click[0].offsetTop + $click.height();
          disableTail = true;
        }
        else if (clickNearBottom && (clickNearLeft || clickNearRight)) {
          positionX = $click[0].offsetLeft + (clickNearLeft ? $click.width() : -popupWidth);
          positionY = $click[0].offsetTop - popupHeight;
          disableTail = true;
        }
        else if (!clickNearTop && !clickNearBottom) {
          if (clickNearLeft || clickNearRight) {
            positionY = $click[0].offsetTop - popupHeight / 2 + $click.width() / 2;
            positionX = $click[0].offsetLeft + (clickNearLeft ? $click.width() + space : -popupWidth + -space);
            // Make sure this does not position the popup off screen
            if (positionX < 0) {
              positionX = 0;
              disableTail = true;
            }
            else {
              tailX = positionX + (clickNearLeft ? - $tail.width() / 2 : popupWidth - $tail.width() / 2);
              tailY = positionY + popupHeight / 2 - $tail.height() / 2;
              tailRotation = (clickNearLeft ? 315 : 135);
            }
          }
        }

        // Contain popup from overflowing bottom edge
        if (positionY + popupHeight > $container.height()) {
          positionY = $container.height() - popupHeight;

          if (popupHeight > $container.height() - ($click[0].offsetTop + $click.height() + space)) {
            disableTail = true;
          }
        }
      }
      else {
        disableTail = true;
      }

      // Contain popup from ovreflowing top edge
      if (positionY < 0) {
        positionY = 0;
      }

      $element.css({top: positionY, left: positionX});
      $tail.css({top: tailY, left: tailX});

      if (!disableTail) {
        $tail.css({
          'left': tailX,
          'top': tailY,
          'transform': 'rotate(' + tailRotation + 'deg)'
        }).show();
      }
      else {
        $tail.hide();
      }
    };

    /**
     * Set element max height, used for animations.
     *
     * @param {H5P.jQuery} $element
     */
    var setElementHeight = function ($element) {
      if (!$element.is(':visible')) {
        // No animation
        $element.css('max-height', 'none');
        return;
      }

      // If this element is shown in the popup, we can't set width to 100%,
      // since it already has a width set in CSS
      var isFeedbackPopup = $element.hasClass('h5p-question-popup');

      // Get natural element height
      var $tmp = $element.clone()
        .css({
          'position': 'absolute',
          'max-height': 'none',
          'width': isFeedbackPopup ? '' : '100%'
        })
        .appendTo($element.parent());

      // Need to take margins into account when calculating available space
      var sideMargins = parseFloat($element.css('margin-left'))
        + parseFloat($element.css('margin-right'));
      var tmpElWidth = $tmp.css('width') ? $tmp.css('width') : '100%';
      $tmp.css('width', 'calc(' + tmpElWidth + ' - ' + sideMargins + 'px)');

      // Apply height to element
      var h = Math.round($tmp.get(0).getBoundingClientRect().height);
      var fontSize = parseFloat($element.css('fontSize'));
      var relativeH = h / fontSize;
      $element.css('max-height', relativeH + 'em');
      $tmp.remove();

      if (h > 0 && sections.buttons && sections.buttons.$element === $element) {
        // Make sure buttons section is visible
        showSection(sections.buttons);

        // Resize buttons after resizing button section
        setTimeout(resizeButtons, 150);
      }
      return h;
    };

    /**
     * Does the actual job of hiding the buttons scheduled for hiding.
     *
     * @private
     * @param {boolean} [relocateFocus] Find a new button to focus
     */
    var hideButtons = function (relocateFocus) {
      for (var i = 0; i < buttonsToHide.length; i++) {
        hideButton(buttonsToHide[i].id);
      }
      buttonsToHide = [];

      if (relocateFocus) {
        self.focusButton();
      }
    };

    /**
     * Does the actual hiding.
     * @private
     * @param {string} buttonId
     */
    var hideButton = function (buttonId) {
      // Using detach() vs hide() makes it harder to cheat.
      buttons[buttonId].$element.detach();
      buttons[buttonId].isVisible = false;
    };

    /**
     * Shows the buttons on the next tick. This is to avoid buttons flickering
     * If they're both added and removed on the same tick.
     *
     * @private
     */
    var toggleButtons = function () {
      // If no buttons section, return
      if (sections.buttons === undefined) {
        return;
      }

      // Clear transition timer, reevaluate if buttons will be detached
      clearTimeout(toggleButtonsTransitionTimer);

      // Show buttons
      for (var i = 0; i < buttonsToShow.length; i++) {
        insert(buttonOrder, buttonsToShow[i].id, buttons, sections.buttons.$element);
        buttons[buttonsToShow[i].id].isVisible = true;
      }
      buttonsToShow = [];

      // Hide buttons
      var numToHide = 0;
      var relocateFocus = false;
      for (var j = 0; j < buttonsToHide.length; j++) {
        var button = buttons[buttonsToHide[j].id];
        if (button.isVisible) {
          numToHide += 1;
        }
        if (button.$element.is(':focus')) {
          // Move focus to the first visible button.
          relocateFocus = true;
        }
      }

      var animationTimer = 150;
      if (sections.feedback && sections.feedback.$element.hasClass('h5p-question-popup')) {
        animationTimer = 0;
      }

      if (numToHide === sections.buttons.$element.children().length) {
        // All buttons are going to be hidden. Hide container using transition.
        hideSection(sections.buttons);
        // Detach buttons
        hideButtons(relocateFocus);
      }
      else {
        hideButtons(relocateFocus);

        // Show button section
        if (!sections.buttons.$element.is(':empty')) {
          showSection(sections.buttons);
          setElementHeight(sections.buttons.$element);

          // Trigger resize after animation
          toggleButtonsTransitionTimer = setTimeout(function () {
            self.trigger('resize');
          }, animationTimer);
        }

        // Resize buttons to fit container
        resizeButtons();
      }

      toggleButtonsTimer = undefined;
    };

    /**
     * Allows for scaling of the question image.
     */
    var scaleImage = function () {
      var $imgSection = sections.image.$element;
      clearTimeout(imageTransitionTimer);

      // Add this here to avoid initial transition of the image making
      // content overflow. Alternatively we need to trigger a resize.
      $imgSection.addClass('animatable');

      if (imageThumb) {

        // Expand image
        $(this).attr('aria-expanded', true);
        $imgSection.addClass('h5p-question-image-fill-width');
        imageThumb = false;

        imageTransitionTimer = setTimeout(function () {
          self.trigger('resize');
        }, 600);
      }
      else {

        // Scale down image
        $(this).attr('aria-expanded', false);
        $imgSection.removeClass('h5p-question-image-fill-width');
        imageThumb = true;

        imageTransitionTimer = setTimeout(function () {
          self.trigger('resize');
        }, 600);
      }
    };

    /**
     * Get scrollable ancestor of element
     *
     * @private
     * @param {H5P.jQuery} $element
     * @param {Number} [currDepth=0] Current recursive calls to ancestor, stop at maxDepth
     * @param {Number} [maxDepth=5] Maximum depth for finding ancestor.
     * @returns {H5P.jQuery} Parent element that is scrollable
     */
    var findScrollableAncestor = function ($element, currDepth, maxDepth) {
      if (!currDepth) {
        currDepth = 0;
      }
      if (!maxDepth) {
        maxDepth = 5;
      }
      // Check validation of element or if we have reached document root
      if (!$element || !($element instanceof $) || document === $element.get(0) || currDepth >= maxDepth) {
        return;
      }

      if ($element.css('overflow-y') === 'auto') {
        return $element;
      }
      else {
        return findScrollableAncestor($element.parent(), currDepth + 1, maxDepth);
      }
    };

    /**
     * Scroll to bottom of Question.
     *
     * @private
     */
    var scrollToBottom = function () {
      if (!$wrapper || ($wrapper.hasClass('h5p-standalone') && !H5P.isFullscreen)) {
        return; // No scroll
      }

      var scrollableAncestor = findScrollableAncestor($wrapper);

      // Scroll to bottom of scrollable ancestor
      if (scrollableAncestor) {
        scrollableAncestor.animate({
          scrollTop: $wrapper.css('height')
        }, "slow");
      }
    };

    /**
     * Resize buttons to fit container width
     *
     * @private
     */
    var resizeButtons = function () {
      if (!buttons || !sections.buttons) {
        return;
      }

      var go = function () {
        // Don't do anything if button elements are not visible yet
        if (!sections.buttons.$element.is(':visible')) {
          return;
        }

        // Width of all buttons
        var buttonsWidth = {
          max: 0,
          min: 0,
          current: 0
        };

        for (var i in buttons) {
          var button = buttons[i];
          if (button.isVisible) {
            setButtonWidth(buttons[i]);
            buttonsWidth.max += button.width.max;
            buttonsWidth.min += button.width.min;
            buttonsWidth.current += button.isTruncated ? button.width.min : button.width.max;
          }
        }

        var makeButtonsFit = function (availableWidth) {
          if (buttonsWidth.max < availableWidth) {
            // It is room for everyone on the right side of the score bar (without truncating)
            if (buttonsWidth.max !== buttonsWidth.current) {
              // Need to make everyone big
              restoreButtonLabels(buttonsWidth.current, availableWidth);
            }
            return true;
          }
          else if (buttonsWidth.min < availableWidth) {
            // Is it room for everyone on the right side of the score bar with truncating?
            if (buttonsWidth.current > availableWidth) {
              removeButtonLabels(buttonsWidth.current, availableWidth);
            }
            else {
              restoreButtonLabels(buttonsWidth.current, availableWidth);
            }
            return true;
          }
          return false;
        };

        toggleFullWidthScorebar(false);

        var buttonSectionWidth = Math.floor(sections.buttons.$element.width()) - 1;

        if (!makeButtonsFit(buttonSectionWidth)) {
          // If we get here we need to wrap:
          toggleFullWidthScorebar(true);
          buttonSectionWidth = Math.floor(sections.buttons.$element.width()) - 1;
          makeButtonsFit(buttonSectionWidth);
        }
      };

      // If visible, resize right away
      if (sections.buttons.$element.is(':visible')) {
        go();
      }
      else { // If not visible, try on the next tick
        // Clear button truncation timer if within a button truncation function
        if (buttonTruncationTimer) {
          clearTimeout(buttonTruncationTimer);
        }
        buttonTruncationTimer = setTimeout(function () {
          buttonTruncationTimer = undefined;
          go();
        }, 0);
      }
    };

    var toggleFullWidthScorebar = function (enabled) {
      if (sections.scorebar &&
          sections.scorebar.$element &&
          sections.scorebar.$element.hasClass('h5p-question-visible')) {
        sections.buttons.$element.addClass('has-scorebar');
        sections.buttons.$element.toggleClass('wrap', enabled);
        sections.scorebar.$element.toggleClass('full-width', enabled);
      }
      else {
        sections.buttons.$element.removeClass('has-scorebar');
      }
    };

    /**
     * Remove button labels until they use less than max width.
     *
     * @private
     * @param {Number} buttonsWidth Total width of all buttons
     * @param {Number} maxButtonsWidth Max width allowed for buttons
     */
    var removeButtonLabels = function (buttonsWidth, maxButtonsWidth) {
      // Reverse traversal
      for (var i = buttonOrder.length - 1; i >= 0; i--) {
        var buttonId = buttonOrder[i];
        var button = buttons[buttonId];
        if (!button.isTruncated && button.isVisible) {
          var $button = button.$element;
          buttonsWidth -= button.width.max - button.width.min;
          // Set tooltip (needed by H5P.Tooltip)
          let buttonText = $button.text();
          $button.attr('data-tooltip', buttonText);

          // Use button text as aria label if a specific one isn't provided
          if (!button.ariaLabel) {
            $button.attr('aria-label', buttonText);
          }
          // Remove label
          $button.html('').addClass('truncated');
          button.isTruncated = true;
          if (buttonsWidth <= maxButtonsWidth) {
            // Buttons are small enough.
            return;
          }
        }
      }
    };

    /**
     * Restore button labels until it fills maximum possible width without exceeding the max width.
     *
     * @private
     * @param {Number} buttonsWidth Total width of all buttons
     * @param {Number} maxButtonsWidth Max width allowed for buttons
     */
    var restoreButtonLabels = function (buttonsWidth, maxButtonsWidth) {
      for (var i = 0; i < buttonOrder.length; i++) {
        var buttonId = buttonOrder[i];
        var button = buttons[buttonId];
        if (button.isTruncated && button.isVisible) {
          // Calculate new total width of buttons with a static pixel for consistency cross-browser
          buttonsWidth += button.width.max - button.width.min + 1;

          if (buttonsWidth > maxButtonsWidth) {
            return;
          }
          // Restore label
          button.$element.html(button.text);

          // Remove tooltip (used by H5P.Tooltip)
          button.$element.removeAttr('data-tooltip');

          // Remove aria-label if a specific one isn't provided
          if (!button.ariaLabel) {
            button.$element.removeAttr('aria-label');
          }

          button.$element.removeClass('truncated');
          button.isTruncated = false;
        }
      }
    };

    /**
     * Helper function for finding index of keyValue in array
     *
     * @param {String} keyValue Value to be found
     * @param {String} key In key
     * @param {Array} array In array
     * @returns {number}
     */
    var existsInArray = function (keyValue, key, array) {
      var i;
      for (i = 0; i < array.length; i++) {
        if (array[i][key] === keyValue) {
          return i;
        }
      }
      return -1;
    };

    /**
     * Show a section
     * @param {Object} section
     */
    var showSection = function (section) {
      section.$element.addClass('h5p-question-visible');
      section.isVisible = true;
    };

    /**
     * Hide a section
     * @param {Object} section
     */
    var hideSection = function (section) {
      section.$element.css('max-height', '');
      section.isVisible = false;

      setTimeout(function () {
        // Only hide if section hasn't been set to visible in the meantime
        if (!section.isVisible) {
          section.$element.removeClass('h5p-question-visible');
        }
      }, 150);
    };

    /**
     * Set behaviour for question.
     *
     * @param {Object} options An object containing behaviour that will be extended by Question
     */
    self.setBehaviour = function (options) {
      $.extend(behaviour, options);
    };

    /**
     * A video to display above the task.
     *
     * @param {object} params
     */
    self.setVideo = function (params) {
      sections.video = {
        $element: $('<div/>', {
          'class': 'h5p-question-video'
        })
      };

      if (disableAutoPlay && params.params.playback) {
        params.params.playback.autoplay = false;
      }

      // Never fit to wrapper
      if (!params.params.visuals) {
        params.params.visuals = {};
      }
      params.params.visuals.fit = false;
      sections.video.instance = H5P.newRunnable(params, self.contentId, sections.video.$element, true);
      var fromVideo = false; // Hack to avoid never ending loop
      sections.video.instance.on('resize', function () {
        fromVideo = true;
        self.trigger('resize');
        fromVideo = false;
      });
      self.on('resize', function () {
        if (!fromVideo) {
          sections.video.instance.trigger('resize');
        }
      });

      return self;
    };

    /**
     * An audio player to display above the task.
     *
     * @param {object} params
     */
    self.setAudio = function (params) {
      params.params = params.params || {};

      sections.audio = {
        $element: $('<div/>', {
          'class': 'h5p-question-audio',
        })
      };

      if (disableAutoPlay) {
        params.params.autoplay = false;
      }
      else if (params.params.playerMode === 'transparent') {
        params.params.autoplay = true; // false doesn't make sense for transparent audio
      }

      sections.audio.instance = H5P.newRunnable(params, self.contentId, sections.audio.$element, true);
      // The height value that is set by H5P.Audio is counter-productive here.
      if (sections.audio.instance.audio) {
        sections.audio.instance.audio.style.height = '';
      }

      return self;
    };

    /**
     * Will stop any playback going on in the task.
     */
    self.pause = function () {
      if (sections.video && sections.video.isVisible) {
        sections.video.instance.pause();
      }
      if (sections.audio && sections.audio.isVisible) {
        sections.audio.instance.pause();
      }
    };

    /**
     * Start playback of video
     */
    self.play = function () {
      if (sections.video && sections.video.isVisible) {
        sections.video.instance.play();
      }
      if (sections.audio && sections.audio.isVisible) {
        sections.audio.instance.play();
      }
    };

    /**
     * Disable auto play, useful in editors.
     */
    self.disableAutoPlay = function () {
      disableAutoPlay = true;
    };

    /**
     * Process HTML escaped string for use as attribute value,
     * e.g. for alt text or title attributes.
     *
     * @param {string} value
     * @return {string} WARNING! Do NOT use for innerHTML.
     */
    self.massageAttributeOutput = function (value) {
      const dparser = new DOMParser().parseFromString(value, 'text/html');
      const div = document.createElement('div');
      div.innerHTML = dparser.documentElement.textContent;;
      return div.textContent || div.innerText || '';
    };

    /**
     * Add task image.
     *
     * @param {string} path Relative
     * @param {Object} [options] Options object
     * @param {string} [options.alt] Text representation
     * @param {string} [options.title] Hover text
     * @param {Boolean} [options.disableImageZooming] Set as true to disable image zooming
     * @param {string} [options.expandImage] Localization strings
     * @param {string} [options.minimizeImage] Localization string

     */
    self.setImage = function (path, options) {
      options = options ? options : {};
      sections.image = {};
      // Image container
      sections.image.$element = $('<div/>', {
        'class': 'h5p-question-image h5p-question-image-fill-width'
      });

      // Inner wrap
      var $imgWrap = $('<div/>', {
        'class': 'h5p-question-image-wrap',
        appendTo: sections.image.$element
      });

      // Image element
      var $img = $('<img/>', {
        src: H5P.getPath(path, self.contentId),
        alt: (options.alt === undefined ? '' : self.massageAttributeOutput(options.alt)),
        title: (options.title === undefined ? '' : self.massageAttributeOutput(options.title)),
        on: {
          load: function () {
            self.trigger('imageLoaded', this);
            self.trigger('resize');
          }
        },
        appendTo: $imgWrap
      });

      // Disable image zooming
      if (options.disableImageZooming) {
        $img.css('maxHeight', 'none');

        // Make sure we are using the correct amount of width at all times
        var determineImgWidth = function () {

          // Remove margins if natural image width is bigger than section width
          var imageSectionWidth = sections.image.$element.get(0).getBoundingClientRect().width;

          // Do not transition, for instant measurements
          $imgWrap.css({
            '-webkit-transition': 'none',
            'transition': 'none'
          });

          // Margin as translateX on both sides of image.
          var diffX = 2 * ($imgWrap.get(0).getBoundingClientRect().left -
            sections.image.$element.get(0).getBoundingClientRect().left);

          if ($img.get(0).naturalWidth >= imageSectionWidth - diffX) {
            sections.image.$element.addClass('h5p-question-image-fill-width');
          }
          else { // Use margin for small res images
            sections.image.$element.removeClass('h5p-question-image-fill-width');
          }

          // Reset transition rules
          $imgWrap.css({
            '-webkit-transition': '',
            'transition': ''
          });
        };

        // Determine image width
        if ($img.is(':visible')) {
          determineImgWidth();
        }
        else {
          $img.on('load', determineImgWidth);
        }

        // Skip adding zoom functionality
        return;
      }

      const setAriaLabel = () => {
        const ariaLabel = $imgWrap.attr('aria-expanded') === 'true'
          ? options.minimizeImage 
          : options.expandImage;
          
          $imgWrap.attr('aria-label', `${ariaLabel} ${options.alt}`);
        };

      var sizeDetermined = false;
      var determineSize = function () {
        if (sizeDetermined || !$img.is(':visible')) {
          return; // Try again next time.
        }

        $imgWrap.addClass('h5p-question-image-scalable')
          .attr('aria-expanded', false)
          .attr('role', 'button')
          .attr('tabIndex', '0')
          .on('click', function (event) {
            if (event.which === 1) {
              scaleImage.apply(this); // Left mouse button click
              setAriaLabel();
            }
          }).on('keypress', function (event) {
            if (event.which === 32) {
              event.preventDefault(); // Prevent default behaviour; page scroll down
              scaleImage.apply(this); // Space bar pressed
              setAriaLabel();
            }
          });

        setAriaLabel();

        sections.image.$element.removeClass('h5p-question-image-fill-width');

        sizeDetermined  = true; // Prevent any futher events
      };

      self.on('resize', determineSize);

      return self;
    };

    /**
     * Add the introduction section.
     *
     * @param {(string|H5P.jQuery)} content
     */
    self.setIntroduction = function (content) {
      register('introduction', content);

      return self;
    };

    /**
     * Add the content section.
     *
     * @param {(string|H5P.jQuery)} content
     * @param {Object} [options]
     * @param {string} [options.class]
     */
    self.setContent = function (content, options) {
      register('content', content);

      if (options && options.class) {
        sections.content.$element.addClass(options.class);
      }

      return self;
    };

    /**
     * Force readspeaker to read text. Useful when you have to use
     * setTimeout for animations.
     */
    self.read = function (content) {
      if (!$read) {
        return; // Not ready yet
      }

      if (readText) {
        // Combine texts if called multiple times
        readText += (readText.substr(-1, 1) === '.' ? ' ' : '. ') + content;
      }
      else {
        readText = content;
      }

      // Set text
      $read.html(readText);

      setTimeout(function () {
        // Stop combining when done reading
        readText = null;
        $read.html('');
      }, 100);
    };

    /**
     * Read feedback
     */
    self.readFeedback = function () {
      var invalidFeedback =
        behaviour.disableReadSpeaker ||
        !showFeedback ||
        !sections.feedback ||
        !sections.feedback.$element;

      if (invalidFeedback) {
        return;
      }

      var $feedbackText = $('.h5p-question-feedback-content-text', sections.feedback.$element);
      if ($feedbackText && $feedbackText.html() && $feedbackText.html().length) {
        self.read($feedbackText.html());
      }
    };

    /**
     * Remove feedback
     *
     * @return {H5P.Question}
     */
    self.removeFeedback = function () {

      clearTimeout(feedbackTransitionTimer);

      if (sections.feedback && showFeedback) {

        showFeedback = false;

        // Hide feedback & scorebar
        hideSection(sections.scorebar);
        hideSection(sections.feedback);

        sectionsIsTransitioning = true;

        // Detach after transition
        feedbackTransitionTimer = setTimeout(function () {
          // Avoiding Transition.onTransitionEnd since it will register multiple events, and there's no way to cancel it if the transition changes back to "show" while the animation is happening.
          if (!showFeedback) {
            sections.feedback.$element.children().detach();
            sections.scorebar.$element.children().detach();

            // Trigger resize after animation
            self.trigger('resize');
          }
          sectionsIsTransitioning = false;
          scoreBar.setScore(0);
        }, 150);

        if ($wrapper) {
          $wrapper.find('.h5p-question-feedback-tail').remove();
        }
      }

      return self;
    };

    /**
     * Set feedback message.
     *
     * @param {string} [content]
     * @param {number} score The score
     * @param {number} maxScore The maximum score for this question
     * @param {string} [scoreBarLabel] Makes it easier for readspeakers to identify the scorebar
     * @param {string} [helpText] Help text that describes the score inside a tip icon
     * @param {object} [popupSettings] Extra settings for popup feedback
     * @param {boolean} [popupSettings.showAsPopup] Should the feedback display as popup?
     * @param {string} [popupSettings.closeText] Translation for close button text
     * @param {object} [popupSettings.click] Element representing where user clicked on screen
     */
    self.setFeedback = function (content, score, maxScore, scoreBarLabel, helpText, popupSettings, scoreExplanationButtonLabel) {
      // Feedback is disabled
      if (behaviour.disableFeedback) {
        return self;
      }

      // Need to toggle buttons right away to avoid flickering/blinking
      // Note: This means content types should invoke hide/showButton before setFeedback
      toggleButtons();

      clickElement = (popupSettings != null && popupSettings.click != null ? popupSettings.click : null);
      clearTimeout(feedbackTransitionTimer);

      var $feedback = $('<div>', {
        'class': 'h5p-question-feedback-container'
      });

      var $feedbackContent = $('<div>', {
        'class': 'h5p-question-feedback-content'
      }).appendTo($feedback);

      // Feedback text
      $('<div>', {
        'class': 'h5p-question-feedback-content-text',
        'html': content
      }).appendTo($feedbackContent);

      var $scorebar = $('<div>', {
        'class': 'h5p-question-scorebar-container'
      });
      if (scoreBar === undefined) {
        scoreBar = JoubelUI.createScoreBar(maxScore, scoreBarLabel, helpText, scoreExplanationButtonLabel);
      }
      scoreBar.appendTo($scorebar);

      $feedbackContent.toggleClass('has-content', content !== undefined && content.length > 0);

      // Feedback for readspeakers
      if (!behaviour.disableReadSpeaker && scoreBarLabel) {
        self.read(scoreBarLabel.replace(':num', score).replace(':total', maxScore) + '. ' + (content ? content : ''));
      }

      showFeedback = true;
      if (sections.feedback) {
        // Update section
        update('feedback', $feedback);
        update('scorebar', $scorebar);
      }
      else {
        // Create section
        register('feedback', $feedback);
        register('scorebar', $scorebar);
        if (initialized && $wrapper) {
          insert(self.order, 'feedback', sections, $wrapper);
          insert(self.order, 'scorebar', sections, $wrapper);
        }
      }

      showSection(sections.feedback);
      showSection(sections.scorebar);

      resizeButtons();

      if (popupSettings != null && popupSettings.showAsPopup == true) {
        makeFeedbackPopup(popupSettings.closeText);
        scoreBar.setScore(score);
      }
      else {
        // Show feedback section
        feedbackTransitionTimer = setTimeout(function () {
          setElementHeight(sections.feedback.$element);
          setElementHeight(sections.scorebar.$element);
          sectionsIsTransitioning = true;

          // Scroll to bottom after showing feedback
          scrollToBottom();

          // Trigger resize after animation
          feedbackTransitionTimer = setTimeout(function () {
            sectionsIsTransitioning = false;
            self.trigger('resize');
            scoreBar.setScore(score);
          }, 150);
        }, 0);
      }

      return self;
    };

    /**
     * Set feedback content (no animation).
     *
     * @param {string} content
     * @param {boolean} [extendContent] True will extend content, instead of replacing it
     */
    self.updateFeedbackContent = function (content, extendContent) {
      if (sections.feedback && sections.feedback.$element) {

        if (extendContent) {
          content = $('.h5p-question-feedback-content', sections.feedback.$element).html() + ' ' + content;
        }

        // Update feedback content html
        $('.h5p-question-feedback-content', sections.feedback.$element).html(content).addClass('has-content');

        // Make sure the height is correct
        setElementHeight(sections.feedback.$element);

        // Need to trigger resize when feedback has finished transitioning
        setTimeout(self.trigger.bind(self, 'resize'), 150);
      }

      return self;
    };

    /**
     * Set the content of the explanation / feedback panel
     *
     * @param {Object} data
     * @param {string} data.correct
     * @param {string} data.wrong
     * @param {string} data.text
     * @param {string} title Title for explanation panel
     *
     * @return {H5P.Question}
     */
    self.setExplanation = function (data, title) {
      if (data) {
        var explainer = new H5P.Question.Explainer(title, data);

        if (sections.explanation) {
          // Update section
          update('explanation', explainer.getElement());
        }
        else {
          register('explanation', explainer.getElement());

          if (initialized && $wrapper) {
            insert(self.order, 'explanation', sections, $wrapper);
          }
        }
      }
      else if (sections.explanation) {
        // Hide explanation section
        sections.explanation.$element.children().detach();
      }

      return self;
    };

    /**
     * Checks to see if button is registered.
     *
     * @param {string} id
     * @returns {boolean}
     */
    self.hasButton = function (id) {
      return (buttons[id] !== undefined);
    };

    /**
     * @typedef {Object} ConfirmationDialog
     * @property {boolean} [enable] Must be true to show confirmation dialog
     * @property {Object} [instance] Instance that uses confirmation dialog
     * @property {jQuery} [$parentElement] Append to this element.
     * @property {Object} [l10n] Translatable fields
     * @property {string} [l10n.header] Header text
     * @property {string} [l10n.body] Body text
     * @property {string} [l10n.cancelLabel]
     * @property {string} [l10n.confirmLabel]
     */

    /**
     * Register buttons for the task.
     *
     * @param {string} id
     * @param {string} text label
     * @param {function} clicked
     * @param {boolean} [visible=true]
     * @param {Object} [options] Options for button
     * @param {Object} [extras] Extra options
     * @param {ConfirmationDialog} [extras.confirmationDialog] Confirmation dialog
     * @param {Object} [extras.contentData] Content data
     * @params {string} [extras.textIfSubmitting] Text to display if submitting
     */
    self.addButton = function (id, text, clicked, visible, options, extras) {
      if (buttons[id]) {
        return self; // Already registered
      }

      if (sections.buttons === undefined)  {
        // We have buttons, register wrapper
        register('buttons');
        if (initialized) {
          insert(self.order, 'buttons', sections, $wrapper);
        }
      }

      extras = extras || {};
      extras.confirmationDialog = extras.confirmationDialog || {};
      options = options || {};

      var confirmationDialog =
        self.addConfirmationDialogToButton(extras.confirmationDialog, clicked);

      /**
       * Handle button clicks through both mouse and keyboard
       * @private
       */
      var handleButtonClick = function () {
        if (extras.confirmationDialog.enable && confirmationDialog) {
          // Show popups section if used
          if (!extras.confirmationDialog.$parentElement) {
            sections.popups.$element.removeClass('hidden');
          }
          confirmationDialog.show($e.position().top);
        }
        else {
          clicked();
        }
      };

      const isSubmitting = extras.contentData && extras.contentData.standalone
        && (extras.contentData.isScoringEnabled || extras.contentData.isReportingEnabled);

      if (isSubmitting && extras.textIfSubmitting) {
        text = extras.textIfSubmitting;
      }

      buttons[id] = {
        isTruncated: false,
        text: text,
        isVisible: false,
        ariaLabel: options['aria-label']
      };

      // The button might be <button> or <a>
      // (dependent on options.href set or not)
      var isAnchorTag = (options.href !== undefined);
      var $e = buttons[id].$element = JoubelUI.createButton($.extend({
        'class': 'h5p-question-' + id,
        html: text,
        on: {
          click: function (event) {
            handleButtonClick();
            if (isAnchorTag) {
              event.preventDefault();
            }
          }
        }
      }, options));
      buttonOrder.push(id);

      H5P.Tooltip($e.get(0), {tooltipSource: 'data-tooltip'});

      // The button might be <button> or <a>. If <a>, the space key is not
      // triggering the click event, must therefore handle this here:
      if (isAnchorTag) {
        $e.on('keypress', function (event) {
          if (event.which === 32) { // Space
            handleButtonClick();
            event.preventDefault();
          }
        });
      }

      if (visible === undefined || visible) {
        // Button should be visible
        $e.appendTo(sections.buttons.$element);
        buttons[id].isVisible = true;
        showSection(sections.buttons);
      }

      return self;
    };

    var setButtonWidth = function (button) {
      var $button = button.$element;
      var $tmp = $button.clone()
        .css({
          'position': 'absolute',
          'white-space': 'nowrap',
          'max-width': 'none'
        }).removeClass('truncated')
        .html(button.text)
        .appendTo($button.parent());

      // Calculate max width (button including text)
      button.width = {
        max: Math.ceil($tmp.outerWidth() + parseFloat($tmp.css('margin-left')) + parseFloat($tmp.css('margin-right')))
      };

      // Calculate min width (truncated, icon only)
      $tmp.html('').addClass('truncated');
      button.width.min = Math.ceil($tmp.outerWidth() + parseFloat($tmp.css('margin-left')) + parseFloat($tmp.css('margin-right')));
      $tmp.remove();
    };

    /**
     * Add confirmation dialog to button
     * @param {ConfirmationDialog} options
     *  A confirmation dialog that will be shown before click handler of button
     *  is triggered
     * @param {function} clicked
     *  Click handler of button
     * @return {H5P.ConfirmationDialog|undefined}
     *  Confirmation dialog if enabled
     */
    self.addConfirmationDialogToButton = function (options, clicked) {
      options = options || {};

      if (!options.enable) {
        return;
      }

      // Confirmation dialog
      var confirmationDialog = new H5P.ConfirmationDialog({
        instance: options.instance,
        headerText: options.l10n.header,
        dialogText: options.l10n.body,
        cancelText: options.l10n.cancelLabel,
        confirmText: options.l10n.confirmLabel
      });

      // Determine parent element
      if (options.$parentElement) {
        const parentElement = options.$parentElement.get(0);
        let dialogParent;
        // If using h5p-content, dialog will not appear on embedded fullscreen
        if (parentElement.classList.contains('h5p-content')) {
          dialogParent = parentElement.querySelector('.h5p-container');
        }

        confirmationDialog.appendTo(dialogParent ?? parentElement);
      }
      else {

        // Create popup section and append to that
        if (sections.popups === undefined) {
          register('popups');
          if (initialized) {
            insert(self.order, 'popups', sections, $wrapper);
          }
          sections.popups.$element.addClass('hidden');
          self.order.push('popups');
        }
        confirmationDialog.appendTo(sections.popups.$element.get(0));
      }

      // Add event listeners
      confirmationDialog.on('confirmed', function () {
        if (!options.$parentElement) {
          sections.popups.$element.addClass('hidden');
        }
        clicked();

        // Trigger to content type
        self.trigger('confirmed');
      });

      confirmationDialog.on('canceled', function () {
        if (!options.$parentElement) {
          sections.popups.$element.addClass('hidden');
        }
        // Trigger to content type
        self.trigger('canceled');
      });

      return confirmationDialog;
    };

    /**
     * Show registered button with given identifier.
     *
     * @param {string} id
     * @param {Number} [priority]
     */
    self.showButton = function (id, priority) {
      var aboutToBeHidden = existsInArray(id, 'id', buttonsToHide) !== -1;
      if (buttons[id] === undefined || (buttons[id].isVisible === true && !aboutToBeHidden)) {
        return self;
      }

      priority = priority || 0;

      // Skip if already being shown
      var indexToShow = existsInArray(id, 'id', buttonsToShow);
      if (indexToShow !== -1) {

        // Update priority
        if (buttonsToShow[indexToShow].priority < priority) {
          buttonsToShow[indexToShow].priority = priority;
        }

        return self;
      }

      // Check if button is going to be hidden on next tick
      var exists = existsInArray(id, 'id', buttonsToHide);
      if (exists !== -1) {

        // Skip hiding if higher priority
        if (buttonsToHide[exists].priority <= priority) {
          buttonsToHide.splice(exists, 1);
          buttonsToShow.push({id: id, priority: priority});
        }

      } // If button is not shown
      else if (!buttons[id].$element.is(':visible')) {

        // Show button on next tick
        buttonsToShow.push({id: id, priority: priority});
      }

      if (!toggleButtonsTimer) {
        toggleButtonsTimer = setTimeout(toggleButtons, 0);
      }

      return self;
    };

    /**
     * Hide registered button with given identifier.
     *
     * @param {string} id
     * @param {number} [priority]
     */
    self.hideButton = function (id, priority) {
      var aboutToBeShown = existsInArray(id, 'id', buttonsToShow) !== -1;
      if (buttons[id] === undefined || (buttons[id].isVisible === false && !aboutToBeShown)) {
        return self;
      }

      priority = priority || 0;

      // Skip if already being hidden
      var indexToHide = existsInArray(id, 'id', buttonsToHide);
      if (indexToHide !== -1) {

        // Update priority
        if (buttonsToHide[indexToHide].priority < priority) {
          buttonsToHide[indexToHide].priority = priority;
        }

        return self;
      }

      // Check if buttons is going to be shown on next tick
      var exists = existsInArray(id, 'id', buttonsToShow);
      if (exists !== -1) {

        // Skip showing if higher priority
        if (buttonsToShow[exists].priority <= priority) {
          buttonsToShow.splice(exists, 1);
          buttonsToHide.push({id: id, priority: priority});
        }
      }
      else if (!buttons[id].$element.is(':visible')) {

        // Make sure it is detached in case the container is hidden.
        hideButton(id);
      }
      else {

        // Hide button on next tick.
        buttonsToHide.push({id: id, priority: priority});
      }

      if (!toggleButtonsTimer) {
        toggleButtonsTimer = setTimeout(toggleButtons, 0);
      }

      return self;
    };

    /**
     * Set focus to the given button. If no button is given the first visible
     * button gets focused. This is useful if you lose focus.
     *
     * @param {string} [id]
     */
    self.focusButton = function (id) {
      if (id === undefined) {
        // Find first button that is visible.
        for (var i = 0; i < buttonOrder.length; i++) {
          var button = buttons[buttonOrder[i]];
          if (button && button.isVisible) {
            // Give that button focus
            button.$element.focus();
            break;
          }
        }
      }
      else if (buttons[id] && buttons[id].$element.is(':visible')) {
        // Set focus to requested button
        buttons[id].$element.focus();
      }

      return self;
    };

    /**
     * Toggle readspeaker functionality
     * @param {boolean} [disable] True to disable, false to enable.
     */
    self.toggleReadSpeaker = function (disable) {
      behaviour.disableReadSpeaker = disable || !behaviour.disableReadSpeaker;
    };

    /**
     * Set new element for section.
     *
     * @param {String} id
     * @param {H5P.jQuery} $element
     */
    self.insertSectionAtElement = function (id, $element) {
      if (sections[id] === undefined) {
        register(id);
      }
      sections[id].parent = $element;

      // Insert section if question is not initialized
      if (!initialized) {
        insert([id], id, sections, $element);
      }

      return self;
    };

    /**
     * Attach content to given container.
     *
     * @param {H5P.jQuery} $container
     */
    self.attach = function ($container) {
      if (self.isRoot()) {
        self.setActivityStarted();
      }

      // The first time we attach we also create our DOM elements.
      if ($wrapper === undefined) {
        if (self.registerDomElements !== undefined &&
           (self.registerDomElements instanceof Function ||
           typeof self.registerDomElements === 'function')) {

          // Give the question type a chance to register before attaching
          self.registerDomElements();
        }

        // Create section for reading messages
        $read = $('<div/>', {
          'aria-live': 'polite',
          'class': 'h5p-hidden-read'
        });
        register('read', $read);
        self.trigger('registerDomElements');
      }

      // Prepare container
      $wrapper = $container;
      $container.html('')
        .addClass('h5p-question h5p-' + type);

      // Add sections in given order
      var $sections = [];
      for (var i = 0; i < self.order.length; i++) {
        var section = self.order[i];
        if (sections[section]) {
          if (sections[section].parent) {
            // Section has a different parent
            sections[section].$element.appendTo(sections[section].parent);
          }
          else {
            $sections.push(sections[section].$element);
          }
          sections[section].isVisible = true;
        }
      }

      // Only append once to DOM for optimal performance
      $container.append($sections);

      // Let others react to dom changes
      self.trigger('domChanged', {
        '$target': $container,
        'library': self.libraryInfo.machineName,
        'contentId': self.contentId,
        'key': 'newLibrary'
      }, {'bubbles': true, 'external': true});

      // ??
      initialized = true;

      return self;
    };

    /**
     * Detach all sections from their parents
     */
    self.detachSections = function () {
      // Deinit Question
      initialized = false;

      // Detach sections
      for (var section in sections) {
        sections[section].$element.detach();
      }

      return self;
    };

    // Listen for resize
    self.on('resize', function () {
      // Allow elements to attach and set their height before resizing
      if (!sectionsIsTransitioning && sections.feedback && showFeedback) {
        // Resize feedback to fit
        setElementHeight(sections.feedback.$element);
      }

      // Re-position feedback popup if in use
      var $element = sections.feedback;
      var $click = clickElement;

      if ($element != null && $element.$element != null && $click != null && $click.$element != null) {
        setTimeout(function () {
          positionFeedbackPopup($element.$element, $click.$element);
        }, 10);
      }

      resizeButtons();
    });
  }

  // Inheritance
  Question.prototype = Object.create(EventDispatcher.prototype);
  Question.prototype.constructor = Question;

  /**
   * Determine the overall feedback to display for the question.
   * Returns empty string if no matching range is found.
   *
   * @param {Object[]} feedbacks
   * @param {number} scoreRatio
   * @return {string}
   */
  Question.determineOverallFeedback = function (feedbacks, scoreRatio) {
    scoreRatio = Math.floor(scoreRatio * 100);

    for (var i = 0; i < feedbacks.length; i++) {
      var feedback = feedbacks[i];
      var hasFeedback = (feedback.feedback !== undefined && feedback.feedback.trim().length !== 0);

      if (feedback.from <= scoreRatio && feedback.to >= scoreRatio && hasFeedback) {
        return feedback.feedback;
      }
    }

    return '';
  };

  return Question;
})(H5P.jQuery, H5P.EventDispatcher, H5P.JoubelUI);
;H5P.Question.Explainer = (function ($) {
  /**
   * Constructor
   *
   * @class
   * @param {string} title
   * @param {array} explanations
   */
  function Explainer(title, explanations) {
    var self = this;

    /**
     * Create the DOM structure
     */
    var createHTML = function () {
      self.$explanation = $('<div>', {
        'class': 'h5p-question-explanation-container'
      });

      // Add title:
      $('<div>', {
        'class': 'h5p-question-explanation-title',
        role: 'heading',
        html: title,
        appendTo: self.$explanation
      });

      var $explanationList = $('<ul>', {
        'class': 'h5p-question-explanation-list',
        appendTo: self.$explanation
      });

      for (var i = 0; i < explanations.length; i++) {
        var feedback = explanations[i];
        var $explanationItem = $('<li>', {
          'class': 'h5p-question-explanation-item',
          appendTo: $explanationList
        });

        var $content = $('<div>', {
          'class': 'h5p-question-explanation-status'
        });

        if (feedback.correct) {
          $('<span>', {
            'class': 'h5p-question-explanation-correct',
            html: feedback.correct,
            appendTo: $content
          });
        }
        if (feedback.wrong) {
          $('<span>', {
            'class': 'h5p-question-explanation-wrong',
            html: feedback.wrong,
            appendTo: $content
          });
        }
        $content.appendTo($explanationItem);

        if (feedback.text) {
          $('<div>', {
            'class': 'h5p-question-explanation-text',
            html: feedback.text,
            appendTo: $explanationItem
          });
        }
      }
    };

    createHTML();

    /**
     * Return the container HTMLElement
     *
     * @return {HTMLElement}
     */
    self.getElement = function () {
      return self.$explanation;
    };
  }

  return Explainer;

})(H5P.jQuery);
;(function (Question) {

  /**
   * Makes it easy to add animated score points for your question type.
   *
   * @class H5P.Question.ScorePoints
   */
  Question.ScorePoints = function () {
    var self = this;

    var elements = [];
    var showElementsTimer;

    /**
     * Create the element that displays the score point element for questions.
     *
     * @param {boolean} isCorrect
     * @return {HTMLElement}
     */
    self.getElement = function (isCorrect) {
      var element = document.createElement('div');
      element.classList.add(isCorrect ? 'h5p-question-plus-one' : 'h5p-question-minus-one');
      element.classList.add('h5p-question-hidden-one');
      elements.push(element);

      // Schedule display animation of all added elements
      if (showElementsTimer) {
        clearTimeout(showElementsTimer);
      }
      showElementsTimer = setTimeout(showElements, 0);

      return element;
    };

    /**
     * @private
     */
    var showElements = function () {
      // Determine delay between triggering animations
      var delay = 0;
      var increment = 150;
      var maxTime = 1000;

      if (elements.length && elements.length > Math.ceil(maxTime / increment)) {
        // Animations will run for more than ~1 second, reduce it.
        increment = maxTime / elements.length;
      }

      for (var i = 0; i < elements.length; i++) {
        // Use timer to trigger show
        setTimeout(showElement(elements[i]), delay);

        // Increse delay for next element
        delay += increment;
      }
    };

    /**
     * Trigger transition animation for the given element
     *
     * @private
     * @param {HTMLElement} element
     * @return {function}
     */
    var showElement = function (element) {
      return function () {
        element.classList.remove('h5p-question-hidden-one');
      };
    };
  };

})(H5P.Question);
;!function(){"use strict";let t=function(){function t(){}return t.extend=function(){for(let t=1;t<arguments.length;t++)for(let e in arguments[t])Object.prototype.hasOwnProperty.call(arguments[t],e)&&("object"==typeof arguments[0][e]&&"object"==typeof arguments[t][e]?this.extend(arguments[0][e],arguments[t][e]):arguments[0][e]=arguments[t][e]);return arguments[0]},t.htmlDecode=function(t){return(new DOMParser).parseFromString(t,"text/html").documentElement.textContent},t.revertRTL=function(e){let s=[],i=[];return e.forEach((e=>{t.containsRTLCharacters(e.solution)?i.push(e):(s=s.concat(i.reverse()),i=[],s.push(e))})),0!==i.length&&(s=s.concat(i.reverse())),s},t.splitWordAlternatives=function(t){return t.split("").reverse().join("").split(/\|(?!\\)/).map((t=>t.split("").reverse().join("").replace("\\|","|"))).reverse()},t.containsRTLCharacters=function(e){return new RegExp("^[^"+t.RTL+"]*?["+t.RTL+"]").test(e)},t.buildCombinations=function(t,e){let s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:" ";const i=[];return t.forEach((t=>{i.push(...e.map((e=>""===e?t:`${e}${s}${t}`)))})),i},t.formatLanguageCode=function(t){if("string"!=typeof t)return t;const e=t.split("-");return e[0]=e[0].toLowerCase(),e.length>1&&(e[1]=e[1].toUpperCase()),t=e.join("-")},t.shuffleArray=function(t){let e,s,i;for(i=t.length-1;i>0;i--)e=Math.floor(Math.random()*(i+1)),s=t[i],t[i]=t[e],t[e]=s;return t},t}();t.RTL="֑-ࣿ";var e=t;let s=function(){function t(s,i){let a=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};this.params=e.extend({audioNotSupported:"Your browser does not support this audio.",a11y:{play:"Play",playSlowly:"Play slowly",triesLeft:"number of tries left: @number",infinite:"infinite",sentence:"Sentence",solution:"Solution",enterText:"Enter what you have heard"},disablePause:!1,type:t.BUTTON_TYPE_NORMAL,callbacks:{playAudio:()=>{}}},i),this.previousState=a,void 0===a.triesLeft?this.triesLeft=this.params.maxTries:null===a.triesLeft?this.triesLeft=1/0:this.triesLeft=a.triesLeft,this.dom=this.createAudioDOM(s,this.params),null===this.dom.firstChild&&this.dom.appendChild(this.getDummyButtonDOM()),this.status=t.STATUS_ENDED}var s=t.prototype;return s.createAudioDOM=function(e,s){const i=H5P.jQuery("<div>",{class:t.AUDIO_WRAPPER});if(void 0!==s.sample){const a={files:s.sample,audioNotSupported:s.audioNotSupported};this.audioInstance=new H5P.Audio(a,e,{previousState:this.previousState.audio}),this.audioInstance.playOriginal=this.audioInstance.play,this.audioInstance.play=this.playOverride.bind(this),this.audioInstance.attach(i),this.button=this.audioInstance.$audioButton.get(0),this.audio=this.audioInstance,s.type===t.BUTTON_TYPE_SLOW?(this.audioInstance.$audioButton.removeClass(t.BUTTON_PLAY).addClass(t.BUTTON_SLOW),this.setLabel(s.a11y.playSlowly)):this.setLabel(s.a11y.play),this.triesLeft<1&&this.audio.disableToggleButton(),this.previousState.audio&&0!==this.previousState.audio.currentTime&&(this.status=t.STATUS_PAUSE,this.audioInstance.$audioButton.addClass(t.BUTTON_PLAY_PAUSED)),this.audioInstance.audio.addEventListener("play",(()=>{s.disablePause&&this.audioInstance.$audioButton.get(0).classList.add("h5p-audio-disabled"),s.type===t.BUTTON_TYPE_SLOW&&this.audioInstance.$audioButton.removeClass(t.BUTTON_SLOW).addClass(t.BUTTON_PAUSE),this.status=t.STATUS_PLAYING,this.params.callbacks.playAudio(this)})),this.audioInstance.audio.addEventListener("pause",(()=>{s.type===t.BUTTON_TYPE_SLOW&&this.audioInstance.$audioButton.removeClass(t.BUTTON_PAUSE).addClass(t.BUTTON_SLOW),this.status=t.STATUS_PAUSE})),this.audioInstance.audio.addEventListener("ended",(()=>{this.handleAudioEnded()})),this.audioInstance.audio&&this.audioInstance.audio.preload&&(this.audioInstance.audio.preload="none")}return i.get(0)},s.getDOM=function(){return this.dom},s.getDummyButtonDOM=function(){const e=document.createElement("div");e.classList.add(t.BUTTON),e.classList.add(t.BUTTON_NONE);const s=document.createElement("div");return s.classList.add(t.INNER_CONTAINER),s.appendChild(e),s},s.play=function(){this.status!==t.STATUS_PLAYING&&this.button.click()},s.pause=function(){this.status===t.STATUS_PLAYING&&this.button.click()},s.stop=function(){this.status===t.STATUS_PLAYING&&(this.resetAudio(),this.handleAudioEnded())},s.handleAudioEnded=function(){this.params.disablePause&&this.audio.$audioButton.get(0).classList.remove("h5p-audio-disabled"),this.handlePlayed(),this.params.type===t.BUTTON_TYPE_SLOW?(this.audio.$audioButton.removeClass(t.BUTTON_PAUSE).addClass(t.BUTTON_SLOW),this.setLabel(this.params.a11y.playSlowly)):this.setLabel(this.params.a11y.play),this.status=t.STATUS_ENDED},s.handlePlayed=function(){this.triesLeft--,0===this.triesLeft&&this.disable()},s.setLabel=function(t){const e=isFinite(this.triesLeft)?this.triesLeft:this.params.a11y.infinite,s=this.params.a11y.triesLeft.replace(/@number/g,e);this.button&&(this.button.setAttribute("aria-label",`${t}. ${s}`),this.button.setAttribute("title",s))},s.reset=function(){this.triesLeft=this.params.maxTries,this.audio&&this.audio.seekTo(0),this.status=t.STATUS_ENDED,this.enable(),this.params.type===t.BUTTON_TYPE_SLOW?this.setLabel(this.params.a11y.playSlowly):this.setLabel(this.params.a11y.play),this.resetAudio()},s.enable=function(){this.button&&this.audio.enableToggleButton()},s.setUntabbable=function(){this.button&&this.button.setAttribute("tabindex","-1")},s.setTabbable=function(){this.button&&this.button.setAttribute("tabindex","0")},s.disable=function(){this.button&&this.audio.disableToggleButton()},s.isEnabled=function(){return!!this.button&&this.audio.isEnabled()},s.resetAudio=function(){this.audio&&this.audio.audio&&this.audio.audio.load&&this.audio.audio.load(),this.button&&(this.button.classList.remove(t.BUTTON_PAUSE),this.button.classList.remove(t.BUTTON_PLAY_PAUSED),this.params.type===t.BUTTON_TYPE_SLOW?this.button.classList.add(t.BUTTON_SLOW):this.button.classList.add(t.BUTTON_PLAY))},s.getCurrentState=function(){return{audio:this.audio?this.audio.getCurrentState():void 0,triesLeft:this.triesLeft}},s.focus=function(){this.button&&this.button.focus()},s.playOverride=function(){this.isDelayingPlay||(0!==this.params.playButtonDelay?(this.isDelayingPlay=!0,this.button.classList.add("h5p-dictation-delay-animation"),this.button.style.animationDuration=`${this.params.playButtonDelay+.01}s`,window.clearTimeout(this.playTimeout),this.playTimeout=window.setTimeout((()=>{this.audioInstance.playOriginal(),this.button.classList.remove("h5p-dictation-delay-animation"),this.button.style.animationDuration="",this.isDelayingPlay=!1}),1e3*this.params.playButtonDelay)):this.audioInstance.playOriginal())},s.isAudioPlaying=function(){return this.audio?.audio?.currentTime>0},t}();s.BUTTON_TYPE_NORMAL=0,s.BUTTON_TYPE_SLOW=1,s.STATUS_PAUSE=0,s.STATUS_PLAYING=1,s.STATUS_ENDED=2,s.AUDIO_WRAPPER="h5p-dictation-audio-wrapper",s.BUTTON="h5p-audio-minimal-button",s.BUTTON_PLAY="h5p-audio-minimal-play",s.BUTTON_PLAY_PAUSED="h5p-audio-minimal-play-paused",s.BUTTON_PAUSE="h5p-audio-minimal-pause",s.BUTTON_SLOW="h5p-audio-minimal-slow",s.BUTTON_NONE="h5p-audio-minimal-none",s.INNER_CONTAINER="h5p-audio-inner";var i=s;let a=function(){function t(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};this.params=e,this.words=document.createElement("div"),this.words.classList.add(t.SOLUTION_TEXT),this.words.setAttribute("role","list"),this.words.setAttribute("aria-label",this.params.a11y.solution),this.words.setAttribute("aria-expanded","false"),this.words.setAttribute("tabindex","0"),this.words.addEventListener("keydown",(t=>{const e=this.words.getAttribute("aria-expanded"),s=this.wordMarked||(this.params.containsRTL?this.words.lastChild:this.words.firstChild);switch(t.code){case"Enter":case"Space":if(t.target!==t.currentTarget)break;"false"===e?(this.words.setAttribute("aria-expanded","true"),s&&s.focus()):(this.words.setAttribute("aria-expanded","false"),s.setAttribute("tabindex","-1"))}})),this.solutionInner=document.createElement("div"),this.solutionInner.classList.add(t.SOLUTION_INNER),this.solutionInner.appendChild(this.words),this.container=document.createElement("div"),this.container.classList.add(t.SOLUTION_CONTAINER),this.container.classList.add(t.HIDE),this.container.appendChild(this.solutionInner)}var s=t.prototype;return s.getDOM=function(){return this.container},s.createSolution=function(t){return this.params.containsRTL&&(t.words=e.revertRTL(t.words)),t.words.map(((e,s)=>this.createSolutionWordDOM(s,e,t.words.length-1!==s)))},s.createSolutionWordDOM=function(t,s){let i=!(arguments.length>2&&void 0!==arguments[2])||arguments[2];"first"===this.params.alternateSolution&&"match"!==s.type&&"typo"!==s.type&&(s.solution=s.solution?e.splitWordAlternatives(s.solution)[0]:"");const a=document.createElement("span");!1===this.params.customTypoDisplay&&"typo"===s.type?a.classList.add("h5p-wrapper-wrong"):a.classList.add(`h5p-wrapper-${s.type}`),i&&a.classList.add("h5p-spacer"),a.setAttribute("tabindex","-1"),a.setAttribute("role","listitem"),this.addSolutionWordListeners(a);const n=`${this.params.a11y.item} ${t+1}.`,o=this.createAriaExplanation(s),r=this.createAriaScore(s.type);return a.setAttribute("aria-label",`${n} ${o} ${r}`),this.appendExplanationTo(a,s),a},s.addSolutionWordListeners=function(t){t.addEventListener("focus",(t=>{this.wordMarked=t.target,t.target.setAttribute("tabindex","0")})),t.addEventListener("keydown",(t=>{const e=this.params.containsRTL?t.target.parentNode.lastChild:t.target.parentNode.firstChild,s=this.params.containsRTL?t.target.parentNode.firstChild:t.target.parentNode.lastChild;switch(t.keyCode){case 37:case 38:t.preventDefault(),t.target.previousSibling&&(t.target.setAttribute("tabindex","-1"),t.target.previousSibling.focus());break;case 39:case 40:t.preventDefault(),t.target.nextSibling&&(t.target.setAttribute("tabindex","-1"),t.target.nextSibling.focus());break;case 36:t.preventDefault(),t.target!==e&&(t.target.setAttribute("tabindex","-1"),e.focus());break;case 35:t.preventDefault(),t.target!==s&&(t.target.setAttribute("tabindex","-1"),s.focus())}}))},s.createAriaExplanation=function(t){const s={match:this.params.a11y.correct,wrong:this.params.a11y.wrong,typo:this.params.a11y.typo,missing:this.params.a11y.missing,added:this.params.a11y.added},i=this.makeReadable(t.answer),a="match"===t.type||"typo"===t.type?e.splitWordAlternatives(t.solution).join(` ${this.params.a11y.or} `):t.solution,n=this.makeReadable(a);let o=`${i}${""===i?"":". "}${s[t.type]}.`;return"wrong"!==t.type&&"typo"!==t.type&&"missing"!==t.type||(o+=` ${this.params.a11y.shouldHaveBeen}. ${n}.`),o},s.makeReadable=function(t){return void 0===t?"":t.replace(/\./g,this.params.a11y.period).replace(/!/g,this.params.a11y.exclamationPoint).replace(/\?/g,this.params.a11y.questionMark).replace(/,/g,this.params.a11y.comma).replace(/'/g,this.params.a11y.singleQuote).replace(/["\u201C\u201E]/g,this.params.a11y.doubleQuote).replace(/:/g,this.params.a11y.colon).replace(/;/g,this.params.a11y.semicolon).replace(/\+/g,this.params.a11y.plus).replace(/-/g,this.params.a11y.minus).replace(/\*/g,this.params.a11y.asterisk).replace(/\//g,this.params.a11y.forwardSlash)},s.createAriaScore=function(t){let e;if(this.params.zeroMistakeMode?(e=0,"match"===t?e=1:"typo"===t&&(e=1-this.params.typoFactor)):(e=-1,"match"===t?e=0:"typo"===t&&(e*=this.params.typoFactor)),0===e)e="";else{e=`${e} ${-1===e||1===e?this.params.a11y.point:this.params.a11y.points}.`}return e},s.appendExplanationTo=function(t,e){const s=new H5P.Question.ScorePoints;if("wrong"===e.type||"added"===e.type||"typo"===e.type){const s=document.createElement("span");s.classList.add(`h5p-answer-${e.type}`),s.innerHTML=e.answer,t.appendChild(s)}if("added"!==e.type){const s=document.createElement("span");s.classList.add(`h5p-solution-${e.type}`),s.innerHTML=e.solution,t.appendChild(s)}if(this.params.zeroMistakeMode){const i=s.getElement(!0);"match"===e.type&&t.appendChild(i),"typo"===e.type&&.5===this.params.typoFactor&&(i.classList.remove("h5p-question-minus-one"),i.classList.add("h5p-question-plus-one-half")),"typo"===e.type&&this.params.typoFactor<1&&t.appendChild(i)}else{const i=s.getElement(!1);"match"!==e.type&&("typo"===e.type&&.5===this.params.typoFactor&&(i.classList.remove("h5p-question-minus-one"),i.classList.add("h5p-question-minus-one-half")),("typo"!==e.type||this.params.typoFactor>0)&&t.appendChild(i))}},s.show=function(e){const s=this.createSolution(e);s.length>0&&"match"===e.words[s.length-1].type?this.words.classList.add("h5p-solution-last-correct"):this.words.classList.remove("h5p-solution-last-correct"),this.words.firstChild||s.forEach((e=>{this.words.appendChild(e),this.container.classList.remove(t.HIDE)}))},s.hide=function(){for(;this.words.firstChild;)this.words.removeChild(this.words.firstChild);this.container.classList.add(t.HIDE)},s.focus=function(){this.words.focus()},s.reset=function(){this.wordMarked=void 0,this.words.setAttribute("aria-expanded","false")},t}();a.SOLUTION_CONTAINER="h5p-solution-container",a.SOLUTION_INNER="h5p-solution-inner",a.SOLUTION_TEXT="h5p-solution-text",a.HIDE="hide";var n=a;let o=function(){function t(s,a,o){let r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};this.index=s,this.position=s+1,this.params=a,this.contentId=o,this.maxTries=a.tries,this.maxTriesAlternative=a.triesAlternative,this.params.sentence.description=(this.params.sentence.description||"").trim(),this.params.callbacks=e.extend({playAudio:()=>{},onInteracted:()=>{},onContextChanged:()=>{},resize:()=>{}},a.callbacks),this.solutionText=e.htmlDecode(a.sentence.text).trim(),this.solutionText=a.ignorePunctuation?t.stripPunctuation(this.solutionText):this.solutionText,this.containsRTL="auto"===this.params.overrideRTL?e.containsRTLCharacters(this.solutionText):"on"===this.params.overrideRTL,this.mistakesMax=t.addSpaces(this.solutionText,{autosplit:this.params.autosplit,wordSeparator:this.params.wordSeparator}).split(a.wordSeparator).length,this.content=document.createElement("div"),this.content.setAttribute("role","group"),this.content.setAttribute("aria-label",`${this.params.a11y.sentence} ${this.position}`),this.content.classList.add(t.CONTENT_WRAPPER);const l=document.createElement("div");l.classList.add(t.CONTENT_DESCRIPTION),l.innerHTML=this.params.sentence.description;const u=document.createElement("div");u.classList.add(t.CONTENT_INTERACTION),this.buttonPlayNormal=new i(o,{sample:a.sentence.sample,audioNotSupported:a.audioNotSupported,disablePause:a.disablePause,type:i.BUTTON_TYPE_NORMAL,maxTries:a.tries,playButtonDelay:a.playButtonDelay,a11y:a.a11y,callbacks:{playAudio:t=>{this.handleButtonClicked(t)}}},r.buttonPlayNormal),u.appendChild(this.buttonPlayNormal.getDOM()),!0===this.params.hasAlternatives&&(this.buttonPlaySlow=new i(o,{sample:a.sentence.sampleAlternative,audioNotSupported:a.audioNotSupported,disablePause:a.disablePause,type:i.BUTTON_TYPE_SLOW,maxTries:a.triesAlternative,playButtonDelay:a.playButtonDelay,a11y:a.a11y,callbacks:{playAudio:t=>{this.handleButtonClicked(t)}}},r.buttonPlaySlow),u.appendChild(this.buttonPlaySlow.getDOM())),l.classList.add(!0===this.params.hasAlternatives?t.CONTENT_DESCRIPTION_TWO_BUTTONS:t.CONTENT_DESCRIPTION_ONE_BUTTON),this.inputField=document.createElement("textarea"),this.inputField.setAttribute("rows",1),this.inputField.setAttribute("spellcheck","false"),this.inputField.setAttribute("autocorrect","off"),this.inputField.setAttribute("autocapitalize","off"),this.inputField.setAttribute("aria-label",this.params.a11y.enterText),this.inputField.addEventListener("input",(()=>{-1===this.inputField.value.indexOf("\n")&&-1===this.inputField.value.indexOf("\r")||(this.inputField.value=this.inputField.value.replace(/[\n\r]/g,"")),this.inputField.style.height="auto";const t=this.previousScrollHeight!==this.inputField.scrollHeight;this.inputField.style.height=this.inputField.scrollHeight+this.inputField.offsetHeight-this.inputField.clientHeight+"px",t&&(this.previousScrollHeight=this.inputField.scrollHeight,this.params.callbacks.resize())})),this.inputField.classList.add(t.INPUT_FIELD),r.userInput?(this.inputField.value=r.userInput,this.oldValue=this.inputField.value||""):this.oldValue="",this.inputField.addEventListener("keydown",(()=>{this.params.callbacks.onContextChanged(this.index)})),this.inputField.addEventListener("blur",(()=>{this.oldValue!==this.inputField.value&&this.params.callbacks.onInteracted(),this.oldValue=this.inputField.value})),this.solution=new n({alternateSolution:this.params.alternateSolution,zeroMistakeMode:this.params.zeroMistakeMode,customTypoDisplay:this.params.customTypoDisplay,typoFactor:this.params.typoFactor,containsRTL:this.containsRTL,a11y:{match:this.params.a11y.correct,wrong:this.params.a11y.wrong,typo:this.params.a11y.typo,missing:this.params.a11y.missing,added:this.params.a11y.added,correct:this.params.a11y.correct,point:this.params.a11y.point,points:this.params.a11y.points,item:this.params.a11y.item,solution:this.params.a11y.solution,or:this.params.a11y.or,shouldHaveBeen:this.params.a11y.shouldHaveBeen,period:this.params.a11y.period,exclamationPoint:this.params.a11y.exclamationPoint,questionMark:this.params.a11y.questionMark,comma:this.params.a11y.comma,singleQuote:this.params.a11y.singleQuote,doubleQuote:this.params.a11y.doubleQuote,colon:this.params.a11y.colon,semicolon:this.params.a11y.semicolon,plus:this.params.a11y.plus,minus:this.params.a11y.minus,asterisk:this.params.a11y.asterisk,forwardSlash:this.params.a11y.forwardSlash}}),this.inputWrapper=document.createElement("div"),this.inputWrapper.classList.add(t.INPUT_WRAPPER),this.inputWrapper.appendChild(this.inputField),this.inputWrapper.appendChild(this.solution.getDOM()),u.appendChild(this.inputWrapper),this.content.appendChild(l),this.content.appendChild(u)}var s=t.prototype;return s.getDOM=function(){return this.content},s.getUserInput=function(){return this.inputField.value},s.setPosition=function(t){this.position=t,this.content.setAttribute("aria-label",`${this.params.a11y.sentence} ${this.position}`)},s.showSolution=function(t){this.solution.show(t),this.buttonPlayNormal&&this.buttonPlayNormal.setUntabbable(),this.buttonPlaySlow&&this.buttonPlaySlow.setUntabbable()},s.hideSolution=function(){this.solution.hide(),this.buttonPlayNormal&&this.buttonPlayNormal.setTabbable(),this.buttonPlaySlow&&this.buttonPlaySlow.setTabbable()},s.getCorrectText=function(){return arguments.length>0&&void 0!==arguments[0]&&arguments[0]?t.addSpaces(this.solutionText,{autosplit:this.params.autosplit,wordSeparator:this.params.wordSeparator}).split(this.params.wordSeparator):this.solutionText},s.getCurrentState=function(){return{index:this.index,userInput:this.getUserInput(),buttonPlayNormal:this.buttonPlayNormal?this.buttonPlayNormal.getCurrentState():void 0,buttonPlaySlow:this.buttonPlaySlow?this.buttonPlaySlow.getCurrentState():void 0}},s.getMaxMistakes=function(){return this.mistakesMax},s.getXAPIDescription=function(){return this.params.sentence.description?`<p>${this.params.sentence.description}</p>`:""},s.reset=function(){this.inputField.value="",this.oldValue="",this.solution.reset(),this.buttonPlayNormal&&this.buttonPlayNormal.reset(),this.buttonPlaySlow&&this.buttonPlaySlow.reset()},s.disable=function(){this.inputField.disabled=!0,this.buttonPlayNormal&&(this.buttonPlayNormal.disable(),this.buttonPlayNormal.resetAudio()),this.buttonPlaySlow&&(this.buttonPlaySlow.disable(),this.buttonPlaySlow.resetAudio())},s.enable=function(){this.inputField.disabled=!1,this.buttonPlayNormal&&this.buttonPlayNormal.enable(),this.buttonPlaySlow&&this.buttonPlaySlow.enable()},s.pauseButtons=function(t){this.buttonPlayNormal&&this.buttonPlayNormal!==t&&this.buttonPlayNormal.pause(),this.buttonPlaySlow&&this.buttonPlaySlow!==t&&this.buttonPlaySlow.pause()},s.stopButtons=function(t){this.buttonPlayNormal&&this.buttonPlayNormal!==t&&this.buttonPlayNormal.stop(),this.buttonPlaySlow&&this.buttonPlaySlow!==t&&this.buttonPlaySlow.stop()},s.handleButtonClicked=function(t){this.params.callbacks.playAudio(t),this.params.callbacks.onInteracted(),this.params.callbacks.onContextChanged(this.index)},s.focus=function(){this.buttonPlayNormal&&this.buttonPlayNormal.focus()},t.addSpaces=function(e){let s=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(s.autosplit="boolean"!=typeof s.autosplit||s.autosplit,s.wordSeparator=s.wordSeparator||" "," "!==s.wordSeparator)return e;const i=new RegExp(`(${t.WORD}|^)(${t.PUNCTUATION.replace("'","")})( |$)`,"g"),a=new RegExp(`( |^)(${t.PUNCTUATION})(${t.WORD}|$)`,"g");return e=e.replace(i,"$1 $2 ").replace(a," $2 $3"),!0===s.autosplit&&(e=(e=(e=e.replace(new RegExp(`(${t.AUTOSPLIT})(?=${t.AUTOSPLIT})`,"g"),"$1 ")).replace(new RegExp(`(${t.AUTOSPLIT})(?=${t.WORD}|d|${t.PUNCTUATION})`,"g"),"$1 ")).replace(new RegExp(`(${t.WORD}|d|${t.PUNCTUATION})(?=${t.AUTOSPLIT})`,"g"),"$1 ")),e.trim()},t.stripPunctuation=function(e){let s=!1;"string"==typeof e&&(s=!0,e=[e]);const i=new RegExp(`^${t.PUNCTUATION}`),a=new RegExp(`${t.PUNCTUATION}$`),n=new RegExp(` ${t.PUNCTUATION}`,"g"),o=new RegExp(`${t.PUNCTUATION.replace("'","")} `,"g");return e=e.map((t=>t.replace(i,"").replace(a,"").replace(n," ").replace(o," "))),s?e.toString().replace(/[ ]{2}/g," "):e},s.computeResults=function(){const e=t.addSpaces(this.getCorrectText(),{autosplit:this.params.autosplit,wordSeparator:this.params.wordSeparator}).split(this.params.wordSeparator);let s=this.getUserInput();this.params.ignorePunctuation&&(s=t.stripPunctuation(s))," "!==this.params.wordSeparator&&(e.forEach((e=>{e.split("|").forEach((e=>{if(-1!==(e=e.trim()).indexOf(" ")){const i=e.replace(/ /g,t.SPACE_ESCAPE);s=s.replace(new RegExp(e,"g"),i)}}))})),s=s.replace(/ /g,this.params.wordSeparator),s=s.replace(new RegExp(t.SPACE_ESCAPE,"g")," "));let i=""===s.trim()?[]:t.addSpaces(s,{autosplit:this.params.autosplit,wordSeparator:this.params.wordSeparator}).split(this.params.wordSeparator).filter((t=>t.length>0));" "!==this.params.wordSeparator&&(i=i.map(((t,e)=>e===i.length-1?t:`${t} `)));const a=this.alignWords(e,i),n=this.computeScore(a),o=n.scoreTotal,r=n.words;return{score:{added:o[t.TYPE_ADDED],missing:o[t.TYPE_MISSING],typo:o[t.TYPE_TYPO],wrong:o[t.TYPE_WRONG],match:o[t.TYPE_MATCH],total:Math.min(o[t.TYPE_ADDED]+o[t.TYPE_MISSING]+o[t.TYPE_TYPO]+o[t.TYPE_WRONG],this.getMaxMistakes())},words:r}},s.computeScore=function(e){const s=[];let i=[];i[t.TYPE_ADDED]=0,i[t.TYPE_MISSING]=0,i[t.TYPE_TYPO]=0,i[t.TYPE_WRONG]=0,i[t.TYPE_MATCH]=0;for(let a=0;a<e.words1.length;a++){const n=e.words1[a],o=e.words2[a];let r="";r=void 0===n?t.TYPE_ADDED:void 0===o?t.TYPE_MISSING:o===n?t.TYPE_MATCH:H5P.TextUtilities.areSimilar(n,o)?t.TYPE_TYPO:t.TYPE_WRONG,i[r]++,s.push({solution:n,answer:o,type:r})}return{scoreTotal:i,words:s}},s.alignWords=function(t,s){const i=t=>t.words1.map(((e,s)=>e===t.words2[s]||H5P.TextUtilities.areSimilar(e,t.words2[s])||!1)),a=function(t,s){let i,a=arguments.length>2&&void 0!==arguments[2]&&arguments[2];if(void 0===s)return;const n=e.splitWordAlternatives(s);return a?n.forEach((e=>{H5P.TextUtilities.areSimilar(e,t)&&(i=i||e)})):n.forEach((e=>{e===t&&(i=i||e)})),i},n=(t,e)=>{e=e.map((t=>""===t?void 0:t));let s=t.map((t=>Array.apply(null,Array(e.length)).concat(t))).reduce(((t,e)=>t.concat(e)),[]);s=s.concat(Array.apply(null,Array(e.length)));const i=Array.apply(null,Array(s.length));let n=0;for(let t=0;t<e.length;t++){const o=e[t];for(let r=s.length-1;r>=n;r--){const l=a(o,s[r]),u=void 0!==o&&void 0!==l,h=-1===e.slice(t+1).indexOf(o);if(u&&h||r===n){s[r]=l,i[r]=o,n=r+1;break}}}for(let t=i.length-1;t>=0;t--){const e=i[t];if(void 0!==e&&e!==s[t]){let n,o=0,r=0;for(;t+o+1<i.length&&void 0===i[t+o+1];){const i=a(e,s[t+o+1]);void 0!==i&&(r=t+o+1,n=i),o++}i[r||t+o]=e,void 0!==n&&(s[r]=n),i[t]=void 0}}for(let t=0;t<i.length;t++){const e=i[t];if(void 0!==e&&e!==s[t]){let n,o=0,r=0;for(;t+o-1>=0&&void 0===i[t+o-1];){const i=a(e,s[t+o-1],!0);void 0!==i&&(r=t+o-1,n=i),o--}i[r||t+o]=e,void 0!==n&&(s[r]=n),i[t]=void 0}}for(let t=s.length-1;t>=0;t--)void 0===s[t]&&void 0===i[t]&&(s.splice(t,1),i.splice(t,1));for(let t=0;t<s.length-1;t++)void 0===s[t]&&void 0===i[t+1]&&(s[t]=s[t+1],s.splice(t+1,1),i.splice(t+1,1));for(let t=0;t<s.length-1;t++)if(i[t]===s[t]&&void 0===s[t+1]&&i[t]!==i[t+1]){let e=0;for(;t+e+1<s.length&&void 0===s[t+e+1];)e++;t+e+1<s.length&&-1!==i.slice(t+1,t+e+1).lastIndexOf(i[t])&&(s[t+e+1]=s[t],s[t]=void 0)}s.reverse(),i.reverse();for(let t=0;t<s.length-1;t++)if(i[t]===s[t]&&void 0===s[t+1]&&i[t]!==i[t+1]){let e=0;for(;t+e+1<s.length&&void 0===s[t+e+1];)e++;t+e+1<s.length&&-1!==i.slice(t+1,t+e+1).lastIndexOf(i[t])&&(s[t+e+1]=s[t],s[t]=void 0)}return s.reverse(),i.reverse(),{words1:s,words2:i}},o=t=>i(t).reduce(((t,e)=>t+(e?1:0)),0);let r=n(t,s);const l=n(t.reverse(),s.reverse());o(l)>o(r)&&(r={words1:l.words1.reverse(),words2:l.words2.reverse()});const u=i(r);let h=-1,d=u.length-1;do{for(h=u.lastIndexOf(!0,d);void 0===r.words1[h+1]&&void 0===r.words2[d];)r.words1.splice(h+1,1),r.words2.splice(d,1),d-=1;d=h-1}while(h>0);return r},s.focusSolution=function(){this.solution.focus()},t}();o.CONTENT_WRAPPER="h5p-sentence",o.CONTENT_DESCRIPTION="h5p-sentence-description",o.CONTENT_DESCRIPTION_ONE_BUTTON="h5p-sentence-description-one-button",o.CONTENT_DESCRIPTION_TWO_BUTTONS="h5p-sentence-description-two-buttons",o.CONTENT_INTERACTION="h5p-sentence-interaction",o.INPUT_WRAPPER="h5p-input-wrapper",o.INPUT_FIELD="h5p-text-input",o.TYPE_ADDED="added",o.TYPE_MISSING="missing",o.TYPE_WRONG="wrong",o.TYPE_MATCH="match",o.TYPE_TYPO="typo",o.AUTOSPLIT="[぀-ヿ一-拿挀-矿砀-賿贀-鿿]",o.PUNCTUATION="[.?!,'\";\\:\\-\\(\\)/\\+\\-\\*«»¿“-„،؟־׀׃׆ -⁯⋯　-。〈-】！（），：；？［］﹁﹂﹏～]",o.WORD="\\w|[À-ÖØ-öø-ÿĀ-ʯ֑-ׇֽֿׁׂׅׄ-؋؍-؞ؠ-ࣿ]",o.SPACE_ESCAPE="astringthatwillneverhappen123@@";var r=o;function l(t,e){return l=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,e){return t.__proto__=e,t},l(t,e)}let u=function(t){function s(s,i,a){var n;(n=t.call(this,"dictation")||this).params=e.extend({media:{},taskDescription:"Please listen carefully and write what you hear.",sentences:[],behaviour:{tries:1/0,triesAlternative:1/0,shuffleSentences:"never",playButtonDelay:0,scoring:{ignorePunctuation:!0,zeroMistakeMode:!1,typoFactor:"100"},textual:{wordSeparator:" ",overrideRTL:"auto",autosplit:!0},feedbackPresentation:{customTypoDisplay:!1,alternateSolution:"first"},enableRetry:!0,enableSolutionsButton:!0,enableCheckButton:!0,enableSolutionOnCheck:!1},l10n:{generalFeedback:"You have made @total mistake(s).",generalFeedbackZeroMistakesMode:"You have entered @total word(s) correctly and @typo word(s) with minor mistakes.",checkAnswer:"Check",tryAgain:"Retry",showSolution:"Show solution",audioNotSupported:"Your browser does not support this audio.",submitAnswer:"Submit"},a11y:{check:"Check the answers. The responses will be marked as correct, incorrect, or unanswered.",showSolution:"Show the solution. The task will be marked with its correct solution.",retry:"Retry the task. Reset all responses and start the task over again.",play:"Play",playSlowly:"Play slowly",triesLeft:"Number of tries left: @number",infinite:"infinite",enterText:"Enter what you have heard.",yourResult:"You got @score out of @total points",solution:"Solution",sentence:"Sentence",item:"Item",correct:"correct",wrong:"wrong",typo:"small mistake",missing:"missing",added:"added",shouldHaveBeen:"Should have been",or:"or",point:"point",points:"points",period:"period",exclamationPoint:"exclamation point",questionMark:"question mark",comma:"comma",singleQuote:"single quote",doubleQuote:"double quote",colon:"colon",semicolon:"semicolon",plus:"plus",minus:"minus",asterisk:"asterisk",forwardSlash:"forward slash"}},s);const o=a&&a.metadata&&a.metadata.defaultLanguage||"en";if(n.languageTag=e.formatLanguageCode(o),!s)return function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(n);n.correctTotal=0,n.contentId=i,n.contentData=a||{},n.randomId=H5P.createUUID(),n.contextId=0,n.sentences=[],window.MSInputMethodContext&&document.documentMode&&n.params.sentences.forEach(((t,e)=>{t.sample&&"audio/x-wav"===t.sample[0].mime&&delete t.sample,t.sampleAlternative&&"audio/x-wav"===t.sampleAlternative[0].mime&&delete t.sampleAlternative}));const l=n.params.sentences.some((t=>void 0!==t.sampleAlternative));return n.params.behaviour.scoring.typoFactor=parseInt(n.params.behaviour.scoring.typoFactor)/100,n.params.sentences=n.params.sentences.filter((t=>void 0!==t.text&&void 0!==t.sample)),n.previousSentenceStates=[...Array(n.params.sentences.length)],n.contentData.previousState&&Array.isArray(n.contentData.previousState)&&n.contentData.previousState.forEach(((t,e)=>{n.previousSentenceStates.length>e&&(t.index=t.index??e,n.previousSentenceStates[e]=t)})),n.params.sentences=n.params.sentences.forEach(((t,e)=>{n.sentences.push(new r(e,{sentence:t,audioNotSupported:n.params.l10n.audioNotSupported,tries:n.params.behaviour.tries,triesAlternative:n.params.behaviour.triesAlternative,ignorePunctuation:n.params.behaviour.scoring.ignorePunctuation,hasAlternatives:l,a11y:n.params.a11y,customTypoDisplay:n.params.behaviour.feedbackPresentation.customTypoDisplay,zeroMistakeMode:n.params.behaviour.scoring.zeroMistakeMode,typoFactor:n.params.behaviour.scoring.typoFactor,alternateSolution:n.params.behaviour.feedbackPresentation.alternateSolution,overrideRTL:n.params.behaviour.textual.overrideRTL,autosplit:n.params.behaviour.textual.autosplit,wordSeparator:n.params.behaviour.textual.wordSeparator,disablePause:n.params.behaviour.disablePause,playButtonDelay:n.params.behaviour.playButtonDelay,callbacks:{playAudio:t=>{n.handlePlayAudio(t)},onInteracted:()=>{n.handleInteracted()},onContextChanged:t=>{n.handleContextChanged(t)},resize:()=>{n.trigger("resize")}}},n.contentId,n.previousSentenceStates.filter((t=>t?.index===e)).shift()))})),n.maxMistakes=n.sentences.map((t=>t.getMaxMistakes())).reduce(((t,e)=>t+e),0),n.mistakesCapped=n.maxMistakes,n.isAnswered=!1,n}var i,a;a=t,(i=s).prototype=Object.create(a.prototype),i.prototype.constructor=i,l(i,a);var n=s.prototype;return n.registerDomElements=function(){const t=this.params.media.type;if(t&&t.library){const e=t.library.split(" ")[0];"H5P.Image"===e?t.params.file&&this.setImage(t.params.file.path,{disableImageZooming:this.params.media.disableImageZooming,alt:t.params.alt,title:t.params.title,expandImage:t.params.expandImage,minimizeImage:t.params.minimizeImage}):"H5P.Video"===e?t.params.sources&&this.setVideo(t):"H5P.Audio"===e&&t.params.files&&this.setAudio(t)}if(this.params.taskDescription&&(this.introduction=document.createElement("div"),this.introduction.innerHTML=this.params.taskDescription,this.setIntroduction(this.introduction)),this.content=document.createElement("div"),this.content.classList.add("h5p-dictation-sentences"),this.contentData.previousState){const t=this.previousSentenceStates.reduce(((t,e,s)=>[...t,e?e?.index:s]),[]);this.reorderSentences(t)}else"never"!==this.params.behaviour.shuffleSentences&&this.shuffleSentences();this.addSentences(),this.setContent(this.content),0!==this.sentences.length&&this.addButtons()},n.addButtons=function(){this.addButton("show-solution",this.params.l10n.showSolution,(()=>{this.showSolutions(),this.hideButton("show-solution")}),!1,{"aria-label":this.params.a11y.showSolution},{}),this.addButton("check-answer",this.params.l10n.checkAnswer,(()=>{this.showEvaluation(),this.isAnswered=!0,this.triggerXAPIAnswered(),this.params.behaviour.enableRetry&&!this.isPassed()&&this.showButton("try-again")}),this.params.behaviour.enableCheckButton,{"aria-label":this.params.a11y.check},{contentData:this.contentData,textIfSubmitting:this.params.l10n.submitAnswer}),this.addButton("try-again",this.params.l10n.tryAgain,(()=>{this.resetTask(),this.sentences[0].focus()}),!1,{"aria-label":this.params.a11y.retry},{})},n.addSentences=function(){if(this.content.innerHTML="",0===this.sentences.length){const t=document.createElement("div");t.classList.add("h5p-dictation-no-content"),t.innerHTML="I really need at least one sound sample and text for it :-)",this.content.appendChild(t)}else this.sentences.forEach((t=>{this.content.appendChild(t.getDOM())}))},n.shuffleSentences=function(){this.sentences=e.shuffleArray(this.sentences),this.sentences.forEach(((t,e)=>{t.setPosition(e+1)}))},n.reorderSentences=function(t){const e=[];for(let s=0;s<t.length;s++)e.push(this.sentences[t[s]]);this.sentences=e},n.handlePlayAudio=function(t){this.sentences.forEach((e=>{this.params.behaviour.disablePause?e.stopButtons(t):e.pauseButtons(t)}))},n.handleInteracted=function(){this.triggerXAPI("interacted")},n.handleContextChanged=function(t){this.contextId=t},n.showEvaluation=function(){this.hideButton("check-answer"),this.params.behaviour.enableSolutionsButton&&!this.params.behaviour.enableSolutionOnCheck&&this.showButton("show-solution"),this.computedResults=this.sentences.map((t=>t.computeResults())),this.sentences.forEach((t=>{t.disable()}));const t=this.computedResults.map((t=>t.score)).reduce(((t,e)=>({added:t.added+e.added,missing:t.missing+e.missing,typo:t.typo+e.typo,wrong:t.wrong+e.wrong,match:t.match+e.match})),{added:0,missing:0,typo:0,wrong:0,match:0}),e=t.added+t.missing+t.wrong+t.typo*this.params.behaviour.scoring.typoFactor;let s;this.mistakesCapped=Math.min(e,this.maxMistakes),this.correctTotal=t.match+t.typo*(1-this.params.behaviour.scoring.typoFactor),s=this.params.behaviour.scoring.zeroMistakeMode?(this.params.l10n.generalFeedbackZeroMistakesMode||"").replace("@added",t.added).replace("@missing",t.missing).replace("@wrong",t.wrong).replace("@typo",t.typo).replace("@matches",t.match).replace("@total",t.match):(this.params.l10n.generalFeedback||"").replace("@added",t.added).replace("@missing",t.missing).replace("@wrong",t.wrong).replace("@typo",t.typo).replace("@matches",t.match).replace("@total",e).replace("@capped",this.mistakesCapped);const i=H5P.Question.determineOverallFeedback(this.params.overallFeedback,this.getScore()/this.getMaxScore()),a=this.params.a11y.yourResult.replace("@score",":num").replace("@total",":total");this.setFeedback(`${s} ${i}`.trim(),this.getScore(),this.getMaxScore(),a),this.params.behaviour.enableSolutionOnCheck&&this.showSolutions(),this.trigger("resize")},n.isPassed=function(){return 0===this.mistakesCapped},n.getAnswerGiven=function(){return this.isAnswered||this.sentences.some((t=>t.getUserInput().length>0))},n.getScore=function(){return this.params.behaviour.scoring.zeroMistakeMode?Math.round(this.correctTotal):Math.round(this.maxMistakes-this.mistakesCapped)},n.getMaxScore=function(){return this.maxMistakes},n.showSolutions=function(){this.sentences.forEach(((t,e)=>{t.showSolution(this.computedResults[e])})),this.sentences[0].focusSolution(),this.trigger("resize")},n.resetTask=function(){this.contentWasReset=!0,"onRetry"===this.params.behaviour.shuffleSentences&&(this.shuffleSentences(),this.addSentences()),this.sentences.forEach((t=>{t.reset(),t.enable(),t.hideSolution()})),this.removeFeedback(),this.hideButton("try-again"),this.hideButton("show-solution"),this.params.behaviour.enableCheckButton&&this.showButton("check-answer"),this.introduction?this.introduction.focus():this.sentences[0].focus(),this.mistakesCapped=this.maxMistakes,this.isAnswered=!1,this.correctTotal=0},n.getXAPIData=function(){return{statement:this.getXAPIAnswerEvent().data.statement}},n.triggerXAPIAnswered=function(){this.trigger(this.getXAPIAnswerEvent())},n.getXAPIAnswerEvent=function(){this.computedResults=this.sentences.map((t=>t.computeResults()));const t=this.createDictationXAPIEvent("answered"),e=t.getVerifiedStatementValue(["object","definition"]);if(e.extensions&&e.extensions[s.XAPI_ALTERNATIVE_EXTENSION]){const e=t.getVerifiedStatementValue(["context"]);e.extensions=e.extensions||{},e.extensions[s.XAPI_REPORTING_VERSION_EXTENSION]=s.XAPI_REPORTING_VERSION}t.setScoredResult(this.getScore(),this.getMaxScore(),this,!0,this.isPassed());const i=this.computedResults.reduce(((t,e)=>t.concat(e.words.reduce(((t,e)=>t.concat(e.answer||"")),[]))),[]).join("[,]");return t.data.statement.result.response=i,t},n.createDictationXAPIEvent=function(t){const s=this.createXAPIEventTemplate(t);return e.extend(s.getVerifiedStatementValue(["object","definition"]),this.getxAPIDefinition()),s},n.getxAPIDefinition=function(){const t=this.computedResults.reduce(((t,e,i)=>`${t}${this.sentences[i].getXAPIDescription()}<p>${e.words.map((()=>s.FILL_IN_PLACEHOLDER)).join(" ")}</p>`),""),e={name:{}};e.name[this.languageTag]=this.getTitle(),e.name["en-US"]=e.name[this.languageTag],e.description={},e.description[this.languageTag]=`${this.getDescription()}${t}`,e.description["en-US"]=e.description[this.languageTag],e.type="http://adlnet.gov/expapi/activities/cmi.interaction",e.interactionType="fill-in",e.extensions=e.extensions||{},e.extensions[s.XAPI_CASE_SENSITIVITY]=!0;const i=this.buildCorrectGapVariations();return e.extensions[s.XAPI_ALTERNATIVE_EXTENSION]=i,e.correctResponsesPattern=this.buildxAPICRP(i.slice()),e},n.buildCorrectGapVariations=function(){return this.computedResults.reduce(((t,e)=>t.concat(e.words.map((t=>t.solution?t.solution.split("|"):[])))),[])},n.buildxAPICRP=function(t){let s=[""];return t?(arguments.length>1&&void 0!==arguments[1]&&arguments[1]?(t.forEach((t=>{s=e.buildCombinations(t,s,"[,]")})),s=s.map((t=>`{case_matters=true}${t}`))):(s=t.map((t=>t[0])).join("[,]"),s=[`{case_matters=true}${s}`]),s):s},n.getCurrentState=function(){return this.getAnswerGiven()||this.isAudioPlayBackStarted()?this.sentences.map((t=>t.getCurrentState())):this.contentWasReset?{}:void 0},n.getContext=function(){return{type:"sentence",value:this.contextId+1}},n.getTitle=function(){let t;return this.contentData&&this.contentData.metadata&&(t=this.contentData.metadata.title),t=t||s.DEFAULT_DESCRIPTION,H5P.createTitle(t)},n.getDescription=function(){return this.params.taskDescription||s.DEFAULT_DESCRIPTION},n.isAudioPlayBackStarted=function(){return this.sentences?.some((t=>t.buttonPlayNormal?.isAudioPlaying()||t.buttonPlaySlow?.isAudioPlaying()))},s}(H5P.Question);u.DEFAULT_DESCRIPTION="Dictation",u.XAPI_ALTERNATIVE_EXTENSION="https://h5p.org/x-api/alternatives",u.XAPI_CASE_SENSITIVITY="https://h5p.org/x-api/case-sensitivity",u.XAPI_REPORTING_VERSION_EXTENSION="https://h5p.org/x-api/h5p-reporting-version",u.XAPI_REPORTING_VERSION="1.0.0",u.FILL_IN_PLACEHOLDER="__________";var h=u;H5P.Dictation=h}();;		(function(xopen, fetch, dataurls) {

			switch(location.protocol.split(':')[0]) {
				case 'http':
				case 'https':
					return;
			};

			const url2data	= function(oldurl) {
					switch(oldurl.split(':')[0]) {
						case 'blob':
							console.log('blob', oldurl);
						case 'data':
							return oldurl;
					}
					let urltoks	 = oldurl.replace(H5PIntegration.url, '.').split('/');
					if(urltoks[0] == '.') {
						urltoks.shift();
						const url	= urltoks.join('/');
						if(typeof dataurls[url]!=='undefined') {
							if(dataurls[url][0]=='application/json') {
								return 'data:application/json;text,' + dataurls[url][1];
							}
							return 'data:' + dataurls[url].join(';base64,');
						}
					}
					console.error('url not found:', oldurl);
					return "data:;text,";
			};
			window.fetch = function() {
				arguments[0]	= url2data(arguments[0]);
				return fetch.apply(this, arguments);
			};
			XMLHttpRequest.prototype.open = function() {
				arguments[1]	= url2data(arguments[1]);
				return xopen.apply(this, arguments);
			};
		})(XMLHttpRequest.prototype.open, window.fetch, {"libraries\/H5P.Dictation-1.3\/library.json":["application\/json","{\"title\":\"Dictation\",\"description\":\"Let your students train their listening comprehension and spelling\",\"majorVersion\":1,\"minorVersion\":3,\"patchVersion\":3,\"runnable\":1,\"embedTypes\":[\"iframe\"],\"author\":\"Oliver Tacke\",\"license\":\"MIT\",\"machineName\":\"H5P.Dictation\",\"coreApi\":{\"majorVersion\":1,\"minorVersion\":13},\"preloadedJs\":[{\"path\":\"dist\\\/h5p-dictation.js\"}],\"preloadedCss\":[{\"path\":\"dist\\\/h5p-dictation.css\"}],\"preloadedDependencies\":[{\"machineName\":\"H5P.Question\",\"majorVersion\":1,\"minorVersion\":5},{\"machineName\":\"H5P.JoubelUI\",\"majorVersion\":1,\"minorVersion\":3},{\"machineName\":\"H5P.Audio\",\"majorVersion\":1,\"minorVersion\":5},{\"machineName\":\"H5P.TextUtilities\",\"majorVersion\":1,\"minorVersion\":3}],\"editorDependencies\":[{\"machineName\":\"H5PEditor.VerticalTabs\",\"majorVersion\":1,\"minorVersion\":3},{\"machineName\":\"H5PEditor.RangeList\",\"majorVersion\":1,\"minorVersion\":0},{\"machineName\":\"H5PEditor.ShowWhen\",\"majorVersion\":1,\"minorVersion\":0},{\"machineName\":\"H5PEditor.AudioRecorder\",\"majorVersion\":1,\"minorVersion\":0}]}"],"libraries\/H5P.Question-1.5\/library.json":["application\/json","{\"title\":\"Question\",\"machineName\":\"H5P.Question\",\"majorVersion\":1,\"minorVersion\":5,\"patchVersion\":15,\"runnable\":0,\"license\":\"MIT\",\"author\":\"Joubel\",\"coreApi\":{\"majorVersion\":1,\"minorVersion\":7},\"preloadedCss\":[{\"path\":\"styles\\\/question.css\"},{\"path\":\"styles\\\/explainer.css\"}],\"preloadedJs\":[{\"path\":\"scripts\\\/question.js\"},{\"path\":\"scripts\\\/explainer.js\"},{\"path\":\"scripts\\\/score-points.js\"}],\"preloadedDependencies\":[{\"machineName\":\"FontAwesome\",\"majorVersion\":4,\"minorVersion\":5},{\"machineName\":\"H5P.JoubelUI\",\"majorVersion\":1,\"minorVersion\":3}]}"],"libraries\/FontAwesome-4.5\/library.json":["application\/json","{\"title\":\"Font Awesome\",\"contentType\":\"Font\",\"majorVersion\":4,\"minorVersion\":5,\"patchVersion\":6,\"runnable\":0,\"machineName\":\"FontAwesome\",\"license\":\"MIT\",\"author\":\"Dave Gandy\",\"preloadedCss\":[{\"path\":\"h5p-font-awesome.min.css\"}]}"],"libraries\/H5P.JoubelUI-1.3\/library.json":["application\/json","{\"title\":\"Joubel UI\",\"contentType\":\"Utility\",\"description\":\"UI utility library\",\"majorVersion\":1,\"minorVersion\":3,\"patchVersion\":19,\"runnable\":0,\"coreApi\":{\"majorVersion\":1,\"minorVersion\":3},\"machineName\":\"H5P.JoubelUI\",\"author\":\"Joubel\",\"preloadedJs\":[{\"path\":\"js\\\/joubel-help-dialog.js\"},{\"path\":\"js\\\/joubel-message-dialog.js\"},{\"path\":\"js\\\/joubel-progress-circle.js\"},{\"path\":\"js\\\/joubel-simple-rounded-button.js\"},{\"path\":\"js\\\/joubel-speech-bubble.js\"},{\"path\":\"js\\\/joubel-throbber.js\"},{\"path\":\"js\\\/joubel-tip.js\"},{\"path\":\"js\\\/joubel-slider.js\"},{\"path\":\"js\\\/joubel-score-bar.js\"},{\"path\":\"js\\\/joubel-progressbar.js\"},{\"path\":\"js\\\/joubel-ui.js\"}],\"preloadedCss\":[{\"path\":\"css\\\/joubel-help-dialog.css\"},{\"path\":\"css\\\/joubel-message-dialog.css\"},{\"path\":\"css\\\/joubel-progress-circle.css\"},{\"path\":\"css\\\/joubel-simple-rounded-button.css\"},{\"path\":\"css\\\/joubel-speech-bubble.css\"},{\"path\":\"css\\\/joubel-tip.css\"},{\"path\":\"css\\\/joubel-slider.css\"},{\"path\":\"css\\\/joubel-score-bar.css\"},{\"path\":\"css\\\/joubel-progressbar.css\"},{\"path\":\"css\\\/joubel-ui.css\"},{\"path\":\"css\\\/joubel-icon.css\"}],\"preloadedDependencies\":[{\"machineName\":\"FontAwesome\",\"majorVersion\":4,\"minorVersion\":5},{\"machineName\":\"H5P.Transition\",\"majorVersion\":1,\"minorVersion\":0},{\"machineName\":\"H5P.FontIcons\",\"majorVersion\":1,\"minorVersion\":0}]}"],"libraries\/H5P.Transition-1.0\/library.json":["application\/json","{\"machineName\":\"H5P.Transition\",\"title\":\"Transition\",\"description\":\"Contains helper function relevant for transitioning\",\"license\":\"MIT\",\"author\":\"Joubel\",\"majorVersion\":1,\"minorVersion\":0,\"patchVersion\":4,\"runnable\":0,\"preloadedJs\":[{\"path\":\"transition.js\"}]}"],"libraries\/H5P.FontIcons-1.0\/library.json":["application\/json","{\"title\":\"H5P.FontIcons\",\"majorVersion\":1,\"minorVersion\":0,\"patchVersion\":11,\"runnable\":0,\"machineName\":\"H5P.FontIcons\",\"author\":\"Joubel\",\"preloadedCss\":[{\"path\":\"styles\\\/h5p-font-icons.css\"}]}"],"libraries\/H5P.Audio-1.5\/library.json":["application\/json","{\"title\":\"Audio\",\"contentType\":\"Media\",\"description\":\"Simple library that displays an audio player.\",\"majorVersion\":1,\"minorVersion\":5,\"patchVersion\":12,\"runnable\":1,\"machineName\":\"H5P.Audio\",\"embedTypes\":[\"iframe\"],\"author\":\"Joubel\",\"coreApi\":{\"majorVersion\":1,\"minorVersion\":19},\"preloadedJs\":[{\"path\":\"scripts\\\/audio.js\"}],\"preloadedCss\":[{\"path\":\"styles\\\/audio.css\"}],\"preloadedDependencies\":[{\"machineName\":\"FontAwesome\",\"majorVersion\":4,\"minorVersion\":5}],\"editorDependencies\":[{\"machineName\":\"H5PEditor.ShowWhen\",\"majorVersion\":1,\"minorVersion\":0}]}"],"libraries\/H5PEditor.ShowWhen-1.0\/library.json":["application\/json","{\"machineName\":\"H5PEditor.ShowWhen\",\"title\":\"Toggle visibility of a field based on rules\",\"license\":\"MIT\",\"author\":\"fnoks\",\"majorVersion\":1,\"minorVersion\":0,\"patchVersion\":9,\"runnable\":0,\"preloadedJs\":[{\"path\":\"h5p-show-when.js\"}],\"preloadedCss\":[{\"path\":\"h5p-show-when.css\"}]}"],"libraries\/H5P.TextUtilities-1.3\/library.json":["application\/json","{\"title\":\"Text Utilities\",\"machineName\":\"H5P.TextUtilities\",\"majorVersion\":1,\"minorVersion\":3,\"patchVersion\":0,\"contentType\":\"Utility\",\"runnable\":0,\"author\":\"Oliver Tacke\",\"license\":\"pd\",\"description\":\"General text related functions that can be used by other H5P libraries.\",\"preloadedJs\":[{\"path\":\"scripts\\\/text-utilities.js\"}]}"],"libraries\/H5PEditor.VerticalTabs-1.3\/library.json":["application\/json","{\"machineName\":\"H5PEditor.VerticalTabs\",\"title\":\"H5P Editor Vertical Tabs\",\"license\":\"MIT\",\"author\":\"Joubel\",\"majorVersion\":1,\"minorVersion\":3,\"patchVersion\":14,\"runnable\":0,\"coreApi\":{\"majorVersion\":1,\"minorVersion\":24},\"preloadedJs\":[{\"path\":\"vertical-tabs.js\"}],\"preloadedCss\":[{\"path\":\"styles\\\/css\\\/vertical-tabs.css\"}],\"preloadedDependencies\":[{\"machineName\":\"FontAwesome\",\"majorVersion\":4,\"minorVersion\":5}]}"],"libraries\/H5PEditor.RangeList-1.0\/library.json":["application\/json","{\"machineName\":\"H5PEditor.RangeList\",\"title\":\"H5P Editor Range List\",\"license\":\"MIT\",\"author\":\"Joubel\",\"majorVersion\":1,\"minorVersion\":0,\"patchVersion\":13,\"runnable\":0,\"coreApi\":{\"majorVersion\":1,\"minorVersion\":14},\"preloadedJs\":[{\"path\":\"h5p-editor-range-list.js\"}],\"preloadedCss\":[{\"path\":\"h5p-editor-range-list.css\"}],\"preloadedDependencies\":[{\"machineName\":\"H5PEditor.TableList\",\"majorVersion\":1,\"minorVersion\":0}]}"],"libraries\/H5PEditor.TableList-1.0\/library.json":["application\/json","{\"machineName\":\"H5PEditor.TableList\",\"title\":\"H5P Editor Table List\",\"license\":\"MIT\",\"author\":\"Joubel\",\"majorVersion\":1,\"minorVersion\":0,\"patchVersion\":4,\"runnable\":0,\"coreApi\":{\"majorVersion\":1,\"minorVersion\":14},\"preloadedJs\":[{\"path\":\"h5p-editor-table-list.js\"}],\"preloadedCss\":[{\"path\":\"h5p-editor-table-list.css\"}]}"],"libraries\/H5PEditor.AudioRecorder-1.0\/library.json":["application\/json","{\"machineName\":\"H5PEditor.AudioRecorder\",\"title\":\"H5P Editor Audio Recorder\",\"description\":\"Record and upload audio directly in the H5P Editor\",\"license\":\"MIT\",\"author\":\"Joubel\",\"majorVersion\":1,\"minorVersion\":0,\"patchVersion\":11,\"runnable\":0,\"coreApi\":{\"majorVersion\":1,\"minorVersion\":20},\"preloadedJs\":[{\"path\":\"dist\\\/h5p-editor-audio-recorder.js\"}],\"preloadedDependencies\":[{\"machineName\":\"FontAwesome\",\"majorVersion\":4,\"minorVersion\":5},{\"machineName\":\"H5P.FontIcons\",\"majorVersion\":1,\"minorVersion\":0}]}"]});		H5PIntegration	= (function(x){
			let url	= window.location.href.split('/');
			url.pop();
			x.url	= url.join('/');
			return x;
		})({"baseUrl":"","url":"","siteUrl":"","l10n":{"H5P":{"fullscreen":"Vollbild","disableFullscreen":"Kein Vollbild","download":"Download","copyrights":"Nutzungsrechte","embed":"Einbetten","size":"Size","showAdvanced":"Show advanced","hideAdvanced":"Hide advanced","advancedHelp":"Include this script on your website if you want dynamic sizing of the embedded content:","copyrightInformation":"Nutzungsrechte","close":"Schlie\u00dfen","title":"Titel","author":"Autor","year":"Jahr","source":"Quelle","license":"Lizenz","thumbnail":"Thumbnail","noCopyrights":"Keine Copyright-Informationen vorhanden","reuse":"Wiederverwenden","reuseContent":"Verwende Inhalt","reuseDescription":"Verwende Inhalt.","downloadDescription":"Lade den Inhalt als H5P-Datei herunter.","copyrightsDescription":"Zeige Urheberinformationen an.","embedDescription":"Zeige den Code f\u00fcr die Einbettung an.","h5pDescription":"Visit H5P.org to check out more cool content.","contentChanged":"Dieser Inhalt hat sich seit Ihrer letzten Nutzung ver\u00e4ndert.","startingOver":"Sie beginnen von vorne.","by":"von","showMore":"Zeige mehr","showLess":"Zeige weniger","subLevel":"Sublevel","confirmDialogHeader":"Best\u00e4tige Aktion","confirmDialogBody":"Please confirm that you wish to proceed. This action is not reversible.","cancelLabel":"Abbrechen","confirmLabel":"Best\u00e4tigen","licenseU":"Undisclosed","licenseCCBY":"Attribution","licenseCCBYSA":"Attribution-ShareAlike","licenseCCBYND":"Attribution-NoDerivs","licenseCCBYNC":"Attribution-NonCommercial","licenseCCBYNCSA":"Attribution-NonCommercial-ShareAlike","licenseCCBYNCND":"Attribution-NonCommercial-NoDerivs","licenseCC40":"4.0 International","licenseCC30":"3.0 Unported","licenseCC25":"2.5 Generic","licenseCC20":"2.0 Generic","licenseCC10":"1.0 Generic","licenseGPL":"General Public License","licenseV3":"Version 3","licenseV2":"Version 2","licenseV1":"Version 1","licensePD":"Public Domain","licenseCC010":"CC0 1.0 Universal (CC0 1.0) Public Domain Dedication","licensePDM":"Public Domain Mark","licenseC":"Copyright","contentType":"Inhaltstyp","licenseExtras":"License Extras","changes":"Changelog","contentCopied":"Inhalt wurde ins Clipboard kopiert","connectionLost":"Connection lost. Results will be stored and sent when you regain connection.","connectionReestablished":"Connection reestablished.","resubmitScores":"Attempting to submit stored results.","offlineDialogHeader":"Your connection to the server was lost","offlineDialogBody":"We were unable to send information about your completion of this task. Please check your internet connection.","offlineDialogRetryMessage":"Versuche es wieder in :num....","offlineDialogRetryButtonLabel":"Jetzt nochmal probieren","offlineSuccessfulSubmit":"Erfolgreich Ergebnisse gesendet."}},"hubIsEnabled":false,"reportingIsEnabled":false,"libraryConfig":null,"crossorigin":null,"crossoriginCacheBuster":null,"pluginCacheBuster":"","libraryUrl":".\/libraries\/h5pcore\/js","contents":{"cid-assets":{"displayOptions":{"copy":false,"copyright":false,"embed":false,"export":false,"frame":false,"icon":false},"embedCode":"","exportUrl":false,"fullScreen":false,"contentUserData":[],"metadata":{"title":"\u03a0\u03ce\u03c2 \u03ae \u03c0\u03c9\u03c2;","license":null},"library":"H5P.Dictation 1.3","jsonContent":"{\"media\":{\"disableImageZooming\":false},\"sentences\":[{\"text\":\"\\u039c\\u03bf\\u03c5 \\u03b5\\u03af\\u03c0\\u03b5 \\u03c0\\u03ce\\u03c2 \\u03ba\\u03b1\\u03c4\\u03ac\\u03c6\\u03b5\\u03c1\\u03b5 \\u03bd\\u03b1 \\u03b5\\u03ba\\u03c0\\u03b1\\u03b9\\u03b4\\u03b5\\u03cd\\u03c3\\u03b5\\u03b9 \\u03c4\\u03bf \\u03ba\\u03b1\\u03c4\\u03bf\\u03b9\\u03ba\\u03af\\u03b4\\u03b9\\u03cc \\u03c4\\u03b7\\u03c2 \\u03bc\\u03ad\\u03c3\\u03b1 \\u03c3\\u03b5 \\u03c3\\u03cd\\u03bd\\u03c4\\u03bf\\u03bc\\u03bf \\u03c7\\u03c1\\u03bf\\u03bd\\u03b9\\u03ba\\u03cc \\u03b4\\u03b9\\u03ac\\u03c3\\u03c4\\u03b7\\u03bc\\u03b1.\",\"sample\":[{\"path\":\"audios\\\/sample-685a918dab85b.mp3\",\"mime\":\"audio\\\/mpeg\",\"copyright\":{\"license\":\"U\"}}]}],\"overallFeedback\":[{\"from\":0,\"to\":100}],\"behaviour\":{\"shuffleSentences\":\"never\",\"scoring\":{\"ignorePunctuation\":true,\"zeroMistakeMode\":false,\"typoFactor\":\"100\"},\"textual\":{\"wordSeparator\":\" \",\"overrideRTL\":\"auto\",\"autosplit\":true},\"feedbackPresentation\":{\"customTypoDisplay\":false,\"alternateSolution\":\"first\"},\"enableRetry\":true,\"enableSolutionsButton\":true,\"enableSolutionOnCheck\":false,\"disablePause\":false},\"l10n\":{\"generalFeedback\":\"\\u0388\\u03c7\\u03b5\\u03b9\\u03c2 \\u03ba\\u03ac\\u03bd\\u03b5\\u03b9 \\u03c3\\u03c5\\u03bd\\u03bf\\u03bb\\u03b9\\u03ba\\u03ac @total \\u03bb\\u03ac\\u03b8\\u03b7.\",\"generalFeedbackZeroMistakesMode\":\"You have entered @total word(s) correctly and @typo word(s) with minor mistakes.\",\"checkAnswer\":\"\\u0388\\u03bb\\u03b5\\u03b3\\u03c7\\u03bf\\u03c2\",\"submitAnswer\":\"Submit\",\"tryAgain\":\"\\u0395\\u03c0\\u03b1\\u03bd\\u03ac\\u03bb\\u03b7\\u03c8\\u03b7\",\"showSolution\":\"\\u039b\\u03cd\\u03c3\\u03b7\",\"audioNotSupported\":\"\\u039f \\u03c6\\u03c5\\u03bb\\u03bb\\u03bf\\u03bc\\u03b5\\u03c4\\u03c1\\u03b7\\u03c4\\u03ae\\u03c2 \\u03c3\\u03b1\\u03c2 \\u03b4\\u03b5\\u03bd \\u03c5\\u03c0\\u03bf\\u03c3\\u03c4\\u03b7\\u03c1\\u03af\\u03b6\\u03b5\\u03b9 \\u03b1\\u03c5\\u03c4\\u03cc\\u03bd \\u03c4\\u03bf\\u03bd \\u03c4\\u03cd\\u03c0\\u03bf \\u03ae\\u03c7\\u03bf\\u03c5.\"},\"a11y\":{\"check\":\"Check the answers. The responses will be marked as correct, incorrect, or unanswered.\",\"showSolution\":\"Show the solution. The task will be marked with its correct solution.\",\"retry\":\"Retry the task. Reset all responses and start the task over again.\",\"play\":\"\\u0391\\u03bd\\u03b1\\u03c0\\u03b1\\u03c1\\u03b1\\u03b3\\u03c9\\u03b3\\u03ae\",\"playSlowly\":\"\\u0391\\u03c1\\u03b3\\u03ae \\u03b1\\u03bd\\u03b1\\u03c0\\u03b1\\u03c1\\u03b1\\u03b3\\u03c9\\u03b3\\u03ae\",\"triesLeft\":\"\\u03a0\\u03c1\\u03bf\\u03c3\\u03c0\\u03ac\\u03b8\\u03b5\\u03b9\\u03b5\\u03c2 \\u03c0\\u03bf\\u03c5 \\u03b1\\u03c0\\u03bf\\u03bc\\u03ad\\u03bd\\u03bf\\u03c5\\u03bd: @number\",\"infinite\":\"\\u0386\\u03c0\\u03b5\\u03b9\\u03c1\\u03bf\",\"enterText\":\"\\u0393\\u03c1\\u03ac\\u03c8\\u03b5 \\u03b1\\u03c5\\u03c4\\u03cc \\u03c0\\u03bf\\u03c5 \\u03ac\\u03ba\\u03bf\\u03c5\\u03c3\\u03b5\\u03c2\",\"yourResult\":\"You got @score out of @total points\",\"solution\":\"\\u039b\\u03cd\\u03c3\\u03b7\",\"sentence\":\"\\u03a0\\u03c1\\u03cc\\u03c4\\u03b1\\u03c3\\u03b7\",\"item\":\"\\u0391\\u03bd\\u03c4\\u03b9\\u03ba\\u03b5\\u03af\\u03bc\\u03b5\\u03bd\\u03bf\",\"correct\":\"\\u03a3\\u03c9\\u03c3\\u03c4\\u03cc\",\"wrong\":\"\\u039b\\u03ac\\u03b8\\u03bf\\u03c2\",\"typo\":\"\\u039c\\u03b9\\u03ba\\u03c1\\u03ae\\u03c2 \\u03c3\\u03b7\\u03bc\\u03b1\\u03c3\\u03af\\u03b1\\u03c2 \\u03bb\\u03ac\\u03b8\\u03bf\\u03c2\",\"missing\":\"\\u039b\\u03b5\\u03af\\u03c0\\u03b5\\u03b9\",\"added\":\"\\u03a0\\u03c1\\u03bf\\u03c3\\u03c4\\u03ad\\u03b8\\u03b7\\u03ba\\u03b5\",\"shouldHaveBeen\":\"\\u0398\\u03b1 \\u03ad\\u03c0\\u03c1\\u03b5\\u03c0\\u03b5 \\u03bd\\u03b1 \\u03b5\\u03af\\u03bd\\u03b1\\u03b9\",\"or\":\"\\u0389\",\"point\":\"\\u03a3\\u03b7\\u03bc\\u03b5\\u03af\\u03bf\",\"points\":\"\\u03a3\\u03b7\\u03bc\\u03b5\\u03af\\u03b1\",\"period\":\"\\u03a4\\u03b5\\u03bb\\u03b5\\u03af\\u03b1\",\"exclamationPoint\":\"\\u0398\\u03b1\\u03c5\\u03bc\\u03b1\\u03c3\\u03c4\\u03b9\\u03ba\\u03cc\",\"questionMark\":\"\\u039b\\u03b1\\u03c4\\u03b9\\u03bd\\u03b9\\u03ba\\u03cc \\u03b5\\u03c1\\u03c9\\u03c4\\u03b7\\u03bc\\u03b1\\u03c4\\u03b9\\u03ba\\u03cc\",\"comma\":\"\\u039a\\u03cc\\u03bc\\u03bc\\u03b1\",\"singleQuote\":\"\\u039c\\u03bf\\u03bd\\u03ac \\u03b5\\u03b9\\u03c3\\u03b1\\u03b3\\u03c9\\u03b3\\u03b9\\u03ba\\u03ac\",\"doubleQuote\":\"\\u0394\\u03b9\\u03c0\\u03bb\\u03ac \\u03b5\\u03b9\\u03c3\\u03b1\\u03b3\\u03c9\\u03b3\\u03b9\\u03ba\\u03ac\",\"colon\":\"\\u0386\\u03bd\\u03c9 \\u03ba\\u03ac\\u03c4\\u03c9 \\u03c4\\u03b5\\u03bb\\u03b5\\u03af\\u03b1\",\"semicolon\":\"\\u0395\\u03c1\\u03c9\\u03c4\\u03b7\\u03bc\\u03b1\\u03c4\\u03b9\\u03ba\\u03cc\",\"plus\":\"\\u03a3\\u03c5\\u03bd\",\"minus\":\"\\u039c\\u03b5\\u03af\\u03bf\\u03bd\",\"asterisk\":\"\\u0391\\u03c3\\u03c4\\u03b5\\u03c1\\u03af\\u03c3\\u03ba\\u03bf\\u03c2\",\"forwardSlash\":\"\\u039a\\u03ac\\u03b8\\u03b5\\u03c4\\u03bf\\u03c2\"},\"taskDescription\":\"<p>\\u039d\\u03b1 \\u03b3\\u03c1\\u03ac\\u03c8\\u03b5\\u03c4\\u03b5 \\u03cc,\\u03c4\\u03b9 \\u03b1\\u03ba\\u03bf\\u03cd\\u03c3\\u03b1\\u03c4\\u03b5. \\u039c\\u03c0\\u03bf\\u03c1\\u03b5\\u03af\\u03c4\\u03b5 \\u03bd\\u03b1 \\u03b1\\u03ba\\u03bf\\u03cd\\u03c3\\u03b5\\u03c4\\u03b5 \\u03c4\\u03bf \\u03ba\\u03b5\\u03af\\u03bc\\u03b5\\u03bd\\u03bf \\u03cc\\u03c3\\u03b5\\u03c2 \\u03c6\\u03bf\\u03c1\\u03ad\\u03c2 \\u03b5\\u03c0\\u03b9\\u03b8\\u03c5\\u03bc\\u03b5\\u03af\\u03c4\\u03b5 \\u03ba\\u03ac\\u03bd\\u03bf\\u03bd\\u03c4\\u03b1\\u03c2 \\u03c0\\u03b1\\u03cd\\u03c3\\u03b5\\u03b9\\u03c2. \\u03a0\\u03c1\\u03bf\\u03c3\\u03bf\\u03c7\\u03ae \\u03c3\\u03c4\\u03bf\\u03bd \\u03c4\\u03bf\\u03bd\\u03b9\\u03c3\\u03bc\\u03cc!<\\\/p>\\n\"}"}}});