var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };

var H5P = H5P || {};

/**
 * H5P-Timer
 *
 * General purpose timer that can be used by other H5P libraries.
 *
 * @param {H5P.jQuery} $
 */
H5P.Timer = function ($, EventDispatcher) {
  /**
   * Create a timer.
   *
   * @constructor
   * @param {number} [interval=Timer.DEFAULT_INTERVAL] - The update interval.
   */
  function Timer() {
    var interval = arguments.length <= 0 || arguments[0] === undefined ? Timer.DEFAULT_INTERVAL : arguments[0];

    var self = this;

    // time on clock and the time the clock has run
    var clockTimeMilliSeconds = 0;
    var playingTimeMilliSeconds = 0;

    // used to update recurring notifications
    var clockUpdateMilliSeconds = 0;

    // indicators for total running time of the timer
    var firstDate = null;
    var startDate = null;
    var lastDate = null;

    // update loop
    var loop = null;

    // timer status
    var status = Timer.STOPPED;

    // indicate counting direction
    var mode = Timer.FORWARD;

    // notifications
    var notifications = [];

    // counter for notifications;
    var notificationsIdCounter = 0;

    // Inheritance
    H5P.EventDispatcher.call(self);

    // sanitize interval
    if (Timer.isInteger(interval)) {
      interval = Math.max(interval, 1);
    }
    else {
      interval = Timer.DEFAULT_INTERVAL;
    }

    /**
     * Get the timer status.
     *
     * @public
     * @return {number} The timer status.
     */
    self.getStatus = function () {
      return status;
    };

    /**
     * Get the timer mode.
     *
     * @public
     * @return {number} The timer mode.
     */
    self.getMode = function () {
      return mode;
    };

    /**
     * Get the time that's on the clock.
     *
     * @private
     * @return {number} The time on the clock.
     */
    var getClockTime = function getClockTime() {
      return clockTimeMilliSeconds;
    };

    /**
     * Get the time the timer was playing so far.
     *
     * @private
     * @return {number} The time played.
     */
    var getPlayingTime = function getPlayingTime() {
      return playingTimeMilliSeconds;
    };

    /**
     * Get the total running time from play() until stop().
     *
     * @private
     * @return {number} The total running time.
     */
    var getRunningTime = function getRunningTime() {
      if (!firstDate) {
        return 0;
      }
      if (status !== Timer.STOPPED) {
        return new Date().getTime() - firstDate.getTime();
      }
      else {
        return !lastDate ? 0 : lastDate.getTime() - firstDate;
      }
    };

    /**
     * Get one of the times.
     *
     * @public
     * @param {number} [type=Timer.TYPE_CLOCK] - Type of the time to get.
     * @return {number} Clock Time, Playing Time or Running Time.
     */
    self.getTime = function () {
      var type = arguments.length <= 0 || arguments[0] === undefined ? Timer.TYPE_CLOCK : arguments[0];

      if (!Timer.isInteger(type)) {
        return;
      }
      // break will never be reached, but for consistency...
      switch (type) {
        case Timer.TYPE_CLOCK:
          return getClockTime();
          break;
        case Timer.TYPE_PLAYING:
          return getPlayingTime();
          break;
        case Timer.TYPE_RUNNING:
          return getRunningTime();
          break;
        default:
          return getClockTime();
      }
    };

    /**
     * Set the clock time.
     *
     * @public
     * @param {number} time - The time in milliseconds.
     */
    self.setClockTime = function (time) {
      if ($.type(time) === 'string') {
        time = Timer.toMilliseconds(time);
      }
      if (!Timer.isInteger(time)) {
        return;
      }
      // notifications only need an update if changing clock against direction
      clockUpdateMilliSeconds = (time - clockTimeMilliSeconds) * mode < 0 ? time - clockTimeMilliSeconds : 0;
      clockTimeMilliSeconds = time;
    };

    /**
     * Reset the timer.
     *
     * @public
     */
    self.reset = function () {
      if (status !== Timer.STOPPED) {
        return;
      }
      clockTimeMilliSeconds = 0;
      playingTimeMilliSeconds = 0;

      firstDate = null;
      lastDate = null;

      loop = null;

      notifications = [];
      notificationsIdCounter = 0;
      self.trigger('reset', {}, {bubbles: true, external: true});
    };

    /**
     * Set timer mode.
     *
     * @public
     * @param {number} mode - The timer mode.
     */
    self.setMode = function (direction) {
      if (direction !== Timer.FORWARD && direction !== Timer.BACKWARD) {
        return;
      }
      mode = direction;
    };

    /**
     * Start the timer.
     *
     * @public
     */
    self.play = function () {
      if (status === Timer.PLAYING) {
        return;
      }
      if (!firstDate) {
        firstDate = new Date();
      }
      startDate = new Date();
      status = Timer.PLAYING;
      self.trigger('play', {}, {bubbles: true, external: true});
      update();
    };

    /**
     * Pause the timer.
     *
     * @public
     */
    self.pause = function () {
      if (status !== Timer.PLAYING) {
        return;
      }
      status = Timer.PAUSED;
      self.trigger('pause', {}, {bubbles: true, external: true});
    };

    /**
     * Stop the timer.
     *
     * @public
     */
    self.stop = function () {
      if (status === Timer.STOPPED) {
        return;
      }
      lastDate = new Date();
      status = Timer.STOPPED;
      self.trigger('stop', {}, {bubbles: true, external: true});
    };

    /**
     * Update the timer until Timer.STOPPED.
     *
     * @private
     */
    var update = function update() {
      var currentMilliSeconds = 0;
      // stop because requested
      if (status === Timer.STOPPED) {
        clearTimeout(loop);
        return;
      }

      //stop because countdown reaches 0
      if (mode === Timer.BACKWARD && clockTimeMilliSeconds <= 0) {
        self.stop();
        return;
      }

      // update times
      if (status === Timer.PLAYING) {
        currentMilliSeconds = new Date().getTime() - startDate;
        clockTimeMilliSeconds += currentMilliSeconds * mode;
        playingTimeMilliSeconds += currentMilliSeconds;
      }
      startDate = new Date();

      checkNotifications();

      loop = setTimeout(function () {
        update();
      }, interval);
    };

    /**
     * Get next notification id.
     *
     * @private
     * @return {number} id - The next id.
     */
    var getNextNotificationId = function getNextNotificationId() {
      return notificationsIdCounter++;
    };

    /**
     * Set a notification
     *
     * @public
     * @param {Object|String} params - Parameters for the notification.
     * @callback callback - Callback function.
     * @return {number} ID of the notification.
     */
    self.notify = function (params, callback) {
      var id = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : getNextNotificationId();

      // common default values for the clock timer
      // TODO: find a better place for this, maybe a JSON file?
      var defaults = {};
      defaults['every_tenth_second'] = { "repeat": 100 };
      defaults['every_second'] = { "repeat": 1000 };
      defaults['every_minute'] = { "repeat": 60000 };
      defaults['every_hour'] = { "repeat": 3600000 };

      // Sanity check for callback function
      if (!callback instanceof Function) {
        return;
      }

      // Get default values
      if ($.type(params) === 'string') {
        params = defaults[params];
      }

      if (params !== null && (typeof params === 'undefined' ? 'undefined' : _typeof(params)) === 'object') {
        // Sanitize type
        if (!params.type) {
          params.type = Timer.TYPE_CLOCK;
        }
        else {
          if (!Timer.isInteger(params.type)) {
            return;
          }
          if (params.type < Timer.TYPE_CLOCK || params.type > Timer.TYPE_RUNNING) {
            return;
          }
        }

        // Sanitize mode
        if (!params.mode) {
          params.mode = Timer.NOTIFY_ABSOLUTE;
        }
        else {
          if (!Timer.isInteger(params.mode)) {
            return;
          }
          if (params.mode < Timer.NOTIFY_ABSOLUTE || params.type > Timer.NOTIFY_RELATIVE) {
            return;
          }
        }

        // Sanitize calltime
        if (!params.calltime) {
          params.calltime = params.mode === Timer.NOTIFY_ABSOLUTE ? self.getTime(params.type) : 0;
        }
        else {
          if ($.type(params.calltime) === 'string') {
            params.calltime = Timer.toMilliseconds(params.calltime);
          }
          if (!Timer.isInteger(params.calltime)) {
            return;
          }
          if (params.calltime < 0) {
            return;
          }
          if (params.mode === Timer.NOTIFY_RELATIVE) {
            params.calltime = Math.max(params.calltime, interval);
            if (params.type === Timer.TYPE_CLOCK) {
              // clock could be running backwards
              params.calltime *= mode;
            }
            params.calltime += self.getTime(params.type);
          }
        }

        // Sanitize repeat
        if ($.type(params.repeat) === 'string') {
          params.repeat = Timer.toMilliseconds(params.repeat);
        }
        // repeat must be >= interval (ideally multiple of interval)
        if (params.repeat !== undefined) {
          if (!Timer.isInteger(params.repeat)) {
            return;
          }
          params.repeat = Math.max(params.repeat, interval);
        }
      }
      else {
        // neither object nor string
        return;
      }

      // add notification
      notifications.push({
        'id': id,
        'type': params.type,
        'calltime': params.calltime,
        'repeat': params.repeat,
        'callback': callback
      });

      return id;
    };

    /**
     * Remove a notification.
     *
     * @public
     * @param {number} id - The id of the notification.
     */
    self.clearNotification = function (id) {
      notifications = $.grep(notifications, function (item) {
        return item.id === id;
      }, true);
    };

    /**
     * Set a new starting time for notifications.
     *
     * @private
     * @param elements {Object] elements - The notifications to be updated.
     * @param deltaMilliSeconds {Number} - The time difference to be set.
     */
    var updateNotificationTime = function updateNotificationTime(elements, deltaMilliSeconds) {
      if (!Timer.isInteger(deltaMilliSeconds)) {
        return;
      }
      elements.forEach(function (element) {
        // remove notification
        self.clearNotification(element.id);

        //rebuild notification with new data
        self.notify({
          'type': element.type,
          'calltime': self.getTime(element.type) + deltaMilliSeconds,
          'repeat': element.repeat
        }, element.callback, element.id);
      });
    };

    /**
     * Check notifications for necessary callbacks.
     *
     * @private
     */
    var checkNotifications = function checkNotifications() {
      var backwards = 1;
      var elements = [];

      // update recurring clock notifications if clock was changed
      if (clockUpdateMilliSeconds !== 0) {
        elements = $.grep(notifications, function (item) {
          return item.type === Timer.TYPE_CLOCK && item.repeat != undefined;
        });
        updateNotificationTime(elements, clockUpdateMilliSeconds);
        clockUpdateMilliSeconds = 0;
      }

      // check all notifications for triggering
      notifications.forEach(function (element) {
        /*
         * trigger if notification time is in the past
         * which means calltime >= Clock Time if mode is BACKWARD (= -1)
         */
        backwards = element.type === Timer.TYPE_CLOCK ? mode : 1;
        if (element.calltime * backwards <= self.getTime(element.type) * backwards) {
          // notify callback function
          element.callback.apply(this);

          // remove notification
          self.clearNotification(element.id);

          // You could use updateNotificationTime() here, but waste some time

          // rebuild notification if it should be repeated
          if (element.repeat) {
            self.notify({
              'type': element.type,
              'calltime': self.getTime(element.type) + element.repeat * backwards,
              'repeat': element.repeat
            }, element.callback, element.id);
          }
        }
      });
    };
  }

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

  /**
   * Generate timecode elements from milliseconds.
   *
   * @private
   * @param {number} milliSeconds - The milliseconds.
   * @return {Object} The timecode elements.
   */
  var toTimecodeElements = function toTimecodeElements(milliSeconds) {
    var years = 0;
    var month = 0;
    var weeks = 0;
    var days = 0;
    var hours = 0;
    var minutes = 0;
    var seconds = 0;
    var tenthSeconds = 0;

    if (!Timer.isInteger(milliSeconds)) {
      return;
    }
    milliSeconds = Math.round(milliSeconds / 100);
    tenthSeconds = milliSeconds - Math.floor(milliSeconds / 10) * 10;
    seconds = Math.floor(milliSeconds / 10);
    minutes = Math.floor(seconds / 60);
    hours = Math.floor(minutes / 60);
    days = Math.floor(hours / 24);
    weeks = Math.floor(days / 7);
    month = Math.floor(days / 30.4375); // roughly (30.4375 = mean of 4 years)
    years = Math.floor(days / 365); // roughly (no leap years considered)
    return {
      years: years,
      month: month,
      weeks: weeks,
      days: days,
      hours: hours,
      minutes: minutes,
      seconds: seconds,
      tenthSeconds: tenthSeconds
    };
  };

  /**
   * Extract humanized time element from time for concatenating.
   *
   * @public
   * @param {number} milliSeconds - The milliSeconds.
   * @param {string} element - Time element: hours, minutes, seconds or tenthSeconds.
   * @param {boolean} [rounded=false] - If true, element value will be rounded.
   * @return {number} The time element.
   */
  Timer.extractTimeElement = function (time, element) {
    var rounded = arguments.length <= 2 || arguments[2] === undefined ? false : arguments[2];

    var timeElements = null;

    if ($.type(time) === 'string') {
      time = Timer.toMilliseconds(time);
    }
    if (!Timer.isInteger(time)) {
      return;
    }
    if ($.type(element) !== 'string') {
      return;
    }
    if ($.type(rounded) !== 'boolean') {
      return;
    }

    if (rounded) {
      timeElements = {
        years: Math.round(time / 31536000000),
        month: Math.round(time / 2629800000),
        weeks: Math.round(time / 604800000),
        days: Math.round(time / 86400000),
        hours: Math.round(time / 3600000),
        minutes: Math.round(time / 60000),
        seconds: Math.round(time / 1000),
        tenthSeconds: Math.round(time / 100)
      };
    }
    else {
      timeElements = toTimecodeElements(time);
    }

    return timeElements[element];
  };

  /**
   * Convert time in milliseconds to timecode.
   *
   * @public
   * @param {number} milliSeconds - The time in milliSeconds.
   * @return {string} The humanized timecode.
   */
  Timer.toTimecode = function (milliSeconds) {
    var timecodeElements = null;
    var timecode = '';

    var minutes = 0;
    var seconds = 0;

    if (!Timer.isInteger(milliSeconds)) {
      return;
    }
    if (milliSeconds < 0) {
      return;
    }

    timecodeElements = toTimecodeElements(milliSeconds);
    minutes = Math.floor(timecodeElements['minutes'] % 60);
    seconds = Math.floor(timecodeElements['seconds'] % 60);

    // create timecode
    if (timecodeElements['hours'] > 0) {
      timecode += timecodeElements['hours'] + ':';
    }
    if (minutes < 10) {
      timecode += '0';
    }
    timecode += minutes + ':';
    if (seconds < 10) {
      timecode += '0';
    }
    timecode += seconds + '.';
    timecode += timecodeElements['tenthSeconds'];

    return timecode;
  };

  /**
   * Convert timecode to milliseconds.
   *
   * @public
   * @param {string} timecode - The timecode.
   * @return {number} Milliseconds derived from timecode
   */
  Timer.toMilliseconds = function (timecode) {
    var head = [];
    var tail = '';

    var hours = 0;
    var minutes = 0;
    var seconds = 0;
    var tenthSeconds = 0;

    if (!Timer.isTimecode(timecode)) {
      return;
    }

    // thx to the regexp we know everything can be converted to a legit integer in range
    head = timecode.split('.')[0].split(':');
    while (head.length < 3) {
      head = ['0'].concat(head);
    }
    hours = parseInt(head[0]);
    minutes = parseInt(head[1]);
    seconds = parseInt(head[2]);

    tail = timecode.split('.')[1];
    if (tail) {
      tenthSeconds = Math.round(parseInt(tail) / Math.pow(10, tail.length - 1));
    }

    return (hours * 36000 + minutes * 600 + seconds * 10 + tenthSeconds) * 100;
  };

  /**
   * Check if a string is a timecode.
   *
   * @public
   * @param {string} value - String to check
   * @return {boolean} true, if string is a timecode
   */
  Timer.isTimecode = function (value) {
    var reg_timecode = /((((((\d+:)?([0-5]))?\d:)?([0-5]))?\d)(\.\d+)?)/;

    if ($.type(value) !== 'string') {
      return false;
    }

    return value === value.match(reg_timecode)[0] ? true : false;
  };

  // Workaround for IE and potentially other browsers within Timer object
  Timer.isInteger = Timer.isInteger || function(value) {
    return typeof value === "number" && isFinite(value) && Math.floor(value) === value;
  };

  // Timer states
  /** @constant {number} */
  Timer.STOPPED = 0;
  /** @constant {number} */
  Timer.PLAYING = 1;
  /** @constant {number} */
  Timer.PAUSED = 2;

  // Timer directions
  /** @constant {number} */
  Timer.FORWARD = 1;
  /** @constant {number} */
  Timer.BACKWARD = -1;

  /** @constant {number} */
  Timer.DEFAULT_INTERVAL = 10;

  // Counter types
  /** @constant {number} */
  Timer.TYPE_CLOCK = 0;
  /** @constant {number} */
  Timer.TYPE_PLAYING = 1;
  /** @constant {number} */
  Timer.TYPE_RUNNING = 2;

  // Notification types
  /** @constant {number} */
  Timer.NOTIFY_ABSOLUTE = 0;
  /** @constant {number} */
  Timer.NOTIFY_RELATIVE = 1;

  return Timer;
}(H5P.jQuery, H5P.EventDispatcher);
;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 || {};

/**
 * 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.FindTheWords = (function ($, UI) {
  const ELEMENT_MIN_SIZE = 32; // PX
  const ELEMENT_MAX_SIZE = 64; // PX
  const MARGIN = 8; //PX
  const VOCABULARY_INLINE_WIDTH = 200;// PX

  /**
   * FindTheWords.
   * @class H5P.FindTheWords
   * @extends H5P.EventDispatcher
   * @param {Object} options
   * @param {number} id
   * @param {Object} extras
   */
  function FindTheWords(options, id, extras) {

    /** @alias H5P.FindTheWords# */
    this.id = id;
    this.extras = extras;
    this.numFound = 0;
    this.isAttempted = false;
    this.isGameStarted = false;

    // Only take the unique words
    const vocabulary = options.wordList
      .split(',')
      .map(function (word) {
        return word.trim();
      })
      .filter(function (word, pos, self) {
        return self.indexOf(word) === pos;
      });

    this.options = $.extend(true, {
      vocabulary: vocabulary,
      height: 5,
      width: 5,
      fillBlanks: true,
      maxAttempts: 5,
      l10n: {
        wordListHeader: 'Find the words'
      }
    }, options);

    H5P.EventDispatcher.call(this);

    this.gridParams = {
      height: this.options.height,
      width: this.options.width,
      orientations: filterOrientations(options.behaviour.orientations),
      fillBlanks: this.options.fillBlanks,
      maxAttempts: this.options.maxAttempts,
      preferOverlap: options.behaviour.preferOverlap,
      vocabulary: this.options.vocabulary,
      gridActive: true,
      fillPool: this.options.behaviour.fillPool
    };

    this.grid = new FindTheWords.WordGrid(this.gridParams);
    this.vocabulary = new FindTheWords.Vocabulary(
      this.options.vocabulary,
      this.options.behaviour.showVocabulary,
      this.options.l10n.wordListHeader
    );
    this.registerDOMElements();

    // responsive functionality
    this.on('resize', function () {
      const currentSize = this.elementSize;
      const currentVocMod = this.isVocModeBlock;
      this.calculateElementSize();
      this.setVocabularyMode();

      if (this.elementSize !== currentSize) {
        this.$puzzleContainer.empty();
        this.grid.appendTo(this.$puzzleContainer, this.elementSize );
        this.grid.drawGrid(MARGIN);

        // If there are already marked elements on the grid mark them
        if (!this.grid.options.gridActive) {
          this.grid.enableGrid();
          this.grid.mark(this.vocabulary.getFound());
          this.grid.disableGrid();
          this.grid.mark(this.vocabulary.getSolved());
        }
        else {
          this.grid.mark(this.vocabulary.getFound());
        }

        this.registerGridEvents();
      }

      // vocabulary adjustments on resize
      if (this.options.behaviour.showVocabulary) {
        if (currentVocMod !== this.isVocModeBlock ) {
          this.vocabulary.setMode(this.isVocModeBlock);
          if (this.isVocModeBlock) {
            this.$puzzleContainer.removeClass('puzzle-inline').addClass('puzzle-block');
          }
          else {
            //initial update has to be done manually
            this.$playArea.css({'width': parseInt(this.$gameContainer.width()) + VOCABULARY_INLINE_WIDTH});
            this.$puzzleContainer.removeClass('puzzle-block').addClass('puzzle-inline');
          }
        }
      }

      // Make the playarea just to fit its content
      if (! this.isVocModeBlock) {
        this.$playArea.css({'width': parseInt(this.$gameContainer.width()) + 2});
      }
      else {
        this.$playArea.css({'width': parseInt(this.$puzzleContainer.width()) + 2});
      }
    });
  }

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

  // private and all prototype function goes there

  /**
   * filterOrientations - Mapping of directions from semantics to those used by algorithm.
   * @param {Object} directions
   * @return {Object[]}
   */
  const filterOrientations = function (directions) {
    return Object.keys(directions).filter(function (key) {
      return directions[key];
    });
  };

  /**
   * registerDOMElements.
   */
  FindTheWords.prototype.registerDOMElements = function () {
    const that = this;

    this.$playArea = $('<div />', {
      class: 'h5p-play-area'
    });

    this.$taskDescription = $('<div />', {
      class: 'h5p-task-description',
      html: this.options.taskDescription,
      tabIndex: 0,
    });

    // timer part
    this.$timer = $('<div/>', {
      class: 'time-status',
      tabindex: 0,
      html: '<span role="term" ><em class="fa fa-clock-o" ></em>' +
        this.options.l10n.timeSpent + '</span >:' +
        '<span role="definition"  class="h5p-time-spent" >0:00</span>'
    });
    this.timer = new FindTheWords.Timer(this.$timer.find('.h5p-time-spent'));

    // counter part
    const counterText = that.options.l10n.found
      .replace('@found', '<span class="h5p-counter">0</span>')
      .replace('@totalWords', '<span><strong>' + this.vocabulary.words.length + '</strong></span>');

    this.$counter = $('<div/>', {
      class: 'counter-status',
      tabindex: 0,
      html: '<div role="term"><span role="definition">' + counterText + '</span></div>'
    });
    this.counter = new FindTheWords.Counter(this.$counter.find('.h5p-counter'));

    // feedback plus progressBar
    this.$feedback = $('<div/>', {
      class: 'feedback-element',
      tabindex: '0'
    });
    this.$progressBar = UI.createScoreBar(this.vocabulary.words.length, 'scoreBarLabel');

    // buttons section
    that.$submitButton = that.createButton('submit', 'check', that.options.l10n.check, that.gameSubmitted);
    if (this.options.behaviour.enableShowSolution) {
      this.$showSolutionButton = this.createButton('solution', 'eye', this.options.l10n.showSolution, that.showSolutions);
    }
    if (this.options.behaviour.enableRetry) {
      this.$retryButton = this.createButton('retry', 'undo', this.options.l10n.tryAgain, that.resetTask);
    }

    // container elements
    this.$gameContainer = $('<div class="game-container"/>');
    this.$puzzleContainer = $('<div class="puzzle-container puzzle-inline" tabIndex="0" role="grid" />');
    this.$vocabularyContainer = $('<div class="vocabulary-container" tabIndex="0" />');
    this.$footerContainer = $('<div class="footer-container" />');
    this.$statusContainer = $('<div />', {
      class: 'game-status',
      'aria-label': 'game-status',
      role: 'group',
      tabindex: '0'
    });
    this.$feedbackContainer = $('<div class="feedback-container"/>');
    this.$buttonContainer = $('<div class="button-container" />');
  };

  /**
   * createButton - creating all buttons used in this game.
   * @param {string} name Buttonname.
   * @param {string} icon Fa icon name.
   * @param {string} param Button text parameter.
   * @param {function} callback Callback function.
   * @return {H5P.JoubelUI.Button} Joubel ui button object.
   */
  FindTheWords.prototype.createButton = function (name, icon, param, callback) {
    const cfunction = callback.bind(this);
    return UI.createButton({
      title: name,
      click: cfunction,
      html: '<span><i class="fa fa-' + icon + '" aria-hidden="true"></i></span>' + param
    });
  };

  /**
   * calculateElementSize - calculate the grid element size according to the container width.
   */
  FindTheWords.prototype.calculateElementSize = function () {
    const containerWidth = this.$container.width();
    const gridCol = this.grid.wordGrid[0].length;
    const gridMaxWidth = gridCol * ELEMENT_MAX_SIZE + 2 * MARGIN;
    const gridElementStdSize = (containerWidth - 2 * MARGIN) / gridCol;

    if (gridMaxWidth < containerWidth) {
      this.elementSize = ELEMENT_MAX_SIZE;
    }
    else if (gridElementStdSize > ELEMENT_MIN_SIZE) {
      this.elementSize = gridElementStdSize;
    }
    else {
      this.elementSize = ELEMENT_MAX_SIZE;
    }
  };

  /**
   * setVocabularyMode - set vocabulary mode (either inline or block).
   */
  FindTheWords.prototype.setVocabularyMode = function () {
    const gridCol = this.grid.wordGrid[0].length;
    this.isVocModeBlock = (this.$container.width() - (gridCol * this.elementSize + 2 * MARGIN) > VOCABULARY_INLINE_WIDTH) ? false : true;
  };

  /**
   * gameSubmitted - callback function for check button.
   */
  FindTheWords.prototype.gameSubmitted = function () {
    const totalScore = this.vocabulary.words.length;
    const scoreText = this.options.l10n.score
      .replace('@score', this.numFound)
      .replace('@total', totalScore);

    this.timer.stop();
    this.$progressBar.setScore(this.numFound);
    this.$feedback.html(scoreText);
    this.$submitButton = this.$submitButton.detach();
    this.grid.disableGrid();

    if (totalScore !== this.numFound) {
      if (this.options.behaviour.enableShowSolution) {
        this.$showSolutionButton.appendTo(this.$buttonContainer);
      }
    }

    if (this.options.behaviour.enableRetry) {
      this.$retryButton.appendTo(this.$buttonContainer);
    }

    this.$feedbackContainer.addClass('feedback-show'); //show feedbackMessage
    this.$feedback.focus();

    const xAPIEvent = this.createXAPIEventTemplate('answered');
    this.addQuestionToXAPI(xAPIEvent);
    this.addResponseToXAPI(xAPIEvent);
    this.trigger(xAPIEvent);

    this.trigger('resize');
  };

  /**
   * showSolutions - call back function for show solution button.
   */
  FindTheWords.prototype.showSolutions = function () {
    this.grid.disableGrid();
    this.grid.mark(this.vocabulary.getNotFound());
    this.vocabulary.solveWords();
    this.$showSolutionButton.detach();
    this.$vocabularyContainer.focus();
    this.trigger('resize');
  };

  /**
   * resetTask - resetting the game.
   */
  FindTheWords.prototype.resetTask = function () {
    this.numFound = 0;
    this.timer.reset();
    this.counter.reset();
    this.$progressBar.reset();
    this.$puzzleContainer.empty();
    this.vocabulary.reset();

    if (this.$showSolutionButton) {
      this.$showSolutionButton.detach();
    }

    this.$retryButton.detach();
    this.$feedbackContainer.removeClass('feedback-show');

    this.grid = new FindTheWords.WordGrid(this.gridParams);
    this.grid.appendTo(this.$puzzleContainer, this.elementSize);
    this.grid.drawGrid(MARGIN);
    this.grid.enableGrid();
    this.registerGridEvents();

    this.$submitButton.appendTo(this.$buttonContainer);
    this.$puzzleContainer.focus();

    this.trigger('resize');
  };

  /**
   * Check whether user is able to play the game.
   * @return {boolean}
   */
  FindTheWords.prototype.getAnswerGiven = function () {
    return this.isAttempted;
  };

  /**
   *  getScore - Return the score obtained.
   * @return {number}
   */
  FindTheWords.prototype.getScore = function () {
    return this.numFound;
  };

  /**
   * Turn the maximum possible score that can be obtained.
   * @return {number}
   */
  FindTheWords.prototype.getMaxScore = function () {
    return this.vocabulary.words.length;
  };

  /**
   * getXAPIData - Get xAPI data.
   * @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-6}
   * @return {Object} xApi data statement
   */
  FindTheWords.prototype.getXAPIData = function () {
    const xAPIEvent = this.createXAPIEventTemplate('answered');
    this.addQuestionToXAPI(xAPIEvent);
    this.addResponseToXAPI(xAPIEvent);
    return {
      statement: xAPIEvent.data.statement
    };
  };

  /**
   * addQuestionToXAPI - Add the question to the definition part of an xAPIEvent.
   * @param {H5P.XAPIEvent} xAPIEvent
   */
  FindTheWords.prototype.addQuestionToXAPI = function (xAPIEvent) {
    const definition = xAPIEvent.getVerifiedStatementValue(
      ['object', 'definition']
    );
    definition.description = {
      'en-US': this.options.taskDescription
    };
    definition.type =
      'http://adlnet.gov/expapi/activities/cmi.interaction';
    definition.interactionType = 'choice';
    definition.correctResponsesPattern = [];
    definition.correctResponsesPattern[0] = this.vocabulary.words.join([',']);
  };

  /**
   * Add the response part to an xAPI event.
   * @param {H5P.XAPIEvent} xAPIEvent
   */
  FindTheWords.prototype.addResponseToXAPI = function (xAPIEvent) {
    const maxScore = this.getMaxScore();
    const score = this.getScore();
    const success = (score === maxScore);
    const response = this.vocabulary.getFound().join('[,]');

    xAPIEvent.setScoredResult(score, maxScore, this, true, success);
    xAPIEvent.data.statement.result.response = response;
  };

  /**
   * registerGridEvents.
   */
  FindTheWords.prototype.registerGridEvents = function () {
    const that = this;

    this.grid.on('drawStart', function () {
      if (!that.isGameStarted) {
        that.timer.play();
        that.triggerXAPI('interacted');
        that.isGameStarted = true;
      }
    });

    this.grid.on('drawEnd', function (event) {
      that.isAttempted = true;
      if (that.vocabulary.checkWord(event.data['markedWord'])) {
        that.numFound++;
        that.counter.increment();
        that.grid.markWord(event.data['wordObject']);
        if (that.numFound === that.vocabulary.words.length) {
          that.gameSubmitted();
        }
      }
    });
  };

  /**
   * attach - main attach function.
   * @param {H5P.jQuery} $container Description.
   */
  FindTheWords.prototype.attach = function ($container) {
    this.$container = $container.addClass('h5p-find-the-words');
    this.triggerXAPI('attempted');

    if (this.grid) {
      this.calculateElementSize();
      this.grid.appendTo(this.$puzzleContainer, this.elementSize );
      this.$puzzleContainer.appendTo(this.$gameContainer);
      if (this.options.behaviour.showVocabulary) {
        this.setVocabularyMode();
        this.vocabulary.appendTo(this.$vocabularyContainer, this.isVocModeBlock);
        this.$vocabularyContainer.appendTo(this.$gameContainer);
      }
    }

    this.$timer.appendTo(this.$statusContainer);
    this.$counter.appendTo(this.$statusContainer);

    this.$feedback.appendTo(this.$feedbackContainer);
    this.$progressBar.appendTo(this.$feedbackContainer);

    this.$submitButton.appendTo(this.$buttonContainer);

    //append status and feedback and button containers to footer
    this.$statusContainer.appendTo(this.$footerContainer);
    this.$feedbackContainer.appendTo(this.$footerContainer);
    this.$buttonContainer.appendTo(this.$footerContainer);

    //append description , cards and footer to main container.
    this.$taskDescription.appendTo(this.$playArea);
    this.$gameContainer.appendTo(this.$playArea);
    this.$footerContainer.appendTo(this.$playArea);
    this.$playArea.appendTo(this.$container);

    this.grid.drawGrid(MARGIN);
    this.registerGridEvents();
    this.trigger('resize');
  };

  return FindTheWords;

}) (H5P.jQuery, H5P.JoubelUI);
;(function (FindTheWords, EventDispatcher, $) {

  /**
   * WordGrid - Handles the word grid part of the game.
   * @class H5P.FindTheWords.WordGrid
   * @extends H5P.EventDispatcher
   * @param {Object} params Description.
   */
  FindTheWords.WordGrid = function (params) {
    /** @alias H5P.FindTheWords.WordGrid# */
    // extending the default parameter set for the grid
    this.options = params;

    EventDispatcher.call(this);

    this.createWordGrid();
  };

  FindTheWords.WordGrid.prototype = Object.create(EventDispatcher.prototype);
  FindTheWords.WordGrid.prototype.constructor = FindTheWords.WordGrid;

  // get i th element position based on the current position for different orientations
  const orientations = {
    horizontal: function (x, y, i) {
      return {
        x: x + i,
        y: y
      };
    },
    horizontalBack: function (x, y, i) {
      return {
        x: x - i,
        y: y
      };
    },
    vertical: function (x, y, i) {
      return {
        x: x,
        y: y + i
      };
    },
    verticalUp: function (x, y, i) {
      return {
        x: x,
        y: y - i
      };
    },
    diagonal: function (x, y, i) {
      return {
        x: x + i,
        y: y + i
      };
    },
    diagonalBack: function (x, y, i) {
      return {
        x: x - i,
        y: y + i
      };
    },
    diagonalUp: function (x, y, i) {
      return {
        x: x + i,
        y: y - i
      };
    },
    diagonalUpBack: function (x, y, i) {
      return {
        x: x - i,
        y: y - i
      };
    }
  };

  /*
   * Determines if an orientation is possible given the starting square (x,y),
   * the height (h) and width (w) of the puzzle, and the length of the word (l).
   * Returns true if the word will fit starting at the square provided using
   * the specified orientation.
   */
  const checkOrientations = {
    horizontal: function (x, y, h, w, l) {
      return w >= x + l;
    },
    horizontalBack: function (x, y, h, w, l) {
      return x + 1 >= l;
    },
    vertical: function (x, y, h, w, l) {
      return h >= y + l;
    },
    verticalUp: function (x, y, h, w, l) {
      return y + 1 >= l;
    },
    diagonal: function (x, y, h, w, l) {
      return (w >= x + l) && (h >= y + l);
    },
    diagonalBack: function (x, y, h, w, l) {
      return (x + 1 >= l) && (h >= y + l);
    },
    diagonalUp: function (x, y, h, w, l) {
      return (w >= x + l) && (y + 1 >= l);
    },
    diagonalUpBack: function (x, y, h, w, l) {
      return (x + 1 >= l) && (y + 1 >= l);
    }
  };

  /*
   *  Determines the next possible valid square given the square (x,y) was ]
   *  invalid and a word lenght of (l).  This greatly reduces the number of
   *  squares that must be checked. Returning {x: x+1, y: y} will always work
   *  but will not be optimal.
   */
  const skipOrientations = {
    horizontal: function (x, y) {
      return {
        x: 0,
        y: y + 1
      };
    },
    horizontalBack: function (x, y, l) {
      return {
        x: l - 1,
        y: y
      };
    },
    vertical: function (x, y) {
      return {
        x: 0,
        y: y + 100
      };
    },
    verticalUp: function (x, y, l) {
      return {
        x: 0,
        y: l - 1
      };
    },
    diagonal: function (x, y) {
      return {
        x: 0,
        y: y + 1
      };
    },
    diagonalBack: function (x, y, l) {
      return {
        x: l - 1,
        y: x >= l - 1 ? y + 1 : y
      };
    },
    diagonalUp: function (x, y, l) {
      return {
        x: 0,
        y: y < l - 1 ? l - 1 : y + 1
      };
    },
    diagonalUpBack: function (x, y, l) {
      return {
        x: l - 1,
        y: x >= l - 1 ? y + 1 : y
      };
    }
  };

  /**
   * calcOverlap - returns the overlap if the word can be fitted with the grid parameters provided.
   * @param {string} word Word to be fitted.
   * @param {Object[]} wordGrid Grid to which word needs to be fitted.
   * @param {number} x Starting x cordinate.
   * @param {nuber} y Starting y cordinate.
   * @param {function} fnGetSquare Function to get the next grid pos as per the specified direction.
   * @return {number} Overlap value if it can be fitted , -1 otherwise.
   */
  const calcOverlap = function (word, wordGrid, x, y, fnGetSquare) {
    let overlap = 0;

    // traverse the squares to determine if the word fits
    for (let index = 0 ; index < word.length; index++) {
      const next = fnGetSquare(x, y, index);
      const square = wordGrid[next.y][next.x];
      if (square === word[index]) {
        overlap++;
      }
      else if (square !== '') {
        return -1;
      }
    }

    return overlap;
  };

  /**
   * findBestLocations - Find the best possible location for a word in the grid.
   * @param {Object[]} wordGrid
   * @param {Object} options
   * @param {string} word
   */
  const findBestLocations = function (wordGrid, options, word) {
    const locations = [];
    const height = options.height;
    const width = options.width;
    const wordLength = word.length;
    let maxOverlap = 0;

    options.orientations.forEach(function (orientation) {

      const check = checkOrientations[orientation];
      const next = orientations[orientation];
      const skipTo = skipOrientations[orientation];

      let x = 0;
      let y = 0;

      while (y < height) {
        if (check(x, y, height, width, wordLength)) {
          const overlap = calcOverlap(word, wordGrid, x, y, next);
          if (overlap >= maxOverlap || (!options.preferOverlap && overlap > -1 )) {
            maxOverlap = overlap;
            locations.push({
              x: x,
              y: y,
              orientation: orientation,
              overlap: overlap
            });
          }
          x++;
          if ( x >= width) {
            x = 0;
            y++;
          }
        }
        else {
          const nextPossible = skipTo(x, y, wordLength);
          x = nextPossible.x;
          y = nextPossible.y;
        }
      }
    });
    return locations;
  };

  /**
   * placeWordInGrid - find the best location and place the word.
   * @param {Object[]} wordGrid
   * @param {Object} options
   * @param {string} word
   */
  const placeWordInGrid = function (wordGrid, options, word) {
    const locations = findBestLocations(wordGrid, options, word);
    if (locations.length === 0) {
      return false;
    }

    const selectedLoc = locations[Math.floor(Math.random() * locations.length)];
    for (let index = 0; index < word.length; index++) {
      const next = orientations[selectedLoc.orientation](selectedLoc.x, selectedLoc.y, index);
      wordGrid[next.y][next.x] = word[index];
    }
    return true;
  };

  /**
   * fillGrid - Create an empty grid and fill it with words.
   * @param {Object[]} words Description.
   * @param {Object} options Description.
   * @return {Object[]|null} Grid array if all words can be fitted, else null.
   */
  const fillGrid = function (words, options) {
    const wordGrid = [];
    for (let i = 0; i < options.height; i++) {
      wordGrid[i] = [];
      for (let j = 0; j < options.width; j++) {
        wordGrid[i][j] = '';
      }
    }

    for (const i in words) {
      if (!placeWordInGrid(wordGrid, options, words[i])) {
        return null;
      }
    }
    return wordGrid;
  };

  /**
   * fillBlanks - fill the unoccupied spaces with blanks.
   * @param {Object[]} wordGrid
   * @param {string} fillPool
   * @return {Object[]} Resulting word grid.
   */
  const fillBlanks = function (wordGrid, fillPool) {
    for (let i = 0; i < wordGrid.length; i++) {
      for (let j = 0;j < wordGrid[0].length; j++) {
        if (!wordGrid[i][j]) {
          const randomLetter = Math.floor(Math.random() * fillPool.length);
          wordGrid[i][j] = fillPool[randomLetter];
        }
      }
    }
    return wordGrid;
  };

  /**
   * calculateCordinates - function to calculate the cordinates & grid postions at which the event occured.
   * @param {number} x X-cordinate of the event.
   * @param {number} y Y-cordinate of the event.
   * @param {number} elementSize Current element size.
   * @return {Object[]} [normalized x, normalized y, row ,col].
   */
  const calculateCordinates = function (x, y, elementSize) {
    const row1 = Math.floor(x / elementSize);
    const col1 = Math.floor(y / elementSize);
    const x_click = row1 * elementSize + (elementSize / 2);
    const y_click = col1 * elementSize + (elementSize / 2);
    return [x_click, y_click, row1, col1];
  };

  /*
   * function to  process the line drawn to find if it is a valid marking
   * in terms of possible grid directions
   * returns directional value if it is a valid marking
   * else return false
   */

  /**
   * getValidDirection - process the line drawn to find if it is a valid marking.
   * @param {number} x1 Starting x cordinate.
   * @param {number} y1 Starting y cordinate.
   * @param {number} x2 Ending x cordinate.
   * @param {number} y2 Ending y cordinate.
   * @return {Object[]|boolean} Direction array if a valid marking, false otherwise.
   */
  const getValidDirection = function (x1, y1, x2, y2) {
    const dirx = (x2 > x1) ? 1 : ((x2 < x1) ? -1 : 0);
    const diry = (y2 > y1) ? 1 : ((y2 < y1) ? -1 : 0);
    let y = y1;
    let x = x1;

    if (dirx !== 0) {
      while (x !== x2) {
        x = x + dirx;
        y = y + diry;
      }
    }
    else {
      while (y !== y2) {
        y = y + diry;
      }
    }

    if (y2 === y) {
      return [dirx, diry];
    }
    else {
      return false;
    }
  };

  // All event handlers are registered here

  /**
   * mouseDownEventHandler.
   * @param {Object} e Event Object.
   * @param {HTMLelement} canvas Html5 canvas element.
   * @param {number} elementSize Element size.
   * @return {Object[]}
   */
  const mouseDownEventHandler = function (e, canvas, elementSize) {
    const x = e.pageX - $(canvas).offset().left;
    const y = e.pageY - $(canvas).offset().top;
    return calculateCordinates(x, y, elementSize);
  };


  /*
   * event handler for handling mousemove events
   * @private
   */

  /**
   * mouseMoveEventHandler.
   * @param {Object} e Event Object.
   * @param {HTMLelement} canvas Html5 Canvas Element.
   * @param {Object[]} srcPos Position from which the movement started.
   * @param {number} eSize  Current element size.
   */
  const mouseMoveEventHandler = function (e, canvas, srcPos, eSize) {
    const offsetTop = ($(canvas).offset().top > eSize * 0.75) ? Math.floor(eSize * 0.75) : $(canvas).offset().top;
    const desX = e.pageX - $(canvas).offset().left;
    const desY = e.pageY - Math.abs(offsetTop);
    const context = canvas.getContext('2d');

    // Draw the current marking
    context.clearRect(0, 0, context.canvas.width, context.canvas.height);
    context.fillStyle = 'rgba(107,177,125,0.3)';
    context.beginPath();
    context.lineCap = 'round';
    context.moveTo(srcPos[0] - (eSize / 8), srcPos[1] + (offsetTop / 8));
    context.strokeStyle = 'rgba(107,177,125,0.4)';
    context.lineWidth = Math.floor(eSize / 2);
    context.lineTo(desX - (eSize / 8), desY + (offsetTop / 8));
    context.stroke();
    context.closePath();
  };

  /*
   * event handler for handling mouseup events
   * @private
   */

  /**
   * mouseUpEventHandler.
   * @param {Object} e Event Object.
   * @param {HTMLelement} canvas Html5 Canvas Element.
   * @param {number} elementSize Current element size.
   * @param {Object[]} clickStart Starting Event location.
   * @return {Object} return staring,ending and direction of the current marking.
   */
  const mouseUpEventHandler = function (e, canvas, elementSize, clickStart) {
    let wordObject = {};
    const offsetTop = ($(canvas).offset().top > elementSize * 0.75) ? Math.floor(elementSize * 0.75) * (-1) : $(canvas).offset().top;
    const x = e.pageX - $(canvas).offset().left;
    const y = e.pageY - Math.abs(offsetTop);
    const clickEnd = calculateCordinates(x, y, elementSize);
    const context = canvas.getContext('2d');

    if ((Math.abs(clickEnd[0] - x) < 20) && (Math.abs(clickEnd[1] - y) < 15)) {
      // Drag ended within permissible range
      wordObject = {
        'start': clickStart,
        'end': clickEnd,
        'dir': getValidDirection(clickStart[2], clickStart[3], clickEnd[2], clickEnd[3])
      };
    }

    // Clear if there any markings started
    context.closePath();
    context.clearRect(0, 0, canvas.width, canvas.height);
    return wordObject;
  };

  /**
   * touchHandler - Mapping touchevents to corresponding mouse events.
   * @param {Object} event Description.
   */
  const touchHandler = function (event) {
    const touches = event.changedTouches;
    const  first = touches[0];
    const simulatedEvent = document.createEvent('MouseEvent');

    let type = '';
    switch (event.type) {
      case 'touchstart':
        type = 'mousedown';
        break;
      case 'touchmove':
        type = 'mousemove';
        break;
      case 'touchend':
        type = 'mouseup';
        break;
      default:
        return;
    }

    // Created and fire a simulated mouse event
    simulatedEvent.initMouseEvent(type, true, true, window, 1,
      first.screenX, first.screenY,
      first.clientX, first.clientY, false,
      false, false, false, 0 /*left*/, null);
    first.target.dispatchEvent(simulatedEvent);
    event.preventDefault();
  };

  FindTheWords.WordGrid.prototype.createWordGrid = function () {
    let wordGrid = null ;
    let attempts = 0;

    // sorting the words by length speedup the word fitting algorithm
    const wordList = this.options.vocabulary.slice(0).sort(function (a, b) {
      return (a.length < b.length);
    });

    while (!wordGrid) {
      while (!wordGrid && attempts++ < this.options.maxAttempts) {
        wordGrid = fillGrid(wordList, this.options);
      }

      // if grid cannot be formed in the current dimensions
      if (!wordGrid) {
        this.options.height++;
        this.options.width++;
        attempts = 0;
      }
    }

    // fill in empty spaces with random letters
    if (this.options.fillBlanks) {
      wordGrid = fillBlanks(wordGrid, this.options.fillPool);
    }

    // set the output puzzle
    this.wordGrid = wordGrid;
  };

  /**
   * markWord - mark the word on the output canvas (permanent).
   * @param {Object} wordParams
   */
  FindTheWords.WordGrid.prototype.markWord = function (wordParams) {
    const dirKey = wordParams['directionKey'];
    const clickStart = wordParams['start'];
    const clickEnd = wordParams['end'];
    const context = this.$outputCanvas[0].getContext('2d');
    const offsetTop = (this.$container.offset().top > this.elementSize * 0.75) ? Math.floor(this.elementSize * 0.75) * (-1) : this.$container.offset().top;
    const topRadius = Math.floor(this.elementSize / 8);
    const bottomRadius = Math.abs(Math.floor(offsetTop / 8));
    const lineWidth = Math.floor(this.elementSize / 4);

    let startingAngle;

    // set the drawing property values
    context.lineWidth = 2;
    context.strokeStyle = 'rgba(107,177,125,0.9)';
    context.fillStyle = 'rgba(107,177,125,0.3)';

    if (!this.options.gridActive) {
      context.strokeStyle = 'rgba(51, 102, 255,0.9)';
      context.fillStyle = 'rgba(51, 102, 255,0.1)';
      context.setLineDash([8, 4]);
    }

    // find the arc starting angle depending on the direction
    switch (dirKey) {
      case 'horizontal': {
        startingAngle = (Math.PI / 2);
        break;
      }
      case 'horizontalBack': {
        startingAngle = -(Math.PI / 2);
        break;
      }
      case 'diagonal': {
        startingAngle = 3 * (Math.PI / 4);
        break;
      }
      case 'diagonalBack': {
        startingAngle = 5 * (Math.PI / 4);
        break;
      }
      case 'diagonalUp': {
        startingAngle = (Math.PI / 4);
        break;
      }
      case 'diagonalUpBack': {
        startingAngle = -(Math.PI / 4);
        break;
      }
      case 'vertical': {
        startingAngle = (Math.PI);
        break;
      }
      case 'verticalUp': {
        startingAngle = 0;
        break;
      }
    }

    // start drawing
    context.beginPath();
    context.arc(clickStart[0] - topRadius, clickStart[1] + bottomRadius, lineWidth, startingAngle, startingAngle + (Math.PI));
    context.arc(clickEnd[0] - topRadius, clickEnd[1] + bottomRadius, lineWidth, startingAngle + (Math.PI), startingAngle + (2 * Math.PI));
    context.closePath();
    context.stroke();
    context.fill();
  };

  /**
   * mark - mark the words if they are not found.
   * @param {Object[]} wordList
   */
  FindTheWords.WordGrid.prototype.mark = function (wordList) {
    const words = wordList;
    const that = this;
    const options = {
      height: this.wordGrid.length,
      width: this.wordGrid[0].length,
      orientations: this.options.orientations,
      preferOverlap: this.options.preferOverlap
    };
    const found = [];
    const notFound = [];

    words.forEach(function (word) {
      const locations = findBestLocations(that.wordGrid, options, word);
      if (locations.length > 0 && locations[0].overlap === word.length) {
        locations[0].word = word;
        found.push(locations[0]);
      }
      else {
        notFound.push(word);
      }
    });

    this.markSolution(found);
  };

  /**
   * markSolution.
   * @param {Object[]} solutions
   */
  FindTheWords.WordGrid.prototype.markSolution = function (solutions) {
    const that = this;

    solutions.forEach(function (solution) {
      const next = orientations[solution.orientation];
      const word = solution.word;
      const startX = solution.x;
      const startY = solution.y;
      const endPos = next(startX, startY, word.length - 1);
      const clickStartX = startX * that.elementSize + (that.elementSize / 2);
      const clickStartY = startY * that.elementSize + (that.elementSize / 2);
      const clickEndX = endPos.x * that.elementSize + (that.elementSize / 2);
      const clickEndY = endPos.y * that.elementSize + (that.elementSize / 2);
      const wordParams = {
        'start': [clickStartX, clickStartY, startX, startY],
        'end': [clickEndX, clickEndY, endPos.x, endPos.y],
        'directionKey': solution.orientation
      };
      that.markWord(wordParams);
    });
  };

  /**
   * disableGrid.
   */
  FindTheWords.WordGrid.prototype.disableGrid = function () {
    this.options.gridActive = false;
  };

  /**
   * enableGrid.
   */
  FindTheWords.WordGrid.prototype.enableGrid = function () {
    this.options.gridActive = true;
  };

  /**
   * appendTo - Placing the container for drawing the grid.
   * @param {H5P.jQuery} $container
   * @param {number} elementSize
   */
  FindTheWords.WordGrid.prototype.appendTo = function ($container, elementSize) {
    this.$container = $container;
    this.canvasWidth = elementSize * this.wordGrid[0].length;
    this.canvasHeight = elementSize * this.wordGrid.length;
    this.elementSize = elementSize;
    $container.css('height', this.canvasHeight);
    $container.css('width', this.canvasWidth);
  };

  /**
   * drawGrid - draw the letter on the canvas element provided.
   * @param {number} margin Description.
   */
  FindTheWords.WordGrid.prototype.drawGrid = function (margin) {
    const that = this;

    const marginResp = (Math.floor(that.elementSize / 8) < margin) ? (Math.floor(that.elementSize / 8)) : margin;
    const offsetTop = (that.$container.offset().top > that.elementSize * 0.75) ? Math.floor(that.elementSize * 0.75) : that.$container.offset().top;

    this.$gridCanvas = $('<canvas id="grid-canvas" class="canvas-element" height="' + that.canvasHeight + 'px" width="' + that.canvasWidth + 'px" />').appendTo(that.$container);
    this.$outputCanvas = $('<canvas class="canvas-element" height="' + that.canvasHeight + 'px" width="' + that.canvasWidth + 'px"/>').appendTo(that.$container);
    this.$drawingCanvas = $('<canvas id="drawing-canvas" class="canvas-element" height="' + that.canvasHeight + 'px" width="' + that.canvasWidth + 'px"/>').appendTo(that.$container);

    const ctx1 = this.$gridCanvas[0].getContext('2d');
    const offset = that.$container.offset();

    ctx1.clearRect(offset.left, offset.top, that.canvasWidth, that.canvasHeight);
    ctx1.font = (that.elementSize / 3 ) + 'px sans-serif';

    that.wordGrid.forEach(function (row, index1) {
      row.forEach(function (element, index2) {
        ctx1.fillText(element.toUpperCase(), index2 * that.elementSize + 2 * marginResp, index1 * that.elementSize + (offsetTop) );
      });
    });

    let clickStart = [];
    let isDragged = false;
    let clickMode = false;

    this.$container[0].addEventListener('keydown', function () {
      //TODO: need to implement for a11y
    }, false);

    this.$drawingCanvas[0].addEventListener('touchstart', function (event) {
      touchHandler(event);
    }, false);

    this.$drawingCanvas[0].addEventListener('touchmove', function (event) {
      touchHandler(event);
    }, false);

    this.$drawingCanvas[0].addEventListener('touchend', function (event) {
      touchHandler(event);
    }, false);

    this.$drawingCanvas.on('mousedown', function (event) {
      if (that.options.gridActive) {
        if (!clickMode) {
          that.enableDrawing = true;
          clickStart = mouseDownEventHandler(event, this, that.elementSize);
          that.trigger('drawStart');
        }
      }
    });

    this.$drawingCanvas.on('mouseup', function (event) {
      if (that.enableDrawing) {
        if (isDragged || clickMode) {
          if (clickMode) {
            clickMode = false;
          }
          let markedWord = '';
          const wordObject = mouseUpEventHandler(event, this, that.elementSize, clickStart);
          const dict = {
            'horizontal' : [1, 0],
            'horizontalBack' : [-1, 0],
            'diagonal' : [1, 1],
            'diagonalBack' : [-1, 1],
            'diagonalUp' : [1, -1],
            'diagonalUpBack' : [-1, -1],
            'vertical' : [0, 1],
            'verticalUp' : [0, -1]
          };

          if (!$.isEmptyObject(wordObject) && wordObject['dir'] !== false) {
            const dir = wordObject['dir'];
            let y1 = wordObject['start'][3];
            let x1 = wordObject['start'][2];
            let x2 = wordObject['end'][2];
            const y2 = wordObject['end'][3];

            do {
              markedWord += that.wordGrid[y1][x1];
              x1 = x1 + dir[0];
              y1 = y1 + dir[1];
            } while (!((y1 === y2) && (x1 === x2)));

            markedWord += that.wordGrid[y2][x2];
            for (const key in dict) {
              if (dict[key][0] === dir[0] && dict[key][1] === dir[1]) {
                wordObject['directionKey'] = key;
                break;
              }
            }
          }
          that.enableDrawing = false;
          isDragged = false;
          that.trigger('drawEnd', {'markedWord': markedWord, 'wordObject': wordObject});
        }
        else if (!clickMode) {
          clickMode = true;
          const offsetTop = (that.$container.offset().top > that.elementSize * 0.75) ? Math.floor(that.elementSize * 0.75) : that.$container.offset().top;
          const context = that.$drawingCanvas[0].getContext('2d');
          //drawing the dot on initial click
          context.clearRect(0, 0, context.canvas.width, context.canvas.height);
          context.lineWidth = Math.floor(that.elementSize / 2);
          context.strokeStyle = 'rgba(107,177,125,0.9)';
          context.fillStyle = 'rgba(107,177,125,0.3)';
          context.beginPath();
          context.arc(clickStart[0] - (that.elementSize / 8), clickStart[1] + Math.floor(offsetTop / 8), that.elementSize / 4, 0, 2 * Math.PI);
          context.fill();
          context.closePath();
        }
      }
    });

    this.$drawingCanvas.on('mousemove', function (event) {
      if (that.enableDrawing ) {
        isDragged = true;
        mouseMoveEventHandler(event, this, clickStart, that.elementSize);
      }
    });
  };

  return FindTheWords.WordGrid;

}) (H5P.FindTheWords, H5P.EventDispatcher, H5P.jQuery);
;(function (FindTheWords, EventDispatcher, $) {

  /**
   * Vocabulary - Handles the vocabulary part.
   * @class H5P.FindTheWords.Vocabulary
   * @param {Object} params
   * @param {boolean} showVocabulary
   */
  FindTheWords.Vocabulary = function (params, showVocabulary, header) {
    /** @alias H5P.FindTheWords.Vocabulary# */
    this.words = params;
    this.header = header;
    this.showVocabulary = showVocabulary;
    this.wordsFound = [];
    this.wordsNotFound = [];
    this.wordsSolved = [];
  };

  FindTheWords.Vocabulary.prototype = Object.create(EventDispatcher.prototype);
  FindTheWords.Vocabulary.prototype.constructor = FindTheWords.Vocabulary;

  /**
   * appendTo - appending vocabulary to the play area.
   * @param {H5P.jQuery} $container
   * @param {string} isModeBlock Either in inline/block mode.
   */
  FindTheWords.Vocabulary.prototype.appendTo = function ($container, isModeBlock) {
    let output = '<div class="vocHeading"><em class="fa fa-book fa-fw" ></em>' +
      this.header + '</div><ul role="list" tabindex="0">';
    this.words.forEach(function (element) {
      const identifierName = element.replace(/ /g, '');
      output += '<li role="presentation" ><div role="listitem"  aria-label="' + identifierName + ' not found" id="' + identifierName + '"class="word">\
      <em class="fa fa-check" ></em>' + element + '</div></li>';
    });
    output += '</ul>';

    $container.html(output);
    $container.addClass('vocabulary-container');
    this.$container = $container;
    this.setMode(isModeBlock);
  };

  /**
   * setMode - set the vocabularies.
   * @param {string} mode
   */
  FindTheWords.Vocabulary.prototype.setMode = function (isModeBlock) {
    this.$container
      .toggleClass('vocabulary-block-container', isModeBlock)
      .toggleClass('vocabulary-inline-container', !isModeBlock);
  };

  /**
   * checkWord - if the marked word belongs to the vocabulary as not found.
   * @param {string} word
   */
  FindTheWords.Vocabulary.prototype.checkWord = function (word) {
    const reverse = word.split('').reverse().join('');
    const originalWord = (this.words.indexOf(word) !== -1) ? word : ( this.words.indexOf(reverse) !== -1) ? reverse : null;

    if (!originalWord || this.wordsFound.indexOf(originalWord) !== -1) {
      return false;
    }

    this.wordsFound.push(originalWord);
    if (this.showVocabulary) {
      const idName = originalWord.replace(/ /g, '');
      this.$container.find('#' + idName).addClass('word-found').attr('aria-label', idName + ' found');
    }

    return true;
  };

  /**
   * reset - reset the vocabulary upon game resetting.
   */
  FindTheWords.Vocabulary.prototype.reset = function () {
    this.wordsFound = [];
    this.wordsNotFound = this.words;
    if (this.showVocabulary) {
      this.$container.find('.word').each(function () {
        $(this).removeClass('word-found').removeClass('word-solved').attr('aria-label', $(this).attr('id') + ' not found');
      });
    }
  };

  /**
   * solveWords - changes on vocabulary upon showing the solution.
   */
  FindTheWords.Vocabulary.prototype.solveWords = function () {
    const that = this;
    that.wordsSolved = that.wordsNotFound;
    if (that.showVocabulary) {
      that.wordsNotFound.forEach(function (word) {
        const idName = word.replace(/ /g, '');
        that.$container.find('#' + idName).addClass('word-solved').attr('aria-label', idName + ' solved');
      });
    }
  };

  /**
   * getNotFound - return the list of words that are not found yet.
   * @return {Object[]}
   */
  FindTheWords.Vocabulary.prototype.getNotFound = function () {
    const that = this;
    this.wordsNotFound = this.words.filter(function (word) {
      return (that.wordsFound.indexOf(word) === -1);
    });
    return this.wordsNotFound;
  };

  /**
   * getFound - returns the words found so far.
   * @return {Object[]}
   */
  FindTheWords.Vocabulary.prototype.getFound = function () {
    const that = this;
    return this.words.filter(function (word) {
      return (that.wordsFound.indexOf(word) !== -1);
    });
  };

  /**
   * getSolved - get the words solved by the game by show solution feature.
   * @return {Object[]}
   */
  FindTheWords.Vocabulary.prototype.getSolved = function () {
    const that = this;
    return this.words.filter(function (word) {
      return (that.wordsSolved.indexOf(word) !== -1);
    });
  };

  return FindTheWords.Vocabulary;

}) (H5P.FindTheWords, H5P.EventDispatcher, H5P.jQuery);
;(function (FindTheWords, Timer) {

  /**
   * FindTheWords.Timer - Adapter between Find the words and H5P.Timer.
   * @class H5P.FindTheWords.Timer
   * @extends H5P.Timer
   * @param {H5P.jQuery} $element
   */
  FindTheWords.Timer = function ($element) {
    /** @alias H5P.FindTheWords.Timer# */
    const that = this;
    // Initialize event inheritance
    Timer.call(that, 100);

    /** @private {string} */
    const naturalState = '0:00';

    /**
     * update - Set up callback for time updates.
     * Formats time stamp for humans.
     *
     * @private
     */
    const update = function () {
      const time = that.getTime();

      const minutes = Timer.extractTimeElement(time, 'minutes');
      let seconds = Timer.extractTimeElement(time, 'seconds') % 60;
      if (seconds < 10) {
        seconds = '0' + seconds;
      }
      $element.text(minutes + ':' + seconds);
    };

    // Setup default behavior
    that.notify('every_tenth_second', update);
    that.on('reset', function () {
      $element.text(naturalState);
      that.notify('every_tenth_second', update);
    });
  };

  // Inheritance
  FindTheWords.Timer.prototype = Object.create(Timer.prototype);
  FindTheWords.Timer.prototype.constructor = FindTheWords.Timer;

}) (H5P.FindTheWords, H5P.Timer);
;(function (FindTheWords) {
  /**
   * Keeps track of the number of times the game is submitted.
   * @class H5P.FindTheWords.Counter
   * @param {H5P.jQuery} $container
   */

  FindTheWords.Counter = function ($container) {
    /** @alias H5P.FindTheWords.Counter# */
    var self = this;
    var current = 0;
    /**
     * @private
     */
    var update = function () {
      $container[0].innerText = current;
    };

    /**
     * Increment the counter.
     */
    self.increment = function () {
      current++;
      update();
    };
    /**
     * Revert counter back to its natural state
     */
    self.reset = function () {
      current = 0;
      update();
    };
  };

})(H5P.FindTheWords);
;		(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.Timer-0.4\/library.json":["application\/json","{\"title\":\"Timer\",\"machineName\":\"H5P.Timer\",\"majorVersion\":0,\"minorVersion\":4,\"patchVersion\":2,\"contentType\":\"Utility\",\"runnable\":0,\"author\":\"Oliver Tacke\",\"license\":\"pd\",\"description\":\"General purpose timer that can be used by other H5P libraries.\",\"preloadedJs\":[{\"path\":\"scripts\\\/timer.js\"}]}"],"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.FindTheWords-1.4\/library.json":["application\/json","{\"title\":\"Find The Words\",\"description\":\"Game to find a list of words in a given word grid\",\"machineName\":\"H5P.FindTheWords\",\"majorVersion\":1,\"minorVersion\":4,\"patchVersion\":4,\"embedTypes\":[\"iframe\"],\"runnable\":1,\"author\":\"JithinSpace\",\"license\":\"MIT\",\"preloadedCss\":[{\"path\":\"styles\\\/h5p-find-the-words.css\"}],\"preloadedJs\":[{\"path\":\"scripts\\\/h5p-find-the-words.js\"},{\"path\":\"scripts\\\/h5p-find-the-words-word-grid.js\"},{\"path\":\"scripts\\\/h5p-find-the-words-vocabulary.js\"},{\"path\":\"scripts\\\/h5p-find-the-words-timer.js\"},{\"path\":\"scripts\\\/h5p-find-the-words-counter.js\"}],\"preloadedDependencies\":[{\"machineName\":\"H5P.Timer\",\"majorVersion\":0,\"minorVersion\":4},{\"machineName\":\"FontAwesome\",\"majorVersion\":4,\"minorVersion\":5},{\"machineName\":\"H5P.JoubelUI\",\"majorVersion\":1,\"minorVersion\":3}]}"]});		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-40206_Deinosavroi_pantou":{"displayOptions":{"copy":false,"copyright":false,"embed":false,"export":false,"frame":false,"icon":false},"embedCode":"","exportUrl":false,"fullScreen":false,"contentUserData":[],"metadata":{"title":"\u0394\u03b5\u03b9\u03bd\u03cc\u03c3\u03b1\u03c5\u03c1\u03bf\u03b9 \u03c0\u03b1\u03bd\u03c4\u03bf\u03cd!","license":"U"},"library":"H5P.FindTheWords 1.4","jsonContent":"{\"taskDescription\":\"\u0394\u03b5\u03b9\u03bd\u03cc\u03c3\u03b1\u03c5\u03c1\u03bf\u03b9 \u03c0\u03b1\u03bd\u03c4\u03bf\u03cd! \u0392\u03c1\u03b5\u03c2 5 \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03ac \u03b5\u03af\u03b4\u03b7 \u03b4\u03b5\u03b9\u03bd\u03bf\u03c3\u03b1\u03cd\u03c1\u03c9\u03bd.\",\"wordList\":\"\u0391\u039a\u0391\u039d\u0398\u039f\u03a3\u0391\u03a5\u03a1\u039f\u03a3,\u03a4\u03a1\u0399\u039a\u0395\u03a1\u0391\u03a4\u039f\u03a0\u0391\u03a3,\u03a3\u03a4\u0395\u0393\u039f\u03a3\u0391\u03a5\u03a1\u039f\u03a3,\u03a0\u03a4\u0395\u03a1\u039f\u03a3\u0391\u03a5\u03a1\u039f\u03a3,\u0392\u03a1\u0391\u03a7\u0399\u039f\u03a3\u0391\u03a5\u03a1\u039f\u03a3\",\"behaviour\":{\"orientations\":{\"horizontal\":true,\"horizontalBack\":false,\"vertical\":true,\"verticalUp\":false,\"diagonal\":true,\"diagonalBack\":false,\"diagonalUp\":false,\"diagonalUpBack\":false},\"fillPool\":\"\u0391\u0392\u0393\u0395\u0397\u0398\u0399\u039a\u039d\u039f\u03a0\u03a1\u03a3\u03a4\u03a5\u03a6\u03a7\",\"preferOverlap\":true,\"showVocabulary\":true,\"enableShowSolution\":true,\"enableRetry\":true},\"l10n\":{\"check\":\"\u0388\u03bb\u03b5\u03b3\u03be\u03b5\",\"tryAgain\":\"\u039e\u03b1\u03bd\u03b1\u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b7\u03c3\u03b5\",\"showSolution\":\"\u0394\u03b5\u03c2 \u03c4\u03b7 \u03bb\u03cd\u03c3\u03b7\",\"found\":\"\u0392\u03c1\u03ae\u03ba\u03b5\u03c2 @found \u03b1\u03c0\u03cc \u03c4\u03b1 @totalWords\",\"timeSpent\":\"\u03a7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03ad\u03c1\u03b1\u03c3\u03b5\",\"score\":\"\u0392\u03c1\u03ae\u03ba\u03b5\u03c2 @score \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 @total \u03bb\u03ad\u03be\u03b5\u03b9\u03c2\",\"wordListHeader\":\"\u0392\u03c1\u03b5\u03c2 \u03c4\u03b9\u03c2 5 \u03bb\u03ad\u03be\u03b5\u03b9\u03c2!\"}}"}}});