src/widgets/Quiz.js
author ymh <ymh.work@gmail.com>
Fri, 18 Oct 2024 10:24:57 +0200
changeset 1074 231ea5ea7de4
parent 1072 ac1eacb3aa33
child 1076 510fd2a482f4
permissions -rw-r--r--
change http to https for default thumb

import Mustache from "mustache";
import quizStyles from "./Quiz.module.css";
import jQuery from "jquery";

const Quiz = function (ns) {
  return class extends ns.Widgets.Widget {
    constructor(player, config) {
      super(player, config);
    }

    static defaults = {
      // annotation_type: "at_quiz",
      quiz_activated: true,
      api_serializer: "ldt_annotate",
      analytics_api: "",
      api_method: "POST",
      user: "",
      userid: "",
    };

    static template =
      '<div class="Ldt-Quiz-Container">' +
      '<div class="Ldt-Quiz-Header">' +
      '  <div class="Ldt-Quiz-Index"></div><div class="Ldt-Quiz-Score"></div>' +
      "</div>" +
      '<div class="Ldt-Quiz-Content">' +
      '  <h1 class="Ldt-Quiz-Title">{{question}}</h1>' +
      '  <div class="Ldt-Quiz-Questions">' +
      "  </div>" +
      "</div>" +
      '<div class="Ldt-Quiz-Footer">' +
      '  <div class="Ldt-Quiz-Votes">' +
      '      <span class="Ldt-Quiz-Votes-Question">Avez-vous trouvé cette question utile ?</span>' +
      '      <div class="Ldt-Quiz-Votes-Buttons">' +
      '          <div class="Ldt-Quiz-Vote-Skip-Block"><a href="#" class="Ldt-Quiz-Vote-Skip">Passer</a></div>' +
      '          <div><input type="button" value="Non" class="Ldt-Quiz-Vote-Useless" /></div>' +
      '          <div><input type="button" value="Oui" class="Ldt-Quiz-Vote-Useful" /></div>' +
      "      </div>" +
      "  </div>" +
      '  <div class="Ldt-Quiz-Submit">' +
      '      <div class="Ldt-Quiz-Submit-Button"><input type="button" value="Valider" /></div>' +
      '      <div class="Ldt-Quiz-Submit-Skip-Link"><a href="#">Passer</a></div><div style="clear:both;"></div>' +
      "  </div>" +
      '  <div class="Ldt-Quiz-Result">Bonne réponse</div>' +
      "</div>" +
      "</div>";

    static annotationTemplate = "";

    update(annotation) {
      var _this = this;

      if (
        this.quiz_activated &&
        this.correct[annotation.id] != 1 &&
        this.correct[annotation.id] != 0
      ) {
        _this.quiz_displayed = true;

        //Pause the current video
        this.media.pause();

        this.annotation = annotation;

        var question = annotation.content.data.question;
        var answers = annotation.content.data.answers;
        var resource = annotation.content.data.resource;

        $(".Ldt-Quiz-Votes").hide();
        $(".Ldt-Pause-Add-Question").hide();
        $(".Ldt-Quiz-Container .Ldt-Quiz-Title").html(question);

        var i = 0;

        var score = Mustache.render(
          '<span class="Ldt-Quiz-Correct-Answer">{{ correctness.0 }}</span> / <span class="Ldt-Quiz-Incorrect-Answer">{{ correctness.1 }}</span>',
          { correctness: this.globalScore() }
        );
        $(".Ldt-Quiz-Index").html(
          Mustache.render("Q{{index}}/{{total}}", {
            index: annotation.number + 1,
            total: this.totalAmount,
          })
        );
        $(".Ldt-Quiz-Score").html(score);
        this.question = new ns.Widgets.UniqueChoiceQuestion(annotation);
        this.resource = new ns.Widgets.UniqueChoiceQuestion(resource);

        if (annotation.content.data.type == "multiple_choice") {
          this.question = new ns.Widgets.MultipleChoiceQuestion(annotation);
          this.resource = new ns.Widgets.MultipleChoiceQuestion(resource);
        } else if (annotation.content.data.type == "unique_choice") {
          this.question = new ns.Widgets.UniqueChoiceQuestion(annotation);
          this.resource = new ns.Widgets.UniqueChoiceQuestion(resource);
        }

        var output = "";
        for (i = 0; i < answers.length; i++) {
          output +=
            '<div class="quiz-question-block"><p>' +
            this.question.renderQuizTemplate(answers[i], i) +
            '<span class="quiz-question-label">' +
            answers[i].content +
            "</span></p></div>";
        }

        var QR = "";
        //If there is an attached resource, display it on the resources overlay
        if (resource != null) {
          QR =
            '<div class="quiz-resource-block" id="resource" >' +
            resource +
            "</div>";
        }
        $(".Ldt-Quiz-Questions").html(QR + output);
        $(".Ldt-Quiz-Overlay").fadeIn();

        $(".Ldt-Quiz-Submit").fadeIn();

        //Let's automatically check the checkbox/radio if we click on the label
        $(".quiz-question-label").click(function () {
          var input = $(this).siblings("input");
          if (input.prop("checked") && input.prop("type") == "radio") {
            // Already checked. Consider a double click on unique question as a validation.
            _this.answer();
          } else {
            input.prop("checked", !input.prop("checked"));
          }
        });

        //In case we click on the first "Skip" link
        $(".Ldt-Quiz-Submit-Skip-Link").click(
          { media: this.media },
          function (event) {
            _this.hide();
            _this.player.trigger("QuizCreator.skip");
            event.data.media.play();
          }
        );
      }
    }

    hide() {
      var _this = this;

      $(".Ldt-Quiz-Votes").hide();
      $(".Ldt-Quiz-Overlay").hide();
      $(".Ldt-Pause-Add-Question").hide();
      _this.quiz_displayed = false;
    }

    answer() {
      var _this = this;

      function insert_timecode_links(s) {
        return (s || "").replace(/\s(\d+:\d+)/, function (match, timecode) {
          return (
            ' <a href="#t=' +
            ns.timestamp2ms(timecode) / 1000 +
            '">' +
            timecode +
            "</a>"
          );
        });
      }

      var answers = _this.annotation.content.data.answers;

      // Augment answers with the correct feedback
      var i = 0;
      var wrong = 0;
      // Signature is an array giving the answers signature: 1 for checked, 0 for unchecked
      // We cannot simply store the right answer index, since there may be multiple-choice questions
      var signature = [];
      _this.$.find(".Ldt-Quiz-Question-Check").each(function (code) {
        var checked = $(this).is(":checked");
        signature.push(checked ? 1 : 0);
        var ans = answers[i];
        if ((ans.correct && !checked) || (!ans.correct && checked)) {
          wrong += 1;
          jQuery(this)
            .parents(".quiz-question-block")
            .append(
              '<div class="quiz-question-feedback quiz-question-incorrect-feedback">' +
                insert_timecode_links(ans.feedback) +
                "</div>"
            );
        } else {
          jQuery(this)
            .parents(".quiz-question-block")
            .append(
              '<div class="quiz-question-feedback quiz-question-correct-feedback">' +
                insert_timecode_links(ans.feedback) +
                "</div>"
            );
        }
        i++;
      });

      if (wrong) {
        $(".Ldt-Quiz-Result").html("Mauvaise réponse");
        $(".Ldt-Quiz-Result").css({ "background-color": "red" });
        this.correct[this.annotation.id] = 0;
      } else {
        $(".Ldt-Quiz-Result").html("Bonne réponse !");
        $(".Ldt-Quiz-Result").css({ "background-color": "green" });
        this.correct[this.annotation.id] = 1;
      }

      $(".Ldt-Quiz-Result").animate(
        { height: "100%" },
        500,
        "linear",
        function () {
          $(".Ldt-Quiz-Result").delay(2000).animate({ height: "0%" }, 500);
        }
      );

      var question_number = this.annotation.number + 1;
      var correctness = this.globalScore();
      var score = "";
      score +=
        '<span class="Ldt-Quiz-Correct-Answer">' +
        correctness[0] +
        '</span> / <span class="Ldt-Quiz-Incorrect-Answer">' +
        correctness[1] +
        "</span>";
      $(".Ldt-Quiz-Index").html("Q" + question_number + "/" + this.totalAmount);
      $(".Ldt-Quiz-Score").html(score);

      this.submit(
        this.user,
        this.userid,
        this.annotation.id,
        wrong ? "wrong_answer" : "right_answer",
        signature.join("")
      );

      //Hide the "Validate" button and display the UI dedicated to votes
      $(".Ldt-Quiz-Submit").fadeOut(400, function () {
        $(".Ldt-Quiz-Votes").show();
      });
    }

    globalScore() {
      // Return 2 variables to know how many right and wrong answers there are
      var values = _.values(this.correct);
      var ok = values.filter(function (s) {
        return s == 1;
      }).length;
      var not_ok = values.filter(function (s) {
        return s == 0;
      }).length;
      return [ok, not_ok];
    }

    refresh() {
      var _annotations = this.getWidgetAnnotations().sortBy(function (
        _annotation
      ) {
        return _annotation.begin;
      });

      var _this = this;

      _this.totalAmount = _annotations.length;
      _this.number = 0;
      _this.correct = {};
      _this.keys = {};

      _annotations.forEach(function (_a) {
        //Fix each annotation as "non-answered yet"
        _this.correct[_a.id] = -1;
        _this.keys[_this.number] = _a.id;
        _a.number = _this.number++;
      });
    }

    draw() {
      var _this = this;
      _this.quiz_displayed = false;
      this.onMediaEvent("enter-annotation", function (annotation) {
        var an = _this.getWidgetAnnotations().filter(function (a) {
          return a === annotation;
        });
        if (an.number === undefined) {
          _this.refresh();
        }
        if (an.length) {
          _this.update(an[0]);
        }
      });
      this.onMdpEvent("Quiz.activate", function () {
        _this.quiz_activated = true;
      });

      this.onMdpEvent("Quiz.deactivate", function () {
        _this.quiz_activated = false;
        _this.hide();
      });

      this.onMdpEvent("Quiz.hide", function () {
        _this.hide();
      });

      this.onMdpEvent("Quiz.refresh", function () {
        _this.refresh();
      });

      this.onMediaEvent("pause", function () {
        if (!_this.quiz_displayed) {
          $(".Ldt-Pause-Add-Question").show();
        }
      });

      this.onMediaEvent("play", function () {
        $(".Ldt-Pause-Add-Question").hide();
      });

      // Add Ldt-Quiz-Overlay widget on top of video player
      _this.overlay = $("<div class='Ldt-Quiz-Overlay'></div>").appendTo(
        $("#" + _this.container)
      );
      _this.PauseAddQuestion = $(
        "<div class='Ldt-Pause-Add-Question' title='Ajoutez une question !'>"
      )
        .on("click", function () {
          _this.player.trigger("QuizCreator.create");
        })
        .appendTo($("#" + _this.container));
      _this.overlay.html(this.template);

      $(".Ldt-Quiz-Overlay").hide();

      $(".Ldt-Quiz-Submit input").click(function () {
        _this.answer();
      });

      //In case we click on the first "Skip" link
      $(".Ldt-Quiz-Submit-Skip-Link").click(
        { media: this.media },
        function (event) {
          _this.submit(
            _this.user,
            _this.userid,
            _this.annotation.id,
            "skipped_answer",
            0
          );
          _this.hide();
          _this.player.trigger("QuizCreator.skip");
          event.data.media.play();
        }
      );

      $(
        '.Ldt-Quiz-Votes-Buttons input[type="button"], .Ldt-Quiz-Votes-Buttons a'
      ).click({ media: this.media }, function (event) {
        var vote_prop, vote_val;

        if ($(this).hasClass("Ldt-Quiz-Vote-Useful")) {
          vote_prop = "useful";
          vote_val = 1;
        } else if ($(this).hasClass("Ldt-Quiz-Vote-Useless")) {
          vote_prop = "useless";
          vote_val = -1;

          $(".Ldt-Ctrl-Quiz-Create")
            .addClass("button_highlight")
            .delay(5000)
            .queue(function () {
              $(this).removeClass("button_highlight").dequeue();
            });
        } else {
          vote_prop = "skipped_vote";
          vote_val = 0;
        }

        _this.submit(
          _this.user,
          _this.userid,
          _this.annotation.id,
          vote_prop,
          vote_val
        );

        //Resume the current video
        event.data.media.play();

        _this.hide();
        $(".Ldt-Pause-Add-Question").hide();

        _this.player.trigger("QuizCreator.skip");
      });

      _this.refresh();
    }
  };
};

const UniqueChoiceQuestion = function (ns) {
  return class extends ns.Widgets.Widget {
    constructor(annotation) {
      this.annotation = annotation;
    }

    renderQuizTemplate(answer, identifier) {
      return (
        '<input type="radio" class="quiz-question Ldt-Quiz-Question-Check Ldt-Quiz-Question-Check-' +
        identifier +
        '" name="question" data-question="' +
        identifier +
        '" value="' +
        identifier +
        '" />'
      );
    }

    renderTemplate(answer, identifier) {
      var id = this.generateUid();
      return (
        '<input type="radio" id="' +
        id +
        '" class="quiz-question-edition Ldt-Quiz-Question-Check Ldt-Quiz-Question-Check-' +
        identifier +
        '" name="question" data-question="' +
        identifier +
        '" value="' +
        identifier +
        '" /><label for="' +
        id +
        '" title="Veuillez sélectionner la réponse correcte"></label>'
      );
    }

    renderFullTemplate(answer, identifier) {
      var correct = answer && answer.correct ? "checked" : "";
      var id = this.generateUid();
      return (
        '<input type="radio" id="' +
        id +
        '" ' +
        correct +
        ' class="quiz-question-edition Ldt-Quiz-Question-Check Ldt-Quiz-Question-Check-' +
        identifier +
        '" name="question" data-question="' +
        identifier +
        '" value="' +
        identifier +
        '" /><label for="' +
        id +
        '"></label>'
      );
    }
  };
};

const MultipleChoiceQuestion = function (ns) {
  return class extends ns.Widgets.Widget {
    constructor(annotation) {
      this.annotation = annotation;
    }

    renderQuizTemplate(answer, identifier) {
      return (
        '<input type="checkbox" class="quiz-question Ldt-Quiz-Question-Check Ldt-Quiz-Question-Check-' +
        identifier +
        '" name="question[' +
        identifier +
        ']" data-question="' +
        identifier +
        '" value="' +
        identifier +
        '" /> '
      );
    }

    renderTemplate(answer, identifier) {
      var id = this.generateUid();
      return (
        '<input type="checkbox" id="' +
        id +
        '" class="quiz-question-edition Ldt-Quiz-Question-Check" name="question[' +
        identifier +
        ']" data-question="' +
        identifier +
        '" value="' +
        identifier +
        '" /><label for="' +
        id +
        '" title="Veuillez sélectionner la ou les réponses correctes"></label>'
      );
    }

    renderFullTemplate(answer, identifier) {
      var correct = answer && answer.correct ? "checked" : "";
      var id = this.generateUid();
      return (
        '<input type="checkbox" id="' +
        id +
        '" ' +
        correct +
        ' class="quiz-question-edition Ldt-Quiz-Question-Check" name="question[' +
        identifier +
        ']" data-question="' +
        identifier +
        '" value="' +
        identifier +
        '" /><label for="' +
        id +
        '"></label> '
      );
    }

    submit(user, user_id, question, prop, val) {
      var _this = this;
      var _url = Mustache.render(this.analytics_api, {
          id: this.source.projectId,
        }),
        donnees = {
          username: user,
          useruuid: user_id,
          subject: question,
          property: prop,
          value: val,
          session: _this.session_id,
        };

      jQuery.ajax({
        url: _url,
        type: this.api_method,
        contentType: "application/json",
        data: JSON.stringify(donnees),
        success: function (_data) {},
        error: function (_xhr, _error, _thrown) {
          ns.log("Error when sending annotation", _thrown);
        },
      });
    }
  };
};

export { Quiz, UniqueChoiceQuestion, MultipleChoiceQuestion, quizStyles };