<commit_before>const $ = H5P.jQuery;

const ENDSCREEN_STYLE_BASE = 'h5p-interactive-video-endscreen';
const ENDSCREEN_STYLE_BUTTON_HIDDEN = 'h5p-interactive-video-endscreen-submit-button-hidden';
const KEY_CODE_ENTER = 13;
const KEY_CODE_SPACE = 32;

/** Class representing an endscreen. */
class Endscreen extends H5P.EventDispatcher {

  /**
   * Create a new end screen.
   *
   * @param {object} parent - Parent object, right now quite tied to Interactive Video.
   * @param {object} [params] - Parameters.
   * @param {object} [params.l10n] - Localization.
   * @param {string} [params.l10n.title] - Title message.
   * @param {string} [params.l10n.information] - Information message.
   * @param {string} [params.l10n.submitButton] - Label for the submit button.
   * @param {string} [params.l10n.submitMessage] - Message after submission.
   * @param {string} [params.l10n.tableRowAnswered] - Row title for answered questions.
   * @param {string} [params.l10n.tableRowScore] - Row title for score.
   * @param {string} [params.l10n.answeredScore] - Label for answered questions without score.
   */
  constructor(parent, params = {}) {
    super();

    this.parent = parent;
    this.l10n = $.extend({
      title: '@answered Questions answered',
      information: 'You have answered @answered questions, click below to submit your answers.',
      informationNoAnswers: 'You have not answered any questions.',
      informationMustHaveAnswer: 'You have to answer at least one question before you can submit your answers.',
      submitButton: 'Submit Answers',
      submitMessage: 'Your answers have been submitted!',
      tableRowAnswered: 'Answered questions',
      tableRowScore: 'Score',
      answeredScore: 'answered'
    }, params.l10n);

    this.buildDOM();
  }

  /**
   * Build the DOM elements for the endscreen.
   */
  buildDOM() {
    // Title Bar with text and close button
    this.$endscreenIntroductionTitleText = $('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-introduction-title-text`});

    const $endscreenCloseButton = $('<div>', {'role': 'button', 'class': `${ENDSCREEN_STYLE_BASE}-close-button`, 'tabindex': '0', 'aria-label': this.parent.l10n.close})
      .click(() => {
        // This is a little bit like Minsky's useless machine, but necessary because of the dual use of the bubble class.
        this.parent.toggleEndscreen(false);
      })
      .keydown(event => {
        if ([KEY_CODE_ENTER, KEY_CODE_SPACE].indexOf(event.which) !== -1) {
          this.parent.toggleEndscreen(false);
          event.preventDefault();
        }
      });

    const $endscreenIntroductionTitle = $('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-introduction-title`})
      .append([this.$endscreenIntroductionTitleText, $endscreenCloseButton]);

    // Description
    this.$endscreenIntroductionText = $('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-introduction-text`});

    // Submit button
    this.$endscreenSubmitButton = $('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-submit-button-container`})
      .addClass(ENDSCREEN_STYLE_BUTTON_HIDDEN)
      .append(H5P.JoubelUI.createButton({class: `${ENDSCREEN_STYLE_BASE}-submit-button`, html: this.l10n.submitButton})
        .click(event => {
          this.handleSubmit();
          event.preventDefault();
        })
        .keydown(event => {
          if ([KEY_CODE_ENTER, KEY_CODE_SPACE].indexOf(event.which) !== -1) {
            this.handleSubmit();
            event.preventDefault();
          }
        })
      );

    // Title row for the table at the bottom
    this.$endscreenOverviewTitle = $('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-overview-title`})
      .append($('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-overview-title-answered-questions`, 'html': this.l10n.tableRowAnswered}))
      .append($('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-overview-title-score`, 'html': this.l10n.tableRowScore}));

    // Table for answered interactions
    this.$endscreenBottomTable = $('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-overview-table`});

    // Endscreen DOM root
    this.$endscreen = $('<div/>', {class: ENDSCREEN_STYLE_BASE})
      .append($('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-introduction`})
        .append($('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-star-symbol`}))
        .append($('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-introduction-container`})
          .append([$endscreenIntroductionTitle, this.$endscreenIntroductionText, this.$endscreenSubmitButton])))
      .append($('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-overview`})
        .append(this.$endscreenOverviewTitle)
        .append(this.$endscreenBottomTable));
  }

  /**
   * Handle click on the submit button.
   *
   * Will fire an 'answered' xAPI statement for all interactions that have
   * been interacted with but have not yet sent 'answered' or completed. Will send
   * a 'completed' xAPI statement for parent (IV) each time.
   */
  handleSubmit() {
    if (this.$endscreenSubmitButton.hasClass(ENDSCREEN_STYLE_BUTTON_HIDDEN)) {
      return;
    }
    this.$endscreenSubmitButton.addClass(ENDSCREEN_STYLE_BUTTON_HIDDEN);
    this.$endscreenIntroductionText.html(`<div class="${ENDSCREEN_STYLE_BASE}-introduction-text-submitted">${this.l10n.submitMessage}</div>`);

    this.answered.forEach(interaction => {
      /*
       * We only need to fire an xAPI answered statement if the user
       * interacted with the content and the content has not sent it so far
       * itself.
       */
      if (interaction.getLastXAPIVerb() !== 'completed' && interaction.getLastXAPIVerb() !== 'answered') {
        const xAPIEvent = new H5P.XAPIEvent();
        xAPIEvent.data.statement = interaction.getXAPIData().statement;
        interaction.setLastXAPIVerb(xAPIEvent.getVerb());

        this.trigger(xAPIEvent);
      }
    });
    /*
     * Override the "completeSent" variable of the parent here, because new
     * submissions basically mean a new attempt of the parent (IV).
     * This is subject to being changed.
     */
    this.parent.triggerXAPIScored(this.parent.getUsersScore(), this.parent.getUsersMaxScore(), 'completed');
  }

  /**
   * Get the DOM of the endscreen.
   *
   * @return {jQuery} DOM of the endscreen.
   */
  getDOM() {
    return this.$endscreen;
  }

  /**
   * Build one row of a table for the endscreen.
   *
   * @param {number} time - Popup time of an interaction in seconds.
   * @param {string} title - Title of the interaction.
   * @param {string} [score=this.l10n.answered] - Score as "score / maxscore" for the interaction, 'answered' if undefined.
   * @return {jQuery} DOM element for the table row.
   */
  buildTableRow(time, title, score = this.l10n.answered) {
    const noLink = (this.parent.skippingPrevented()) ? ` ${ENDSCREEN_STYLE_BASE}-no-link` : '';
    return $('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-overview-table-row${noLink}`})
      .append($('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-overview-table-row-time`, html: H5P.InteractiveVideo.humanizeTime(time)})
        .click(() => {
          this.jump(time);
        }))
      .append($('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-overview-table-row-title`, html: title}))
      .click(() => {
        this.jump(time);
      })
      .append($('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-overview-table-row-score`, html: score || this.l10n.tableAnswered}));
  }

  /**
   * Jump to a certain point in the video.
   *
   * @param {number} time - Time in seconds to jump to.
   */
  jump(time) {
    if (!this.parent.skippingPrevented()) {
      this.parent.seek(time);
      this.parent.toggleEndscreen(false);
    }
  }

  /**
   * Update the endscreen.
   *
   * @param {H5P.Interaction[]} [interactions] - Interactions from IV.
   */
  update(interactions = []) {
    // Filter for interactions that have been answered and sort chronologically
    this.answered = interactions
      .filter(interaction => interaction.getProgress() !== undefined)
      .sort((a, b) => a.getDuration().from > b.getDuration().from);

    this.$endscreenBottomTable.empty();

    // No chaining because we need the variable later
    this.answered.forEach(interaction => {
      const time = interaction.getDuration().from;
      const title = this.getDescription(interaction);
      const score = (interaction.getInstance().getScore && interaction.getInstance().getScore() !== null && interaction.getInstance().getMaxScore) ? interaction.getInstance().getScore() + ' / ' + interaction.getInstance().getMaxScore() : this.l10n.answeredScore;
      this.$endscreenBottomTable.append(this.buildTableRow(time, title, score));
    });

    const number = this.answered.length;

    this.$endscreenIntroductionTitleText.html(this.l10n.title.replace('@answered', number));

    if (number === 0) {
      this.$endscreenIntroductionText.html(`<div class="${ENDSCREEN_STYLE_BASE}-bold-text">${this.l10n.informationNoAnswers}</div><div>${this.l10n.informationMustHaveAnswer}<div>`);
    }
    else {
      this.$endscreenIntroductionText.html(this.l10n.information.replace('@answered', number));
    }

    // Only show submit button (again) if there are answered interactions
    if (number > 0) {
      this.$endscreenSubmitButton.removeClass(ENDSCREEN_STYLE_BUTTON_HIDDEN);
    }
  }

  /**
   * Get description of the interaction.
   *
   * @return {string} Task description or interaction title.
   */
  getDescription(interaction) {
    if (typeof interaction.getInstance === 'function' && typeof interaction.getInstance().getTitle === 'function') {
      return interaction.getInstance().getTitle();
    }
    return interaction.getTitle();
  }
}

export default Endscreen;
<commit_msg>Add button role to endscreen table items<commit_after>const $ = H5P.jQuery;

const ENDSCREEN_STYLE_BASE = 'h5p-interactive-video-endscreen';
const ENDSCREEN_STYLE_BUTTON_HIDDEN = 'h5p-interactive-video-endscreen-submit-button-hidden';
const KEY_CODE_ENTER = 13;
const KEY_CODE_SPACE = 32;

/** Class representing an endscreen. */
class Endscreen extends H5P.EventDispatcher {

  /**
   * Create a new end screen.
   *
   * @param {object} parent - Parent object, right now quite tied to Interactive Video.
   * @param {object} [params] - Parameters.
   * @param {object} [params.l10n] - Localization.
   * @param {string} [params.l10n.title] - Title message.
   * @param {string} [params.l10n.information] - Information message.
   * @param {string} [params.l10n.submitButton] - Label for the submit button.
   * @param {string} [params.l10n.submitMessage] - Message after submission.
   * @param {string} [params.l10n.tableRowAnswered] - Row title for answered questions.
   * @param {string} [params.l10n.tableRowScore] - Row title for score.
   * @param {string} [params.l10n.answeredScore] - Label for answered questions without score.
   */
  constructor(parent, params = {}) {
    super();

    this.parent = parent;
    this.l10n = $.extend({
      title: '@answered Questions answered',
      information: 'You have answered @answered questions, click below to submit your answers.',
      informationNoAnswers: 'You have not answered any questions.',
      informationMustHaveAnswer: 'You have to answer at least one question before you can submit your answers.',
      submitButton: 'Submit Answers',
      submitMessage: 'Your answers have been submitted!',
      tableRowAnswered: 'Answered questions',
      tableRowScore: 'Score',
      answeredScore: 'answered'
    }, params.l10n);

    this.buildDOM();
  }

  /**
   * Build the DOM elements for the endscreen.
   */
  buildDOM() {
    // Title Bar with text and close button
    this.$endscreenIntroductionTitleText = $('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-introduction-title-text`});

    const $endscreenCloseButton = $('<div>', {'role': 'button', 'class': `${ENDSCREEN_STYLE_BASE}-close-button`, 'tabindex': '0', 'aria-label': this.parent.l10n.close})
      .click(() => {
        // This is a little bit like Minsky's useless machine, but necessary because of the dual use of the bubble class.
        this.parent.toggleEndscreen(false);
      })
      .keydown(event => {
        if ([KEY_CODE_ENTER, KEY_CODE_SPACE].indexOf(event.which) !== -1) {
          this.parent.toggleEndscreen(false);
          event.preventDefault();
        }
      });

    const $endscreenIntroductionTitle = $('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-introduction-title`})
      .append([this.$endscreenIntroductionTitleText, $endscreenCloseButton]);

    // Description
    this.$endscreenIntroductionText = $('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-introduction-text`});

    // Submit button
    this.$endscreenSubmitButton = $('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-submit-button-container`})
      .addClass(ENDSCREEN_STYLE_BUTTON_HIDDEN)
      .append(H5P.JoubelUI.createButton({class: `${ENDSCREEN_STYLE_BASE}-submit-button`, html: this.l10n.submitButton})
        .click(event => {
          this.handleSubmit();
          event.preventDefault();
        })
        .keydown(event => {
          if ([KEY_CODE_ENTER, KEY_CODE_SPACE].indexOf(event.which) !== -1) {
            this.handleSubmit();
            event.preventDefault();
          }
        })
      );

    // Title row for the table at the bottom
    this.$endscreenOverviewTitle = $('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-overview-title`})
      .append($('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-overview-title-answered-questions`, 'html': this.l10n.tableRowAnswered}))
      .append($('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-overview-title-score`, 'html': this.l10n.tableRowScore}));

    // Table for answered interactions
    this.$endscreenBottomTable = $('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-overview-table`});

    // Endscreen DOM root
    this.$endscreen = $('<div/>', {class: ENDSCREEN_STYLE_BASE})
      .append($('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-introduction`})
        .append($('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-star-symbol`}))
        .append($('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-introduction-container`})
          .append([$endscreenIntroductionTitle, this.$endscreenIntroductionText, this.$endscreenSubmitButton])))
      .append($('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-overview`})
        .append(this.$endscreenOverviewTitle)
        .append(this.$endscreenBottomTable));
  }

  /**
   * Handle click on the submit button.
   *
   * Will fire an 'answered' xAPI statement for all interactions that have
   * been interacted with but have not yet sent 'answered' or completed. Will send
   * a 'completed' xAPI statement for parent (IV) each time.
   */
  handleSubmit() {
    if (this.$endscreenSubmitButton.hasClass(ENDSCREEN_STYLE_BUTTON_HIDDEN)) {
      return;
    }
    this.$endscreenSubmitButton.addClass(ENDSCREEN_STYLE_BUTTON_HIDDEN);
    this.$endscreenIntroductionText.html(`<div class="${ENDSCREEN_STYLE_BASE}-introduction-text-submitted">${this.l10n.submitMessage}</div>`);

    this.answered.forEach(interaction => {
      /*
       * We only need to fire an xAPI answered statement if the user
       * interacted with the content and the content has not sent it so far
       * itself.
       */
      if (interaction.getLastXAPIVerb() !== 'completed' && interaction.getLastXAPIVerb() !== 'answered') {
        const xAPIEvent = new H5P.XAPIEvent();
        xAPIEvent.data.statement = interaction.getXAPIData().statement;
        interaction.setLastXAPIVerb(xAPIEvent.getVerb());

        this.trigger(xAPIEvent);
      }
    });
    /*
     * Override the "completeSent" variable of the parent here, because new
     * submissions basically mean a new attempt of the parent (IV).
     * This is subject to being changed.
     */
    this.parent.triggerXAPIScored(this.parent.getUsersScore(), this.parent.getUsersMaxScore(), 'completed');
  }

  /**
   * Get the DOM of the endscreen.
   *
   * @return {jQuery} DOM of the endscreen.
   */
  getDOM() {
    return this.$endscreen;
  }

  /**
   * Build one row of a table for the endscreen.
   *
   * @param {number} time - Popup time of an interaction in seconds.
   * @param {string} title - Title of the interaction.
   * @param {string} [score=this.l10n.answered] - Score as "score / maxscore" for the interaction, 'answered' if undefined.
   * @return {jQuery} DOM element for the table row.
   */
  buildTableRow(time, title, score = this.l10n.answered) {
    const noLink = (this.parent.skippingPrevented()) ? ` ${ENDSCREEN_STYLE_BASE}-no-link` : '';
    return $('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-overview-table-row${noLink}`})
      .append($('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-overview-table-row-time`, html: H5P.InteractiveVideo.humanizeTime(time), role: 'button'})
        .click(() => {
          this.jump(time);
        }))
      .append($('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-overview-table-row-title`, html: title, role: 'button'}))
      .click(() => {
        this.jump(time);
      })
      .append($('<div/>', {class: `${ENDSCREEN_STYLE_BASE}-overview-table-row-score`, html: score || this.l10n.tableAnswered}));
  }

  /**
   * Jump to a certain point in the video.
   *
   * @param {number} time - Time in seconds to jump to.
   */
  jump(time) {
    if (!this.parent.skippingPrevented()) {
      this.parent.seek(time);
      this.parent.toggleEndscreen(false);
    }
  }

  /**
   * Update the endscreen.
   *
   * @param {H5P.Interaction[]} [interactions] - Interactions from IV.
   */
  update(interactions = []) {
    // Filter for interactions that have been answered and sort chronologically
    this.answered = interactions
      .filter(interaction => interaction.getProgress() !== undefined)
      .sort((a, b) => a.getDuration().from > b.getDuration().from);

    this.$endscreenBottomTable.empty();

    // No chaining because we need the variable later
    this.answered.forEach(interaction => {
      const time = interaction.getDuration().from;
      const title = this.getDescription(interaction);
      const score = (interaction.getInstance().getScore && interaction.getInstance().getScore() !== null && interaction.getInstance().getMaxScore) ? interaction.getInstance().getScore() + ' / ' + interaction.getInstance().getMaxScore() : this.l10n.answeredScore;
      this.$endscreenBottomTable.append(this.buildTableRow(time, title, score));
    });

    const number = this.answered.length;

    this.$endscreenIntroductionTitleText.html(this.l10n.title.replace('@answered', number));

    if (number === 0) {
      this.$endscreenIntroductionText.html(`<div class="${ENDSCREEN_STYLE_BASE}-bold-text">${this.l10n.informationNoAnswers}</div><div>${this.l10n.informationMustHaveAnswer}<div>`);
    }
    else {
      this.$endscreenIntroductionText.html(this.l10n.information.replace('@answered', number));
    }

    // Only show submit button (again) if there are answered interactions
    if (number > 0) {
      this.$endscreenSubmitButton.removeClass(ENDSCREEN_STYLE_BUTTON_HIDDEN);
    }
  }

  /**
   * Get description of the interaction.
   *
   * @return {string} Task description or interaction title.
   */
  getDescription(interaction) {
    if (typeof interaction.getInstance === 'function' && typeof interaction.getInstance().getTitle === 'function') {
      return interaction.getInstance().getTitle();
    }
    return interaction.getTitle();
  }
}

export default Endscreen;

{'fraction_non_alphanumeric': 0.14868278599783472, 'fraction_numerical': 0.006495849873691808, 'mean_word_length': 3.439917601281758, 'pattern_counts': {'":': 0, '<': 51, '<?xml version=': 0, '>': 75, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5737298', 'n_tokens_mistral': 6103, 'n_tokens_neox': 5923, 'n_words': 1302}